diff --git a/doc/source/whatsnew/v1.5.3.rst b/doc/source/whatsnew/v1.5.3.rst index 581d28e10bd67..ab80e21e9adce 100644 --- a/doc/source/whatsnew/v1.5.3.rst +++ b/doc/source/whatsnew/v1.5.3.rst @@ -29,6 +29,7 @@ Bug fixes - Bug in :meth:`.Styler.to_excel` leading to error when unrecognized ``border-style`` (e.g. ``"hair"``) provided to Excel writers (:issue:`48649`) - Bug when chaining several :meth:`.Styler.concat` calls, only the last styler was concatenated (:issue:`49207`) - Fixed bug when instantiating a :class:`DataFrame` subclass inheriting from ``typing.Generic`` that triggered a ``UserWarning`` on python 3.11 (:issue:`49649`) +- :class:`Period` now raises a warning when created with data that contains timezone information. This is necessary because :class:`Period`, :class:`PeriodArray` and :class:`PeriodIndex` do not support timezones and hence drop any timezone information used when creating them. (:issue:`47005`) - .. --------------------------------------------------------------------------- diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 86fa965be92c4..23c4629d757f2 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1,3 +1,5 @@ +import warnings + cimport numpy as cnp from cpython.object cimport ( Py_EQ, @@ -2613,6 +2615,13 @@ class Period(_Period): raise ValueError(msg) if ordinal is None: + if dt.tzinfo: + # GH 47005 + warnings.warn( + "The pandas.Period class does not support timezones. " + f"The timezone given in '{value}' will be ignored.", + UserWarning + ) base = freq_to_dtype_code(freq) ordinal = period_ordinal(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index bdaaeb20b3508..06e4f02c576c1 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -511,7 +511,11 @@ def _parsed_string_to_bounds(self, reso: Resolution, parsed: dt.datetime): ------- lower, upper: pd.Timestamp """ - per = Period(parsed, freq=reso.attr_abbrev) + with warnings.catch_warnings(): + # Period looses tzinfo. We ignore the corresponding warning here, + # and add the lost tzinfo below. + warnings.simplefilter("ignore", UserWarning) + per = Period(parsed, freq=reso.attr_abbrev) start, end = per.start_time, per.end_time # GH 24076 diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index 69f46a333503d..06e98f72b7ff6 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -15,6 +15,7 @@ Generator, cast, ) +import warnings from dateutil.relativedelta import relativedelta import matplotlib.dates as mdates @@ -262,7 +263,9 @@ def get_datevalue(date, freq): if isinstance(date, Period): return date.asfreq(freq).ordinal elif isinstance(date, (str, datetime, pydt.date, pydt.time, np.datetime64)): - return Period(date, freq).ordinal + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + return Period(date, freq).ordinal elif ( is_integer(date) or is_float(date) diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index fbd6f362bd9e7..dd7f30551be0f 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -1007,7 +1007,7 @@ class TestPeriodArray(SharedTests): index_cls = PeriodIndex array_cls = PeriodArray scalar_type = Period - example_dtype = PeriodIndex([], freq="W").dtype + example_dtype = PeriodIndex([], freq="Q").dtype @pytest.fixture def arr1d(self, period_index): diff --git a/pandas/tests/indexes/datetimes/methods/test_to_period.py b/pandas/tests/indexes/datetimes/methods/test_to_period.py index e8048e63afbf7..99d692750c08d 100644 --- a/pandas/tests/indexes/datetimes/methods/test_to_period.py +++ b/pandas/tests/indexes/datetimes/methods/test_to_period.py @@ -117,11 +117,13 @@ def test_to_period_millisecond(self): ) with tm.assert_produces_warning(UserWarning): - # warning that timezone info will be lost + # GH 21333 - warning that timezone info will be lost period = index.to_period(freq="L") assert 2 == len(period) - assert period[0] == Period("2007-01-01 10:11:12.123Z", "L") - assert period[1] == Period("2007-01-01 10:11:13.789Z", "L") + with tm.assert_produces_warning(UserWarning): + # GH 47005 - warning that timezone info will be lost + assert period[0] == Period("2007-01-01 10:11:12.123Z", "L") + assert period[1] == Period("2007-01-01 10:11:13.789Z", "L") def test_to_period_microsecond(self): index = DatetimeIndex( @@ -132,11 +134,13 @@ def test_to_period_microsecond(self): ) with tm.assert_produces_warning(UserWarning): - # warning that timezone info will be lost + # GH 21333 - warning that timezone info will be lost period = index.to_period(freq="U") assert 2 == len(period) - assert period[0] == Period("2007-01-01 10:11:12.123456Z", "U") - assert period[1] == Period("2007-01-01 10:11:13.789123Z", "U") + with tm.assert_produces_warning(UserWarning): + # GH 47005 - warning that timezone info will be lost + assert period[0] == Period("2007-01-01 10:11:12.123456Z", "U") + assert period[1] == Period("2007-01-01 10:11:13.789123Z", "U") @pytest.mark.parametrize( "tz", diff --git a/pandas/tests/indexes/period/test_period_range.py b/pandas/tests/indexes/period/test_period_range.py index c94ddf57c0ee1..15334ca2e8c53 100644 --- a/pandas/tests/indexes/period/test_period_range.py +++ b/pandas/tests/indexes/period/test_period_range.py @@ -20,7 +20,17 @@ def test_required_arguments(self): with pytest.raises(ValueError, match=msg): period_range("2011-1-1", "2012-1-1", "B") - @pytest.mark.parametrize("freq", ["D", "W", "M", "Q", "A"]) + @pytest.mark.parametrize( + "freq", + [ + "D", + # Parsing week strings is not fully supported. See GH 48000. + pytest.param("W", marks=pytest.mark.filterwarnings("ignore:.*timezone")), + "M", + "Q", + "A", + ], + ) def test_construction_from_string(self, freq): # non-empty expected = date_range( @@ -119,3 +129,13 @@ def test_errors(self): msg = "periods must be a number, got foo" with pytest.raises(TypeError, match=msg): period_range(start="2017Q1", periods="foo") + + +def test_range_tz(): + # GH 47005 Time zone should be ignored with warning. + with tm.assert_produces_warning(UserWarning): + pi_tz = period_range( + "2022-01-01 06:00:00+02:00", "2022-01-01 09:00:00+02:00", freq="H" + ) + pi_naive = period_range("2022-01-01 06:00:00", "2022-01-01 09:00:00", freq="H") + tm.assert_index_equal(pi_tz, pi_naive) diff --git a/pandas/tests/indexing/test_datetime.py b/pandas/tests/indexing/test_datetime.py index dc2fe85679181..05d7fe79c1318 100644 --- a/pandas/tests/indexing/test_datetime.py +++ b/pandas/tests/indexing/test_datetime.py @@ -170,3 +170,11 @@ def test_getitem_str_slice_millisecond_resolution(self, frame_or_series): ], ) tm.assert_equal(result, expected) + + +def test_slice_with_datestring_tz(): + # GH 24076 + # GH 16785 + df = DataFrame([0], index=pd.DatetimeIndex(["2019-01-01"], tz="US/Pacific")) + sliced = df["2019-01-01 12:00:00+04:00":"2019-01-01 13:00:00+04:00"] + tm.assert_frame_equal(sliced, df) diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py index e32708c4402e4..1c69bcd62b4eb 100644 --- a/pandas/tests/resample/test_period_index.py +++ b/pandas/tests/resample/test_period_index.py @@ -263,12 +263,16 @@ def test_with_local_timezone_pytz(self): series = Series(1, index=index) series = series.tz_convert(local_timezone) - result = series.resample("D", kind="period").mean() - - # Create the expected series - # Index is moved back a day with the timezone conversion from UTC to - # Pacific - expected_index = period_range(start=start, end=end, freq="D") - offsets.Day() + # see gh-47005 + with tm.assert_produces_warning(UserWarning): + result = series.resample("D", kind="period").mean() + + # Create the expected series + # Index is moved back a day with the timezone conversion from UTC to + # Pacific + expected_index = ( + period_range(start=start, end=end, freq="D") - offsets.Day() + ) expected = Series(1.0, index=expected_index) tm.assert_series_equal(result, expected) @@ -304,14 +308,16 @@ def test_with_local_timezone_dateutil(self): series = Series(1, index=index) series = series.tz_convert(local_timezone) - result = series.resample("D", kind="period").mean() - - # Create the expected series - # Index is moved back a day with the timezone conversion from UTC to - # Pacific - expected_index = ( - period_range(start=start, end=end, freq="D", name="idx") - offsets.Day() - ) + # see gh-47005 + with tm.assert_produces_warning(UserWarning): + result = series.resample("D", kind="period").mean() + + # Create the expected series + # Index is moved back a day with the timezone conversion from UTC to + # Pacific + expected_index = ( + period_range(start=start, end=end, freq="D", name="idx") - offsets.Day() + ) expected = Series(1.0, index=expected_index) tm.assert_series_equal(result, expected) @@ -504,7 +510,10 @@ def test_resample_tz_localized(self): tm.assert_series_equal(result, expected) # for good measure - result = s.resample("D", kind="period").mean() + # see gh-47005 + with tm.assert_produces_warning(UserWarning): + result = s.resample("D", kind="period").mean() + ex_index = period_range("2001-09-20", periods=1, freq="D") expected = Series([1.5], index=ex_index) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 112f23b3b0f16..2b749e2d5f31c 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -1555,3 +1555,18 @@ def test_invalid_frequency_error_message(): msg = "Invalid frequency: " with pytest.raises(ValueError, match=msg): Period("2012-01-02", freq="WOM-1MON") + + +@pytest.mark.parametrize( + "val", + [ + ("20220101T123456", "Z"), + ("2012-12-12T06:06:06", "-06:00"), + ], +) +def test_period_with_timezone(val): + # GH 47005 Time zone should be ignored with warning. + with tm.assert_produces_warning(UserWarning): + p_tz = Period("".join(val), freq="s") + p_naive = Period(val[0], freq="s") + assert p_tz == p_naive