diff --git a/pandas/_libs/tslibs/conversion.pxd b/pandas/_libs/tslibs/conversion.pxd index 623d9f14d646b..2cf75944a8196 100644 --- a/pandas/_libs/tslibs/conversion.pxd +++ b/pandas/_libs/tslibs/conversion.pxd @@ -13,11 +13,11 @@ cdef class _TSObject: bint fold -cdef convert_to_tsobject(object ts, object tz, object unit, +cdef convert_to_tsobject(object ts, tzinfo tz, object unit, bint dayfirst, bint yearfirst, int32_t nanos=*) -cdef _TSObject convert_datetime_to_tsobject(datetime ts, object tz, +cdef _TSObject convert_datetime_to_tsobject(datetime ts, tzinfo tz, int32_t nanos=*) cdef int64_t get_datetime64_nanos(object val) except? -1 diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index ac24dd546d9d3..82a9ad9d3f49b 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -306,7 +306,7 @@ cdef class _TSObject: return self.value -cdef convert_to_tsobject(object ts, object tz, object unit, +cdef convert_to_tsobject(object ts, tzinfo tz, object unit, bint dayfirst, bint yearfirst, int32_t nanos=0): """ Extract datetime and int64 from any of: @@ -325,13 +325,10 @@ cdef convert_to_tsobject(object ts, object tz, object unit, cdef: _TSObject obj - if tz is not None: - tz = maybe_get_tz(tz) - obj = _TSObject() if isinstance(ts, str): - return convert_str_to_tsobject(ts, tz, unit, dayfirst, yearfirst) + return _convert_str_to_tsobject(ts, tz, unit, dayfirst, yearfirst) if ts is None or ts is NaT: obj.value = NPY_NAT @@ -373,16 +370,16 @@ cdef convert_to_tsobject(object ts, object tz, object unit, f'Timestamp') if tz is not None: - localize_tso(obj, tz) + _localize_tso(obj, tz) if obj.value != NPY_NAT: - # check_overflows needs to run after localize_tso + # check_overflows needs to run after _localize_tso check_dts_bounds(&obj.dts) check_overflows(obj) return obj -cdef _TSObject convert_datetime_to_tsobject(datetime ts, object tz, +cdef _TSObject convert_datetime_to_tsobject(datetime ts, tzinfo tz, int32_t nanos=0): """ Convert a datetime (or Timestamp) input `ts`, along with optional timezone @@ -445,8 +442,8 @@ cdef _TSObject convert_datetime_to_tsobject(datetime ts, object tz, return obj -cdef _TSObject create_tsobject_tz_using_offset(npy_datetimestruct dts, - int tzoffset, tzinfo tz=None): +cdef _TSObject _create_tsobject_tz_using_offset(npy_datetimestruct dts, + int tzoffset, tzinfo tz=None): """ Convert a datetimestruct `dts`, along with initial timezone offset `tzoffset` to a _TSObject (with timezone object `tz` - optional). @@ -499,9 +496,9 @@ cdef _TSObject create_tsobject_tz_using_offset(npy_datetimestruct dts, return obj -cdef _TSObject convert_str_to_tsobject(object ts, object tz, object unit, - bint dayfirst=False, - bint yearfirst=False): +cdef _TSObject _convert_str_to_tsobject(object ts, tzinfo tz, object unit, + bint dayfirst=False, + bint yearfirst=False): """ Convert a string input `ts`, along with optional timezone object`tz` to a _TSObject. @@ -531,11 +528,6 @@ cdef _TSObject convert_str_to_tsobject(object ts, object tz, object unit, int out_local = 0, out_tzoffset = 0 bint do_parse_datetime_string = False - if tz is not None: - tz = maybe_get_tz(tz) - - assert isinstance(ts, str) - if len(ts) == 0 or ts in nat_strings: ts = NaT elif ts == 'now': @@ -556,12 +548,12 @@ cdef _TSObject convert_str_to_tsobject(object ts, object tz, object unit, if not string_to_dts_failed: check_dts_bounds(&dts) if out_local == 1: - return create_tsobject_tz_using_offset(dts, - out_tzoffset, tz) + return _create_tsobject_tz_using_offset(dts, + out_tzoffset, tz) else: ts = dtstruct_to_dt64(&dts) if tz is not None: - # shift for localize_tso + # shift for _localize_tso ts = tz_localize_to_utc(np.array([ts], dtype='i8'), tz, ambiguous='raise')[0] @@ -612,7 +604,7 @@ cdef inline check_overflows(_TSObject obj): # ---------------------------------------------------------------------- # Localization -cdef inline void localize_tso(_TSObject obj, tzinfo tz): +cdef inline void _localize_tso(_TSObject obj, tzinfo tz): """ Given the UTC nanosecond timestamp in obj.value, find the wall-clock representation of that timestamp in the given timezone. diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 1328bc8a5175f..e104b722ea119 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -20,6 +20,7 @@ from cpython.datetime cimport ( datetime, time, tzinfo, + tzinfo as tzinfo_type, # alias bc `tzinfo` is a kwarg below PyDateTime_Check, PyDelta_Check, PyTZInfo_Check, @@ -932,7 +933,7 @@ class Timestamp(_Timestamp): second=None, microsecond=None, nanosecond=None, - tzinfo=None, + tzinfo_type tzinfo=None, *, fold=None ): @@ -957,18 +958,17 @@ class Timestamp(_Timestamp): # # Mixing pydatetime positional and keyword arguments is forbidden! - cdef _TSObject ts + cdef: + _TSObject ts + tzinfo_type tzobj _date_attributes = [year, month, day, hour, minute, second, microsecond, nanosecond] if tzinfo is not None: - if not PyTZInfo_Check(tzinfo): - # tzinfo must be a datetime.tzinfo object, GH#17690 - raise TypeError( - f"tzinfo must be a datetime.tzinfo object, not {type(tzinfo)}" - ) - elif tz is not None: + # GH#17690 tzinfo must be a datetime.tzinfo object, ensured + # by the cython annotation. + if tz is not None: raise ValueError('Can provide at most one of tz, tzinfo') # User passed tzinfo instead of tz; avoid silently ignoring @@ -1055,7 +1055,8 @@ class Timestamp(_Timestamp): raise ValueError("Cannot pass a datetime or Timestamp with tzinfo with " "the tz parameter. Use tz_convert instead.") - ts = convert_to_tsobject(ts_input, tz, unit, 0, 0, nanosecond or 0) + tzobj = maybe_get_tz(tz) + ts = convert_to_tsobject(ts_input, tzobj, unit, 0, 0, nanosecond or 0) if ts.value == NPY_NAT: return NaT @@ -1378,15 +1379,16 @@ default 'raise' cdef: npy_datetimestruct dts - int64_t value, value_tz, offset - object _tzinfo, result, k, v + int64_t value, value_tz + object k, v datetime ts_input + tzinfo_type tzobj # set to naive if needed - _tzinfo = self.tzinfo + tzobj = self.tzinfo value = self.value - if _tzinfo is not None: - value_tz = tz_convert_single(value, _tzinfo, UTC) + if tzobj is not None: + value_tz = tz_convert_single(value, tzobj, UTC) value += value - value_tz # setup components @@ -1419,30 +1421,30 @@ default 'raise' if nanosecond is not None: dts.ps = validate('nanosecond', nanosecond) * 1000 if tzinfo is not object: - _tzinfo = tzinfo + tzobj = tzinfo # reconstruct & check bounds - if _tzinfo is not None and treat_tz_as_pytz(_tzinfo): + if tzobj is not None and treat_tz_as_pytz(tzobj): # replacing across a DST boundary may induce a new tzinfo object # see GH#18319 - ts_input = _tzinfo.localize(datetime(dts.year, dts.month, dts.day, - dts.hour, dts.min, dts.sec, - dts.us), - is_dst=not bool(fold)) - _tzinfo = ts_input.tzinfo + ts_input = tzobj.localize(datetime(dts.year, dts.month, dts.day, + dts.hour, dts.min, dts.sec, + dts.us), + is_dst=not bool(fold)) + tzobj = ts_input.tzinfo else: kwargs = {'year': dts.year, 'month': dts.month, 'day': dts.day, 'hour': dts.hour, 'minute': dts.min, 'second': dts.sec, - 'microsecond': dts.us, 'tzinfo': _tzinfo, + 'microsecond': dts.us, 'tzinfo': tzobj, 'fold': fold} ts_input = datetime(**kwargs) - ts = convert_datetime_to_tsobject(ts_input, _tzinfo) + ts = convert_datetime_to_tsobject(ts_input, tzobj) value = ts.value + (dts.ps // 1000) if value != NPY_NAT: check_dts_bounds(&dts) - return create_timestamp_from_ts(value, dts, _tzinfo, self.freq, fold) + return create_timestamp_from_ts(value, dts, tzobj, self.freq, fold) def to_julian_date(self) -> np.float64: """ diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py index 770753f42a4c8..316a299ba1cbb 100644 --- a/pandas/tests/scalar/timestamp/test_constructors.py +++ b/pandas/tests/scalar/timestamp/test_constructors.py @@ -174,7 +174,10 @@ def test_constructor_invalid(self): def test_constructor_invalid_tz(self): # GH#17690 - msg = "must be a datetime.tzinfo" + msg = ( + "Argument 'tzinfo' has incorrect type " + r"\(expected datetime.tzinfo, got str\)" + ) with pytest.raises(TypeError, match=msg): Timestamp("2017-10-22", tzinfo="US/Eastern")