Skip to content

Conversation

@greenc-FNAL
Copy link
Contributor

@greenc-FNAL greenc-FNAL commented Dec 19, 2025

Non-test changes:

1. Python/C++ Interoperability Enhancements

Files: modulewrap.cpp, lifelinewrap.cpp

  • Feature: RAII for Python Objects

    • Change: Introduced PyObjectPtr (a std::shared_ptr alias with a custom PyObjectDeleter) to manage Python object reference counts.
    • Rationalization: Manual reference counting (Py_INCREF/Py_DECREF) is error-prone, especially in the presence of C++ exceptions. If an exception is thrown, manual decrements might be skipped, leading to memory leaks.
    • Resolution: PyObjectPtr ensures that Py_DECREF is called automatically when the pointer goes out of scope, even during stack unwinding.
  • Fix: Robust Annotation Parsing

    • Change: Rewrote the argument parsing logic in parse_args to iterate over the __annotations__ dictionary using PyDict_Next and explicitly skip the "return" key.
    • Root Cause: The previous implementation relied on PyDict_Values, which returns all values including the return type annotation. Depending on dictionary iteration order (which can vary or be insertion-ordered), the return type could be mistakenly interpreted as an input argument type.
    • Diagnosis: Likely diagnosed by observing type mismatch errors when Python functions had return type annotations.
  • Fix: Flexible Input Conversion (List vs. NumPy)

    • Change: Replaced rigid macro-based vector converters with explicit implementations (py_to_vint, py_to_vuint, etc.) that accept both Python list and NumPy ndarray objects.
    • Root Cause: The previous converters strictly expected NumPy arrays. Users passing standard Python lists would cause runtime errors or type mismatches.
    • Resolution: The new converters check the input type (PyList_Check vs PyArray_Check) and handle data extraction accordingly.
  • Fix: Memory Safety in Cyclic GC

    • Change: Added PyObject_GC_UnTrack(pyobj) in ll_dealloc (lifelinewrap.cpp).
    • Root Cause: Python objects that support cyclic garbage collection must be untracked before deallocation to prevent the GC from visiting invalid memory. Missing this can lead to segfaults during interpreter shutdown or garbage collection cycles.
  • Fix: Type String Matching

    • Change: Replaced brittle fixed-offset string comparisons (e.g., inp_type.compare(pos, ...)) with robust substring searching (suffix.find(...)). Corrected a typo where double64]] was checked instead of float64]].
    • Root Cause: The fixed-offset logic assumed a specific string format for type signatures, which could break if the format changed slightly. The typo prevented float64 arrays from being correctly identified.

2. Build System & CI Improvements

Files: CMakeLists.txt, CMakeLists.txt

  • Enhancement: Reduced Build Dependencies

    • Change: Removed the dependency on the external packaging Python module in CMakeLists.txt.
    • Rationalization: The build system previously used packaging.version to check module versions. This required the packaging library to be installed in the build environment.
    • Resolution: Implemented a lightweight, inline version parser (splitting strings by .) to perform the check using only the standard library.
  • Fix: GCC 14+ Warning Suppression

    • Change: Added -Wno-maybe-uninitialized to compile options for GCC 14.1+.
    • Root Cause: Newer GCC versions have more aggressive static analysis that produces false positives for uninitialized variables in complex C++ templates used by the project.

3. Documentation

Files: copilot-instructions.md

  • New Feature: Added a comprehensive instructions file for GitHub Copilot.
  • Rationalization: To standardize the behavior of AI assistants working in the repository, ensuring they follow project-specific coding standards (formatting, error handling) and workflow guidelines.

Test code changes and additions:

1. New Tests: py:vectypes and py:types

