Skip to content

Commit f3c46cd

Browse files
authored
API: make Timestamp/Timedelta _as_unit public as_unit (#48819)
* API: make Timestamp/Timedelta _as_unit public as_unit * update test * update test * update tests * fix pyi typo * fixup * fixup
1 parent 72e923e commit f3c46cd

27 files changed

+156
-103
lines changed

doc/source/reference/arrays.rst

+4
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ Properties
139139
Timestamp.second
140140
Timestamp.tz
141141
Timestamp.tzinfo
142+
Timestamp.unit
142143
Timestamp.value
143144
Timestamp.week
144145
Timestamp.weekofyear
@@ -149,6 +150,7 @@ Methods
149150
.. autosummary::
150151
:toctree: api/
151152

153+
Timestamp.as_unit
152154
Timestamp.astimezone
153155
Timestamp.ceil
154156
Timestamp.combine
@@ -242,6 +244,7 @@ Properties
242244
Timedelta.nanoseconds
243245
Timedelta.resolution
244246
Timedelta.seconds
247+
Timedelta.unit
245248
Timedelta.value
246249
Timedelta.view
247250

@@ -250,6 +253,7 @@ Methods
250253
.. autosummary::
251254
:toctree: api/
252255

256+
Timedelta.as_unit
253257
Timedelta.ceil
254258
Timedelta.floor
255259
Timedelta.isoformat

pandas/_libs/tslib.pyx

+2-2
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ cpdef array_to_datetime(
551551
raise ValueError('Cannot mix tz-aware with '
552552
'tz-naive values')
553553
if isinstance(val, _Timestamp):
554-
iresult[i] = val._as_unit("ns").value
554+
iresult[i] = val.as_unit("ns").value
555555
else:
556556
iresult[i] = pydatetime_to_dt64(val, &dts)
557557
check_dts_bounds(&dts)
@@ -906,7 +906,7 @@ def array_to_datetime_with_tz(ndarray values, tzinfo tz):
906906
else:
907907
# datetime64, tznaive pydatetime, int, float
908908
ts = ts.tz_localize(tz)
909-
ts = ts._as_unit("ns")
909+
ts = ts.as_unit("ns")
910910
ival = ts.value
911911

912912
# Analogous to: result[i] = ival

pandas/_libs/tslibs/nattype.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@ class NaTType:
127127
__le__: _NatComparison
128128
__gt__: _NatComparison
129129
__ge__: _NatComparison
130+
def as_unit(self, unit: str, round_ok: bool = ...) -> NaTType: ...

pandas/_libs/tslibs/nattype.pyx

+16
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,22 @@ default 'raise'
11951195
def tzinfo(self) -> None:
11961196
return None
11971197

1198+
def as_unit(self, str unit, bint round_ok=True) -> "NaTType":
1199+
"""
1200+
Convert the underlying int64 representaton to the given unit.
1201+
1202+
Parameters
1203+
----------
1204+
unit : {"ns", "us", "ms", "s"}
1205+
round_ok : bool, default True
1206+
If False and the conversion requires rounding, raise.
1207+
1208+
Returns
1209+
-------
1210+
Timestamp
1211+
"""
1212+
return c_NaT
1213+
11981214

11991215
c_NaT = NaTType() # C-visible
12001216
NaT = c_NaT # Python-visible

pandas/_libs/tslibs/timedeltas.pyi

+2-2
Original file line numberDiff line numberDiff line change
@@ -152,5 +152,5 @@ class Timedelta(timedelta):
152152
def to_numpy(self) -> np.timedelta64: ...
153153
def view(self, dtype: npt.DTypeLike = ...) -> object: ...
154154
@property
155-
def _unit(self) -> str: ...
156-
def _as_unit(self, unit: str, round_ok: bool = ...) -> Timedelta: ...
155+
def unit(self) -> str: ...
156+
def as_unit(self, unit: str, round_ok: bool = ...) -> Timedelta: ...

pandas/_libs/tslibs/timedeltas.pyx

+19-2
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ cdef convert_to_timedelta64(object ts, str unit):
339339
elif isinstance(ts, _Timedelta):
340340
# already in the proper format
341341
if ts._creso != NPY_FR_ns:
342-
ts = ts._as_unit("ns").asm8
342+
ts = ts.as_unit("ns").asm8
343343
else:
344344
ts = np.timedelta64(ts.value, "ns")
345345
elif is_timedelta64_object(ts):
@@ -1081,6 +1081,10 @@ cdef class _Timedelta(timedelta):
10811081
# TODO: add nanos/1e9?
10821082
return self.days * 24 * 3600 + self.seconds + self.microseconds / 1_000_000
10831083

1084+
@property
1085+
def unit(self) -> str:
1086+
return npy_unit_to_abbrev(self._creso)
1087+
10841088
def __hash__(_Timedelta self):
10851089
if self._has_ns():
10861090
# Note: this does *not* satisfy the invariance
@@ -1500,7 +1504,20 @@ cdef class _Timedelta(timedelta):
15001504
# exposing as classmethod for testing
15011505
return _timedelta_from_value_and_reso(value, reso)
15021506

1503-
def _as_unit(self, str unit, bint round_ok=True):
1507+
def as_unit(self, str unit, bint round_ok=True):
1508+
"""
1509+
Convert the underlying int64 representaton to the given unit.
1510+
1511+
Parameters
1512+
----------
1513+
unit : {"ns", "us", "ms", "s"}
1514+
round_ok : bool, default True
1515+
If False and the conversion requires rounding, raise.
1516+
1517+
Returns
1518+
-------
1519+
Timedelta
1520+
"""
15041521
dtype = np.dtype(f"m8[{unit}]")
15051522
reso = get_unit_from_dtype(dtype)
15061523
return self._as_creso(reso, round_ok=round_ok)

pandas/_libs/tslibs/timestamps.pyi

+2-2
Original file line numberDiff line numberDiff line change
@@ -220,5 +220,5 @@ class Timestamp(datetime):
220220
@property
221221
def daysinmonth(self) -> int: ...
222222
@property
223-
def _unit(self) -> str: ...
224-
def _as_unit(self, unit: str, round_ok: bool = ...) -> Timestamp: ...
223+
def unit(self) -> str: ...
224+
def as_unit(self, unit: str, round_ok: bool = ...) -> Timestamp: ...

pandas/_libs/tslibs/timestamps.pyx

+15-2
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ cdef class _Timestamp(ABCTimestamp):
233233
resolution = MinMaxReso("resolution") # GH#21336, GH#21365
234234

235235
@property
236-
def _unit(self) -> str:
236+
def unit(self) -> str:
237237
"""
238238
The abbreviation associated with self._creso.
239239
"""
@@ -993,7 +993,20 @@ cdef class _Timestamp(ABCTimestamp):
993993
value = convert_reso(self.value, self._creso, reso, round_ok=round_ok)
994994
return type(self)._from_value_and_reso(value, reso=reso, tz=self.tzinfo)
995995

996-
def _as_unit(self, str unit, bint round_ok=True):
996+
def as_unit(self, str unit, bint round_ok=True):
997+
"""
998+
Convert the underlying int64 representaton to the given unit.
999+
1000+
Parameters
1001+
----------
1002+
unit : {"ns", "us", "ms", "s"}
1003+
round_ok : bool, default True
1004+
If False and the conversion requires rounding, raise.
1005+
1006+
Returns
1007+
-------
1008+
Timestamp
1009+
"""
9971010
dtype = np.dtype(f"M8[{unit}]")
9981011
reso = get_unit_from_dtype(dtype)
9991012
try:

pandas/core/arrays/datetimelike.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ def isin(self, values) -> npt.NDArray[np.bool_]:
816816

817817
if self.dtype.kind in ["m", "M"]:
818818
self = cast("DatetimeArray | TimedeltaArray", self)
819-
values = values._as_unit(self._unit)
819+
values = values.as_unit(self.unit)
820820

821821
try:
822822
self._check_compatible_with(values)
@@ -1116,7 +1116,7 @@ def _add_datetimelike_scalar(self, other) -> DatetimeArray:
11161116
# i.e. np.datetime64("NaT")
11171117
# In this case we specifically interpret NaT as a datetime, not
11181118
# the timedelta interpretation we would get by returning self + NaT
1119-
result = self._ndarray + NaT.to_datetime64().astype(f"M8[{self._unit}]")
1119+
result = self._ndarray + NaT.to_datetime64().astype(f"M8[{self.unit}]")
11201120
# Preserve our resolution
11211121
return DatetimeArray._simple_new(result, dtype=result.dtype)
11221122

@@ -1128,10 +1128,10 @@ def _add_datetimelike_scalar(self, other) -> DatetimeArray:
11281128
result = checked_add_with_arr(
11291129
self.asi8, other_i8, arr_mask=self._isnan, b_mask=o_mask
11301130
)
1131-
res_values = result.view(f"M8[{self._unit}]")
1131+
res_values = result.view(f"M8[{self.unit}]")
11321132

1133-
dtype = tz_to_dtype(tz=other.tz, unit=self._unit)
1134-
res_values = result.view(f"M8[{self._unit}]")
1133+
dtype = tz_to_dtype(tz=other.tz, unit=self.unit)
1134+
res_values = result.view(f"M8[{self.unit}]")
11351135
new_freq = self._get_arithmetic_result_freq(other)
11361136
return DatetimeArray._simple_new(res_values, dtype=dtype, freq=new_freq)
11371137

@@ -1191,7 +1191,7 @@ def _sub_datetimelike(self, other: Timestamp | DatetimeArray) -> TimedeltaArray:
11911191
res_values = checked_add_with_arr(
11921192
self.asi8, -other_i8, arr_mask=self._isnan, b_mask=o_mask
11931193
)
1194-
res_m8 = res_values.view(f"timedelta64[{self._unit}]")
1194+
res_m8 = res_values.view(f"timedelta64[{self.unit}]")
11951195

11961196
new_freq = self._get_arithmetic_result_freq(other)
11971197
return TimedeltaArray._simple_new(res_m8, dtype=res_m8.dtype, freq=new_freq)
@@ -1989,13 +1989,13 @@ def _creso(self) -> int:
19891989
return get_unit_from_dtype(self._ndarray.dtype)
19901990

19911991
@cache_readonly
1992-
def _unit(self) -> str:
1992+
def unit(self) -> str:
19931993
# e.g. "ns", "us", "ms"
19941994
# error: Argument 1 to "dtype_to_unit" has incompatible type
19951995
# "ExtensionDtype"; expected "Union[DatetimeTZDtype, dtype[Any]]"
19961996
return dtype_to_unit(self.dtype) # type: ignore[arg-type]
19971997

1998-
def _as_unit(self: TimelikeOpsT, unit: str) -> TimelikeOpsT:
1998+
def as_unit(self: TimelikeOpsT, unit: str) -> TimelikeOpsT:
19991999
dtype = np.dtype(f"{self.dtype.kind}8[{unit}]")
20002000
new_values = astype_overflowsafe(self._ndarray, dtype, round_ok=True)
20012001

@@ -2017,9 +2017,9 @@ def _ensure_matching_resos(self, other):
20172017
if self._creso != other._creso:
20182018
# Just as with Timestamp/Timedelta, we cast to the higher resolution
20192019
if self._creso < other._creso:
2020-
self = self._as_unit(other._unit)
2020+
self = self.as_unit(other.unit)
20212021
else:
2022-
other = other._as_unit(self._unit)
2022+
other = other.as_unit(self.unit)
20232023
return self, other
20242024

20252025
# --------------------------------------------------------------

pandas/core/arrays/datetimes.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,9 @@ def _from_sequence_not_strict(
351351
data_unit = np.datetime_data(subarr.dtype)[0]
352352
data_dtype = tz_to_dtype(tz, data_unit)
353353
result = cls._simple_new(subarr, freq=freq, dtype=data_dtype)
354-
if unit is not None and unit != result._unit:
354+
if unit is not None and unit != result.unit:
355355
# If unit was specified in user-passed dtype, cast to it here
356-
result = result._as_unit(unit)
356+
result = result.as_unit(unit)
357357

358358
if inferred_freq is None and freq is not None:
359359
# this condition precludes `freq_infer`
@@ -843,7 +843,7 @@ def tz_convert(self, tz) -> DatetimeArray:
843843
)
844844

845845
# No conversion since timestamps are all UTC to begin with
846-
dtype = tz_to_dtype(tz, unit=self._unit)
846+
dtype = tz_to_dtype(tz, unit=self.unit)
847847
return self._simple_new(self._ndarray, dtype=dtype, freq=self.freq)
848848

849849
@dtl.ravel_compat
@@ -1018,8 +1018,8 @@ def tz_localize(
10181018
nonexistent=nonexistent,
10191019
creso=self._creso,
10201020
)
1021-
new_dates = new_dates.view(f"M8[{self._unit}]")
1022-
dtype = tz_to_dtype(tz, unit=self._unit)
1021+
new_dates = new_dates.view(f"M8[{self.unit}]")
1022+
dtype = tz_to_dtype(tz, unit=self.unit)
10231023

10241024
freq = None
10251025
if timezones.is_utc(tz) or (len(self) == 1 and not isna(new_dates[0])):

pandas/core/arrays/timedeltas.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,10 @@ def _generate_range(cls, start, end, periods, freq, closed=None):
268268
)
269269

270270
if start is not None:
271-
start = Timedelta(start)._as_unit("ns")
271+
start = Timedelta(start).as_unit("ns")
272272

273273
if end is not None:
274-
end = Timedelta(end)._as_unit("ns")
274+
end = Timedelta(end).as_unit("ns")
275275

276276
left_closed, right_closed = validate_endpoints(closed)
277277

@@ -298,7 +298,7 @@ def _unbox_scalar(self, value) -> np.timedelta64:
298298
if value is NaT:
299299
return np.timedelta64(value.value, "ns")
300300
else:
301-
return value._as_unit(self._unit).asm8
301+
return value.as_unit(self.unit).asm8
302302

303303
def _scalar_from_string(self, value) -> Timedelta | NaTType:
304304
return Timedelta(value)

pandas/core/dtypes/common.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,7 @@ def is_datetime64_ns_dtype(arr_or_dtype) -> bool:
931931
else:
932932
return False
933933
return tipo == DT64NS_DTYPE or (
934-
isinstance(tipo, DatetimeTZDtype) and tipo._unit == "ns"
934+
isinstance(tipo, DatetimeTZDtype) and tipo.unit == "ns"
935935
)
936936

937937

pandas/core/dtypes/dtypes.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ def na_value(self) -> NaTType:
676676
# error: Signature of "str" incompatible with supertype "PandasExtensionDtype"
677677
@cache_readonly
678678
def str(self) -> str: # type: ignore[override]
679-
return f"|M8[{self._unit}]"
679+
return f"|M8[{self.unit}]"
680680

681681
def __init__(self, unit: str_type | DatetimeTZDtype = "ns", tz=None) -> None:
682682
if isinstance(unit, DatetimeTZDtype):
@@ -720,7 +720,7 @@ def _creso(self) -> int:
720720
"ms": dtypes.NpyDatetimeUnit.NPY_FR_ms,
721721
"us": dtypes.NpyDatetimeUnit.NPY_FR_us,
722722
"ns": dtypes.NpyDatetimeUnit.NPY_FR_ns,
723-
}[self._unit]
723+
}[self.unit]
724724
return reso.value
725725

726726
@property

pandas/core/window/ewm.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def _calculate_deltas(
123123
"""
124124
_times = np.asarray(times.view(np.int64), dtype=np.float64)
125125
# TODO: generalize to non-nano?
126-
_halflife = float(Timedelta(halflife)._as_unit("ns").value)
126+
_halflife = float(Timedelta(halflife).as_unit("ns").value)
127127
return np.diff(_times) / _halflife
128128

129129

pandas/tests/arrays/test_datetimes.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,12 @@ def test_add_mismatched_reso_doesnt_downcast(self):
215215
# https://github.com/pandas-dev/pandas/pull/48748#issuecomment-1260181008
216216
td = pd.Timedelta(microseconds=1)
217217
dti = pd.date_range("2016-01-01", periods=3) - td
218-
dta = dti._data._as_unit("us")
218+
dta = dti._data.as_unit("us")
219219

220-
res = dta + td._as_unit("us")
220+
res = dta + td.as_unit("us")
221221
# even though the result is an even number of days
222222
# (so we _could_ downcast to unit="s"), we do not.
223-
assert res._unit == "us"
223+
assert res.unit == "us"
224224

225225
@pytest.mark.parametrize(
226226
"scalar",
@@ -240,32 +240,32 @@ def test_add_timedeltalike_scalar_mismatched_reso(self, dta_dti, scalar):
240240
exp_reso = max(dta._creso, td._creso)
241241
exp_unit = npy_unit_to_abbrev(exp_reso)
242242

243-
expected = (dti + td)._data._as_unit(exp_unit)
243+
expected = (dti + td)._data.as_unit(exp_unit)
244244
result = dta + scalar
245245
tm.assert_extension_array_equal(result, expected)
246246

247247
result = scalar + dta
248248
tm.assert_extension_array_equal(result, expected)
249249

250-
expected = (dti - td)._data._as_unit(exp_unit)
250+
expected = (dti - td)._data.as_unit(exp_unit)
251251
result = dta - scalar
252252
tm.assert_extension_array_equal(result, expected)
253253

254254
def test_sub_datetimelike_scalar_mismatch(self):
255255
dti = pd.date_range("2016-01-01", periods=3)
256-
dta = dti._data._as_unit("us")
256+
dta = dti._data.as_unit("us")
257257

258-
ts = dta[0]._as_unit("s")
258+
ts = dta[0].as_unit("s")
259259

260260
result = dta - ts
261-
expected = (dti - dti[0])._data._as_unit("us")
261+
expected = (dti - dti[0])._data.as_unit("us")
262262
assert result.dtype == "m8[us]"
263263
tm.assert_extension_array_equal(result, expected)
264264

265265
def test_sub_datetime64_reso_mismatch(self):
266266
dti = pd.date_range("2016-01-01", periods=3)
267-
left = dti._data._as_unit("s")
268-
right = left._as_unit("ms")
267+
left = dti._data.as_unit("s")
268+
right = left.as_unit("ms")
269269

270270
result = left - right
271271
exp_values = np.array([0, 0, 0], dtype="m8[ms]")

pandas/tests/arrays/test_timedeltas.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def test_add_pdnat(self, tda):
104104
def test_add_datetimelike_scalar(self, tda, tz_naive_fixture):
105105
ts = pd.Timestamp("2016-01-01", tz=tz_naive_fixture)
106106

107-
expected = tda._as_unit("ns") + ts
107+
expected = tda.as_unit("ns") + ts
108108
res = tda + ts
109109
tm.assert_extension_array_equal(res, expected)
110110
res = ts + tda

0 commit comments

Comments
 (0)