@@ -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 )
0 commit comments