Files: vectypes.py, test_types.py, pyvectypes.jsonnet, pytypes.jsonnet, verify_extended.py

  • Rationale:

    • The existing tests primarily covered basic integer and string types.
    • There was a gap in coverage for:
      • Floating point types (float, double).
      • Unsigned integers (unsigned int, unsigned long).
      • 64-bit integers (long, int64_t).
      • NumPy array interoperability (passing vectors from C++ to Python as NumPy arrays).
    • These tests were added to verify the robustness of the new modulewrap.cpp converters.
  • Coverage Improvements:

    • py:types: Validates scalar type conversion between C++ and Python for float, double, and unsigned int.
    • py:vectypes: Validates vector/array conversion. It tests:
      • Creation of NumPy arrays from scalar inputs (collectify_*).
      • Summation of NumPy arrays back to scalars (sum_array_*).
      • Handling of all major numeric types: int32, uint32, int64, uint64, float32, float64.
    • verify_extended.py: Introduces specialized verifiers (VerifierFloat, VerifierUInt, etc.) that handle type-specific assertions (e.g., epsilon comparison for floats).
  • Problems Exposed:

    • Integer Overflow/Underflow: The py:vectypes test exposed a logic error in source.cpp where large 64-bit hashes were being used in arithmetic (100 - id), causing underflow for unsigned types and wrapping for signed types. This was fixed by introducing modulo arithmetic to keep values small and predictable.
    • Type Mismatches: The strict type checking in the new tests likely exposed the need for the robust annotation parsing and explicit type converters implemented in modulewrap.cpp.
  • Regression Detection:

    • Type Conversion Breakages: These tests will fail if future changes to modulewrap.cpp break the mapping between C++ types (like std::vector<int>) and Python types (like numpy.ndarray or list).
    • Precision Loss: The float/double tests will catch regressions where 64-bit precision is accidentally truncated to 32-bit.
    • Sign Errors: The unsigned integer tests will detect if unsigned values are incorrectly cast to signed values (e.g., treating UINT_MAX as -1).

2. Test Infrastructure Enhancements

Files: CMakeLists.txt, source.cpp

  • Rationale:

    • To support the new tests and ensure the test environment is consistent with real-world usage.
    • To fix flaky or incorrect test data generation.
  • Changes:

    • CMakeLists.txt:
      • Added py:vectypes and py:types to the test suite.
      • Enhanced PYTHONPATH setup to explicitly include Python_SITELIB and Python_SITEARCH. This ensures tests running in embedded environments (like Spack) can find installed packages.
      • Replaced the external packaging dependency with a simple inline version parser for the module check.
    • source.cpp:
      • Expanded the C++ data provider to generate all required types (float, double, uint, int64, uint64).
      • Fix: Changed data generation logic from id.number() to id.number() % N to prevent integer overflow and ensure deterministic summation results.

3. Code Quality & Modernization

