Skip to content

Conversation

@timosachsenberg
Copy link
Collaborator

@timosachsenberg timosachsenberg commented Jan 7, 2026

Summary

When autowrap splits classes across multiple output modules for parallel compilation, enum classes (e.g., _PyPolarity, _PySpectrumType) may be defined in one module but used in isinstance() type assertions in another module. This causes Cython compilation errors like:

pyopenms/_pyopenms_5.pyx:5313:32: undeclared name not builtin: _PyPolarity

Solution

This PR adds create_foreign_enum_imports() which generates the necessary Python-level imports for enum classes across modules:

from ._pyopenms_2 import _PyPolarity

This is separate from create_foreign_cimports() which only handles Cython-level cimports. The _PyEnumName classes are pure Python IntEnum subclasses and need regular Python imports.

Testing

Tested with pyOpenMS build on OpenMS develop branch (PR converting plain enums to enum class):

  • All 8 pyopenms modules compile successfully
  • Cross-module enum imports are generated in pxd files
  • Runtime tests pass for SpectrumSettings.setType(), IonSource.setPolarity(), etc.

Fixes #236

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Generate Python-side imports for scoped enums and emit Python enum instances for scoped enums while preserving prior behavior for unscoped enums.
  • Bug Fixes

    • Avoid duplicate enum-import emission across generated files.
  • Tests

    • Added unit and integration tests plus cross-module fixtures to validate import generation and enum conversion.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Adds generation of cross-module Python imports for scoped/wrapped enums, updates enum-to-Python conversion to use Python enum wrappers for scoped enums, invokes the import generation during .pyx output, and adds unit/integration tests and fixtures. A duplicate definition of the new method was introduced accidentally.

Changes

Cohort / File(s) Summary
Code generation — foreign enum imports
autowrap/CodeGenerator.py
Added create_foreign_enum_imports() and call in create_pyx_file() to emit Python from <module> import <EnumName> or from <module> import _Py<EnumName> for scoped enums used across modules. Note: identical method was added twice (duplicate definition) in the same file.
Enum conversion logic
autowrap/ConversionProvider.py
EnumConverter.output_conversion updated to wrap scoped enums into Python enum classes when producing output conversion code (_Py{Name}(<int_expr>) if wrap-attach, otherwise {Name}(<int_expr>)). Unscoped enums retain prior int-cast behavior.
Tests & fixtures — unit & integration
tests/test_code_generator.py, tests/test_files/enum_cross_module/*
Added test_create_foreign_enum_imports() and test_cross_module_scoped_enum_imports(tmpdir) plus C++ headers/pxd/pyx fixtures (EnumProvider.*, EnumConsumer.*) to verify cross-module import emission, wrap-attach naming (_Py*), wrap-ignore skipping, and generated code compilation.

Sequence Diagram(s)

(Skipped — changes are code-generation and tests; they do not introduce a new multi-component runtime control flow that benefits from a sequence diagram.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Suggested reviewers

  • jpfeuffer
  • poshul

Poem

"🐇 I nibbled through code, found a missing line,

Pulled in _Py names where enums must align,
Modules now chat where they once stood apart,
A tiny hop fixed a crucial part,
Hooray — code compiles with a rabbit's heart!"

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR contains one notable issue: duplicate definition of create_foreign_enum_imports() method in CodeGenerator.py, which is unrelated to the stated objectives and represents an error rather than out-of-scope functionality. Remove the duplicate create_foreign_enum_imports() method definition in CodeGenerator.py, keeping only one copy of the method to avoid compilation errors and confusion.
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 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 enum class imports for type assertions' directly describes the main change: generating Python-level imports for enum classes used across different generated modules to fix Cython compilation errors.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #236: generates cross-module Python imports for scoped enum classes via create_foreign_enum_imports(), handles wrap-attach enums with _Py prefixes, skips wrap-ignore enums, and includes comprehensive tests validating the solution.

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

✨ Finishing touches
  • 📝 Generate docstrings

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: 1

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd815fb and bde92f7.

📒 Files selected for processing (1)
  • autowrap/CodeGenerator.py
🧰 Additional context used
🧬 Code graph analysis (1)
autowrap/CodeGenerator.py (2)
autowrap/Code.py (2)
  • Code (41-90)
  • add (56-75)
autowrap/DeclResolver.py (1)
  • ResolvedEnum (128-139)
⏰ 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.1.0, 3.10)
  • GitHub Check: test (==3.2.0, 3.11)
  • GitHub Check: test (==3.2.0, 3.13)
  • GitHub Check: test (==3.2.0, 3.10)
  • 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.13)
🔇 Additional comments (2)
autowrap/CodeGenerator.py (2)

281-281: LGTM! Appropriate placement for cross-module enum imports.

The call is correctly positioned after create_foreign_cimports() and before code generation, ensuring enum imports are available when needed.


2040-2067: Inconsistency: AI summary mentions duplicate method, but only one definition found.

The AI summary states "A second identical public method create_foreign_enum_imports(self) was added (duplicated definition)", but only one definition is visible in the provided code (lines 2040-2067). If a duplicate exists elsewhere, it should be removed to avoid conflicts.

This commit fixes two issues with enum class support in autowrap:

1. Cross-module imports (CodeGenerator.py):
   When autowrap splits classes across multiple output modules, enum classes
   may be defined in one module but used in type assertions in another.
   Added create_foreign_enum_imports() to generate necessary Python imports.

2. Return type conversion (ConversionProvider.py):
   When C++ methods return enum class types, the generated code was returning
   raw int values instead of wrapping them in the Python enum class. This
   caused type mismatches when passing return values to setter methods.

   Before: py_result = <int>_r
   After:  py_result = _PyEnumName(<int>_r)

Fixes #236

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@timosachsenberg timosachsenberg force-pushed the fix-cross-module-enum-imports branch from bde92f7 to 0a38f40 Compare January 7, 2026 20:36
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: 1

🤖 Fix all issues with AI agents
In @autowrap/CodeGenerator.py:
- Around line 2040-2067: In create_foreign_enum_imports(), restrict Python-level
imports to scoped enums only (i.e., check resolved.is_scoped or equivalent)
since unscoped enums use Cython cimport; pick the correct import symbol name
based on wrap_attach (use "_Py" + resolved.name when resolved.wrap_attach is
true, otherwise use resolved.name), keep the wrap_ignore guard, and append the
generated Code() to self.top_level_pyx_code (not self.top_level_code) so the
import lands in the .pyx output.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bde92f7 and 0a38f40.

📒 Files selected for processing (2)
  • autowrap/CodeGenerator.py
  • autowrap/ConversionProvider.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.1.0, 3.13)
  • GitHub Check: test (==3.2.0, 3.13)
  • GitHub Check: test (==3.2.0, 3.12)
  • GitHub Check: test (==3.1.0, 3.12)
  • GitHub Check: test (==3.2.0, 3.10)
  • GitHub Check: test (==3.1.0, 3.11)
  • GitHub Check: test (==3.2.0, 3.11)
  • GitHub Check: test (==3.1.0, 3.10)
🔇 Additional comments (2)
autowrap/ConversionProvider.py (1)

434-442: LGTM! Correct enum output conversion for scoped enums.

The implementation properly wraps scoped enum values in their Python enum class wrapper while maintaining backward compatibility for non-scoped enums. The naming logic (_Py prefix for wrap-attach, original name otherwise) is consistent with matching_python_type_full (lines 400-403) and type_check_expression (lines 410-416).

autowrap/CodeGenerator.py (1)

2040-2067: No action needed. Verification confirms only one definition of create_foreign_enum_imports(self) exists at line 2040 in autowrap/CodeGenerator.py. There is no duplicate method definition.

Likely an incorrect or invalid review comment.

@jpfeuffer
Copy link
Contributor

Check coderabbit and if possible add tests.
Not sure if we have multi module tests already. I think so.

…tination

Address three issues identified by CodeRabbit review:

1. Add scoped enum check: Only scoped enums (Python IntEnum classes) need
   Python imports. Unscoped enums are cdef classes and use cimport.

2. Fix naming logic: Use _Py prefix only when wrap-attach annotation is
   present, otherwise use the original enum name.

3. Fix output destination: Append to top_level_pyx_code (for .pyx) instead
   of top_level_code (for .pxd) since these are Python imports.

Add tests:
- Unit test: test_create_foreign_enum_imports() tests the method in isolation
- Integration test: test_cross_module_scoped_enum_imports() tests full
  multi-module scenario with both wrap-attach and non-wrap-attach enums

Co-Authored-By: Claude Opus 4.5 <[email protected]>
inc_dirs=autowrap_include_dirs, extra_opts=None, out=m_filename
)

print("Test passed: Cross-module scoped enum imports work correctly")
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we import the modules in the end and do some real python tests?
Otherwise looks great

Extend test_cross_module_scoped_enum_imports to:
- Use cythonize for proper package compilation with relative imports
- Run runtime tests that verify enums work across module boundaries
- Test Priority enum (no wrap-attach) and Task.TaskStatus (with wrap-attach)
- Test isinstance type checking with wrong enum types
- Skip runtime tests gracefully if build environment is incomplete

The generated code verification (import statements, isinstance checks) runs
unconditionally; runtime tests are skipped if Cython compilation fails due
to missing dependencies like AutowrapRefHolder.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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 (4)
tests/test_code_generator.py (4)

766-769: Weak assertion may mask false positives.

The condition "Priority" not in pxd_generated_code or "cimport" in pxd_generated_code could pass incorrectly if the pxd contains both an erroneous from ... import Priority statement AND an unrelated cimport statement elsewhere. Consider a stricter check:

Suggested stricter assertion
-        assert "Priority" not in pxd_generated_code or "cimport" in pxd_generated_code, (
-            f"Enum Python imports should not be in top_level_code (pxd). "
-            f"Generated pxd code:\n{pxd_generated_code}"
-        )
+        # Ensure no Python-style imports for enums appear in pxd (cimports are fine)
+        assert "from module1 import Priority" not in pxd_generated_code, (
+            f"Enum Python imports should not be in top_level_code (pxd). "
+            f"Generated pxd code:\n{pxd_generated_code}"
+        )

945-951: Minor cleanup: unused variable and subprocess best practice.

The stdout variable is captured but never used. Also, while shell=True is safe here since the command uses only sys.executable, consider using a list form for clarity and to avoid shell parsing.

Suggested cleanup
         result = subprocess.Popen(
-            f"{sys.executable} setup.py build_ext --force --inplace",
-            shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+            [sys.executable, "setup.py", "build_ext", "--force", "--inplace"],
+            stdout=subprocess.PIPE, stderr=subprocess.PIPE
         )
-        stdout, stderr = result.communicate()
+        _, stderr = result.communicate()

996-1006: Replace assert False with pytest.fail() for robustness.

Using assert False can be stripped by python -O. Since this is a pytest test, pytest.fail() is more appropriate and semantically clearer.

Suggested fix
             # Runtime Test 7: Type checking works - wrong enum type should raise
             try:
                 runner.isHighPriority(EnumProvider.Task.TaskStatus.PENDING)
-                assert False, "Should have raised AssertionError for wrong enum type"
+                pytest.fail("Should have raised AssertionError for wrong enum type")
             except AssertionError as e:
                 assert "wrong type" in str(e)

             try:
                 runner.isCompleted(EnumProvider.Priority.HIGH)
-                assert False, "Should have raised AssertionError for wrong enum type"
+                pytest.fail("Should have raised AssertionError for wrong enum type")
             except AssertionError as e:
                 assert "wrong type" in str(e)

975-981: Stylistic: explicit boolean comparisons.

Static analysis flags == True / == False comparisons. While acceptable in tests for clarity, the idiomatic Python style uses truthy/falsy checks. This is a minor nitpick.

Optional: idiomatic boolean checks
-            assert runner.isHighPriority(EnumProvider.Priority.HIGH) == True
-            assert runner.isHighPriority(EnumProvider.Priority.LOW) == False
+            assert runner.isHighPriority(EnumProvider.Priority.HIGH)
+            assert not runner.isHighPriority(EnumProvider.Priority.LOW)
             assert runner.getDefaultPriority() == EnumProvider.Priority.MEDIUM

             # Runtime Test 4: TaskRunner can use Task.TaskStatus enum from EnumProvider
-            assert runner.isCompleted(EnumProvider.Task.TaskStatus.COMPLETED) == True
-            assert runner.isCompleted(EnumProvider.Task.TaskStatus.PENDING) == False
+            assert runner.isCompleted(EnumProvider.Task.TaskStatus.COMPLETED)
+            assert not runner.isCompleted(EnumProvider.Task.TaskStatus.PENDING)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bac10f9 and 261118c.

📒 Files selected for processing (1)
  • tests/test_code_generator.py
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_code_generator.py (2)
autowrap/CodeGenerator.py (2)
  • CodeGenerator (106-2227)
  • create_foreign_enum_imports (2040-2072)
autowrap/DeclResolver.py (1)
  • ResolvedEnum (128-139)
🪛 Ruff (0.14.10)
tests/test_code_generator.py

945-945: subprocess call with shell=True identified, security issue

(S602)


949-949: Unpacked variable stdout is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


975-975: Avoid equality comparisons to True; use runner.isHighPriority(EnumProvider.Priority.HIGH): for truth checks

Replace with runner.isHighPriority(EnumProvider.Priority.HIGH)

(E712)


976-976: Avoid equality comparisons to False; use not runner.isHighPriority(EnumProvider.Priority.LOW): for false checks

Replace with not runner.isHighPriority(EnumProvider.Priority.LOW)

(E712)


980-980: Avoid equality comparisons to True; use ...: for truth checks

Replace comparison

(E712)


981-981: Avoid equality comparisons to False; use not ...: for false checks

Replace comparison

(E712)


998-998: Do not assert False (python -O removes these calls), raise AssertionError()

Replace assert False

(B011)


1004-1004: Do not assert False (python -O removes these calls), raise AssertionError()

Replace assert False

(B011)

⏰ 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.12)
  • GitHub Check: test (==3.2.0, 3.11)
  • GitHub Check: test (==3.1.0, 3.12)
  • GitHub Check: test (==3.1.0, 3.10)
  • GitHub Check: test (==3.1.0, 3.11)
  • GitHub Check: test (==3.2.0, 3.13)
  • GitHub Check: test (==3.1.0, 3.13)
  • GitHub Check: test (==3.2.0, 3.10)
🔇 Additional comments (1)
tests/test_code_generator.py (1)

645-774: Comprehensive test coverage for cross-module enum imports.

These tests thoroughly validate the new create_foreign_enum_imports() functionality:

  • Unit test (test_create_foreign_enum_imports): Covers wrap-attach prefix naming, non-wrapped enums, unscoped enum exclusion, wrap-ignore exclusion, and correct placement in pyx vs pxd.
  • Integration test (test_cross_module_scoped_enum_imports): End-to-end validation with actual Cython compilation and runtime type checking across module boundaries.

The graceful skip on compilation failure (line 951) is a good pattern for CI environments with incomplete toolchains.

Also applies to: 777-1018

@timosachsenberg
Copy link
Collaborator Author

Test Verification

Verified that the unit test correctly catches the bug by temporarily reverting to the buggy code:

Code State Test Result Reason
Buggy (original) ❌ FAILED pyx_generated_code is empty - imports went to .pxd instead of .pyx
Fixed ✅ PASSED Imports correctly go to top_level_pyx_code

The test validates all three fixes:

  1. Scoped check - Only scoped enums are imported (unscoped use cimport)
  2. Naming - _Py prefix only used for wrap-attach enums
  3. Output destination - Imports go to .pyx file (top_level_pyx_code), not .pxd

Error message with buggy code:

AssertionError: Expected 'from module1 import _PyStatus' for scoped enum with wrap-attach.
Generated pyx code:

assert 'from module1 import _PyStatus' in ''

@timosachsenberg timosachsenberg merged commit e37b334 into master Jan 8, 2026
11 checks passed
@timosachsenberg
Copy link
Collaborator Author

Fix for Cross-Module Scoped Enum Type Checking

I encountered and fixed an issue with scoped enums (enum class) in multi-module builds (e.g., pyOpenMS with _pyopenms_1.pyx through _pyopenms_8.pyx).

Problem

Scoped enums are wrapped as Python IntEnum classes (e.g., _PySpectrumType). When module A uses an enum defined in module B in a type assertion like:

assert isinstance(in_0, _PySpectrumType)

the Python class _PySpectrumType must be available.

Issue 1 - Compile-time: Cython needs the name to be declared/imported, otherwise you get:

undeclared name not builtin: _PySpectrumType

Issue 2 - Runtime: Adding module-level imports like:

from ._pyopenms_3 import _PySpectrumType

causes circular import errors. The modules form an import chain during initialization (1 → 8 → 7 → ... → 2), and when module 2 tries to import from module 3, module 3 hasn't finished initializing yet.

Solution

Instead of module-level imports, use globals().get() for late binding in type assertions:

assert isinstance(in_0, globals().get('_PySpectrumType', int))

This approach:

  • Compiles successfully - globals().get() is always valid Python syntax
  • Resolves at runtime - The enum class is looked up after all modules are loaded
  • Falls back gracefully - int works for IntEnum values (IntEnum inherits from int)

Implementation

ConversionProvider.py - Modified EnumConverter.type_check_expression():

# Before:
return "isinstance(%s, %s)" % (argument_var, name)

# After:
return "isinstance(%s, globals().get('%s', int))" % (argument_var, name)

CodeGenerator.py - The create_foreign_enum_imports() method becomes a no-op (with documentation explaining why).

Results

All 491 pyOpenMS unit tests pass, including 82 static method tests that specifically test enum conversion functions.

timosachsenberg added a commit that referenced this pull request Jan 8, 2026
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]>
timosachsenberg added a commit that referenced this pull request Jan 8, 2026
…#238)

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]>
timosachsenberg added a commit that referenced this pull request Jan 8, 2026
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]>
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 imports fail when classes are split across generated modules

3 participants