Merge ~cjwatson/launchpadlib:remove-py2 into launchpadlib:main

Proposed by Colin Watson
Status: Needs review
Proposed branch: ~cjwatson/launchpadlib:remove-py2
Merge into: launchpadlib:main
Diff against target: 1032 lines (+125/-199)
26 files modified
.pre-commit-config.yaml (+4/-5)
NEWS.rst (+4/-0)
contrib/_pythonpath.py (+0/-2)
contrib/close-my-bugs.py (+7/-7)
contrib/commercial-member-api.py (+10/-10)
contrib/delete_bugtasks.py (+4/-6)
contrib/lp-bug-ifier.py (+3/-3)
contrib/lpapi.py (+11/-12)
contrib/nopriv-api.py (+6/-6)
contrib/sample-person-api.py (+6/-6)
contrib/upload_release_tarball.py (+12/-12)
pyproject.toml (+1/-1)
setup.py (+2/-3)
src/launchpadlib/apps.py (+1/-1)
src/launchpadlib/bin/launchpad-request-token (+1/-5)
src/launchpadlib/credentials.py (+18/-45)
src/launchpadlib/docs/conf.py (+7/-9)
src/launchpadlib/launchpad.py (+4/-11)
src/launchpadlib/testing/helpers.py (+2/-4)
src/launchpadlib/testing/launchpad.py (+6/-16)
src/launchpadlib/testing/tests/test_launchpad.py (+2/-2)
src/launchpadlib/tests/test_credential_store.py (+2/-6)
src/launchpadlib/tests/test_http.py (+2/-7)
src/launchpadlib/tests/test_launchpad.py (+3/-9)
src/launchpadlib/uris.py (+2/-5)
tox.ini (+5/-6)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Review via email: mp+461678@code.launchpad.net

Commit message

Remove support for Python 2

Description of the change

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.

I also took advantage of the opportunity to simplify coverage testing.

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

Hi Colin, thank you for your contribution!

The changes look good to me with a couple of comments. 👍

review: Approve
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Matěj Cepl (mcepl) wrote :

Two more dependencies, which can be removed.

Revision history for this message
Jürgen Gmach (jugmac00) wrote :

I'd prefer to keep using pytest.

~cjwatson/launchpadlib:remove-py2 updated
49f266b... by Colin Watson

Remove mock dependency

Revision history for this message
Colin Watson (cjwatson) :

Unmerged commits

49f266b... by Colin Watson

Remove mock dependency

Succeeded
[SUCCEEDED] lint:0 (build)
[SUCCEEDED] tests:0 (build)
12 of 2 results
bcd20d9... by Colin Watson

Simplify coverage testing

We no longer need the more complex arrangements after dropping Python 2
support.

Succeeded
[SUCCEEDED] lint:0 (build)
[SUCCEEDED] tests:0 (build)
12 of 2 results
0b5b426... by Colin Watson

Apply pyupgrade --py3-plus

