Skip to content
This repository has been archived by the owner on Jan 23, 2024. It is now read-only.

Support for Python 3 / latest MongoEngine #99

Merged
merged 4 commits into from
Oct 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ from flask_mongorest import MongoRest
from flask_mongorest.views import ResourceView
from flask_mongorest.resources import Resource
from flask_mongorest import operators as ops
from flask_mongorest import methods
from flask_mongorest import methods


app = Flask(__name__)
Expand Down
1 change: 1 addition & 0 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ test:
pre:
- flake8 ./
override:
- pyenv global 2.7.11 3.5.1
- tox
2 changes: 1 addition & 1 deletion example/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
'HOST': 'localhost',
'PORT': 27017,
'DB': 'mongorest_example_app',
'TZ_AWARE': True,
'TZ_AWARE': False,
},
)

Expand Down
5 changes: 4 additions & 1 deletion example/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ class Post(Document):
author = ReferenceField(User)
editor = ReferenceField(User)
tags = ListField(StringField(max_length=30))
user_lists = ListField(SafeReferenceField(User))
try:
user_lists = ListField(SafeReferenceField(User))
except NameError:
user_lists = ListField(ReferenceField(User))
sections = ListField(EmbeddedDocumentField(Content))
content = EmbeddedDocumentField(Content)
is_published = BooleanField()
Expand Down
12 changes: 7 additions & 5 deletions flask_mongorest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ def __init__(self, app, **kwargs):

def register(self, **kwargs):
def decorator(klass):

# Construct a url based on a 'name' kwarg with a fallback to a Mongo document's name
document_name = klass.resource.document.__name__.lower()
name = kwargs.pop('name', document_name)
url = kwargs.pop('url', '/%s/' % document_name)
# Construct a url based on a 'name' kwarg with a fallback to the
# view's class name. Note that the name must be unique.
name = kwargs.pop('name', klass.__name__)
url = kwargs.pop('url', None)
if not url:
document_name = klass.resource.document.__name__.lower()
url = '/%s/' % document_name

# Insert the url prefix, if it exists
if self.url_prefix:
Expand Down
54 changes: 33 additions & 21 deletions flask_mongorest/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@
from bson.dbref import DBRef
from bson.objectid import ObjectId
from flask import request, url_for
from urlparse import urlparse
from mongoengine.base.proxy import DocumentProxy
from mongoengine.fields import EmbeddedDocumentField, ListField, ReferenceField, GenericReferenceField, SafeReferenceField
try:
from urllib.parse import urlparse
except ImportError: # Python 2
from urlparse import urlparse

try: # closeio/mongoengine
from mongoengine.base.proxy import DocumentProxy
from mongoengine.fields import SafeReferenceField
except ImportError:
DocumentProxy = None
SafeReferenceField = None

from mongoengine.fields import EmbeddedDocumentField, ListField, ReferenceField, GenericReferenceField
from mongoengine.fields import DictField

from cleancat import ValidationError as SchemaValidationError
Expand All @@ -18,14 +28,12 @@
class ResourceMeta(type):
def __init__(cls, name, bases, classdict):
if classdict.get('__metaclass__') is not ResourceMeta:
for document,resource in cls.child_document_resources.iteritems():
for document, resource in cls.child_document_resources.items():
if resource == name:
cls.child_document_resources[document] = cls
type.__init__(cls, name, bases, classdict)


class Resource(object):

# MongoEngine Document class related to this resource (required)
document = None

Expand Down Expand Up @@ -90,8 +98,6 @@ class Resource(object):
# Must start and end with a "/"
uri_prefix = None

__metaclass__ = ResourceMeta

def __init__(self, view_method=None):
"""
Initializes a resource. Optionally, a method class can be given to
Expand All @@ -104,7 +110,7 @@ def __init__(self, view_method=None):
self._related_resources = self.get_related_resources()
self._rename_fields = self.get_rename_fields()
self._reverse_rename_fields = {}
for k, v in self._rename_fields.iteritems():
for k, v in self._rename_fields.items():
self._reverse_rename_fields[v] = k
assert len(self._rename_fields) == len(self._reverse_rename_fields), \
'Cannot rename multiple fields to the same name'
Expand Down Expand Up @@ -159,7 +165,7 @@ def raw_data(self):
raise ValidationError({'error': "Chunked Transfer-Encoding is not supported."})

try:
self._raw_data = json.loads(request.data, parse_constant=self._enforce_strict_json)
self._raw_data = json.loads(request.data.decode(), parse_constant=self._enforce_strict_json)
except ValueError:
raise ValidationError({'error': 'The request contains invalid JSON.'})
if not isinstance(self._raw_data, dict):
Expand Down Expand Up @@ -296,7 +302,7 @@ def get_filters(self):
and hence use the Gte operator to filter the data.
"""
filters = {}
for field, operators in getattr(self, 'filters', {}).iteritems():
for field, operators in getattr(self, 'filters', {}).items():
field_filters = {}
for op in operators:
if op.op == 'exact':
Expand Down Expand Up @@ -372,7 +378,7 @@ def get(obj, field_name, field_instance=None):
self._related_resources[field_name]().serialize_field(field_value, **kwargs)
)
else:
if isinstance(field_value, DocumentProxy):
if DocumentProxy and isinstance(field_value, DocumentProxy):
# Don't perform a DBRef isinstance check below since
# it might trigger an extra query.
return field_value.to_dbref()
Expand All @@ -392,7 +398,7 @@ def get(obj, field_name, field_instance=None):
if field_instance.field:
return {
key: get(elem, field_name, field_instance=field_instance.field)
for (key, elem) in field_value.iteritems()
for (key, elem) in field_value.items()
}
# ... or simply return the dict intact, if the field type
# wasn't specified
Expand Down Expand Up @@ -452,7 +458,7 @@ def get(obj, field_name, field_instance=None):
value = related_resource.serialize_field(value)
elif isinstance(value, dict):
value = dict((k, related_resource.serialize_field(v))
for (k, v) in value.iteritems())
for (k, v) in value.items())
else: # assume queryset or list
value = [related_resource.serialize_field(o)
for o in value]
Expand Down Expand Up @@ -509,13 +515,13 @@ def validate_request(self, obj=None):
# E.g. if a -> b, b -> c, then a should never be renamed to c.
fields_to_delete = []
fields_to_update = {}
for k, v in self._rename_fields.iteritems():
for k, v in self._rename_fields.items():
if v in self.data:
fields_to_update[k] = self.data[v]
fields_to_delete.append(v)
for k in fields_to_delete:
del self.data[k]
for k, v in fields_to_update.iteritems():
for k, v in fields_to_update.items():
self.data[k] = v

# If CleanCat schema exists on this resource, use it to perform the
Expand Down Expand Up @@ -578,7 +584,7 @@ def fetch_related_resources(self, objs, only_fields=None):
# above, and map the results to each object that references them.
# TODO This is in dire need of refactoring, or a complete overhaul
hints = {}
for field_name, q_obj in document_queryset.iteritems():
for field_name, q_obj in document_queryset.items():
doc = self.get_related_resources()[field_name].document

# Create a QuerySet based on the query object
Expand Down Expand Up @@ -606,7 +612,7 @@ def fetch_related_resources(self, objs, only_fields=None):
for obj in document_queryset[field_name]:
hint_field_instance = obj._fields[hint_field]
# Don't trigger a query for SafeReferenceFields
if isinstance(hint_field_instance, SafeReferenceField):
if SafeReferenceField and isinstance(hint_field_instance, SafeReferenceField):
hinted = obj._db_data[hint_field]
if hint_field_instance.dbref:
hinted = hinted.id
Expand All @@ -622,7 +628,7 @@ def fetch_related_resources(self, objs, only_fields=None):
# Assign the results to each object
# TODO This is in dire need of refactoring, or a complete overhaul
for obj in objs:
for field_name, hint_index in hints.iteritems():
for field_name, hint_index in hints.items():
obj_id = obj.id
if isinstance(obj_id, DBRef):
obj_id = obj_id.id
Expand All @@ -642,7 +648,7 @@ def apply_filters(self, qs, params=None):
if params is None:
params = self.params

for key, value in params.iteritems():
for key, value in params.items():
# If this is a resource identified by a URI, we need
# to extract the object id at this point since
# MongoEngine only understands the object id
Expand Down Expand Up @@ -871,7 +877,7 @@ def update_object(self, obj, data=None, save=True, parent_resources=None):

# If we're comparing reference fields, only compare ids without
# hitting the database
if isinstance(obj._fields.get(field), ReferenceField):
if hasattr(obj, '_db_data') and isinstance(obj._fields.get(field), ReferenceField):
db_val = obj._db_data.get(field)
id_from_obj = db_val and getattr(db_val, 'id', db_val)
id_from_data = value and getattr(value, 'pk', value)
Expand All @@ -891,3 +897,9 @@ def update_object(self, obj, data=None, save=True, parent_resources=None):
def delete_object(self, obj, parent_resources=None):
obj.delete()

