From a97288147add6a01c2648f4a0c0c9efd668caac1 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:35:03 -0800 Subject: [PATCH 01/16] Replace zoneinfo as default string inference, fix imports --- pandas/_libs/tslibs/timezones.pyx | 12 ++---- pandas/conftest.py | 37 ++++++------------- pandas/tests/arrays/test_datetimes.py | 23 ++---------- .../datetimes/methods/test_tz_localize.py | 30 ++++----------- .../timestamp/methods/test_tz_localize.py | 18 ++------- .../scalar/timestamp/test_constructors.py | 12 +++--- 6 files changed, 34 insertions(+), 98 deletions(-) diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index 10e5790dd1c35..c2052a2e6f441 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -2,17 +2,11 @@ from datetime import ( timedelta, timezone, ) +import zoneinfo +from zoneinfo import ZoneInfo from pandas.compat._optional import import_optional_dependency -try: - # py39+ - import zoneinfo - from zoneinfo import ZoneInfo -except ImportError: - zoneinfo = None - ZoneInfo = None - from cpython.datetime cimport ( datetime, timedelta, @@ -167,7 +161,7 @@ cpdef inline tzinfo maybe_get_tz(object tz): elif tz == "UTC" or tz == "utc": tz = utc_stdlib else: - tz = pytz.timezone(tz) + tz = zoneinfo.ZoneInfo(tz) elif is_integer_object(tz): tz = timezone(timedelta(seconds=tz)) elif isinstance(tz, tzinfo): diff --git a/pandas/conftest.py b/pandas/conftest.py index 94805313ccfc1..1c9629b11f074 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -96,12 +96,6 @@ import zoneinfo -try: - zoneinfo.ZoneInfo("UTC") -except zoneinfo.ZoneInfoNotFoundError: - zoneinfo = None # type: ignore[assignment] - - # ---------------------------------------------------------------- # Configuration / Settings # ---------------------------------------------------------------- @@ -1214,14 +1208,9 @@ def deco(*args): 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"), # type: ignore[list-item] - zoneinfo.ZoneInfo("UTC"), # type: ignore[list-item] - ] - ) TIMEZONE_IDS = [repr(i) for i in TIMEZONES] @@ -1247,12 +1236,9 @@ def tz_aware_fixture(request): tz_aware_fixture2 = tz_aware_fixture -_UTCS = ["utc", "dateutil/UTC", utc, tzutc(), timezone.utc] -if zoneinfo is not None: - _UTCS.append(zoneinfo.ZoneInfo("UTC")) - - -@pytest.fixture(params=_UTCS) +@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. @@ -1958,12 +1944,13 @@ def using_infer_string() -> bool: return pd.options.future.infer_string is True -warsaws = ["Europe/Warsaw", "dateutil/Europe/Warsaw"] -if zoneinfo is not None: - warsaws.append(zoneinfo.ZoneInfo("Europe/Warsaw")) # type: ignore[arg-type] - - -@pytest.fixture(params=warsaws) +@pytest.fixture( + params=[ + "Europe/Warsaw", + "dateutil/Europe/Warsaw", + zoneinfo.ZoneInfo("Europe/Warsaw"), + ] +) def warsaw(request) -> str: """ tzinfo for Europe/Warsaw using pytz, dateutil, or zoneinfo. diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py index 8f0576cc65a27..b4039acb7f876 100644 --- a/pandas/tests/arrays/test_datetimes.py +++ b/pandas/tests/arrays/test_datetimes.py @@ -5,12 +5,7 @@ from datetime import timedelta import operator - -try: - from zoneinfo import ZoneInfo -except ImportError: - # Cannot assign to a type - ZoneInfo = None # type: ignore[misc, assignment] +from zoneinfo import ZoneInfo import numpy as np import pytest @@ -723,19 +718,9 @@ def test_tz_localize_t2d(self): roundtrip = expected.tz_localize("US/Pacific") tm.assert_datetime_array_equal(roundtrip, dta) - easts = ["US/Eastern", "dateutil/US/Eastern"] - if ZoneInfo is not None: - try: - tz = ZoneInfo("US/Eastern") - except KeyError: - # no tzdata - pass - else: - # Argument 1 to "append" of "list" has incompatible type "ZoneInfo"; - # expected "str" - easts.append(tz) # type: ignore[arg-type] - - @pytest.mark.parametrize("tz", easts) + @pytest.mark.parametrize( + "tz", ["US/Eastern", "dateutil/US/Eastern", ZoneInfo("US/Eastern")] + ) def test_iter_zoneinfo_fold(self, tz): # GH#49684 utc_vals = np.array( diff --git a/pandas/tests/indexes/datetimes/methods/test_tz_localize.py b/pandas/tests/indexes/datetimes/methods/test_tz_localize.py index ad7769c6b9671..2927720487bb6 100644 --- a/pandas/tests/indexes/datetimes/methods/test_tz_localize.py +++ b/pandas/tests/indexes/datetimes/methods/test_tz_localize.py @@ -2,6 +2,7 @@ datetime, timedelta, ) +from zoneinfo import ZoneInfo import dateutil.tz from dateutil.tz import gettz @@ -19,22 +20,12 @@ ) import pandas._testing as tm -try: - from zoneinfo import ZoneInfo -except ImportError: - # Cannot assign to a type [misc] - ZoneInfo = None # type: ignore[misc, assignment] - -easts = [pytz.timezone("US/Eastern"), gettz("US/Eastern")] -if ZoneInfo is not None: - try: - tz = ZoneInfo("US/Eastern") - except KeyError: - # no tzdata - pass - else: - easts.append(tz) +@pytest.fixture( + params=[pytz.timezone("US/Eastern"), gettz("US/Eastern"), ZoneInfo("US/Eastern")] +) +def tz(request): + return request.param class TestTZLocalize: @@ -88,7 +79,6 @@ def test_dti_tz_localize_nonexistent_raise_coerce(self): expected = dti.tz_convert("US/Eastern") tm.assert_index_equal(result, expected) - @pytest.mark.parametrize("tz", easts) def test_dti_tz_localize_ambiguous_infer(self, tz): # November 6, 2011, fall back, repeat 2 AM hour # With no repeated hours, we cannot infer the transition @@ -96,7 +86,6 @@ def test_dti_tz_localize_ambiguous_infer(self, tz): with pytest.raises(pytz.AmbiguousTimeError, match="Cannot infer dst time"): dr.tz_localize(tz) - @pytest.mark.parametrize("tz", easts) def test_dti_tz_localize_ambiguous_infer2(self, tz, unit): # With repeated hours, we can infer the transition dr = date_range( @@ -116,7 +105,6 @@ def test_dti_tz_localize_ambiguous_infer2(self, tz, unit): result2 = DatetimeIndex(times, tz=tz, ambiguous="infer").as_unit(unit) tm.assert_index_equal(result2, expected) - @pytest.mark.parametrize("tz", easts) def test_dti_tz_localize_ambiguous_infer3(self, tz): # When there is no dst transition, nothing special happens dr = date_range(datetime(2011, 6, 1, 0), periods=10, freq=offsets.Hour()) @@ -124,7 +112,6 @@ def test_dti_tz_localize_ambiguous_infer3(self, tz): localized_infer = dr.tz_localize(tz, ambiguous="infer") tm.assert_index_equal(localized, localized_infer) - @pytest.mark.parametrize("tz", easts) def test_dti_tz_localize_ambiguous_times(self, tz): # March 13, 2011, spring forward, skip from 2 AM to 3 AM dr = date_range(datetime(2011, 3, 13, 1, 30), periods=3, freq=offsets.Hour()) @@ -245,7 +232,6 @@ def test_dti_tz_localize_tzlocal(self): dti2 = dti.tz_localize(None) tm.assert_numpy_array_equal(dti2.asi8 - offset, dti.asi8) - @pytest.mark.parametrize("tz", easts) def test_dti_tz_localize_ambiguous_nat(self, tz): times = [ "11/06/2011 00:00", @@ -270,7 +256,6 @@ def test_dti_tz_localize_ambiguous_nat(self, tz): # right is datetime64[ns, tzfile('/usr/share/zoneinfo/US/Eastern')] tm.assert_numpy_array_equal(di_test.values, localized.values) - @pytest.mark.parametrize("tz", easts) def test_dti_tz_localize_ambiguous_flags(self, tz, unit): # November 6, 2011, fall back, repeat 2 AM hour @@ -321,8 +306,7 @@ def test_dti_tz_localize_ambiguous_flags(self, tz, unit): dr = dr.append(dr) tm.assert_index_equal(dr, localized) - @pytest.mark.parametrize("tz", easts) - def test_dti_tz_localize_ambiguous_flags2(self, tz, unit): + def test_dti_tz_localize_ambiguous_flags2(self, tz): # When there is no dst transition, nothing special happens dr = date_range(datetime(2011, 6, 1, 0), periods=10, freq=offsets.Hour()) is_dst = np.array([1] * 10) diff --git a/pandas/tests/scalar/timestamp/methods/test_tz_localize.py b/pandas/tests/scalar/timestamp/methods/test_tz_localize.py index 0786cc58a4f95..ce92a2feff30b 100644 --- a/pandas/tests/scalar/timestamp/methods/test_tz_localize.py +++ b/pandas/tests/scalar/timestamp/methods/test_tz_localize.py @@ -1,5 +1,6 @@ from datetime import timedelta import re +from zoneinfo import ZoneInfo from dateutil.tz import gettz import pytest @@ -17,12 +18,6 @@ Timestamp, ) -try: - from zoneinfo import ZoneInfo -except ImportError: - # Cannot assign to a type - ZoneInfo = None # type: ignore[misc, assignment] - class TestTimestampTZLocalize: @pytest.mark.skip_ubsan @@ -64,15 +59,8 @@ def test_tz_localize_ambiguous_bool(self, unit): with pytest.raises(pytz.AmbiguousTimeError, match=msg): ts.tz_localize("dateutil/US/Central") - if ZoneInfo is not None: - try: - tz = ZoneInfo("US/Central") - except KeyError: - # no tzdata - pass - else: - with pytest.raises(pytz.AmbiguousTimeError, match=msg): - ts.tz_localize(tz) + with pytest.raises(pytz.AmbiguousTimeError, match=msg): + ts.tz_localize(ZoneInfo("US/Central")) result = ts.tz_localize("US/Central", ambiguous=True) assert result == expected0 diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py index bbda9d3ee7dce..916e923a210d6 100644 --- a/pandas/tests/scalar/timestamp/test_constructors.py +++ b/pandas/tests/scalar/timestamp/test_constructors.py @@ -160,15 +160,13 @@ def test_timestamp_constructor_retain_fold(self, tz, fold): expected = fold assert result == expected - try: - _tzs = [ + @pytest.mark.parametrize( + "tz", + [ "dateutil/Europe/London", zoneinfo.ZoneInfo("Europe/London"), - ] - except zoneinfo.ZoneInfoNotFoundError: - _tzs = ["dateutil/Europe/London"] - - @pytest.mark.parametrize("tz", _tzs) + ], + ) @pytest.mark.parametrize( "ts_input,fold_out", [ From 4b6ab5d9d4ca0cb1eb1a45b90ccd2cd45baa192b Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:09:03 -0800 Subject: [PATCH 02/16] Adjust docs and tests --- asv_bench/benchmarks/tslibs/timestamp.py | 15 ++-- asv_bench/benchmarks/tslibs/tslib.py | 4 +- asv_bench/benchmarks/tslibs/tz_convert.py | 5 +- doc/source/reference/testing.rst | 2 + doc/source/user_guide/timeseries.rst | 12 +-- pandas/_libs/tslibs/__init__.py | 8 +- pandas/_libs/tslibs/nattype.pyx | 6 +- pandas/_libs/tslibs/strptime.pyx | 6 +- pandas/_libs/tslibs/timestamps.pyx | 6 +- pandas/_libs/tslibs/timezones.pyx | 78 +++++++++------- pandas/_libs/tslibs/tzconversion.pyx | 38 ++++++-- pandas/conftest.py | 49 +++++----- pandas/core/dtypes/dtypes.py | 6 +- pandas/core/indexes/datetimes.py | 6 +- pandas/errors/__init__.py | 4 + pandas/tests/arithmetic/test_datetime64.py | 8 +- pandas/tests/arrays/test_array.py | 18 ++-- pandas/tests/arrays/test_datetimes.py | 6 +- pandas/tests/dtypes/test_dtypes.py | 2 +- pandas/tests/dtypes/test_inference.py | 4 +- .../frame/constructors/test_from_records.py | 8 +- pandas/tests/frame/methods/test_at_time.py | 11 ++- pandas/tests/frame/methods/test_to_dict.py | 14 +-- pandas/tests/frame/test_alter_axes.py | 9 +- pandas/tests/frame/test_constructors.py | 4 +- pandas/tests/groupby/test_timegrouper.py | 12 +-- .../indexes/datetimes/methods/test_astype.py | 3 +- .../indexes/datetimes/methods/test_insert.py | 90 +++++++++---------- .../indexes/datetimes/methods/test_shift.py | 29 ++++-- .../datetimes/methods/test_to_period.py | 11 ++- .../datetimes/methods/test_tz_convert.py | 6 +- .../datetimes/methods/test_tz_localize.py | 41 ++++----- .../indexes/datetimes/test_constructors.py | 25 ++++-- .../indexes/datetimes/test_date_range.py | 21 +++-- .../tests/indexes/datetimes/test_formats.py | 8 +- pandas/tests/indexes/datetimes/test_setops.py | 5 +- .../tests/indexes/datetimes/test_timezones.py | 21 +++-- pandas/tests/indexes/multi/test_reshape.py | 10 +-- pandas/tests/io/json/test_ujson.py | 2 +- pandas/tests/io/parser/test_parse_dates.py | 2 +- pandas/tests/resample/test_datetime_index.py | 4 +- pandas/tests/resample/test_period_index.py | 20 +++-- pandas/tests/reshape/merge/test_merge_asof.py | 7 +- pandas/tests/scalar/test_nat.py | 4 +- .../scalar/timestamp/methods/test_replace.py | 11 ++- .../scalar/timestamp/methods/test_round.py | 6 +- .../methods/test_timestamp_method.py | 2 +- .../timestamp/methods/test_to_pydatetime.py | 3 +- .../timestamp/methods/test_tz_localize.py | 46 +++++----- .../tests/scalar/timestamp/test_arithmetic.py | 6 +- .../scalar/timestamp/test_constructors.py | 54 ++++++----- pandas/tests/scalar/timestamp/test_formats.py | 14 +-- .../tests/scalar/timestamp/test_timestamp.py | 15 ++-- .../series/accessors/test_dt_accessor.py | 11 ++- pandas/tests/series/indexing/test_datetime.py | 2 +- pandas/tests/series/methods/test_fillna.py | 11 ++- .../tests/series/methods/test_tz_localize.py | 10 +-- pandas/tests/tools/test_to_datetime.py | 21 +++-- pandas/tests/tseries/holiday/test_holiday.py | 9 +- pandas/tests/tseries/offsets/test_dst.py | 20 ++--- .../offsets/test_offsets_properties.py | 3 +- pandas/tests/tslibs/test_conversion.py | 22 +++-- pandas/tests/tslibs/test_resolution.py | 5 +- pandas/tests/tslibs/test_timezones.py | 31 ++++--- pandas/tests/tslibs/test_tzconversion.py | 19 ++-- 65 files changed, 563 insertions(+), 408 deletions(-) diff --git a/asv_bench/benchmarks/tslibs/timestamp.py b/asv_bench/benchmarks/tslibs/timestamp.py index 082220ee0dff2..6145966fb6a0e 100644 --- a/asv_bench/benchmarks/tslibs/timestamp.py +++ b/asv_bench/benchmarks/tslibs/timestamp.py @@ -1,7 +1,10 @@ -from datetime import datetime +from datetime import ( + datetime, + timezone, +) +import zoneinfo import numpy as np -import pytz from pandas import Timestamp @@ -12,7 +15,7 @@ class TimestampConstruction: def setup(self): self.npdatetime64 = np.datetime64("2020-01-01 00:00:00") self.dttime_unaware = datetime(2020, 1, 1, 0, 0, 0) - self.dttime_aware = datetime(2020, 1, 1, 0, 0, 0, 0, pytz.UTC) + self.dttime_aware = datetime(2020, 1, 1, 0, 0, 0, 0, timezone.utc) self.ts = Timestamp("2020-01-01 00:00:00") def time_parse_iso8601_no_tz(self): @@ -113,7 +116,7 @@ def setup(self, tz): self.ts = Timestamp("2017-08-25 08:16:14", tz=tz) def time_replace_tz(self, tz): - self.ts.replace(tzinfo=pytz.timezone("US/Eastern")) + self.ts.replace(tzinfo=zoneinfo.ZoneInfo("US/Eastern")) def time_replace_None(self, tz): self.ts.replace(tzinfo=None) @@ -144,8 +147,8 @@ def time_ceil(self, tz): class TimestampAcrossDst: def setup(self): - dt = datetime(2016, 3, 27, 1) - self.tzinfo = pytz.timezone("CET").localize(dt, is_dst=False).tzinfo + dt = datetime(2016, 3, 27, 1, fold=0) + self.tzinfo = dt.astimezone(zoneinfo.ZoneInfo("Europe/Berlin")).tzinfo self.ts2 = Timestamp(dt) def time_replace_across_dst(self): diff --git a/asv_bench/benchmarks/tslibs/tslib.py b/asv_bench/benchmarks/tslibs/tslib.py index 97ec80201dd16..6394c6f7f023a 100644 --- a/asv_bench/benchmarks/tslibs/tslib.py +++ b/asv_bench/benchmarks/tslibs/tslib.py @@ -19,13 +19,13 @@ timedelta, timezone, ) +import zoneinfo from dateutil.tz import ( gettz, tzlocal, ) import numpy as np -import pytz try: from pandas._libs.tslibs import ints_to_pydatetime @@ -37,7 +37,7 @@ None, timezone.utc, timezone(timedelta(minutes=60)), - pytz.timezone("US/Pacific"), + zoneinfo.ZoneInfo("US/Pacific"), gettz("Asia/Tokyo"), tzlocal_obj, ] diff --git a/asv_bench/benchmarks/tslibs/tz_convert.py b/asv_bench/benchmarks/tslibs/tz_convert.py index c6b510efdca69..c87adb5e5d0e9 100644 --- a/asv_bench/benchmarks/tslibs/tz_convert.py +++ b/asv_bench/benchmarks/tslibs/tz_convert.py @@ -1,5 +1,6 @@ +from datetime import timezone + import numpy as np -from pytz import UTC from pandas._libs.tslibs.tzconversion import tz_localize_to_utc @@ -41,7 +42,7 @@ def time_tz_convert_from_utc(self, size, tz): # dti = DatetimeIndex(self.i8data, tz=tz) # dti.tz_localize(None) if old_sig: - tz_convert_from_utc(self.i8data, UTC, tz) + tz_convert_from_utc(self.i8data, timezone.utc, tz) else: tz_convert_from_utc(self.i8data, tz) diff --git a/doc/source/reference/testing.rst b/doc/source/reference/testing.rst index a5d61703aceed..1c3be9db0391a 100644 --- a/doc/source/reference/testing.rst +++ b/doc/source/reference/testing.rst @@ -24,6 +24,7 @@ Exceptions and warnings .. autosummary:: :toctree: api/ + errors.AmbiguousTimeError errors.AbstractMethodError errors.AttributeConflictWarning errors.CategoricalConversionWarning @@ -45,6 +46,7 @@ Exceptions and warnings errors.LossySetitemError errors.MergeError errors.NoBufferPresent + errors.NonExistentTimeError errors.NullFrequencyError errors.NumbaUtilError errors.NumExprClobberingError diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst index 0f0e6271d8329..39916c2bca7aa 100644 --- a/doc/source/user_guide/timeseries.rst +++ b/doc/source/user_guide/timeseries.rst @@ -2326,7 +2326,7 @@ Time zone handling ------------------ pandas provides rich support for working with timestamps in different time -zones using the ``pytz`` and ``dateutil`` libraries or :class:`datetime.timezone` +zones using the ``zoneinfo``, ``pytz``, ``dateutil`` libraries or :class:`datetime.timezone` objects from the standard library. @@ -2343,10 +2343,12 @@ By default, pandas objects are time zone unaware: To localize these dates to a time zone (assign a particular time zone to a naive date), you can use the ``tz_localize`` method or the ``tz`` keyword argument in :func:`date_range`, :class:`Timestamp`, or :class:`DatetimeIndex`. -You can either pass ``pytz`` or ``dateutil`` time zone objects or Olson time zone database strings. -Olson time zone strings will return ``pytz`` time zone objects by default. +You can either pass ``zoneinfo``, ``pytz`` or ``dateutil`` time zone objects or Olson time zone database strings. +Olson time zone strings will return ``zoneinfo.ZoneInfo`` time zone objects by default. To return ``dateutil`` time zone objects, append ``dateutil/`` before the string. +* For ``zoneinfo``, a list of available timezones are available from +``zoneinfo.available_timezones()``. * In ``pytz`` you can find a list of common (and less common) time zones using ``from pytz import common_timezones, all_timezones``. * ``dateutil`` uses the OS time zones so there isn't a fixed list available. For @@ -2556,7 +2558,7 @@ Ambiguous times when localizing because daylight savings time (DST) in a local time zone causes some times to occur twice within one day ("clocks fall back"). The following options are available: -* ``'raise'``: Raises a ``pytz.AmbiguousTimeError`` (the default behavior) +* ``'raise'``: Raises an :class:`errors.AmbiguousTimeError` (the default behavior) * ``'infer'``: Attempt to determine the correct offset base on the monotonicity of the timestamps * ``'NaT'``: Replaces ambiguous times with ``NaT`` * ``bool``: ``True`` represents a DST time, ``False`` represents non-DST time. An array-like of ``bool`` values is supported for a sequence of times. @@ -2591,7 +2593,7 @@ A DST transition may also shift the local time ahead by 1 hour creating nonexist local times ("clocks spring forward"). The behavior of localizing a timeseries with nonexistent times can be controlled by the ``nonexistent`` argument. The following options are available: -* ``'raise'``: Raises a ``pytz.NonExistentTimeError`` (the default behavior) +* ``'raise'``: Raises a :class:`errors.NonExistentTimeError` (the default behavior) * ``'NaT'``: Replaces nonexistent times with ``NaT`` * ``'shift_forward'``: Shifts nonexistent times forward to the closest real time * ``'shift_backward'``: Shifts nonexistent times backward to the closest real time diff --git a/pandas/_libs/tslibs/__init__.py b/pandas/_libs/tslibs/__init__.py index 88a9a259ac8ec..abbf2be9fe67f 100644 --- a/pandas/_libs/tslibs/__init__.py +++ b/pandas/_libs/tslibs/__init__.py @@ -1,4 +1,6 @@ __all__ = [ + "AmbiguousTimeError", + "NonExistentTimeError", "dtypes", "localize_pydatetime", "NaT", @@ -76,7 +78,11 @@ ) from pandas._libs.tslibs.timestamps import Timestamp from pandas._libs.tslibs.timezones import tz_compare -from pandas._libs.tslibs.tzconversion import tz_convert_from_utc_single +from pandas._libs.tslibs.tzconversion import ( + AmbiguousTimeError, + NonExistentTimeError, + tz_convert_from_utc_single, +) from pandas._libs.tslibs.vectorized import ( dt64arr_to_periodarr, get_resolution, diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index cd5e6e521b79f..3e4ff5a923768 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -1345,13 +1345,13 @@ default 'raise' Replace timezone (not a conversion): - >>> import pytz - >>> ts.replace(tzinfo=pytz.timezone('US/Pacific')) + >>> import zoneinfo + >>> ts.replace(tzinfo=zoneinfo.Zoneinfo('US/Pacific')) Timestamp('2020-03-14 15:32:52.192548651-0700', tz='US/Pacific') Analogous for ``pd.NaT``: - >>> pd.NaT.replace(tzinfo=pytz.timezone('US/Pacific')) + >>> pd.NaT.replace(tzinfo=zoneinfo.Zoneinfo('US/Pacific')) NaT """, ) diff --git a/pandas/_libs/tslibs/strptime.pyx b/pandas/_libs/tslibs/strptime.pyx index c09835c9661f3..da3edaec50792 100644 --- a/pandas/_libs/tslibs/strptime.pyx +++ b/pandas/_libs/tslibs/strptime.pyx @@ -16,6 +16,7 @@ FUNCTIONS: strptime -- Calculates the time struct represented by the passed-in string """ from datetime import timezone +import zoneinfo from cpython.datetime cimport ( PyDate_Check, @@ -38,7 +39,6 @@ from _thread import allocate_lock as _thread_allocate_lock import re import numpy as np -import pytz cimport numpy as cnp from numpy cimport ( @@ -725,7 +725,7 @@ cdef tzinfo _parse_with_format( week_of_year_start = 0 elif parse_code == 17: # e.g. val='2011-12-30T00:00:00.000000UTC'; fmt='%Y-%m-%dT%H:%M:%S.%f%Z' - tz = pytz.timezone(found_dict["Z"]) + tz = zoneinfo.ZoneInfo(found_dict["Z"]) elif parse_code == 19: # e.g. val='March 1, 2018 12:00:00+0400'; fmt='%B %d, %Y %H:%M:%S%z' tz = parse_timezone_directive(found_dict["z"]) @@ -966,7 +966,7 @@ class TimeRE(_TimeRE): if key == "Z": # lazy computation if self._Z is None: - self._Z = self.__seqToRE(pytz.all_timezones, "Z") + self._Z = self.__seqToRE(zoneinfo.available_timezones(), "Z") # Note: handling Z is the key difference vs using the stdlib # _strptime.TimeRE. test_to_datetime_parse_tzname_or_tzoffset with # fmt='%Y-%m-%d %H:%M:%S %Z' fails with the stdlib version. diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index d4cd90613ca5b..e7b60033a0bd4 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -2422,13 +2422,13 @@ default 'raise' Replace timezone (not a conversion): - >>> import pytz - >>> ts.replace(tzinfo=pytz.timezone('US/Pacific')) + >>> import zoneinfo + >>> ts.replace(tzinfo=zoneinfo.Zoneinfo('US/Pacific')) Timestamp('2020-03-14 15:32:52.192548651-0700', tz='US/Pacific') Analogous for ``pd.NaT``: - >>> pd.NaT.replace(tzinfo=pytz.timezone('US/Pacific')) + >>> pd.NaT.replace(tzinfo=zoneinfo.Zoneinfo('US/Pacific')) NaT """ diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index c2052a2e6f441..49b1a5790cb8e 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -3,7 +3,6 @@ from datetime import ( timezone, ) import zoneinfo -from zoneinfo import ZoneInfo from pandas.compat._optional import import_optional_dependency @@ -22,8 +21,11 @@ from dateutil.tz import ( tzutc as _dateutil_tzutc, ) import numpy as np -import pytz -from pytz.tzinfo import BaseTzInfo as _pytz_BaseTzInfo + +try: + import pytz +except ImportError: + pytz = None cimport numpy as cnp from numpy cimport int64_t @@ -39,7 +41,7 @@ from pandas._libs.tslibs.util cimport ( cdef int64_t NPY_NAT = get_nat() cdef tzinfo utc_stdlib = timezone.utc -cdef tzinfo utc_pytz = pytz.utc +cdef tzinfo utc_pytz = None cdef tzinfo utc_dateutil_str = dateutil_gettz("UTC") # NB: *not* the same as tzutc() cdef tzinfo utc_zoneinfo = None @@ -50,13 +52,13 @@ cdef tzinfo utc_zoneinfo = None cdef bint is_utc_zoneinfo(tzinfo tz): # Workaround for cases with missing tzdata # https://github.com/pandas-dev/pandas/pull/46425#discussion_r830633025 - if tz is None or zoneinfo is None: + if tz is None: return False global utc_zoneinfo if utc_zoneinfo is None: try: - utc_zoneinfo = ZoneInfo("UTC") + utc_zoneinfo = zoneinfo.ZoneInfo("UTC") except zoneinfo.ZoneInfoNotFoundError: return False # Warn if tzdata is too old, even if there is a system tzdata to alert @@ -66,20 +68,31 @@ cdef bint is_utc_zoneinfo(tzinfo tz): return tz is utc_zoneinfo +cdef bint is_utc_pytz(tzinfo tz): + if tz is None: + return False + + global utc_pytz + if utc_pytz is None: + if pytz is None: + return False + utc_pytz = pytz.UTC + + return tz is utc_pytz + + cpdef inline bint is_utc(tzinfo tz): return ( - tz is utc_pytz - or tz is utc_stdlib + tz is utc_stdlib or isinstance(tz, _dateutil_tzutc) or tz is utc_dateutil_str or is_utc_zoneinfo(tz) + or is_utc_pytz(tz) ) cdef bint is_zoneinfo(tzinfo tz): - if ZoneInfo is None: - return False - return isinstance(tz, ZoneInfo) + return isinstance(tz, zoneinfo.ZoneInfo) cdef bint is_tzlocal(tzinfo tz): @@ -113,27 +126,26 @@ cpdef inline object get_timezone(tzinfo tz): raise TypeError("tz argument cannot be None") if is_utc(tz): return tz + elif is_zoneinfo(tz): + return tz.key + elif treat_tz_as_dateutil(tz): + if ".tar.gz" in tz._filename: + raise ValueError( + "Bad tz filename. Dateutil on python 3 on windows has a " + "bug which causes tzfile._filename to be the same for all " + "timezone files. Please construct dateutil timezones " + 'implicitly by passing a string like "dateutil/Europe' + '/London" when you construct your pandas objects instead ' + "of passing a timezone object. See " + "https://github.com/pandas-dev/pandas/pull/7362") + return "dateutil/" + tz._filename + elif treat_tz_as_pytz(tz): + zone = tz.zone + if zone is None: + return tz + return zone else: - if treat_tz_as_dateutil(tz): - if ".tar.gz" in tz._filename: - raise ValueError( - "Bad tz filename. Dateutil on python 3 on windows has a " - "bug which causes tzfile._filename to be the same for all " - "timezone files. Please construct dateutil timezones " - 'implicitly by passing a string like "dateutil/Europe' - '/London" when you construct your pandas objects instead ' - "of passing a timezone object. See " - "https://github.com/pandas-dev/pandas/pull/7362") - return "dateutil/" + tz._filename - else: - # tz is a pytz timezone or unknown. - try: - zone = tz.zone - if zone is None: - return tz - return zone - except AttributeError: - return tz + return tz cpdef inline tzinfo maybe_get_tz(object tz): @@ -200,7 +212,7 @@ cdef object tz_cache_key(tzinfo tz): the same tz file). Also, pytz objects are not always hashable so we use str(tz) instead. """ - if isinstance(tz, _pytz_BaseTzInfo): + if pytz is not None and isinstance(tz, pytz.tzinfo.BaseTzInfo): return tz.zone elif isinstance(tz, _dateutil_tzfile): if ".tar.gz" in tz._filename: @@ -438,6 +450,6 @@ def tz_standardize(tz: tzinfo) -> tzinfo: >>> tz_standardize(tz) """ - if treat_tz_as_pytz(tz): + if treat_tz_as_pytz(tz) and pytz is not None: return pytz.timezone(str(tz)) return tz diff --git a/pandas/_libs/tslibs/tzconversion.pyx b/pandas/_libs/tslibs/tzconversion.pyx index 2c4f0cd14db13..dc4fd37179789 100644 --- a/pandas/_libs/tslibs/tzconversion.pyx +++ b/pandas/_libs/tslibs/tzconversion.pyx @@ -15,7 +15,6 @@ from cython cimport Py_ssize_t import_datetime() import numpy as np -import pytz cimport numpy as cnp from numpy cimport ( @@ -52,6 +51,33 @@ from pandas._libs.tslibs.timezones cimport ( ) +class AmbiguousTimeError(Exception): + """ + Raised when a datetime is localized on a local time that occurs twice. + + Examples + -------- + >>> pd.Timestamp(2024, 11, 3, 1, 30).tz_localize("US/Pacific") + Traceback (most recent call last): + AmbiguousTimeError: Cannot infer dst time from 2024-11-03 01:30:00, + try using the 'ambiguous' argument + """ + pass + + +class NonExistentTimeError(Exception): + """ + Raised when a datetime is localized on a local time that doesn't exist. + + Examples + -------- + >>> pd.Timestamp(2024, 3, 10, 2, 30).tz_localize("US/Pacific") + Traceback (most recent call last): + NonExistentTimeError: 2024-03-10 02:30:00 + """ + pass + + cdef const int64_t[::1] _deltas_placeholder = np.array([], dtype=np.int64) @@ -368,7 +394,7 @@ timedelta-like} result[i] = NPY_NAT else: stamp = _render_tstamp(val, creso=creso) - raise pytz.AmbiguousTimeError( + raise AmbiguousTimeError( f"Cannot infer dst time from {stamp}, try using the " "'ambiguous' argument" ) @@ -428,7 +454,7 @@ timedelta-like} result[i] = NPY_NAT else: stamp = _render_tstamp(val, creso=creso) - raise pytz.NonExistentTimeError(stamp) + raise NonExistentTimeError(stamp) return result.base # .base to get underlying ndarray @@ -630,7 +656,7 @@ cdef ndarray[int64_t] _get_dst_hours( if trans_idx.size == 1: # see test_tz_localize_to_utc_ambiguous_infer stamp = _render_tstamp(vals[trans_idx[0]], creso=creso) - raise pytz.AmbiguousTimeError( + raise AmbiguousTimeError( f"Cannot infer dst time from {stamp} as there " "are no repeated times" ) @@ -652,14 +678,14 @@ cdef ndarray[int64_t] _get_dst_hours( if grp.size == 1 or np.all(delta > 0): # see test_tz_localize_to_utc_ambiguous_infer stamp = _render_tstamp(vals[grp[0]], creso=creso) - raise pytz.AmbiguousTimeError(stamp) + raise AmbiguousTimeError(stamp) # Find the index for the switch and pull from a for dst and b # for standard switch_idxs = (delta <= 0).nonzero()[0] if switch_idxs.size > 1: # see test_tz_localize_to_utc_ambiguous_infer - raise pytz.AmbiguousTimeError( + raise AmbiguousTimeError( f"There are {switch_idxs.size} dst switches when " "there should only be 1." ) diff --git a/pandas/conftest.py b/pandas/conftest.py index 1c9629b11f074..d7d9b98452130 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -43,10 +43,6 @@ from hypothesis import strategies as st import numpy as np import pytest -from pytz import ( - FixedOffset, - utc, -) from pandas._config.config import _get_option @@ -94,7 +90,10 @@ del pa has_pyarrow = True -import zoneinfo +try: + import pytz +except ImportError: + pytz = None # ---------------------------------------------------------------- # Configuration / Settings @@ -1202,15 +1201,21 @@ def deco(*args): "UTC-02:15", tzutc(), tzlocal(), - FixedOffset(300), - FixedOffset(0), - FixedOffset(-300), timezone.utc, timezone(timedelta(hours=1)), timezone(timedelta(hours=-1), name="foo"), - zoneinfo.ZoneInfo("US/Pacific"), - zoneinfo.ZoneInfo("UTC"), ] + +if pytz is not None: + TIMEZONES.extend( + ( + pytz.FixedOffset(300), + pytz.FixedOffset(0), + pytz.FixedOffset(-300), + pytz.timezone("US/Pacific"), + pytz.timezone("UTC"), + ) + ) TIMEZONE_IDS = [repr(i) for i in TIMEZONES] @@ -1236,9 +1241,13 @@ def tz_aware_fixture(request): tz_aware_fixture2 = tz_aware_fixture -@pytest.fixture( - params=["utc", "dateutil/UTC", utc, tzutc(), timezone.utc, zoneinfo.ZoneInfo("UTC")] -) +_UTC = ["UTC", "utc", "dateutil/UTC", tzutc(), timezone.utc] + +if pytz is not None: + _UTC.append(pytz.utc) + + +@pytest.fixture(params=_UTC) def utc_fixture(request): """ Fixture to provide variants of UTC timezone strings and tzinfo objects. @@ -1944,13 +1953,13 @@ def using_infer_string() -> bool: return pd.options.future.infer_string is True -@pytest.fixture( - params=[ - "Europe/Warsaw", - "dateutil/Europe/Warsaw", - zoneinfo.ZoneInfo("Europe/Warsaw"), - ] -) +_warsaws = ["Europe/Warsaw", "dateutil/Europe/Warsaw"] + +if pytz is not None: + _warsaws.append(pytz.timezone("Europe/Warsaw")) + + +@pytest.fixture(params=_warsaws) def warsaw(request) -> str: """ tzinfo for Europe/Warsaw using pytz, dateutil, or zoneinfo. diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 5b51bc9debb33..281afcfcf9296 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -17,9 +17,9 @@ cast, ) import warnings +import zoneinfo import numpy as np -import pytz from pandas._libs import ( lib, @@ -773,7 +773,7 @@ def __init__(self, unit: str_type | DatetimeTZDtype = "ns", tz=None) -> None: tz = timezones.maybe_get_tz(tz) tz = timezones.tz_standardize(tz) elif tz is not None: - raise pytz.UnknownTimeZoneError(tz) + raise zoneinfo.ZoneInfoNotFoundError(tz) if tz is None: raise TypeError("A 'tz' is required.") @@ -858,7 +858,7 @@ def construct_from_string(cls, string: str_type) -> DatetimeTZDtype: return cls(unit=d["unit"], tz=d["tz"]) except (KeyError, TypeError, ValueError) as err: # KeyError if maybe_get_tz tries and fails to get a - # pytz timezone (actually pytz.UnknownTimeZoneError). + # zoneinfo timezone (actually zoneinfo.ZoneInfoNotFoundError). # TypeError if we pass a nonsense tz; # ValueError if we pass a unit other than "ns" raise TypeError(msg) from err diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index c978abd8c2427..58774c31d05ea 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -6,7 +6,6 @@ import warnings import numpy as np -import pytz from pandas._libs import ( NaT, @@ -24,6 +23,7 @@ to_offset, ) from pandas._libs.tslibs.offsets import prefix_mapping +from pandas.errors import NonExistentTimeError from pandas.util._decorators import ( cache_readonly, doc, @@ -148,7 +148,7 @@ class DatetimeIndex(DatetimeTimedeltaMixin): One of pandas date offset strings or corresponding objects. The string 'infer' can be passed in order to set the frequency of the index as the inferred frequency upon creation. - tz : pytz.timezone or dateutil.tz.tzfile or datetime.tzinfo or str + tz : zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or str Set the Timezone of the data. normalize : bool, default False Normalize start/end dates to midnight before generating date range. @@ -601,7 +601,7 @@ def get_loc(self, key): elif isinstance(key, str): try: parsed, reso = self._parse_with_reso(key) - except (ValueError, pytz.NonExistentTimeError) as err: + except (ValueError, NonExistentTimeError) as err: raise KeyError(key) from err self._disallow_mismatched_indexing(parsed) diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index d47e02cda1837..49d4789976718 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -8,6 +8,8 @@ from pandas._config.config import OptionError from pandas._libs.tslibs import ( + AmbiguousTimeError, + NonExistentTimeError, OutOfBoundsDatetime, OutOfBoundsTimedelta, ) @@ -829,6 +831,7 @@ class InvalidComparison(Exception): __all__ = [ "AbstractMethodError", + "AmbiguousTimeError", "AttributeConflictWarning", "CategoricalConversionWarning", "ClosedFileError", @@ -848,6 +851,7 @@ class InvalidComparison(Exception): "LossySetitemError", "MergeError", "NoBufferPresent", + "NonExistentTimeError", "NullFrequencyError", "NumbaUtilError", "NumExprClobberingError", diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index 2dafaf277be8f..c01feca707d3a 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -5,6 +5,7 @@ datetime, time, timedelta, + timezone, ) from itertools import ( product, @@ -14,7 +15,6 @@ import numpy as np import pytest -import pytz from pandas._libs.tslibs.conversion import localize_pydatetime from pandas._libs.tslibs.offsets import shift_months @@ -1837,8 +1837,10 @@ def test_dt64tz_series_sub_dtitz(self): def test_sub_datetime_compat(self, unit): # see GH#14088 - ser = Series([datetime(2016, 8, 23, 12, tzinfo=pytz.utc), NaT]).dt.as_unit(unit) - dt = datetime(2016, 8, 22, 12, tzinfo=pytz.utc) + ser = Series([datetime(2016, 8, 23, 12, tzinfo=timezone.utc), NaT]).dt.as_unit( + unit + ) + dt = datetime(2016, 8, 22, 12, tzinfo=timezone.utc) # The datetime object has "us" so we upcast lower units exp_unit = tm.get_finest_unit(unit, "us") exp = Series([Timedelta("1 days"), NaT]).dt.as_unit(exp_unit) diff --git a/pandas/tests/arrays/test_array.py b/pandas/tests/arrays/test_array.py index a84fefebf044c..c51f7e295d974 100644 --- a/pandas/tests/arrays/test_array.py +++ b/pandas/tests/arrays/test_array.py @@ -1,10 +1,10 @@ import datetime import decimal import re +import zoneinfo import numpy as np import pytest -import pytz import pandas as pd import pandas._testing as tm @@ -272,9 +272,6 @@ def test_array_copy(): assert tm.shares_memory(a, b) -cet = pytz.timezone("CET") - - @pytest.mark.parametrize( "data, expected", [ @@ -313,11 +310,18 @@ def test_array_copy(): ), ( [ - datetime.datetime(2000, 1, 1, tzinfo=cet), - datetime.datetime(2001, 1, 1, tzinfo=cet), + datetime.datetime( + 2000, 1, 1, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin") + ), + datetime.datetime( + 2001, 1, 1, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin") + ), ], DatetimeArray._from_sequence( - ["2000", "2001"], dtype=pd.DatetimeTZDtype(tz=cet, unit="ns") + ["2000", "2001"], + dtype=pd.DatetimeTZDtype( + tz=zoneinfo.ZoneInfo("Europe/Berlin"), unit="ns" + ), ), ), # timedelta diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py index b4039acb7f876..9503cd6204cc4 100644 --- a/pandas/tests/arrays/test_datetimes.py +++ b/pandas/tests/arrays/test_datetimes.py @@ -5,7 +5,6 @@ from datetime import timedelta import operator -from zoneinfo import ZoneInfo import numpy as np import pytest @@ -719,10 +718,13 @@ def test_tz_localize_t2d(self): tm.assert_datetime_array_equal(roundtrip, dta) @pytest.mark.parametrize( - "tz", ["US/Eastern", "dateutil/US/Eastern", ZoneInfo("US/Eastern")] + "tz", ["US/Eastern", "dateutil/US/Eastern", "pytz/US/Eastern"] ) def test_iter_zoneinfo_fold(self, tz): # GH#49684 + if tz.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + tz = pytz.timezone(tz.removeprefix("pytz/")) utc_vals = np.array( [1320552000, 1320555600, 1320559200, 1320562800], dtype=np.int64 ) diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index c52d49034f74e..f3466d53a1edc 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -3,7 +3,6 @@ import numpy as np import pytest -import pytz from pandas._libs.tslibs.dtypes import NpyDatetimeUnit @@ -391,6 +390,7 @@ def test_empty(self): def test_tz_standardize(self): # GH 24713 + pytz = pytest.importorskip("pytz") tz = pytz.timezone("US/Eastern") dr = date_range("2013-01-01", periods=3, tz="US/Eastern") dtype = DatetimeTZDtype("ns", dr.tz) diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py index f1f5cb1620345..caa2f3909f129 100644 --- a/pandas/tests/dtypes/test_inference.py +++ b/pandas/tests/dtypes/test_inference.py @@ -11,6 +11,7 @@ datetime, time, timedelta, + timezone, ) from decimal import Decimal from fractions import Fraction @@ -26,7 +27,6 @@ import numpy as np import pytest -import pytz from pandas._libs import ( lib, @@ -1019,7 +1019,7 @@ def test_maybe_convert_objects_itemsize(self, data0, data1): def test_mixed_dtypes_remain_object_array(self): # GH14956 - arr = np.array([datetime(2015, 1, 1, tzinfo=pytz.utc), 1], dtype=object) + arr = np.array([datetime(2015, 1, 1, tzinfo=timezone.utc), 1], dtype=object) result = lib.maybe_convert_objects(arr, convert_non_numeric=True) tm.assert_numpy_array_equal(result, arr) diff --git a/pandas/tests/frame/constructors/test_from_records.py b/pandas/tests/frame/constructors/test_from_records.py index 3622571f1365d..dc23d86fe4204 100644 --- a/pandas/tests/frame/constructors/test_from_records.py +++ b/pandas/tests/frame/constructors/test_from_records.py @@ -1,10 +1,12 @@ from collections.abc import Iterator -from datetime import datetime +from datetime import ( + datetime, + timezone, +) from decimal import Decimal import numpy as np import pytest -import pytz from pandas._config import using_pyarrow_string_dtype @@ -279,7 +281,7 @@ def test_from_records_series_categorical_index(self): tm.assert_frame_equal(frame, expected) def test_frame_from_records_utc(self): - rec = {"datum": 1.5, "begin_time": datetime(2006, 4, 27, tzinfo=pytz.utc)} + rec = {"datum": 1.5, "begin_time": datetime(2006, 4, 27, tzinfo=timezone.utc)} # it works DataFrame.from_records([rec], index="begin_time") diff --git a/pandas/tests/frame/methods/test_at_time.py b/pandas/tests/frame/methods/test_at_time.py index 1ebe9920933d1..36e691bffc7da 100644 --- a/pandas/tests/frame/methods/test_at_time.py +++ b/pandas/tests/frame/methods/test_at_time.py @@ -1,8 +1,11 @@ -from datetime import time +from datetime import ( + time, + timezone, +) +import zoneinfo import numpy as np import pytest -import pytz from pandas._libs.tslibs import timezones @@ -65,7 +68,7 @@ def test_at_time_nonexistent(self, frame_or_series): assert len(rs) == 0 @pytest.mark.parametrize( - "hour", ["1:00", "1:00AM", time(1), time(1, tzinfo=pytz.UTC)] + "hour", ["1:00", "1:00AM", time(1), time(1, tzinfo=timezone.utc)] ) def test_at_time_errors(self, hour): # GH#24043 @@ -83,7 +86,7 @@ def test_at_time_tz(self): # GH#24043 dti = date_range("2018", periods=3, freq="h", tz="US/Pacific") df = DataFrame(list(range(len(dti))), index=dti) - result = df.at_time(time(4, tzinfo=pytz.timezone("US/Eastern"))) + result = df.at_time(time(4, tzinfo=zoneinfo.ZoneInfo("US/Eastern"))) expected = df.iloc[1:2] tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/frame/methods/test_to_dict.py b/pandas/tests/frame/methods/test_to_dict.py index 61f0ad30b4519..e6de1331e2729 100644 --- a/pandas/tests/frame/methods/test_to_dict.py +++ b/pandas/tests/frame/methods/test_to_dict.py @@ -2,11 +2,13 @@ OrderedDict, defaultdict, ) -from datetime import datetime +from datetime import ( + datetime, + timezone, +) import numpy as np import pytest -import pytz from pandas import ( NA, @@ -206,15 +208,15 @@ def test_to_dict_tz(self): # GH#18372 When converting to dict with orient='records' columns of # datetime that are tz-aware were not converted to required arrays data = [ - (datetime(2017, 11, 18, 21, 53, 0, 219225, tzinfo=pytz.utc),), - (datetime(2017, 11, 18, 22, 6, 30, 61810, tzinfo=pytz.utc),), + (datetime(2017, 11, 18, 21, 53, 0, 219225, tzinfo=timezone.utc),), + (datetime(2017, 11, 18, 22, 6, 30, 61810, tzinfo=timezone.utc),), ] df = DataFrame(list(data), columns=["d"]) result = df.to_dict(orient="records") expected = [ - {"d": Timestamp("2017-11-18 21:53:00.219225+0000", tz=pytz.utc)}, - {"d": Timestamp("2017-11-18 22:06:30.061810+0000", tz=pytz.utc)}, + {"d": Timestamp("2017-11-18 21:53:00.219225+0000", tz=timezone.utc)}, + {"d": Timestamp("2017-11-18 22:06:30.061810+0000", tz=timezone.utc)}, ] tm.assert_dict_equal(result[0], expected[0]) tm.assert_dict_equal(result[1], expected[1]) diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index c68171ab254c7..b4c16b94fcf8b 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -1,6 +1,7 @@ -from datetime import datetime - -import pytz +from datetime import ( + datetime, + timezone, +) from pandas import DataFrame import pandas._testing as tm @@ -13,7 +14,7 @@ def test_set_axis_setattr_index(self): # GH 6785 # set the index manually - df = DataFrame([{"ts": datetime(2014, 4, 1, tzinfo=pytz.utc), "foo": 1}]) + df = DataFrame([{"ts": datetime(2014, 4, 1, tzinfo=timezone.utc), "foo": 1}]) expected = df.set_index("ts") df.index = df["ts"] df.pop("ts") diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index c66b6a0f8b99b..392feb27f35ab 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -14,12 +14,12 @@ ) import functools import re +import zoneinfo import numpy as np from numpy import ma from numpy.ma import mrecords import pytest -import pytz from pandas._config import using_pyarrow_string_dtype @@ -1923,7 +1923,7 @@ def test_constructor_with_datetimes2(self): def test_constructor_with_datetimes3(self): # GH 7594 # don't coerce tz-aware - tz = pytz.timezone("US/Eastern") + tz = zoneinfo.ZoneInfo("US/Eastern") dt = tz.localize(datetime(2012, 1, 1)) df = DataFrame({"End Date": dt}, index=[0]) diff --git a/pandas/tests/groupby/test_timegrouper.py b/pandas/tests/groupby/test_timegrouper.py index 8ef7c2b8ce859..31b196ca3faff 100644 --- a/pandas/tests/groupby/test_timegrouper.py +++ b/pandas/tests/groupby/test_timegrouper.py @@ -4,11 +4,11 @@ from datetime import ( datetime, timedelta, + timezone, ) import numpy as np import pytest -import pytz import pandas as pd from pandas import ( @@ -773,12 +773,12 @@ def test_groupby_with_timezone_selection(self): def test_timezone_info(self): # see gh-11682: Timezone info lost when broadcasting # scalar datetime to DataFrame - - df = DataFrame({"a": [1], "b": [datetime.now(pytz.utc)]}) - assert df["b"][0].tzinfo == pytz.utc + utc = timezone.utc + df = DataFrame({"a": [1], "b": [datetime.now(utc)]}) + assert df["b"][0].tzinfo == utc df = DataFrame({"a": [1, 2, 3]}) - df["b"] = datetime.now(pytz.utc) - assert df["b"][0].tzinfo == pytz.utc + df["b"] = datetime.now(utc) + assert df["b"][0].tzinfo == utc def test_datetime_count(self): df = DataFrame( diff --git a/pandas/tests/indexes/datetimes/methods/test_astype.py b/pandas/tests/indexes/datetimes/methods/test_astype.py index c0bc6601769b1..81dc3b3ecc45e 100644 --- a/pandas/tests/indexes/datetimes/methods/test_astype.py +++ b/pandas/tests/indexes/datetimes/methods/test_astype.py @@ -3,7 +3,6 @@ import dateutil import numpy as np import pytest -import pytz import pandas as pd from pandas import ( @@ -251,6 +250,8 @@ def _check_rng(rng): _check_rng(rng_utc) def test_index_convert_to_datetime_array_explicit_pytz(self): + pytz = pytest.importorskip("pytz") + def _check_rng(rng): converted = rng.to_pydatetime() assert isinstance(converted, np.ndarray) diff --git a/pandas/tests/indexes/datetimes/methods/test_insert.py b/pandas/tests/indexes/datetimes/methods/test_insert.py index ebfe490e0e067..b3c400b8cf97b 100644 --- a/pandas/tests/indexes/datetimes/methods/test_insert.py +++ b/pandas/tests/indexes/datetimes/methods/test_insert.py @@ -1,8 +1,8 @@ from datetime import datetime +import zoneinfo import numpy as np import pytest -import pytz from pandas import ( NA, @@ -133,49 +133,49 @@ def test_insert3(self, unit): assert result.name == expected.name assert result.freq is None - def test_insert4(self, unit): - for tz in ["US/Pacific", "Asia/Singapore"]: - idx = date_range( - "1/1/2000 09:00", periods=6, freq="h", tz=tz, name="idx", unit=unit - ) - # preserve freq - expected = date_range( - "1/1/2000 09:00", periods=7, freq="h", tz=tz, name="idx", unit=unit - ) - for d in [ - Timestamp("2000-01-01 15:00", tz=tz), - pytz.timezone(tz).localize(datetime(2000, 1, 1, 15)), - ]: - result = idx.insert(6, d) - tm.assert_index_equal(result, expected) - assert result.name == expected.name - assert result.freq == expected.freq - assert result.tz == expected.tz - - expected = DatetimeIndex( - [ - "2000-01-01 09:00", - "2000-01-01 10:00", - "2000-01-01 11:00", - "2000-01-01 12:00", - "2000-01-01 13:00", - "2000-01-01 14:00", - "2000-01-01 10:00", - ], - name="idx", - tz=tz, - freq=None, - ).as_unit(unit) - # reset freq to None - for d in [ - Timestamp("2000-01-01 10:00", tz=tz), - pytz.timezone(tz).localize(datetime(2000, 1, 1, 10)), - ]: - result = idx.insert(6, d) - tm.assert_index_equal(result, expected) - assert result.name == expected.name - assert result.tz == expected.tz - assert result.freq is None + @pytest.mark.parametrize("tz", ["US/Pacific", "Asia/Singapore"]) + def test_insert4(self, unit, tz): + idx = date_range( + "1/1/2000 09:00", periods=6, freq="h", tz=tz, name="idx", unit=unit + ) + # preserve freq + expected = date_range( + "1/1/2000 09:00", periods=7, freq="h", tz=tz, name="idx", unit=unit + ) + for d in [ + Timestamp("2000-01-01 15:00", tz=tz), + datetime(2000, 1, 1, 15).astimezone(zoneinfo.ZoneInfo(tz)), + ]: + result = idx.insert(6, d) + tm.assert_index_equal(result, expected) + assert result.name == expected.name + assert result.freq == expected.freq + assert result.tz == expected.tz + + expected = DatetimeIndex( + [ + "2000-01-01 09:00", + "2000-01-01 10:00", + "2000-01-01 11:00", + "2000-01-01 12:00", + "2000-01-01 13:00", + "2000-01-01 14:00", + "2000-01-01 10:00", + ], + name="idx", + tz=tz, + freq=None, + ).as_unit(unit) + # reset freq to None + for d in [ + Timestamp("2000-01-01 10:00", tz=tz), + datetime(2000, 1, 1, 10).astimezone(zoneinfo.ZoneInfo(tz)), + ]: + result = idx.insert(6, d) + tm.assert_index_equal(result, expected) + assert result.name == expected.name + assert result.tz == expected.tz + assert result.freq is None # TODO: also changes DataFrame.__setitem__ with expansion def test_insert_mismatched_tzawareness(self): @@ -214,7 +214,7 @@ def test_insert_mismatched_tz(self): assert expected.dtype == idx.dtype tm.assert_index_equal(result, expected) - item = datetime(2000, 1, 4, tzinfo=pytz.timezone("US/Eastern")) + item = datetime(2000, 1, 4, tzinfo=zoneinfo.ZoneInfo("US/Eastern")) result = idx.insert(3, item) expected = Index( list(idx[:3]) + [item.astimezone(idx.tzinfo)] + list(idx[3:]), diff --git a/pandas/tests/indexes/datetimes/methods/test_shift.py b/pandas/tests/indexes/datetimes/methods/test_shift.py index d8bdcc2a17685..e6a2cb668f3ec 100644 --- a/pandas/tests/indexes/datetimes/methods/test_shift.py +++ b/pandas/tests/indexes/datetimes/methods/test_shift.py @@ -1,7 +1,7 @@ from datetime import datetime +import zoneinfo import pytest -import pytz from pandas.errors import NullFrequencyError @@ -13,8 +13,6 @@ ) import pandas._testing as tm -START, END = datetime(2009, 1, 1), datetime(2010, 1, 1) - class TestDatetimeIndexShift: # ------------------------------------------------------------- @@ -122,8 +120,7 @@ def test_dti_shift_across_dst(self, unit): ) def test_dti_shift_near_midnight(self, shift, result_time, unit): # GH 8616 - dt = datetime(2014, 11, 14, 0) - dt_est = pytz.timezone("EST").localize(dt) + dt_est = datetime(2014, 11, 14, 0).astimezone(zoneinfo.ZoneInfo("US/Eastern")) idx = DatetimeIndex([dt_est]).as_unit(unit) ser = Series(data=[1], index=idx) result = ser.shift(shift, freq="h") @@ -133,13 +130,17 @@ def test_dti_shift_near_midnight(self, shift, result_time, unit): def test_shift_periods(self, unit): # GH#22458 : argument 'n' was deprecated in favor of 'periods' - idx = date_range(start=START, end=END, periods=3, unit=unit) + idx = date_range( + start=datetime(2009, 1, 1), end=datetime(2010, 1, 1), periods=3, unit=unit + ) tm.assert_index_equal(idx.shift(periods=0), idx) tm.assert_index_equal(idx.shift(0), idx) @pytest.mark.parametrize("freq", ["B", "C"]) def test_shift_bday(self, freq, unit): - rng = date_range(START, END, freq=freq, unit=unit) + rng = date_range( + datetime(2009, 1, 1), datetime(2010, 1, 1), freq=freq, unit=unit + ) shifted = rng.shift(5) assert shifted[0] == rng[5] assert shifted.freq == rng.freq @@ -153,11 +154,21 @@ def test_shift_bday(self, freq, unit): assert shifted.freq == rng.freq def test_shift_bmonth(self, unit): - rng = date_range(START, END, freq=pd.offsets.BMonthEnd(), unit=unit) + rng = date_range( + datetime(2009, 1, 1), + datetime(2010, 1, 1), + freq=pd.offsets.BMonthEnd(), + unit=unit, + ) shifted = rng.shift(1, freq=pd.offsets.BDay()) assert shifted[0] == rng[0] + pd.offsets.BDay() - rng = date_range(START, END, freq=pd.offsets.BMonthEnd(), unit=unit) + rng = date_range( + datetime(2009, 1, 1), + datetime(2010, 1, 1), + freq=pd.offsets.BMonthEnd(), + unit=unit, + ) with tm.assert_produces_warning(pd.errors.PerformanceWarning): shifted = rng.shift(1, freq=pd.offsets.CDay()) assert shifted[0] == rng[0] + pd.offsets.CDay() diff --git a/pandas/tests/indexes/datetimes/methods/test_to_period.py b/pandas/tests/indexes/datetimes/methods/test_to_period.py index 00c0216a9b3b5..b9d466fc0eed7 100644 --- a/pandas/tests/indexes/datetimes/methods/test_to_period.py +++ b/pandas/tests/indexes/datetimes/methods/test_to_period.py @@ -1,7 +1,8 @@ +from datetime import timezone + import dateutil.tz from dateutil.tz import tzlocal import pytest -import pytz from pandas._libs.tslibs.ccalendar import MONTHS from pandas._libs.tslibs.offsets import MonthEnd @@ -169,7 +170,13 @@ def test_to_period_microsecond(self): @pytest.mark.parametrize( "tz", - ["US/Eastern", pytz.utc, tzlocal(), "dateutil/US/Eastern", dateutil.tz.tzutc()], + [ + "US/Eastern", + timezone.utc, + tzlocal(), + "dateutil/US/Eastern", + dateutil.tz.tzutc(), + ], ) def test_to_period_tz(self, tz): ts = date_range("1/1/2000", "2/1/2000", tz=tz) diff --git a/pandas/tests/indexes/datetimes/methods/test_tz_convert.py b/pandas/tests/indexes/datetimes/methods/test_tz_convert.py index b2cf488ac8313..4755dfa06f300 100644 --- a/pandas/tests/indexes/datetimes/methods/test_tz_convert.py +++ b/pandas/tests/indexes/datetimes/methods/test_tz_convert.py @@ -4,7 +4,6 @@ from dateutil.tz import gettz import numpy as np import pytest -import pytz from pandas._libs.tslibs import timezones @@ -260,11 +259,14 @@ def test_dti_tz_convert_tzlocal(self): [ "US/Eastern", "dateutil/US/Eastern", - pytz.timezone("US/Eastern"), + "pytz/US/Eastern", gettz("US/Eastern"), ], ) def test_dti_tz_convert_utc_to_local_no_modify(self, tz): + if tz.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + tz = pytz.timezone(tz.removeprefix("pytz/")) rng = date_range("3/11/2012", "3/12/2012", freq="h", tz="utc") rng_eastern = rng.tz_convert(tz) diff --git a/pandas/tests/indexes/datetimes/methods/test_tz_localize.py b/pandas/tests/indexes/datetimes/methods/test_tz_localize.py index 2927720487bb6..eb2f4f190dea4 100644 --- a/pandas/tests/indexes/datetimes/methods/test_tz_localize.py +++ b/pandas/tests/indexes/datetimes/methods/test_tz_localize.py @@ -1,6 +1,7 @@ from datetime import ( datetime, timedelta, + timezone, ) from zoneinfo import ZoneInfo @@ -8,23 +9,24 @@ from dateutil.tz import gettz import numpy as np import pytest -import pytz from pandas import ( DatetimeIndex, Timestamp, bdate_range, date_range, + errors, offsets, to_datetime, ) import pandas._testing as tm -@pytest.fixture( - params=[pytz.timezone("US/Eastern"), gettz("US/Eastern"), ZoneInfo("US/Eastern")] -) +@pytest.fixture(params=["pytz/US/Eastern", gettz("US/Eastern"), ZoneInfo("US/Eastern")]) def tz(request): + if isinstance(request.param, str) and request.param.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + return pytz.timezone(request.param.removeprefix("pytz/")) return request.param @@ -67,10 +69,10 @@ def test_dti_tz_localize_nonexistent_raise_coerce(self): times = ["2015-03-08 01:00", "2015-03-08 02:00", "2015-03-08 03:00"] index = DatetimeIndex(times) tz = "US/Eastern" - with pytest.raises(pytz.NonExistentTimeError, match="|".join(times)): + with pytest.raises(errors.NonExistentTimeError, match="|".join(times)): index.tz_localize(tz=tz) - with pytest.raises(pytz.NonExistentTimeError, match="|".join(times)): + with pytest.raises(errors.NonExistentTimeError, match="|".join(times)): index.tz_localize(tz=tz, nonexistent="raise") result = index.tz_localize(tz=tz, nonexistent="NaT") @@ -83,7 +85,7 @@ def test_dti_tz_localize_ambiguous_infer(self, tz): # November 6, 2011, fall back, repeat 2 AM hour # With no repeated hours, we cannot infer the transition dr = date_range(datetime(2011, 11, 6, 0), periods=5, freq=offsets.Hour()) - with pytest.raises(pytz.AmbiguousTimeError, match="Cannot infer dst time"): + with pytest.raises(errors.AmbiguousTimeError, match="Cannot infer dst time"): dr.tz_localize(tz) def test_dti_tz_localize_ambiguous_infer2(self, tz, unit): @@ -115,7 +117,7 @@ def test_dti_tz_localize_ambiguous_infer3(self, tz): def test_dti_tz_localize_ambiguous_times(self, tz): # March 13, 2011, spring forward, skip from 2 AM to 3 AM dr = date_range(datetime(2011, 3, 13, 1, 30), periods=3, freq=offsets.Hour()) - with pytest.raises(pytz.NonExistentTimeError, match="2011-03-13 02:30:00"): + with pytest.raises(errors.NonExistentTimeError, match="2011-03-13 02:30:00"): dr.tz_localize(tz) # after dst transition, it works @@ -125,12 +127,12 @@ def test_dti_tz_localize_ambiguous_times(self, tz): # November 6, 2011, fall back, repeat 2 AM hour dr = date_range(datetime(2011, 11, 6, 1, 30), periods=3, freq=offsets.Hour()) - with pytest.raises(pytz.AmbiguousTimeError, match="Cannot infer dst time"): + with pytest.raises(errors.AmbiguousTimeError, match="Cannot infer dst time"): dr.tz_localize(tz) # UTC is OK dr = date_range( - datetime(2011, 3, 13), periods=48, freq=offsets.Minute(30), tz=pytz.utc + datetime(2011, 3, 13), periods=48, freq=offsets.Minute(30), tz=timezone.utc ) @pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"]) @@ -161,22 +163,13 @@ def test_dti_tz_localize(self, prefix): tm.assert_numpy_array_equal(dti3.values, dti_utc.values) dti = date_range(start="11/6/2011 1:59", end="11/6/2011 2:00", freq="ms") - with pytest.raises(pytz.AmbiguousTimeError, match="Cannot infer dst time"): + with pytest.raises(errors.AmbiguousTimeError, match="Cannot infer dst time"): dti.tz_localize(tzstr) dti = date_range(start="3/13/2011 1:59", end="3/13/2011 2:00", freq="ms") - with pytest.raises(pytz.NonExistentTimeError, match="2011-03-13 02:00:00"): + with pytest.raises(errors.NonExistentTimeError, match="2011-03-13 02:00:00"): dti.tz_localize(tzstr) - @pytest.mark.parametrize( - "tz", - [ - "US/Eastern", - "dateutil/US/Eastern", - pytz.timezone("US/Eastern"), - gettz("US/Eastern"), - ], - ) def test_dti_tz_localize_utc_conversion(self, tz): # Localizing to time zone should: # 1) check for DST ambiguities @@ -191,7 +184,7 @@ def test_dti_tz_localize_utc_conversion(self, tz): # DST ambiguity, this should fail rng = date_range("3/11/2012", "3/12/2012", freq="30min") # Is this really how it should fail?? - with pytest.raises(pytz.NonExistentTimeError, match="2012-03-11 02:00:00"): + with pytest.raises(errors.NonExistentTimeError, match="2012-03-11 02:00:00"): rng.tz_localize(tz) def test_dti_tz_localize_roundtrip(self, tz_aware_fixture): @@ -316,8 +309,8 @@ def test_dti_tz_localize_ambiguous_flags2(self, tz): def test_dti_tz_localize_bdate_range(self): dr = bdate_range("1/1/2009", "1/1/2010") - dr_utc = bdate_range("1/1/2009", "1/1/2010", tz=pytz.utc) - localized = dr.tz_localize(pytz.utc) + dr_utc = bdate_range("1/1/2009", "1/1/2010", tz=timezone.utc) + localized = dr.tz_localize(timezone.utc) tm.assert_index_equal(dr_utc, localized) @pytest.mark.parametrize( diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index 7fdf17c797213..a82c58acdf8d3 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -7,13 +7,13 @@ ) from functools import partial from operator import attrgetter +import zoneinfo import dateutil import dateutil.tz from dateutil.tz import gettz import numpy as np import pytest -import pytz from pandas._libs.tslibs import ( OutOfBoundsDatetime, @@ -27,6 +27,7 @@ Index, Timestamp, date_range, + errors, offsets, to_datetime, ) @@ -757,14 +758,13 @@ def test_disallow_setting_tz(self): dti = DatetimeIndex(["2010"], tz="UTC") msg = "Cannot directly set timezone" with pytest.raises(AttributeError, match=msg): - dti.tz = pytz.timezone("US/Pacific") + dti.tz = zoneinfo.ZoneInfo("US/Pacific") @pytest.mark.parametrize( "tz", [ None, "America/Los_Angeles", - pytz.timezone("America/Los_Angeles"), Timestamp("2000", tz="America/Los_Angeles").tz, ], ) @@ -779,13 +779,15 @@ def test_constructor_start_end_with_tz(self, tz): freq="D", ) tm.assert_index_equal(result, expected) - # Especially assert that the timezone is consistent for pytz - assert pytz.timezone("America/Los_Angeles") is result.tz + # Especially assert that the timezone is consistent + assert zoneinfo.ZoneInfo("America/Los_Angeles") is result.tz @pytest.mark.parametrize("tz", ["US/Pacific", "US/Eastern", "Asia/Tokyo"]) def test_constructor_with_non_normalized_pytz(self, tz): # GH 18595 - non_norm_tz = Timestamp("2010", tz=tz).tz + pytz = pytest.importorskip("pytz") + tz_in = pytz.timezone(tz) + non_norm_tz = Timestamp("2010", tz=tz_in).tz result = DatetimeIndex(["2010"], tz=non_norm_tz) assert pytz.timezone(tz) is result.tz @@ -933,7 +935,9 @@ def test_index_constructor_with_numpy_object_array_and_timestamp_tz_with_nan(sel expected = DatetimeIndex([Timestamp("2019", tz="UTC"), pd.NaT]) tm.assert_index_equal(result, expected) - @pytest.mark.parametrize("tz", [pytz.timezone("US/Eastern"), gettz("US/Eastern")]) + @pytest.mark.parametrize( + "tz", [zoneinfo.ZoneInfo("US/Eastern"), gettz("US/Eastern")] + ) def test_dti_from_tzaware_datetime(self, tz): d = [datetime(2012, 8, 19, tzinfo=tz)] @@ -979,7 +983,7 @@ def test_dti_convert_datetime_list(self, tzstr): @pytest.mark.parametrize( "tz", [ - pytz.timezone("US/Eastern"), + "pytz/US/Eastern", gettz("US/Eastern"), ], ) @@ -988,6 +992,9 @@ def test_dti_convert_datetime_list(self, tzstr): def test_dti_ambiguous_matches_timestamp(self, tz, use_str, box_cls, request): # GH#47471 check that we get the same raising behavior in the DTI # constructor and Timestamp constructor + if isinstance(tz, str) and tz.startswith("pytz/"): + pytz = pytest.importorskip(tz) + tz = pytz.timezone(tz.removeprefix("pytz/")) dtstr = "2013-11-03 01:59:59.999999" item = dtstr if not use_str: @@ -1003,7 +1010,7 @@ def test_dti_ambiguous_matches_timestamp(self, tz, use_str, box_cls, request): mark = pytest.mark.xfail(reason="We implicitly get fold=0.") request.applymarker(mark) - with pytest.raises(pytz.AmbiguousTimeError, match=dtstr): + with pytest.raises(errors.AmbiguousTimeError, match=dtstr): box_cls(item, tz=tz) @pytest.mark.parametrize("tz", [None, "UTC", "US/Pacific"]) diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index e26f35f4e8258..a1431d2ef1dd9 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -11,8 +11,6 @@ import numpy as np import pytest -import pytz -from pytz import timezone from pandas._libs.tslibs import timezones from pandas._libs.tslibs.offsets import ( @@ -34,6 +32,7 @@ Timestamp, bdate_range, date_range, + errors, offsets, ) import pandas._testing as tm @@ -97,6 +96,7 @@ def test_date_range_timestamp_equiv_dateutil(self): assert ts == stamp def test_date_range_timestamp_equiv_explicit_pytz(self): + pytz = pytest.importorskip("pytz") rng = date_range("20090415", "20090519", tz=pytz.timezone("US/Eastern")) stamp = rng[0] @@ -504,7 +504,8 @@ def test_range_bug(self, unit): def test_range_tz_pytz(self): # see gh-2906 - tz = timezone("US/Eastern") + pytz = pytest.importorskip("pytz") + tz = pytz.timezone("US/Eastern") start = tz.localize(datetime(2011, 1, 1)) end = tz.localize(datetime(2011, 1, 3)) @@ -531,14 +532,16 @@ def test_range_tz_pytz(self): ], ) def test_range_tz_dst_straddle_pytz(self, start, end): - start = Timestamp(start, tz="US/Eastern") - end = Timestamp(end, tz="US/Eastern") + pytz = pytest.importorskip("pytz") + tz = pytz.timezone("US/Eastern") + start = Timestamp(start, tz=tz) + end = Timestamp(end, tz=tz) dr = date_range(start, end, freq="D") assert dr[0] == start assert dr[-1] == end assert np.all(dr.hour == 0) - dr = date_range(start, end, freq="D", tz="US/Eastern") + dr = date_range(start, end, freq="D", tz=tz) assert dr[0] == start assert dr[-1] == end assert np.all(dr.hour == 0) @@ -547,7 +550,7 @@ def test_range_tz_dst_straddle_pytz(self, start, end): start.replace(tzinfo=None), end.replace(tzinfo=None), freq="D", - tz="US/Eastern", + tz=tz, ) assert dr[0] == start assert dr[-1] == end @@ -906,7 +909,7 @@ def test_date_range_ambiguous_endpoint(self, tz): # construction with an ambiguous end-point # GH#11626 - with pytest.raises(pytz.AmbiguousTimeError, match="Cannot infer dst time"): + with pytest.raises(errors.AmbiguousTimeError, match="Cannot infer dst time"): date_range( "2013-10-26 23:00", "2013-10-27 01:00", tz="Europe/London", freq="h" ) @@ -930,7 +933,7 @@ def test_date_range_ambiguous_endpoint(self, tz): def test_date_range_nonexistent_endpoint(self, tz, option, expected): # construction with an nonexistent end-point - with pytest.raises(pytz.NonExistentTimeError, match="2019-03-10 02:00:00"): + with pytest.raises(errors.NonExistentTimeError, match="2019-03-10 02:00:00"): date_range( "2019-03-10 00:00", "2019-03-10 02:00", tz="US/Pacific", freq="h" ) diff --git a/pandas/tests/indexes/datetimes/test_formats.py b/pandas/tests/indexes/datetimes/test_formats.py index f8b01f7985535..f158e31d02367 100644 --- a/pandas/tests/indexes/datetimes/test_formats.py +++ b/pandas/tests/indexes/datetimes/test_formats.py @@ -1,9 +1,11 @@ -from datetime import datetime +from datetime import ( + datetime, + timezone, +) import dateutil.tz import numpy as np import pytest -import pytz import pandas as pd from pandas import ( @@ -276,7 +278,7 @@ def test_dti_summary(self): result = idx._summary() assert result == expected - @pytest.mark.parametrize("tz", [None, pytz.utc, dateutil.tz.tzutc()]) + @pytest.mark.parametrize("tz", [None, timezone.utc, dateutil.tz.tzutc()]) @pytest.mark.parametrize("freq", ["B", "C"]) def test_dti_business_repr_etc_smoke(self, tz, freq): # only really care that it works diff --git a/pandas/tests/indexes/datetimes/test_setops.py b/pandas/tests/indexes/datetimes/test_setops.py index fc3a1d4721841..5290fb0c80b84 100644 --- a/pandas/tests/indexes/datetimes/test_setops.py +++ b/pandas/tests/indexes/datetimes/test_setops.py @@ -1,11 +1,11 @@ from datetime import ( datetime, + timedelta, timezone, ) import numpy as np import pytest -import pytz import pandas.util._test_decorators as td @@ -560,6 +560,7 @@ def test_intersection_list(self): tm.assert_index_equal(res, idx) def test_month_range_union_tz_pytz(self, sort): + pytz = pytest.importorskip("pytz") tz = pytz.timezone("US/Eastern") early_start = datetime(2011, 1, 1) @@ -648,7 +649,7 @@ def test_intersection_bug(self): assert result.freq == b.freq @pytest.mark.parametrize( - "tz", [None, "UTC", "Europe/Berlin", pytz.FixedOffset(-60)] + "tz", [None, "UTC", "Europe/Berlin", timezone(timedelta(hours=-1))] ) def test_intersection_dst_transition(self, tz): # GH 46702: Europe/Berlin has DST transition diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index daa5b346eb4ec..21ab5b0390359 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -7,11 +7,11 @@ timezone, tzinfo, ) +import zoneinfo from dateutil.tz import gettz import numpy as np import pytest -import pytz from pandas._libs.tslibs import ( conversion, @@ -205,15 +205,17 @@ def test_utc_box_timestamp_and_localize(self, tzstr): rng_eastern[0].tzinfo ) - @pytest.mark.parametrize("tz", [pytz.timezone("US/Central"), gettz("US/Central")]) + @pytest.mark.parametrize( + "tz", [zoneinfo.ZoneInfo("US/Central"), gettz("US/Central")] + ) def test_with_tz(self, tz): # just want it to work - start = datetime(2011, 3, 12, tzinfo=pytz.utc) + start = datetime(2011, 3, 12, tzinfo=timezone.utc) dr = bdate_range(start, periods=50, freq=pd.offsets.Hour()) - assert dr.tz is pytz.utc + assert dr.tz is timezone.utc # DateRange with naive datetimes - dr = bdate_range("1/1/2005", "1/1/2009", tz=pytz.utc) + dr = bdate_range("1/1/2005", "1/1/2009", tz=timezone.utc) dr = bdate_range("1/1/2005", "1/1/2009", tz=tz) # normalized @@ -230,13 +232,16 @@ def test_with_tz(self, tz): # datetimes with tzinfo set dr = bdate_range( - datetime(2005, 1, 1, tzinfo=pytz.utc), datetime(2009, 1, 1, tzinfo=pytz.utc) + datetime(2005, 1, 1, tzinfo=timezone.utc), + datetime(2009, 1, 1, tzinfo=timezone.utc), ) msg = "Start and end cannot both be tz-aware with different timezones" with pytest.raises(Exception, match=msg): - bdate_range(datetime(2005, 1, 1, tzinfo=pytz.utc), "1/1/2009", tz=tz) + bdate_range(datetime(2005, 1, 1, tzinfo=timezone.utc), "1/1/2009", tz=tz) - @pytest.mark.parametrize("tz", [pytz.timezone("US/Eastern"), gettz("US/Eastern")]) + @pytest.mark.parametrize( + "tz", [zoneinfo.ZoneInfo("US/Eastern"), gettz("US/Eastern")] + ) def test_dti_convert_tz_aware_datetime_datetime(self, tz): # GH#1581 dates = [datetime(2000, 1, 1), datetime(2000, 1, 2), datetime(2000, 1, 3)] diff --git a/pandas/tests/indexes/multi/test_reshape.py b/pandas/tests/indexes/multi/test_reshape.py index 06dbb33aadf97..2a0749ec3385b 100644 --- a/pandas/tests/indexes/multi/test_reshape.py +++ b/pandas/tests/indexes/multi/test_reshape.py @@ -1,8 +1,8 @@ from datetime import datetime +import zoneinfo import numpy as np import pytest -import pytz import pandas as pd from pandas import ( @@ -114,11 +114,11 @@ def test_append_index(): result = idx1.append(midx_lv2) # see gh-7112 - tz = pytz.timezone("Asia/Tokyo") + tz = zoneinfo.ZoneInfo("Asia/Tokyo") expected_tuples = [ - (1.1, tz.localize(datetime(2011, 1, 1))), - (1.2, tz.localize(datetime(2011, 1, 2))), - (1.3, tz.localize(datetime(2011, 1, 3))), + (1.1, datetime(2011, 1, 1).astimezone(tz)), + (1.2, datetime(2011, 1, 2).astimezone(tz)), + (1.3, datetime(2011, 1, 3).astimezone(tz)), ] expected = Index([1.1, 1.2, 1.3] + expected_tuples) tm.assert_index_equal(result, expected) diff --git a/pandas/tests/io/json/test_ujson.py b/pandas/tests/io/json/test_ujson.py index ce7bb74240c53..36f216c2dd3c0 100644 --- a/pandas/tests/io/json/test_ujson.py +++ b/pandas/tests/io/json/test_ujson.py @@ -10,7 +10,6 @@ import dateutil import numpy as np import pytest -import pytz import pandas._libs.json as ujson from pandas.compat import IS64 @@ -370,6 +369,7 @@ def test_encode_time_conversion_basic(self, test): def test_encode_time_conversion_pytz(self): # see gh-11473: to_json segfaults with timezone-aware datetimes + pytz = pytest.importorskip("pytz") test = datetime.time(10, 12, 15, 343243, pytz.utc) output = ujson.ujson_dumps(test) expected = f'"{test.isoformat()}"' diff --git a/pandas/tests/io/parser/test_parse_dates.py b/pandas/tests/io/parser/test_parse_dates.py index f02dd997e62d0..18710eae3738d 100644 --- a/pandas/tests/io/parser/test_parse_dates.py +++ b/pandas/tests/io/parser/test_parse_dates.py @@ -14,7 +14,6 @@ from dateutil.parser import parse as du_parse import numpy as np import pytest -import pytz from pandas._libs.tslibs import parsing @@ -1019,6 +1018,7 @@ def test_parse_tz_aware(all_parsers): {"x": [0.5]}, index=Index([Timestamp("2012-06-13 01:39:00+00:00")], name="Date") ) if parser.engine == "pyarrow": + pytz = pytest.importorskip("pytz") expected_tz = pytz.utc else: expected_tz = timezone.utc diff --git a/pandas/tests/resample/test_datetime_index.py b/pandas/tests/resample/test_datetime_index.py index dfcdc2ce26bcf..17781e9fe2735 100644 --- a/pandas/tests/resample/test_datetime_index.py +++ b/pandas/tests/resample/test_datetime_index.py @@ -1,9 +1,9 @@ from datetime import datetime from functools import partial +import zoneinfo import numpy as np import pytest -import pytz from pandas._libs import lib from pandas._typing import DatetimeNaTType @@ -1715,7 +1715,7 @@ def test_resample_dst_anchor2(unit): def test_downsample_across_dst(unit): # GH 8531 - tz = pytz.timezone("Europe/Berlin") + tz = zoneinfo.ZoneInfo("Europe/Berlin") dt = datetime(2014, 10, 26) dates = date_range(tz.localize(dt), periods=4, freq="2h").as_unit(unit) result = Series(5, index=dates).resample("h").mean() diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index 7b3edbad6666b..868730a6b30b6 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -1,10 +1,12 @@ -from datetime import datetime +from datetime import ( + datetime, + timezone, +) import warnings import dateutil import numpy as np import pytest -import pytz from pandas._libs.tslibs.ccalendar import ( DAYS, @@ -311,7 +313,7 @@ def test_resample_incompat_freq(self): @pytest.mark.parametrize( "tz", [ - pytz.timezone("America/Los_Angeles"), + "America/Los_Angeles", dateutil.tz.gettz("America/Los_Angeles"), ], ) @@ -319,9 +321,13 @@ def test_with_local_timezone(self, tz): # see gh-5430 local_timezone = tz - start = datetime(year=2013, month=11, day=1, hour=0, minute=0, tzinfo=pytz.utc) + start = datetime( + year=2013, month=11, day=1, hour=0, minute=0, tzinfo=timezone.utc + ) # 1 day later - end = datetime(year=2013, month=11, day=2, hour=0, minute=0, tzinfo=pytz.utc) + end = datetime( + year=2013, month=11, day=2, hour=0, minute=0, tzinfo=timezone.utc + ) index = date_range(start, end, freq="h", name="idx") @@ -343,7 +349,7 @@ def test_with_local_timezone(self, tz): @pytest.mark.parametrize( "tz", [ - pytz.timezone("America/Los_Angeles"), + "America/Los_Angeles", dateutil.tz.gettz("America/Los_Angeles"), ], ) @@ -360,7 +366,7 @@ def test_resample_with_tz(self, tz, unit): index=exp_dti, ) tm.assert_series_equal(result, expected) - # Especially assert that the timezone is LMT for pytz + # Especially assert that the timezone is LMT assert result.index.tz == tz def test_resample_nonexistent_time_bin_edge(self): diff --git a/pandas/tests/reshape/merge/test_merge_asof.py b/pandas/tests/reshape/merge/test_merge_asof.py index 4fc57c14ec4c3..bd364de26a3c4 100644 --- a/pandas/tests/reshape/merge/test_merge_asof.py +++ b/pandas/tests/reshape/merge/test_merge_asof.py @@ -2,7 +2,6 @@ import numpy as np import pytest -import pytz import pandas.util._test_decorators as td @@ -2071,7 +2070,7 @@ def test_tolerance_tz(self, unit): start=to_datetime("2016-01-02"), freq="D", periods=5, - tz=pytz.timezone("UTC"), + tz=datetime.timezone.utc, unit=unit, ), "value1": np.arange(5), @@ -2083,7 +2082,7 @@ def test_tolerance_tz(self, unit): start=to_datetime("2016-01-01"), freq="D", periods=5, - tz=pytz.timezone("UTC"), + tz=datetime.timezone.utc, unit=unit, ), "value2": list("ABCDE"), @@ -2097,7 +2096,7 @@ def test_tolerance_tz(self, unit): start=to_datetime("2016-01-02"), freq="D", periods=5, - tz=pytz.timezone("UTC"), + tz=datetime.timezone.utc, unit=unit, ), "value1": np.arange(5), diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py index e352e2601cef3..b52affa21289a 100644 --- a/pandas/tests/scalar/test_nat.py +++ b/pandas/tests/scalar/test_nat.py @@ -3,10 +3,10 @@ timedelta, ) import operator +import zoneinfo import numpy as np import pytest -import pytz from pandas._libs.tslibs import iNaT from pandas.compat.numpy import np_version_gte1p24p3 @@ -361,7 +361,7 @@ def test_nat_doc_strings(compare): (Timestamp("2014-01-01"), "timestamp"), (Timestamp("2014-01-01", tz="UTC"), "timestamp"), (Timestamp("2014-01-01", tz="US/Eastern"), "timestamp"), - (pytz.timezone("Asia/Tokyo").localize(datetime(2014, 1, 1)), "timestamp"), + (datetime(2014, 1, 1).astimezone(zoneinfo.ZoneInfo("Asia/Tokyo")), "timestamp"), ], ) def test_nat_arithmetic_scalar(op_name, value, val_type): diff --git a/pandas/tests/scalar/timestamp/methods/test_replace.py b/pandas/tests/scalar/timestamp/methods/test_replace.py index d67de79a8dd10..69abfce362245 100644 --- a/pandas/tests/scalar/timestamp/methods/test_replace.py +++ b/pandas/tests/scalar/timestamp/methods/test_replace.py @@ -1,9 +1,9 @@ from datetime import datetime +import zoneinfo from dateutil.tz import gettz import numpy as np import pytest -import pytz from pandas._libs.tslibs import ( OutOfBoundsDatetime, @@ -108,8 +108,8 @@ def test_replace_tzinfo_equiv_tz_localize_none(self): @td.skip_if_windows def test_replace_tzinfo(self): # GH#15683 - dt = datetime(2016, 3, 27, 1) - tzinfo = pytz.timezone("CET").localize(dt, is_dst=False).tzinfo + dt = datetime(2016, 3, 27, 1, fold=1) + tzinfo = dt.astimezone(zoneinfo.ZoneInfo("Europe/Berlin")).tzinfo result_dt = dt.replace(tzinfo=tzinfo) result_pd = Timestamp(dt).replace(tzinfo=tzinfo) @@ -134,13 +134,16 @@ def test_replace_tzinfo(self): @pytest.mark.parametrize( "tz, normalize", [ - (pytz.timezone("US/Eastern"), lambda x: x.tzinfo.normalize(x)), + ("pytz/US/Eastern", lambda x: x.tzinfo.normalize(x)), (gettz("US/Eastern"), lambda x: x), ], ) def test_replace_across_dst(self, tz, normalize): # GH#18319 check that 1) timezone is correctly normalized and # 2) that hour is not incorrectly changed by this normalization + if isinstance(tz, str) and tz.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + tz = pytz.timezone(tz.removeprefix("pytz/")) ts_naive = Timestamp("2017-12-03 16:03:30") ts_aware = conversion.localize_pydatetime(ts_naive, tz) diff --git a/pandas/tests/scalar/timestamp/methods/test_round.py b/pandas/tests/scalar/timestamp/methods/test_round.py index 2fb0e1a8d3397..297ff61fa15fb 100644 --- a/pandas/tests/scalar/timestamp/methods/test_round.py +++ b/pandas/tests/scalar/timestamp/methods/test_round.py @@ -4,7 +4,6 @@ ) import numpy as np import pytest -import pytz from pandas._libs import lib from pandas._libs.tslibs import ( @@ -18,6 +17,7 @@ from pandas._libs.tslibs.dtypes import NpyDatetimeUnit from pandas._libs.tslibs.period import INVALID_FREQ_ERR_MSG +from pandas import errors import pandas._testing as tm @@ -182,7 +182,7 @@ def test_round_dst_border_ambiguous(self, method, unit): assert result is NaT msg = "Cannot infer dst time" - with pytest.raises(pytz.AmbiguousTimeError, match=msg): + with pytest.raises(errors.AmbiguousTimeError, match=msg): getattr(ts, method)("h", ambiguous="raise") @pytest.mark.parametrize( @@ -205,7 +205,7 @@ def test_round_dst_border_nonexistent(self, method, ts_str, freq, unit): assert result is NaT msg = "2018-03-11 02:00:00" - with pytest.raises(pytz.NonExistentTimeError, match=msg): + with pytest.raises(errors.NonExistentTimeError, match=msg): getattr(ts, method)(freq, nonexistent="raise") @pytest.mark.parametrize( diff --git a/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py b/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py index 67985bd4ba566..6465cb116774f 100644 --- a/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py +++ b/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py @@ -1,7 +1,7 @@ # NB: This is for the Timestamp.timestamp *method* specifically, not # the Timestamp class in general. -from pytz import utc +from datetime.timezone import utc from pandas._libs.tslibs import Timestamp import pandas.util._test_decorators as td diff --git a/pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py b/pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py index 57f57e56201c8..d6fd3d4c782dd 100644 --- a/pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py +++ b/pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py @@ -3,7 +3,7 @@ timedelta, ) -import pytz +import pytest from pandas._libs.tslibs.timezones import dateutil_gettz as gettz import pandas.util._test_decorators as td @@ -42,6 +42,7 @@ def test_timestamp_to_pydatetime_dateutil(self): assert stamp.tzinfo == dtval.tzinfo def test_timestamp_to_pydatetime_explicit_pytz(self): + pytz = pytest.importorskip("pytz") stamp = Timestamp("20090415", tz=pytz.timezone("US/Eastern")) dtval = stamp.to_pydatetime() assert stamp == dtval diff --git a/pandas/tests/scalar/timestamp/methods/test_tz_localize.py b/pandas/tests/scalar/timestamp/methods/test_tz_localize.py index ce92a2feff30b..6b1f53337d2ef 100644 --- a/pandas/tests/scalar/timestamp/methods/test_tz_localize.py +++ b/pandas/tests/scalar/timestamp/methods/test_tz_localize.py @@ -1,18 +1,16 @@ from datetime import timedelta import re -from zoneinfo import ZoneInfo from dateutil.tz import gettz import pytest -import pytz -from pytz.exceptions import ( + +from pandas._libs.tslibs.dtypes import NpyDatetimeUnit +from pandas.errors import ( AmbiguousTimeError, NonExistentTimeError, + OutOfBoundsDatetime, ) -from pandas._libs.tslibs.dtypes import NpyDatetimeUnit -from pandas.errors import OutOfBoundsDatetime - from pandas import ( NaT, Timestamp, @@ -45,28 +43,28 @@ def test_tz_localize_pushes_out_of_bounds(self): with pytest.raises(OutOfBoundsDatetime, match=msg): Timestamp.max.tz_localize("US/Pacific") - def test_tz_localize_ambiguous_bool(self, unit): + @pytest.mark.parameterize( + "tz", ["US/Central", "dateutil/US/Central", "pytz/US/Central"] + ) + def test_tz_localize_ambiguous_bool(self, unit, tz): # make sure that we are correctly accepting bool values as ambiguous # GH#14402 + if isinstance(tz, str) and tz.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + tz = pytz.timezone(tz.removeprefix("pytz/")) ts = Timestamp("2015-11-01 01:00:03").as_unit(unit) - expected0 = Timestamp("2015-11-01 01:00:03-0500", tz="US/Central") - expected1 = Timestamp("2015-11-01 01:00:03-0600", tz="US/Central") + expected0 = Timestamp("2015-11-01 01:00:03-0500", tz=tz) + expected1 = Timestamp("2015-11-01 01:00:03-0600", tz=tz) msg = "Cannot infer dst time from 2015-11-01 01:00:03" - with pytest.raises(pytz.AmbiguousTimeError, match=msg): - ts.tz_localize("US/Central") - - with pytest.raises(pytz.AmbiguousTimeError, match=msg): - ts.tz_localize("dateutil/US/Central") - - with pytest.raises(pytz.AmbiguousTimeError, match=msg): - ts.tz_localize(ZoneInfo("US/Central")) + with pytest.raises(AmbiguousTimeError, match=msg): + ts.tz_localize(tz) - result = ts.tz_localize("US/Central", ambiguous=True) + result = ts.tz_localize(tz, ambiguous=True) assert result == expected0 assert result._creso == getattr(NpyDatetimeUnit, f"NPY_FR_{unit}").value - result = ts.tz_localize("US/Central", ambiguous=False) + result = ts.tz_localize(tz, ambiguous=False) assert result == expected1 assert result._creso == getattr(NpyDatetimeUnit, f"NPY_FR_{unit}").value @@ -193,9 +191,10 @@ def test_tz_localize_roundtrip(self, stamp, tz_aware_fixture): def test_tz_localize_ambiguous_compat(self): # validate that pytz and dateutil are compat for dst # when the transition happens + pytz = pytest.importorskip("pytz") naive = Timestamp("2013-10-27 01:00:00") - pytz_zone = "Europe/London" + pytz_zone = pytz.timezone("Europe/London") dateutil_zone = "dateutil/Europe/London" result_pytz = naive.tz_localize(pytz_zone, ambiguous=False) result_dateutil = naive.tz_localize(dateutil_zone, ambiguous=False) @@ -224,13 +223,16 @@ def test_tz_localize_ambiguous_compat(self): @pytest.mark.parametrize( "tz", [ - pytz.timezone("US/Eastern"), + "pytz/US/Eastern", gettz("US/Eastern"), "US/Eastern", "dateutil/US/Eastern", ], ) def test_timestamp_tz_localize(self, tz): + if isinstance(tz, str) and tz.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + tz = pytz.timezone(tz.removeprefix("pytz/")) stamp = Timestamp("3/11/2012 04:00") result = stamp.tz_localize(tz) @@ -325,7 +327,7 @@ def test_timestamp_tz_localize_nonexistent_raise(self, warsaw, unit): tz = warsaw ts = Timestamp("2015-03-29 02:20:00").as_unit(unit) msg = "2015-03-29 02:20:00" - with pytest.raises(pytz.NonExistentTimeError, match=msg): + with pytest.raises(NonExistentTimeError, match=msg): ts.tz_localize(tz, nonexistent="raise") msg = ( "The nonexistent argument must be one of 'raise', 'NaT', " diff --git a/pandas/tests/scalar/timestamp/test_arithmetic.py b/pandas/tests/scalar/timestamp/test_arithmetic.py index 2d58513989a66..6baceee5e9a97 100644 --- a/pandas/tests/scalar/timestamp/test_arithmetic.py +++ b/pandas/tests/scalar/timestamp/test_arithmetic.py @@ -7,7 +7,6 @@ from dateutil.tz import gettz import numpy as np import pytest -import pytz from pandas._libs.tslibs import ( OutOfBoundsDatetime, @@ -294,7 +293,7 @@ def test_subtract_different_utc_objects(self, utc_fixture, utc_fixture2): @pytest.mark.parametrize( "tz", [ - pytz.timezone("US/Eastern"), + "pytz/US/Eastern", gettz("US/Eastern"), "US/Eastern", "dateutil/US/Eastern", @@ -302,6 +301,9 @@ def test_subtract_different_utc_objects(self, utc_fixture, utc_fixture2): ) def test_timestamp_add_timedelta_push_over_dst_boundary(self, tz): # GH#1389 + if isinstance(tz, str) and tz.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + tz = pytz.timezone(tz.removeprefix("pytz/")) # 4 hours before DST transition stamp = Timestamp("3/10/2012 22:00", tz=tz) diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py index 916e923a210d6..b21af063c8d5d 100644 --- a/pandas/tests/scalar/timestamp/test_constructors.py +++ b/pandas/tests/scalar/timestamp/test_constructors.py @@ -15,11 +15,14 @@ ) import numpy as np import pytest -import pytz from pandas._libs.tslibs.dtypes import NpyDatetimeUnit from pandas.compat import PY310 -from pandas.errors import OutOfBoundsDatetime +from pandas.errors import ( + AmbiguousTimeError, + NonExistentTimeError, + OutOfBoundsDatetime, +) from pandas import ( NA, @@ -123,6 +126,7 @@ def test_timestamp_constructor_pytz_fold_raise(self): # Test for GH#25057 # pytz doesn't support fold. Check that we raise # if fold is passed with pytz + pytz = pytest.importorskip("pytz") msg = "pytz timezones do not support fold. Please use dateutil timezones." tz = pytz.timezone("Europe/London") with pytest.raises(ValueError, match=msg): @@ -164,7 +168,8 @@ def test_timestamp_constructor_retain_fold(self, tz, fold): "tz", [ "dateutil/Europe/London", - zoneinfo.ZoneInfo("Europe/London"), + "Europe/London", + "pytz/Europe/London", ], ) @pytest.mark.parametrize( @@ -182,6 +187,9 @@ def test_timestamp_constructor_infer_fold_from_value(self, tz, ts_input, fold_ou # Test for GH#25057 # Check that we infer fold correctly based on timestamps since utc # or strings + if isinstance(tz, str) and tz.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + tz = pytz.timezone(tz.removeprefix("pytz/")) ts = Timestamp(ts_input, tz=tz) result = ts.fold expected = fold_out @@ -563,11 +571,11 @@ def test_constructor(self): timezones = [ (None, 0), ("UTC", 0), - (pytz.utc, 0), + (timezone.utc, 0), ("Asia/Tokyo", 9), ("US/Eastern", -4), ("dateutil/US/Pacific", -7), - (pytz.FixedOffset(-180), -3), + (timezone(timedelta(hours=-3)), -3), (dateutil.tz.tzoffset(None, 18000), 5), ] @@ -621,11 +629,11 @@ def test_constructor_with_stringoffset(self): timezones = [ (None, 0), ("UTC", 0), - (pytz.utc, 0), + (timezone.utc, 0), ("Asia/Tokyo", 9), ("US/Eastern", -4), ("dateutil/US/Pacific", -7), - (pytz.FixedOffset(-180), -3), + (timezone(timedelta(hours=-3)), -3), (dateutil.tz.tzoffset(None, 18000), 5), ] @@ -705,7 +713,7 @@ def test_constructor_invalid_tz(self): msg = "at most one of" with pytest.raises(ValueError, match=msg): - Timestamp("2017-10-22", tzinfo=pytz.utc, tz="UTC") + Timestamp("2017-10-22", tzinfo=timezone.utc, tz="UTC") msg = "Cannot pass a date attribute keyword argument when passing a date string" with pytest.raises(ValueError, match=msg): @@ -718,11 +726,11 @@ def test_constructor_tz_or_tzinfo(self): # GH#17943, GH#17690, GH#5168 stamps = [ Timestamp(year=2017, month=10, day=22, tz="UTC"), - Timestamp(year=2017, month=10, day=22, tzinfo=pytz.utc), - Timestamp(year=2017, month=10, day=22, tz=pytz.utc), - Timestamp(datetime(2017, 10, 22), tzinfo=pytz.utc), + Timestamp(year=2017, month=10, day=22, tzinfo=timezone.utc), + Timestamp(year=2017, month=10, day=22, tz=timezone.utc), + Timestamp(datetime(2017, 10, 22), tzinfo=timezone.utc), Timestamp(datetime(2017, 10, 22), tz="UTC"), - Timestamp(datetime(2017, 10, 22), tz=pytz.utc), + Timestamp(datetime(2017, 10, 22), tz=timezone.utc), ] assert all(ts == stamps[0] for ts in stamps) @@ -752,7 +760,7 @@ def test_constructor_tz_or_tzinfo(self): tz="UTC", ), Timestamp(2000, 1, 2, 3, 4, 5, 6, None, nanosecond=1), - Timestamp(2000, 1, 2, 3, 4, 5, 6, tz=pytz.UTC, nanosecond=1), + Timestamp(2000, 1, 2, 3, 4, 5, 6, tz=timezone.utc, nanosecond=1), ], ) def test_constructor_nanosecond(self, result): @@ -897,21 +905,21 @@ def test_construct_timestamp_near_dst(self, offset): def test_construct_with_different_string_format(self, arg): # GH 12064 result = Timestamp(arg) - expected = Timestamp(datetime(2013, 1, 1), tz=pytz.FixedOffset(540)) + expected = Timestamp(datetime(2013, 1, 1), tz=timezone(timedelta(hours=9))) assert result == expected @pytest.mark.parametrize("box", [datetime, Timestamp]) def test_raise_tz_and_tzinfo_in_datetime_input(self, box): # GH 23579 - kwargs = {"year": 2018, "month": 1, "day": 1, "tzinfo": pytz.utc} + kwargs = {"year": 2018, "month": 1, "day": 1, "tzinfo": timezone.utc} msg = "Cannot pass a datetime or Timestamp" with pytest.raises(ValueError, match=msg): Timestamp(box(**kwargs), tz="US/Pacific") msg = "Cannot pass a datetime or Timestamp" with pytest.raises(ValueError, match=msg): - Timestamp(box(**kwargs), tzinfo=pytz.timezone("US/Pacific")) + Timestamp(box(**kwargs), tzinfo=zoneinfo.ZoneInfo("US/Pacific")) - def test_dont_convert_dateutil_utc_to_pytz_utc(self): + def test_dont_convert_dateutil_utc_to_default_utc(self): result = Timestamp(datetime(2018, 1, 1), tz=tzutc()) expected = Timestamp(datetime(2018, 1, 1)).tz_localize(tzutc()) assert result == expected @@ -953,7 +961,7 @@ def test_timestamp_constructor_near_dst_boundary(self): assert result == expected msg = "Cannot infer dst time from 2015-10-25 02:00:00" - with pytest.raises(pytz.AmbiguousTimeError, match=msg): + with pytest.raises(AmbiguousTimeError, match=msg): Timestamp("2015-10-25 02:00", tz=tz) result = Timestamp("2017-03-26 01:00", tz="Europe/Paris") @@ -961,7 +969,7 @@ def test_timestamp_constructor_near_dst_boundary(self): assert result == expected msg = "2017-03-26 02:00" - with pytest.raises(pytz.NonExistentTimeError, match=msg): + with pytest.raises(NonExistentTimeError, match=msg): Timestamp("2017-03-26 02:00", tz="Europe/Paris") # GH#11708 @@ -980,7 +988,7 @@ def test_timestamp_constructor_near_dst_boundary(self): assert result == expected msg = "2017-03-26 02:00" - with pytest.raises(pytz.NonExistentTimeError, match=msg): + with pytest.raises(NonExistentTimeError, match=msg): Timestamp("2017-03-26 02:00", tz="Europe/Paris") result = Timestamp("2017-03-26 02:00:00+0100", tz="Europe/Paris") @@ -995,7 +1003,7 @@ def test_timestamp_constructor_near_dst_boundary(self): @pytest.mark.parametrize( "tz", [ - pytz.timezone("US/Eastern"), + "pytz/US/Eastern", gettz("US/Eastern"), "US/Eastern", "dateutil/US/Eastern", @@ -1004,7 +1012,9 @@ def test_timestamp_constructor_near_dst_boundary(self): def test_timestamp_constructed_by_date_and_tz(self, tz): # GH#2993, Timestamp cannot be constructed by datetime.date # and tz correctly - + if isinstance(tz, str) and tz.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + tz = pytz.timezone(tz.removeprefix("pytz/")) result = Timestamp(date(2012, 3, 11), tz=tz) expected = Timestamp("3/11/2012", tz=tz) diff --git a/pandas/tests/scalar/timestamp/test_formats.py b/pandas/tests/scalar/timestamp/test_formats.py index 6a578b0a9eb09..387122a9e46b2 100644 --- a/pandas/tests/scalar/timestamp/test_formats.py +++ b/pandas/tests/scalar/timestamp/test_formats.py @@ -1,9 +1,11 @@ -from datetime import datetime +from datetime import ( + datetime, + timezone, +) import pprint import dateutil.tz import pytest -import pytz # a test below uses pytz but only inside a `eval` call from pandas import Timestamp @@ -178,14 +180,14 @@ def test_repr_matches_pydatetime_no_tz(self): ts_nanos_micros = Timestamp(1200) assert str(ts_nanos_micros) == "1970-01-01 00:00:00.000001200" - def test_repr_matches_pydatetime_tz_pytz(self): - dt_date = datetime(2013, 1, 2, tzinfo=pytz.utc) + def test_repr_matches_pydatetime_tz_stdlib(self): + dt_date = datetime(2013, 1, 2, tzinfo=timezone.utc) assert str(dt_date) == str(Timestamp(dt_date)) - dt_datetime = datetime(2013, 1, 2, 12, 1, 3, tzinfo=pytz.utc) + dt_datetime = datetime(2013, 1, 2, 12, 1, 3, tzinfo=timezone.utc) assert str(dt_datetime) == str(Timestamp(dt_datetime)) - dt_datetime_us = datetime(2013, 1, 2, 12, 1, 3, 45, tzinfo=pytz.utc) + dt_datetime_us = datetime(2013, 1, 2, 12, 1, 3, 45, tzinfo=timezone.utc) assert str(dt_datetime_us) == str(Timestamp(dt_datetime_us)) def test_repr_matches_pydatetime_tz_dateutil(self): diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py index 44a16e51f2c47..006b7a606a912 100644 --- a/pandas/tests/scalar/timestamp/test_timestamp.py +++ b/pandas/tests/scalar/timestamp/test_timestamp.py @@ -9,6 +9,7 @@ import locale import time import unicodedata +import zoneinfo from dateutil.tz import ( tzlocal, @@ -20,8 +21,6 @@ ) import numpy as np import pytest -import pytz -from pytz import utc from pandas._libs.tslibs.dtypes import NpyDatetimeUnit from pandas._libs.tslibs.timezones import ( @@ -259,7 +258,7 @@ def test_dow_parametric(self, ts, sign): class TestTimestamp: - @pytest.mark.parametrize("tz", [None, pytz.timezone("US/Pacific")]) + @pytest.mark.parametrize("tz", [None, zoneinfo.ZoneInfo("US/Pacific")]) def test_disallow_setting_tz(self, tz): # GH#3746 ts = Timestamp("2010") @@ -311,7 +310,7 @@ def compare(x, y): assert int((Timestamp(x)._value - Timestamp(y)._value) / 1e9) == 0 compare(Timestamp.now(), datetime.now()) - compare(Timestamp.now("UTC"), datetime.now(pytz.timezone("UTC"))) + compare(Timestamp.now("UTC"), datetime.now(timezone.utc)) compare(Timestamp.now("UTC"), datetime.now(tzutc())) msg = "Timestamp.utcnow is deprecated" with tm.assert_produces_warning(FutureWarning, match=msg): @@ -329,12 +328,12 @@ def compare(x, y): compare( # Support tz kwarg in Timestamp.fromtimestamp Timestamp.fromtimestamp(current_time, "UTC"), - datetime.fromtimestamp(current_time, utc), + datetime.fromtimestamp(current_time, timezone.utc), ) compare( # Support tz kwarg in Timestamp.fromtimestamp Timestamp.fromtimestamp(current_time, tz="UTC"), - datetime.fromtimestamp(current_time, utc), + datetime.fromtimestamp(current_time, timezone.utc), ) date_component = datetime.now(timezone.utc) @@ -586,9 +585,9 @@ def test_month_name(self, dt64, ts): assert ts.month_name() == alt.month_name() def test_tz_convert(self, ts): - ts = Timestamp._from_value_and_reso(ts._value, ts._creso, utc) + ts = Timestamp._from_value_and_reso(ts._value, ts._creso, timezone.utc) - tz = pytz.timezone("US/Pacific") + tz = zoneinfo.ZoneInfo("US/Pacific") result = ts.tz_convert(tz) assert isinstance(result, Timestamp) diff --git a/pandas/tests/series/accessors/test_dt_accessor.py b/pandas/tests/series/accessors/test_dt_accessor.py index 2365ff62b1680..af2ccf4212822 100644 --- a/pandas/tests/series/accessors/test_dt_accessor.py +++ b/pandas/tests/series/accessors/test_dt_accessor.py @@ -9,10 +9,13 @@ import numpy as np import pytest -import pytz from pandas._libs.tslibs.timezones import maybe_get_tz -from pandas.errors import SettingWithCopyError +from pandas.errors import ( + AmbiguousTimeError, + NonExistentTimeError, + SettingWithCopyError, +) from pandas.core.dtypes.common import ( is_integer_dtype, @@ -365,7 +368,7 @@ def test_dt_round_tz_ambiguous(self, method): tm.assert_series_equal(result, expected) # raise - with tm.external_error_raised(pytz.AmbiguousTimeError): + with tm.external_error_raised(AmbiguousTimeError): getattr(df1.date.dt, method)("h", ambiguous="raise") @pytest.mark.parametrize( @@ -387,7 +390,7 @@ def test_dt_round_tz_nonexistent(self, method, ts_str, freq): expected = Series([pd.NaT]).dt.tz_localize(result.dt.tz) tm.assert_series_equal(result, expected) - with pytest.raises(pytz.NonExistentTimeError, match="2018-03-11 02:00:00"): + with pytest.raises(NonExistentTimeError, match="2018-03-11 02:00:00"): getattr(ser.dt, method)(freq, nonexistent="raise") @pytest.mark.parametrize("freq", ["ns", "us", "1000us"]) diff --git a/pandas/tests/series/indexing/test_datetime.py b/pandas/tests/series/indexing/test_datetime.py index a6e4b4f78e25a..4a9ada7960bb3 100644 --- a/pandas/tests/series/indexing/test_datetime.py +++ b/pandas/tests/series/indexing/test_datetime.py @@ -13,7 +13,6 @@ ) import numpy as np import pytest -import pytz from pandas._libs import index as libindex @@ -69,6 +68,7 @@ def test_fancy_setitem(): @pytest.mark.parametrize("tz_source", ["pytz", "dateutil"]) def test_getitem_setitem_datetime_tz(tz_source): if tz_source == "pytz": + pytz = pytest.importorskip(tz_source) tzget = pytz.timezone else: # handle special case for utc in dateutil diff --git a/pandas/tests/series/methods/test_fillna.py b/pandas/tests/series/methods/test_fillna.py index a70d9f39ff488..56bf38e64e53b 100644 --- a/pandas/tests/series/methods/test_fillna.py +++ b/pandas/tests/series/methods/test_fillna.py @@ -6,7 +6,6 @@ import numpy as np import pytest -import pytz from pandas import ( Categorical, @@ -965,7 +964,7 @@ def test_ffill(self): def test_ffill_mixed_dtypes_without_missing_data(self): # GH#14956 - series = Series([datetime(2015, 1, 1, tzinfo=pytz.utc), 1]) + series = Series([datetime(2015, 1, 1, tzinfo=timezone.utc), 1]) result = series.ffill() tm.assert_series_equal(series, result) @@ -1034,16 +1033,16 @@ def test_datetime64tz_fillna_round_issue(self): # GH#14872 data = Series( - [NaT, NaT, datetime(2016, 12, 12, 22, 24, 6, 100001, tzinfo=pytz.utc)] + [NaT, NaT, datetime(2016, 12, 12, 22, 24, 6, 100001, tzinfo=timezone.utc)] ) filled = data.bfill() expected = Series( [ - datetime(2016, 12, 12, 22, 24, 6, 100001, tzinfo=pytz.utc), - datetime(2016, 12, 12, 22, 24, 6, 100001, tzinfo=pytz.utc), - datetime(2016, 12, 12, 22, 24, 6, 100001, tzinfo=pytz.utc), + datetime(2016, 12, 12, 22, 24, 6, 100001, tzinfo=timezone.utc), + datetime(2016, 12, 12, 22, 24, 6, 100001, tzinfo=timezone.utc), + datetime(2016, 12, 12, 22, 24, 6, 100001, tzinfo=timezone.utc), ] ) diff --git a/pandas/tests/series/methods/test_tz_localize.py b/pandas/tests/series/methods/test_tz_localize.py index 45620a721f442..f142b4d36634d 100644 --- a/pandas/tests/series/methods/test_tz_localize.py +++ b/pandas/tests/series/methods/test_tz_localize.py @@ -1,7 +1,6 @@ from datetime import timezone import pytest -import pytz from pandas._libs.tslibs import timezones @@ -11,6 +10,7 @@ Series, Timestamp, date_range, + errors, ) import pandas._testing as tm @@ -28,7 +28,7 @@ def test_series_tz_localize_ambiguous_bool(self): expected0 = Series([expected0]) expected1 = Series([expected1]) - with tm.external_error_raised(pytz.AmbiguousTimeError): + with tm.external_error_raised(errors.AmbiguousTimeError): ser.dt.tz_localize("US/Central") result = ser.dt.tz_localize("US/Central", ambiguous=True) @@ -79,11 +79,11 @@ def test_tz_localize_nonexistent(self, warsaw, method, exp, unit): df = ser.to_frame() if method == "raise": - with tm.external_error_raised(pytz.NonExistentTimeError): + with tm.external_error_raised(errors.NonExistentTimeError): dti.tz_localize(tz, nonexistent=method) - with tm.external_error_raised(pytz.NonExistentTimeError): + with tm.external_error_raised(errors.NonExistentTimeError): ser.tz_localize(tz, nonexistent=method) - with tm.external_error_raised(pytz.NonExistentTimeError): + with tm.external_error_raised(errors.NonExistentTimeError): df.tz_localize(tz, nonexistent=method) elif exp == "invalid": diff --git a/pandas/tests/tools/test_to_datetime.py b/pandas/tests/tools/test_to_datetime.py index 9d364c2f86ac5..80eef6543ba78 100644 --- a/pandas/tests/tools/test_to_datetime.py +++ b/pandas/tests/tools/test_to_datetime.py @@ -10,12 +10,12 @@ ) from decimal import Decimal import locale +import zoneinfo from dateutil.parser import parse from dateutil.tz.tz import tzoffset import numpy as np import pytest -import pytz from pandas._libs import tslib from pandas._libs.tslibs import ( @@ -465,9 +465,11 @@ def test_to_datetime_format_weeks(self, value, fmt, expected, cache): ["2010-01-01 12:00:00 Z", "2010-01-01 12:00:00 Z"], [ Timestamp( - "2010-01-01 12:00:00", tzinfo=pytz.FixedOffset(0) - ), # pytz coerces to UTC - Timestamp("2010-01-01 12:00:00", tzinfo=pytz.FixedOffset(0)), + "2010-01-01 12:00:00", tzinfo=timezone(timedelta(minutes=0)) + ), + Timestamp( + "2010-01-01 12:00:00", tzinfo=timezone(timedelta(minutes=0)) + ), ], ], ], @@ -1261,6 +1263,7 @@ def test_to_datetime_different_offsets(self, cache): def test_to_datetime_tz_pytz(self, cache): # see gh-8260 + pytz = pytest.importorskip("pytz") us_eastern = pytz.timezone("US/Eastern") arr = np.array( [ @@ -1828,7 +1831,9 @@ def test_to_datetime_fixed_offset(self): ["2020-10-26 00:00:00+06:00", Timestamp("2018-01-01", tz="US/Pacific")], [ "2020-10-26 00:00:00+06:00", - datetime(2020, 1, 1, 18, tzinfo=pytz.timezone("Australia/Melbourne")), + datetime(2020, 1, 1, 18).astimezone( + zoneinfo.ZoneInfo("Australia/Melbourne") + ), ], ], ) @@ -2508,7 +2513,7 @@ def test_to_datetime_iso8601_non_padded(self, input, format): ) def test_to_datetime_iso8601_with_timezone_valid(self, input, format): # https://github.com/pandas-dev/pandas/issues/12649 - expected = Timestamp(2020, 1, 1, tzinfo=pytz.UTC) + expected = Timestamp(2020, 1, 1, tzinfo=timezone.utc) result = to_datetime(input, format=format) assert result == expected @@ -2950,7 +2955,7 @@ def test_infer_datetime_format_zero_tz(self, ts, zero_tz): # GH 41047 ser = Series([ts + zero_tz]) result = to_datetime(ser) - tz = pytz.utc if zero_tz == "Z" else None + tz = timezone.utc if zero_tz == "Z" else None expected = Series([Timestamp(ts, tz=tz)]) tm.assert_series_equal(result, expected) @@ -3398,7 +3403,7 @@ def test_invalid_origins(self, origin, exc, units): def test_invalid_origins_tzinfo(self): # GH16842 with pytest.raises(ValueError, match="must be tz-naive"): - to_datetime(1, unit="D", origin=datetime(2000, 1, 1, tzinfo=pytz.utc)) + to_datetime(1, unit="D", origin=datetime(2000, 1, 1, tzinfo=timezone.utc)) def test_incorrect_value_exception(self): # GH47495 diff --git a/pandas/tests/tseries/holiday/test_holiday.py b/pandas/tests/tseries/holiday/test_holiday.py index b2eefd04ef93b..232ebcd0544c2 100644 --- a/pandas/tests/tseries/holiday/test_holiday.py +++ b/pandas/tests/tseries/holiday/test_holiday.py @@ -1,4 +1,7 @@ -from datetime import datetime +from datetime import ( + datetime, + timezone, +) import pytest from pytz import utc @@ -128,9 +131,9 @@ def test_holiday_dates(holiday, start_date, end_date, expected): # Verify that timezone info is preserved. assert list( holiday.dates( - utc.localize(Timestamp(start_date)), utc.localize(Timestamp(end_date)) + Timestamp(start_date, tz=timezone.utc), Timestamp(end_date, tz=timezone.utc) ) - ) == [utc.localize(dt) for dt in expected] + ) == [dt.astimezone(timezone.utc) for dt in expected] @pytest.mark.parametrize( diff --git a/pandas/tests/tseries/offsets/test_dst.py b/pandas/tests/tseries/offsets/test_dst.py index b22dc0b330817..c8ccbcfcd3bcf 100644 --- a/pandas/tests/tseries/offsets/test_dst.py +++ b/pandas/tests/tseries/offsets/test_dst.py @@ -4,7 +4,6 @@ from datetime import timedelta import pytest -import pytz from pandas._libs.tslibs import Timestamp from pandas._libs.tslibs.offsets import ( @@ -29,14 +28,13 @@ YearBegin, YearEnd, ) -from pandas.errors import PerformanceWarning +from pandas.errors import ( + AmbiguousTimeError, + PerformanceWarning, +) from pandas import DatetimeIndex import pandas._testing as tm -from pandas.util.version import Version - -# error: Module has no attribute "__version__" -pytz_version = Version(pytz.__version__) # type: ignore[attr-defined] def get_utc_offset_hours(ts): @@ -98,13 +96,13 @@ def _test_offset(self, offset_name, offset_n, tstart, expected_utc_offset): "second": "2013-11-03 01:59:01.999999", "microsecond": "2013-11-03 01:59:59.000001", }[offset_name] - with pytest.raises(pytz.AmbiguousTimeError, match=err_msg): + with pytest.raises(AmbiguousTimeError, match=err_msg): tstart + offset # While we're here, let's check that we get the same behavior in a # vectorized path dti = DatetimeIndex([tstart]) warn_msg = "Non-vectorized DateOffset" - with pytest.raises(pytz.AmbiguousTimeError, match=err_msg): + with pytest.raises(AmbiguousTimeError, match=err_msg): with tm.assert_produces_warning(PerformanceWarning, match=warn_msg): dti + offset return @@ -219,10 +217,6 @@ def test_all_offset_classes(self, tup): Timestamp("1905-07-01"), MonthBegin(66), "Africa/Lagos", - marks=pytest.mark.xfail( - pytz_version < Version("2020.5") or pytz_version == Version("2022.2"), - reason="GH#41906: pytz utc transition dates changed", - ), ), ( Timestamp("2021-10-01 01:15"), @@ -256,5 +250,5 @@ def test_nontick_offset_with_ambiguous_time_error(original_dt, target_dt, offset localized_dt = original_dt.tz_localize(tz) msg = f"Cannot infer dst time from {target_dt}, try using the 'ambiguous' argument" - with pytest.raises(pytz.AmbiguousTimeError, match=msg): + with pytest.raises(AmbiguousTimeError, match=msg): localized_dt + offset diff --git a/pandas/tests/tseries/offsets/test_offsets_properties.py b/pandas/tests/tseries/offsets/test_offsets_properties.py index 1b4fa9292c403..ed08fe99638c4 100644 --- a/pandas/tests/tseries/offsets/test_offsets_properties.py +++ b/pandas/tests/tseries/offsets/test_offsets_properties.py @@ -12,7 +12,6 @@ given, ) import pytest -import pytz import pandas as pd from pandas._testing._hypothesis import ( @@ -33,7 +32,7 @@ def test_on_offset_implementations(dt, offset): # (dt + offset) - offset == dt try: compare = (dt + offset) - offset - except (pytz.NonExistentTimeError, pytz.AmbiguousTimeError): + except (pd.errors.NonExistentTimeError, pd.errors.AmbiguousTimeError): # When dt + offset does not exist or is DST-ambiguous, assume(False) to # indicate to hypothesis that this is not a valid test case # DST-ambiguous example (GH41906): diff --git a/pandas/tests/tslibs/test_conversion.py b/pandas/tests/tslibs/test_conversion.py index 6a0b86cbd03ee..f62910b5e1f1c 100644 --- a/pandas/tests/tslibs/test_conversion.py +++ b/pandas/tests/tslibs/test_conversion.py @@ -1,8 +1,10 @@ -from datetime import datetime +from datetime import ( + datetime, + timezone, +) import numpy as np import pytest -from pytz import UTC from pandas._libs.tslibs import ( OutOfBoundsTimedelta, @@ -55,7 +57,7 @@ def _compare_local_to_utc(tz_didx, naive_didx): def test_tz_localize_to_utc_copies(): # GH#46460 arr = np.arange(5, dtype="i8") - result = tz_convert_from_utc(arr, tz=UTC) + result = tz_convert_from_utc(arr, tz=timezone.utc) tm.assert_numpy_array_equal(result, arr) assert not np.shares_memory(arr, result) @@ -100,7 +102,7 @@ def test_tz_convert_readonly(): # GH#35530 arr = np.array([0], dtype=np.int64) arr.setflags(write=False) - result = tz_convert_from_utc(arr, UTC) + result = tz_convert_from_utc(arr, timezone.utc) tm.assert_numpy_array_equal(result, arr) @@ -141,14 +143,18 @@ class SubDatetime(datetime): "dt, expected", [ pytest.param( - Timestamp("2000-01-01"), Timestamp("2000-01-01", tz=UTC), id="timestamp" + Timestamp("2000-01-01"), + Timestamp("2000-01-01", tz=timezone.utc), + id="timestamp", ), pytest.param( - datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=UTC), id="datetime" + datetime(2000, 1, 1), + datetime(2000, 1, 1, tzinfo=timezone.utc), + id="datetime", ), pytest.param( SubDatetime(2000, 1, 1), - SubDatetime(2000, 1, 1, tzinfo=UTC), + SubDatetime(2000, 1, 1, tzinfo=timezone.utc), id="subclassed_datetime", ), ], @@ -157,5 +163,5 @@ def test_localize_pydatetime_dt_types(dt, expected): # GH 25851 # ensure that subclassed datetime works with # localize_pydatetime - result = conversion.localize_pydatetime(dt, UTC) + result = conversion.localize_pydatetime(dt, timezone.utc) assert result == expected diff --git a/pandas/tests/tslibs/test_resolution.py b/pandas/tests/tslibs/test_resolution.py index 690962f1daa5e..109c37e2c2bdf 100644 --- a/pandas/tests/tslibs/test_resolution.py +++ b/pandas/tests/tslibs/test_resolution.py @@ -1,6 +1,7 @@ +import datetime + import numpy as np import pytest -import pytz from pandas._libs.tslibs import ( Resolution, @@ -23,7 +24,7 @@ def test_get_resolution_non_nano_data(): res = get_resolution(arr, None, NpyDatetimeUnit.NPY_FR_us.value) assert res == Resolution.RESO_US - res = get_resolution(arr, pytz.UTC, NpyDatetimeUnit.NPY_FR_us.value) + res = get_resolution(arr, datetime.timezone.utc, NpyDatetimeUnit.NPY_FR_us.value) assert res == Resolution.RESO_US diff --git a/pandas/tests/tslibs/test_timezones.py b/pandas/tests/tslibs/test_timezones.py index 28e4889983fb9..5564720d70982 100644 --- a/pandas/tests/tslibs/test_timezones.py +++ b/pandas/tests/tslibs/test_timezones.py @@ -6,7 +6,6 @@ import dateutil.tz import pytest -import pytz from pandas._libs.tslibs import ( conversion, @@ -22,17 +21,18 @@ def test_is_utc(utc_fixture): assert timezones.is_utc(tz) -@pytest.mark.parametrize("tz_name", list(pytz.common_timezones)) -def test_cache_keys_are_distinct_for_pytz_vs_dateutil(tz_name): - tz_p = timezones.maybe_get_tz(tz_name) - tz_d = timezones.maybe_get_tz("dateutil/" + tz_name) +def test_cache_keys_are_distinct_for_pytz_vs_dateutil(): + pytz = pytest.importorskip("pytz") + for tz_name in pytz.common_timezones: + tz_p = timezones.maybe_get_tz(tz_name) + tz_d = timezones.maybe_get_tz("dateutil/" + tz_name) - if tz_d is None: - pytest.skip(tz_name + ": dateutil does not know about this one") + if tz_d is None: + pytest.skip(tz_name + ": dateutil does not know about this one") - if not (tz_name == "UTC" and is_platform_windows()): - # they both end up as tzwin("UTC") on windows - assert timezones._p_tz_cache_key(tz_p) != timezones._p_tz_cache_key(tz_d) + if not (tz_name == "UTC" and is_platform_windows()): + # they both end up as tzwin("UTC") on windows + assert timezones._p_tz_cache_key(tz_p) != timezones._p_tz_cache_key(tz_d) def test_tzlocal_repr(): @@ -76,12 +76,15 @@ def test_tz_compare_utc(utc_fixture, utc_fixture2): @pytest.fixture( params=[ - (pytz.timezone("US/Eastern"), lambda tz, x: tz.localize(x)), + ("pytz/US/Eastern", lambda tz, x: tz.localize(x)), (dateutil.tz.gettz("US/Eastern"), lambda tz, x: x.replace(tzinfo=tz)), ] ) def infer_setup(request): eastern, localize = request.param + if isinstance(eastern, str) and eastern.startswith("pytz/"): + pytz = pytest.importorskip("pytz") + eastern = pytz.timezone(eastern.removeprefix("pytz/")) start_naive = datetime(2001, 1, 1) end_naive = datetime(2009, 1, 1) @@ -111,7 +114,7 @@ def test_infer_tz_compat(infer_setup): def test_infer_tz_utc_localize(infer_setup): _, _, start, end, start_naive, end_naive = infer_setup - utc = pytz.utc + utc = timezone.utc start = utc.localize(start_naive) end = utc.localize(end_naive) @@ -124,7 +127,7 @@ def test_infer_tz_mismatch(infer_setup, ordered): eastern, _, _, _, start_naive, end_naive = infer_setup msg = "Inputs must both have the same timezone" - utc = pytz.utc + utc = timezone.utc start = utc.localize(start_naive) end = conversion.localize_pydatetime(end_naive, eastern) @@ -139,7 +142,7 @@ def test_maybe_get_tz_invalid_types(): timezones.maybe_get_tz(44.0) with pytest.raises(TypeError, match=""): - timezones.maybe_get_tz(pytz) + timezones.maybe_get_tz(pytest) msg = "" with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/tslibs/test_tzconversion.py b/pandas/tests/tslibs/test_tzconversion.py index c1a56ffb71b02..ab513ceb0d825 100644 --- a/pandas/tests/tslibs/test_tzconversion.py +++ b/pandas/tests/tslibs/test_tzconversion.py @@ -1,9 +1,12 @@ +import zoneinfo + import numpy as np import pytest -import pytz from pandas._libs.tslibs.tzconversion import tz_localize_to_utc +from pandas import errors + class TestTZLocalizeToUTC: def test_tz_localize_to_utc_ambiguous_infer(self): @@ -11,13 +14,15 @@ def test_tz_localize_to_utc_ambiguous_infer(self): val = 1_320_541_200_000_000_000 vals = np.array([val, val - 1, val], dtype=np.int64) - with pytest.raises(pytz.AmbiguousTimeError, match="2011-11-06 01:00:00"): - tz_localize_to_utc(vals, pytz.timezone("US/Eastern"), ambiguous="infer") + with pytest.raises(errors.AmbiguousTimeError, match="2011-11-06 01:00:00"): + tz_localize_to_utc(vals, zoneinfo.ZoneInfo("US/Eastern"), ambiguous="infer") - with pytest.raises(pytz.AmbiguousTimeError, match="are no repeated times"): - tz_localize_to_utc(vals[:1], pytz.timezone("US/Eastern"), ambiguous="infer") + with pytest.raises(errors.AmbiguousTimeError, match="are no repeated times"): + tz_localize_to_utc( + vals[:1], zoneinfo.ZoneInfo("US/Eastern"), ambiguous="infer" + ) vals[1] += 1 msg = "There are 2 dst switches when there should only be 1" - with pytest.raises(pytz.AmbiguousTimeError, match=msg): - tz_localize_to_utc(vals, pytz.timezone("US/Eastern"), ambiguous="infer") + with pytest.raises(errors.AmbiguousTimeError, match=msg): + tz_localize_to_utc(vals, zoneinfo.ZoneInfo("US/Eastern"), ambiguous="infer") From b57f2ce9bf51d679623fbb9da824901f89324c30 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:25:08 -0800 Subject: [PATCH 03/16] Make pytz optional, docs --- .circleci/config.yml | 2 +- .github/workflows/unit-tests.yml | 8 ++++---- README.md | 1 - ci/deps/actions-310.yaml | 2 +- ci/deps/actions-311-downstream_compat.yaml | 2 +- ci/deps/actions-311-numpydev.yaml | 1 - ci/deps/actions-311-pyarrownightly.yaml | 1 - ci/deps/actions-311-sanitizers.yaml | 1 - ci/deps/actions-311.yaml | 2 +- ci/deps/actions-312.yaml | 2 +- ci/deps/actions-39-minimum_versions.yaml | 2 +- ci/deps/actions-39.yaml | 2 +- ci/deps/actions-pypy-39.yaml | 1 - ci/deps/circle-310-arm64.yaml | 2 +- ci/meta.yaml | 1 - doc/source/getting_started/install.rst | 12 +++++++++++- doc/source/user_guide/io.rst | 2 +- doc/source/user_guide/timeseries.rst | 6 +++--- environment.yml | 2 +- pandas/__init__.py | 2 +- pandas/_testing/_hypothesis.py | 3 +-- pandas/compat/_optional.py | 1 + pandas/core/arrays/datetimes.py | 12 ++++++------ pandas/util/_print_versions.py | 1 - pyproject.toml | 3 ++- requirements-dev.txt | 2 +- 26 files changed, 40 insertions(+), 36 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 90afb1ce29684..811c7bae46899 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,7 +34,7 @@ jobs: /opt/python/cp311-cp311/bin/python -m venv ~/virtualenvs/pandas-dev . ~/virtualenvs/pandas-dev/bin/activate python -m pip install --no-cache-dir -U pip wheel setuptools meson-python==0.13.1 meson[ninja]==1.2.1 - python -m pip install --no-cache-dir versioneer[toml] cython numpy python-dateutil pytz pytest>=7.3.2 pytest-xdist>=2.2.0 hypothesis>=6.46.1 + python -m pip install --no-cache-dir versioneer[toml] cython numpy python-dateutil pytest>=7.3.2 pytest-xdist>=2.2.0 hypothesis>=6.46.1 python -m pip install --no-cache-dir --no-build-isolation -e . --config-settings=setup-args="--werror" python -m pip list --no-cache-dir - run: | diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index a3cffb4b03b93..3dd8e091caca8 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -274,7 +274,7 @@ jobs: . ~/virtualenvs/pandas-dev/bin/activate python -m pip install --no-cache-dir -U pip wheel setuptools meson[ninja]==1.2.1 meson-python==0.13.1 python -m pip install numpy --config-settings=setup-args="-Dallow-noblas=true" - python -m pip install --no-cache-dir versioneer[toml] cython python-dateutil pytz pytest>=7.3.2 pytest-xdist>=2.2.0 hypothesis>=6.46.1 + python -m pip install --no-cache-dir versioneer[toml] cython python-dateutil pytest>=7.3.2 pytest-xdist>=2.2.0 hypothesis>=6.46.1 python -m pip install --no-cache-dir --no-build-isolation -e . --config-settings=setup-args="--werror" python -m pip list --no-cache-dir export PANDAS_CI=1 @@ -312,7 +312,7 @@ jobs: /opt/python/cp311-cp311/bin/python -m venv ~/virtualenvs/pandas-dev . ~/virtualenvs/pandas-dev/bin/activate python -m pip install --no-cache-dir -U pip wheel setuptools meson-python==0.13.1 meson[ninja]==1.2.1 - python -m pip install --no-cache-dir versioneer[toml] cython numpy python-dateutil pytz pytest>=7.3.2 pytest-xdist>=2.2.0 hypothesis>=6.46.1 + python -m pip install --no-cache-dir versioneer[toml] cython numpy python-dateutil pytest>=7.3.2 pytest-xdist>=2.2.0 hypothesis>=6.46.1 python -m pip install --no-cache-dir --no-build-isolation -e . --config-settings=setup-args="--werror" python -m pip list --no-cache-dir @@ -333,7 +333,7 @@ jobs: # In general, this will remain frozen(present, but not running) until: # - The next unreleased Python version has released beta 1 # - This version should be available on GitHub Actions. - # - Our required build/runtime dependencies(numpy, pytz, Cython, python-dateutil) + # - Our required build/runtime dependencies(numpy, Cython, python-dateutil) # support that unreleased Python version. # To unfreeze, comment out the ``if: false`` condition, and make sure you update # the name of the workflow and Python version in actions/setup-python ``python-version:`` @@ -385,7 +385,7 @@ jobs: python -m pip install --upgrade pip setuptools wheel meson[ninja]==1.2.1 meson-python==0.13.1 python -m pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy python -m pip install versioneer[toml] - python -m pip install python-dateutil pytz tzdata cython hypothesis>=6.46.1 pytest>=7.3.2 pytest-xdist>=2.2.0 pytest-cov + python -m pip install python-dateutil tzdata cython hypothesis>=6.46.1 pytest>=7.3.2 pytest-xdist>=2.2.0 pytest-cov python -m pip install -ve . --no-build-isolation --no-index --no-deps --config-settings=setup-args="--werror" python -m pip list diff --git a/README.md b/README.md index 6fa20d237babe..0b55cfdc88f3f 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,6 @@ details, see the commit logs at https://github.com/pandas-dev/pandas. ## Dependencies - [NumPy - Adds support for large, multi-dimensional arrays, matrices and high-level mathematical functions to operate on these arrays](https://www.numpy.org) - [python-dateutil - Provides powerful extensions to the standard datetime module](https://dateutil.readthedocs.io/en/stable/index.html) -- [pytz - Brings the Olson tz database into Python which allows accurate and cross platform timezone calculations](https://github.com/stub42/pytz) See the [full installation instructions](https://pandas.pydata.org/pandas-docs/stable/install.html#dependencies) for minimum supported versions of required, recommended and optional dependencies. diff --git a/ci/deps/actions-310.yaml b/ci/deps/actions-310.yaml index 45f114322015b..b1b1ec4a23f2f 100644 --- a/ci/deps/actions-310.yaml +++ b/ci/deps/actions-310.yaml @@ -21,7 +21,6 @@ dependencies: # required dependencies - python-dateutil - numpy - - pytz # optional dependencies - beautifulsoup4>=4.11.2 @@ -47,6 +46,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 + - pytz>=2020.1 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/actions-311-downstream_compat.yaml b/ci/deps/actions-311-downstream_compat.yaml index d6bf9ec7843de..22899638488d4 100644 --- a/ci/deps/actions-311-downstream_compat.yaml +++ b/ci/deps/actions-311-downstream_compat.yaml @@ -22,7 +22,6 @@ dependencies: # required dependencies - python-dateutil - numpy - - pytz # optional dependencies - beautifulsoup4>=4.11.2 @@ -48,6 +47,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 + - pytz>=2020.1 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/actions-311-numpydev.yaml b/ci/deps/actions-311-numpydev.yaml index b62e8630f2059..1f305cf4e2652 100644 --- a/ci/deps/actions-311-numpydev.yaml +++ b/ci/deps/actions-311-numpydev.yaml @@ -21,7 +21,6 @@ dependencies: # pandas dependencies - python-dateutil - - pytz - pip - pip: diff --git a/ci/deps/actions-311-pyarrownightly.yaml b/ci/deps/actions-311-pyarrownightly.yaml index d84063ac2a9ba..9c2ae62bbfebb 100644 --- a/ci/deps/actions-311-pyarrownightly.yaml +++ b/ci/deps/actions-311-pyarrownightly.yaml @@ -19,7 +19,6 @@ dependencies: # required dependencies - python-dateutil - numpy - - pytz - pip - pip: diff --git a/ci/deps/actions-311-sanitizers.yaml b/ci/deps/actions-311-sanitizers.yaml index f5f04c90bffad..bcfb053a71b97 100644 --- a/ci/deps/actions-311-sanitizers.yaml +++ b/ci/deps/actions-311-sanitizers.yaml @@ -23,7 +23,6 @@ dependencies: # required dependencies - python-dateutil - numpy - - pytz # pandas dependencies - pip diff --git a/ci/deps/actions-311.yaml b/ci/deps/actions-311.yaml index d14686696e669..799d9d02bb089 100644 --- a/ci/deps/actions-311.yaml +++ b/ci/deps/actions-311.yaml @@ -21,7 +21,6 @@ dependencies: # required dependencies - python-dateutil - numpy - - pytz # optional dependencies - beautifulsoup4>=4.11.2 @@ -47,6 +46,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 + - pytz>=2020.1 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/actions-312.yaml b/ci/deps/actions-312.yaml index 86aaf24b4e15c..79ba9e0e462c0 100644 --- a/ci/deps/actions-312.yaml +++ b/ci/deps/actions-312.yaml @@ -21,7 +21,6 @@ dependencies: # required dependencies - python-dateutil - numpy - - pytz # optional dependencies - beautifulsoup4>=4.11.2 @@ -47,6 +46,7 @@ dependencies: - pyreadstat>=1.2.0 # - pytables>=3.8.0 - python-calamine>=0.1.7 + - pytz>=2020.1 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/actions-39-minimum_versions.yaml b/ci/deps/actions-39-minimum_versions.yaml index 115ccf01ccaad..1d51993b4db37 100644 --- a/ci/deps/actions-39-minimum_versions.yaml +++ b/ci/deps/actions-39-minimum_versions.yaml @@ -23,7 +23,6 @@ dependencies: # required dependencies - python-dateutil=2.8.2 - numpy=1.23.5 - - pytz=2020.1 # optional dependencies - beautifulsoup4=4.11.2 @@ -49,6 +48,7 @@ dependencies: - pyreadstat=1.2.0 - pytables=3.8.0 - python-calamine=0.1.7 + - pytz=2020.1 - pyxlsb=1.0.10 - s3fs=2022.11.0 - scipy=1.10.0 diff --git a/ci/deps/actions-39.yaml b/ci/deps/actions-39.yaml index 31ee74174cd46..b40e856e7fb22 100644 --- a/ci/deps/actions-39.yaml +++ b/ci/deps/actions-39.yaml @@ -21,7 +21,6 @@ dependencies: # required dependencies - python-dateutil - numpy - - pytz # optional dependencies - beautifulsoup4>=4.11.2 @@ -47,6 +46,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 + - pytz>=2020.1 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/actions-pypy-39.yaml b/ci/deps/actions-pypy-39.yaml index d9c8dd81b7c33..1f1944e5a0909 100644 --- a/ci/deps/actions-pypy-39.yaml +++ b/ci/deps/actions-pypy-39.yaml @@ -22,6 +22,5 @@ dependencies: # required - numpy - python-dateutil - - pytz - pip: - tzdata>=2022.7 diff --git a/ci/deps/circle-310-arm64.yaml b/ci/deps/circle-310-arm64.yaml index a19ffd485262d..229838948c1ce 100644 --- a/ci/deps/circle-310-arm64.yaml +++ b/ci/deps/circle-310-arm64.yaml @@ -21,7 +21,6 @@ dependencies: # required dependencies - python-dateutil - numpy - - pytz # optional dependencies - beautifulsoup4>=4.11.2 @@ -47,6 +46,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 + - pytz>=2020.1 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/meta.yaml b/ci/meta.yaml index aac5593e493b7..9631a41e6cbd7 100644 --- a/ci/meta.yaml +++ b/ci/meta.yaml @@ -37,7 +37,6 @@ requirements: - numpy >=1.21.6 # [py<311] - numpy >=1.23.2 # [py>=311] - python-dateutil >=2.8.2 - - pytz >=2020.1 - python-tzdata >=2022.7 test: diff --git a/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst index b54a220f74ca3..2b723fd5a9b96 100644 --- a/doc/source/getting_started/install.rst +++ b/doc/source/getting_started/install.rst @@ -205,7 +205,6 @@ Package Minimum support ================================================================ ========================== `NumPy `__ 1.23.5 `python-dateutil `__ 2.8.2 -`pytz `__ 2020.1 `tzdata `__ 2022.7 ================================================================ ========================== @@ -428,3 +427,14 @@ Dependency Minimum Version pip extra Notes ========================= ================== =================== ============================================================= dataframe-api-compat 0.1.7 consortium-standard Consortium Standard-compatible implementation based on pandas ========================= ================== =================== ============================================================= + +Timezone +^^^^^^^^ + +Installable with ``pip install "pandas[timezone]"`` + +========================= ================== =================== ============================================================= +Dependency Minimum Version pip extra Notes +========================= ================== =================== ============================================================= +pytz 2020.1 timezone Alternative timezone library to ``zoneinfo``. +========================= ================== =================== ============================================================= diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst index bb5b4e056d527..e876d073e0eae 100644 --- a/doc/source/user_guide/io.rst +++ b/doc/source/user_guide/io.rst @@ -5096,7 +5096,7 @@ Caveats convenience you can use ``store.flush(fsync=True)`` to do this for you. * Once a ``table`` is created columns (DataFrame) are fixed; only exactly the same columns can be appended -* Be aware that timezones (e.g., ``pytz.timezone('US/Eastern')``) +* Be aware that timezones (e.g., ``zoneinfo.ZoneInfo('US/Eastern')``) are not necessarily equal across timezone versions. So if data is localized to a specific timezone in the HDFStore using one version of a timezone library and that data is updated with another version, the data diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst index 39916c2bca7aa..c81baae48f6db 100644 --- a/doc/source/user_guide/timeseries.rst +++ b/doc/source/user_guide/timeseries.rst @@ -2358,9 +2358,9 @@ To return ``dateutil`` time zone objects, append ``dateutil/`` before the string import dateutil - # pytz - rng_pytz = pd.date_range("3/6/2012 00:00", periods=3, freq="D", tz="Europe/London") - rng_pytz.tz + # zoneinfo + rng_zinfo = pd.date_range("3/6/2012 00:00", periods=3, freq="D", tz="Europe/London") + rng_zinfo.tz # dateutil rng_dateutil = pd.date_range("3/6/2012 00:00", periods=3, freq="D") diff --git a/environment.yml b/environment.yml index 58eb69ad1f070..23b42fada0c6b 100644 --- a/environment.yml +++ b/environment.yml @@ -23,7 +23,6 @@ dependencies: # required dependencies - python-dateutil - numpy<2 - - pytz # optional dependencies - beautifulsoup4>=4.11.2 @@ -49,6 +48,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 + - pytz>=2020.1 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/pandas/__init__.py b/pandas/__init__.py index ed524c2bb3619..e759e9477a4a5 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -6,7 +6,7 @@ __docformat__ = "restructuredtext" # Let users know if they're missing any of our hard dependencies -_hard_dependencies = ("numpy", "pytz", "dateutil") +_hard_dependencies = ("numpy", "dateutil") _missing_dependencies = [] for _dependency in _hard_dependencies: diff --git a/pandas/_testing/_hypothesis.py b/pandas/_testing/_hypothesis.py index f9f653f636c4c..9012dc69748de 100644 --- a/pandas/_testing/_hypothesis.py +++ b/pandas/_testing/_hypothesis.py @@ -5,7 +5,6 @@ from hypothesis import strategies as st from hypothesis.extra.dateutil import timezones as dateutil_timezones -from hypothesis.extra.pytz import timezones as pytz_timezones from pandas.compat import is_platform_windows @@ -56,7 +55,7 @@ DATETIME_JAN_1_1900_OPTIONAL_TZ = st.datetimes( min_value=pd.Timestamp(1900, 1, 1).to_pydatetime(), # pyright: ignore[reportGeneralTypeIssues] max_value=pd.Timestamp(1900, 1, 1).to_pydatetime(), # pyright: ignore[reportGeneralTypeIssues] - timezones=st.one_of(st.none(), dateutil_timezones(), pytz_timezones()), + timezones=st.one_of(st.none(), dateutil_timezones(), st.timezones()), ) DATETIME_IN_PD_TIMESTAMP_RANGE_NO_TZ = st.datetimes( diff --git a/pandas/compat/_optional.py b/pandas/compat/_optional.py index 0f63d3d3002c3..8bcb963666df5 100644 --- a/pandas/compat/_optional.py +++ b/pandas/compat/_optional.py @@ -43,6 +43,7 @@ "pyarrow": "10.0.1", "pyreadstat": "1.2.0", "pytest": "7.3.2", + "pytz": "2020.1", "python-calamine": "0.1.7", "pyxlsb": "1.0.10", "s3fs": "2022.11.0", diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 4b804598681fa..b2fcf1ce1aaed 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -593,7 +593,7 @@ def tz(self) -> tzinfo | None: Returns ------- - datetime.tzinfo, pytz.tzinfo.BaseTZInfo, dateutil.tz.tz.tzfile, or None + zoneinfo.ZoneInfo, datetime.tzinfo, pytz.tzinfo.BaseTZInfo, dateutil.tz.tz.tzfile, or None Returns None when the array is tz-naive. Examples @@ -615,7 +615,7 @@ def tz(self) -> tzinfo | None: ... "2/1/2020 11:00:00+00:00"]) >>> idx.tz datetime.timezone.utc - """ + """ # noqa: E501 # GH 18595 return getattr(self.dtype, "tz", None) @@ -863,7 +863,7 @@ def tz_convert(self, tz) -> Self: Parameters ---------- - tz : str, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or None Time zone for time. Corresponding timestamps would be converted to this time zone of the Datetime Array/Index. A `tz` of None will convert to UTC and remove the timezone information. @@ -920,7 +920,7 @@ def tz_convert(self, tz) -> Self: '2014-08-01 08:00:00', '2014-08-01 09:00:00'], dtype='datetime64[ns]', freq='h') - """ + """ # noqa: E501 tz = timezones.maybe_get_tz(tz) if self.tz is None: @@ -952,7 +952,7 @@ def tz_localize( Parameters ---------- - tz : str, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or None Time zone to convert timestamps to. Passing ``None`` will remove the time zone information preserving local time. ambiguous : 'infer', 'NaT', bool array, default 'raise' @@ -1078,7 +1078,7 @@ def tz_localize( 0 2015-03-29 03:30:00+02:00 1 2015-03-29 03:30:00+02:00 dtype: datetime64[ns, Europe/Warsaw] - """ + """ # noqa: E501 nonexistent_options = ("raise", "NaT", "shift_forward", "shift_backward") if nonexistent not in nonexistent_options and not isinstance( nonexistent, timedelta diff --git a/pandas/util/_print_versions.py b/pandas/util/_print_versions.py index e39c2f7badb1d..f88127fa9aad3 100644 --- a/pandas/util/_print_versions.py +++ b/pandas/util/_print_versions.py @@ -67,7 +67,6 @@ def _get_dependency_info() -> dict[str, JSONSerializable]: "pandas", # required "numpy", - "pytz", "dateutil", # install / build, "setuptools", diff --git a/pyproject.toml b/pyproject.toml index ffd3a7e1f1f33..bd9a26feeb78d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ dependencies = [ "numpy>=1.23.5; python_version<'3.12'", "numpy>=1.26.0; python_version>='3.12'", "python-dateutil>=2.8.2", - "pytz>=2020.1", "tzdata>=2022.7" ] classifiers = [ @@ -85,6 +84,7 @@ output-formatting = ['jinja2>=3.1.2', 'tabulate>=0.9.0'] clipboard = ['PyQt5>=5.15.9', 'qtpy>=2.3.0'] compression = ['zstandard>=0.19.0'] consortium-standard = ['dataframe-api-compat>=0.1.7'] +timezone = ['pytz>=2020.1'] all = ['adbc-driver-postgresql>=0.8.0', 'adbc-driver-sqlite>=0.8.0', 'beautifulsoup4>=4.11.2', @@ -113,6 +113,7 @@ all = ['adbc-driver-postgresql>=0.8.0', 'pytest>=7.3.2', 'pytest-xdist>=2.2.0', 'python-calamine>=0.1.7', + 'pytz>=2020.1', 'pyxlsb>=1.0.10', 'qtpy>=2.3.0', 'scipy>=1.10.0', diff --git a/requirements-dev.txt b/requirements-dev.txt index 5a63e59e1db88..7ab298ac27e28 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,7 +14,6 @@ PyQt5>=5.15.9 coverage python-dateutil numpy<2 -pytz beautifulsoup4>=4.11.2 blosc bottleneck>=1.3.6 @@ -38,6 +37,7 @@ pymysql>=1.0.2 pyreadstat>=1.2.0 tables>=3.8.0 python-calamine>=0.1.7 +pytz>=2020.1 pyxlsb>=1.0.10 s3fs>=2022.11.0 scipy>=1.10.0 From 69ca55f15a315f0e0d7e9f9c0a947f702d518220 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:27:39 -0800 Subject: [PATCH 04/16] Bumpy pytz --- ci/deps/actions-310.yaml | 2 +- ci/deps/actions-311-downstream_compat.yaml | 2 +- ci/deps/actions-311.yaml | 2 +- ci/deps/actions-312.yaml | 2 +- ci/deps/actions-39-minimum_versions.yaml | 2 +- ci/deps/actions-39.yaml | 2 +- ci/deps/circle-310-arm64.yaml | 2 +- doc/source/getting_started/install.rst | 2 +- environment.yml | 2 +- pandas/compat/_optional.py | 2 +- pyproject.toml | 4 ++-- requirements-dev.txt | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ci/deps/actions-310.yaml b/ci/deps/actions-310.yaml index b1b1ec4a23f2f..c2f74238b4ce9 100644 --- a/ci/deps/actions-310.yaml +++ b/ci/deps/actions-310.yaml @@ -46,7 +46,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 - - pytz>=2020.1 + - pytz>=2023.3 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/actions-311-downstream_compat.yaml b/ci/deps/actions-311-downstream_compat.yaml index 22899638488d4..3b0f500bdd93c 100644 --- a/ci/deps/actions-311-downstream_compat.yaml +++ b/ci/deps/actions-311-downstream_compat.yaml @@ -47,7 +47,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 - - pytz>=2020.1 + - pytz>=2023.3 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/actions-311.yaml b/ci/deps/actions-311.yaml index 799d9d02bb089..aa2d0721ea291 100644 --- a/ci/deps/actions-311.yaml +++ b/ci/deps/actions-311.yaml @@ -46,7 +46,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 - - pytz>=2020.1 + - pytz>=2023.3 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/actions-312.yaml b/ci/deps/actions-312.yaml index 79ba9e0e462c0..4e9d6d04a11e1 100644 --- a/ci/deps/actions-312.yaml +++ b/ci/deps/actions-312.yaml @@ -46,7 +46,7 @@ dependencies: - pyreadstat>=1.2.0 # - pytables>=3.8.0 - python-calamine>=0.1.7 - - pytz>=2020.1 + - pytz>=2023.3 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/actions-39-minimum_versions.yaml b/ci/deps/actions-39-minimum_versions.yaml index 1d51993b4db37..7f39ddec99490 100644 --- a/ci/deps/actions-39-minimum_versions.yaml +++ b/ci/deps/actions-39-minimum_versions.yaml @@ -48,7 +48,7 @@ dependencies: - pyreadstat=1.2.0 - pytables=3.8.0 - python-calamine=0.1.7 - - pytz=2020.1 + - pytz=2023.3 - pyxlsb=1.0.10 - s3fs=2022.11.0 - scipy=1.10.0 diff --git a/ci/deps/actions-39.yaml b/ci/deps/actions-39.yaml index b40e856e7fb22..f3e60575993f5 100644 --- a/ci/deps/actions-39.yaml +++ b/ci/deps/actions-39.yaml @@ -46,7 +46,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 - - pytz>=2020.1 + - pytz>=2023.3 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/ci/deps/circle-310-arm64.yaml b/ci/deps/circle-310-arm64.yaml index 229838948c1ce..ed10c633d828f 100644 --- a/ci/deps/circle-310-arm64.yaml +++ b/ci/deps/circle-310-arm64.yaml @@ -46,7 +46,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 - - pytz>=2020.1 + - pytz>=2023.3 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst index 2b723fd5a9b96..d820b5fa2afa8 100644 --- a/doc/source/getting_started/install.rst +++ b/doc/source/getting_started/install.rst @@ -436,5 +436,5 @@ Installable with ``pip install "pandas[timezone]"`` ========================= ================== =================== ============================================================= Dependency Minimum Version pip extra Notes ========================= ================== =================== ============================================================= -pytz 2020.1 timezone Alternative timezone library to ``zoneinfo``. +pytz 2023.3 timezone Alternative timezone library to ``zoneinfo``. ========================= ================== =================== ============================================================= diff --git a/environment.yml b/environment.yml index 23b42fada0c6b..f20eacac0f8a5 100644 --- a/environment.yml +++ b/environment.yml @@ -48,7 +48,7 @@ dependencies: - pyreadstat>=1.2.0 - pytables>=3.8.0 - python-calamine>=0.1.7 - - pytz>=2020.1 + - pytz>=2023.3 - pyxlsb>=1.0.10 - s3fs>=2022.11.0 - scipy>=1.10.0 diff --git a/pandas/compat/_optional.py b/pandas/compat/_optional.py index 8bcb963666df5..3d95bf2c51a80 100644 --- a/pandas/compat/_optional.py +++ b/pandas/compat/_optional.py @@ -43,7 +43,7 @@ "pyarrow": "10.0.1", "pyreadstat": "1.2.0", "pytest": "7.3.2", - "pytz": "2020.1", + "pytz": "2023.3", "python-calamine": "0.1.7", "pyxlsb": "1.0.10", "s3fs": "2022.11.0", diff --git a/pyproject.toml b/pyproject.toml index bd9a26feeb78d..feed1462f6cee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ output-formatting = ['jinja2>=3.1.2', 'tabulate>=0.9.0'] clipboard = ['PyQt5>=5.15.9', 'qtpy>=2.3.0'] compression = ['zstandard>=0.19.0'] consortium-standard = ['dataframe-api-compat>=0.1.7'] -timezone = ['pytz>=2020.1'] +timezone = ['pytz>=2023.3'] all = ['adbc-driver-postgresql>=0.8.0', 'adbc-driver-sqlite>=0.8.0', 'beautifulsoup4>=4.11.2', @@ -113,7 +113,7 @@ all = ['adbc-driver-postgresql>=0.8.0', 'pytest>=7.3.2', 'pytest-xdist>=2.2.0', 'python-calamine>=0.1.7', - 'pytz>=2020.1', + 'pytz>=2023.3', 'pyxlsb>=1.0.10', 'qtpy>=2.3.0', 'scipy>=1.10.0', diff --git a/requirements-dev.txt b/requirements-dev.txt index 7ab298ac27e28..6093983e28024 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,7 +37,7 @@ pymysql>=1.0.2 pyreadstat>=1.2.0 tables>=3.8.0 python-calamine>=0.1.7 -pytz>=2020.1 +pytz>=2023.3 pyxlsb>=1.0.10 s3fs>=2022.11.0 scipy>=1.10.0 From a2adcf1ff3d54f9280669b97e3f5177277a22c44 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:49:34 -0800 Subject: [PATCH 05/16] Add whatsnew, more docs --- doc/source/whatsnew/v3.0.0.rst | 36 ++++++++++++++++++++++++++++-- pandas/_libs/tslibs/nattype.pyx | 8 +++---- pandas/_libs/tslibs/timestamps.pyx | 8 +++---- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 26587bcb1da7d..130b667069c8b 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -75,12 +75,44 @@ Optional libraries below the lowest tested version may still work, but are not c +-----------------+---------------------+ | Package | New Minimum Version | +=================+=====================+ -| | | +| pytz | 2023.3 | +-----------------+---------------------+ See :ref:`install.dependencies` and :ref:`install.optional_dependencies` for more. -.. _whatsnew_300.api_breaking.other: +.. _whatsnew_300.api_breaking.pytz: + +``pytz`` now an optional dependency +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +pandas now uses :py:mod:`zoneinfo` from the standard library as the default timezone implementation when passing a timezone +string to various methods. (:issue:`34916`) + +*Old behavior:* + +.. code-block:: ipython + + In [1]: ts = pd.Timestamp(2024, 1, 1).tz_localize("US/Pacific") + In [2]: ts.tz + + +*New behavior:* + +.. ipython:: python + + ts = pd.Timestamp(2024, 1, 1).tz_localize("US/Pacific") + tz.tz + + +``pytz`` timezone objects are still supported when passed directly, but they will no longer be returned by default +from string inputs. Moreover, ``pytz`` is no longer a required dependency of pandas, but can be installed +with the pip extra ``pip install pandas[timezone]``. + + +Additionally, pandas no longer throws ``pytz`` exceptions for timezone operations leading to ambiguous or nonexistent +times. These operations will now yield :class:`errors.AmbiguousTimeError` and :class:`errors.NonexistentError` +and are accessible from :mod:`errors`. + Other API changes ^^^^^^^^^^^^^^^^^ diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 3e4ff5a923768..775457bed6df8 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -793,7 +793,7 @@ class NaTType(_NaT): Parameters ---------- - tz : str, pytz.timezone, dateutil.tz.tzfile or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile or None Time zone for time which Timestamp will be converted to. None will remove timezone holding UTC time. @@ -839,7 +839,7 @@ class NaTType(_NaT): ---------- ordinal : int Date corresponding to a proleptic Gregorian ordinal. - tz : str, pytz.timezone, dateutil.tz.tzfile or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile or None Time zone for the Timestamp. Notes @@ -1198,7 +1198,7 @@ timedelta}, default 'raise' Parameters ---------- - tz : str, pytz.timezone, dateutil.tz.tzfile or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile or None Time zone for time which Timestamp will be converted to. None will remove timezone holding UTC time. @@ -1245,7 +1245,7 @@ timedelta}, default 'raise' Parameters ---------- - tz : str, pytz.timezone, dateutil.tz.tzfile or None + tz : str, zoneinfo.ZoneInfo, , dateutil.tz.tzfile or None Time zone for time which Timestamp will be converted to. None will remove timezone holding local time. diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index e7b60033a0bd4..a3a1c3d0de9f1 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -1281,7 +1281,7 @@ class Timestamp(_Timestamp): hour, minute, second, microsecond : int, optional, default 0 tzinfo : datetime.tzinfo, optional, default None nanosecond : int, optional, default 0 - tz : str, pytz.timezone, dateutil.tz.tzfile or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile or None Time zone for time which Timestamp will have. unit : str Unit used for conversion if ts_input is of type int or float. The @@ -1343,7 +1343,7 @@ class Timestamp(_Timestamp): ---------- ordinal : int Date corresponding to a proleptic Gregorian ordinal. - tz : str, pytz.timezone, dateutil.tz.tzfile or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile or None Time zone for the Timestamp. Notes @@ -2223,7 +2223,7 @@ timedelta}, default 'raise' Parameters ---------- - tz : str, pytz.timezone, dateutil.tz.tzfile or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile or None Time zone for time which Timestamp will be converted to. None will remove timezone holding local time. @@ -2325,7 +2325,7 @@ default 'raise' Parameters ---------- - tz : str, pytz.timezone, dateutil.tz.tzfile or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile or None Time zone for time which Timestamp will be converted to. None will remove timezone holding UTC time. From 91c69ec68e3334f4fc82185431ff2ab1af54d0d2 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:31:05 -0800 Subject: [PATCH 06/16] FIx test errors --- pandas/_libs/tslibs/conversion.pyx | 22 +++++++-- pandas/_libs/tslibs/nattype.pyx | 2 +- pandas/io/json/_table_schema.py | 3 +- pandas/tests/dtypes/test_dtypes.py | 2 +- pandas/tests/frame/methods/test_join.py | 2 +- .../indexes/datetimes/methods/test_insert.py | 48 +++++++++++-------- .../indexes/datetimes/methods/test_shift.py | 5 +- .../datetimes/methods/test_tz_convert.py | 2 +- .../indexes/datetimes/test_constructors.py | 4 +- .../tests/indexes/datetimes/test_timezones.py | 2 +- pandas/tests/indexes/multi/test_reshape.py | 12 ++--- pandas/tests/io/test_parquet.py | 4 +- pandas/tests/resample/test_datetime_index.py | 4 +- pandas/tests/resample/test_period_index.py | 2 - .../reshape/concat/test_append_common.py | 6 +-- .../methods/test_timestamp_method.py | 4 +- pandas/tests/tseries/holiday/test_holiday.py | 2 +- pandas/tests/tseries/offsets/test_dst.py | 18 +++++-- pandas/tests/tslibs/test_api.py | 2 + pandas/tests/tslibs/test_timezones.py | 6 +-- 20 files changed, 93 insertions(+), 59 deletions(-) diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index 3a55f5fa0c003..07e19aa773e01 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -59,6 +59,10 @@ from pandas._libs.tslibs.np_datetime cimport ( import_pandas_datetime() from pandas._libs.tslibs.np_datetime import OutOfBoundsDatetime +from pandas._libs.tslibs.tzconversion import ( + AmbiguousTimeError, + NonExistentTimeError, +) from pandas._libs.tslibs.nattype cimport ( NPY_NAT, @@ -69,6 +73,7 @@ from pandas._libs.tslibs.timestamps cimport _Timestamp from pandas._libs.tslibs.timezones cimport ( get_utcoffset, is_utc, + treat_tz_as_pytz, ) from pandas._libs.tslibs.tzconversion cimport ( Localizer, @@ -742,11 +747,18 @@ cdef datetime _localize_pydatetime(datetime dt, tzinfo tz): identically, i.e. discards nanos from Timestamps. It also assumes that the `tz` input is not None. """ - try: - # datetime.replace with pytz may be incorrect result - # TODO: try to respect `fold` attribute - return tz.localize(dt, is_dst=None) - except AttributeError: + if treat_tz_as_pytz(tz): + import pytz + try: + # datetime.replace with pytz may be incorrect result + # TODO: try to respect `fold` attribute + return tz.localize(dt, is_dst=None) + except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError) as err: + if isinstance(err, pytz.AmbiguousTimeError): + raise AmbiguousTimeError(str(err)) from err + else: + raise NonExistentTimeError(str(err)) from err + else: return dt.replace(tzinfo=tz) diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 775457bed6df8..8f80369107ff4 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -1245,7 +1245,7 @@ timedelta}, default 'raise' Parameters ---------- - tz : str, zoneinfo.ZoneInfo, , dateutil.tz.tzfile or None + tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile or None Time zone for time which Timestamp will be converted to. None will remove timezone holding local time. diff --git a/pandas/io/json/_table_schema.py b/pandas/io/json/_table_schema.py index c279eeea78c6b..072f729c22aa8 100644 --- a/pandas/io/json/_table_schema.py +++ b/pandas/io/json/_table_schema.py @@ -147,8 +147,7 @@ def convert_pandas_type_to_json_field(arr) -> dict[str, JSONSerializable]: # timezone.utc has no "zone" attr field["tz"] = "UTC" else: - # error: "tzinfo" has no attribute "zone" - field["tz"] = dtype.tz.zone # type: ignore[attr-defined] + field["tz"] = str(dtype.tz) elif isinstance(dtype, ExtensionDtype): field["extDtype"] = dtype.name return field diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index f3466d53a1edc..255f40b65b20b 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -392,7 +392,7 @@ def test_tz_standardize(self): # GH 24713 pytz = pytest.importorskip("pytz") tz = pytz.timezone("US/Eastern") - dr = date_range("2013-01-01", periods=3, tz="US/Eastern") + dr = date_range("2013-01-01", periods=3, tz=tz) dtype = DatetimeTZDtype("ns", dr.tz) assert dtype.tz == tz dtype = DatetimeTZDtype("ns", dr[0].tz) diff --git a/pandas/tests/frame/methods/test_join.py b/pandas/tests/frame/methods/test_join.py index 82802dd6e99eb..90ce9f27184cb 100644 --- a/pandas/tests/frame/methods/test_join.py +++ b/pandas/tests/frame/methods/test_join.py @@ -561,4 +561,4 @@ def test_frame_join_tzaware(self): expected = test1.index.union(test2.index) tm.assert_index_equal(result.index, expected) - assert result.index.tz.zone == "US/Central" + assert result.index.tz.key == "US/Central" diff --git a/pandas/tests/indexes/datetimes/methods/test_insert.py b/pandas/tests/indexes/datetimes/methods/test_insert.py index b3c400b8cf97b..4a5b7bcc1a86f 100644 --- a/pandas/tests/indexes/datetimes/methods/test_insert.py +++ b/pandas/tests/indexes/datetimes/methods/test_insert.py @@ -134,7 +134,12 @@ def test_insert3(self, unit): assert result.freq is None @pytest.mark.parametrize("tz", ["US/Pacific", "Asia/Singapore"]) - def test_insert4(self, unit, tz): + @pytest.mark.parametrize( + "to_ts", + [lambda x: x, lambda x: x.to_pydatetime()], + ids=["Timestamp", "datetime"], + ) + def test_insert4(self, unit, tz, to_ts): idx = date_range( "1/1/2000 09:00", periods=6, freq="h", tz=tz, name="idx", unit=unit ) @@ -142,16 +147,24 @@ def test_insert4(self, unit, tz): expected = date_range( "1/1/2000 09:00", periods=7, freq="h", tz=tz, name="idx", unit=unit ) - for d in [ - Timestamp("2000-01-01 15:00", tz=tz), - datetime(2000, 1, 1, 15).astimezone(zoneinfo.ZoneInfo(tz)), - ]: - result = idx.insert(6, d) - tm.assert_index_equal(result, expected) - assert result.name == expected.name - assert result.freq == expected.freq - assert result.tz == expected.tz + tz = zoneinfo.ZoneInfo(tz) + d = to_ts(Timestamp("2000-01-01 15:00", tz=tz)) + result = idx.insert(6, d) + tm.assert_index_equal(result, expected) + assert result.name == expected.name + assert result.freq == expected.freq + assert result.tz == expected.tz + @pytest.mark.parametrize("tz", ["US/Pacific", "Asia/Singapore"]) + @pytest.mark.parametrize( + "to_ts", + [lambda x: x, lambda x: x.to_pydatetime()], + ids=["Timestamp", "datetime"], + ) + def test_insert4_no_freq(self, unit, tz, to_ts): + idx = date_range( + "1/1/2000 09:00", periods=6, freq="h", tz=tz, name="idx", unit=unit + ) expected = DatetimeIndex( [ "2000-01-01 09:00", @@ -167,15 +180,12 @@ def test_insert4(self, unit, tz): freq=None, ).as_unit(unit) # reset freq to None - for d in [ - Timestamp("2000-01-01 10:00", tz=tz), - datetime(2000, 1, 1, 10).astimezone(zoneinfo.ZoneInfo(tz)), - ]: - result = idx.insert(6, d) - tm.assert_index_equal(result, expected) - assert result.name == expected.name - assert result.tz == expected.tz - assert result.freq is None + d = to_ts(Timestamp("2000-01-01 10:00", tz=tz)) + result = idx.insert(6, d) + tm.assert_index_equal(result, expected) + assert result.name == expected.name + assert result.tz == expected.tz + assert result.freq is None # TODO: also changes DataFrame.__setitem__ with expansion def test_insert_mismatched_tzawareness(self): diff --git a/pandas/tests/indexes/datetimes/methods/test_shift.py b/pandas/tests/indexes/datetimes/methods/test_shift.py index e6a2cb668f3ec..16f8200027a59 100644 --- a/pandas/tests/indexes/datetimes/methods/test_shift.py +++ b/pandas/tests/indexes/datetimes/methods/test_shift.py @@ -120,11 +120,12 @@ def test_dti_shift_across_dst(self, unit): ) def test_dti_shift_near_midnight(self, shift, result_time, unit): # GH 8616 - dt_est = datetime(2014, 11, 14, 0).astimezone(zoneinfo.ZoneInfo("US/Eastern")) + tz = zoneinfo.ZoneInfo("US/Eastern") + dt_est = datetime(2014, 11, 14, 0, tzinfo=tz) idx = DatetimeIndex([dt_est]).as_unit(unit) ser = Series(data=[1], index=idx) result = ser.shift(shift, freq="h") - exp_index = DatetimeIndex([result_time], tz="EST").as_unit(unit) + exp_index = DatetimeIndex([result_time], tz=tz).as_unit(unit) expected = Series(1, index=exp_index) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/indexes/datetimes/methods/test_tz_convert.py b/pandas/tests/indexes/datetimes/methods/test_tz_convert.py index 4755dfa06f300..9eabb742b93a4 100644 --- a/pandas/tests/indexes/datetimes/methods/test_tz_convert.py +++ b/pandas/tests/indexes/datetimes/methods/test_tz_convert.py @@ -264,7 +264,7 @@ def test_dti_tz_convert_tzlocal(self): ], ) def test_dti_tz_convert_utc_to_local_no_modify(self, tz): - if tz.startswith("pytz/"): + if isinstance(tz, str) and tz.startswith("pytz/"): pytz = pytest.importorskip("pytz") tz = pytz.timezone(tz.removeprefix("pytz/")) rng = date_range("3/11/2012", "3/12/2012", freq="h", tz="utc") diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index a82c58acdf8d3..cff045d35401a 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -175,7 +175,7 @@ def test_construction_caching(self): "dtns": date_range("20130101", periods=3, freq="ns"), } ) - assert df.dttz.dtype.tz.zone == "US/Eastern" + assert df.dttz.dtype.tz.key == "US/Eastern" @pytest.mark.parametrize( "kwargs", @@ -212,7 +212,7 @@ def test_construction_with_alt_tz_localize(self, kwargs, tz_aware_fixture): # incompat tz/dtype msg = "cannot supply both a tz and a dtype with a tz" with pytest.raises(ValueError, match=msg): - DatetimeIndex(i.tz_localize(None).asi8, dtype=i.dtype, tz="US/Pacific") + DatetimeIndex(i.tz_localize(None).asi8, dtype=i.dtype, tz="US/Hawaii") def test_construction_index_with_mixed_timezones(self): # gh-11488: no tz results in DatetimeIndex diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index 21ab5b0390359..622a35c802286 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -201,7 +201,7 @@ def test_utc_box_timestamp_and_localize(self, tzstr): rng_eastern = rng.tz_convert(tzstr) # test not valid for dateutil timezones. # assert 'EDT' in repr(rng_eastern[0].tzinfo) - assert "EDT" in repr(rng_eastern[0].tzinfo) or "tzfile" in repr( + assert "US/Eastern" in repr(rng_eastern[0].tzinfo) or "tzfile" in repr( rng_eastern[0].tzinfo ) diff --git a/pandas/tests/indexes/multi/test_reshape.py b/pandas/tests/indexes/multi/test_reshape.py index 2a0749ec3385b..cc3dadc6bb61c 100644 --- a/pandas/tests/indexes/multi/test_reshape.py +++ b/pandas/tests/indexes/multi/test_reshape.py @@ -116,9 +116,9 @@ def test_append_index(): # see gh-7112 tz = zoneinfo.ZoneInfo("Asia/Tokyo") expected_tuples = [ - (1.1, datetime(2011, 1, 1).astimezone(tz)), - (1.2, datetime(2011, 1, 2).astimezone(tz)), - (1.3, datetime(2011, 1, 3).astimezone(tz)), + (1.1, datetime(2011, 1, 1, tzinfo=tz)), + (1.2, datetime(2011, 1, 2, tzinfo=tz)), + (1.3, datetime(2011, 1, 3, tzinfo=tz)), ] expected = Index([1.1, 1.2, 1.3] + expected_tuples) tm.assert_index_equal(result, expected) @@ -138,9 +138,9 @@ def test_append_index(): expected = Index._simple_new( np.array( [ - (1.1, tz.localize(datetime(2011, 1, 1)), "A"), - (1.2, tz.localize(datetime(2011, 1, 2)), "B"), - (1.3, tz.localize(datetime(2011, 1, 3)), "C"), + (1.1, datetime(2011, 1, 1, tzinfo=tz), "A"), + (1.2, datetime(2011, 1, 2, tzinfo=tz), "B"), + (1.3, datetime(2011, 1, 3, tzinfo=tz), "C"), ] + expected_tuples, dtype=object, diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py index 83a962ec26a7e..17655734099d2 100644 --- a/pandas/tests/io/test_parquet.py +++ b/pandas/tests/io/test_parquet.py @@ -1147,9 +1147,11 @@ def test_infer_string_large_string_type(self, tmp_path, pa): class TestParquetFastParquet(Base): def test_basic(self, fp, df_full): + pytz = pytest.importorskip("pytz") + tz = pytz.timezone("US/Eastern") df = df_full - dti = pd.date_range("20130101", periods=3, tz="US/Eastern") + dti = pd.date_range("20130101", periods=3, tz=tz) dti = dti._with_freq(None) # freq doesn't round-trip df["datetime_tz"] = dti df["timedelta"] = pd.timedelta_range("1 day", periods=3) diff --git a/pandas/tests/resample/test_datetime_index.py b/pandas/tests/resample/test_datetime_index.py index 17781e9fe2735..7d93cd78a7030 100644 --- a/pandas/tests/resample/test_datetime_index.py +++ b/pandas/tests/resample/test_datetime_index.py @@ -1717,11 +1717,11 @@ def test_downsample_across_dst(unit): # GH 8531 tz = zoneinfo.ZoneInfo("Europe/Berlin") dt = datetime(2014, 10, 26) - dates = date_range(tz.localize(dt), periods=4, freq="2h").as_unit(unit) + dates = date_range(dt.astimezone(tz), periods=4, freq="2h").as_unit(unit) result = Series(5, index=dates).resample("h").mean() expected = Series( [5.0, np.nan] * 3 + [5.0], - index=date_range(tz.localize(dt), periods=7, freq="h").as_unit(unit), + index=date_range(dt.astimezone(tz), periods=7, freq="h").as_unit(unit), ) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index 868730a6b30b6..562cc53c601dc 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -366,8 +366,6 @@ def test_resample_with_tz(self, tz, unit): index=exp_dti, ) tm.assert_series_equal(result, expected) - # Especially assert that the timezone is LMT - assert result.index.tz == tz def test_resample_nonexistent_time_bin_edge(self): # GH 19375 diff --git a/pandas/tests/reshape/concat/test_append_common.py b/pandas/tests/reshape/concat/test_append_common.py index 31c3ef3176222..d355dde641dbb 100644 --- a/pandas/tests/reshape/concat/test_append_common.py +++ b/pandas/tests/reshape/concat/test_append_common.py @@ -353,14 +353,14 @@ def test_concatlike_datetimetz_to_object(self, tz_aware_fixture): tm.assert_series_equal(res, Series(exp, index=[0, 1, 0, 1])) # different tz - dti3 = pd.DatetimeIndex(["2012-01-01", "2012-01-02"], tz="US/Pacific") + dti3 = pd.DatetimeIndex(["2012-01-01", "2012-01-02"], tz="US/Hawaii") exp = Index( [ pd.Timestamp("2011-01-01", tz=tz), pd.Timestamp("2011-01-02", tz=tz), - pd.Timestamp("2012-01-01", tz="US/Pacific"), - pd.Timestamp("2012-01-02", tz="US/Pacific"), + pd.Timestamp("2012-01-01", tz="US/Hawaii"), + pd.Timestamp("2012-01-02", tz="US/Hawaii"), ], dtype=object, ) diff --git a/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py b/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py index 6465cb116774f..b27d30e7a0ace 100644 --- a/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py +++ b/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py @@ -1,7 +1,7 @@ # NB: This is for the Timestamp.timestamp *method* specifically, not # the Timestamp class in general. -from datetime.timezone import utc +from datetime import timezone from pandas._libs.tslibs import Timestamp import pandas.util._test_decorators as td @@ -15,7 +15,7 @@ def test_timestamp(self, fixed_now_ts): # GH#17329 # tz-naive --> treat it as if it were UTC for purposes of timestamp() ts = fixed_now_ts - uts = ts.replace(tzinfo=utc) + uts = ts.replace(tzinfo=timezone.utc) assert ts.timestamp() == uts.timestamp() tsc = Timestamp("2014-10-11 11:00:01.12345678", tz="US/Central") diff --git a/pandas/tests/tseries/holiday/test_holiday.py b/pandas/tests/tseries/holiday/test_holiday.py index 232ebcd0544c2..7854aa622639e 100644 --- a/pandas/tests/tseries/holiday/test_holiday.py +++ b/pandas/tests/tseries/holiday/test_holiday.py @@ -133,7 +133,7 @@ def test_holiday_dates(holiday, start_date, end_date, expected): holiday.dates( Timestamp(start_date, tz=timezone.utc), Timestamp(end_date, tz=timezone.utc) ) - ) == [dt.astimezone(timezone.utc) for dt in expected] + ) == [dt.replace(tzinfo=timezone.utc) for dt in expected] @pytest.mark.parametrize( diff --git a/pandas/tests/tseries/offsets/test_dst.py b/pandas/tests/tseries/offsets/test_dst.py index c8ccbcfcd3bcf..393346cf80193 100644 --- a/pandas/tests/tseries/offsets/test_dst.py +++ b/pandas/tests/tseries/offsets/test_dst.py @@ -36,6 +36,8 @@ from pandas import DatetimeIndex import pandas._testing as tm +pytz = pytest.importorskip("pytz") + def get_utc_offset_hours(ts): # take a Timestamp and compute total hours of utc offset @@ -50,7 +52,10 @@ class TestDST: # test both basic names and dateutil timezones timezone_utc_offsets = { - "US/Eastern": {"utc_offset_daylight": -4, "utc_offset_standard": -5}, + pytz.timezone("US/Eastern"): { + "utc_offset_daylight": -4, + "utc_offset_standard": -5, + }, "dateutil/US/Pacific": {"utc_offset_daylight": -7, "utc_offset_standard": -8}, } valid_date_offsets_singular = [ @@ -87,7 +92,10 @@ def _test_offset(self, offset_name, offset_n, tstart, expected_utc_offset): if ( offset_name in ["hour", "minute", "second", "microsecond"] and offset_n == 1 - and tstart == Timestamp("2013-11-03 01:59:59.999999-0500", tz="US/Eastern") + and tstart + == Timestamp( + "2013-11-03 01:59:59.999999-0500", tz=pytz.timezone("US/Eastern") + ) ): # This addition results in an ambiguous wall time err_msg = { @@ -138,7 +146,9 @@ def _test_offset(self, offset_name, offset_n, tstart, expected_utc_offset): assert datepart_offset == offset.kwds[offset_name] else: # the offset should be the same as if it was done in UTC - assert t == (tstart.tz_convert("UTC") + offset).tz_convert("US/Pacific") + assert t == (tstart.tz_convert("UTC") + offset).tz_convert( + pytz.timezone("US/Pacific") + ) def _make_timestamp(self, string, hrs_offset, tz): if hrs_offset >= 0: @@ -247,7 +257,7 @@ def test_all_offset_classes(self, tup): def test_nontick_offset_with_ambiguous_time_error(original_dt, target_dt, offset, tz): # .apply for non-Tick offsets throws AmbiguousTimeError when the target dt # is dst-ambiguous - localized_dt = original_dt.tz_localize(tz) + localized_dt = original_dt.tz_localize(pytz.timezone(tz)) msg = f"Cannot infer dst time from {target_dt}, try using the 'ambiguous' argument" with pytest.raises(AmbiguousTimeError, match=msg): diff --git a/pandas/tests/tslibs/test_api.py b/pandas/tests/tslibs/test_api.py index 42d055326c2a5..e13e595304938 100644 --- a/pandas/tests/tslibs/test_api.py +++ b/pandas/tests/tslibs/test_api.py @@ -24,6 +24,8 @@ def test_namespace(): ] api = [ + "AmbiguousTimeError", + "NonExistentTimeError", "BaseOffset", "NaT", "NaTType", diff --git a/pandas/tests/tslibs/test_timezones.py b/pandas/tests/tslibs/test_timezones.py index 5564720d70982..0cc64860d437f 100644 --- a/pandas/tests/tslibs/test_timezones.py +++ b/pandas/tests/tslibs/test_timezones.py @@ -116,8 +116,8 @@ def test_infer_tz_utc_localize(infer_setup): _, _, start, end, start_naive, end_naive = infer_setup utc = timezone.utc - start = utc.localize(start_naive) - end = utc.localize(end_naive) + start = start_naive.astimezone(utc) + end = end_naive.astimezone(utc) assert timezones.infer_tzinfo(start, end) is utc @@ -128,7 +128,7 @@ def test_infer_tz_mismatch(infer_setup, ordered): msg = "Inputs must both have the same timezone" utc = timezone.utc - start = utc.localize(start_naive) + start = start_naive.astimezone(utc) end = conversion.localize_pydatetime(end_naive, eastern) args = (start, end) if ordered else (end, start) From 60c3f91ea01aa7ecc29b36ad3fa0be56ecb53a3f Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:35:37 -0800 Subject: [PATCH 07/16] FIx some tests --- pandas/tests/frame/test_constructors.py | 3 +-- pandas/tests/tseries/holiday/test_holiday.py | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 392feb27f35ab..82067a78b51d5 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -1923,8 +1923,7 @@ def test_constructor_with_datetimes2(self): def test_constructor_with_datetimes3(self): # GH 7594 # don't coerce tz-aware - tz = zoneinfo.ZoneInfo("US/Eastern") - dt = tz.localize(datetime(2012, 1, 1)) + dt = datetime(2012, 1, 1, tzinfo=zoneinfo.ZoneInfo("US/Eastern")) df = DataFrame({"End Date": dt}, index=[0]) assert df.iat[0, 0] == dt diff --git a/pandas/tests/tseries/holiday/test_holiday.py b/pandas/tests/tseries/holiday/test_holiday.py index 7854aa622639e..b6af8425b0add 100644 --- a/pandas/tests/tseries/holiday/test_holiday.py +++ b/pandas/tests/tseries/holiday/test_holiday.py @@ -4,7 +4,6 @@ ) import pytest -from pytz import utc from pandas import ( DatetimeIndex, @@ -197,8 +196,10 @@ def test_holidays_within_dates(holiday, start, expected): # Verify that timezone info is preserved. assert list( - holiday.dates(utc.localize(Timestamp(start)), utc.localize(Timestamp(start))) - ) == [utc.localize(dt) for dt in expected] + holiday.dates( + Timestamp(start, tz=timezone.utc), Timestamp(start, tz=timezone.utc) + ) + ) == [dt.replace(tzinfo=timezone.utc) for dt in expected] @pytest.mark.parametrize( From 40900de7139a9edc0579f3c2b6147f3041d91613 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:46:00 -0800 Subject: [PATCH 08/16] Fix more tests --- pandas/tests/frame/methods/test_tz_convert.py | 4 ++-- .../scalar/timestamp/methods/test_tz_localize.py | 11 ++++++----- pandas/tests/scalar/timestamp/test_constructors.py | 4 ---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pandas/tests/frame/methods/test_tz_convert.py b/pandas/tests/frame/methods/test_tz_convert.py index 90bec4dfb5be6..c6a3cd5bbf57f 100644 --- a/pandas/tests/frame/methods/test_tz_convert.py +++ b/pandas/tests/frame/methods/test_tz_convert.py @@ -22,7 +22,7 @@ def test_tz_convert(self, frame_or_series): expected = DataFrame({"a": 1}, rng.tz_convert("Europe/Berlin")) expected = tm.get_obj(expected, frame_or_series) - assert result.index.tz.zone == "Europe/Berlin" + assert result.index.tz.key == "Europe/Berlin" tm.assert_equal(result, expected) def test_tz_convert_axis1(self): @@ -32,7 +32,7 @@ def test_tz_convert_axis1(self): obj = obj.T result = obj.tz_convert("Europe/Berlin", axis=1) - assert result.columns.tz.zone == "Europe/Berlin" + assert result.columns.tz.key == "Europe/Berlin" expected = DataFrame({"a": 1}, rng.tz_convert("Europe/Berlin")) diff --git a/pandas/tests/scalar/timestamp/methods/test_tz_localize.py b/pandas/tests/scalar/timestamp/methods/test_tz_localize.py index 6b1f53337d2ef..ff8d44bbbe47a 100644 --- a/pandas/tests/scalar/timestamp/methods/test_tz_localize.py +++ b/pandas/tests/scalar/timestamp/methods/test_tz_localize.py @@ -22,28 +22,29 @@ class TestTimestampTZLocalize: def test_tz_localize_pushes_out_of_bounds(self): # GH#12677 # tz_localize that pushes away from the boundary is OK + pytz = pytest.importorskip("pytz") msg = ( f"Converting {Timestamp.min.strftime('%Y-%m-%d %H:%M:%S')} " f"underflows past {Timestamp.min}" ) - pac = Timestamp.min.tz_localize("US/Pacific") + pac = Timestamp.min.tz_localize(pytz.timezone("US/Pacific")) assert pac._value > Timestamp.min._value pac.tz_convert("Asia/Tokyo") # tz_convert doesn't change value with pytest.raises(OutOfBoundsDatetime, match=msg): - Timestamp.min.tz_localize("Asia/Tokyo") + Timestamp.min.tz_localize(pytz.timezone("Asia/Tokyo")) # tz_localize that pushes away from the boundary is OK msg = ( f"Converting {Timestamp.max.strftime('%Y-%m-%d %H:%M:%S')} " f"overflows past {Timestamp.max}" ) - tokyo = Timestamp.max.tz_localize("Asia/Tokyo") + tokyo = Timestamp.max.tz_localize(pytz.timezone("Asia/Tokyo")) assert tokyo._value < Timestamp.max._value tokyo.tz_convert("US/Pacific") # tz_convert doesn't change value with pytest.raises(OutOfBoundsDatetime, match=msg): - Timestamp.max.tz_localize("US/Pacific") + Timestamp.max.tz_localize(pytz.timezone("US/Pacific")) - @pytest.mark.parameterize( + @pytest.mark.parametrize( "tz", ["US/Central", "dateutil/US/Central", "pytz/US/Central"] ) def test_tz_localize_ambiguous_bool(self, unit, tz): diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py index b21af063c8d5d..3b5cba5add312 100644 --- a/pandas/tests/scalar/timestamp/test_constructors.py +++ b/pandas/tests/scalar/timestamp/test_constructors.py @@ -169,7 +169,6 @@ def test_timestamp_constructor_retain_fold(self, tz, fold): [ "dateutil/Europe/London", "Europe/London", - "pytz/Europe/London", ], ) @pytest.mark.parametrize( @@ -187,9 +186,6 @@ def test_timestamp_constructor_infer_fold_from_value(self, tz, ts_input, fold_ou # Test for GH#25057 # Check that we infer fold correctly based on timestamps since utc # or strings - if isinstance(tz, str) and tz.startswith("pytz/"): - pytz = pytest.importorskip("pytz") - tz = pytz.timezone(tz.removeprefix("pytz/")) ts = Timestamp(ts_input, tz=tz) result = ts.fold expected = fold_out From b8edf3d08ceb67995e8a579cbb6a99b291ddfb34 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:35:46 -0800 Subject: [PATCH 09/16] A few more tests --- pandas/tests/io/test_feather.py | 2 +- pandas/tests/io/test_pickle.py | 1 + pandas/tests/test_downstream.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/test_feather.py b/pandas/tests/io/test_feather.py index 22a7d3b83a459..0d82d83d3262e 100644 --- a/pandas/tests/io/test_feather.py +++ b/pandas/tests/io/test_feather.py @@ -90,7 +90,7 @@ def test_basic(self): df["timedeltas"] = pd.timedelta_range("1 day", periods=3) df["intervals"] = pd.interval_range(0, 3, 3) - assert df.dttz.dtype.tz.zone == "US/Eastern" + assert df.dttz.dtype.tz.key == "US/Eastern" expected = df.copy() expected.loc[1, "bool_with_null"] = None diff --git a/pandas/tests/io/test_pickle.py b/pandas/tests/io/test_pickle.py index f6c7f66abe5d3..a27f2269e2589 100644 --- a/pandas/tests/io/test_pickle.py +++ b/pandas/tests/io/test_pickle.py @@ -110,6 +110,7 @@ def test_flatten_buffer(data): def test_pickles(datapath): + pytest.importorskip("pytz") if not is_platform_little_endian(): pytest.skip("known failure on non-little endian") diff --git a/pandas/tests/test_downstream.py b/pandas/tests/test_downstream.py index 10776fe5d050f..da432117e3570 100644 --- a/pandas/tests/test_downstream.py +++ b/pandas/tests/test_downstream.py @@ -219,7 +219,7 @@ def test_missing_required_dependency(): subprocess.check_output(call, stderr=subprocess.STDOUT) output = exc.value.stdout.decode() - for name in ["numpy", "pytz", "dateutil"]: + for name in ["numpy", "dateutil"]: assert name in output From c7b6a1375d62bdfaf907752a41160e6d7c229d57 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 30 Jan 2024 18:53:45 -0800 Subject: [PATCH 10/16] Fix doc and typing warnings --- doc/source/user_guide/io.rst | 4 +++- doc/source/user_guide/timeseries.rst | 16 ++++++++-------- doc/source/whatsnew/v3.0.0.rst | 2 +- pandas/_libs/tslibs/tzconversion.pyi | 3 +++ pandas/conftest.py | 7 +++++-- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst index 3bbfa6b04fb30..c8300302fab7a 100644 --- a/doc/source/user_guide/io.rst +++ b/doc/source/user_guide/io.rst @@ -5358,6 +5358,8 @@ See the documentation for `pyarrow `__ an .. ipython:: python + import pytz + df = pd.DataFrame( { "a": list("abc"), @@ -5366,7 +5368,7 @@ See the documentation for `pyarrow `__ an "d": np.arange(4.0, 7.0, dtype="float64"), "e": [True, False, True], "f": pd.date_range("20130101", periods=3), - "g": pd.date_range("20130101", periods=3, tz="US/Eastern"), + "g": pd.date_range("20130101", periods=3, tz=pytz.timezone("US/Eastern")), "h": pd.Categorical(list("abc")), "i": pd.Categorical(list("abc"), ordered=True), } diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst index c81baae48f6db..48ea51218c123 100644 --- a/doc/source/user_guide/timeseries.rst +++ b/doc/source/user_guide/timeseries.rst @@ -2344,15 +2344,13 @@ To localize these dates to a time zone (assign a particular time zone to a naive you can use the ``tz_localize`` method or the ``tz`` keyword argument in :func:`date_range`, :class:`Timestamp`, or :class:`DatetimeIndex`. You can either pass ``zoneinfo``, ``pytz`` or ``dateutil`` time zone objects or Olson time zone database strings. -Olson time zone strings will return ``zoneinfo.ZoneInfo`` time zone objects by default. +Olson time zone strings will return :py:class:`zoneinfo.ZoneInfo` time zone objects by default. To return ``dateutil`` time zone objects, append ``dateutil/`` before the string. -* For ``zoneinfo``, a list of available timezones are available from -``zoneinfo.available_timezones()``. -* In ``pytz`` you can find a list of common (and less common) time zones using - ``from pytz import common_timezones, all_timezones``. +* For ``zoneinfo``, a list of available timezones are available from ``zoneinfo.available_timezones()``. +* In ``pytz`` you can find a list of common (and less common) time zones using ``pytz.all_timezones``. * ``dateutil`` uses the OS time zones so there isn't a fixed list available. For - common zones, the names are the same as ``pytz``. + common zones, the names are the same as ``pytz`` and ``zoneinfo``. .. ipython:: python @@ -2457,7 +2455,7 @@ you can use the ``tz_convert`` method. .. warning:: - If you are using dates beyond 2038-01-18, due to current deficiencies + If you are using dates beyond 2038-01-18 with ``pytz``, due to current deficiencies in the underlying libraries caused by the year 2038 problem, daylight saving time (DST) adjustments to timezone aware dates will not be applied. If and when the underlying libraries are fixed, the DST transitions will be applied. @@ -2466,9 +2464,11 @@ you can use the ``tz_convert`` method. .. ipython:: python + import pytz + d_2037 = "2037-03-31T010101" d_2038 = "2038-03-31T010101" - DST = "Europe/London" + DST = pytz.timezone("Europe/London") assert pd.Timestamp(d_2037, tz=DST) != pd.Timestamp(d_2037, tz="GMT") assert pd.Timestamp(d_2038, tz=DST) == pd.Timestamp(d_2038, tz="GMT") diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index ee654c0813122..376043e6da733 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -101,7 +101,7 @@ string to various methods. (:issue:`34916`) .. ipython:: python ts = pd.Timestamp(2024, 1, 1).tz_localize("US/Pacific") - tz.tz + ts.tz ``pytz`` timezone objects are still supported when passed directly, but they will no longer be returned by default diff --git a/pandas/_libs/tslibs/tzconversion.pyi b/pandas/_libs/tslibs/tzconversion.pyi index 2108fa0f35547..4d60a2481e2c7 100644 --- a/pandas/_libs/tslibs/tzconversion.pyi +++ b/pandas/_libs/tslibs/tzconversion.pyi @@ -8,6 +8,9 @@ import numpy as np from pandas._typing import npt +class AmbiguousTimeError(Exception): ... +class NonExistentTimeError(Exception): ... + # tz_convert_from_utc_single exposed for testing def tz_convert_from_utc_single( utc_val: np.int64, tz: tzinfo, creso: int = ... diff --git a/pandas/conftest.py b/pandas/conftest.py index 6fb05969baee4..3fbada71bfcf7 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -33,6 +33,7 @@ from typing import ( TYPE_CHECKING, Callable, + Module, ) from dateutil.tz import ( @@ -90,6 +91,8 @@ del pa has_pyarrow = True + +pytz: Module | None try: import pytz except ImportError: @@ -1207,7 +1210,7 @@ def deco(*args): ] if pytz is not None: - TIMEZONES.extend( + TIMEZONES.extend( # type: ignore[arg-type] ( pytz.FixedOffset(300), pytz.FixedOffset(0), @@ -1988,7 +1991,7 @@ def using_infer_string() -> bool: _warsaws = ["Europe/Warsaw", "dateutil/Europe/Warsaw"] if pytz is not None: - _warsaws.append(pytz.timezone("Europe/Warsaw")) + _warsaws.append(pytz.timezone("Europe/Warsaw")) # type: ignore[arg-type] @pytest.fixture(params=_warsaws) From a7a6395897eee7f70944a65242af3a1b6d53475b Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:23:53 -0800 Subject: [PATCH 11/16] Fix typing --- pandas/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 3fbada71bfcf7..e706cd140e4d6 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -33,7 +33,6 @@ from typing import ( TYPE_CHECKING, Callable, - Module, ) from dateutil.tz import ( @@ -82,6 +81,7 @@ Hashable, Iterator, ) + from types import ModuleType try: import pyarrow as pa @@ -92,7 +92,7 @@ has_pyarrow = True -pytz: Module | None +pytz: ModuleType | None try: import pytz except ImportError: From e5d6ec7f31f12030bd5e3f2a8a52a5b2ac060d47 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:16:08 -0800 Subject: [PATCH 12/16] Remove ignores --- pandas/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 093dedb8bb897..7f8daeb88fd34 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -1208,7 +1208,7 @@ def deco(*args): ] if pytz is not None: - TIMEZONES.extend( # type: ignore[arg-type] + TIMEZONES.extend( ( pytz.FixedOffset(300), pytz.FixedOffset(0), @@ -1983,7 +1983,7 @@ def using_infer_string() -> bool: _warsaws = ["Europe/Warsaw", "dateutil/Europe/Warsaw"] if pytz is not None: - _warsaws.append(pytz.timezone("Europe/Warsaw")) # type: ignore[arg-type] + _warsaws.append(pytz.timezone("Europe/Warsaw")) @pytest.fixture(params=_warsaws) From 58b6271207df3ea9319babf227ca9901f4e64151 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:21:56 -0800 Subject: [PATCH 13/16] Fix pylint, timezone test --- pandas/compat/pyarrow.py | 2 ++ pandas/conftest.py | 3 ++- pandas/tests/io/test_parquet.py | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pandas/compat/pyarrow.py b/pandas/compat/pyarrow.py index 2e151123ef2c9..d6276c5582905 100644 --- a/pandas/compat/pyarrow.py +++ b/pandas/compat/pyarrow.py @@ -16,6 +16,7 @@ pa_version_under14p0 = _palv < Version("14.0.0") pa_version_under14p1 = _palv < Version("14.0.1") pa_version_under15p0 = _palv < Version("15.0.0") + pa_version_under16p0 = _palv < Version("16.0.0") except ImportError: pa_not_found = True pa_version_under10p1 = True @@ -25,3 +26,4 @@ pa_version_under14p0 = True pa_version_under14p1 = True pa_version_under15p0 = True + pa_version_under16p0 = True diff --git a/pandas/conftest.py b/pandas/conftest.py index 78f63d11ca607..672c71cffb6bf 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -32,6 +32,7 @@ import os from typing import ( TYPE_CHECKING, + Any, Callable, ) @@ -1980,7 +1981,7 @@ def using_infer_string() -> bool: return pd.options.future.infer_string is True -_warsaws = ["Europe/Warsaw", "dateutil/Europe/Warsaw"] +_warsaws: list[Any] = ["Europe/Warsaw", "dateutil/Europe/Warsaw"] if pytz is not None: _warsaws.append(pytz.timezone("Europe/Warsaw")) diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py index f857c23965752..7c30254ba1330 100644 --- a/pandas/tests/io/test_parquet.py +++ b/pandas/tests/io/test_parquet.py @@ -15,6 +15,7 @@ pa_version_under11p0, pa_version_under13p0, pa_version_under15p0, + pa_version_under16p0, ) import pandas as pd @@ -961,7 +962,10 @@ def test_timestamp_nanoseconds(self, pa): check_round_trip(df, pa, write_kwargs={"version": ver}) def test_timezone_aware_index(self, request, pa, timezone_aware_date_list): - if timezone_aware_date_list.tzinfo != datetime.timezone.utc: + if ( + timezone_aware_date_list.tzinfo != datetime.timezone.utc + and pa_version_under16p0 + ): request.applymarker( pytest.mark.xfail( reason="temporary skip this test until it is properly resolved: " From 0911f0a46c2ee84c051f9e4389b1b3ab6cfe00f1 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:07:29 -0800 Subject: [PATCH 14/16] Address pytz UTC usage --- pandas/_libs/tslibs/timezones.pyx | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index 49b1a5790cb8e..a2c3d1cc45a96 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -22,10 +22,7 @@ from dateutil.tz import ( ) import numpy as np -try: - import pytz -except ImportError: - pytz = None +pytz = import_optional_dependency("pytz", errors="ignore") cimport numpy as cnp from numpy cimport int64_t @@ -41,7 +38,7 @@ from pandas._libs.tslibs.util cimport ( cdef int64_t NPY_NAT = get_nat() cdef tzinfo utc_stdlib = timezone.utc -cdef tzinfo utc_pytz = None +cdef object utc_pytz = pytz.UTC if pytz else None cdef tzinfo utc_dateutil_str = dateutil_gettz("UTC") # NB: *not* the same as tzutc() cdef tzinfo utc_zoneinfo = None @@ -68,26 +65,13 @@ cdef bint is_utc_zoneinfo(tzinfo tz): return tz is utc_zoneinfo -cdef bint is_utc_pytz(tzinfo tz): - if tz is None: - return False - - global utc_pytz - if utc_pytz is None: - if pytz is None: - return False - utc_pytz = pytz.UTC - - return tz is utc_pytz - - cpdef inline bint is_utc(tzinfo tz): return ( tz is utc_stdlib or isinstance(tz, _dateutil_tzutc) or tz is utc_dateutil_str or is_utc_zoneinfo(tz) - or is_utc_pytz(tz) + or (utc_pytz is not None and tz is utc_pytz) ) From fcf9ced6f8a839126de89a76de0d5a386f2123fd Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 15 Apr 2024 10:52:07 -0700 Subject: [PATCH 15/16] Fix typo --- doc/source/getting_started/install.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst index 768f070b9c714..672b8fa4f20d4 100644 --- a/doc/source/getting_started/install.rst +++ b/doc/source/getting_started/install.rst @@ -430,4 +430,3 @@ Dependency Minimum Version pip extra Notes ========================= ================== =================== ============================================================= pytz 2023.3 timezone Alternative timezone library to ``zoneinfo``. ========================= ================== =================== ============================================================= -======= From 235429f646195145812bd8f325f9f557acea6e41 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 15 Apr 2024 10:54:40 -0700 Subject: [PATCH 16/16] Add see also --- pandas/_libs/tslibs/tzconversion.pyx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pandas/_libs/tslibs/tzconversion.pyx b/pandas/_libs/tslibs/tzconversion.pyx index 86cc1d6f4cce0..509ff8f67ec10 100644 --- a/pandas/_libs/tslibs/tzconversion.pyx +++ b/pandas/_libs/tslibs/tzconversion.pyx @@ -55,6 +55,11 @@ class AmbiguousTimeError(Exception): """ Raised when a datetime is localized on a local time that occurs twice. + See Also + -------- + DataFrame.tz_localize: Localize tz-naive index of DataFrame to target time zone. + Series.tz_localize: Localize tz-naive index of Series to target time zone. + Examples -------- >>> pd.Timestamp(2024, 11, 3, 1, 30).tz_localize("US/Pacific") @@ -69,6 +74,11 @@ class NonExistentTimeError(Exception): """ Raised when a datetime is localized on a local time that doesn't exist. + See Also + -------- + DataFrame.tz_localize: Localize tz-naive index of DataFrame to target time zone. + Series.tz_localize: Localize tz-naive index of Series to target time zone. + Examples -------- >>> pd.Timestamp(2024, 3, 10, 2, 30).tz_localize("US/Pacific")