Merge lp:~brian.curtin/ubuntu-sso-client/py3-unicode into lp:ubuntu-sso-client

Proposed by Brian Curtin on 2012-06-29
Status: Merged
Approved by: Alejandro J. Cura on 2012-07-31
Approved revision: 998
Merged at revision: 987
Proposed branch: lp:~brian.curtin/ubuntu-sso-client/py3-unicode
Merge into: lp:ubuntu-sso-client
Diff against target: 470 lines (+118/-53)
9 files modified
ubuntu_sso/account.py (+12/-10)
ubuntu_sso/credentials.py (+3/-3)
ubuntu_sso/keyring/__init__.py (+13/-8)
ubuntu_sso/keyring/tests/test_common.py (+11/-9)
ubuntu_sso/keyring/tests/test_linux.py (+3/-1)
ubuntu_sso/main/__init__.py (+2/-1)
ubuntu_sso/main/tests/test_common.py (+5/-3)
ubuntu_sso/tests/test_account.py (+20/-18)
ubuntu_sso/utils/compat.py (+49/-0)
To merge this branch: bzr merge lp:~brian.curtin/ubuntu-sso-client/py3-unicode
Reviewer Review Type Date Requested Status
Mike McCracken (community) Approve on 2012-07-31
Alejandro J. Cura (community) 2012-06-29 Approve on 2012-07-24
Review via email: mp+112856@code.launchpad.net

Commit Message

The first of several changes to prepare sso for Python 3 Unicode usage

Description of the Change

The first of several branches which prepare our code to work with both Python 2 and 3 with respect to Unicode. Since we support Python 2.6 and beyond as well as Python 3, we make use of the "from __future__ import unicode_literals" compile change, which makes things a lot easier.

In addition, I introduced ubuntu_sso.utils.compat (at the very end of the diff) which is the start of the minimal compatibility layer we'll need to maintain between Python 2 and 3. It's a fairly common approach, and as stated in the accompanying comment, it's outlined at http://python3porting.com/noconv.html#more-bytes-strings-and-unicode

