Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ if(NOT CMAKE_BUILD_TYPE)
endif()

add_subdirectory(phlex)
add_subdirectory(plugins)

if(PHLEX_USE_FORM)
set(BUILD_SHARED_LIBS OFF) # Temporary
Expand Down
209 changes: 209 additions & 0 deletions plugins/python/configwrap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#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;
};
// clang-format on

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

py_config_map* pyconfig = PyObject_New(py_config_map, &PhlexConfig_Type);
pyconfig->ph_config = config;

return (PyObject*)pyconfig;
}

static PyObject* pcm_subscript(py_config_map* pycmap, PyObject* args)
{
// Retrieve a named configuration setting.
//
// Configuration is only accessible through templated lookups and it is up to
// the caller to figure the correct one. Here, conversion is attempted in order,
// unless an optional type is provided. On failure, the setting is returned as
// a string.
//
// Since the configuration is read-only, values are cached in the implicit
// dictionary such that they can continue to be inspected.
//
// Python arguments expected:
// name: the property to retrieve
// type: the type to cast to (optional) and one of: int, float (for C++
// double), or str (for C++ std::string)
// tuple as standin for std::vector
// coll: boolean, set to True if this is a collection of <type>

PyObject *pyname = nullptr, *type = nullptr;
int coll = 0;
if (PyTuple_Check(args)) {
if (!PyArg_ParseTuple(args, "U|Op:__getitem__", &pyname, &type, &coll))
return nullptr;
} else
pyname = args;

// cached lookup
#if PY_VERSION_HEX >= 0x030d0000
PyObject* pyvalue = nullptr;
PyObject_GetOptionalAttr((PyObject*)pycmap, pyname, &pyvalue);
#else
PyObject* pyvalue = PyObject_GetAttr((PyObject*)pycmap, pyname);
if (!pyvalue)
PyErr_Clear();
#endif
if (pyvalue)
return pyvalue;

std::string cname = PyUnicode_AsUTF8(pyname);

// typed conversion if provided
if (type == (PyObject*)&PyUnicode_Type) {
if (!coll) {
try {
auto const& cvalue = pycmap->ph_config->get<std::string>(cname);
pyvalue = PyUnicode_FromString(cvalue.c_str());
} catch (...) {
PyErr_Format(PyExc_TypeError, "property \"%s\" is not a string", cname.c_str());
}
} else {
try {
auto const& cvalue = pycmap->ph_config->get<std::vector<std::string>>(cname);
pyvalue = PyTuple_New(cvalue.size());
for (Py_ssize_t i = 0; i < (Py_ssize_t)cvalue.size(); ++i) {
PyObject* item = PyUnicode_FromString(cvalue[i].c_str());
PyTuple_SetItem(pyvalue, i, item);
}
} catch (...) {
PyErr_Format(
PyExc_TypeError, "property \"%s\" is not a collection of strings", cname.c_str());
}
}
} else if (type == (PyObject*)&PyLong_Type) {
if (!coll) {
try {
auto const& cvalue = pycmap->ph_config->get<long>(cname);
pyvalue = PyLong_FromLong(cvalue);
} catch (...) {
PyErr_Format(PyExc_TypeError, "property \"%s\" is not an integer", cname.c_str());
}
} else {
try {
auto const& cvalue = pycmap->ph_config->get<std::vector<std::string>>(cname);
pyvalue = PyTuple_New(cvalue.size());
for (Py_ssize_t i = 0; i < (Py_ssize_t)cvalue.size(); ++i) {
PyObject* item = PyLong_FromString(cvalue[i].c_str(), nullptr, 10);
PyTuple_SetItem(pyvalue, i, item);
}
} catch (...) {
PyErr_Format(
PyExc_TypeError, "property \"%s\" is not a collection of integers", cname.c_str());
}
}
} else if (type) {
PyErr_SetString(PyExc_TypeError, "requested type not supported");
}

if (type)
return pyvalue; // may be nullptr

// untyped (guess) conversion
if (!pyvalue) {
try {
auto const& cvalue = pycmap->ph_config->get<long>(cname);
pyvalue = PyLong_FromLong(cvalue);
} catch (std::runtime_error const&) {
}
}
if (!pyvalue) {
try {
auto const& cvalue = pycmap->ph_config->get<std::string>(cname);
pyvalue = PyUnicode_FromStringAndSize(cvalue.c_str(), cvalue.size());
} catch (std::runtime_error const&) {
}
}

// cache if found

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
0, // 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
0, // tp_dictoffset
0, // tp_init
0, // tp_alloc
0, // 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
43 changes: 43 additions & 0 deletions plugins/python/errorwrap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include "wrap.hpp"

#include <exception>
#include <string>

using namespace phlex::experimental;

void phlex::experimental::throw_runtime_error_from_py_error(bool check_error)
{
PyGILRAII g;

if (check_error) {
if (!PyErr_Occurred())
return;
}

std::string msg;

#if PY_VERSION_HEX < 0x30c000000
PyObject *type = nullptr, *value = nullptr, *traceback = nullptr;
PyErr_Fetch(&type, &value, &traceback);
if (value) {
PyObject* pymsg = PyObject_Str(value);
msg = PyUnicode_AsUTF8(pymsg);
Py_DECREF(pymsg);
} else {
msg = "unknown Python error occurred";
}
Py_XDECREF(traceback);
Py_XDECREF(value);
Py_XDECREF(type);
#else
PyObject* exc = PyErr_GetRaisedException();
if (exc) {
PyObject* pymsg = PyObject_Str(exc);
msg = PyUnicode_AsString(pymsg);
Py_DECREF(pymsg);
Py_DECREF(exc);
}
#endif

throw std::runtime_error(msg.c_str());
}
105 changes: 105 additions & 0 deletions plugins/python/lifelinewrap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#include <memory>
#include <string>

#include "wrap.hpp"

using namespace phlex::experimental;

static py_lifeline_t* ll_new(PyTypeObject* pytype, PyObject*, PyObject*)
{
py_lifeline_t* pyobj = (py_lifeline_t*)pytype->tp_alloc(pytype, 0);
if (!pyobj)
PyErr_Print();
pyobj->m_view = nullptr;
new (&pyobj->m_source) std::shared_ptr<void>{};

return pyobj;
}

static int ll_traverse(py_lifeline_t* pyobj, visitproc visit, void* args)
{
if (pyobj->m_view)
visit(pyobj->m_view, args);
return 0;
}

static int ll_clear(py_lifeline_t* pyobj)
{
Py_CLEAR(pyobj->m_view);
return 0;
}

static void ll_dealloc(py_lifeline_t* pyobj)
{
Py_CLEAR(pyobj->m_view);
typedef std::shared_ptr<void> generic_shared_t;
pyobj->m_source.~generic_shared_t();
}

// clang-format off
PyTypeObject phlex::experimental::PhlexLifeline_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
(char*) "pyphlex.lifeline", // tp_name
sizeof(py_lifeline_t), // tp_basicsize
0, // tp_itemsize
(destructor)ll_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
0, // 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 | Py_TPFLAGS_HAVE_GC, // tp_flags
(char*)"internal", // tp_doc
(traverseproc)ll_traverse, // tp_traverse
(inquiry)ll_clear, // 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
0, // tp_dictoffset
0, // tp_init
0, // tp_alloc
(newfunc)ll_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