Added Python 3 support to MongoEngine

This commit is contained in:
Laine
2012-08-01 17:21:48 -07:00
parent 4b3cea9e78
commit 91aa90ad4a
17 changed files with 673 additions and 535 deletions

View File

@@ -1,18 +1,19 @@
import operator
import sys
import warnings
from collections import defaultdict
from functools import partial
from queryset import QuerySet, QuerySetManager
from queryset import DoesNotExist, MultipleObjectsReturned
from queryset import DO_NOTHING
from mongoengine import signals
from mongoengine.python3_support import PY3, txt_type
import sys
import pymongo
from bson import ObjectId
import operator
from functools import partial
from bson.dbref import DBRef
@@ -402,7 +403,7 @@ class ComplexBaseField(BaseField):
"""
errors = {}
if self.field:
if hasattr(value, 'iteritems'):
if hasattr(value, 'iteritems') or hasattr(value, 'items'):
sequence = value.iteritems()
else:
sequence = enumerate(value)
@@ -491,7 +492,7 @@ class DocumentMetaclass(type):
attrs.update(_get_mixin_fields(p_base))
return attrs
metaclass = attrs.get('__metaclass__')
metaclass = attrs.get('my_metaclass')
super_new = super(DocumentMetaclass, cls).__new__
if metaclass and issubclass(metaclass, DocumentMetaclass):
return super_new(cls, name, bases, attrs)
@@ -583,7 +584,9 @@ class DocumentMetaclass(type):
raise InvalidDocumentError("Reverse delete rules are not supported for EmbeddedDocuments (field: %s)" % field.name)
f.document_type.register_delete_rule(new_class, field.name, delete_rule)
if field.name and hasattr(Document, field.name) and EmbeddedDocument not in new_class.mro():
if (field.name and
hasattr(Document, field.name) and
EmbeddedDocument not in new_class.mro()):
raise InvalidDocumentError("%s is a document method and not a valid field name" % field.name)
module = attrs.get('__module__')
@@ -602,6 +605,22 @@ class DocumentMetaclass(type):
global _document_registry
_document_registry[doc_class_name] = new_class
# in Python 2, User-defined methods objects have special read-only
# attributes 'im_func' and 'im_self' which contain the function obj
# and class instance object respectively. With Python 3 these special
# attributes have been replaced by __func__ and __self__. The Blinker
# module continues to use im_func and im_self, so the code below
# copies __func__ into im_func and __self__ into im_self for
# classmethod objects in Document derived classes.
if PY3:
for key, val in new_class.__dict__.items():
if isinstance(val, classmethod):
f = val.__get__(new_class)
if hasattr(f, '__func__') and not hasattr(f, 'im_func'):
f.__dict__.update({'im_func':getattr(f, '__func__')})
if hasattr(f, '__self__') and not hasattr(f, 'im_self'):
f.__dict__.update({'im_self':getattr(f, '__self__')})
return new_class
def add_to_class(self, name, value):
@@ -623,7 +642,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
#
# Also assume a class is abstract if it has abstract set to True in
# its meta dictionary. This allows custom Document superclasses.
if (attrs.get('__metaclass__') == TopLevelDocumentMetaclass or
if (attrs.get('my_metaclass') == TopLevelDocumentMetaclass or
('meta' in attrs and attrs['meta'].get('abstract', False))):
# Make sure no base class was non-abstract
non_abstract_bases = [b for b in bases
@@ -1189,14 +1208,17 @@ Invalid data to create a `%s` instance.\n%s""".strip() % (cls._class_name, error
def __repr__(self):
try:
u = unicode(self).encode('utf-8')
u = txt_type(self)
except (UnicodeEncodeError, UnicodeDecodeError):
u = '[Bad Unicode data]'
return '<%s: %s>' % (self.__class__.__name__, u)
def __str__(self):
if hasattr(self, '__unicode__'):
return unicode(self).encode('utf-8')
if PY3:
return self.__unicode__()
else:
return unicode(self).encode('utf-8')
return '%s object' % self.__class__.__name__
def __eq__(self, other):
@@ -1338,10 +1360,9 @@ class BaseDict(dict):
if sys.version_info < (2, 5):
# Prior to Python 2.5, Exception was an old-style class
import types
def subclass_exception(name, parents, unused):
import types
return types.ClassType(name, parents, {})
from types import ClassType
return ClassType(name, parents, {})
else:
def subclass_exception(name, parents, module):
return type(name, parents, {'__module__': module})

View File

@@ -1,4 +1,3 @@
from django.http import Http404
from mongoengine.queryset import QuerySet
from mongoengine.base import BaseDocument
from mongoengine.base import ValidationError
@@ -27,6 +26,7 @@ def get_document_or_404(cls, *args, **kwargs):
try:
return queryset.get(*args, **kwargs)
except (queryset._document.DoesNotExist, ValidationError):
from django.http import Http404
raise Http404('No %s matches the given query.' % queryset._document._class_name)
def get_list_or_404(cls, *args, **kwargs):
@@ -42,5 +42,6 @@ def get_list_or_404(cls, *args, **kwargs):
queryset = _get_queryset(cls)
obj_list = list(queryset.filter(*args, **kwargs))
if not obj_list:
from django.http import Http404
raise Http404('No %s matches the given query.' % queryset._document._class_name)
return obj_list

View File

@@ -1,10 +1,28 @@
#coding: utf-8
from django.test import TestCase
from django.conf import settings
from nose.plugins.skip import SkipTest
from mongoengine.python3_support import PY3
from mongoengine import connect
try:
from django.test import TestCase
from django.conf import settings
except Exception as err:
if PY3:
from unittest import TestCase
# Dummy value so no error
class settings:
MONGO_DATABASE_NAME = 'dummy'
else:
raise err
class MongoTestCase(TestCase):
def setUp(self):
if PY3:
raise SkipTest('django does not have Python 3 support')
"""
TestCase class that clear the collection between the tests
"""

View File

@@ -2,7 +2,7 @@ import pymongo
from bson.dbref import DBRef
from mongoengine import signals
from mongoengine import signals, queryset
from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument,
BaseDict, BaseList)
from queryset import OperationError
@@ -23,6 +23,9 @@ class EmbeddedDocument(BaseDocument):
:class:`~mongoengine.EmbeddedDocumentField` field type.
"""
# The __metaclass__ attribute is removed by 2to3 when running with Python3
# my_metaclass is defined so that metaclass can be queried in Python 2 & 3
my_metaclass = DocumentMetaclass
__metaclass__ = DocumentMetaclass
def __init__(self, *args, **kwargs):
@@ -91,9 +94,12 @@ class Document(BaseDocument):
disabled by either setting types to False on the specific index or
by setting index_types to False on the meta dictionary for the document.
"""
# The __metaclass__ attribute is removed by 2to3 when running with Python3
# my_metaclass is defined so that metaclass can be queried in Python 2 & 3
my_metaclass = TopLevelDocumentMetaclass
__metaclass__ = TopLevelDocumentMetaclass
@apply
def pk():
"""Primary key alias
"""
@@ -102,6 +108,7 @@ class Document(BaseDocument):
def fset(self, value):
return setattr(self, self._meta['id_field'], value)
return property(fget, fset)
pk = pk()
@classmethod
def _get_db(cls):
@@ -244,12 +251,11 @@ class Document(BaseDocument):
def cascade_save(self, *args, **kwargs):
"""Recursively saves any references / generic references on an object"""
from fields import ReferenceField, GenericReferenceField
import fields
_refs = kwargs.get('_refs', []) or []
for name, cls in self._fields.items():
if not isinstance(cls, (ReferenceField, GenericReferenceField)):
if not isinstance(cls, (fields.ReferenceField, fields.GenericReferenceField)):
continue
ref = getattr(self, name)
@@ -304,8 +310,8 @@ class Document(BaseDocument):
.. versionadded:: 0.5
"""
from dereference import DeReference
self._data = DeReference()(self._data, max_depth)
import dereference
self._data = dereference.DeReference()(self._data, max_depth)
return self
def reload(self, max_depth=1):
@@ -360,10 +366,9 @@ class Document(BaseDocument):
"""Drops the entire collection associated with this
:class:`~mongoengine.Document` type from the database.
"""
from mongoengine.queryset import QuerySet
db = cls._get_db()
db.drop_collection(cls._get_collection_name())
QuerySet._reset_already_indexed(cls)
queryset.QuerySet._reset_already_indexed(cls)
class DynamicDocument(Document):
@@ -379,7 +384,12 @@ class DynamicDocument(Document):
There is one caveat on Dynamic Documents: fields cannot start with `_`
"""
# The __metaclass__ attribute is removed by 2to3 when running with Python3
# my_metaclass is defined so that metaclass can be queried in Python 2 & 3
my_metaclass = TopLevelDocumentMetaclass
__metaclass__ = TopLevelDocumentMetaclass
_dynamic = True
def __delattr__(self, *args, **kwargs):
@@ -398,7 +408,11 @@ class DynamicEmbeddedDocument(EmbeddedDocument):
information about dynamic documents.
"""
# The __metaclass__ attribute is removed by 2to3 when running with Python3
# my_metaclass is defined so that metaclass can be queried in Python 2 & 3
my_metaclass = DocumentMetaclass
__metaclass__ = DocumentMetaclass
_dynamic = True
def __delattr__(self, *args, **kwargs):

View File

@@ -1,18 +1,23 @@
import datetime
import time
import decimal
import gridfs
import re
import sys
import time
import uuid
import warnings
from operator import itemgetter
import gridfs
from bson import Binary, DBRef, SON, ObjectId
from mongoengine.python3_support import (PY3, b, bin_type,
txt_type, str_types, StringIO)
from base import (BaseField, ComplexBaseField, ObjectIdField,
ValidationError, get_document, BaseDocument)
from queryset import DO_NOTHING, QuerySet
from document import Document, EmbeddedDocument
from connection import get_db, DEFAULT_CONNECTION_NAME
from operator import itemgetter
try:
@@ -21,12 +26,6 @@ except ImportError:
Image = None
ImageOps = None
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
__all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField',
'ObjectIdField', 'ReferenceField', 'ValidationError', 'MapField',
@@ -846,8 +845,9 @@ class BinaryField(BaseField):
return Binary(value)
def validate(self, value):
if not isinstance(value, (basestring, Binary)):
self.error('BinaryField only accepts string or bson Binary values')
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__))
if self.max_bytes is not None and len(value) > self.max_bytes:
self.error('Binary value is too long')
@@ -903,12 +903,14 @@ class GridFSProxy(object):
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.grid_id)
def __cmp__(self, other):
if not isinstance(other, GridFSProxy):
return -1
return cmp((self.grid_id, self.collection_name, self.db_alias),
(other.grid_id, other.collection_name, other.db_alias))
def __eq__(self, other):
if isinstance(other, GridFSProxy):
return ((self.grid_id == other.grid_id) and
(self.collection_name == other.collection_name) and
(self.db_alias == other.db_alias))
else:
return False
@property
def fs(self):
if not self._fs:
@@ -1020,7 +1022,8 @@ class FileField(BaseField):
def __set__(self, instance, value):
key = self.name
if (hasattr(value, 'read') and not isinstance(value, GridFSProxy)) or isinstance(value, basestring):
if ((hasattr(value, 'read') and not
isinstance(value, GridFSProxy)) or isinstance(value, str_types)):
# using "FileField() = file/string" notation
grid_file = instance._data.get(self.name)
# If a file already exists, delete it
@@ -1211,7 +1214,7 @@ class ImageField(FileField):
for att_name, att in extra_args.items():
if att and (isinstance(att, tuple) or isinstance(att, list)):
setattr(self, att_name, dict(
map(None, params_size, att)))
zip(params_size, att)))
else:
setattr(self, att_name, None)

View File

@@ -0,0 +1,29 @@
"""Helper functions and types to aid with Python 3 support."""
import sys
PY3 = sys.version_info[0] == 3
if PY3:
import codecs
from io import BytesIO as StringIO
# return s converted to binary. b('test') should be equivalent to b'test'
def b(s):
return codecs.latin_1_encode(s)[0]
bin_type = bytes
txt_type = str
else:
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
# Conversion to binary only necessary in Python 3
def b(s):
return s
bin_type = str
txt_type = unicode
str_types = (bin_type, txt_type)

View File

@@ -3,7 +3,7 @@ import re
import copy
import itertools
import operator
import functools
from functools import partial
import pymongo
@@ -121,12 +121,12 @@ class QueryTreeTransformerVisitor(QNodeVisitor):
# that ANDs the necessary part with the $or part.
clauses = []
for or_group in itertools.product(*or_groups):
q_object = reduce(lambda a, b: a & b, and_parts, Q())
q_object = reduce(lambda a, b: a & b, or_group, q_object)
q_object = functools.reduce(lambda a, b: a & b, and_parts, Q())
q_object = functools.reduce(lambda a, b: a & b, or_group, q_object)
clauses.append(q_object)
# Finally, $or the generated clauses in to one query. Each of the
# clauses is sufficient for the query to succeed.
return reduce(lambda a, b: a | b, clauses, Q())
return functools.reduce(lambda a, b: a | b, clauses, Q())
if combination.operation == combination.OR:
children = []