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

ENH: support zoneinfo tzinfos #46425

Merged
merged 21 commits into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pandas/_libs/tslibs/conversion.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ from pandas._libs.tslibs.timezones cimport (
is_fixed_offset,
is_tzlocal,
is_utc,
is_zoneinfo,
maybe_get_tz,
tz_compare,
utc_pytz as UTC,
Expand Down Expand Up @@ -532,7 +533,7 @@ cdef _TSObject _create_tsobject_tz_using_offset(npy_datetimestruct dts,
# see PEP 495 https://www.python.org/dev/peps/pep-0495/#the-fold-attribute
if is_utc(tz):
pass
elif is_tzlocal(tz):
elif is_tzlocal(tz) or is_zoneinfo(tz):
localize_tzinfo_api(obj.value, tz, &obj.fold)
else:
trans, deltas, typ = get_dst_info(tz)
Expand Down
1 change: 1 addition & 0 deletions pandas/_libs/tslibs/timezones.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ cdef tzinfo utc_pytz

cpdef bint is_utc(tzinfo tz)
cdef bint is_tzlocal(tzinfo tz)
cdef bint is_zoneinfo(tzinfo tz)

cdef bint treat_tz_as_pytz(tzinfo tz)

Expand Down
40 changes: 38 additions & 2 deletions pandas/_libs/tslibs/timezones.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ from datetime import (
timezone,
)

try:
# py39+
import zoneinfo
from zoneinfo import ZoneInfo
except ImportError:
zoneinfo = None
ZoneInfo = None

from cpython.datetime cimport (
datetime,
timedelta,
Expand Down Expand Up @@ -42,18 +50,42 @@ cdef tzinfo utc_stdlib = timezone.utc
cdef tzinfo utc_pytz = UTC
cdef tzinfo utc_dateutil_str = dateutil_gettz("UTC") # NB: *not* the same as tzutc()

cdef tzinfo utc_zoneinfo = None


# ----------------------------------------------------------------------

cdef inline bint is_utc_zoneinfo(tzinfo tz):
# https://github.com/pandas-dev/pandas/pull/46425#discussion_r830633025
if tz is None or zoneinfo is None:
return False

global utc_zoneinfo
jreback marked this conversation as resolved.
Show resolved Hide resolved
if utc_zoneinfo is None:
try:
utc_zoneinfo = ZoneInfo("UTC")
except zoneinfo.ZoneInfoNotFoundError:
return False

return tz is utc_zoneinfo


cpdef inline bint is_utc(tzinfo tz):
return (
tz is utc_pytz
or tz is utc_stdlib
or isinstance(tz, _dateutil_tzutc)
or tz is utc_dateutil_str
or is_utc_zoneinfo(tz)
)


cdef inline bint is_zoneinfo(tzinfo tz):
if ZoneInfo is None:
return False
return isinstance(tz, ZoneInfo)


cdef inline bint is_tzlocal(tzinfo tz):
return isinstance(tz, _dateutil_tzlocal)

Expand Down Expand Up @@ -210,6 +242,8 @@ cdef inline bint is_fixed_offset(tzinfo tz):
return 1
else:
return 0
elif is_zoneinfo(tz):
return 0
# This also implicitly accepts datetime.timezone objects which are
# considered fixed
return 1
Expand Down Expand Up @@ -264,6 +298,8 @@ cdef object get_dst_info(tzinfo tz):
# e.g. pytz.FixedOffset, matplotlib.dates._UTC,
# psycopg2.tz.FixedOffsetTimezone
num = int(get_utcoffset(tz, None).total_seconds()) * 1_000_000_000
# If we have e.g. ZoneInfo here, the get_utcoffset call will return None,
# so the total_seconds() call will raise AttributeError.
return (np.array([NPY_NAT + 1], dtype=np.int64),
np.array([num], dtype=np.int64),
"unknown")
Expand Down Expand Up @@ -291,13 +327,13 @@ cdef object get_dst_info(tzinfo tz):
# deltas
deltas = np.array([v.offset for v in (
tz._ttinfo_before,) + tz._trans_idx], dtype='i8')
deltas *= 1000000000
deltas *= 1_000_000_000
typ = 'dateutil'

elif is_fixed_offset(tz):
trans = np.array([NPY_NAT + 1], dtype=np.int64)
deltas = np.array([tz._ttinfo_std.offset],
dtype='i8') * 1000000000
dtype='i8') * 1_000_000_000
typ = 'fixed'
else:
# 2018-07-12 this is not reached in the tests, and this case
Expand Down
11 changes: 6 additions & 5 deletions pandas/_libs/tslibs/tzconversion.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ from pandas._libs.tslibs.timezones cimport (
is_fixed_offset,
is_tzlocal,
is_utc,
is_zoneinfo,
utc_pytz,
)

Expand All @@ -60,7 +61,7 @@ cdef int64_t tz_localize_to_utc_single(
elif is_utc(tz) or tz is None:
return val

elif is_tzlocal(tz):
elif is_tzlocal(tz) or is_zoneinfo(tz):
return val - _tz_localize_using_tzinfo_api(val, tz, to_utc=True)

elif is_fixed_offset(tz):
Expand Down Expand Up @@ -135,7 +136,7 @@ timedelta-like}

result = np.empty(n, dtype=np.int64)

if is_tzlocal(tz):
if is_tzlocal(tz) or is_zoneinfo(tz):
for i in range(n):
v = vals[i]
if v == NPY_NAT:
Expand Down Expand Up @@ -484,8 +485,8 @@ cdef int64_t tz_convert_from_utc_single(

if is_utc(tz):
return utc_val
elif is_tzlocal(tz):
return utc_val + _tz_localize_using_tzinfo_api(utc_val, tz, to_utc=False)
elif is_tzlocal(tz) or is_zoneinfo(tz):
return utc_val + _tz_localize_using_tzinfo_api(utc_val, tz, to_utc=False, fold=fold)
else:
trans, deltas, typ = get_dst_info(tz)
tdata = <int64_t*>cnp.PyArray_DATA(trans)
Expand Down Expand Up @@ -569,7 +570,7 @@ cdef const int64_t[:] _tz_convert_from_utc(const int64_t[:] stamps, tzinfo tz):

if is_utc(tz) or tz is None:
use_utc = True
elif is_tzlocal(tz):
elif is_tzlocal(tz) or is_zoneinfo(tz):
use_tzlocal = True
else:
trans, deltas, typ = get_dst_info(tz)
Expand Down
11 changes: 6 additions & 5 deletions pandas/_libs/tslibs/vectorized.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ from .timezones cimport (
get_dst_info,
is_tzlocal,
is_utc,
is_zoneinfo,
)
from .tzconversion cimport (
bisect_right_i8,
Expand Down Expand Up @@ -117,7 +118,7 @@ def ints_to_pydatetime(

if is_utc(tz) or tz is None:
use_utc = True
elif is_tzlocal(tz):
elif is_tzlocal(tz) or is_zoneinfo(tz):
use_tzlocal = True
else:
trans, deltas, typ = get_dst_info(tz)
Expand Down Expand Up @@ -204,7 +205,7 @@ def get_resolution(const int64_t[:] stamps, tzinfo tz=None) -> Resolution:

if is_utc(tz) or tz is None:
use_utc = True
elif is_tzlocal(tz):
elif is_tzlocal(tz) or is_zoneinfo(tz):
use_tzlocal = True
else:
trans, deltas, typ = get_dst_info(tz)
Expand Down Expand Up @@ -272,7 +273,7 @@ cpdef ndarray[int64_t] normalize_i8_timestamps(const int64_t[:] stamps, tzinfo t

if is_utc(tz) or tz is None:
use_utc = True
elif is_tzlocal(tz):
elif is_tzlocal(tz) or is_zoneinfo(tz):
use_tzlocal = True
else:
trans, deltas, typ = get_dst_info(tz)
Expand Down Expand Up @@ -334,7 +335,7 @@ def is_date_array_normalized(const int64_t[:] stamps, tzinfo tz=None) -> bool:

if is_utc(tz) or tz is None:
use_utc = True
elif is_tzlocal(tz):
elif is_tzlocal(tz) or is_zoneinfo(tz):
use_tzlocal = True
else:
trans, deltas, typ = get_dst_info(tz)
Expand Down Expand Up @@ -385,7 +386,7 @@ def dt64arr_to_periodarr(const int64_t[:] stamps, int freq, tzinfo tz):

if is_utc(tz) or tz is None:
use_utc = True
elif is_tzlocal(tz):
elif is_tzlocal(tz) or is_zoneinfo(tz):
use_tzlocal = True
else:
trans, deltas, typ = get_dst_info(tz)
Expand Down
14 changes: 13 additions & 1 deletion pandas/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
del pa
has_pyarrow = True

zoneinfo = None
if pd.compat.PY39:
# Import "zoneinfo" could not be resolved (reportMissingImports)
import zoneinfo # type: ignore[no-redef]

# Until https://github.com/numpy/numpy/issues/19078 is sorted out, just suppress
suppress_npdev_promotion_warning = pytest.mark.filterwarnings(
"ignore:Promotion of numbers and bools:FutureWarning"
Expand Down Expand Up @@ -1166,6 +1171,8 @@ def iris(datapath):
timezone(timedelta(hours=1)),
timezone(timedelta(hours=-1), name="foo"),
]
if zoneinfo is not None:
TIMEZONES.extend([zoneinfo.ZoneInfo("US/Pacific"), zoneinfo.ZoneInfo("UTC")])
TIMEZONE_IDS = [repr(i) for i in TIMEZONES]


Expand All @@ -1191,7 +1198,12 @@ def tz_aware_fixture(request):
tz_aware_fixture2 = tz_aware_fixture


@pytest.fixture(params=["utc", "dateutil/UTC", utc, tzutc(), timezone.utc])
_UTCS = ["utc", "dateutil/UTC", utc, tzutc(), timezone.utc]
if zoneinfo is not None:
_UTCS.append(zoneinfo.ZoneInfo("UTC"))


@pytest.fixture(params=_UTCS)
def utc_fixture(request):
"""
Fixture to provide variants of UTC timezone strings and tzinfo objects.
Expand Down
12 changes: 11 additions & 1 deletion pandas/tests/indexes/datetimes/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
OutOfBoundsDatetime,
conversion,
)
from pandas.compat import PY39

import pandas as pd
from pandas import (
Expand All @@ -31,6 +32,9 @@
period_array,
)

if PY39:
import zoneinfo


class TestDatetimeIndex:
@pytest.mark.parametrize(
Expand Down Expand Up @@ -1128,7 +1132,12 @@ def test_timestamp_constructor_retain_fold(tz, fold):
assert result == expected


@pytest.mark.parametrize("tz", ["dateutil/Europe/London"])
_tzs = ["dateutil/Europe/London"]
if PY39:
_tzs = ["dateutil/Europe/London", zoneinfo.ZoneInfo("Europe/London")]


@pytest.mark.parametrize("tz", _tzs)
@pytest.mark.parametrize(
"ts_input,fold_out",
[
Expand All @@ -1148,6 +1157,7 @@ def test_timestamp_constructor_infer_fold_from_value(tz, ts_input, fold_out):
result = ts.fold
expected = fold_out
assert result == expected
# TODO: belongs in Timestamp tests?


@pytest.mark.parametrize("tz", ["dateutil/Europe/London"])
Expand Down