Skip to content

Commit 2fca7a5

Browse files
jbrockmendelyehoshuadimarsky
authored andcommitted
ENH: port time.pxd from cython (pandas-dev#45864)
* ENH: port time.pxd from cython * update API test * manually convert to dict for windows compat * mypy fixup
1 parent 4a9f048 commit 2fca7a5

File tree

4 files changed

+126
-1
lines changed

4 files changed

+126
-1
lines changed

pandas/_libs/tslibs/ctime.pyx

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""
2+
Cython implementation of (parts of) the standard library time module.
3+
"""
4+
5+
from cpython.exc cimport PyErr_SetFromErrno
6+
from libc.stdint cimport int64_t
7+
8+
9+
cdef extern from "Python.h":
10+
ctypedef int64_t _PyTime_t
11+
_PyTime_t _PyTime_GetSystemClock() nogil
12+
double _PyTime_AsSecondsDouble(_PyTime_t t) nogil
13+
14+
from libc.time cimport (
15+
localtime as libc_localtime,
16+
time_t,
17+
tm,
18+
)
19+
20+
21+
def pytime():
22+
"""
23+
python-exposed for testing
24+
"""
25+
return time()
26+
27+
28+
def pylocaltime():
29+
"""
30+
python-exposed for testing
31+
"""
32+
lt = localtime()
33+
# https://github.com/pandas-dev/pandas/pull/45864#issuecomment-1033021599
34+
return {
35+
"tm_year": lt.tm_year,
36+
"tm_mon": lt.tm_mon,
37+
"tm_mday": lt.tm_mday,
38+
"tm_hour": lt.tm_hour,
39+
"tm_min": lt.tm_min,
40+
"tm_sec": lt.tm_sec,
41+
"tm_wday": lt.tm_wday,
42+
"tm_yday": lt.tm_yday,
43+
"tm_isdst": lt.tm_isdst,
44+
}
45+
46+
47+
cdef inline double time() nogil:
48+
cdef:
49+
_PyTime_t tic
50+
51+
tic = _PyTime_GetSystemClock()
52+
return _PyTime_AsSecondsDouble(tic)
53+
54+
55+
cdef inline int _raise_from_errno() except -1 with gil:
56+
PyErr_SetFromErrno(RuntimeError)
57+
return <int>-1 # Let the C compiler know that this function always raises.
58+
59+
60+
cdef inline tm localtime() nogil except *:
61+
"""
62+
Analogue to the stdlib time.localtime. The returned struct
63+
has some entries that the stdlib version does not: tm_gmtoff, tm_zone
64+
"""
65+
cdef:
66+
time_t tic = <time_t>time()
67+
tm* result
68+
69+
result = libc_localtime(&tic)
70+
if result is NULL:
71+
_raise_from_errno()
72+
# Fix 0-based date values (and the 1900-based year).
73+
# See tmtotuple() in https://github.com/python/cpython/blob/master/Modules/timemodule.c
74+
result.tm_year += 1900
75+
result.tm_mon += 1
76+
result.tm_wday = (result.tm_wday + 6) % 7
77+
result.tm_yday += 1
78+
return result[0]

pandas/tests/tslibs/test_api.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,6 @@ def test_namespace():
5252
]
5353

5454
expected = set(submodules + api)
55-
names = [x for x in dir(tslibs) if not x.startswith("__")]
55+
# exclude "ctime" bc it is not (yet) imported outside of tests
56+
names = [x for x in dir(tslibs) if not x.startswith("__") and x != "ctime"]
5657
assert set(names) == expected

pandas/tests/tslibs/test_ctime.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
These tests are adapted from cython's tests
3+
https://github.com/cython/cython/blob/master/tests/run/time_pxd.pyx
4+
"""
5+
# TODO(cython3): use cython's cpython.time implementation
6+
7+
8+
import time
9+
10+
# error: Module "pandas._libs.tslibs" has no attribute "ctime"
11+
from pandas._libs.tslibs import ctime # type: ignore[attr-defined]
12+
13+
14+
def test_time():
15+
# check that ctime.time() matches time.time() to within call-time tolerance
16+
tic1 = time.time()
17+
tic2 = ctime.pytime()
18+
tic3 = time.time()
19+
20+
assert tic1 <= tic3 # sanity check
21+
assert tic1 <= tic2
22+
assert tic2 <= tic3
23+
24+
25+
def test_localtime():
26+
ltp = time.localtime()
27+
ltc = ctime.pylocaltime()
28+
29+
if ltp.tm_sec != ltc["tm_sec"]:
30+
# If the time.localtime call is just before the end of a second and the
31+
# ctime.localtime call is just after the beginning of the next second,
32+
# re-call. This should not occur twice in a row.
33+
ltp = time.localtime()
34+
ltc = ctime.pylocaltime()
35+
36+
assert ltp.tm_year == ltc["tm_year"] or (ltp.tm_year, ltc["tm_year"])
37+
assert ltp.tm_mon == ltc["tm_mon"] or (ltp.tm_mon, ltc["tm_mon"])
38+
assert ltp.tm_mday == ltc["tm_mday"] or (ltp.tm_mday, ltc["tm_mday"])
39+
assert ltp.tm_hour == ltc["tm_hour"] or (ltp.tm_hour, ltc["tm_hour"])
40+
assert ltp.tm_min == ltc["tm_min"] or (ltp.tm_min, ltc["tm_min"])
41+
assert ltp.tm_sec == ltc["tm_sec"] or (ltp.tm_sec, ltc["tm_sec"])
42+
assert ltp.tm_wday == ltc["tm_wday"] or (ltp.tm_wday, ltc["tm_wday"])
43+
assert ltp.tm_yday == ltc["tm_yday"] or (ltp.tm_yday, ltc["tm_yday"])
44+
assert ltp.tm_isdst == ltc["tm_isdst"] or (ltp.tm_isdst, ltc["tm_isdst"])

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ class CheckSDist(sdist_class):
210210
"pandas/_libs/parsers.pyx",
211211
"pandas/_libs/tslibs/base.pyx",
212212
"pandas/_libs/tslibs/ccalendar.pyx",
213+
"pandas/_libs/tslibs/ctime.pyx",
213214
"pandas/_libs/tslibs/dtypes.pyx",
214215
"pandas/_libs/tslibs/period.pyx",
215216
"pandas/_libs/tslibs/strptime.pyx",
@@ -495,6 +496,7 @@ def srcpath(name=None, suffix=".pyx", subdir="src"):
495496
"_libs.tslib": {"pyxfile": "_libs/tslib", "depends": tseries_depends},
496497
"_libs.tslibs.base": {"pyxfile": "_libs/tslibs/base"},
497498
"_libs.tslibs.ccalendar": {"pyxfile": "_libs/tslibs/ccalendar"},
499+
"_libs.tslibs.ctime": {"pyxfile": "_libs/tslibs/ctime"},
498500
"_libs.tslibs.dtypes": {"pyxfile": "_libs/tslibs/dtypes"},
499501
"_libs.tslibs.conversion": {
500502
"pyxfile": "_libs/tslibs/conversion",

0 commit comments

Comments
 (0)