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
1diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
2index ec0cbbd..e9cff43 100644
3--- a/.pre-commit-config.yaml
4+++ b/.pre-commit-config.yaml
5@@ -12,19 +12,18 @@ repos:
6 - id: check-yaml
7 - id: debug-statements
8 - repo: https://github.com/PyCQA/flake8
9- rev: 5.0.4
10+ rev: 7.0.0
11 hooks:
12 - id: flake8
13 - repo: https://github.com/asottile/pyupgrade
14- rev: v2.38.4 # v3 drops Python 2 support
15+ rev: v3.15.1
16 hooks:
17 - id: pyupgrade
18- args: [--keep-percent-format]
19+ args: [--keep-percent-format, --py3-plus]
20 - repo: https://github.com/psf/black
21- rev: 21.12b0 # v22 drops Python 2 support
22+ rev: 24.2.0
23 hooks:
24 - id: black
25- additional_dependencies: ['click<8.1']
26 - repo: https://github.com/get-woke/woke
27 rev: v0.19.0
28 hooks:
29diff --git a/NEWS.rst b/NEWS.rst
30index 46c712a..aec5c42 100644
31--- a/NEWS.rst
32+++ b/NEWS.rst
33@@ -2,6 +2,10 @@
34 NEWS for launchpadlib
35 =====================
36
37+2.0.0
38+=====
39+- Remove support for Python 2.
40+
41 1.11.0 (2023-01-09)
42 ===================
43 - Move the ``keyring`` dependency to a new ``keyring`` extra.
44diff --git a/contrib/_pythonpath.py b/contrib/_pythonpath.py
45index 6bf7934..04b8147 100644
46--- a/contrib/_pythonpath.py
47+++ b/contrib/_pythonpath.py
48@@ -1,5 +1,3 @@
49-__metaclass__ = type
50-
51 import sys
52 import os
53
54diff --git a/contrib/close-my-bugs.py b/contrib/close-my-bugs.py
55index ca925a2..bdf16c7 100755
56--- a/contrib/close-my-bugs.py
57+++ b/contrib/close-my-bugs.py
58@@ -1,4 +1,4 @@
59-#!/usr/bin/env python
60+#!/usr/bin/env python3
61
62 # Copyright (C) 2009-2013 Canonical Ltd.
63 #
64@@ -92,22 +92,22 @@ def main(args):
65 **extra_kwargs)]
66
67 for task in committed_tasks:
68- print "Bug #%s: %s" % (task.bug.id, task.bug.title)
69+ print("Bug #%s: %s" % (task.bug.id, task.bug.title))
70
71 if options.dry_run:
72- print '\n*** Nothing changed. Re-run without --dry-run/-n to commit.'
73+ print('\n*** Nothing changed. Re-run without --dry-run/-n to commit.')
74 else:
75 if not options.force:
76- answer = raw_input("Mark these bugs as Fix Released? [y/N]")
77+ answer = input("Mark these bugs as Fix Released? [y/N]")
78 if answer in ("n", "N") or not answer:
79- print "Ok, leaving them alone."
80+ print("Ok, leaving them alone.")
81 return
82
83 for task in committed_tasks:
84- print "Releasing %s" % task.bug.id
85+ print("Releasing %s" % task.bug.id)
86 task.status = FIX_RELEASED
87 task.lp_save()
88- print "Done."
89+ print("Done.")
90
91 return 0
92
93diff --git a/contrib/commercial-member-api.py b/contrib/commercial-member-api.py
94index 1e84b25..f5c0031 100755
95--- a/contrib/commercial-member-api.py
96+++ b/contrib/commercial-member-api.py
97@@ -1,4 +1,4 @@
98-#!/usr/bin/python
99+#!/usr/bin/python3
100 # -*-doctest-*-
101
102 """
103@@ -6,16 +6,16 @@
104 >>> lp = lpapi.lp_factory('dev')
105 >>> bzr = lp.projects['bzr']
106 >>> bzr.reviewer_whiteboard = "Check on licensing"
107- >>> print bzr.reviewer_whiteboard
108+ >>> print(bzr.reviewer_whiteboard)
109 Check on licensing
110 >>> bzr.lp_save()
111- >>> print bzr.reviewer_whiteboard
112+ >>> print(bzr.reviewer_whiteboard)
113 Check on licensing
114
115 >>> from operator import attrgetter
116 >>> def print_projs(projs):
117 ... for p in sorted(projs, key=attrgetter('name')):
118- ... print p.name
119+ ... print(p.name)
120
121 >>> inactive = lp.projects.licensing_search(active=False)
122 >>> print_projs(inactive)
123@@ -133,11 +133,11 @@
124 launchpad
125
126 >>> l = projs[2]
127- >>> print l.name
128+ >>> print(l.name)
129 launchpad
130- >>> print l.description
131+ >>> print(l.description)
132 Launchpad's design is inspired by the Description of a Project (DOAP) framework by Edd Dumbill, with extensions for actual releases of products.
133- >>> print l.summary
134+ >>> print(l.summary)
135 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.
136
137 """
138@@ -151,9 +151,9 @@ if __name__ == '__main__':
139 pass
140
141 # Create correct credentials.
142- print "Login as 'commercial-member@canonical.com' in your browser."
143- print "Press <Enter> when done."
144- raw_input()
145+ print("Login as 'commercial-member@canonical.com' in your browser.")
146+ print("Press <Enter> when done.")
147+ input()
148
149 # Import _pythonpath and the lpapi module. _pythonpath must
150 # precede the import of lpapi as it redefines sys.path.
151diff --git a/contrib/delete_bugtasks.py b/contrib/delete_bugtasks.py
152index a9eefe5..490d392 100755
153--- a/contrib/delete_bugtasks.py
154+++ b/contrib/delete_bugtasks.py
155@@ -1,6 +1,4 @@
156-#!/usr/bin/python
157-
158-__metaclass__ = type
159+#!/usr/bin/python3
160
161 from collections import defaultdict
162 from optparse import OptionParser
163@@ -44,7 +42,7 @@ class SharedBugsFixer:
164 def log(self, message, leader=' ', error=False):
165 """Report to STDOUT."""
166 if error or self.verbose:
167- print '%s%s' % (leader, message)
168+ print('%s%s' % (leader, message))
169
170 def _get_target_type(self, bug_target):
171 """Return the bug target entity type."""
172@@ -105,9 +103,9 @@ class SharedBugsFixer:
173 self.log("! bug affects 1 pillar now.")
174 except UnsupportedSeriesSplit:
175 self.log("! This script cannot split bugs that affect series.")
176- except (KeyError, Unauthorized), e:
177+ except (KeyError, Unauthorized):
178 self.log("! bug %s is owned by someone else" % pillar_name)
179- except Exception, e:
180+ except Exception as e:
181 # Something went very wrong.
182 self.log("!! %s" % str(e), error=True)
183
184diff --git a/contrib/lp-bug-ifier.py b/contrib/lp-bug-ifier.py
185index d911c5c..5197a70 100755
186--- a/contrib/lp-bug-ifier.py
187+++ b/contrib/lp-bug-ifier.py
188@@ -1,4 +1,4 @@
189-#!/usr/bin/env python
190+#!/usr/bin/env python3
191
192 """
193 Scan stdin for text matching bug references and insert the bug title into the
194@@ -25,7 +25,7 @@ from launchpadlib.launchpad import Launchpad
195
196
197 bug_re = re.compile(r"[Bb]ug(?:\s|<br\s*/>)*(?:\#|report|number\.?|num\.?|no\.?)?"
198- "(?:\s|<br\s*/>)*(?P<bugnum>\d+)")
199+ r"(?:\s|<br\s*/>)*(?P<bugnum>\d+)")
200
201 launchpad = Launchpad.login_with(os.path.basename(sys.argv[0]), 'production')
202 bugs = launchpad.bugs
203@@ -44,7 +44,7 @@ def add_summary_to_bug(match):
204
205 def main():
206 text = sys.stdin.read()
207- print bug_re.sub(add_summary_to_bug, text)
208+ print(bug_re.sub(add_summary_to_bug, text))
209
210 if __name__ == '__main__':
211 main()
212diff --git a/contrib/lpapi.py b/contrib/lpapi.py
213index dfd1633..2c8730f 100644
214--- a/contrib/lpapi.py
215+++ b/contrib/lpapi.py
216@@ -1,8 +1,7 @@
217-#!/usr/bin/python2.4
218+#!/usr/bin/python3
219 import os
220 import sys
221-from urlparse import urljoin
222-import commands
223+from urllib.parse import urljoin
224
225 try:
226 from launchpadlib.launchpad import (
227@@ -11,8 +10,8 @@ try:
228 from launchpadlib.errors import *
229 import launchpadlib
230 except ImportError:
231- print >> sys.stderr, "Usage:"
232- print >> sys.stderr, " PYTHONPATH=somebranch/lib %s" % sys.argv[0]
233+ print("Usage:", file=sys.stderr)
234+ print(" PYTHONPATH=somebranch/lib %s" % sys.argv[0], file=sys.stderr)
235 raise
236
237
238@@ -42,12 +41,12 @@ class LPSystem:
239 self.auth_file = os.path.join(home, self.auth_file_name)
240 self.credentials = Credentials()
241 self.credentials.load(open(self.auth_file))
242- print >> sys.stderr, "Loading credentials..."
243+ print("Loading credentials...", file=sys.stderr)
244 try:
245 self.launchpad = Launchpad(self.credentials, self.endpoint,
246 cache=cache_dir)
247 except launchpadlib.errors.HTTPError:
248- raise InvalidCredentials, (
249+ raise InvalidCredentials(
250 "Please remove %s and rerun %s to authenticate." % (
251 self.auth_file, sys.argv[0]))
252 except IOError:
253@@ -58,9 +57,9 @@ class LPSystem:
254 self.endpoint,
255 cache=cache_dir)
256 self.launchpad.credentials.save(open(self.auth_file, "w"))
257- print >> sys.stderr, "Credentials saved"
258- except launchpadlib.errors.HTTPError, err:
259- print >> sys.stderr, err.content
260+ print("Credentials saved", file=sys.stderr)
261+ except launchpadlib.errors.HTTPError as err:
262+ print(err.content, file=sys.stderr)
263 raise
264
265 @property
266@@ -102,6 +101,6 @@ def lp_factory(system_name, app_name='just_testing'):
267 lpinstance = systems[system_name]
268 return lpinstance(app_name).launchpad
269 except KeyError:
270- print >> sys.stderr, "System '%s' not supported." % system_name
271- print >> sys.stderr, "Use one of: ", systems.keys()
272+ print("System '%s' not supported." % system_name, file=sys.stderr)
273+ print("Use one of: ", systems.keys(), file=sys.stderr)
274 return None
275diff --git a/contrib/nopriv-api.py b/contrib/nopriv-api.py
276index abe2b28..66cadc6 100755
277--- a/contrib/nopriv-api.py
278+++ b/contrib/nopriv-api.py
279@@ -1,4 +1,4 @@
280-#!/usr/bin/python
281+#!/usr/bin/python3
282 # -*-doctest-*-
283
284 """
285@@ -6,10 +6,10 @@
286 >>> lp = lpapi.lp_factory('dev')
287
288 >>> bzr = lp.projects['bzr']
289- >>> print bzr.reviewer_whiteboard
290+ >>> print(bzr.reviewer_whiteboard)
291 tag:launchpad.net:2008:redacted
292 >>> bzr.reviewer_whiteboard = "Check on licensing"
293- >>> print bzr.reviewer_whiteboard
294+ >>> print(bzr.reviewer_whiteboard)
295 Check on licensing
296 >>> bzr.lp_save()
297 ...
298@@ -78,9 +78,9 @@ if __name__ == '__main__':
299 pass
300
301 # Create correct credentials.
302- print "Login as 'no-priv@canonical.com' in your browser."
303- print "Press <Enter> when done."
304- raw_input()
305+ print("Login as 'no-priv@canonical.com' in your browser.")
306+ print("Press <Enter> when done.")
307+ input()
308
309 # Import _pythonpath and the lpapi module. _pythonpath must
310 # precede the import of lpapi as it redefines sys.path.
311diff --git a/contrib/sample-person-api.py b/contrib/sample-person-api.py
312index a7b3488..51118b4 100755
313--- a/contrib/sample-person-api.py
314+++ b/contrib/sample-person-api.py
315@@ -1,4 +1,4 @@
316-#!/usr/bin/python
317+#!/usr/bin/python3
318 # -*-doctest-*-
319
320 """
321@@ -6,10 +6,10 @@
322 >>> lp = lpapi.lp_factory('dev')
323
324 >>> bzr = lp.projects['bzr']
325- >>> print bzr.reviewer_whiteboard
326+ >>> print(bzr.reviewer_whiteboard)
327 tag:launchpad.net:2008:redacted
328 >>> bzr.reviewer_whiteboard = "Check on licensing"
329- >>> print bzr.reviewer_whiteboard
330+ >>> print(bzr.reviewer_whiteboard)
331 Check on licensing
332 >>> bzr.lp_save()
333 ...
334@@ -78,9 +78,9 @@ if __name__ == '__main__':
335 pass
336
337 # Create correct credentials.
338- print "Login as 'test@canonical.com' in your browser."
339- print "Press <Enter> when done."
340- raw_input()
341+ print("Login as 'test@canonical.com' in your browser.")
342+ print("Press <Enter> when done.")
343+ input()
344
345 # Import _pythonpath and the lpapi module. _pythonpath must
346 # precede the import of lpapi as it redefines sys.path.
347diff --git a/contrib/upload_release_tarball.py b/contrib/upload_release_tarball.py
348index 335987a..7a51370 100755
349--- a/contrib/upload_release_tarball.py
350+++ b/contrib/upload_release_tarball.py
351@@ -1,4 +1,4 @@
352-#!/usr/bin/python
353+#!/usr/bin/python3
354 #
355 # This script uploads a tarball as a file for a (possibly new)
356 # release. It takes these command-line arguments:
357@@ -61,14 +61,14 @@ if os.path.exists(signature_path):
358 else:
359 # There is no signature.
360 if options.force:
361- print ('WARNING: Signature file "%s" is not present. Continuing '
362- 'without it.' % signature_path)
363+ print('WARNING: Signature file "%s" is not present. Continuing '
364+ 'without it.' % signature_path)
365 signature_name = None
366 signature = None
367 else:
368- print 'ERROR: Signature file "%s" is not present.' % signature_path
369- print 'Run "gpg --armor --sign --detach-sig" on the tarball.'
370- print 'Or re-run this script with the --force option.'
371+ print('ERROR: Signature file "%s" is not present.' % signature_path)
372+ print('Run "gpg --armor --sign --detach-sig" on the tarball.')
373+ print('Or re-run this script with the --force option.')
374 sys.exit(-1)
375
376 # Now we interact with Launchpad.
377@@ -92,8 +92,8 @@ series = matching_series[0]
378 matching_milestones = [milestone for milestone in series.active_milestones
379 if milestone.name == version_name]
380 if len(matching_milestones) == 0:
381- print 'No milestone "%s" for %s/%s. Creating it.' % (
382- version_name, project.name, series.name)
383+ print('No milestone "%s" for %s/%s. Creating it.' % (
384+ version_name, project.name, series.name))
385 milestone = series.newMilestone(name=version_name)
386 else:
387 milestone = matching_milestones[0]
388@@ -106,8 +106,8 @@ if len(matching_releases) == 0:
389 #
390 # The changelog and release notes could go into this operation
391 # invocation.
392- print "No release for %s/%s/%s. Creating it." % (
393- project.name, series.name, version_name)
394+ print("No release for %s/%s/%s. Creating it." % (
395+ project.name, series.name, version_name))
396 release = milestone.createProductRelease(
397 date_released=datetime.now(pytz.UTC))
398 else:
399@@ -124,5 +124,5 @@ if signature is not None:
400 result = release.add_file(**kwargs)
401
402 # We know this succeeded because add_file didn't raise an exception.
403-print "Success!"
404-print result.self_link
405+print("Success!")
406+print(result.self_link)
407diff --git a/pyproject.toml b/pyproject.toml
408index 486bbe6..1f331da 100644
409--- a/pyproject.toml
410+++ b/pyproject.toml
411@@ -1,3 +1,3 @@
412 [tool.black]
413 line-length = 79
414-target-version = ['py27']
415+target-version = ['py35']
416diff --git a/setup.py b/setup.py
417index 0f4d391..38bf7b5 100755
418--- a/setup.py
419+++ b/setup.py
420@@ -1,4 +1,4 @@
421-#!/usr/bin/env python
422+#!/usr/bin/env python3
423
424 # Copyright 2008-2022 Canonical Ltd.
425 #
426@@ -46,7 +46,6 @@ install_requires = [
427 'importlib-metadata; python_version < "3.8"',
428 "lazr.restfulclient>=0.14.2",
429 "lazr.uri",
430- "six",
431 ]
432
433 setup(
434@@ -64,6 +63,7 @@ setup(
435 description=open("README.rst").readline().strip(),
436 long_description=generate("src/launchpadlib/docs/index.rst", "NEWS.rst"),
437 license="LGPL v3",
438+ python_requires=">=3.5",
439 install_requires=install_requires,
440 url="https://help.launchpad.net/API/launchpadlib",
441 project_urls={
442@@ -99,7 +99,6 @@ setup(
443 # Dependencies only needed by launchpadlib's own tests.
444 "test": [
445 "coverage",
446- 'mock; python_version < "3"',
447 "pytest",
448 ],
449 },
450diff --git a/src/launchpadlib/apps.py b/src/launchpadlib/apps.py
451index e163a7b..df7720f 100644
452--- a/src/launchpadlib/apps.py
453+++ b/src/launchpadlib/apps.py
454@@ -30,7 +30,7 @@ from launchpadlib.credentials import Credentials
455 from launchpadlib.uris import lookup_web_root
456
457
458-class RequestTokenApp(object):
459+class RequestTokenApp:
460 """An application that creates request tokens."""
461
462 def __init__(self, web_root, consumer_name, context):
463diff --git a/src/launchpadlib/bin/launchpad-request-token b/src/launchpadlib/bin/launchpad-request-token
464index 8205b45..c81a23f 100755
465--- a/src/launchpadlib/bin/launchpad-request-token
466+++ b/src/launchpadlib/bin/launchpad-request-token
467@@ -1,4 +1,4 @@
468-#!/usr/bin/python
469+#!/usr/bin/python3
470
471 # Copyright 2009 Canonical Ltd.
472
473@@ -22,10 +22,6 @@ This script will create a Launchpad request token and print to STDOUT
474 some JSON data about the token and the available access levels.
475 """
476
477-from __future__ import print_function
478-
479-__metaclass__ = type
480-
481 from optparse import OptionParser
482 from launchpadlib.apps import RequestTokenApp
483
484diff --git a/src/launchpadlib/credentials.py b/src/launchpadlib/credentials.py
485index 5789fc0..abf55a2 100644
486--- a/src/launchpadlib/credentials.py
487+++ b/src/launchpadlib/credentials.py
488@@ -14,11 +14,8 @@
489 # You should have received a copy of the GNU Lesser General Public License
490 # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
491
492-from __future__ import print_function
493-
494 """launchpadlib credentials and authentication support."""
495
496-__metaclass__ = type
497 __all__ = [
498 "AccessToken",
499 "AnonymousAccessToken",
500@@ -29,40 +26,20 @@ __all__ = [
501 "Credentials",
502 ]
503
504-try:
505- from cStringIO import StringIO
506-except ImportError:
507- from io import StringIO
508-
509+from base64 import (
510+ b64decode,
511+ b64encode,
512+)
513 import httplib2
514+from io import StringIO
515 import json
516 import os
517 from select import select
518 import stat
519 from sys import stdin
520 import time
521-
522-try:
523- from urllib.parse import urlencode
524-except ImportError:
525- from urllib import urlencode
526-try:
527- from urllib.parse import urljoin
528-except ImportError:
529- from urlparse import urljoin
530+from urllib.parse import urlencode, urljoin, parse_qs
531 import webbrowser
532-from base64 import (
533- b64decode,
534- b64encode,
535-)
536-
537-from six.moves.urllib.parse import parse_qs
538-
539-if bytes is str:
540- # Python 2
541- unicode_type = unicode # noqa: F821
542-else:
543- unicode_type = str
544
545 from lazr.restfulclient.errors import HTTPError
546 from lazr.restfulclient.authorize.oauth import (
547@@ -135,7 +112,7 @@ class Credentials(OAuthAuthorizer):
548 sio = StringIO()
549 self.save(sio)
550 serialized = sio.getvalue()
551- if isinstance(serialized, unicode_type):
552+ if isinstance(serialized, str):
553 serialized = serialized.encode("utf-8")
554 return serialized
555
556@@ -146,7 +123,7 @@ class Credentials(OAuthAuthorizer):
557 This should probably be moved into OAuthAuthorizer.
558 """
559 credentials = cls()
560- if not isinstance(value, unicode_type):
561+ if not isinstance(value, str):
562 value = value.decode("utf-8")
563 credentials.load(StringIO(value))
564 return credentials
565@@ -255,7 +232,7 @@ class AccessToken(_AccessToken):
566 @classmethod
567 def from_string(cls, query_string):
568 """Create and return a new `AccessToken` from the given string."""
569- if not isinstance(query_string, unicode_type):
570+ if not isinstance(query_string, str):
571 query_string = query_string.decode("utf-8")
572 params = parse_qs(query_string, keep_blank_values=False)
573 key = params["oauth_token"]
574@@ -280,10 +257,10 @@ class AnonymousAccessToken(_AccessToken):
575 """
576
577 def __init__(self):
578- super(AnonymousAccessToken, self).__init__("", "")
579+ super().__init__("", "")
580
581
582-class CredentialStore(object):
583+class CredentialStore:
584 """Store OAuth credentials locally.
585
586 This is a generic superclass. To implement a specific way of
587@@ -369,7 +346,7 @@ class KeyringCredentialStore(CredentialStore):
588 B64MARKER = b"<B64>"
589
590 def __init__(self, credential_save_failed=None, fallback=False):
591- super(KeyringCredentialStore, self).__init__(credential_save_failed)
592+ super().__init__(credential_save_failed)
593 self._fallback = None
594 if fallback:
595 self._fallback = MemoryCredentialStore(credential_save_failed)
596@@ -438,7 +415,7 @@ class KeyringCredentialStore(CredentialStore):
597 else:
598 raise
599 if credential_string is not None:
600- if isinstance(credential_string, unicode_type):
601+ if isinstance(credential_string, str):
602 credential_string = credential_string.encode("utf8")
603 if credential_string.startswith(self.B64MARKER):
604 try:
605@@ -468,9 +445,7 @@ class UnencryptedFileCredentialStore(CredentialStore):
606 """
607
608 def __init__(self, filename, credential_save_failed=None):
609- super(UnencryptedFileCredentialStore, self).__init__(
610- credential_save_failed
611- )
612+ super().__init__(credential_save_failed)
613 self.filename = filename
614
615 def do_save(self, credentials, unique_key):
616@@ -495,7 +470,7 @@ class MemoryCredentialStore(CredentialStore):
617 """
618
619 def __init__(self, credential_save_failed=None):
620- super(MemoryCredentialStore, self).__init__(credential_save_failed)
621+ super().__init__(credential_save_failed)
622 self._credentials = {}
623
624 def do_save(self, credentials, unique_key):
625@@ -507,7 +482,7 @@ class MemoryCredentialStore(CredentialStore):
626 return self._credentials.get(unique_key)
627
628
629-class RequestTokenAuthorizationEngine(object):
630+class RequestTokenAuthorizationEngine:
631 """The superclass of all request token authorizers.
632
633 This base class does not implement request token authorization,
634@@ -774,15 +749,13 @@ class AuthorizeRequestTokenWithBrowser(AuthorizeRequestTokenWithURL):
635 # It doesn't look like we're doing anything here, but we
636 # are discarding the passed-in values for consumer_name and
637 # allow_access_levels.
638- super(AuthorizeRequestTokenWithBrowser, self).__init__(
639+ super().__init__(
640 service_root, application_name, None, credential_save_failed
641 )
642
643 def notify_end_user_authorization_url(self, authorization_url):
644 """Notify the end-user of the URL."""
645- super(
646- AuthorizeRequestTokenWithBrowser, self
647- ).notify_end_user_authorization_url(authorization_url)
648+ super().notify_end_user_authorization_url(authorization_url)
649
650 try:
651 browser_obj = webbrowser.get()
652diff --git a/src/launchpadlib/docs/conf.py b/src/launchpadlib/docs/conf.py
653index 268a8ce..235e7a9 100644
654--- a/src/launchpadlib/docs/conf.py
655+++ b/src/launchpadlib/docs/conf.py
656@@ -1,5 +1,3 @@
657-# -*- coding: utf-8 -*-
658-#
659 # launchpadlib documentation build configuration file, created by
660 # sphinx-quickstart on Tue Nov 5 23:48:15 2019.
661 #
662@@ -47,9 +45,9 @@ source_suffix = ".rst"
663 master_doc = "index"
664
665 # General information about the project.
666-project = u"launchpadlib"
667-copyright = u"2008-2019, Canonical Ltd."
668-author = u"LAZR Developers <lazr-developers@lists.launchpad.net>"
669+project = "launchpadlib"
670+copyright = "2008-2019, Canonical Ltd."
671+author = "LAZR Developers <lazr-developers@lists.launchpad.net>"
672
673 # The version info for the project you're documenting, acts as replacement for
674 # |version| and |release|, also used in various other places throughout the
675@@ -140,8 +138,8 @@ latex_documents = [
676 (
677 master_doc,
678 "launchpadlib.tex",
679- u"launchpadlib Documentation",
680- u"LAZR Developers \\textless{}lazr-developers@lists.launchpad.net\\textgreater{}", # noqa: E501
681+ "launchpadlib Documentation",
682+ "LAZR Developers \\textless{}lazr-developers@lists.launchpad.net\\textgreater{}", # noqa: E501
683 "manual",
684 ),
685 ]
686@@ -152,7 +150,7 @@ latex_documents = [
687 # One entry per manual page. List of tuples
688 # (source start file, name, description, authors, manual section).
689 man_pages = [
690- (master_doc, "launchpadlib", u"launchpadlib Documentation", [author], 1)
691+ (master_doc, "launchpadlib", "launchpadlib Documentation", [author], 1)
692 ]
693
694
695@@ -165,7 +163,7 @@ texinfo_documents = [
696 (
697 master_doc,
698 "launchpadlib",
699- u"launchpadlib Documentation",
700+ "launchpadlib Documentation",
701 author,
702 "launchpadlib",
703 "One line description of project.",
704diff --git a/src/launchpadlib/launchpad.py b/src/launchpadlib/launchpad.py
705index d8c6ba6..6b8cea6 100644
706--- a/src/launchpadlib/launchpad.py
707+++ b/src/launchpadlib/launchpad.py
708@@ -16,18 +16,13 @@
709
710 """Root Launchpad API class."""
711
712-__metaclass__ = type
713 __all__ = [
714 "Launchpad",
715 ]
716
717 import errno
718 import os
719-
720-try:
721- from urllib.parse import urlsplit
722-except ImportError:
723- from urlparse import urlsplit
724+from urllib.parse import urlsplit
725 import warnings
726
727 try:
728@@ -130,7 +125,7 @@ class LaunchpadOAuthAwareHttp(RestfulHttp):
729 def __init__(self, launchpad, authorization_engine, *args):
730 self.launchpad = launchpad
731 self.authorization_engine = authorization_engine
732- super(LaunchpadOAuthAwareHttp, self).__init__(*args)
733+ super().__init__(*args)
734
735 def _bad_oauth_token(self, response, content):
736 """Helper method to detect an error caused by a bad OAuth token."""
737@@ -141,9 +136,7 @@ class LaunchpadOAuthAwareHttp(RestfulHttp):
738 )
739
740 def _request(self, *args):
741- response, content = super(LaunchpadOAuthAwareHttp, self)._request(
742- *args
743- )
744+ response, content = super()._request(*args)
745 return self.retry_on_bad_token(response, content, *args)
746
747 def retry_on_bad_token(self, response, content, *args):
748@@ -227,7 +220,7 @@ class Launchpad(ServiceRoot):
749 # case we need to authorize a new token during use.
750 self.authorization_engine = authorization_engine
751
752- super(Launchpad, self).__init__(
753+ super().__init__(
754 credentials, service_root, cache, timeout, proxy_info, version
755 )
756
757diff --git a/src/launchpadlib/testing/helpers.py b/src/launchpadlib/testing/helpers.py
758index e625f5e..c9a7cec 100644
759--- a/src/launchpadlib/testing/helpers.py
760+++ b/src/launchpadlib/testing/helpers.py
761@@ -18,8 +18,6 @@
762
763 """launchpadlib testing helpers."""
764
765-
766-__metaclass__ = type
767 __all__ = [
768 "BadSaveKeyring",
769 "fake_keyring",
770@@ -64,7 +62,7 @@ class NoNetworkAuthorizationEngine(RequestTokenAuthorizationEngine):
771 ACCESS_TOKEN_KEY = "access_key:84"
772
773 def __init__(self, *args, **kwargs):
774- super(NoNetworkAuthorizationEngine, self).__init__(*args, **kwargs)
775+ super().__init__(*args, **kwargs)
776 # Set up some instrumentation.
777 self.request_tokens_obtained = 0
778 self.access_tokens_obtained = 0
779@@ -144,7 +142,7 @@ class TestableLaunchpad(Launchpad):
780 generally pass in fully-formed Credentials objects.
781 :param service_root: Defaults to 'test_dev'.
782 """
783- super(TestableLaunchpad, self).__init__(
784+ super().__init__(
785 credentials,
786 authorization_engine,
787 credential_store,
788diff --git a/src/launchpadlib/testing/launchpad.py b/src/launchpadlib/testing/launchpad.py
789index aa2ee6d..3edaad4 100644
790--- a/src/launchpadlib/testing/launchpad.py
791+++ b/src/launchpadlib/testing/launchpad.py
792@@ -65,23 +65,15 @@ Where 'https://api.launchpad.net/devel/' is the URL for the WADL file, found
793 also in the WADL file itelf.
794 """
795
796+from collections.abc import Callable
797 from datetime import datetime
798
799-try:
800- from collections.abc import Callable
801-except ImportError:
802- from collections import Callable
803-import sys
804-
805-if sys.version_info[0] >= 3:
806- basestring = str
807-
808
809 class IntegrityError(Exception):
810 """Raised when bad sample data is used with a L{FakeLaunchpad} instance."""
811
812
813-class FakeLaunchpad(object):
814+class FakeLaunchpad:
815 """A fake Launchpad API class for unit tests that depend on L{Launchpad}.
816
817 @param application: A C{wadllib.application.Application} instance for a
818@@ -188,7 +180,7 @@ def wadl_tag(tag_name):
819 return "{http://research.sun.com/wadl/2006/10}" + tag_name
820
821
822-class FakeResource(object):
823+class FakeResource:
824 """
825 Represents valid sample data on L{FakeLaunchpad} instances.
826
827@@ -434,7 +426,7 @@ class FakeResource(object):
828 if param is None:
829 raise IntegrityError("%s not found" % name)
830 if param.type is None:
831- if not isinstance(value, basestring):
832+ if not isinstance(value, str):
833 raise IntegrityError(
834 "%s is not a str or unicode for %s" % (value, name)
835 )
836@@ -594,7 +586,7 @@ class FakeRoot(FakeResource):
837 resource_type = application.get_resource_type(
838 application.markup_url + "#service-root"
839 )
840- super(FakeRoot, self).__init__(application, resource_type)
841+ super().__init__(application, resource_type)
842
843
844 class FakeEntry(FakeResource):
845@@ -612,9 +604,7 @@ class FakeCollection(FakeResource):
846 name=None,
847 child_resource_type=None,
848 ):
849- super(FakeCollection, self).__init__(
850- application, resource_type, values
851- )
852+ super().__init__(application, resource_type, values)
853 self.__dict__.update(
854 {"_name": name, "_child_resource_type": child_resource_type}
855 )
856diff --git a/src/launchpadlib/testing/tests/test_launchpad.py b/src/launchpadlib/testing/tests/test_launchpad.py
857index c988d2b..cff4dd2 100644
858--- a/src/launchpadlib/testing/tests/test_launchpad.py
859+++ b/src/launchpadlib/testing/tests/test_launchpad.py
860@@ -160,8 +160,8 @@ class FakeLaunchpadTest(ResourcedTestCase):
861 dicts that represent objects. Plain string values can be represented
862 as C{unicode} strings.
863 """
864- self.launchpad.me = dict(name=u"foo")
865- self.assertEqual(u"foo", self.launchpad.me.name)
866+ self.launchpad.me = dict(name="foo")
867+ self.assertEqual("foo", self.launchpad.me.name)
868
869 def test_datetime_property(self):
870 """
871diff --git a/src/launchpadlib/tests/test_credential_store.py b/src/launchpadlib/tests/test_credential_store.py
872index 3049fbe..b6fe597 100644
873--- a/src/launchpadlib/tests/test_credential_store.py
874+++ b/src/launchpadlib/tests/test_credential_store.py
875@@ -169,9 +169,7 @@ class TestKeyringCredentialStore(CredentialStoreTestCase):
876 # handled correctly. (See bug lp:877374)
877 class UnicodeInMemoryKeyring(InMemoryKeyring):
878 def get_password(self, service, username):
879- password = super(UnicodeInMemoryKeyring, self).get_password(
880- service, username
881- )
882+ password = super().get_password(service, username)
883 if isinstance(password, unicode_type):
884 password = password.encode("utf-8")
885 return password
886@@ -194,9 +192,7 @@ class TestKeyringCredentialStore(CredentialStoreTestCase):
887
888 class UnencodedInMemoryKeyring(InMemoryKeyring):
889 def get_password(self, service, username):
890- pw = super(UnencodedInMemoryKeyring, self).get_password(
891- service, username
892- )
893+ pw = super().get_password(service, username)
894 return b64decode(pw[5:])
895
896 self.keyring = UnencodedInMemoryKeyring()
897diff --git a/src/launchpadlib/tests/test_http.py b/src/launchpadlib/tests/test_http.py
898index 6924e4f..e2287f6 100644
899--- a/src/launchpadlib/tests/test_http.py
900+++ b/src/launchpadlib/tests/test_http.py
901@@ -17,15 +17,10 @@
902 """Tests for the LaunchpadOAuthAwareHTTP class."""
903
904 from collections import deque
905-from json import dumps
906+from json import dumps, JSONDecodeError
907 import tempfile
908 import unittest
909
910-try:
911- from json import JSONDecodeError
912-except ImportError:
913- JSONDecodeError = ValueError
914-
915 from launchpadlib.errors import Unauthorized
916 from launchpadlib.credentials import UnencryptedFileCredentialStore
917 from launchpadlib.launchpad import (
918@@ -75,7 +70,7 @@ class SimulatedResponsesHttp(LaunchpadOAuthAwareHttp):
919 :param responses: A list of HttpResponse objects to use
920 in response to requests.
921 """
922- super(SimulatedResponsesHttp, self).__init__(*args)
923+ super().__init__(*args)
924 self.sent_responses = []
925 self.unsent_responses = responses
926 self.cache = None
927diff --git a/src/launchpadlib/tests/test_launchpad.py b/src/launchpadlib/tests/test_launchpad.py
928index 66462c5..47410d5 100644
929--- a/src/launchpadlib/tests/test_launchpad.py
930+++ b/src/launchpadlib/tests/test_launchpad.py
931@@ -16,8 +16,6 @@
932
933 """Tests for the Launchpad class."""
934
935-__metaclass__ = type
936-
937 from contextlib import contextmanager
938 import os
939 import shutil
940@@ -25,11 +23,7 @@ import socket
941 import stat
942 import tempfile
943 import unittest
944-
945-try:
946- from unittest.mock import patch
947-except ImportError:
948- from mock import patch
949+from unittest.mock import patch
950 import warnings
951
952 from lazr.restfulclient.resource import ServiceRoot
953@@ -351,11 +345,11 @@ class TestLaunchpadLoginWith(KeyringTest):
954 """Tests for Launchpad.login_with()."""
955
956 def setUp(self):
957- super(TestLaunchpadLoginWith, self).setUp()
958+ super().setUp()
959 self.temp_dir = tempfile.mkdtemp()
960
961 def tearDown(self):
962- super(TestLaunchpadLoginWith, self).tearDown()
963+ super().tearDown()
964 shutil.rmtree(self.temp_dir)
965
966 def test_dirs_created(self):
967diff --git a/src/launchpadlib/uris.py b/src/launchpadlib/uris.py
968index dda802e..1bcc8f6 100644
969--- a/src/launchpadlib/uris.py
970+++ b/src/launchpadlib/uris.py
971@@ -20,18 +20,15 @@ The code in this module lets users say "staging" when they mean
972 "https://api.staging.launchpad.net/".
973 """
974
975-__metaclass__ = type
976 __all__ = [
977 "lookup_service_root",
978 "lookup_web_root",
979 "web_root_for_service_root",
980 ]
981-try:
982- from urllib.parse import urlparse
983-except ImportError:
984- from urlparse import urlparse
985
986+from urllib.parse import urlparse
987 import warnings
988+
989 from lazr.uri import URI
990
991 LPNET_SERVICE_ROOT = "https://api.launchpad.net/"
992diff --git a/tox.ini b/tox.ini
993index aaa62c9..ecde674 100644
994--- a/tox.ini
995+++ b/tox.ini
996@@ -1,13 +1,13 @@
997 [tox]
998 envlist =
999- py27,py35,py36,py37,py38,py39,py310,py311,lint,docs
1000+ py35,py36,py37,py38,py39,py310,py311,lint,docs
1001 requires = virtualenv<20.22
1002
1003 [testenv]
1004 deps =
1005 .[test,testing]
1006 commands =
1007- coverage run -m pytest src {posargs}
1008+ pytest src {posargs}
1009
1010 [testenv:lint]
1011 # necessary to build the woke linter
1012@@ -34,17 +34,16 @@ deps =
1013 .[docs]
1014
1015 [testenv:coverage]
1016-description = usage: tox -e py27,py35,py310,coverage
1017 basepython =
1018 python3
1019 deps =
1020 .[testing,test]
1021 commands =
1022+ coverage erase
1023+ coverage run -m pytest src {posargs}
1024 coverage combine
1025 coverage html
1026- coverage report -m --fail-under=89
1027-depends =
1028- py27,py35,py310
1029+ coverage report -m --fail-under=91
1030
1031 [coverage:run]
1032 parallel=True

Subscribers

People subscribed via source and target branches