From 242f39929f4318e54a71dbd5dc64889a60f335c8 Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 7 Feb 2022 12:33:35 -0800 Subject: [PATCH 1/4] ENH: port time.pxd from cython --- pandas/_libs/tslibs/ctime.pyx | 66 +++++++++++++++++++++++++++++++ pandas/tests/tslibs/test_ctime.py | 43 ++++++++++++++++++++ setup.py | 2 + 3 files changed, 111 insertions(+) create mode 100644 pandas/_libs/tslibs/ctime.pyx create mode 100644 pandas/tests/tslibs/test_ctime.py diff --git a/pandas/_libs/tslibs/ctime.pyx b/pandas/_libs/tslibs/ctime.pyx new file mode 100644 index 0000000000000..e2118ab5a0ea5 --- /dev/null +++ b/pandas/_libs/tslibs/ctime.pyx @@ -0,0 +1,66 @@ +""" +Cython implementation of (parts of) the standard library time module. +""" + +from cpython.exc cimport PyErr_SetFromErrno +from libc.stdint cimport int64_t + + +cdef extern from "Python.h": + ctypedef int64_t _PyTime_t + _PyTime_t _PyTime_GetSystemClock() nogil + double _PyTime_AsSecondsDouble(_PyTime_t t) nogil + +from libc.time cimport ( + localtime as libc_localtime, + time_t, + tm, +) + + +def pytime(): + """ + python-exposed for testing + """ + return time() + + +def pylocaltime(): + """ + python-exposed for testing + """ + return localtime() + + +cdef inline double time() nogil: + cdef: + _PyTime_t tic + + tic = _PyTime_GetSystemClock() + return _PyTime_AsSecondsDouble(tic) + + +cdef inline int _raise_from_errno() except -1 with gil: + PyErr_SetFromErrno(RuntimeError) + return -1 # Let the C compiler know that this function always raises. + + +cdef inline tm localtime() nogil except *: + """ + Analogue to the stdlib time.localtime. The returned struct + has some entries that the stdlib version does not: tm_gmtoff, tm_zone + """ + cdef: + time_t tic = time() + tm* result + + result = libc_localtime(&tic) + if result is NULL: + _raise_from_errno() + # Fix 0-based date values (and the 1900-based year). + # See tmtotuple() in https://github.com/python/cpython/blob/master/Modules/timemodule.c + result.tm_year += 1900 + result.tm_mon += 1 + result.tm_wday = (result.tm_wday + 6) % 7 + result.tm_yday += 1 + return result[0] diff --git a/pandas/tests/tslibs/test_ctime.py b/pandas/tests/tslibs/test_ctime.py new file mode 100644 index 0000000000000..9112142bd0e28 --- /dev/null +++ b/pandas/tests/tslibs/test_ctime.py @@ -0,0 +1,43 @@ +""" +These tests are adapted from cython's tests +https://github.com/cython/cython/blob/master/tests/run/time_pxd.pyx +""" +# TODO(cython3): use cython's cpython.time implementation + + +import time + +from pandas._libs.tslibs import ctime + + +def test_time(): + # check that ctime.time() matches time.time() to within call-time tolerance + tic1 = time.time() + tic2 = ctime.pytime() + tic3 = time.time() + + assert tic1 <= tic3 # sanity check + assert tic1 <= tic2 + assert tic2 <= tic3 + + +def test_localtime(): + ltp = time.localtime() + ltc = ctime.pylocaltime() + + if ltp.tm_sec != ltc["tm_sec"]: + # If the time.localtime call is just before the end of a second and the + # ctime.localtime call is just after the beginning of the next second, + # re-call. This should not occur twice in a row. + ltp = time.localtime() + ltc = ctime.pylocaltime() + + assert ltp.tm_year == ltc["tm_year"] or (ltp.tm_year, ltc["tm_year"]) + assert ltp.tm_mon == ltc["tm_mon"] or (ltp.tm_mon, ltc["tm_mon"]) + assert ltp.tm_mday == ltc["tm_mday"] or (ltp.tm_mday, ltc["tm_mday"]) + assert ltp.tm_hour == ltc["tm_hour"] or (ltp.tm_hour, ltc["tm_hour"]) + assert ltp.tm_min == ltc["tm_min"] or (ltp.tm_min, ltc["tm_min"]) + assert ltp.tm_sec == ltc["tm_sec"] or (ltp.tm_sec, ltc["tm_sec"]) + assert ltp.tm_wday == ltc["tm_wday"] or (ltp.tm_wday, ltc["tm_wday"]) + assert ltp.tm_yday == ltc["tm_yday"] or (ltp.tm_yday, ltc["tm_yday"]) + assert ltp.tm_isdst == ltc["tm_isdst"] or (ltp.tm_isdst, ltc["tm_isdst"]) diff --git a/setup.py b/setup.py index db65ea72e4a96..62704dc4423c8 100755 --- a/setup.py +++ b/setup.py @@ -210,6 +210,7 @@ class CheckSDist(sdist_class): "pandas/_libs/parsers.pyx", "pandas/_libs/tslibs/base.pyx", "pandas/_libs/tslibs/ccalendar.pyx", + "pandas/_libs/tslibs/ctime.pyx", "pandas/_libs/tslibs/dtypes.pyx", "pandas/_libs/tslibs/period.pyx", "pandas/_libs/tslibs/strptime.pyx", @@ -495,6 +496,7 @@ def srcpath(name=None, suffix=".pyx", subdir="src"): "_libs.tslib": {"pyxfile": "_libs/tslib", "depends": tseries_depends}, "_libs.tslibs.base": {"pyxfile": "_libs/tslibs/base"}, "_libs.tslibs.ccalendar": {"pyxfile": "_libs/tslibs/ccalendar"}, + "_libs.tslibs.ctime": {"pyxfile": "_libs/tslibs/ctime"}, "_libs.tslibs.dtypes": {"pyxfile": "_libs/tslibs/dtypes"}, "_libs.tslibs.conversion": { "pyxfile": "_libs/tslibs/conversion", From 89a865a1f49858c6c4191da4071ff7b9e0db8cfb Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 7 Feb 2022 15:52:48 -0800 Subject: [PATCH 2/4] update API test --- pandas/tests/tslibs/test_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/tslibs/test_api.py b/pandas/tests/tslibs/test_api.py index d7abb19530837..755ac3d144246 100644 --- a/pandas/tests/tslibs/test_api.py +++ b/pandas/tests/tslibs/test_api.py @@ -52,5 +52,6 @@ def test_namespace(): ] expected = set(submodules + api) - names = [x for x in dir(tslibs) if not x.startswith("__")] + # exclude "ctime" bc it is not (yet) imported outside of tests + names = [x for x in dir(tslibs) if not x.startswith("__") and x != "ctime"] assert set(names) == expected From 0418038f855a26725dada7954180e4a638ac1a6d Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 8 Feb 2022 15:31:14 -0800 Subject: [PATCH 3/4] manually convert to dict for windows compat --- pandas/_libs/tslibs/ctime.pyx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/ctime.pyx b/pandas/_libs/tslibs/ctime.pyx index e2118ab5a0ea5..f5c0546e5f5b6 100644 --- a/pandas/_libs/tslibs/ctime.pyx +++ b/pandas/_libs/tslibs/ctime.pyx @@ -29,7 +29,19 @@ def pylocaltime(): """ python-exposed for testing """ - return localtime() + lt = localtime() + # https://github.com/pandas-dev/pandas/pull/45864#issuecomment-1033021599 + return { + "tm_year": lt.tm_year, + "tm_mon": lt.tm_mon, + "tm_mday": lt.tm_mday, + "tm_hour": lt.tm_hour, + "tm_min": lt.tm_min, + "tm_sec": lt.tm_sec, + "tm_wday": lt.tm_wday, + "tm_yday": lt.tm_yday, + "tm_isdst": lt.tm_isdst, + } cdef inline double time() nogil: From ba67da531faa47d4bf09267c4ab3938d6e96049e Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 13 Feb 2022 10:39:58 -0800 Subject: [PATCH 4/4] mypy fixup --- pandas/tests/tslibs/test_ctime.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/tslibs/test_ctime.py b/pandas/tests/tslibs/test_ctime.py index 9112142bd0e28..25b911a07e6d6 100644 --- a/pandas/tests/tslibs/test_ctime.py +++ b/pandas/tests/tslibs/test_ctime.py @@ -7,7 +7,8 @@ import time -from pandas._libs.tslibs import ctime +# error: Module "pandas._libs.tslibs" has no attribute "ctime" +from pandas._libs.tslibs import ctime # type: ignore[attr-defined] def test_time():