Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
a198f3d
proof-of-concept Python support using converter nodes
wlav Sep 23, 2025
947f842
Merge branch 'main' into python-support
wlav Oct 27, 2025
4541abd
add wrapper codes to the library
wlav Oct 27, 2025
1a86bec
code cleanup (clang-format) and simplifications
wlav Oct 27, 2025
1d2936a
clang-format fixes and disable it for the Python Type definitions
wlav Oct 27, 2025
61e895d
clang-format fixes of the header files
wlav Oct 27, 2025
7fcb88b
extend supported types to a couple more builins and retrieve informat…
wlav Oct 30, 2025
6343575
fix cmake formatting
wlav Oct 30, 2025
86f5b30
move py:phlex property underneath the HAS_CPPYY block
wlav Oct 30, 2025
0e5c8f9
add missing registration helper module
wlav Oct 30, 2025
be99c00
Python exception -> std::runtime_error
wlav Nov 3, 2025
10b8af1
observer to check adder algorithm output
wlav Nov 3, 2025
bbf3d1b
move initial GIL release later and make sure it only happens once
wlav Nov 3, 2025
136da63
a function with no configured output becomes an observer
wlav Nov 3, 2025
c4a80c9
remove spurious printout
wlav Nov 3, 2025
135df83
change configuration lookup failures into Python exceptions
wlav Nov 3, 2025
7ff5946
make sure that the adder sum result is non-zero
wlav Nov 3, 2025
6554826
improve testing by adding an observer that asserts the algorithm output
wlav Nov 3, 2025
10e8040
move the GIL RAII to the common wrapper header file for reuse
wlav Nov 3, 2025
f108c2d
Merge branch 'main' into python-support
wlav Nov 4, 2025
08c1ec4
update to new registration API
wlav Nov 4, 2025
1aa6a57
fix vector indexing error if no outputs provided
wlav Nov 4, 2025
a3075a7
add error helper to pymodule.so
wlav Nov 4, 2025
21cf613
fix cmake formatting to conform to the rules
wlav Nov 4, 2025
5173c8c
add a way to pass configuration to python modules
wlav Nov 5, 2025
6714ef0
support callable instances
wlav Nov 5, 2025
59c13ae
trivial demonstrator of std::vector<int> input to a Python algorithm
wlav Nov 5, 2025
242c856
add vector test files
wlav Nov 6, 2025
993d42d
make python module registration resemble the C++ one more closely
wlav Nov 6, 2025
43a090e
simplify life-times by letting the node take a reference to the regis…
wlav Nov 6, 2025
f6586e3
rename "pymodule" property to "pyplugin"
wlav Nov 6, 2025
bbab231
formatting fixes (clang-format getting confused by PyObject_HEAD)
wlav Nov 13, 2025
0d16fea
vector support goes through Numpy views for now, so add that depedency
wlav Nov 13, 2025
5e36597
add a lifeline object to tie life times of handles and views onto them
wlav Nov 13, 2025
818051c
use numpy views instead of array copies to handle std::vector
wlav Nov 13, 2025
6360b8f
explicitly collect tests based on activation before setting properties
wlav Nov 13, 2025
c214ec6
protect all of import_numpy to prevent an "unused variable" warning
wlav Nov 13, 2025
9ae87a9
clang format remove empty line
wlav Nov 13, 2025
4d0a5a4
another hiding of unused variables attempt to make coverage happy
wlav Nov 13, 2025
7bcb611
one more attempt to compile w/o errors if numpy isn't installed
wlav Nov 13, 2025
93161f5
add additional vector types and use numpy.typing in the annotations
wlav Dec 3, 2025
c4a8651
move python support module from test to plugins directory
wlav Dec 4, 2025
73fc25e
split up the generic register into transform and observe registration
wlav Dec 8, 2025
c80ce98
fix typos and errors/missing comments
wlav Dec 8, 2025
3d04386
clang-format fixes
wlav Dec 8, 2025
6731f34
enable the "python" subdirectory in "plugins"
wlav Dec 8, 2025
268438b
simplify retrievel of configuration values in python by using the dec…
wlav Dec 12, 2025
014ec6e
expose kind() for the held json values to simplify conversion to Python
wlav Dec 12, 2025
a78d905
Range of cosmetic fixes.
wlav Dec 15, 2025
c057274
clang-format fixes
wlav Dec 15, 2025
a91c716
return false in initalize on failure to initialize custom types
wlav Dec 15, 2025
9761843
add an internal cache for python-side configuration
wlav Dec 15, 2025
0fd6947
rename kind -> prototype_internal_kind
wlav Dec 15, 2025
5aa3f34
clang format fixes
wlav Dec 15, 2025
951de8c
allow an annotation of `None` as "no output"
wlav Dec 16, 2025
a6c10aa
add a test to check mapping of configuration types
wlav Dec 16, 2025
20e6968
uniquely tie converter nodes to algorithms for lifetime management
wlav Dec 17, 2025
a7c42de
Merge branch 'main' into python-support
wlav Dec 17, 2025
1504fe7
cleanup
wlav Dec 17, 2025
e3b18f6
upgrade to new registration interface (note: DOES NOT WORK)
wlav Dec 17, 2025
9b762a3
upgrade to new interface
wlav Dec 17, 2025
c888e1f
clang-format fix
wlav Dec 17, 2025
3b8fbe4
cmake formatting fixes
wlav Dec 17, 2025
44601a2
add module-level docstrings to all python modules
wlav Dec 17, 2025
0f12e69
fix Phlex plugin search path
wlav Dec 17, 2025
f3a5828
add support for numpy -> std::vector returns (through copying)
wlav Dec 17, 2025
16771f2
use the "job" layer as a default for now until a python representatio…
wlav Dec 17, 2025
5911ff1
ruff fixes
wlav Dec 17, 2025
b341ebc
fix formatting for the case where numpy isn't installed
wlav Dec 17, 2025
b90701c
add a way for testing failures/error paths for code coverage
wlav Dec 17, 2025
976cdd4
Catch exceptions in main to ensure graceful exit and coverage data ge…
greenc-FNAL Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion phlex/app/phlex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,13 @@
if (not vm["parallel"].defaulted()) {
max_concurrency = vm["parallel"].as<int>();
}
phlex::experimental::run(configurations, max_concurrency);
try {
phlex::experimental::run(configurations, max_concurrency);
} catch (std::exception const& e) {
std::cerr << e.what() << '\n';
return 1;
} catch (...) {
std::cerr << "Unknown exception caught.\n";
return 1;

Check warning on line 102 in phlex/app/phlex.cpp

View check run for this annotation

Codecov / codecov/patch

phlex/app/phlex.cpp#L101-L102

Added lines #L101 - L102 were not covered by tests
}
}
20 changes: 20 additions & 0 deletions phlex/configuration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <optional>
#include <string>
#include <utility>
#include <vector>

