From bbf1d974a17424b80fee4ffc4777a6e28e09be62 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 17 Oct 2024 15:40:19 +0200 Subject: [PATCH 01/12] Install pybind in Docker setup --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f2c9430..7ca41bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,10 @@ RUN apt-get update && \ apt-get install -y \ build-essential \ cmake \ - libgtest-dev && \ + libgtest-dev \ + pybind11-dev \ + python3-dev \ + python3-pybind11 && \ apt-get clean From 91d8c04f2b51515518ceefe265b06d303e4aa0a9 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 17 Oct 2024 15:40:36 +0200 Subject: [PATCH 02/12] Create python bindings header --- include/util_caching/python_bindings.hpp | 83 ++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 include/util_caching/python_bindings.hpp diff --git a/include/util_caching/python_bindings.hpp b/include/util_caching/python_bindings.hpp new file mode 100644 index 0000000..893b4f1 --- /dev/null +++ b/include/util_caching/python_bindings.hpp @@ -0,0 +1,83 @@ +#include + +#include +#include +#include + +#include "cache.hpp" + +namespace util_caching::python_api { + +namespace py = pybind11; + +/*! + * \brief Bindings for a Cache that is based on number comparisons + * + * This function binds the Cache class for a specific number-based key type (NumberT) and value type (ValueT). + * Call this function once inside PYBIND11_MODULE macro to create a Python module with the bindings. + */ +template +void bindNumberBasedCache(py::module& module) { + using CacheT = Cache; + using ApproximateNumberT = policies::ApproximateNumber; + + py::class_>(module, "ApproximateNumber") + .def(py::init(), py::arg("threshold")) + .def("__call__", &ApproximateNumberT::operator(), "Compare two values"); + + py::class_>(module, "Cache") + .def(py::init<>()) + // We cannot pass template parameters to python functions, therefore we need to explicitly bind all + // instantiations to different python functions. + // We need to use the lambdas here to handle the seconds argument, defining the comparison policy. + .def( + "cached", + [](CacheT& self, const NumberT& key) { return self.template cached>(key); }, + py::arg("key")) + // Approximate match: using ApproximateNumber policy + .def( + "cached", + [](CacheT& self, const NumberT& key, const ApproximateNumberT& policy) { + return self.template cached(key, policy); + }, + py::arg("key"), + py::arg("policy"), + "Check if the value is approximately cached based on the given policy") + .def("cache", &CacheT::cache, py::arg("key"), py::arg("value")) + .def("reset", &CacheT::reset); +} + +/*! + * \brief Bindings for a Cache that is based on time comparisons + * + * This function binds the Cache class for a specific time-based key type (TimeT) and value type (ValueT). + * Call this function once inside PYBIND11_MODULE macro to create a Python module with the bindings. + */ +template +void bindTimeBasedCache(py::module& module) { + using CacheT = Cache; + using ApproximateTimeT = policies::ApproximateTime; + + py::class_(module, "ApproximateTime") + .def(py::init(), py::arg("threshold")) + .def("__call__", &ApproximateTimeT::operator(), "Compare two time points"); + + py::class_>(module, "Cache") + .def(py::init<>()) + .def( + "cached", + [](CacheT& self, const TimeT& key) { return self.template cached>(key); }, + py::arg("key")) + .def( + "cached", + [](CacheT& self, const TimeT& key, const ApproximateTimeT& policy) { + return self.template cached(key, policy); + }, + py::arg("key"), + py::arg("policy"), + "Check if the value is approximately cached based on the given policy") + .def("cache", &CacheT::cache, py::arg("key"), py::arg("value")) + .def("reset", &CacheT::reset); +} + +} // namespace util_caching::python_api From 318346829d530dd548da1e8558236aa040a59cab Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 17 Oct 2024 15:41:13 +0200 Subject: [PATCH 03/12] Add pythond bindings required to run unit tests --- test/python_bindings.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 test/python_bindings.cpp diff --git a/test/python_bindings.cpp b/test/python_bindings.cpp new file mode 100644 index 0000000..9b7db3e --- /dev/null +++ b/test/python_bindings.cpp @@ -0,0 +1,16 @@ +#include + +#include "cache.hpp" +#include "python_bindings.hpp" + +namespace py = pybind11; + +using namespace util_caching; + +PYBIND11_MODULE(util_caching_py, mainModule) { + py::module numberBased = mainModule.def_submodule("number_based"); + python_api::bindNumberBasedCache(numberBased); + + py::module timeBased = mainModule.def_submodule("time_based"); + python_api::bindTimeBasedCache(timeBased); +} From e5a22bb8b2267b7501ce9f2cc985a96c8b2f63d3 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Mon, 11 Nov 2024 11:58:28 +0100 Subject: [PATCH 04/12] Add python unit tests to test/CMakeLists If pybind is available, this will compile the bindings, copy the python files to the build directory and add the test to ctest. --- test/CMakeLists.txt | 52 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 90c48e4..55711e9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -43,6 +43,11 @@ endif() ################### find_package(GTest) +find_package(pybind11 CONFIG) + +if(NOT GTEST_FOUND AND NOT pybind11_FOUND) + message(WARNING "Neither GTest nor pybind11 found. Cannot compile tests!") +endif() # Find installed lib and its dependencies, if this is build as top-level project if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) @@ -50,13 +55,14 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) endif() -########### -## Build ## -########### +#################### +## C++ Unit Tests ## +#################### if(GTEST_FOUND) file(GLOB_RECURSE _tests CONFIGURE_DEPENDS "*.cpp" "*.cc") list(FILTER _tests EXCLUDE REGEX "${CMAKE_CURRENT_BINARY_DIR}") + list(REMOVE_ITEM _tests "${CMAKE_CURRENT_SOURCE_DIR}/python_bindings.cpp") foreach(_test ${_tests}) get_filename_component(_test_name ${_test} NAME_WE) @@ -80,6 +86,42 @@ if(GTEST_FOUND) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endforeach() -else() - message(WARNING "GTest not found. Cannot compile tests!") endif() + +####################### +## Python Unit Tests ## +####################### + +if(pybind11_FOUND) + # Find Python3 to run tests via ctest + find_package(Python3 REQUIRED) + + # Python bindings modules + pybind11_add_module(util_caching_py + python_bindings.cpp + ) + target_link_libraries(util_caching_py PUBLIC + ${PROJECT_NAME} + ) + + file(GLOB_RECURSE _py_tests CONFIGURE_DEPENDS "*.py") + + # Copy Python test files to build directory + foreach(_py_test ${_py_tests}) + get_filename_component(_py_test_name ${_py_test} NAME) + string(REGEX REPLACE "-test" "" PY_TEST_NAME ${_py_test_name}) + set(PY_TEST_NAME ${PROJECT_NAME}-pytest-${PY_TEST_NAME}) + + message(STATUS + "Adding python unittest \"${PY_TEST_NAME}\" with working dir ${PROJECT_SOURCE_DIR}/${TEST_FOLDER} \n _test: ${_py_test}" + ) + + configure_file(${_py_test} ${PY_TEST_NAME} COPYONLY) + + add_test(NAME ${PY_TEST_NAME} + COMMAND ${Python3_EXECUTABLE} ${PY_TEST_NAME} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + endforeach() +endif() + From 7bf8b60dbcdebf7afb406e37da71679a2b1ed2b5 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 17 Oct 2024 15:41:38 +0200 Subject: [PATCH 05/12] Add unit test analogous to C++ unit test --- test/util_caching.py | 86 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test/util_caching.py diff --git a/test/util_caching.py b/test/util_caching.py new file mode 100644 index 0000000..0221e47 --- /dev/null +++ b/test/util_caching.py @@ -0,0 +1,86 @@ +import os +import unittest +import time + +from util_caching_py import number_based, time_based + + +class CacheTest(unittest.TestCase): + def setUp(self): + self.key1 = 1.0 + self.key2 = 1.2 + self.key3 = 1.6 + self.time1 = time.time() + self.time2 = self.time1 + 0.01 # 10 milliseconds later + self.time3 = self.time1 + 1.1 # 1100 milliseconds later + self.time4 = self.time1 + 2.1 # 2100 milliseconds later + self.cache_by_number = number_based.Cache() + self.cache_by_time = time_based.Cache() + self.approximate_number_policy = number_based.ApproximateNumber(0.5) + self.approximate_time_policy = time_based.ApproximateTime(100) + self.approximate_time_policy_2 = time_based.ApproximateTime(1000) + + def test_with_number_key(self): + self.assertIsNone(self.cache_by_number.cached(self.key1)) + self.cache_by_number.cache(self.key1, 1.0) + + # exact match + self.assertTrue(self.cache_by_number.cached(self.key1)) + self.assertEqual(self.cache_by_number.cached(self.key1), 1.0) + + # approximate match + self.assertTrue( + self.cache_by_number.cached(self.key2, self.approximate_number_policy) + ) + self.assertEqual( + self.cache_by_number.cached(self.key2, self.approximate_number_policy), + 1.0, + ) + + # over threshold + self.assertIsNone( + self.cache_by_number.cached(self.key3, self.approximate_number_policy) + ) + + def test_with_time_key(self): + self.assertIsNone(self.cache_by_time.cached(self.time1)) + self.cache_by_time.cache(self.time1, 1.0) + + # exact match + self.assertTrue(self.cache_by_time.cached(self.time1)) + self.assertEqual(self.cache_by_time.cached(self.time1), 1.0) + + # approximate match with milliseconds + self.assertTrue( + self.cache_by_time.cached(self.time2, self.approximate_time_policy) + ) + self.assertEqual( + self.cache_by_time.cached(self.time2, self.approximate_time_policy), 1.0 + ) + + # approximate match with seconds + self.assertTrue( + self.cache_by_time.cached(self.time2, self.approximate_time_policy_2) + ) + self.assertEqual( + self.cache_by_time.cached(self.time2, self.approximate_time_policy_2), 1.0 + ) + + # over threshold + self.assertIsNone( + self.cache_by_time.cached(self.time3, self.approximate_time_policy) + ) + # expect 2s after rounding to integer which is over threshold + self.assertIsNone( + self.cache_by_time.cached(self.time4, self.approximate_time_policy_2) + ) + + +if __name__ == "__main__": + header = "Running " + os.path.basename(__file__) + + print("=" * len(header)) + print(header) + print("=" * len(header) + "\n") + unittest.main(exit=False) + print("=" * len(header) + "\n") From e9b7ca1341047627f1c2bb8c4daddbd2b5864bd0 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Fri, 18 Oct 2024 17:06:10 +0200 Subject: [PATCH 06/12] Add a note about the python bindings to the readme. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index d1b37b3..3dfbcfd 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,13 @@ or by specifying one comparison policy and threshold (100ms for example), and re More usage please check the unittest. +## Python bindings + +The library can be used in Python via pybind11 bindings. +Since util_caching is a template library, we need to explicitly instantiate the template for the types we want to use in Python. +For this, we provide the convenience functions `bindNumberBasedCache` and `bindTimeBasedCache`. +Check the unit test for a usage example. + ## Installation From 80bc6cea4adbc5947910f48e472bf8102d86cae8 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Mon, 21 Oct 2024 10:31:09 +0200 Subject: [PATCH 07/12] Use parameter packs for policy binding. Split binding functions into separate namespaces. --- include/util_caching/python_bindings.hpp | 97 +++++++++++++++--------- test/python_bindings.cpp | 8 +- 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/include/util_caching/python_bindings.hpp b/include/util_caching/python_bindings.hpp index 893b4f1..97990ec 100644 --- a/include/util_caching/python_bindings.hpp +++ b/include/util_caching/python_bindings.hpp @@ -10,22 +10,40 @@ namespace util_caching::python_api { namespace py = pybind11; +namespace number_based { +namespace internal { +template +void bindPolicies(py::class_>& cacheClass) { + (cacheClass.def( + "cached", + [](CacheT& self, const NumberT& key, const ComparisonPolicyTs& policy) { + return self.template cached(key, policy); + }, + py::arg("key"), + py::arg("policy")), + ...); +} +} // namespace internal + +template +void bindApproximatePolicy(py::module& module, const std::string& name = "ApproximateNumber") { + using ApproximateNumberT = policies::ApproximateNumber; + py::class_>(module, name.c_str()) + .def(py::init(), py::arg("threshold")) + .def("__call__", &ApproximateNumberT::operator(), "Compare two numbers"); +} + /*! * \brief Bindings for a Cache that is based on number comparisons * * This function binds the Cache class for a specific number-based key type (NumberT) and value type (ValueT). * Call this function once inside PYBIND11_MODULE macro to create a Python module with the bindings. */ -template -void bindNumberBasedCache(py::module& module) { +template +void bindCache(py::module& module) { using CacheT = Cache; - using ApproximateNumberT = policies::ApproximateNumber; - - py::class_>(module, "ApproximateNumber") - .def(py::init(), py::arg("threshold")) - .def("__call__", &ApproximateNumberT::operator(), "Compare two values"); - - py::class_>(module, "Cache") + py::class_> cache(module, "Cache"); + cache .def(py::init<>()) // We cannot pass template parameters to python functions, therefore we need to explicitly bind all // instantiations to different python functions. @@ -34,17 +52,36 @@ void bindNumberBasedCache(py::module& module) { "cached", [](CacheT& self, const NumberT& key) { return self.template cached>(key); }, py::arg("key")) - // Approximate match: using ApproximateNumber policy - .def( - "cached", - [](CacheT& self, const NumberT& key, const ApproximateNumberT& policy) { - return self.template cached(key, policy); - }, - py::arg("key"), - py::arg("policy"), - "Check if the value is approximately cached based on the given policy") .def("cache", &CacheT::cache, py::arg("key"), py::arg("value")) .def("reset", &CacheT::reset); + + internal::bindPolicies(cache); +} + +} // namespace number_based + +namespace time_based { + +namespace internal { +template +void bindPolicies(py::class_>& cache) { + (cache.def( + "cached", + [](CacheT& self, const TimeT& key, const ComparisonPolicyTs& policy) { + return self.template cached(key, policy); + }, + py::arg("key"), + py::arg("policy")), + ...); +} +} // namespace internal + +template +void bindApproximatePolicy(py::module& module, const std::string& name = "ApproximateTime") { + using ApproximateTimeT = policies::ApproximateTime; + py::class_>(module, name.c_str()) + .def(py::init(), py::arg("threshold")) + .def("__call__", &ApproximateTimeT::operator(), "Compare two time points"); } /*! @@ -53,31 +90,21 @@ void bindNumberBasedCache(py::module& module) { * This function binds the Cache class for a specific time-based key type (TimeT) and value type (ValueT). * Call this function once inside PYBIND11_MODULE macro to create a Python module with the bindings. */ -template -void bindTimeBasedCache(py::module& module) { +template +void bindCache(py::module& module) { using CacheT = Cache; - using ApproximateTimeT = policies::ApproximateTime; - py::class_(module, "ApproximateTime") - .def(py::init(), py::arg("threshold")) - .def("__call__", &ApproximateTimeT::operator(), "Compare two time points"); - - py::class_>(module, "Cache") - .def(py::init<>()) + py::class_> cache(module, "Cache"); + cache.def(py::init<>()) .def( "cached", [](CacheT& self, const TimeT& key) { return self.template cached>(key); }, py::arg("key")) - .def( - "cached", - [](CacheT& self, const TimeT& key, const ApproximateTimeT& policy) { - return self.template cached(key, policy); - }, - py::arg("key"), - py::arg("policy"), - "Check if the value is approximately cached based on the given policy") .def("cache", &CacheT::cache, py::arg("key"), py::arg("value")) .def("reset", &CacheT::reset); + + internal::bindPolicies(cache); } +} // namespace time_based } // namespace util_caching::python_api diff --git a/test/python_bindings.cpp b/test/python_bindings.cpp index 9b7db3e..b16c44a 100644 --- a/test/python_bindings.cpp +++ b/test/python_bindings.cpp @@ -2,6 +2,7 @@ #include "cache.hpp" #include "python_bindings.hpp" +#include "types.hpp" namespace py = pybind11; @@ -9,8 +10,11 @@ using namespace util_caching; PYBIND11_MODULE(util_caching_py, mainModule) { py::module numberBased = mainModule.def_submodule("number_based"); - python_api::bindNumberBasedCache(numberBased); + python_api::number_based::bindApproximatePolicy(numberBased); + python_api::number_based::bindCache>(numberBased); py::module timeBased = mainModule.def_submodule("time_based"); - python_api::bindTimeBasedCache(timeBased); + python_api::time_based::bindApproximatePolicy(timeBased); + python_api::time_based::bindCache>( + timeBased); } From 2632f96a0161ce0f070b5dbc7ceb2b014fd345ce Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Mon, 21 Oct 2024 10:53:33 +0200 Subject: [PATCH 08/12] Bind extra comparison policies and add missing unit tests --- test/python_bindings.cpp | 22 +++++++++++++++++++--- test/util_caching.py | 13 +++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/test/python_bindings.cpp b/test/python_bindings.cpp index b16c44a..9388f7e 100644 --- a/test/python_bindings.cpp +++ b/test/python_bindings.cpp @@ -8,13 +8,29 @@ namespace py = pybind11; using namespace util_caching; +struct SomePolicyWithoutParams { + SomePolicyWithoutParams() = default; + bool operator()(const Time& /*lhs*/, const Time& /*rhs*/) const { + return true; + } +}; + PYBIND11_MODULE(util_caching_py, mainModule) { + using ApproximateNumberT = policies::ApproximateNumber; + using ApproximateTimeT = policies::ApproximateTime; + using ApproximateTimeSecondsT = policies::ApproximateTime; + + py::class_>(mainModule, "SomePolicyWithoutParams") + .def(py::init<>()) + .def("__call__", &SomePolicyWithoutParams::operator()); + py::module numberBased = mainModule.def_submodule("number_based"); python_api::number_based::bindApproximatePolicy(numberBased); - python_api::number_based::bindCache>(numberBased); + python_api::number_based::bindCache(numberBased); py::module timeBased = mainModule.def_submodule("time_based"); - python_api::time_based::bindApproximatePolicy(timeBased); - python_api::time_based::bindCache>( + python_api::time_based::bindApproximatePolicy(timeBased, "ApproximateTime"); + python_api::time_based::bindApproximatePolicy(timeBased, "ApproximateTimeSeconds"); + python_api::time_based::bindCache( timeBased); } diff --git a/test/util_caching.py b/test/util_caching.py index 0221e47..360e901 100644 --- a/test/util_caching.py +++ b/test/util_caching.py @@ -2,7 +2,7 @@ import unittest import time -from util_caching_py import number_based, time_based +from util_caching_py import number_based, time_based, SomePolicyWithoutParams class CacheTest(unittest.TestCase): @@ -18,7 +18,8 @@ def setUp(self): self.cache_by_time = time_based.Cache() self.approximate_number_policy = number_based.ApproximateNumber(0.5) self.approximate_time_policy = time_based.ApproximateTime(100) - self.approximate_time_policy_2 = time_based.ApproximateTime(1000) + self.approximate_time_policy_2 = time_based.ApproximateTimeSeconds(1) + self.dummy_policy = SomePolicyWithoutParams() def test_with_number_key(self): self.assertIsNone(self.cache_by_number.cached(self.key1)) @@ -70,11 +71,19 @@ def test_with_time_key(self): self.assertIsNone( self.cache_by_time.cached(self.time3, self.approximate_time_policy) ) + # exactly 1s after rounding to integer + self.assertTrue( + self.cache_by_time.cached(self.time3, self.approximate_time_policy_2) + ) # expect 2s after rounding to integer which is over threshold self.assertIsNone( self.cache_by_time.cached(self.time4, self.approximate_time_policy_2) ) + def test_with_other_comparison_policy(self): + self.cache_by_time.cache(self.time1, 1.0) + self.assertTrue(self.cache_by_time.cached(self.time2, self.dummy_policy)) + if __name__ == "__main__": header = "Running " + os.path.basename(__file__) From 7bd12ae89cbfebcaefa6f78b9f8913a7ff0c5b83 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Mon, 21 Oct 2024 11:04:54 +0200 Subject: [PATCH 09/12] Add a bunch of explanatory comments More documentation --- include/util_caching/python_bindings.hpp | 167 ++++++++++++++--------- test/python_bindings.cpp | 32 ++++- 2 files changed, 132 insertions(+), 67 deletions(-) diff --git a/include/util_caching/python_bindings.hpp b/include/util_caching/python_bindings.hpp index 97990ec..5d779da 100644 --- a/include/util_caching/python_bindings.hpp +++ b/include/util_caching/python_bindings.hpp @@ -12,98 +12,139 @@ namespace py = pybind11; namespace number_based { namespace internal { + +/*! + * \brief Bind the comparison policies to the Cache class + * + * This function binds the comparison policies to the Cache class. The policies + * are passed as variadic template arguments. The function overloads the + * `cached` function for each policy. + */ template -void bindPolicies(py::class_>& cacheClass) { - (cacheClass.def( - "cached", - [](CacheT& self, const NumberT& key, const ComparisonPolicyTs& policy) { - return self.template cached(key, policy); - }, - py::arg("key"), - py::arg("policy")), - ...); +void bindPolicies(py::class_> &cacheClass) { + (cacheClass.def( + "cached", + [](CacheT &self, const NumberT &key, const ComparisonPolicyTs &policy) { + return self.template cached(key, policy); + }, + py::arg("key"), py::arg("policy")), + ...); } } // namespace internal +/*! + * \brief Bind the ApproximateNumber policy + * + * This function adds bindings for the ApproximateNumber policy to the given + * python module under the given name. + */ template -void bindApproximatePolicy(py::module& module, const std::string& name = "ApproximateNumber") { - using ApproximateNumberT = policies::ApproximateNumber; - py::class_>(module, name.c_str()) - .def(py::init(), py::arg("threshold")) - .def("__call__", &ApproximateNumberT::operator(), "Compare two numbers"); +void bindApproximatePolicy(py::module &module, + const std::string &name = "ApproximateNumber") { + using ApproximateNumberT = policies::ApproximateNumber; + py::class_>( + module, name.c_str()) + .def(py::init(), py::arg("threshold")) + .def("__call__", &ApproximateNumberT::operator(), "Compare two numbers"); } /*! * \brief Bindings for a Cache that is based on number comparisons * - * This function binds the Cache class for a specific number-based key type (NumberT) and value type (ValueT). - * Call this function once inside PYBIND11_MODULE macro to create a Python module with the bindings. + * This function binds the Cache class for a specific number-based key type + * (NumberT) and value type (ValueT). Optionally, add a list of comparison + * policies to the list of template parameters. The `cached` function will be + * overloaded for each one of them. Call this function once inside + * PYBIND11_MODULE macro to create the bindings for the Cache class. */ template -void bindCache(py::module& module) { - using CacheT = Cache; - py::class_> cache(module, "Cache"); - cache - .def(py::init<>()) - // We cannot pass template parameters to python functions, therefore we need to explicitly bind all - // instantiations to different python functions. - // We need to use the lambdas here to handle the seconds argument, defining the comparison policy. - .def( - "cached", - [](CacheT& self, const NumberT& key) { return self.template cached>(key); }, - py::arg("key")) - .def("cache", &CacheT::cache, py::arg("key"), py::arg("value")) - .def("reset", &CacheT::reset); - - internal::bindPolicies(cache); +void bindCache(py::module &module) { + using CacheT = Cache; + py::class_> cache(module, "Cache"); + cache + .def(py::init<>()) + // We cannot pass template parameters to python functions, therefore we + // need to explicitly bind all instantiations to different python + // functions. We need to use the lambdas here to handle the seconds + // argument, defining the comparison policy. + .def( + "cached", + [](CacheT &self, const NumberT &key) { + return self.template cached>(key); + }, + py::arg("key")) + .def("cache", &CacheT::cache, py::arg("key"), py::arg("value")) + .def("reset", &CacheT::reset); + + internal::bindPolicies(cache); } } // namespace number_based namespace time_based { - namespace internal { + +/*! + * \brief Bind the comparison policies to the Cache class + * + * This function binds the comparison policies to the Cache class. The policies + * are passed as variadic template arguments. The function overloads the + * `cached` function for each policy. + */ template -void bindPolicies(py::class_>& cache) { - (cache.def( - "cached", - [](CacheT& self, const TimeT& key, const ComparisonPolicyTs& policy) { - return self.template cached(key, policy); - }, - py::arg("key"), - py::arg("policy")), - ...); +void bindPolicies(py::class_> &cache) { + (cache.def( + "cached", + [](CacheT &self, const TimeT &key, const ComparisonPolicyTs &policy) { + return self.template cached(key, policy); + }, + py::arg("key"), py::arg("policy")), + ...); } } // namespace internal +/*! + * \brief Bind the ApproximateTime policy + * + * This function adds bindings for the ApproximateTime policy to the given + * python module under the given name. + */ template -void bindApproximatePolicy(py::module& module, const std::string& name = "ApproximateTime") { - using ApproximateTimeT = policies::ApproximateTime; - py::class_>(module, name.c_str()) - .def(py::init(), py::arg("threshold")) - .def("__call__", &ApproximateTimeT::operator(), "Compare two time points"); +void bindApproximatePolicy(py::module &module, + const std::string &name = "ApproximateTime") { + using ApproximateTimeT = policies::ApproximateTime; + py::class_>(module, + name.c_str()) + .def(py::init(), py::arg("threshold")) + .def("__call__", &ApproximateTimeT::operator(), + "Compare two time points"); } /*! - * \brief Bindings for a Cache that is based on time comparisons + * \brief Bindings for a Cache that is based on time comparisons. * - * This function binds the Cache class for a specific time-based key type (TimeT) and value type (ValueT). - * Call this function once inside PYBIND11_MODULE macro to create a Python module with the bindings. + * This function binds the Cache class for a specific time-based key type + * (TimeT) and value type (ValueT). Optionally, add a list of comparison + * policies to the list of template parameters. The `cached` function will be + * overloaded for each one of them. Call this function once inside + * PYBIND11_MODULE macro to create the bindings for the Cache class. */ template -void bindCache(py::module& module) { - using CacheT = Cache; - - py::class_> cache(module, "Cache"); - cache.def(py::init<>()) - .def( - "cached", - [](CacheT& self, const TimeT& key) { return self.template cached>(key); }, - py::arg("key")) - .def("cache", &CacheT::cache, py::arg("key"), py::arg("value")) - .def("reset", &CacheT::reset); - - internal::bindPolicies(cache); +void bindCache(py::module &module) { + using CacheT = Cache; + + py::class_> cache(module, "Cache"); + cache.def(py::init<>()) + .def( + "cached", + [](CacheT &self, const TimeT &key) { + return self.template cached>(key); + }, + py::arg("key")) + .def("cache", &CacheT::cache, py::arg("key"), py::arg("value")) + .def("reset", &CacheT::reset); + + internal::bindPolicies(cache); } } // namespace time_based diff --git a/test/python_bindings.cpp b/test/python_bindings.cpp index 9388f7e..d7fe52a 100644 --- a/test/python_bindings.cpp +++ b/test/python_bindings.cpp @@ -8,6 +8,12 @@ namespace py = pybind11; using namespace util_caching; +/*! + * \brief A policy that always returns true + * + * Custom policies have to be defined in C++ and then bound to Python. + * To overload the `cache` function, the policy has to be passed as a template parameter to the `bindCache` function. + */ struct SomePolicyWithoutParams { SomePolicyWithoutParams() = default; bool operator()(const Time& /*lhs*/, const Time& /*rhs*/) const { @@ -15,22 +21,40 @@ struct SomePolicyWithoutParams { } }; +/*! + * \brief The python module definition that allows running equivalent unit tests in python. + */ PYBIND11_MODULE(util_caching_py, mainModule) { + // Just some aliases to make the code more readable using ApproximateNumberT = policies::ApproximateNumber; using ApproximateTimeT = policies::ApproximateTime; using ApproximateTimeSecondsT = policies::ApproximateTime; + // Since we want to use this policy in python, we need to be able to instatiate it there py::class_>(mainModule, "SomePolicyWithoutParams") .def(py::init<>()) .def("__call__", &SomePolicyWithoutParams::operator()); + // Adding a submodule is optional but a good way to structure the bindings py::module numberBased = mainModule.def_submodule("number_based"); + // If we want to use a policy, we need to bind it. For the builtin policies, we can use this convenience function. python_api::number_based::bindApproximatePolicy(numberBased); - python_api::number_based::bindCache(numberBased); - + // The core binding, the cache class itself. + python_api::number_based::bindCache(numberBased); + + // Same as above, but for the time-based cache py::module timeBased = mainModule.def_submodule("time_based"); + // We can bind the builtin comparison policy for different time units but then we have to name them differently python_api::time_based::bindApproximatePolicy(timeBased, "ApproximateTime"); python_api::time_based::bindApproximatePolicy(timeBased, "ApproximateTimeSeconds"); - python_api::time_based::bindCache( - timeBased); + // The core binding, the cache class itself. + python_api::time_based::bindCache(timeBased); } From 3b5261d20911896305f15dc2465a9e22a07d9c44 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Mon, 21 Oct 2024 15:14:05 +0200 Subject: [PATCH 10/12] Add more details to the readme --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3dfbcfd..7eb09a6 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,24 @@ More usage please check the unittest. ## Python bindings The library can be used in Python via pybind11 bindings. -Since util_caching is a template library, we need to explicitly instantiate the template for the types we want to use in Python. -For this, we provide the convenience functions `bindNumberBasedCache` and `bindTimeBasedCache`. -Check the unit test for a usage example. +Since util_caching is a template library, + you need to explicitly instantiate the template for the types you want to use in Python. +For this, we provide convenience functions to bind the library for the desired types. +Simply call them in a pybind11 module definition, e.g.: + +```cpp +PYBIND11_MODULE(util_caching, m) { + python_api::number_based::bindCache(m); +} +``` +and use them in Python: + +```python +from util_caching import Cache +cache = Cache() +cache.cache(1.0, 2.0) +``` +We re-implemented all of the C++ unit tests in Python, so take a closer look at those for more advanced usage examples. ## Installation @@ -119,6 +134,7 @@ find_package(util_caching REQUIRED) First make sure all dependencies are installed: - [Googletest](https://github.com/google/googletest) (only if you want to build unit tests) +- [pybind11](https://pybind11.readthedocs.io/en/stable/) (only if you want to build Python bindings and unit tests) See also the [`Dockerfile`](./Dockerfile) for how to install these packages under Debian or Ubuntu. From 5e145c2c40a63742ca9205e22089647eb0cf6e90 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 14 Nov 2024 14:56:10 +0100 Subject: [PATCH 11/12] Fix for tests now being independent CMake project --- test/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 55711e9..ef0d6bc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -101,7 +101,7 @@ if(pybind11_FOUND) python_bindings.cpp ) target_link_libraries(util_caching_py PUBLIC - ${PROJECT_NAME} + util_caching ) file(GLOB_RECURSE _py_tests CONFIGURE_DEPENDS "*.py") From 3d21e0718f98c417dcb06a33abb3565c8a4179dd Mon Sep 17 00:00:00 2001 From: ll-nick <68419636+ll-nick@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:37:39 +0100 Subject: [PATCH 12/12] Apply suggestions from code review Co-authored-by: Piotr Spieker --- README.md | 6 +++--- test/python_bindings.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7eb09a6..f19eaaa 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ More usage please check the unittest. ## Python bindings The library can be used in Python via pybind11 bindings. -Since util_caching is a template library, +Since `util_caching` is a templated C++ library, you need to explicitly instantiate the template for the types you want to use in Python. For this, we provide convenience functions to bind the library for the desired types. Simply call them in a pybind11 module definition, e.g.: @@ -133,8 +133,8 @@ find_package(util_caching REQUIRED) ### Building from source using CMake First make sure all dependencies are installed: -- [Googletest](https://github.com/google/googletest) (only if you want to build unit tests) -- [pybind11](https://pybind11.readthedocs.io/en/stable/) (only if you want to build Python bindings and unit tests) +- [Googletest](https://github.com/google/googletest) (optional, if you want to build unit tests) +- [pybind11](https://pybind11.readthedocs.io/en/stable/) (optional, if you want to build Python bindings and unit tests) See also the [`Dockerfile`](./Dockerfile) for how to install these packages under Debian or Ubuntu. diff --git a/test/python_bindings.cpp b/test/python_bindings.cpp index d7fe52a..0287770 100644 --- a/test/python_bindings.cpp +++ b/test/python_bindings.cpp @@ -22,7 +22,7 @@ struct SomePolicyWithoutParams { }; /*! - * \brief The python module definition that allows running equivalent unit tests in python. + * \brief The python module definition that allows running python unit tests equivalent to the native C++ ones. */ PYBIND11_MODULE(util_caching_py, mainModule) { // Just some aliases to make the code more readable