← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:py3only-version-detection into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:py3only-version-detection into launchpad:master.

Commit message:
Remove simple cases of Python 2 version detection

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/406806
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3only-version-detection into launchpad:master.
diff --git a/lib/lp/app/security.py b/lib/lp/app/security.py
index 6fae684..08fe1e5 100644
--- a/lib/lp/app/security.py
+++ b/lib/lp/app/security.py
@@ -13,7 +13,6 @@ __all__ = [
 
 from itertools import repeat
 
-import six
 from six.moves import zip as izip
 from zope.component import queryAdapter
 from zope.interface import implementer
@@ -128,9 +127,6 @@ class non_boolean_izip(izip):
             "DelegatedAuthorization results can't be used in boolean "
             "expressions.")
 
-    if six.PY2:
-        __nonzero__ = __bool__
-
 
 class DelegatedAuthorization(AuthorizationBase):
 
diff --git a/lib/lp/bugs/externalbugtracker/bugzilla.py b/lib/lp/bugs/externalbugtracker/bugzilla.py
index 830f680..2f941e8 100644
--- a/lib/lp/bugs/externalbugtracker/bugzilla.py
+++ b/lib/lp/bugs/externalbugtracker/bugzilla.py
@@ -13,7 +13,6 @@ __all__ = [
 
 from email.utils import parseaddr
 import re
-import string
 import xml.parsers.expat
 
 from defusedxml import minidom
@@ -190,8 +189,7 @@ class Bugzilla(ExternalBugTracker):
         bad_chars = b''.join(six.int2byte(i) for i in range(0, 32))
         for char in b'\n', b'\r', b'\t':
             bad_chars = bad_chars.replace(char, b'')
-        maketrans = bytes.maketrans if six.PY3 else string.maketrans
-        trans_map = maketrans(bad_chars, b' ' * len(bad_chars))
+        trans_map = bytes.maketrans(bad_chars, b' ' * len(bad_chars))
         contents = contents.translate(trans_map)
         # Don't use forbid_dtd=True here; Bugzilla XML responses seem to
         # include DOCTYPE declarations.
diff --git a/lib/lp/bugs/mail/handler.py b/lib/lp/bugs/mail/handler.py
index 3a151e2..fdf9a31 100644
--- a/lib/lp/bugs/mail/handler.py
+++ b/lib/lp/bugs/mail/handler.py
@@ -13,7 +13,6 @@ import os
 
 from lazr.lifecycle.event import ObjectCreatedEvent
 from lazr.lifecycle.interfaces import IObjectCreatedEvent
-import six
 import transaction
 from zope.component import getUtility
 from zope.event import notify
@@ -69,9 +68,6 @@ class BugTaskCommandGroup:
     def __bool__(self):
         return len(self._commands) > 0
 
-    if six.PY2:
-        __nonzero__ = __bool__
-
     def __str__(self):
         text_commands = [str(cmd) for cmd in self.commands]
         return '\n'.join(text_commands).strip()
@@ -98,9 +94,6 @@ class BugCommandGroup(BugTaskCommandGroup):
         else:
             return super(BugCommandGroup, self).__bool__()
 
-    if six.PY2:
-        __nonzero__ = __bool__
-
     def __str__(self):
         text_commands = [super(BugCommandGroup, self).__str__()]
         for group in self.groups:
diff --git a/lib/lp/bugs/scripts/bugexport.py b/lib/lp/bugs/scripts/bugexport.py
index 70e340c..b607f40 100644
--- a/lib/lp/bugs/scripts/bugexport.py
+++ b/lib/lp/bugs/scripts/bugexport.py
@@ -8,7 +8,6 @@ __all__ = [
     ]
 
 import base64
-import sys
 
 
 try:
@@ -28,12 +27,6 @@ from lp.services.librarian.browser import ProxiedLibraryFileAlias
 BUGS_XMLNS = 'https://launchpad.net/xmlns/2006/bugs'
 
 
-if sys.version_info[0] >= 3:
-    encodebytes = base64.encodebytes
-else:
-    encodebytes = base64.encodestring
-
-
 def addnode(parent, elementname, content, **attrs):
     node = ET.SubElement(parent, elementname, attrs)
     node.text = content
@@ -104,7 +97,8 @@ def serialise_bugtask(bugtask):
                     attachment.libraryfile.mimetype)
             # Attach the attachment file contents, base 64 encoded.
             addnode(attachment_node, 'contents',
-                    encodebytes(attachment.libraryfile.read()).decode('ASCII'))
+                    base64.encodebytes(
+                        attachment.libraryfile.read()).decode('ASCII'))
 
     return bug_node
 
diff --git a/lib/lp/bugs/scripts/tests/test_bugsummaryrebuild.py b/lib/lp/bugs/scripts/tests/test_bugsummaryrebuild.py
index 70ae789..f7530e2 100644
--- a/lib/lp/bugs/scripts/tests/test_bugsummaryrebuild.py
+++ b/lib/lp/bugs/scripts/tests/test_bugsummaryrebuild.py
@@ -3,8 +3,6 @@
 
 __metaclass__ = type
 
-import sys
-
 from testtools.content import text_content
 from testtools.matchers import MatchesRegex
 import transaction
@@ -153,12 +151,10 @@ class TestBugSummaryRebuild(TestCaseWithFactory):
             rebuild_bugsummary_for_target(product, log)
         self.assertEqual(1, get_bugsummary_rows(product).count())
         self.assertEqual(0, get_bugsummaryjournal_rows(product).count())
-        long_type = int if sys.version_info[0] >= 3 else long
         self.assertThat(
             log.getLogBufferAndClear(),
             MatchesRegex(
-                'DEBUG Rebuilding %s\nDEBUG Added {.*: %r}' %
-                (product.name, long_type(1))))
+                'DEBUG Rebuilding %s\nDEBUG Added {.*: 1}' % product.name))
 
     def test_script(self):
         product = self.factory.makeProduct()
diff --git a/lib/lp/code/browser/tests/test_branchmergeproposal.py b/lib/lp/code/browser/tests/test_branchmergeproposal.py
index c4a776d..5f21254 100644
--- a/lib/lp/code/browser/tests/test_branchmergeproposal.py
+++ b/lib/lp/code/browser/tests/test_branchmergeproposal.py
@@ -13,9 +13,11 @@ from datetime import (
     datetime,
     timedelta,
     )
-from difflib import unified_diff
+from difflib import (
+    diff_bytes,
+    unified_diff,
+    )
 import doctest
-from functools import partial
 import hashlib
 import re
 
@@ -144,14 +146,6 @@ from lp.testing.views import (
     )
 
 
-if six.PY3:
-    from difflib import diff_bytes
-
-    unified_diff_bytes = partial(diff_bytes, unified_diff)
-else:
-    unified_diff_bytes = unified_diff
-
-
 class GitHostingClientMixin:
 
     def setUp(self):
@@ -1427,22 +1421,21 @@ class TestBranchMergeProposalView(TestCaseWithFactory):
     def test_preview_diff_utf8(self):
         """A preview_diff in utf-8 should be decoded as utf-8."""
         text = ''.join(six.unichr(x) for x in range(255))
-        diff_bytes = ''.join(unified_diff([''], [text])).encode('utf-8')
-        self.setPreviewDiff(diff_bytes)
+        diff = ''.join(unified_diff([''], [text])).encode('utf-8')
+        self.setPreviewDiff(diff)
         transaction.commit()
         view = create_initialized_view(self.bmp, '+index')
-        self.assertEqual(diff_bytes.decode('utf-8'),
-                         view.preview_diff_text)
+        self.assertEqual(diff.decode('utf-8'), view.preview_diff_text)
         self.assertTrue(view.diff_available)
 
     def test_preview_diff_all_chars(self):
         """preview_diff should work on diffs containing all possible bytes."""
         text = b''.join(six.int2byte(x) for x in range(255))
-        diff_bytes = b''.join(unified_diff_bytes([b''], [text]))
-        self.setPreviewDiff(diff_bytes)
+        diff = b''.join(diff_bytes(unified_diff, [b''], [text]))
+        self.setPreviewDiff(diff)
         transaction.commit()
         view = create_initialized_view(self.bmp, '+index')
