From 53441804a1ef6f07422ebfa134b283db26f9d353 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 18 Mar 2022 15:13:24 -0700 Subject: [PATCH 01/12] ENH: support zoneinfo tzinfos --- pandas/_libs/tslibs/conversion.pyx | 11 +++--- pandas/_libs/tslibs/timezones.pxd | 1 + pandas/_libs/tslibs/timezones.pyx | 17 ++++++++-- pandas/_libs/tslibs/tzconversion.pxd | 2 +- pandas/_libs/tslibs/tzconversion.pyx | 34 ++++++++++--------- pandas/_libs/tslibs/vectorized.pyx | 23 +++++++------ pandas/conftest.py | 2 ++ .../indexes/datetimes/test_constructors.py | 5 ++- 8 files changed, 58 insertions(+), 37 deletions(-) diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index 0adf6f722c9ce..40043ad91a5d6 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -51,6 +51,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, @@ -71,7 +72,7 @@ from pandas._libs.tslibs.nattype cimport ( ) from pandas._libs.tslibs.tzconversion cimport ( bisect_right_i8, - tz_convert_utc_to_tzlocal, + tz_convert_utc_to_tz, tz_localize_to_utc_single, ) @@ -555,8 +556,8 @@ 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): - tz_convert_utc_to_tzlocal(obj.value, tz, &obj.fold) + elif is_tzlocal(tz) or is_zoneinfo(tz): + tz_convert_utc_to_tz(obj.value, tz, &obj.fold) else: trans, deltas, typ = get_dst_info(tz) @@ -724,8 +725,8 @@ cdef inline void _localize_tso(_TSObject obj, tzinfo tz): pass elif obj.value == NPY_NAT: pass - elif is_tzlocal(tz): - local_val = tz_convert_utc_to_tzlocal(obj.value, tz, &obj.fold) + elif is_tzlocal(tz) or is_zoneinfo(tz): + local_val = tz_convert_utc_to_tz(obj.value, tz, &obj.fold) dt64_to_dtstruct(local_val, &obj.dts) else: # Adjust datetime64 timestamp, recompute datetimestruct diff --git a/pandas/_libs/tslibs/timezones.pxd b/pandas/_libs/tslibs/timezones.pxd index 13f196a567952..d1f46b39b2940 100644 --- a/pandas/_libs/tslibs/timezones.pxd +++ b/pandas/_libs/tslibs/timezones.pxd @@ -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) diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index 224c5be1f3b7d..a717f92778201 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -2,6 +2,7 @@ from datetime import ( timedelta, timezone, ) +from zoneinfo import ZoneInfo from cpython.datetime cimport ( datetime, @@ -41,7 +42,7 @@ cdef int64_t NPY_NAT = get_nat() 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 = ZoneInfo("UTC") # ---------------------------------------------------------------------- @@ -51,9 +52,15 @@ cpdef inline bint is_utc(tzinfo tz): or tz is utc_stdlib or isinstance(tz, _dateutil_tzutc) or tz is utc_dateutil_str + # NB: we are assuming the user does not clear zoneinfo cache + or tz is utc_zoneinfo ) +cdef inline bint is_zoneinfo(tzinfo tz): + return isinstance(tz, ZoneInfo) + + cdef inline bint is_tzlocal(tzinfo tz): return isinstance(tz, _dateutil_tzlocal) @@ -210,6 +217,8 @@ cdef inline bint is_fixed_offset(tzinfo tz): return 1 else: return 0 + elif is_zoneinfo(tz): + return False # This also implicitly accepts datetime.timezone objects which are # considered fixed return 1 @@ -264,6 +273,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") @@ -291,13 +302,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 diff --git a/pandas/_libs/tslibs/tzconversion.pxd b/pandas/_libs/tslibs/tzconversion.pxd index 0837a5c436197..3a234cd727764 100644 --- a/pandas/_libs/tslibs/tzconversion.pxd +++ b/pandas/_libs/tslibs/tzconversion.pxd @@ -2,7 +2,7 @@ from cpython.datetime cimport tzinfo from numpy cimport int64_t -cdef int64_t tz_convert_utc_to_tzlocal( +cdef int64_t tz_convert_utc_to_tz( int64_t utc_val, tzinfo tz, bint* fold=* ) except? -1 cpdef int64_t tz_convert_from_utc_single(int64_t val, tzinfo tz) diff --git a/pandas/_libs/tslibs/tzconversion.pyx b/pandas/_libs/tslibs/tzconversion.pyx index 1a1aa6dfec5a0..a8f83d4cb19e9 100644 --- a/pandas/_libs/tslibs/tzconversion.pyx +++ b/pandas/_libs/tslibs/tzconversion.pyx @@ -43,6 +43,7 @@ from pandas._libs.tslibs.timezones cimport ( is_fixed_offset, is_tzlocal, is_utc, + is_zoneinfo, ) @@ -60,8 +61,8 @@ cdef int64_t tz_localize_to_utc_single( elif is_utc(tz) or tz is None: return val - elif is_tzlocal(tz): - return _tz_convert_tzlocal_utc(val, tz, to_utc=True) + elif is_tzlocal(tz) or is_zoneinfo(tz): + return _tz_localize_using_tzinfo_api(val, tz, to_utc=True) elif is_fixed_offset(tz): # TODO: in this case we should be able to use get_utcoffset, @@ -136,13 +137,13 @@ 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: result[i] = NPY_NAT else: - result[i] = _tz_convert_tzlocal_utc(v, tz, to_utc=True) + result[i] = _tz_localize_using_tzinfo_api(v, tz, to_utc=True) return result # silence false-positive compiler warning @@ -402,7 +403,7 @@ cdef ndarray[int64_t] _get_dst_hours( # ---------------------------------------------------------------------- # Timezone Conversion -cdef int64_t tz_convert_utc_to_tzlocal( +cdef int64_t tz_convert_utc_to_tz( int64_t utc_val, tzinfo tz, bint* fold=NULL ) except? -1: """ @@ -418,7 +419,7 @@ cdef int64_t tz_convert_utc_to_tzlocal( ------- local_val : int64_t """ - return _tz_convert_tzlocal_utc(utc_val, tz, to_utc=False, fold=fold) + return _tz_localize_using_tzinfo_api(utc_val, tz, to_utc=False, fold=fold) cpdef int64_t tz_convert_from_utc_single(int64_t val, tzinfo tz): @@ -448,8 +449,8 @@ cpdef int64_t tz_convert_from_utc_single(int64_t val, tzinfo tz): if is_utc(tz): return val - elif is_tzlocal(tz): - return _tz_convert_tzlocal_utc(val, tz, to_utc=False) + elif is_tzlocal(tz) or is_zoneinfo(tz): + return _tz_localize_using_tzinfo_api(val, tz, to_utc=False) elif is_fixed_offset(tz): _, deltas, _ = get_dst_info(tz) delta = deltas[0] @@ -515,7 +516,7 @@ cdef const int64_t[:] _tz_convert_from_utc(const int64_t[:] vals, 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) @@ -539,7 +540,7 @@ cdef const int64_t[:] _tz_convert_from_utc(const int64_t[:] vals, tzinfo tz): # The pattern used in vectorized.pyx checks for use_utc here, # but we handle that case above. if use_tzlocal: - converted[i] = _tz_convert_tzlocal_utc(val, tz, to_utc=False) + converted[i] = _tz_localize_using_tzinfo_api(val, tz, to_utc=False) elif use_fixed: converted[i] = val + delta else: @@ -551,11 +552,12 @@ cdef const int64_t[:] _tz_convert_from_utc(const int64_t[:] vals, tzinfo tz): # OSError may be thrown by tzlocal on windows at or close to 1970-01-01 # see https://github.com/pandas-dev/pandas/pull/37591#issuecomment-720628241 -cdef int64_t _tz_convert_tzlocal_utc(int64_t val, tzinfo tz, bint to_utc=True, - bint* fold=NULL) except? -1: +cdef int64_t _tz_localize_using_tzinfo_api( + int64_t val, tzinfo tz, bint to_utc=True, bint* fold=NULL +) except? -1: """ - Convert the i8 representation of a datetime from a tzlocal timezone to - UTC, or vice-versa. + Convert the i8 representation of a datetime from a general-case timezone to + UTC, or vice-versa using the datetime/tzinfo API. Private, not intended for use outside of tslibs.conversion @@ -564,10 +566,10 @@ cdef int64_t _tz_convert_tzlocal_utc(int64_t val, tzinfo tz, bint to_utc=True, val : int64_t tz : tzinfo to_utc : bint - True if converting tzlocal _to_ UTC, False if going the other direction + True if converting _to_ UTC, False if going the other direction. fold : bint*, default NULL pointer to fold: whether datetime ends up in a fold or not - after adjustment + after adjustment. Only passed with to_utc=False. Returns diff --git a/pandas/_libs/tslibs/vectorized.pyx b/pandas/_libs/tslibs/vectorized.pyx index ada6d7f6495bf..8a5e081bbdc14 100644 --- a/pandas/_libs/tslibs/vectorized.pyx +++ b/pandas/_libs/tslibs/vectorized.pyx @@ -37,10 +37,11 @@ from .timezones cimport ( get_dst_info, is_tzlocal, is_utc, + is_zoneinfo, ) from .tzconversion cimport ( bisect_right_i8, - tz_convert_utc_to_tzlocal, + tz_convert_utc_to_tz, ) # ------------------------------------------------------------------------- @@ -113,7 +114,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) @@ -137,7 +138,7 @@ def ints_to_pydatetime( if use_utc: local_val = value elif use_tzlocal: - local_val = tz_convert_utc_to_tzlocal(value, tz) + local_val = tz_convert_utc_to_tz(value, tz) elif use_fixed: local_val = value + delta else: @@ -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) @@ -223,7 +224,7 @@ def get_resolution(const int64_t[:] stamps, tzinfo tz=None) -> Resolution: if use_utc: local_val = stamps[i] elif use_tzlocal: - local_val = tz_convert_utc_to_tzlocal(stamps[i], tz) + local_val = tz_convert_utc_to_tz(stamps[i], tz) elif use_fixed: local_val = stamps[i] + delta else: @@ -270,7 +271,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) @@ -290,7 +291,7 @@ cpdef ndarray[int64_t] normalize_i8_timestamps(const int64_t[:] stamps, tzinfo t if use_utc: local_val = stamps[i] elif use_tzlocal: - local_val = tz_convert_utc_to_tzlocal(stamps[i], tz) + local_val = tz_convert_utc_to_tz(stamps[i], tz) elif use_fixed: local_val = stamps[i] + delta else: @@ -332,7 +333,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) @@ -348,7 +349,7 @@ def is_date_array_normalized(const int64_t[:] stamps, tzinfo tz=None) -> bool: if use_utc: local_val = stamps[i] elif use_tzlocal: - local_val = tz_convert_utc_to_tzlocal(stamps[i], tz) + local_val = tz_convert_utc_to_tz(stamps[i], tz) elif use_fixed: local_val = stamps[i] + delta else: @@ -380,7 +381,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) @@ -400,7 +401,7 @@ def dt64arr_to_periodarr(const int64_t[:] stamps, int freq, tzinfo tz): if use_utc: local_val = stamps[i] elif use_tzlocal: - local_val = tz_convert_utc_to_tzlocal(stamps[i], tz) + local_val = tz_convert_utc_to_tz(stamps[i], tz) elif use_fixed: local_val = stamps[i] + delta else: diff --git a/pandas/conftest.py b/pandas/conftest.py index 8c10a0375d4da..dc27767816e1f 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -30,6 +30,7 @@ from decimal import Decimal import operator import os +import zoneinfo from dateutil.tz import ( tzlocal, @@ -1165,6 +1166,7 @@ def iris(datapath): timezone.utc, timezone(timedelta(hours=1)), timezone(timedelta(hours=-1), name="foo"), + zoneinfo.ZoneInfo("US/Pacific"), ] TIMEZONE_IDS = [repr(i) for i in TIMEZONES] diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index b1e764ceb7009..288f3838b1cf8 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -5,6 +5,7 @@ ) from functools import partial from operator import attrgetter +import zoneinfo import dateutil import numpy as np @@ -1128,7 +1129,9 @@ def test_timestamp_constructor_retain_fold(tz, fold): assert result == expected -@pytest.mark.parametrize("tz", ["dateutil/Europe/London"]) +@pytest.mark.parametrize( + "tz", ["dateutil/Europe/London", zoneinfo.ZoneInfo("Europe/London")] +) @pytest.mark.parametrize( "ts_input,fold_out", [ From 7786aa98009092cd0706e39ce6623dd1669ff330 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 18 Mar 2022 18:38:39 -0700 Subject: [PATCH 02/12] add backports.zoneinfo to ci deps --- ci/deps/actions-38-downstream_compat.yaml | 1 + ci/deps/actions-38-minimum_versions.yaml | 1 + pandas/conftest.py | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ci/deps/actions-38-downstream_compat.yaml b/ci/deps/actions-38-downstream_compat.yaml index 40f48884f1822..355875cd5ab2a 100644 --- a/ci/deps/actions-38-downstream_compat.yaml +++ b/ci/deps/actions-38-downstream_compat.yaml @@ -19,6 +19,7 @@ dependencies: - python-dateutil - numpy - pytz + - backports.zoneinfo # optional dependencies - beautifulsoup4 diff --git a/ci/deps/actions-38-minimum_versions.yaml b/ci/deps/actions-38-minimum_versions.yaml index abba5ddd60325..ed8bcf162abd4 100644 --- a/ci/deps/actions-38-minimum_versions.yaml +++ b/ci/deps/actions-38-minimum_versions.yaml @@ -20,6 +20,7 @@ dependencies: - python-dateutil=2.8.1 - numpy=1.18.5 - pytz=2020.1 + - backports.zoneinfo # optional dependencies, markupsafe for jinja2 - beautifulsoup4=4.8.2 diff --git a/pandas/conftest.py b/pandas/conftest.py index dc27767816e1f..9212648da8400 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -1167,6 +1167,7 @@ def iris(datapath): timezone(timedelta(hours=1)), timezone(timedelta(hours=-1), name="foo"), zoneinfo.ZoneInfo("US/Pacific"), + zoneinfo.ZoneInfo("UTC"), ] TIMEZONE_IDS = [repr(i) for i in TIMEZONES] @@ -1193,7 +1194,9 @@ def tz_aware_fixture(request): tz_aware_fixture2 = tz_aware_fixture -@pytest.fixture(params=["utc", "dateutil/UTC", utc, tzutc(), timezone.utc]) +@pytest.fixture( + params=["utc", "dateutil/UTC", utc, tzutc(), timezone.utc, zoneinfo.ZoneInfo("UTC")] +) def utc_fixture(request): """ Fixture to provide variants of UTC timezone strings and tzinfo objects. From de7764732f13536432a3c1ef0c4965d8de4cadd8 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 18 Mar 2022 18:39:40 -0700 Subject: [PATCH 03/12] add backports.zoneinfo to pypy file --- ci/deps/actions-pypy-38.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/deps/actions-pypy-38.yaml b/ci/deps/actions-pypy-38.yaml index ad05d2ab2dacc..3df80501b7bcd 100644 --- a/ci/deps/actions-pypy-38.yaml +++ b/ci/deps/actions-pypy-38.yaml @@ -18,3 +18,4 @@ dependencies: - numpy - python-dateutil - pytz + - backports.zoneinfo From 63860a9a5ec9c468cf05efa1ced77b1369f8e1f0 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 20 Mar 2022 12:54:43 -0700 Subject: [PATCH 04/12] py38 compat --- ci/deps/actions-38-downstream_compat.yaml | 1 - ci/deps/actions-38-minimum_versions.yaml | 1 - ci/deps/actions-pypy-38.yaml | 1 - pandas/_libs/tslibs/timezones.pyx | 33 +++++++++++++++++++---- pandas/conftest.py | 18 ++++++++----- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/ci/deps/actions-38-downstream_compat.yaml b/ci/deps/actions-38-downstream_compat.yaml index 355875cd5ab2a..40f48884f1822 100644 --- a/ci/deps/actions-38-downstream_compat.yaml +++ b/ci/deps/actions-38-downstream_compat.yaml @@ -19,7 +19,6 @@ dependencies: - python-dateutil - numpy - pytz - - backports.zoneinfo # optional dependencies - beautifulsoup4 diff --git a/ci/deps/actions-38-minimum_versions.yaml b/ci/deps/actions-38-minimum_versions.yaml index ed8bcf162abd4..abba5ddd60325 100644 --- a/ci/deps/actions-38-minimum_versions.yaml +++ b/ci/deps/actions-38-minimum_versions.yaml @@ -20,7 +20,6 @@ dependencies: - python-dateutil=2.8.1 - numpy=1.18.5 - pytz=2020.1 - - backports.zoneinfo # optional dependencies, markupsafe for jinja2 - beautifulsoup4=4.8.2 diff --git a/ci/deps/actions-pypy-38.yaml b/ci/deps/actions-pypy-38.yaml index 3df80501b7bcd..ad05d2ab2dacc 100644 --- a/ci/deps/actions-pypy-38.yaml +++ b/ci/deps/actions-pypy-38.yaml @@ -18,4 +18,3 @@ dependencies: - numpy - python-dateutil - pytz - - backports.zoneinfo diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index a717f92778201..06c863fd88b8a 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -2,7 +2,12 @@ from datetime import ( timedelta, timezone, ) -from zoneinfo import ZoneInfo + +try: + # py39+ + from zoneinfo import ZoneInfo +except ImportError: + ZoneInfo = None from cpython.datetime cimport ( datetime, @@ -42,22 +47,40 @@ cdef int64_t NPY_NAT = get_nat() 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 = ZoneInfo("UTC") + +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: + return False + + global utc_zoneinfo + if utc_zoneinfo is None: + try: + utc_zoneinfo = ZoneInfo("UTC") + except 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 - # NB: we are assuming the user does not clear zoneinfo cache - or tz is utc_zoneinfo + or is_utc_zoneinfo(tz) ) cdef inline bint is_zoneinfo(tzinfo tz): + if ZoneInfo is None: + return False return isinstance(tz, ZoneInfo) @@ -218,7 +241,7 @@ cdef inline bint is_fixed_offset(tzinfo tz): else: return 0 elif is_zoneinfo(tz): - return False + return 0 # This also implicitly accepts datetime.timezone objects which are # considered fixed return 1 diff --git a/pandas/conftest.py b/pandas/conftest.py index 9212648da8400..7882882003e55 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -30,7 +30,6 @@ from decimal import Decimal import operator import os -import zoneinfo from dateutil.tz import ( tzlocal, @@ -76,6 +75,10 @@ del pa has_pyarrow = True +zoneinfo = None +if pd.compat.PY39: + import zoneinfo + # 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" @@ -1166,9 +1169,9 @@ def iris(datapath): timezone.utc, timezone(timedelta(hours=1)), timezone(timedelta(hours=-1), name="foo"), - zoneinfo.ZoneInfo("US/Pacific"), - zoneinfo.ZoneInfo("UTC"), ] +if zoneinfo is not None: + TIMEZONES.extend([zoneinfo.ZoneInfo("US/Pacific"), zoneinfo.ZoneInfo("UTC")]) TIMEZONE_IDS = [repr(i) for i in TIMEZONES] @@ -1194,9 +1197,12 @@ def tz_aware_fixture(request): tz_aware_fixture2 = tz_aware_fixture -@pytest.fixture( - params=["utc", "dateutil/UTC", utc, tzutc(), timezone.utc, zoneinfo.ZoneInfo("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. From 02101eac38231d03f94df65fe1e155b476e1012a Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 20 Mar 2022 19:46:41 -0700 Subject: [PATCH 05/12] fix zoneinfo check --- pandas/_libs/tslibs/timezones.pyx | 3 ++- .../tests/indexes/datetimes/test_constructors.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index 06c863fd88b8a..bc76ad19138b7 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -5,6 +5,7 @@ from datetime import ( try: # py39+ + import zoneinfo from zoneinfo import ZoneInfo except ImportError: ZoneInfo = None @@ -62,7 +63,7 @@ cdef inline bint is_utc_zoneinfo(tzinfo tz): if utc_zoneinfo is None: try: utc_zoneinfo = ZoneInfo("UTC") - except ZoneInfoNotFoundError: + except zoneinfo.ZoneInfoNotFoundError: return False return tz is utc_zoneinfo diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index 288f3838b1cf8..c2bff053cb453 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -5,7 +5,6 @@ ) from functools import partial from operator import attrgetter -import zoneinfo import dateutil import numpy as np @@ -16,6 +15,7 @@ OutOfBoundsDatetime, conversion, ) +from pandas.compat import PY39 import pandas as pd from pandas import ( @@ -32,6 +32,9 @@ period_array, ) +if PY39: + import zoneinfo + class TestDatetimeIndex: @pytest.mark.parametrize( @@ -1129,9 +1132,12 @@ def test_timestamp_constructor_retain_fold(tz, fold): assert result == expected -@pytest.mark.parametrize( - "tz", ["dateutil/Europe/London", zoneinfo.ZoneInfo("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", [ From 8cdc27b705a84db28ff0e757de5b9e9e5321c194 Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 21 Mar 2022 10:59:10 -0700 Subject: [PATCH 06/12] fix check on py38 --- pandas/_libs/tslibs/timezones.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index bc76ad19138b7..ccc739f26c63f 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -8,6 +8,7 @@ try: import zoneinfo from zoneinfo import ZoneInfo except ImportError: + zoneinfo = None ZoneInfo = None from cpython.datetime cimport ( @@ -56,7 +57,7 @@ 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: + if tz is None or zoneinfo is None: return False global utc_zoneinfo From 60ca7c0cc3e657ad3a633fe2f195530231ba8f70 Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 22 Mar 2022 08:25:19 -0700 Subject: [PATCH 07/12] mypy fixup --- pandas/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 7882882003e55..9be7ec41b5563 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -77,7 +77,8 @@ zoneinfo = None if pd.compat.PY39: - import zoneinfo + # Import "zoneinfo" could not be resolved (reportMissingImports) + import zoneinfo # type: ignore # Until https://github.com/numpy/numpy/issues/19078 is sorted out, just suppress suppress_npdev_promotion_warning = pytest.mark.filterwarnings( From 0061f2c3f1fd53a40ec1984ba8f8cd9de967a6cd Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 23 Mar 2022 09:09:50 -0700 Subject: [PATCH 08/12] mypy fixup --- pandas/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 9be7ec41b5563..ecdc0f10b1f56 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -78,7 +78,7 @@ zoneinfo = None if pd.compat.PY39: # Import "zoneinfo" could not be resolved (reportMissingImports) - import zoneinfo # type: ignore + 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( From c0bc310339d9a346beb64cf0d8a41ec08ce15f8b Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 23 Mar 2022 22:48:27 -0700 Subject: [PATCH 09/12] fix tznaive acse --- pandas/_libs/tslibs/tzconversion.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/tzconversion.pyx b/pandas/_libs/tslibs/tzconversion.pyx index 4d0dccc2152bb..5d2a4766d7442 100644 --- a/pandas/_libs/tslibs/tzconversion.pyx +++ b/pandas/_libs/tslibs/tzconversion.pyx @@ -525,7 +525,7 @@ cdef const int64_t[:] _tz_convert_from_utc(const int64_t[:] stamps, tzinfo tz): int64_t[::1] result - if is_utc(tz): + if is_utc(tz) or tz is None: # Much faster than going through the "standard" pattern below return stamps.copy() From 6f7b7b19ef2bf2f6af953b7ce2ef56c37b62155f Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 12 Apr 2022 12:30:19 -0700 Subject: [PATCH 10/12] fix fold --- pandas/_libs/tslibs/tzconversion.pyx | 2 +- pandas/tests/indexes/datetimes/test_constructors.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/tzconversion.pyx b/pandas/_libs/tslibs/tzconversion.pyx index ad92b05964c5c..806df7928a5a1 100644 --- a/pandas/_libs/tslibs/tzconversion.pyx +++ b/pandas/_libs/tslibs/tzconversion.pyx @@ -486,7 +486,7 @@ cdef int64_t tz_convert_from_utc_single( if is_utc(tz): return utc_val elif is_tzlocal(tz) or is_zoneinfo(tz): - return utc_val + _tz_localize_using_tzinfo_api(utc_val, tz, to_utc=False) + 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 = cnp.PyArray_DATA(trans) diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index c2bff053cb453..4ac2c15b7d98e 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -1157,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"]) From 22c2063493a8a88037381147750e0ec61512e5de Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 16 Apr 2022 11:37:21 -0700 Subject: [PATCH 11/12] whatnsew --- doc/source/whatsnew/v1.5.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 358d9447b131d..301e132591b5f 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -481,7 +481,7 @@ Timedelta Time Zones ^^^^^^^^^^ -- +- Bug in :class:`Timestamp` constructor raising when passed a ``ZoneInfo`` tzinfo object (:issue:`46425`) - Numeric From 8dc732a514a052f1e1b8550f7e51c1b696e30d2c Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 17 Apr 2022 12:48:39 -0700 Subject: [PATCH 12/12] flesh out comment --- pandas/_libs/tslibs/timezones.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index 28280980d2ac0..22a154be5fcad 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -56,7 +56,8 @@ cdef tzinfo utc_zoneinfo = None # ---------------------------------------------------------------------- cdef inline bint is_utc_zoneinfo(tzinfo tz): - # https://github.com/pandas-dev/pandas/pull/46425#discussion_r830633025 + # Workaround for cases with missing tzdata + # https://github.com/pandas-dev/pandas/pull/46425#discussion_r830633025 if tz is None or zoneinfo is None: return False