Skip to content

Commit 2a1eb95

Browse files
committed
Cursor class implementation to encapsulate returned objects.
Work on #36.
1 parent 31866dd commit 2a1eb95

File tree

1 file changed

+55
-48
lines changed

1 file changed

+55
-48
lines changed

marrow/mongo/core/trait/queryable.py

+55-48
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
from collections import Mapping
21
from functools import reduce
32
from operator import and_
43
from typing import Union, Set
54

6-
from pymongo.cursor import Cursor, CursorType
5+
from pymongo.cursor import Cursor as PyMongoCursor
76
from pymongo.collection import Collection as PyMongoCollection
87

98
from ..types import Optional, Set, Mapping, Tuple
@@ -13,49 +12,6 @@
1312
from ....package.loader import traverse
1413

1514

16-
class QueryableFilter(Filter):
17-
"""Additional behaviour for queryable filters.
18-
19-
This provides a simplified query syntax, free of nearly all boilerplate (repeated mandatory code).
20-
21-
Additional semantics include:
22-
23-
* Filters generated from Queryable collections may be iterated to issue the query and produce a PyMongo Cursor.
24-
25-
for record in User.age > 27:
26-
print(record)
27-
28-
* Filters may be combined as before (using binary "&" and "|" operators), however this has been extended to support
29-
sets of additional criteria, and additional operators: addition and subtraction. Where addition performs an "or"
30-
to include additional results, a subtraction performs an "and" of the inverse of the set contents, excluding
31-
results.
32-
33-
The last offers a few powerful additions:
34-
35-
* Examine the "contents" of a "room" record (an `@property` returning a `QueryableFilter`) and return only the
36-
siblings of the current record:
37-
38-
siblings = self.room.contents - {self}
39-
"""
40-
41-
def __getitem__(self, item:Union[str,int,slice]):
42-
"""Retrieve the value of the given string key, or apply skip/limit options."""
43-
44-
if isinstance(item, str):
45-
return super().__getitem__(item)
46-
47-
return None
48-
49-
def __iter__(self) -> Cursor:
50-
if hasattr(self.document, 'find') and getattr(self.document, '__bound__', None):
51-
return self.document.find(self)
52-
53-
elif self.collection:
54-
return self.collection.find(self)
55-
56-
raise TypeError("Can not iterate an unbound Filter instance.")
57-
58-
5915
class Queryable(Collection):
6016
"""Extend active collection behaviours to include querying.
6117
@@ -109,7 +65,51 @@ class Queryable(Collection):
10965
'use_cursor': 'useCursor',
11066
}
11167

112-
# _Filter = QueryableFilter
68+
Filter = Filter
69+
70+
class Cursor(PyMongoCursor):
71+
def __init__(self, document_class, *args, **kw):
72+
self._Document = document_class
73+
super().__init__(*args, **kw)
74+
75+
def __getitem__(self, index):
76+
return self._Document.from_mongo(super().__getitem__(index))
77+
78+
def first(self):
79+
try:
80+
return self.limit(-1).next()
81+
except StopIteration:
82+
return None
83+
84+
def next(self):
85+
return self._Document.from_mongo(super().__next__())
86+
87+
__next__ = next
88+
89+
__cursor_defaults__ = {
90+
#projection=None, # Apply a default projection.
91+
#skip=0, # Apply a default initial offset.
92+
#limit=0, # Apply a default integer record count.
93+
#no_cursor_timeout=False, # Disable cursor timeouts.
94+
#cursor_type=CursorType.NON_TAILABLE, # Alter the default "type" of cursor requested.
95+
#sort=None, # Default sort order.
96+
#allow_partial_results=False,
97+
#oplog_replay=False,
98+
#modifiers=None,
99+
#batch_size=0,
100+
#manipulate=True,
101+
#collation=None,
102+
#hint=None,
103+
#max_scan=None,
104+
#max_time_ms=None,
105+
#max=None,
106+
#min=None,
107+
#return_key=False,
108+
#show_record_id=False,
109+
#snapshot=False,
110+
#comment=None,
111+
#session=None,
112+
}
113113

114114
@classmethod
115115
def _prepare_query(cls, mapping:Mapping[str,str], valid, *args, **kw) -> Tuple[Collection, PyMongoCollection, Filter, dict]:
@@ -236,18 +236,25 @@ def _prepare_aggregate(cls, *args, **kw) -> Tuple[Collection, PyMongoCollection,
236236
return cls, collection, stages, options
237237

238238
@classmethod
239-
def find(cls, *args, **kw) -> Cursor:
239+
def find(cls, *args, **kw) -> PyMongoCursor:
240240
"""Query the collection this class is bound to.
241241
242242
Additional arguments are processed according to `_prepare_find` prior to passing to PyMongo, where positional
243243
parameters are interpreted as query fragments, parametric keyword arguments combined, and other keyword
244244
arguments passed along with minor transformation.
245245
246+
Defaults for values passed to `find_one` (the `Cursor` class) may be specified as a dictionary / mapping
247+
attribute of your model named `__cursor_defaults__`.
248+
249+
Documents returned by the resulting Cursor will be automatically encapsulated in their associated Document
250+
subclass.
251+
246252
https://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.find
247253
"""
248254

249255
Doc, collection, query, options = cls._prepare_find(*args, **kw)
250-
return collection.find(query, **options)
256+
return cls.Cursor(cls, collection, query, **{**cls.__cursor_defaults__, **options})
257+
# The above, somewhat odd ** expansion structure, avoids "multiple values for keyword" errors.
251258

252259
@classmethod
253260
def find_one(cls, *args, **kw) -> Optional[Document]:

0 commit comments

Comments
 (0)