Skip to content

Wrapper Decorator for Reduce function #323

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 70 additions & 23 deletions couchdb/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@

class Field(object):
"""Basic unit for mapping a piece of data between Python and JSON.

Instances of this class can be added to subclasses of `Document` to describe
the mapping of a document.
"""
Expand Down Expand Up @@ -191,7 +191,7 @@ def _to_json(self, value):
class ViewField(object):
r"""Descriptor that can be used to bind a view definition to a property of
a `Document` class.

>>> class Person(Document):
... name = TextField()
... age = IntegerField()
Expand All @@ -201,51 +201,73 @@ class ViewField(object):
... }''')
>>> Person.by_name
<ViewDefinition '_design/people/_view/by_name'>

>>> print(Person.by_name.map_fun)
function(doc) {
emit(doc.name, doc);
}

That property can be used as a function, which will execute the view.

>>> from couchdb import Database
>>> db = Database('python-tests')

>>> Person.by_name(db, count=3)
<ViewResults <PermanentView '_design/people/_view/by_name'> {'count': 3}>

The results produced by the view are automatically wrapped in the
`Document` subclass the descriptor is bound to. In this example, it would
return instances of the `Person` class. But please note that this requires
the values of the view results to be dictionaries that can be mapped to the
mapping defined by the containing `Document` class. Alternatively, the
``include_docs`` query option can be used to inline the actual documents in
the view results, which will then be used instead of the values.

If you use Python view functions, this class can also be used as a
decorator:

>>> class Person(Document):
... name = TextField()
... age = IntegerField()
...
... @ViewField.define('people')
... def by_name(doc):
... yield doc['name'], doc

>>> Person.by_name
<ViewDefinition '_design/people/_view/by_name'>

>>> print(Person.by_name.map_fun)
def by_name(doc):
yield doc['name'], doc

Alternatively, a map and reduce function can be used explicitly. The results
with a reduce function will always return the actual documents rather than
the results mapped by the containing `Document` class.

>>> class Item(Document):
... sku = TextField()
... color = IntegerField()
...
... stock = ViewField('people')
...
... @stock.map
... def map(doc):
... yield doc['sku'], doc
...
... @stock.reduce
... def reduce(keys, values, rereduce):
... if rereduce:
... yield sum(values)
... else:
... yield len(values)

"""

def __init__(self, design, map_fun, reduce_fun=None, name=None,
def __init__(self, design, map_fun=None, reduce_fun=None, name=None,
language='javascript', wrapper=DEFAULT, **defaults):
"""Initialize the view descriptor.

:param design: the name of the design document
:param map_fun: the map function code
:param reduce_fun: the reduce function code (optional)
Expand All @@ -271,8 +293,33 @@ def define(cls, design, name=None, language='python', wrapper=DEFAULT,
view code).
"""
def view_wrapped(fun):
return cls(design, fun, language=language, wrapper=wrapper,
**defaults)
return cls(design, fun, name=name, language=language,
wrapper=wrapper, **defaults)
return view_wrapped

@property
def map(self):
"""Property method for use as a decorator to set a reduce function
"""
if self.map_fun is not None:
raise AttributeError("ViewField already has a map function")

def view_wrapped(fun):
self.map_fun = fun
return self
return view_wrapped

@property
def reduce(self):
"""Property method for use as a decorator to set a reduce function
"""
if self.reduce_fun is not None:
raise AttributeError("ViewField already has a reduce function")

def view_wrapped(fun):
self.reduce_fun = fun
self.defaults['include_docs'] = True
return self
return view_wrapped

def __get__(self, instance, cls=None):
Expand Down Expand Up @@ -322,7 +369,7 @@ def _set_id(self, value):
@property
def rev(self):
"""The document revision.

:rtype: basestring
"""
if hasattr(self._data, 'rev'): # When data is client.Document
Expand All @@ -331,18 +378,18 @@ def rev(self):

def items(self):
"""Return the fields as a list of ``(name, value)`` tuples.

This method is provided to enable easy conversion to native dictionary
objects, for example to allow use of `mapping.Document` instances with
`client.Database.update`.

>>> class Post(Document):
... title = TextField()
... author = TextField()
>>> post = Post(id='foo-bar', title='Foo bar', author='Joe')
>>> sorted(post.items())
[('_id', 'foo-bar'), ('author', u'Joe'), ('title', u'Foo bar')]

:return: a list of ``(name, value)`` tuples
"""
retval = []
Expand All @@ -358,7 +405,7 @@ def items(self):
@classmethod
def load(cls, db, id):
"""Load a specific document from the given database.

:param db: the `Database` object to retrieve the document from
:param id: the document ID
:return: the `Document` instance, or `None` if no document with the
Expand All @@ -378,7 +425,7 @@ def store(self, db):
def query(cls, db, map_fun, reduce_fun, language='javascript', **options):
"""Execute a CouchDB temporary view and map the result values back to
objects of this mapping.

Note that by default, any properties of the document that are not
included in the values of the view will be treated as if they were
missing from the document. If you want to load the full document for
Expand All @@ -391,7 +438,7 @@ def query(cls, db, map_fun, reduce_fun, language='javascript', **options):
def view(cls, db, viewname, **options):
"""Execute a CouchDB named view and map the result values back to
objects of this mapping.

Note that by default, any properties of the document that are not
included in the values of the view will be treated as if they were
missing from the document. If you want to load the full document for
Expand Down Expand Up @@ -448,7 +495,7 @@ def _to_json(self, value):

class DateField(Field):
"""Mapping field for storing dates.

>>> field = DateField()
>>> field._to_python('2007-04-01')
datetime.date(2007, 4, 1)
Expand Down Expand Up @@ -541,7 +588,7 @@ def _to_json(self, value):

class DictField(Field):
"""Field type for nested dictionaries.

>>> from couchdb import Server
>>> server = Server()
>>> db = server.create('python-tests')
Expand Down
61 changes: 61 additions & 0 deletions couchdb/tests/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,73 @@ def test_query(self):
self.assertEqual(type(results.rows[0]), self.Item)


class WrappingDecoratorTestCase(testutil.TempDatabaseMixin, unittest.TestCase):
class Person(mapping.Document):
name = mapping.TextField()
age = mapping.IntegerField()

count_by_name = mapping.ViewField('people')

@count_by_name.map
def map(doc):
yield doc['name'], doc

@count_by_name.reduce
def reduce(keys, values, rereduce):
if rereduce:
yield sum(values)
else:
yield len(values)

def test_viewfield_is_viewfield(self):
self.assertIsInstance(self.Person.count_by_name, mapping.ViewDefinition)

def test_viewfield_has_map_func(self):
self.assertIsNotNone(self.Person.count_by_name.map_fun)

def test_get_map_func_source(self):
self.assertEqual(str(self.Person.count_by_name.map_fun),
'def map(doc):\n'
' yield doc[\'name\'], doc')

def test_viewfield_has_reduce_func(self):
self.assertIsNotNone(self.Person.count_by_name.reduce_fun)

def test_get_reduce_func_source(self):
self.assertEqual(str(self.Person.count_by_name.reduce_fun),
'def reduce(keys, values, rereduce):\n'
' if rereduce:\n'
' yield sum(values)\n'
' else:\n'
' yield len(values)')

def test_viewfield_defaults_include_docs_set_to_true(self):
self.assertTrue(self.Person.count_by_name.defaults['include_docs'])

def test_second_reduce_function_is_not_allowed(self):
with self.assertRaises(AttributeError):
class Person(mapping.Document):
@mapping.ViewField.define('people')
def by_name(doc):
yield doc['name'], doc

@by_name.reduce
def by_name(keys, values, rereduce):
yield values

# second reducer
@by_name.reduce
def by_name(keys, values, rereduce):
yield values


def suite():
suite = unittest.TestSuite()
suite.addTest(testutil.doctest_suite(mapping))
suite.addTest(unittest.makeSuite(DocumentTestCase, 'test'))
suite.addTest(unittest.makeSuite(ListFieldTestCase, 'test'))
suite.addTest(unittest.makeSuite(WrappingTestCase, 'test'))
suite.addTest(unittest.makeSuite(WrappingDecoratorTestCase, 'test'))
return suite


Expand Down