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
diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
index ed832e8..0377cff 100644
--- a/lib/lp/app/browser/tales.py
+++ b/lib/lp/app/browser/tales.py
@@ -55,6 +55,7 @@ from lp.registry.interfaces.distributionsourcepackage import (
55from lp.registry.interfaces.person import IPerson55from 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.services.compat import tzname
58from lp.services.utils import round_half_up59from lp.services.utils import round_half_up
59from lp.services.webapp.authorization import check_permission60from lp.services.webapp.authorization import check_permission
60from lp.services.webapp.canonicalurl import nearest_adapter61from lp.services.webapp.canonicalurl import nearest_adapter
@@ -1292,7 +1293,8 @@ class PersonFormatterAPI(ObjectFormatterAPI):
1292 def local_time(self):1293 def local_time(self):
1293 """Return the local time for this person."""1294 """Return the local time for this person."""
1294 time_zone = self._context.time_zone1295 time_zone = self._context.time_zone
1295 return datetime.now(pytz.timezone(time_zone)).strftime("%T %Z")1296 dt = datetime.now(pytz.timezone(time_zone))
1297 return "%s %s" % (dt.strftime("%T"), tzname(dt))
12961298
1297 def url(self, view_name=None, rootsite="mainsite"):1299 def url(self, view_name=None, rootsite="mainsite"):
1298 """See `ObjectFormatterAPI`.1300 """See `ObjectFormatterAPI`.
@@ -2383,7 +2385,7 @@ class DateTimeFormatterAPI:
2383 def time(self):2385 def time(self):
2384 if self._datetime.tzinfo:2386 if self._datetime.tzinfo:
2385 value = self._datetime.astimezone(getUtility(ILaunchBag).time_zone)2387 value = self._datetime.astimezone(getUtility(ILaunchBag).time_zone)
2386 return value.strftime("%T %Z")2388 return "%s %s" % (value.strftime("%T"), tzname(value))
2387 else:2389 else:
2388 return self._datetime.strftime("%T")2390 return self._datetime.strftime("%T")
23892391
diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py
index 7925a3c..259007d 100644
--- a/lib/lp/app/widgets/date.py
+++ b/lib/lp/app/widgets/date.py
@@ -17,7 +17,7 @@ __all__ = [
17 "DatetimeDisplayWidget",17 "DatetimeDisplayWidget",
18]18]
1919
20from datetime import datetime20from datetime import datetime, timezone
2121
22import pytz22import pytz
23from zope.browserpage import ViewPageTemplateFile23from zope.browserpage import ViewPageTemplateFile
@@ -32,6 +32,7 @@ from zope.formlib.textwidgets import TextWidget
32from zope.formlib.widget import DisplayWidget32from zope.formlib.widget import DisplayWidget
3333
34from lp.app.validators import LaunchpadValidationError34from lp.app.validators import LaunchpadValidationError
35from lp.services.compat import tzname
35from lp.services.utils import round_half_up36from lp.services.utils import round_half_up
36from lp.services.webapp.escaping import html_escape37from lp.services.webapp.escaping import html_escape
37from lp.services.webapp.interfaces import ILaunchBag38from lp.services.webapp.interfaces import ILaunchBag
@@ -217,7 +218,13 @@ class DateTimeWidget(TextWidget):
217 @property218 @property
218 def time_zone_name(self):219 def time_zone_name(self):
219 """The name of the widget time zone for display in the widget."""220 """The name of the widget time zone for display in the widget."""
220 return self.time_zone.zone221 # XXX cjwatson 2023-03-09: In Python < 3.6, `timezone.utc.tzname`
222 # returns "UTC+00:00" rather than "UTC". Drop this once we require
223 # Python >= 3.6.
224 if self.time_zone is timezone.utc:
225 return "UTC"
226 else:
227 return self.time_zone.tzname(None)
221228
222 def _align_date_constraints_with_time_zone(self):229 def _align_date_constraints_with_time_zone(self):
223 """Ensure that from_date and to_date use the widget time zone."""230 """Ensure that from_date and to_date use the widget time zone."""
@@ -225,14 +232,22 @@ class DateTimeWidget(TextWidget):
225 if self.from_date.tzinfo is None:232 if self.from_date.tzinfo is None:
226 # Timezone-naive constraint is interpreted as being in the233 # Timezone-naive constraint is interpreted as being in the
227 # widget time zone.234 # widget time zone.
228 self.from_date = self.time_zone.localize(self.from_date)235 if hasattr(self.time_zone, "localize"): # pytz
236 self.from_date = self.time_zone.localize(self.from_date)
237 else:
238 self.from_date = self.from_date.replace(
239 tzinfo=self.time_zone
240 )
229 else:241 else:
230 self.from_date = self.from_date.astimezone(self.time_zone)242 self.from_date = self.from_date.astimezone(self.time_zone)
231 if isinstance(self.to_date, datetime):243 if isinstance(self.to_date, datetime):
232 if self.to_date.tzinfo is None:244 if self.to_date.tzinfo is None:
233 # Timezone-naive constraint is interpreted as being in the245 # Timezone-naive constraint is interpreted as being in the
234 # widget time zone.246 # widget time zone.
235 self.to_date = self.time_zone.localize(self.to_date)247 if hasattr(self.time_zone, "localize"): # pytz
248 self.to_date = self.time_zone.localize(self.to_date)
249 else:
250 self.to_date = self.to_date.replace(tzinfo=self.time_zone)
236 else:251 else:
237 self.to_date = self.to_date.astimezone(self.time_zone)252 self.to_date = self.to_date.astimezone(self.time_zone)
238253
@@ -411,7 +426,10 @@ class DateTimeWidget(TextWidget):
411 dt = datetime(year, month, day, hour, minute, int(second), micro)426 dt = datetime(year, month, day, hour, minute, int(second), micro)
412 except (DateTimeError, ValueError, IndexError) as v:427 except (DateTimeError, ValueError, IndexError) as v:
413 raise ConversionError("Invalid date value", v)428 raise ConversionError("Invalid date value", v)
414 return self.time_zone.localize(dt)429 if hasattr(self.time_zone, "localize"): # pytz
430 return self.time_zone.localize(dt)
431 else:
432 return dt.replace(tzinfo=self.time_zone)
415433
416 def _toFormValue(self, value):434 def _toFormValue(self, value):
417 """Convert a date to its string representation.435 """Convert a date to its string representation.
@@ -621,4 +639,6 @@ class DatetimeDisplayWidget(DisplayWidget):
621 if value == self.context.missing_value:639 if value == self.context.missing_value:
622 return ""640 return ""
623 value = value.astimezone(time_zone)641 value = value.astimezone(time_zone)
624 return html_escape(value.strftime("%Y-%m-%d %H:%M:%S %Z"))642 return html_escape(
643 "%s %s" % (value.strftime("%Y-%m-%d %H:%M:%S", tzname(value)))
644 )
diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
index 9d66d8b..a89a362 100644
--- a/lib/lp/blueprints/browser/sprint.py
+++ b/lib/lp/blueprints/browser/sprint.py
@@ -61,6 +61,7 @@ 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
64from lp.services.database.bulk import load_referencing65from lp.services.database.bulk import load_referencing
65from lp.services.helpers import shortlist66from lp.services.helpers import shortlist
66from lp.services.propertycache import cachedproperty67from lp.services.propertycache import cachedproperty
@@ -228,30 +229,31 @@ class SprintView(HasSpecificationsView):
228 def formatDateTime(self, dt):229 def formatDateTime(self, dt):
229 """Format a datetime value according to the sprint's time zone"""230 """Format a datetime value according to the sprint's time zone"""
230 dt = dt.astimezone(self.tzinfo)231 dt = dt.astimezone(self.tzinfo)
231 return dt.strftime("%Y-%m-%d %H:%M %Z")232 return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M"), tzname(dt))
232233
233 def formatDate(self, dt):234 def formatDate(self, dt):
234 """Format a date value according to the sprint's time zone"""235 """Format a date value according to the sprint's time zone"""
235 dt = dt.astimezone(self.tzinfo)236 dt = dt.astimezone(self.tzinfo)
236 return dt.strftime("%Y-%m-%d")237 return dt.strftime("%Y-%m-%d")
237238
238 _local_timeformat = "%H:%M %Z on %A, %Y-%m-%d"239 def _formatLocal(self, dt):
240 return "%s %s on %s" % (
241 dt.strftime("%H:%M"),
242 tzname(dt),
243 dt.strftime("%A, %Y-%m-%d"),
244 )
239245
240 @property246 @property
241 def local_start(self):247 def local_start(self):
242 """The sprint start time, in the local time zone, as text."""248 """The sprint start time, in the local time zone, as text."""
243 tz = pytz.timezone(self.context.time_zone)249 tz = pytz.timezone(self.context.time_zone)
244 return self.context.time_starts.astimezone(tz).strftime(250 return self._formatLocal(self.context.time_starts.astimezone(tz))
245 self._local_timeformat
246 )
247251
248 @property252 @property
249 def local_end(self):253 def local_end(self):
250 """The sprint end time, in the local time zone, as text."""254 """The sprint end time, in the local time zone, as text."""
251 tz = pytz.timezone(self.context.time_zone)255 tz = pytz.timezone(self.context.time_zone)
252 return self.context.time_ends.astimezone(tz).strftime(256 return self._formatLocal(self.context.time_ends.astimezone(tz))
253 self._local_timeformat
254 )
255257
256258
257class SprintAddView(LaunchpadFormView):259class SprintAddView(LaunchpadFormView):
diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py
index fd77ea5..d9f92a2 100644
--- a/lib/lp/bugs/scripts/uct/models.py
+++ b/lib/lp/bugs/scripts/uct/models.py
@@ -42,6 +42,7 @@ 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
45from lp.services.propertycache import cachedproperty46from lp.services.propertycache import cachedproperty
4647
47__all__ = [48__all__ = [
@@ -410,7 +411,7 @@ class UCTRecord:
410411
411 @classmethod412 @classmethod
412 def _format_datetime(cls, dt: datetime) -> str:413 def _format_datetime(cls, dt: datetime) -> str:
413 return dt.strftime("%Y-%m-%d %H:%M:%S %Z")414 return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M:%S"), tzname(dt))
414415
415 @classmethod416 @classmethod
416 def _format_notes(cls, notes: List[Tuple[str, str]]) -> str:417 def _format_notes(cls, notes: List[Tuple[str, str]]) -> str:
diff --git a/lib/lp/services/compat.py b/lib/lp/services/compat.py
index 15a0f4c..86d833a 100644
--- a/lib/lp/services/compat.py
+++ b/lib/lp/services/compat.py
@@ -1,16 +1,19 @@
1# Copyright 2020 Canonical Ltd. This software is licensed under the1# Copyright 2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Python 2/3 compatibility layer.4"""Python compatibility layer.
55
6Use this for things that six doesn't provide.6Use this for things that six doesn't provide.
7"""7"""
88
9__all__ = [9__all__ = [
10 "message_as_bytes",10 "message_as_bytes",
11 "tzname",
11]12]
1213
13import io14import io
15from datetime import datetime, time, timezone
16from typing import Union
1417
1518
16def message_as_bytes(message):19def message_as_bytes(message):
@@ -21,3 +24,18 @@ def message_as_bytes(message):
21 g = BytesGenerator(fp, mangle_from_=False, maxheaderlen=0, policy=compat32)24 g = BytesGenerator(fp, mangle_from_=False, maxheaderlen=0, policy=compat32)
22 g.flatten(message)25 g.flatten(message)
23 return fp.getvalue()26 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/translations/vocabularies.py b/lib/lp/translations/vocabularies.py
index ff69730..fa9d754 100644
--- a/lib/lp/translations/vocabularies.py
+++ b/lib/lp/translations/vocabularies.py
@@ -17,6 +17,7 @@ from storm.locals import Desc, Not, Or
17from zope.schema.vocabulary import SimpleTerm17from zope.schema.vocabulary import SimpleTerm
1818
19from lp.registry.interfaces.distroseries import IDistroSeries19from lp.registry.interfaces.distroseries import IDistroSeries
20from lp.services.compat import tzname
20from lp.services.database.sqlobject import AND21from lp.services.database.sqlobject import AND
21from lp.services.webapp.vocabulary import (22from lp.services.webapp.vocabulary import (
22 NamedStormVocabulary,23 NamedStormVocabulary,
@@ -136,7 +137,10 @@ class FilteredLanguagePackVocabularyBase(StormVocabularyBase):
136137
137 def toTerm(self, obj):138 def toTerm(self, obj):
138 return SimpleTerm(139 return SimpleTerm(
139 obj, obj.id, "%s" % obj.date_exported.strftime("%F %T %Z")140 obj,
141 obj.id,
142 "%s %s"
143 % (obj.date_exported.strftime("%F %T"), tzname(obj.date_exported)),
140 )144 )
141145
142 @property146 @property
@@ -174,8 +178,12 @@ class FilteredLanguagePackVocabulary(FilteredLanguagePackVocabularyBase):
174 return SimpleTerm(178 return SimpleTerm(
175 obj,179 obj,
176 obj.id,180 obj.id,
177 "%s (%s)"181 "%s %s (%s)"
178 % (obj.date_exported.strftime("%F %T %Z"), obj.type.title),182 % (
183 obj.date_exported.strftime("%F %T"),
184 tzname(obj.date_exported),
185 obj.type.title,
186 ),
179 )187 )
180188
181 @property189 @property

Subscribers

People subscribed via source and target branches

to status/vote changes: