Skip to content

Implement NaT properties/methods directly #17765

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 7 commits into from
Oct 5, 2017
Merged
Show file tree
Hide file tree
Changes from 3 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
201 changes: 110 additions & 91 deletions pandas/_libs/tslib.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ PyDateTime_IMPORT
cdef int64_t NPY_NAT = util.get_nat()
iNaT = NPY_NAT


from tslibs.timezones cimport (
is_utc, is_tzlocal, is_fixed_offset,
treat_tz_as_dateutil, treat_tz_as_pytz,
Expand Down Expand Up @@ -862,6 +861,24 @@ class NaTType(_NaT):
return NaT
return NotImplemented

# GH9513 NaT methods (except to_datetime64) to raise, return np.nan, or
# return NaT create functions that raise, for binding to NaTType
astimezone = _make_error_func('astimezone', Timestamp)
fromordinal = _make_error_func('fromordinal', Timestamp)

# _nat_methods
to_pydatetime = _make_nat_func('to_pydatetime', Timestamp)

now = _make_nat_func('now', Timestamp)
today = _make_nat_func('today', Timestamp)
round = _make_nat_func('round', Timestamp)
floor = _make_nat_func('floor', Timestamp)
ceil = _make_nat_func('ceil', Timestamp)

tz_convert = _make_nat_func('tz_convert', Timestamp)
tz_localize = _make_nat_func('tz_localize', Timestamp)
replace = _make_nat_func('replace', Timestamp)


def __nat_unpickle(*args):
# return constant defined in the module
Expand Down Expand Up @@ -1320,6 +1337,32 @@ cdef _nat_rdivide_op(self, other):
return np.nan
return NotImplemented


def _make_nat_func(func_name, cls):
def f(*args, **kwargs):
return NaT
f.__name__ = func_name
f.__doc__ = getattr(cls, func_name).__doc__
return f


def _make_nan_func(func_name, cls):
def f(*args, **kwargs):
return np.nan
f.__name__ = func_name
f.__doc__ = getattr(cls, func_name).__doc__
return f


def _make_error_func(func_name, cls):
def f(*args, **kwargs):
raise ValueError("NaTType does not support " + func_name)

f.__name__ = func_name
f.__doc__ = getattr(cls, func_name).__doc__
return f


cdef class _NaT(_Timestamp):

def __hash__(_NaT self):
Expand Down Expand Up @@ -1384,6 +1427,71 @@ cdef class _NaT(_Timestamp):
return NaT
return NotImplemented

# ----------------------------------------------------------------------
# inject the Timestamp field properties
# these by definition return np.nan

year = property(fget=lambda self: np.nan)
quarter = property(fget=lambda self: np.nan)
month = property(fget=lambda self: np.nan)
day = property(fget=lambda self: np.nan)
hour = property(fget=lambda self: np.nan)
minute = property(fget=lambda self: np.nan)
second = property(fget=lambda self: np.nan)
millisecond = property(fget=lambda self: np.nan)
microsecond = property(fget=lambda self: np.nan)
nanosecond = property(fget=lambda self: np.nan)

week = property(fget=lambda self: np.nan)
dayofyear = property(fget=lambda self: np.nan)
weekofyear = property(fget=lambda self: np.nan)
days_in_month = property(fget=lambda self: np.nan)
daysinmonth = property(fget=lambda self: np.nan)
dayofweek = property(fget=lambda self: np.nan)
weekday_name = property(fget=lambda self: np.nan)

# inject Timedelta properties
days = property(fget=lambda self: np.nan)
seconds = property(fget=lambda self: np.nan)
microseconds = property(fget=lambda self: np.nan)
nanoseconds = property(fget=lambda self: np.nan)

# inject pd.Period properties
qyear = property(fget=lambda self: np.nan)

# ----------------------------------------------------------------------
# GH9513 NaT methods (except to_datetime64) to raise, return np.nan, or
# return NaT create functions that raise, for binding to NaTType
# These are the ones that can get their docstrings from datetime.

# nan methods
weekday = _make_nan_func('weekday', datetime)
isoweekday = _make_nan_func('isoweekday', datetime)

# _nat_methods
date = _make_nat_func('date', datetime)

utctimetuple = _make_error_func('utctimetuple', datetime)
utcnow = _make_error_func('utcnow', datetime)
utcfromtimestamp = _make_error_func('utcfromtimestamp', datetime)
timetz = _make_error_func('timetz', datetime)
timetuple = _make_error_func('timetuple', datetime)
strptime = _make_error_func('strptime', datetime)
strftime = _make_error_func('strftime', datetime)
isocalendar = _make_error_func('isocalendar', datetime)
fromtimestamp = _make_error_func('fromtimestamp', datetime)
dst = _make_error_func('dst', datetime)
ctime = _make_error_func('ctime', datetime)
combine = _make_error_func('combine', datetime)

time = _make_error_func('time', datetime)
toordinal = _make_error_func('toordinal', datetime)
tzname = _make_error_func('tzname', datetime)
utcoffset = _make_error_func('utcoffset', datetime)

if PY3:
timestamp = _make_error_func('timestamp', datetime)


# lightweight C object to hold datetime & int64 pair
cdef class _TSObject:
Expand Down Expand Up @@ -1537,7 +1645,7 @@ cdef _TSObject convert_datetime_to_tsobject(datetime ts, object tz,
if is_timestamp(ts):
obj.value += ts.nanosecond
obj.dts.ps = ts.nanosecond * 1000

if nanos:
obj.value += nanos
obj.dts.ps = nanos * 1000
Expand Down Expand Up @@ -3255,95 +3363,6 @@ cpdef convert_to_timedelta64(object ts, object unit):
return ts.astype('timedelta64[ns]')


#----------------------------------------------------------------------
# NaT methods/property setups


# inject the Timestamp field properties
# these by definition return np.nan
fields = ['year', 'quarter', 'month', 'day', 'hour',
'minute', 'second', 'millisecond', 'microsecond', 'nanosecond',
'week', 'dayofyear', 'weekofyear', 'days_in_month', 'daysinmonth',
'dayofweek', 'weekday_name', 'days', 'seconds', 'microseconds',
'nanoseconds', 'qyear']
for field in fields:
prop = property(fget=lambda self: np.nan)
setattr(NaTType, field, prop)


# define how we are handling NaT methods & inject
# to the NaTType class; these can return NaT, np.nan
# or raise respectively
_nat_methods = ['date', 'now', 'replace', 'to_pydatetime',
'today', 'round', 'floor', 'ceil', 'tz_convert',
'tz_localize']
_nan_methods = ['weekday', 'isoweekday']
_implemented_methods = [
'to_datetime', 'to_datetime64', 'isoformat', 'total_seconds']
_implemented_methods.extend(_nat_methods)
_implemented_methods.extend(_nan_methods)


def _get_docstring(_method_name):
# NaT serves double duty as Timestamp & Timedelta
# missing value, so need to acquire doc-strings for both

try:
return getattr(Timestamp, _method_name).__doc__
except AttributeError:
pass

try:
return getattr(Timedelta, _method_name).__doc__
except AttributeError:
pass

return None


for _method_name in _nat_methods:

def _make_nat_func(func_name):
def f(*args, **kwargs):
return NaT
f.__name__ = func_name
f.__doc__ = _get_docstring(func_name)
return f

setattr(NaTType, _method_name, _make_nat_func(_method_name))


for _method_name in _nan_methods:

def _make_nan_func(func_name):
def f(*args, **kwargs):
return np.nan
f.__name__ = func_name
f.__doc__ = _get_docstring(func_name)
return f

setattr(NaTType, _method_name, _make_nan_func(_method_name))


# GH9513 NaT methods (except to_datetime64) to raise, return np.nan, or
# return NaT create functions that raise, for binding to NaTType
for _maybe_method_name in dir(NaTType):
_maybe_method = getattr(NaTType, _maybe_method_name)
if (callable(_maybe_method)
and not _maybe_method_name.startswith("_")
and _maybe_method_name not in _implemented_methods):

def _make_error_func(func_name):
def f(*args, **kwargs):
raise ValueError("NaTType does not support " + func_name)
f.__name__ = func_name
f.__doc__ = _get_docstring(func_name)
return f

setattr(NaTType, _maybe_method_name,
_make_error_func(_maybe_method_name))


#----------------------------------------------------------------------
# Conversion routines

Expand Down
40 changes: 40 additions & 0 deletions pandas/tests/scalar/test_nat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from pandas.util import testing as tm
from pandas._libs.tslib import iNaT

from pandas.compat import callable


@pytest.mark.parametrize('nat, idx', [(Timestamp('NaT'), DatetimeIndex),
(Timedelta('NaT'), TimedeltaIndex),
Expand Down Expand Up @@ -156,6 +158,44 @@ def test_NaT_methods():
assert NaT.isoformat() == 'NaT'


def test_NaT_docstrings():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add the issue number as a comment

nat_names = dir(NaT)

ts_names = dir(Timestamp)
ts_missing = [x for x in ts_names if x not in nat_names and
not x.startswith('_')]
ts_missing.sort()
ts_expected = ['freqstr', 'normalize', 'offset',
'to_julian_date', 'to_period', 'tz']
assert ts_missing == ts_expected

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

give a comment before each 'section' of the test so a future reader gets what you are testing

ts_overlap = [x for x in nat_names if x in ts_names and
not x.startswith('_') and
callable(getattr(Timestamp, x))]
for name in ts_overlap:
tsdoc = getattr(Timestamp, name).__doc__
natdoc = getattr(NaT, name).__doc__
assert tsdoc == natdoc

td_names = dir(Timedelta)
td_missing = [x for x in td_names if x not in nat_names and
not x.startswith('_')]
td_missing.sort()
td_expected = ['components', 'delta', 'is_populated',
'to_pytimedelta', 'to_timedelta64', 'view']
assert td_missing == td_expected

td_overlap = [x for x in nat_names if x in td_names and
x not in ts_names and # Timestamp __doc__ takes priority
not x.startswith('_') and
callable(getattr(Timedelta, x))]
assert td_overlap == ['total_seconds']
for name in td_overlap:
tddoc = getattr(Timedelta, name).__doc__
natdoc = getattr(NaT, name).__doc__
assert tddoc == natdoc


@pytest.mark.parametrize('klass', [Timestamp, Timedelta])
def test_isoformat(klass):

Expand Down