Skip to content

Commit 94e6a39

Browse files
jbrockmendelKrzysztof Chomski
authored and
Krzysztof Chomski
committed
Implement NaT properties/methods directly (pandas-dev#17765)
1 parent c1e68cf commit 94e6a39

File tree

2 files changed

+164
-91
lines changed

2 files changed

+164
-91
lines changed

pandas/_libs/tslib.pyx

+115-91
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# -*- coding: utf-8 -*-
22
# cython: profile=False
3+
# cython: linetrace=False
4+
# distutils: define_macros=CYTHON_TRACE=0
5+
# distutils: define_macros=CYTHON_TRACE_NOGIL=0
36

47
cimport numpy as np
58
from numpy cimport (int8_t, int32_t, int64_t, import_array, ndarray,
@@ -79,7 +82,6 @@ PyDateTime_IMPORT
7982
cdef int64_t NPY_NAT = util.get_nat()
8083
iNaT = NPY_NAT
8184

82-
8385
from tslibs.timezones cimport (
8486
is_utc, is_tzlocal, is_fixed_offset,
8587
treat_tz_as_dateutil, treat_tz_as_pytz,
@@ -783,6 +785,32 @@ class Timestamp(_Timestamp):
783785
_nat_strings = set(['NaT', 'nat', 'NAT', 'nan', 'NaN', 'NAN'])
784786

785787

788+
def _make_nat_func(func_name, cls):
789+
def f(*args, **kwargs):
790+
return NaT
791+
f.__name__ = func_name
792+
f.__doc__ = getattr(cls, func_name).__doc__
793+
return f
794+
795+
796+
def _make_nan_func(func_name, cls):
797+
def f(*args, **kwargs):
798+
return np.nan
799+
f.__name__ = func_name
800+
f.__doc__ = getattr(cls, func_name).__doc__
801+
return f
802+
803+
804+
def _make_error_func(func_name, cls):
805+
def f(*args, **kwargs):
806+
raise ValueError("NaTType does not support " + func_name)
807+
808+
f.__name__ = func_name
809+
if cls is not None:
810+
f.__doc__ = getattr(cls, func_name).__doc__
811+
return f
812+
813+
786814
class NaTType(_NaT):
787815
"""(N)ot-(A)-(T)ime, the time equivalent of NaN"""
788816

@@ -865,6 +893,90 @@ class NaTType(_NaT):
865893
return NaT
866894
return NotImplemented
867895

896+
# ----------------------------------------------------------------------
897+
# inject the Timestamp field properties
898+
# these by definition return np.nan
899+
900+
year = property(fget=lambda self: np.nan)
901+
quarter = property(fget=lambda self: np.nan)
902+
month = property(fget=lambda self: np.nan)
903+
day = property(fget=lambda self: np.nan)
904+
hour = property(fget=lambda self: np.nan)
905+
minute = property(fget=lambda self: np.nan)
906+
second = property(fget=lambda self: np.nan)
907+
millisecond = property(fget=lambda self: np.nan)
908+
microsecond = property(fget=lambda self: np.nan)
909+
nanosecond = property(fget=lambda self: np.nan)
910+
911+
week = property(fget=lambda self: np.nan)
912+
dayofyear = property(fget=lambda self: np.nan)
913+
weekofyear = property(fget=lambda self: np.nan)
914+
days_in_month = property(fget=lambda self: np.nan)
915+
daysinmonth = property(fget=lambda self: np.nan)
916+
dayofweek = property(fget=lambda self: np.nan)
917+
weekday_name = property(fget=lambda self: np.nan)
918+
919+
# inject Timedelta properties
920+
days = property(fget=lambda self: np.nan)
921+
seconds = property(fget=lambda self: np.nan)
922+
microseconds = property(fget=lambda self: np.nan)
923+
nanoseconds = property(fget=lambda self: np.nan)
924+
925+
# inject pd.Period properties
926+
qyear = property(fget=lambda self: np.nan)
927+
928+
# ----------------------------------------------------------------------
929+
# GH9513 NaT methods (except to_datetime64) to raise, return np.nan, or
930+
# return NaT create functions that raise, for binding to NaTType
931+
# These are the ones that can get their docstrings from datetime.
932+
933+
# nan methods
934+
weekday = _make_nan_func('weekday', datetime)
935+
isoweekday = _make_nan_func('isoweekday', datetime)
936+
937+
# _nat_methods
938+
date = _make_nat_func('date', datetime)
939+
940+
utctimetuple = _make_error_func('utctimetuple', datetime)
941+
timetz = _make_error_func('timetz', datetime)
942+
timetuple = _make_error_func('timetuple', datetime)
943+
strptime = _make_error_func('strptime', datetime)
944+
strftime = _make_error_func('strftime', datetime)
945+
isocalendar = _make_error_func('isocalendar', datetime)
946+
dst = _make_error_func('dst', datetime)
947+
ctime = _make_error_func('ctime', datetime)
948+
time = _make_error_func('time', datetime)
949+
toordinal = _make_error_func('toordinal', datetime)
950+
tzname = _make_error_func('tzname', datetime)
951+
utcoffset = _make_error_func('utcoffset', datetime)
952+
953+
# Timestamp has empty docstring for some methods.
954+
utcfromtimestamp = _make_error_func('utcfromtimestamp', None)
955+
fromtimestamp = _make_error_func('fromtimestamp', None)
956+
combine = _make_error_func('combine', None)
957+
utcnow = _make_error_func('utcnow', None)
958+
959+
if PY3:
960+
timestamp = _make_error_func('timestamp', datetime)
961+
962+
# GH9513 NaT methods (except to_datetime64) to raise, return np.nan, or
963+
# return NaT create functions that raise, for binding to NaTType
964+
astimezone = _make_error_func('astimezone', Timestamp)
965+
fromordinal = _make_error_func('fromordinal', Timestamp)
966+
967+
# _nat_methods
968+
to_pydatetime = _make_nat_func('to_pydatetime', Timestamp)
969+
970+
now = _make_nat_func('now', Timestamp)
971+
today = _make_nat_func('today', Timestamp)
972+
round = _make_nat_func('round', Timestamp)
973+
floor = _make_nat_func('floor', Timestamp)
974+
ceil = _make_nat_func('ceil', Timestamp)
975+
976+
tz_convert = _make_nat_func('tz_convert', Timestamp)
977+
tz_localize = _make_nat_func('tz_localize', Timestamp)
978+
replace = _make_nat_func('replace', Timestamp)
979+
868980

869981
def __nat_unpickle(*args):
870982
# return constant defined in the module
@@ -1323,6 +1435,7 @@ cdef _nat_rdivide_op(self, other):
13231435
return np.nan
13241436
return NotImplemented
13251437

1438+
13261439
cdef class _NaT(_Timestamp):
13271440

13281441
def __hash__(_NaT self):
@@ -1540,7 +1653,7 @@ cdef _TSObject convert_datetime_to_tsobject(datetime ts, object tz,
15401653
if is_timestamp(ts):
15411654
obj.value += ts.nanosecond
15421655
obj.dts.ps = ts.nanosecond * 1000
1543-
1656+
15441657
if nanos:
15451658
obj.value += nanos
15461659
obj.dts.ps = nanos * 1000
@@ -3258,95 +3371,6 @@ cpdef convert_to_timedelta64(object ts, object unit):
32583371
return ts.astype('timedelta64[ns]')
32593372

32603373

3261-
#----------------------------------------------------------------------
3262-
# NaT methods/property setups
3263-
3264-
3265-
# inject the Timestamp field properties
3266-
# these by definition return np.nan
3267-
fields = ['year', 'quarter', 'month', 'day', 'hour',
3268-
'minute', 'second', 'millisecond', 'microsecond', 'nanosecond',
3269-
'week', 'dayofyear', 'weekofyear', 'days_in_month', 'daysinmonth',
3270-
'dayofweek', 'weekday_name', 'days', 'seconds', 'microseconds',
3271-
'nanoseconds', 'qyear']
3272-
for field in fields:
3273-
prop = property(fget=lambda self: np.nan)
3274-
setattr(NaTType, field, prop)
3275-
3276-
3277-
# define how we are handling NaT methods & inject
3278-
# to the NaTType class; these can return NaT, np.nan
3279-
# or raise respectively
3280-
_nat_methods = ['date', 'now', 'replace', 'to_pydatetime',
3281-
'today', 'round', 'floor', 'ceil', 'tz_convert',
3282-
'tz_localize']
3283-
_nan_methods = ['weekday', 'isoweekday']
3284-
_implemented_methods = [
3285-
'to_datetime', 'to_datetime64', 'isoformat', 'total_seconds']
3286-
_implemented_methods.extend(_nat_methods)
3287-
_implemented_methods.extend(_nan_methods)
3288-
3289-
3290-
def _get_docstring(_method_name):
3291-
# NaT serves double duty as Timestamp & Timedelta
3292-
# missing value, so need to acquire doc-strings for both
3293-
3294-
try:
3295-
return getattr(Timestamp, _method_name).__doc__
3296-
except AttributeError:
3297-
pass
3298-
3299-
try:
3300-
return getattr(Timedelta, _method_name).__doc__
3301-
except AttributeError:
3302-
pass
3303-
3304-
return None
3305-
3306-
3307-
for _method_name in _nat_methods:
3308-
3309-
def _make_nat_func(func_name):
3310-
def f(*args, **kwargs):
3311-
return NaT
3312-
f.__name__ = func_name
3313-
f.__doc__ = _get_docstring(func_name)
3314-
return f
3315-
3316-
setattr(NaTType, _method_name, _make_nat_func(_method_name))
3317-
3318-
3319-
for _method_name in _nan_methods:
3320-
3321-
def _make_nan_func(func_name):
3322-
def f(*args, **kwargs):
3323-
return np.nan
3324-
f.__name__ = func_name
3325-
f.__doc__ = _get_docstring(func_name)
3326-
return f
3327-
3328-
setattr(NaTType, _method_name, _make_nan_func(_method_name))
3329-
3330-
3331-
# GH9513 NaT methods (except to_datetime64) to raise, return np.nan, or
3332-
# return NaT create functions that raise, for binding to NaTType
3333-
for _maybe_method_name in dir(NaTType):
3334-
_maybe_method = getattr(NaTType, _maybe_method_name)
3335-
if (callable(_maybe_method)
3336-
and not _maybe_method_name.startswith("_")
3337-
and _maybe_method_name not in _implemented_methods):
3338-
3339-
def _make_error_func(func_name):
3340-
def f(*args, **kwargs):
3341-
raise ValueError("NaTType does not support " + func_name)
3342-
f.__name__ = func_name
3343-
f.__doc__ = _get_docstring(func_name)
3344-
return f
3345-
3346-
setattr(NaTType, _maybe_method_name,
3347-
_make_error_func(_maybe_method_name))
3348-
3349-
33503374
#----------------------------------------------------------------------
33513375
# Conversion routines
33523376

pandas/tests/scalar/test_nat.py

+49
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from pandas.util import testing as tm
1111
from pandas._libs.tslib import iNaT
1212

13+
from pandas.compat import callable
14+
1315

1416
@pytest.mark.parametrize('nat, idx', [(Timestamp('NaT'), DatetimeIndex),
1517
(Timedelta('NaT'), TimedeltaIndex),
@@ -156,6 +158,53 @@ def test_NaT_methods():
156158
assert NaT.isoformat() == 'NaT'
157159

158160

161+
def test_NaT_docstrings():
162+
# GH#17327
163+
nat_names = dir(NaT)
164+
165+
# NaT should have *most* of the Timestamp methods, with matching
166+
# docstrings. The attributes that are not expected to be present in NaT
167+
# are private methods plus `ts_expected` below.
168+
ts_names = dir(Timestamp)
169+
ts_missing = [x for x in ts_names if x not in nat_names and
170+
not x.startswith('_')]
171+
ts_missing.sort()
172+
ts_expected = ['freqstr', 'normalize', 'offset',
173+
'to_julian_date', 'to_period', 'tz']
174+
assert ts_missing == ts_expected
175+
176+
ts_overlap = [x for x in nat_names if x in ts_names and
177+
not x.startswith('_') and
178+
callable(getattr(Timestamp, x))]
179+
for name in ts_overlap:
180+
tsdoc = getattr(Timestamp, name).__doc__
181+
natdoc = getattr(NaT, name).__doc__
182+
assert tsdoc == natdoc
183+
184+
# NaT should have *most* of the Timedelta methods, with matching
185+
# docstrings. The attributes that are not expected to be present in NaT
186+
# are private methods plus `td_expected` below.
187+
# For methods that are both Timestamp and Timedelta methods, the
188+
# Timestamp docstring takes priority.
189+
td_names = dir(Timedelta)
190+
td_missing = [x for x in td_names if x not in nat_names and
191+
not x.startswith('_')]
192+
td_missing.sort()
193+
td_expected = ['components', 'delta', 'is_populated',
194+
'to_pytimedelta', 'to_timedelta64', 'view']
195+
assert td_missing == td_expected
196+
197+
td_overlap = [x for x in nat_names if x in td_names and
198+
x not in ts_names and # Timestamp __doc__ takes priority
199+
not x.startswith('_') and
200+
callable(getattr(Timedelta, x))]
201+
assert td_overlap == ['total_seconds']
202+
for name in td_overlap:
203+
tddoc = getattr(Timedelta, name).__doc__
204+
natdoc = getattr(NaT, name).__doc__
205+
assert tddoc == natdoc
206+
207+
159208
@pytest.mark.parametrize('klass', [Timestamp, Timedelta])
160209
def test_isoformat(klass):
161210

0 commit comments

Comments
 (0)