Querysets now utilises a local cache
Changed __len__ behavour in the queryset (#247, #311)
This commit is contained in:
		| @@ -26,6 +26,7 @@ __all__ = ('QuerySet', 'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL') | ||||
|  | ||||
| # The maximum number of items to display in a QuerySet.__repr__ | ||||
| REPR_OUTPUT_SIZE = 20 | ||||
| ITER_CHUNK_SIZE = 100 | ||||
|  | ||||
| # Delete rules | ||||
| DO_NOTHING = 0 | ||||
| @@ -63,6 +64,9 @@ class QuerySet(object): | ||||
|         self._none = False | ||||
|         self._as_pymongo = False | ||||
|         self._as_pymongo_coerce = False | ||||
|         self._result_cache = [] | ||||
|         self._has_more = True | ||||
|         self._len = None | ||||
|  | ||||
|         # If inheritance is allowed, only return instances and instances of | ||||
|         # subclasses of the class being used | ||||
| @@ -109,13 +113,60 @@ class QuerySet(object): | ||||
|         queryset._class_check = class_check | ||||
|         return queryset | ||||
|  | ||||
|     def __len__(self): | ||||
|         """Since __len__ is called quite frequently (for example, as part of | ||||
|         list(qs) we populate the result cache and cache the length. | ||||
|         """ | ||||
|         if self._len is not None: | ||||
|             return self._len | ||||
|         if self._has_more: | ||||
|             # populate the cache | ||||
|             list(self._iter_results()) | ||||
|  | ||||
|         self._len = len(self._result_cache) | ||||
|         return self._len | ||||
|  | ||||
|     def __iter__(self): | ||||
|         """Support iterator protocol""" | ||||
|         queryset = self | ||||
|         if queryset._iter: | ||||
|             queryset = self.clone() | ||||
|         queryset.rewind() | ||||
|         return queryset | ||||
|         """Iteration utilises a results cache which iterates the cursor | ||||
|         in batches of ``ITER_CHUNK_SIZE``. | ||||
|  | ||||
|         If ``self._has_more`` the cursor hasn't been exhausted so cache then | ||||
|         batch.  Otherwise iterate the result_cache. | ||||
|         """ | ||||
|         self._iter = True | ||||
|         if self._has_more: | ||||
|             return self._iter_results() | ||||
|  | ||||
|         # iterating over the cache. | ||||
|         return iter(self._result_cache) | ||||
|  | ||||
|     def _iter_results(self): | ||||
|         """A generator for iterating over the result cache. | ||||
|  | ||||
|         Also populates the cache if there are more possible results to yield. | ||||
|         Raises StopIteration when there are no more results""" | ||||
|         pos = 0 | ||||
|         while True: | ||||
|             upper = len(self._result_cache) | ||||
|             while pos < upper: | ||||
|                 yield self._result_cache[pos] | ||||
|                 pos = pos + 1 | ||||
|             if not self._has_more: | ||||
|                 raise StopIteration | ||||
|             if len(self._result_cache) <= pos: | ||||
|                 self._populate_cache() | ||||
|  | ||||
|     def _populate_cache(self): | ||||
|         """ | ||||
|         Populates the result cache with ``ITER_CHUNK_SIZE`` more entries | ||||
|         (until the cursor is exhausted). | ||||
|         """ | ||||
|         if self._has_more: | ||||
|             try: | ||||
|                 for i in xrange(ITER_CHUNK_SIZE): | ||||
|                     self._result_cache.append(self.next()) | ||||
|             except StopIteration: | ||||
|                 self._has_more = False | ||||
|  | ||||
|     def __getitem__(self, key): | ||||
|         """Support skip and limit using getitem and slicing syntax. | ||||
| @@ -157,22 +208,15 @@ class QuerySet(object): | ||||
|  | ||||
|     def __repr__(self): | ||||
|         """Provides the string representation of the QuerySet | ||||
|  | ||||
|         .. versionchanged:: 0.6.13 Now doesnt modify the cursor | ||||
|         """ | ||||
|  | ||||
|         if self._iter: | ||||
|             return '.. queryset mid-iteration ..' | ||||
|  | ||||
|         data = [] | ||||
|         for i in xrange(REPR_OUTPUT_SIZE + 1): | ||||
|             try: | ||||
|                 data.append(self.next()) | ||||
|             except StopIteration: | ||||
|                 break | ||||
|         self._populate_cache() | ||||
|         data = self._result_cache[:REPR_OUTPUT_SIZE + 1] | ||||
|         if len(data) > REPR_OUTPUT_SIZE: | ||||
|             data[-1] = "...(remaining elements truncated)..." | ||||
|  | ||||
|         self.rewind() | ||||
|         return repr(data) | ||||
|  | ||||
|     # Core functions | ||||
| @@ -201,7 +245,7 @@ class QuerySet(object): | ||||
|             result = queryset.next() | ||||
|         except StopIteration: | ||||
|             msg = ("%s matching query does not exist." | ||||
|                     % queryset._document._class_name) | ||||
|                    % queryset._document._class_name) | ||||
|             raise queryset._document.DoesNotExist(msg) | ||||
|         try: | ||||
|             queryset.next() | ||||
| @@ -352,7 +396,12 @@ class QuerySet(object): | ||||
|         """ | ||||
|         if self._limit == 0: | ||||
|             return 0 | ||||
|         return self._cursor.count(with_limit_and_skip=with_limit_and_skip) | ||||
|         if with_limit_and_skip and self._len is not None: | ||||
|             return self._len | ||||
|         count = self._cursor.count(with_limit_and_skip=with_limit_and_skip) | ||||
|         if with_limit_and_skip: | ||||
|             self._len = count | ||||
|         return count | ||||
|  | ||||
|     def delete(self, write_concern=None): | ||||
|         """Delete the documents matched by the query. | ||||
| @@ -910,7 +959,7 @@ class QuerySet(object): | ||||
|             mr_args['out'] = output | ||||
|  | ||||
|         results = getattr(queryset._collection, map_reduce_function)( | ||||
|                             map_f, reduce_f, **mr_args) | ||||
|                           map_f, reduce_f, **mr_args) | ||||
|  | ||||
|         if map_reduce_function == 'map_reduce': | ||||
|             results = results.find() | ||||
| @@ -1084,20 +1133,18 @@ class QuerySet(object): | ||||
|     def next(self): | ||||
|         """Wrap the result in a :class:`~mongoengine.Document` object. | ||||
|         """ | ||||
|         self._iter = True | ||||
|         try: | ||||
|             if self._limit == 0 or self._none: | ||||
|                 raise StopIteration | ||||
|             if self._scalar: | ||||
|                 return self._get_scalar(self._document._from_son( | ||||
|                                         self._cursor.next())) | ||||
|             if self._as_pymongo: | ||||
|                 return self._get_as_pymongo(self._cursor.next()) | ||||
|         if self._limit == 0 or self._none: | ||||
|             raise StopIteration | ||||
|  | ||||
|             return self._document._from_son(self._cursor.next()) | ||||
|         except StopIteration, e: | ||||
|             self.rewind() | ||||
|             raise e | ||||
|         raw_doc = self._cursor.next() | ||||
|         if self._as_pymongo: | ||||
|             return self._get_as_pymongo(raw_doc) | ||||
|  | ||||
|         doc = self._document._from_son(raw_doc) | ||||
|         if self._scalar: | ||||
|             return self._get_scalar(doc) | ||||
|  | ||||
|         return doc | ||||
|  | ||||
|     def rewind(self): | ||||
|         """Rewind the cursor to its unevaluated state. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user