# Py2/3 compatible way to do metaclasses (or six.add_metaclass)
body = vars(Resource).copy()
body.pop('__dict__', None)
body.pop('__weakref__', None)

Resource = ResourceMeta(Resource.__name__, Resource.__bases__, body)
11 changes: 8 additions & 3 deletions flask_mongorest/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def isint(int_str):
class MongoEncoder(json.JSONEncoder):
def default(self, value, **kwargs):
if isinstance(value, ObjectId):
return unicode(value)
return str(value)
if isinstance(value, DBRef):
return value.id
if isinstance(value, datetime.datetime):
Expand All @@ -28,6 +28,11 @@ def default(self, value, **kwargs):
return str(value)
return super(MongoEncoder, self).default(value, **kwargs)

try:
cmp
except NameError: # Python 3
cmp = lambda a, b: (a>b)-(a<b)

def cmp_fields(ordering):
# Takes a list of fields and directions and returns a
# comparison function for sorted() to perform client-side
Expand Down Expand Up @@ -55,7 +60,7 @@ def equal(a, b):
if isinstance(a, dict) and isinstance(b, dict):
if sorted(a.keys()) != sorted(b.keys()):
return False
for k, v in a.iteritems():
for k, v in a.items():
if not equal(b[k], v):
return False
return True
Expand All @@ -73,7 +78,7 @@ def equal(a, b):
# Don't evaluate lazy documents
if getattr(a, '_lazy', False) and getattr(b, '_lazy', False):
return True
return equal(a.to_dict(), b.to_dict())
return equal(dict(a.to_mongo()), dict(b.to_mongo()))

# Since comparing an aware and unaware datetime results in an
# exception and we may assign unaware datetimes to objects that
Expand Down
14 changes: 7 additions & 7 deletions flask_mongorest/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@

def serialize_mongoengine_validation_error(e):
def serialize_errors(errors):
if hasattr(errors, 'iteritems'):
return dict((k, serialize_errors(v)) for (k, v) in errors.iteritems())
if hasattr(errors, 'items'):
return dict((k, serialize_errors(v)) for (k, v) in errors.items())
else:
return unicode(errors)
return str(errors)

if e.errors:
return {'field-errors': serialize_errors(e.errors)}
Expand Down Expand Up @@ -61,7 +61,7 @@ def _dispatch_request(self, *args, **kwargs):
except Unauthorized as e:
return {'error': 'Unauthorized'}, '401 Unauthorized'
except NotFound as e:
return {'error': unicode(e)}, '404 Not Found'
return {'error': str(e)}, '404 Not Found'

def handle_validation_error(self, e):
if isinstance(e, ValidationError):
Expand Down Expand Up @@ -138,7 +138,7 @@ def post(self, **kwargs):
self._resource.validate_request()
try:
obj = self._resource.create_object()
except Exception, e:
except Exception as e:
self.handle_validation_error(e)

# Check if we have permission to create this object
Expand All @@ -161,7 +161,7 @@ def process_object(self, obj):

try:
obj = self._resource.update_object(obj)
except Exception, e:
except Exception as e:
self.handle_validation_error(e)

def process_objects(self, objs):
Expand All @@ -174,7 +174,7 @@ def process_objects(self, objs):
for obj in objs:
self.process_object(obj)
count += 1
except ValidationError, e:
except ValidationError as e:
e.message['count'] = count
raise e
else:
Expand Down
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ mimerender
python-dateutil
sphinx
cleancat>=0.3
Flask==0.9
Flask>=0.9
Flask-Views
Flask-WTF==0.8.4
pymongo<3.0
flake8
10 changes: 10 additions & 0 deletions requirements3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-e git://github.com/closeio/cleancat.git#egg=cleancat-dev
mongoengine
flask-mongoengine
mimerender
python-dateutil
sphinx
Flask>=0.9
Flask-Views
pymongo
flake8
1 change: 0 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[nosetests]
verbosity=2
detailed-errors=True
with-coverage=True
cover-package=flask_mongorest
cover-erase=True

Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
'Flask-MongoEngine',
'mimerender',
'nose',
'coverage',
'python-dateutil',
'cleancat'
],
Expand Down