Updated .only() behaviour - now like exclude it is chainable (#202)

This commit is contained in:
Ross Lawley 2013-04-23 14:06:29 +00:00
parent 81c7007f80
commit e2f3406e89
5 changed files with 73 additions and 25 deletions

View File

@ -4,6 +4,7 @@ Changelog
Changes in 0.8.X Changes in 0.8.X
================ ================
- Updated .only() behaviour - now like exclude it is chainable (#202)
- Added with_limit_and_skip support to count() (#235) - Added with_limit_and_skip support to count() (#235)
- Removed __len__ from queryset (#247) - Removed __len__ from queryset (#247)
- Objects queryset manager now inherited (#256) - Objects queryset manager now inherited (#256)

View File

@ -114,6 +114,21 @@ explicit `queryset.count()` to update::
# New code # New code
Animal.objects(type="mammal").count()) Animal.objects(type="mammal").count())
.only() now inline with .exclude()
----------------------------------
The behaviour of `.only()` was highly ambious, now it works in the mirror fashion
to `.exclude()`. Chaining `.only()` calls will increase the fields required::
# Old code
Animal.objects().only(['type', 'name']).only('name', 'order') # Would have returned just `name`
# New code
Animal.objects().only('name')
Animal.objects().only(['name']).only('order') # Would return `name` and `order`
Client Client
====== ======
PyMongo 2.4 came with a new connection client; MongoClient_ and started the PyMongo 2.4 came with a new connection client; MongoClient_ and started the

View File

@ -7,11 +7,20 @@ class QueryFieldList(object):
ONLY = 1 ONLY = 1
EXCLUDE = 0 EXCLUDE = 0
def __init__(self, fields=[], value=ONLY, always_include=[]): def __init__(self, fields=None, value=ONLY, always_include=None, _only_called=False):
"""The QueryFieldList builder
:param fields: A list of fields used in `.only()` or `.exclude()`
:param value: How to handle the fields; either `ONLY` or `EXCLUDE`
:param always_include: Any fields to always_include eg `_cls`
:param _only_called: Has `.only()` been called? If so its a set of fields
otherwise it performs a union.
"""
self.value = value self.value = value
self.fields = set(fields) self.fields = set(fields or [])
self.always_include = set(always_include) self.always_include = set(always_include or [])
self._id = None self._id = None
self._only_called = _only_called
self.slice = {} self.slice = {}
def __add__(self, f): def __add__(self, f):
@ -26,7 +35,10 @@ class QueryFieldList(object):
self.slice = {} self.slice = {}
elif self.value is self.ONLY and f.value is self.ONLY: elif self.value is self.ONLY and f.value is self.ONLY:
self._clean_slice() self._clean_slice()
self.fields = self.fields.intersection(f.fields) if self._only_called:
self.fields = self.fields.union(f.fields)
else:
self.fields = f.fields
elif self.value is self.EXCLUDE and f.value is self.EXCLUDE: elif self.value is self.EXCLUDE and f.value is self.EXCLUDE:
self.fields = self.fields.union(f.fields) self.fields = self.fields.union(f.fields)
self._clean_slice() self._clean_slice()
@ -46,6 +58,9 @@ class QueryFieldList(object):
self.fields = self.fields.union(self.always_include) self.fields = self.fields.union(self.always_include)
else: else:
self.fields -= self.always_include self.fields -= self.always_include
if getattr(f, '_only_called', False):
self._only_called = True
return self return self
def __nonzero__(self): def __nonzero__(self):

View File

@ -624,19 +624,35 @@ class QuerySet(object):
post = BlogPost.objects(...).only("title", "author.name") post = BlogPost.objects(...).only("title", "author.name")
.. note :: `only()` is chainable and will perform a union ::
So with the following it will fetch both: `title` and `author.name`::
post = BlogPost.objects.only("title").only("author.name")
:func:`~mongoengine.queryset.QuerySet.all_fields` will reset any
field filters.
:param fields: fields to include :param fields: fields to include
.. versionadded:: 0.3 .. versionadded:: 0.3
.. versionchanged:: 0.5 - Added subfield support .. versionchanged:: 0.5 - Added subfield support
""" """
fields = dict([(f, QueryFieldList.ONLY) for f in fields]) fields = dict([(f, QueryFieldList.ONLY) for f in fields])
return self.fields(**fields) return self.fields(True, **fields)
def exclude(self, *fields): def exclude(self, *fields):
"""Opposite to .only(), exclude some document's fields. :: """Opposite to .only(), exclude some document's fields. ::
post = BlogPost.objects(...).exclude("comments") post = BlogPost.objects(...).exclude("comments")
.. note :: `exclude()` is chainable and will perform a union ::
So with the following it will exclude both: `title` and `author.name`::
post = BlogPost.objects.exclude("title").exclude("author.name")
:func:`~mongoengine.queryset.QuerySet.all_fields` will reset any
field filters.
:param fields: fields to exclude :param fields: fields to exclude
.. versionadded:: 0.5 .. versionadded:: 0.5
@ -644,7 +660,7 @@ class QuerySet(object):
fields = dict([(f, QueryFieldList.EXCLUDE) for f in fields]) fields = dict([(f, QueryFieldList.EXCLUDE) for f in fields])
return self.fields(**fields) return self.fields(**fields)
def fields(self, **kwargs): def fields(self, _only_called=False, **kwargs):
"""Manipulate how you load this document's fields. Used by `.only()` """Manipulate how you load this document's fields. Used by `.only()`
and `.exclude()` to manipulate which fields to retrieve. Fields also and `.exclude()` to manipulate which fields to retrieve. Fields also
allows for a greater level of control for example: allows for a greater level of control for example:
@ -678,7 +694,8 @@ class QuerySet(object):
for value, group in itertools.groupby(fields, lambda x: x[1]): for value, group in itertools.groupby(fields, lambda x: x[1]):
fields = [field for field, value in group] fields = [field for field, value in group]
fields = queryset._fields_to_dbfields(fields) fields = queryset._fields_to_dbfields(fields)
queryset._loaded_fields += QueryFieldList(fields, value=value) queryset._loaded_fields += QueryFieldList(fields, value=value, _only_called=_only_called)
return queryset return queryset
def all_fields(self): def all_fields(self):

View File

@ -20,47 +20,47 @@ class QueryFieldListTest(unittest.TestCase):
def test_include_include(self): def test_include_include(self):
q = QueryFieldList() q = QueryFieldList()
q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.ONLY) q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.ONLY, _only_called=True)
self.assertEqual(q.as_dict(), {'a': True, 'b': True}) self.assertEqual(q.as_dict(), {'a': 1, 'b': 1})
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY) q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
self.assertEqual(q.as_dict(), {'b': True}) self.assertEqual(q.as_dict(), {'a': 1, 'b': 1, 'c': 1})
def test_include_exclude(self): def test_include_exclude(self):
q = QueryFieldList() q = QueryFieldList()
q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.ONLY) q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.ONLY)
self.assertEqual(q.as_dict(), {'a': True, 'b': True}) self.assertEqual(q.as_dict(), {'a': 1, 'b': 1})
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.EXCLUDE) q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.EXCLUDE)
self.assertEqual(q.as_dict(), {'a': True}) self.assertEqual(q.as_dict(), {'a': 1})
def test_exclude_exclude(self): def test_exclude_exclude(self):
q = QueryFieldList() q = QueryFieldList()
q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.EXCLUDE) q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.EXCLUDE)
self.assertEqual(q.as_dict(), {'a': False, 'b': False}) self.assertEqual(q.as_dict(), {'a': 0, 'b': 0})
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.EXCLUDE) q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.EXCLUDE)
self.assertEqual(q.as_dict(), {'a': False, 'b': False, 'c': False}) self.assertEqual(q.as_dict(), {'a': 0, 'b': 0, 'c': 0})
def test_exclude_include(self): def test_exclude_include(self):
q = QueryFieldList() q = QueryFieldList()
q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.EXCLUDE) q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.EXCLUDE)
self.assertEqual(q.as_dict(), {'a': False, 'b': False}) self.assertEqual(q.as_dict(), {'a': 0, 'b': 0})
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY) q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
self.assertEqual(q.as_dict(), {'c': True}) self.assertEqual(q.as_dict(), {'c': 1})
def test_always_include(self): def test_always_include(self):
q = QueryFieldList(always_include=['x', 'y']) q = QueryFieldList(always_include=['x', 'y'])
q += QueryFieldList(fields=['a', 'b', 'x'], value=QueryFieldList.EXCLUDE) q += QueryFieldList(fields=['a', 'b', 'x'], value=QueryFieldList.EXCLUDE)
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY) q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
self.assertEqual(q.as_dict(), {'x': True, 'y': True, 'c': True}) self.assertEqual(q.as_dict(), {'x': 1, 'y': 1, 'c': 1})
def test_reset(self): def test_reset(self):
q = QueryFieldList(always_include=['x', 'y']) q = QueryFieldList(always_include=['x', 'y'])
q += QueryFieldList(fields=['a', 'b', 'x'], value=QueryFieldList.EXCLUDE) q += QueryFieldList(fields=['a', 'b', 'x'], value=QueryFieldList.EXCLUDE)
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY) q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
self.assertEqual(q.as_dict(), {'x': True, 'y': True, 'c': True}) self.assertEqual(q.as_dict(), {'x': 1, 'y': 1, 'c': 1})
q.reset() q.reset()
self.assertFalse(q) self.assertFalse(q)
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY) q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
self.assertEqual(q.as_dict(), {'x': True, 'y': True, 'b': True, 'c': True}) self.assertEqual(q.as_dict(), {'x': 1, 'y': 1, 'b': 1, 'c': 1})
def test_using_a_slice(self): def test_using_a_slice(self):
q = QueryFieldList() q = QueryFieldList()
@ -97,7 +97,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
qs = MyDoc.objects.fields(**dict(((i, 1) for i in include))) qs = MyDoc.objects.fields(**dict(((i, 1) for i in include)))
self.assertEqual(qs._loaded_fields.as_dict(), self.assertEqual(qs._loaded_fields.as_dict(),
{'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1}) {'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
qs = qs.only(*only) qs = qs.only(*only)
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1}) self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
qs = qs.exclude(*exclude) qs = qs.exclude(*exclude)
@ -134,15 +134,15 @@ class OnlyExcludeAllTest(unittest.TestCase):
qs = qs.only(*only) qs = qs.only(*only)
qs = qs.fields(slice__b=5) qs = qs.fields(slice__b=5)
self.assertEqual(qs._loaded_fields.as_dict(), self.assertEqual(qs._loaded_fields.as_dict(),
{'b': {'$slice': 5}, 'c': 1}) {'b': {'$slice': 5}, 'c': 1})
qs = qs.fields(slice__c=[5, 1]) qs = qs.fields(slice__c=[5, 1])
self.assertEqual(qs._loaded_fields.as_dict(), self.assertEqual(qs._loaded_fields.as_dict(),
{'b': {'$slice': 5}, 'c': {'$slice': [5, 1]}}) {'b': {'$slice': 5}, 'c': {'$slice': [5, 1]}})
qs = qs.exclude('c') qs = qs.exclude('c')
self.assertEqual(qs._loaded_fields.as_dict(), self.assertEqual(qs._loaded_fields.as_dict(),
{'b': {'$slice': 5}}) {'b': {'$slice': 5}})
def test_only(self): def test_only(self):
"""Ensure that QuerySet.only only returns the requested fields. """Ensure that QuerySet.only only returns the requested fields.
@ -328,7 +328,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
Numbers.drop_collection() Numbers.drop_collection()
numbers = Numbers(n=[0,1,2,3,4,5,-5,-4,-3,-2,-1]) numbers = Numbers(n=[0, 1, 2, 3, 4, 5, -5, -4, -3, -2, -1])
numbers.save() numbers.save()
# first three # first three
@ -368,7 +368,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
Numbers.drop_collection() Numbers.drop_collection()
numbers = Numbers() numbers = Numbers()
numbers.embedded = EmbeddedNumber(n=[0,1,2,3,4,5,-5,-4,-3,-2,-1]) numbers.embedded = EmbeddedNumber(n=[0, 1, 2, 3, 4, 5, -5, -4, -3, -2, -1])
numbers.save() numbers.save()
# first three # first three