From b248eb65a1643c9319f31cb2373dad6d4031bee8 Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 26 Nov 2025 15:37:20 -0800 Subject: [PATCH 1/4] API: Timedelta constructor from keywords give microseconds --- pandas/_libs/tslibs/timedeltas.pyx | 9 ++++++++- pandas/conftest.py | 2 +- pandas/tests/arithmetic/test_numeric.py | 10 +++++----- pandas/tests/base/test_value_counts.py | 2 +- .../scalar/timedelta/methods/test_as_unit.py | 18 +++++++++--------- .../tests/scalar/timedelta/test_arithmetic.py | 8 ++++---- .../scalar/timedelta/test_constructors.py | 12 ++++++------ .../tests/scalar/timedelta/test_timedelta.py | 2 +- 8 files changed, 35 insertions(+), 28 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 4d30415a396db..f8673b7a696e7 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -2045,7 +2045,7 @@ class Timedelta(_Timedelta): int(ns) + int(us * 1_000) + int(ms * 1_000_000) - + seconds + + seconds, "ns" ) except OverflowError as err: # GH#55503 @@ -2055,6 +2055,13 @@ class Timedelta(_Timedelta): ) raise OutOfBoundsTimedelta(msg) from err + if ( + "nanoseconds" not in kwargs + and cnp.get_timedelta64_value(value) % 1000 == 0 + ): + # If possible, give a microsecond unit + value = value.astype("m8[us]") + disallow_ambiguous_unit(unit) cdef: diff --git a/pandas/conftest.py b/pandas/conftest.py index ee3320ccea35d..0408ef78f1c6f 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -938,7 +938,7 @@ def rand_series_with_duplicate_datetimeindex() -> Series: Timestamp("2011-01-01", tz="US/Eastern").as_unit("s"), DatetimeTZDtype(unit="s", tz="US/Eastern"), ), - (Timedelta(seconds=500), "timedelta64[ns]"), + (Timedelta(seconds=500), "timedelta64[us]"), ] ) def ea_scalar_and_dtype(request): diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index d9d9343a9b56e..2a568c2e4f9c2 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -223,8 +223,8 @@ def test_div_td64arr(self, left, box_cls): @pytest.mark.parametrize( "scalar_td", [ - Timedelta(days=1), - Timedelta(days=1).to_timedelta64(), + Timedelta(days=1).as_unit("ns"), + Timedelta(days=1).as_unit("ns").to_timedelta64(), Timedelta(days=1).to_pytimedelta(), Timedelta(days=1).to_timedelta64().astype("timedelta64[s]"), Timedelta(days=1).to_timedelta64().astype("timedelta64[ms]"), @@ -254,9 +254,9 @@ def test_numeric_arr_mul_tdscalar(self, scalar_td, numeric_idx, box_with_array): @pytest.mark.parametrize( "scalar_td", [ - Timedelta(days=1), - Timedelta(days=1).to_timedelta64(), - Timedelta(days=1).to_pytimedelta(), + Timedelta(days=1).as_unit("ns"), + Timedelta(days=1).as_unit("ns").to_timedelta64(), + Timedelta(days=1).as_unit("ns").to_pytimedelta(), ], ids=lambda x: type(x).__name__, ) diff --git a/pandas/tests/base/test_value_counts.py b/pandas/tests/base/test_value_counts.py index d792c36bd8da5..64ef7c1242c0a 100644 --- a/pandas/tests/base/test_value_counts.py +++ b/pandas/tests/base/test_value_counts.py @@ -359,7 +359,7 @@ def test_value_counts_object_inference_deprecated(): + [Timestamp("2016-01-02")] + [Timestamp("2016-01-01") + Timedelta(days=i) for i in range(1, 5)] ), - DatetimeIndex(pd.date_range("2016-01-01", periods=5, freq="D")), + DatetimeIndex(pd.date_range("2016-01-01", periods=5, freq="D", unit="us")), ], [ TimedeltaIndex( diff --git a/pandas/tests/scalar/timedelta/methods/test_as_unit.py b/pandas/tests/scalar/timedelta/methods/test_as_unit.py index 8660141e5a537..005c4d1d5fa99 100644 --- a/pandas/tests/scalar/timedelta/methods/test_as_unit.py +++ b/pandas/tests/scalar/timedelta/methods/test_as_unit.py @@ -10,29 +10,29 @@ class TestAsUnit: def test_as_unit(self): td = Timedelta(days=1) - assert td.as_unit("ns") is td + assert td.as_unit("us") is td - res = td.as_unit("us") - assert res._value == td._value // 1000 - assert res._creso == NpyDatetimeUnit.NPY_FR_us.value + res = td.as_unit("ns") + assert res._value == td._value * 1000 + assert res._creso == NpyDatetimeUnit.NPY_FR_ns.value - rt = res.as_unit("ns") + rt = res.as_unit("us") assert rt._value == td._value assert rt._creso == td._creso res = td.as_unit("ms") - assert res._value == td._value // 1_000_000 + assert res._value == td._value // 1_000 assert res._creso == NpyDatetimeUnit.NPY_FR_ms.value - rt = res.as_unit("ns") + rt = res.as_unit("us") assert rt._value == td._value assert rt._creso == td._creso res = td.as_unit("s") - assert res._value == td._value // 1_000_000_000 + assert res._value == td._value // 1_000_000 assert res._creso == NpyDatetimeUnit.NPY_FR_s.value - rt = res.as_unit("ns") + rt = res.as_unit("us") assert rt._value == td._value assert rt._creso == td._creso diff --git a/pandas/tests/scalar/timedelta/test_arithmetic.py b/pandas/tests/scalar/timedelta/test_arithmetic.py index 1f6ece9f3e8f1..d0f67457b601f 100644 --- a/pandas/tests/scalar/timedelta/test_arithmetic.py +++ b/pandas/tests/scalar/timedelta/test_arithmetic.py @@ -818,11 +818,11 @@ def test_mod_numeric(self): assert isinstance(result, Timedelta) assert result == Timedelta(0) - result = td % 1e12 + result = td % 1e9 assert isinstance(result, Timedelta) assert result == Timedelta(minutes=3, seconds=20) - result = td % int(1e12) + result = td % int(1e9) assert isinstance(result, Timedelta) assert result == Timedelta(minutes=3, seconds=20) @@ -876,8 +876,8 @@ def test_divmod_numeric(self): # GH#19365 td = Timedelta(days=2, hours=6) - result = divmod(td, 53 * 3600 * 1e9) - assert result[0] == Timedelta(1, unit="ns") + result = divmod(td, 53 * 3600 * 1e6) + assert result[0] == Timedelta(1, unit="us").as_unit("us") assert isinstance(result[1], Timedelta) assert result[1] == Timedelta(hours=1) diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index 28614c513e8ef..36642a9fb4a30 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -272,14 +272,14 @@ def test_construction(): assert Timedelta(10, unit="D")._value == expected assert Timedelta(10.0, unit="D")._value == expected assert Timedelta("10 days")._value == expected - assert Timedelta(days=10)._value == expected - assert Timedelta(days=10.0)._value == expected + assert Timedelta(days=10)._value == expected // 1000 + assert Timedelta(days=10.0)._value == expected // 1000 expected += np.timedelta64(10, "s").astype("m8[ns]").view("i8") assert Timedelta("10 days 00:00:10")._value == expected - assert Timedelta(days=10, seconds=10)._value == expected - assert Timedelta(days=10, milliseconds=10 * 1000)._value == expected - assert Timedelta(days=10, microseconds=10 * 1000 * 1000)._value == expected + assert Timedelta(days=10, seconds=10)._value == expected // 1000 + assert Timedelta(days=10, milliseconds=10 * 1000)._value == expected // 1000 + assert Timedelta(days=10, microseconds=10 * 1000 * 1000)._value == expected // 1000 # rounding cases assert Timedelta(82739999850000)._value == 82739999850000 @@ -411,7 +411,7 @@ def test_construction(): def test_td_construction_with_np_dtypes(npdtype, item): # GH#8757: test construction with np dtypes pykwarg, npkwarg = item - expected = np.timedelta64(1, npkwarg).astype("m8[ns]").view("i8") + expected = np.timedelta64(1, npkwarg).astype("m8[us]").view("i8") assert Timedelta(**{pykwarg: npdtype(1)})._value == expected diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index cf878b1164b3f..9ddd4e62ac973 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -664,7 +664,7 @@ def test_resolution_deprecated(self): # GH#21344 td = Timedelta(days=4, hours=3) result = td.resolution - assert result == Timedelta(nanoseconds=1) + assert result == Timedelta(microseconds=1) # Check that the attribute is available on the class, mirroring # the stdlib timedelta behavior From 55c24543479a46db84677a773fbd861fcc0c95c1 Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 26 Nov 2025 18:12:35 -0800 Subject: [PATCH 2/4] show tzs in tzinfo assertion --- pandas/tests/tseries/offsets/test_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/tseries/offsets/test_common.py b/pandas/tests/tseries/offsets/test_common.py index 34181f28bb1a0..35f84d5940c82 100644 --- a/pandas/tests/tseries/offsets/test_common.py +++ b/pandas/tests/tseries/offsets/test_common.py @@ -150,7 +150,7 @@ def test_apply_out_of_range(request, tz_naive_fixture, _offset): request.applymarker( pytest.mark.xfail(reason="After GH#49737 t.tzinfo is None on CI") ) - assert str(t.tzinfo) == str(result.tzinfo) + assert str(t.tzinfo) == str(result.tzinfo), (t.tzinfo, result.tzinfo) except OutOfBoundsDatetime: pass From 1791f231d7c9c124edf7a61d49bfc17cfd9c9d4a Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 30 Nov 2025 15:01:15 -0800 Subject: [PATCH 3/4] targeted tests --- .../tests/scalar/timedelta/test_constructors.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index 36642a9fb4a30..ea764b8b92eeb 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -19,6 +19,21 @@ import pandas._testing as tm +class TestTimedeltaConstructorKeywordBased: + # Tests for constructing a Timedelta from keywords like the pytimedelta + # base class + def test_nanosecond_keyword(self): + # GH#63216 + td = Timedelta(nanoseconds=1000) + assert td.unit == "ns" + + def test_noninteger_microseconds(self): + # GH#63216 + td = Timedelta(microseconds=1.5) + assert td.unit == "ns" + assert td == Timedelta(nanoseconds=1500) + + class TestTimedeltaConstructorUnitKeyword: @pytest.mark.parametrize("unit", ["Y", "y", "M"]) def test_unit_m_y_raises(self, unit): From ac5ccde50a01f3753be447631457b4146a5d303b Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 2 Dec 2025 11:48:44 -0800 Subject: [PATCH 4/4] Expand xfail --- pandas/tests/tseries/offsets/test_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/tseries/offsets/test_common.py b/pandas/tests/tseries/offsets/test_common.py index 35f84d5940c82..4f89305bba5e8 100644 --- a/pandas/tests/tseries/offsets/test_common.py +++ b/pandas/tests/tseries/offsets/test_common.py @@ -145,7 +145,7 @@ def test_apply_out_of_range(request, tz_naive_fixture, _offset): elif ( isinstance(tz, tzlocal) and is_platform_windows() - and _offset in (QuarterEnd, BQuarterBegin, BQuarterEnd) + and _offset in (QuarterEnd, BQuarterBegin, BQuarterEnd, FY5253Quarter) ): request.applymarker( pytest.mark.xfail(reason="After GH#49737 t.tzinfo is None on CI")