Colin Watson has proposed merging ~cjwatson/launchpad:dateutil.tz into launchpad:master.
Commit message: Remove direct dependencies on pytz Requested reviews: Launchpad code reviewers (launchpad-reviewers) For more details, see: https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/444821 `dateutil.tz` is a better fit for Python's modern timezone provider interface, and has fewer footguns as a result. The only significant downside is that we have to reimplement something similar to `pytz.common_timezones` for use by our timezone vocabulary. Fortunately this isn't too difficult. -- Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:dateutil.tz into launchpad:master.
diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py index 0e0b0e5..44781e8 100644 --- a/lib/lp/app/browser/tales.py +++ b/lib/lp/app/browser/tales.py @@ -12,7 +12,7 @@ from email.utils import formatdate, mktime_tz from textwrap import dedent from urllib.parse import quote -import pytz +from dateutil import tz from lazr.restful.utils import get_current_browser_request from lazr.uri import URI from zope.browserpage import ViewPageTemplateFile @@ -1293,7 +1293,7 @@ class PersonFormatterAPI(ObjectFormatterAPI): def local_time(self): """Return the local time for this person.""" time_zone = self._context.time_zone - dt = datetime.now(pytz.timezone(time_zone)) + dt = datetime.now(tz.gettz(time_zone)) return "%s %s" % (dt.strftime("%T"), tzname(dt)) def url(self, view_name=None, rootsite="mainsite"): diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py index e024dfa..83eb950 100644 --- a/lib/lp/app/widgets/date.py +++ b/lib/lp/app/widgets/date.py @@ -19,7 +19,7 @@ __all__ = [ from datetime import datetime, timezone, tzinfo -import pytz +from dateutil import tz from zope.browserpage import ViewPageTemplateFile from zope.component import getUtility from zope.datetime import DateTimeError, parse @@ -217,14 +217,14 @@ class DateTimeWidget(TextWidget): >>> widget.required_time_zone_name = "Africa/Maseru" >>> print(widget.time_zone_name) Africa/Maseru - >>> print(widget.time_zone) - Africa/Maseru + >>> print(widget.time_zone) # doctest: +ELLIPSIS + tzfile('.../Africa/Maseru') """ if self.time_zone_name == "UTC": return timezone.utc else: - return pytz.timezone(self.time_zone_name) + return tz.gettz(self.time_zone_name) def _align_date_constraints_with_time_zone(self): """Ensure that from_date and to_date use the widget time zone.""" @@ -232,22 +232,14 @@ class DateTimeWidget(TextWidget): if self.from_date.tzinfo is None: # Timezone-naive constraint is interpreted as being in the # widget time zone. - 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 - ) + 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. - 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) + self.to_date = self.to_date.replace(tzinfo=self.time_zone) else: self.to_date = self.to_date.astimezone(self.time_zone) @@ -426,10 +418,7 @@ 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) - if hasattr(self.time_zone, "localize"): # pytz - return self.time_zone.localize(dt) - else: - return dt.replace(tzinfo=self.time_zone) + return dt.replace(tzinfo=self.time_zone) def _toFormValue(self, value): """Convert a date to its string representation. diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py index 41148c0..debe896 100644 --- a/lib/lp/blueprints/browser/sprint.py +++ b/lib/lp/blueprints/browser/sprint.py @@ -27,7 +27,7 @@ import io from collections import defaultdict from typing import List -import pytz +from dateutil import tz from lazr.restful.utils import smartquote from zope.component import getUtility from zope.formlib.widget import CustomWidgetFactory @@ -190,7 +190,7 @@ class SprintView(HasSpecificationsView): def initialize(self): self.notices = [] self.latest_specs_limit = 5 - self.tzinfo = pytz.timezone(self.context.time_zone) + self.tzinfo = tz.gettz(self.context.time_zone) def attendance(self): """establish if this user is attending""" @@ -246,14 +246,18 @@ class SprintView(HasSpecificationsView): @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._formatLocal(self.context.time_starts.astimezone(tz)) + return self._formatLocal( + self.context.time_starts.astimezone( + tz.gettz(self.context.time_zone) + ) + ) @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._formatLocal(self.context.time_ends.astimezone(tz)) + return self._formatLocal( + self.context.time_ends.astimezone(tz.gettz(self.context.time_zone)) + ) class SprintAddView(LaunchpadFormView): diff --git a/lib/lp/blueprints/browser/sprintattendance.py b/lib/lp/blueprints/browser/sprintattendance.py index 194f340..baeea2f 100644 --- a/lib/lp/blueprints/browser/sprintattendance.py +++ b/lib/lp/blueprints/browser/sprintattendance.py @@ -10,7 +10,7 @@ __all__ = [ from datetime import timedelta -import pytz +from dateutil import tz from zope.formlib.widget import CustomWidgetFactory from lp import _ @@ -56,7 +56,7 @@ class BaseSprintAttendanceAddView(LaunchpadFormView): # after the sprint. We will accept a time just before or just after # and map those to the beginning and end times, respectively, in # self.getDates(). - time_zone = pytz.timezone(self.context.time_zone) + time_zone = tz.gettz(self.context.time_zone) from_date = self.context.time_starts.astimezone(time_zone) to_date = self.context.time_ends.astimezone(time_zone) self.starts_widget.from_date = from_date - timedelta(days=1) @@ -142,16 +142,16 @@ class BaseSprintAttendanceAddView(LaunchpadFormView): @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( + time_zone = tz.gettz(self.context.time_zone) + return self.context.time_starts.astimezone(time_zone).strftime( self._local_timeformat ) @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( + time_zone = tz.gettz(self.context.time_zone) + return self.context.time_ends.astimezone(time_zone).strftime( self._local_timeformat ) diff --git a/lib/lp/bugs/doc/bugnotification-email.rst b/lib/lp/bugs/doc/bugnotification-email.rst index 9f07a1e..94a7a2b 100644 --- a/lib/lp/bugs/doc/bugnotification-email.rst +++ b/lib/lp/bugs/doc/bugnotification-email.rst @@ -585,12 +585,12 @@ method requires a from address, a to person, a body, a subject and a sending date for the mail. >>> from datetime import datetime - >>> import pytz + >>> from dateutil import tz >>> from_address = get_bugmail_from_address(lp_janitor, bug_four) >>> to_person = getUtility(IPersonSet).getByEmail("foo....@canonical.com") - >>> sending_date = pytz.timezone("Europe/Prague").localize( - ... datetime(2008, 5, 20, 11, 5, 47) + >>> sending_date = datetime( + ... 2008, 5, 20, 11, 5, 47, tzinfo=tz.gettz("Europe/Prague") ... ) >>> notification_email = bug_four_notification_builder.build( diff --git a/lib/lp/bugs/doc/externalbugtracker.rst b/lib/lp/bugs/doc/externalbugtracker.rst index 84a1835..b2288ef 100644 --- a/lib/lp/bugs/doc/externalbugtracker.rst +++ b/lib/lp/bugs/doc/externalbugtracker.rst @@ -361,8 +361,8 @@ the time is. If the difference between what we and the remote system think the time is, an error is raised. - >>> import pytz >>> from datetime import datetime, timedelta, timezone + >>> from dateutil import tz >>> utc_now = datetime.now(timezone.utc) >>> class PositiveTimeSkewExternalBugTracker(TestExternalBugTracker): ... def getCurrentDBTime(self): @@ -417,7 +417,7 @@ than the UTC time. >>> class LocalTimeExternalBugTracker(TestExternalBugTracker): ... def getCurrentDBTime(self): - ... local_time = utc_now.astimezone(pytz.timezone("US/Eastern")) + ... local_time = utc_now.astimezone(tz.gettz("US/Eastern")) ... return local_time + timedelta(minutes=1) ... >>> bug_watch_updater.updateBugWatches( diff --git a/lib/lp/bugs/tests/bugs-emailinterface.rst b/lib/lp/bugs/tests/bugs-emailinterface.rst index b0d3575..5f87927 100644 --- a/lib/lp/bugs/tests/bugs-emailinterface.rst +++ b/lib/lp/bugs/tests/bugs-emailinterface.rst @@ -3193,7 +3193,7 @@ we'll create a new bug on firefox and link it to a remote bug. >>> no_priv = getUtility(IPersonSet).getByName("no-priv") >>> from datetime import datetime, timezone - >>> import pytz + >>> from dateutil import tz >>> creation_date = datetime(2008, 4, 12, 10, 12, 12, tzinfo=timezone.utc) We create the initial bug message separately from the bug itself so that @@ -3238,7 +3238,7 @@ importing machinery. >>> bug_watch = getUtility(IBugWatchSet).get(bug_watch.id) >>> comment_date = datetime( - ... 2008, 5, 19, 16, 19, 12, tzinfo=pytz.timezone("Europe/Prague") + ... 2008, 5, 19, 16, 19, 12, tzinfo=tz.gettz("Europe/Prague") ... ) >>> initial_mail = ( @@ -3265,7 +3265,7 @@ Now someone uses the email interface to respond to the comment that has been submitted. >>> comment_date = datetime( - ... 2008, 5, 20, 11, 24, 12, tzinfo=pytz.timezone("Europe/Prague") + ... 2008, 5, 20, 11, 24, 12, tzinfo=tz.gettz("Europe/Prague") ... ) >>> reply_mail = ( @@ -3318,7 +3318,7 @@ to an email that isn't linked to the bug, the new message will be linked to the bug and will not have its bugwatch field set. >>> comment_date = datetime( - ... 2008, 5, 21, 11, 9, 12, tzinfo=pytz.timezone("Europe/Prague") + ... 2008, 5, 21, 11, 9, 12, tzinfo=tz.gettz("Europe/Prague") ... ) >>> initial_mail = ( @@ -3338,7 +3338,7 @@ to the bug and will not have its bugwatch field set. >>> message = getUtility(IMessageSet).fromEmail(initial_mail, no_priv) >>> comment_date = datetime( - ... 2008, 5, 21, 12, 52, 12, tzinfo=pytz.timezone("Europe/Prague") + ... 2008, 5, 21, 12, 52, 12, tzinfo=tz.gettz("Europe/Prague") ... ) >>> reply_mail = ( diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py index 1484dab..f6c7ca2 100644 --- a/lib/lp/registry/browser/person.py +++ b/lib/lp/registry/browser/person.py @@ -56,7 +56,7 @@ from operator import attrgetter, itemgetter from textwrap import dedent from urllib.parse import quote, urlencode -import pytz +from dateutil import tz from lazr.config import as_timedelta from lazr.delegates import delegate_to from lazr.restful.interface import copy_field @@ -2043,9 +2043,7 @@ class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin): @property def time_zone_offset(self): """Return a string with offset from UTC""" - return datetime.now(pytz.timezone(self.context.time_zone)).strftime( - "%z" - ) + return datetime.now(tz.gettz(self.context.time_zone)).strftime("%z") class PersonParticipationView(LaunchpadView): @@ -4419,13 +4417,13 @@ class PersonEditTimeZoneView(LaunchpadFormView): @action(_("Update"), name="update") def action_update(self, action, data): """Set the time zone for the person.""" - tz = data.get("time_zone") - if tz is None: + time_zone = data.get("time_zone") + if time_zone is None: raise UnexpectedFormData("No location received.") # XXX salgado, 2012-02-16, bug=933699: Use setLocation() because it's # the cheaper way to set the timezone of a person. Once the bug is # fixed we'll be able to get rid of this hack. - self.context.setLocation(None, None, tz, self.user) + self.context.setLocation(None, None, time_zone, self.user) def archive_to_person(archive): diff --git a/lib/lp/services/webapp/doc/launchbag.rst b/lib/lp/services/webapp/doc/launchbag.rst index 41e7b6e..091abaa 100644 --- a/lib/lp/services/webapp/doc/launchbag.rst +++ b/lib/lp/services/webapp/doc/launchbag.rst @@ -115,4 +115,4 @@ After the LaunchBag has been cleared, the correct time zone is returned. >>> launchbag.time_zone_name 'Europe/Paris' >>> launchbag.time_zone - <... 'Europe/Paris' ...> + tzfile('.../Europe/Paris') diff --git a/lib/lp/services/webapp/launchbag.py b/lib/lp/services/webapp/launchbag.py index f20dfe0..34118a3 100644 --- a/lib/lp/services/webapp/launchbag.py +++ b/lib/lp/services/webapp/launchbag.py @@ -10,7 +10,7 @@ The collection of stuff we have traversed. import threading from datetime import timezone -import pytz +from dateutil import tz from zope.component import getUtility from zope.interface import implementer @@ -161,7 +161,7 @@ class LaunchBag: if self.time_zone_name == "UTC": self._store.time_zone = timezone.utc else: - self._store.time_zone = pytz.timezone(self.time_zone_name) + self._store.time_zone = tz.gettz(self.time_zone_name) return self._store.time_zone diff --git a/lib/lp/services/worlddata/doc/vocabularies.rst b/lib/lp/services/worlddata/doc/vocabularies.rst index 4e4ec6e..1f698b3 100644 --- a/lib/lp/services/worlddata/doc/vocabularies.rst +++ b/lib/lp/services/worlddata/doc/vocabularies.rst @@ -12,12 +12,10 @@ TimezoneName The TimezoneName vocabulary should only contain timezone names that do not raise an exception when instantiated. - >>> import pytz + >>> from dateutil import tz >>> timezone_vocabulary = vocabulary_registry.get(None, "TimezoneName") >>> for timezone in timezone_vocabulary: - ... # Assign the return value of pytz.timezone() to the zone - ... # variable to prevent printing out the return value. - ... zone = pytz.timezone(timezone.value) + ... _ = tz.gettz(timezone.value) ... LanguageVocabulary diff --git a/lib/lp/services/worlddata/vocabularies.py b/lib/lp/services/worlddata/vocabularies.py index bed76b1..72db756 100644 --- a/lib/lp/services/worlddata/vocabularies.py +++ b/lib/lp/services/worlddata/vocabularies.py @@ -7,8 +7,6 @@ __all__ = [ "TimezoneNameVocabulary", ] -import pytz -import six from zope.component import getUtility from zope.interface import alsoProvides from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary @@ -19,14 +17,57 @@ from lp.services.worlddata.interfaces.timezone import ITimezoneNameVocabulary from lp.services.worlddata.model.country import Country from lp.services.worlddata.model.language import Language -# create a sorted list of the common time zone names, with UTC at the start -_values = sorted(six.ensure_text(tz) for tz in pytz.common_timezones) -_values.remove("UTC") -_values.insert(0, "UTC") -_timezone_vocab = SimpleVocabulary.fromValues(_values) +def _common_timezones(): + """A list of useful, current time zone names. + + This is inspired by `pytz.common_timezones`, which seems to be + approximately the list supported by `tzdata` with the additions of some + Canada- and US-specific names. Since we're aiming for current rather + than historical zone names, `zone1970.tab` seems appropriate. + """ + zones = set() + with open("/usr/share/zoneinfo/zone.tab") as zone_tab: + for line in zone_tab: + if line.startswith("#"): + continue + zones.add(line.rstrip("\n").split("\t")[2]) + # Backward-compatible US zone names, still in common use. + zones.update( + { + "US/Alaska", + "US/Arizona", + "US/Central", + "US/Eastern", + "US/Hawaii", + "US/Mountain", + "US/Pacific", + } + ) + # Backward-compatible Canadian zone names; see + # https://bugs.launchpad.net/pytz/+bug/506341. + zones.update( + { + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + } + ) + # pytz has this in addition to UTC. Perhaps it's more understandable + # for people not steeped in time zone lore. + zones.add("GMT") + + # UTC comes first, then everything else. + yield "UTC" + zones.discard("UTC") + yield from sorted(zones) + + +_timezone_vocab = SimpleVocabulary.fromValues(_common_timezones()) alsoProvides(_timezone_vocab, ITimezoneNameVocabulary) -del _values def TimezoneNameVocabulary(context=None): diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py index 28ffc4b..6508601 100644 --- a/lib/lp/snappy/tests/test_snapbuildbehaviour.py +++ b/lib/lp/snappy/tests/test_snapbuildbehaviour.py @@ -12,8 +12,8 @@ from textwrap import dedent from urllib.parse import urlsplit import fixtures -import pytz from aptsources.sourceslist import SourceEntry +from dateutil import tz from pymacaroons import Macaroon from testtools import ExpectedException from testtools.matchers import ( @@ -101,8 +101,8 @@ class FormatAsRfc3339TestCase(TestCase): self.assertEqual("2016-01-01T00:00:00Z", format_as_rfc3339(ts)) def test_tzinfo_is_ignored(self): - tz = datetime(2016, 1, 1, tzinfo=pytz.timezone("US/Eastern")) - self.assertEqual("2016-01-01T00:00:00Z", format_as_rfc3339(tz)) + time_zone = datetime(2016, 1, 1, tzinfo=tz.gettz("US/Eastern")) + self.assertEqual("2016-01-01T00:00:00Z", format_as_rfc3339(time_zone)) class TestSnapBuildBehaviourBase(TestCaseWithFactory): diff --git a/requirements/types.txt b/requirements/types.txt index 221aad3..c42c102 100644 --- a/requirements/types.txt +++ b/requirements/types.txt @@ -4,7 +4,7 @@ types-beautifulsoup4==4.9.0 types-bleach==3.3.1 types-oauthlib==3.1.0 types-psycopg2==2.9.21.4 -types-pytz==0.1.0 +types-python-dateutil==2.8.1 types-requests==0.1.13 types-six==0.1.9 types-urllib3==1.26.25.4 diff --git a/setup.cfg b/setup.cfg index ef37294..b2a0f29 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,12 +79,12 @@ install_requires = pymemcache pyparsing pystache + python-dateutil python-debian python-keystoneclient python-openid2 python-subunit python-swiftclient - pytz PyYAML rabbitfixture requests
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp