diff --git a/couchdb/tests/view.py b/couchdb/tests/view.py index 79a18dd9..e6b05578 100644 --- a/couchdb/tests/view.py +++ b/couchdb/tests/view.py @@ -102,6 +102,90 @@ def test_reduce_empty(self): self.assertEqual(output.getvalue(), b'[true, [0]]\n') + def command_ddoc_add(self): + return [ + 'ddoc', + 'new', + '_design/test_update', + { + '_id': '_design/test_update', + '_rev': '8-d7379de23a751dc2a19e5638a7bbc5cc', + 'language': 'python', + 'updates': { + 'inc': '''\ +def fun(obj, req): + if obj is not None: + obj['field'] += 1 + return [obj, {"body": "."}] +''', + } + }, + ] + + def test_update(self): + import json + request = { + 'method': 'POST', + 'query': {}, + 'body': '', + } + commands = [ + self.command_ddoc_add(), + [ + 'ddoc', + '_design/test_update', + ['updates', 'inc'], + [None, request] + ], + [ + 'ddoc', + '_design/test_update', + ['updates', 'inc'], + [{'field': 41, 'other_field': 'x'}, request] + ], + ] + input = StringIO(b'\n'.join(json.dumps(c).encode('utf-8') + for c in commands)) + output = StringIO() + view.run(input=input, output=output) + results = [ + json.loads(l.decode('utf-8')) + for l in output.getvalue().strip().split(b'\n') + ] + self.assertEqual(len(results), 3) + self.assertEqual(results[0], True) + self.assertEqual(results[1], ['up', None, {'body': '.'}]) + self.assertEqual( + results[2], + ['up', {'field': 42, 'other_field': 'x'}, {'body': '.'}]) + + def test_update_wrong_method(self): + import json + request = { + 'method': 'GET', + 'query': {}, + 'body': '', + } + commands = [ + self.command_ddoc_add(), + [ + 'ddoc', + '_design/test_update', + ['updates', 'inc'], + [None, request] + ], + ] + input = StringIO(b'\n'.join(json.dumps(c).encode('utf-8') + for c in commands)) + output = StringIO() + view.run(input=input, output=output) + results = [ + json.loads(l.decode('utf-8')) + for l in output.getvalue().strip().split(b'\n') + ] + self.assertEqual(len(results), 2) + self.assertEqual(results[1][2]['status'], 405) + def suite(): suite = unittest.TestSuite() diff --git a/couchdb/view.py b/couchdb/view.py index 0bb7e315..6d2d4da6 100755 --- a/couchdb/view.py +++ b/couchdb/view.py @@ -23,6 +23,7 @@ log = logging.getLogger('couchdb.view') +ddocs = {} def run(input=sys.stdin, output=sys.stdout): r"""CouchDB view function handler implementation for Python. @@ -34,10 +35,14 @@ def run(input=sys.stdin, output=sys.stdout): def _writejson(obj): obj = json.encode(obj) - if isinstance(obj, util.utype): - obj = obj.encode('utf-8') - output.write(obj) - output.write(b'\n') + if hasattr(output, 'encoding'): + output.write(obj) + output.write('\n') + else: + if isinstance(obj, util.utype): + obj = obj.encode('utf-8') + output.write(obj) + output.write(b'\n') output.flush() def _log(message): @@ -49,7 +54,7 @@ def reset(config=None): del functions[:] return True - def add_fun(string): + def compile_fun(string): string = BOM_UTF8 + string.encode('utf-8') globals_ = {} try: @@ -69,7 +74,10 @@ def add_fun(string): function = list(globals_.values())[0] if type(function) is not FunctionType: return err - functions.append(function) + return function + + def add_fun(string): + functions.append(compile_fun(string)) return True def map_doc(doc): @@ -128,8 +136,40 @@ def rereduce(*cmd): # Note: weird kwargs is for Python 2.5 compat return reduce(*cmd, **{'rereduce': True}) - handlers = {'reset': reset, 'add_fun': add_fun, 'map_doc': map_doc, - 'reduce': reduce, 'rereduce': rereduce} + def ddoc(*cmd): + if cmd[0] == 'new': + ddoc = cmd[2] + ddoc['updates'] = dict( + (name, compile_fun(value)) + for name, value in ddoc['updates'].items()) + ddocs[cmd[1]] = ddoc + return True + else: + ddoc = ddocs[cmd[0]] + action = cmd[1] + if action[0] == 'updates': + if cmd[2][1]['method'] not in ('POST', 'PUT'): + return [ + 'up', + None, + {'status': 405, 'body': 'Method not allowed'}, + ] + else: + fun = ddoc['updates'][action[1]] + doc, body = fun(*cmd[2]) + res = ['up', doc, body] + sys.stderr.flush() + return res + + + handlers = { + 'add_fun': add_fun, + 'ddoc': ddoc, + 'map_doc': map_doc, + 'reduce': reduce, + 'rereduce': rereduce, + 'reset': reset, + } try: while True: diff --git a/doc/views.rst b/doc/views.rst index 6fe913ff..973499ed 100644 --- a/doc/views.rst +++ b/doc/views.rst @@ -1,10 +1,11 @@ Writing views in Python ======================= -The couchdb-python package comes with a view server to allow you to write -views in Python instead of JavaScript. When couchdb-python is installed, it -will install a script called couchpy that runs the view server. To enable -this for your CouchDB server, add the following section to local.ini:: +The couchdb-python package comes with a query server to allow you to +write views or update handlers in Python instead of JavaScript. When +couchdb-python is installed, it will install a script called couchpy +that runs the view server. To enable this for your CouchDB server, add +the following section to local.ini:: [query_servers] python=/usr/bin/couchpy @@ -18,3 +19,15 @@ the language pull-down menu. Here's some sample view code to get you started:: Note that the ``map`` function uses the Python ``yield`` keyword to emit values, where JavaScript views use an ``emit()`` function. + +Here's an example update handler code that will increment the +``field`` member of an existing document. The name of the ``field`` to +increment is passed in the query string:: + + def fun(obj, req): + field = req.query['field'] + if obj is not None and field in obj: + obj[field] += 1 + return [obj, {"body": "incremented"}] + else: + return [None, {"body": "no such document or field"}]