diff --git a/doc/source/whatsnew/v0.24.0.rst b/doc/source/whatsnew/v0.24.0.rst index 98941b6d353bb..a214ee41e99a7 100644 --- a/doc/source/whatsnew/v0.24.0.rst +++ b/doc/source/whatsnew/v0.24.0.rst @@ -1036,6 +1036,7 @@ Deprecations - Constructing a :class:`TimedeltaIndex` from data with ``datetime64``-dtyped data is deprecated, will raise ``TypeError`` in a future version (:issue:`23539`) - The ``keep_tz=False`` option (the default) of the ``keep_tz`` keyword of :meth:`DatetimeIndex.to_series` is deprecated (:issue:`17832`). +- Timezone converting a tz-aware ``datetime.datetime`` or :class:`Timestamp` with :class:`Timestamp` and the ``tz`` argument is now deprecated. Instead, use :meth:`Timestamp.tz_convert` (:issue:`23579`) .. _whatsnew_0240.deprecations.datetimelike_int_ops: diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index eaaa170a387e9..3eaa6ceade00e 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -700,6 +700,9 @@ class Timestamp(_Timestamp): elif tz is not None: raise ValueError('Can provide at most one of tz, tzinfo') + # User passed tzinfo instead of tz; avoid silently ignoring + tz, tzinfo = tzinfo, None + if is_string_object(ts_input): # User passed a date string to parse. # Check that the user didn't also pass a date attribute kwarg. @@ -709,24 +712,23 @@ class Timestamp(_Timestamp): elif ts_input is _no_input: # User passed keyword arguments. - if tz is None: - # Handle the case where the user passes `tz` and not `tzinfo` - tz = tzinfo - return Timestamp(datetime(year, month, day, hour or 0, - minute or 0, second or 0, - microsecond or 0, tzinfo), - nanosecond=nanosecond, tz=tz) + ts_input = datetime(year, month, day, hour or 0, + minute or 0, second or 0, + microsecond or 0) elif is_integer_object(freq): # User passed positional arguments: # Timestamp(year, month, day[, hour[, minute[, second[, # microsecond[, nanosecond[, tzinfo]]]]]]) - return Timestamp(datetime(ts_input, freq, tz, unit or 0, - year or 0, month or 0, day or 0, - minute), nanosecond=hour, tz=minute) - - if tzinfo is not None: - # User passed tzinfo instead of tz; avoid silently ignoring - tz, tzinfo = tzinfo, None + ts_input = datetime(ts_input, freq, tz, unit or 0, + year or 0, month or 0, day or 0) + nanosecond = hour + tz = minute + freq = None + + if getattr(ts_input, 'tzinfo', None) is not None and tz is not None: + warnings.warn("Passing a datetime or Timestamp with tzinfo and the" + " tz parameter will raise in the future. Use" + " tz_convert instead.", FutureWarning) ts = convert_to_tsobject(ts_input, tz, unit, 0, 0, nanosecond or 0) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index ce1ca01cd3234..81fad1e279fdc 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -45,7 +45,12 @@ def _to_m8(key, tz=None): """ if not isinstance(key, Timestamp): # this also converts strings - key = Timestamp(key, tz=tz) + key = Timestamp(key) + if key.tzinfo is not None and tz is not None: + # Don't tz_localize(None) if key is already tz-aware + key = key.tz_convert(tz) + else: + key = key.tz_localize(tz) return np.int64(conversion.pydt_to_i8(key)).view(_NS_DTYPE) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 1687dfa15fa77..7d8873fe6a642 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -9336,8 +9336,14 @@ def describe_categorical_1d(data): if is_datetime64_any_dtype(data): tz = data.dt.tz asint = data.dropna().values.view('i8') + top = Timestamp(top) + if top.tzinfo is not None and tz is not None: + # Don't tz_localize(None) if key is already tz-aware + top = top.tz_convert(tz) + else: + top = top.tz_localize(tz) names += ['top', 'freq', 'first', 'last'] - result += [Timestamp(top, tz=tz), freq, + result += [top, freq, Timestamp(asint.min(), tz=tz), Timestamp(asint.max(), tz=tz)] else: diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 75f990096c677..bc79e5d12643b 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -937,7 +937,10 @@ def get_value(self, series, key): # needed to localize naive datetimes if self.tz is not None: - key = Timestamp(key, tz=self.tz) + if key.tzinfo is not None: + key = Timestamp(key).tz_convert(self.tz) + else: + key = Timestamp(key).tz_localize(self.tz) return self.get_value_maybe_box(series, key) @@ -963,7 +966,11 @@ def get_value(self, series, key): def get_value_maybe_box(self, series, key): # needed to localize naive datetimes if self.tz is not None: - key = Timestamp(key, tz=self.tz) + key = Timestamp(key) + if key.tzinfo is not None: + key = key.tz_convert(self.tz) + else: + key = key.tz_localize(self.tz) elif not isinstance(key, Timestamp): key = Timestamp(key) values = self._engine.get_value(com.values_from_object(series), @@ -986,7 +993,10 @@ def get_loc(self, key, method=None, tolerance=None): if isinstance(key, datetime): # needed to localize naive datetimes - key = Timestamp(key, tz=self.tz) + if key.tzinfo is None: + key = Timestamp(key, tz=self.tz) + else: + key = Timestamp(key).tz_convert(self.tz) return Index.get_loc(self, key, method, tolerance) elif isinstance(key, timedelta): @@ -1010,7 +1020,11 @@ def get_loc(self, key, method=None, tolerance=None): pass try: - stamp = Timestamp(key, tz=self.tz) + stamp = Timestamp(key) + if stamp.tzinfo is not None and self.tz is not None: + stamp = stamp.tz_convert(self.tz) + else: + stamp = stamp.tz_localize(self.tz) return Index.get_loc(self, stamp, method, tolerance) except KeyError: raise KeyError(key) diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py index b63e44c6c3437..c777b89eeaf12 100644 --- a/pandas/io/formats/format.py +++ b/pandas/io/formats/format.py @@ -1246,7 +1246,10 @@ def _format_datetime64(x, tz=None, nat_rep='NaT'): return nat_rep if tz is not None or not isinstance(x, Timestamp): - x = Timestamp(x, tz=tz) + if getattr(x, 'tzinfo', None) is not None: + x = Timestamp(x).tz_convert(tz) + else: + x = Timestamp(x).tz_localize(tz) return str(x) diff --git a/pandas/tests/indexes/datetimes/test_construction.py b/pandas/tests/indexes/datetimes/test_construction.py index 42a75f277faa6..4b2c07af6af68 100644 --- a/pandas/tests/indexes/datetimes/test_construction.py +++ b/pandas/tests/indexes/datetimes/test_construction.py @@ -521,6 +521,12 @@ def test_construction_from_replaced_timestamps_with_dst(self): tz='Australia/Melbourne') tm.assert_index_equal(result, expected) + def test_construction_with_tz_and_tz_aware_dti(self): + # GH 23579 + dti = date_range('2016-01-01', periods=3, tz='US/Central') + with pytest.raises(TypeError): + DatetimeIndex(dti, tz='Asia/Tokyo') + class TestTimeSeries(object): diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py index 47f91fdf25756..c1f532d56304c 100644 --- a/pandas/tests/scalar/timestamp/test_timestamp.py +++ b/pandas/tests/scalar/timestamp/test_timestamp.py @@ -244,7 +244,10 @@ def test_constructor(self): assert conversion.pydt_to_i8(result) == expected_tz # should convert to UTC - result = Timestamp(result, tz='UTC') + if tz is not None: + result = Timestamp(result).tz_convert('UTC') + else: + result = Timestamp(result, tz='UTC') expected_utc = expected - offset * 3600 * 1000000000 assert result.value == expected_utc assert conversion.pydt_to_i8(result) == expected_utc @@ -295,7 +298,7 @@ def test_constructor_with_stringoffset(self): assert conversion.pydt_to_i8(result) == expected_tz # should convert to UTC - result = Timestamp(result, tz='UTC') + result = Timestamp(result).tz_convert('UTC') expected_utc = expected assert result.value == expected_utc assert conversion.pydt_to_i8(result) == expected_utc @@ -558,7 +561,7 @@ def test_construct_timestamp_near_dst(self, offset): # GH 20854 expected = Timestamp('2016-10-30 03:00:00{}'.format(offset), tz='Europe/Helsinki') - result = Timestamp(expected, tz='Europe/Helsinki') + result = Timestamp(expected).tz_convert('Europe/Helsinki') assert result == expected @pytest.mark.parametrize('arg', [ @@ -580,6 +583,13 @@ def test_constructor_invalid_frequency(self): with pytest.raises(ValueError, match="Invalid frequency:"): Timestamp('2012-01-01', freq=[]) + @pytest.mark.parametrize('box', [datetime, Timestamp]) + def test_depreciate_tz_and_tzinfo_in_datetime_input(self, box): + # GH 23579 + kwargs = {'year': 2018, 'month': 1, 'day': 1, 'tzinfo': utc} + with tm.assert_produces_warning(FutureWarning): + Timestamp(box(**kwargs), tz='US/Pacific') + class TestTimestamp(object):