← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:py3-services-unicode into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:py3-services-unicode into launchpad:master.

Commit message:
Port unicode() calls in lp.services to Python 3

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/391189

This unfortunately requires some contextual clues, because `six.text_type(b'foo')` returns `u'foo'` on Python 2 but `"b'foo'"` on Python 3, while `six.ensure_text` works on bytes or text but not on other types.  Use single-argument `six.text_type` in cases where we know that the argument is not bytes, `six.ensure_text` where we know the argument is either bytes or text, and something more elaborate otherwise.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-services-unicode into launchpad:master.
diff --git a/lib/lp/services/apachelogparser/base.py b/lib/lp/services/apachelogparser/base.py
index f2e556a..e5fbaa7 100644
--- a/lib/lp/services/apachelogparser/base.py
+++ b/lib/lp/services/apachelogparser/base.py
@@ -11,6 +11,7 @@ from lazr.uri import (
     URI,
     )
 import pytz
+import six
 from zope.component import getUtility
 
 from lp.services.apachelogparser.model.parsedapachelog import ParsedApacheLog
@@ -34,7 +35,7 @@ def get_files_to_parse(file_paths):
     file_paths = sorted(file_paths, key=lambda path: os.stat(path).st_mtime)
     for file_path in file_paths:
         fd, file_size = get_fd_and_file_size(file_path)
-        first_line = unicode(fd.readline())
+        first_line = six.ensure_text(fd.readline())
         parsed_file = store.find(ParsedApacheLog, first_line=first_line).one()
         position = 0
         if parsed_file is not None:
@@ -169,7 +170,7 @@ def parse_file(fd, start_position, logger, get_download_key, parsed_lines=0):
 
 def create_or_update_parsedlog_entry(first_line, parsed_bytes):
     """Create or update the ParsedApacheLog with the given first_line."""
-    first_line = unicode(first_line)
+    first_line = six.ensure_text(first_line)
     parsed_file = IStore(ParsedApacheLog).find(
         ParsedApacheLog, first_line=first_line).one()
     if parsed_file is None:
diff --git a/lib/lp/services/apachelogparser/model/parsedapachelog.py b/lib/lp/services/apachelogparser/model/parsedapachelog.py
index 3bab7f6..128a46c 100644
--- a/lib/lp/services/apachelogparser/model/parsedapachelog.py
+++ b/lib/lp/services/apachelogparser/model/parsedapachelog.py
@@ -4,6 +4,7 @@
 __metaclass__ = type
 __all__ = ['ParsedApacheLog']
 
