-
Notifications
You must be signed in to change notification settings - Fork 23
Fix circular import issue with cross-module scoped enum type checking #238
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
Conversation
PR #237 introduced module-level Python imports for scoped enum classes across modules. While this fixed the Cython compile-time error, it causes circular import errors at runtime when modules form import chains during initialization. Problem: - Module 1 starts loading and imports from Module 8 - Module 8 imports from Module 7, etc. - When Module 2 tries to import from Module 3, Module 3 hasn't finished initializing yet → ImportError Solution: - Make create_foreign_enum_imports() a no-op (remove module-level imports) - Use globals().get() for late binding in type assertions: `isinstance(x, globals().get('_PyEnumName', int))` This approach: - Compiles successfully (globals().get() is always valid Python) - Resolves the enum class at runtime after all modules are loaded - Falls back to 'int' which works for IntEnum (inherits from int) Also updated output_conversion() to use the same pattern for wrapping return values in the enum class. Tested with pyOpenMS (491 tests pass including 82 static method tests). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
📝 WalkthroughWalkthroughThe PR removes module-level import generation for cross-module scoped enums and replaces type checking and output conversion with late-binding via Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
|
rabbit hole level 10 mittlerweile... @jpfeuffer hat das late binding Nachteile? damit hat pyopenms zumindest gebaut |
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.
Actionable comments posted: 0
🧹 Nitpick comments (2)
autowrap/ConversionProvider.py (2)
405-435: Scoped-enum type checks now depend on local globals; confirm cross-module behaviorSwitching scoped-enum checks to
isinstance(arg, globals().get('Name', int))avoids hard references and circular imports, but it also means that in a consuming module that does not bind the enum class into its own globals, the check silently degrades toisinstance(arg, int). In that case, anyintor unrelatedIntEnuminstance will satisfy the assertion, so you lose the previous ability to distinguish between different IntEnum types at runtime in cross-module call sites. Please confirm that, for all intended consumers, the corresponding Python enum classes are guaranteed to be bound into that module’s globals (so the fallback tointis only a safety net and not the common path); otherwise this is a real weakening of type validation that might make tests like the “wrong enum type should raise” expectations intest_cross_module_scoped_enum_importsimpossible to satisfy reliably. A more robust (but still late-bound) alternative would be to resolve the enum from its defining module viasys.modulesandgetattr, falling back tointonly if that also fails.
456-463: Scoped-enum output conversion may return plain ints in cross-module consumersThe new
output_conversionfor scoped enums wraps the integer value viaglobals().get('Name', lambda x: x)(<int>_r), which is fine when the enum class is in the current module’s globals, but in cross-module wrappers that don’t expose the enum symbol this will quietly return a bareintinstead of an enum instance. That’s likely acceptable given the IntEnum–intcompatibility and matches your comment, but it’s worth calling out as an intentional behavioral change: downstream code can no longer rely onisinstance(result, EnumClass)across modules unlessEnumClassis explicitly re-exported into the consumer module.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
autowrap/CodeGenerator.pyautowrap/ConversionProvider.pytests/test_code_generator.py
⏰ 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.1.0, 3.13)
- GitHub Check: test (==3.1.0, 3.11)
- GitHub Check: test (==3.2.0, 3.12)
- GitHub Check: test (==3.1.0, 3.12)
- GitHub Check: test (==3.1.0, 3.10)
- GitHub Check: test (==3.2.0, 3.11)
- GitHub Check: test (==3.2.0, 3.10)
🔇 Additional comments (3)
autowrap/CodeGenerator.py (1)
2041-2077: create_foreign_enum_imports no-op is clearly documented and consistentThe function is now an explicit, documented no-op, which cleanly centralizes enum import behavior into the converters while avoiding circular imports. Keeping the call site but making the implementation inert is reasonable for backward compatibility.
tests/test_code_generator.py (2)
645-747: Test correctly pins create_foreign_enum_imports as a no-opThis test now explicitly asserts that
create_foreign_enum_imports()produces no Python-level imports, which is exactly the new contract and guards against reintroducing circular imports in future changes. Scanningtop_level_pyx_codeis the right place to enforce this.
752-979: Cross-module scoped-enum test nicely validates late binding, but depends on enum globals being presentThe integration test does a good job of pinning the new behavior: it forbids module-level enum imports in
EnumConsumer.pyx, requires theglobals().get(...)pattern, and then exercises real cross-module usage at runtime. One subtle dependency is that the negative checks (runner.isHighPriority(TaskStatus.PENDING)etc. expectingAssertionError) only hold if the relevant enum classes are actually bound intoEnumConsumer’s globals so thatglobals().get('...')resolves to the enum type rather than falling back tointas per the updatedEnumConverter. It’s worth double-checking that the codegen path does establish these bindings for all such enums; otherwise those assertions will start silently accepting mismatched IntEnums.
|
Hmmm not 100% sure but it looks like it is kind of ok although very ugly. I am actually incredibly surprised that we did not have a circular import yet. Ask the AI how/why this works for normal classes. I bet we have normal classes that span module boundaries. Do we do the type check on Cython/C class level? I don't think so. The only correct solution would be to split openms instead and make a real dependency tree (which I planned for a long time already anyway). I.e. not split into N equal parts "randomly". But not sure if we can split OpenMS enough to reach that level of modularity to compile so fast. |
|
ow I can explain the key difference. The issue is specific to scoped enums because of how they're wrapped versus other types: Different Type Checking Approaches From ConversionProvider.py:586-587 (regular classes): This looks similar to scoped enums, but the crucial difference is that cpp_type.base_type here refers to a Cython cdef class - these are C extension types that exist at the Cython/C level and don't require Python module imports. From ConversionProvider.py:406-408 (non-scoped enums): Non-scoped enums just check if the integer value is valid - no class reference at all. Scoped enums are unique because they're wrapped as IntEnum Python classes (for better Python ergonomics), and these are pure Python objects that need to be imported across modules - triggering the circular import problem when modules depend on each other during initialization. Scoped enums are the only case where autowrap generates code that references a Python class (IntEnum) defined in another autowrap module. Everything else either:
|
|
Interesting but how does this work? The input to the function in the pyx is a python object, e.g. MSExperiment. How does |
|
@jpfeuffer Good question! The key insight is that there are two different names for each wrapped class:
Looking at generated code (e.g., # C++ type imported with underscore prefix
from libcpp_test cimport Int as _Int
# Python wrapper class (same name, no underscore)
cdef class Int:
cdef shared_ptr[_Int] inst
...
# isinstance check uses the cdef class name (no underscore)
assert isinstance(i, Int), 'arg i wrong type'So Why no
Why scoped enums are different: |
Changes in this release: - Fix inheritance documentation with Sphinx RST syntax and method grouping (#231) - Fix circular import issue with cross-module scoped enum type checking (#238) - Fix cross-module enum class imports for type assertions (#237) - Add test for enum-based overload resolution (#234) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Summary
PR #237 introduced module-level Python imports for scoped enum classes across modules. While this fixed the Cython compile-time error, it causes circular import errors at runtime when modules form import chains during initialization.
Problem
When autowrap generates multi-module builds (like pyOpenMS with
_pyopenms_1through_pyopenms_8), the modules import each other during initialization:_PyChecksumTypefrom Module 3, Module 3 hasn't finished initializing yetThis results in:
Solution
Instead of module-level imports, use
globals().get()for late binding:ConversionProvider.py - Modified
type_check_expression():CodeGenerator.py - Made
create_foreign_enum_imports()a no-op with documentation explaining why.This approach:
globals().get()is always valid Python syntax)intwhich works for IntEnum values (IntEnum inherits from int)Testing
test_code_generator.pyto verify the new behaviorRelated
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
Bug Fixes
Refactor
Tests
✏️ Tip: You can customize this high-level summary in your review settings.