Merge lp:~cjwatson/ubiquity/libsoup-timezone into lp:ubiquity

Proposed by Colin Watson
Status: Merged
Merged at revision: 5006
Proposed branch: lp:~cjwatson/ubiquity/libsoup-timezone
Merge into: lp:ubiquity
Diff against target: 219 lines (+112/-42) (has conflicts)
4 files modified
debian/changelog (+10/-0)
debian/control (+1/-1)
tests/test_timezone.py (+18/-2)
ubiquity/plugins/ubi-timezone.py (+83/-39)
Text conflict in debian/changelog
To merge this branch: bzr merge lp:~cjwatson/ubiquity/libsoup-timezone
Reviewer Review Type Date Requested Status
Evan (community) Needs Information
Review via email: mp+77324@code.launchpad.net

Description of the change

Fix bug 837217 by fetching geoname data asynchronously using libsoup.

To post a comment you must log in.
Revision history for this message
Evan (ev) wrote :

./tests/run-pyflakes
ubiquity/plugins/ubi-timezone.py:116: undefined name 'Gtk'
ubiquity/plugins/ubi-timezone.py:116: undefined name 'GObject'
ubiquity/plugins/ubi-timezone.py:116: undefined name 'GObject'
ubiquity/plugins/ubi-timezone.py:117: undefined name 'GObject'
ubiquity/plugins/ubi-timezone.py:117: undefined name 'GObject'
ubiquity/plugins/ubi-timezone.py:118: undefined name 'GObject'

Revision history for this message
Evan (ev) wrote :

