From bf54f80d170d7d7f3828b0a6cb6c1837859ff00a Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 3 Oct 2017 09:05:02 -0700 Subject: [PATCH 1/6] implement nat methods/properties directoly add test --- pandas/_libs/tslib.pyx | 201 +++++++++++++++++--------------- pandas/tests/scalar/test_nat.py | 40 +++++++ 2 files changed, 150 insertions(+), 91 deletions(-) diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index ff20ea287bd9d..657a90ed4a513 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -83,7 +83,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, @@ -866,6 +865,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 @@ -1328,6 +1345,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): @@ -1392,6 +1435,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: @@ -1545,7 +1653,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 @@ -3263,95 +3371,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 diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py index 6f852f2b394e1..eb9d31d9134a6 100644 --- a/pandas/tests/scalar/test_nat.py +++ b/pandas/tests/scalar/test_nat.py @@ -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), @@ -156,6 +158,44 @@ def test_NaT_methods(): assert NaT.isoformat() == 'NaT' +def test_NaT_docstrings(): + 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 + + 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): From fd788fe567b5192ea48f1d9a4ad6eb934a72fea7 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 3 Oct 2017 09:19:01 -0700 Subject: [PATCH 2/6] flake8 whitespace fixup --- pandas/tests/scalar/test_nat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py index eb9d31d9134a6..a036c1fc7e3cf 100644 --- a/pandas/tests/scalar/test_nat.py +++ b/pandas/tests/scalar/test_nat.py @@ -186,7 +186,7 @@ def test_NaT_docstrings(): 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 + x not in ts_names and # Timestamp __doc__ takes priority not x.startswith('_') and callable(getattr(Timedelta, x))] assert td_overlap == ['total_seconds'] From b65b3a0d128627d755fa824a3b1de9057c486886 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 3 Oct 2017 10:17:31 -0700 Subject: [PATCH 3/6] comments per reviewer request --- pandas/tests/scalar/test_nat.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py index a036c1fc7e3cf..135e4c544de41 100644 --- a/pandas/tests/scalar/test_nat.py +++ b/pandas/tests/scalar/test_nat.py @@ -159,8 +159,12 @@ def test_NaT_methods(): def test_NaT_docstrings(): + # 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('_')] @@ -177,6 +181,11 @@ def test_NaT_docstrings(): 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('_')] From 7a272f5403bebe8a6746a47765ed2775da91aa7e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 3 Oct 2017 10:31:48 -0700 Subject: [PATCH 4/6] amend some NaT error funcs to have empty docstrings to match Timestamp --- pandas/_libs/tslib.pyx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index 0d1fc1a307d49..c6144d0eac64b 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -1359,7 +1359,8 @@ def _make_error_func(func_name, cls): raise ValueError("NaTType does not support " + func_name) f.__name__ = func_name - f.__doc__ = getattr(cls, func_name).__doc__ + if cls is not None: + f.__doc__ = getattr(cls, func_name).__doc__ return f @@ -1473,22 +1474,23 @@ cdef class _NaT(_Timestamp): 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) + # Timestamp has empty docstring for some methods. + utcfromtimestamp = _make_error_func('utcfromtimestamp', None) + fromtimestamp = _make_error_func('fromtimestamp', None) + combine = _make_error_func('combine', None) + if PY3: timestamp = _make_error_func('timestamp', datetime) From 330616b31ec2623ccff97b361b6d82482001818b Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 3 Oct 2017 14:28:56 -0700 Subject: [PATCH 5/6] Pin methods+properties to NaTType instead of _NaT --- pandas/_libs/tslib.pyx | 184 ++++++++++++++++++++--------------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index c6144d0eac64b..94b21527930ac 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -779,6 +779,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""" @@ -861,6 +887,72 @@ 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. + 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) @@ -1338,32 +1430,6 @@ cdef _nat_rdivide_op(self, other): 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 - if cls is not None: - f.__doc__ = getattr(cls, func_name).__doc__ - return f - - cdef class _NaT(_Timestamp): def __hash__(_NaT self): @@ -1428,72 +1494,6 @@ 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) - 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. - utcfromtimestamp = _make_error_func('utcfromtimestamp', None) - fromtimestamp = _make_error_func('fromtimestamp', None) - combine = _make_error_func('combine', None) - - if PY3: - timestamp = _make_error_func('timestamp', datetime) - # lightweight C object to hold datetime & int64 pair cdef class _TSObject: From c4f4ab5e3ba8895568213d63d2a9b5f75b430368 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 3 Oct 2017 16:49:59 -0700 Subject: [PATCH 6/6] dummy commit to force CI --- pandas/_libs/tslib.pyx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index 94b21527930ac..0ae4548c14f43 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -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,