Merge ~cjwatson/launchpad:drop-py35 into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Guruprasad
Approved revision: 2dae207022ad6503241b0f6043a086fa1a1a53d7
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:drop-py35
Merge into: launchpad:master
Diff against target: 809 lines (+61/-165)
34 files modified
lib/lp/app/browser/tales.py (+2/-4)
lib/lp/app/widgets/date.py (+1/-4)
lib/lp/blueprints/browser/sprint.py (+8/-16)
lib/lp/bugs/scripts/uct/models.py (+1/-2)
lib/lp/bugs/tests/bug.py (+1/-1)
lib/lp/buildmaster/tests/builderproxy.py (+1/-1)
lib/lp/buildmaster/tests/fetchservice.py (+1/-1)
lib/lp/charms/browser/tests/test_charmrecipe.py (+3/-5)
lib/lp/charms/tests/test_charmhubclient.py (+1/-3)
lib/lp/charms/tests/test_charmrecipe.py (+4/-10)
lib/lp/code/browser/gitrepository.py (+1/-3)
lib/lp/code/model/tests/test_githosting.py (+1/-3)
lib/lp/oci/model/ociregistryclient.py (+1/-1)
lib/lp/oci/tests/test_ociregistryclient.py (+6/-6)
lib/lp/registry/interfaces/ssh.py (+1/-11)
lib/lp/services/auth/utils.py (+2/-5)
lib/lp/services/compat.py (+0/-18)
lib/lp/services/librarian/tests/test_client.py (+0/-3)
lib/lp/services/oauth/stories/authorize-token.rst (+2/-2)
lib/lp/services/oauth/stories/request-token.rst (+1/-1)
lib/lp/services/signing/testing/fakesigning.py (+3/-3)
lib/lp/services/signing/tests/test_proxy.py (+1/-1)
lib/lp/services/twistedsupport/xmlrpc.py (+1/-3)
lib/lp/services/webapp/tests/test_candid.py (+1/-2)
lib/lp/services/webapp/tests/test_view_model.py (+3/-3)
lib/lp/services/webapp/url.py (+2/-2)
lib/lp/snappy/browser/tests/test_snap.py (+3/-9)
lib/lp/snappy/tests/test_snap.py (+1/-3)
lib/lp/snappy/tests/test_snapstoreclient.py (+1/-3)
lib/lp/soyuz/wsgi/archiveauth.py (+3/-13)
lib/lp/soyuz/wsgi/tests/test_archiveauth.py (+0/-6)
lib/lp/testing/swift/fakeswift.py (+1/-3)
lib/lp/translations/vocabularies.py (+3/-11)
requirements/launchpad.txt (+0/-3)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Review via email: mp+462554@code.launchpad.net

Commit message

Drop various bits of code to handle Python <= 3.5

Description of the change

