Skip to content

Commit 7dfad08

Browse files
committed
Fixes #105 Added a mutator function for the ViewField decorator which allows a reduce function to be set on a ViewField using a second decorator.
1 parent e8eab1f commit 7dfad08

File tree

2 files changed

+131
-23
lines changed

2 files changed

+131
-23
lines changed

couchdb/mapping.py

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979

8080
class Field(object):
8181
"""Basic unit for mapping a piece of data between Python and JSON.
82-
82+
8383
Instances of this class can be added to subclasses of `Document` to describe
8484
the mapping of a document.
8585
"""
@@ -191,7 +191,7 @@ def _to_json(self, value):
191191
class ViewField(object):
192192
r"""Descriptor that can be used to bind a view definition to a property of
193193
a `Document` class.
194-
194+
195195
>>> class Person(Document):
196196
... name = TextField()
197197
... age = IntegerField()
@@ -201,51 +201,73 @@ class ViewField(object):
201201
... }''')
202202
>>> Person.by_name
203203
<ViewDefinition '_design/people/_view/by_name'>
204-
204+
205205
>>> print(Person.by_name.map_fun)
206206
function(doc) {
207207
emit(doc.name, doc);
208208
}
209-
209+
210210
That property can be used as a function, which will execute the view.
211-
211+
212212
>>> from couchdb import Database
213213
>>> db = Database('python-tests')
214-
214+
215215
>>> Person.by_name(db, count=3)
216216
<ViewResults <PermanentView '_design/people/_view/by_name'> {'count': 3}>
217-
217+
218218
The results produced by the view are automatically wrapped in the
219219
`Document` subclass the descriptor is bound to. In this example, it would
220220
return instances of the `Person` class. But please note that this requires
221221
the values of the view results to be dictionaries that can be mapped to the
222222
mapping defined by the containing `Document` class. Alternatively, the
223223
``include_docs`` query option can be used to inline the actual documents in
224224
the view results, which will then be used instead of the values.
225-
225+
226226
If you use Python view functions, this class can also be used as a
227227
decorator:
228-
228+
229229
>>> class Person(Document):
230230
... name = TextField()
231231
... age = IntegerField()
232232
...
233233
... @ViewField.define('people')
234234
... def by_name(doc):
235235
... yield doc['name'], doc
236-
236+
237237
>>> Person.by_name
238238
<ViewDefinition '_design/people/_view/by_name'>
239239
240240
>>> print(Person.by_name.map_fun)
241241
def by_name(doc):
242242
yield doc['name'], doc
243+
244+
Alternatively, a map and reduce function can be used explicitly. The results
245+
with a reduce function will always return the actual documents rather than
246+
the results mapped by the containing `Document` class.
247+
248+
>>> class Item(Document):
249+
... sku = TextField()
250+
... color = IntegerField()
251+
...
252+
... stock = ViewField('people')
253+
...
254+
... @stock.map
255+
... def map(doc):
256+
... yield doc['sku'], doc)
257+
...
258+
... @stock.reduce
259+
... def reduce(keys, values, rereduce):
260+
... if rereduce:
261+
... yield sum(values)
262+
... else:
263+
... yield len(values)
264+
243265
"""
244266

245-
def __init__(self, design, map_fun, reduce_fun=None, name=None,
267+
def __init__(self, design, map_fun=None, reduce_fun=None, name=None,
246268
language='javascript', wrapper=DEFAULT, **defaults):
247269
"""Initialize the view descriptor.
248-
270+
249271
:param design: the name of the design document
250272
:param map_fun: the map function code
251273
:param reduce_fun: the reduce function code (optional)
@@ -271,8 +293,33 @@ def define(cls, design, name=None, language='python', wrapper=DEFAULT,
271293
view code).
272294
"""
273295
def view_wrapped(fun):
274-
return cls(design, fun, language=language, wrapper=wrapper,
275-
**defaults)
296+
return cls(design, fun, name=name, language=language,
297+
wrapper=wrapper, **defaults)
298+
return view_wrapped
299+
300+
@property
301+
def map(self):
302+
"""Property method for use as a decorator to set a reduce function
303+
"""
304+
if self.map_fun is not None:
305+
raise AttributeError("ViewField already has a map function")
306+
307+
def view_wrapped(fun):
308+
self.map_fun = fun
309+
return self
310+
return view_wrapped
311+
312+
@property
313+
def reduce(self):
314+
"""Property method for use as a decorator to set a reduce function
315+
"""
316+
if self.reduce_fun is not None:
317+
raise AttributeError("ViewField already has a reduce function")
318+
319+
def view_wrapped(fun):
320+
self.reduce_fun = fun
321+
self.defaults['include_docs'] = True
322+
return self
276323
return view_wrapped
277324

278325
def __get__(self, instance, cls=None):
@@ -322,7 +369,7 @@ def _set_id(self, value):
322369
@property
323370
def rev(self):
324371
"""The document revision.
325-
372+
326373
:rtype: basestring
327374
"""
328375
if hasattr(self._data, 'rev'): # When data is client.Document
@@ -331,18 +378,18 @@ def rev(self):
331378

332379
def items(self):
333380
"""Return the fields as a list of ``(name, value)`` tuples.
334-
381+
335382
This method is provided to enable easy conversion to native dictionary
336383
objects, for example to allow use of `mapping.Document` instances with
337384
`client.Database.update`.
338-
385+
339386
>>> class Post(Document):
340387
... title = TextField()
341388
... author = TextField()
342389
>>> post = Post(id='foo-bar', title='Foo bar', author='Joe')
343390
>>> sorted(post.items())
344391
[('_id', 'foo-bar'), ('author', u'Joe'), ('title', u'Foo bar')]
345-
392+
346393
:return: a list of ``(name, value)`` tuples
347394
"""
348395
retval = []
@@ -358,7 +405,7 @@ def items(self):
358405
@classmethod
359406
def load(cls, db, id):
360407
"""Load a specific document from the given database.
361-
408+
362409
:param db: the `Database` object to retrieve the document from
363410
:param id: the document ID
364411
:return: the `Document` instance, or `None` if no document with the
@@ -378,7 +425,7 @@ def store(self, db):
378425
def query(cls, db, map_fun, reduce_fun, language='javascript', **options):
379426
"""Execute a CouchDB temporary view and map the result values back to
380427
objects of this mapping.
381-
428+
382429
Note that by default, any properties of the document that are not
383430
included in the values of the view will be treated as if they were
384431
missing from the document. If you want to load the full document for
@@ -391,7 +438,7 @@ def query(cls, db, map_fun, reduce_fun, language='javascript', **options):
391438
def view(cls, db, viewname, **options):
392439
"""Execute a CouchDB named view and map the result values back to
393440
objects of this mapping.
394-
441+
395442
Note that by default, any properties of the document that are not
396443
included in the values of the view will be treated as if they were
397444
missing from the document. If you want to load the full document for
@@ -448,7 +495,7 @@ def _to_json(self, value):
448495

449496
class DateField(Field):
450497
"""Mapping field for storing dates.
451-
498+
452499
>>> field = DateField()
453500
>>> field._to_python('2007-04-01')
454501
datetime.date(2007, 4, 1)
@@ -541,7 +588,7 @@ def _to_json(self, value):
541588

542589
class DictField(Field):
543590
"""Field type for nested dictionaries.
544-
591+
545592
>>> from couchdb import Server
546593
>>> server = Server()
547594
>>> db = server.create('python-tests')

couchdb/tests/mapping.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,73 @@ def test_query(self):
266266
self.assertEqual(type(results.rows[0]), self.Item)
267267

268268

269+
class WrappingDecoratorTestCase(testutil.TempDatabaseMixin, unittest.TestCase):
270+
class Person(mapping.Document):
271+
name = mapping.TextField()
272+
age = mapping.IntegerField()
273+
274+
count_by_name = mapping.ViewField('people')
275+
276+
@count_by_name.map
277+
def map(doc):
278+
yield doc['name'], doc
279+
280+
@count_by_name.reduce
281+
def reduce(keys, values, rereduce):
282+
if rereduce:
283+
yield sum(values)
284+
else:
285+
yield len(values)
286+
287+
def test_viewfield_is_viewfield(self):
288+
self.assertIsInstance(self.Person.count_by_name, mapping.ViewDefinition)
289+
290+
def test_viewfield_has_map_func(self):
291+
self.assertIsNotNone(self.Person.count_by_name.map_fun)
292+
293+
def test_get_map_func_source(self):
294+
self.assertEqual(str(self.Person.count_by_name.map_fun),
295+
'def map(doc):\n'
296+
' yield doc[\'name\'], doc')
297+
298+
def test_viewfield_has_reduce_func(self):
299+
self.assertIsNotNone(self.Person.count_by_name.reduce_fun)
300+
301+
def test_get_reduce_func_source(self):
302+
self.assertEqual(str(self.Person.count_by_name.reduce_fun),
303+
'def reduce(keys, values, rereduce):\n'
304+
' if rereduce:\n'
305+
' yield sum(values)\n'
306+
' else:\n'
307+
' yield len(values)')
308+
309+
def test_viewfield_defaults_include_docs_set_to_true(self):
310+
self.assertTrue(self.Person.count_by_name.defaults['include_docs'])
311+
312+
def test_second_reduce_function_is_not_allowed(self):
313+
with self.assertRaises(AttributeError):
314+
class Person(mapping.Document):
315+
@mapping.ViewField.define('people')
316+
def by_name(doc):
317+
yield doc['name'], doc
318+
319+
@by_name.reduce
320+
def by_name(keys, values, rereduce):
321+
yield values
322+
323+
# second reducer
324+
@by_name.reduce
325+
def by_name(keys, values, rereduce):
326+
yield values
327+
328+
269329
def suite():
270330
suite = unittest.TestSuite()
271331
suite.addTest(testutil.doctest_suite(mapping))
272332
suite.addTest(unittest.makeSuite(DocumentTestCase, 'test'))
273333
suite.addTest(unittest.makeSuite(ListFieldTestCase, 'test'))
274334
suite.addTest(unittest.makeSuite(WrappingTestCase, 'test'))
335+
suite.addTest(unittest.makeSuite(WrappingDecoratorTestCase, 'test'))
275336
return suite
276337

277338

0 commit comments

Comments
 (0)