Skip to content

Commit 0f8a776

Browse files
rwgkcopybara-github
authored andcommitted
Add support for absl::InfiniteFuture, absl::InfinitePast, matching established Google-internal conventions under absl/python.
Also replace floating-point arithmetic in the handling of microseconds with integer arithmetic, to sidestep subtle round-off issues. See the source code comment in the new `internal::GetTimestampMicrosFromDateTimeObj()` function for details. PiperOrigin-RevId: 529169464
1 parent 23b460e commit 0f8a776

File tree

3 files changed

+130
-0
lines changed

3 files changed

+130
-0
lines changed

pybind11_abseil/absl_casters.h

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,67 @@ struct type_caster<absl::TimeZone> {
111111
};
112112

113113
namespace internal {
114+
114115
inline void EnsurePyDateTime_IMPORT() {
115116
if (PyDateTimeAPI == nullptr) {
116117
PyDateTime_IMPORT;
117118
}
118119
}
120+
121+
constexpr int64_t GetInt64PythonErrorIndicatorSet = INT64_MAX;
122+
123+
inline int64_t GetTimestampMicrosFromDateTimeObj(PyObject* dt_obj) {
124+
// Part 1: Integer seconds.
125+
PyObject* dt_timestamp_py = PyObject_CallMethod(dt_obj, "timestamp", nullptr);
126+
if (dt_timestamp_py == nullptr) {
127+
return GetInt64PythonErrorIndicatorSet;
128+
}
129+
double dt_timestamp_dbl = PyFloat_AsDouble(dt_timestamp_py);
130+
Py_DECREF(dt_timestamp_py);
131+
if (PyErr_Occurred()) {
132+
return GetInt64PythonErrorIndicatorSet;
133+
}
134+
// The fractional part is intentionally discarded here because
135+
// IEEE 754 binary64 precision (aka double precision) is insufficient for
136+
// loss-free representation of micro-second resolution timestamps in the
137+
// [datetime.datetime.min, datetime.datetime.max] range:
138+
// https://github.com/rwgk/stuff/blob/f688c13c6cf5cefa1b41013d2f636fd10e0ba091/python_datetime/datetime_timestamp_floating_point_behavior_output.txt
139+
auto dt_timestamp_secs_int64 =
140+
static_cast<int64_t>(std::floor(dt_timestamp_dbl));
141+
142+
// Part 2: Integer microseconds.
143+
auto dt_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt_obj);
144+
static_assert(sizeof(dt_microsecond) >= 3,
145+
"Decimal value 999999 needs at least 3 bytes.");
146+
147+
return dt_timestamp_secs_int64 * 1000000 +
148+
static_cast<int64_t>(dt_microsecond);
149+
}
150+
151+
// The latest and earliest dates Python's datetime module can represent.
152+
constexpr absl::Time::Breakdown kDatetimeInfiniteFuture = {
153+
9999, 12, 31, 23, 59, 59, absl::Microseconds(999999)};
154+
constexpr absl::Time::Breakdown kDatetimeInfinitePast = {
155+
1, 1, 1, 0, 0, 0, absl::ZeroDuration()};
156+
157+
// NOTE: Python datetime tzinfo is deliberately ignored.
158+
// Rationale:
159+
// * datetime.datetime.min,max have tzinfo=None.
160+
// * In contrast, the conversions here return datetime.datetime.min,max with
161+
// tzinfo replaced (UTC).
162+
// * It would be disruptive (and unproductive) to change the behavior of the
163+
// conversions here.
164+
// * tzinfo for datetime.datetime.min,max is rather meaningless in general,
165+
// but especially so when those are used as placeholders for infinity.
166+
inline bool is_special_datetime(const absl::Time::Breakdown& bd_py,
167+
const absl::Time::Breakdown& bd_special) {
168+
return (bd_py.year == bd_special.year && bd_py.month == bd_special.month &&
169+
bd_py.day == bd_special.day && bd_py.hour == bd_special.hour &&
170+
bd_py.minute == bd_special.minute &&
171+
bd_py.second == bd_special.second &&
172+
bd_py.subsecond == bd_special.subsecond);
173+
}
174+
119175
} // namespace internal
120176

121177
// Convert between absl::Duration and python datetime.timedelta.
@@ -191,6 +247,35 @@ struct type_caster<absl::Time> {
191247

192248
// Conversion part 1 (Python->C++)
193249
bool load(handle src, bool convert) {
250+
// As early as possible to avoid mid-process surprises.
251+
internal::EnsurePyDateTime_IMPORT();
252+
if (PyDateTime_Check(src.ptr())) {
253+
absl::Time::Breakdown bd_py = {
254+
PyDateTime_GET_YEAR(src.ptr()),
255+
PyDateTime_GET_MONTH(src.ptr()),
256+
PyDateTime_GET_DAY(src.ptr()),
257+
PyDateTime_DATE_GET_HOUR(src.ptr()),
258+
PyDateTime_DATE_GET_MINUTE(src.ptr()),
259+
PyDateTime_DATE_GET_SECOND(src.ptr()),
260+
absl::Microseconds(PyDateTime_DATE_GET_MICROSECOND(src.ptr()))};
261+
if (internal::is_special_datetime(bd_py,
262+
internal::kDatetimeInfiniteFuture)) {
263+
value = absl::InfiniteFuture();
264+
return true;
265+
}
266+
if (internal::is_special_datetime(bd_py,
267+
internal::kDatetimeInfinitePast)) {
268+
value = absl::InfinitePast();
269+
return true;
270+
}
271+
int64_t dt_timestamp_micros =
272+
internal::GetTimestampMicrosFromDateTimeObj(src.ptr());
273+
if (dt_timestamp_micros == internal::GetInt64PythonErrorIndicatorSet) {
274+
throw error_already_set();
275+
}
276+
value = absl::FromUnixMicros(dt_timestamp_micros);
277+
return true;
278+
}
194279
if (convert) {
195280
if (PyLong_Check(src.ptr())) {
196281
value = absl::FromUnixSeconds(src.cast<int64_t>());
@@ -246,13 +331,29 @@ struct type_caster<absl::Time> {
246331
// This function truncates fractional microseconds as the python datetime
247332
// objects cannot support a resolution higher than this.
248333
auto py_datetime_t = module::import("datetime").attr("datetime");
334+
if (src == absl::InfiniteFuture()) {
335+
// For compatibility with absl/python/time.cc
336+
return replace_tzinfo_utc(py_datetime_t(9999, 12, 31, 23, 59, 59, 999999))
337+
.release();
338+
}
339+
if (src == absl::InfinitePast()) {
340+
// For compatibility with absl/python/time.cc
341+
return replace_tzinfo_utc(py_datetime_t(1, 1, 1, 0, 0, 0, 0)).release();
342+
}
249343
auto py_from_timestamp = py_datetime_t.attr("fromtimestamp");
250344
auto py_timezone_t = module::import("dateutil.tz").attr("gettz");
251345
auto py_timezone = py_timezone_t(absl::LocalTimeZone().name());
252346
double as_seconds = static_cast<double>(absl::ToUnixMicros(src)) / 1e6;
253347
auto py_datetime = py_from_timestamp(as_seconds, "tz"_a = py_timezone);
254348
return py_datetime.release();
255349
}
350+
351+
private:
352+
static object replace_tzinfo_utc(handle dt) {
353+
auto py_timezone_utc =
354+
module::import("datetime").attr("timezone").attr("utc");
355+
return dt.attr("replace")(arg("tzinfo") = py_timezone_utc);
356+
}
256357
};
257358

258359
template <typename CivilTimeUnitType>

pybind11_abseil/tests/absl_example.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,12 @@ PYBIND11_MODULE(absl_example, m) {
345345
m.def("absl_time_overloads", [](const absl::Time&) { return "absl::Time"; });
346346
m.def("absl_time_overloads", [](int) { return "int"; });
347347
m.def("absl_time_overloads", [](float) { return "float"; });
348+
m.def("make_infinite_future", []() { return absl::InfiniteFuture(); });
349+
m.def("is_infinite_future",
350+
[](const absl::Time& time) { return time == absl::InfiniteFuture(); });
351+
m.def("make_infinite_past", []() { return absl::InfinitePast(); });
352+
m.def("is_infinite_past",
353+
[](const absl::Time& time) { return time == absl::InfinitePast(); });
348354

349355
m.def("roundtrip_duration", &RoundtripDuration, arg("duration"));
350356
m.def("roundtrip_time", &RoundtripTime, arg("time"));

pybind11_abseil/tests/absl_test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def test_dst_datetime_from_timestamp(self, offs):
132132
time_local_naive = time_local_aware.replace(tzinfo=None)
133133
for time in (time_utc, time_local_aware, time_local_naive):
134134
self.assertTrue(absl_example.check_datetime(time, secs))
135+
self.assertEqual(int(absl_example.roundtrip_time(time).timestamp()), secs)
135136

136137
def test_pass_datetime_pre_unix_epoch(self):
137138
dt = datetime.datetime(1969, 7, 16, 10, 56, 7, microsecond=140)
@@ -267,6 +268,28 @@ def test_from_datetime_time(self):
267268
# Conversion from datetime.time to absl::Duration ignores tzinfo!
268269
self.assertEqual((dt2 - dt1).seconds, 0)
269270

271+
def test_infinite_future(self):
272+
inff = absl_example.make_infinite_future()
273+
self.assertEqual(inff.replace(tzinfo=None), datetime.datetime.max)
274+
self.assertTrue(absl_example.is_infinite_future(inff))
275+
self.assertFalse(
276+
absl_example.is_infinite_future(
277+
inff - datetime.timedelta(microseconds=1)
278+
)
279+
)
280+
self.assertLess(self.TEST_DATETIME_UTC, inff)
281+
282+
def test_infinite_past(self):
283+
infp = absl_example.make_infinite_past()
284+
self.assertEqual(infp.replace(tzinfo=None), datetime.datetime.min)
285+
self.assertTrue(absl_example.is_infinite_past(infp))
286+
self.assertFalse(
287+
absl_example.is_infinite_future(
288+
infp + datetime.timedelta(microseconds=1)
289+
)
290+
)
291+
self.assertGreater(self.TEST_DATETIME_UTC, infp)
292+
270293

271294
def make_read_only_numpy_array():
272295
values = np.zeros(5, dtype=np.int32)

0 commit comments

Comments
 (0)