(I'm happy to fix those up as I merge)

Revision history for this message
Evan (ev) wrote :

Shouldn't the block of code that does a lookup against self.zones still live in the changed() function? Otherwise, the user will only get results, including locally-computed ones, if the callback for hitting the geonames server succeeds.

review: Needs Information
Revision history for this message
Evan (ev) wrote :

(This caused the test_city_entry test to fail, which expects to get some local results upon setting the entry to Eastern.)

Revision history for this message
Colin Watson (cjwatson) wrote :

Fixed the missing imports, thanks.

The callback is supposed to fire no matter what, whether by cancellation or timeout or whatever; in my tests, I got locally-computed results even when the network was dropping all outbound port-80 packets. But perhaps it would be better to set the completion model straight away and fill it in with network results once they arrive; the main gotcha there is making sure that we only cache once we get network results.

Revision history for this message
Evan (ev) wrote :

Apologies, I was indeed wrong about the callback only firing on success.

Matthew suggests that a reasonable threshold is that if the timeout is going to be greater than 2-3 seconds, we should show the locally-computed results while we wait for network results.

4997. By Colin Watson

missing imports

4998. By Colin Watson

Show tzdb completion entries after a short timeout.

4999. By Colin Watson

need to encode text before passing to urllib.quote

5000. By Colin Watson

fix up tests

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/changelog'
2--- debian/changelog 2011-09-30 13:49:36 +0000
3+++ debian/changelog 2011-09-30 21:34:24 +0000
4@@ -1,3 +1,4 @@
5+<<<<<<< TREE
6 ubiquity (2.8.0) UNRELEASED; urgency=low
7
8 * Fix SSID UTF-8 decoding, and replace any still-invalid characters with
9@@ -29,6 +30,15 @@
10
11 -- Adam Conrad <adconrad@ubuntu.com> Wed, 28 Sep 2011 16:43:42 -0600
12
13+=======
14+ubiquity (2.7.38) UNRELEASED; urgency=low
15+
16+ * GTK frontend:
17+ - Fetch geoname data asynchronously (LP: #837217).
18+
19+ -- Colin Watson <cjwatson@ubuntu.com> Wed, 28 Sep 2011 13:43:44 +0100
20+
21+>>>>>>> MERGE-SOURCE
22 ubiquity (2.7.37) oneiric; urgency=low
23
24 [ Evan Dandrea ]
25
26=== modified file 'debian/control'
27--- debian/control 2011-09-28 09:54:01 +0000
28+++ debian/control 2011-09-30 21:34:24 +0000
29@@ -43,7 +43,7 @@
30
31 Package: ubiquity-frontend-gtk
32 Architecture: any
33-Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, ubiquity (= ${binary:Version}), python-dbus, gir1.2-gtk-3.0, gir1.2-gdkpixbuf-2.0, gir1.2-gstreamer-0.10, gir1.2-vte-2.90, gir1.2-webkit-3.0, iso-codes, x-window-manager, gksu, python-xklavier, gir1.2-timezonemap-1.0, python-libxml2, python-cairo, python-appindicator, gstreamer0.10-plugins-good
34+Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, ubiquity (= ${binary:Version}), python-dbus, gir1.2-gtk-3.0, gir1.2-gdkpixbuf-2.0, gir1.2-gstreamer-0.10, gir1.2-soup-2.4, gir1.2-vte-2.90, gir1.2-webkit-3.0, iso-codes, x-window-manager, gksu, python-xklavier, gir1.2-timezonemap-1.0, python-libxml2, python-cairo, python-appindicator, gstreamer0.10-plugins-good
35 Suggests: gnome-control-center
36 Conflicts: ubuntu-express-frontend-gtk, espresso-frontend-gtk, ubiquity (<< 2.4.3)
37 Replaces: ubuntu-express-frontend-gtk, espresso-frontend-gtk, ubiquity (<< 2.4.3)
38
39=== modified file 'tests/test_timezone.py'
40--- tests/test_timezone.py 2011-08-30 17:20:27 +0000
41+++ tests/test_timezone.py 2011-09-30 21:34:24 +0000
42@@ -19,10 +19,26 @@
43 controller.dbfilter = self.ubi_timezone.Page(None, db=db)
44 self.gtk = self.ubi_timezone.PageGtk(controller)
45
46+ @mock.patch('gi.repository.Soup.SessionAsync')
47+ @mock.patch('gi.repository.Soup.Message')
48 @mock.patch('json.loads')
49- @mock.patch('urllib2.build_opener')
50- def test_city_entry(self, opener_mock, json_mock):
51+ def test_city_entry(self, json_mock, *args):
52+ from gi.repository import GObject
53+
54+ # Patch GObject.timeout_add_seconds to call the supplied function
55+ # immediately rather than waiting for the interval to expire.
56+ def side_effect_factory(real_method):
57+ def side_effect(interval, function, data):
58+ function(data)
59+ return side_effect
60+
61 json_mock.return_value = []
62+ real_method = GObject.timeout_add_seconds
63+ method = mock.patch('gi.repository.GObject.timeout_add_seconds')
64+ timeout_mock = method.start()
65+ timeout_mock.side_effect = side_effect_factory(real_method)
66+ self.addCleanup(method.stop)
67+
68 self.gtk.set_timezone('America/New_York')
69 self.gtk.city_entry.set_text('Eastern')
70 self.gtk.changed(self.gtk.city_entry)
71
72=== modified file 'ubiquity/plugins/ubi-timezone.py'
73--- ubiquity/plugins/ubi-timezone.py 2011-08-30 17:20:27 +0000
74+++ ubiquity/plugins/ubi-timezone.py 2011-09-30 21:34:24 +0000
75@@ -52,6 +52,9 @@
76 self.timezone = None
77 self.zones = []
78 self.plugin_widgets = self.page
79+ self.geoname_cache = {}
80+ self.geoname_session = None
81+ self.geoname_timeout_id = None
82
83 def plugin_translate(self, lang):
84 #c = self.controller
85@@ -84,54 +87,95 @@
86
87
88 def changed(self, entry):
89- from gi.repository import Gtk, GObject
90+ import urllib
91+ from gi.repository import Gtk, GObject, Soup
92+
93 text = self.city_entry.get_text().decode('utf-8')
94 if not text:
95 return
96 # TODO if the completion widget has a selection, return? How do we
97 # determine this?
98- if text in self.changed.cache:
99- model = self.changed.cache[text]
100+ if text in self.geoname_cache:
101+ model = self.geoname_cache[text]
102+ self.city_entry.get_completion().set_model(model)
103 else:
104- # fetch
105 model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING,
106 GObject.TYPE_STRING, GObject.TYPE_STRING,
107 GObject.TYPE_STRING)
108- self.changed.cache[text] = model
109- # TODO benchmark this
110- results = [(name, self.tzdb.get_loc(city))
111- for (name, city) in
112- [(x[0], x[1]) for x in self.zones
113- if x[0].lower().split('(', 1)[-1] \
114- .startswith(text.lower())]]
115- for result in results:
116- # We use name rather than loc.human_zone for i18n.
117- # TODO this looks pretty awful for US results:
118- # United States (New York) (United States)
119- # Might want to match the debconf format.
120- name, loc = result
121- model.append([name, '', loc.human_country,
122- str(loc.latitude), str(loc.longitude)])
123-
124- try:
125- import urllib2, urllib, json
126- opener = urllib2.build_opener()
127- opener.addheaders = [('User-agent', 'Ubiquity/1.0')]
128- url = opener.open(_geoname_url %
129- (urllib.quote(text), misc.get_release().version))
130- for result in json.loads(url.read()):
131- model.append([result['name'],
132- result['admin1'],
133- result['country'],
134- result['latitude'],
135- result['longitude']])
136- except Exception, e:
137- print 'exception:', e
138- # TODO because we don't return here, we could cache a
139- # result that doesn't include the geonames results because
140- # of a network error.
141- self.city_entry.get_completion().set_model(model)
142- changed.cache = {}
143+
144+ if self.geoname_session is None:
145+ self.geoname_session = Soup.SessionAsync()
146+ url = _geoname_url % (urllib.quote(text.encode('UTF-8')),
147+ misc.get_release().version)
148+ message = Soup.Message.new('GET', url)
149+ message.request_headers.append('User-agent', 'Ubiquity/1.0')
150+ self.geoname_session.abort()
151+ if self.geoname_timeout_id is not None:
152+ GObject.source_remove(self.geoname_timeout_id)
153+ self.geoname_timeout_id = \
154+ GObject.timeout_add_seconds(2, self.geoname_timeout,
155+ (text, model))
156+ self.geoname_session.queue_message(message, self.geoname_cb,
157+ (text, model))
158+
159+ def geoname_add_tzdb(self, text, model):
160+ if len(model):
161+ # already added
162+ return
163+
164+ # TODO benchmark this
165+ results = [(name, self.tzdb.get_loc(city))
166+ for (name, city) in
167+ [(x[0], x[1]) for x in self.zones
168+ if x[0].lower().split('(', 1)[-1] \
169+ .startswith(text.lower())]]
170+ for result in results:
171+ # We use name rather than loc.human_zone for i18n.
172+ # TODO this looks pretty awful for US results:
173+ # United States (New York) (United States)
174+ # Might want to match the debconf format.
175+ name, loc = result
176+ model.append([name, '', loc.human_country,
177+ str(loc.latitude), str(loc.longitude)])
178+
179+ def geoname_timeout(self, user_data):
180+ text, model = user_data
181+ self.geoname_add_tzdb(text, model)
182+ self.geoname_timeout_id = None
183+ self.city_entry.get_completion().set_model(model)
184+ return False
185+
186+ def geoname_cb(self, session, message, user_data):
187+ import syslog
188+ import json
189+ from gi.repository import GObject, Soup
190+
191+ text, model = user_data
192+
193+ if self.geoname_timeout_id is not None:
194+ GObject.source_remove(self.geoname_timeout_id)
195+ self.geoname_timeout_id = None
196+ self.geoname_add_tzdb(text, model)
197+
198+ if message.status_code == Soup.KnownStatusCode.CANCELLED:
199+ # Silently ignore cancellation.
200+ pass
201+ elif message.status_code != Soup.KnownStatusCode.OK:
202+ # Log but otherwise ignore failures.
203+ syslog.syslog('Geoname lookup for "%s" failed: %d %s' %
204+ (text, message.status_code, message.reason_phrase))
205+ else:
206+ for result in json.loads(message.response_body.data):
207+ model.append([result['name'],
208+ result['admin1'],
209+ result['country'],
210+ result['latitude'],
211+ result['longitude']])
212+
213+ # Only cache positive results.
214+ self.geoname_cache[text] = model
215+
216+ self.city_entry.get_completion().set_model(model)
217
218 def setup_page(self):
219 # TODO Put a frame around the completion to add contrast (LP: #605908)

Subscribers

People subscribed via source and target branches

to status/vote changes: