Refactor the chrono cast functions into chrono.h.

Add unit tests and documentation for the chrono cast.
This commit is contained in:
Trent Houliston 2016-08-25 23:08:04 +10:00
parent 6ddfd1e090
commit 352149e892
8 changed files with 330 additions and 109 deletions

View File

@ -161,6 +161,7 @@ if (PYBIND11_INSTALL)
set(PYBIND11_HEADERS
include/pybind11/attr.h
include/pybind11/cast.h
include/pybind11/chrono.h
include/pybind11/common.h
include/pybind11/complex.h
include/pybind11/descr.h

View File

@ -758,6 +758,29 @@ Please refer to the supplemental example for details.
length queries (``__len__``), iterators (``__iter__``), the slicing
protocol and other kinds of useful operations.
C++11 chrono datatypes
======================
When including the additional header file :file:`pybind11/chrono.h` conversions from c++11 chrono datatypes
to corresponding python datetime objects are automatically enabled.
The following rules describe how the conversions are applied.
Objects of type ``std::chrono::system_clock::time_point`` are converted into datetime.datetime objects.
These objects are those that specifically come from the system_clock as this is the only clock that measures wall time.
Objects of type ``std::chrono::[other_clock]::time_point`` are converted into datetime.time objects.
These objects are those that come from all clocks that are not the system_clock (e.g. steady_clock).
Clocks other than the system_clock are not measured from wall date/time and instead have any start time
(often when the computer was turned on).
Therefore as these clocks can only measure time from an arbitrary start point they are represented as time without date.
Objects of type ``std::chrono::duration`` are converted into datetime.timedelta objects.
.. note::
Pythons datetime implementation is limited to microsecond precision.
The extra precision that c++11 clocks can have have (nanoseconds) will be lost upon conversion.
The rounding policy from c++ to python is via ``std::chrono::duration_cast<>`` (rounding towards 0 in microseconds).
Return value policies
=====================

View File

@ -297,6 +297,10 @@ as arguments and return values, refer to the section on binding :ref:`classes`.
+---------------------------------+--------------------------+-------------------------------+
| ``std::function<...>`` | STL polymorphic function | :file:`pybind11/functional.h` |
+---------------------------------+--------------------------+-------------------------------+
| ``std::chrono::duration<...>`` | STL time duration | :file:`pybind11/chrono.h` |
+---------------------------------+--------------------------+-------------------------------+
| ``std::chrono::time_point<...>``| STL date/time | :file:`pybind11/chrono.h` |
+---------------------------------+--------------------------+-------------------------------+
| ``Eigen::Matrix<...>`` | Eigen: dense matrix | :file:`pybind11/eigen.h` |
+---------------------------------+--------------------------+-------------------------------+
| ``Eigen::Map<...>`` | Eigen: mapped memory | :file:`pybind11/eigen.h` |

View File

@ -472,115 +472,6 @@ public:
PYBIND11_TYPE_CASTER(bool, _("bool"));
};
template <typename Rep, typename Period> class type_caster<std::chrono::duration<Rep, Period>> {
public:
typedef std::chrono::duration<Rep, Period> type;
typedef std::chrono::duration<std::chrono::hours::rep, std::ratio<86400>> days;
bool load(handle src, bool) {
using namespace std::chrono;
PyDateTime_IMPORT;
if (!src) return false;
if (PyDelta_Check(src.ptr())) {
const PyDateTime_Delta* delta = reinterpret_cast<PyDateTime_Delta*>(src.ptr());
value = duration_cast<duration<Rep, Period>>(
days(delta->days)
+ seconds(delta->seconds)
+ microseconds(delta->microseconds));
return true;
}
else return false;
}
static handle cast(const std::chrono::duration<Rep, Period> &src, return_value_policy /* policy */, handle /* parent */) {
using namespace std::chrono;
PyDateTime_IMPORT;
int dd = duration_cast<days>(src).count();
int ss = duration_cast<seconds>(src % days(1)).count();
int us = duration_cast<microseconds>(src % seconds(1)).count();
return PyDelta_FromDSU(dd, ss, us);
}
PYBIND11_TYPE_CASTER(type, _("datetime.timedelta"));
};
template <typename Duration> class type_caster<std::chrono::time_point<std::chrono::system_clock, Duration>> {
public:
typedef std::chrono::time_point<std::chrono::system_clock, Duration> type;
bool load(handle src, bool) {
using namespace std::chrono;
PyDateTime_IMPORT;
if (!src) return false;
if (PyDateTime_Check(src.ptr())) {
std::tm cal;
cal.tm_sec = PyDateTime_DATE_GET_SECOND(src.ptr());
cal.tm_min = PyDateTime_DATE_GET_MINUTE(src.ptr());
cal.tm_hour = PyDateTime_DATE_GET_HOUR(src.ptr());
cal.tm_mday = PyDateTime_GET_DAY(src.ptr());
cal.tm_mon = PyDateTime_GET_MONTH(src.ptr()) - 1;
cal.tm_year = PyDateTime_GET_YEAR(src.ptr());
cal.tm_isdst = -1;
value = system_clock::from_time_t(mktime(&cal));
return true;
}
else return false;
}
static handle cast(const std::chrono::time_point<std::chrono::system_clock, Duration> &src, return_value_policy /* policy */, handle /* parent */) {
using namespace std::chrono;
PyDateTime_IMPORT;
time_t tt = system_clock::to_time_t(src);
tm *ltime = localtime(&tt); // this function uses static memory so it's best
tm localtime = *ltime; // to copy it out asap just in case
return PyDateTime_FromDateAndTime(localtime.tm_year, localtime.tm_mon + 1
, localtime.tm_mday
, localtime.tm_hour
, localtime.tm_min
, localtime.tm_sec
, (duration_cast<microseconds>(src.time_since_epoch()) % seconds(1)).count());
}
PYBIND11_TYPE_CASTER(type, _("datetime.datetime"));
};
template <typename Clock, typename Duration> class type_caster<std::chrono::time_point<Clock, Duration>> {
public:
typedef std::chrono::time_point<Clock, Duration> type;
bool load(handle src, bool) {
using namespace std::chrono;
PyDateTime_IMPORT;
if (!src) return false;
if (PyTime_Check(src.ptr())) {
value = type(duration_cast<Duration>(
hours(PyDateTime_TIME_GET_HOUR(src.ptr()))
+ minutes(PyDateTime_TIME_GET_MINUTE(src.ptr()))
+ seconds(PyDateTime_TIME_GET_SECOND(src.ptr()))
+ microseconds(PyDateTime_TIME_GET_MICROSECOND(src.ptr()))
));
return true;
}
else return false;
}
static handle cast(const std::chrono::time_point<Clock, Duration> &src, return_value_policy /* policy */, handle /* parent */) {
using namespace std::chrono;
PyDateTime_IMPORT;
Duration d = src.time_since_epoch();
return PyTime_FromTime(duration_cast<hours>(d).count()
, duration_cast<minutes>(d % hours(1)).count()
, duration_cast<seconds>(d % minutes(1)).count()
, duration_cast<microseconds>(d % seconds(1)).count());
}
PYBIND11_TYPE_CASTER(type, _("datetime.time"));
};
template <> class type_caster<std::string> {
public:
bool load(handle src, bool) {

146
include/pybind11/chrono.h Normal file
View File

@ -0,0 +1,146 @@
/*
pybind11/chrono.h: Transparent conversion between std::chrono and python's datetime
Copyright (c) 2016 Trent Houliston <trent@houliston.me> and
Wenzel Jakob <wenzel.jakob@epfl.ch>
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/
#pragma once
#include "pybind11.h"
#include <chrono>
NAMESPACE_BEGIN(pybind11)
NAMESPACE_BEGIN(detail)
template <typename Rep, typename Period> class type_caster<std::chrono::duration<Rep, Period>> {
public:
typedef std::chrono::duration<Rep, Period> type;
typedef std::chrono::duration<std::chrono::hours::rep, std::ratio<86400>> days;
bool load(handle src, bool) {
using namespace std::chrono;
PyDateTime_IMPORT;
if (!src) return false;
if (PyDelta_Check(src.ptr())) {
// The accessor macros for timedelta exist in some versions of python but not others (e.g. Mac OSX default python)
// Therefore we are just doing what the macros do explicitly
const PyDateTime_Delta* delta = reinterpret_cast<PyDateTime_Delta*>(src.ptr());
value = duration_cast<duration<Rep, Period>>(
days(delta->days)
+ seconds(delta->seconds)
+ microseconds(delta->microseconds));
return true;
}
else return false;
}
static handle cast(const std::chrono::duration<Rep, Period> &src, return_value_policy /* policy */, handle /* parent */) {
using namespace std::chrono;
PyDateTime_IMPORT;
// Declare these special duration types so the conversions happen with the correct primitive types (int)
typedef duration<int, std::ratio<86400>> dd_t;
typedef duration<int, std::ratio<1>> ss_t;
typedef duration<int, std::micro> us_t;
return PyDelta_FromDSU(
duration_cast<dd_t>(src).count()
, duration_cast<ss_t>(src % days(1)).count()
, duration_cast<us_t>(src % seconds(1)).count());
}
PYBIND11_TYPE_CASTER(type, _("datetime.timedelta"));
};
template <typename Duration> class type_caster<std::chrono::time_point<std::chrono::system_clock, Duration>> {
public:
typedef std::chrono::time_point<std::chrono::system_clock, Duration> type;
bool load(handle src, bool) {
using namespace std::chrono;
PyDateTime_IMPORT;
if (!src) return false;
if (PyDateTime_Check(src.ptr())) {
std::tm cal;
cal.tm_sec = PyDateTime_DATE_GET_SECOND(src.ptr());
cal.tm_min = PyDateTime_DATE_GET_MINUTE(src.ptr());
cal.tm_hour = PyDateTime_DATE_GET_HOUR(src.ptr());
cal.tm_mday = PyDateTime_GET_DAY(src.ptr());
cal.tm_mon = PyDateTime_GET_MONTH(src.ptr()) - 1;
cal.tm_year = PyDateTime_GET_YEAR(src.ptr()) - 1900;
cal.tm_isdst = -1;
value = system_clock::from_time_t(mktime(&cal)) + microseconds(PyDateTime_DATE_GET_MICROSECOND(src.ptr()));
return true;
}
else return false;
}
static handle cast(const std::chrono::time_point<std::chrono::system_clock, Duration> &src, return_value_policy /* policy */, handle /* parent */) {
using namespace std::chrono;
PyDateTime_IMPORT;
time_t tt = system_clock::to_time_t(src);
// this function uses static memory so it's best to copy it out asap just in case
tm *ltime = localtime(&tt);
tm localtime = *ltime;
// Declare these special duration types so the conversions happen with the correct primitive types (int)
using us_t = duration<int, std::micro>;
return PyDateTime_FromDateAndTime(localtime.tm_year + 1900
, localtime.tm_mon + 1
, localtime.tm_mday
, localtime.tm_hour
, localtime.tm_min
, localtime.tm_sec
, (duration_cast<us_t>(src.time_since_epoch() % seconds(1))).count());
}
PYBIND11_TYPE_CASTER(type, _("datetime.datetime"));
};
template <typename Clock, typename Duration> class type_caster<std::chrono::time_point<Clock, Duration>> {
public:
typedef std::chrono::time_point<Clock, Duration> type;
bool load(handle src, bool) {
using namespace std::chrono;
PyDateTime_IMPORT;
if (!src) return false;
if (PyTime_Check(src.ptr())) {
value = type(duration_cast<Duration>(
hours(PyDateTime_TIME_GET_HOUR(src.ptr()))
+ minutes(PyDateTime_TIME_GET_MINUTE(src.ptr()))
+ seconds(PyDateTime_TIME_GET_SECOND(src.ptr()))
+ microseconds(PyDateTime_TIME_GET_MICROSECOND(src.ptr()))
));
return true;
}
else return false;
}
static handle cast(const std::chrono::time_point<Clock, Duration> &src, return_value_policy /* policy */, handle /* parent */) {
using namespace std::chrono;
PyDateTime_IMPORT;
// Declare these special duration types so the conversions happen with the correct primitive types (int)
typedef duration<int, std::ratio<3600>> hh_t;
typedef duration<int, std::ratio<60>> mm_t;
typedef duration<int, std::ratio<1>> ss_t;
typedef duration<int, std::micro> us_t;
Duration d = src.time_since_epoch();
return PyTime_FromTime(duration_cast<hh_t>(d).count()
, duration_cast<mm_t>(d % hours(1)).count()
, duration_cast<ss_t>(d % minutes(1)).count()
, duration_cast<us_t>(d % seconds(1)).count());
}
PYBIND11_TYPE_CASTER(type, _("datetime.time"));
};
NAMESPACE_END(detail)
NAMESPACE_END(pybind11)

View File

@ -7,6 +7,7 @@ set(PYBIND11_TEST_FILES
test_alias_initialization.cpp
test_buffers.cpp
test_callbacks.cpp
test_chrono.cpp
test_class_args.cpp
test_constants_and_functions.cpp
test_eigen.cpp

53
tests/test_chrono.cpp Normal file
View File

@ -0,0 +1,53 @@
/*
tests/test_chrono.cpp -- test conversions to/from std::chrono types
Copyright (c) 2016 Trent Houliston <trent@houliston.me> and
Wenzel Jakob <wenzel.jakob@epfl.ch>
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/
#include "pybind11_tests.h"
#include "constructor_stats.h"
#include <pybind11/chrono.h>
// Return the current time off the wall clock
std::chrono::system_clock::time_point test_chrono1() {
return std::chrono::system_clock::now();
}
// Round trip the passed in system clock time
std::chrono::system_clock::time_point test_chrono2(std::chrono::system_clock::time_point t) {
return t;
}
// Round trip the passed in duration
std::chrono::system_clock::duration test_chrono3(std::chrono::system_clock::duration d) {
return d;
}
// Difference between two passed in time_points
std::chrono::system_clock::duration test_chrono4(std::chrono::system_clock::time_point a, std::chrono::system_clock::time_point b) {
return a - b;
}
// Return the current time off the steady_clock
std::chrono::steady_clock::time_point test_chrono5() {
return std::chrono::steady_clock::now();
}
// Round trip a steady clock timepoint
std::chrono::steady_clock::time_point test_chrono6(std::chrono::steady_clock::time_point t) {
return t;
}
test_initializer chrono([] (py::module &m) {
m.def("test_chrono1", &test_chrono1);
m.def("test_chrono2", &test_chrono2);
m.def("test_chrono3", &test_chrono3);
m.def("test_chrono4", &test_chrono4);
m.def("test_chrono5", &test_chrono5);
m.def("test_chrono6", &test_chrono6);
});

102
tests/test_chrono.py Normal file
View File

@ -0,0 +1,102 @@
def test_chrono_system_clock():
from pybind11_tests import test_chrono1
import datetime
# Get the time from both c++ and datetime
date1 = 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 = abs(date1 - date2)
# There should never be a days/seconds difference
assert diff.days == 0
assert diff.seconds == 0
# 500 microseconds is a very long time to execute this
assert diff.microseconds < 500
def test_chrono_system_clock_roundtrip():
from pybind11_tests import test_chrono2
import datetime
date1 = datetime.datetime.today()
# Roundtrip the time
date2 = 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.days == 0
assert diff.seconds == 0
assert diff.microseconds == 0
def test_chrono_duration_roundtrip():
from pybind11_tests import test_chrono3
import datetime
# Get the difference betwen 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 = test_chrono3(diff)
assert cpp_diff.days == diff.days
assert cpp_diff.seconds == diff.seconds
assert cpp_diff.microseconds == diff.microseconds
def test_chrono_duration_subtraction_equivalence():
from pybind11_tests import test_chrono4
import datetime
date1 = datetime.datetime.today()
date2 = datetime.datetime.today()
diff = date2 - date1
cpp_diff = test_chrono4(date2, date1)
assert cpp_diff.days == diff.days
assert cpp_diff.seconds == diff.seconds
assert cpp_diff.microseconds == diff.microseconds
def test_chrono_steady_clock():
from pybind11_tests import test_chrono5
import datetime
time1 = test_chrono5()
time2 = test_chrono5()
assert isinstance(time1, datetime.time)
assert isinstance(time2, datetime.time)
def test_chrono_steady_clock_roundtrip():
from pybind11_tests import test_chrono6
import datetime
time1 = datetime.time(second=10, microsecond=100)
time2 = test_chrono6(time1)
assert isinstance(time2, datetime.time)
# They should be identical (no information lost on roundtrip)
assert time1.hour == time2.hour
assert time1.minute == time2.minute
assert time1.second == time2.second
assert time1.microsecond == time2.microsecond