diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index f6ff544..f61a277 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -25,17 +25,32 @@ jobs: - { name: "Ubuntu Latest GCC", os: ubuntu-latest, - build_type: "Release", cc: "gcc", cxx: "g++" + build_type: "Release", cc: "gcc", cxx: "g++", + mpi_type: "none" } - { name: "Ubuntu Latest Clang", os: ubuntu-latest, - build_type: "Release", cc: "clang", cxx: "clang++" + build_type: "Release", cc: "clang", cxx: "clang++", + mpi_type: "none" + } + - { + name: "Ubuntu Latest GCC (MPI)", + os: ubuntu-latest, + build_type: "Release", cc: "gcc", cxx: "g++", + mpi_type: "mpich" + } + - { + name: "Ubuntu Latest Clang (MPI)", + os: ubuntu-latest, + build_type: "Release", cc: "clang", cxx: "clang++", + mpi_type: "mpich" } - { name: "macOS Latest Clang", os: macos-latest, - build_type: "Release", cc: "clang", cxx: "clang++" + build_type: "Release", cc: "clang", cxx: "clang++", + mpi_type: "none" } steps: - name: Checkout repository and submodules @@ -43,6 +58,26 @@ jobs: with: submodules: recursive + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install MPI + if: ${{ matrix.config.mpi_type != 'none' }} + uses: mpi4py/setup-mpi@v1.3.5 + with: + mpi: ${{matrix.config.mpi_type}} + + - name: Install Python binding dependencies + run: | + python3 -m pip install pybind11 + + - name: Install mpi4py + if: ${{ matrix.config.mpi_type != 'none' }} + run: | + python -m pip install mpi4py + - name: Create Build Environment # Some projects don't allow in-source building, so create a separate build directory # We'll use this as our working directory for all subsequent commands @@ -56,7 +91,13 @@ jobs: # Note the current convention is to use the -S and -B options here to specify source # and build directories, but this is only available with CMake 3.13 and higher. # The CMake binaries on the Github Actions machines are (as of this writing) 3.12 - run: cmake $GITHUB_WORKSPACE -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_C_COMPILER=${{matrix.config.cc}} -DCMAKE_CXX_COMPILER=${{matrix.config.cxx}} + run: | + EXTRA_OPTS="" + if [ "${{ matrix.config.mpi_type }}" != "none" ]; then + EXTRA_OPTS="-DENABLE_MPI=ON" + fi + cmake $GITHUB_WORKSPACE -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_C_COMPILER=${{matrix.config.cc}} -DCMAKE_CXX_COMPILER=${{matrix.config.cxx}} -DENABLE_PYTHON_BINDINGS=ON -Dpybind11_DIR=$(pybind11-config --cmakedir) \ + $EXTRA_OPTS - name: Build working-directory: ${{github.workspace}}/build diff --git a/CMakeLists.txt b/CMakeLists.txt index 841367c..394c3d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,11 @@ if (NOT DEFINED CMAKE_INSTALL_LIBDIR) set(CMAKE_INSTALL_LIBDIR lib) endif() +# Find Python by location +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.15) + cmake_policy(SET CMP0094 NEW) +endif() + set(BLT_EXPORT_THIRDPARTY ON CACHE BOOL "") if (DEFINED BLT_SOURCE_DIR) @@ -31,6 +36,19 @@ endif() include(${BLT_SOURCE_DIR}/SetupBLT.cmake) +option(ENABLE_PYTHON_BINDINGS "Build/Install Python bindings for Adiak" FALSE) + +if (ENABLE_PYTHON_BINDINGS) + find_package(Python COMPONENTS Interpreter Development REQUIRED) + find_package(pybind11 CONFIG REQUIRED) + + if (pybind11_FOUND) + set(ADIAK_HAVE_PYBIND11 TRUE) + else() + message(WARNING "Python bindings requested, but pybind11 not found") + endif() +endif() + add_subdirectory(src) if (ENABLE_TESTS) add_subdirectory(tests) diff --git a/cmake/get_python_install_paths.py b/cmake/get_python_install_paths.py new file mode 100644 index 0000000..eb48232 --- /dev/null +++ b/cmake/get_python_install_paths.py @@ -0,0 +1,14 @@ +import sys +import sysconfig + +if len(sys.argv) != 3 or sys.argv[1] not in ("purelib", "platlib"): + raise RuntimeError( + "Usage: python get_python_install_paths.py " + ) + +install_dir = sysconfig.get_path(sys.argv[1], sys.argv[2], {"userbase": "", "base": ""}) + +if install_dir.startswith("/"): + install_dir = install_dir[1:] + +print(install_dir, end="") diff --git a/docs/sphinx/PythonSupport.rst b/docs/sphinx/PythonSupport.rst new file mode 100644 index 0000000..abedbad --- /dev/null +++ b/docs/sphinx/PythonSupport.rst @@ -0,0 +1,106 @@ +Python support +============== + +Adiak provides Python bindings based on `pybind11 `_ +for the metadata/annotation APIs (``init``, ``value``, ``fini``) and a set of convenience +collectors (e.g., ``collect_all()``, ``walltime()``). +To build Adiak with Python support, enable +the :code:`ENABLE_PYTHON_BINDINGS` option in the CMake configuration: + +.. code-block:: sh + + $ cmake -DENABLE_PYTHON_BINDINGS=ON .. + +If you want to initialize Adiak with an MPI communicator from Python, also enable (or auto-detect) +MPI at configure time (mpi4py is only needed at runtime if you actually pass a communicator): + +.. code-block:: sh + + $ cmake -DENABLE_PYTHON_BINDINGS=ON -DENABLE_MPI=ON .. + +Using the Python module +----------------------- + +The Python module requires pybind11 and an installation of Python that both supports +pybind11 and provides development headers (e.g., :code:`Python.h`) and libraries +(e.g., :code:`libpython3.8.so`). + +The Adiak Python module is installed in either :code:`lib/pythonX.Y/site-packages/` and/or +:code:`lib64/pythonX.Y/site-packages` in the Adiak installation directory. In these paths, +:code:`X.Y` corresponds to the major and minor version numbers of the Python installation used. +Additionally, :code:`lib/` and :code:`lib64/` will be used in accordance with the configuration +of the Python installed. + +To use the Adiak Python module, simply add the directories above to :code:`PYTHONPATH` or +:code:`sys.path`. Note that the module will be automatically added to :code:`PYTHONPATH` when +loading the Adiak package with Spack if the :code:`python` variant is enabled. +The module can then be imported with :code:`import pyadiak`. + +Example: basic usage (serial) +----------------------------- + +.. code-block:: python + + from datetime import datetime + from pathlib import Path + + from pyadiak.annotations import init, value, fini, collect_all, walltime, cputime, systime + from pyadiak.types import Version, Path as APath, CatStr, Category + + def main(): + init(None) # serial mode + + value("str", "s") + value("compiler", Version("gcc@8.1.0")) + value("mydouble", 3.14) + value("problemsize", 14000, category=Category.Tuning) + value("countdown", 9876543210) + + grid = [4.5, 1.18, 0.24, 8.92] + value("gridvalues", grid) + + names = {"bob", "jim", "greg"} + value("allnames", names) + + # Flatten nested structures (or use JsonStr) + names_arr = ["first", "second", "third"] + xs = [1.0, 2.0, 3.0] + ys = [1.0, 4.0, 9.0] + value("points.names", names_arr) + value("points.x", xs) + value("points.y", ys) + + # Time & paths are auto-wrapped; shown explicitly here for clarity + value("birthday", datetime.fromtimestamp(286551000)) # Timepoint + value("nullpath", APath(Path("/dev/null"))) # Path + value("githash", CatStr("a0c93767478f23602c2eb317f641b091c52cf374")) + + collect_all() + walltime() + cputime() + systime() + + fini() + + if __name__ == "__main__": + main() + + +Example: MPI usage (optional) +----------------------------- + +If built with :code:`-DENABLE_MPI=ON` and :code:`mpi4py` is available: + +.. code-block:: python + + from mpi4py import MPI + from pyadiak.annotations import init, value, fini + + def main(): + comm = MPI.COMM_WORLD + init(comm) # initialize with communicator + value("rank", comm.Get_rank()) + fini() + + if __name__ == "__main__": + main() diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index 0b32d94..d8a1771 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -33,6 +33,7 @@ by printf-inspired strings. ToolsUsingAdiak ApplicationAPI ToolAPI + PythonSupport Contributing ================== diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6266aa2..8704ea9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,3 +53,10 @@ install(TARGETS ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(EXPORT adiak-targets NAMESPACE adiak:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/adiak) + +if (ENABLE_PYTHON_BINDINGS) + add_subdirectory(interface/python) + if (MPI_FOUND) + find_package(MPI REQUIRED CXX) + endif() +endif() diff --git a/src/interface/python/CMakeLists.txt b/src/interface/python/CMakeLists.txt new file mode 100644 index 0000000..f4cfe33 --- /dev/null +++ b/src/interface/python/CMakeLists.txt @@ -0,0 +1,68 @@ +set(PYADIAK_BINDING_SOURCES + mod.cpp + pyadiak_tool.cpp + pyadiak.cpp + types.cpp +) + +set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) + +set(PYADIAK_SYSCONFIG_SCHEME "posix_user" CACHE STRING "Scheme used for searching for pyadiak's install path. Valid options can be determined with 'sysconfig.get_scheme_names()'") + +execute_process( + COMMAND + ${Python_EXECUTABLE} ${CMAKE_SOURCE_DIR}/cmake/get_python_install_paths.py purelib ${PYADIAK_SYSCONFIG_SCHEME} + OUTPUT_VARIABLE + PYADIAK_SITELIB) +execute_process( + COMMAND + ${Python_EXECUTABLE} ${CMAKE_SOURCE_DIR}/cmake/get_python_install_paths.py platlib ${PYADIAK_SYSCONFIG_SCHEME} + OUTPUT_VARIABLE + PYADIAK_SITEARCH) + +set(PYADIAK_SITELIB "${PYADIAK_SITELIB}/pyadiak") +set(PYADIAK_SITEARCH "${PYADIAK_SITEARCH}/pyadiak") + +pybind11_add_module(__pyadiak_impl ${PYADIAK_BINDING_SOURCES}) +target_link_libraries(__pyadiak_impl PUBLIC adiak) +target_compile_features(__pyadiak_impl PUBLIC cxx_std_11) +target_include_directories(__pyadiak_impl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +execute_process( + COMMAND python3 -c "import mpi4py, os; print(os.path.join(os.path.dirname(mpi4py.__file__), 'include'))" + OUTPUT_VARIABLE MPI4PY_INCLUDE_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) +include_directories(${MPI4PY_INCLUDE_DIR}) + +add_custom_target( + pyadiak_py_source_copy ALL # Always build pycaliper_test + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/pyadiak + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/pyadiak ${CMAKE_CURRENT_BINARY_DIR}/pyadiak + COMMENT "Copying pyadiak Python source to ${CMAKE_CURRENT_BINARY_DIR}/pyadiak" +) +add_dependencies(__pyadiak_impl pyadiak_py_source_copy) + +install( + DIRECTORY + pyadiak/ + DESTINATION + ${PYADIAK_SITELIB} +) + +install( + TARGETS + __pyadiak_impl + ARCHIVE DESTINATION + ${PYADIAK_SITEARCH} + LIBRARY DESTINATION + ${PYADIAK_SITEARCH} +) + +# Put the compiled extension inside the pyadiak package in the build tree +set(_py_pkg_dir "${CMAKE_BINARY_DIR}/src/interface/python/pyadiak") + +set_target_properties(__pyadiak_impl PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${_py_pkg_dir}" # where the .so (MODULE/LIBRARY) goes + RUNTIME_OUTPUT_DIRECTORY "${_py_pkg_dir}" + ARCHIVE_OUTPUT_DIRECTORY "${_py_pkg_dir}" +) diff --git a/src/interface/python/common.hpp b/src/interface/python/common.hpp new file mode 100644 index 0000000..2704271 --- /dev/null +++ b/src/interface/python/common.hpp @@ -0,0 +1,12 @@ +#ifndef ADIAK_INTERFACE_PYTHON_COMMON_HPP_ +#define ADIAK_INTERFACE_PYTHON_COMMON_HPP_ + +#include +#include +#include +#include +#include + +namespace py = pybind11; + +#endif /* ADIAK_INTERFACE_PYTHON_COMMON_HPP_ */ \ No newline at end of file diff --git a/src/interface/python/mod.cpp b/src/interface/python/mod.cpp new file mode 100644 index 0000000..9408140 --- /dev/null +++ b/src/interface/python/mod.cpp @@ -0,0 +1,17 @@ +#include "pyadiak.hpp" +#include "pyadiak_tool.hpp" + +PYBIND11_MODULE(__pyadiak_impl, m) { + // TODO add version + + auto types_module = + m.def_submodule("types", "Submodule for type wrappers in Adiak"); + adiak::python::create_adiak_types_mod(types_module); + + auto annotation_module = + m.def_submodule("annotations", "Submodule for annotation APIs"); + adiak::python::create_adiak_annotation_mod(annotation_module); + + auto tool_module = m.def_submodule("tools", "Submodule for tool APIs"); + adiak::python::create_adiak_tool_mod(tool_module); +} \ No newline at end of file diff --git a/src/interface/python/pyadiak.cpp b/src/interface/python/pyadiak.cpp new file mode 100644 index 0000000..3560d00 --- /dev/null +++ b/src/interface/python/pyadiak.cpp @@ -0,0 +1,299 @@ +#include "pyadiak.hpp" +#include "adiak.hpp" +#if defined(ENABLE_MPI) + #include + #include +#endif +#include "types.hpp" + +#include +#include +#include +#include +#include +#include + +// Bindings for MPI Comm derived from this StackOverflow post: +// https://stackoverflow.com/a/62449190 +struct mpi4py_comm { + mpi4py_comm() = default; + + #if defined(ENABLE_MPI) + mpi4py_comm(MPI_Comm comm) : m_comm(comm) {} + MPI_Comm m_comm; + #else + // no-op placeholder so the type still compiles + mpi4py_comm(int /*unused*/ = 0) {} + std::nullptr_t m_comm = nullptr; + #endif +}; + +namespace pybind11 { +namespace detail { + +#if defined(ENABLE_MPI) +template <> struct type_caster { + PYBIND11_TYPE_CASTER(mpi4py_comm, _("mpi4py_comm")); + + bool load(py::handle src, bool) { + // If the comm is None, produce a mpi4py_comm object with m_comm set to + // MPI_COMM_NULL + if (src.is(py::none())) { + value.m_comm = MPI_COMM_NULL; + return !PyErr_Occurred(); + } + // Get the underlying PyObject pointer to the mpi4py object + PyObject *py_capi_comm = src.ptr(); + // Make sure the PyObject pointer's underlying type is mpi4py's + // PyMPIComm_Type. If the type check passes, use PyMPIComm_Get (from + // mpi4py's C API) to get the actual MPI_Comm. If the type check fails, tell + // pybind11 that the conversion is invalid. + if (PyObject_TypeCheck(py_capi_comm, &PyMPIComm_Type)) { + value.m_comm = *PyMPIComm_Get(py_capi_comm); + } else { + return false; + } + // Tell pybind11 that the conversion to C++ was successfull so long as there + // were no Python errors + return !PyErr_Occurred(); + } + + static py::handle cast(mpi4py_comm src, py::return_value_policy, py::handle) { + return PyMPIComm_New(src.m_comm); + } +}; +#endif + +} // namespace detail +} // namespace pybind11 + +namespace adiak { +namespace python { + +// Custom wrapper for adiak::init +// The only difference is that it uses the behavior of the pybind11 +// type_caster above to decide whether to pass an MPI communicator +// to adiak::init +void init(mpi4py_comm comm) { +#if defined(ENABLE_MPI) + if (comm.m_comm == MPI_COMM_NULL) { + adiak::init(nullptr); + } else { + adiak::init(&comm.m_comm); + } +#else + adiak::init(comm.m_comm); +#endif +} + +// Custom version of adiak::value for types that provide .to_adiak() +// Use detection instead of inheritance to improve portability across compilers. +template ().to_adiak())> +bool value(std::string name, const T& value, int category = adiak_general, + std::string subcategory = "") { + (void)sizeof(U); // participates in overload resolution only if to_adiak() is valid + return adiak::value(name, value.to_adiak(), category, subcategory); +} + +template ().to_adiak())> +bool value_vec(std::string name, const std::vector& value, + int category = adiak_general, std::string subcategory = "") { + (void)sizeof(U); + using adiak_type = decltype(std::declval().to_adiak()); + std::vector adiak_vec; + std::transform(value.cbegin(), value.cend(), std::back_inserter(adiak_vec), + [](const T& v) { return v.to_adiak(); }); + return adiak::value(name, adiak_vec, category, subcategory); +} + +// Custom version of adiak::value for sets of types that provide .to_adiak() +template ().to_adiak())> +bool value_set(std::string name, const std::set& value, + int category = adiak_general, std::string subcategory = "") { + (void)sizeof(U); + using adiak_type = decltype(std::declval().to_adiak()); + std::set adiak_set; + std::transform(value.cbegin(), value.cend(), std::inserter(adiak_set, adiak_set.end()), + [](const T& v) { return v.to_adiak(); }); + return adiak::value(name, adiak_set, category, subcategory); +} + +// Macros to help generate the lambdas that pybind11 needs to bind the inline +// functions in adiak.hpp +// +// clang-format off +#define GENERATE_API_CALL_NO_ARGS(name) \ + [](){ return adiak::name(); } +#define GENERATE_API_CALL_ONE_ARG(name, arg_type) [](arg_type val){ return adiak::name(val); } +// clang-format on + +// Function to populate the Python module +void create_adiak_annotation_mod(py::module_ &mod) { + // Try to import mpi4py so that we can early abort if + // we can't properly create bindings + #if defined(ENABLE_MPI) + if (import_mpi4py() < 0) { + throw std::runtime_error( + "Cannot load mpi4py within the Adiak Python bindings"); + } + #endif + +#if defined(ENABLE_MPI) + // Robust overload that accepts mpi4py.MPI.Comm (or None) without needing a type_caster + mod.def("init", [](py::object comm_obj) { + if (comm_obj.is_none()) { + adiak::init(nullptr); + return; + } + // Validate it is an mpi4py.MPI.Comm and extract the MPI_Comm + PyObject* obj = comm_obj.ptr(); + if (!PyObject_TypeCheck(obj, &PyMPIComm_Type)) { + throw py::type_error("adiak.init expects mpi4py.MPI.Comm or None"); + } + MPI_Comm comm = *PyMPIComm_Get(obj); + adiak::init(&comm); +}); +#else + // Non-MPI build: accept anything and ignore (acts like None) + mod.def("init", [](py::object) { + adiak::init(nullptr); + }); +#endif + + mod.def("fini", GENERATE_API_CALL_NO_ARGS(fini)); + // Python has no concept of templates, so pybind11 requires + // us to specify the types for all template functions. This requires us to + // iterate over all acceptable value types for adiak::value. + // + // Because we need to use Python-compatible types everywhere, + // we use special wrappers for types not natively supported by pybind11. + // To bridge these types to Adiak, we use custom versions of "value". + // + // We map all these specializations to the Python "value" function. + // Pybind11 will do the work of selecting the correct version of this + // function for us. + mod.def("value", [](const std::string& name, int val, int category, const std::string& subcategory) { + return adiak::value(name, static_cast(val), category, subcategory); + }); + mod.def("value", [](const std::string& name, unsigned int val, int category, const std::string& subcategory) { + return adiak::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, double val, int category, const std::string& subcategory) { + return adiak::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::string& val, int category, const std::string& subcategory) { + return adiak::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const adiak::python::Timepoint& val, int category, const std::string& subcategory) { + return adiak::python::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const adiak::python::Date& val, int category, const std::string& subcategory) { + return adiak::python::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const adiak::python::Version& val, int category, const std::string& subcategory) { + return adiak::python::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const adiak::python::Path& val, int category, const std::string& subcategory) { + return adiak::python::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const adiak::python::CatStr& val, int category, const std::string& subcategory) { + return adiak::python::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const adiak::python::JsonStr& val, int category, const std::string& subcategory) { + return adiak::python::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + std::vector converted(val.begin(), val.end()); + return adiak::value(name, converted, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + return adiak::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + return adiak::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + return adiak::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + return adiak::python::value_vec(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + return adiak::python::value_vec(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + return adiak::python::value_vec(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + return adiak::python::value_vec(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + return adiak::python::value_vec(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::vector& val, int category, const std::string& subcategory) { + return adiak::python::value_vec(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + std::set converted(val.begin(), val.end()); + return adiak::value(name, converted, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + return adiak::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + return adiak::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + return adiak::value(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + return adiak::python::value_set(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + return adiak::python::value_set(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + return adiak::python::value_set(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + return adiak::python::value_set(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + return adiak::python::value_set(name, val, category, subcategory); + }); + mod.def("value", [](const std::string& name, const std::set& val, int category, const std::string& subcategory) { + return adiak::python::value_set(name, val, category, subcategory); + }); + // Bind all the other Adiak functions to custom lambda functions + // created by the GENERATE_API_CALL_* macros. These labmdas are + // used over the plain versions of these functions to avoid any issues + // with those functions being marked 'inline'. + mod.def("adiakversion", GENERATE_API_CALL_NO_ARGS(adiakversion)); + mod.def("user", GENERATE_API_CALL_NO_ARGS(user)); + mod.def("uid", GENERATE_API_CALL_NO_ARGS(uid)); + mod.def("launchdate", GENERATE_API_CALL_NO_ARGS(launchdate)); + mod.def("launchday", GENERATE_API_CALL_NO_ARGS(launchday)); + mod.def("executable", GENERATE_API_CALL_NO_ARGS(executable)); + mod.def("executablepath", GENERATE_API_CALL_NO_ARGS(executablepath)); + mod.def("workdir", GENERATE_API_CALL_NO_ARGS(workdir)); + mod.def("libraries", GENERATE_API_CALL_NO_ARGS(libraries)); + mod.def("cmdline", GENERATE_API_CALL_NO_ARGS(cmdline)); + mod.def("hostname", GENERATE_API_CALL_NO_ARGS(hostname)); + mod.def("clustername", GENERATE_API_CALL_NO_ARGS(clustername)); + mod.def("walltime", GENERATE_API_CALL_NO_ARGS(walltime)); + mod.def("systime", GENERATE_API_CALL_NO_ARGS(systime)); + mod.def("cputime", GENERATE_API_CALL_NO_ARGS(cputime)); + mod.def("jobsize", GENERATE_API_CALL_NO_ARGS(jobsize)); + mod.def("hostlist", GENERATE_API_CALL_NO_ARGS(hostlist)); + mod.def("numhosts", GENERATE_API_CALL_NO_ARGS(numhosts)); + mod.def("mpi_version", GENERATE_API_CALL_NO_ARGS(mpi_version)); + mod.def("mpi_library", GENERATE_API_CALL_NO_ARGS(mpi_library)); + mod.def("mpi_library_version", + GENERATE_API_CALL_NO_ARGS(mpi_library_version)); + mod.def("flush", GENERATE_API_CALL_ONE_ARG(flush, std::string)); + mod.def("clean", GENERATE_API_CALL_NO_ARGS(clean)); + mod.def("collect_all", GENERATE_API_CALL_NO_ARGS(collect_all)); +} + +} // namespace python +} // namespace adiak \ No newline at end of file diff --git a/src/interface/python/pyadiak.hpp b/src/interface/python/pyadiak.hpp new file mode 100644 index 0000000..de866dd --- /dev/null +++ b/src/interface/python/pyadiak.hpp @@ -0,0 +1,14 @@ +#ifndef ADIAK_INTERFACE_PYTHON_PYADIAK_HPP_ +#define ADIAK_INTERFACE_PYTHON_PYADIAK_HPP_ + +#include "types.hpp" + +namespace adiak { +namespace python { + +void create_adiak_annotation_mod(py::module_ &mod); + +} // namespace python +} // namespace adiak + +#endif /* ADIAK_INTERFACE_PYTHON_PYADIAK_HPP_ */ diff --git a/src/interface/python/pyadiak/__init__.py b/src/interface/python/pyadiak/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/interface/python/pyadiak/annotations.py b/src/interface/python/pyadiak/annotations.py new file mode 100644 index 0000000..e483bd6 --- /dev/null +++ b/src/interface/python/pyadiak/annotations.py @@ -0,0 +1,112 @@ +from datetime import date, datetime, time +from pathlib import Path as PyPath +from typing import Any +from warnings import warn + +from pyadiak.__pyadiak_impl.annotations import ( + adiakversion, + clean, + clustername, + cmdline, + collect_all, + cputime, + executable, + executablepath, + fini, + flush, + hostlist, + hostname, + jobsize, + launchdate, + launchday, + libraries, + mpi_library, + mpi_library_version, + mpi_version, + numhosts, + systime, + uid, + user, + walltime, + workdir, +) +from pyadiak.__pyadiak_impl.annotations import ( + init as cpp_init, +) +from pyadiak.__pyadiak_impl.annotations import ( + value as cpp_value, +) +from pyadiak.__pyadiak_impl.annotations import ( + fini as cpp_fini, +) +from pyadiak.types import * + +def fini(): + cpp_fini() + +def init(comm=None): + try: + from mpi4py.MPI import Comm + + if comm is not None and not isinstance(comm, Comm): + raise TypeError( + "The 'comm' argument to 'init' must be of type mpi4py.MPI.Comm" + ) + cpp_init(comm) + except ImportError: + if comm is not None: + warn( + "A value was passed to the 'comm' argument, but there is no way for it to be the correct type because mpi4py cannot be imported. If you want to pass an MPI communicator, install mpi4py first.", + RuntimeWarning, + ) + cpp_init(None) + + +def value(name: str, value: Any, category=Category.General, subcategory="") -> bool: + wrapped_value = value + if isinstance(value, date): + wrapped_value = Date(value) + elif isinstance(value, (time, datetime)): + wrapped_value = Timepoint(value) + elif isinstance(value, PyPath): + wrapped_value = Path(value) + if not isinstance(category, Category): + raise TypeError( + f"Invalid type ('{type(category)}') for 'category' parameter. Should be 'Category'" + ) + if not isinstance(subcategory, str): + raise TypeError( + f"Invalid type ('{type(subcategory)}') for 'subcategory' parameter. Should be 'str'" + ) + return cpp_value(name, wrapped_value, int(category), subcategory) + + +__all__ = [ + "init", + "fini", + "value", + "adiakversion", + "user", + "uid", + "launchdate", + "launchday", + "executable", + "executablepath", + "workdir", + "libraries", + "cmdline", + "hostname", + "clustername", + "walltime", + "systime", + "cputime", + "jobsize", + "hostlist", + "numhosts", + "mpi_version", + "mpi_library", + "mpi_library_version", + "flush", + "clean", + "collect_all", +] diff --git a/src/interface/python/pyadiak/tools.py b/src/interface/python/pyadiak/tools.py new file mode 100644 index 0000000..38f1029 --- /dev/null +++ b/src/interface/python/pyadiak/tools.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +from enum import IntEnum +from typing import Any, Callable, List, Tuple + +from pyadiak.__pyadiak_impl.tools import ( + AdiakDataType, + AdiakNumerical, + AdiakTypeRaw, +) +from pyadiak.__pyadiak_impl.tools import ( + get_nameval as cpp_get_nameval, +) +from pyadiak.__pyadiak_impl.tools import ( + list_namevals as cpp_list_namevals, +) +from pyadiak.__pyadiak_impl.tools import ( + register_cb as cpp_register_cb, +) +from pyadiak.__pyadiak_impl.tools import ( + type_to_string as cpp_type_to_string, +) +from pyadiak.types import Category + + +class Type(IntEnum): + Unset = AdiakTypeRaw.AdiakTypeUnset + Long = AdiakTypeRaw.AdiakTypeLong + ULong = AdiakTypeRaw.AdiakTypeULong + Int = AdiakTypeRaw.AdiakTypeInt + UInt = AdiakTypeRaw.AdiakTypeUInt + Double = AdiakTypeRaw.AdiakTypeDouble + Date = AdiakTypeRaw.AdiakTypeDate + Timeval = AdiakTypeRaw.AdiakTypeTimeval + Version = AdiakTypeRaw.AdiakTypeVersion + String = AdiakTypeRaw.AdiakTypeString + CatString = AdiakTypeRaw.AdiakTypeCatString + Path = AdiakTypeRaw.AdiakTypePath + Range = AdiakTypeRaw.AdiakTypeRange + Set = AdiakTypeRaw.AdiakTypeSet + List = AdiakTypeRaw.AdiakTypeList + Tuple = AdiakTypeRaw.AdiakTypeTuple + LongLong = AdiakTypeRaw.AdiakTypeLongLong + ULongLong = AdiakTypeRaw.AdiakTypeULongLong + JsonString = AdiakTypeRaw.AdiakTypeJsonString + + @staticmethod + def from_cpp_type(v: AdiakTypeRaw) -> Type: + return Type(int(v)) + + +class Numerical(IntEnum): + Unset = AdiakNumerical.AdiakNumericalUnset + Categorical = AdiakNumerical.AdiakNumericalCategorical + Ordinal = AdiakNumerical.AdiakNumericalOrdinal + Interval = AdiakNumerical.AdiakNumericalInterval + Rational = AdiakNumerical.AdiakNumericalRational + + @staticmethod + def from_cpp_numerical(v: AdiakNumerical) -> Numerical: + return Numerical(int(v)) + + +class DataType: + def __init__(self, raw_datatype: AdiakDataType): + self.dtype = raw_datatype + + def get_dtype(self) -> Type: + cpp_type = self.dtype.get_dtype() + return Type.from_cpp_type(cpp_type) + + def get_numerical(self) -> Numerical: + cpp_num = self.dtype.get_numerical() + return Numerical.from_cpp_numerical(cpp_num) + + def get_num_elems(self) -> int: + return self.dtype.get_num_elems() + + def get_num_subtypes_in_container(self) -> int: + return self.dtype.get_num_subtypes_in_container() + + def is_reference(self) -> bool: + return self.dtype.is_reference() + + def get_subtypes(self) -> List[DataType]: + cpp_types = self.get_subtypes() + return [DataType(ct) for ct in cpp_types] + + +AdiakToolCallback = Callable[str, Category, str, Any, DataType] + + +def __wrap_cb_in_caster_func(cb: AdiakToolCallback) -> Callable: + def __wrapper_func( + name: str, cat: int, subcat: str, val: Any, dtype: AdiakDataType + ): + local_category = Category(cat) + local_dtype = DataType(dtype) + cb(str, local_category, subcat, val, local_dtype) + + return __wrapper_func + + +def register_cb( + adiak_version: int, + cat: Category, + callback: AdiakToolCallback, + report_on_all_ranks: bool, +): + if not isinstance(adiak_version, int): + raise TypeError("The 'adiak_version' parameter must be an int") + if not isinstance(cat, Category): + raise TypeError("The 'cat' parameter must be a 'pyadiak.types.Category'") + if not isinstance(callback, AdiakToolCallback): + raise TypeError( + "The 'callback' parameter must be a 'pyadiak.tools.AdiakToolCallback'" + ) + wrapped_cb = __wrap_cb_in_caster_func(callback) + cpp_register_cb(adiak_version, int(cat), wrapped_cb, report_on_all_ranks) + + +def list_namevals(adiak_version: int, cat: Category, callback: AdiakToolCallback): + if not isinstance(adiak_version, int): + raise TypeError("The 'adiak_version' parameter must be an int") + if not isinstance(cat, Category): + raise TypeError("The 'cat' parameter must be a 'pyadiak.types.Category'") + if not isinstance(callback, AdiakToolCallback): + raise TypeError( + "The 'callback' parameter must be a 'pyadiak.tools.AdiakToolCallback'" + ) + wrapped_cb = __wrap_cb_in_caster_func(callback) + cpp_list_namevals(adiak_version, int(cat), wrapped_cb) + + +def type_to_string(dtype: DataType, long_form=False) -> str: + if not isinstance(dtype, DataType): + raise TypeError("The 'dtype' parameter must be a 'pyadiak.tools.DataType'") + return cpp_type_to_string(dtype.dtype, long_form) + + +def get_nameval(name: str) -> Tuple[DataType, Any, Category, str]: + cpp_tup = cpp_get_nameval(name) + return (DataType(cpp_tup[0]), cpp_tup[1], Category(cpp_tup[2]), cpp_tup[3]) + + +__all__ = [ + "Type", + "Numerical", + "DataType", + "AdiakToolCallback", + "register_cb", + "list_namevals", + "type_to_string", + "get_nameval", +] diff --git a/src/interface/python/pyadiak/types.py b/src/interface/python/pyadiak/types.py new file mode 100644 index 0000000..2525ca0 --- /dev/null +++ b/src/interface/python/pyadiak/types.py @@ -0,0 +1,26 @@ +from enum import IntEnum + +from pyadiak.__pyadiak_impl.types import ( + ADIAK_CATEGORY_ALL, + ADIAK_CATEGORY_CONTROL, + ADIAK_CATEGORY_GENERAL, + ADIAK_CATEGORY_PERFORMANCE, + ADIAK_CATEGORY_UNSET, + CatStr, + Date, + JsonStr, + Path, + Timepoint, + Version, +) + + +class Category(IntEnum): + Unset = ADIAK_CATEGORY_UNSET + All = ADIAK_CATEGORY_ALL + General = ADIAK_CATEGORY_GENERAL + Performance = ADIAK_CATEGORY_PERFORMANCE + Control = ADIAK_CATEGORY_CONTROL + + +__all__ = ["CatStr", "Date", "JsonStr", "Path", "Timepoint", "Version", "Category"] diff --git a/src/interface/python/pyadiak_tool.cpp b/src/interface/python/pyadiak_tool.cpp new file mode 100644 index 0000000..13586c3 --- /dev/null +++ b/src/interface/python/pyadiak_tool.cpp @@ -0,0 +1,278 @@ +#include "pyadiak_tool.hpp" +#include "adiak_tool.h" +#include "types.hpp" + +#include +#include +#include +#include +#include + +namespace adiak { +namespace python { + +class PyadiakDataType { +public: + PyadiakDataType() = default; + + PyadiakDataType(adiak_datatype_t *c_type) + : m_adiak_dtype_ptr(c_type), m_dtype(c_type->dtype), + m_numerical(c_type->numerical), m_num_elems(c_type->num_elements), + m_num_subtypes(c_type->num_subtypes), m_subtypes(), + m_is_reference(c_type->is_reference == 0 ? false : true), + m_num_ref_elements(c_type->num_ref_elements) { + for (int i = 0; i < m_num_subtypes; i++) { + m_subtypes.push_back(PyadiakDataType(c_type->subtype[i])); + } + } + + adiak_type_t get_dtype() const { return m_dtype; } + + adiak_numerical_t get_numerical() const { return m_numerical; } + + int get_num_elems() const { + if (m_is_reference) { + return m_num_ref_elements; + } else { + return m_num_elems; + } + } + + int get_num_subtypes_in_container() const { return m_num_subtypes; } + + bool is_reference() const { return m_is_reference; } + + std::vector get_subtypes() const { return m_subtypes; } + + adiak_datatype_t *get_adiak_dtype() { return m_adiak_dtype_ptr; } + +private: + adiak_datatype_t *m_adiak_dtype_ptr; + adiak_type_t m_dtype; + adiak_numerical_t m_numerical; + int m_num_elems; + int m_num_subtypes; + std::vector m_subtypes; + bool m_is_reference; + int m_num_ref_elements; +}; + +py::object convert_adiak_value_to_python(adiak_value_t *val, + adiak_datatype_t *type) { + // TODO figure out if I need to do anything else to cast custom types to + // py::object + switch (type->dtype) { + case adiak_long: + return py::cast(val->v_long); + break; + case adiak_ulong: + return py::cast((unsigned long)val->v_long); + break; + case adiak_int: + return py::cast(val->v_int); + break; + case adiak_uint: + return py::cast((unsigned int)val->v_int); + break; + case adiak_double: + return py::cast(val->v_double); + case adiak_date: { + std::chrono::seconds chrono_seconds(val->v_long); + return py::cast(adiak::python::Date( + std::chrono::system_clock::time_point(chrono_seconds))); + break; + } + case adiak_timeval: { + struct timeval* tv = static_cast(val->v_ptr); + uint64_t num_microseconds = tv->tv_sec * 1000000 + tv->tv_usec; + std::chrono::microseconds chrono_usec(num_microseconds); + return py::cast(adiak::python::Timepoint( + std::chrono::system_clock::time_point(chrono_usec))); + break; + } + case adiak_version: + return py::cast(adiak::python::Version(std::string(static_cast(val->v_ptr)))); + break; + case adiak_string: + return py::cast(std::string(static_cast(val->v_ptr))); + break; + case adiak_catstring: + return py::cast(adiak::python::CatStr(std::string(static_cast(val->v_ptr)))); + break; + case adiak_jsonstring: + return py::cast(adiak::python::JsonStr(std::string(static_cast(val->v_ptr)))); + break; + case adiak_path: + return py::cast(adiak::python::Path(std::string(static_cast(val->v_ptr)))); + break; + case adiak_range: { + if (type->num_subtypes != 2) { + throw std::runtime_error( + "Mismatch in Adiak between 'range' type and number of subtypes"); + } + std::tuple range_tuple = std::make_tuple( + convert_adiak_value_to_python(&(val->v_subval[0]), type->subtype[0]), + convert_adiak_value_to_python(&(val->v_subval[1]), type->subtype[1])); + return py::cast(range_tuple); + break; + } + case adiak_set: { + if (type->num_subtypes != 1) { + throw std::runtime_error( + "Mismatch in Adiak between 'set' type and number of subtypes"); + } + adiak_datatype_t *subtype = type->subtype[0]; + int num_elems = + type->is_reference != 0 ? type->num_ref_elements : type->num_elements; + std::set set_val; + for (int i = 0; i < num_elems; i++) { + set_val.insert( + convert_adiak_value_to_python(&(val->v_subval[i]), subtype)); + } + return py::cast(set_val); + break; + } + case adiak_list: { + if (type->num_subtypes != 1) { + throw std::runtime_error( + "Mismatch in Adiak between 'list' type and number of subtypes"); + } + adiak_datatype_t *subtype = type->subtype[0]; + int num_elems = + type->is_reference != 0 ? type->num_ref_elements : type->num_elements; + std::vector list_val; + for (int i = 0; i < num_elems; i++) { + list_val.push_back( + convert_adiak_value_to_python(&(val->v_subval[i]), subtype)); + } + return py::cast(list_val); + break; + } + case adiak_tuple: { + int num_subtypes = type->num_subtypes; + int num_elems = + type->is_reference != 0 ? type->num_ref_elements : type->num_elements; + if (num_subtypes != num_elems) { + throw std::runtime_error( + "Mismatch in Adiak between 'tuple' type and number of subtypes"); + } + std::vector list_val; + for (int i = 0; i < num_elems; i++) { + list_val.push_back( + convert_adiak_value_to_python(&(val->v_subval[i]), type->subtype[i])); + } + py::tuple tup_val = py::cast(list_val); + return tup_val; + break; + } + case adiak_longlong: + return py::cast(val->v_longlong); + break; + case adiak_ulonglong: + return py::cast(val->v_longlong); + break; + default: + throw std::runtime_error("Invalid datatype obtained from Adiak"); + } +} + +void pyadiak_tool_nameval_cb(const char *name, int category, + const char *subcategory, adiak_value_t *value, + adiak_datatype_t *t, void *py_cb) { + py::handle nv_cb((PyObject *)py_cb); + PyadiakDataType py_dtype(t); + py::object py_val = convert_adiak_value_to_python(value, t); + nv_cb(name, category, subcategory, py_val, py_dtype); +} + +void pyadiak_register_cb(int adiak_version, int category, py::function nv_cb, + bool report_on_all_ranks) { + PyObject *nv_cb_obj = nv_cb.ptr(); + Py_INCREF(nv_cb_obj); + adiak_register_cb(adiak_version, category, pyadiak_tool_nameval_cb, + report_on_all_ranks ? 1 : 0, (void *)nv_cb_obj); +} + +void pyadiak_list_namevals(int adiak_version, int category, + py::function nv_cb) { + PyObject *nv_cb_obj = nv_cb.ptr(); + Py_INCREF(nv_cb_obj); + adiak_list_namevals(adiak_version, category, pyadiak_tool_nameval_cb, + (void *)nv_cb_obj); +} + +std::string pyadiak_type_to_string(PyadiakDataType &type, bool long_form) { + adiak_datatype_t *adiak_dtype = type.get_adiak_dtype(); + if (adiak_dtype == nullptr) { + throw std::runtime_error("Provided DataType is invalid"); + } + char *alloced_str = adiak_type_to_string(adiak_dtype, long_form ? 1 : 0); + if (alloced_str == nullptr) { + throw std::runtime_error("Failed to get string for type"); + } + std::string py_str(alloced_str); + free(alloced_str); + return py_str; +} + +std::tuple +pyadiak_get_nameval(const char *name) { + adiak_datatype_t *dtype = nullptr; + adiak_value_t *val = nullptr; + int category = 0; + const char *subcat = nullptr; + int rc = adiak_get_nameval(name, &dtype, &val, &category, &subcat); + if (rc != 0) { + throw std::runtime_error("Failed to get value for name"); + } + py::object py_val = convert_adiak_value_to_python(val, dtype); + PyadiakDataType py_dtype(dtype); + return std::make_tuple(py_dtype, py_val, category, subcat); +} + +void create_adiak_tool_mod(py::module_ &mod) { + py::enum_(mod, "AdiakTypeRaw") + .value("AdiakTypeUnset", adiak_type_unset) + .value("AdiakTypeLong", adiak_long) + .value("AdiakTypeULong", adiak_ulong) + .value("AdiakTypeInt", adiak_int) + .value("AdiakTypeUInt", adiak_uint) + .value("AdiakTypeDouble", adiak_double) + .value("AdiakTypeDate", adiak_date) + .value("AdiakTypeTimeval", adiak_timeval) + .value("AdiakTypeVersion", adiak_version) + .value("AdiakTypeString", adiak_string) + .value("AdiakTypeCatString", adiak_catstring) + .value("AdiakTypePath", adiak_path) + .value("AdiakTypeRange", adiak_range) + .value("AdiakTypeSet", adiak_set) + .value("AdiakTypeList", adiak_list) + .value("AdiakTypeTuple", adiak_tuple) + .value("AdiakTypeLongLong", adiak_longlong) + .value("AdiakTypeULongLong", adiak_ulonglong) + .value("AdiakTypeJsonString", adiak_jsonstring) + .export_values(); + py::enum_(mod, "AdiakNumerical") + .value("AdiakNumericalUnset", adiak_numerical_unset) + .value("AdiakNumericalCategorical", adiak_categorical) + .value("AdiakNumericalOrdinal", adiak_ordinal) + .value("AdiakNumericalInterval", adiak_interval) + .value("AdiakNumericalRational", adiak_rational) + .export_values(); + py::class_(mod, "AdiakDataType") + .def(py::init<>()) + .def("get_dtype", &PyadiakDataType::get_dtype) + .def("get_numerical", &PyadiakDataType::get_numerical) + .def("get_num_elems", &PyadiakDataType::get_num_elems) + .def("get_num_subtypes_in_container", + &PyadiakDataType::get_num_subtypes_in_container) + .def("is_reference", &PyadiakDataType::is_reference) + .def("get_subtypes", &PyadiakDataType::get_subtypes); + mod.def("register_cb", &pyadiak_register_cb); + mod.def("list_namevals", &pyadiak_list_namevals); + mod.def("type_to_string", &pyadiak_type_to_string); + mod.def("get_nameval", &pyadiak_get_nameval); +} + +} // namespace python +} // namespace adiak diff --git a/src/interface/python/pyadiak_tool.hpp b/src/interface/python/pyadiak_tool.hpp new file mode 100644 index 0000000..fc58eac --- /dev/null +++ b/src/interface/python/pyadiak_tool.hpp @@ -0,0 +1,14 @@ +#ifndef ADIAK_INTERFACE_PYTHON_PYADIAK_TOOL_HPP_ +#define ADIAK_INTERFACE_PYTHON_PYADIAK_TOOL_HPP_ + +#include "common.hpp" + +namespace adiak { +namespace python { + +void create_adiak_tool_mod(py::module_ &mod); + +} +} // namespace adiak + +#endif /* ADIAK_INTERFACE_PYTHON_PYADIAK_TOOL_HPP_*/ \ No newline at end of file diff --git a/src/interface/python/types.cpp b/src/interface/python/types.cpp new file mode 100644 index 0000000..737f1eb --- /dev/null +++ b/src/interface/python/types.cpp @@ -0,0 +1,77 @@ +#include "types.hpp" +#include "adiak.hpp" + +namespace adiak { +namespace python { + +Timepoint::Timepoint(std::chrono::system_clock::time_point time) + : DataContainer( + time) {} + +struct timeval *Timepoint::to_adiak() const { + auto time_since_epoch = std::chrono::duration_cast( + m_v.time_since_epoch()); + m_time_for_adiak.tv_sec = time_since_epoch.count() / 1000000; + m_time_for_adiak.tv_usec = time_since_epoch.count() % 1000000; + return &m_time_for_adiak; +} + +Date::Date(std::chrono::system_clock::time_point time) + : DataContainer(time) {} + +adiak::date Date::to_adiak() const { + auto time_since_epoch = std::chrono::duration_cast( + m_v.time_since_epoch()); + return adiak::date(time_since_epoch.count()); +} + +Version::Version(const std::string &ver) + : DataContainer(ver) {} + +adiak::version Version::to_adiak() const { return adiak::version(m_v); } + +Path::Path(const std::string &p) : DataContainer(p) {} + +adiak::path Path::to_adiak() const { return adiak::path(m_v); } + +CatStr::CatStr(const std::string &cs) + : DataContainer(cs) {} + +adiak::catstring CatStr::to_adiak() const { return adiak::catstring(m_v); } + +JsonStr::JsonStr(const std::string &js) + : DataContainer(js) {} + +adiak::jsonstring JsonStr::to_adiak() const { return adiak::jsonstring(m_v); } + +void create_adiak_types_mod(py::module_ &mod) { + mod.attr("ADIAK_CATEGORY_UNSET") = py::int_(adiak_category_unset); + mod.attr("ADIAK_CATEGORY_ALL") = py::int_(adiak_category_all); + mod.attr("ADIAK_CATEGORY_GENERAL") = py::int_(adiak_general); + mod.attr("ADIAK_CATEGORY_PERFORMANCE") = py::int_(adiak_performance); + mod.attr("ADIAK_CATEGORY_CONTROL") = py::int_(adiak_control); + // Bind the custom data containers to Python. + // Note that we don't wrap the DataContainer base class or the 'to_adiak' + // methods because we don't need those exposed to Python. + py::class_(mod, "Timepoint") + .def(py::init()) + .def("to_python", &adiak::python::Timepoint::to_python); + py::class_(mod, "Date") + .def(py::init()) + .def("to_python", &adiak::python::Date::to_python); + py::class_(mod, "Version") + .def(py::init()) + .def("to_python", &adiak::python::Version::to_python); + py::class_(mod, "Path") + .def(py::init()) + .def("to_python", &adiak::python::Path::to_python); + py::class_(mod, "CatStr") + .def(py::init()) + .def("to_python", &adiak::python::CatStr::to_python); + py::class_(mod, "JsonStr") + .def(py::init()) + .def("to_python", &adiak::python::JsonStr::to_python); +} + +} // namespace python +} // namespace adiak \ No newline at end of file diff --git a/src/interface/python/types.hpp b/src/interface/python/types.hpp new file mode 100644 index 0000000..634c315 --- /dev/null +++ b/src/interface/python/types.hpp @@ -0,0 +1,142 @@ +#ifndef ADIAK_INTERFACE_PYTHON_TYPES_HPP_ +#define ADIAK_INTERFACE_PYTHON_TYPES_HPP_ + +#include "common.hpp" +#include + +#include +#include + +namespace adiak { + inline bool operator<(const date& a, const date& b) { + return a.v < b.v; + } + inline bool operator<(const version& a, const version& b) { + return a.v < b.v; + } + inline bool operator<(const path& a, const path& b) { + return a.v < b.v; + } + inline bool operator<(const catstring& a, const catstring& b) { + return a.v < b.v; + } + inline bool operator<(const jsonstring& a, const jsonstring& b) { + return a.v < b.v; + } +namespace python { + +// The 'is_templated_base_of' type trait is derived from the following +// StackOverflow post: +// https://stackoverflow.com/a/49163138 +template