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
diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
index d6b2760..fc1c2fc 100644
--- a/lib/lp/app/browser/tales.py
+++ b/lib/lp/app/browser/tales.py
@@ -56,7 +56,6 @@ from lp.registry.interfaces.person import IPerson
56from lp.registry.interfaces.product import IProduct56from lp.registry.interfaces.product import IProduct
57from lp.registry.interfaces.projectgroup import IProjectGroup57from lp.registry.interfaces.projectgroup import IProjectGroup
58from lp.registry.interfaces.socialaccount import SOCIAL_PLATFORM_TYPES_MAP58from lp.registry.interfaces.socialaccount import SOCIAL_PLATFORM_TYPES_MAP
59from lp.services.compat import tzname
60from lp.services.utils import round_half_up59from lp.services.utils import round_half_up
61from lp.services.webapp.authorization import check_permission60from lp.services.webapp.authorization import check_permission
62from lp.services.webapp.canonicalurl import nearest_adapter61from lp.services.webapp.canonicalurl import nearest_adapter
@@ -1301,8 +1300,7 @@ class PersonFormatterAPI(ObjectFormatterAPI):
1301 def local_time(self):1300 def local_time(self):
1302 """Return the local time for this person."""1301 """Return the local time for this person."""
1303 time_zone = self._context.time_zone1302 time_zone = self._context.time_zone
1304 dt = datetime.now(tz.gettz(time_zone))1303 return datetime.now(tz.gettz(time_zone)).strftime("%T %Z")
1305 return "%s %s" % (dt.strftime("%T"), tzname(dt))
13061304
1307 def url(self, view_name=None, rootsite="mainsite"):1305 def url(self, view_name=None, rootsite="mainsite"):
1308 """See `ObjectFormatterAPI`.1306 """See `ObjectFormatterAPI`.
@@ -2390,7 +2388,7 @@ class DateTimeFormatterAPI:
2390 def time(self):2388 def time(self):
2391 if self._datetime.tzinfo:2389 if self._datetime.tzinfo:
2392 value = self._datetime.astimezone(getUtility(ILaunchBag).time_zone)2390 value = self._datetime.astimezone(getUtility(ILaunchBag).time_zone)
2393 return "%s %s" % (value.strftime("%T"), tzname(value))2391 return value.strftime("%T %Z")
2394 else:2392 else:
2395 return self._datetime.strftime("%T")2393 return self._datetime.strftime("%T")
23962394
diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py
index 99d8f3f..885518d 100644
--- a/lib/lp/app/widgets/date.py
+++ b/lib/lp/app/widgets/date.py
@@ -33,7 +33,6 @@ from zope.formlib.textwidgets import TextWidget
33from zope.formlib.widget import DisplayWidget33from zope.formlib.widget import DisplayWidget
3434
35from lp.app.validators import LaunchpadValidationError35from lp.app.validators import LaunchpadValidationError
36from lp.services.compat import tzname
37from lp.services.utils import round_half_up36from lp.services.utils import round_half_up
38from lp.services.webapp.escaping import html_escape37from lp.services.webapp.escaping import html_escape
39from lp.services.webapp.interfaces import ILaunchBag38from lp.services.webapp.interfaces import ILaunchBag
@@ -638,6 +637,4 @@ class DatetimeDisplayWidget(DisplayWidget):
638 if value == self.context.missing_value:637 if value == self.context.missing_value:
639 return ""638 return ""
640 value = value.astimezone(time_zone)639 value = value.astimezone(time_zone)
641 return html_escape(640 return html_escape(value.strftime("%Y-%m-%d %H:%M:%S %Z"))
642 "%s %s" % (value.strftime("%Y-%m-%d %H:%M:%S", tzname(value)))
643 )
diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
index a22ebaa..91ee23b 100644
--- a/lib/lp/blueprints/browser/sprint.py
+++ b/lib/lp/blueprints/browser/sprint.py
@@ -61,7 +61,6 @@ from lp.registry.browser.menu import (
61 RegistryCollectionActionMenuBase,61 RegistryCollectionActionMenuBase,
62)62)
63from lp.registry.interfaces.person import IPersonSet63from lp.registry.interfaces.person import IPersonSet
64from lp.services.compat import tzname
65from lp.services.database.bulk import load_referencing64from lp.services.database.bulk import load_referencing
66from lp.services.helpers import shortlist65from lp.services.helpers import shortlist
67from lp.services.propertycache import cachedproperty66from lp.services.propertycache import cachedproperty
@@ -225,35 +224,28 @@ class SprintView(HasSpecificationsView):
225 def formatDateTime(self, dt):224 def formatDateTime(self, dt):
226 """Format a datetime value according to the sprint's time zone"""225 """Format a datetime value according to the sprint's time zone"""
227 dt = dt.astimezone(self.tzinfo)226 dt = dt.astimezone(self.tzinfo)
228 return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M"), tzname(dt))227 return dt.strftime("%Y-%m-%d %H:%M %Z")
229228
230 def formatDate(self, dt):229 def formatDate(self, dt):
231 """Format a date value according to the sprint's time zone"""230 """Format a date value according to the sprint's time zone"""
232 dt = dt.astimezone(self.tzinfo)231 dt = dt.astimezone(self.tzinfo)
233 return dt.strftime("%Y-%m-%d")232 return dt.strftime("%Y-%m-%d")
234233
235 def _formatLocal(self, dt):234 _local_timeformat = "%H:%M %Z on %A, %Y-%m-%d"
236 return "%s %s on %s" % (
237 dt.strftime("%H:%M"),
238 tzname(dt),
239 dt.strftime("%A, %Y-%m-%d"),
240 )
241235
242 @property236 @property
243 def local_start(self):237 def local_start(self):
244 """The sprint start time, in the local time zone, as text."""238 """The sprint start time, in the local time zone, as text."""
245 return self._formatLocal(239 return self.context.time_starts.astimezone(
246 self.context.time_starts.astimezone(240 tz.gettz(self.context.time_zone)
247 tz.gettz(self.context.time_zone)241 ).strftime(self._local_timeformat)
248 )
249 )
250242
251 @property243 @property
252 def local_end(self):244 def local_end(self):
253 """The sprint end time, in the local time zone, as text."""245 """The sprint end time, in the local time zone, as text."""
254 return self._formatLocal(246 return self.context.time_ends.astimezone(
255 self.context.time_ends.astimezone(tz.gettz(self.context.time_zone))247 tz.gettz(self.context.time_zone)
256 )248 ).strftime(self._local_timeformat)
257249
258250
259class SprintAddView(LaunchpadFormView):251class SprintAddView(LaunchpadFormView):
diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py
index e11ce8c..418f074 100644
--- a/lib/lp/bugs/scripts/uct/models.py
+++ b/lib/lp/bugs/scripts/uct/models.py
@@ -42,7 +42,6 @@ from lp.registry.model.person import Person
42from lp.registry.model.product import Product42from lp.registry.model.product import Product
43from lp.registry.model.sourcepackage import SourcePackage43from lp.registry.model.sourcepackage import SourcePackage
44from lp.registry.model.sourcepackagename import SourcePackageName44from lp.registry.model.sourcepackagename import SourcePackageName
45from lp.services.compat import tzname
46from lp.services.propertycache import cachedproperty45from lp.services.propertycache import cachedproperty
4746
48__all__ = [47__all__ = [
@@ -389,7 +388,7 @@ class UCTRecord:
389388
390 @classmethod389 @classmethod
391 def _format_datetime(cls, dt: datetime) -> str:390 def _format_datetime(cls, dt: datetime) -> str:
392 return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M:%S"), tzname(dt))391 return dt.strftime("%Y-%m-%d %H:%M:%S %Z")
393392
394 @classmethod393 @classmethod
395 def _format_notes(cls, notes: List[Tuple[str, str]]) -> str:394 def _format_notes(cls, notes: List[Tuple[str, str]]) -> str:
diff --git a/lib/lp/bugs/tests/bug.py b/lib/lp/bugs/tests/bug.py
index 9798b9b..97dc8b5 100644
--- a/lib/lp/bugs/tests/bug.py
+++ b/lib/lp/bugs/tests/bug.py
@@ -40,7 +40,7 @@ def print_also_notified(bug_page):
4040
41def print_subscribers(bug_page, subscription_level=None, reverse=False):41def print_subscribers(bug_page, subscription_level=None, reverse=False):
42 """Print the subscribers listed in the subscribers JSON portlet."""42 """Print the subscribers listed in the subscribers JSON portlet."""
43 details = json.loads(bug_page.decode())43 details = json.loads(bug_page)
4444
45 if details is None:45 if details is None:
46 # No subscribers at all.46 # No subscribers at all.
diff --git a/lib/lp/buildmaster/tests/builderproxy.py b/lib/lp/buildmaster/tests/builderproxy.py
index a891b6c..fece785 100644
--- a/lib/lp/buildmaster/tests/builderproxy.py
+++ b/lib/lp/buildmaster/tests/builderproxy.py
@@ -28,7 +28,7 @@ class ProxyAuthAPITokensResource(resource.Resource):
28 self.requests = []28 self.requests = []
2929
30 def render_POST(self, request):30 def render_POST(self, request):
31 content = json.loads(request.content.read().decode("UTF-8"))31 content = json.loads(request.content.read())
32 self.requests.append(32 self.requests.append(
33 {33 {
34 "method": request.method,34 "method": request.method,
diff --git a/lib/lp/buildmaster/tests/fetchservice.py b/lib/lp/buildmaster/tests/fetchservice.py
index dd21e27..3fd879c 100644
--- a/lib/lp/buildmaster/tests/fetchservice.py
+++ b/lib/lp/buildmaster/tests/fetchservice.py
@@ -27,7 +27,7 @@ class FetchServiceAuthAPITokensResource(resource.Resource):
27 self.requests = []27 self.requests = []
2828
29 def render_POST(self, request):29 def render_POST(self, request):
30 content = json.loads(request.content.read().decode("UTF-8"))30 content = json.loads(request.content.read())
31 self.requests.append(31 self.requests.append(
32 {32 {
33 "method": request.method,33 "method": request.method,
diff --git a/lib/lp/charms/browser/tests/test_charmrecipe.py b/lib/lp/charms/browser/tests/test_charmrecipe.py
index d3a27b7..08313ab 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipe.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipe.py
@@ -423,7 +423,7 @@ class TestCharmRecipeAddView(BaseTestCharmRecipeView):
423 url=Equals("http://charmhub.example/v1/tokens"),423 url=Equals("http://charmhub.example/v1/tokens"),
424 method=Equals("POST"),424 method=Equals("POST"),
425 body=AfterPreprocessing(425 body=AfterPreprocessing(
426 lambda b: json.loads(b.decode()),426 json.loads,
427 Equals(427 Equals(
428 {428 {
429 "description": ("charmhub-name for launchpad.test"),429 "description": ("charmhub-name for launchpad.test"),
@@ -1094,7 +1094,7 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
1094 url=Equals("http://charmhub.example/v1/tokens"),1094 url=Equals("http://charmhub.example/v1/tokens"),
1095 method=Equals("POST"),1095 method=Equals("POST"),
1096 body=AfterPreprocessing(1096 body=AfterPreprocessing(
1097 lambda b: json.loads(b.decode()),1097 json.loads,
1098 Equals(1098 Equals(
1099 {1099 {
1100 "description": (f"{store_name} for launchpad.test"),1100 "description": (f"{store_name} for launchpad.test"),
@@ -1303,9 +1303,7 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
1303 ),1303 ),
1304 }1304 }
1305 ),1305 ),
1306 body=AfterPreprocessing(1306 body=AfterPreprocessing(json.loads, Equals({})),
1307 lambda b: json.loads(b.decode()), Equals({})
1308 ),
1309 )1307 )
1310 self.assertThat(1308 self.assertThat(
1311 responses.calls,1309 responses.calls,
diff --git a/lib/lp/charms/tests/test_charmhubclient.py b/lib/lp/charms/tests/test_charmhubclient.py
index 7ca8734..34cd783 100644
--- a/lib/lp/charms/tests/test_charmhubclient.py
+++ b/lib/lp/charms/tests/test_charmhubclient.py
@@ -119,9 +119,7 @@ class RequestMatches(MatchesAll):
119 if json_data is not None:119 if json_data is not None:
120 matchers.append(120 matchers.append(
121 MatchesStructure(121 MatchesStructure(
122 body=AfterPreprocessing(122 body=AfterPreprocessing(json.loads, Equals(json_data))
123 lambda b: json.loads(b.decode()), Equals(json_data)
124 )
125 )123 )
126 )124 )
127 elif file_data is not None:125 elif file_data is not None:
diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py
index 0b41a97..1493f6f 100644
--- a/lib/lp/charms/tests/test_charmrecipe.py
+++ b/lib/lp/charms/tests/test_charmrecipe.py
@@ -1043,7 +1043,7 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
1043 url=Equals("http://charmhub.example/v1/tokens"),1043 url=Equals("http://charmhub.example/v1/tokens"),
1044 method=Equals("POST"),1044 method=Equals("POST"),
1045 body=AfterPreprocessing(1045 body=AfterPreprocessing(
1046 lambda b: json.loads(b.decode()),1046 json.loads,
1047 Equals(1047 Equals(
1048 {1048 {
1049 "description": (1049 "description": (
@@ -1158,9 +1158,7 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
1158 ),1158 ),
1159 }1159 }
1160 ),1160 ),
1161 body=AfterPreprocessing(1161 body=AfterPreprocessing(json.loads, Equals({})),
1162 lambda b: json.loads(b.decode()), Equals({})
1163 ),
1164 )1162 )
1165 self.assertThat(1163 self.assertThat(
1166 responses.calls,1164 responses.calls,
@@ -2164,9 +2162,7 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
2164 "package-view-revisions",2162 "package-view-revisions",
2165 ],2163 ],
2166 }2164 }
2167 self.assertEqual(2165 self.assertEqual(expected_body, json.loads(call.request.body))
2168 expected_body, json.loads(call.request.body.decode("UTF-8"))
2169 )
2170 self.assertEqual({"root": root_macaroon_raw}, recipe.store_secrets)2166 self.assertEqual({"root": root_macaroon_raw}, recipe.store_secrets)
2171 return response, root_macaroon_raw2167 return response, root_macaroon_raw
21722168
@@ -2276,9 +2272,7 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
2276 ),2272 ),
2277 }2273 }
2278 ),2274 ),
2279 body=AfterPreprocessing(2275 body=AfterPreprocessing(json.loads, Equals({})),
2280 lambda b: json.loads(b.decode()), Equals({})
2281 ),
2282 )2276 )
2283 self.assertThat(2277 self.assertThat(
2284 responses.calls,2278 responses.calls,
diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py
index d6da0d3..089aee6 100644
--- a/lib/lp/code/browser/gitrepository.py
+++ b/lib/lp/code/browser/gitrepository.py
@@ -1101,9 +1101,7 @@ class GitRepositoryPermissionsView(LaunchpadFormView):
1101 field_type = field_bits[0]1101 field_type = field_bits[0]
1102 try:1102 try:
1103 ref_pattern = decode_form_field_id(field_bits[1])1103 ref_pattern = decode_form_field_id(field_bits[1])
1104 # base64.b32decode raises TypeError for decoding errors on Python 2,1104 except binascii.Error:
1105 # but binascii.Error on Python 3.
1106 except (TypeError, binascii.Error):
1107 raise UnexpectedFormData(1105 raise UnexpectedFormData(
1108 "Cannot parse field name: %s" % field_name1106 "Cannot parse field name: %s" % field_name
1109 )1107 )
diff --git a/lib/lp/code/model/tests/test_githosting.py b/lib/lp/code/model/tests/test_githosting.py
index 0f5cf62..c876123 100644
--- a/lib/lp/code/model/tests/test_githosting.py
+++ b/lib/lp/code/model/tests/test_githosting.py
@@ -106,9 +106,7 @@ class TestGitHostingClient(TestCase):
106 ),106 ),
107 )107 )
108 if json_data is not None:108 if json_data is not None:
109 self.assertEqual(109 self.assertEqual(json_data, json.loads(request.body))
110 json_data, json.loads(request.body.decode("UTF-8"))
111 )
112 timeline = get_request_timeline(get_current_browser_request())110 timeline = get_request_timeline(get_current_browser_request())
113 action = timeline.actions[-1]111 action = timeline.actions[-1]
114 self.assertEqual("git-hosting-%s" % method.lower(), action.category)112 self.assertEqual("git-hosting-%s" % method.lower(), action.category)
diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py
index 04061a7..0a1211d 100644
--- a/lib/lp/oci/model/ociregistryclient.py
+++ b/lib/lp/oci/model/ociregistryclient.py
@@ -69,7 +69,7 @@ class OCIRegistryClient:
69 """Read JSON out of a `LibraryFileAlias`."""69 """Read JSON out of a `LibraryFileAlias`."""
70 try:70 try:
71 reference.open()71 reference.open()
72 return json.loads(reference.read().decode("UTF-8"))72 return json.loads(reference.read())
73 finally:73 finally:
74 reference.close()74 reference.close()
7575
diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py
index c3dec1c..6d01ea9 100644
--- a/lib/lp/oci/tests/test_ociregistryclient.py
+++ b/lib/lp/oci/tests/test_ociregistryclient.py
@@ -220,7 +220,7 @@ class TestOCIRegistryClient(
220 # We should have uploaded to the digest, not the tag220 # We should have uploaded to the digest, not the tag
221 self.assertIn("sha256:", responses.calls[1].request.url)221 self.assertIn("sha256:", responses.calls[1].request.url)
222 self.assertNotIn("edge", responses.calls[1].request.url)222 self.assertNotIn("edge", responses.calls[1].request.url)
223 request = json.loads(responses.calls[1].request.body.decode("UTF-8"))223 request = json.loads(responses.calls[1].request.body)
224224
225 layer_matchers = [225 layer_matchers = [
226 MatchesDict(226 MatchesDict(
@@ -327,7 +327,7 @@ class TestOCIRegistryClient(
327327
328 self.client.upload(self.build)328 self.client.upload(self.build)
329329
330 request = json.loads(responses.calls[1].request.body.decode("UTF-8"))330 request = json.loads(responses.calls[1].request.body)
331331
332 layer_matchers = [332 layer_matchers = [
333 MatchesDict(333 MatchesDict(
@@ -1078,7 +1078,7 @@ class TestOCIRegistryClient(
1078 },1078 },
1079 ],1079 ],
1080 },1080 },
1081 json.loads(send_manifest_call.request.body.decode("UTF-8")),1081 json.loads(send_manifest_call.request.body),
1082 )1082 )
10831083
1084 @responses.activate1084 @responses.activate
@@ -1195,7 +1195,7 @@ class TestOCIRegistryClient(
1195 },1195 },
1196 ],1196 ],
1197 },1197 },
1198 json.loads(send_manifest_call.request.body.decode("UTF-8")),1198 json.loads(send_manifest_call.request.body),
1199 )1199 )
12001200
1201 @responses.activate1201 @responses.activate
@@ -1267,7 +1267,7 @@ class TestOCIRegistryClient(
1267 }1267 }
1268 ],1268 ],
1269 },1269 },
1270 json.loads(send_manifest_call.request.body.decode("UTF-8")),1270 json.loads(send_manifest_call.request.body),
1271 )1271 )
12721272
1273 @responses.activate1273 @responses.activate
@@ -1441,7 +1441,7 @@ class TestOCIRegistryClient(
1441 },1441 },
1442 ],1442 ],
1443 },1443 },
1444 json.loads(responses.calls[2].request.body.decode("UTF-8")),1444 json.loads(responses.calls[2].request.body),
1445 )1445 )
14461446
14471447
diff --git a/lib/lp/registry/interfaces/ssh.py b/lib/lp/registry/interfaces/ssh.py
index 5559526..e99a000 100644
--- a/lib/lp/registry/interfaces/ssh.py
+++ b/lib/lp/registry/interfaces/ssh.py
@@ -176,15 +176,5 @@ class SSHKeyAdditionError(Exception):
176 )176 )
177 if "exception" in kwargs:177 if "exception" in kwargs:
178 exception = kwargs.pop("exception")178 exception = kwargs.pop("exception")
179 try:179 msg = "%s (%s)" % (msg, exception)
180 exception_text = str(exception)
181 except UnicodeDecodeError:
182 # On Python 2, Key.fromString can raise exceptions with
183 # non-UTF-8 messages.
184 exception_text = (
185 bytes(exception)
186 .decode("unicode_escape")
187 .encode("unicode_escape")
188 )
189 msg = "%s (%s)" % (msg, exception_text)
190 super().__init__(msg, *args, **kwargs)180 super().__init__(msg, *args, **kwargs)
diff --git a/lib/lp/services/auth/utils.py b/lib/lp/services/auth/utils.py
index f4c997b..c1a780c 100644
--- a/lib/lp/services/auth/utils.py
+++ b/lib/lp/services/auth/utils.py
@@ -7,12 +7,9 @@ __all__ = [
7 "create_access_token_secret",7 "create_access_token_secret",
8]8]
99
10import binascii10import secrets
11import os
1211
1312
14# XXX cjwatson 2021-09-30: Replace this with secrets.token_hex(32) once we
15# can rely on Python 3.6 everywhere.
16def create_access_token_secret():13def create_access_token_secret():
17 """Create a secret suitable for use in a personal access token."""14 """Create a secret suitable for use in a personal access token."""
18 return binascii.hexlify(os.urandom(32)).decode("ASCII")15 return secrets.token_hex(32)
diff --git a/lib/lp/services/compat.py b/lib/lp/services/compat.py
index 86d833a..ee2c2fe 100644
--- a/lib/lp/services/compat.py
+++ b/lib/lp/services/compat.py
@@ -8,12 +8,9 @@ Use this for things that six doesn't provide.
88
9__all__ = [9__all__ = [
10 "message_as_bytes",10 "message_as_bytes",
11 "tzname",
12]11]
1312
14import io13import io
15from datetime import datetime, time, timezone
16from typing import Union
1714
1815
19def message_as_bytes(message):16def message_as_bytes(message):
@@ -24,18 +21,3 @@ def message_as_bytes(message):
24 g = BytesGenerator(fp, mangle_from_=False, maxheaderlen=0, policy=compat32)21 g = BytesGenerator(fp, mangle_from_=False, maxheaderlen=0, policy=compat32)
25 g.flatten(message)22 g.flatten(message)
26 return fp.getvalue()23 return fp.getvalue()
27
28
29def tzname(obj: Union[datetime, time]) -> str:
30 """Return this (date)time object's time zone name as a string.
31
32 Python 3.5's `timezone.utc.tzname` returns "UTC+00:00", rather than
33 "UTC" which is what we prefer. Paper over this until we can rely on
34 Python >= 3.6 everywhere.
35 """
36 if obj.tzinfo is None:
37 return ""
38 elif obj.tzinfo is timezone.utc:
39 return "UTC"
40 else:
41 return obj.tzname()
diff --git a/lib/lp/services/librarian/tests/test_client.py b/lib/lp/services/librarian/tests/test_client.py
index 012d1e1..f258bab 100644
--- a/lib/lp/services/librarian/tests/test_client.py
+++ b/lib/lp/services/librarian/tests/test_client.py
@@ -154,10 +154,7 @@ class LibrarianFileWrapperTestCase(TestCase):
154 def test_unbounded_read_incorrect_length(self):154 def test_unbounded_read_incorrect_length(self):
155 file = self.makeFile(extra_content_length=1)155 file = self.makeFile(extra_content_length=1)
156 with ExpectedException(http.client.IncompleteRead):156 with ExpectedException(http.client.IncompleteRead):
157 # Python 3 notices the short response on the first read.
158 self.assertEqual(b"abcdef", file.read())157 self.assertEqual(b"abcdef", file.read())
159 # Python 2 only notices the short response on the next read.
160 file.read()
161158
162 def test_bounded_read_correct_length(self):159 def test_bounded_read_correct_length(self):
163 file = self.makeFile()160 file = self.makeFile()
diff --git a/lib/lp/services/oauth/stories/authorize-token.rst b/lib/lp/services/oauth/stories/authorize-token.rst
index 61f3cc5..a3c30d1 100644
--- a/lib/lp/services/oauth/stories/authorize-token.rst
+++ b/lib/lp/services/oauth/stories/authorize-token.rst
@@ -169,7 +169,7 @@ the list of authentication levels.
169 >>> json_browser.open(169 >>> json_browser.open(
170 ... "http://launchpad.test/+authorize-token?%s" % urlencode(params)170 ... "http://launchpad.test/+authorize-token?%s" % urlencode(params)
171 ... )171 ... )
172 >>> json_token = json.loads(json_browser.contents.decode())172 >>> json_token = json.loads(json_browser.contents)
173 >>> sorted(json_token.keys())173 >>> sorted(json_token.keys())
174 ['access_levels', 'oauth_token', 'oauth_token_consumer']174 ['access_levels', 'oauth_token', 'oauth_token_consumer']
175175
@@ -190,7 +190,7 @@ the list of authentication levels.
190 ... )190 ... )
191 ... % urlencode(params)191 ... % urlencode(params)
192 ... )192 ... )
193 >>> json_token = json.loads(json_browser.contents.decode())193 >>> json_token = json.loads(json_browser.contents)
194 >>> sorted(194 >>> sorted(
195 ... (level["value"], level["title"])195 ... (level["value"], level["title"])
196 ... for level in json_token["access_levels"]196 ... for level in json_token["access_levels"]
diff --git a/lib/lp/services/oauth/stories/request-token.rst b/lib/lp/services/oauth/stories/request-token.rst
index 2bc110a..799fcc0 100644
--- a/lib/lp/services/oauth/stories/request-token.rst
+++ b/lib/lp/services/oauth/stories/request-token.rst
@@ -30,7 +30,7 @@ levels.
30 >>> json_browser.open(30 >>> json_browser.open(
31 ... "http://launchpad.test/+request-token", data=urlencode(data)31 ... "http://launchpad.test/+request-token", data=urlencode(data)
32 ... )32 ... )
33 >>> token = json.loads(json_browser.contents.decode())33 >>> token = json.loads(json_browser.contents)
34 >>> sorted(token.keys())34 >>> sorted(token.keys())
35 ['access_levels', 'oauth_token', 'oauth_token_consumer',35 ['access_levels', 'oauth_token', 'oauth_token_consumer',
36 'oauth_token_secret']36 'oauth_token_secret']
diff --git a/lib/lp/services/signing/testing/fakesigning.py b/lib/lp/services/signing/testing/fakesigning.py
index eb7b27f..243408b 100644
--- a/lib/lp/services/signing/testing/fakesigning.py
+++ b/lib/lp/services/signing/testing/fakesigning.py
@@ -89,7 +89,7 @@ class GenerateResource(BoxedAuthenticationResource):
89 self.requests = []89 self.requests = []
9090
91 def render_POST(self, request):91 def render_POST(self, request):
92 payload = json.loads(self._decrypt(request).decode("UTF-8"))92 payload = json.loads(self._decrypt(request))
93 self.requests.append(payload)93 self.requests.append(payload)
94 # We don't need to bother with generating a real key here. Just94 # We don't need to bother with generating a real key here. Just
95 # make up some random data.95 # make up some random data.
@@ -117,7 +117,7 @@ class SignResource(BoxedAuthenticationResource):
117 self.requests = []117 self.requests = []
118118
119 def render_POST(self, request):119 def render_POST(self, request):
120 payload = json.loads(self._decrypt(request).decode("UTF-8"))120 payload = json.loads(self._decrypt(request))
121 self.requests.append(payload)121 self.requests.append(payload)
122 _, public_key = self.keys[payload["fingerprint"]]122 _, public_key = self.keys[payload["fingerprint"]]
123 # We don't need to bother with generating a real signature here.123 # We don't need to bother with generating a real signature here.
@@ -143,7 +143,7 @@ class InjectResource(BoxedAuthenticationResource):
143 self.requests = []143 self.requests = []
144144
145 def render_POST(self, request):145 def render_POST(self, request):
146 payload = json.loads(self._decrypt(request).decode("UTF-8"))146 payload = json.loads(self._decrypt(request))
147 self.requests.append(payload)147 self.requests.append(payload)
148 private_key = base64.b64decode(payload["private-key"].encode("UTF-8"))148 private_key = base64.b64decode(payload["private-key"].encode("UTF-8"))
149 public_key = base64.b64decode(payload["public-key"].encode("UTF-8"))149 public_key = base64.b64decode(payload["public-key"].encode("UTF-8"))
diff --git a/lib/lp/services/signing/tests/test_proxy.py b/lib/lp/services/signing/tests/test_proxy.py
index 1db9530..6bb14ff 100644
--- a/lib/lp/services/signing/tests/test_proxy.py
+++ b/lib/lp/services/signing/tests/test_proxy.py
@@ -93,7 +93,7 @@ class SigningServiceResponseFactory:
93 """93 """
94 box = Box(self.service_private_key, self.client_public_key)94 box = Box(self.service_private_key, self.client_public_key)
95 decrypted = box.decrypt(value, self.nonce, encoder=Base64Encoder)95 decrypted = box.decrypt(value, self.nonce, encoder=Base64Encoder)
96 return json.loads(decrypted.decode("UTF-8"))96 return json.loads(decrypted)
9797
98 def getAPISignedContent(self, call_index=0):98 def getAPISignedContent(self, call_index=0):
99 """Returns the signed message returned by the API.99 """Returns the signed message returned by the API.
diff --git a/lib/lp/services/twistedsupport/xmlrpc.py b/lib/lp/services/twistedsupport/xmlrpc.py
index 7cdf389..8e75dfc 100644
--- a/lib/lp/services/twistedsupport/xmlrpc.py
+++ b/lib/lp/services/twistedsupport/xmlrpc.py
@@ -56,9 +56,7 @@ def trap_fault(failure, *fault_classes):
56 :param failure: A Twisted L{Failure}.56 :param failure: A Twisted L{Failure}.
57 :param *fault_codes: `LaunchpadFault` subclasses.57 :param *fault_codes: `LaunchpadFault` subclasses.
58 :raise Exception: if 'failure' is not a Fault failure, or if the fault58 :raise Exception: if 'failure' is not a Fault failure, or if the fault
59 code does not match the given codes. In line with L{Failure.trap},59 code does not match the given codes.
60 the exception is the L{Failure} itself on Python 2 and the
61 underlying exception on Python 3.
62 :return: The Fault if it matches one of the codes.60 :return: The Fault if it matches one of the codes.
63 """61 """
64 failure.trap(xmlrpc.Fault)62 failure.trap(xmlrpc.Fault)
diff --git a/lib/lp/services/webapp/tests/test_candid.py b/lib/lp/services/webapp/tests/test_candid.py
index ea1898b..dfb4a8a 100644
--- a/lib/lp/services/webapp/tests/test_candid.py
+++ b/lib/lp/services/webapp/tests/test_candid.py
@@ -499,8 +499,7 @@ class TestCandidCallbackView(TestCaseWithFactory):
499 }499 }
500 ),500 ),
501 body=AfterPreprocessing(501 body=AfterPreprocessing(
502 lambda b: json.loads(b.decode()),502 json.loads, MatchesDict({"code": Equals("test code")})
503 MatchesDict({"code": Equals("test code")}),
504 ),503 ),
505 )504 )
506 discharge_matcher = MatchesStructure(505 discharge_matcher = MatchesStructure(
diff --git a/lib/lp/services/webapp/tests/test_view_model.py b/lib/lp/services/webapp/tests/test_view_model.py
index 329fa64..f4b6ed6 100644
--- a/lib/lp/services/webapp/tests/test_view_model.py
+++ b/lib/lp/services/webapp/tests/test_view_model.py
@@ -123,7 +123,7 @@ class TestJsonModelView(BrowserTestCase):
123 lp.services.webapp.tests.ProductModelTestView = ProductModelTestView123 lp.services.webapp.tests.ProductModelTestView = ProductModelTestView
124 self.configZCML()124 self.configZCML()
125 browser = self.getUserBrowser(self.url)125 browser = self.getUserBrowser(self.url)
126 cache = json.loads(browser.contents.decode())126 cache = json.loads(browser.contents)
127 self.assertThat(cache, KeysEqual("related_features", "context"))127 self.assertThat(cache, KeysEqual("related_features", "context"))
128128
129 def test_JsonModel_custom_cache(self):129 def test_JsonModel_custom_cache(self):
@@ -140,7 +140,7 @@ class TestJsonModelView(BrowserTestCase):
140 lp.services.webapp.tests.ProductModelTestView = ProductModelTestView140 lp.services.webapp.tests.ProductModelTestView = ProductModelTestView
141 self.configZCML()141 self.configZCML()
142 browser = self.getUserBrowser(self.url)142 browser = self.getUserBrowser(self.url)
143 cache = json.loads(browser.contents.decode())143 cache = json.loads(browser.contents)
144 self.assertThat(144 self.assertThat(
145 cache, KeysEqual("related_features", "context", "target_info")145 cache, KeysEqual("related_features", "context", "target_info")
146 )146 )
@@ -165,7 +165,7 @@ class TestJsonModelView(BrowserTestCase):
165 lp.services.webapp.tests.ProductModelTestView = ProductModelTestView165 lp.services.webapp.tests.ProductModelTestView = ProductModelTestView
166 self.configZCML()166 self.configZCML()
167 browser = self.getUserBrowser(self.url)167 browser = self.getUserBrowser(self.url)
168 cache = json.loads(browser.contents.decode())168 cache = json.loads(browser.contents)
169 self.assertThat(169 self.assertThat(
170 cache, KeysEqual("related_features", "context", "target_info")170 cache, KeysEqual("related_features", "context", "target_info")
171 )171 )
diff --git a/lib/lp/services/webapp/url.py b/lib/lp/services/webapp/url.py
index ba3df8a..59c68dc 100644
--- a/lib/lp/services/webapp/url.py
+++ b/lib/lp/services/webapp/url.py
@@ -90,7 +90,7 @@ def urlparse(url, scheme="", allow_fragments=True):
9090
91 The url parameter should contain ASCII characters only. This91 The url parameter should contain ASCII characters only. This
92 function ensures that the original urlparse is called always with a92 function ensures that the original urlparse is called always with a
93 str object, and never unicode (Python 2) or bytes (Python 3).93 str object, and never bytes.
9494
95 >>> tuple(urlparse("http://foo.com/bar"))95 >>> tuple(urlparse("http://foo.com/bar"))
96 ('http', 'foo.com', '/bar', '', '', '')96 ('http', 'foo.com', '/bar', '', '', '')
@@ -120,7 +120,7 @@ def urlsplit(url, scheme="", allow_fragments=True):
120120
121 The url parameter should contain ASCII characters only. This121 The url parameter should contain ASCII characters only. This
122 function ensures that the original urlsplit is called always with a122 function ensures that the original urlsplit is called always with a
123 str object, and never unicode (Python 2) or bytes (Python 3).123 str object, and never bytes.
124124
125 >>> tuple(urlsplit("http://foo.com/baz"))125 >>> tuple(urlsplit("http://foo.com/baz"))
126 ('http', 'foo.com', '/baz', '', '')126 ('http', 'foo.com', '/baz', '', '')
diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
index 8d2a48a..819d770 100644
--- a/lib/lp/snappy/browser/tests/test_snap.py
+++ b/lib/lp/snappy/browser/tests/test_snap.py
@@ -653,9 +653,7 @@ class TestSnapAddView(BaseTestSnapView):
653 ],653 ],
654 "permissions": ["package_upload"],654 "permissions": ["package_upload"],
655 }655 }
656 self.assertEqual(656 self.assertEqual(expected_body, json.loads(call.request.body))
657 expected_body, json.loads(call.request.body.decode("UTF-8"))
658 )
659 self.assertEqual(303, browser.responseStatusCode)657 self.assertEqual(303, browser.responseStatusCode)
660 parsed_location = urlsplit(browser.headers["Location"])658 parsed_location = urlsplit(browser.headers["Location"])
661 self.assertEqual(659 self.assertEqual(
@@ -1737,9 +1735,7 @@ class TestSnapEditView(BaseTestSnapView):
1737 "packages": [{"name": "two", "series": self.snappyseries.name}],1735 "packages": [{"name": "two", "series": self.snappyseries.name}],
1738 "permissions": ["package_upload"],1736 "permissions": ["package_upload"],
1739 }1737 }
1740 self.assertEqual(1738 self.assertEqual(expected_body, json.loads(call.request.body))
1741 expected_body, json.loads(call.request.body.decode("UTF-8"))
1742 )
1743 self.assertEqual(303, browser.responseStatusCode)1739 self.assertEqual(303, browser.responseStatusCode)
1744 parsed_location = urlsplit(browser.headers["Location"])1740 parsed_location = urlsplit(browser.headers["Location"])
1745 self.assertEqual(1741 self.assertEqual(
@@ -1820,9 +1816,7 @@ class TestSnapAuthorizeView(BaseTestSnapView):
1820 ],1816 ],
1821 "permissions": ["package_upload"],1817 "permissions": ["package_upload"],
1822 }1818 }
1823 self.assertEqual(1819 self.assertEqual(expected_body, json.loads(call.request.body))
1824 expected_body, json.loads(call.request.body.decode("UTF-8"))
1825 )
1826 self.assertEqual(1820 self.assertEqual(
1827 {"root": root_macaroon_raw}, self.snap.store_secrets1821 {"root": root_macaroon_raw}, self.snap.store_secrets
1828 )1822 )
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index 32a2d5c..9f671b8 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -4892,9 +4892,7 @@ class TestSnapWebservice(TestCaseWithFactory):
4892 ],4892 ],
4893 "permissions": ["package_upload"],4893 "permissions": ["package_upload"],
4894 }4894 }
4895 self.assertEqual(4895 self.assertEqual(expected_body, json.loads(call.request.body))
4896 expected_body, json.loads(call.request.body.decode("UTF-8"))
4897 )
4898 self.assertEqual({"root": root_macaroon_raw}, snap.store_secrets)4896 self.assertEqual({"root": root_macaroon_raw}, snap.store_secrets)
4899 return response, root_macaroon.third_party_caveats()[0]4897 return response, root_macaroon.third_party_caveats()[0]
49004898
diff --git a/lib/lp/snappy/tests/test_snapstoreclient.py b/lib/lp/snappy/tests/test_snapstoreclient.py
index d6f75bd..ddbf457 100644
--- a/lib/lp/snappy/tests/test_snapstoreclient.py
+++ b/lib/lp/snappy/tests/test_snapstoreclient.py
@@ -227,9 +227,7 @@ class RequestMatches(Matcher):
227 if mismatch is not None:227 if mismatch is not None:
228 return mismatch228 return mismatch
229 if self.json_data is not None:229 if self.json_data is not None:
230 mismatch = Equals(self.json_data).match(230 mismatch = Equals(self.json_data).match(json.loads(request.body))
231 json.loads(request.body.decode("UTF-8"))
232 )
233 if mismatch is not None:231 if mismatch is not None:
234 return mismatch232 return mismatch
235 if self.form_data is not None:233 if self.form_data is not None:
diff --git a/lib/lp/soyuz/wsgi/archiveauth.py b/lib/lp/soyuz/wsgi/archiveauth.py
index 006143d..17440e8 100644
--- a/lib/lp/soyuz/wsgi/archiveauth.py
+++ b/lib/lp/soyuz/wsgi/archiveauth.py
@@ -11,10 +11,8 @@ __all__ = [
11]11]
1212
13import crypt13import crypt
14import string
15import sys14import sys
16import time15import time
17from random import SystemRandom
18from xmlrpc.client import Fault, ServerProxy16from xmlrpc.client import Fault, ServerProxy
1917
20import six18import six
@@ -49,16 +47,6 @@ def _get_archive_reference(environ):
49 _log(environ, "No archive reference found in URL '%s'.", path)47 _log(environ, "No archive reference found in URL '%s'.", path)
5048
5149
52_sr = SystemRandom()
53
54
55def _crypt_sha256(word):
56 """crypt.crypt(word, crypt.METHOD_SHA256), backported from Python 3.5."""
57 saltchars = string.ascii_letters + string.digits + "./"
58 salt = "$5$" + "".join(_sr.choice(saltchars) for _ in range(16))
59 return crypt.crypt(word, salt)
60
61
62_memcache_client = memcache_client_factory(timeline=False)50_memcache_client = memcache_client_factory(timeline=False)
6351
6452
@@ -91,7 +79,9 @@ def check_password(environ, user, password):
91 proxy.checkArchiveAuthToken(archive_reference, user, password)79 proxy.checkArchiveAuthToken(archive_reference, user, password)
92 # Cache positive responses for a minute to reduce database load.80 # Cache positive responses for a minute to reduce database load.
93 _memcache_client.set(81 _memcache_client.set(
94 memcache_key, _crypt_sha256(password), int(time.time()) + 6082 memcache_key,
83 crypt.crypt(password, crypt.METHOD_SHA256),
84 int(time.time()) + 60,
95 )85 )
96 _log(environ, "%s@%s: Authorized.", user, archive_reference)86 _log(environ, "%s@%s: Authorized.", user, archive_reference)
97 return True87 return True
diff --git a/lib/lp/soyuz/wsgi/tests/test_archiveauth.py b/lib/lp/soyuz/wsgi/tests/test_archiveauth.py
index 3ac5ff9..16e8a2b 100644
--- a/lib/lp/soyuz/wsgi/tests/test_archiveauth.py
+++ b/lib/lp/soyuz/wsgi/tests/test_archiveauth.py
@@ -106,12 +106,6 @@ class TestWSGIArchiveAuth(TestCaseWithFactory):
106 self.assertEqual({}, self.memcache_fixture._cache)106 self.assertEqual({}, self.memcache_fixture._cache)
107 self.assertLogs("No archive found for '~nonexistent/unknown/bad'.")107 self.assertLogs("No archive found for '~nonexistent/unknown/bad'.")
108108
109 def test_crypt_sha256(self):
110 crypted_password = archiveauth._crypt_sha256("secret")
111 self.assertEqual(
112 crypted_password, crypt.crypt("secret", crypted_password)
113 )
114
115 def makeArchiveAndToken(self):109 def makeArchiveAndToken(self):
116 archive = self.factory.makeArchive(private=True)110 archive = self.factory.makeArchive(private=True)
117 archive_path = "/%s/%s/ubuntu" % (archive.owner.name, archive.name)111 archive_path = "/%s/%s/ubuntu" % (archive.owner.name, archive.name)
diff --git a/lib/lp/testing/swift/fakeswift.py b/lib/lp/testing/swift/fakeswift.py
index 325c390..68126cb 100644
--- a/lib/lp/testing/swift/fakeswift.py
+++ b/lib/lp/testing/swift/fakeswift.py
@@ -107,9 +107,7 @@ class FakeKeystone(resource.Resource):
107 if "application/json" not in request.getHeader("content-type"):107 if "application/json" not in request.getHeader("content-type"):
108 request.setResponseCode(http.BAD_REQUEST)108 request.setResponseCode(http.BAD_REQUEST)
109 return b""109 return b""
110 # XXX cjwatson 2020-06-15: Python 3.5 doesn't allow this to be a110 credentials = json.loads(request.content.read())
111 # binary file; 3.6 does.
112 credentials = json.loads(request.content.read().decode("UTF-8"))
113 if "auth" not in credentials:111 if "auth" not in credentials:
114 request.setResponseCode(http.FORBIDDEN)112 request.setResponseCode(http.FORBIDDEN)
115 return b""113 return b""
diff --git a/lib/lp/translations/vocabularies.py b/lib/lp/translations/vocabularies.py
index a564444..d9c03dc 100644
--- a/lib/lp/translations/vocabularies.py
+++ b/lib/lp/translations/vocabularies.py
@@ -18,7 +18,6 @@ from storm.locals import Desc, Not, Or
18from zope.schema.vocabulary import SimpleTerm18from zope.schema.vocabulary import SimpleTerm
1919
20from lp.registry.interfaces.distroseries import IDistroSeries20from lp.registry.interfaces.distroseries import IDistroSeries
21from lp.services.compat import tzname
22from lp.services.webapp.vocabulary import (21from lp.services.webapp.vocabulary import (
23 NamedStormVocabulary,22 NamedStormVocabulary,
24 StormVocabularyBase,23 StormVocabularyBase,
@@ -102,10 +101,7 @@ class FilteredLanguagePackVocabularyBase(StormVocabularyBase):
102101
103 def toTerm(self, obj):102 def toTerm(self, obj):
104 return SimpleTerm(103 return SimpleTerm(
105 obj,104 obj, obj.id, "%s" % obj.date_exported.strftime("%F %T %Z")
106 obj.id,
107 "%s %s"
108 % (obj.date_exported.strftime("%F %T"), tzname(obj.date_exported)),
109 )105 )
110106
111 @property107 @property
@@ -143,12 +139,8 @@ class FilteredLanguagePackVocabulary(FilteredLanguagePackVocabularyBase):
143 return SimpleTerm(139 return SimpleTerm(
144 obj,140 obj,
145 obj.id,141 obj.id,
146 "%s %s (%s)"142 "%s (%s)"
147 % (143 % (obj.date_exported.strftime("%F %T %Z"), obj.type.title),
148 obj.date_exported.strftime("%F %T"),
149 tzname(obj.date_exported),
150 obj.type.title,
151 ),
152 )144 )
153145
154 @property146 @property
diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt
index cc28c81..b7b7957 100644
--- a/requirements/launchpad.txt
+++ b/requirements/launchpad.txt
@@ -121,9 +121,6 @@ patiencediff==0.2.2
121pexpect==4.8.0121pexpect==4.8.0
122pgbouncer==0.0.9122pgbouncer==0.0.9
123pickleshare==0.7.5123pickleshare==0.7.5
124# pkginfo 1.7.0 dropped Python 3.5 support, but we need the features of the
125# newer version, and both our and the package's test suite show no
126# incompatibilities
127pkginfo==1.8.2124pkginfo==1.8.2
128prettytable==0.7.2125prettytable==0.7.2
129prompt-toolkit==2.0.10126prompt-toolkit==2.0.10

Subscribers

People subscribed via source and target branches

to status/vote changes: