Skip to content
Open
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
46 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
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably the issue is elsewhere, but why is this read from the configuration as a list of strings and converted to a list of integers, instead of being read as a list of integers to begin with?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy&paste error, probably. Will fix.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You seem to have forgotten floats.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration interface isn't finalized; I was trying to get away with the absolute minimum possible. Can add floats (doubles), though, as it's likely someone will try it with the prototype.

PyErr_SetString(PyExc_TypeError, "requested type not supported");
}

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

// untyped (guess) conversion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we introspect the value (using this, or the is_* members) and just generate the correct type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish. I asked for this. The boost::json object isn't accessible with the current interface, which itself isn't final in any way. Once the configuration interface has improved, this code can follow.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look like it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, need to add.


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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have the Python exception type (and maybe even the traceback, if posible) available in the C++ exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that can be done.

}
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