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 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
206 changes: 115 additions & 91 deletions pandas/_libs/tslib.pyx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
# cython: profile=False
# cython: linetrace=False
# distutils: define_macros=CYTHON_TRACE=0
# distutils: define_macros=CYTHON_TRACE_NOGIL=0

cimport numpy as np
from numpy cimport (int8_t, int32_t, int64_t, import_array, ndarray,
Expand Down Expand Up @@ -79,7 +82,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 @@ -780,6 +782,32 @@ class Timestamp(_Timestamp):
_nat_strings = set(['NaT', 'nat', 'NAT', 'nan', 'NaN', 'NAN'])


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
if cls is not None:
f.__doc__ = getattr(cls, func_name).__doc__
return f


class NaTType(_NaT):
"""(N)ot-(A)-(T)ime, the time equivalent of NaN"""

Expand Down Expand Up @@ -862,6 +890,90 @@ class NaTType(_NaT):
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)
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)
dst = _make_error_func('dst', datetime)
ctime = _make_error_func('ctime', 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)

# Timestamp has empty docstring for some methods.
Copy link
Contributor

Choose a reason for hiding this comment

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

these are not defined directly in Timestamp/_Timestamp rather directly in datetime.datetime, maybe direct to there? (I don't know where its actually defined, nor why the inheritence messes this up)

Copy link
Member Author

Choose a reason for hiding this comment

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

They are defined in Timestamp and have empty docstrings there. No idea why. Will add to todo list.

Copy link
Contributor

Choose a reason for hiding this comment

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

ok great.

utcfromtimestamp = _make_error_func('utcfromtimestamp', None)
fromtimestamp = _make_error_func('fromtimestamp', None)
combine = _make_error_func('combine', None)
utcnow = _make_error_func('utcnow', None)

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

# 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 +1432,7 @@ cdef _nat_rdivide_op(self, other):
return np.nan
return NotImplemented


cdef class _NaT(_Timestamp):

def __hash__(_NaT self):
Expand Down Expand Up @@ -1537,7 +1650,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 +3368,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
49 changes: 49 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,53 @@ 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

# GH#17327
nat_names = dir(NaT)

# NaT should have *most* of the Timestamp methods, with matching
# docstrings. The attributes that are not expected to be present in NaT
# are private methods plus `ts_expected` below.
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

# NaT should have *most* of the Timedelta methods, with matching
# docstrings. The attributes that are not expected to be present in NaT
# are private methods plus `td_expected` below.
# For methods that are both Timestamp and Timedelta methods, the
# Timestamp docstring takes priority.
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