+import six
 from storm.locals import (
     Int,
     Storm,
@@ -31,6 +32,6 @@ class ParsedApacheLog(Storm):
 
     def __init__(self, first_line, bytes_read):
         super(ParsedApacheLog, self).__init__()
-        self.first_line = unicode(first_line)
+        self.first_line = six.ensure_text(first_line)
         self.bytes_read = bytes_read
         IStore(self.__class__).add(self)
diff --git a/lib/lp/services/features/model.py b/lib/lp/services/features/model.py
index 3a6b070..2eeb63f 100644
--- a/lib/lp/services/features/model.py
+++ b/lib/lp/services/features/model.py
@@ -12,6 +12,7 @@ __metaclass__ = type
 from datetime import datetime
 
 import pytz
+import six
 from storm.locals import (
     DateTime,
     Int,
@@ -58,9 +59,9 @@ class FeatureFlagChangelogEntry(Storm):
 
     def __init__(self, diff, comment, person):
         super(FeatureFlagChangelogEntry, self).__init__()
-        self.diff = unicode(diff)
+        self.diff = six.ensure_text(diff)
         self.date_changed = datetime.now(pytz.timezone('UTC'))
-        self.comment = unicode(comment)
+        self.comment = six.ensure_text(comment)
         self.person = person
 
 
diff --git a/lib/lp/services/features/rulesource.py b/lib/lp/services/features/rulesource.py
index 65e3037..6f8310f 100644
--- a/lib/lp/services/features/rulesource.py
+++ b/lib/lp/services/features/rulesource.py
@@ -19,6 +19,7 @@ from collections import (
     )
 import re
 
+import six
 from storm.locals import Desc
 
 from lp.services.features.model import (
@@ -100,7 +101,7 @@ class FeatureRuleSource(object):
                 continue
             flag, scope, priority_str, value = re.split('[ \t]+', line, 3)
             priority = int(priority_str)
-            r.append((flag, scope, priority, unicode(value)))
+            r.append((flag, scope, priority, six.ensure_text(value)))
             if priority in seen_priorities[flag]:
                 raise DuplicatePriorityError(flag, priority)
             seen_priorities[flag].add(priority)
@@ -146,8 +147,8 @@ class StormFeatureRuleSource(FeatureRuleSource):
         store.execute('DELETE FROM FeatureFlag')
         for (flag, scope, priority, value) in new_rules:
             store.add(FeatureFlag(
-                scope=unicode(scope),
-                flag=unicode(flag),
+                scope=six.ensure_text(scope),
+                flag=six.ensure_text(flag),
                 value=value,
                 priority=priority))
         store.flush()
diff --git a/lib/lp/services/features/testing.py b/lib/lp/services/features/testing.py
index 3a2d69b..e4ed8be 100644
--- a/lib/lp/services/features/testing.py
+++ b/lib/lp/services/features/testing.py
@@ -91,7 +91,7 @@ class FeatureFixtureMixin:
                 flag=flag_name,
                 scope='default',
                 priority=999,
-                value=unicode(value))
+                value=six.text_type(value))
             for flag_name, value in six.iteritems(self.desired_features)
                 if value is not None]
 
diff --git a/lib/lp/services/features/xmlrpc.py b/lib/lp/services/features/xmlrpc.py
index d7d681b..e2c6e54 100644
--- a/lib/lp/services/features/xmlrpc.py
+++ b/lib/lp/services/features/xmlrpc.py
@@ -9,6 +9,7 @@ __all__ = [
     'FeatureFlagApplication',
     ]
 
+import six
 from zope.component import getUtility
 from zope.interface import implementer
 
@@ -52,7 +53,7 @@ class FeatureFlagApplication:
                     scopes.append(TeamScope(lambda: person))
             else:
                 scopes.append(FixedScope(scope_name))
-        flag_name = unicode(flag_name)
+        flag_name = six.ensure_text(flag_name)
         controller = FeatureController(
             MultiScopeHandler(scopes).lookup, StormFeatureRuleSource())
         return controller.getFlag(flag_name)
diff --git a/lib/lp/services/feeds/feed.py b/lib/lp/services/feeds/feed.py
index 76ff15b..352e540 100644
--- a/lib/lp/services/feeds/feed.py
+++ b/lib/lp/services/feeds/feed.py
@@ -21,6 +21,7 @@ import operator
 import os
 import time
 
+import six
 from six.moves.urllib.parse import urljoin
 from zope.browserpage import ViewPageTemplateFile
 from zope.component import getUtility
@@ -295,7 +296,7 @@ class FeedTypedData:
             for a_tag in a_tags:
                 if a_tag['href'].startswith('/'):
                     a_tag['href'] = urljoin(self.root_url, a_tag['href'])
-            altered_content = unicode(soup)
+            altered_content = six.text_type(soup)
         else:
             altered_content = self._content
 
@@ -303,7 +304,7 @@ class FeedTypedData:
             altered_content = html_escape(altered_content)
         elif self.content_type == 'xhtml':
             soup = BeautifulSoup(altered_content)
-            altered_content = unicode(soup)
+            altered_content = six.text_type(soup)
         return altered_content
 
 
diff --git a/lib/lp/services/feeds/tests/helper.py b/lib/lp/services/feeds/tests/helper.py
index ca7c741..af37ade 100644
--- a/lib/lp/services/feeds/tests/helper.py
+++ b/lib/lp/services/feeds/tests/helper.py
@@ -26,6 +26,7 @@ if socket.getdefaulttimeout() != original_timeout:
 from cStringIO import StringIO
 from textwrap import wrap
 
+import six
 from zope.interface import (
     Attribute,
     implementer,
@@ -125,7 +126,7 @@ def validate_feed(content, content_type, base_uri):
                     max(error_line_number - 2, 1),
                     min(error_line_number + 3, len(lines))))
                 for line_number in line_number_range:
-                    unicode_line = unicode(
+                    unicode_line = six.ensure_text(
                         lines[line_number - 1], 'ascii', 'replace')
                     ascii_line = unicode_line.encode('ascii', 'replace')
                     wrapped_lines = wrap(ascii_line, max_line_length)
diff --git a/lib/lp/services/fields/__init__.py b/lib/lp/services/fields/__init__.py
index 5675366..9ad33f6 100644
--- a/lib/lp/services/fields/__init__.py
+++ b/lib/lp/services/fields/__init__.py
@@ -64,6 +64,7 @@ from lazr.uri import (
     InvalidURIError,
     URI,
     )
+import six
 from zope.component import getUtility
 from zope.interface import (
     implementer,
@@ -614,7 +615,7 @@ class URIField(TextLine):
                 uri = uri.ensureSlash()
             else:
                 uri = uri.ensureNoSlash()
-        input = unicode(uri)
+        input = six.text_type(uri)
         return input
 
     def _validate(self, value):
diff --git a/lib/lp/services/gpg/doc/gpg-encryption.txt b/lib/lp/services/gpg/doc/gpg-encryption.txt
index 1c79391..d26a497 100644
--- a/lib/lp/services/gpg/doc/gpg-encryption.txt
+++ b/lib/lp/services/gpg/doc/gpg-encryption.txt
@@ -5,6 +5,7 @@ This document describes the current procedure to encrypt and
 decrypt contents in Launchpad, and demonstrates that the methods
 can support Unicode contents.
 
+    >>> import six
     >>> from lp.testing.gpgkeys import (
     ...     import_public_test_keys, import_secret_test_key, decrypt_content)
     >>> import transaction
@@ -83,7 +84,7 @@ Decrypt a unicode content:
 
     >>> content = u'a\xe7ucar'
     >>> cipher = gpghandler.encryptContent(content.encode('iso-8859-1'), key)
-    >>> cipher = unicode(cipher)
+    >>> cipher = six.ensure_text(cipher)
     >>> plain = decrypt_content(cipher, 'test')    
     Traceback (most recent call last):
     ...
diff --git a/lib/lp/services/mail/sendmail.py b/lib/lp/services/mail/sendmail.py
index 6f894e3..86d5ed1 100644
--- a/lib/lp/services/mail/sendmail.py
+++ b/lib/lp/services/mail/sendmail.py
@@ -452,7 +452,7 @@ def sendmail(message, to_addrs=None, bulk=True):
     message_detail = message['Subject']
     if not isinstance(message_detail, six.string_types):
         # Might be a Header object; can be squashed.
-        message_detail = unicode(message_detail)
+        message_detail = six.text_type(message_detail)
     if _immediate_mail_delivery:
         # Immediate email delivery is not unit tested, and won't be.
         # The immediate-specific stuff is pretty simple though so this
diff --git a/lib/lp/services/messages/model/message.py b/lib/lp/services/messages/model/message.py
index 10a62ee..98c1f2a 100644
--- a/lib/lp/services/messages/model/message.py
+++ b/lib/lp/services/messages/model/message.py
@@ -153,7 +153,7 @@ class Message(SQLBase):
 
     @classmethod
     def chunks_text(cls, chunks):
-        bits = [unicode(chunk) for chunk in chunks if chunk.content]
+        bits = [six.text_type(chunk) for chunk in chunks if chunk.content]
         return '\n\n'.join(bits)
 
     # XXX flacoste 2006-09-08: Bogus attribute only present so that
@@ -270,7 +270,7 @@ class MessageSet:
             re_encoded_bits.append(
                 (self.decode(bytes, charset).encode('utf-8'), 'utf-8'))
 
-        return unicode(email.header.make_header(re_encoded_bits))
+        return six.text_type(email.header.make_header(re_encoded_bits))
 
     def fromEmail(self, email_message, owner=None, filealias=None,
                   parsed_message=None, create_missing_persons=False,
@@ -571,8 +571,8 @@ class UserToUserEmail(Storm):
         # Initialize.
         self.sender = sender
         self.recipient = recipient
-        self.message_id = unicode(message_id, 'ascii')
-        self.subject = unicode(make_header(decode_header(subject)))
+        self.message_id = six.ensure_text(message_id, 'ascii')
+        self.subject = six.text_type(make_header(decode_header(subject)))
         # Add the object to the store of the sender.  Our StormMigrationGuide
         # recommends against this saying "Note that the constructor should not
         # usually add the object to a store -- leave that for a FooSet.new()
diff --git a/lib/lp/services/oauth/browser/__init__.py b/lib/lp/services/oauth/browser/__init__.py
index adb72dc..61e8845 100644
--- a/lib/lp/services/oauth/browser/__init__.py
+++ b/lib/lp/services/oauth/browser/__init__.py
@@ -16,6 +16,7 @@ from datetime import (
 from lazr.restful import HTTPResource
 import pytz
 import simplejson
+import six
 from zope.component import getUtility
 from zope.formlib.form import (
     Action,
@@ -415,7 +416,7 @@ class OAuthAccessTokenView(LaunchpadView):
 
     def _set_status_and_error(self, error):
         self.request.response.setStatus(403)
-        return unicode(error)
+        return six.text_type(error)
 
     def __call__(self):
         """Create an access token and respond with its key/secret/context.
diff --git a/lib/lp/services/tokens.py b/lib/lp/services/tokens.py
index 6e88cfa..35f7f42 100644
--- a/lib/lp/services/tokens.py
+++ b/lib/lp/services/tokens.py
@@ -11,6 +11,8 @@ __all__ = [
 
 import random
 
+import six
+
 
 def create_token(token_length):
     """Create a random token string.
@@ -23,4 +25,4 @@ def create_token(token_length):
     token = ''.join(
         random.SystemRandom().choice(characters)
         for count in range(token_length))
-    return unicode(token)
+    return six.ensure_text(token)
diff --git a/lib/lp/services/utils.py b/lib/lp/services/utils.py
index d3def35..dc15176 100644
--- a/lib/lp/services/utils.py
+++ b/lib/lp/services/utils.py
@@ -165,8 +165,10 @@ def value_string(item):
         return '(not set)'
     elif zope_isinstance(item, BaseItem):
         return item.title
+    elif zope_isinstance(item, bytes):
+        return six.ensure_text(item)
     else:
-        return unicode(item)
+        return six.text_type(item)
 
 
 def text_delta(instance_delta, delta_names, state_names, interface):
diff --git a/lib/lp/services/webapp/authentication.py b/lib/lp/services/webapp/authentication.py
index c48bb33..acab459 100644
--- a/lib/lp/services/webapp/authentication.py
+++ b/lib/lp/services/webapp/authentication.py
@@ -257,7 +257,7 @@ class LaunchpadPrincipal:
 
     def __init__(self, id, title, description, account,
                  access_level=AccessLevel.WRITE_PRIVATE, scope_url=None):
-        self.id = unicode(id)
+        self.id = six.text_type(id)
         self.title = title
         self.description = description
         self.access_level = access_level
diff --git a/lib/lp/services/webapp/error.py b/lib/lp/services/webapp/error.py
index a883698..6b15896 100644
--- a/lib/lp/services/webapp/error.py
+++ b/lib/lp/services/webapp/error.py
@@ -17,6 +17,7 @@ __all__ = [
 import sys
 import traceback
 
+import six
 from six.moves import http_client
 from zope.browser.interfaces import ISystemErrorView
 from zope.browserpage import ViewPageTemplateFile
@@ -205,7 +206,7 @@ class NotFoundView(SystemErrorView):
             # to show a link back to the referring site, so we can't use
             # replace or ignore.  Best to just pretent it doesn't exist.
             try:
-                return unicode(referrer)
+                return six.ensure_text(referrer)
             except UnicodeDecodeError:
                 return None
         else:
diff --git a/lib/lp/services/webapp/errorlog.py b/lib/lp/services/webapp/errorlog.py
index df20c1d..4c3ed01 100644
--- a/lib/lp/services/webapp/errorlog.py
+++ b/lib/lp/services/webapp/errorlog.py
@@ -193,10 +193,10 @@ def attach_http_request(report, context):
 
     if principal is not None and principal is not missing:
         username = ', '.join([
-                unicode(login),
-                unicode(request.principal.id),
-                unicode(request.principal.title),
-                unicode(request.principal.description)])
+                six.ensure_text(login),
+                six.text_type(request.principal.id),
+                six.ensure_text(request.principal.title),
+                six.ensure_text(request.principal.description)])
         report['username'] = username
 
     if getattr(request, '_orig_env', None):
@@ -217,7 +217,7 @@ def attach_http_request(report, context):
         args = request.getPositionalArguments()
         # Request variables are strings: this could move to its own key and be
         # raw.
-        report['req_vars']['xmlrpc args'] = unicode(args)
+        report['req_vars']['xmlrpc args'] = six.text_type(args)
 
 
 def attach_ignore_from_exception(report, context):
diff --git a/lib/lp/services/webapp/escaping.py b/lib/lp/services/webapp/escaping.py
index bce499e..e271477 100644
--- a/lib/lp/services/webapp/escaping.py
+++ b/lib/lp/services/webapp/escaping.py
@@ -48,7 +48,11 @@ def html_escape(message):
         # It is possible that the message is wrapped in an
         # internationalized object, so we need to translate it
         # first. See bug #54987.
-        raw = unicode(translate_if_i18n(message))
+        raw = translate_if_i18n(message)
+        if isinstance(raw, bytes):
+            raw = six.ensure_text(raw)
+        else:
+            raw = six.text_type(raw)
         for needle, replacement in HTML_REPLACEMENTS:
             raw = raw.replace(needle, replacement)
         return raw
@@ -86,7 +90,11 @@ def translate_if_i18n(obj_or_msgid):
 class structured:
 
     def __init__(self, text, *reps, **kwreps):
-        text = unicode(translate_if_i18n(text))
+        text = translate_if_i18n(text)
+        if isinstance(text, bytes):
+            text = six.ensure_text(text)
+        else:
+            text = six.text_type(text)
         self.text = text
         if reps and kwreps:
             raise TypeError(
diff --git a/lib/lp/services/webapp/publication.py b/lib/lp/services/webapp/publication.py
index a289f44..eb98ae1 100644
--- a/lib/lp/services/webapp/publication.py
+++ b/lib/lp/services/webapp/publication.py
@@ -19,6 +19,7 @@ from lazr.uri import (
     URI,
     )
 from psycopg2.extensions import TransactionRollbackError
+import six
 from six.moves.urllib.parse import quote
 from storm.database import STATE_DISCONNECTED
 from storm.exceptions import (
@@ -248,7 +249,7 @@ class LaunchpadBrowserPublication(
         threadrequestfile = open_for_writing(
             'logs/thread-%s.request' % threadid, 'w')
         try:
-            request_txt = unicode(request).encode('UTF-8')
+            request_txt = six.text_type(request).encode('UTF-8')
         except Exception:
             request_txt = 'Exception converting request to string\n\n'
             try:
diff --git a/lib/lp/services/webapp/publisher.py b/lib/lp/services/webapp/publisher.py
index 1c19bd8..199be4b 100644
--- a/lib/lp/services/webapp/publisher.py
+++ b/lib/lp/services/webapp/publisher.py
@@ -775,8 +775,8 @@ def canonical_url(
          request is not None and
          root_url.startswith(request.getApplicationURL()))
         or force_local_path):
-        return unicode('/' + path)
-    return unicode(root_url + path)
+        return u'/' + path
+    return six.ensure_text(root_url + path)
 
 
 def canonical_name(name):
diff --git a/lib/lp/services/webapp/servers.py b/lib/lp/services/webapp/servers.py
index e23198e..f9a01b4 100644
--- a/lib/lp/services/webapp/servers.py
+++ b/lib/lp/services/webapp/servers.py
@@ -861,7 +861,8 @@ class LaunchpadBrowserResponse(NotificationResponse, BrowserResponse):
             else:
                 status = 303
         super(LaunchpadBrowserResponse, self).redirect(
-            unicode(location).encode('UTF-8'), status=status, trusted=trusted)
+            six.ensure_str(six.text_type(location)),
+            status=status, trusted=trusted)
 
 
 def adaptResponseToSession(response):
diff --git a/lib/lp/services/webapp/sorting.py b/lib/lp/services/webapp/sorting.py
index efc4ee2..72d04dc 100644
--- a/lib/lp/services/webapp/sorting.py
+++ b/lib/lp/services/webapp/sorting.py
@@ -84,7 +84,7 @@ def sorted_version_numbers(sequence, key=_identity):
 
     >>> class series:
     ...   def __init__(self, name):
-    ...     self.name = unicode(name)
+    ...     self.name = six.ensure_text(name)
     >>> bzr_versions = [series('0.9'), series('0.10'), series('0.11'),
     ...                 series('bzr-0.9'), series('bzr-0.10'),
     ...                 series('bzr-0.11'), series('foo')]
@@ -130,7 +130,7 @@ def sorted_dotted_numbers(sequence, key=_identity):
 
     >>> class series:
     ...   def __init__(self, name):
-    ...     self.name = unicode(name)
+    ...     self.name = six.ensure_text(name)
     >>> bzr_versions = [series('0.9'), series('0.10'), series('0.11'),
     ...                 series('bzr-0.9'), series('bzr-0.10'),
     ...                 series('bzr-0.11'), series('foo')]
diff --git a/lib/lp/services/webapp/tests/test_errorlog.py b/lib/lp/services/webapp/tests/test_errorlog.py
index 20e4b52..3a04f8c 100644
--- a/lib/lp/services/webapp/tests/test_errorlog.py
+++ b/lib/lp/services/webapp/tests/test_errorlog.py
@@ -14,6 +14,7 @@ from lazr.batchnavigator.interfaces import InvalidBatchSizeError
 from lazr.restful.declarations import error_status
 import oops_amqp
 import pytz
+import six
 from six.moves import http_client
 import testtools
 from timeline.timeline import Timeline
@@ -655,5 +656,5 @@ class TestHooks(testtools.TestCase):
             'http_request': {'SIMPLE': 'string', 'COMPLEX': complexthing}}
         attach_http_request(report, context)
         self.assertEqual(
-            {'SIMPLE': 'string', 'COMPLEX': unicode(complexthing)},
+            {'SIMPLE': 'string', 'COMPLEX': six.text_type(complexthing)},
             report['req_vars'])
diff --git a/lib/lp/services/webapp/tests/test_launchpad_login_source.txt b/lib/lp/services/webapp/tests/test_launchpad_login_source.txt
index ff9e54d..27a520c 100644
--- a/lib/lp/services/webapp/tests/test_launchpad_login_source.txt
+++ b/lib/lp/services/webapp/tests/test_launchpad_login_source.txt
@@ -4,6 +4,7 @@ LaunchpadLoginSource is used to create principals, from login
 information, passing the email address to getPrincipalByLogin. If no
 person is found with the given email address, None is returned
 
+    >>> import six
     >>> from lp.services.webapp.authentication import (
     ...     LaunchpadLoginSource)
     >>> login_source = LaunchpadLoginSource()
@@ -21,7 +22,7 @@ account id.
     >>> from lp.services.webapp.interfaces import ILaunchpadPrincipal
     >>> ILaunchpadPrincipal.providedBy(principal)
     True
-    >>> principal.id == unicode(account.id)
+    >>> principal.id == six.text_type(account.id)
     True
 
 The corresponding Account and Person records are also in the