launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29738
[Merge] ~cjwatson/launchpad:timezone-utc-prepare into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:timezone-utc-prepare into launchpad:master.
Commit message:
Prepare for use of non-pytz timezones
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/438651
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.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:timezone-utc-prepare into launchpad:master.
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 (
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.projectgroup import IProjectGroup
+from lp.services.compat import tzname
from lp.services.utils import round_half_up
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.canonicalurl import nearest_adapter
@@ -1292,7 +1293,8 @@ class PersonFormatterAPI(ObjectFormatterAPI):
def local_time(self):
"""Return the local time for this person."""
time_zone = self._context.time_zone
- return datetime.now(pytz.timezone(time_zone)).strftime("%T %Z")
+ dt = datetime.now(pytz.timezone(time_zone))
+ return "%s %s" % (dt.strftime("%T"), tzname(dt))
def url(self, view_name=None, rootsite="mainsite"):
"""See `ObjectFormatterAPI`.
@@ -2383,7 +2385,7 @@ class DateTimeFormatterAPI:
def time(self):
if self._datetime.tzinfo:
value = self._datetime.astimezone(getUtility(ILaunchBag).time_zone)
- return value.strftime("%T %Z")
+ return "%s %s" % (value.strftime("%T"), tzname(value))
else:
return self._datetime.strftime("%T")
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__ = [
"DatetimeDisplayWidget",
]
-from datetime import datetime
+from datetime import datetime, timezone
import pytz
from zope.browserpage import ViewPageTemplateFile
@@ -32,6 +32,7 @@ from zope.formlib.textwidgets import TextWidget
from zope.formlib.widget import DisplayWidget
from lp.app.validators import LaunchpadValidationError
+from lp.services.compat import tzname
from lp.services.utils import round_half_up
from lp.services.webapp.escaping import html_escape
from lp.services.webapp.interfaces import ILaunchBag
@@ -217,7 +218,13 @@ class DateTimeWidget(TextWidget):
@property
def time_zone_name(self):
"""The name of the widget time zone for display in the widget."""
- return self.time_zone.zone
+ # XXX cjwatson 2023-03-09: In Python < 3.6, `timezone.utc.tzname`
+ # returns "UTC+00:00" rather than "UTC". Drop this once we require
+ # Python >= 3.6.
+ if self.time_zone is timezone.utc:
+ return "UTC"
+ else:
+ return self.time_zone.tzname(None)
def _align_date_constraints_with_time_zone(self):
"""Ensure that from_date and to_date use the widget time zone."""
@@ -225,14 +232,22 @@ class DateTimeWidget(TextWidget):
if self.from_date.tzinfo is None:
# Timezone-naive constraint is interpreted as being in the
# widget time zone.
- self.from_date = self.time_zone.localize(self.from_date)
+ if hasattr(self.time_zone, "localize"): # pytz
+ self.from_date = self.time_zone.localize(self.from_date)
+ else:
+ self.from_date = self.from_date.replace(
+ tzinfo=self.time_zone
+ )
else:
self.from_date = self.from_date.astimezone(self.time_zone)
if isinstance(self.to_date, datetime):
if self.to_date.tzinfo is None:
# Timezone-naive constraint is interpreted as being in the
# widget time zone.
- self.to_date = self.time_zone.localize(self.to_date)
+ if hasattr(self.time_zone, "localize"): # pytz
+ self.to_date = self.time_zone.localize(self.to_date)
+ else:
+ self.to_date = self.to_date.replace(tzinfo=self.time_zone)
else:
self.to_date = self.to_date.astimezone(self.time_zone)
@@ -411,7 +426,10 @@ class DateTimeWidget(TextWidget):
dt = datetime(year, month, day, hour, minute, int(second), micro)
except (DateTimeError, ValueError, IndexError) as v:
raise ConversionError("Invalid date value", v)
- return self.time_zone.localize(dt)
+ if hasattr(self.time_zone, "localize"): # pytz
+ return self.time_zone.localize(dt)
+ else:
+ return dt.replace(tzinfo=self.time_zone)
def _toFormValue(self, value):
"""Convert a date to its string representation.
@@ -621,4 +639,6 @@ class DatetimeDisplayWidget(DisplayWidget):
if value == self.context.missing_value:
return ""
value = value.astimezone(time_zone)
- return html_escape(value.strftime("%Y-%m-%d %H:%M:%S %Z"))
+ return html_escape(
+ "%s %s" % (value.strftime("%Y-%m-%d %H:%M:%S", tzname(value)))
+ )
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 (
RegistryCollectionActionMenuBase,
)
from lp.registry.interfaces.person import IPersonSet
+from lp.services.compat import tzname
from lp.services.database.bulk import load_referencing
from lp.services.helpers import shortlist
from lp.services.propertycache import cachedproperty
@@ -228,30 +229,31 @@ class SprintView(HasSpecificationsView):
def formatDateTime(self, dt):
"""Format a datetime value according to the sprint's time zone"""
dt = dt.astimezone(self.tzinfo)
- return dt.strftime("%Y-%m-%d %H:%M %Z")
+ return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M"), tzname(dt))
def formatDate(self, dt):
"""Format a date value according to the sprint's time zone"""
dt = dt.astimezone(self.tzinfo)
return dt.strftime("%Y-%m-%d")
- _local_timeformat = "%H:%M %Z on %A, %Y-%m-%d"
+ def _formatLocal(self, dt):
+ return "%s %s on %s" % (
+ dt.strftime("%H:%M"),
+ tzname(dt),
+ dt.strftime("%A, %Y-%m-%d"),
+ )
@property
def local_start(self):
"""The sprint start time, in the local time zone, as text."""
tz = pytz.timezone(self.context.time_zone)
- return self.context.time_starts.astimezone(tz).strftime(
- self._local_timeformat
- )
+ return self._formatLocal(self.context.time_starts.astimezone(tz))
@property
def local_end(self):
"""The sprint end time, in the local time zone, as text."""
tz = pytz.timezone(self.context.time_zone)
- return self.context.time_ends.astimezone(tz).strftime(
- self._local_timeformat
- )
+ return self._formatLocal(self.context.time_ends.astimezone(tz))
class 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
from lp.registry.model.product import Product
from lp.registry.model.sourcepackage import SourcePackage
from lp.registry.model.sourcepackagename import SourcePackageName
+from lp.services.compat import tzname
from lp.services.propertycache import cachedproperty
__all__ = [
@@ -410,7 +411,7 @@ class UCTRecord:
@classmethod
def _format_datetime(cls, dt: datetime) -> str:
- return dt.strftime("%Y-%m-%d %H:%M:%S %Z")
+ return "%s %s" % (dt.strftime("%Y-%m-%d %H:%M:%S"), tzname(dt))
@classmethod
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 @@
# Copyright 2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Python 2/3 compatibility layer.
+"""Python compatibility layer.
Use this for things that six doesn't provide.
"""
__all__ = [
"message_as_bytes",
+ "tzname",
]
import io
+from datetime import datetime, time, timezone
+from typing import Union
def message_as_bytes(message):
@@ -21,3 +24,18 @@ def message_as_bytes(message):
g = BytesGenerator(fp, mangle_from_=False, maxheaderlen=0, policy=compat32)
g.flatten(message)
return fp.getvalue()
+
+
+def tzname(obj: Union[datetime, time]) -> str:
+ """Return this (date)time object's time zone name as a string.
+
+ Python 3.5's `timezone.utc.tzname` returns "UTC+00:00", rather than
+ "UTC" which is what we prefer. Paper over this until we can rely on
+ Python >= 3.6 everywhere.
+ """
+ if obj.tzinfo is None:
+ return ""
+ elif obj.tzinfo is timezone.utc:
+ return "UTC"
+ else:
+ 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
from zope.schema.vocabulary import SimpleTerm
from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.services.compat import tzname
from lp.services.database.sqlobject import AND
from lp.services.webapp.vocabulary import (
NamedStormVocabulary,
@@ -136,7 +137,10 @@ class FilteredLanguagePackVocabularyBase(StormVocabularyBase):
def toTerm(self, obj):
return SimpleTerm(
- obj, obj.id, "%s" % obj.date_exported.strftime("%F %T %Z")
+ obj,
+ obj.id,
+ "%s %s"
+ % (obj.date_exported.strftime("%F %T"), tzname(obj.date_exported)),
)
@property
@@ -174,8 +178,12 @@ class FilteredLanguagePackVocabulary(FilteredLanguagePackVocabularyBase):
return SimpleTerm(
obj,
obj.id,
- "%s (%s)"
- % (obj.date_exported.strftime("%F %T %Z"), obj.type.title),
+ "%s %s (%s)"
+ % (
+ obj.date_exported.strftime("%F %T"),
+ tzname(obj.date_exported),
+ obj.type.title,
+ ),
)
@property