From dd4af2df81fe98797a00d770cf91c44f6fca6d55 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 7 Aug 2012 13:07:51 +0100 Subject: [PATCH] Python 2.5 and 3.1 support confirmed --- .travis.yml | 2 ++ mongoengine/base.py | 15 +++++--------- mongoengine/django/tests.py | 4 ++-- mongoengine/fields.py | 19 ++++++++++++------ .../{python3_support.py => python_support.py} | 20 ++++++++++++++++--- mongoengine/queryset.py | 16 ++------------- setup.cfg | 3 +-- setup.py | 9 ++++++++- tests/test_django.py | 2 +- tests/test_document.py | 19 +++++++++--------- tests/test_fields.py | 10 +++++----- tests/test_queryset.py | 4 +++- 12 files changed, 68 insertions(+), 55 deletions(-) rename mongoengine/{python3_support.py => python_support.py} (51%) diff --git a/.travis.yml b/.travis.yml index 47ad0681..3ec14eab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ # http://travis-ci.org/#!/MongoEngine/mongoengine language: python python: + - 2.5 - 2.6 - 2.7 + - 3.1 - 3.2 install: - if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then sudo apt-get install zlib1g zlib1g-dev; fi diff --git a/mongoengine/base.py b/mongoengine/base.py index 11075ef6..e1d7ebbc 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -10,7 +10,7 @@ from queryset import DoesNotExist, MultipleObjectsReturned from queryset import DO_NOTHING from mongoengine import signals -from mongoengine.python3_support import PY3, txt_type +from mongoengine.python_support import PY3, PY25, txt_type import pymongo from bson import ObjectId @@ -894,10 +894,8 @@ class BaseDocument(object): if not is_list and '_cls' in value: cls = get_document(value['_cls']) - # Make keys str instead of unicode - for key,val in value.items(): - del value[key] - value[str(key)] = val + if PY25: + value = dict([(str(k), v) for k, v in value.items()]) value = cls(**value) value._dynamic = True value._changed_fields = [] @@ -1019,12 +1017,9 @@ class BaseDocument(object): raise InvalidDocumentError(""" Invalid data to create a `%s` instance.\n%s""".strip() % (cls._class_name, errors)) - # Make all keys str instead of unicode - for key,val in data.items(): - del data[key] - data[str(key)] = val + if PY25: + data = dict([(str(k), v) for k, v in data.items()]) obj = cls(**data) - obj._changed_fields = changed_fields obj._created = False return obj diff --git a/mongoengine/django/tests.py b/mongoengine/django/tests.py index 566a3984..1fab6fc3 100644 --- a/mongoengine/django/tests.py +++ b/mongoengine/django/tests.py @@ -1,7 +1,7 @@ #coding: utf-8 from nose.plugins.skip import SkipTest -from mongoengine.python3_support import PY3 +from mongoengine.python_support import PY3 from mongoengine import connect try: @@ -18,7 +18,7 @@ except Exception as err: class MongoTestCase(TestCase): - + def setUp(self): if PY3: raise SkipTest('django does not have Python 3 support') diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 600a8e67..448b0c6a 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -10,8 +10,8 @@ from operator import itemgetter import gridfs from bson import Binary, DBRef, SON, ObjectId -from mongoengine.python3_support import (PY3, bin_type, - txt_type, str_types, StringIO) +from mongoengine.python_support import (PY3, bin_type, txt_type, + str_types, StringIO) from base import (BaseField, ComplexBaseField, ObjectIdField, ValidationError, get_document, BaseDocument) from queryset import DO_NOTHING, QuerySet @@ -840,13 +840,20 @@ class BinaryField(BaseField): self.max_bytes = max_bytes super(BinaryField, self).__init__(**kwargs) + def __set__(self, instance, value): + """Handle bytearrays in python 3.1""" + if PY3 and isinstance(value, bytearray): + value = bin_type(value) + return super(BinaryField, self).__set__(instance, value) + def to_mongo(self, value): return Binary(value) def validate(self, value): if not isinstance(value, (bin_type, txt_type, Binary)): self.error("BinaryField only accepts instances of " - "(%s, %s, Binary)" % (bin_type.__name__,txt_type.__name__)) + "(%s, %s, Binary)" % ( + bin_type.__name__, txt_type.__name__)) if self.max_bytes is not None and len(value) > self.max_bytes: self.error('Binary value is too long') @@ -1270,9 +1277,9 @@ class SequenceField(IntField): """ Generate and Increment the counter """ - sequence_id = "%s.%s"%(self.owner_document._get_collection_name(), - self.name) - collection = get_db(alias = self.db_alias )[self.collection_name] + sequence_id = "%s.%s" % (self.owner_document._get_collection_name(), + self.name) + collection = get_db(alias=self.db_alias)[self.collection_name] counter = collection.find_and_modify(query={"_id": sequence_id}, update={"$inc": {"next": 1}}, new=True, diff --git a/mongoengine/python3_support.py b/mongoengine/python_support.py similarity index 51% rename from mongoengine/python3_support.py rename to mongoengine/python_support.py index 0682d873..70c31815 100644 --- a/mongoengine/python3_support.py +++ b/mongoengine/python_support.py @@ -1,13 +1,14 @@ -"""Helper functions and types to aid with Python 3 support.""" +"""Helper functions and types to aid with Python 2.5 - 3 support.""" import sys PY3 = sys.version_info[0] == 3 +PY25 = sys.version_info[:2] == (2, 5) if PY3: import codecs from io import BytesIO as StringIO - # return s converted to binary. b('test') should be equivalent to b'test' + # return s converted to binary. b('test') should be equivalent to b'test' def b(s): return codecs.latin_1_encode(s)[0] @@ -24,6 +25,19 @@ else: return s bin_type = str - txt_type = unicode + txt_type = unicode str_types = (bin_type, txt_type) + +if PY25: + def product(*args, **kwds): + pools = map(tuple, args) * kwds.get('repeat', 1) + result = [[]] + for pool in pools: + result = [x + [y] for x in result for y in pool] + for prod in result: + yield tuple(prod) + reduce = reduce +else: + from itertools import product + from functools import reduce diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index f849c131..2851d5d1 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -3,9 +3,9 @@ import re import copy import itertools import operator -import functools -import sys + from functools import partial +from mongoengine.python_support import product, reduce import pymongo from bson.code import Code @@ -15,18 +15,6 @@ from mongoengine import signals __all__ = ['queryset_manager', 'Q', 'InvalidQueryError', 'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL'] -if sys.version_info < (2,6,0): - def product(*args, **kwds): - pools = map(tuple, args) * kwds.get('repeat', 1) - result = [[]] - for pool in pools: - result = [x+[y] for x in result for y in pool] - for prod in result: - yield tuple(prod) -else: - from itertools import product - from functools import reduce - # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 diff --git a/setup.cfg b/setup.cfg index ad9b9289..3b1a561c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,5 @@ detailed-errors = 1 #cover-html = 1 #cover-html-dir = ../htmlcov #cover-package = mongoengine -where = tests py3where = build -#tests = test_bugfix.py +where = tests diff --git a/setup.py b/setup.py index 5f59b1a4..398bd98a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ -from setuptools import setup, find_packages import os import sys +from setuptools import setup, find_packages # Hack to silence atexit traceback in newer python versions try: @@ -16,6 +16,7 @@ try: except: pass + def get_version(version_tuple): version = '%s.%s' % (version_tuple[0], version_tuple[1]) if version_tuple[2]: @@ -38,7 +39,13 @@ CLASSIFIERS = [ 'Operating System :: OS Independent', 'Programming Language :: Python', "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.5", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.1", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: Implementation :: CPython", 'Topic :: Database', 'Topic :: Software Development :: Libraries :: Python Modules', ] diff --git a/tests/test_django.py b/tests/test_django.py index 32a3fe14..678d7cfe 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -1,7 +1,7 @@ from __future__ import with_statement import unittest from nose.plugins.skip import SkipTest -from mongoengine.python3_support import PY3 +from mongoengine.python_support import PY3 from mongoengine import * try: diff --git a/tests/test_document.py b/tests/test_document.py index ae6aaa20..788b1e31 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -43,7 +43,7 @@ class DocumentTest(unittest.TestCase): self.warning_list = [] showwarning_default = warnings.showwarning - + def append_to_warning_list(message,category, *args): self.warning_list.append({"message":message, "category":category}) @@ -55,7 +55,7 @@ class DocumentTest(unittest.TestCase): class InheritedClass(SimpleBase): b = IntField() - + # restore default handling of warnings warnings.showwarning = showwarning_default @@ -150,7 +150,7 @@ class DocumentTest(unittest.TestCase): def append_to_warning_list(message, category, *args): self.warning_list.append({'message':message, 'category':category}) - + # add warnings to self.warning_list instead of stderr warnings.showwarning = append_to_warning_list warnings.simplefilter("always") @@ -209,7 +209,6 @@ class DocumentTest(unittest.TestCase): } self.assertEqual(Dog._superclasses, dog_superclasses) - def test_external_superclasses(self): """Ensure that the correct list of sub and super classes is assembled. when importing part of the model @@ -568,22 +567,22 @@ class DocumentTest(unittest.TestCase): class Drinker(Document): drink = GenericReferenceField() - + try: warnings.simplefilter("error") - + class AcloholicDrink(Drink): meta = {'collection': 'booze'} - + except SyntaxWarning, w: warnings.simplefilter("ignore") - + class AlcoholicDrink(Drink): meta = {'collection': 'booze'} - + else: raise AssertionError("SyntaxWarning should be triggered") - + warnings.resetwarnings() Drink.drop_collection() diff --git a/tests/test_fields.py b/tests/test_fields.py index 1d6b32cd..82099382 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -14,7 +14,7 @@ from nose.plugins.skip import SkipTest from mongoengine import * from mongoengine.connection import get_db from mongoengine.base import _document_registry, NotRegistered -from mongoengine.python3_support import PY3, b, StringIO, bin_type +from mongoengine.python_support import PY3, b, StringIO, bin_type TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), 'mongoengine.png') @@ -383,7 +383,7 @@ class FieldTest(unittest.TestCase): self.assertNotEqual(log.date, d1) self.assertEqual(log.date, d2) - if not PY3: + if not PY3: # Pre UTC dates microseconds below 1000 are dropped # This does not seem to be true in PY3 d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999) @@ -1744,7 +1744,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(doc_b.the_file.grid_id, doc_c.the_file.grid_id) # Test with default - doc_d = GridDocument(the_file=b('')) + doc_d = GridDocument(the_file=b('')) doc_d.save() doc_e = GridDocument.objects.with_id(doc_d.id) @@ -2157,11 +2157,11 @@ class FieldTest(unittest.TestCase): post = Post(title='hello world') post.comments.append(Comment(content='hello', author=bob)) post.comments.append(Comment(author=bob)) - + self.assertRaises(ValidationError, post.validate) try: post.validate() - except ValidationError, error: + except ValidationError, error: # ValidationError.errors property self.assertTrue(hasattr(error, 'errors')) self.assertTrue(isinstance(error.errors, dict)) diff --git a/tests/test_queryset.py b/tests/test_queryset.py index e6569160..7e425508 100644 --- a/tests/test_queryset.py +++ b/tests/test_queryset.py @@ -9,7 +9,7 @@ from bson import ObjectId from mongoengine import * from mongoengine.connection import get_connection -from mongoengine.python3_support import PY3 +from mongoengine.python_support import PY3 from mongoengine.tests import query_counter from mongoengine.queryset import (QuerySet, QuerySetManager, MultipleObjectsReturned, DoesNotExist, @@ -1455,6 +1455,8 @@ class QuerySetTest(unittest.TestCase): name = StringField() parent = ReferenceField('self', reverse_delete_rule=CASCADE) + Category.drop_collection() + num_children = 3 base = Category(name='Root') base.save()