Merge ~cjwatson/launchpad:drop-py35 into launchpad:master
- Git
- lp:~cjwatson/launchpad
- drop-py35
- Merge into master
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) |
Related bugs: |
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.
Guruprasad (lgp171188) wrote : | # |
Guruprasad (lgp171188) wrote : | # |
LGTM 👍 Thank you!
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?
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.)
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
1 | diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py |
2 | index 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 | |
32 | diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py |
33 | index 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")) |
52 | diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py |
53 | index 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): |
108 | diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py |
109 | index 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: |
129 | diff --git a/lib/lp/bugs/tests/bug.py b/lib/lp/bugs/tests/bug.py |
130 | index 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. |
142 | diff --git a/lib/lp/buildmaster/tests/builderproxy.py b/lib/lp/buildmaster/tests/builderproxy.py |
143 | index 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, |
155 | diff --git a/lib/lp/buildmaster/tests/fetchservice.py b/lib/lp/buildmaster/tests/fetchservice.py |
156 | index 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, |
168 | diff --git a/lib/lp/charms/browser/tests/test_charmrecipe.py b/lib/lp/charms/browser/tests/test_charmrecipe.py |
169 | index 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, |
201 | diff --git a/lib/lp/charms/tests/test_charmhubclient.py b/lib/lp/charms/tests/test_charmhubclient.py |
202 | index 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: |
216 | diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py |
217 | index 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, |
262 | diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py |
263 | index 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 | ) |
277 | diff --git a/lib/lp/code/model/tests/test_githosting.py b/lib/lp/code/model/tests/test_githosting.py |
278 | index 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) |
292 | diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py |
293 | index 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 | |
305 | diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py |
306 | index 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 | |
363 | diff --git a/lib/lp/registry/interfaces/ssh.py b/lib/lp/registry/interfaces/ssh.py |
364 | index 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) |
384 | diff --git a/lib/lp/services/auth/utils.py b/lib/lp/services/auth/utils.py |
385 | index 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) |
403 | diff --git a/lib/lp/services/compat.py b/lib/lp/services/compat.py |
404 | index 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() |
439 | diff --git a/lib/lp/services/librarian/tests/test_client.py b/lib/lp/services/librarian/tests/test_client.py |
440 | index 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() |
454 | diff --git a/lib/lp/services/oauth/stories/authorize-token.rst b/lib/lp/services/oauth/stories/authorize-token.rst |
455 | index 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"] |
476 | diff --git a/lib/lp/services/oauth/stories/request-token.rst b/lib/lp/services/oauth/stories/request-token.rst |
477 | index 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'] |
489 | diff --git a/lib/lp/services/signing/testing/fakesigning.py b/lib/lp/services/signing/testing/fakesigning.py |
490 | index 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")) |
520 | diff --git a/lib/lp/services/signing/tests/test_proxy.py b/lib/lp/services/signing/tests/test_proxy.py |
521 | index 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. |
533 | diff --git a/lib/lp/services/twistedsupport/xmlrpc.py b/lib/lp/services/twistedsupport/xmlrpc.py |
534 | index 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) |
548 | diff --git a/lib/lp/services/webapp/tests/test_candid.py b/lib/lp/services/webapp/tests/test_candid.py |
549 | index 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( |
562 | diff --git a/lib/lp/services/webapp/tests/test_view_model.py b/lib/lp/services/webapp/tests/test_view_model.py |
563 | index 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 | ) |
593 | diff --git a/lib/lp/services/webapp/url.py b/lib/lp/services/webapp/url.py |
594 | index 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', '', '') |
615 | diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py |
616 | index 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 | ) |
652 | diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py |
653 | index 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 | |
667 | diff --git a/lib/lp/snappy/tests/test_snapstoreclient.py b/lib/lp/snappy/tests/test_snapstoreclient.py |
668 | index 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: |
682 | diff --git a/lib/lp/soyuz/wsgi/archiveauth.py b/lib/lp/soyuz/wsgi/archiveauth.py |
683 | index 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 |
725 | diff --git a/lib/lp/soyuz/wsgi/tests/test_archiveauth.py b/lib/lp/soyuz/wsgi/tests/test_archiveauth.py |
726 | index 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) |
742 | diff --git a/lib/lp/testing/swift/fakeswift.py b/lib/lp/testing/swift/fakeswift.py |
743 | index 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"" |
757 | diff --git a/lib/lp/translations/vocabularies.py b/lib/lp/translations/vocabularies.py |
758 | index 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 |
796 | diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt |
797 | index 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 |
> 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!