From 27d18a156ea94fedb6556811b39dd6e9e349476e Mon Sep 17 00:00:00 2001
From: Colin Watson <cjwatson@ubuntu.com>
Date: Sun, 3 Mar 2024 16:07:27 +0000
Subject: [PATCH] Remove support for Python 2

I noticed that a number of the scripts in `contrib/` were Python-2-only,
so I did a basic untested port of those while I was here.

Apply pyupgrade --py3-plus

Simplify coverage testing

We no longer need the more complex arrangements after dropping Python 2
support.
---
 NEWS.rst                                         |    4 +
 pyproject.toml                                   |    2 
 setup.py                                         |    4 -
 src/launchpadlib/apps.py                         |    2 
 src/launchpadlib/credentials.py                  |   63 ++++++-----------------
 src/launchpadlib/docs/conf.py                    |   16 ++---
 src/launchpadlib/launchpad.py                    |   15 +----
 src/launchpadlib/testing/helpers.py              |    6 --
 src/launchpadlib/testing/launchpad.py            |   22 ++------
 src/launchpadlib/testing/tests/test_launchpad.py |    4 -
 src/launchpadlib/tests/test_credential_store.py  |    8 --
 src/launchpadlib/tests/test_http.py              |    9 ---
 src/launchpadlib/tests/test_launchpad.py         |   12 +---
 src/launchpadlib/uris.py                         |    7 --
 14 files changed, 56 insertions(+), 118 deletions(-)

--- a/NEWS.rst
+++ b/NEWS.rst
@@ -2,6 +2,10 @@
 NEWS for launchpadlib
 =====================
 
+2.0.0
+=====
+- Remove support for Python 2.
+
 1.11.0 (2023-01-09)
 ===================
 - Move the ``keyring`` dependency to a new ``keyring`` extra.
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,3 @@
 [tool.black]
 line-length = 79
-target-version = ['py27']
+target-version = ['py35']
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # Copyright 2008-2022 Canonical Ltd.
 #
@@ -46,7 +46,6 @@ install_requires = [
     'importlib-metadata; python_version < "3.8"',
     "lazr.restfulclient>=0.14.2",
     "lazr.uri",
-    "six",
 ]
 
 setup(
@@ -64,6 +63,7 @@ setup(
     description=open("README.rst").readline().strip(),
     long_description=generate("src/launchpadlib/docs/index.rst", "NEWS.rst"),
     license="LGPL v3",
+    python_requires=">=3.5",
     install_requires=install_requires,
     url="https://help.launchpad.net/API/launchpadlib",
     project_urls={
--- a/src/launchpadlib/apps.py
+++ b/src/launchpadlib/apps.py
@@ -30,7 +30,7 @@ from launchpadlib.credentials import Cre
 from launchpadlib.uris import lookup_web_root
 
 
-class RequestTokenApp(object):
+class RequestTokenApp:
     """An application that creates request tokens."""
 
     def __init__(self, web_root, consumer_name, context):
--- a/src/launchpadlib/credentials.py
+++ b/src/launchpadlib/credentials.py
@@ -14,11 +14,8 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
 
-from __future__ import print_function
-
 """launchpadlib credentials and authentication support."""
 
-__metaclass__ = type
 __all__ = [
     "AccessToken",
     "AnonymousAccessToken",
@@ -29,40 +26,20 @@ __all__ = [
     "Credentials",
 ]
 
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from io import StringIO
-
+from base64 import (
+    b64decode,
+    b64encode,
+)
 import httplib2
+from io import StringIO
 import json
 import os
 from select import select
 import stat
 from sys import stdin
 import time
-
-try:
-    from urllib.parse import urlencode
-except ImportError:
-    from urllib import urlencode
-try:
-    from urllib.parse import urljoin
-except ImportError:
-    from urlparse import urljoin
+from urllib.parse import urlencode, urljoin, parse_qs
 import webbrowser
-from base64 import (
-    b64decode,
-    b64encode,
-)
-
-from six.moves.urllib.parse import parse_qs
-
-if bytes is str:
-    # Python 2
-    unicode_type = unicode  # noqa: F821
-else:
-    unicode_type = str
 
 from lazr.restfulclient.errors import HTTPError
 from lazr.restfulclient.authorize.oauth import (
@@ -135,7 +112,7 @@ class Credentials(OAuthAuthorizer):
         sio = StringIO()
         self.save(sio)
         serialized = sio.getvalue()
-        if isinstance(serialized, unicode_type):
+        if isinstance(serialized, str):
             serialized = serialized.encode("utf-8")
         return serialized
 
@@ -146,7 +123,7 @@ class Credentials(OAuthAuthorizer):
         This should probably be moved into OAuthAuthorizer.
         """
         credentials = cls()
-        if not isinstance(value, unicode_type):
+        if not isinstance(value, str):
             value = value.decode("utf-8")
         credentials.load(StringIO(value))
         return credentials
@@ -255,7 +232,7 @@ class AccessToken(_AccessToken):
     @classmethod
     def from_string(cls, query_string):
         """Create and return a new `AccessToken` from the given string."""
-        if not isinstance(query_string, unicode_type):
+        if not isinstance(query_string, str):
             query_string = query_string.decode("utf-8")
         params = parse_qs(query_string, keep_blank_values=False)
         key = params["oauth_token"]
@@ -280,10 +257,10 @@ class AnonymousAccessToken(_AccessToken)
     """
 
     def __init__(self):
-        super(AnonymousAccessToken, self).__init__("", "")
+        super().__init__("", "")
 
 
-class CredentialStore(object):
+class CredentialStore:
     """Store OAuth credentials locally.
 
     This is a generic superclass. To implement a specific way of
@@ -369,7 +346,7 @@ class KeyringCredentialStore(CredentialS
     B64MARKER = b"<B64>"
 
     def __init__(self, credential_save_failed=None, fallback=False):
-        super(KeyringCredentialStore, self).__init__(credential_save_failed)
+        super().__init__(credential_save_failed)
         self._fallback = None
         if fallback:
             self._fallback = MemoryCredentialStore(credential_save_failed)
@@ -438,7 +415,7 @@ class KeyringCredentialStore(CredentialS
             else:
                 raise
         if credential_string is not None:
-            if isinstance(credential_string, unicode_type):
+            if isinstance(credential_string, str):
                 credential_string = credential_string.encode("utf8")
             if credential_string.startswith(self.B64MARKER):
                 try:
@@ -468,9 +445,7 @@ class UnencryptedFileCredentialStore(Cre
     """
 
     def __init__(self, filename, credential_save_failed=None):
-        super(UnencryptedFileCredentialStore, self).__init__(
-            credential_save_failed
-        )
+        super().__init__(credential_save_failed)
         self.filename = filename
 
     def do_save(self, credentials, unique_key):
@@ -495,7 +470,7 @@ class MemoryCredentialStore(CredentialSt
     """
 
     def __init__(self, credential_save_failed=None):
-        super(MemoryCredentialStore, self).__init__(credential_save_failed)
+        super().__init__(credential_save_failed)
         self._credentials = {}
 
     def do_save(self, credentials, unique_key):
@@ -507,7 +482,7 @@ class MemoryCredentialStore(CredentialSt
         return self._credentials.get(unique_key)
 
 
-class RequestTokenAuthorizationEngine(object):
+class RequestTokenAuthorizationEngine:
     """The superclass of all request token authorizers.
 
     This base class does not implement request token authorization,
@@ -774,15 +749,13 @@ class AuthorizeRequestTokenWithBrowser(A
         # It doesn't look like we're doing anything here, but we
         # are discarding the passed-in values for consumer_name and
         # allow_access_levels.
-        super(AuthorizeRequestTokenWithBrowser, self).__init__(
+        super().__init__(
             service_root, application_name, None, credential_save_failed
         )
 
     def notify_end_user_authorization_url(self, authorization_url):
         """Notify the end-user of the URL."""
-        super(
-            AuthorizeRequestTokenWithBrowser, self
-        ).notify_end_user_authorization_url(authorization_url)
+        super().notify_end_user_authorization_url(authorization_url)
 
         try:
             browser_obj = webbrowser.get()
--- a/src/launchpadlib/docs/conf.py
+++ b/src/launchpadlib/docs/conf.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # launchpadlib documentation build configuration file, created by
 # sphinx-quickstart on Tue Nov  5 23:48:15 2019.
 #
@@ -47,9 +45,9 @@ source_suffix = ".rst"
 master_doc = "index"
 
 # General information about the project.
-project = u"launchpadlib"
-copyright = u"2008-2019, Canonical Ltd."
-author = u"LAZR Developers <lazr-developers@lists.launchpad.net>"
+project = "launchpadlib"
+copyright = "2008-2019, Canonical Ltd."
+author = "LAZR Developers <lazr-developers@lists.launchpad.net>"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -140,8 +138,8 @@ latex_documents = [
     (
         master_doc,
         "launchpadlib.tex",
-        u"launchpadlib Documentation",
-        u"LAZR Developers \\textless{}lazr-developers@lists.launchpad.net\\textgreater{}",  # noqa: E501
+        "launchpadlib Documentation",
+        "LAZR Developers \\textless{}lazr-developers@lists.launchpad.net\\textgreater{}",  # noqa: E501
         "manual",
     ),
 ]
@@ -152,7 +150,7 @@ latex_documents = [
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    (master_doc, "launchpadlib", u"launchpadlib Documentation", [author], 1)
+    (master_doc, "launchpadlib", "launchpadlib Documentation", [author], 1)
 ]
 
 
@@ -165,7 +163,7 @@ texinfo_documents = [
     (
         master_doc,
         "launchpadlib",
-        u"launchpadlib Documentation",
+        "launchpadlib Documentation",
         author,
         "launchpadlib",
         "One line description of project.",
--- a/src/launchpadlib/launchpad.py
+++ b/src/launchpadlib/launchpad.py
@@ -16,18 +16,13 @@
 
 """Root Launchpad API class."""
 
-__metaclass__ = type
 __all__ = [
     "Launchpad",
 ]
 
 import errno
 import os
-
-try:
-    from urllib.parse import urlsplit
-except ImportError:
-    from urlparse import urlsplit
+from urllib.parse import urlsplit
 import warnings
 
 try:
@@ -130,7 +125,7 @@ class LaunchpadOAuthAwareHttp(RestfulHtt
     def __init__(self, launchpad, authorization_engine, *args):
         self.launchpad = launchpad
         self.authorization_engine = authorization_engine
-        super(LaunchpadOAuthAwareHttp, self).__init__(*args)
+        super().__init__(*args)
 
     def _bad_oauth_token(self, response, content):
         """Helper method to detect an error caused by a bad OAuth token."""
@@ -141,9 +136,7 @@ class LaunchpadOAuthAwareHttp(RestfulHtt
         )
 
     def _request(self, *args):
-        response, content = super(LaunchpadOAuthAwareHttp, self)._request(
-            *args
-        )
+        response, content = super()._request(*args)
         return self.retry_on_bad_token(response, content, *args)
 
     def retry_on_bad_token(self, response, content, *args):
@@ -227,7 +220,7 @@ class Launchpad(ServiceRoot):
         # case we need to authorize a new token during use.
         self.authorization_engine = authorization_engine
 
-        super(Launchpad, self).__init__(
+        super().__init__(
             credentials, service_root, cache, timeout, proxy_info, version
         )
 
--- a/src/launchpadlib/testing/helpers.py
+++ b/src/launchpadlib/testing/helpers.py
@@ -18,8 +18,6 @@
 
 """launchpadlib testing helpers."""
 
-
-__metaclass__ = type
 __all__ = [
     "BadSaveKeyring",
     "fake_keyring",
@@ -64,7 +62,7 @@ class NoNetworkAuthorizationEngine(Reque
     ACCESS_TOKEN_KEY = "access_key:84"
 
     def __init__(self, *args, **kwargs):
-        super(NoNetworkAuthorizationEngine, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         # Set up some instrumentation.
         self.request_tokens_obtained = 0
         self.access_tokens_obtained = 0
@@ -144,7 +142,7 @@ class TestableLaunchpad(Launchpad):
             generally pass in fully-formed Credentials objects.
         :param service_root: Defaults to 'test_dev'.
         """
-        super(TestableLaunchpad, self).__init__(
+        super().__init__(
             credentials,
             authorization_engine,
             credential_store,
--- a/src/launchpadlib/testing/launchpad.py
+++ b/src/launchpadlib/testing/launchpad.py
@@ -65,23 +65,15 @@ Where 'https://api.launchpad.net/devel/'
 also in the WADL file itelf.
 """
 
+from collections.abc import Callable
 from datetime import datetime
 
-try:
-    from collections.abc import Callable
-except ImportError:
-    from collections import Callable
-import sys
-
-if sys.version_info[0] >= 3:
-    basestring = str
-
 
 class IntegrityError(Exception):
     """Raised when bad sample data is used with a L{FakeLaunchpad} instance."""
 
 
-class FakeLaunchpad(object):
+class FakeLaunchpad:
     """A fake Launchpad API class for unit tests that depend on L{Launchpad}.
 
     @param application: A C{wadllib.application.Application} instance for a
@@ -188,7 +180,7 @@ def wadl_tag(tag_name):
     return "{http://research.sun.com/wadl/2006/10}" + tag_name
 
 
-class FakeResource(object):
+class FakeResource:
     """
     Represents valid sample data on L{FakeLaunchpad} instances.
 
@@ -434,7 +426,7 @@ class FakeResource(object):
             if param is None:
                 raise IntegrityError("%s not found" % name)
             if param.type is None:
-                if not isinstance(value, basestring):
+                if not isinstance(value, str):
                     raise IntegrityError(
                         "%s is not a str or unicode for %s" % (value, name)
                     )
@@ -594,7 +586,7 @@ class FakeRoot(FakeResource):
         resource_type = application.get_resource_type(
             application.markup_url + "#service-root"
         )
-        super(FakeRoot, self).__init__(application, resource_type)
+        super().__init__(application, resource_type)
 
 
 class FakeEntry(FakeResource):
@@ -612,9 +604,7 @@ class FakeCollection(FakeResource):
         name=None,
         child_resource_type=None,
     ):
-        super(FakeCollection, self).__init__(
-            application, resource_type, values
-        )
+        super().__init__(application, resource_type, values)
         self.__dict__.update(
             {"_name": name, "_child_resource_type": child_resource_type}
         )
--- a/src/launchpadlib/testing/tests/test_launchpad.py
+++ b/src/launchpadlib/testing/tests/test_launchpad.py
@@ -160,8 +160,8 @@ class FakeLaunchpadTest(ResourcedTestCas
         dicts that represent objects.  Plain string values can be represented
         as C{unicode} strings.
         """
-        self.launchpad.me = dict(name=u"foo")
-        self.assertEqual(u"foo", self.launchpad.me.name)
+        self.launchpad.me = dict(name="foo")
+        self.assertEqual("foo", self.launchpad.me.name)
 
     def test_datetime_property(self):
         """
--- a/src/launchpadlib/tests/test_credential_store.py
+++ b/src/launchpadlib/tests/test_credential_store.py
@@ -169,9 +169,7 @@ class TestKeyringCredentialStore(Credent
         # handled correctly.  (See bug lp:877374)
         class UnicodeInMemoryKeyring(InMemoryKeyring):
             def get_password(self, service, username):
-                password = super(UnicodeInMemoryKeyring, self).get_password(
-                    service, username
-                )
+                password = super().get_password(service, username)
                 if isinstance(password, unicode_type):
                     password = password.encode("utf-8")
                 return password
@@ -194,9 +192,7 @@ class TestKeyringCredentialStore(Credent
 
         class UnencodedInMemoryKeyring(InMemoryKeyring):
             def get_password(self, service, username):
-                pw = super(UnencodedInMemoryKeyring, self).get_password(
-                    service, username
-                )
+                pw = super().get_password(service, username)
                 return b64decode(pw[5:])
 
         self.keyring = UnencodedInMemoryKeyring()
--- a/src/launchpadlib/tests/test_http.py
+++ b/src/launchpadlib/tests/test_http.py
@@ -17,15 +17,10 @@
 """Tests for the LaunchpadOAuthAwareHTTP class."""
 
 from collections import deque
-from json import dumps
+from json import dumps, JSONDecodeError
 import tempfile
 import unittest
 
-try:
-    from json import JSONDecodeError
-except ImportError:
-    JSONDecodeError = ValueError
-
 from launchpadlib.errors import Unauthorized
 from launchpadlib.credentials import UnencryptedFileCredentialStore
 from launchpadlib.launchpad import (
@@ -75,7 +70,7 @@ class SimulatedResponsesHttp(LaunchpadOA
         :param responses: A list of HttpResponse objects to use
             in response to requests.
         """
-        super(SimulatedResponsesHttp, self).__init__(*args)
+        super().__init__(*args)
         self.sent_responses = []
         self.unsent_responses = responses
         self.cache = None
--- a/src/launchpadlib/tests/test_launchpad.py
+++ b/src/launchpadlib/tests/test_launchpad.py
@@ -16,8 +16,6 @@
 
 """Tests for the Launchpad class."""
 
-__metaclass__ = type
-
 from contextlib import contextmanager
 import os
 import shutil
@@ -25,11 +23,7 @@ import socket
 import stat
 import tempfile
 import unittest
-
-try:
-    from unittest.mock import patch
-except ImportError:
-    from mock import patch
+from unittest.mock import patch
 import warnings
 
 from lazr.restfulclient.resource import ServiceRoot
@@ -351,11 +345,11 @@ class TestLaunchpadLoginWith(KeyringTest
     """Tests for Launchpad.login_with()."""
 
     def setUp(self):
-        super(TestLaunchpadLoginWith, self).setUp()
+        super().setUp()
         self.temp_dir = tempfile.mkdtemp()
 
     def tearDown(self):
-        super(TestLaunchpadLoginWith, self).tearDown()
+        super().tearDown()
         shutil.rmtree(self.temp_dir)
 
     def test_dirs_created(self):
--- a/src/launchpadlib/uris.py
+++ b/src/launchpadlib/uris.py
@@ -20,18 +20,15 @@ The code in this module lets users say "
 "https://api.staging.launchpad.net/".
 """
 
-__metaclass__ = type
 __all__ = [
     "lookup_service_root",
     "lookup_web_root",
     "web_root_for_service_root",
 ]
-try:
-    from urllib.parse import urlparse
-except ImportError:
-    from urlparse import urlparse
 
+from urllib.parse import urlparse
 import warnings
+
 from lazr.uri import URI
 
 LPNET_SERVICE_ROOT = "https://api.launchpad.net/"