-        self.assertEqual(diff_bytes.decode('windows-1252', 'replace'),
+        self.assertEqual(diff.decode('windows-1252', 'replace'),
                          view.preview_diff_text)
         self.assertTrue(view.diff_available)
 
@@ -1450,8 +1443,8 @@ class TestBranchMergeProposalView(TestCaseWithFactory):
         # The preview_diff will recover from a timeout set to get the
         # librarian content.
         text = b''.join(six.int2byte(x) for x in range(255))
-        diff_bytes = b''.join(unified_diff_bytes([b''], [text]))
-        preview_diff = self.setPreviewDiff(diff_bytes)
+        diff = b''.join(diff_bytes(unified_diff, [b''], [text]))
+        preview_diff = self.setPreviewDiff(diff)
         transaction.commit()
 
         def fake_open(*args):
@@ -1470,8 +1463,8 @@ class TestBranchMergeProposalView(TestCaseWithFactory):
         # librarian content.  (This can happen e.g. on staging replicas of
         # the production database.)
         text = b''.join(six.int2byte(x) for x in range(255))
-        diff_bytes = b''.join(unified_diff_bytes([b''], [text]))
-        preview_diff = self.setPreviewDiff(diff_bytes)
+        diff = b''.join(diff_bytes(unified_diff, [b''], [text]))
+        preview_diff = self.setPreviewDiff(diff)
         transaction.commit()
 
         def fake_open(*args):
diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
index 28239d7..2cb78bc 100644
--- a/lib/lp/registry/browser/person.py
+++ b/lib/lp/registry/browser/person.py
@@ -4296,9 +4296,6 @@ class ContactViaWebNotificationRecipientSet:
         """See `INotificationRecipientSet`."""
         return len(self) > 0
 
-    if six.PY2:
-        __nonzero__ = __bool__
-
     def getReason(self, person_or_email):
         """See `INotificationRecipientSet`."""
         if person_or_email not in self:
diff --git a/lib/lp/registry/interfaces/sourcepackagename.py b/lib/lp/registry/interfaces/sourcepackagename.py
index 89c0a8c..1d07542 100644
--- a/lib/lp/registry/interfaces/sourcepackagename.py
+++ b/lib/lp/registry/interfaces/sourcepackagename.py
@@ -10,7 +10,6 @@ __all__ = [
     'ISourcePackageNameSet',
     ]
 
-import six
 from zope.interface import (
     Attribute,
     Interface,
@@ -40,10 +39,6 @@ class ISourcePackageName(Interface):
     def __str__():
         """Return the name"""
 
-    if six.PY2:
-        def __unicode__():
-            """Return the name"""
-
 
 class ISourcePackageNameSet(Interface):
     """A set of SourcePackageName."""
diff --git a/lib/lp/registry/model/productrelease.py b/lib/lp/registry/model/productrelease.py
index 7ae8b4b..6dfc8b3 100644
--- a/lib/lp/registry/model/productrelease.py
+++ b/lib/lp/registry/model/productrelease.py
@@ -14,7 +14,6 @@ from io import (
     BytesIO,
     )
 import os
-import sys
 
 from sqlobject import (
     ForeignKey,
@@ -137,10 +136,7 @@ class ProductRelease(SQLBase):
             file_size = len(file_or_data)
             file_obj = BytesIO(file_or_data)
         else:
-            file_types = [BufferedIOBase]
-            if sys.version_info[0] < 3:
-                file_types.append(file)
-            assert isinstance(file_or_data, tuple(file_types)), (
+            assert isinstance(file_or_data, BufferedIOBase), (
                 "file_or_data is not an expected type")
             file_obj = file_or_data
             start = file_obj.tell()
diff --git a/lib/lp/registry/tests/test_nickname.py b/lib/lp/registry/tests/test_nickname.py
index dbe5c45..6ca86bb 100644
--- a/lib/lp/registry/tests/test_nickname.py
+++ b/lib/lp/registry/tests/test_nickname.py
@@ -5,8 +5,6 @@
 
 __metaclass__ = type
 
-import sys
-
 from zope.component import getUtility
 
 from lp.registry.interfaces.person import IPersonSet
@@ -45,26 +43,17 @@ class TestNicknameGeneration(TestCaseWithFactory):
         # adding random suffixes to the required length.
         self.assertIs(None, getUtility(IPersonSet).getByName('i'))
         nick = generate_nick('i@xxxxxxxxxxx')
-        if sys.version_info[0] >= 3:
-            self.assertEqual('i-d', nick)
-        else:
-            self.assertEqual('i-b', nick)
+        self.assertEqual('i-d', nick)
 
     def test_can_create_noncolliding_nicknames(self):
         # Given the same email address, generate_nick doesn't recreate the
         # same nick once that nick is used.
         self.factory.makePerson(name='bar')
         nick = generate_nick('bar@xxxxxxxxxxx')
-        if sys.version_info[0] >= 3:
-            self.assertEqual('bar-1', nick)
-        else:
-            self.assertEqual('bar-3', nick)
+        self.assertEqual('bar-1', nick)
 
         # If we used the previously created nick and get another bar@ email
         # address, another new nick is generated.
         self.factory.makePerson(name=nick)
         nick = generate_nick('bar@xxxxxxxxxxx')
-        if sys.version_info[0] >= 3:
-            self.assertEqual('3-bar', nick)
-        else:
-            self.assertEqual('5-bar', nick)
+        self.assertEqual('3-bar', nick)
diff --git a/lib/lp/registry/tests/test_ssh.py b/lib/lp/registry/tests/test_ssh.py
index 2f3e700..006e185 100644
--- a/lib/lp/registry/tests/test_ssh.py
+++ b/lib/lp/registry/tests/test_ssh.py
@@ -5,8 +5,6 @@
 
 __metaclass__ = type
 
-import sys
-
 from testtools.matchers import StartsWith
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -205,14 +203,10 @@ class TestSSHKeySet(TestCaseWithFactory):
             keyset.new,
             person, 'ssh-rsa badkeytext comment'
         )
-        if sys.version_info[0] >= 3:
-            expected_message = "unknown blob type: b'\\xc7_'"
-        else:
-            expected_message = "unknown blob type: \\xc7_"
         self.assertRaisesWithContent(
             SSHKeyAdditionError,
             "Invalid SSH key data: 'ssh-rsa asdfasdf comment' "
-            "(%s)" % expected_message,
+            "(unknown blob type: b'\\xc7_')",
             keyset.new,
             person, 'ssh-rsa asdfasdf comment'
         )
diff --git a/lib/lp/scripts/utilities/importpedant.py b/lib/lp/scripts/utilities/importpedant.py
index 5bf143e..93e7da9 100644
--- a/lib/lp/scripts/utilities/importpedant.py
+++ b/lib/lp/scripts/utilities/importpedant.py
@@ -7,7 +7,6 @@ from operator import attrgetter
 import types
 import warnings
 
-import six
 from six.moves import builtins
 
 
@@ -174,8 +173,7 @@ class NotFoundPolicyViolation(PedantDisagreesError):
 # The names of the arguments form part of the interface of __import__(...),
 # and must not be changed, as code may choose to invoke __import__ using
 # keyword arguments - e.g. the encodings module in Python 2.6.
-def import_pedant(name, globals={}, locals={}, fromlist=[],
-                  level=(0 if six.PY3 else -1)):
+def import_pedant(name, globals={}, locals={}, fromlist=[], level=0):
     global naughty_imports
 
     module = original_import(name, globals, locals, fromlist, level)
diff --git a/lib/lp/scripts/utilities/test.py b/lib/lp/scripts/utilities/test.py
index 375d70c..00795c2 100755
--- a/lib/lp/scripts/utilities/test.py
+++ b/lib/lp/scripts/utilities/test.py
@@ -91,7 +91,7 @@ def configure_environment():
     # Suppress accessibility warning because the test runner does not have UI.
     os.environ['GTK_MODULES'] = ''
 
-    if six.PY3 and distro.linux_distribution()[:2] == ('Ubuntu', '18.04'):
+    if distro.linux_distribution()[:2] == ('Ubuntu', '18.04'):
         # XXX cjwatson 2020-10-09: Certain versions of Python crash when
         # importing readline into a process that has libedit loaded
         # (https://bugs.python.org/issue38634,
diff --git a/lib/lp/services/compat.py b/lib/lp/services/compat.py
index 4d8ee9d..3673a45 100644
--- a/lib/lp/services/compat.py
+++ b/lib/lp/services/compat.py
@@ -32,19 +32,13 @@ try:
 except ImportError:
     from unittest import mock
 
-import six
-
-
-if six.PY3:
-    def message_as_bytes(message):
-        from email.generator import BytesGenerator
-        from email.policy import compat32
-
-        fp = io.BytesIO()
-        g = BytesGenerator(
-            fp, mangle_from_=False, maxheaderlen=0, policy=compat32)
-        g.flatten(message)
-        return fp.getvalue()
-else:
-    def message_as_bytes(message):
-        return message.as_string()
+
+def message_as_bytes(message):
+    from email.generator import BytesGenerator
+    from email.policy import compat32
+
+    fp = io.BytesIO()
+    g = BytesGenerator(
+        fp, mangle_from_=False, maxheaderlen=0, policy=compat32)
+    g.flatten(message)
+    return fp.getvalue()
diff --git a/lib/lp/services/encoding.py b/lib/lp/services/encoding.py
index 3c1728b..aa5f74e 100644
--- a/lib/lp/services/encoding.py
+++ b/lib/lp/services/encoding.py
@@ -228,7 +228,7 @@ def wsgi_native_string(s):
     Python 2, we enforce this here.
     """
     result = six.ensure_str(s, encoding='ISO-8859-1')
-    if six.PY3 and isinstance(s, six.text_type):
+    if isinstance(s, six.text_type):
         # Ensure we're limited to ISO-8859-1.
         result.encode('ISO-8859-1')
     return result
diff --git a/lib/lp/services/limitedlist.py b/lib/lp/services/limitedlist.py
index 9c9ca8b..1cb0db5 100644
--- a/lib/lp/services/limitedlist.py
+++ b/lib/lp/services/limitedlist.py
@@ -6,8 +6,6 @@ __all__ = [
     'LimitedList',
     ]
 
-import sys
-
 
 class LimitedList(list):
     """A mutable sequence that takes a limited number of elements."""
@@ -64,15 +62,6 @@ class LimitedList(list):
             self._ensureLength()
         return result
 
-    if sys.version_info[0] < 3:
-        # list.__setslice__ exists on Python 2, so we must override it in
-        # this subclass.  (If it didn't exist, as is the case on Python 3,
-        # then __setitem__ above would be good enough.)
-        def __setslice__(self, i, j, sequence):
-            result = super(LimitedList, self).__setslice__(i, j, sequence)
-            self._ensureLength()
-            return result
-
     def append(self, value):
         result = super(LimitedList, self).append(value)
         self._ensureLength()
diff --git a/lib/lp/services/mail/interfaces.py b/lib/lp/services/mail/interfaces.py
index 41818a4..59fdfec 100644
--- a/lib/lp/services/mail/interfaces.py
+++ b/lib/lp/services/mail/interfaces.py
@@ -19,7 +19,6 @@ __all__ = [
     'UnknownRecipientError',
     ]
 
-import six
 from zope.interface import (
     Attribute,
     Interface,
@@ -156,9 +155,6 @@ class INotificationRecipientSet(Interface):
     def __bool__():
         """Return False when the set is empty, True when it's not."""
 
-    if six.PY2:
-        __nonzero__ = __bool__
-
     def getReason(person_or_email):
         """Return a reason tuple containing (text, header) for an address.
 
diff --git a/lib/lp/services/mail/notificationrecipientset.py b/lib/lp/services/mail/notificationrecipientset.py
index 6004399..68f9014 100644
--- a/lib/lp/services/mail/notificationrecipientset.py
+++ b/lib/lp/services/mail/notificationrecipientset.py
@@ -88,9 +88,6 @@ class NotificationRecipientSet:
         """See `INotificationRecipientSet`."""
         return bool(self._personToRationale)
 
-    if six.PY2:
-        __nonzero__ = __bool__
-
     def getReason(self, person_or_email):
         """See `INotificationRecipientSet`."""
         if zope_isinstance(person_or_email, six.string_types):
diff --git a/lib/lp/services/scripts/base.py b/lib/lp/services/scripts/base.py
index cd3f19b..989eab6 100644
--- a/lib/lp/services/scripts/base.py
+++ b/lib/lp/services/scripts/base.py
@@ -468,11 +468,7 @@ def cronscript_enabled(control_url, name, log):
     # traceback and continue on using the defaults.
     try:
         with response:
-            if sys.version_info[:2] >= (3, 2):
-                read_file = cron_config.read_file
-            else:
-                read_file = cron_config.readfp
-            read_file(io.StringIO(response.text))
+            cron_config.read_file(io.StringIO(response.text))
     except Exception:
         log.exception("Error parsing %s", control_url)
 
diff --git a/lib/lp/services/tests/test_encoding.py b/lib/lp/services/tests/test_encoding.py
index 4a0e235..ed44acc 100644
--- a/lib/lp/services/tests/test_encoding.py
+++ b/lib/lp/services/tests/test_encoding.py
@@ -7,8 +7,6 @@ from doctest import (
     )
 import unittest
 
-import six
-
 import lp.services.encoding
 from lp.services.encoding import wsgi_native_string
 from lp.testing import TestCase
@@ -16,22 +14,14 @@ from lp.testing import TestCase
 
 class TestWSGINativeString(TestCase):
 
-    def _toNative(self, s):
-        if six.PY3:
-            return s
-        else:
-            return s.encode('ISO-8859-1')
-
     def test_not_bytes_or_unicode(self):
         self.assertRaises(TypeError, wsgi_native_string, object())
 
     def test_bytes_iso_8859_1(self):
-        self.assertEqual(
-            self._toNative(u'foo\xfe'), wsgi_native_string(b'foo\xfe'))
+        self.assertEqual(u'foo\xfe', wsgi_native_string(b'foo\xfe'))
 
     def test_unicode_iso_8859_1(self):
-        self.assertEqual(
-            self._toNative(u'foo\xfe'), wsgi_native_string(u'foo\xfe'))
+        self.assertEqual(u'foo\xfe', wsgi_native_string(u'foo\xfe'))
 
     def test_unicode_not_iso_8859_1(self):
         self.assertRaises(UnicodeEncodeError, wsgi_native_string, u'foo\u2014')
diff --git a/lib/lp/services/twistedsupport/tests/test_xmlrpc.py b/lib/lp/services/twistedsupport/tests/test_xmlrpc.py
index 126d275..59e45fc 100644
--- a/lib/lp/services/twistedsupport/tests/test_xmlrpc.py
+++ b/lib/lp/services/twistedsupport/tests/test_xmlrpc.py
@@ -5,12 +5,6 @@
 
 __metaclass__ = type
 
-import sys
-
-from testtools.matchers import (
-    LessThan,
-    Not,
-    )
 from twisted.python.failure import Failure
 
 from lp.services.twistedsupport import extract_result
@@ -58,11 +52,7 @@ class TestTrapFault(TestCase):
     def assertRaisesFailure(self, failure, function, *args, **kwargs):
         try:
             function(*args, **kwargs)
-        except Failure as raised_failure:
-            self.assertThat(sys.version_info, LessThan((3, 0)))
-            self.assertEqual(failure, raised_failure)
         except Exception as raised_exception:
-            self.assertThat(sys.version_info, Not(LessThan((3, 0))))
             self.assertEqual(failure.value, raised_exception)
 
     def test_raises_non_faults(self):
diff --git a/lib/lp/services/twistedsupport/xmlrpc.py b/lib/lp/services/twistedsupport/xmlrpc.py
index 3521641..70f5598 100644
--- a/lib/lp/services/twistedsupport/xmlrpc.py
+++ b/lib/lp/services/twistedsupport/xmlrpc.py
@@ -10,8 +10,6 @@ __all__ = [
     'trap_fault',
     ]
 
-import sys
-
 from twisted.internet import defer
 from twisted.web import xmlrpc
 
@@ -68,7 +66,4 @@ def trap_fault(failure, *fault_classes):
     fault = failure.value
     if fault.faultCode in [cls.error_code for cls in fault_classes]:
         return fault
-    if sys.version_info >= (3, 0):
-        failure.raiseException()
-    else:
-        raise failure
+    failure.raiseException()
diff --git a/lib/lp/services/webapp/pgsession.py b/lib/lp/services/webapp/pgsession.py
index 0a5c664..de05da4 100644
--- a/lib/lp/services/webapp/pgsession.py
+++ b/lib/lp/services/webapp/pgsession.py
@@ -32,33 +32,30 @@ HOURS = 60 * MINUTES
 DAYS = 24 * HOURS
 
 
-if six.PY3:
-    class Python2FriendlyUnpickler(pickle._Unpickler):
-        """An unpickler that handles Python 2 datetime objects.
-
-        Python 3 versions before 3.6 fail to unpickle Python 2 datetime
-        objects (https://bugs.python.org/issue22005); even in Python >= 3.6
-        they require passing a different encoding to pickle.loads, which may
-        have undesirable effects on other objects being unpickled.  Work
-        around this by instead patching in a different encoding just for the
-        argument to datetime.datetime.
-        """
+class Python2FriendlyUnpickler(pickle._Unpickler):
+    """An unpickler that handles Python 2 datetime objects.
+
+    Python 3 versions before 3.6 fail to unpickle Python 2 datetime objects
+    (https://bugs.python.org/issue22005); even in Python >= 3.6 they require
+    passing a different encoding to pickle.loads, which may have undesirable
+    effects on other objects being unpickled.  Work around this by instead
+    patching in a different encoding just for the argument to
+    datetime.datetime.
+    """
 
-        def find_class(self, module, name):
-            if module == 'datetime' and name == 'datetime':
-                original_encoding = self.encoding
-                self.encoding = 'bytes'
+    def find_class(self, module, name):
+        if module == 'datetime' and name == 'datetime':
+            original_encoding = self.encoding
+            self.encoding = 'bytes'
 
-                def datetime_factory(pickle_data):
-                    self.encoding = original_encoding
-                    return datetime(pickle_data)
+            def datetime_factory(pickle_data):
+                self.encoding = original_encoding
+                return datetime(pickle_data)
 
-                return datetime_factory
-            else:
-                return super(Python2FriendlyUnpickler, self).find_class(
-                    module, name)
-else:
-    Python2FriendlyUnpickler = pickle.Unpickler
+            return datetime_factory
+        else:
+            return super(Python2FriendlyUnpickler, self).find_class(
+                module, name)
 
 
 class PGSessionBase:
diff --git a/lib/lp/services/webapp/publisher.py b/lib/lp/services/webapp/publisher.py
index 168b22e..f4d42cd 100644
--- a/lib/lp/services/webapp/publisher.py
+++ b/lib/lp/services/webapp/publisher.py
@@ -632,11 +632,6 @@ class CanonicalAbsoluteURL:
         self.context = context
         self.request = request
 
-    if six.PY2:
-        def __unicode__(self):
-            """Returns the URL as a unicode string."""
-            raise NotImplementedError()
-
     def __str__(self):
         """Returns an ASCII string with all unicode characters url quoted."""
         return canonical_url(self.context, self.request)
diff --git a/lib/lp/services/webapp/servers.py b/lib/lp/services/webapp/servers.py
index d637916..a2ef356 100644
--- a/lib/lp/services/webapp/servers.py
+++ b/lib/lp/services/webapp/servers.py
@@ -211,9 +211,6 @@ class StepsToGo(six.Iterator):
     def __bool__(self):
         return bool(self._stack)
 
-    if six.PY2:
-        __nonzero__ = __bool__
-
 
 class ApplicationServerSettingRequestFactory:
     """Create a request and call its setApplicationServer method.
@@ -527,20 +524,9 @@ def get_query_string_params(request):
     if query_string is None:
         query_string = ''
 
-    kwargs = {}
-    if not six.PY2:
-        kwargs['encoding'] = 'UTF-8'
-        kwargs['errors'] = 'replace'
-    parsed_qs = parse_qs(query_string, keep_blank_values=True, **kwargs)
-    if six.PY2:
-        decoded_qs = {}
-        for key, values in six.iteritems(parsed_qs):
-            decoded_qs[key] = [
-                (value.decode('UTF-8', 'replace') if isinstance(value, bytes)
-                 else value)
-                for value in values]
-        parsed_qs = decoded_qs
-    return parsed_qs
+    return parse_qs(
+        query_string, keep_blank_values=True,
+        encoding='UTF-8', errors='replace')
 
 
 class LaunchpadBrowserRequestMixin:
diff --git a/lib/lp/services/webapp/tests/test_servers.py b/lib/lp/services/webapp/tests/test_servers.py
index ed66bfa..1b09775 100644
--- a/lib/lp/services/webapp/tests/test_servers.py
+++ b/lib/lp/services/webapp/tests/test_servers.py
@@ -21,7 +21,6 @@ from lazr.restful.testing.webservice import (
     IGenericEntry,
     WebServiceTestCase,
     )
-import six
 from talisker.context import Context
 from talisker.logs import logging_context
 from zope.component import (
@@ -441,15 +440,12 @@ class TestBasicLaunchpadRequest(TestCase):
     def test_request_with_invalid_query_string_recovers(self):
         # When the query string has invalid utf-8, it is decoded with
         # replacement.
-        if six.PY2:
-            env = {'QUERY_STRING': 'field.title=subproc\xe9s '}
-        else:
-            # PEP 3333 requires environment variables to be native strings,
-            # so we can't actually get a bytes object in here on Python 3
-            # (both because the WSGI runner will never put it there, and
-            # because parse_qs would crash if we did).  Test the next best
-            # thing, namely percent-encoded invalid UTF-8.
-            env = {'QUERY_STRING': 'field.title=subproc%E9s '}
+        # PEP 3333 requires environment variables to be native strings, so
+        # we can't actually get a bytes object in here on Python 3 (both
+        # because the WSGI runner will never put it there, and because
+        # parse_qs would crash if we did).  Test the next best thing, namely
+        # percent-encoded invalid UTF-8.
+        env = {'QUERY_STRING': 'field.title=subproc%E9s '}
         request = LaunchpadBrowserRequest(io.BytesIO(b''), env)
         self.assertEqual(
             [u'subproc\ufffds '], request.query_string_params['field.title'])
diff --git a/lib/lp/services/webapp/url.py b/lib/lp/services/webapp/url.py
index e438fd9..3a05c33 100644
--- a/lib/lp/services/webapp/url.py
+++ b/lib/lp/services/webapp/url.py
@@ -6,7 +6,6 @@
 __metaclass__ = type
 __all__ = ['urlappend', 'urlparse', 'urlsplit']
 
-import six
 import six.moves.urllib.parse as urlparse_module
 from six.moves.urllib.parse import (
     urljoin,
@@ -81,9 +80,7 @@ def urlappend(baseurl, path):
 
 def _ensure_ascii_str(url):
     """Ensure that `url` only contains ASCII, and convert it to a `str`."""
-    if six.PY2:
-        url = url.encode('ascii')
-    elif isinstance(url, bytes):
+    if isinstance(url, bytes):
         url = url.decode('ascii')
     else:
         # Ignore the result; just check that `url` is pure ASCII.
diff --git a/lib/lp/services/webhooks/payload.py b/lib/lp/services/webhooks/payload.py
index f7c7e3d..d73cd1b 100644
--- a/lib/lp/services/webhooks/payload.py
+++ b/lib/lp/services/webhooks/payload.py
@@ -12,7 +12,6 @@ __all__ = [
 from io import BytesIO
 
 from lazr.restful.interfaces import IFieldMarshaller
-import six
 from zope.component import getMultiAdapter
 from zope.interface import implementer
 from zope.traversing.browser.interfaces import IAbsoluteURL
@@ -43,11 +42,6 @@ class WebhookAbsoluteURL:
         self.context = context
         self.request = request
 
-    if six.PY2:
-        def __unicode__(self):
-            """Returns the URL as a unicode string."""
-            raise NotImplementedError()
-
     def __str__(self):
         """Returns an ASCII string with all unicode characters url quoted."""
         return canonical_url(self.context, force_local_path=True)
diff --git a/lib/lp/soyuz/adapters/archivesourcepublication.py b/lib/lp/soyuz/adapters/archivesourcepublication.py
index 34e4919..f04807d 100644
--- a/lib/lp/soyuz/adapters/archivesourcepublication.py
+++ b/lib/lp/soyuz/adapters/archivesourcepublication.py
@@ -17,7 +17,6 @@ __all__ = [
 from collections import defaultdict
 
 from lazr.delegates import delegate_to
-import six
 from zope.component import getUtility
 
 from lp.registry.model.distroseries import DistroSeries
@@ -104,9 +103,6 @@ class ArchiveSourcePublications:
         """Are there any sources to iterate?"""
         return self.has_sources
 
-    if six.PY2:
-        __nonzero__ = __bool__
-
     def __iter__(self):
         """`ArchiveSourcePublication` iterator."""
         results = []
diff --git a/lib/lp/soyuz/interfaces/binarypackagename.py b/lib/lp/soyuz/interfaces/binarypackagename.py
index bfef027..69973c6 100644
--- a/lib/lp/soyuz/interfaces/binarypackagename.py
+++ b/lib/lp/soyuz/interfaces/binarypackagename.py
@@ -11,7 +11,6 @@ __all__ = [
     'IBinaryPackageNameSet',
     ]
 
-import six
 from zope.interface import Interface
 from zope.schema import (
     Int,
@@ -31,10 +30,6 @@ class IBinaryPackageName(Interface):
     def __str__():
         """Return the name"""
 
-    if six.PY2:
-        def __unicode__():
-            """Return the name"""
-
 
 class IBinaryPackageNameSet(Interface):
 
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 32c9923..084cee1 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -38,7 +38,6 @@ from itertools import count
 import os
 import sys
 from textwrap import dedent
-import types
 import uuid
 import warnings
 
@@ -5210,7 +5209,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
 # Some factory methods return simple Python types. We don't add
 # security wrappers for them, as well as for objects created by
 # other Python libraries.
-unwrapped_types = {
+unwrapped_types = frozenset({
     BaseRecipeBranch,
     DSCFile,
     Message,
@@ -5218,10 +5217,7 @@ unwrapped_types = {
     int,
     str,
     six.text_type,
-    }
-if sys.version_info[0] < 3:
-    unwrapped_types.add(types.InstanceType)
-unwrapped_types = frozenset(unwrapped_types)
+    })
 
 
 def is_security_proxied_or_harmless(obj):
diff --git a/lib/lp/testing/systemdocs.py b/lib/lp/testing/systemdocs.py
index 5e9bdac..29bb200 100644
--- a/lib/lp/testing/systemdocs.py
+++ b/lib/lp/testing/systemdocs.py
@@ -232,19 +232,16 @@ class PrettyPrinter(pprint.PrettyPrinter, object):
                 return '"%s"' % obj, True, False
             else:
                 return "'%s'" % obj.replace("'", "\\'"), True, False
-        elif sys.version_info[0] < 3 and isinstance(obj, long):
-            return repr(int(obj)), True, False
         else:
             return super(PrettyPrinter, self).format(
                 obj, contexts, maxlevels, level)
 
     # Disable wrapping of long strings on Python >= 3.5, which is unhelpful
     # in doctests.  There seems to be no reasonable public API for this.
-    if sys.version_info[:2] >= (3, 5):
-        _dispatch = dict(pprint.PrettyPrinter._dispatch)
-        del _dispatch[six.text_type.__repr__]
-        del _dispatch[bytes.__repr__]
-        del _dispatch[bytearray.__repr__]
+    _dispatch = dict(pprint.PrettyPrinter._dispatch)
+    del _dispatch[six.text_type.__repr__]
+    del _dispatch[bytes.__repr__]
+    del _dispatch[bytearray.__repr__]
 
 
 # XXX cjwatson 2018-05-13: Once all doctests are made safe for the standard
diff --git a/lib/lp/translations/doc/poexport-queue.txt b/lib/lp/translations/doc/poexport-queue.txt
index 7854639..562eb08 100644
--- a/lib/lp/translations/doc/poexport-queue.txt
+++ b/lib/lp/translations/doc/poexport-queue.txt
@@ -139,14 +139,12 @@ It's not clear that it's possible to trigger this failure mode normally on
 Python 3 at all, because bytes will just be formatted as b'...'.  For now,
 inject a mock exception in that case so that the test can pass.
 
-    >>> if six.PY3:
-    ...     from lp.services.compat import mock
-    ...     patcher = mock.patch.object(result, 'failure')
-    ...     mock_failure = patcher.start()
-    ...     mock_failure.__str__.side_effect = lambda: b'\xc3'.decode('UTF-8')
+    >>> from lp.services.compat import mock
+    >>> patcher = mock.patch.object(result, 'failure')
+    >>> mock_failure = patcher.start()
+    >>> mock_failure.__str__.side_effect = lambda: b'\xc3'.decode('UTF-8')
     >>> result.notify()
-    >>> if six.PY3:
-    ...     patcher.stop()
+    >>> patcher.stop()
 
     >>> test_emails = pop_notifications()
     >>> len(test_emails)
diff --git a/lib/lp/translations/model/translatedlanguage.py b/lib/lp/translations/model/translatedlanguage.py
index e9d120a..7c7606c 100644
--- a/lib/lp/translations/model/translatedlanguage.py
+++ b/lib/lp/translations/model/translatedlanguage.py
@@ -4,7 +4,6 @@
 __all__ = ['TranslatedLanguageMixin']
 
 import pytz
-import six
 from storm.expr import (
     Coalesce,
     Desc,
@@ -75,9 +74,6 @@ class POFilesByPOTemplates(object):
     def __bool__(self):
         return bool(self.templates_collection.select(POTemplate).any())
 
-    if six.PY2:
-        __nonzero__ = __bool__
-
 
 @implementer(ITranslatedLanguage)
 class TranslatedLanguageMixin(object):
diff --git a/lib/sqlobject/__init__.py b/lib/sqlobject/__init__.py
index 5450477..bc94046 100644
--- a/lib/sqlobject/__init__.py
+++ b/lib/sqlobject/__init__.py
@@ -51,8 +51,6 @@ def sqlrepr(value, dbname=None):
             return "'f'"
     elif isinstance(value, int):
         return repr(int(value))
-    elif six.PY2 and isinstance(value, long):
-        return str(value)
     elif isinstance(value, float):
         return repr(value)
     elif value is None:
diff --git a/scripts/process-one-mail.py b/scripts/process-one-mail.py
index 0fa7484..3cb8598 100755
--- a/scripts/process-one-mail.py
+++ b/scripts/process-one-mail.py
@@ -31,10 +31,8 @@ class ProcessMail(LaunchpadScript):
         # with handling a mailbox, which we're avoiding here.
         if len(self.args) >= 1:
             from_file = open(self.args[0], 'rb')
-        elif sys.version_info[0] >= 3:
-            from_file = sys.stdin.buffer
         else:
-            from_file = sys.stdin
+            from_file = sys.stdin.buffer
         self.logger.debug("reading message from %r" % (from_file,))
         raw_mail = from_file.read()
         if from_file != sys.stdin: