From 36ade18319fd54138fd2632e39f39857853f5a4f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 9 Nov 2021 22:32:29 +0100 Subject: [PATCH] Geography: Add 'timezone' command --- plugins/Geography/plugin.py | 75 ++++++++++++++++++++++++++++++++++++- plugins/Geography/test.py | 73 +++++++++++++++++++++++++++++++----- 2 files changed, 137 insertions(+), 11 deletions(-) diff --git a/plugins/Geography/plugin.py b/plugins/Geography/plugin.py index 59cc2d3d3..fca150e40 100644 --- a/plugins/Geography/plugin.py +++ b/plugins/Geography/plugin.py @@ -28,18 +28,91 @@ ### +import datetime + from supybot import utils, plugins, ircutils, callbacks from supybot.commands import * from supybot.i18n import PluginInternationalization +from . import nominatim +from . import wikidata + _ = PluginInternationalization("Geography") class Geography(callbacks.Plugin): - """Provides geography facts, such as timezones.""" + """Provides geography facts, such as timezones. + + This plugin uses data from `Wikidata `_ + and `OSM/Nominatim `. + """ threaded = True + @wrap(["text"]) + def timezone(self, irc, msg, args, query): + """ + + Returns the timezone used in the given location. For example, + the name could be "Paris" or "Paris, France". + This uses data from Wikidata and Nominatim.""" + osmids = nominatim.search_osmids(query) + if not osmids: + irc.error(_("Could not find the location"), Raise=True) + + now = datetime.datetime.now(tz=datetime.timezone.utc) + + for osmid in osmids: + uri = wikidata.uri_from_osmid(osmid) + if not uri: + continue + + # Get the timezone object (and handle various errors) + try: + timezone = wikidata.timezone_from_uri(uri) + except utils.time.UnknownTimeZone as e: + irc.error( + format(_("Could not understand timezone: %s"), e.args[0]), + Raise=True, + ) + except utils.time.MissingTimezoneLibrary: + irc.error( + _( + "Timezone-related commands are not available. " + "Your administrator need to either upgrade Python to " + "version 3.9 or greater, or install pytz." + ), + Raise=True, + ) + except utils.time.TimezoneException as e: + irc.error(e.args[0], Raise=True) + + # Extract a human-friendly name, depending on the type of + # the timezone object: + if hasattr(timezone, "key"): + # instance of zoneinfo.ZoneInfo + irc.reply(timezone.key) + return + elif hasattr(timezone, "zone"): + # instance of pytz.timezone + irc.reply(timezone.zone) + return + else: + # probably datetime.timezone built from a constant offset + try: + offset = timezone.utcoffset(now).seconds + except NotImplementedError: + continue + + hours = int(offset / 3600) + minutes = int(offset / 60 % 60) + irc.reply("UTC+%0.2i:%0.2i" % (hours, minutes)) + return + + irc.error( + _("Could not find the timezone of this location."), Raise=True + ) + Class = Geography diff --git a/plugins/Geography/test.py b/plugins/Geography/test.py index 330fa6626..035e424f2 100644 --- a/plugins/Geography/test.py +++ b/plugins/Geography/test.py @@ -29,7 +29,19 @@ ### import datetime +import contextlib from unittest import skipIf +from unittest.mock import patch + +try: + import pytz +except ImportError: + pytz = None + +try: + import zoneinfo +except ImportError: + zoneinfo = None from supybot.test import * from supybot import utils @@ -41,6 +53,43 @@ from . import nominatim class GeographyTestCase(PluginTestCase): plugins = ("Geography",) + @skipIf(not pytz, "pytz is not available") + @patch.object(nominatim, "search_osmids", return_value=[42]) + @patch.object(wikidata, "uri_from_osmid", return_value="foo") + def testTimezonePytz(self, _, __): + tz = pytz.timezone("Europe/Paris") + + with patch.object(wikidata, "timezone_from_uri", return_value=tz): + self.assertResponse("timezone Foo Bar", "Europe/Paris") + + @skipIf(not zoneinfo, "Python is older than 3.9") + @patch.object(nominatim, "search_osmids", return_value=[42]) + @patch.object(wikidata, "uri_from_osmid", return_value="foo") + def testTimezoneZoneinfo(self, _, __): + tz = zoneinfo.ZoneInfo("Europe/Paris") + + with patch.object(wikidata, "timezone_from_uri", return_value=tz): + self.assertResponse("timezone Foo Bar", "Europe/Paris") + + @skipIf(not zoneinfo, "Python is older than 3.9") + @patch.object(nominatim, "search_osmids", return_value=[42]) + @patch.object(wikidata, "uri_from_osmid", return_value="foo") + def testTimezoneAbsolute(self, _, __): + tz = datetime.timezone(datetime.timedelta(hours=4)) + + with patch.object(wikidata, "timezone_from_uri", return_value=tz): + self.assertResponse("timezone Foo Bar", "UTC+04:00") + + tz = datetime.timezone(datetime.timedelta(hours=4, minutes=30)) + + with patch.object(wikidata, "timezone_from_uri", return_value=tz): + self.assertResponse("timezone Foo Bar", "UTC+04:30") + + @skipIf(not network, "Network test") + def testTimezoneIntegration(self): + self.assertResponse("timezone Metz, France", "Europe/Paris") + self.assertResponse("timezone Saint-Denis, La Réunion", "UTC+04:00") + class GeographyWikidataTestCase(SupyTestCase): @skipIf(not network, "Network test") @@ -56,7 +105,7 @@ class GeographyWikidataTestCase(SupyTestCase): @skipIf(not network, "Network test") def testDirect(self): - """The queried object directly has a timezone property""" + # The queried object directly has a timezone property self.assertEqual( # New York wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q1384"), @@ -65,8 +114,8 @@ class GeographyWikidataTestCase(SupyTestCase): @skipIf(not network, "Network test") def testParent(self): - """The queried object does not have a TZ property - but it is part of an object that does""" + # The queried object does not have a TZ property but it is part + # of an object that does self.assertEqual( # Metz, France wikidata.timezone_from_uri( @@ -77,8 +126,8 @@ class GeographyWikidataTestCase(SupyTestCase): @skipIf(not network, "Network test") def testParentAndIgnoreSelf(self): - """The queried object has a TZ property, but it's useless to us; - however it is part of an object that has a useful one.""" + # The queried object has a TZ property, but it's useless to us; + # however it is part of an object that has a useful one.""" self.assertEqual( # New York City, NY wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q60"), @@ -87,11 +136,12 @@ class GeographyWikidataTestCase(SupyTestCase): @skipIf(not network, "Network test") def testParentQualifiedIgnorePreferred(self): - """The queried object does not have a TZ property, - and is part of an object that does. - However, this parent's 'preferred' timezone is not the - right one, so we must make sure to select the right one - based on P518 ('applies to part').""" + # The queried object does not have a TZ property, + # and is part of an object that does. + # However, this parent's 'preferred' timezone is not the + # right one, so we must make sure to select the right one + # based on P518 ('applies to part'). + # La Réunion is a French region, but in UTC+4. # France has a bunch of timezone statements, and 'Europe/Paris' # is marked as Preferred because it is the time of metropolitan @@ -113,5 +163,8 @@ class GeographyNominatimTestCase(SupyTestCase): results = nominatim.search_osmids("Metz, France") self.assertEqual(results[0], 450381, results) + results = nominatim.search_osmids("Saint-Denis, La Réunion") + self.assertEqual(results[0], 192468, results) + # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: