Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
74f90b4
Improve test coverage of Python/C++ interface code
greenc-FNAL Dec 19, 2025
7084b3e
Initial plan
Copilot Jan 12, 2026
27c595b
Add Variant helper and address review comments
Copilot Jan 12, 2026
2603252
Initial plan
Copilot Jan 12, 2026
553529e
Fix code review comments
Copilot Jan 12, 2026
a2c1188
Fix ruff F722 and mypy errors in vectypes.py by using type aliases wi…
Copilot Jan 12, 2026
70f9a04
Apply cmake-format fixes
github-actions[bot] Jan 12, 2026
81ad563
Simplify metaclass implementation per code review feedback
Copilot Jan 12, 2026
dce3e7a
Apply Python linting fixes
github-actions[bot] Jan 12, 2026
1f2b01c
Merge pull request #7 from greenc-FNAL/copilot/update-ruff-and-mypy-s…
greenc-FNAL Jan 12, 2026
55313c7
Fix CodeQL alert
greenc-FNAL Jan 12, 2026
bf616d8
Apply clang-format fixes
github-actions[bot] Jan 14, 2026
440c902
Fix Python tests and enforce NumPy requirement
greenc-FNAL Jan 14, 2026
38a38fe
Apply cmake-format fixes
github-actions[bot] Jan 14, 2026
690cb26
More tests to fill gaps
greenc-FNAL Jan 14, 2026
1354192
Apply cmake-format fixes
github-actions[bot] Jan 14, 2026
6b1f634
Apply Python linting fixes
github-actions[bot] Jan 14, 2026
b0b30b1
Address remaining `ruff` issues
greenc-FNAL Jan 14, 2026
3c3043c
Per Gemini 3 Pro, get GIL when updating ref count
greenc-FNAL Jan 14, 2026
90d2593
Attempt to address CI hangs in `py:badbool` and `py:raise` tests
greenc-FNAL Jan 14, 2026
ed5e23e
More coverage improvement
greenc-FNAL Jan 14, 2026
b1c55bf
Apply Python linting fixes
github-actions[bot] Jan 14, 2026
f8a3c8d
Apply cmake-format fixes
github-actions[bot] Jan 14, 2026
a9d5d70
Silence inapposite complaints; remove unused class
greenc-FNAL Jan 14, 2026
3aeb5c5
More hang protection
greenc-FNAL Jan 14, 2026
4ea5f61
Extra diagnostics to debug hangs during testing
greenc-FNAL Jan 14, 2026
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
55 changes: 55 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,58 @@ All Markdown files must strictly follow these markdownlint rules:
- **MD034**: No bare URLs (for example, use a markdown link like `[text](destination)` instead of a plain URL)
- **MD036**: Use # headings, not **Bold:** for titles
- **MD040**: Always specify code block language (for example, use '```bash', '```python', '```text', etc.)

## Development & Testing Workflows

### Build and Test

- **Environment**: Always source `setup-env.sh` before building or testing. This applies to all environments (Dev Container, local machine, HPC).
- **Configuration**:
- **Presets**: Prefer `CMakePresets.json` workflows (e.g., `cmake --preset default`).
- **Generator**: Prefer `Ninja` over `Makefiles` when available (`-G Ninja`).
- **Build**:
- **Parallelism**: Always use multiple cores. Ninja does this by default. For `make`, use `cmake --build build -j $(nproc)`.
- **Test**:
- **Parallelism**: Run tests in parallel using `ctest -j $(nproc)` or `ctest --parallel <N>`.
- **Selection**: Run specific tests with `ctest -R "regex"` (e.g., `ctest -R "py:*"`).
- **Debugging**: Use `ctest --output-on-failure` to see logs for failed tests.

### Python Integration

- **Naming**: Avoid naming Python test scripts `types.py` or other names that shadow standard library modules. This causes obscure import errors (e.g., `ModuleNotFoundError: No module named 'numpy'`).
- **PYTHONPATH**: When running tests in Spack environments, ensure `PYTHONPATH` includes `site-packages`. In CMake, explicitly add `Python_SITELIB` and `Python_SITEARCH` to `TEST_PYTHONPATH`.
- **Test Structure**:
- **C++ Driver**: Provides data streams (e.g., `test/python/driver.cpp`).
- **Jsonnet Config**: Wires the graph (e.g., `test/python/pytypes.jsonnet`).
- **Python Script**: Implements algorithms (e.g., `test/python/test_types.py`).
- **Type Conversion**: `plugins/python/src/modulewrap.cpp` handles C++ ↔ Python conversion.
- **Mechanism**: Uses string comparison of type names (e.g., `"float64]]"`). This is brittle.
- **Requirement**: Ensure converters exist for all types used in tests (e.g., `float`, `double`, `unsigned int`, and their vector equivalents).
- **Warning**: Exact type matches are required. `numpy.float32` != `float`.

### Coverage Analysis

- **Tooling**: The project uses LLVM source-based coverage.
- **Requirement**: The `phlex` binary must catch exceptions in `main` to ensure coverage data is flushed to disk even when tests fail/crash.
- **Generation**:
- **CMake Targets**: `coverage-xml`, `coverage-html` (if configured).
- **Manual**:
1. Run tests with `LLVM_PROFILE_FILE` set (e.g., `export LLVM_PROFILE_FILE="profraw/%m-%p.profraw"`).
2. Merge profiles: `llvm-profdata merge -sparse profraw/*.profraw -o coverage.profdata`.
3. Generate report: `llvm-cov show -instr-profile=coverage.profdata -format=html ...`

### Local GitHub Actions Testing (`act`)

- **Tool**: Use `act` to run GitHub Actions workflows locally.
- **Configuration**: Ensure `.actrc` exists in the workspace root with the following content to use a compatible runner image:
```text
-P ubuntu-latest=catthehacker/ubuntu:act-latest
```
- **Usage**:
- List jobs: `act -l`
- Run specific job: `act -j <job_name>` (e.g., `act -j python-check`)
- Run specific event: `act pull_request`
- **Troubleshooting**:
- **Docker Socket**: `act` requires access to the Docker socket. In dev containers, this may require specific mount configurations or permissions.
- **Artifacts**: `act` creates a `phlex-src` directory (or similar) for checkout. Ensure this is cleaned up or ignored by tools like `mypy`.

5 changes: 4 additions & 1 deletion .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,11 @@ jobs:
mkdir -p "$PROFILE_ROOT"
export LLVM_PROFILE_FILE="$PROFILE_ROOT/%m-%p.profraw"

# Enable Python plugin debug diagnostics
export PHLEX_PYTHON_DEBUG=1

echo "::group::Running ctest for coverage"
if ctest --progress --output-on-failure -j "$(nproc)"; then
if ctest --progress --verbose --output-on-failure -j "$(nproc)"; then
echo "::endgroup::"
echo "✅ All tests passed."
else
Expand Down
26 changes: 15 additions & 11 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# Build directories
build/
build-cov/
_build/
*.dir/
phlex-src
phlex-build/
CMakeCache.txt
/phlex-src/
/phlex-build/
/CMakeCache.txt
CMakeFiles/
_deps/
/_deps/
_codeql_detected_source_root

# CMake user-specific presets (not generated by Spack)
CMakeUserPresets.json
/CMakeUserPresets.json

# Coverage reports
coverage.xml
coverage.info
coverage-html/
.coverage-generated/
.coverage-artifacts/
/coverage.profdata
/coverage_*.txt
/coverage.xml
/coverage.info
/coverage-html/
/profraw/
/.coverage-generated/
/.coverage-artifacts/
*.gcda
*.gcno
*.gcov
Expand Down Expand Up @@ -45,4 +49,4 @@ __pycache__/
.DS_Store
# act (local workflow testing)
.act-artifacts/
.secrets
.secrets
15 changes: 7 additions & 8 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,10 @@ add_compile_options(
)

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(
CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "14.1"
AND CMAKE_COMPILER_VERSION VERSION_LESS "15"
)
# GCC 14.1 issues many false positives re. array-bounds and
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "14.1")
# GCC 14.1+ issues many false positives re. array-bounds and
# stringop-overflow
add_compile_options(-Wno-array-bounds -Wno-stringop-overflow)
add_compile_options(-Wno-array-bounds -Wno-stringop-overflow -Wno-maybe-uninitialized)
endif()
endif()

Expand Down Expand Up @@ -108,7 +105,8 @@ if(ENABLE_TSAN)
-g
-O1
# Ensure no optimizations interfere with TSan
"$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-fno-omit-frame-pointer -fno-optimize-sibling-calls>"
"$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-fno-omit-frame-pointer>"
"$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-fno-optimize-sibling-calls>"
)
add_link_options(-fsanitize=thread)
else()
Expand All @@ -130,7 +128,8 @@ if(ENABLE_ASAN)
-g
-O1
# Ensure no optimizations interfere with ASan
"$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-fno-omit-frame-pointer -fno-optimize-sibling-calls>"
"$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-fno-omit-frame-pointer>"
"$<$<COMPILE_LANG_AND_ID:CXX,GNU>:-fno-optimize-sibling-calls>"
)
add_link_options(-fsanitize=address)
else()
Expand Down
78 changes: 59 additions & 19 deletions plugins/python/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,23 +1,63 @@
find_package(Python 3.12 COMPONENTS Interpreter Development NumPy REQUIRED)

if(Python_NumPy_VERSION VERSION_LESS "2.0.0")
message(
FATAL_ERROR
"NumPy version is too low: ${Python_NumPy_VERSION} found, at least 2.0.0 required"
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}
installed_version = getattr(${MODULE_NAME}, '__version__', None)
if not installed_version:
sys.exit(2)

def parse(v):
return tuple(map(int, v.split('.')[:3]))

if parse(installed_version) >= parse('${MIN_VERSION}'):
sys.exit(0)
else:
sys.exit(2) # Version too low
except ImportError:
sys.exit(1)
except Exception:
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
)
endif()
target_link_libraries(pymodule PRIVATE phlex::module Python::Python Python::NumPy)
target_compile_definitions(pymodule PRIVATE NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION)

# 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
)
target_link_libraries(pymodule PRIVATE phlex::module Python::Python Python::NumPy)
target_compile_definitions(pymodule PRIVATE NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION)

install(TARGETS pymodule LIBRARY DESTINATION lib)
install(TARGETS pymodule LIBRARY DESTINATION lib)
endif()
55 changes: 55 additions & 0 deletions plugins/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 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 `type_name() == "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).
- System/Spack `site-packages` (for dependencies like `numpy`).
- **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.
2 changes: 2 additions & 0 deletions plugins/python/src/lifelinewrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ static int ll_clear(py_lifeline_t* pyobj)

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

// clang-format off
Expand Down
Loading
Loading