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: 1024 lines (+125/-198)
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/-2)
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) :

Unmerged commits

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..7bbf7ce 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={
442diff --git a/src/launchpadlib/apps.py b/src/launchpadlib/apps.py
443index e163a7b..df7720f 100644
444--- a/src/launchpadlib/apps.py
445+++ b/src/launchpadlib/apps.py
446@@ -30,7 +30,7 @@ from launchpadlib.credentials import Credentials
447 from launchpadlib.uris import lookup_web_root
448
449
450-class RequestTokenApp(object):
451+class RequestTokenApp:
452 """An application that creates request tokens."""
453
454 def __init__(self, web_root, consumer_name, context):
455diff --git a/src/launchpadlib/bin/launchpad-request-token b/src/launchpadlib/bin/launchpad-request-token
456index 8205b45..c81a23f 100755
457--- a/src/launchpadlib/bin/launchpad-request-token
458+++ b/src/launchpadlib/bin/launchpad-request-token
459@@ -1,4 +1,4 @@
460-#!/usr/bin/python
461+#!/usr/bin/python3
462
463 # Copyright 2009 Canonical Ltd.
464
465@@ -22,10 +22,6 @@ This script will create a Launchpad request token and print to STDOUT
466 some JSON data about the token and the available access levels.
467 """
468
469-from __future__ import print_function
470-
471-__metaclass__ = type
472-
473 from optparse import OptionParser
474 from launchpadlib.apps import RequestTokenApp
475
476diff --git a/src/launchpadlib/credentials.py b/src/launchpadlib/credentials.py
477index 5789fc0..abf55a2 100644
478--- a/src/launchpadlib/credentials.py
479+++ b/src/launchpadlib/credentials.py
480@@ -14,11 +14,8 @@
481 # You should have received a copy of the GNU Lesser General Public License
482 # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
483
484-from __future__ import print_function
485-
486 """launchpadlib credentials and authentication support."""
487
488-__metaclass__ = type
489 __all__ = [
490 "AccessToken",
491 "AnonymousAccessToken",
492@@ -29,40 +26,20 @@ __all__ = [
493 "Credentials",
494 ]
495
496-try:
497- from cStringIO import StringIO
498-except ImportError:
499- from io import StringIO
500-
501+from base64 import (
502+ b64decode,
503+ b64encode,
504+)
505 import httplib2
506+from io import StringIO
507 import json
508 import os
509 from select import select
510 import stat
511 from sys import stdin
512 import time
513-
514-try:
515- from urllib.parse import urlencode
516-except ImportError:
517- from urllib import urlencode
518-try:
519- from urllib.parse import urljoin
520-except ImportError:
521- from urlparse import urljoin
522+from urllib.parse import urlencode, urljoin, parse_qs
523 import webbrowser
524-from base64 import (
525- b64decode,
526- b64encode,
527-)
528-
529-from six.moves.urllib.parse import parse_qs
530-
531-if bytes is str:
532- # Python 2
533- unicode_type = unicode # noqa: F821
534-else:
535- unicode_type = str
536
537 from lazr.restfulclient.errors import HTTPError
538 from lazr.restfulclient.authorize.oauth import (
539@@ -135,7 +112,7 @@ class Credentials(OAuthAuthorizer):
540 sio = StringIO()
541 self.save(sio)
542 serialized = sio.getvalue()
543- if isinstance(serialized, unicode_type):
544+ if isinstance(serialized, str):
545 serialized = serialized.encode("utf-8")
546 return serialized
547
548@@ -146,7 +123,7 @@ class Credentials(OAuthAuthorizer):
549 This should probably be moved into OAuthAuthorizer.
550 """
551 credentials = cls()
552- if not isinstance(value, unicode_type):
553+ if not isinstance(value, str):
554 value = value.decode("utf-8")
555 credentials.load(StringIO(value))
556 return credentials
557@@ -255,7 +232,7 @@ class AccessToken(_AccessToken):
558 @classmethod
559 def from_string(cls, query_string):
560 """Create and return a new `AccessToken` from the given string."""
561- if not isinstance(query_string, unicode_type):
562+ if not isinstance(query_string, str):
563 query_string = query_string.decode("utf-8")
564 params = parse_qs(query_string, keep_blank_values=False)
565 key = params["oauth_token"]
566@@ -280,10 +257,10 @@ class AnonymousAccessToken(_AccessToken):
567 """
568
569 def __init__(self):
570- super(AnonymousAccessToken, self).__init__("", "")
571+ super().__init__("", "")
572
573
574-class CredentialStore(object):
575+class CredentialStore:
576 """Store OAuth credentials locally.
577
578 This is a generic superclass. To implement a specific way of
579@@ -369,7 +346,7 @@ class KeyringCredentialStore(CredentialStore):
580 B64MARKER = b"<B64>"
581
582 def __init__(self, credential_save_failed=None, fallback=False):
583- super(KeyringCredentialStore, self).__init__(credential_save_failed)
584+ super().__init__(credential_save_failed)
585 self._fallback = None
586 if fallback:
587 self._fallback = MemoryCredentialStore(credential_save_failed)
588@@ -438,7 +415,7 @@ class KeyringCredentialStore(CredentialStore):
589 else:
590 raise
591 if credential_string is not None:
592- if isinstance(credential_string, unicode_type):
593+ if isinstance(credential_string, str):
594 credential_string = credential_string.encode("utf8")
595 if credential_string.startswith(self.B64MARKER):
596 try:
597@@ -468,9 +445,7 @@ class UnencryptedFileCredentialStore(CredentialStore):
598 """
599
600 def __init__(self, filename, credential_save_failed=None):
601- super(UnencryptedFileCredentialStore, self).__init__(
602- credential_save_failed
603- )
604+ super().__init__(credential_save_failed)
605 self.filename = filename
606
607 def do_save(self, credentials, unique_key):
608@@ -495,7 +470,7 @@ class MemoryCredentialStore(CredentialStore):
609 """
610
611 def __init__(self, credential_save_failed=None):
612- super(MemoryCredentialStore, self).__init__(credential_save_failed)
613+ super().__init__(credential_save_failed)
614 self._credentials = {}
615
616 def do_save(self, credentials, unique_key):
617@@ -507,7 +482,7 @@ class MemoryCredentialStore(CredentialStore):
618 return self._credentials.get(unique_key)
619
620
621-class RequestTokenAuthorizationEngine(object):
622+class RequestTokenAuthorizationEngine:
623 """The superclass of all request token authorizers.
624
625 This base class does not implement request token authorization,
626@@ -774,15 +749,13 @@ class AuthorizeRequestTokenWithBrowser(AuthorizeRequestTokenWithURL):
627 # It doesn't look like we're doing anything here, but we
628 # are discarding the passed-in values for consumer_name and
629 # allow_access_levels.
630- super(AuthorizeRequestTokenWithBrowser, self).__init__(
631+ super().__init__(
632 service_root, application_name, None, credential_save_failed
633 )
634
635 def notify_end_user_authorization_url(self, authorization_url):
636 """Notify the end-user of the URL."""
637- super(
638- AuthorizeRequestTokenWithBrowser, self
639- ).notify_end_user_authorization_url(authorization_url)
640+ super().notify_end_user_authorization_url(authorization_url)
641
642 try:
643 browser_obj = webbrowser.get()
644diff --git a/src/launchpadlib/docs/conf.py b/src/launchpadlib/docs/conf.py
645index 268a8ce..235e7a9 100644
646--- a/src/launchpadlib/docs/conf.py
647+++ b/src/launchpadlib/docs/conf.py
648@@ -1,5 +1,3 @@
649-# -*- coding: utf-8 -*-
650-#
651 # launchpadlib documentation build configuration file, created by
652 # sphinx-quickstart on Tue Nov 5 23:48:15 2019.
653 #
654@@ -47,9 +45,9 @@ source_suffix = ".rst"
655 master_doc = "index"
656
657 # General information about the project.
658-project = u"launchpadlib"
659-copyright = u"2008-2019, Canonical Ltd."
660-author = u"LAZR Developers <lazr-developers@lists.launchpad.net>"
661+project = "launchpadlib"
662+copyright = "2008-2019, Canonical Ltd."
663+author = "LAZR Developers <lazr-developers@lists.launchpad.net>"
664
665 # The version info for the project you're documenting, acts as replacement for
666 # |version| and |release|, also used in various other places throughout the
667@@ -140,8 +138,8 @@ latex_documents = [
668 (
669 master_doc,
670 "launchpadlib.tex",
671- u"launchpadlib Documentation",
672- u"LAZR Developers \\textless{}lazr-developers@lists.launchpad.net\\textgreater{}", # noqa: E501
673+ "launchpadlib Documentation",
674+ "LAZR Developers \\textless{}lazr-developers@lists.launchpad.net\\textgreater{}", # noqa: E501
675 "manual",
676 ),
677 ]
678@@ -152,7 +150,7 @@ latex_documents = [
679 # One entry per manual page. List of tuples
680 # (source start file, name, description, authors, manual section).
681 man_pages = [
682- (master_doc, "launchpadlib", u"launchpadlib Documentation", [author], 1)
683+ (master_doc, "launchpadlib", "launchpadlib Documentation", [author], 1)
684 ]
685
686
687@@ -165,7 +163,7 @@ texinfo_documents = [
688 (
689 master_doc,
690 "launchpadlib",
691- u"launchpadlib Documentation",
692+ "launchpadlib Documentation",
693 author,
694 "launchpadlib",
695 "One line description of project.",
696diff --git a/src/launchpadlib/launchpad.py b/src/launchpadlib/launchpad.py
697index d8c6ba6..6b8cea6 100644
698--- a/src/launchpadlib/launchpad.py
699+++ b/src/launchpadlib/launchpad.py
700@@ -16,18 +16,13 @@
701
702 """Root Launchpad API class."""
703
704-__metaclass__ = type
705 __all__ = [
706 "Launchpad",
707 ]
708
709 import errno
710 import os
711-
712-try:
713- from urllib.parse import urlsplit
714-except ImportError:
715- from urlparse import urlsplit
716+from urllib.parse import urlsplit
717 import warnings
718
719 try:
720@@ -130,7 +125,7 @@ class LaunchpadOAuthAwareHttp(RestfulHttp):
721 def __init__(self, launchpad, authorization_engine, *args):
722 self.launchpad = launchpad
723 self.authorization_engine = authorization_engine
724- super(LaunchpadOAuthAwareHttp, self).__init__(*args)
725+ super().__init__(*args)
726
727 def _bad_oauth_token(self, response, content):
728 """Helper method to detect an error caused by a bad OAuth token."""
729@@ -141,9 +136,7 @@ class LaunchpadOAuthAwareHttp(RestfulHttp):
730 )
731
732 def _request(self, *args):
733- response, content = super(LaunchpadOAuthAwareHttp, self)._request(
734- *args
735- )
736+ response, content = super()._request(*args)
737 return self.retry_on_bad_token(response, content, *args)
738
739 def retry_on_bad_token(self, response, content, *args):
740@@ -227,7 +220,7 @@ class Launchpad(ServiceRoot):
741 # case we need to authorize a new token during use.
742 self.authorization_engine = authorization_engine
743
744- super(Launchpad, self).__init__(
745+ super().__init__(
746 credentials, service_root, cache, timeout, proxy_info, version
747 )
748
749diff --git a/src/launchpadlib/testing/helpers.py b/src/launchpadlib/testing/helpers.py
750index e625f5e..c9a7cec 100644
751--- a/src/launchpadlib/testing/helpers.py
752+++ b/src/launchpadlib/testing/helpers.py
753@@ -18,8 +18,6 @@
754
755 """launchpadlib testing helpers."""
756
757-
758-__metaclass__ = type
759 __all__ = [
760 "BadSaveKeyring",
761 "fake_keyring",
762@@ -64,7 +62,7 @@ class NoNetworkAuthorizationEngine(RequestTokenAuthorizationEngine):
763 ACCESS_TOKEN_KEY = "access_key:84"
764
765 def __init__(self, *args, **kwargs):
766- super(NoNetworkAuthorizationEngine, self).__init__(*args, **kwargs)
767+ super().__init__(*args, **kwargs)
768 # Set up some instrumentation.
769 self.request_tokens_obtained = 0
770 self.access_tokens_obtained = 0
771@@ -144,7 +142,7 @@ class TestableLaunchpad(Launchpad):
772 generally pass in fully-formed Credentials objects.
773 :param service_root: Defaults to 'test_dev'.
774 """
775- super(TestableLaunchpad, self).__init__(
776+ super().__init__(
777 credentials,
778 authorization_engine,
779 credential_store,
780diff --git a/src/launchpadlib/testing/launchpad.py b/src/launchpadlib/testing/launchpad.py
781index aa2ee6d..3edaad4 100644
782--- a/src/launchpadlib/testing/launchpad.py
783+++ b/src/launchpadlib/testing/launchpad.py
784@@ -65,23 +65,15 @@ Where 'https://api.launchpad.net/devel/' is the URL for the WADL file, found
785 also in the WADL file itelf.
786 """
787
788+from collections.abc import Callable
789 from datetime import datetime
790
791-try:
792- from collections.abc import Callable
793-except ImportError:
794- from collections import Callable
795-import sys
796-
797-if sys.version_info[0] >= 3:
798- basestring = str
799-
800
801 class IntegrityError(Exception):
802 """Raised when bad sample data is used with a L{FakeLaunchpad} instance."""
803
804
805-class FakeLaunchpad(object):
806+class FakeLaunchpad:
807 """A fake Launchpad API class for unit tests that depend on L{Launchpad}.
808
809 @param application: A C{wadllib.application.Application} instance for a
810@@ -188,7 +180,7 @@ def wadl_tag(tag_name):
811 return "{http://research.sun.com/wadl/2006/10}" + tag_name
812
813
814-class FakeResource(object):
815+class FakeResource:
816 """
817 Represents valid sample data on L{FakeLaunchpad} instances.
818
819@@ -434,7 +426,7 @@ class FakeResource(object):
820 if param is None:
821 raise IntegrityError("%s not found" % name)
822 if param.type is None:
823- if not isinstance(value, basestring):
824+ if not isinstance(value, str):
825 raise IntegrityError(
826 "%s is not a str or unicode for %s" % (value, name)
827 )
828@@ -594,7 +586,7 @@ class FakeRoot(FakeResource):
829 resource_type = application.get_resource_type(
830 application.markup_url + "#service-root"
831 )
832- super(FakeRoot, self).__init__(application, resource_type)
833+ super().__init__(application, resource_type)
834
835
836 class FakeEntry(FakeResource):
837@@ -612,9 +604,7 @@ class FakeCollection(FakeResource):
838 name=None,
839 child_resource_type=None,
840 ):
841- super(FakeCollection, self).__init__(
842- application, resource_type, values
843- )
844+ super().__init__(application, resource_type, values)
845 self.__dict__.update(
846 {"_name": name, "_child_resource_type": child_resource_type}
847 )
848diff --git a/src/launchpadlib/testing/tests/test_launchpad.py b/src/launchpadlib/testing/tests/test_launchpad.py
849index c988d2b..cff4dd2 100644
850--- a/src/launchpadlib/testing/tests/test_launchpad.py
851+++ b/src/launchpadlib/testing/tests/test_launchpad.py
852@@ -160,8 +160,8 @@ class FakeLaunchpadTest(ResourcedTestCase):
853 dicts that represent objects. Plain string values can be represented
854 as C{unicode} strings.
855 """
856- self.launchpad.me = dict(name=u"foo")
857- self.assertEqual(u"foo", self.launchpad.me.name)
858+ self.launchpad.me = dict(name="foo")
859+ self.assertEqual("foo", self.launchpad.me.name)
860
861 def test_datetime_property(self):
862 """
863diff --git a/src/launchpadlib/tests/test_credential_store.py b/src/launchpadlib/tests/test_credential_store.py
864index 3049fbe..b6fe597 100644
865--- a/src/launchpadlib/tests/test_credential_store.py
866+++ b/src/launchpadlib/tests/test_credential_store.py
867@@ -169,9 +169,7 @@ class TestKeyringCredentialStore(CredentialStoreTestCase):
868 # handled correctly. (See bug lp:877374)
869 class UnicodeInMemoryKeyring(InMemoryKeyring):
870 def get_password(self, service, username):
871- password = super(UnicodeInMemoryKeyring, self).get_password(
872- service, username
873- )
874+ password = super().get_password(service, username)
875 if isinstance(password, unicode_type):
876 password = password.encode("utf-8")
877 return password
878@@ -194,9 +192,7 @@ class TestKeyringCredentialStore(CredentialStoreTestCase):
879
880 class UnencodedInMemoryKeyring(InMemoryKeyring):
881 def get_password(self, service, username):
882- pw = super(UnencodedInMemoryKeyring, self).get_password(
883- service, username
884- )
885+ pw = super().get_password(service, username)
886 return b64decode(pw[5:])
887
888 self.keyring = UnencodedInMemoryKeyring()
889diff --git a/src/launchpadlib/tests/test_http.py b/src/launchpadlib/tests/test_http.py
890index 6924e4f..e2287f6 100644
891--- a/src/launchpadlib/tests/test_http.py
892+++ b/src/launchpadlib/tests/test_http.py
893@@ -17,15 +17,10 @@
894 """Tests for the LaunchpadOAuthAwareHTTP class."""
895
896 from collections import deque
897-from json import dumps
898+from json import dumps, JSONDecodeError
899 import tempfile
900 import unittest
901
902-try:
903- from json import JSONDecodeError
904-except ImportError:
905- JSONDecodeError = ValueError
906-
907 from launchpadlib.errors import Unauthorized
908 from launchpadlib.credentials import UnencryptedFileCredentialStore
909 from launchpadlib.launchpad import (
910@@ -75,7 +70,7 @@ class SimulatedResponsesHttp(LaunchpadOAuthAwareHttp):
911 :param responses: A list of HttpResponse objects to use
912 in response to requests.
913 """
914- super(SimulatedResponsesHttp, self).__init__(*args)
915+ super().__init__(*args)
916 self.sent_responses = []
917 self.unsent_responses = responses
918 self.cache = None
919diff --git a/src/launchpadlib/tests/test_launchpad.py b/src/launchpadlib/tests/test_launchpad.py
920index 66462c5..47410d5 100644
921--- a/src/launchpadlib/tests/test_launchpad.py
922+++ b/src/launchpadlib/tests/test_launchpad.py
923@@ -16,8 +16,6 @@
924
925 """Tests for the Launchpad class."""
926
927-__metaclass__ = type
928-
929 from contextlib import contextmanager
930 import os
931 import shutil
932@@ -25,11 +23,7 @@ import socket
933 import stat
934 import tempfile
935 import unittest
936-
937-try:
938- from unittest.mock import patch
939-except ImportError:
940- from mock import patch
941+from unittest.mock import patch
942 import warnings
943
944 from lazr.restfulclient.resource import ServiceRoot
945@@ -351,11 +345,11 @@ class TestLaunchpadLoginWith(KeyringTest):
946 """Tests for Launchpad.login_with()."""
947
948 def setUp(self):
949- super(TestLaunchpadLoginWith, self).setUp()
950+ super().setUp()
951 self.temp_dir = tempfile.mkdtemp()
952
953 def tearDown(self):
954- super(TestLaunchpadLoginWith, self).tearDown()
955+ super().tearDown()
956 shutil.rmtree(self.temp_dir)
957
958 def test_dirs_created(self):
959diff --git a/src/launchpadlib/uris.py b/src/launchpadlib/uris.py
960index dda802e..1bcc8f6 100644
961--- a/src/launchpadlib/uris.py
962+++ b/src/launchpadlib/uris.py
963@@ -20,18 +20,15 @@ The code in this module lets users say "staging" when they mean
964 "https://api.staging.launchpad.net/".
965 """
966
967-__metaclass__ = type
968 __all__ = [
969 "lookup_service_root",
970 "lookup_web_root",
971 "web_root_for_service_root",
972 ]
973-try:
974- from urllib.parse import urlparse
975-except ImportError:
976- from urlparse import urlparse
977
978+from urllib.parse import urlparse
979 import warnings
980+
981 from lazr.uri import URI
982
983 LPNET_SERVICE_ROOT = "https://api.launchpad.net/"
984diff --git a/tox.ini b/tox.ini
985index aaa62c9..ecde674 100644
986--- a/tox.ini
987+++ b/tox.ini
988@@ -1,13 +1,13 @@
989 [tox]
990 envlist =
991- py27,py35,py36,py37,py38,py39,py310,py311,lint,docs
992+ py35,py36,py37,py38,py39,py310,py311,lint,docs
993 requires = virtualenv<20.22
994
995 [testenv]
996 deps =
997 .[test,testing]
998 commands =
999- coverage run -m pytest src {posargs}
1000+ pytest src {posargs}
1001
1002 [testenv:lint]
1003 # necessary to build the woke linter
1004@@ -34,17 +34,16 @@ deps =
1005 .[docs]
1006
1007 [testenv:coverage]
1008-description = usage: tox -e py27,py35,py310,coverage
1009 basepython =
1010 python3
1011 deps =
1012 .[testing,test]
1013 commands =
1014+ coverage erase
1015+ coverage run -m pytest src {posargs}
1016 coverage combine
1017 coverage html
1018- coverage report -m --fail-under=89
1019-depends =
1020- py27,py35,py310
1021+ coverage report -m --fail-under=91
1022
1023 [coverage:run]
1024 parallel=True

Subscribers

People subscribed via source and target branches