diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt index 53b052a955b45..0d177f193b68e 100644 --- a/doc/source/whatsnew/v0.22.0.txt +++ b/doc/source/whatsnew/v0.22.0.txt @@ -40,7 +40,7 @@ Backwards incompatible API changes Other API Changes ^^^^^^^^^^^^^^^^^ -- +- :class:`Timestamp` will no longer silently ignore unused or invalid `tz` or `tzinfo` arguments (:issue:`17690`) - - diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index a0aae6a5de707..6f534d4acd11a 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -30,6 +30,7 @@ from util cimport (is_integer_object, is_float_object, is_datetime64_object, is_timedelta64_object, INT64_MAX) cimport util +from cpython.datetime cimport PyTZInfo_Check # this is our datetime.pxd from datetime cimport ( pandas_datetimestruct, @@ -68,7 +69,7 @@ from .tslibs.parsing import parse_datetime_string cimport cython -from pandas.compat import iteritems, callable +from pandas.compat import iteritems import collections import warnings @@ -373,12 +374,23 @@ class Timestamp(_Timestamp): FutureWarning) freq = offset + if tzinfo is not None: + if not PyTZInfo_Check(tzinfo): + # tzinfo must be a datetime.tzinfo object, GH#17690 + raise TypeError('tzinfo must be a datetime.tzinfo object, ' + 'not %s' % type(tzinfo)) + elif tz is not None: + raise ValueError('Can provide at most one of tz, tzinfo') + if 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), - tz=tzinfo) + tz=tz) elif is_integer_object(freq): # User passed positional arguments: # Timestamp(year, month, day[, hour[, minute[, second[, @@ -387,6 +399,10 @@ class Timestamp(_Timestamp): year or 0, month or 0, day or 0, hour), tz=hour) + if tzinfo is not None: + # User passed tzinfo instead of tz; avoid silently ignoring + tz, tzinfo = tzinfo, None + ts = convert_to_tsobject(ts_input, tz, unit, 0, 0) if ts.value == NPY_NAT: diff --git a/pandas/tests/scalar/test_timestamp.py b/pandas/tests/scalar/test_timestamp.py index c1b9f858a08de..9a2e5565ae41b 100644 --- a/pandas/tests/scalar/test_timestamp.py +++ b/pandas/tests/scalar/test_timestamp.py @@ -175,6 +175,24 @@ def test_constructor_invalid(self): with tm.assert_raises_regex(ValueError, 'Cannot convert Period'): Timestamp(Period('1000-01-01')) + def test_constructor_invalid_tz(self): + # GH#17690, GH#5168 + with tm.assert_raises_regex(TypeError, 'must be a datetime.tzinfo'): + Timestamp('2017-10-22', tzinfo='US/Eastern') + + with tm.assert_raises_regex(ValueError, 'at most one of'): + Timestamp('2017-10-22', tzinfo=utc, tz='UTC') + + 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=utc), + Timestamp(year=2017, month=10, day=22, tz=utc), + Timestamp(datetime(2017, 10, 22), tzinfo=utc), + Timestamp(datetime(2017, 10, 22), tz='UTC'), + Timestamp(datetime(2017, 10, 22), tz=utc)] + assert all(ts == stamps[0] for ts in stamps) + def test_constructor_positional(self): # see gh-10758 with pytest.raises(TypeError):