Skip to content

ENH: don't silently ignore dtype in NaT/Timestamp/Timedelta to_numpy #44460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v1.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ Other enhancements
- :meth:`read_excel` now accepts a ``decimal`` argument that allow the user to specify the decimal point when parsing string columns to numeric (:issue:`14403`)
- :meth:`.GroupBy.mean` now supports `Numba <http://numba.pydata.org/>`_ execution with the ``engine`` keyword (:issue:`43731`)
- :meth:`Timestamp.isoformat`, now handles the ``timespec`` argument from the base :class:``datetime`` class (:issue:`26131`)
- :meth:`NaT.to_numpy` ``dtype`` argument is now respected, so ``np.timedelta64`` can be returned (:issue:`44460`)
-

.. ---------------------------------------------------------------------------

Expand Down
4 changes: 3 additions & 1 deletion pandas/_libs/tslibs/nattype.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class NaTType(datetime):
value: np.int64
def asm8(self) -> np.datetime64: ...
def to_datetime64(self) -> np.datetime64: ...
def to_numpy(self, dtype=..., copy: bool = ...) -> np.datetime64: ...
def to_numpy(
self, dtype=..., copy: bool = ...
) -> np.datetime64 | np.timedelta64: ...
@property
def is_leap_year(self) -> bool: ...
@property
Expand Down
26 changes: 21 additions & 5 deletions pandas/_libs/tslibs/nattype.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -258,19 +258,20 @@ cdef class _NaT(datetime):
"""
return np.datetime64('NaT', "ns")

def to_numpy(self, dtype=None, copy=False) -> np.datetime64:
def to_numpy(self, dtype=None, copy=False) -> np.datetime64 | np.timedelta64:
"""
Convert the Timestamp to a NumPy datetime64.
Convert the Timestamp to a NumPy datetime64 or timedelta64.

.. versionadded:: 0.25.0

This is an alias method for `Timestamp.to_datetime64()`. The dtype and
copy parameters are available here only for compatibility. Their values
With the default 'dtype', this is an alias method for `NaT.to_datetime64()`.

The copy parameter is available here only for compatibility. Its value
will not affect the return value.

Returns
-------
numpy.datetime64
numpy.datetime64 or numpy.timedelta64

See Also
--------
Expand All @@ -286,7 +287,22 @@ cdef class _NaT(datetime):

>>> pd.NaT.to_numpy()
numpy.datetime64('NaT')

>>> pd.NaT.to_numpy("m8[ns]")
numpy.timedelta64('NaT','ns')
"""
if dtype is not None:
# GH#44460
dtype = np.dtype(dtype)
if dtype.kind == "M":
return np.datetime64("NaT").astype(dtype)
elif dtype.kind == "m":
return np.timedelta64("NaT").astype(dtype)
else:
raise ValueError(
"NaT.to_numpy dtype must be a datetime64 dtype, timedelta64 "
"dtype, or None."
)
return self.to_datetime64()

def __repr__(self) -> str:
Expand Down
4 changes: 4 additions & 0 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,10 @@ cdef class _Timedelta(timedelta):
--------
Series.to_numpy : Similar method for Series.
"""
if dtype is not None or copy is not False:
raise ValueError(
"Timedelta.to_numpy dtype and copy arguments are ignored"
)
return self.to_timedelta64()

def view(self, dtype):
Expand Down
4 changes: 4 additions & 0 deletions pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,10 @@ cdef class _Timestamp(ABCTimestamp):
>>> pd.NaT.to_numpy()
numpy.datetime64('NaT')
"""
if dtype is not None or copy is not False:
raise ValueError(
"Timestamp.to_numpy dtype and copy arguments are ignored."
)
return self.to_datetime64()

def to_period(self, freq=None):
Expand Down
21 changes: 21 additions & 0 deletions pandas/tests/scalar/test_nat.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ def test_nat_doc_strings(compare):
if klass == Timestamp and method == "isoformat":
return

if method == "to_numpy":
# GH#44460 can return either dt64 or td64 depending on dtype,
# different docstring is intentional
return

nat_doc = getattr(NaT, method).__doc__
assert klass_doc == nat_doc

Expand Down Expand Up @@ -511,6 +516,22 @@ def test_to_numpy_alias():

assert isna(expected) and isna(result)

# GH#44460
result = NaT.to_numpy("M8[s]")
assert isinstance(result, np.datetime64)
assert result.dtype == "M8[s]"

result = NaT.to_numpy("m8[ns]")
assert isinstance(result, np.timedelta64)
assert result.dtype == "m8[ns]"

result = NaT.to_numpy("m8[s]")
assert isinstance(result, np.timedelta64)
assert result.dtype == "m8[s]"

with pytest.raises(ValueError, match="NaT.to_numpy dtype must be a "):
NaT.to_numpy(np.int64)


@pytest.mark.parametrize(
"other",
Expand Down
7 changes: 7 additions & 0 deletions pandas/tests/scalar/timedelta/test_timedelta.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,13 @@ def test_to_numpy_alias(self):
td = Timedelta("10m7s")
assert td.to_timedelta64() == td.to_numpy()

# GH#44460
msg = "dtype and copy arguments are ignored"
with pytest.raises(ValueError, match=msg):
td.to_numpy("m8[s]")
with pytest.raises(ValueError, match=msg):
td.to_numpy(copy=True)

@pytest.mark.parametrize(
"freq,s1,s2",
[
Expand Down
7 changes: 7 additions & 0 deletions pandas/tests/scalar/timestamp/test_timestamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,13 @@ def test_to_numpy_alias(self):
ts = Timestamp(datetime.now())
assert ts.to_datetime64() == ts.to_numpy()

# GH#44460
msg = "dtype and copy arguments are ignored"
with pytest.raises(ValueError, match=msg):
ts.to_numpy("M8[s]")
with pytest.raises(ValueError, match=msg):
ts.to_numpy(copy=True)


class SubDatetime(datetime):
pass
Expand Down