Merge ~cjwatson/launchpad:timezone-utc-prepare into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: b07ee10556832a6d9ffa13d19481ad214035e486
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:timezone-utc-prepare
Merge into: launchpad:master
Diff against target: 268 lines (+72/-21)
6 files modified
lib/lp/app/browser/tales.py (+4/-2)
lib/lp/app/widgets/date.py (+26/-6)
lib/lp/blueprints/browser/sprint.py (+10/-8)
lib/lp/bugs/scripts/uct/models.py (+2/-1)
lib/lp/services/compat.py (+19/-1)
lib/lp/translations/vocabularies.py (+11/-3)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+438651@code.launchpad.net

Commit message

Prepare for use of non-pytz timezones

Description of the change

It would be good to be able to port away from `pytz`; in particular, `pytz.UTC` can be replaced with the standard library's `datetime.timezone.utc` these days. However, there were a few `pytz`-specific assumptions in the date widget, and we currently need to work around some slightly suboptimal behaviour of `datetime.timezone.utc` in Python 3.5.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) :
review: Approve

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 ed832e8..0377cff 100644
3--- a/lib/lp/app/browser/tales.py
4+++ b/lib/lp/app/browser/tales.py
5@@ -55,6 +55,7 @@ from lp.registry.interfaces.distributionsourcepackage import (
6 from lp.registry.interfaces.person import IPerson
7 from lp.registry.interfaces.product import IProduct
8 from lp.registry.interfaces.projectgroup import IProjectGroup
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@@ -1292,7 +1293,8 @@ class PersonFormatterAPI(ObjectFormatterAPI):
14 def local_time(self):
15 """Return the local time for this person."""
16 time_zone = self._context.time_zone
17- return datetime.now(pytz.timezone(time_zone)).strftime("%T %Z")
18+ dt = datetime.now(pytz.timezone(time_zone))
19+ return "%s %s" % (dt.strftime("%T"), tzname(dt))
20
21 def url(self, view_name=None, rootsite="mainsite"):
22 """See `ObjectFormatterAPI`.
23@@ -2383,7 +2385,7 @@ class DateTimeFormatterAPI:
24 def time(self):
25 if self._datetime.tzinfo:
26 value = self._datetime.astimezone(getUtility(ILaunchBag).time_zone)
27- return value.strftime("%T %Z")
28+ return "%s %s" % (value.strftime("%T"), tzname(value))
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 7925a3c..259007d 100644
34--- a/lib/lp/app/widgets/date.py
35+++ b/lib/lp/app/widgets/date.py
36@@ -17,7 +17,7 @@ __all__ = [
37 "DatetimeDisplayWidget",
38 ]
39
40-from datetime import datetime
41+from datetime import datetime, timezone
42
43 import pytz
44 from zope.browserpage import ViewPageTemplateFile
45@@ -32,6 +32,7 @@ from zope.formlib.textwidgets import TextWidget
46 from zope.formlib.widget import DisplayWidget
47
48 from lp.app.validators import LaunchpadValidationError
49+from lp.services.compat import tzname
50 from lp.services.utils import round_half_up
51 from lp.services.webapp.escaping import html_escape
52 from lp.services.webapp.interfaces import ILaunchBag
53@@ -217,7 +218,13 @@ class DateTimeWidget(TextWidget):
54 @property
55 def time_zone_name(self):
56 """The name of the widget time zone for display in the widget."""
57- return self.time_zone.zone
58+ # XXX cjwatson 2023-03-09: In Python < 3.6, `timezone.utc.tzname`
59+ # returns "UTC+00:00" rather than "UTC". Drop this once we require
60+ # Python >= 3.6.
61+ if self.time_zone is timezone.utc:
62+ return "UTC"
63+ else:
64+ return self.time_zone.tzname(None)
65
66 def _align_date_constraints_with_time_zone(self):
67 """Ensure that from_date and to_date use the widget time zone."""
68@@ -225,14 +232,22 @@ class DateTimeWidget(TextWidget):
69 if self.from_date.tzinfo is None:
70 # Timezone-naive constraint is interpreted as being in the
71 # widget time zone.
72- self.from_date = self.time_zone.localize(self.from_date)
73+ if hasattr(self.time_zone, "localize"): # pytz
74+ self.from_date = self.time_zone.localize(self.from_date)
75+ else:
76+ self.from_date = self.from_date.replace(
77+ tzinfo=self.time_zone
78+ )
79 else:
80 self.from_date = self.from_date.astimezone(self.time_zone)
81 if isinstance(self.to_date, datetime):
82 if self.to_date.tzinfo is None:
83 # Timezone-naive constraint is interpreted as being in the
84 # widget time zone.
85- self.to_date = self.time_zone.localize(self.to_date)
86+ if hasattr(self.time_zone, "localize"): # pytz
87+ self.to_date = self.time_zone.localize(self.to_date)
88+ else:
89+ self.to_date = self.to_date.replace(tzinfo=self.time_zone)
90 else:
91 self.to_date = self.to_date.astimezone(self.time_zone)
92
93@@ -411,7 +426,10 @@ class DateTimeWidget(TextWidget):
94 dt = datetime(year, month, day, hour, minute, int(second), micro)
95 except (DateTimeError, ValueError, IndexError) as v:
96 raise ConversionError("Invalid date value", v)
97- return self.time_zone.localize(dt)
98+ if hasattr(self.time_zone, "localize"): # pytz
99+ return self.time_zone.localize(dt)
100+ else:
101+ return dt.replace(tzinfo=self.time_zone)
102
103 def _toFormValue(self, value):
104 """Convert a date to its string representation.
105@@ -621,4 +639,6 @@ class DatetimeDisplayWidget(DisplayWidget):
106 if value == self.context.missing_value:
107 return ""
108 value = value.astimezone(time_zone)
109- return html_escape(value.strftime("%Y-%m-%d %H:%M:%S %Z"))
110+ return html_escape(
111+ "%s %s" % (value.strftime("%Y-%m-%d %H:%M:%S", tzname(value)))
112+ )
113diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
114index 9d66d8b..a89a362 100644
115--- a/lib/lp/blueprints/browser/sprint.py
116+++ b/lib/lp/blueprints/browser/sprint.py
117@@ -61,6 +61,7 @@ from lp.registry.browser.menu import (
118 RegistryCollectionActionMenuBase,
119 )
120 from lp.registry.interfaces.person import IPersonSet
121+from lp.services.compat import tzname
122 from lp.services.database.bulk import load_referencing
123 from lp.services.helpers import shortlist
124 from lp.services.propertycache import cachedproperty
125@@ -228,30 +229,31 @@ class SprintView(HasSpecificationsView):
126 def formatDateTime(self, dt):
127 """Format a datetime value according to the sprint's time zone"""
128 dt = dt.astimezone(self.tzinfo)
129- return dt.strftime("%Y-%m-%d %H:%M %Z")
130+ return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M"), tzname(dt))
131
132 def formatDate(self, dt):
133 """Format a date value according to the sprint's time zone"""
134 dt = dt.astimezone(self.tzinfo)
135 return dt.strftime("%Y-%m-%d")
136
137- _local_timeformat = "%H:%M %Z on %A, %Y-%m-%d"
138+ def _formatLocal(self, dt):
139+ return "%s %s on %s" % (
140+ dt.strftime("%H:%M"),
141+ tzname(dt),
142+ dt.strftime("%A, %Y-%m-%d"),
143+ )
144
145 @property
146 def local_start(self):
147 """The sprint start time, in the local time zone, as text."""
148 tz = pytz.timezone(self.context.time_zone)
149- return self.context.time_starts.astimezone(tz).strftime(
150- self._local_timeformat
151- )
152+ return self._formatLocal(self.context.time_starts.astimezone(tz))
153
154 @property
155 def local_end(self):
156 """The sprint end time, in the local time zone, as text."""
157 tz = pytz.timezone(self.context.time_zone)
158- return self.context.time_ends.astimezone(tz).strftime(
159- self._local_timeformat
160- )
161+ return self._formatLocal(self.context.time_ends.astimezone(tz))
162
163
164 class SprintAddView(LaunchpadFormView):
165diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py
166index fd77ea5..d9f92a2 100644
167--- a/lib/lp/bugs/scripts/uct/models.py
168+++ b/lib/lp/bugs/scripts/uct/models.py
169@@ -42,6 +42,7 @@ from lp.registry.model.person import Person
170 from lp.registry.model.product import Product
171 from lp.registry.model.sourcepackage import SourcePackage
172 from lp.registry.model.sourcepackagename import SourcePackageName
173+from lp.services.compat import tzname
174 from lp.services.propertycache import cachedproperty
175
176 __all__ = [
177@@ -410,7 +411,7 @@ class UCTRecord:
178
179 @classmethod
180 def _format_datetime(cls, dt: datetime) -> str:
181- return dt.strftime("%Y-%m-%d %H:%M:%S %Z")
182+ return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M:%S"), tzname(dt))
183
184 @classmethod
185 def _format_notes(cls, notes: List[Tuple[str, str]]) -> str:
186diff --git a/lib/lp/services/compat.py b/lib/lp/services/compat.py
187index 15a0f4c..86d833a 100644
188--- a/lib/lp/services/compat.py
189+++ b/lib/lp/services/compat.py
190@@ -1,16 +1,19 @@
191 # Copyright 2020 Canonical Ltd. This software is licensed under the
192 # GNU Affero General Public License version 3 (see the file LICENSE).
193
194-"""Python 2/3 compatibility layer.
195+"""Python compatibility layer.
196
197 Use this for things that six doesn't provide.
198 """
199
200 __all__ = [
201 "message_as_bytes",
202+ "tzname",
203 ]
204
205 import io
206+from datetime import datetime, time, timezone
207+from typing import Union
208
209
210 def message_as_bytes(message):
211@@ -21,3 +24,18 @@ def message_as_bytes(message):
212 g = BytesGenerator(fp, mangle_from_=False, maxheaderlen=0, policy=compat32)
213 g.flatten(message)
214 return fp.getvalue()
215+
216+
217+def tzname(obj: Union[datetime, time]) -> str:
218+ """Return this (date)time object's time zone name as a string.
219+
220+ Python 3.5's `timezone.utc.tzname` returns "UTC+00:00", rather than
221+ "UTC" which is what we prefer. Paper over this until we can rely on
222+ Python >= 3.6 everywhere.
223+ """
224+ if obj.tzinfo is None:
225+ return ""
226+ elif obj.tzinfo is timezone.utc:
227+ return "UTC"
228+ else:
229+ return obj.tzname()
230diff --git a/lib/lp/translations/vocabularies.py b/lib/lp/translations/vocabularies.py
231index ff69730..fa9d754 100644
232--- a/lib/lp/translations/vocabularies.py
233+++ b/lib/lp/translations/vocabularies.py
234@@ -17,6 +17,7 @@ from storm.locals import Desc, Not, Or
235 from zope.schema.vocabulary import SimpleTerm
236
237 from lp.registry.interfaces.distroseries import IDistroSeries
238+from lp.services.compat import tzname
239 from lp.services.database.sqlobject import AND
240 from lp.services.webapp.vocabulary import (
241 NamedStormVocabulary,
242@@ -136,7 +137,10 @@ class FilteredLanguagePackVocabularyBase(StormVocabularyBase):
243
244 def toTerm(self, obj):
245 return SimpleTerm(
246- obj, obj.id, "%s" % obj.date_exported.strftime("%F %T %Z")
247+ obj,
248+ obj.id,
249+ "%s %s"
250+ % (obj.date_exported.strftime("%F %T"), tzname(obj.date_exported)),
251 )
252
253 @property
254@@ -174,8 +178,12 @@ class FilteredLanguagePackVocabulary(FilteredLanguagePackVocabularyBase):
255 return SimpleTerm(
256 obj,
257 obj.id,
258- "%s (%s)"
259- % (obj.date_exported.strftime("%F %T %Z"), obj.type.title),
260+ "%s %s (%s)"
261+ % (
262+ obj.date_exported.strftime("%F %T"),
263+ tzname(obj.date_exported),
264+ obj.type.title,
265+ ),
266 )
267
268 @property

Subscribers

People subscribed via source and target branches

to status/vote changes: