Skip to content

Commit 3b88712

Browse files
authored
Cleaner as_pymongo (#1549)
1 parent 33e9ef2 commit 3b88712

File tree

2 files changed

+101
-76
lines changed

2 files changed

+101
-76
lines changed

mongoengine/queryset/base.py

Lines changed: 28 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ def __init__(self, document, collection):
6767
self._scalar = []
6868
self._none = False
6969
self._as_pymongo = False
70-
self._as_pymongo_coerce = False
7170
self._search_text = None
7271

7372
# If inheritance is allowed, only return instances and instances of
@@ -728,11 +727,12 @@ def _clone_into(self, new_qs):
728727
'%s is not a subclass of BaseQuerySet' % new_qs.__name__)
729728

730729
copy_props = ('_mongo_query', '_initial_query', '_none', '_query_obj',
731-
'_where_clause', '_loaded_fields', '_ordering', '_snapshot',
732-
'_timeout', '_class_check', '_slave_okay', '_read_preference',
733-
'_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce',
730+
'_where_clause', '_loaded_fields', '_ordering',
731+
'_snapshot', '_timeout', '_class_check', '_slave_okay',
732+
'_read_preference', '_iter', '_scalar', '_as_pymongo',
734733
'_limit', '_skip', '_hint', '_auto_dereference',
735-
'_search_text', 'only_fields', '_max_time_ms', '_comment')
734+
'_search_text', 'only_fields', '_max_time_ms',
735+
'_comment')
736736

737737
for prop in copy_props:
738738
val = getattr(self, prop)
@@ -939,7 +939,8 @@ def fields(self, _only_called=False, **kwargs):
939939
940940
posts = BlogPost.objects(...).fields(slice__comments=5)
941941
942-
:param kwargs: A set keywors arguments identifying what to include.
942+
:param kwargs: A set of keyword arguments identifying what to
943+
include, exclude, or slice.
943944
944945
.. versionadded:: 0.5
945946
"""
@@ -1128,16 +1129,15 @@ def values_list(self, *fields):
11281129
"""An alias for scalar"""
11291130
return self.scalar(*fields)
11301131

1131-
def as_pymongo(self, coerce_types=False):
1132+
def as_pymongo(self):
11321133
"""Instead of returning Document instances, return raw values from
11331134
pymongo.
11341135
1135-
:param coerce_types: Field types (if applicable) would be use to
1136-
coerce types.
1136+
This method is particularly useful if you don't need dereferencing
1137+
and care primarily about the speed of data retrieval.
11371138
"""
11381139
queryset = self.clone()
11391140
queryset._as_pymongo = True
1140-
queryset._as_pymongo_coerce = coerce_types
11411141
return queryset
11421142

11431143
def max_time_ms(self, ms):
@@ -1799,59 +1799,25 @@ def lookup(obj, name):
17991799

18001800
return tuple(data)
18011801

1802-
def _get_as_pymongo(self, row):
1803-
# Extract which fields paths we should follow if .fields(...) was
1804-
# used. If not, handle all fields.
1805-
if not getattr(self, '__as_pymongo_fields', None):
1806-
self.__as_pymongo_fields = []
1807-
1808-
for field in self._loaded_fields.fields - set(['_cls']):
1809-
self.__as_pymongo_fields.append(field)
1810-
while '.' in field:
1811-
field, _ = field.rsplit('.', 1)
1812-
self.__as_pymongo_fields.append(field)
1813-
1814-
all_fields = not self.__as_pymongo_fields
1815-
1816-
def clean(data, path=None):
1817-
path = path or ''
1818-
1819-
if isinstance(data, dict):
1820-
new_data = {}
1821-
for key, value in data.iteritems():
1822-
new_path = '%s.%s' % (path, key) if path else key
1823-
1824-
if all_fields:
1825-
include_field = True
1826-
elif self._loaded_fields.value == QueryFieldList.ONLY:
1827-
include_field = new_path in self.__as_pymongo_fields
1828-
else:
1829-
include_field = new_path not in self.__as_pymongo_fields
1802+
def _get_as_pymongo(self, doc):
1803+
"""Clean up a PyMongo doc, removing fields that were only fetched
1804+
for the sake of MongoEngine's implementation, and return it.
1805+
"""
1806+
# Always remove _cls as a MongoEngine's implementation detail.
1807+
if '_cls' in doc:
1808+
del doc['_cls']
1809+
1810+
# If the _id was not included in a .only or was excluded in a .exclude,
1811+
# remove it from the doc (we always fetch it so that we can properly
1812+
# construct documents).
1813+
fields = self._loaded_fields
1814+
if fields and '_id' in doc and (
1815+
(fields.value == QueryFieldList.ONLY and '_id' not in fields.fields) or
1816+
(fields.value == QueryFieldList.EXCLUDE and '_id' in fields.fields)
1817+
):
1818+
del doc['_id']
18301819

1831-
if include_field:
1832-
new_data[key] = clean(value, path=new_path)
1833-
data = new_data
1834-
elif isinstance(data, list):
1835-
data = [clean(d, path=path) for d in data]
1836-
else:
1837-
if self._as_pymongo_coerce:
1838-
# If we need to coerce types, we need to determine the
1839-
# type of this field and use the corresponding
1840-
# .to_python(...)
1841-
EmbeddedDocumentField = _import_class('EmbeddedDocumentField')
1842-
1843-
obj = self._document
1844-
for chunk in path.split('.'):
1845-
obj = getattr(obj, chunk, None)
1846-
if obj is None:
1847-
break
1848-
elif isinstance(obj, EmbeddedDocumentField):
1849-
obj = obj.document_type
1850-
if obj and data is not None:
1851-
data = obj.to_python(data)
1852-
return data
1853-
1854-
return clean(row)
1820+
return doc
18551821

18561822
def _sub_js_fields(self, code):
18571823
"""When fields are specified with [~fieldname] syntax, where