f22d37e... by Colin Watson

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.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ec0cbbd..e9cff43 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -12,19 +12,18 @@ repos:
12 - id: check-yaml12 - id: check-yaml
13 - id: debug-statements13 - id: debug-statements
14- repo: https://github.com/PyCQA/flake814- repo: https://github.com/PyCQA/flake8
15 rev: 5.0.415 rev: 7.0.0
16 hooks:16 hooks:
17 - id: flake817 - id: flake8
18- repo: https://github.com/asottile/pyupgrade18- repo: https://github.com/asottile/pyupgrade
19 rev: v2.38.4 # v3 drops Python 2 support19 rev: v3.15.1
20 hooks:20 hooks:
21 - id: pyupgrade21 - id: pyupgrade
22 args: [--keep-percent-format]22 args: [--keep-percent-format, --py3-plus]
23- repo: https://github.com/psf/black23- repo: https://github.com/psf/black
24 rev: 21.12b0 # v22 drops Python 2 support24 rev: 24.2.0
25 hooks:25 hooks:
26 - id: black26 - id: black
27 additional_dependencies: ['click<8.1']
28- repo: https://github.com/get-woke/woke27- repo: https://github.com/get-woke/woke
29 rev: v0.19.028 rev: v0.19.0
30 hooks:29 hooks:
diff --git a/NEWS.rst b/NEWS.rst
index 46c712a..aec5c42 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -2,6 +2,10 @@
2NEWS for launchpadlib2NEWS for launchpadlib
3=====================3=====================
44
52.0.0
6=====
7- Remove support for Python 2.
8
51.11.0 (2023-01-09)91.11.0 (2023-01-09)
6===================10===================
7- Move the ``keyring`` dependency to a new ``keyring`` extra.11- Move the ``keyring`` dependency to a new ``keyring`` extra.
diff --git a/contrib/_pythonpath.py b/contrib/_pythonpath.py
index 6bf7934..04b8147 100644
--- a/contrib/_pythonpath.py
+++ b/contrib/_pythonpath.py
@@ -1,5 +1,3 @@
1__metaclass__ = type
2
3import sys1import sys
4import os2import os
53
diff --git a/contrib/close-my-bugs.py b/contrib/close-my-bugs.py
index ca925a2..bdf16c7 100755
--- a/contrib/close-my-bugs.py
+++ b/contrib/close-my-bugs.py
@@ -1,4 +1,4 @@
1#!/usr/bin/env python1#!/usr/bin/env python3
22
3# Copyright (C) 2009-2013 Canonical Ltd.3# Copyright (C) 2009-2013 Canonical Ltd.
4#4#
@@ -92,22 +92,22 @@ def main(args):
92 **extra_kwargs)]92 **extra_kwargs)]
9393
94 for task in committed_tasks:94 for task in committed_tasks:
95 print "Bug #%s: %s" % (task.bug.id, task.bug.title)95 print("Bug #%s: %s" % (task.bug.id, task.bug.title))
9696
97 if options.dry_run:97 if options.dry_run:
98 print '\n*** Nothing changed. Re-run without --dry-run/-n to commit.'98 print('\n*** Nothing changed. Re-run without --dry-run/-n to commit.')
99 else:99 else:
100 if not options.force:100 if not options.force:
101 answer = raw_input("Mark these bugs as Fix Released? [y/N]")101 answer = input("Mark these bugs as Fix Released? [y/N]")
102 if answer in ("n", "N") or not answer:102 if answer in ("n", "N") or not answer:
103 print "Ok, leaving them alone."103 print("Ok, leaving them alone.")
104 return104 return
105105
106 for task in committed_tasks:106 for task in committed_tasks:
107 print "Releasing %s" % task.bug.id107 print("Releasing %s" % task.bug.id)
108 task.status = FIX_RELEASED108 task.status = FIX_RELEASED
109 task.lp_save()109 task.lp_save()
110 print "Done."110 print("Done.")
111111
112 return 0112 return 0
113113
diff --git a/contrib/commercial-member-api.py b/contrib/commercial-member-api.py
index 1e84b25..f5c0031 100755
--- a/contrib/commercial-member-api.py
+++ b/contrib/commercial-member-api.py
@@ -1,4 +1,4 @@
1#!/usr/bin/python1#!/usr/bin/python3
2# -*-doctest-*-2# -*-doctest-*-
33
4"""4"""
@@ -6,16 +6,16 @@
6 >>> lp = lpapi.lp_factory('dev')6 >>> lp = lpapi.lp_factory('dev')
7 >>> bzr = lp.projects['bzr']7 >>> bzr = lp.projects['bzr']
8 >>> bzr.reviewer_whiteboard = "Check on licensing"8 >>> bzr.reviewer_whiteboard = "Check on licensing"
9 >>> print bzr.reviewer_whiteboard9 >>> print(bzr.reviewer_whiteboard)
10 Check on licensing10 Check on licensing
11 >>> bzr.lp_save()11 >>> bzr.lp_save()
12 >>> print bzr.reviewer_whiteboard12 >>> print(bzr.reviewer_whiteboard)
13 Check on licensing13 Check on licensing
1414
15 >>> from operator import attrgetter15 >>> from operator import attrgetter
16 >>> def print_projs(projs):16 >>> def print_projs(projs):
17 ... for p in sorted(projs, key=attrgetter('name')):17 ... for p in sorted(projs, key=attrgetter('name')):
18 ... print p.name18 ... print(p.name)
1919
20 >>> inactive = lp.projects.licensing_search(active=False)20 >>> inactive = lp.projects.licensing_search(active=False)
21 >>> print_projs(inactive)21 >>> print_projs(inactive)
@@ -133,11 +133,11 @@
133 launchpad133 launchpad
134134
135 >>> l = projs[2]135 >>> l = projs[2]
136 >>> print l.name136 >>> print(l.name)
137 launchpad137 launchpad
138 >>> print l.description138 >>> print(l.description)
139 Launchpad's design is inspired by the Description of a Project (DOAP) framework by Edd Dumbill, with extensions for actual releases of products.139 Launchpad's design is inspired by the Description of a Project (DOAP) framework by Edd Dumbill, with extensions for actual releases of products.
140 >>> print l.summary140 >>> print(l.summary)
141 Launchpad is a catalogue of libre software projects and products. Projects registered in the Launchpad are linked to their translations in Rosetta, their bugs in Malone, their RCS imports in Bazaar, and their packages in Soyuz.141 Launchpad is a catalogue of libre software projects and products. Projects registered in the Launchpad are linked to their translations in Rosetta, their bugs in Malone, their RCS imports in Bazaar, and their packages in Soyuz.
142142
143"""143"""
@@ -151,9 +151,9 @@ if __name__ == '__main__':
151 pass151 pass
152152
153 # Create correct credentials.153 # Create correct credentials.
154 print "Login as 'commercial-member@canonical.com' in your browser."154 print("Login as 'commercial-member@canonical.com' in your browser.")
155 print "Press <Enter> when done."155 print("Press <Enter> when done.")
156 raw_input()156 input()
157157
158 # Import _pythonpath and the lpapi module. _pythonpath must158 # Import _pythonpath and the lpapi module. _pythonpath must
159 # precede the import of lpapi as it redefines sys.path.159 # precede the import of lpapi as it redefines sys.path.
diff --git a/contrib/delete_bugtasks.py b/contrib/delete_bugtasks.py
index a9eefe5..490d392 100755
--- a/contrib/delete_bugtasks.py
+++ b/contrib/delete_bugtasks.py
@@ -1,6 +1,4 @@
1#!/usr/bin/python1#!/usr/bin/python3
2
3__metaclass__ = type
42
5from collections import defaultdict3from collections import defaultdict
6from optparse import OptionParser4from optparse import OptionParser
@@ -44,7 +42,7 @@ class SharedBugsFixer:
44 def log(self, message, leader=' ', error=False):42 def log(self, message, leader=' ', error=False):
45 """Report to STDOUT."""43 """Report to STDOUT."""
46 if error or self.verbose:44 if error or self.verbose:
47 print '%s%s' % (leader, message)45 print('%s%s' % (leader, message))
4846
49 def _get_target_type(self, bug_target):47 def _get_target_type(self, bug_target):
50 """Return the bug target entity type."""48 """Return the bug target entity type."""
@@ -105,9 +103,9 @@ class SharedBugsFixer:
105 self.log("! bug affects 1 pillar now.")103 self.log("! bug affects 1 pillar now.")
106 except UnsupportedSeriesSplit:104 except UnsupportedSeriesSplit:
107 self.log("! This script cannot split bugs that affect series.")105 self.log("! This script cannot split bugs that affect series.")
108 except (KeyError, Unauthorized), e:106 except (KeyError, Unauthorized):
109 self.log("! bug %s is owned by someone else" % pillar_name)107 self.log("! bug %s is owned by someone else" % pillar_name)
110 except Exception, e:108 except Exception as e:
111 # Something went very wrong.109 # Something went very wrong.
112 self.log("!! %s" % str(e), error=True)110 self.log("!! %s" % str(e), error=True)
113111
diff --git a/contrib/lp-bug-ifier.py b/contrib/lp-bug-ifier.py
index d911c5c..5197a70 100755
--- a/contrib/lp-bug-ifier.py
+++ b/contrib/lp-bug-ifier.py
@@ -1,4 +1,4 @@
1#!/usr/bin/env python1#!/usr/bin/env python3
22
3"""3"""
4Scan stdin for text matching bug references and insert the bug title into the4Scan stdin for text matching bug references and insert the bug title into the
@@ -25,7 +25,7 @@ from launchpadlib.launchpad import Launchpad
2525
2626
27bug_re = re.compile(r"[Bb]ug(?:\s|<br\s*/>)*(?:\#|report|number\.?|num\.?|no\.?)?"27bug_re = re.compile(r"[Bb]ug(?:\s|<br\s*/>)*(?:\#|report|number\.?|num\.?|no\.?)?"
28 "(?:\s|<br\s*/>)*(?P<bugnum>\d+)")28 r"(?:\s|<br\s*/>)*(?P<bugnum>\d+)")
2929
30launchpad = Launchpad.login_with(os.path.basename(sys.argv[0]), 'production')30launchpad = Launchpad.login_with(os.path.basename(sys.argv[0]), 'production')
31bugs = launchpad.bugs31bugs = launchpad.bugs
@@ -44,7 +44,7 @@ def add_summary_to_bug(match):
4444
45def main():45def main():
46 text = sys.stdin.read()46 text = sys.stdin.read()
47 print bug_re.sub(add_summary_to_bug, text)47 print(bug_re.sub(add_summary_to_bug, text))
4848
49if __name__ == '__main__':49if __name__ == '__main__':
50 main()50 main()
diff --git a/contrib/lpapi.py b/contrib/lpapi.py
index dfd1633..2c8730f 100644
--- a/contrib/lpapi.py
+++ b/contrib/lpapi.py
@@ -1,8 +1,7 @@
1#!/usr/bin/python2.41#!/usr/bin/python3
2import os2import os
3import sys3import sys
4from urlparse import urljoin4from urllib.parse import urljoin
5import commands
65
7try:6try:
8 from launchpadlib.launchpad import (7 from launchpadlib.launchpad import (
@@ -11,8 +10,8 @@ try:
11 from launchpadlib.errors import *10 from launchpadlib.errors import *
12 import launchpadlib11 import launchpadlib
13except ImportError:12except ImportError:
14 print >> sys.stderr, "Usage:"13 print("Usage:", file=sys.stderr)
15 print >> sys.stderr, " PYTHONPATH=somebranch/lib %s" % sys.argv[0]14 print(" PYTHONPATH=somebranch/lib %s" % sys.argv[0], file=sys.stderr)
16 raise15 raise
1716
1817
@@ -42,12 +41,12 @@ class LPSystem:
42 self.auth_file = os.path.join(home, self.auth_file_name)41 self.auth_file = os.path.join(home, self.auth_file_name)
43 self.credentials = Credentials()42 self.credentials = Credentials()
44 self.credentials.load(open(self.auth_file))43 self.credentials.load(open(self.auth_file))
45 print >> sys.stderr, "Loading credentials..."44 print("Loading credentials...", file=sys.stderr)
46 try:45 try:
47 self.launchpad = Launchpad(self.credentials, self.endpoint,46 self.launchpad = Launchpad(self.credentials, self.endpoint,
48 cache=cache_dir)47 cache=cache_dir)
49 except launchpadlib.errors.HTTPError:48 except launchpadlib.errors.HTTPError:
50 raise InvalidCredentials, (49 raise InvalidCredentials(
51 "Please remove %s and rerun %s to authenticate." % (50 "Please remove %s and rerun %s to authenticate." % (
52 self.auth_file, sys.argv[0]))51 self.auth_file, sys.argv[0]))
53 except IOError:52 except IOError:
@@ -58,9 +57,9 @@ class LPSystem:
58 self.endpoint,57 self.endpoint,
59 cache=cache_dir)58 cache=cache_dir)
60 self.launchpad.credentials.save(open(self.auth_file, "w"))59 self.launchpad.credentials.save(open(self.auth_file, "w"))
61 print >> sys.stderr, "Credentials saved"60 print("Credentials saved", file=sys.stderr)
62 except launchpadlib.errors.HTTPError, err:61 except launchpadlib.errors.HTTPError as err:
63 print >> sys.stderr, err.content62 print(err.content, file=sys.stderr)
64 raise63 raise
6564
66 @property65 @property
@@ -102,6 +101,6 @@ def lp_factory(system_name, app_name='just_testing'):
102 lpinstance = systems[system_name]101 lpinstance = systems[system_name]
103 return lpinstance(app_name).launchpad102 return lpinstance(app_name).launchpad
104 except KeyError:103 except KeyError:
105 print >> sys.stderr, "System '%s' not supported." % system_name104 print("System '%s' not supported." % system_name, file=sys.stderr)
106 print >> sys.stderr, "Use one of: ", systems.keys()105 print("Use one of: ", systems.keys(), file=sys.stderr)
107 return None106 return None
diff --git a/contrib/nopriv-api.py b/contrib/nopriv-api.py
index abe2b28..66cadc6 100755
--- a/contrib/nopriv-api.py
+++ b/contrib/nopriv-api.py
@@ -1,4 +1,4 @@
1#!/usr/bin/python1#!/usr/bin/python3
2# -*-doctest-*-2# -*-doctest-*-
33
4"""4"""
@@ -6,10 +6,10 @@
6 >>> lp = lpapi.lp_factory('dev')6 >>> lp = lpapi.lp_factory('dev')
77
8 >>> bzr = lp.projects['bzr']8 >>> bzr = lp.projects['bzr']
9 >>> print bzr.reviewer_whiteboard9 >>> print(bzr.reviewer_whiteboard)
10 tag:launchpad.net:2008:redacted10 tag:launchpad.net:2008:redacted
11 >>> bzr.reviewer_whiteboard = "Check on licensing"11 >>> bzr.reviewer_whiteboard = "Check on licensing"
12 >>> print bzr.reviewer_whiteboard12 >>> print(bzr.reviewer_whiteboard)
13 Check on licensing13 Check on licensing
14 >>> bzr.lp_save()14 >>> bzr.lp_save()
15 ...15 ...
@@ -78,9 +78,9 @@ if __name__ == '__main__':
78 pass78 pass
7979
80 # Create correct credentials.80 # Create correct credentials.
81 print "Login as 'no-priv@canonical.com' in your browser."81 print("Login as 'no-priv@canonical.com' in your browser.")
82 print "Press <Enter> when done."82 print("Press <Enter> when done.")
83 raw_input()83 input()
8484
85 # Import _pythonpath and the lpapi module. _pythonpath must85 # Import _pythonpath and the lpapi module. _pythonpath must
86 # precede the import of lpapi as it redefines sys.path.86 # precede the import of lpapi as it redefines sys.path.
diff --git a/contrib/sample-person-api.py b/contrib/sample-person-api.py
index a7b3488..51118b4 100755
--- a/contrib/sample-person-api.py
+++ b/contrib/sample-person-api.py
@@ -1,4 +1,4 @@
1#!/usr/bin/python1#!/usr/bin/python3
2# -*-doctest-*-2# -*-doctest-*-
33
4"""4"""
@@ -6,10 +6,10 @@
6 >>> lp = lpapi.lp_factory('dev')6 >>> lp = lpapi.lp_factory('dev')
77
8 >>> bzr = lp.projects['bzr']8 >>> bzr = lp.projects['bzr']
9 >>> print bzr.reviewer_whiteboard9 >>> print(bzr.reviewer_whiteboard)
10 tag:launchpad.net:2008:redacted10 tag:launchpad.net:2008:redacted
11 >>> bzr.reviewer_whiteboard = "Check on licensing"11 >>> bzr.reviewer_whiteboard = "Check on licensing"
12 >>> print bzr.reviewer_whiteboard12 >>> print(bzr.reviewer_whiteboard)
13 Check on licensing13 Check on licensing
14 >>> bzr.lp_save()14 >>> bzr.lp_save()
15 ...15 ...
@@ -78,9 +78,9 @@ if __name__ == '__main__':
78 pass78 pass
7979
80 # Create correct credentials.80 # Create correct credentials.
81 print "Login as 'test@canonical.com' in your browser."81 print("Login as 'test@canonical.com' in your browser.")
82 print "Press <Enter> when done."82 print("Press <Enter> when done.")
83 raw_input()83 input()
8484
85 # Import _pythonpath and the lpapi module. _pythonpath must85 # Import _pythonpath and the lpapi module. _pythonpath must
86 # precede the import of lpapi as it redefines sys.path.86 # precede the import of lpapi as it redefines sys.path.
diff --git a/contrib/upload_release_tarball.py b/contrib/upload_release_tarball.py
index 335987a..7a51370 100755
--- a/contrib/upload_release_tarball.py
+++ b/contrib/upload_release_tarball.py
@@ -1,4 +1,4 @@
1#!/usr/bin/python1#!/usr/bin/python3
2#2#
3# This script uploads a tarball as a file for a (possibly new)3# This script uploads a tarball as a file for a (possibly new)
4# release. It takes these command-line arguments:4# release. It takes these command-line arguments:
@@ -61,14 +61,14 @@ if os.path.exists(signature_path):
61else:61else:
62 # There is no signature.62 # There is no signature.
63 if options.force:63 if options.force:
64 print ('WARNING: Signature file "%s" is not present. Continuing '64 print('WARNING: Signature file "%s" is not present. Continuing '
65 'without it.' % signature_path)65 'without it.' % signature_path)
66 signature_name = None66 signature_name = None
67 signature = None67 signature = None
68 else:68 else:
69 print 'ERROR: Signature file "%s" is not present.' % signature_path69 print('ERROR: Signature file "%s" is not present.' % signature_path)
70 print 'Run "gpg --armor --sign --detach-sig" on the tarball.'70 print('Run "gpg --armor --sign --detach-sig" on the tarball.')
71 print 'Or re-run this script with the --force option.'71 print('Or re-run this script with the --force option.')
72 sys.exit(-1)72 sys.exit(-1)
7373
74# Now we interact with Launchpad.74# Now we interact with Launchpad.
@@ -92,8 +92,8 @@ series = matching_series[0]
92matching_milestones = [milestone for milestone in series.active_milestones92matching_milestones = [milestone for milestone in series.active_milestones
93 if milestone.name == version_name]93 if milestone.name == version_name]
94if len(matching_milestones) == 0:94if len(matching_milestones) == 0:
95 print 'No milestone "%s" for %s/%s. Creating it.' % (95 print('No milestone "%s" for %s/%s. Creating it.' % (
96 version_name, project.name, series.name)96 version_name, project.name, series.name))
97 milestone = series.newMilestone(name=version_name)97 milestone = series.newMilestone(name=version_name)
98else:98else:
99 milestone = matching_milestones[0]99 milestone = matching_milestones[0]
@@ -106,8 +106,8 @@ if len(matching_releases) == 0:
106 #106 #
107 # The changelog and release notes could go into this operation107 # The changelog and release notes could go into this operation
108 # invocation.108 # invocation.
109 print "No release for %s/%s/%s. Creating it." % (109 print("No release for %s/%s/%s. Creating it." % (
110 project.name, series.name, version_name)110 project.name, series.name, version_name))
111 release = milestone.createProductRelease(111 release = milestone.createProductRelease(
112 date_released=datetime.now(pytz.UTC))112 date_released=datetime.now(pytz.UTC))
113else:113else:
@@ -124,5 +124,5 @@ if signature is not None:
124result = release.add_file(**kwargs)124result = release.add_file(**kwargs)
125125
126# We know this succeeded because add_file didn't raise an exception.126# We know this succeeded because add_file didn't raise an exception.
127print "Success!"127print("Success!")
128print result.self_link128print(result.self_link)
diff --git a/pyproject.toml b/pyproject.toml
index 486bbe6..1f331da 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,3 @@
1[tool.black]1[tool.black]
2line-length = 792line-length = 79
3target-version = ['py27']3target-version = ['py35']
diff --git a/setup.py b/setup.py
index 0f4d391..38bf7b5 100755
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
1#!/usr/bin/env python1#!/usr/bin/env python3
22
3# Copyright 2008-2022 Canonical Ltd.3# Copyright 2008-2022 Canonical Ltd.
4#4#
@@ -46,7 +46,6 @@ install_requires = [
46 'importlib-metadata; python_version < "3.8"',46 'importlib-metadata; python_version < "3.8"',
47 "lazr.restfulclient>=0.14.2",47 "lazr.restfulclient>=0.14.2",
48 "lazr.uri",48 "lazr.uri",
49 "six",
50]49]
5150
52setup(51setup(
@@ -64,6 +63,7 @@ setup(
64 description=open("README.rst").readline().strip(),63 description=open("README.rst").readline().strip(),
65 long_description=generate("src/launchpadlib/docs/index.rst", "NEWS.rst"),64 long_description=generate("src/launchpadlib/docs/index.rst", "NEWS.rst"),
66 license="LGPL v3",65 license="LGPL v3",
66 python_requires=">=3.5",
67 install_requires=install_requires,67 install_requires=install_requires,
68 url="https://help.launchpad.net/API/launchpadlib",68 url="https://help.launchpad.net/API/launchpadlib",
69 project_urls={69 project_urls={
@@ -99,7 +99,6 @@ setup(
99 # Dependencies only needed by launchpadlib's own tests.99 # Dependencies only needed by launchpadlib's own tests.
100 "test": [100 "test": [
101 "coverage",101 "coverage",
102 'mock; python_version < "3"',
103 "pytest",102 "pytest",
104 ],103 ],
105 },104 },
diff --git a/src/launchpadlib/apps.py b/src/launchpadlib/apps.py
index e163a7b..df7720f 100644
--- a/src/launchpadlib/apps.py
+++ b/src/launchpadlib/apps.py
@@ -30,7 +30,7 @@ from launchpadlib.credentials import Credentials
30from launchpadlib.uris import lookup_web_root30from launchpadlib.uris import lookup_web_root
3131
3232
33class RequestTokenApp(object):33class RequestTokenApp:
34 """An application that creates request tokens."""34 """An application that creates request tokens."""
3535
36 def __init__(self, web_root, consumer_name, context):36 def __init__(self, web_root, consumer_name, context):
diff --git a/src/launchpadlib/bin/launchpad-request-token b/src/launchpadlib/bin/launchpad-request-token
index 8205b45..c81a23f 100755
--- a/src/launchpadlib/bin/launchpad-request-token
+++ b/src/launchpadlib/bin/launchpad-request-token
@@ -1,4 +1,4 @@
1#!/usr/bin/python1#!/usr/bin/python3
22
3# Copyright 2009 Canonical Ltd.3# Copyright 2009 Canonical Ltd.
44
@@ -22,10 +22,6 @@ This script will create a Launchpad request token and print to STDOUT
22some JSON data about the token and the available access levels.22some JSON data about the token and the available access levels.
23"""23"""
2424
25from __future__ import print_function
26
27__metaclass__ = type
28
29from optparse import OptionParser25from optparse import OptionParser
30from launchpadlib.apps import RequestTokenApp26from launchpadlib.apps import RequestTokenApp
3127
diff --git a/src/launchpadlib/credentials.py b/src/launchpadlib/credentials.py
index 5789fc0..abf55a2 100644
--- a/src/launchpadlib/credentials.py
+++ b/src/launchpadlib/credentials.py
@@ -14,11 +14,8 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.15# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
1616
17from __future__ import print_function
18
19"""launchpadlib credentials and authentication support."""17"""launchpadlib credentials and authentication support."""
2018
21__metaclass__ = type
22__all__ = [19__all__ = [
23 "AccessToken",20 "AccessToken",
24 "AnonymousAccessToken",21 "AnonymousAccessToken",
@@ -29,40 +26,20 @@ __all__ = [
29 "Credentials",26 "Credentials",
30]27]
3128
32try:29from base64 import (
33 from cStringIO import StringIO30 b64decode,
34except ImportError:31 b64encode,
35 from io import StringIO32)
36
37import httplib233import httplib2
34from io import StringIO
38import json35import json
39import os36import os
40from select import select37from select import select
41import stat38import stat
42from sys import stdin39from sys import stdin
43import time40import time
4441from urllib.parse import urlencode, urljoin, parse_qs
45try:
46 from urllib.parse import urlencode
47except ImportError:
48 from urllib import urlencode
49try:
50 from urllib.parse import urljoin
51except ImportError:
52 from urlparse import urljoin
53import webbrowser42import webbrowser
54from base64 import (
55 b64decode,
56 b64encode,
57)
58
59from six.moves.urllib.parse import parse_qs
60
61if bytes is str:
62 # Python 2
63 unicode_type = unicode # noqa: F821
64else:
65 unicode_type = str
6643
67from lazr.restfulclient.errors import HTTPError44from lazr.restfulclient.errors import HTTPError
68from lazr.restfulclient.authorize.oauth import (45from lazr.restfulclient.authorize.oauth import (
@@ -135,7 +112,7 @@ class Credentials(OAuthAuthorizer):
135 sio = StringIO()112 sio = StringIO()
136 self.save(sio)113 self.save(sio)
137 serialized = sio.getvalue()114 serialized = sio.getvalue()
138 if isinstance(serialized, unicode_type):115 if isinstance(serialized, str):
139 serialized = serialized.encode("utf-8")116 serialized = serialized.encode("utf-8")
140 return serialized117 return serialized
141118
@@ -146,7 +123,7 @@ class Credentials(OAuthAuthorizer):
146 This should probably be moved into OAuthAuthorizer.123 This should probably be moved into OAuthAuthorizer.
147 """124 """
148 credentials = cls()125 credentials = cls()
149 if not isinstance(value, unicode_type):126 if not isinstance(value, str):
150 value = value.decode("utf-8")127 value = value.decode("utf-8")
151 credentials.load(StringIO(value))128 credentials.load(StringIO(value))
152 return credentials129 return credentials
@@ -255,7 +232,7 @@ class AccessToken(_AccessToken):
255 @classmethod232 @classmethod
256 def from_string(cls, query_string):233 def from_string(cls, query_string):
257 """Create and return a new `AccessToken` from the given string."""234 """Create and return a new `AccessToken` from the given string."""
258 if not isinstance(query_string, unicode_type):235 if not isinstance(query_string, str):
259 query_string = query_string.decode("utf-8")236 query_string = query_string.decode("utf-8")
260 params = parse_qs(query_string, keep_blank_values=False)237 params = parse_qs(query_string, keep_blank_values=False)
261 key = params["oauth_token"]238 key = params["oauth_token"]
@@ -280,10 +257,10 @@ class AnonymousAccessToken(_AccessToken):
280 """257 """
281258
282 def __init__(self):259 def __init__(self):
283 super(AnonymousAccessToken, self).__init__("", "")260 super().__init__("", "")
284261
285262
286class CredentialStore(object):263class CredentialStore:
287 """Store OAuth credentials locally.264 """Store OAuth credentials locally.
288265
289 This is a generic superclass. To implement a specific way of266 This is a generic superclass. To implement a specific way of
@@ -369,7 +346,7 @@ class KeyringCredentialStore(CredentialStore):
369 B64MARKER = b"<B64>"346 B64MARKER = b"<B64>"
370347
371 def __init__(self, credential_save_failed=None, fallback=False):348 def __init__(self, credential_save_failed=None, fallback=False):
372 super(KeyringCredentialStore, self).__init__(credential_save_failed)349 super().__init__(credential_save_failed)
373 self._fallback = None350 self._fallback = None
374 if fallback:351 if fallback:
375 self._fallback = MemoryCredentialStore(credential_save_failed)352 self._fallback = MemoryCredentialStore(credential_save_failed)
@@ -438,7 +415,7 @@ class KeyringCredentialStore(CredentialStore):
438 else:415 else:
439 raise416 raise
440 if credential_string is not None:417 if credential_string is not None:
441 if isinstance(credential_string, unicode_type):418 if isinstance(credential_string, str):
442 credential_string = credential_string.encode("utf8")419 credential_string = credential_string.encode("utf8")
443 if credential_string.startswith(self.B64MARKER):420 if credential_string.startswith(self.B64MARKER):
444 try:421 try:
@@ -468,9 +445,7 @@ class UnencryptedFileCredentialStore(CredentialStore):
468 """445 """
469446
470 def __init__(self, filename, credential_save_failed=None):447 def __init__(self, filename, credential_save_failed=None):
471 super(UnencryptedFileCredentialStore, self).__init__(448 super().__init__(credential_save_failed)
472 credential_save_failed
473 )
474 self.filename = filename449 self.filename = filename
475450
476 def do_save(self, credentials, unique_key):451 def do_save(self, credentials, unique_key):
@@ -495,7 +470,7 @@ class MemoryCredentialStore(CredentialStore):
495 """470 """
496471
497 def __init__(self, credential_save_failed=None):472 def __init__(self, credential_save_failed=None):
498 super(MemoryCredentialStore, self).__init__(credential_save_failed)473 super().__init__(credential_save_failed)
499 self._credentials = {}474 self._credentials = {}
500475
501 def do_save(self, credentials, unique_key):476 def do_save(self, credentials, unique_key):
@@ -507,7 +482,7 @@ class MemoryCredentialStore(CredentialStore):
507 return self._credentials.get(unique_key)482 return self._credentials.get(unique_key)
508483
509484
510class RequestTokenAuthorizationEngine(object):485class RequestTokenAuthorizationEngine:
511 """The superclass of all request token authorizers.486 """The superclass of all request token authorizers.
512487
513 This base class does not implement request token authorization,488 This base class does not implement request token authorization,
@@ -774,15 +749,13 @@ class AuthorizeRequestTokenWithBrowser(AuthorizeRequestTokenWithURL):
774 # It doesn't look like we're doing anything here, but we749 # It doesn't look like we're doing anything here, but we
775 # are discarding the passed-in values for consumer_name and750 # are discarding the passed-in values for consumer_name and
776 # allow_access_levels.751 # allow_access_levels.
777 super(AuthorizeRequestTokenWithBrowser, self).__init__(752 super().__init__(
778 service_root, application_name, None, credential_save_failed753 service_root, application_name, None, credential_save_failed
779 )754 )
780755
781 def notify_end_user_authorization_url(self, authorization_url):756 def notify_end_user_authorization_url(self, authorization_url):
782 """Notify the end-user of the URL."""757 """Notify the end-user of the URL."""
783 super(758 super().notify_end_user_authorization_url(authorization_url)
784 AuthorizeRequestTokenWithBrowser, self
785 ).notify_end_user_authorization_url(authorization_url)
786759
787 try:760 try:
788 browser_obj = webbrowser.get()761 browser_obj = webbrowser.get()
diff --git a/src/launchpadlib/docs/conf.py b/src/launchpadlib/docs/conf.py
index 268a8ce..235e7a9 100644
--- a/src/launchpadlib/docs/conf.py
+++ b/src/launchpadlib/docs/conf.py
@@ -1,5 +1,3 @@
1# -*- coding: utf-8 -*-
2#
3# launchpadlib documentation build configuration file, created by1# launchpadlib documentation build configuration file, created by
4# sphinx-quickstart on Tue Nov 5 23:48:15 2019.2# sphinx-quickstart on Tue Nov 5 23:48:15 2019.
5#3#
@@ -47,9 +45,9 @@ source_suffix = ".rst"
47master_doc = "index"45master_doc = "index"
4846
49# General information about the project.47# General information about the project.
50project = u"launchpadlib"48project = "launchpadlib"
51copyright = u"2008-2019, Canonical Ltd."49copyright = "2008-2019, Canonical Ltd."
52author = u"LAZR Developers <lazr-developers@lists.launchpad.net>"50author = "LAZR Developers <lazr-developers@lists.launchpad.net>"
5351
54# The version info for the project you're documenting, acts as replacement for52# The version info for the project you're documenting, acts as replacement for
55# |version| and |release|, also used in various other places throughout the53# |version| and |release|, also used in various other places throughout the
@@ -140,8 +138,8 @@ latex_documents = [
140 (138 (
141 master_doc,139 master_doc,
142 "launchpadlib.tex",140 "launchpadlib.tex",
143 u"launchpadlib Documentation",141 "launchpadlib Documentation",
144 u"LAZR Developers \\textless{}lazr-developers@lists.launchpad.net\\textgreater{}", # noqa: E501142 "LAZR Developers \\textless{}lazr-developers@lists.launchpad.net\\textgreater{}", # noqa: E501
145 "manual",143 "manual",
146 ),144 ),
147]145]
@@ -152,7 +150,7 @@ latex_documents = [
152# One entry per manual page. List of tuples150# One entry per manual page. List of tuples
153# (source start file, name, description, authors, manual section).151# (source start file, name, description, authors, manual section).
154man_pages = [152man_pages = [
155 (master_doc, "launchpadlib", u"launchpadlib Documentation", [author], 1)153 (master_doc, "launchpadlib", "launchpadlib Documentation", [author], 1)
156]154]
157155
158156
@@ -165,7 +163,7 @@ texinfo_documents = [
165 (163 (
166 master_doc,164 master_doc,
167 "launchpadlib",165 "launchpadlib",
168 u"launchpadlib Documentation",166 "launchpadlib Documentation",
169 author,167 author,
170 "launchpadlib",168 "launchpadlib",
171 "One line description of project.",169 "One line description of project.",
diff --git a/src/launchpadlib/launchpad.py b/src/launchpadlib/launchpad.py
index d8c6ba6..6b8cea6 100644
--- a/src/launchpadlib/launchpad.py
+++ b/src/launchpadlib/launchpad.py
@@ -16,18 +16,13 @@
1616
17"""Root Launchpad API class."""17"""Root Launchpad API class."""
1818
19__metaclass__ = type
20__all__ = [19__all__ = [
21 "Launchpad",20 "Launchpad",
22]21]
2322
24import errno23import errno
25import os24import os
2625from urllib.parse import urlsplit
27try:
28 from urllib.parse import urlsplit
29except ImportError:
30 from urlparse import urlsplit
31import warnings26import warnings
3227
33try:28try:
@@ -130,7 +125,7 @@ class LaunchpadOAuthAwareHttp(RestfulHttp):
130 def __init__(self, launchpad, authorization_engine, *args):125 def __init__(self, launchpad, authorization_engine, *args):
131 self.launchpad = launchpad126 self.launchpad = launchpad
132 self.authorization_engine = authorization_engine127 self.authorization_engine = authorization_engine
133 super(LaunchpadOAuthAwareHttp, self).__init__(*args)128 super().__init__(*args)
134129
135 def _bad_oauth_token(self, response, content):130 def _bad_oauth_token(self, response, content):
136 """Helper method to detect an error caused by a bad OAuth token."""131 """Helper method to detect an error caused by a bad OAuth token."""
@@ -141,9 +136,7 @@ class LaunchpadOAuthAwareHttp(RestfulHttp):
141 )136 )
142137
143 def _request(self, *args):138 def _request(self, *args):
144 response, content = super(LaunchpadOAuthAwareHttp, self)._request(139 response, content = super()._request(*args)
145 *args
146 )
147 return self.retry_on_bad_token(response, content, *args)140 return self.retry_on_bad_token(response, content, *args)
148141
149 def retry_on_bad_token(self, response, content, *args):142 def retry_on_bad_token(self, response, content, *args):
@@ -227,7 +220,7 @@ class Launchpad(ServiceRoot):
227 # case we need to authorize a new token during use.220 # case we need to authorize a new token during use.
228 self.authorization_engine = authorization_engine221 self.authorization_engine = authorization_engine
229222
230 super(Launchpad, self).__init__(223 super().__init__(
231 credentials, service_root, cache, timeout, proxy_info, version224 credentials, service_root, cache, timeout, proxy_info, version
232 )225 )
233226
diff --git a/src/launchpadlib/testing/helpers.py b/src/launchpadlib/testing/helpers.py
index e625f5e..c9a7cec 100644
--- a/src/launchpadlib/testing/helpers.py
+++ b/src/launchpadlib/testing/helpers.py
@@ -18,8 +18,6 @@
1818
19"""launchpadlib testing helpers."""19"""launchpadlib testing helpers."""
2020
21
22__metaclass__ = type
23__all__ = [21__all__ = [
24 "BadSaveKeyring",22 "BadSaveKeyring",
25 "fake_keyring",23 "fake_keyring",
@@ -64,7 +62,7 @@ class NoNetworkAuthorizationEngine(RequestTokenAuthorizationEngine):
64 ACCESS_TOKEN_KEY = "access_key:84"62 ACCESS_TOKEN_KEY = "access_key:84"
6563
66 def __init__(self, *args, **kwargs):64 def __init__(self, *args, **kwargs):
67 super(NoNetworkAuthorizationEngine, self).__init__(*args, **kwargs)65 super().__init__(*args, **kwargs)
68 # Set up some instrumentation.66 # Set up some instrumentation.
69 self.request_tokens_obtained = 067 self.request_tokens_obtained = 0
70 self.access_tokens_obtained = 068 self.access_tokens_obtained = 0
@@ -144,7 +142,7 @@ class TestableLaunchpad(Launchpad):
144 generally pass in fully-formed Credentials objects.142 generally pass in fully-formed Credentials objects.
145 :param service_root: Defaults to 'test_dev'.143 :param service_root: Defaults to 'test_dev'.
146 """144 """
147 super(TestableLaunchpad, self).__init__(145 super().__init__(
148 credentials,146 credentials,
149 authorization_engine,147 authorization_engine,
150 credential_store,148 credential_store,
diff --git a/src/launchpadlib/testing/launchpad.py b/src/launchpadlib/testing/launchpad.py
index aa2ee6d..3edaad4 100644
--- a/src/launchpadlib/testing/launchpad.py
+++ b/src/launchpadlib/testing/launchpad.py
@@ -65,23 +65,15 @@ Where 'https://api.launchpad.net/devel/' is the URL for the WADL file, found
65also in the WADL file itelf.65also in the WADL file itelf.
66"""66"""
6767
68from collections.abc import Callable
68from datetime import datetime69from datetime import datetime
6970
70try:
71 from collections.abc import Callable
72except ImportError:
73 from collections import Callable
74import sys
75
76if sys.version_info[0] >= 3:
77 basestring = str
78
7971
80class IntegrityError(Exception):72class IntegrityError(Exception):
81 """Raised when bad sample data is used with a L{FakeLaunchpad} instance."""73 """Raised when bad sample data is used with a L{FakeLaunchpad} instance."""
8274
8375
84class FakeLaunchpad(object):76class FakeLaunchpad:
85 """A fake Launchpad API class for unit tests that depend on L{Launchpad}.77 """A fake Launchpad API class for unit tests that depend on L{Launchpad}.
8678
87 @param application: A C{wadllib.application.Application} instance for a79 @param application: A C{wadllib.application.Application} instance for a
@@ -188,7 +180,7 @@ def wadl_tag(tag_name):
188 return "{http://research.sun.com/wadl/2006/10}" + tag_name180 return "{http://research.sun.com/wadl/2006/10}" + tag_name
189181
190182
191class FakeResource(object):183class FakeResource:
192 """184 """
193 Represents valid sample data on L{FakeLaunchpad} instances.185 Represents valid sample data on L{FakeLaunchpad} instances.
194186
@@ -434,7 +426,7 @@ class FakeResource(object):
434 if param is None:426 if param is None:
435 raise IntegrityError("%s not found" % name)427 raise IntegrityError("%s not found" % name)
436 if param.type is None:428 if param.type is None:
437 if not isinstance(value, basestring):429 if not isinstance(value, str):
438 raise IntegrityError(430 raise IntegrityError(
439 "%s is not a str or unicode for %s" % (value, name)431 "%s is not a str or unicode for %s" % (value, name)
440 )432 )
@@ -594,7 +586,7 @@ class FakeRoot(FakeResource):
594 resource_type = application.get_resource_type(586 resource_type = application.get_resource_type(
595 application.markup_url + "#service-root"587 application.markup_url + "#service-root"
596 )588 )
597 super(FakeRoot, self).__init__(application, resource_type)589 super().__init__(application, resource_type)
598590
599591
600class FakeEntry(FakeResource):592class FakeEntry(FakeResource):
@@ -612,9 +604,7 @@ class FakeCollection(FakeResource):
612 name=None,604 name=None,
613 child_resource_type=None,605 child_resource_type=None,
614 ):606 ):
615 super(FakeCollection, self).__init__(607 super().__init__(application, resource_type, values)
616 application, resource_type, values
617 )
618 self.__dict__.update(608 self.__dict__.update(
619 {"_name": name, "_child_resource_type": child_resource_type}609 {"_name": name, "_child_resource_type": child_resource_type}
620 )610 )
diff --git a/src/launchpadlib/testing/tests/test_launchpad.py b/src/launchpadlib/testing/tests/test_launchpad.py
index c988d2b..cff4dd2 100644
--- a/src/launchpadlib/testing/tests/test_launchpad.py
+++ b/src/launchpadlib/testing/tests/test_launchpad.py
@@ -160,8 +160,8 @@ class FakeLaunchpadTest(ResourcedTestCase):
160 dicts that represent objects. Plain string values can be represented160 dicts that represent objects. Plain string values can be represented
161 as C{unicode} strings.161 as C{unicode} strings.
162 """162 """
163 self.launchpad.me = dict(name=u"foo")163 self.launchpad.me = dict(name="foo")
164 self.assertEqual(u"foo", self.launchpad.me.name)164 self.assertEqual("foo", self.launchpad.me.name)
165165
166 def test_datetime_property(self):166 def test_datetime_property(self):
167 """167 """
diff --git a/src/launchpadlib/tests/test_credential_store.py b/src/launchpadlib/tests/test_credential_store.py
index 3049fbe..b6fe597 100644
--- a/src/launchpadlib/tests/test_credential_store.py
+++ b/src/launchpadlib/tests/test_credential_store.py
@@ -169,9 +169,7 @@ class TestKeyringCredentialStore(CredentialStoreTestCase):
169 # handled correctly. (See bug lp:877374)169 # handled correctly. (See bug lp:877374)
170 class UnicodeInMemoryKeyring(InMemoryKeyring):170 class UnicodeInMemoryKeyring(InMemoryKeyring):
171 def get_password(self, service, username):171 def get_password(self, service, username):
172 password = super(UnicodeInMemoryKeyring, self).get_password(172 password = super().get_password(service, username)
173 service, username
174 )
175 if isinstance(password, unicode_type):173 if isinstance(password, unicode_type):
176 password = password.encode("utf-8")174 password = password.encode("utf-8")
177 return password175 return password
@@ -194,9 +192,7 @@ class TestKeyringCredentialStore(CredentialStoreTestCase):
194192
195 class UnencodedInMemoryKeyring(InMemoryKeyring):193 class UnencodedInMemoryKeyring(InMemoryKeyring):
196 def get_password(self, service, username):194 def get_password(self, service, username):
197 pw = super(UnencodedInMemoryKeyring, self).get_password(195 pw = super().get_password(service, username)
198 service, username
199 )
200 return b64decode(pw[5:])196 return b64decode(pw[5:])
201197
202 self.keyring = UnencodedInMemoryKeyring()198 self.keyring = UnencodedInMemoryKeyring()
diff --git a/src/launchpadlib/tests/test_http.py b/src/launchpadlib/tests/test_http.py
index 6924e4f..e2287f6 100644
--- a/src/launchpadlib/tests/test_http.py
+++ b/src/launchpadlib/tests/test_http.py
@@ -17,15 +17,10 @@
17"""Tests for the LaunchpadOAuthAwareHTTP class."""17"""Tests for the LaunchpadOAuthAwareHTTP class."""
1818
19from collections import deque19from collections import deque
20from json import dumps20from json import dumps, JSONDecodeError
21import tempfile21import tempfile
22import unittest22import unittest
2323
24try:
25 from json import JSONDecodeError
26except ImportError:
27 JSONDecodeError = ValueError
28
29from launchpadlib.errors import Unauthorized24from launchpadlib.errors import Unauthorized
30from launchpadlib.credentials import UnencryptedFileCredentialStore25from launchpadlib.credentials import UnencryptedFileCredentialStore
31from launchpadlib.launchpad import (26from launchpadlib.launchpad import (
@@ -75,7 +70,7 @@ class SimulatedResponsesHttp(LaunchpadOAuthAwareHttp):
75 :param responses: A list of HttpResponse objects to use70 :param responses: A list of HttpResponse objects to use
76 in response to requests.71 in response to requests.
77 """72 """
78 super(SimulatedResponsesHttp, self).__init__(*args)73 super().__init__(*args)
79 self.sent_responses = []74 self.sent_responses = []
80 self.unsent_responses = responses75 self.unsent_responses = responses
81 self.cache = None76 self.cache = None
diff --git a/src/launchpadlib/tests/test_launchpad.py b/src/launchpadlib/tests/test_launchpad.py
index 66462c5..47410d5 100644
--- a/src/launchpadlib/tests/test_launchpad.py
+++ b/src/launchpadlib/tests/test_launchpad.py
@@ -16,8 +16,6 @@
1616
17"""Tests for the Launchpad class."""17"""Tests for the Launchpad class."""
1818
19__metaclass__ = type
20
21from contextlib import contextmanager19from contextlib import contextmanager
22import os20import os
23import shutil21import shutil
@@ -25,11 +23,7 @@ import socket
25import stat23import stat
26import tempfile24import tempfile
27import unittest25import unittest
2826from unittest.mock import patch
29try:
30 from unittest.mock import patch
31except ImportError:
32 from mock import patch
33import warnings27import warnings
3428
35from lazr.restfulclient.resource import ServiceRoot29from lazr.restfulclient.resource import ServiceRoot
@@ -351,11 +345,11 @@ class TestLaunchpadLoginWith(KeyringTest):
351 """Tests for Launchpad.login_with()."""345 """Tests for Launchpad.login_with()."""
352346
353 def setUp(self):347 def setUp(self):
354 super(TestLaunchpadLoginWith, self).setUp()348 super().setUp()
355 self.temp_dir = tempfile.mkdtemp()349 self.temp_dir = tempfile.mkdtemp()
356350
357 def tearDown(self):351 def tearDown(self):
358 super(TestLaunchpadLoginWith, self).tearDown()352 super().tearDown()
359 shutil.rmtree(self.temp_dir)353 shutil.rmtree(self.temp_dir)
360354
361 def test_dirs_created(self):355 def test_dirs_created(self):
diff --git a/src/launchpadlib/uris.py b/src/launchpadlib/uris.py
index dda802e..1bcc8f6 100644
--- a/src/launchpadlib/uris.py
+++ b/src/launchpadlib/uris.py
@@ -20,18 +20,15 @@ The code in this module lets users say "staging" when they mean
20"https://api.staging.launchpad.net/".20"https://api.staging.launchpad.net/".
21"""21"""
2222
23__metaclass__ = type
24__all__ = [23__all__ = [
25 "lookup_service_root",24 "lookup_service_root",
26 "lookup_web_root",25 "lookup_web_root",
27 "web_root_for_service_root",26 "web_root_for_service_root",
28]27]
29try:
30 from urllib.parse import urlparse
31except ImportError:
32 from urlparse import urlparse
3328
29from urllib.parse import urlparse
34import warnings30import warnings
31
35from lazr.uri import URI32from lazr.uri import URI
3633
37LPNET_SERVICE_ROOT = "https://api.launchpad.net/"34LPNET_SERVICE_ROOT = "https://api.launchpad.net/"
diff --git a/tox.ini b/tox.ini
index aaa62c9..ecde674 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,13 +1,13 @@
1[tox]1[tox]
2envlist =2envlist =
3 py27,py35,py36,py37,py38,py39,py310,py311,lint,docs3 py35,py36,py37,py38,py39,py310,py311,lint,docs
4requires = virtualenv<20.224requires = virtualenv<20.22
55
6[testenv]6[testenv]
7deps =7deps =
8 .[test,testing]8 .[test,testing]
9commands =9commands =
10 coverage run -m pytest src {posargs}10 pytest src {posargs}
1111
12[testenv:lint]12[testenv:lint]
13# necessary to build the woke linter13# necessary to build the woke linter
@@ -34,17 +34,16 @@ deps =
34 .[docs]34 .[docs]
3535
36[testenv:coverage]36[testenv:coverage]
37description = usage: tox -e py27,py35,py310,coverage
38basepython =37basepython =
39 python338 python3
40deps =39deps =
41 .[testing,test]40 .[testing,test]
42commands =41commands =
42 coverage erase
43 coverage run -m pytest src {posargs}
43 coverage combine44 coverage combine
44 coverage html45 coverage html
45 coverage report -m --fail-under=8946 coverage report -m --fail-under=91
46depends =
47 py27,py35,py310
4847
49[coverage:run]48[coverage:run]
50parallel=True49parallel=True

Subscribers

People subscribed via source and target branches