Hi! I wanted to do a real-world performance test of my new laptop, so I thought of doing a full Launchpad test run on it (about 2h, if you're curious - it was usually more like 9h on my old laptop by the time I left Canonical), and I used a random half-finished refactoring branch I had lying around. Since it passes, you might as well have the results.

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

> about 2h, if you're curious - it was usually more like 9h on my old laptop by the time I left Canonical

It took ~6h hour on my 5-year-old laptop (8th-gen mobile i5) when I tried it many months ago. So it looks like the cumulative generation-over-generation improvements are significant enough to make a meaningful improvement. 2 hours is close enough to the time it takes to run on buildbot!

And thank you for finishing your WIP branches and sharing them with us!

Revision history for this message
Guruprasad (lgp171188) wrote :

LGTM 👍 Thank you!

review: Approve
Revision history for this message
Guruprasad (lgp171188) wrote :

Colin,

> doing a full Launchpad test run on it (about 2h,

Can you share more details on how you ran the test suite, the invocation for example? Did you run the tests in parallel?

Revision history for this message
Colin Watson (cjwatson) wrote :

Just `bin/with-xvfb bin/test -vvc` in a focal container after the usual container setup. No parallelization. (I expect it would go faster with at least a little parallelization.)

Revision history for this message
Guruprasad (lgp171188) wrote :

> Just `bin/with-xvfb bin/test -vvc` in a focal container after the usual container setup. No parallelization. (I expect it would go faster with at least a little parallelization.)

I am pleasantly surprised at how faster the Ryzen CPU on your new laptop is, compared to the 8th-gen mobile i5 on mine!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
2index d6b2760..fc1c2fc 100644
3--- a/lib/lp/app/browser/tales.py
4+++ b/lib/lp/app/browser/tales.py
5@@ -56,7 +56,6 @@ from lp.registry.interfaces.person import IPerson
6 from lp.registry.interfaces.product import IProduct
7 from lp.registry.interfaces.projectgroup import IProjectGroup
8 from lp.registry.interfaces.socialaccount import SOCIAL_PLATFORM_TYPES_MAP
9-from lp.services.compat import tzname
10 from lp.services.utils import round_half_up
11 from lp.services.webapp.authorization import check_permission
12 from lp.services.webapp.canonicalurl import nearest_adapter
13@@ -1301,8 +1300,7 @@ class PersonFormatterAPI(ObjectFormatterAPI):
14 def local_time(self):
15 """Return the local time for this person."""
16 time_zone = self._context.time_zone
17- dt = datetime.now(tz.gettz(time_zone))
18- return "%s %s" % (dt.strftime("%T"), tzname(dt))
19+ return datetime.now(tz.gettz(time_zone)).strftime("%T %Z")
20
21 def url(self, view_name=None, rootsite="mainsite"):
22 """See `ObjectFormatterAPI`.
23@@ -2390,7 +2388,7 @@ class DateTimeFormatterAPI:
24 def time(self):
25 if self._datetime.tzinfo:
26 value = self._datetime.astimezone(getUtility(ILaunchBag).time_zone)
27- return "%s %s" % (value.strftime("%T"), tzname(value))
28+ return value.strftime("%T %Z")
29 else:
30 return self._datetime.strftime("%T")
31
32diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py
33index 99d8f3f..885518d 100644
34--- a/lib/lp/app/widgets/date.py
35+++ b/lib/lp/app/widgets/date.py
36@@ -33,7 +33,6 @@ from zope.formlib.textwidgets import TextWidget
37 from zope.formlib.widget import DisplayWidget
38
39 from lp.app.validators import LaunchpadValidationError
40-from lp.services.compat import tzname
41 from lp.services.utils import round_half_up
42 from lp.services.webapp.escaping import html_escape
43 from lp.services.webapp.interfaces import ILaunchBag
44@@ -638,6 +637,4 @@ class DatetimeDisplayWidget(DisplayWidget):
45 if value == self.context.missing_value:
46 return ""
47 value = value.astimezone(time_zone)
48- return html_escape(
49- "%s %s" % (value.strftime("%Y-%m-%d %H:%M:%S", tzname(value)))
50- )
51+ return html_escape(value.strftime("%Y-%m-%d %H:%M:%S %Z"))
52diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
53index a22ebaa..91ee23b 100644
54--- a/lib/lp/blueprints/browser/sprint.py
55+++ b/lib/lp/blueprints/browser/sprint.py
56@@ -61,7 +61,6 @@ from lp.registry.browser.menu import (
57 RegistryCollectionActionMenuBase,
58 )
59 from lp.registry.interfaces.person import IPersonSet
60-from lp.services.compat import tzname
61 from lp.services.database.bulk import load_referencing
62 from lp.services.helpers import shortlist
63 from lp.services.propertycache import cachedproperty
64@@ -225,35 +224,28 @@ class SprintView(HasSpecificationsView):
65 def formatDateTime(self, dt):
66 """Format a datetime value according to the sprint's time zone"""
67 dt = dt.astimezone(self.tzinfo)
68- return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M"), tzname(dt))
69+ return dt.strftime("%Y-%m-%d %H:%M %Z")
70
71 def formatDate(self, dt):
72 """Format a date value according to the sprint's time zone"""
73 dt = dt.astimezone(self.tzinfo)
74 return dt.strftime("%Y-%m-%d")
75
76- def _formatLocal(self, dt):
77- return "%s %s on %s" % (
78- dt.strftime("%H:%M"),
79- tzname(dt),
80- dt.strftime("%A, %Y-%m-%d"),
81- )
82+ _local_timeformat = "%H:%M %Z on %A, %Y-%m-%d"
83
84 @property
85 def local_start(self):
86 """The sprint start time, in the local time zone, as text."""
87- return self._formatLocal(
88- self.context.time_starts.astimezone(
89- tz.gettz(self.context.time_zone)
90- )
91- )
92+ return self.context.time_starts.astimezone(
93+ tz.gettz(self.context.time_zone)
94+ ).strftime(self._local_timeformat)
95
96 @property
97 def local_end(self):
98 """The sprint end time, in the local time zone, as text."""
99- return self._formatLocal(
100- self.context.time_ends.astimezone(tz.gettz(self.context.time_zone))
101- )
102+ return self.context.time_ends.astimezone(
103+ tz.gettz(self.context.time_zone)
104+ ).strftime(self._local_timeformat)
105
106
107 class SprintAddView(LaunchpadFormView):
108diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py
109index e11ce8c..418f074 100644
110--- a/lib/lp/bugs/scripts/uct/models.py
111+++ b/lib/lp/bugs/scripts/uct/models.py
112@@ -42,7 +42,6 @@ from lp.registry.model.person import Person
113 from lp.registry.model.product import Product
114 from lp.registry.model.sourcepackage import SourcePackage
115 from lp.registry.model.sourcepackagename import SourcePackageName
116-from lp.services.compat import tzname
117 from lp.services.propertycache import cachedproperty
118
119 __all__ = [
120@@ -389,7 +388,7 @@ class UCTRecord:
121
122 @classmethod
123 def _format_datetime(cls, dt: datetime) -> str:
124- return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M:%S"), tzname(dt))
125+ return dt.strftime("%Y-%m-%d %H:%M:%S %Z")
126
127 @classmethod
128 def _format_notes(cls, notes: List[Tuple[str, str]]) -> str:
129diff --git a/lib/lp/bugs/tests/bug.py b/lib/lp/bugs/tests/bug.py
130index 9798b9b..97dc8b5 100644
131--- a/lib/lp/bugs/tests/bug.py
132+++ b/lib/lp/bugs/tests/bug.py
133@@ -40,7 +40,7 @@ def print_also_notified(bug_page):
134
135 def print_subscribers(bug_page, subscription_level=None, reverse=False):
136 """Print the subscribers listed in the subscribers JSON portlet."""
137- details = json.loads(bug_page.decode())
138+ details = json.loads(bug_page)
139
140 if details is None:
141 # No subscribers at all.
142diff --git a/lib/lp/buildmaster/tests/builderproxy.py b/lib/lp/buildmaster/tests/builderproxy.py
143index a891b6c..fece785 100644
144--- a/lib/lp/buildmaster/tests/builderproxy.py
145+++ b/lib/lp/buildmaster/tests/builderproxy.py
146@@ -28,7 +28,7 @@ class ProxyAuthAPITokensResource(resource.Resource):
147 self.requests = []
148
149 def render_POST(self, request):
150- content = json.loads(request.content.read().decode("UTF-8"))
151+ content = json.loads(request.content.read())
152 self.requests.append(
153 {
154 "method": request.method,
155diff --git a/lib/lp/buildmaster/tests/fetchservice.py b/lib/lp/buildmaster/tests/fetchservice.py
156index dd21e27..3fd879c 100644
157--- a/lib/lp/buildmaster/tests/fetchservice.py
158+++ b/lib/lp/buildmaster/tests/fetchservice.py
159@@ -27,7 +27,7 @@ class FetchServiceAuthAPITokensResource(resource.Resource):
160 self.requests = []
161
162 def render_POST(self, request):
163- content = json.loads(request.content.read().decode("UTF-8"))
164+ content = json.loads(request.content.read())
165 self.requests.append(
166 {
167 "method": request.method,
168diff --git a/lib/lp/charms/browser/tests/test_charmrecipe.py b/lib/lp/charms/browser/tests/test_charmrecipe.py
169index d3a27b7..08313ab 100644
170--- a/lib/lp/charms/browser/tests/test_charmrecipe.py
171+++ b/lib/lp/charms/browser/tests/test_charmrecipe.py
172@@ -423,7 +423,7 @@ class TestCharmRecipeAddView(BaseTestCharmRecipeView):
173 url=Equals("http://charmhub.example/v1/tokens"),
174 method=Equals("POST"),
175 body=AfterPreprocessing(
176- lambda b: json.loads(b.decode()),
177+ json.loads,
178 Equals(
179 {
180 "description": ("charmhub-name for launchpad.test"),
181@@ -1094,7 +1094,7 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
182 url=Equals("http://charmhub.example/v1/tokens"),
183 method=Equals("POST"),
184 body=AfterPreprocessing(
185- lambda b: json.loads(b.decode()),
186+ json.loads,
187 Equals(
188 {
189 "description": (f"{store_name} for launchpad.test"),
190@@ -1303,9 +1303,7 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
191 ),
192 }
193 ),
194- body=AfterPreprocessing(
195- lambda b: json.loads(b.decode()), Equals({})
196- ),
197+ body=AfterPreprocessing(json.loads, Equals({})),
198 )
199 self.assertThat(
200 responses.calls,
201diff --git a/lib/lp/charms/tests/test_charmhubclient.py b/lib/lp/charms/tests/test_charmhubclient.py
202index 7ca8734..34cd783 100644
203--- a/lib/lp/charms/tests/test_charmhubclient.py
204+++ b/lib/lp/charms/tests/test_charmhubclient.py
205@@ -119,9 +119,7 @@ class RequestMatches(MatchesAll):
206 if json_data is not None:
207 matchers.append(
208 MatchesStructure(
209- body=AfterPreprocessing(
210- lambda b: json.loads(b.decode()), Equals(json_data)
211- )
212+ body=AfterPreprocessing(json.loads, Equals(json_data))
213 )
214 )
215 elif file_data is not None:
216diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py
217index 0b41a97..1493f6f 100644
218--- a/lib/lp/charms/tests/test_charmrecipe.py
219+++ b/lib/lp/charms/tests/test_charmrecipe.py
220@@ -1043,7 +1043,7 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
221 url=Equals("http://charmhub.example/v1/tokens"),
222 method=Equals("POST"),
223 body=AfterPreprocessing(
224- lambda b: json.loads(b.decode()),
225+ json.loads,
226 Equals(
227 {
228 "description": (
229@@ -1158,9 +1158,7 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
230 ),
231 }
232 ),
233- body=AfterPreprocessing(
234- lambda b: json.loads(b.decode()), Equals({})
235- ),
236+ body=AfterPreprocessing(json.loads, Equals({})),
237 )
238 self.assertThat(
239 responses.calls,
240@@ -2164,9 +2162,7 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
241 "package-view-revisions",
242 ],
243 }
244- self.assertEqual(
245- expected_body, json.loads(call.request.body.decode("UTF-8"))
246- )
247+ self.assertEqual(expected_body, json.loads(call.request.body))
248 self.assertEqual({"root": root_macaroon_raw}, recipe.store_secrets)
249 return response, root_macaroon_raw
250
251@@ -2276,9 +2272,7 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
252 ),
253 }
254 ),
255- body=AfterPreprocessing(
256- lambda b: json.loads(b.decode()), Equals({})
257- ),
258+ body=AfterPreprocessing(json.loads, Equals({})),
259 )
260 self.assertThat(
261 responses.calls,
262diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py
263index d6da0d3..089aee6 100644
264--- a/lib/lp/code/browser/gitrepository.py
265+++ b/lib/lp/code/browser/gitrepository.py
266@@ -1101,9 +1101,7 @@ class GitRepositoryPermissionsView(LaunchpadFormView):
267 field_type = field_bits[0]
268 try:
269 ref_pattern = decode_form_field_id(field_bits[1])
270- # base64.b32decode raises TypeError for decoding errors on Python 2,
271- # but binascii.Error on Python 3.
272- except (TypeError, binascii.Error):
273+ except binascii.Error:
274 raise UnexpectedFormData(
275 "Cannot parse field name: %s" % field_name
276 )
277diff --git a/lib/lp/code/model/tests/test_githosting.py b/lib/lp/code/model/tests/test_githosting.py
278index 0f5cf62..c876123 100644
279--- a/lib/lp/code/model/tests/test_githosting.py
280+++ b/lib/lp/code/model/tests/test_githosting.py
281@@ -106,9 +106,7 @@ class TestGitHostingClient(TestCase):
282 ),
283 )
284 if json_data is not None:
285- self.assertEqual(
286- json_data, json.loads(request.body.decode("UTF-8"))
287- )
288+ self.assertEqual(json_data, json.loads(request.body))
289 timeline = get_request_timeline(get_current_browser_request())
290 action = timeline.actions[-1]
291 self.assertEqual("git-hosting-%s" % method.lower(), action.category)
292diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py
293index 04061a7..0a1211d 100644
294--- a/lib/lp/oci/model/ociregistryclient.py
295+++ b/lib/lp/oci/model/ociregistryclient.py
296@@ -69,7 +69,7 @@ class OCIRegistryClient:
297 """Read JSON out of a `LibraryFileAlias`."""
298 try:
299 reference.open()
300- return json.loads(reference.read().decode("UTF-8"))
301+ return json.loads(reference.read())
302 finally:
303 reference.close()
304
305diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py
306index c3dec1c..6d01ea9 100644
307--- a/lib/lp/oci/tests/test_ociregistryclient.py
308+++ b/lib/lp/oci/tests/test_ociregistryclient.py
309@@ -220,7 +220,7 @@ class TestOCIRegistryClient(
310 # We should have uploaded to the digest, not the tag
311 self.assertIn("sha256:", responses.calls[1].request.url)
312 self.assertNotIn("edge", responses.calls[1].request.url)
313- request = json.loads(responses.calls[1].request.body.decode("UTF-8"))
314+ request = json.loads(responses.calls[1].request.body)
315
316 layer_matchers = [
317 MatchesDict(
318@@ -327,7 +327,7 @@ class TestOCIRegistryClient(
319
320 self.client.upload(self.build)
321
322- request = json.loads(responses.calls[1].request.body.decode("UTF-8"))
323+ request = json.loads(responses.calls[1].request.body)
324
325 layer_matchers = [
326 MatchesDict(
327@@ -1078,7 +1078,7 @@ class TestOCIRegistryClient(
328 },
329 ],
330 },
331- json.loads(send_manifest_call.request.body.decode("UTF-8")),
332+ json.loads(send_manifest_call.request.body),
333 )
334
335 @responses.activate
336@@ -1195,7 +1195,7 @@ class TestOCIRegistryClient(
337 },
338 ],
339 },
340- json.loads(send_manifest_call.request.body.decode("UTF-8")),
341+ json.loads(send_manifest_call.request.body),
342 )
343
344 @responses.activate
345@@ -1267,7 +1267,7 @@ class TestOCIRegistryClient(
346 }
347 ],
348 },
349- json.loads(send_manifest_call.request.body.decode("UTF-8")),
350+ json.loads(send_manifest_call.request.body),
351 )
352
353 @responses.activate
354@@ -1441,7 +1441,7 @@ class TestOCIRegistryClient(
355 },
356 ],
357 },
358- json.loads(responses.calls[2].request.body.decode("UTF-8")),
359+ json.loads(responses.calls[2].request.body),
360 )
361
362
363diff --git a/lib/lp/registry/interfaces/ssh.py b/lib/lp/registry/interfaces/ssh.py
364index 5559526..e99a000 100644
365--- a/lib/lp/registry/interfaces/ssh.py
366+++ b/lib/lp/registry/interfaces/ssh.py
367@@ -176,15 +176,5 @@ class SSHKeyAdditionError(Exception):
368 )
369 if "exception" in kwargs:
370 exception = kwargs.pop("exception")
371- try:
372- exception_text = str(exception)
373- except UnicodeDecodeError:
374- # On Python 2, Key.fromString can raise exceptions with
375- # non-UTF-8 messages.
376- exception_text = (
377- bytes(exception)
378- .decode("unicode_escape")
379- .encode("unicode_escape")
380- )
381- msg = "%s (%s)" % (msg, exception_text)
382+ msg = "%s (%s)" % (msg, exception)
383 super().__init__(msg, *args, **kwargs)
384diff --git a/lib/lp/services/auth/utils.py b/lib/lp/services/auth/utils.py
385index f4c997b..c1a780c 100644
386--- a/lib/lp/services/auth/utils.py
387+++ b/lib/lp/services/auth/utils.py
388@@ -7,12 +7,9 @@ __all__ = [
389 "create_access_token_secret",
390 ]
391
392-import binascii
393-import os
394+import secrets
395
396
397-# XXX cjwatson 2021-09-30: Replace this with secrets.token_hex(32) once we
398-# can rely on Python 3.6 everywhere.
399 def create_access_token_secret():
400 """Create a secret suitable for use in a personal access token."""
401- return binascii.hexlify(os.urandom(32)).decode("ASCII")
402+ return secrets.token_hex(32)
403diff --git a/lib/lp/services/compat.py b/lib/lp/services/compat.py
404index 86d833a..ee2c2fe 100644
405--- a/lib/lp/services/compat.py
406+++ b/lib/lp/services/compat.py
407@@ -8,12 +8,9 @@ Use this for things that six doesn't provide.
408
409 __all__ = [
410 "message_as_bytes",
411- "tzname",
412 ]
413
414 import io
415-from datetime import datetime, time, timezone
416-from typing import Union
417
418
419 def message_as_bytes(message):
420@@ -24,18 +21,3 @@ def message_as_bytes(message):
421 g = BytesGenerator(fp, mangle_from_=False, maxheaderlen=0, policy=compat32)
422 g.flatten(message)
423 return fp.getvalue()
424-
425-
426-def tzname(obj: Union[datetime, time]) -> str:
427- """Return this (date)time object's time zone name as a string.
428-
429- Python 3.5's `timezone.utc.tzname` returns "UTC+00:00", rather than
430- "UTC" which is what we prefer. Paper over this until we can rely on
431- Python >= 3.6 everywhere.
432- """
433- if obj.tzinfo is None:
434- return ""
435- elif obj.tzinfo is timezone.utc:
436- return "UTC"
437- else:
438- return obj.tzname()
439diff --git a/lib/lp/services/librarian/tests/test_client.py b/lib/lp/services/librarian/tests/test_client.py
440index 012d1e1..f258bab 100644
441--- a/lib/lp/services/librarian/tests/test_client.py
442+++ b/lib/lp/services/librarian/tests/test_client.py
443@@ -154,10 +154,7 @@ class LibrarianFileWrapperTestCase(TestCase):
444 def test_unbounded_read_incorrect_length(self):
445 file = self.makeFile(extra_content_length=1)
446 with ExpectedException(http.client.IncompleteRead):
447- # Python 3 notices the short response on the first read.
448 self.assertEqual(b"abcdef", file.read())
449- # Python 2 only notices the short response on the next read.
450- file.read()
451
452 def test_bounded_read_correct_length(self):
453 file = self.makeFile()
454diff --git a/lib/lp/services/oauth/stories/authorize-token.rst b/lib/lp/services/oauth/stories/authorize-token.rst
455index 61f3cc5..a3c30d1 100644
456--- a/lib/lp/services/oauth/stories/authorize-token.rst
457+++ b/lib/lp/services/oauth/stories/authorize-token.rst
458@@ -169,7 +169,7 @@ the list of authentication levels.
459 >>> json_browser.open(
460 ... "http://launchpad.test/+authorize-token?%s" % urlencode(params)
461 ... )
462- >>> json_token = json.loads(json_browser.contents.decode())
463+ >>> json_token = json.loads(json_browser.contents)
464 >>> sorted(json_token.keys())
465 ['access_levels', 'oauth_token', 'oauth_token_consumer']
466
467@@ -190,7 +190,7 @@ the list of authentication levels.
468 ... )
469 ... % urlencode(params)
470 ... )
471- >>> json_token = json.loads(json_browser.contents.decode())
472+ >>> json_token = json.loads(json_browser.contents)
473 >>> sorted(
474 ... (level["value"], level["title"])
475 ... for level in json_token["access_levels"]
476diff --git a/lib/lp/services/oauth/stories/request-token.rst b/lib/lp/services/oauth/stories/request-token.rst
477index 2bc110a..799fcc0 100644
478--- a/lib/lp/services/oauth/stories/request-token.rst
479+++ b/lib/lp/services/oauth/stories/request-token.rst
480@@ -30,7 +30,7 @@ levels.
481 >>> json_browser.open(
482 ... "http://launchpad.test/+request-token", data=urlencode(data)
483 ... )
484- >>> token = json.loads(json_browser.contents.decode())
485+ >>> token = json.loads(json_browser.contents)
486 >>> sorted(token.keys())
487 ['access_levels', 'oauth_token', 'oauth_token_consumer',
488 'oauth_token_secret']
489diff --git a/lib/lp/services/signing/testing/fakesigning.py b/lib/lp/services/signing/testing/fakesigning.py
490index eb7b27f..243408b 100644
491--- a/lib/lp/services/signing/testing/fakesigning.py
492+++ b/lib/lp/services/signing/testing/fakesigning.py
493@@ -89,7 +89,7 @@ class GenerateResource(BoxedAuthenticationResource):
494 self.requests = []
495
496 def render_POST(self, request):
497- payload = json.loads(self._decrypt(request).decode("UTF-8"))
498+ payload = json.loads(self._decrypt(request))
499 self.requests.append(payload)
500 # We don't need to bother with generating a real key here. Just
501 # make up some random data.
502@@ -117,7 +117,7 @@ class SignResource(BoxedAuthenticationResource):
503 self.requests = []
504
505 def render_POST(self, request):
506- payload = json.loads(self._decrypt(request).decode("UTF-8"))
507+ payload = json.loads(self._decrypt(request))
508 self.requests.append(payload)
509 _, public_key = self.keys[payload["fingerprint"]]
510 # We don't need to bother with generating a real signature here.
511@@ -143,7 +143,7 @@ class InjectResource(BoxedAuthenticationResource):
512 self.requests = []
513
514 def render_POST(self, request):
515- payload = json.loads(self._decrypt(request).decode("UTF-8"))
516+ payload = json.loads(self._decrypt(request))
517 self.requests.append(payload)
518 private_key = base64.b64decode(payload["private-key"].encode("UTF-8"))
519 public_key = base64.b64decode(payload["public-key"].encode("UTF-8"))
520diff --git a/lib/lp/services/signing/tests/test_proxy.py b/lib/lp/services/signing/tests/test_proxy.py
521index 1db9530..6bb14ff 100644
522--- a/lib/lp/services/signing/tests/test_proxy.py
523+++ b/lib/lp/services/signing/tests/test_proxy.py
524@@ -93,7 +93,7 @@ class SigningServiceResponseFactory:
525 """
526 box = Box(self.service_private_key, self.client_public_key)
527 decrypted = box.decrypt(value, self.nonce, encoder=Base64Encoder)
528- return json.loads(decrypted.decode("UTF-8"))
529+ return json.loads(decrypted)
530
531 def getAPISignedContent(self, call_index=0):
532 """Returns the signed message returned by the API.
533diff --git a/lib/lp/services/twistedsupport/xmlrpc.py b/lib/lp/services/twistedsupport/xmlrpc.py
534index 7cdf389..8e75dfc 100644
535--- a/lib/lp/services/twistedsupport/xmlrpc.py
536+++ b/lib/lp/services/twistedsupport/xmlrpc.py
537@@ -56,9 +56,7 @@ def trap_fault(failure, *fault_classes):
538 :param failure: A Twisted L{Failure}.
539 :param *fault_codes: `LaunchpadFault` subclasses.
540 :raise Exception: if 'failure' is not a Fault failure, or if the fault
541- code does not match the given codes. In line with L{Failure.trap},
542- the exception is the L{Failure} itself on Python 2 and the
543- underlying exception on Python 3.
544+ code does not match the given codes.
545 :return: The Fault if it matches one of the codes.
546 """
547 failure.trap(xmlrpc.Fault)
548diff --git a/lib/lp/services/webapp/tests/test_candid.py b/lib/lp/services/webapp/tests/test_candid.py
549index ea1898b..dfb4a8a 100644
550--- a/lib/lp/services/webapp/tests/test_candid.py
551+++ b/lib/lp/services/webapp/tests/test_candid.py
552@@ -499,8 +499,7 @@ class TestCandidCallbackView(TestCaseWithFactory):
553 }
554 ),
555 body=AfterPreprocessing(
556- lambda b: json.loads(b.decode()),
557- MatchesDict({"code": Equals("test code")}),
558+ json.loads, MatchesDict({"code": Equals("test code")})
559 ),
560 )
561 discharge_matcher = MatchesStructure(
562diff --git a/lib/lp/services/webapp/tests/test_view_model.py b/lib/lp/services/webapp/tests/test_view_model.py
563index 329fa64..f4b6ed6 100644
564--- a/lib/lp/services/webapp/tests/test_view_model.py
565+++ b/lib/lp/services/webapp/tests/test_view_model.py
566@@ -123,7 +123,7 @@ class TestJsonModelView(BrowserTestCase):
567 lp.services.webapp.tests.ProductModelTestView = ProductModelTestView
568 self.configZCML()
569 browser = self.getUserBrowser(self.url)
570- cache = json.loads(browser.contents.decode())
571+ cache = json.loads(browser.contents)
572 self.assertThat(cache, KeysEqual("related_features", "context"))
573
574 def test_JsonModel_custom_cache(self):
575@@ -140,7 +140,7 @@ class TestJsonModelView(BrowserTestCase):
576 lp.services.webapp.tests.ProductModelTestView = ProductModelTestView
577 self.configZCML()
578 browser = self.getUserBrowser(self.url)
579- cache = json.loads(browser.contents.decode())
580+ cache = json.loads(browser.contents)
581 self.assertThat(
582 cache, KeysEqual("related_features", "context", "target_info")
583 )
584@@ -165,7 +165,7 @@ class TestJsonModelView(BrowserTestCase):
585 lp.services.webapp.tests.ProductModelTestView = ProductModelTestView
586 self.configZCML()
587 browser = self.getUserBrowser(self.url)
588- cache = json.loads(browser.contents.decode())
589+ cache = json.loads(browser.contents)
590 self.assertThat(
591 cache, KeysEqual("related_features", "context", "target_info")
592 )
593diff --git a/lib/lp/services/webapp/url.py b/lib/lp/services/webapp/url.py
594index ba3df8a..59c68dc 100644
595--- a/lib/lp/services/webapp/url.py
596+++ b/lib/lp/services/webapp/url.py
597@@ -90,7 +90,7 @@ def urlparse(url, scheme="", allow_fragments=True):
598
599 The url parameter should contain ASCII characters only. This
600 function ensures that the original urlparse is called always with a
601- str object, and never unicode (Python 2) or bytes (Python 3).
602+ str object, and never bytes.
603
604 >>> tuple(urlparse("http://foo.com/bar"))
605 ('http', 'foo.com', '/bar', '', '', '')
606@@ -120,7 +120,7 @@ def urlsplit(url, scheme="", allow_fragments=True):
607
608 The url parameter should contain ASCII characters only. This
609 function ensures that the original urlsplit is called always with a
610- str object, and never unicode (Python 2) or bytes (Python 3).
611+ str object, and never bytes.
612
613 >>> tuple(urlsplit("http://foo.com/baz"))
614 ('http', 'foo.com', '/baz', '', '')
615diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
616index 8d2a48a..819d770 100644
617--- a/lib/lp/snappy/browser/tests/test_snap.py
618+++ b/lib/lp/snappy/browser/tests/test_snap.py
619@@ -653,9 +653,7 @@ class TestSnapAddView(BaseTestSnapView):
620 ],
621 "permissions": ["package_upload"],
622 }
623- self.assertEqual(
624- expected_body, json.loads(call.request.body.decode("UTF-8"))
625- )
626+ self.assertEqual(expected_body, json.loads(call.request.body))
627 self.assertEqual(303, browser.responseStatusCode)
628 parsed_location = urlsplit(browser.headers["Location"])
629 self.assertEqual(
630@@ -1737,9 +1735,7 @@ class TestSnapEditView(BaseTestSnapView):
631 "packages": [{"name": "two", "series": self.snappyseries.name}],
632 "permissions": ["package_upload"],
633 }
634- self.assertEqual(
635- expected_body, json.loads(call.request.body.decode("UTF-8"))
636- )
637+ self.assertEqual(expected_body, json.loads(call.request.body))
638 self.assertEqual(303, browser.responseStatusCode)
639 parsed_location = urlsplit(browser.headers["Location"])
640 self.assertEqual(
641@@ -1820,9 +1816,7 @@ class TestSnapAuthorizeView(BaseTestSnapView):
642 ],
643 "permissions": ["package_upload"],
644 }
645- self.assertEqual(
646- expected_body, json.loads(call.request.body.decode("UTF-8"))
647- )
648+ self.assertEqual(expected_body, json.loads(call.request.body))
649 self.assertEqual(
650 {"root": root_macaroon_raw}, self.snap.store_secrets
651 )
652diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
653index 32a2d5c..9f671b8 100644
654--- a/lib/lp/snappy/tests/test_snap.py
655+++ b/lib/lp/snappy/tests/test_snap.py
656@@ -4892,9 +4892,7 @@ class TestSnapWebservice(TestCaseWithFactory):
657 ],
658 "permissions": ["package_upload"],
659 }
660- self.assertEqual(
661- expected_body, json.loads(call.request.body.decode("UTF-8"))
662- )
663+ self.assertEqual(expected_body, json.loads(call.request.body))
664 self.assertEqual({"root": root_macaroon_raw}, snap.store_secrets)
665 return response, root_macaroon.third_party_caveats()[0]
666
667diff --git a/lib/lp/snappy/tests/test_snapstoreclient.py b/lib/lp/snappy/tests/test_snapstoreclient.py
668index d6f75bd..ddbf457 100644
669--- a/lib/lp/snappy/tests/test_snapstoreclient.py
670+++ b/lib/lp/snappy/tests/test_snapstoreclient.py
671@@ -227,9 +227,7 @@ class RequestMatches(Matcher):
672 if mismatch is not None:
673 return mismatch
674 if self.json_data is not None:
675- mismatch = Equals(self.json_data).match(
676- json.loads(request.body.decode("UTF-8"))
677- )
678+ mismatch = Equals(self.json_data).match(json.loads(request.body))
679 if mismatch is not None:
680 return mismatch
681 if self.form_data is not None:
682diff --git a/lib/lp/soyuz/wsgi/archiveauth.py b/lib/lp/soyuz/wsgi/archiveauth.py
683index 006143d..17440e8 100644
684--- a/lib/lp/soyuz/wsgi/archiveauth.py
685+++ b/lib/lp/soyuz/wsgi/archiveauth.py
686@@ -11,10 +11,8 @@ __all__ = [
687 ]
688
689 import crypt
690-import string
691 import sys
692 import time
693-from random import SystemRandom
694 from xmlrpc.client import Fault, ServerProxy
695
696 import six
697@@ -49,16 +47,6 @@ def _get_archive_reference(environ):
698 _log(environ, "No archive reference found in URL '%s'.", path)
699
700
701-_sr = SystemRandom()
702-
703-
704-def _crypt_sha256(word):
705- """crypt.crypt(word, crypt.METHOD_SHA256), backported from Python 3.5."""
706- saltchars = string.ascii_letters + string.digits + "./"
707- salt = "$5$" + "".join(_sr.choice(saltchars) for _ in range(16))
708- return crypt.crypt(word, salt)
709-
710-
711 _memcache_client = memcache_client_factory(timeline=False)
712
713
714@@ -91,7 +79,9 @@ def check_password(environ, user, password):
715 proxy.checkArchiveAuthToken(archive_reference, user, password)
716 # Cache positive responses for a minute to reduce database load.
717 _memcache_client.set(
718- memcache_key, _crypt_sha256(password), int(time.time()) + 60
719+ memcache_key,
720+ crypt.crypt(password, crypt.METHOD_SHA256),
721+ int(time.time()) + 60,
722 )
723 _log(environ, "%s@%s: Authorized.", user, archive_reference)
724 return True
725diff --git a/lib/lp/soyuz/wsgi/tests/test_archiveauth.py b/lib/lp/soyuz/wsgi/tests/test_archiveauth.py
726index 3ac5ff9..16e8a2b 100644
727--- a/lib/lp/soyuz/wsgi/tests/test_archiveauth.py
728+++ b/lib/lp/soyuz/wsgi/tests/test_archiveauth.py
729@@ -106,12 +106,6 @@ class TestWSGIArchiveAuth(TestCaseWithFactory):
730 self.assertEqual({}, self.memcache_fixture._cache)
731 self.assertLogs("No archive found for '~nonexistent/unknown/bad'.")
732
733- def test_crypt_sha256(self):
734- crypted_password = archiveauth._crypt_sha256("secret")
735- self.assertEqual(
736- crypted_password, crypt.crypt("secret", crypted_password)
737- )
738-
739 def makeArchiveAndToken(self):
740 archive = self.factory.makeArchive(private=True)
741 archive_path = "/%s/%s/ubuntu" % (archive.owner.name, archive.name)
742diff --git a/lib/lp/testing/swift/fakeswift.py b/lib/lp/testing/swift/fakeswift.py
743index 325c390..68126cb 100644
744--- a/lib/lp/testing/swift/fakeswift.py
745+++ b/lib/lp/testing/swift/fakeswift.py
746@@ -107,9 +107,7 @@ class FakeKeystone(resource.Resource):
747 if "application/json" not in request.getHeader("content-type"):
748 request.setResponseCode(http.BAD_REQUEST)
749 return b""
750- # XXX cjwatson 2020-06-15: Python 3.5 doesn't allow this to be a
751- # binary file; 3.6 does.
752- credentials = json.loads(request.content.read().decode("UTF-8"))
753+ credentials = json.loads(request.content.read())
754 if "auth" not in credentials:
755 request.setResponseCode(http.FORBIDDEN)
756 return b""
757diff --git a/lib/lp/translations/vocabularies.py b/lib/lp/translations/vocabularies.py
758index a564444..d9c03dc 100644
759--- a/lib/lp/translations/vocabularies.py
760+++ b/lib/lp/translations/vocabularies.py
761@@ -18,7 +18,6 @@ from storm.locals import Desc, Not, Or
762 from zope.schema.vocabulary import SimpleTerm
763
764 from lp.registry.interfaces.distroseries import IDistroSeries
765-from lp.services.compat import tzname
766 from lp.services.webapp.vocabulary import (
767 NamedStormVocabulary,
768 StormVocabularyBase,
769@@ -102,10 +101,7 @@ class FilteredLanguagePackVocabularyBase(StormVocabularyBase):
770
771 def toTerm(self, obj):
772 return SimpleTerm(
773- obj,
774- obj.id,
775- "%s %s"
776- % (obj.date_exported.strftime("%F %T"), tzname(obj.date_exported)),
777+ obj, obj.id, "%s" % obj.date_exported.strftime("%F %T %Z")
778 )
779
780 @property
781@@ -143,12 +139,8 @@ class FilteredLanguagePackVocabulary(FilteredLanguagePackVocabularyBase):
782 return SimpleTerm(
783 obj,
784 obj.id,
785- "%s %s (%s)"
786- % (
787- obj.date_exported.strftime("%F %T"),
788- tzname(obj.date_exported),
789- obj.type.title,
790- ),
791+ "%s (%s)"
792+ % (obj.date_exported.strftime("%F %T %Z"), obj.type.title),
793 )
794
795 @property
796diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt
797index cc28c81..b7b7957 100644
798--- a/requirements/launchpad.txt
799+++ b/requirements/launchpad.txt
800@@ -121,9 +121,6 @@ patiencediff==0.2.2
801 pexpect==4.8.0
802 pgbouncer==0.0.9
803 pickleshare==0.7.5
804-# pkginfo 1.7.0 dropped Python 3.5 support, but we need the features of the
805-# newer version, and both our and the package's test suite show no
806-# incompatibilities
807 pkginfo==1.8.2
808 prettytable==0.7.2
809 prompt-toolkit==2.0.10

Subscribers

People subscribed via source and target branches

to status/vote changes: