From 8b26b675bad5ac38ea93783c6d412448e61af228 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Mon, 8 Nov 2021 20:24:50 +0100 Subject: [PATCH] Use stdlib instead of pytz on Python >= 3.9 Python 3.9 introduced the zoneinfo module, which provides the only feature we used pytz for (getting a datetime.tzinfo object from an IANA timezone id); so let's use it instead of a third-party dependency. --- plugins/Time/plugin.py | 26 +++++++-------- plugins/Time/test.py | 17 ++++++---- requirements.txt | 2 +- src/utils/__init__.py | 2 +- src/utils/time.py | 73 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 src/utils/time.py diff --git a/plugins/Time/plugin.py b/plugins/Time/plugin.py index f47f42ff6..5bb344c88 100644 --- a/plugins/Time/plugin.py +++ b/plugins/Time/plugin.py @@ -71,11 +71,6 @@ try: except ImportError: tzlocal = None -try: - import pytz -except ImportError: - pytz = None - class Time(callbacks.Plugin): """This plugin allows you to use different time-related functions.""" @internationalizeDocstring @@ -200,19 +195,22 @@ class Time(callbacks.Plugin): @internationalizeDocstring def tztime(self, irc, msg, args, timezone): - """/ + """/ (or //) Takes a city and its region, and returns its local time. This command uses the IANA Time Zone Database.""" - if pytz is None: - irc.error(_('Python-tz is required by the command, but is not ' - 'installed on this computer.'), Raise=True) try: - timezone = pytz.timezone(timezone) - except pytz.UnknownTimeZoneError: - irc.error(_('Unknown timezone'), Raise=True) - format = self.registryValue("format", msg.channel, irc.network) - irc.reply(datetime.now(timezone).strftime(format)) + timezone = utils.time.iana_timezone(timezone) + except utils.time.UnknownTimeZone: + irc.error(_('Unknown timezone')) + except utils.time.MissingTimezoneLibrary: + irc.error(_('Python-tz is required by the command, but is not ' + 'installed on this computer.')) + except utils.time.UnknownTimeZone as e: + irc.error(e.args[0]) + else: + format = self.registryValue("format", msg.channel, irc.network) + irc.reply(datetime.now(timezone).strftime(format)) tztime = wrap(tztime, ['text']) def ddate(self, irc, msg, args, year=None, month=None, day=None): diff --git a/plugins/Time/test.py b/plugins/Time/test.py index 1f6bec1e6..f85c3e621 100644 --- a/plugins/Time/test.py +++ b/plugins/Time/test.py @@ -28,14 +28,18 @@ # POSSIBILITY OF SUCH DAMAGE. ### +import sys from supybot.test import * -try: - import pytz -except ImportError: - has_pytz = False +if sys.version_info >= (3, 9): + has_tz_lib = True else: - has_pytz = True + try: + import pytz + except ImportError: + has_tz_lib = False + else: + has_tz_lib = True try: import dateutil @@ -88,9 +92,10 @@ class TimeTestCase(PluginTestCase): self.assertNotError('ctime') self.assertNotError('time %Y') - @skipIf(not has_pytz, 'pytz is missing') + @skipIf(not has_tz_lib, 'python version is older than 3.9 and pytz is missing') def testTztime(self): self.assertNotError('tztime Europe/Paris') + self.assertNotError('tztime America/Indiana/Knox') self.assertError('tztime Europe/Gniarf') @skipIf(not has_dateutil, 'python-dateutil is missing') diff --git a/requirements.txt b/requirements.txt index 64d15ec1d..f1c707452 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ setuptools chardet -pytz +pytz;python_version<'3.9' python-dateutil python-gnupg feedparser diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 8a130f0ea..4b43ea523 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -63,6 +63,6 @@ internationalization = builtins.get('supybotInternationalization', None) # These imports need to happen below the block above, so things get put into # __builtins__ appropriately. from .gen import * -from . import crypt, error, file, iter, net, python, seq, str, transaction, web +from . import crypt, error, file, iter, net, python, seq, str, time, transaction, web # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/src/utils/time.py b/src/utils/time.py new file mode 100644 index 000000000..9739b69a3 --- /dev/null +++ b/src/utils/time.py @@ -0,0 +1,73 @@ +### +# Copyright (c) 2021, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +### + +import re +import sys + +if sys.version_info >= (3, 9): + import zoneinfo +else: + zoneinfo = None + +try: + import pytz +except ImportError: + pytz = None + +_IANA_TZ_RE = re.compile("([\w_-]+/)+[\w_-]+") + +class TimezoneException(Exception): + pass + +class MissingTimezoneLibrary(TimezoneException): + pass + +class UnknownTimeZone(TimezoneException): + pass + +def iana_timezone(name): + if not _IANA_TZ_RE.match(name): + raise UnknownTimeZone(name) + + if zoneinfo: + try: + return zoneinfo.ZoneInfo(name) + except zoneinfo.ZoneInfoNotFoundError as e: + raise UnknownTimeZone(e.args[0]) from None + elif pytz: + try: + timezone = pytz.timezone(name) + except pytz.UnknownTimeZoneError as e: + raise UnknownTimeZone(e.args[0]) from None + else: + raise MissingTimezoneLibrary( + "Could not find a timezone library. " + "Update Python to version 3.9 or newer, or install pytz." + )