Skip to content

Commit 5337514

Browse files
authored
Merge branch 'main' into shiny-new-feature
2 parents f63f40a + 00a7c41 commit 5337514

File tree

27 files changed

+528
-254
lines changed

27 files changed

+528
-254
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,7 @@ Other Deprecations
808808
- Deprecated option "future.no_silent_downcasting", as it is no longer used. In a future version accessing this option will raise (:issue:`59502`)
809809
- Deprecated passing non-Index types to :meth:`Index.join`; explicitly convert to Index first (:issue:`62897`)
810810
- Deprecated silent casting of non-datetime 'other' to datetime in :meth:`Series.combine_first` (:issue:`62931`)
811+
- Deprecated silently casting strings to :class:`Timedelta` in binary operations with :class:`Timedelta` (:issue:`59653`)
811812
- Deprecated slicing on a :class:`Series` or :class:`DataFrame` with a :class:`DatetimeIndex` using a ``datetime.date`` object, explicitly cast to :class:`Timestamp` instead (:issue:`35830`)
812813
- Deprecated support for the Dataframe Interchange Protocol (:issue:`56732`)
813814
- Deprecated the 'inplace' keyword from :meth:`Resampler.interpolate`, as passing ``True`` raises ``AttributeError`` (:issue:`58690`)

pandas/_libs/tslibs/timedeltas.pyx

Lines changed: 87 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -333,58 +333,39 @@ cdef convert_to_timedelta64(object ts, str unit):
333333
334334
Handle these types of objects:
335335
- timedelta/Timedelta
336-
- timedelta64
337-
- an offset
338-
- np.int64 (with unit providing a possible modifier)
339-
- None/NaT
340336
341-
Return an ns based int64
337+
Return an timedelta64[ns] object
342338
"""
343339
# Caller is responsible for checking unit not in ["Y", "y", "M"]
344-
if checknull_with_nat_and_na(ts):
345-
return np.timedelta64(NPY_NAT, "ns")
346-
elif isinstance(ts, _Timedelta):
340+
if isinstance(ts, _Timedelta):
347341
# already in the proper format
348342
if ts._creso != NPY_FR_ns:
349343
ts = ts.as_unit("ns").asm8
350344
else:
351345
ts = np.timedelta64(ts._value, "ns")
352-
elif cnp.is_timedelta64_object(ts):
353-
ts = ensure_td64ns(ts)
354-
elif is_integer_object(ts):
355-
if ts == NPY_NAT:
356-
return np.timedelta64(NPY_NAT, "ns")
357-
else:
358-
ts = _maybe_cast_from_unit(ts, unit)
359-
elif is_float_object(ts):
360-
ts = _maybe_cast_from_unit(ts, unit)
361-
elif isinstance(ts, str):
362-
if (len(ts) > 0 and ts[0] == "P") or (len(ts) > 1 and ts[:2] == "-P"):
363-
ts = parse_iso_format_string(ts)
364-
else:
365-
ts = parse_timedelta_string(ts)
366-
ts = np.timedelta64(ts, "ns")
367-
elif is_tick_object(ts):
368-
ts = np.timedelta64(ts.nanos, "ns")
369346

370-
if PyDelta_Check(ts):
347+
elif PyDelta_Check(ts):
371348
ts = np.timedelta64(delta_to_nanoseconds(ts), "ns")
372349
elif not cnp.is_timedelta64_object(ts):
373350
raise TypeError(f"Invalid type for timedelta scalar: {type(ts)}")
374351
return ts.astype("timedelta64[ns]")
375352

376353

377-
cdef _maybe_cast_from_unit(ts, str unit):
354+
cdef _numeric_to_td64ns(object item, str unit):
378355
# caller is responsible for checking
379356
# assert unit not in ["Y", "y", "M"]
357+
# assert is_integer_object(item) or is_float_object(item)
358+
if is_integer_object(item) and item == NPY_NAT:
359+
return np.timedelta64(NPY_NAT, "ns")
360+
380361
try:
381-
ts = cast_from_unit(ts, unit)
362+
item = cast_from_unit(item, unit)
382363
except OutOfBoundsDatetime as err:
383364
raise OutOfBoundsTimedelta(
384-
f"Cannot cast {ts} from {unit} to 'ns' without overflow."
365+
f"Cannot cast {item} from {unit} to 'ns' without overflow."
385366
) from err
386367

387-
ts = np.timedelta64(ts, "ns")
368+
ts = np.timedelta64(item, "ns")
388369
return ts
389370

390371

@@ -408,10 +389,11 @@ def array_to_timedelta64(
408389
cdef:
409390
Py_ssize_t i, n = values.size
410391
ndarray result = np.empty((<object>values).shape, dtype="m8[ns]")
411-
object item
392+
object item, td64ns_obj
412393
int64_t ival
413394
cnp.broadcast mi = cnp.PyArray_MultiIterNew2(result, values)
414395
cnp.flatiter it
396+
str parsed_unit = parse_timedelta_unit(unit or "ns")
415397

416398
if values.descr.type_num != cnp.NPY_OBJECT:
417399
# raise here otherwise we segfault below
@@ -431,70 +413,63 @@ def array_to_timedelta64(
431413
)
432414
cnp.PyArray_ITER_NEXT(it)
433415

434-
# Usually, we have all strings. If so, we hit the fast path.
435-
# If this path fails, we try conversion a different way, and
436-
# this is where all of the error handling will take place.
437-
try:
438-
for i in range(n):
439-
# Analogous to: item = values[i]
440-
item = <object>(<PyObject**>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
416+
for i in range(n):
417+
item = <object>(<PyObject**>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
441418

442-
ival = _item_to_timedelta64_fastpath(item)
419+
try:
420+
if checknull_with_nat_and_na(item):
421+
ival = NPY_NAT
443422

444-
# Analogous to: iresult[i] = ival
445-
(<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = ival
423+
elif cnp.is_timedelta64_object(item):
424+
td64ns_obj = ensure_td64ns(item)
425+
ival = cnp.get_timedelta64_value(td64ns_obj)
446426

447-
cnp.PyArray_MultiIter_NEXT(mi)
427+
elif isinstance(item, _Timedelta):
428+
if item._creso != NPY_FR_ns:
429+
ival = item.as_unit("ns")._value
430+
else:
431+
ival = item._value
432+
433+
elif PyDelta_Check(item):
434+
# i.e. isinstance(item, timedelta)
435+
ival = delta_to_nanoseconds(item)
436+
437+
elif isinstance(item, str):
438+
if (
439+
(len(item) > 0 and item[0] == "P")
440+
or (len(item) > 1 and item[:2] == "-P")
441+
):
442+
ival = parse_iso_format_string(item)
443+
else:
444+
ival = parse_timedelta_string(item)
448445

449-
except (TypeError, ValueError):
450-
cnp.PyArray_MultiIter_RESET(mi)
446+
elif is_tick_object(item):
447+
ival = item.nanos
451448

452-
parsed_unit = parse_timedelta_unit(unit or "ns")
453-
for i in range(n):
454-
item = <object>(<PyObject**>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
449+
elif is_integer_object(item) or is_float_object(item):
450+
td64ns_obj = _numeric_to_td64ns(item, parsed_unit)
451+
ival = cnp.get_timedelta64_value(td64ns_obj)
455452

456-
ival = _item_to_timedelta64(item, parsed_unit, errors)
453+
else:
454+
raise TypeError(f"Invalid type for timedelta scalar: {type(item)}")
455+
456+
except ValueError as err:
457+
if errors == "coerce":
458+
ival = NPY_NAT
459+
elif "unit abbreviation w/o a number" in str(err):
460+
# re-raise with more pertinent message
461+
msg = f"Could not convert '{item}' to NumPy timedelta"
462+
raise ValueError(msg) from err
463+
else:
464+
raise
457465

458-
(<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = ival
466+
(<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = ival
459467

460-
cnp.PyArray_MultiIter_NEXT(mi)
468+
cnp.PyArray_MultiIter_NEXT(mi)
461469

462470
return result
463471

464472

465-
cdef int64_t _item_to_timedelta64_fastpath(object item) except? -1:
466-
"""
467-
See array_to_timedelta64.
468-
"""
469-
if item is NaT:
470-
# we allow this check in the fast-path because NaT is a C-object
471-
# so this is an inexpensive check
472-
return NPY_NAT
473-
else:
474-
return parse_timedelta_string(item)
475-
476-
477-
cdef int64_t _item_to_timedelta64(
478-
object item,
479-
str parsed_unit,
480-
str errors
481-
) except? -1:
482-
"""
483-
See array_to_timedelta64.
484-
"""
485-
try:
486-
return cnp.get_timedelta64_value(convert_to_timedelta64(item, parsed_unit))
487-
except ValueError as err:
488-
if errors == "coerce":
489-
return NPY_NAT
490-
elif "unit abbreviation w/o a number" in str(err):
491-
# re-raise with more pertinent message
492-
msg = f"Could not convert '{item}' to NumPy timedelta"
493-
raise ValueError(msg) from err
494-
else:
495-
raise
496-
497-
498473
@cython.cpow(True)
499474
cdef int64_t parse_timedelta_string(str ts) except? -1:
500475
"""
@@ -798,7 +773,7 @@ def _binary_op_method_timedeltalike(op, name):
798773
return NotImplemented
799774