Files: adder.py, all_config.py, reducer.py, sumit.py, verify.py

  • Rationale:

    • To comply with the project's stricter linting rules (ruff, mypy) introduced in this commit.
  • Changes:

    • Formatting: Applied standard Python formatting (whitespace, indentation).
    • Linting: Fixed issues like:
      • Comparison to False (changed == False to is False or kept as is with # noqa if intentional for testing).
      • Missing docstrings or blank lines.
      • Unused imports.
    • Type Hinting: Added or corrected type hints to satisfy mypy.
  • Regression Detection:

    • Static Analysis: By enforcing these standards, the CI pipeline can now detect syntax errors, undefined variables, and type inconsistencies before tests are even run.

@greenc-FNAL
Copy link
Contributor Author

@phlexbot format

@github-actions
Copy link
Contributor

Automatic cmake-format fixes pushed (commit e60d811).
⚠️ Note: Some issues may require manual review and fixing.

@greenc-FNAL
Copy link
Contributor Author

@phlexbot format

@github-actions
Copy link
Contributor

No automatic cmake-format fixes were necessary.

@github-actions
Copy link
Contributor

Automatic clang-format fixes pushed (commit 2aa7957).
⚠️ Note: Some issues may require manual review and fixing.

@greenc-FNAL
Copy link
Contributor Author

@phlexbot python-fix

@github-actions
Copy link
Contributor

Automatic Python linting fixes pushed (commit 092dec4).
⚠️ Note: Some issues may require manual review and fixing.

@codecov
Copy link

codecov bot commented Dec 19, 2025

Codecov Report

❌ Patch coverage is 72.60982% with 106 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
plugins/python/src/modulewrap.cpp 74.07% 43 Missing and 55 partials ⚠️
phlex/core/edge_maker.hpp 0.00% 3 Missing and 2 partials ⚠️
phlex/core/edge_maker.cpp 0.00% 2 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (72.60%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

@@            Coverage Diff             @@
##             main     #213      +/-   ##
==========================================
+ Coverage   74.28%   78.22%   +3.94%     
==========================================
  Files         124      124              
  Lines        2955     3261     +306     
  Branches      513      588      +75     
==========================================
+ Hits         2195     2551     +356     
+ Misses        540      448      -92     
- Partials      220      262      +42     
Flag Coverage Δ
unittests 78.22% <72.60%> (+3.94%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
plugins/python/src/lifelinewrap.cpp 53.33% <100.00%> (+3.33%) ⬆️
phlex/core/edge_maker.cpp 68.00% <0.00%> (-9.28%) ⬇️
phlex/core/edge_maker.hpp 83.33% <0.00%> (-16.67%) ⬇️
plugins/python/src/modulewrap.cpp 73.42% <74.07%> (+27.38%) ⬆️

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 022f754...8d007e0. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@greenc-FNAL greenc-FNAL force-pushed the maintenance/improve-test-coverage branch from 38fc738 to 47661fa Compare December 19, 2025 17:19
@greenc-FNAL
Copy link
Contributor Author

@phlexbot format

@github-actions
Copy link
Contributor

No automatic clang-format fixes were necessary.

@github-actions
Copy link
Contributor

Automatic cmake-format fixes pushed (commit bb954c8).
⚠️ Note: Some issues may require manual review and fixing.

@greenc-FNAL
Copy link
Contributor Author

@phlexbot python-fix

@github-actions
Copy link
Contributor

Automatic Python linting fixes pushed (commit cfdb09a).
⚠️ Note: Some issues may require manual review and fixing.

@greenc-FNAL greenc-FNAL force-pushed the maintenance/improve-test-coverage branch 3 times, most recently from c9ccf06 to 1159796 Compare December 21, 2025 17:30
@greenc-FNAL
Copy link
Contributor Author

@phlexbot format

@github-actions
Copy link
Contributor

Automatic clang-format fixes pushed (commit b145a22).
⚠️ Note: Some issues may require manual review and fixing.

@greenc-FNAL
Copy link
Contributor Author

@phlexbot format

@github-actions
Copy link
Contributor

Automatic cmake-format fixes pushed (commit cc928e7).
⚠️ Note: Some issues may require manual review and fixing.

@github-actions
Copy link
Contributor

No automatic clang-format fixes were necessary.

greenc-FNAL and others added 27 commits February 10, 2026 10:06
This commit renames the `AdjustAnnotations` class to `Variant` in both
Python and C++ source files, as well as in tests and build configurations.
All references to the old name have been removed, and related files
have been renamed for consistency.

- Renamed `AdjustAnnotations` to `Variant` in `plugins/python/python/phlex/__init__.py`
- Renamed C++ class `AdjustAnnotations` to `Variant` in `plugins/python/src/modulewrap.cpp`
- Updated `test/python/adder.py` to use `Variant`
- Renamed `test/python/unit_test_annotations.py` to `test/python/unit_test_variant.py`
- Renamed `test/python/pymismatch_annotations.jsonnet` to `test/python/pymismatch_variant.jsonnet`
- Updated `test/python/CMakeLists.txt` to reflect the name and file changes.

Co-authored-by: greenc-FNAL <[email protected]>
This commit reverts the use of the `PyObjectPtr` RAII wrapper to
`intptr_t` in `modulewrap.cpp` and restores fixed-offset string
comparisons for NumPy types, as requested to align with parallel work.

Key changes:
- Reverted `PyObjectPtr` to `intptr_t` throughout `modulewrap.cpp`.
- Restored manual reference counting in callbacks and converters.
- Reverted substring searches to fixed-offset comparisons using `compare()`.
- Preserved the `float64` type correction in type comparisons.
- Preserved all new test coverage and reference-counting fixes.

Co-authored-by: greenc-FNAL <[email protected]>
Modified `insert_input_converters` and `md_transform` in
`plugins/python/src/modulewrap.cpp` to use fixed-length comparisons
for NumPy dtypes.

The previous use of `std::string::npos` required the dtype suffix
(e.g., `"int32]]"`) to be at the absolute end of the annotation string.
With the introduction of PEP 604 union types (e.g., `ndarray | list`),
the annotation string can contain trailing characters, causing
matching to fail.

Using an explicit length in `std::string::compare` allows the matcher
to correctly identify the dtype regardless of trailing content.

Co-authored-by: greenc-FNAL <[email protected]>
- Added safety checks for null ports in `phlex/core/edge_maker.cpp`
  and `phlex/core/edge_maker.hpp` to prevent intermittent segfaults
  during graph setup, especially in benchmarks.
- Improved diagnostic error messages when null ports are encountered.

Co-authored-by: greenc-FNAL <[email protected]>
This commit addresses several issues in the Python support plugin:

1. AttributeError in Variant: Added __code__ and __defaults__ attributes
   to the Variant class to support introspection and fix unit tests.
2. Robust Annotation Parsing: Updated parse_args in modulewrap.cpp to
   use PyDict_Next and explicitly skip the "return" key, fulfilling the
   requirement to handle OrderedDicts correctly.
3. Extended List Support: Added support for list[unsigned int], list[long],
   and list[unsigned long] in modulewrap.cpp converters.
4. Precise Annotations: Updated vectypes.py to use string literal
   annotations that match the new C++ converters.

These changes resolve the reported failures in unit_test_variant,
py:vectypes, and py:veclists.

Co-authored-by: greenc-FNAL <[email protected]>
…ting issues

This commit addresses several issues in the Python support plugin:

1. AttributeError in Variant: Added __code__ and __defaults__ attributes
   to the Variant class to support introspection and fix unit tests.
2. Robust Annotation Parsing: Updated parse_args in modulewrap.cpp to
   use PyDict_Next and explicitly skip the "return" key, fulfilling the
   requirement to handle OrderedDicts correctly.
3. Extended List Support: Added support for list[unsigned int], list[long],
   and list[unsigned long] in modulewrap.cpp converters.
4. Precise Annotations and Linting: Updated vectypes.py to use string
   literal annotations that match the new C++ converters and added
   necessary ruff/mypy suppression comments to resolve CI failures.
5. Improved Type Safety in Tests: Updated list return values in
   vectypes.py to use correct type constructors (e.g., long()).

These changes resolve the reported failures in unit_test_variant,
py:vectypes, py:veclists, and the subsequent CI linting failures.

Co-authored-by: greenc-FNAL <[email protected]>
…ve checks

This commit addresses several issues in the Python support plugin:

1. AttributeError in Variant: Added __code__ and __defaults__ attributes
   to the Variant class to support introspection and fix unit tests.
2. Robust Annotation Parsing: Updated parse_args in modulewrap.cpp to
   use PyDict_Next and explicitly skip the "return" key, fulfilling the
   requirement to handle OrderedDicts correctly.
3. Extended List Support: Added support for list[unsigned int], list[long],
   and list[unsigned long] in modulewrap.cpp converters.
4. Precise Annotations and Linting: Updated vectypes.py to use string
   literal annotations that match the new C++ converters and added
   necessary ruff/mypy suppression comments.
5. Defensive Fixes in C++:
   - Added NULL check in lifeline_transform before calling Py_TYPE.
   - Fixed py_callback assignment operator reference counting.
   - Ensured lifeline_transform is used for observers (callv).
   - Robustified annotation_as_text with extra NULL checks.

These changes resolve the reported failures in unit_test_variant,
py:vectypes, py:veclists, and ensure the code passes CI linting.

Co-authored-by: greenc-FNAL <[email protected]>
This commit fixes multiple issues reported in GitHub Actions:
1. Fixes AttributeError in unit_test_variant.py by exposing __code__ and
   __defaults__ in the Variant class.
2. Robustifies annotation parsing in modulewrap.cpp by iterating over
   input_labels and using named lookups, ensuring correct type-to-argument
   mapping and skipping the "return" annotation correctly.
3. Adds support for list[unsigned int], list[long], and list[unsigned long]
   in the Python bridge.
4. Fixes SegFaults and "missing positional argument" errors by adding
   comprehensive NULL checks in VECTOR_CONVERTER and py_callback, and
   transitioning py_callback to use PyObject_Call with argument tuples.
5. Updates test/python/vectypes.py with precise string annotations and
   linter suppressions to satisfy ruff and mypy.

Co-authored-by: greenc-FNAL <[email protected]>
This commit addresses the "bundle of failures" caused by the previous
attempt and ensures proper reference counting:
1. Restores positional annotation parsing in modulewrap.cpp using
   PyDict_Next with skip-return logic. This ensures compatibility when
   Phlex labels do not match Python parameter names (fixing py:basic/py:add).
2. Correctly enforces annotation count matching, fixing py:mismatch.
3. Fixes reference leaks in md_transform and md_observe by adding
   Py_DECREF(callable) after ownership is transferred to the callback.
4. Uses PyObject_TypeCheck in lifeline_transform for safer type validation.
5. Maintains robust argument passing and NULL handling in py_callback.
6. Keeps Variant introspection attributes in phlex/__init__.py.

Co-authored-by: greenc-FNAL <[email protected]>
This commit addresses multiple issues:
1. Fixes AttributeError in unit_test_variant.py by exposing __code__ and
   __defaults__ in the Variant class.
2. Robustifies annotation parsing in modulewrap.cpp by ensuring correct
   ordering and robustly skipping the "return" annotation.
3. Fixes SegFaults in Python-C++ bridge by:
   - Ensuring the GIL is held during py_callback construction.
   - Adding NULL checks for m_callable in py_callback methods.
   - Replacing placement new with assignment in VECTOR_CONVERTER to avoid
     double construction of std::shared_ptr.
   - Correctly managing reference counts of positional arguments in all
     error paths of py_callback.
   - Using PyObject_TypeCheck in lifeline_transform for safer validation.
4. Correctly manages references in md_transform and md_observe by releasing
   the callable reference returned by parse_args after it is captured by the
   callback.
5. Adds support for list[unsigned int], list[long], and list[unsigned long]
   in the Python bridge.
6. Updates test/python/vectypes.py with precise string annotations and
   linter suppressions.

Co-authored-by: greenc-FNAL <[email protected]>
Address Wim's review: remove unnecessary checks and use msg_from_py_error

Co-authored-by: greenc-FNAL <[email protected]>

Restore NUMPY_ARRAY_CONVERTER macro and fix annotation parsing ordering

Co-authored-by: greenc-FNAL <[email protected]>

Fix GIL protection and unsigned type comparison issue from code review

Co-authored-by: greenc-FNAL <[email protected]>

Clean up redundant checks and improve code clarity

Co-authored-by: greenc-FNAL <[email protected]>

Add Py_IsInitialized check and optimize dictionary lookup

Co-authored-by: greenc-FNAL <[email protected]>

Improve error messages and handling in parse_args

Co-authored-by: greenc-FNAL <[email protected]>

Apply clang-format fixes
…avior (#13)

* Initial plan

* Fix Python parameter names to match input_family labels

Co-authored-by: greenc-FNAL <[email protected]>

* Address code review feedback: fix typo and improve docstring

Co-authored-by: greenc-FNAL <[email protected]>

* Add comment explaining unused parameter k in test function

Co-authored-by: greenc-FNAL <[email protected]>

* Address copilot review comments: fix docstrings and restore two_args signature

Co-authored-by: greenc-FNAL <[email protected]>

* Apply ruff fixes

* Narrow exception handling to catch only KeyError in verify.py

Co-authored-by: greenc-FNAL <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: greenc-FNAL <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit reverts a recent change that incorrectly enforced label-based
matching for Python annotations in `modulewrap.cpp`. Phlex correctly
uses positional matching to map input labels to algorithm parameters,
decoupling them from internal parameter names.

Changes:
- In `modulewrap.cpp`, `parse_args` now iterates through annotations
  positionally, skipping the "return" key.
- Reference counting for attribute names in `parse_args` is improved to
  prevent potential use-after-free issues.
- `test/python/verify.py` is cleaned up by restoring `BoolVerifier` and
  using generic parameter names in `Verifier`, ensuring compatibility with
  positional mapping.
- Redundant specialized verifiers like `VerifierSumIjk` are removed.
- Docstring examples in `verify.py` are corrected.

These changes resolve failures in tests like `test_mismatch.py` and
restore the flexibility of the Phlex Python interop.

Co-authored-by: greenc-FNAL <[email protected]>
This commit reverts the recent change to label-based annotation matching
in `modulewrap.cpp`. Phlex utilizes positional matching to decouple
input labels from Python parameter names.

Key changes:
- `modulewrap.cpp`: Restored positional iteration of `__annotations__`
  in `parse_args`. This correctly restores signature mismatch detection
  at registration time.
- `modulewrap.cpp`: Improved reference counting for `__annotations__`
  lookup to avoid potential use-after-free.
- `test/python/verify.py`: Removed redundant `VerifierSumIjk` and
  `BoolVerifier` (restored generic `BoolVerifier` as a separate class to
  avoid monkey-patching issues).
- `test/python/verify.py`: Restored generic parameter name `value` to
  `Verifier`, demonstrating the decoupling of labels from names.
- Fixed docstring examples in `verify.py`.

These changes resolve the "misunderstanding" of the annotations system
while preserving the docstring and test coverage improvements from
recent commits.

Co-authored-by: greenc-FNAL <[email protected]>
- Restored robust positional annotation matching in modulewrap.cpp using __code__ inspection.
- Updated configwrap.cpp to raise KeyError for missing properties, aligning with Python semantics.
- Added support for Python lists in vector converters (NUMPY_ARRAY_CONVERTER).
- Fixed reference counting and potential use-after-free issues in Python/C++ bridge.
- Standardized error message prefixes for numeric type conversion failures.
- Simplified and cleaned up test/python/verify.py.
- Removed debugging artifacts.

Co-authored-by: greenc-FNAL <[email protected]>
@greenc-FNAL greenc-FNAL force-pushed the maintenance/improve-test-coverage branch from 21a5e93 to cd074f1 Compare February 10, 2026 16:08
@greenc-FNAL
Copy link
Contributor Author

Review the full CodeQL report for details.

// While this check may not be reliable in all threading scenarios (e.g., offloaded threads),
// it prevents crashes during normal interpreter shutdown
if (Py_IsInitialized()) {
PyGILRAII gil;
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably makes more sense to grab the GIL prior to checking Py_IsInitialized().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What happens if you try to grab the GIL and we're no longer initialized due to shutdown?

Copy link
Contributor

Choose a reason for hiding this comment

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

I checked and apparently it's a case of "this would be a programming error". From PyGILState_Ensure():

assert(_PyEval_ThreadsInitialized());

prior to getting the lock.

I think this one is unwinnable: Py_IsInitialized() checks a variable of the global _PyRuntime, but that can, in a treaded environment unprotected by the GIL, be changed right after the call.

Copy link
Contributor

Choose a reason for hiding this comment

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

Note as an aside that the interpreter isn't currently finalized (was a separate discussion) and callback was leaked. There's the issue, independent of the interpreter being alive, of circular references. In that case, Python modules will be offloaded in the interpreter's 2nd cleanup phase, ie. m_callable will go hard dodo-bird. This can be solved once Phlex gets a cleanup state to mimic the registration one and all callables can be decref'ed there, rather than at any indeterminate point when the destructor is called.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or maybe register an atexit() Python-side (but those aren't guaranteed to run).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would it make sense to remove the cleanup logic from the destructor entirely, since neither solution is safe, and replace it with an annotation like:

  // TODO: cleanup deferred to Phlex shutdown hook
  // Cannot safely Py_DECREF during arbitrary destruction due to:
  // - TOCTOU race on Py_IsInitialized() without GIL
  // - Module offloading in interpreter cleanup phase 2

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively I could implement the Python-side atexit(), but even when it runs the only thing we're going to improve is the Valgrind leak report.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, until we get a proper place to finalize the interpreter (see other discussion), atexit() will not be called either... As for valgrind, this being a pure Python object, its memory sits in an arena that should be de-allocated wholesale. It would not surprise me if valgrind is happy.


PyObject* result =
PyObject_CallFunctionObjArgs((PyObject*)m_callable, lifeline_transform(args)..., nullptr);
PyObject* arg_tuple = PyTuple_New(N);
Copy link
Contributor

Choose a reason for hiding this comment

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

This one I asked before as well, what is the benefit of this change?

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 original motivation was to get explicit control over argument lifetime for debugging reference counting issues. However, if we believe that PyObject_CallFunctionObjArgs is working as expected, I can revert.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand that first sentence? For the second part, PyObject_CallFunctionObjArgs is an API from the interpreter, so ... I don't expect it to behave incorrectly?

My objection to the new code is the introduction of a tuple, which both adds memory allocation/deallocation and required copying/ref-counting (it's both an unnecessary run-time overhead and more code to maintain) whereas PyObject_CallFunctionObjArgs is explicitly designed to avoid that if vector calls are supported by the callable (which they will be if it's an ordinary function created by the interpreter).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was added back when I didn't understand what might have been happening, and didn't trust anything. I can revert.


PyObject* result =
PyObject_CallFunctionObjArgs((PyObject*)m_callable, (PyObject*)args..., nullptr);
PyObject* arg_tuple = PyTuple_New(N);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above, why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See above.

{
// helper to decrement reference counts of N arguments
(Py_DECREF((PyObject*)args), ...);
(Py_XDECREF((PyObject*)args), ...);
Copy link
Contributor

Choose a reason for hiding this comment

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

args can't be null as the conversion node will have thrown

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True; will revert.

if (ul == (unsigned long)-1 && PyErr_Occurred() && PyLong_Check(pyobject)) {
PyErr_Clear();
long i = PyLong_AS_LONG(pyobject);
long i = PyLong_AsLong(pyobject);
Copy link
Contributor

Choose a reason for hiding this comment

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

Not needed b/c above is a PyLong_Check() already.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True; will revert.

} \
\
PyArrayObject* arr = (PyArrayObject*)pyobj; \
if (pyobj && PyArray_Check((PyObject*)pyobj)) { \
Copy link
Contributor

Choose a reason for hiding this comment

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

here and below, pyobj isn't null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair; will remove unnecessary checks.


if (!PySequence_Check(input) || (output && !PySequence_Check(output))) {
PyErr_SetString(PyExc_TypeError, "input and output need to be sequences");
if (!PyList_Check(input) && !PyTuple_Check(input)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this restriction?

Copy link
Contributor Author

@greenc-FNAL greenc-FNAL Feb 10, 2026

Choose a reason for hiding this comment

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

The restriction was added because PySequence_GetItem requires additional error handling that wasn't robust. However, if you prefer supporting the full sequence protocol, I can refactor using PySequence_Fast to handle arbitrary sequences safely.

Copy link
Contributor

Choose a reason for hiding this comment

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

For now it probably doesn't matter a whole lot, but it might buy us flexibility when dealing with configuration once it's fully fleshed out. What I have in mind is that the configuration has associated types (implied, but the JSON parser will tell you). These types go to Python types in the translation and then something is lost. E.g. both signed and unsigned int becomes an ordinary Python int. We may well end up providing something more custom in the configuration to retain type information. Whether sequences from the configuration will ever be anything other than list or tuple for this reason, I don't know, but that was the underlying thought.

return nullptr;
}

if (output && !PyList_Check(output) && !PyTuple_Check(output)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above? Any object implementing the sequence protocol should do?

Py_DECREF(callm);
}
}
Py_DECREF(sann);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this moved to further down? I see no use of sann between here and the eventual Py_DECREF?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, no reason to delay.

for (Py_ssize_t i = 0; i < (PyList_GET_SIZE(values) - (ret ? 1 : 0)); ++i) {
PyObject* item = PyList_GET_ITEM(values, i);
input_types.push_back(annotation_as_text(item));
// Match annotation types to input labels positionally by looking up parameter names.
Copy link
Contributor

Choose a reason for hiding this comment

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

This now lives Python-side in Variant, if you got the most recent update. In there, it's far less code and uses the inspect module, making it far easier to maintain.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reference/commit to most recent update, please?

Copy link
Contributor

Choose a reason for hiding this comment

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

Was discussed on slack, not github I see (https://app.slack.com/client/T03RN7KU3/C06HP8Y0PFA). Code is here: https://github.com/wlav/phlex/blob/python-coverage/test/python/variant.py

Basically, it introduces an OrderedDict based on the signature provided by inspect, for the same net result as this C++ code. Note that inspect inspects the code object same way as the proposed C++ code here, but since inspect is a standard library module, it means we don't have to update it if anything changes to the definition of the code object.

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'll compare, but I think I already pulled it after your Slack comment. I'll adjust the C++ to account for its presence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants