Merge lp:~sil/desktopcouch/glib-callback-for-changes into lp:desktopcouch

Proposed by Stuart Langridge
Status: Work in progress
Proposed branch: lp:~sil/desktopcouch/glib-callback-for-changes
Merge into: lp:desktopcouch
Diff against target: 166 lines (+136/-1)
2 files modified
desktopcouch/records/server.py (+97/-1)
desktopcouch/records/tests/test_server.py (+39/-0)
To merge this branch: bzr merge lp:~sil/desktopcouch/glib-callback-for-changes
Reviewer Review Type Date Requested Status
dobey (community) Needs Fixing
Vincenzo Di Somma (community) Needs Fixing
Eric Casteleijn (community) Approve
Review via email: mp+40415@code.launchpad.net

Commit message

An implementation of glib_callback_for_changes which allows a glib program to attach a callback which is called whenever a change happens in a desktopcouch database.

Description of the change

An implementation of glib_callback_for_changes which allows a glib program to attach a callback which is called whenever a change happens in a desktopcouch database.

Use in a glib program:

def my_callback(data):
    print "a record with id %s was changed" % data["id"]
db = CouchDatabase("whatever")
db.glib_callback_for_changes(my_callback)
gtk.main()

To post a comment you must log in.
Revision history for this message
Eric Casteleijn (thisfred) wrote :

I like this functionality, but would like it better if it weren't a member of CouchDatabase (which I would like to remain agnostic of anything platform specific.) My intuition is that it doesn't need to be, since it makes relatively little use of self. I would probably strive for a function called something like 'add_glib_callback' that takes a database object and a callback function.

In addition I'd like a more generic callback function which knows nothing about glib, which could then be used by the glib one.

I know little of glib, so I don't know how realistic these preferences are.

Revision history for this message
Eric Casteleijn (thisfred) wrote :

In addition, I get an import error.

===============================================================================
[ERROR]: desktopcouch.records.tests.test_server.TestCouchDatabase.test_glib_callback_for_changes

Traceback (most recent call last):
Failure: testtools.testresult.real._StringException: Text attachment: traceback
------------
Traceback (most recent call last):
  File "/usr/lib/python2.6/dist-packages/testtools/runtest.py", line 128, in _run_user
    return fn(*args)
  File "/usr/lib/python2.6/dist-packages/testtools/testcase.py", line 368, in _run_test_method
    testMethod()
  File "/home/eric/canonical/desktopcouch/glib-callback-for-changes/desktopcouch/records/tests/test_server.py", line 705, in test_glib_callback_for_changes
    self.database.glib_callback_for_changes(callback)
  File "/home/eric/canonical/desktopcouch/glib-callback-for-changes/desktopcouch/records/server.py", line 121, in glib_callback_for_changes
    from gi.repository import Soup
ImportError: cannot import name Soup

review: Needs Fixing
Revision history for this message
Eric Casteleijn (thisfred) wrote :

After installing gir1.0-soup-2.4 (which points to why I'd like this somewhere separate, since I don't want that as a dependency of the library,) the tests pass, but:

== Pylint notices ==
************* Module desktopcouch.records.server
C0301:179: Line too long (81/80)
R0914:108:CouchDatabase.glib_callback_for_changes: Too many local variables (26/15)
E0611:123:CouchDatabase.glib_callback_for_changes: No name 'repository' in module 'gi'
C0111:129:CouchDatabase.glib_callback_for_changes.got_chunk: Missing docstring
W0613:129:CouchDatabase.glib_callback_for_changes.got_chunk: Unused argument 'msg'
C0111:152:CouchDatabase.glib_callback_for_changes.complete: Missing docstring
W0613:152:CouchDatabase.glib_callback_for_changes.complete: Unused argument 'args'
E1103:176:CouchDatabase.glib_callback_for_changes: Instance of 'Http' has no 'oauth_data' member (but some types could not be inferred)
W0612:180:CouchDatabase.glib_callback_for_changes: Unused variable 'fragment'
W0612:180:CouchDatabase.glib_callback_for_changes: Unused variable 'netloc'
W0612:180:CouchDatabase.glib_callback_for_changes: Unused variable 'path'
W0612:125:CouchDatabase.glib_callback_for_changes: Unused variable 'urllib'
W0612:180:CouchDatabase.glib_callback_for_changes: Unused variable 'schema'
W0201:159:CouchDatabase.glib_callback_for_changes: Attribute '_soupsessions' defined outside __init__
************* Module desktopcouch.records.tests.test_server
W0232:699:TestCouchDatabase.test_glib_callback_for_changes.PythonCannotSetNonGlobalOuterScopeVars: Class has no __init__ method
C0111:699:TestCouchDatabase.test_glib_callback_for_changes.PythonCannotSetNonGlobalOuterScopeVars: Missing docstring
C0321:699:TestCouchDatabase.test_glib_callback_for_changes.PythonCannotSetNonGlobalOuterScopeVars: More than one statement on a single line
R0903:699:TestCouchDatabase.test_glib_callback_for_changes.PythonCannotSetNonGlobalOuterScopeVars: Too few public methods (0/2)
W0201:705:TestCouchDatabase.test_glib_callback_for_changes.callback: Attribute 'success' defined outside __init__
C0111:704:TestCouchDatabase.test_glib_callback_for_changes.callback: Missing docstring
W0613:704:TestCouchDatabase.test_glib_callback_for_changes.callback: Unused argument 'data'
C0111:707:TestCouchDatabase.test_glib_callback_for_changes.timeout: Missing docstring
C0111:709:TestCouchDatabase.test_glib_callback_for_changes.insert_record: Missing docstring

Revision history for this message
Stuart Langridge (sil) wrote :

You'll note that the glib callback function imports the Python gir stuff whe nit's called, not at the top level in the module, precisely so that using gir is not a dependency *unless you use glib_callback_for_changes*. The apt dependency system isn't set up all that well for this; putting this one function in a package all by itself would be daft. I suggest that the resulting package Suggests python-gir-soup as per http://www.debian.org/doc/debian-policy/ch-relationships.html#s-binarydeps.

The function delves into interior details of CouchDatabase in order to compute a _changes URL. Something external to CouchDatabase should not be using this sort of internal API; it's an implementation detail which we may change.

You're right about the pylint stuff, although I don't understand why I didn't get told the same things when running the tests? What am I doing wrong there?

Revision history for this message
Eric Casteleijn (thisfred) wrote :

> You'll note that the glib callback function imports the Python gir stuff whe
> nit's called, not at the top level in the module, precisely so that using gir
> is not a dependency *unless you use glib_callback_for_changes*. The apt
> dependency system isn't set up all that well for this; putting this one
> function in a package all by itself would be daft. I suggest that the
> resulting package Suggests python-gir-soup as per http://www.debian.org/doc
> /debian-policy/ch-relationships.html#s-binarydeps.

Yeah, I really hate having a function that will break as soon as you call it, though. I don't suggest having this in a separate package, but having it as a top level function won't show it in introspection of CouchDatabase, and allows us to move it into desktopcouch.application, where I think the dependency (or suggestion) is less of a problem.

> The function delves into interior details of CouchDatabase in order to compute
> a _changes URL. Something external to CouchDatabase should not be using this
> sort of internal API; it's an implementation detail which we may change.

If so we can reimplement the function, I don't consider it as big a problem as having a broken method on our most central class.

> You're right about the pylint stuff, although I don't understand why I didn't
> get told the same things when running the tests? What am I doing wrong there?

The preferred way to run the tests now is ./runtests.py which takes care of the python path and everything else. It will run lint on the changed files only unless there are no local changes, and then it will lint every file that was changed in the branch, so if you want to lint the whole branch, make sure all your changes are committed.

201. By Stuart Langridge

bow and scrape before the almighty god pylint

202. By Stuart Langridge

merge from trunk

Revision history for this message
Eric Casteleijn (thisfred) wrote :

Looks good now.

review: Approve
203. By Stuart Langridge

correct enable message

Revision history for this message
Vincenzo Di Somma (vds) wrote :

I might be wrong but in Lucid I don't find a package to satisfy the required dependency and the tests break.

===============================================================================
[ERROR]: desktopcouch.records.tests.test_server.TestCouchDatabase.test_glib_callback_for_changes

Traceback (most recent call last):
Failure: testtools.testresult.real._StringException: Text attachment: traceback
------------
Traceback (most recent call last):
  File "/usr/lib/python2.6/dist-packages/testtools/runtest.py", line 128, in _run_user
    return fn(*args)
  File "/usr/lib/python2.6/dist-packages/testtools/testcase.py", line 368, in _run_test_method
    testMethod()
  File "/home/vds/canonical/desktopcouch/glib-callback-for-changes/desktopcouch/records/tests/test_server.py", line 725, in test_glib_callback_for_changes
    self.database.glib_callback_for_changes(callback)
  File "/home/vds/canonical/desktopcouch/glib-callback-for-changes/desktopcouch/records/server.py", line 126, in glib_callback_for_changes
    from gi.repository import Soup # pylint: disable=E0611
ImportError: No module named gi.repository

review: Needs Fixing
Revision history for this message
dobey (dobey) wrote :

Also, please use timeout_add_seconds(X) instead of timeout_add(X * 1000), as it is the preferred way to do timeouts that large.

review: Needs Fixing

Unmerged revisions

203. By Stuart Langridge

correct enable message

202. By Stuart Langridge

merge from trunk

201. By Stuart Langridge

bow and scrape before the almighty god pylint

200. By Stuart Langridge

An implementation of glib_callback_for_changes which allows a glib program to attach a callback which is called whenever a change happens in a desktopcouch database

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'desktopcouch/records/server.py'
--- desktopcouch/records/server.py 2010-11-10 21:17:11 +0000
+++ desktopcouch/records/server.py 2010-11-16 14:16:13 +0000
@@ -25,7 +25,7 @@
25import uuid25import uuid
2626
27from couchdb import Server27from couchdb import Server
28from couchdb.client import Resource28from couchdb.client import Resource, uri as couchdburi
29import desktopcouch29import desktopcouch
30from desktopcouch.records import server_base30from desktopcouch.records import server_base
31from desktopcouch.platform import find_port31from desktopcouch.platform import find_port
@@ -65,6 +65,9 @@
65 ctx=desktopcouch.local_files.DEFAULT_CONTEXT):65 ctx=desktopcouch.local_files.DEFAULT_CONTEXT):
66 self.ctx = ctx66 self.ctx = ctx
67 self.server_uri = uri67 self.server_uri = uri
68
69 self._soupsessions = []
70
68 super(CouchDatabase, self).__init__(71 super(CouchDatabase, self).__init__(
69 database, uri, record_factory=record_factory, create=create,72 database, uri, record_factory=record_factory, create=create,
70 server_class=server_class, oauth_tokens=oauth_tokens, ctx=ctx)73 server_class=server_class, oauth_tokens=oauth_tokens, ctx=ctx)
@@ -104,3 +107,96 @@
104 uri = self.server_uri107 uri = self.server_uri
105 super(CouchDatabase, self)._reconnect(uri=uri)108 super(CouchDatabase, self)._reconnect(uri=uri)
106 # pylint: enable=W0221109 # pylint: enable=W0221
110
111 def glib_callback_for_changes(self, callback, # pylint: disable=R0914
112 error_callback=None, since=None):
113 """Set up a callback to be called, asynchronously, whenever a
114 change is fired by this database's CouchDB _changes feed.
115 Note: this callback requires the glib mainloop, but it does
116 not start it. It is the caller's responsibility to run the glib
117 mainloop.
118 The since parameter is a CouchDB sequence number from which you
119 wish to monitor changes. The default, None, means to monitor changes
120 from now on. If the caller remembers the last seqno it saw, then
121 it can pass that as since to get all changes since that last seqno
122 sent to the callback as well as any future changes.
123 The callback is called with one argument, a dictionary parsed from
124 the CouchDB changes feed item, containing id, seq, and changes
125 members."""
126 from gi.repository import Soup # pylint: disable=E0611
127 from oauth import oauth
128 import cgi, json
129 session = Soup.SessionAsync()
130
131 # inner functions to handle changes when they arrive
132 def got_chunk(msg, chunkbuffer): # pylint: disable=W0613
133 """Internal function to handle one line from _changes."""
134 data = ''.join([chr(x) for x in chunkbuffer.get_data()])
135 if not data.strip():
136 # got a heartbeat
137 return
138 try:
139 actual_data = json.loads(data)
140 except ValueError:
141 # weird, we got something that wasn't JSON. Ignore it.
142 if error_callback is not None:
143 error_callback({
144 "reason": "Got some data that wasn't JSON",
145 "error": "could_not_parse_json",
146 "data": data
147 })
148 return
149 if "id" not in actual_data or "changes" not in actual_data or \
150 "seq" not in actual_data:
151 if error_callback is not None:
152 error_callback(actual_data)
153 return
154 callback(actual_data)
155
156 def complete(*args): # pylint: disable=W0613
157 """Internal function called if connection ever completes."""
158 # should never get called, because connection is long-lived
159 # it is called when the loop is ended, though.
160 pass
161
162 # make sure session does not quit when function ends
163 self._soupsessions.append(session)
164
165 # Calculate the changes URI for this database, and oauth-sign it
166 # so that libsoup is able to fetch it
167 params = dict(feed="continuous",
168 heartbeat=1000 # newline every 10 seconds to keep connection alive
169 )
170 if since is None:
171 params["since"] = self.db.info()["update_seq"]
172 else:
173 params["since"] = since
174 changes_feed_uri = couchdburi( # pylint: disable=W0142
175 self._server.resource.uri, self.db.name, "_changes",
176 **params)
177 # note: this function does its own oauth signing rather than calling
178 # the similar oauth signing stuff in server_base.
179 oauth_data = \
180 self._server.resource.http.oauth_data # pylint: disable=E1103
181 consumer = oauth.OAuthConsumer(oauth_data["consumer_key"],
182 oauth_data["consumer_secret"])
183 token = oauth.OAuthToken(oauth_data["token"],
184 oauth_data["token_secret"])
185 parts = urlparse.urlparse(changes_feed_uri)
186 query = list(parts)[4]
187 querystr_as_dict = dict(cgi.parse_qsl(query))
188 req = oauth.OAuthRequest.from_consumer_and_token(
189 consumer,
190 token,
191 http_method = "GET",
192 http_url = changes_feed_uri,
193 parameters = querystr_as_dict
194 )
195 req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(), consumer,
196 token)
197 signed_changes_feed_uri = req.to_url()
198
199 msg = Soup.Message.new("GET", signed_changes_feed_uri)
200 session.queue_message(msg, complete, signed_changes_feed_uri)
201 msg.connect("got-chunk", got_chunk)
202
107203
=== modified file 'desktopcouch/records/tests/test_server.py'
--- desktopcouch/records/tests/test_server.py 2010-11-16 13:52:34 +0000
+++ desktopcouch/records/tests/test_server.py 2010-11-16 14:16:13 +0000
@@ -690,3 +690,42 @@
690 self.fail()690 self.fail()
691 except FieldsConflict, e:691 except FieldsConflict, e:
692 self.assertEqual({('field1',): (22, 11)}, e.conflicts)692 self.assertEqual({('field1',): (22, 11)}, e.conflicts)
693
694 def test_glib_callback_for_changes(self):
695 """Test glib_callback_for_changes method"""
696 import gobject
697
698 # set var on namespace class as per example 7 at
699 # http://www.saltycrane.com/blog/2008/01/python-variable-scope-notes/
700 # because Python 2 can't set outer-but-non-global variables
701 class PythonCannotSetNonGlobalOuterScopeVars: # pylint: disable=R0903
702 """Internal class to provide namespace for outer variable."""
703 def __init__(self):
704 """Pointless init method just to make pylint happy."""
705 self.success = False
706 p = PythonCannotSetNonGlobalOuterScopeVars()
707
708 loop = gobject.MainLoop()
709 def callback(data): # pylint: disable=W0613
710 """Callback handler to confirm that a callback occurs."""
711 p.success = True
712 loop.quit()
713 def timeout():
714 """Timeout handler to catch failure to call the callback."""
715 loop.quit()
716 def insert_record():
717 """Trigger a record insertion (and thus a callback)."""
718 self.database.put_record(Record({
719 "first": "value1", "second": "value2",
720 "record_type": "callbacktest.com"}))
721
722 # Set up the callback, then in 1s insert a record; if the callback
723 # works, then that insert will call the callback; if it doesn't,
724 # then the timeout will be called.
725
726 self.database.glib_callback_for_changes(callback)
727 gobject.timeout_add(1000, insert_record)
728 gobject.timeout_add(3000, timeout)
729 loop.run()
730 self.assertTrue(p.success)
731

Subscribers

People subscribed via source and target branches