800775
try:
801-
other = Timedelta(other)
776+
other = _wrapped_to_timedelta(other)
802777
except ValueError:
803778
# failed to parse as timedelta
804779
return NotImplemented
@@ -2154,12 +2129,14 @@ class Timedelta(_Timedelta):
21542129
new_value = delta_to_nanoseconds(value, reso=new_reso)
21552130
return cls._from_value_and_reso(new_value, reso=new_reso)
21562131

2132+
elif checknull_with_nat_and_na(value):
2133+
return NaT
2134+
21572135
elif is_integer_object(value) or is_float_object(value):
21582136
# unit=None is de-facto 'ns'
21592137
unit = parse_timedelta_unit(unit)
2160-
value = convert_to_timedelta64(value, unit)
2161-
elif checknull_with_nat_and_na(value):
2162-
return NaT
2138+
value = _numeric_to_td64ns(value, unit)
2139+
21632140
else:
21642141
raise ValueError(
21652142
"Value must be Timedelta, string, integer, "
@@ -2341,7 +2318,7 @@ class Timedelta(_Timedelta):
23412318
def __truediv__(self, other):
23422319
if _should_cast_to_timedelta(other):
23432320
# We interpret NaT as timedelta64("NaT")
2344-
other = Timedelta(other)
2321+
other = _wrapped_to_timedelta(other)
23452322
if other is NaT:
23462323
return np.nan
23472324
if other._creso != self._creso:
@@ -2374,7 +2351,7 @@ class Timedelta(_Timedelta):
23742351
def __rtruediv__(self, other):
23752352
if _should_cast_to_timedelta(other):
23762353
# We interpret NaT as timedelta64("NaT")
2377-
other = Timedelta(other)
2354+
other = _wrapped_to_timedelta(other)
23782355
if other is NaT:
23792356
return np.nan
23802357
if self._creso != other._creso:
@@ -2402,7 +2379,7 @@ class Timedelta(_Timedelta):
24022379
# just defer
24032380
if _should_cast_to_timedelta(other):
24042381
# We interpret NaT as timedelta64("NaT")
2405-
other = Timedelta(other)
2382+
other = _wrapped_to_timedelta(other)
24062383
if other is NaT:
24072384
return np.nan
24082385
if self._creso != other._creso:
@@ -2457,7 +2434,7 @@ class Timedelta(_Timedelta):
24572434
# just defer
24582435
if _should_cast_to_timedelta(other):
24592436
# We interpret NaT as timedelta64("NaT")
2460-
other = Timedelta(other)
2437+
other = _wrapped_to_timedelta(other)
24612438
if other is NaT:
24622439
return np.nan
24632440
if self._creso != other._creso:
@@ -2525,6 +2502,7 @@ def truediv_object_array(ndarray left, ndarray right):
25252502
if cnp.get_timedelta64_value(td64) == NPY_NAT:
25262503
# td here should be interpreted as a td64 NaT
25272504
if _should_cast_to_timedelta(obj):
2505+
_wrapped_to_timedelta(obj) # deprecate if allowing string
25282506
res_value = np.nan
25292507
else:
25302508
# if its a number then let numpy handle division, otherwise
@@ -2554,6 +2532,7 @@ def floordiv_object_array(ndarray left, ndarray right):
25542532
if cnp.get_timedelta64_value(td64) == NPY_NAT:
25552533
# td here should be interpreted as a td64 NaT
25562534
if _should_cast_to_timedelta(obj):
2535+
_wrapped_to_timedelta(obj) # deprecate allowing string
25572536
res_value = np.nan
25582537
else:
25592538
# if its a number then let numpy handle division, otherwise
@@ -2585,6 +2564,23 @@ cdef bint is_any_td_scalar(object obj):
25852564
)
25862565

25872566

2567+
cdef inline _wrapped_to_timedelta(object other):
2568+
# Helper for deprecating cases where we cast str to Timedelta
2569+
td = Timedelta(other)
2570+
if isinstance(other, str):
2571+
from pandas.errors import Pandas4Warning
2572+
warnings.warn(
2573+
# GH#59653
2574+
"Scalar operations between Timedelta and string are "
2575+
"deprecated and will raise in a future version. "
2576+
"Explicitly cast to Timedelta first.",
2577+
Pandas4Warning,
2578+
stacklevel=find_stack_level(),
2579+
)
2580+
# When this is enforced, remove str from _should_cast_to_timedelta
2581+
return td
2582+
2583+
25882584
cdef bint _should_cast_to_timedelta(object obj):
25892585
"""
25902586
Should we treat this object as a Timedelta for the purpose of a binary op

pandas/conftest.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ def index(request):
722722
- ...
723723
"""
724724
# copy to avoid mutation, e.g. setting .name
725-
return indices_dict[request.param].copy()
725+
return indices_dict[request.param].copy(deep=False)
726726

727727

728728
@pytest.fixture(
@@ -735,7 +735,7 @@ def index_flat(request):
735735
index fixture, but excluding MultiIndex cases.
736736
"""
737737
key = request.param
738-
return indices_dict[key].copy()
738+
return indices_dict[key].copy(deep=False)
739739

740740

741741
@pytest.fixture(
@@ -758,18 +758,15 @@ def index_with_missing(request):
758758
759759
MultiIndex is excluded because isna() is not defined for MultiIndex.
760760
"""
761-
762-
# GH 35538. Use deep copy to avoid illusive bug on np-dev
763-
# GHA pipeline that writes into indices_dict despite copy
764-
ind = indices_dict[request.param].copy(deep=True)
765-
vals = ind.values.copy()
761+
ind = indices_dict[request.param]
766762
if request.param in ["tuples", "mi-with-dt64tz-level", "multi"]:
767763
# For setting missing values in the top level of MultiIndex
768764
vals = ind.tolist()
769765
vals[0] = (None,) + vals[0][1:]
770766
vals[-1] = (None,) + vals[-1][1:]
771767
return MultiIndex.from_tuples(vals)
772768
else:
769+
vals = ind.values.copy()
773770
vals[0] = None
774771
vals[-1] = None
775772
return type(ind)(vals)
@@ -850,7 +847,7 @@ def index_or_series_obj(request):
850847
Fixture for tests on indexes, series and series with a narrow dtype
851848
copy to avoid mutation, e.g. setting .name
852849
"""
853-
return _index_or_series_objs[request.param].copy(deep=True)
850+
return _index_or_series_objs[request.param].copy(deep=False)
854851

855852

856853
_typ_objects_series = {
@@ -873,7 +870,7 @@ def index_or_series_memory_obj(request):
873870
series with empty objects type
874871
copy to avoid mutation, e.g. setting .name
875872
"""
876-
return _index_or_series_memory_objs[request.param].copy(deep=True)
873+
return _index_or_series_memory_objs[request.param].copy(deep=False)
877874

878875

879876
# ----------------------------------------------------------------

0 commit comments

Comments
 (0)