Skip to content

Commit fbaebd6

Browse files
rwgkcopybara-github
authored andcommitted
Add from-Python conversions from datetime.time to absl::Duration, absl::Time
For compatibility with Google-internal Clif_PyObjAs/From implementations. This is to support the PyCLIF-pybind11 integration. PiperOrigin-RevId: 526718225
1 parent 633f291 commit fbaebd6

File tree

3 files changed

+110
-19
lines changed

3 files changed

+110
-19
lines changed

pybind11_abseil/absl_casters.h

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
#include <pybind11/pybind11.h>
3535
#include <pybind11/stl.h>
3636

37+
// Must NOT appear before at least one pybind11 include.
38+
#include <datetime.h> // Python datetime builtin.
39+
3740
#include <cmath>
3841
#include <cstdint>
3942
#include <tuple>
@@ -107,6 +110,14 @@ struct type_caster<absl::TimeZone> {
107110
}
108111
};
109112

113+
namespace internal {
114+
inline void EnsurePyDateTime_IMPORT() {
115+
if (PyDateTimeAPI == nullptr) {
116+
PyDateTime_IMPORT;
117+
}
118+
}
119+
} // namespace internal
120+
110121
// Convert between absl::Duration and python datetime.timedelta.
111122
template <>
112123
struct type_caster<absl::Duration> {
@@ -117,33 +128,41 @@ struct type_caster<absl::Duration> {
117128

118129
// Conversion part 1 (Python->C++)
119130
bool load(handle src, bool convert) {
131+
// As early as possible to avoid mid-process surprises.
132+
internal::EnsurePyDateTime_IMPORT();
120133
if (!convert) {
121134
return false;
122135
}
123136
if (PyFloat_Check(src.ptr())) {
124137
value = absl::Seconds(src.cast<double>());
125138
return true;
126-
} else if (PyLong_Check(src.ptr())) {
139+
}
140+
if (PyLong_Check(src.ptr())) {
127141
value = absl::Seconds(src.cast<int64_t>());
128142
return true;
129-
} else {
130-
// Ensure that absl::Duration is converted from a Python
131-
// datetime.timedelta.
132-
if (!hasattr(src, "days") || !hasattr(src, "seconds") ||
133-
!hasattr(src, "microseconds")) {
134-
return false;
135-
}
136-
auto py_duration_t = module::import("datetime").attr("timedelta");
137-
if (src == object(py_duration_t.attr("max"))) {
138-
value = absl::InfiniteDuration();
139-
} else {
140-
value = absl::Hours(24 * GetInt64Attr(src, "days")) +
141-
absl::Seconds(GetInt64Attr(src, "seconds")) +
142-
absl::Microseconds(GetInt64Attr(src, "microseconds"));
143-
}
143+
}
144+
if (PyTime_Check(src.ptr())) {
145+
value = absl::Hours(PyDateTime_TIME_GET_HOUR(src.ptr())) +
146+
absl::Minutes(PyDateTime_TIME_GET_MINUTE(src.ptr())) +
147+
absl::Seconds(PyDateTime_TIME_GET_SECOND(src.ptr())) +
148+
absl::Microseconds(PyDateTime_TIME_GET_MICROSECOND(src.ptr()));
144149
return true;
145150
}
146-
return false;
151+
// Ensure that absl::Duration is converted from a Python
152+
// datetime.timedelta.
153+
if (!hasattr(src, "days") || !hasattr(src, "seconds") ||
154+
!hasattr(src, "microseconds")) {
155+
return false;
156+
}
157+
auto py_duration_t = module::import("datetime").attr("timedelta");
158+
if (src == object(py_duration_t.attr("max"))) {
159+
value = absl::InfiniteDuration();
160+
} else {
161+
value = absl::Hours(24 * GetInt64Attr(src, "days")) +
162+
absl::Seconds(GetInt64Attr(src, "seconds")) +
163+
absl::Microseconds(GetInt64Attr(src, "microseconds"));
164+
}
165+
return true;
147166
}
148167

149168
// Conversion part 2 (C++ -> Python)
@@ -172,15 +191,32 @@ struct type_caster<absl::Time> {
172191

173192
// Conversion part 1 (Python->C++)
174193
bool load(handle src, bool convert) {
194+
// As early as possible to avoid mid-process surprises.
195+
internal::EnsurePyDateTime_IMPORT();
175196
if (convert) {
176197
if (PyLong_Check(src.ptr())) {
177198
value = absl::FromUnixSeconds(src.cast<int64_t>());
178199
return true;
179-
} else if (PyFloat_Check(src.ptr())) {
200+
}
201+
if (PyFloat_Check(src.ptr())) {
180202
value = absl::time_internal::FromUnixDuration(absl::Seconds(
181203
src.cast<double>()));
182204
return true;
183205
}
206+
if (PyTime_Check(src.ptr())) {
207+
// Adapted from absl/python/time.cc
208+
// Copyright 2018 The Abseil Authors.
209+
timeval tv{PyDateTime_TIME_GET_HOUR(src.ptr()) * 3600 +
210+
PyDateTime_TIME_GET_MINUTE(src.ptr()) * 60 +
211+
PyDateTime_TIME_GET_SECOND(src.ptr()),
212+
PyDateTime_TIME_GET_MICROSECOND(src.ptr())};
213+
value = absl::TimeFromTimeval(tv);
214+
int utcoffset;
215+
if (PyTzOffset(src.ptr(), &utcoffset)) {
216+
value += absl::Seconds(utcoffset);
217+
}
218+
return true;
219+
}
184220
}
185221
if (!hasattr(src, "year") || !hasattr(src, "month") ||
186222
!hasattr(src, "day")) {
@@ -233,6 +269,25 @@ struct type_caster<absl::Time> {
233269
auto py_datetime = py_from_timestamp(as_seconds, "tz"_a = py_timezone);
234270
return py_datetime.release();
235271
}
272+
273+
private:
274+
// Adapted from absl/python/time.cc
275+
// Copyright 2018 The Abseil Authors.
276+
bool PyTzOffset(PyObject* datetime, int* utcoffset) {
277+
PyObject* offset = PyObject_CallMethod(datetime, "utcoffset", nullptr);
278+
279+
if (!offset || !PyDelta_Check(offset)) {
280+
return false;
281+
}
282+
283+
if (utcoffset) {
284+
*utcoffset = PyDateTime_DELTA_GET_SECONDS(offset) +
285+
PyDateTime_DELTA_GET_DAYS(offset) * 24 * 3600;
286+
}
287+
288+
Py_DECREF(offset);
289+
return true;
290+
}
236291
};
237292

238293
template <typename CivilTimeUnitType>

pybind11_abseil/tests/absl_example.cc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ bool CheckCivilYear(absl::CivilYear datetime, double secs) {
9898
return datetime == MakeCivilYear(secs);
9999
}
100100

101+
absl::Duration RoundtripDuration(const absl::Duration& duration) {
102+
return duration;
103+
}
104+
105+
absl::Time RoundtripTime(const absl::Time& time) { return time; }
106+
101107
absl::TimeZone RoundtripTimeZone(absl::TimeZone timezone) { return timezone; }
102108

103109
// Since a span does not own its elements, we must create a class to own them
@@ -340,7 +346,8 @@ PYBIND11_MODULE(absl_example, m) {
340346
m.def("absl_time_overloads", [](int) { return "int"; });
341347
m.def("absl_time_overloads", [](float) { return "float"; });
342348

343-
// absl::TimeZone bindings
349+
m.def("roundtrip_duration", &RoundtripDuration, arg("duration"));
350+
m.def("roundtrip_time", &RoundtripTime, arg("time"));
344351
m.def("roundtrip_timezone", &RoundtripTimeZone, arg("timezone"));
345352

346353
// absl::CivilTime bindings

pybind11_abseil/tests/absl_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
from pybind11_abseil.tests import absl_example
1616

1717

18+
def dt_time(h=0, m=0, s=0, micros=0, tzoff=0):
19+
return datetime.time(h, m, s, micros).replace(
20+
tzinfo=datetime.timezone(datetime.timedelta(seconds=tzoff))
21+
)
22+
23+
1824
class AbslTimeTest(parameterized.TestCase):
1925
SECONDS_IN_DAY = 24 * 60 * 60
2026
POSITIVE_SECS = 3 * SECONDS_IN_DAY + 2.5
@@ -242,6 +248,29 @@ def test_timezone(self):
242248
with self.assertRaises(TypeError):
243249
absl_example.roundtrip_timezone('Not a timezone')
244250

251+
@parameterized.parameters(
252+
absl_example.roundtrip_duration, absl_example.roundtrip_time
253+
)
254+
def test_from_datetime_time(self, rt):
255+
dt1 = rt(dt_time(h=13))
256+
dt2 = rt(dt_time(h=15))
257+
self.assertEqual((dt2 - dt1).seconds, 2 * 3600)
258+
dt1 = rt(dt_time(m=12))
259+
dt2 = rt(dt_time(m=16))
260+
self.assertEqual((dt2 - dt1).seconds, 4 * 60)
261+
dt1 = rt(dt_time(s=11))
262+
dt2 = rt(dt_time(s=17))
263+
self.assertEqual((dt2 - dt1).seconds, 6)
264+
dt1 = rt(dt_time(micros=10))
265+
dt2 = rt(dt_time(micros=18))
266+
self.assertEqual((dt2 - dt1).microseconds, 8)
267+
dt1 = rt(dt_time(tzoff=9))
268+
dt2 = rt(dt_time(tzoff=19))
269+
# Conversion from datetime.time to absl::Duration ignores tzinfo!
270+
self.assertEqual(
271+
(dt2 - dt1).seconds, 0 if rt is absl_example.roundtrip_duration else 10
272+
)
273+
245274

246275
def make_read_only_numpy_array():
247276
values = np.zeros(5, dtype=np.int32)

0 commit comments

Comments
 (0)