tests/queryset/queryset.py

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4047,6 +4047,35 @@ class Person(Document):
40474047
plist = list(Person.objects.scalar('name', 'state'))
40484048
self.assertEqual(plist, [(u'Wilson JR', s1)])
40494049

4050+
def test_generic_reference_field_with_only_and_as_pymongo(self):
4051+
class TestPerson(Document):
4052+
name = StringField()
4053+
4054+
class TestActivity(Document):
4055+
name = StringField()
4056+
owner = GenericReferenceField()
4057+
4058+
TestPerson.drop_collection()
4059+
TestActivity.drop_collection()
4060+
4061+
person = TestPerson(name='owner')
4062+
person.save()
4063+
4064+
a1 = TestActivity(name='a1', owner=person)
4065+
a1.save()
4066+
4067+
activity = TestActivity.objects(owner=person).scalar('id', 'owner').no_dereference().first()
4068+
self.assertEqual(activity[0], a1.pk)
4069+
self.assertEqual(activity[1]['_ref'], DBRef('test_person', person.pk))
4070+
4071+
activity = TestActivity.objects(owner=person).only('id', 'owner')[0]
4072+
self.assertEqual(activity.pk, a1.pk)
4073+
self.assertEqual(activity.owner, person)
4074+
4075+
activity = TestActivity.objects(owner=person).only('id', 'owner').as_pymongo().first()
4076+
self.assertEqual(activity['_id'], a1.pk)
4077+
self.assertTrue(activity['owner']['_ref'], DBRef('test_person', person.pk))
4078+
40504079
def test_scalar_db_field(self):
40514080

40524081
class TestDoc(Document):
@@ -4392,21 +4421,44 @@ class Doc(Document):
43924421
self.assertEqual(doc_objects, Doc.objects.from_json(json_data))
43934422

43944423
def test_as_pymongo(self):
4395-
43964424
from decimal import Decimal
43974425

