@@ -111,11 +111,67 @@ struct type_caster<absl::TimeZone> {
111111};
112112
113113namespace internal {
114+
114115inline 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
258359template <typename CivilTimeUnitType>
0 commit comments