(compat may make sense to move into something like devtools at a later time. I figure it's best to keep small changes self-contained for the time being before we do any cross-project changes)

This change covers modules in the following packages:
ubuntu_sso
ubuntu_sso.gtk
ubuntu_sso.keyring
ubuntu_sso.main

To post a comment you must log in.
Alejandro J. Cura (alecu) wrote :

This series of py3k branches is looking awesome, thanks for working on it!

One thing we should consider: in every file where we add the "from __future__ import unicode_literals" we should be extra careful about the string literals that we are not turning from u"..." to "...".

I can see two different cases happening here:
 * In some places the old code assumes that conversions when comparing unicode with str are automatic, so to make this work on 3 we should leave old string literal as they are.
 * In some other places some old string literals should be changed from "..." to "...".encode(...), because they are truly using "byte literals". For instance in many of the tests in ubuntu_sso/keyring/tests/test_common.py that fake some functions that will still return bytes on py3.

review: Needs Fixing
Brian Curtin (brian.curtin) wrote :

I think the above changes should have this addressed. Rather than changing the tests, I made the implementation more Python 3 safe. keyring.gethostname actually would not work on 3 anyway.

Alejandro J. Cura (alecu) wrote :

== Python Lint Notices ==

ubuntu_sso/qt/__init__.py:
    50: [W0511] TODO

ubuntu_sso/utils/compat.py:
    42: [W0622] Redefining built-in 'basestring'
    40: [C0103] Invalid name "text_type" (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
    41: [C0103] Invalid name "binary_type" (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
    42: [C0103] Invalid name "basestring" (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
    44: [C0103] Invalid name "text_type" (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
    45: [C0103] Invalid name "binary_type" (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
    46: [C0103] Invalid name "basestring" (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)

ubuntu_sso/utils/webclient/common.py:
    286: [W0511] TODO

Alejandro J. Cura (alecu) :
review: Needs Fixing
Alejandro J. Cura (alecu) wrote :

Other than lint issues, I've run the tests on Ubuntu, and ran the sso client irl, and it seems to be working fine.

998. By Brian Curtin on 2012-07-24

Disable lint warnings for basestring redefine, invalid names

Alejandro J. Cura (alecu) wrote :

Looks good!

review: Approve
Mike McCracken (mikemc) wrote :

Looks good to me.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntu_sso/account.py'
2--- ubuntu_sso/account.py 2012-06-22 21:04:49 +0000
3+++ ubuntu_sso/account.py 2012-07-24 20:55:21 +0000
4@@ -35,19 +35,21 @@
5
6 """
7
8+from __future__ import unicode_literals
9+
10 import os
11 import re
12
13 from twisted.internet import defer
14
15 from ubuntu_sso.logger import setup_logging
16-from ubuntu_sso.utils import webclient
17+from ubuntu_sso.utils import compat, webclient
18 from ubuntu_sso.utils.webclient import restful
19 from ubuntu_sso.utils.webclient.common import WebClientError
20
21
22 logger = setup_logging("ubuntu_sso.account")
23-SERVICE_URL = u"https://login.ubuntu.com/api/1.0/"
24+SERVICE_URL = "https://login.ubuntu.com/api/1.0/"
25 SSO_STATUS_OK = 'ok'
26 SSO_STATUS_ERROR = 'error'
27
28@@ -112,7 +114,7 @@
29 result = {}
30 for key, val in errdict.items():
31 # workaround until bug #624955 is solved
32- if isinstance(val, basestring):
33+ if isinstance(val, compat.basestring):
34 result[key] = val
35 else:
36 result[key] = "\n".join(val)
37@@ -125,7 +127,7 @@
38 filename)
39 restful_client = restful.RestfulClient(self.service_url)
40 try:
41- captcha = yield restful_client.restcall(u"captchas.new")
42+ captcha = yield restful_client.restcall("captchas.new")
43 finally:
44 restful_client.shutdown()
45
46@@ -162,7 +164,7 @@
47 logger.error('register_user: InvalidPasswordError')
48 raise InvalidPasswordError()
49
50- result = yield restful_client.restcall(u"registration.register",
51+ result = yield restful_client.restcall("registration.register",
52 email=email, password=password,
53 displayname=displayname,
54 captcha_id=captcha_id,
55@@ -189,7 +191,7 @@
56 password=password)
57 try:
58 credentials = yield restful_client.restcall(
59- u"authentications.authenticate",
60+ "authentications.authenticate",
61 token_name=token_name)
62 except WebClientError:
63 logger.exception('login failed with:')
64@@ -208,7 +210,7 @@
65 restful_client = restful.RestfulClient(self.service_url,
66 oauth_credentials=token)
67 try:
68- me_info = yield restful_client.restcall(u"accounts.me")
69+ me_info = yield restful_client.restcall("accounts.me")
70 finally:
71 restful_client.shutdown()
72 key = 'preferred_email'
73@@ -229,7 +231,7 @@
74 restful_client = restful.RestfulClient(self.service_url,
75 oauth_credentials=credentials)
76 try:
77- result = yield restful_client.restcall(u"accounts.validate_email",
78+ result = yield restful_client.restcall("accounts.validate_email",
79 email_token=email_token)
80 finally:
81 restful_client.shutdown()
82@@ -247,7 +249,7 @@
83 """Request a token to reset the password for the account 'email'."""
84 restful_client = restful.RestfulClient(self.service_url)
85 try:
86- operation = u"registration.request_password_reset_token"
87+ operation = "registration.request_password_reset_token"
88 result = yield restful_client.restcall(operation, email=email)
89 except WebClientError as e:
90 logger.exception('request_password_reset_token failed with:')
91@@ -272,7 +274,7 @@
92 restful_client = restful.RestfulClient(self.service_url)
93 try:
94 result = yield restful_client.restcall(
95- u"registration.set_new_password",
96+ "registration.set_new_password",
97 email=email, token=token,
98 new_password=new_password)
99 except WebClientError as e:
100
101=== modified file 'ubuntu_sso/credentials.py'
102--- ubuntu_sso/credentials.py 2012-07-13 21:07:38 +0000
103+++ ubuntu_sso/credentials.py 2012-07-24 20:55:21 +0000
104@@ -54,7 +54,7 @@
105 )
106 from ubuntu_sso.keyring import Keyring
107 from ubuntu_sso.logger import setup_logging
108-from ubuntu_sso.utils import get_bin_cmd, runner
109+from ubuntu_sso.utils import compat, get_bin_cmd, runner
110
111
112 logger = setup_logging('ubuntu_sso.credentials')
113@@ -189,8 +189,8 @@
114 value = getattr(self, arg)
115 if value:
116 args.append('--%s' % arg)
117- if not isinstance(value, basestring):
118- value = str(value)
119+ if not isinstance(value, compat.basestring):
120+ value = compat.text_type(value)
121 args.append(value)
122
123 if login_only:
124
125=== modified file 'ubuntu_sso/keyring/__init__.py'
126--- ubuntu_sso/keyring/__init__.py 2012-06-27 20:41:19 +0000
127+++ ubuntu_sso/keyring/__init__.py 2012-07-24 20:55:21 +0000
128@@ -28,6 +28,8 @@
129 # files in the program, then also delete it here.
130 """Implementations of different keyrings."""
131
132+from __future__ import unicode_literals
133+
134 import socket
135 import sys
136
137@@ -41,11 +43,12 @@
138 from twisted.internet.defer import inlineCallbacks, returnValue
139
140 from ubuntu_sso.logger import setup_logging
141+from ubuntu_sso.utils import compat
142
143 logger = setup_logging("ubuntu_sso.keyring")
144
145-TOKEN_SEPARATOR = u' @ '
146-SEPARATOR_REPLACEMENT = u' AT '
147+TOKEN_SEPARATOR = ' @ '
148+SEPARATOR_REPLACEMENT = ' AT '
149
150 U1_APP_NAME = "Ubuntu One"
151 U1_KEY_NAME = "UbuntuOne token for https://ubuntuone.com"
152@@ -58,7 +61,9 @@
153 def gethostname():
154 """Get the hostname, return the name as unicode."""
155 sys_encoding = sys.getfilesystemencoding()
156- hostname = socket.gethostname().decode(sys_encoding)
157+ hostname = socket.gethostname()
158+ if isinstance(hostname, compat.binary_type):
159+ return hostname.decode(sys_encoding)
160 return hostname
161
162
163@@ -68,10 +73,10 @@
164 computer_name = gethostname()
165 quoted_computer_name = quote(computer_name)
166
167- assert isinstance(computer_name, unicode)
168- assert isinstance(quoted_computer_name, unicode)
169+ assert isinstance(computer_name, compat.text_type)
170+ assert isinstance(quoted_computer_name, compat.text_type)
171
172- return u"%s - %s" % (quoted_app_name, quoted_computer_name)
173+ return "%s - %s" % (quoted_app_name, quoted_computer_name)
174
175
176 def get_token_name(app_name):
177@@ -80,8 +85,8 @@
178 computer_name = computer_name.replace(TOKEN_SEPARATOR,
179 SEPARATOR_REPLACEMENT)
180
181- assert isinstance(computer_name, unicode)
182- assert isinstance(computer_name, unicode)
183+ assert isinstance(computer_name, compat.text_type)
184+ assert isinstance(computer_name, compat.text_type)
185
186 return TOKEN_SEPARATOR.join((app_name, computer_name))
187
188
189=== modified file 'ubuntu_sso/keyring/tests/test_common.py'
190--- ubuntu_sso/keyring/tests/test_common.py 2012-04-09 17:38:24 +0000
191+++ ubuntu_sso/keyring/tests/test_common.py 2012-07-24 20:55:21 +0000
192@@ -33,6 +33,8 @@
193 # files in the program, then also delete it here.
194 """Tests for the keyring common module."""
195
196+from __future__ import unicode_literals
197+
198 from twisted.trial.unittest import TestCase
199
200 from ubuntu_sso import keyring
201@@ -49,7 +51,7 @@
202
203 def test_get_hostname_uses_filesystem_encoding(self):
204 """The fs encoding is used to decode the name returned by socket."""
205- fake_hostname = u"Привет-ПК"
206+ fake_hostname = "Привет-ПК"
207 hostname_koi8r = fake_hostname.encode("koi8-r")
208 self.patch(keyring.socket, "gethostname", lambda: hostname_koi8r)
209 self.patch(keyring.sys, "getfilesystemencoding", lambda: "koi8-r")
210@@ -67,35 +69,35 @@
211
212 def test_get_simple_token_name(self):
213 """A simple token name is built right."""
214- sample_app_name = u"UbuntuTwo"
215+ sample_app_name = "UbuntuTwo"
216 sample_hostname = "Darkstar"
217 expected_result = "UbuntuTwo @ Darkstar"
218 self.check_build(sample_app_name, sample_hostname, expected_result)
219
220 def test_get_complex_token_name_for_app_name(self):
221 """A complex token name is built right too."""
222- sample_app_name = u"Ubuntu @ Eleven"
223+ sample_app_name = "Ubuntu @ Eleven"
224 sample_hostname = "Mate+Cocido"
225 expected_result = "Ubuntu @ Eleven @ Mate+Cocido"
226 self.check_build(sample_app_name, sample_hostname, expected_result)
227
228 def test_get_complex_token_name_for_hostname(self):
229 """A complex token name is built right too."""
230- sample_app_name = u"Ubuntu Eleven"
231+ sample_app_name = "Ubuntu Eleven"
232 sample_hostname = "Mate @ Cocido"
233 expected_result = "Ubuntu Eleven @ Mate AT Cocido"
234 self.check_build(sample_app_name, sample_hostname, expected_result)
235
236 def test_get_unicode_appname_token_name(self):
237 """A token name with unicode in the app name."""
238- sample_app_name = u"Ubuntu 四百六十九"
239+ sample_app_name = "Ubuntu 四百六十九"
240 sample_hostname = "Darkstar"
241- expected_result = u"Ubuntu 四百六十九 @ Darkstar"
242+ expected_result = "Ubuntu 四百六十九 @ Darkstar"
243 self.check_build(sample_app_name, sample_hostname, expected_result)
244
245 def test_get_utf8_hostname_token_name(self):
246 """A token name with utf8 in the host name."""
247- sample_app_name = u"Ubuntu Eleven"
248- sample_hostname = u"Привет-ПК"
249- expected_result = u"Ubuntu Eleven @ Привет-ПК"
250+ sample_app_name = "Ubuntu Eleven"
251+ sample_hostname = "Привет-ПК"
252+ expected_result = "Ubuntu Eleven @ Привет-ПК"
253 self.check_build(sample_app_name, sample_hostname, expected_result)
254
255=== modified file 'ubuntu_sso/keyring/tests/test_linux.py'
256--- ubuntu_sso/keyring/tests/test_linux.py 2012-06-22 16:55:35 +0000
257+++ ubuntu_sso/keyring/tests/test_linux.py 2012-07-24 20:55:21 +0000
258@@ -33,6 +33,8 @@
259 # files in the program, then also delete it here.
260 """Tests for the keyring.py module."""
261
262+from __future__ import unicode_literals
263+
264 from twisted.internet import defer
265 from twisted.internet.defer import inlineCallbacks
266 from twisted.trial.unittest import TestCase
267@@ -133,7 +135,7 @@
268 self.mock_service = None
269 self.service = self.patch(keyring, "SecretService",
270 self.get_mock_service)
271- self.patch(common_keyring, "gethostname", lambda: u"darkstar")
272+ self.patch(common_keyring, "gethostname", lambda: "darkstar")
273
274 def get_mock_service(self):
275 """Create only one instance of the mock service per test."""
276
277=== modified file 'ubuntu_sso/main/__init__.py'
278--- ubuntu_sso/main/__init__.py 2012-07-12 15:54:13 +0000
279+++ ubuntu_sso/main/__init__.py 2012-07-24 20:55:21 +0000
280@@ -52,6 +52,7 @@
281 )
282 from ubuntu_sso.keyring import get_token_name, Keyring
283 from ubuntu_sso.logger import setup_logging, log_call
284+from ubuntu_sso.utils import compat
285
286
287 logger = setup_logging("ubuntu_sso.main")
288@@ -109,7 +110,7 @@
289 result["message"] = e.__class__.__doc__
290 elif isinstance(e.args[0], dict):
291 result.update(e.args[0])
292- elif isinstance(e.args[0], basestring):
293+ elif isinstance(e.args[0], compat.basestring):
294 result["message"] = e.args[0]
295
296 return result
297
298=== modified file 'ubuntu_sso/main/tests/test_common.py'
299--- ubuntu_sso/main/tests/test_common.py 2012-04-09 17:38:24 +0000
300+++ ubuntu_sso/main/tests/test_common.py 2012-07-24 20:55:21 +0000
301@@ -28,6 +28,8 @@
302 # files in the program, then also delete it here.
303 """Tests for the main SSO client code."""
304
305+from __future__ import unicode_literals
306+
307 import logging
308
309 from functools import partial
310@@ -137,7 +139,7 @@
311 class MyException(Exception):
312 """Custom Exception."""
313
314- message = u'My custom error for ♥ Ubuntu'
315+ message = 'My custom error for ♥ Ubuntu'
316 my_exc = MyException(message)
317 result = except_to_errdict(my_exc)
318 self.assertEqual(result["errtype"], my_exc.__class__.__name__)
319@@ -145,7 +147,7 @@
320
321 def test_first_arg_is_unicode(self):
322 """If the first arg is a unicode, use it as the message."""
323- sample_string = u"a sample string"
324+ sample_string = "a sample string"
325 e = TestExceptToErrdictException(sample_string)
326 result = except_to_errdict(e)
327 self.assertEqual(result["errtype"], e.__class__.__name__)
328@@ -160,7 +162,7 @@
329
330 def test_some_other_thing_as_first_arg(self):
331 """If first arg is not basestring nor dict, then repr all args."""
332- sample_args = (None, u"unicode2\ufffd", "errorcode3")
333+ sample_args = (None, "unicode2\ufffd", "errorcode3")
334 e = TestExceptToErrdictException(*sample_args)
335 result = except_to_errdict(e)
336 self.assertEqual(result["errtype"], e.__class__.__name__)
337
338=== modified file 'ubuntu_sso/tests/test_account.py'
339--- ubuntu_sso/tests/test_account.py 2012-07-03 21:04:09 +0000
340+++ ubuntu_sso/tests/test_account.py 2012-07-24 20:55:21 +0000
341@@ -31,6 +31,8 @@
342 # files in the program, then also delete it here.
343 """Tests for the SSO account code."""
344
345+from __future__ import unicode_literals
346+
347 import os
348
349 from twisted.trial.unittest import TestCase
350@@ -69,7 +71,7 @@
351 CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \
352 "Can't reset password for this account"
353 RESET_TOKEN_INVALID_CONTENT = "AuthToken matching query does not exist."
354-EMAIL_ALREADY_REGISTERED = u'a@example.com'
355+EMAIL_ALREADY_REGISTERED = 'a@example.com'
356 STATUS_UNKNOWN = {'status': 'yadda-yadda'}
357 STATUS_ERROR = {'status': SSO_STATUS_ERROR,
358 'errors': {'something': ['Bla', 'Ble']}}
359@@ -177,12 +179,12 @@
360
361 def fake_accounts_me(self):
362 """Fake the 'me' information."""
363- return {u'username': u'Wh46bKY',
364- u'preferred_email': self.preferred_email,
365- u'displayname': u'',
366- u'unverified_emails': [u'aaaaaa@example.com'],
367- u'verified_emails': [],
368- u'openid_identifier': u'Wh46bKY'}
369+ return {'username': 'Wh46bKY',
370+ 'preferred_email': self.preferred_email,
371+ 'displayname': '',
372+ 'unverified_emails': ['aaaaaa@example.com'],
373+ 'verified_emails': [],
374+ 'openid_identifier': 'Wh46bKY'}
375
376 def check_all_kwargs_unicode(self, **kwargs):
377 """Check that the values of all keyword arguments are unicode."""
378@@ -254,27 +256,27 @@
379 @defer.inlineCallbacks
380 def test_register_user_checks_valid_email(self):
381 """Email is validated."""
382- self.register_kwargs['email'] = u'notavalidemail'
383+ self.register_kwargs['email'] = 'notavalidemail'
384 d = self.processor.register_user(**self.register_kwargs)
385 yield self.assertFailure(d, InvalidEmailError)
386
387 @defer.inlineCallbacks
388 def test_register_user_checks_valid_password(self):
389 """Password is validated."""
390- self.register_kwargs['password'] = u''
391+ self.register_kwargs['password'] = ''
392 d = self.processor.register_user(**self.register_kwargs)
393 yield self.assertFailure(d, InvalidPasswordError)
394
395 # 7 chars, one less than expected
396- self.register_kwargs['password'] = u'tesT3it'
397- d = self.processor.register_user(**self.register_kwargs)
398- yield self.assertFailure(d, InvalidPasswordError)
399-
400- self.register_kwargs['password'] = u'test3it!' # no upper case
401- d = self.processor.register_user(**self.register_kwargs)
402- yield self.assertFailure(d, InvalidPasswordError)
403-
404- self.register_kwargs['password'] = u'testIt!!' # no number
405+ self.register_kwargs['password'] = 'tesT3it'
406+ d = self.processor.register_user(**self.register_kwargs)
407+ yield self.assertFailure(d, InvalidPasswordError)
408+
409+ self.register_kwargs['password'] = 'test3it!' # no upper case
410+ d = self.processor.register_user(**self.register_kwargs)
411+ yield self.assertFailure(d, InvalidPasswordError)
412+
413+ self.register_kwargs['password'] = 'testIt!!' # no number
414 d = self.processor.register_user(**self.register_kwargs)
415 yield self.assertFailure(d, InvalidPasswordError)
416
417
418=== added file 'ubuntu_sso/utils/compat.py'
419--- ubuntu_sso/utils/compat.py 1970-01-01 00:00:00 +0000
420+++ ubuntu_sso/utils/compat.py 2012-07-24 20:55:21 +0000
421@@ -0,0 +1,49 @@
422+# -*- coding: utf-8 -*-
423+#
424+# Copyright 2012 Canonical Ltd.
425+#
426+# This program is free software: you can redistribute it and/or modify it
427+# under the terms of the GNU General Public License version 3, as published
428+# by the Free Software Foundation.
429+#
430+# This program is distributed in the hope that it will be useful, but
431+# WITHOUT ANY WARRANTY; without even the implied warranties of
432+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
433+# PURPOSE. See the GNU General Public License for more details.
434+#
435+# You should have received a copy of the GNU General Public License along
436+# with this program. If not, see <http://www.gnu.org/licenses/>.
437+#
438+# In addition, as a special exception, the copyright holders give
439+# permission to link the code of portions of this program with the
440+# OpenSSL library under certain conditions as described in each
441+# individual source file, and distribute linked combinations
442+# including the two.
443+# You must obey the GNU General Public License in all respects
444+# for all of the code used other than OpenSSL. If you modify
445+# file(s) with this exception, you may extend this exception to your
446+# version of the file(s), but you are not obligated to do so. If you
447+# do not wish to do so, delete this exception statement from your
448+# version. If you delete this exception statement from all source
449+# files in the program, then also delete it here.
450+"""Python 2 and 3 compatibility."""
451+
452+from __future__ import unicode_literals
453+
454+# The following approach was outlined in Lennart Regebro's
455+# "Porting to Python 3" book.
456+# http://python3porting.com/noconv.html#more-bytes-strings-and-unicode
457+
458+import sys
459+
460+# Disable redefined builtin, invalid name warning
461+# pylint: disable=W0622,C0103
462+
463+if sys.version_info < (3,):
464+ text_type = unicode
465+ binary_type = str
466+ basestring = basestring
467+else:
468+ text_type = str
469+ binary_type = bytes
470+ basestring = str

Subscribers

People subscribed via source and target branches