Merge ~cjwatson/launchpadlib:remove-py2 into launchpadlib:main
- Git
- lp:~cjwatson/launchpadlib
- remove-py2
- Merge into 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) |
Related bugs: |
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
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. -
lint:0 (build) tests:0 (build) 1 → 2 of 2 results First • Previous • Next • Last - 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
1 | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml |
2 | index 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: |
29 | diff --git a/NEWS.rst b/NEWS.rst |
30 | index 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. |
44 | diff --git a/contrib/_pythonpath.py b/contrib/_pythonpath.py |
45 | index 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 | |
54 | diff --git a/contrib/close-my-bugs.py b/contrib/close-my-bugs.py |
55 | index 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 | |
93 | diff --git a/contrib/commercial-member-api.py b/contrib/commercial-member-api.py |
94 | index 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. |
151 | diff --git a/contrib/delete_bugtasks.py b/contrib/delete_bugtasks.py |
152 | index 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 | |
184 | diff --git a/contrib/lp-bug-ifier.py b/contrib/lp-bug-ifier.py |
185 | index 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() |
212 | diff --git a/contrib/lpapi.py b/contrib/lpapi.py |
213 | index 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 |
275 | diff --git a/contrib/nopriv-api.py b/contrib/nopriv-api.py |
276 | index 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. |
311 | diff --git a/contrib/sample-person-api.py b/contrib/sample-person-api.py |
312 | index 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. |
347 | diff --git a/contrib/upload_release_tarball.py b/contrib/upload_release_tarball.py |
348 | index 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) |
407 | diff --git a/pyproject.toml b/pyproject.toml |
408 | index 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'] |
416 | diff --git a/setup.py b/setup.py |
417 | index 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={ |
442 | diff --git a/src/launchpadlib/apps.py b/src/launchpadlib/apps.py |
443 | index 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): |
455 | diff --git a/src/launchpadlib/bin/launchpad-request-token b/src/launchpadlib/bin/launchpad-request-token |
456 | index 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 | |
476 | diff --git a/src/launchpadlib/credentials.py b/src/launchpadlib/credentials.py |
477 | index 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() |
644 | diff --git a/src/launchpadlib/docs/conf.py b/src/launchpadlib/docs/conf.py |
645 | index 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.", |
696 | diff --git a/src/launchpadlib/launchpad.py b/src/launchpadlib/launchpad.py |
697 | index 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 | |
749 | diff --git a/src/launchpadlib/testing/helpers.py b/src/launchpadlib/testing/helpers.py |
750 | index 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, |
780 | diff --git a/src/launchpadlib/testing/launchpad.py b/src/launchpadlib/testing/launchpad.py |
781 | index 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 | ) |
848 | diff --git a/src/launchpadlib/testing/tests/test_launchpad.py b/src/launchpadlib/testing/tests/test_launchpad.py |
849 | index 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 | """ |
863 | diff --git a/src/launchpadlib/tests/test_credential_store.py b/src/launchpadlib/tests/test_credential_store.py |
864 | index 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() |
889 | diff --git a/src/launchpadlib/tests/test_http.py b/src/launchpadlib/tests/test_http.py |
890 | index 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 |
919 | diff --git a/src/launchpadlib/tests/test_launchpad.py b/src/launchpadlib/tests/test_launchpad.py |
920 | index 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): |
959 | diff --git a/src/launchpadlib/uris.py b/src/launchpadlib/uris.py |
960 | index 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/" |
984 | diff --git a/tox.ini b/tox.ini |
985 | index 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 |
Hi Colin, thank you for your contribution!
The changes look good to me with a couple of comments. 👍