1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
|
# Ported from pybind11/tests/test_chrono.py
import test_chrono_ext as m
import time
import datetime
import sys
import pytest
def test_chrono_system_clock():
# Get the time from both c++ and datetime
date0 = datetime.datetime.today()
date1 = m.test_chrono1()
date2 = datetime.datetime.today()
# The returned value should be a datetime
assert isinstance(date1, datetime.datetime)
# The numbers should vary by a very small amount (time it took to execute)
diff_python = abs(date2 - date0)
diff = abs(date1 - date2)
# There should never be a days difference
assert diff.days == 0
# Since datetime.datetime.today() calls time.time(), and on some platforms
# that has 1 second accuracy, we compare this way
assert diff.seconds <= diff_python.seconds
def test_chrono_system_clock_roundtrip():
date1 = datetime.datetime.today()
# Roundtrip the time
date2 = m.test_chrono2(date1)
# The returned value should be a datetime
assert isinstance(date2, datetime.datetime)
# They should be identical (no information lost on roundtrip)
diff = abs(date1 - date2)
assert diff == datetime.timedelta(0)
def test_chrono_system_clock_roundtrip_date():
date1 = datetime.date.today()
# Roundtrip the time
datetime2 = m.test_chrono2(date1)
date2 = datetime2.date()
time2 = datetime2.time()
# The returned value should be a datetime
assert isinstance(datetime2, datetime.datetime)
assert isinstance(date2, datetime.date)
assert isinstance(time2, datetime.time)
# They should be identical (no information lost on roundtrip)
diff = abs(date1 - date2)
assert diff.days == 0
assert diff.seconds == 0
assert diff.microseconds == 0
# Year, Month & Day should be the same after the round trip
assert date1 == date2
# There should be no time information
assert time2.hour == 0
assert time2.minute == 0
assert time2.second == 0
assert time2.microsecond == 0
SKIP_TZ_ENV_ON_WIN = pytest.mark.skipif(
"sys.platform == 'win32'",
reason="TZ environment variable only supported on POSIX"
)
@pytest.mark.parametrize(
"time1",
[
datetime.datetime.today().time(),
datetime.time(0, 0, 0),
datetime.time(0, 0, 0, 1),
datetime.time(0, 28, 45, 109827),
datetime.time(0, 59, 59, 999999),
datetime.time(1, 0, 0),
datetime.time(5, 59, 59, 0),
datetime.time(5, 59, 59, 1),
],
)
@pytest.mark.parametrize(
"tz",
[
None,
pytest.param("Europe/Brussels", marks=SKIP_TZ_ENV_ON_WIN),
pytest.param("Asia/Pyongyang", marks=SKIP_TZ_ENV_ON_WIN),
pytest.param("America/New_York", marks=SKIP_TZ_ENV_ON_WIN),
],
)
def test_chrono_system_clock_roundtrip_time(time1, tz, monkeypatch):
if tz is not None:
monkeypatch.setenv("TZ", f"/usr/share/zoneinfo/{tz}")
# Roundtrip the time
datetime2 = m.test_chrono2(time1)
date2 = datetime2.date()
time2 = datetime2.time()
# The returned value should be a datetime
assert isinstance(datetime2, datetime.datetime)
assert isinstance(date2, datetime.date)
assert isinstance(time2, datetime.time)
# Hour, Minute, Second & Microsecond should be the same after the round trip
assert time1 == time2
# There should be no date information (i.e. date = python base date)
assert date2.year == 1970
assert date2.month == 1
assert date2.day == 1
def test_chrono_duration_roundtrip():
# Get the difference between two times (a timedelta)
date1 = datetime.datetime.today()
date2 = datetime.datetime.today()
diff = date2 - date1
# Make sure this is a timedelta
assert isinstance(diff, datetime.timedelta)
cpp_diff = m.test_chrono3(diff)
assert cpp_diff == diff
# Negative timedelta roundtrip
diff = datetime.timedelta(microseconds=-1)
cpp_diff = m.test_chrono3(diff)
assert cpp_diff == diff
def test_chrono_duration_subtraction_equivalence():
date1 = datetime.datetime.today()
date2 = datetime.datetime.today()
diff = date2 - date1
cpp_diff = m.test_chrono4(date2, date1)
assert cpp_diff == diff
def test_chrono_duration_subtraction_equivalence_date():
date1 = datetime.date.today()
date2 = datetime.date.today()
diff = date2 - date1
cpp_diff = m.test_chrono4(date2, date1)
assert cpp_diff == diff
def test_chrono_steady_clock():
time1 = m.test_chrono5()
assert isinstance(time1, datetime.timedelta)
def test_chrono_steady_clock_roundtrip():
time1 = datetime.timedelta(days=10, seconds=10, microseconds=100)
time2 = m.test_chrono6(time1)
assert isinstance(time2, datetime.timedelta)
# They should be identical (no information lost on roundtrip)
assert time1 == time2
# Floating point conversion also works
assert m.test_chrono6(time1.total_seconds()) == time1
def test_floating_point_duration():
# Test using a floating point number in seconds
time = m.test_chrono7(35.525123)
assert isinstance(time, datetime.timedelta)
assert time.seconds == 35
assert 525122 <= time.microseconds <= 525123
diff = m.test_chrono_float_diff(43.789012, 1.123456)
assert diff.seconds == 42
assert 665556 <= diff.microseconds <= 665557
def test_nano_timepoint():
time = datetime.datetime.now()
time1 = m.test_nano_timepoint(time, datetime.timedelta(seconds=60))
assert time1 == time + datetime.timedelta(seconds=60)
def test_chrono_different_resolutions():
resolutions = m.different_resolutions()
time = datetime.datetime.now()
resolutions.timestamp_h = time
resolutions.timestamp_m = time
resolutions.timestamp_s = time
resolutions.timestamp_ms = time
resolutions.timestamp_us = time
assert time == resolutions.timestamp_us
time = time.replace(microsecond=(time.microsecond // 1000) * 1000)
assert time == resolutions.timestamp_ms
time = time.replace(microsecond=0)
assert time == resolutions.timestamp_s
time = time.replace(second=0)
assert time == resolutions.timestamp_m
time = time.replace(minute=0)
assert time == resolutions.timestamp_h
# Tests below this point are new in nanobind
def test_chrono_misc():
from datetime import datetime, timedelta
advance_datetime = m.test_nano_timepoint
difference_between_datetimes = m.test_nano_timepoint_diff
roundtrip_datetime = m.test_nano_timepoint_roundtrip
d1 = datetime(2023, 4, 5, 12, 0, 0, 0)
d2 = datetime(2023, 4, 5, 12, 30, 0, 123)
# datetime -> time_point and duration -> timedelta conversion
assert difference_between_datetimes(d1, d2) == d1 - d2
assert difference_between_datetimes(d2, d1) == d2 - d1
# date -> time_point conversion
assert difference_between_datetimes(d2, d1.date()) == timedelta(
hours=12, minutes=30, microseconds=123
)
# time -> time_point conversion
assert difference_between_datetimes(d2.time(), d1.time()) == timedelta(
minutes=30, microseconds=123
)
assert roundtrip_datetime(d1.time()) == datetime(1970, 1, 1, 12, 0, 0)
for td in (
timedelta(seconds=5),
timedelta(microseconds=123),
timedelta(days=1, seconds=10),
timedelta(seconds=-5),
timedelta(microseconds=-123),
timedelta(days=-1, seconds=-10),
):
# timedelta -> duration conversion
assert advance_datetime(d1, td) == d1 + td
# float -> duration conversion
assert advance_datetime(d1, td.total_seconds()) == d1 + td
# time_point -> datetime conversion
assert roundtrip_datetime(d1) == d1
assert roundtrip_datetime(d2) == d2
@pytest.mark.parametrize(
"test_type,roundtrip_name",
[
(datetime.timedelta, 'test_chrono7'),
(datetime.datetime, 'test_nano_timepoint_roundtrip'),
]
)
def test_chrono_invalid(test_type, roundtrip_name):
roundtrip = getattr(m, roundtrip_name)
# Can't pass None or an integer where a duration or timepoint is expected
with pytest.raises(TypeError, match="incompatible function arguments"):
roundtrip(None)
with pytest.raises(TypeError, match="incompatible function arguments"):
roundtrip(42)
# Can't pass a duration where a timepoint is expected, or vice versa
with pytest.raises(TypeError, match="incompatible function arguments"):
if test_type is datetime.datetime:
roundtrip(datetime.timedelta(seconds=5))
else:
roundtrip(datetime.datetime.now())
# On the limited API we access timedelta/datetime objects via
# regular attribute access; test that invalid results are handled
# reasonably. On the full API we use Python's <datetime.h> header
# so we'll always access the true C-level datetime slot and can't
# be fooled by tricks like this. PyPy uses normal attribute access
# and works like the limited API in this respect.
class fake_type(test_type):
@property
def seconds(self):
return self.override_value
@property
def second(self):
return self.override_value
if test_type is datetime.datetime:
fake_val = fake_type.fromtimestamp(time.time())
replace_overridden = lambda s: fake_val.replace(second=s)
else:
fake_val = fake_type(days=1, seconds=10, microseconds=123456)
replace_overridden = lambda s: fake_type(
days=1, seconds=s, microseconds=123456
)
for fake_result, errtype in (
("hi", "TypeError"),
(0, None),
(2**64, "Python int too large to convert to C long"),
(2**32, "OverflowError"),
):
fake_val.override_value = fake_result
if not m.access_via_python:
assert roundtrip(fake_val) == fake_val
elif errtype is None:
assert roundtrip(fake_val) == replace_overridden(fake_result)
elif test_type is datetime.timedelta and sys.implementation.name == "pypy":
# pypy's cpyext module converts timedelta to a C structure
# before the nanobind function even gets called, producing
# a different exception than the one we're testing below.
# datetime still works as it doesn't have its attributes
# converted but instead is implemented with Python
# attribute accesses.
pass
else:
from test.support import catch_unraisable_exception
with catch_unraisable_exception() as cm:
with pytest.raises(TypeError, match="incompatible function arguments"):
roundtrip(fake_val)
assert cm.unraisable is not None
assert errtype in repr(cm.unraisable.exc_value)
|