4426+
class LastLogin(EmbeddedDocument):
4427+
location = StringField()
4428+
ip = StringField()
4429+
43984430
class User(Document):
43994431
id = ObjectIdField('_id')
44004432
name = StringField()
44014433
age = IntField()
44024434
price = DecimalField()
4435+
last_login = EmbeddedDocumentField(LastLogin)
44034436

44044437
User.drop_collection()
4405-
User(name="Bob Dole", age=89, price=Decimal('1.11')).save()
4406-
User(name="Barack Obama", age=51, price=Decimal('2.22')).save()
4438+
4439+
User.objects.create(name="Bob Dole", age=89, price=Decimal('1.11'))
4440+
User.objects.create(
4441+
name="Barack Obama",
4442+
age=51,
4443+
price=Decimal('2.22'),
4444+
last_login=LastLogin(
4445+
location='White House',
4446+
ip='104.107.108.116'
4447+
)
4448+
)
4449+
4450+
results = User.objects.as_pymongo()
4451+
self.assertEqual(
4452+
set(results[0].keys()),
4453+
set(['_id', 'name', 'age', 'price'])
4454+
)
4455+
self.assertEqual(
4456+
set(results[1].keys()),
4457+
set(['_id', 'name', 'age', 'price', 'last_login'])
4458+
)
44074459

44084460
results = User.objects.only('id', 'name').as_pymongo()
4409-
self.assertEqual(sorted(results[0].keys()), sorted(['_id', 'name']))
4461+
self.assertEqual(set(results[0].keys()), set(['_id', 'name']))
44104462

44114463
users = User.objects.only('name', 'price').as_pymongo()
44124464
results = list(users)
@@ -4417,16 +4469,20 @@ class User(Document):
44174469
self.assertEqual(results[1]['name'], 'Barack Obama')
44184470
self.assertEqual(results[1]['price'], 2.22)
44194471

4420-
# Test coerce_types
4421-
users = User.objects.only(
4422-
'name', 'price').as_pymongo(coerce_types=True)
4472+
users = User.objects.only('name', 'last_login').as_pymongo()
44234473
results = list(users)
44244474
self.assertTrue(isinstance(results[0], dict))
44254475
self.assertTrue(isinstance(results[1], dict))
4426-
self.assertEqual(results[0]['name'], 'Bob Dole')
4427-
self.assertEqual(results[0]['price'], Decimal('1.11'))
4428-
self.assertEqual(results[1]['name'], 'Barack Obama')
4429-
self.assertEqual(results[1]['price'], Decimal('2.22'))
4476+
self.assertEqual(results[0], {
4477+
'name': 'Bob Dole'
4478+
})
4479+
self.assertEqual(results[1], {
4480+
'name': 'Barack Obama',
4481+
'last_login': {
4482+
'location': 'White House',
4483+
'ip': '104.107.108.116'
4484+
}
4485+
})
44304486

44314487
def test_as_pymongo_json_limit_fields(self):
44324488

@@ -4590,7 +4646,6 @@ def __unicode__(self):
45904646

45914647
def test_no_cache(self):
45924648
"""Ensure you can add meta data to file"""
4593-
45944649
class Noddy(Document):
45954650
fields = DictField()
45964651

@@ -4608,15 +4663,19 @@ class Noddy(Document):
46084663

46094664
self.assertEqual(len(list(docs)), 100)
46104665

4666+
# Can't directly get a length of a no-cache queryset.
46114667
with self.assertRaises(TypeError):
46124668
len(docs)
46134669

4670+
# Another iteration over the queryset should result in another db op.
46144671
with query_counter() as q:
4615-
self.assertEqual(q, 0)
46164672
list(docs)
46174673
self.assertEqual(q, 1)
4674+
4675+
# ... and another one to double-check.
4676+
with query_counter() as q:
46184677
list(docs)
4619-
self.assertEqual(q, 2)
4678+
self.assertEqual(q, 1)
46204679

46214680
def test_nested_queryset_iterator(self):
46224681
# Try iterating the same queryset twice, nested.

0 commit comments

Comments
 (0)