-
Notifications
You must be signed in to change notification settings - Fork 10
Improve test coverage for Python support #213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 82 commits
c09acae
00a2df4
caee90e
a48740e
b61e53d
635f1fa
62430b1
c1f5891
3cfdb11
1ff1a89
fb31a36
f870d15
abd4bf5
7e3329b
22bb188
650f33b
fb75668
33e5c48
ef9c17a
4615922
8d7eaf0
4d39398
1275243
0d59220
f4f3c56
980ab44
5dea7e2
ee90164
915b5b5
bff0584
cca9b58
7cf2e8d
0e9b6ba
f61c346
3a7e448
659589b
909b48b
eb3e3a7
737690c
1877231
3775d41
eda7f4c
14f6411
bb5fa1a
432c76a
cb73ed3
06a9158
2c1dec6
aa60daf
fa63ba9
8997a03
3d216af
9432c81
ecd7d46
c5907d3
71b407f
3c31c1a
b483e9a
7166c6b
db8ac8a
9b1d0ba
3e1a9f7
d4ce833
a06d2fa
8870a9b
ff089ab
95d2fbe
57fe76b
e3bf9db
485e3e2
e1de309
8d12e61
40bff07
047bdd2
1c41cf5
510e762
b0510b5
e1d2b41
81bff12
20ac0fc
281a74f
fb0df44
3c30056
72cb86c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like many of the changes in this file revert those made in #219. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Phlex Python Plugin Architecture | ||
|
|
||
| This directory contains the C++ source code for the Phlex Python plugin, which enables Phlex to execute Python code as part of its computation graph. | ||
|
|
||
| ## Architecture Overview | ||
|
|
||
| The integration is built on the **Python C API** (not `pybind11`) to maintain strict control over the interpreter lifecycle and memory management. | ||
|
|
||
| ### 1. The "Type Bridge" (`modulewrap.cpp`) | ||
|
|
||
| The core of the integration is the type conversion layer in `src/modulewrap.cpp`. This layer is responsible for: | ||
|
|
||
| - Converting Phlex `Product` objects (C++) into Python objects (e.g., `PyObject*`, `numpy.ndarray`). | ||
| - Converting Python return values back into Phlex `Product` objects. | ||
|
|
||
| **Critical Implementation Detail:** | ||
| The type mapping relies on **string comparison** of type names. | ||
|
|
||
| - **Mechanism**: The C++ code checks whether `type_name()` contains `"float64]]"` to identify a 2D array of doubles. | ||
| - **Brittleness**: This is a fragile contract. If the type name changes (e.g., `numpy` changes its string representation) or if a user provides a slightly different type (e.g., `float` vs `np.float32`), the bridge may fail. | ||
| - **Extension**: When adding support for new types, you must explicitly add converters in `modulewrap.cpp` for both scalar and vector/array versions. | ||
|
|
||
| ### 2. Hybrid Configuration | ||
|
|
||
| Phlex uses a hybrid configuration model involving three languages: | ||
|
|
||
| 1. **Jsonnet** (`*.jsonnet`): Defines the computation graph structure. It specifies: | ||
| - The nodes in the graph. | ||
| - The Python module/class to load for specific nodes. | ||
| - Configuration parameters passed to the Python object. | ||
| 2. **C++ Driver**: The executable that: | ||
| - Parses the Jsonnet configuration. | ||
| - Initializes the Phlex core. | ||
| - Loads the Python interpreter and the specified plugin. | ||
| 3. **Python Code** (`*.py`): Implements the algorithmic logic. | ||
|
|
||
| ### 3. Environment & Testing | ||
|
|
||
| Because the Python interpreter is embedded within the C++ application, the runtime environment is critical. | ||
|
|
||
| - **PYTHONPATH**: Must be set correctly to include: | ||
| - The build directory (for generated modules). | ||
| - The source directory (for user scripts). | ||
| - Do not append system/Spack `site-packages`; `pymodule.cpp` adjusts `sys.path` based on `CMAKE_PREFIX_PATH` and active virtual environments. | ||
| - **Naming Collisions**: | ||
| - **Warning**: Do not name test files `types.py`, `test.py`, `code.py`, or other names that shadow standard library modules. | ||
| - **Consequence**: Shadowing can cause obscure failures in internal libraries (e.g., `numpy` failing to import because it tries to import `types` from the standard library but gets your local file instead). | ||
|
|
||
| ## Development Guidelines | ||
|
|
||
| 1. **Adding New Types**: | ||
| - Update `src/modulewrap.cpp` to handle the new C++ type. | ||
| - Add a corresponding test case in `test/python/` to verify the round-trip conversion. | ||
| 2. **Testing**: | ||
| - Use `ctest` to run tests. | ||
| - Tests are integration tests: they run the full C++ application which loads the Python script. | ||
| - Debugging: Use `ctest --output-on-failure` to see Python exceptions. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| """Annotation helper for C++ typing variants. | ||
|
|
||
| Python algorithms are generic, like C++ templates, but the Phlex registration | ||
| process requires a single unique signature. These helpers generate annotated | ||
| functions for registration with the proper C++ types. | ||
| """ | ||
|
|
||
| import collections | ||
| import copy | ||
| import inspect | ||
| from typing import Any, Callable | ||
|
|
||
|
|
||
| class MissingAnnotation(Exception): | ||
| """Exception noting the missing of an argument in the provied annotations.""" | ||
|
|
||
| def __init__(self, arg: str): | ||
| """Construct exception from the name of the argument without annotation.""" | ||
| self.arg = arg | ||
|
|
||
| def __str__(self): | ||
| """Report the argument that is missing an annotation.""" | ||
| return "argument '%s' is not annotated" % self.arg | ||
|
|
||
|
|
||
| class Variant: | ||
| """Wrapper to associate custom annotations with a callable. | ||
|
|
||
| This class wraps a callable and provides custom ``__annotations__`` and | ||
| ``__name__`` attributes, allowing the same underlying function or callable | ||
| object to be registered multiple times with different type annotations. | ||
|
|
||
| By default, the provided callable is kept by reference, but can be cloned | ||
| (e.g. for callable instances) if requested. | ||
|
|
||
| Phlex will recognize the "phlex_callable" data member, allowing an unwrap | ||
| and thus saving an indirection. To detect performance degradation, the | ||
| wrapper is not callable by default. | ||
|
|
||
| Attributes: | ||
| phlex_callable (Callable): The underlying callable (public). | ||
| __annotations__ (dict): Type information of arguments and return product. | ||
| __name__ (str): The name associated with this variant. | ||
|
|
||
| Examples: | ||
| >>> def add(i: Number, j: Number) -> Number: | ||
| ... return i + j | ||
| ... | ||
| >>> int_adder = Variant(add, {"i": int, "j": int, "return": int}, "iadd") | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| f: Callable, | ||
| annotations: dict[str, str | type | Any], | ||
| name: str, | ||
| clone: bool | str = False, | ||
| allow_call: bool = False, | ||
| ): | ||
| """Annotate the callable F. | ||
|
|
||
| Args: | ||
| f (Callable): Annotable function. | ||
| annotations (dict): Type information of arguments and return product. | ||
| name (str): Name to assign to this variant. | ||
| clone (bool|str): If True (or "deep"), creates a shallow (deep) copy | ||
| of the callable. | ||
| allow_call (bool): Allow this wrapper to forward to the callable. | ||
| """ | ||
| if clone == "deep": | ||
| self.phlex_callable = copy.deepcopy(f) | ||
| elif clone: | ||
| self.phlex_callable = copy.copy(f) | ||
| else: | ||
| self.phlex_callable = f | ||
|
|
||
| # annotions are expected as an ordinary dict and should be ordered, but | ||
| # we do not require it, so re-order based on the function's co_varnames | ||
| self.__annotations__ = collections.OrderedDict() | ||
|
|
||
| sig = inspect.signature(self.phlex_callable) | ||
| for k, v in sig.parameters.items(): | ||
| try: | ||
| self.__annotations__[k] = annotations[k] | ||
| except KeyError as e: | ||
| if v.default is inspect.Parameter.empty: | ||
| raise MissingAnnotation(k) from e | ||
|
|
||
| self.__annotations__["return"] = annotations.get("return", None) | ||
|
|
||
| self.__name__ = name | ||
| self.__code__ = getattr(self.phlex_callable, "__code__", None) | ||
| self.__defaults__ = getattr(self.phlex_callable, "__defaults__", None) | ||
| self._allow_call = allow_call | ||
|
|
||
| def __call__(self, *args, **kwargs): | ||
| """Raises an error if called directly. | ||
|
|
||
| Variant instances should not be called directly. The framework should | ||
| extract ``phlex_callable`` instead and call that. | ||
|
|
||
| Raises: | ||
| AssertionError: To indicate incorrect usage, unless overridden. | ||
| """ | ||
| assert self._allow_call, ( | ||
| f"Variant '{self.__name__}' was called directly. " | ||
| f"The framework should extract phlex_callable instead." | ||
| ) | ||
| return self.phlex_callable(*args, **kwargs) # type: ignore |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,9 +31,14 @@ static int ll_clear(py_lifeline_t* pyobj) | |
|
|
||
| static void ll_dealloc(py_lifeline_t* pyobj) | ||
| { | ||
| // This type participates in GC; untrack before clearing references so the | ||
| // collector does not traverse a partially torn-down object during dealloc. | ||
| PyObject_GC_UnTrack(pyobj); | ||
| Py_CLEAR(pyobj->m_view); | ||
| typedef std::shared_ptr<void> generic_shared_t; | ||
| pyobj->m_source.~generic_shared_t(); | ||
| // Use tp_free to pair with tp_alloc for GC-tracked Python objects. | ||
| Py_TYPE(pyobj)->tp_free((PyObject*)pyobj); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks scary. I don't claim that it's wrong. But do we understand why these changes are required?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, these are correct. |
||
| } | ||
|
|
||
| // clang-format off | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't some of these changes suggest the build directory may be at
/? Is this ever the case?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It pegs the ignore expression to the top-level directory, otherwise it matches in subdirectories also.