Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle ZoneInfo objects in get_timezone_location, get_timezone_name #741

Merged
merged 15 commits into from Nov 10, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -40,7 +40,7 @@ matrix:
install:
- bash .ci/deps.${TRAVIS_OS_NAME}.sh
- pip install --upgrade pip
- pip install --upgrade $CDECIMAL pytest==4.3.1 pytest-cov==2.6.1 freezegun==0.3.12
- pip install --upgrade $CDECIMAL pytest==4.3.1 pytest-cov==2.6.1 freezegun==0.3.12 'backports.zoneinfo;python_version>="3.6" and python_version<"3.9"'
- pip install --editable .

script:
Expand Down
8 changes: 6 additions & 2 deletions babel/dates.py
Expand Up @@ -503,8 +503,10 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME, return_city=False):
dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo)
locale = Locale.parse(locale)

if hasattr(tzinfo, 'zone'):
if hasattr(tzinfo, 'zone'): # pytz object
zone = tzinfo.zone
elif hasattr(tzinfo, 'key') and tzinfo.key is not None: # ZoneInfo object
zone = tzinfo.key
else:
zone = tzinfo.tzname(dt or datetime.utcnow())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stanza seems to be more or less the one in the below change fragment – could they be refactored into one function like get_tz_name(tzinfo, default=None)?


Expand Down Expand Up @@ -619,8 +621,10 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo)
locale = Locale.parse(locale)

if hasattr(tzinfo, 'zone'):
if hasattr(tzinfo, 'zone'): # pytz object
zone = tzinfo.zone
elif hasattr(tzinfo, 'key') and tzinfo.key is not None: # ZoneInfo object
zone = tzinfo.key
else:
zone = tzinfo.tzname(dt)

Expand Down
150 changes: 110 additions & 40 deletions tests/test_dates.py
Expand Up @@ -24,6 +24,23 @@
from babel.util import FixedOffsetTimezone


@pytest.fixture(params=["pytz.timezone", "zoneinfo.ZoneInfo"])
def timezone_getter(request):
if request.param == "pytz.timezone":
return timezone
elif request.param == "zoneinfo.ZoneInfo":
try:
import zoneinfo
except ImportError:
try:
from backports import zoneinfo
except ImportError:
pytest.skip("zoneinfo not available")
return zoneinfo.ZoneInfo
else:
raise NotImplementedError
akx marked this conversation as resolved.
Show resolved Hide resolved


class DateTimeFormatTestCase(unittest.TestCase):

def test_quarter_format(self):
Expand Down Expand Up @@ -583,60 +600,113 @@ def test_get_timezone_gmt():
assert dates.get_timezone_gmt(dt, 'long', locale='fr_FR') == u'UTC-07:00'


def test_get_timezone_location():
tz = timezone('America/St_Johns')
def test_get_timezone_location(timezone_getter):
tz = timezone_getter('America/St_Johns')
assert (dates.get_timezone_location(tz, locale='de_DE') ==
u"Kanada (St. John\u2019s) Zeit")
assert (dates.get_timezone_location(tz, locale='en') ==
u'Canada (St. John’s) Time')
assert (dates.get_timezone_location(tz, locale='en', return_city=True) ==
u'St. John’s')

tz = timezone('America/Mexico_City')
tz = timezone_getter('America/Mexico_City')
assert (dates.get_timezone_location(tz, locale='de_DE') ==
u'Mexiko (Mexiko-Stadt) Zeit')

tz = timezone('Europe/Berlin')
tz = timezone_getter('Europe/Berlin')
assert (dates.get_timezone_location(tz, locale='de_DE') ==
u'Deutschland (Berlin) Zeit')


def test_get_timezone_name():
dt = time(15, 30, tzinfo=timezone('America/Los_Angeles'))
assert (dates.get_timezone_name(dt, locale='en_US') ==
u'Pacific Standard Time')
assert (dates.get_timezone_name(dt, locale='en_US', return_zone=True) ==
u'America/Los_Angeles')
assert dates.get_timezone_name(dt, width='short', locale='en_US') == u'PST'

tz = timezone('America/Los_Angeles')
assert dates.get_timezone_name(tz, locale='en_US') == u'Pacific Time'
assert dates.get_timezone_name(tz, 'short', locale='en_US') == u'PT'

tz = timezone('Europe/Berlin')
assert (dates.get_timezone_name(tz, locale='de_DE') ==
u'Mitteleurop\xe4ische Zeit')
assert (dates.get_timezone_name(tz, locale='pt_BR') ==
u'Hor\xe1rio da Europa Central')

tz = timezone('America/St_Johns')
assert dates.get_timezone_name(tz, locale='de_DE') == u'Neufundland-Zeit'

tz = timezone('America/Los_Angeles')
assert dates.get_timezone_name(tz, locale='en', width='short',
zone_variant='generic') == u'PT'
assert dates.get_timezone_name(tz, locale='en', width='short',
zone_variant='standard') == u'PST'
assert dates.get_timezone_name(tz, locale='en', width='short',
zone_variant='daylight') == u'PDT'
assert dates.get_timezone_name(tz, locale='en', width='long',
zone_variant='generic') == u'Pacific Time'
assert dates.get_timezone_name(tz, locale='en', width='long',
zone_variant='standard') == u'Pacific Standard Time'
assert dates.get_timezone_name(tz, locale='en', width='long',
zone_variant='daylight') == u'Pacific Daylight Time'

localnow = datetime.utcnow().replace(tzinfo=timezone('UTC')).astimezone(dates.LOCALTZ)
@pytest.mark.parametrize(
"tzname, params, expected",
[
("America/Los_Angeles", {"locale": "en_US"}, u"Pacific Time"),
("America/Los_Angeles", {"width": "short", "locale": "en_US"}, u"PT"),
("Europe/Berlin", {"locale": "de_DE"}, u"Mitteleurop\xe4ische Zeit"),
("Europe/Berlin", {"locale": "pt_BR"}, u"Hor\xe1rio da Europa Central"),
("America/St_Johns", {"locale": "de_DE"}, u"Neufundland-Zeit"),
(
"America/Los_Angeles",
{"locale": "en", "width": "short", "zone_variant": "generic"},
u"PT",
),
(
"America/Los_Angeles",
{"locale": "en", "width": "short", "zone_variant": "standard"},
u"PST",
),
(
"America/Los_Angeles",
{"locale": "en", "width": "short", "zone_variant": "daylight"},
u"PDT",
),
(
"America/Los_Angeles",
{"locale": "en", "width": "long", "zone_variant": "generic"},
u"Pacific Time",
),
(
"America/Los_Angeles",
{"locale": "en", "width": "long", "zone_variant": "standard"},
u"Pacific Standard Time",
),
(
"America/Los_Angeles",
{"locale": "en", "width": "long", "zone_variant": "daylight"},
u"Pacific Daylight Time",
),
("Europe/Berlin", {"locale": "en_US"}, u"Central European Time"),
],
)
def test_get_timezone_name_tzinfo(timezone_getter, tzname, params, expected):
tz = timezone_getter(tzname)
assert dates.get_timezone_name(tz, **params) == expected


@pytest.mark.parametrize("timezone_getter", ["pytz.timezone"], indirect=True)
@pytest.mark.parametrize(
"tzname, params, expected",
[
("America/Los_Angeles", {"locale": "en_US"}, u"Pacific Standard Time"),
(
"America/Los_Angeles",
{"locale": "en_US", "return_zone": True},
u"America/Los_Angeles",
),
("America/Los_Angeles", {"width": "short", "locale": "en_US"}, u"PST"),
],
)
def test_get_timezone_name_time_pytz(timezone_getter, tzname, params, expected):
"""pytz (by design) can't determine if the time is in DST or not,
so it will always return Standard time"""
dt = time(15, 30, tzinfo=timezone_getter(tzname))
assert dates.get_timezone_name(dt, **params) == expected


@pytest.mark.parametrize(
"tzname, params, expected",
[
("America/Los_Angeles", {"locale": "en_US"}, u"Pacific Daylight Time"),
(
"America/Los_Angeles",
{"locale": "en_US", "return_zone": True},
u"America/Los_Angeles",
),
("America/Los_Angeles", {"width": "short", "locale": "en_US"}, u"PDT"),
],
)
@pytest.mark.parametrize("timezone_getter", ["zoneinfo.ZoneInfo"], indirect=True)
def test_get_timezone_name_time_zoneinfo(timezone_getter, tzname, params, expected):
"""zoneinfo will correctly detect DST from the object.
FIXME: this test will only succeed in the winter.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how should we address this? Should I just test with a full datetime object instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I... guess? Getting a timezone from just a time-of-day seems weird anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall I just delete this test case?
Alternatively, I can try patching datetime.utcnow() to return a constant datetime, and test against that. But it seems that the datetime module is not patchable: https://stackoverflow.com/a/192857/714760. So it would be really hard to test this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove this test.

"""
dt = time(15, 30, tzinfo=timezone_getter(tzname))
assert dates.get_timezone_name(dt, **params) == expected


def test_get_timezone_name_misc(timezone_getter):
localnow = datetime.utcnow().replace(tzinfo=timezone_getter('UTC')).astimezone(dates.LOCALTZ)
assert (dates.get_timezone_name(None, locale='en_US') ==
dates.get_timezone_name(localnow, locale='en_US'))

Expand Down
1 change: 1 addition & 0 deletions tox.ini
Expand Up @@ -7,6 +7,7 @@ deps =
pytest-cov==2.6.1
cdecimal: m3-cdecimal
freezegun==0.3.12
backports.zoneinfo;python_version>"3.6" and python_version<"3.9"
whitelist_externals = make
commands = make clean-cldr test
passenv = PYTHON_TEST_FLAGS
Expand Down