From 53441804a1ef6f07422ebfa134b283db26f9d353 Mon Sep 17 00:00:00 2001
From: Brock <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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 = <int64_t*>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 <jbrockmendel@gmail.com>
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 <jbrockmendel@gmail.com>
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