namespace phlex::experimental {
Expand Down Expand Up @@ -48,6 +49,25 @@ namespace phlex::experimental {

std::vector<std::string> keys() const;

// Internal function for prototype purposes; do not use as this will change.
std::pair<boost::json::kind, bool> prototype_internal_kind(std::string const& key) const
{
auto const& value = config_.at(key); // may throw

auto k = value.kind();
bool is_array = k == boost::json::kind::array;

if (is_array) {
// The current configuration interface only supports homogenous containers,
// thus checking only the first element suffices. (This assumes arrays are
// not nested, which is fine for now.)
boost::json::array const& arr = value.as_array();
k = arr.empty() ? boost::json::kind::null : arr[0].kind();
}

return std::make_pair(k, is_array);
}

private:
boost::json::object config_;
};
Expand Down
5 changes: 5 additions & 0 deletions plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Phlex provided core plugins

# plugin for running Python algorithms in phlex
add_subdirectory(python)

add_library(layer_generator layer_generator.cpp)
target_link_libraries(layer_generator PRIVATE phlex::core)

Expand Down
94 changes: 94 additions & 0 deletions plugins/python/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
find_package(
Python 3.12
COMPONENTS Interpreter Development
QUIET
)

if(Python_FOUND)

# Verify installation of necessary python modules for specific tests

function(check_python_module_version MODULE_NAME MIN_VERSION OUT_VAR)
execute_process(
COMMAND
${Python_EXECUTABLE} -c "import sys
try:
import ${MODULE_NAME}
from packaging.version import parse as parse_version
installed_version = getattr(${MODULE_NAME}, '__version__', None)
if parse_version(installed_version) >= parse_version('${MIN_VERSION}'):
sys.exit(0)
else:
sys.exit(2) # Version too low
except ImportError:
sys.exit(1)"
RESULT_VARIABLE _module_check_result
)

if(_module_check_result EQUAL 0)
set(${OUT_VAR}
TRUE
PARENT_SCOPE
)
elseif(_module_check_result EQUAL 1)
set(${OUT_VAR}
FALSE
PARENT_SCOPE
) # silent b/c common
elseif(_module_check_result EQUAL 2)
message(
WARNING
"Python module '${MODULE_NAME}' found but version too low (min required: ${MIN_VERSION})."
)
set(${OUT_VAR}
FALSE
PARENT_SCOPE
)
else()
message(
WARNING "Unknown error while checking Python module '${MODULE_NAME}'."
)
set(${OUT_VAR}
FALSE
PARENT_SCOPE
)
endif()
endfunction()

check_python_module_version("numpy" "2.0.0" HAS_NUMPY)

# Phlex module to run Python algorithms
add_library(
pymodule MODULE src/pymodule.cpp src/modulewrap.cpp src/configwrap.cpp
src/lifelinewrap.cpp src/errorwrap.cpp
)
include_directories(pymodule, ${Python_INCLUDE_DIRS})
target_link_libraries(
pymodule
PRIVATE phlex::module ${Python_LIBRARIES}
PUBLIC Python::Python
)

# numpy support if installed
if(HAS_NUMPY)

# locate numpy's header directory
execute_process(
COMMAND "${Python_EXECUTABLE}" -c
"import numpy; print(numpy.get_include(), end='')"
RESULT_VARIABLE NUMPY_RESULT
OUTPUT_VARIABLE NUMPY_INCLUDE_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)

if(NUMPY_RESULT EQUAL 0)
include_directories(pymodule PRIVATE ${NUMPY_INCLUDE_DIR})
target_compile_definitions(
pymodule PRIVATE PHLEX_HAVE_NUMPY=1
NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION
)
endif()

endif()

endif() # Python available
213 changes: 213 additions & 0 deletions plugins/python/src/configwrap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
#include <cstdint>
#include <string>

#include "phlex/configuration.hpp"
#include "wrap.hpp"

using namespace phlex::experimental;

// Create a dict-like access to the configuration from Python.
// clang-format off
struct phlex::experimental::py_config_map {
PyObject_HEAD
phlex::experimental::configuration const* ph_config;
PyObject* ph_config_cache;
};
// clang-format on

PyObject* phlex::experimental::wrap_configuration(configuration const* config)
{
if (!config) {
PyErr_SetString(PyExc_ValueError, "provided configuration is null");
return nullptr;

Check warning on line 22 in plugins/python/src/configwrap.cpp

View check run for this annotation

Codecov / codecov/patch

plugins/python/src/configwrap.cpp#L21-L22

Added lines #L21 - L22 were not covered by tests
}

py_config_map* pyconfig =
(py_config_map*)PhlexConfig_Type.tp_new(&PhlexConfig_Type, nullptr, nullptr);

pyconfig->ph_config = config;

return (PyObject*)pyconfig;
}

//= CPyCppyy low level view construction/destruction =========================
static py_config_map* pcm_new(PyTypeObject* subtype, PyObject*, PyObject*)
{
py_config_map* pcm = (py_config_map*)subtype->tp_alloc(subtype, 0);
if (!pcm)
return nullptr;

Check warning on line 38 in plugins/python/src/configwrap.cpp

View check run for this annotation

Codecov / codecov/patch

plugins/python/src/configwrap.cpp#L38

Added line #L38 was not covered by tests

pcm->ph_config_cache = PyDict_New();

return pcm;
}

static void pcm_dealloc(py_config_map* pcm)
{
Py_DECREF(pcm->ph_config_cache);
Py_TYPE(pcm)->tp_free((PyObject*)pcm);
}

static PyObject* pcm_subscript(py_config_map* pycmap, PyObject* pykey)
{
// Retrieve a named configuration setting.
//
// Configuration should have a single in-memory representation, which is why
// the current approach retrieves it from the equivalent C++ object, ie. after
// the JSON input has been parsed, even as there are Python JSON parsers.
//
// pykey: the lookup key to retrieve the configuration value

if (!PyUnicode_Check(pykey)) {
PyErr_SetString(PyExc_TypeError, "__getitem__ expects a string key");
return nullptr;

Check warning on line 63 in plugins/python/src/configwrap.cpp

View check run for this annotation

Codecov / codecov/patch

plugins/python/src/configwrap.cpp#L62-L63

Added lines #L62 - L63 were not covered by tests
}

// cached lookup
PyObject* pyvalue = PyDict_GetItem(pycmap->ph_config_cache, pykey);
if (pyvalue) {
Py_INCREF(pyvalue);
return pyvalue;
}
PyErr_Clear();

std::string ckey = PyUnicode_AsUTF8(pykey);

try {
auto k = pycmap->ph_config->prototype_internal_kind(ckey);
if (k.second /* is array */) {
if (k.first == boost::json::kind::bool_) {
auto const& cvalue = pycmap->ph_config->get<std::vector<bool>>(ckey);
pyvalue = PyTuple_New(cvalue.size());
for (Py_ssize_t i = 0; i < (Py_ssize_t)cvalue.size(); ++i) {
PyObject* item = PyLong_FromLong((long)cvalue[i]);
PyTuple_SetItem(pyvalue, i, item);
}
} else if (k.first == boost::json::kind::int64) {
auto const& cvalue = pycmap->ph_config->get<std::vector<std::int64_t>>(ckey);
pyvalue = PyTuple_New(cvalue.size());
for (Py_ssize_t i = 0; i < (Py_ssize_t)cvalue.size(); ++i) {
PyObject* item = PyLong_FromLong(cvalue[i]);
PyTuple_SetItem(pyvalue, i, item);
}
} else if (k.first == boost::json::kind::uint64) {
auto const& cvalue = pycmap->ph_config->get<std::vector<std::uint64_t>>(ckey);
pyvalue = PyTuple_New(cvalue.size());

Check warning on line 95 in plugins/python/src/configwrap.cpp

View check run for this annotation

Codecov / codecov/patch

plugins/python/src/configwrap.cpp#L94-L95

Added lines #L94 - L95 were not covered by tests
for (Py_ssize_t i = 0; i < (Py_ssize_t)cvalue.size(); ++i) {
PyObject* item = PyLong_FromUnsignedLong(cvalue[i]);
PyTuple_SetItem(pyvalue, i, item);

Check warning on line 98 in plugins/python/src/configwrap.cpp

View check run for this annotation

Codecov / codecov/patch

plugins/python/src/configwrap.cpp#L97-L98

Added lines #L97 - L98 were not covered by tests
}
} else if (k.first == boost::json::kind::double_) {
auto const& cvalue = pycmap->ph_config->get<std::vector<double>>(ckey);
pyvalue = PyTuple_New(cvalue.size());
for (Py_ssize_t i = 0; i < (Py_ssize_t)cvalue.size(); ++i) {
PyObject* item = PyFloat_FromDouble(cvalue[i]);
PyTuple_SetItem(pyvalue, i, item);
}
} else if (k.first == boost::json::kind::string) {
auto const& cvalue = pycmap->ph_config->get<std::vector<std::string>>(ckey);
pyvalue = PyTuple_New(cvalue.size());
for (Py_ssize_t i = 0; i < (Py_ssize_t)cvalue.size(); ++i) {
PyObject* item = PyUnicode_FromStringAndSize(cvalue[i].c_str(), cvalue[i].size());
PyTuple_SetItem(pyvalue, i, item);
}
}
} else {
if (k.first == boost::json::kind::bool_) {
auto cvalue = pycmap->ph_config->get<bool>(ckey);
pyvalue = PyBool_FromLong((long)cvalue);
} else if (k.first == boost::json::kind::int64) {
auto cvalue = pycmap->ph_config->get<std::int64_t>(ckey);
pyvalue = PyLong_FromLong(cvalue);
} else if (k.first == boost::json::kind::uint64) {
auto cvalue = pycmap->ph_config->get<std::uint64_t>(ckey);
pyvalue = PyLong_FromUnsignedLong(cvalue);

Check warning on line 124 in plugins/python/src/configwrap.cpp

View check run for this annotation

Codecov / codecov/patch

plugins/python/src/configwrap.cpp#L123-L124

Added lines #L123 - L124 were not covered by tests
} else if (k.first == boost::json::kind::double_) {
auto cvalue = pycmap->ph_config->get<double>(ckey);
pyvalue = PyFloat_FromDouble(cvalue);
} else if (k.first == boost::json::kind::string) {
auto const& cvalue = pycmap->ph_config->get<std::string>(ckey);
pyvalue = PyUnicode_FromStringAndSize(cvalue.c_str(), cvalue.size());
}
}
} catch (std::runtime_error const&) {
PyErr_Format(PyExc_TypeError, "property \"%s\" does not exist", ckey.c_str());
}

// cache if found
if (pyvalue) {
PyDict_SetItem(pycmap->ph_config_cache, pykey, pyvalue);
}

return pyvalue;
}

static PyMappingMethods pcm_as_mapping = {nullptr, (binaryfunc)pcm_subscript, nullptr};

// clang-format off
PyTypeObject phlex::experimental::PhlexConfig_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
(char*) "pyphlex.configuration", // tp_name
sizeof(py_config_map), // tp_basicsize
0, // tp_itemsize
(destructor)pcm_dealloc, // tp_dealloc
0, // tp_vectorcall_offset / tp_print
0, // tp_getattr
0, // tp_setattr
0, // tp_as_async / tp_compare
0, // tp_repr
0, // tp_as_number
0, // tp_as_sequence
&pcm_as_mapping, // tp_as_mapping
0, // tp_hash
0, // tp_call
0, // tp_str
0, // tp_getattro
0, // tp_setattro
0, // tp_as_buffer
Py_TPFLAGS_DEFAULT, // tp_flags
(char*)"phlex configuration object-as-dictionary", // tp_doc
0, // tp_traverse
0, // tp_clear
0, // tp_richcompare
0, // tp_weaklistoffset
0, // tp_iter
0, // tp_iternext
0, // tp_methods
0, // tp_members
0, // tp_getset
0, // tp_base
0, // tp_dict
0, // tp_descr_get
0, // tp_descr_set
offsetof(py_config_map, ph_config_cache), // tp_dictoffset
0, // tp_init
0, // tp_alloc
(newfunc)pcm_new, // tp_new
0, // tp_free
0, // tp_is_gc
0, // tp_bases
0, // tp_mro
0, // tp_cache
0, // tp_subclasses
0 // tp_weaklist
#if PY_VERSION_HEX >= 0x02030000
, 0 // tp_del
#endif
#if PY_VERSION_HEX >= 0x02060000
, 0 // tp_version_tag
#endif
#if PY_VERSION_HEX >= 0x03040000
, 0 // tp_finalize
#endif
#if PY_VERSION_HEX >= 0x03080000
, 0 // tp_vectorcall
#endif
#if PY_VERSION_HEX >= 0x030c0000
, 0 // tp_watched
#endif
#if PY_VERSION_HEX >= 0x030d0000
, 0 // tp_versions_used
#endif
};
// clang-format on
Loading
Loading