diff --git a/CHANGELOG.md b/CHANGELOG.md index 8857b2f..bc0692a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +autowrap 0.24.0 (unreleased) + +- Support for enums with the same name in different C++ namespaces using + scoped enum declarations with `wrap-as` annotation for renaming +- Support for arbitrary key types in `operator[]` (getitem/setitem), not + just integer types like `size_t` + autowrap 0.23.0 Support for Cython 3.1! This means the removal of some py2 compatibility code, no more python distinction between long and int, some fixes to multiline comment processing. diff --git a/tests/test_code_generator.py b/tests/test_code_generator.py index 97efcea..03b0050 100644 --- a/tests/test_code_generator.py +++ b/tests/test_code_generator.py @@ -50,6 +50,29 @@ def test_enums(): + """ + Test wrapping of C++ enums, including enums with the same name in different namespaces. + + This test demonstrates how autowrap handles: + 1. Scoped enums (enum class) from C++ mapped to Python Enum classes + 2. Enums with the same name in different namespaces (Foo::MyEnum vs Foo2::MyEnum) + 3. Type-safe enum validation (passing wrong enum type raises AssertionError) + 4. Enum documentation via wrap-doc annotation + + Pattern for wrapping namespaced enums in .pxd files: + cpdef enum class Foo_MyEnum "Foo::MyEnum": + # wrap-attach: + # Foo + # wrap-as: + # MyEnum + A + B + C + + This creates Foo.MyEnum in Python that maps to Foo::MyEnum in C++. + + See tests/test_files/enums.pxd for the full example. + """ if int(cython_version[0]) < 3: return target = os.path.join(test_files, "enums.pyx") @@ -66,12 +89,26 @@ def test_enums(): include_dirs, ) + # Test 1: Enums with same name in different namespaces are both accessible + # Foo.MyEnum and Foo2.MyEnum are separate enum types + assert mod.Foo.MyEnum + assert mod.Foo.MyEnum.B + assert mod.Foo2.MyEnum + assert mod.Foo2.MyEnum.D + + # Test 2: Enum documentation is preserved via wrap-doc foo = mod.Foo() my_enum = mod.Foo.MyEnum assert "Testing Enum documentation." in my_enum.__doc__ + + # Test 3: Correct enum type is accepted myenum_a = mod.Foo.MyEnum.A - myenum2_a = mod.Foo.MyEnum2.A assert foo.enumToInt(myenum_a) == 1 + + # Test 4: Wrong enum type raises AssertionError (type-safe validation) + # Even though MyEnum2.A has the same numeric value as MyEnum.A, + # it's a different enum type and should be rejected + myenum2_a = mod.Foo.MyEnum2.A with pytest.raises(AssertionError): foo.enumToInt(myenum2_a) diff --git a/tests/test_files/enums.hpp b/tests/test_files/enums.hpp index 569bcf3..cea4f6d 100644 --- a/tests/test_files/enums.hpp +++ b/tests/test_files/enums.hpp @@ -1,16 +1,38 @@ +/** + * ============================================================================= + * Example: C++ Enums in Different Namespaces + * ============================================================================= + * + * This file demonstrates C++ code with enums that have the same name but exist + * in different scopes (class Foo and namespace Foo2). See enums.pxd for how to + * wrap these with autowrap to avoid naming conflicts in Python. + * + * C++ Structure: + * - Foo::MyEnum (scoped enum inside class Foo) + * - Foo::MyEnum2 (second enum inside class Foo) + * - Foo2::MyEnum (scoped enum inside namespace Foo2 - same name as Foo::MyEnum) + * + * ============================================================================= + */ + +// Class with nested scoped enums class Foo { public: + // First enum - will be accessible as Foo.MyEnum in Python enum class MyEnum { - A,B,C + A, B, C }; + // Second enum in same class - will be accessible as Foo.MyEnum2 in Python enum class MyEnum2 { - A,B,C + A, B, C }; + // Method that accepts MyEnum - demonstrates type-safe enum usage + // In Python, passing Foo2.MyEnum.A will raise AssertionError (wrong type) int enumToInt(MyEnum e) { switch(e) @@ -19,5 +41,18 @@ class Foo case MyEnum::B : return 2; case MyEnum::C : return 3; } + return 0; // unreachable, but silences compiler warning + }; +}; + +// Separate namespace with an enum of the same name as Foo::MyEnum +// This demonstrates the namespace collision problem that autowrap solves +namespace Foo2 +{ + // This enum has the same name "MyEnum" as Foo::MyEnum + // In Python, it will be accessible as Foo2.MyEnum (no conflict) + enum class MyEnum + { + A, C, D }; -}; \ No newline at end of file +}; diff --git a/tests/test_files/enums.pxd b/tests/test_files/enums.pxd index d2006ff..14e6796 100644 --- a/tests/test_files/enums.pxd +++ b/tests/test_files/enums.pxd @@ -1,25 +1,104 @@ # cython: language_level=2 +# +# ============================================================================= +# Example: Wrapping C++ Enums in Different Namespaces +# ============================================================================= +# +# This file demonstrates how to wrap C++ enums that have the same name but +# exist in different namespaces (e.g., Foo::MyEnum and Foo2::MyEnum). +# +# THE PROBLEM: +# When two C++ classes/namespaces define enums with the same name, Cython +# cannot distinguish them at the module level, causing naming conflicts. +# +# THE SOLUTION: +# Use a unique Cython identifier with a C++ name string to map to the +# actual C++ enum, then use wrap-as to expose it with the desired Python name. +# +# PATTERN: +# cpdef enum class "": +# # wrap-attach: +# # +# # wrap-as: +# # +# +# EXAMPLE: +# For Foo::MyEnum and Foo2::MyEnum, declare them as: +# - Foo_MyEnum "Foo::MyEnum" -> exposed as Foo.MyEnum in Python +# - Foo2_MyEnum "Foo2::MyEnum" -> exposed as Foo2.MyEnum in Python +# +# IMPORTANT: +# - Use `cpdef enum class` (not `cpdef enum`) for scoped enums +# - Scoped enums generate Python Enum classes with proper type checking +# - The wrap-attach directive attaches the enum to the specified class +# - The wrap-as directive controls the Python-visible name +# +# RESULT IN PYTHON: +# foo = Foo() +# foo.enumToInt(Foo.MyEnum.A) # Works - correct enum type +# foo.enumToInt(Foo2.MyEnum.A) # Raises AssertionError - wrong enum type +# +# ============================================================================= cdef extern from "enums.hpp": cdef cppclass Foo: + # Method accepts Foo_MyEnum (which maps to Foo::MyEnum in C++) + int enumToInt(Foo_MyEnum e) + +cdef extern from "enums.hpp": + cdef cppclass Foo2: + # Foo2 has no methods, but we still wrap it to attach its enum + pass - int enumToInt(MyEnum e) +# ----------------------------------------------------------------------------- +# Enums in the "Foo" namespace +# ----------------------------------------------------------------------------- cdef extern from "enums.hpp" namespace "Foo": - cpdef enum class MyEnum "Foo::MyEnum": + # Foo::MyEnum - attached to class Foo, exposed as Foo.MyEnum + cpdef enum class Foo_MyEnum "Foo::MyEnum": # wrap-attach: # Foo # + # wrap-as: + # MyEnum + # # wrap-doc: # Testing Enum documentation. A B C - cpdef enum class MyEnum2 "Foo::MyEnum2": + # Foo::MyEnum2 - a second enum in the same class + cpdef enum class Foo_MyEnum2 "Foo::MyEnum2": # wrap-attach: # Foo + # + # wrap-as: + # MyEnum2 A B - C \ No newline at end of file + C + + +# ----------------------------------------------------------------------------- +# Enums in the "Foo2" namespace +# ----------------------------------------------------------------------------- +cdef extern from "enums.hpp" namespace "Foo2": + + # Foo2::MyEnum - same name as Foo::MyEnum but in different namespace + # Attached to Foo2 class, exposed as Foo2.MyEnum (no conflict with Foo.MyEnum) + cpdef enum class Foo2_MyEnum "Foo2::MyEnum": + # wrap-attach: + # Foo2 + # + # wrap-as: + # MyEnum + # + # wrap-doc: + # This is a second enum in another namespace. + A + C + D +