Skip to content

Conversation

@timosachsenberg
Copy link
Collaborator

@timosachsenberg timosachsenberg commented Jan 9, 2026

Summary

Fixes #239 - Cross-module enum class getter/setter inconsistency in multi-module builds.

In multi-module builds (e.g., pyOpenMS with _pyopenms_1.pyx through _pyopenms_8.pyx), scoped enums (enum class) wrapped as Python IntEnum classes could fail when used across modules.

Problem

The previous globals().get() approach only searched the current module's namespace:

  • When MSSpectrum (in module 4) called getType() returning SpectrumType (defined in module 3), the getter returned a plain int because _PySpectrumType wasn't in module 4's globals
  • The setter then failed with AttributeError: 'int' object has no attribute 'value' because it expected the IntEnum wrapper

Solution

Add a cross-module enum registry and lookup mechanism:

  1. Per-module registry (_scoped_enum_registry): A dict that stores enums defined in each module
  2. Lookup function (_get_scoped_enum_class):
    • First checks the local registry (fast path for same-module enums)
    • Then searches all loaded pyopenms.* modules via sys.modules
    • Caches results for subsequent lookups
  3. The helper code is added to top_level_pyx_code so it only appears in .pyx files, not .pxd files (which cannot contain def statements)

Changes

  • CodeGenerator.py:

    • Added create_scoped_enum_helpers() method
    • Added import sys as _sys to default cimports
    • Added enum registration after each scoped enum class definition
    • Updated docstring for create_foreign_enum_imports()
  • ConversionProvider.py:

    • Updated type_check_expression() to use _get_scoped_enum_class()
    • Updated output_conversion() to use _get_scoped_enum_class() with lambda fallback

Test plan

  • Tested with pyOpenMS - s.setType(s.getType()) now works for both SpectrumSettings (same module) and MSSpectrum (cross-module)
  • Full pyOpenMS test suite passes (170/173, 3 failures due to missing test data files)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a StatusTracker class to enable getter/setter round-trips for two enums across modules.
  • Bug Fixes

    • Improved cross-module support and resolution for scoped enums so enum values propagate reliably between modules.

✏️ Tip: You can customize this high-level summary in your review settings.

In multi-module builds (e.g., pyOpenMS with _pyopenms_1.pyx through
_pyopenms_8.pyx), scoped enums (enum class) wrapped as Python IntEnum
classes could fail when used across modules.

Problem:
- The previous globals().get() approach only searched the current module
- When MSSpectrum (module 4) called getType() returning SpectrumType
  (defined in module 3), the getter returned a plain int
- The setter then failed with "AttributeError: 'int' object has no
  attribute 'value'" because it expected the IntEnum wrapper

Solution:
- Add a per-module registry (_scoped_enum_registry) that stores enums
  defined in that module
- Add a lookup function (_get_scoped_enum_class) that:
  1. First checks the local registry (fast path for same-module enums)
  2. Then searches all loaded pyopenms modules via sys.modules
  3. Caches results for subsequent lookups
- The helper code is added to top_level_pyx_code so it only appears in
  .pyx files, not .pxd files (which cannot contain def statements)

This fixes issue #239.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 9, 2026

Warning

Rate limit exceeded

@timosachsenberg has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 56 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between e662de5 and 0ca51d9.

📒 Files selected for processing (1)
  • autowrap/version.py
📝 Walkthrough

Walkthrough

Adds a module-local scoped-enum registry and a runtime lookup function (_get_scoped_enum_class), emits helper code from CodeGenerator, updates generated enum conversion to use the new lookup, and adds tests and Cython declarations to validate cross-module enum getter/setter round-trips.

Changes

Cohort / File(s) Summary
Scoped Enum Registry & Codegen
autowrap/CodeGenerator.py
Adds create_scoped_enum_helpers() to emit a module-local registry and _get_scoped_enum_class helper into .pyx output; wires helper emission into create_pyx_file; adds import sys as _sys to default C imports.
Enum Conversion Logic
autowrap/ConversionProvider.py
Replaces globals().get(...) with _get_scoped_enum_class(...) in enum type checks and output conversion to perform centralized cross-module enum lookup.
Tests Updated / Added
tests/test_code_generator.py, tests/test_files/enum_cross_module/*
Tests updated to assert registry-based lookup; adds runtime round-trip tests for class-attached and standalone cross-module enums.
Cython/C++ Test Declarations
tests/test_files/enum_cross_module/EnumConsumer.hpp, tests/test_files/enum_cross_module/EnumConsumer.pxd
Adds StatusTracker C++ class and corresponding pxd declarations exposing getters/setters for Task::TaskStatus and Priority to exercise cross-module enum round-trips.

Sequence Diagram(s)

sequenceDiagram
    participant PyUser as Python caller
    participant PyModule as Generated module code (getter/setter)
    participant ScopedRegistry as _get_scoped_enum_class / module registry
    participant CppObj as C++ instance via Cython

    PyUser->>PyModule: call getStatus()
    PyModule->>CppObj: call native getter -> int
    PyModule->>ScopedRegistry: lookup Python enum wrapper for type
    ScopedRegistry-->>PyModule: return EnumClass (or identity)
    PyModule-->>PyUser: return EnumClass(int)

    PyUser->>PyModule: call setStatus(EnumClass instance)
    PyModule->>ScopedRegistry: verify/lookup expected EnumClass
    PyModule->>CppObj: convert EnumClass to int and call native setter
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • jpfeuffer
  • poshul

Poem

🐰
A tiny registry hops into place,
Enums cross modules with nimble grace,
No more globals lost in the fray,
Getter and setter now find their way.
— a rabbit, delighted 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix cross-module scoped enum lookup in multi-module builds' directly and concisely summarizes the main change: implementing a cross-module lookup mechanism for scoped enums in multi-module builds to fix the globals().get() limitation.
Linked Issues check ✅ Passed The PR fully addresses issue #239 by implementing a per-module registry and _get_scoped_enum_class() lookup function that replaces globals().get(), enabling correct cross-module scoped enum resolution for getter/setter roundtrips.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing cross-module scoped enum lookup: CodeGenerator adds registry helpers, ConversionProvider uses new lookup, tests validate registry-based resolution, and EnumConsumer adds StatusTracker for cross-module roundtrip testing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
autowrap/CodeGenerator.py (1)

2123-2176: Consider making the package name configurable.

The cross-module lookup hardcodes 'pyopenms' at line 2165, which limits this feature to only the pyOpenMS project. For a general-purpose solution that works with other multi-module autowrap projects, consider making the package name configurable or auto-detecting it from the module structure.

Possible approaches

Option 1: Infer from current module name

 |    # Slow path: search all loaded pyopenms modules
+|    current_module = __name__.split('.')[0]  # Get package name from current module
 |    for mod_name, mod in list(_sys.modules.items()):
-|        if mod is not None and (mod_name.startswith('pyopenms.') or mod_name == 'pyopenms'):
+|        if mod is not None and (mod_name.startswith(current_module + '.') or mod_name == current_module):

Option 2: Search all modules (slower but more general)

 |    # Slow path: search all loaded modules
 |    for mod_name, mod in list(_sys.modules.items()):
-|        if mod is not None and (mod_name.startswith('pyopenms.') or mod_name == 'pyopenms'):
+|        if mod is not None:
 |            enum_cls = getattr(mod, name, None)
 |            if enum_cls is not None:

Option 3: Add configuration parameter
Pass the package name as a parameter to the code generator and use it in the template.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e79f2a and 7597114.

📒 Files selected for processing (2)
  • autowrap/CodeGenerator.py
  • autowrap/ConversionProvider.py
🧰 Additional context used
🧬 Code graph analysis (1)
autowrap/CodeGenerator.py (1)
autowrap/Code.py (2)
  • add (56-75)
  • Code (41-90)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (==3.2.0, 3.13)
  • GitHub Check: test (==3.2.0, 3.11)
  • GitHub Check: test (==3.1.0, 3.11)
  • GitHub Check: test (==3.2.0, 3.12)
  • GitHub Check: test (==3.1.0, 3.10)
  • GitHub Check: test (==3.2.0, 3.10)
  • GitHub Check: test (==3.1.0, 3.13)
  • GitHub Check: test (==3.1.0, 3.12)
🔇 Additional comments (6)
autowrap/CodeGenerator.py (4)

282-282: LGTM! Proper integration of scoped enum helper generation.

The call is well-placed in the code generation sequence, after foreign imports and before includes.


525-528: LGTM! Scoped enum registration is correctly placed.

Registration occurs immediately after the scoped enum class definition, enabling the fast-path lookup for same-module enum access.


2088-2122: Well-documented no-op approach to avoid circular imports.

The extensive documentation clearly explains why module-level imports are avoided and how the runtime lookup solution works instead. This is a good design decision for multi-module builds.


2209-2209: LGTM! Necessary import for cross-module enum lookup.

The underscore prefix _sys is good practice to avoid potential naming conflicts with user code.

autowrap/ConversionProvider.py (2)

405-431: LGTM! Proper integration with cross-module enum lookup.

The change from globals().get() to _get_scoped_enum_class() correctly implements the cross-module lookup mechanism. The int fallback is appropriate since IntEnum inherits from int, making isinstance() checks still work correctly.


445-459: LGTM! Output conversion correctly uses cross-module lookup.

The change from globals().get() to _get_scoped_enum_class() with a lambda fallback maintains the original behavior while enabling cross-module enum resolution. The lambda fallback ensures that if the enum class isn't found, the int value is returned unchanged.

| return _scoped_enum_registry[name]
| # Slow path: search all loaded pyopenms modules
| for mod_name, mod in list(_sys.modules.items()):
| if mod is not None and (mod_name.startswith('pyopenms.') or mod_name == 'pyopenms'):
Copy link
Contributor

Choose a reason for hiding this comment

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

Nope 😄 this will not work generally

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

if only pyopenms uses it ;)

The existing test_cross_module_scoped_enum_imports test had a gap:
it didn't test the scenario where a class in module B uses both
getter and setter methods for an enum defined in module A.

This is the exact pattern that failed in pyOpenMS:
  s.setType(s.getType())  # Cross-module roundtrip

Changes:
- Add StatusTracker class to EnumConsumer with getter/setter for
  enums from EnumProvider (both Task::TaskStatus and Priority)
- Add Runtime Test 8 that validates tracker.setX(tracker.getX())
  works correctly across module boundaries
- Update test assertions to check for _get_scoped_enum_class()
  pattern instead of the old globals().get() pattern

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@jpfeuffer
Copy link
Contributor

When pyopenms splits the module, it knows which decls are where. Maybe one should fix it in the create_cpp_extension of pyopenms instead. To make sure it creates a set of modules without circular dependencies.

Maybe we also need to rethink scoped enums. Maybe we need a real Cython implementation of them that behave like regular enums, such that we can do type checking on Cython level and dont need to import all python modules.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@timosachsenberg timosachsenberg merged commit b6826b8 into master Jan 9, 2026
11 checks passed
@jpfeuffer
Copy link
Contributor

@timosachsenberg I dont think this was a good idea. See my comments.

@timosachsenberg
Copy link
Collaborator Author

ups too late :) ok how can we fix this such that it works generally?
the bug was introduced because I fixed and confirmed it in pyopenms and I openend a PR from there

@jpfeuffer
Copy link
Contributor

jpfeuffer commented Jan 9, 2026

See my comments. Maybe the AI has better ideas if it knows other places/ways where this could be solved.
If it considers this latae binding solution still as the best, you must somehow pass the names of the modules that are built at the same time and inject at the marked line.

@timosachsenberg
Copy link
Collaborator Author

Fixed in a76f252 - the lookup is now package-agnostic. It auto-detects the package name at runtime using __package__ or __name__.rsplit() and searches sibling modules dynamically.

@jpfeuffer
Copy link
Contributor

Not bad but in this case the user needs to make sure they are named in a specific way. We should document this somewhere.
And autowrap doesnt know the names of all modules it builds together??

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.

Cross-module enum class getter/setter inconsistency in multi-module builds

3 participants