Skip to content

Commit bac10f9

Browse files
Fix create_foreign_enum_imports: scoped check, naming, and output destination
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 <noreply@anthropic.com>
1 parent 0a38f40 commit bac10f9

File tree

6 files changed

+415
-6
lines changed

6 files changed

+415
-6
lines changed

autowrap/CodeGenerator.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,7 +2046,8 @@ def create_foreign_enum_imports(self):
20462046
Python-level imports for these enum classes.
20472047
20482048
Note: This is separate from create_foreign_cimports() which handles Cython-level
2049-
cimports. The _PyEnumName classes are pure Python and need regular imports.
2049+
cimports. Scoped enum classes are pure Python IntEnum subclasses and need
2050+
regular imports. Unscoped enums are cdef classes and use cimport instead.
20502051
"""
20512052
code = Code()
20522053
L.info("Create foreign enum imports for module %s" % self.target_path)
@@ -2058,13 +2059,17 @@ def create_foreign_enum_imports(self):
20582059
if os.path.basename(self.target_path).split(".pyx")[0] != module:
20592060
for resolved in self.all_decl[module]["decls"]:
20602061
if resolved.__class__ in (ResolvedEnum,):
2061-
if not resolved.wrap_ignore:
2062-
# Generate Python import for the _Py prefixed enum class
2063-
# This is needed for isinstance() checks in type assertions
2064-
py_name = "_Py" + resolved.name
2062+
# Only import scoped enums (Python IntEnum classes)
2063+
# Unscoped enums are cdef classes and use cimport
2064+
if resolved.scoped and not resolved.wrap_ignore:
2065+
# Determine the correct Python name based on wrap-attach
2066+
if resolved.cpp_decl.annotations.get("wrap-attach"):
2067+
py_name = "_Py" + resolved.name
2068+
else:
2069+
py_name = resolved.name
20652070
code.add("from $mname import $py_name", locals())
20662071

2067-
self.top_level_code.append(code)
2072+
self.top_level_pyx_code.append(code)
20682073

20692074
def create_cimports(self):
20702075
self.create_std_cimports()

tests/test_code_generator.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,3 +640,268 @@ def test_enum_class_forward_declaration(tmpdir):
640640

641641
finally:
642642
os.chdir(curdir)
643+
644+
645+
def test_create_foreign_enum_imports():
646+
"""
647+
Test that create_foreign_enum_imports() generates correct Python imports for
648+
scoped enums used across multiple modules.
649+
650+
This test verifies:
651+
1. Scoped enums WITH wrap-attach get '_Py' prefix (e.g., '_PyStatus')
652+
2. Scoped enums WITHOUT wrap-attach keep original name (e.g., 'Status')
653+
3. Unscoped enums are NOT imported (they use cimport via create_foreign_cimports)
654+
4. wrap-ignored enums are skipped
655+
5. Imports are added to top_level_pyx_code (for .pyx), not top_level_code (for .pxd)
656+
657+
Background:
658+
-----------
659+
When autowrap splits classes across multiple output modules for parallel
660+
compilation, scoped enum classes (e.g., _PyPolarity, _PySpectrumType) may be
661+
defined in one module but used in isinstance() type assertions in another.
662+
663+
Scoped enums are implemented as Python IntEnum subclasses and need regular
664+
Python imports. Unscoped enums are cdef classes and use Cython cimport.
665+
"""
666+
import tempfile
667+
import shutil
668+
from autowrap.CodeGenerator import CodeGenerator
669+
from autowrap.DeclResolver import ResolvedEnum
670+
from autowrap.PXDParser import EnumDecl
671+
672+
# Create a temporary directory for generated files
673+
test_dir = tempfile.mkdtemp()
674+
try:
675+
# Create mock EnumDecl objects for testing
676+
def make_enum_decl(name, scoped, annotations=None):
677+
"""Helper to create mock EnumDecl objects."""
678+
if annotations is None:
679+
annotations = {}
680+
decl = EnumDecl(
681+
name=name,
682+
scoped=scoped,
683+
items=[("A", 0), ("B", 1)],
684+
annotations=annotations,
685+
pxd_path="/fake/path.pxd"
686+
)
687+
return decl
688+
689+
# Create mock ResolvedEnum objects
690+
def make_resolved_enum(name, scoped, wrap_ignore=False, wrap_attach=None):
691+
"""Helper to create mock ResolvedEnum objects."""
692+
annotations = {"wrap-ignore": wrap_ignore}
693+
if wrap_attach:
694+
annotations["wrap-attach"] = wrap_attach
695+
decl = make_enum_decl(name, scoped, annotations)
696+
resolved = ResolvedEnum(decl)
697+
resolved.pxd_import_path = "module1"
698+
return resolved
699+
700+
# Test enums in module1 (source module)
701+
scoped_with_attach = make_resolved_enum("Status", scoped=True, wrap_attach="SomeClass")
702+
scoped_no_attach = make_resolved_enum("Priority", scoped=True, wrap_attach=None)
703+
unscoped_enum = make_resolved_enum("OldEnum", scoped=False)
704+
ignored_enum = make_resolved_enum("IgnoredEnum", scoped=True, wrap_ignore=True)
705+
706+
module1_decls = [scoped_with_attach, scoped_no_attach, unscoped_enum, ignored_enum]
707+
708+
# Set up multi-module scenario
709+
# module1 contains the enums, module2 needs to import them
710+
master_dict = {
711+
"module1": {"decls": module1_decls, "addons": [], "files": []},
712+
"module2": {"decls": [], "addons": [], "files": []},
713+
}
714+
715+
# Create CodeGenerator for module2
716+
target = os.path.join(test_dir, "module2.pyx")
717+
cg = CodeGenerator(
718+
[], # module2 has no decls of its own
719+
{}, # empty instance_map
720+
pyx_target_path=target,
721+
all_decl=master_dict,
722+
)
723+
724+
# Call the method we're testing
725+
cg.create_foreign_enum_imports()
726+
727+
# Get the generated code from top_level_pyx_code (NOT top_level_code)
728+
pyx_generated_code = ""
729+
for code_block in cg.top_level_pyx_code:
730+
pyx_generated_code += code_block.render()
731+
732+
pxd_generated_code = ""
733+
for code_block in cg.top_level_code:
734+
pxd_generated_code += code_block.render()
735+
736+
# Test 1: Scoped enum WITH wrap-attach should use _Py prefix
737+
assert "from module1 import _PyStatus" in pyx_generated_code, (
738+
f"Expected 'from module1 import _PyStatus' for scoped enum with wrap-attach. "
739+
f"Generated pyx code:\n{pyx_generated_code}"
740+
)
741+
742+
# Test 2: Scoped enum WITHOUT wrap-attach should use original name
743+
assert "from module1 import Priority" in pyx_generated_code, (
744+
f"Expected 'from module1 import Priority' for scoped enum without wrap-attach. "
745+
f"Generated pyx code:\n{pyx_generated_code}"
746+
)
747+
748+
# Test 3: Unscoped enum should NOT be imported (uses cimport instead)
749+
assert "OldEnum" not in pyx_generated_code, (
750+
f"Unscoped enum 'OldEnum' should not be in Python imports (uses cimport). "
751+
f"Generated pyx code:\n{pyx_generated_code}"
752+
)
753+
754+
# Test 4: wrap-ignored enum should NOT be imported
755+
assert "IgnoredEnum" not in pyx_generated_code, (
756+
f"wrap-ignored enum 'IgnoredEnum' should not be in imports. "
757+
f"Generated pyx code:\n{pyx_generated_code}"
758+
)
759+
760+
# Test 5: Imports should be in top_level_pyx_code, NOT top_level_code
761+
# (top_level_code goes to .pxd, top_level_pyx_code goes to .pyx)
762+
assert "_PyStatus" not in pxd_generated_code, (
763+
f"Enum Python imports should not be in top_level_code (pxd). "
764+
f"Generated pxd code:\n{pxd_generated_code}"
765+
)
766+
assert "Priority" not in pxd_generated_code or "cimport" in pxd_generated_code, (
767+
f"Enum Python imports should not be in top_level_code (pxd). "
768+
f"Generated pxd code:\n{pxd_generated_code}"
769+
)
770+
771+
print("Test passed: create_foreign_enum_imports generates correct imports")
772+
773+
finally:
774+
shutil.rmtree(test_dir, ignore_errors=True)
775+
776+
777+
def test_cross_module_scoped_enum_imports(tmpdir):
778+
"""
779+
Integration test for cross-module scoped enum imports.
780+
781+
This test verifies that create_foreign_enum_imports() correctly generates
782+
Python imports for scoped enums used across module boundaries:
783+
784+
1. Scoped enum WITH wrap-attach (Task.TaskStatus):
785+
- Should generate: from .EnumProvider import _PyTaskStatus
786+
- Used in isinstance() checks as _PyTaskStatus
787+
788+
2. Scoped enum WITHOUT wrap-attach (Priority):
789+
- Should generate: from .EnumProvider import Priority
790+
- Used in isinstance() checks as Priority
791+
792+
The test:
793+
- Parses two modules: EnumProvider (defines enums) and EnumConsumer (uses enums)
794+
- Generates Cython code for both modules
795+
- Verifies EnumConsumer.pyx contains correct Python imports for both enum types
796+
- Runs Cython compilation to ensure the generated code is valid
797+
"""
798+
import shutil
799+
import re
800+
801+
test_dir = tmpdir.strpath
802+
enum_test_files = os.path.join(
803+
os.path.dirname(__file__), "test_files", "enum_cross_module"
804+
)
805+
806+
curdir = os.getcwd()
807+
os.chdir(test_dir)
808+
809+
# Copy test files to temp directory
810+
for f in ["EnumProvider.hpp", "EnumProvider.pxd", "EnumConsumer.hpp", "EnumConsumer.pxd"]:
811+
src = os.path.join(enum_test_files, f)
812+
dst = os.path.join(test_dir, f)
813+
shutil.copy(src, dst)
814+
815+
try:
816+
mnames = ["EnumProvider", "EnumConsumer"]
817+
818+
# Step 1: Parse all pxd files
819+
pxd_files = ["EnumProvider.pxd", "EnumConsumer.pxd"]
820+
full_pxd_files = [os.path.join(test_dir, f) for f in pxd_files]
821+
decls, instance_map = autowrap.parse(full_pxd_files, test_dir, num_processes=1)
822+
823+
# Step 2: Map declarations to their source modules
824+
pxd_decl_mapping = {}
825+
for de in decls:
826+
tmp = pxd_decl_mapping.get(de.cpp_decl.pxd_path, [])
827+
tmp.append(de)
828+
pxd_decl_mapping[de.cpp_decl.pxd_path] = tmp
829+
830+
masterDict = {}
831+
masterDict[mnames[0]] = {
832+
"decls": pxd_decl_mapping.get(full_pxd_files[0], []),
833+
"addons": [],
834+
"files": [full_pxd_files[0]],
835+
}
836+
masterDict[mnames[1]] = {
837+
"decls": pxd_decl_mapping.get(full_pxd_files[1], []),
838+
"addons": [],
839+
"files": [full_pxd_files[1]],
840+
}
841+
842+
# Step 3: Generate Cython code for each module
843+
converters = []
844+
generated_pyx_content = {}
845+
846+
for modname in mnames:
847+
m_filename = "%s.pyx" % modname
848+
cimports, manual_code = autowrap.Main.collect_manual_code(masterDict[modname]["addons"])
849+
autowrap.Main.register_converters(converters)
850+
autowrap_include_dirs = autowrap.generate_code(
851+
masterDict[modname]["decls"],
852+
instance_map,
853+
target=m_filename,
854+
debug=True,
855+
manual_code=manual_code,
856+
extra_cimports=cimports,
857+
all_decl=masterDict,
858+
add_relative=True,
859+
)
860+
masterDict[modname]["inc_dirs"] = autowrap_include_dirs
861+
862+
# Read generated pyx file
863+
with open(m_filename, "r") as f:
864+
generated_pyx_content[modname] = f.read()
865+
866+
# Step 4: Verify EnumConsumer.pyx has correct Python imports
867+
consumer_pyx = generated_pyx_content.get("EnumConsumer", "")
868+
869+
# Test 1: Scoped enum WITH wrap-attach should be imported with _Py prefix
870+
# Task_TaskStatus has wrap-attach:Task, so it becomes _PyTask_TaskStatus in Python
871+
# (the _Py prefix is added to the full Cython enum name)
872+
assert "from .EnumProvider import _PyTask_TaskStatus" in consumer_pyx, (
873+
f"Expected 'from .EnumProvider import _PyTask_TaskStatus' for scoped enum with wrap-attach.\n"
874+
f"EnumConsumer.pyx content:\n{consumer_pyx}"
875+
)
876+
877+
# Test 2: Scoped enum WITHOUT wrap-attach should be imported with original name
878+
assert "from .EnumProvider import Priority" in consumer_pyx, (
879+
f"Expected 'from .EnumProvider import Priority' for scoped enum without wrap-attach.\n"
880+
f"EnumConsumer.pyx content:\n{consumer_pyx}"
881+
)
882+
883+
# Test 3: Verify isinstance checks use the correct enum class names
884+
# For wrap-attach enum: isinstance(s, _PyTask_TaskStatus)
885+
assert "isinstance(s, _PyTask_TaskStatus)" in consumer_pyx, (
886+
f"Expected isinstance check with _PyTask_TaskStatus for wrap-attach enum.\n"
887+
f"EnumConsumer.pyx content:\n{consumer_pyx}"
888+
)
889+
890+
# For non-wrap-attach enum: isinstance(p, Priority)
891+
assert "isinstance(p, Priority)" in consumer_pyx, (
892+
f"Expected isinstance check with Priority for non-wrap-attach enum.\n"
893+
f"EnumConsumer.pyx content:\n{consumer_pyx}"
894+
)
895+
896+
# Step 5: Run Cython compilation to verify the generated code is valid
897+
for modname in mnames:
898+
m_filename = "%s.pyx" % modname
899+
autowrap_include_dirs = masterDict[modname]["inc_dirs"]
900+
autowrap.Main.run_cython(
901+
inc_dirs=autowrap_include_dirs, extra_opts=None, out=m_filename
902+
)
903+
904+
print("Test passed: Cross-module scoped enum imports work correctly")
905+
906+
finally:
907+
os.chdir(curdir)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Test file for cross-module scoped enum imports.
3+
*
4+
* This file uses enums defined in EnumProvider.hpp to test
5+
* cross-module enum import functionality.
6+
*/
7+
8+
#ifndef ENUM_CONSUMER_HPP
9+
#define ENUM_CONSUMER_HPP
10+
11+
#include "EnumProvider.hpp"
12+
13+
class TaskRunner {
14+
public:
15+
TaskRunner() {}
16+
17+
// Method using standalone enum (Priority)
18+
bool isHighPriority(Priority p) {
19+
return p == Priority::HIGH;
20+
}
21+
22+
// Method using class-attached enum (Task::TaskStatus)
23+
bool isCompleted(Task::TaskStatus s) {
24+
return s == Task::TaskStatus::COMPLETED;
25+
}
26+
27+
// Method returning standalone enum
28+
Priority getDefaultPriority() {
29+
return Priority::MEDIUM;
30+
}
31+
32+
// Method returning class-attached enum
33+
Task::TaskStatus getDefaultStatus() {
34+
return Task::TaskStatus::PENDING;
35+
}
36+
};
37+
38+
#endif // ENUM_CONSUMER_HPP
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# cython: language_level=3
2+
#
3+
# Test file for cross-module scoped enum imports.
4+
#
5+
# This module imports and uses enums from EnumProvider to test
6+
# that create_foreign_enum_imports() generates correct Python imports.
7+
#
8+
from libcpp cimport bool
9+
from EnumProvider cimport Priority, Task_TaskStatus
10+
11+
cdef extern from "EnumConsumer.hpp":
12+
13+
cdef cppclass TaskRunner:
14+
TaskRunner()
15+
bool isHighPriority(Priority p)
16+
bool isCompleted(Task_TaskStatus s)
17+
Priority getDefaultPriority()
18+
Task_TaskStatus getDefaultStatus()

0 commit comments

Comments
 (0)