diff --git a/.gitignore b/.gitignore index 3c9448d4..2796bfdd 100644 --- a/.gitignore +++ b/.gitignore @@ -163,19 +163,15 @@ develop-eggs #Mr Developer .mr.developer.cfg -# generated py extensions -tests/test_files/gil_testing_wrapper.pyx -tests/test_files/libcpp_stl_test.pyx -tests/test_files/libcpp_utf8_string_test.pyx -tests/test_files/libcpp_utf8_output_string_test.pyx -tests/test_files/iteratorwrapper.pyx -tests/test_files/namespaces.pyx -tests/test_files/number_conv.pyx -tests/test_files/enums.pyx -tests/test_files/wrapped_container_wrapper.pyx -tests/test_files/wrap_len_wrapper.pyx - -# generated typestubs +# generated py extensions and typestubs in tests +tests/test_files/generated/* +!tests/test_files/generated/.gitkeep +!tests/test_files/generated/numpy_vector/ +tests/test_files/generated/numpy_vector/* +!tests/test_files/generated/numpy_vector/.gitkeep +!tests/test_files/generated/addons/ +tests/test_files/generated/addons/* +!tests/test_files/generated/addons/.gitkeep tests/test_files/*.pyi _codeql_detected_source_root diff --git a/autowrap/CodeGenerator.py b/autowrap/CodeGenerator.py index 65bcdf2f..5fa60fa4 100644 --- a/autowrap/CodeGenerator.py +++ b/autowrap/CodeGenerator.py @@ -2060,6 +2060,7 @@ def create_default_cimports(self): |from libcpp.string cimport string as libcpp_utf8_output_string |from libcpp.set cimport set as libcpp_set |from libcpp.vector cimport vector as libcpp_vector + |from libcpp.vector cimport vector as libcpp_vector_as_np |from libcpp.pair cimport pair as libcpp_pair |from libcpp.map cimport map as libcpp_map |from libcpp.unordered_map cimport unordered_map as libcpp_unordered_map @@ -2069,7 +2070,7 @@ def create_default_cimports(self): |from libcpp.optional cimport optional as libcpp_optional |from libcpp.string_view cimport string_view as libcpp_string_view |from libcpp cimport bool - |from libc.string cimport const_char + |from libc.string cimport const_char, memcpy |from cython.operator cimport dereference as deref, + preincrement as inc, address as address """ @@ -2095,6 +2096,7 @@ def create_default_cimports(self): |import numpy as np |cimport numpy as numpy |import numpy as numpy + |from cpython.ref cimport Py_INCREF """ ) @@ -2107,7 +2109,50 @@ def create_std_cimports(self): code.add(stmt) self.top_level_code.append(code) + + # If numpy is enabled, inline the ArrayWrapper/ArrayView classes + if self.include_numpy: + self.inline_array_wrappers() + return code + + def inline_array_wrappers(self): + """Inline ArrayWrapper class definitions for buffer protocol support. + + ArrayWrapper classes are used for value returns where data is already copied. + For reference returns, Cython memory views are used instead (no wrapper needed). + """ + # Read the combined ArrayWrappers.pyx file (which has attributes already inline) + autowrap_dir = os.path.dirname(os.path.abspath(__file__)) + array_wrappers_pyx = os.path.join(autowrap_dir, "data_files", "autowrap", "ArrayWrappers.pyx") + + if not os.path.exists(array_wrappers_pyx): + L.warning("ArrayWrappers.pyx not found, skipping inline array wrappers") + return + + with open(array_wrappers_pyx, 'r') as f: + pyx_content = f.read() + + # Remove the first few lines (cython directives and module docstring) + # Keep everything from the first import onward + lines = pyx_content.split('\n') + start_idx = 0 + for i, line in enumerate(lines): + if line.strip().startswith('from cpython.buffer'): + start_idx = i + break + + wrapper_code_str = '\n'.join(lines[start_idx:]) + + code = Code() + code.add(""" + |# Inlined ArrayWrapper classes for buffer protocol support (value returns) + |# Reference returns use Cython memory views instead + """) + # Add the wrapper code directly + code.add(wrapper_code_str) + + self.top_level_code.append(code) def create_includes(self): code = Code() diff --git a/autowrap/ConversionProvider.py b/autowrap/ConversionProvider.py index cacf5cd7..31dc3fb4 100644 --- a/autowrap/ConversionProvider.py +++ b/autowrap/ConversionProvider.py @@ -1970,6 +1970,373 @@ def output_conversion(self, cpp_type: CppType, input_cpp_var: str, output_py_var return code +class StdVectorAsNumpyConverter(TypeConverterBase): + """ + Converter for libcpp_vector_as_np - wraps std::vector as numpy arrays. + + This converter uses a special type name 'libcpp_vector_as_np' in PXD files + to distinguish from the standard list-based vector conversion. + + Key features: + - For references (&): Returns numpy VIEW using Cython memory views (no copy) + - For const refs: Sets readonly flag with setflags(write=False) + - For non-const refs: Returns writable view + - Memory view automatically keeps owner alive + - For value returns: Uses ArrayWrapper with buffer protocol (single copy via swap) + - For inputs: Accepts numpy arrays, creates temporary C++ vector + - Supports nested vectors for 2D arrays + - Uses fast memcpy for efficient data transfer + + Usage in PXD: + from libcpp.vector cimport vector as libcpp_vector_as_np + + cdef extern from "mylib.hpp": + cdef cppclass MyClass: + libcpp_vector_as_np[double] getData() # Returns numpy array + void processData(libcpp_vector_as_np[double] data) # Accepts numpy array + """ + + # Mapping of C++ types to numpy dtype strings + NUMPY_DTYPE_MAP = { + "float": "float32", + "double": "float64", + "int8_t": "int8", + "int16_t": "int16", + "int": "int32", + "int32_t": "int32", + "int64_t": "int64", + "long": "int64", + "uint8_t": "uint8", + "uint16_t": "uint16", + "uint32_t": "uint32", + "unsigned int": "uint32", + "uint64_t": "uint64", + "unsigned long": "uint64", + "size_t": "uint64", + "bool": "bool_", + } + + # Map numpy dtypes to C types for memcpy + CTYPE_MAP = { + "float32": "float", + "float64": "double", + "int8": "int8_t", + "int16": "int16_t", + "int32": "int", + "int64": "long", + "uint8": "uint8_t", + "uint16": "uint16_t", + "uint32": "unsigned int", + "uint64": "unsigned long", + "bool_": "bool", + } + + def get_base_types(self) -> List[str]: + return ["libcpp_vector_as_np"] + + def matches(self, cpp_type: CppType) -> bool: + """Match vectors of numeric types and nested vectors.""" + if not cpp_type.template_args: + return False + (tt,) = cpp_type.template_args + + # Check if inner type is a numeric type that numpy supports + if tt.base_type in self.NUMPY_DTYPE_MAP: + return True + + # Check if it's a nested vector + if tt.base_type == "libcpp_vector_as_np" and tt.template_args: + # Recursively check nested vector + return self.matches(tt) + + return False + + def _get_numpy_dtype(self, cpp_type: CppType) -> str: + """Get numpy dtype string for a C++ type.""" + return self.NUMPY_DTYPE_MAP.get(cpp_type.base_type, "float64") + + def _is_nested_vector(self, cpp_type: CppType) -> bool: + """Check if this is a nested vector.""" + if not cpp_type.template_args: + return False + (tt,) = cpp_type.template_args + return tt.base_type == "libcpp_vector_as_np" + + def matching_python_type(self, cpp_type: CppType) -> str: + """Return Cython type for function signature. + + Use proper numpy.ndarray type annotations that Cython understands. + This ensures only numpy arrays are accepted, not lists. + """ + (tt,) = cpp_type.template_args + + if self._is_nested_vector(cpp_type): + # For 2D arrays + (inner_tt,) = tt.template_args + dtype = self._get_numpy_dtype(inner_tt) + return f"numpy.ndarray[numpy.{dtype}_t, ndim=2]" + else: + # For 1D arrays + dtype = self._get_numpy_dtype(tt) + return f"numpy.ndarray[numpy.{dtype}_t, ndim=1]" + + def matching_python_type_full(self, cpp_type: CppType) -> str: + """Return type hint for type checkers (for docstrings). + + This provides proper numpy type hints for documentation and type checking tools. + """ + (tt,) = cpp_type.template_args + + if self._is_nested_vector(cpp_type): + # For 2D arrays, use proper NDArray type hint syntax + (inner_tt,) = tt.template_args + dtype = self._get_numpy_dtype(inner_tt) + return f"numpy.ndarray[numpy.{dtype}_t, ndim=2]" + else: + # For 1D arrays, use proper NDArray type hint syntax + dtype = self._get_numpy_dtype(tt) + return f"numpy.ndarray[numpy.{dtype}_t, ndim=1]" + + def type_check_expression(self, cpp_type: CppType, argument_var: str) -> str: + """Check if argument is a numpy array (strict - no lists).""" + # Only accept numpy arrays, not lists or other array-like objects + return f"isinstance({argument_var}, numpy.ndarray)" + + def input_conversion( + self, cpp_type: CppType, argument_var: str, arg_num: int + ) -> Tuple[Code, str, Union[Code, str]]: + """Convert numpy array to C++ vector for input parameters. + + The argument is already guaranteed to be a numpy array with the correct type + thanks to Cython's type annotation, so we don't need to call asarray(). + """ + (tt,) = cpp_type.template_args + temp_var = "v%d" % arg_num + + if self._is_nested_vector(cpp_type): + # Handle nested vectors (2D arrays) + (inner_tt,) = tt.template_args + inner_type = self.converters.cython_type(inner_tt) + outer_inner_type = self.converters.cython_type(tt) + dtype = self._get_numpy_dtype(inner_tt) + + code = Code().add( + """ + |# Convert 2D numpy array to nested C++ vector + |cdef libcpp_vector[$outer_inner_type] * $temp_var = new libcpp_vector[$outer_inner_type]() + |cdef size_t i_$arg_num, j_$arg_num + |cdef libcpp_vector[$inner_type] row_$arg_num + |for i_$arg_num in range($argument_var.shape[0]): + | row_$arg_num = libcpp_vector[$inner_type]() + | for j_$arg_num in range($argument_var.shape[1]): + | row_$arg_num.push_back(<$inner_type>$argument_var[i_$arg_num, j_$arg_num]) + | $temp_var.push_back(row_$arg_num) + """, + dict( + argument_var=argument_var, + temp_var=temp_var, + inner_type=inner_type, + outer_inner_type=outer_inner_type, + dtype=dtype, + arg_num=arg_num, + ), + ) + cleanup = "del %s" % temp_var + return code, "deref(%s)" % temp_var, cleanup + else: + # Handle simple vectors (1D arrays) + # No need for asarray() - Cython already ensures correct type + inner_type = self.converters.cython_type(tt) + dtype = self._get_numpy_dtype(tt) + ctype = self.CTYPE_MAP.get(dtype, "double") + + code = Code().add( + """ + |# Convert 1D numpy array to C++ vector (fast memcpy) + |cdef libcpp_vector[$inner_type] * $temp_var = new libcpp_vector[$inner_type]() + |cdef size_t n_$arg_num = $argument_var.shape[0] + |$temp_var.resize(n_$arg_num) + |if n_$arg_num > 0: + | memcpy($temp_var.data(), numpy.PyArray_DATA($argument_var), n_$arg_num * sizeof($ctype)) + """, + dict( + argument_var=argument_var, + temp_var=temp_var, + inner_type=inner_type, + dtype=dtype, + arg_num=arg_num, + ctype=ctype, + ), + ) + + cleanup = "del %s" % temp_var + return code, "deref(%s)" % temp_var, cleanup + + def call_method(self, res_type: CppType, cy_call_str: str, with_const: bool = True) -> str: + # For reference returns, use address() to get a pointer and avoid copying + cy_res_type = self.converters.cython_type(res_type) # type: CppType + if cy_res_type.is_ref: + # Create a copy of the type without the reference flag + cy_ptr_type = cy_res_type.copy() + cy_ptr_type.is_ref = False + base_type_str = cy_ptr_type.toString(with_const) + return "cdef %s * _r = address(%s)" % (base_type_str, cy_call_str) + return "_r = %s" % cy_call_str + + def _get_wrapper_class_name(self, cpp_type: CppType) -> str: + """Get the appropriate ArrayWrapper class name suffix for a type.""" + type_map = { + "float": "Float", + "double": "Double", + "int8_t": "Int8", + "int16_t": "Int16", + "int32_t": "Int32", + "int": "Int32", + "int64_t": "Int64", + "long": "Int64", + "uint8_t": "UInt8", + "uint16_t": "UInt16", + "uint32_t": "UInt32", + "unsigned int": "UInt32", + "uint64_t": "UInt64", + "unsigned long": "UInt64", + } + return type_map.get(cpp_type.base_type, "Double") + + def _get_numpy_type_enum(self, cpp_type: CppType) -> str: + """Get the numpy type enum for PyArray_SimpleNewFromData.""" + type_map = { + "float": "NPY_FLOAT32", + "double": "NPY_FLOAT64", + "int8_t": "NPY_INT8", + "int16_t": "NPY_INT16", + "int32_t": "NPY_INT32", + "int": "NPY_INT32", + "int64_t": "NPY_INT64", + "long": "NPY_INT64", + "uint8_t": "NPY_UINT8", + "uint16_t": "NPY_UINT16", + "uint32_t": "NPY_UINT32", + "unsigned int": "NPY_UINT32", + "uint64_t": "NPY_UINT64", + "unsigned long": "NPY_UINT64", + "bool": "NPY_BOOL", + } + return type_map.get(cpp_type.base_type, "NPY_FLOAT64") + + def output_conversion( + self, cpp_type: CppType, input_cpp_var: str, output_py_var: str + ) -> Optional[Code]: + """Convert C++ vector to numpy array. + + Uses Cython memory views for references and ArrayWrapper for value returns: + - For references: Create zero-copy view using Cython memory view syntax + - For const references: Set readonly flag after creating view + - For value returns: Use owning wrapper (data is already a copy) + - Always set .base attribute to prevent garbage collection + """ + (tt,) = cpp_type.template_args + + if self._is_nested_vector(cpp_type): + # Handle nested vectors (2D arrays) - always copy for now + (inner_tt,) = tt.template_args + inner_type = self.converters.cython_type(inner_tt) + dtype = self._get_numpy_dtype(inner_tt) + ctype = self.CTYPE_MAP.get(dtype, "double") + + code = Code().add( + """ + |# Convert nested C++ vector to 2D numpy array (copy) + |cdef size_t n_rows = $input_cpp_var.size() + |cdef size_t n_cols = $input_cpp_var[0].size() if n_rows > 0 else 0 + |cdef object $output_py_var = numpy.empty((n_rows, n_cols), dtype=numpy.$dtype) + |cdef size_t i, j + |cdef $ctype* row_ptr + |for i in range(n_rows): + | row_ptr = <$ctype*>$input_cpp_var[i].data() + | for j in range(n_cols): + | $output_py_var[i, j] = row_ptr[j] + """, + dict( + input_cpp_var=input_cpp_var, + output_py_var=output_py_var, + inner_type=inner_type, + dtype=dtype, + ctype=ctype, + ), + ) + return code + else: + # Handle simple vectors (1D arrays) + inner_type = self.converters.cython_type(tt) + dtype = self._get_numpy_dtype(tt) + ctype = self.CTYPE_MAP.get(dtype, "double") + wrapper_suffix = self._get_wrapper_class_name(tt) + + # Check if this is a reference return (view opportunity) + if cpp_type.is_ref: + # Reference return: Use Cython memory view for zero-copy access + # For const references: set readonly flag + # Explicitly set .base to self to keep the owner alive (not the memory view) + # Note: input_cpp_var is a pointer (from address() in call_method), so dereference it + if cpp_type.is_const: + code = Code().add( + """ + |# Convert C++ const vector reference to numpy array VIEW (zero-copy, readonly) + |cdef size_t _size_$output_py_var = deref($input_cpp_var).size() + |cdef numpy.npy_intp[1] _shape_$output_py_var + |_shape_$output_py_var[0] = _size_$output_py_var + |cdef object $output_py_var = numpy.PyArray_SimpleNewFromData(1, _shape_$output_py_var, numpy.$npy_type, deref($input_cpp_var).data()) + |$output_py_var.setflags(write=False) + |# Set base to self to keep owner alive + |Py_INCREF(self) + |numpy.PyArray_SetBaseObject($output_py_var, self) + """, + dict( + input_cpp_var=input_cpp_var, + output_py_var=output_py_var, + ctype=ctype, + npy_type=self._get_numpy_type_enum(tt), + ), + ) + else: + code = Code().add( + """ + |# Convert C++ vector reference to numpy array VIEW (zero-copy, writable) + |cdef size_t _size_$output_py_var = deref($input_cpp_var).size() + |cdef numpy.npy_intp[1] _shape_$output_py_var + |_shape_$output_py_var[0] = _size_$output_py_var + |cdef object $output_py_var = numpy.PyArray_SimpleNewFromData(1, _shape_$output_py_var, numpy.$npy_type, deref($input_cpp_var).data()) + |# Set base to self to keep owner alive + |Py_INCREF(self) + |numpy.PyArray_SetBaseObject($output_py_var, self) + """, + dict( + input_cpp_var=input_cpp_var, + output_py_var=output_py_var, + ctype=ctype, + npy_type=self._get_numpy_type_enum(tt), + ), + ) + return code + else: + # Value return - use owning wrapper (data is already a copy via move/swap) + code = Code().add( + """ + |# Convert C++ vector to numpy array using owning wrapper (data already copied) + |cdef ArrayWrapper$wrapper_suffix _wrapper_$output_py_var = ArrayWrapper$wrapper_suffix() + |_wrapper_$output_py_var.set_data($input_cpp_var) + |cdef object $output_py_var = numpy.asarray(_wrapper_$output_py_var) + """, + dict( + input_cpp_var=input_cpp_var, + output_py_var=output_py_var, + wrapper_suffix=wrapper_suffix, + ), + ) + return code + + class StdStringConverter(TypeConverterBase): """ This converter deals with functions that expect/return a C++ std::string. @@ -3286,6 +3653,7 @@ def setup_converter_registry(classes_to_wrap, enums_to_wrap, instance_map): converters.register(StdStringConverter()) converters.register(StdStringUnicodeConverter()) converters.register(StdStringUnicodeOutputConverter()) + converters.register(StdVectorAsNumpyConverter()) converters.register(StdVectorConverter()) converters.register(StdSetConverter()) converters.register(StdMapConverter()) diff --git a/autowrap/Types.py b/autowrap/Types.py index 79259bc8..934890c2 100644 --- a/autowrap/Types.py +++ b/autowrap/Types.py @@ -209,7 +209,7 @@ def check_for_recursion(self): def _check_for_recursion(self, seen_base_types): # Currently, only nested std::vector<> can be handled - if self.base_type in seen_base_types and not self.base_type == "libcpp_vector": + if self.base_type in seen_base_types and not self.base_type in ["libcpp_vector", "libcpp_vector_as_np"]: raise Exception("recursion check failed") seen_base_types.add(self.base_type) for t in self.template_args or []: diff --git a/autowrap/Utils.py b/autowrap/Utils.py index fc3c1d5a..19c74a48 100644 --- a/autowrap/Utils.py +++ b/autowrap/Utils.py @@ -78,6 +78,7 @@ def compile_and_import(name, source_files, include_dirs=None, **kws): print("\n") print("tempdir=", tempdir) print("\n") + for source_file in source_files: if source_file[-4:] != ".pyx" and source_file[-4:] != ".cpp": raise NameError("Expected pyx and/or cpp files as source files for compilation.") diff --git a/autowrap/data_files/autowrap/ArrayWrappers.pyx b/autowrap/data_files/autowrap/ArrayWrappers.pyx new file mode 100644 index 00000000..1163f46b --- /dev/null +++ b/autowrap/data_files/autowrap/ArrayWrappers.pyx @@ -0,0 +1,697 @@ +# cython: language_level=3 +# cython: embedsignature=True +""" +Generic array wrapper classes with buffer protocol support. + +This module provides owning wrappers for all numeric types. +The classes implement the Python buffer protocol, allowing zero-copy integration +with numpy and other buffer-aware Python libraries. + +Owning wrappers directly hold a std::vector and transfer ownership via swap. +For reference returns, Cython memory views are used instead (see ConversionProvider). + +Supported types: float, double, int8, int16, int32, int64, uint8, uint16, uint32, uint64 +""" + +from cpython.buffer cimport PyBUF_FORMAT, PyBUF_ND, PyBUF_STRIDES, PyBUF_WRITABLE +from cpython cimport Py_buffer +from libcpp.vector cimport vector as libcpp_vector +from libcpp cimport bool as cbool +from libc.stdint cimport int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t +cimport cython +from libc.stdlib cimport malloc, free + + +# Static format strings for buffer protocol +cdef char* FORMAT_FLOAT = b'f' +cdef char* FORMAT_DOUBLE = b'd' +cdef char* FORMAT_INT8 = b'b' +cdef char* FORMAT_INT16 = b'h' +cdef char* FORMAT_INT32 = b'i' +cdef char* FORMAT_INT64 = b'q' +cdef char* FORMAT_UINT8 = b'B' +cdef char* FORMAT_UINT16 = b'H' +cdef char* FORMAT_UINT32 = b'I' +cdef char* FORMAT_UINT64 = b'Q' + +############################################################################# +# Owning Wrapper Classes (directly hold libcpp_vector) +############################################################################# + + +cdef class ArrayWrapperFloat: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperFloat(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[float] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[float]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(float) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(float) + buffer.readonly = 0 + if flags & PyBUF_FORMAT: + buffer.format = FORMAT_FLOAT + else: + buffer.format = NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(float) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + +cdef class ArrayWrapperDouble: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperDouble(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[double] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[double]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(double) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(double) + buffer.readonly = 0 + if flags & PyBUF_FORMAT: + buffer.format = FORMAT_DOUBLE + else: + buffer.format = NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(double) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + +cdef class ArrayWrapperInt8: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperInt8(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[int8_t] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[int8_t]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(int8_t) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(int8_t) + buffer.readonly = 0 + buffer.format = FORMAT_INT8 if (flags & PyBUF_FORMAT) else NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(int8_t) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + +cdef class ArrayWrapperInt16: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperInt16(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[int16_t] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[int16_t]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(int16_t) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(int16_t) + buffer.readonly = 0 + buffer.format = FORMAT_INT16 if (flags & PyBUF_FORMAT) else NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(int16_t) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + +cdef class ArrayWrapperInt32: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperInt32(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[int32_t] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[int32_t]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(int32_t) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(int32_t) + buffer.readonly = 0 + buffer.format = FORMAT_INT32 if (flags & PyBUF_FORMAT) else NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(int32_t) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + +cdef class ArrayWrapperInt64: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperInt64(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[int64_t] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[int64_t]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(int64_t) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(int64_t) + buffer.readonly = 0 + buffer.format = FORMAT_INT64 if (flags & PyBUF_FORMAT) else NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(int64_t) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + +cdef class ArrayWrapperUInt8: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperUInt8(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[uint8_t] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[uint8_t]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(uint8_t) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(uint8_t) + buffer.readonly = 0 + buffer.format = FORMAT_UINT8 if (flags & PyBUF_FORMAT) else NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(uint8_t) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + +cdef class ArrayWrapperUInt16: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperUInt16(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[uint16_t] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[uint16_t]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(uint16_t) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(uint16_t) + buffer.readonly = 0 + buffer.format = FORMAT_UINT16 if (flags & PyBUF_FORMAT) else NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(uint16_t) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + +cdef class ArrayWrapperUInt32: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperUInt32(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[uint32_t] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[uint32_t]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(uint32_t) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(uint32_t) + buffer.readonly = 0 + buffer.format = FORMAT_UINT32 if (flags & PyBUF_FORMAT) else NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(uint32_t) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + +cdef class ArrayWrapperUInt64: + """ + Owning wrapper for std::vector with buffer protocol support. + + This class owns its data via a C++ vector and can be converted to numpy arrays. + The numpy array will be a view into this wrapper's data, so the wrapper + must be kept alive while the numpy array is in use. + + Example: + wrapper = ArrayWrapperUInt64(size=10) + arr = np.asarray(wrapper) + arr.base = wrapper # Keep wrapper alive + """ + cdef libcpp_vector[uint64_t] vec + + def __init__(self, size=0): + """Initialize with optional size.""" + if size > 0: + self.vec.resize(size) + + def resize(self, size_t new_size): + """Resize the array.""" + self.vec.resize(new_size) + + def size(self): + """Get the current size.""" + return self.vec.size() + + def set_data(self, libcpp_vector[uint64_t]& data): + """Set data by swapping with a C++ vector.""" + self.vec.swap(data) + + def __getbuffer__(self, Py_buffer *buffer, int flags): + # Allocate shape and strides array (2 elements: [shape, strides]) + cdef Py_ssize_t *shape_and_strides = malloc(2 * sizeof(Py_ssize_t)) + if shape_and_strides == NULL: + raise MemoryError("Unable to allocate shape/strides buffer") + + shape_and_strides[0] = self.vec.size() # shape + shape_and_strides[1] = sizeof(uint64_t) # strides + + buffer.buf = self.vec.data() + buffer.obj = self + buffer.len = shape_and_strides[0] * sizeof(uint64_t) + buffer.readonly = 0 + buffer.format = FORMAT_UINT64 if (flags & PyBUF_FORMAT) else NULL + buffer.ndim = 1 + if flags & PyBUF_ND: + buffer.shape = shape_and_strides + else: + buffer.shape = NULL + if flags & PyBUF_STRIDES: + buffer.strides = shape_and_strides + 1 + else: + buffer.strides = NULL + buffer.suboffsets = NULL + buffer.itemsize = sizeof(uint64_t) + buffer.internal = shape_and_strides # Store pointer so we can free it later + + def __releasebuffer__(self, Py_buffer *buffer): + if buffer.internal != NULL: + free(buffer.internal) + buffer.internal = NULL + + diff --git a/pyproject.toml b/pyproject.toml index 97a1591b..283944ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,10 +42,12 @@ dev = [ "mypy", "build", "twine", + "numpy>=1.20", ] test = [ "pytest>=6.0", "pytest-cov", + "numpy>=1.20", ] [project.urls] diff --git a/tests/int_container_class_wrapped.pyx b/tests/int_container_class_wrapped.pyx deleted file mode 100644 index e35abb52..00000000 --- a/tests/int_container_class_wrapped.pyx +++ /dev/null @@ -1,6 +0,0 @@ -from libcpp.string cimport string as cpp_string -from libcpp.vector cimport vector as cpp_vector -from tests.test_files.int_container_class cimport X as _X -from tests.test_files.int_container_class cimport XContainer as _XContainer -cdef class X: - pass diff --git a/tests/test_code_generator.py b/tests/test_code_generator.py index 6a8d9986..20d594bf 100644 --- a/tests/test_code_generator.py +++ b/tests/test_code_generator.py @@ -73,7 +73,7 @@ def test_enums(): See tests/test_files/enums.pxd for the full example. """ - target = os.path.join(test_files, "enums.pyx") + target = os.path.join(test_files, "generated", "enums.pyx") include_dirs = autowrap.parse_and_generate_code( ["enums.pxd"], root=test_files, target=target, debug=True @@ -112,7 +112,7 @@ def test_enums(): def test_number_conv(): - target = os.path.join(test_files, "number_conv.pyx") + target = os.path.join(test_files, "generated", "number_conv.pyx") include_dirs = autowrap.parse_and_generate_code( ["number_conv.pxd"], root=test_files, target=target, debug=True @@ -186,7 +186,7 @@ def test_shared_ptr(): def test_inherited(): - target = os.path.join(test_files, "inherited.pyx") + target = os.path.join(test_files, "generated", "inherited.pyx") include_dirs = autowrap.parse_and_generate_code( ["inherited.pxd"], root=test_files, target=target, debug=True ) @@ -207,7 +207,7 @@ def test_inherited(): def test_templated(): - target = os.path.join(test_files, "templated_wrapper.pyx") + target = os.path.join(test_files, "generated", "templated_wrapper.pyx") decls, instance_map = autowrap.parse(["templated.pxd"], root=test_files) @@ -296,7 +296,7 @@ def test_templated(): def test_gil_unlock(): - target = os.path.join(test_files, "gil_testing_wrapper.pyx") + target = os.path.join(test_files, "generated", "gil_testing_wrapper.pyx") include_dirs = autowrap.parse_and_generate_code( ["gil_testing.pxd"], root=test_files, target=target, debug=True ) @@ -314,7 +314,7 @@ def test_gil_unlock(): def test_automatic_string_conversion(): - target = os.path.join(test_files, "libcpp_utf8_string_test.pyx") + target = os.path.join(test_files, "generated", "libcpp_utf8_string_test.pyx") include_dirs = autowrap.parse_and_generate_code( ["libcpp_utf8_string_test.pxd"], root=test_files, target=target, debug=True ) @@ -352,7 +352,7 @@ def test_automatic_string_conversion(): def test_automatic_output_string_conversion(): - target = os.path.join(test_files, "libcpp_utf8_output_string_test.pyx") + target = os.path.join(test_files, "generated", "libcpp_utf8_output_string_test.pyx") include_dirs = autowrap.parse_and_generate_code( ["libcpp_utf8_output_string_test.pxd"], root=test_files, diff --git a/tests/test_code_generator_cpp17_stl.py b/tests/test_code_generator_cpp17_stl.py index 28674ac1..542fe182 100644 --- a/tests/test_code_generator_cpp17_stl.py +++ b/tests/test_code_generator_cpp17_stl.py @@ -55,7 +55,7 @@ def test_cpp17_stl_containers(): 4. Mutable references allow in-place modification 5. Optional values handle None correctly """ - target = os.path.join(test_files, "cpp17_stl_test.pyx") + target = os.path.join(test_files, "generated", "cpp17_stl_test.pyx") include_dirs = autowrap.parse_and_generate_code( ["cpp17_stl_test.pxd"], root=test_files, target=target, debug=True diff --git a/tests/test_code_generator_libcpp.py b/tests/test_code_generator_libcpp.py index cea6615a..dcb6d6f2 100644 --- a/tests/test_code_generator_libcpp.py +++ b/tests/test_code_generator_libcpp.py @@ -93,7 +93,7 @@ def sub_libcpp_copy_constructors(libcpp): def test_libcpp(): - target = os.path.join(test_files, "libcpp_test.pyx") + target = os.path.join(test_files, "generated", "libcpp_test.pyx") include_dirs = autowrap.parse_and_generate_code( ["libcpp_test.pxd"], root=test_files, target=target, debug=True diff --git a/tests/test_code_generator_minimal.py b/tests/test_code_generator_minimal.py index bbdf898d..d710a620 100644 --- a/tests/test_code_generator_minimal.py +++ b/tests/test_code_generator_minimal.py @@ -79,7 +79,7 @@ def output_conversion(self, cpp_type, input_cpp_var, output_py_var): special_converters.append(SpecialIntConverter()) - target = os.path.join(test_files, "minimal_wrapper.pyx") + target = os.path.join(test_files, "generated", "minimal_wrapper.pyx") include_dirs = autowrap.parse_and_generate_code( ["minimal.pxd", "minimal_td.pxd"], root=test_files, target=target, debug=True ) diff --git a/tests/test_code_generator_stllibcpp.py b/tests/test_code_generator_stllibcpp.py index f7dfa2e0..87a1293f 100644 --- a/tests/test_code_generator_stllibcpp.py +++ b/tests/test_code_generator_stllibcpp.py @@ -51,7 +51,7 @@ def test_stl_libcpp(): - target = os.path.join(test_files, "libcpp_stl_test.pyx") + target = os.path.join(test_files, "generated", "libcpp_stl_test.pyx") include_dirs = autowrap.parse_and_generate_code( ["libcpp_stl_test.pxd"], root=test_files, target=target, debug=True diff --git a/tests/test_files/.gitignore b/tests/test_files/.gitignore index 0d53b6c7..240dba3b 100644 --- a/tests/test_files/.gitignore +++ b/tests/test_files/.gitignore @@ -1,5 +1,23 @@ *.o *.cpp *.so -libcpp_test.pyx +# All generated files are in generated/ subdirectory +generated/* +!generated/.gitkeep +!generated/numpy_vector/ +generated/numpy_vector/* +!generated/numpy_vector/.gitkeep +!generated/addons/ +generated/addons/* +!generated/addons/.gitkeep + +# Also ignore any generated .pyx files that might end up in the main directory +*_wrapper.pyx +*_test.pyx out.pyx +enums.pyx +number_conv.pyx +inherited.pyx +itertest.pyx +int_container_class.pyx +numpy_vector/*.pyx diff --git a/tests/test_files/cpp17_stl_test.pyx b/tests/test_files/cpp17_stl_test.pyx deleted file mode 100644 index f0fb9f9b..00000000 --- a/tests/test_files/cpp17_stl_test.pyx +++ /dev/null @@ -1,372 +0,0 @@ -#Generated with autowrap 0.23.0 and Cython (Parser) 3.2.1 -#cython: c_string_encoding=ascii -#cython: embedsignature=False -from enum import Enum as _PyEnum -from cpython cimport Py_buffer -from cpython cimport bool as pybool_t -from libcpp.string cimport string as libcpp_string -from libcpp.string cimport string as libcpp_utf8_string -from libcpp.string cimport string as libcpp_utf8_output_string -from libcpp.set cimport set as libcpp_set -from libcpp.vector cimport vector as libcpp_vector -from libcpp.pair cimport pair as libcpp_pair -from libcpp.map cimport map as libcpp_map -from libcpp.unordered_map cimport unordered_map as libcpp_unordered_map -from libcpp.unordered_set cimport unordered_set as libcpp_unordered_set -from libcpp.deque cimport deque as libcpp_deque -from libcpp.list cimport list as libcpp_list -from libcpp.optional cimport optional as libcpp_optional -from libcpp.string_view cimport string_view as libcpp_string_view -from libcpp cimport bool -from libc.string cimport const_char -from cython.operator cimport dereference as deref, preincrement as inc, address as address -from AutowrapRefHolder cimport AutowrapRefHolder -from AutowrapPtrHolder cimport AutowrapPtrHolder -from AutowrapConstPtrHolder cimport AutowrapConstPtrHolder -from libcpp.memory cimport shared_ptr -from cpp17_stl_test cimport _Cpp17STLTest as __Cpp17STLTest - -cdef extern from "autowrap_tools.hpp": - char * _cast_const_away(char *) - -cdef class _Cpp17STLTest: - """ - Cython implementation of __Cpp17STLTest - """ - - cdef shared_ptr[__Cpp17STLTest] inst - - def __dealloc__(self): - self.inst.reset() - - - def __init__(self): - """ - __init__(self) -> None - """ - self.inst = shared_ptr[__Cpp17STLTest](new __Cpp17STLTest()) - - def getUnorderedMap(self): - """ - getUnorderedMap(self) -> Dict[bytes, int] - """ - _r = self.inst.get().getUnorderedMap() - py_result = dict() - cdef libcpp_unordered_map[libcpp_string, int].iterator it__r = _r.begin() - while it__r != _r.end(): - py_result[(deref(it__r).first)] = (deref(it__r).second) - inc(it__r) - return py_result - - def sumUnorderedMapValues(self, dict m ): - """ - sumUnorderedMapValues(self, m: Dict[bytes, int] ) -> int - """ - assert isinstance(m, dict) and all(isinstance(k, bytes) for k in m.keys()) and all(isinstance(v, int) for v in m.values()), 'arg m wrong type' - cdef libcpp_unordered_map[libcpp_string, int] * v0 = new libcpp_unordered_map[libcpp_string, int]() - for _loop_key_m, _loop_value_m in m.items(): - deref(v0)[ _loop_key_m ] = _loop_value_m - cdef int _r = self.inst.get().sumUnorderedMapValues(deref(v0)) - replace = dict() - cdef libcpp_unordered_map[libcpp_string, int].iterator it_m = v0.begin() - while it_m != v0.end(): - replace[ deref(it_m).first] = deref(it_m).second - inc(it_m) - m.clear() - m.update(replace) - del v0 - py_result = _r - return py_result - - def lookupUnorderedMap(self, dict m , bytes key ): - """ - lookupUnorderedMap(self, m: Dict[bytes, int] , key: bytes ) -> int - """ - assert isinstance(m, dict) and all(isinstance(k, bytes) for k in m.keys()) and all(isinstance(v, int) for v in m.values()), 'arg m wrong type' - assert isinstance(key, bytes), 'arg key wrong type' - cdef libcpp_unordered_map[libcpp_string, int] * v0 = new libcpp_unordered_map[libcpp_string, int]() - for _loop_key_m, _loop_value_m in m.items(): - deref(v0)[ _loop_key_m ] = _loop_value_m - - cdef int _r = self.inst.get().lookupUnorderedMap(deref(v0), (key)) - replace = dict() - cdef libcpp_unordered_map[libcpp_string, int].iterator it_m = v0.begin() - while it_m != v0.end(): - replace[ deref(it_m).first] = deref(it_m).second - inc(it_m) - m.clear() - m.update(replace) - del v0 - py_result = _r - return py_result - - def hasKeyUnorderedMap(self, dict m , bytes key ): - """ - hasKeyUnorderedMap(self, m: Dict[bytes, int] , key: bytes ) -> bool - """ - assert isinstance(m, dict) and all(isinstance(k, bytes) for k in m.keys()) and all(isinstance(v, int) for v in m.values()), 'arg m wrong type' - assert isinstance(key, bytes), 'arg key wrong type' - cdef libcpp_unordered_map[libcpp_string, int] * v0 = new libcpp_unordered_map[libcpp_string, int]() - for _loop_key_m, _loop_value_m in m.items(): - deref(v0)[ _loop_key_m ] = _loop_value_m - - cdef bool _r = self.inst.get().hasKeyUnorderedMap(deref(v0), (key)) - replace = dict() - cdef libcpp_unordered_map[libcpp_string, int].iterator it_m = v0.begin() - while it_m != v0.end(): - replace[ deref(it_m).first] = deref(it_m).second - inc(it_m) - m.clear() - m.update(replace) - del v0 - py_result = _r - return py_result - - def getValueUnorderedMap(self, dict m , bytes key ): - """ - getValueUnorderedMap(self, m: Dict[bytes, int] , key: bytes ) -> int - """ - assert isinstance(m, dict) and all(isinstance(k, bytes) for k in m.keys()) and all(isinstance(v, int) for v in m.values()), 'arg m wrong type' - assert isinstance(key, bytes), 'arg key wrong type' - cdef libcpp_unordered_map[libcpp_string, int] * v0 = new libcpp_unordered_map[libcpp_string, int]() - for _loop_key_m, _loop_value_m in m.items(): - deref(v0)[ _loop_key_m ] = _loop_value_m - - cdef int _r = self.inst.get().getValueUnorderedMap(deref(v0), (key)) - replace = dict() - cdef libcpp_unordered_map[libcpp_string, int].iterator it_m = v0.begin() - while it_m != v0.end(): - replace[ deref(it_m).first] = deref(it_m).second - inc(it_m) - m.clear() - m.update(replace) - del v0 - py_result = _r - return py_result - - def getUnorderedSet(self): - """ - getUnorderedSet(self) -> Set[int] - """ - _r = self.inst.get().getUnorderedSet() - py_result = set() - cdef libcpp_unordered_set[int].iterator it__r = _r.begin() - while it__r != _r.end(): - py_result.add(deref(it__r)) - inc(it__r) - return py_result - - def sumUnorderedSet(self, set s ): - """ - sumUnorderedSet(self, s: Set[int] ) -> int - """ - assert isinstance(s, set) and all(isinstance(li, int) for li in s), 'arg s wrong type' - cdef libcpp_unordered_set[int] * v0 = new libcpp_unordered_set[int]() - for item0 in s: - v0.insert( item0) - cdef int _r = self.inst.get().sumUnorderedSet(deref(v0)) - replace = set() - cdef libcpp_unordered_set[int].iterator it_s = v0.begin() - while it_s != v0.end(): - replace.add( deref(it_s)) - inc(it_s) - s.clear() - s.update(replace) - del v0 - py_result = _r - return py_result - - def hasValueUnorderedSet(self, set s , int value ): - """ - hasValueUnorderedSet(self, s: Set[int] , value: int ) -> bool - """ - assert isinstance(s, set) and all(isinstance(li, int) for li in s), 'arg s wrong type' - assert isinstance(value, int), 'arg value wrong type' - cdef libcpp_unordered_set[int] * v0 = new libcpp_unordered_set[int]() - for item0 in s: - v0.insert( item0) - - cdef bool _r = self.inst.get().hasValueUnorderedSet(deref(v0), (value)) - replace = set() - cdef libcpp_unordered_set[int].iterator it_s = v0.begin() - while it_s != v0.end(): - replace.add( deref(it_s)) - inc(it_s) - s.clear() - s.update(replace) - del v0 - py_result = _r - return py_result - - def countUnorderedSet(self, set s , int value ): - """ - countUnorderedSet(self, s: Set[int] , value: int ) -> int - """ - assert isinstance(s, set) and all(isinstance(li, int) for li in s), 'arg s wrong type' - assert isinstance(value, int), 'arg value wrong type' - cdef libcpp_unordered_set[int] * v0 = new libcpp_unordered_set[int]() - for item0 in s: - v0.insert( item0) - - cdef size_t _r = self.inst.get().countUnorderedSet(deref(v0), (value)) - replace = set() - cdef libcpp_unordered_set[int].iterator it_s = v0.begin() - while it_s != v0.end(): - replace.add( deref(it_s)) - inc(it_s) - s.clear() - s.update(replace) - del v0 - py_result = _r - return py_result - - def findUnorderedSet(self, set s , int value ): - """ - findUnorderedSet(self, s: Set[int] , value: int ) -> int - """ - assert isinstance(s, set) and all(isinstance(li, int) for li in s), 'arg s wrong type' - assert isinstance(value, int), 'arg value wrong type' - cdef libcpp_unordered_set[int] * v0 = new libcpp_unordered_set[int]() - for item0 in s: - v0.insert( item0) - - cdef int _r = self.inst.get().findUnorderedSet(deref(v0), (value)) - replace = set() - cdef libcpp_unordered_set[int].iterator it_s = v0.begin() - while it_s != v0.end(): - replace.add( deref(it_s)) - inc(it_s) - s.clear() - s.update(replace) - del v0 - py_result = _r - return py_result - - def getDeque(self): - """ - getDeque(self) -> List[int] - """ - _r = self.inst.get().getDeque() - py_result = [_r.at(i) for i in range(_r.size())] - return py_result - - def sumDeque(self, list d ): - """ - sumDeque(self, d: List[int] ) -> int - """ - assert isinstance(d, list) and all(isinstance(li, int) for li in d), 'arg d wrong type' - cdef libcpp_deque[int] v0 - for item0 in d: - v0.push_back( item0) - cdef int _r = self.inst.get().sumDeque(v0) - d[:] = [v0.at(i) for i in range(v0.size())] - py_result = _r - return py_result - - def doubleDequeElements(self, list d ): - """ - doubleDequeElements(self, d: List[int] ) -> None - """ - assert isinstance(d, list) and all(isinstance(li, int) for li in d), 'arg d wrong type' - cdef libcpp_deque[int] v0 - for item0 in d: - v0.push_back( item0) - self.inst.get().doubleDequeElements(v0) - d[:] = [v0.at(i) for i in range(v0.size())] - - def getList(self): - """ - getList(self) -> List[float] - """ - _r = self.inst.get().getList() - py_result = [] - cdef libcpp_list[double].iterator it__r = _r.begin() - while it__r != _r.end(): - py_result.append(deref(it__r)) - inc(it__r) - return py_result - - def sumList(self, list l ): - """ - sumList(self, l: List[float] ) -> float - """ - assert isinstance(l, list) and all(isinstance(li, float) for li in l), 'arg l wrong type' - cdef libcpp_list[double] v0 - for item in l: - v0.push_back(item) - cdef double _r = self.inst.get().sumList(v0) - l[:] = [] - cdef libcpp_list[double].iterator it_l = v0.begin() - while it_l != v0.end(): - l.append(deref(it_l)) - inc(it_l) - py_result = _r - return py_result - - def doubleListElements(self, list l ): - """ - doubleListElements(self, l: List[float] ) -> None - """ - assert isinstance(l, list) and all(isinstance(li, float) for li in l), 'arg l wrong type' - cdef libcpp_list[double] v0 - for item in l: - v0.push_back(item) - self.inst.get().doubleListElements(v0) - l[:] = [] - cdef libcpp_list[double].iterator it_l = v0.begin() - while it_l != v0.end(): - l.append(deref(it_l)) - inc(it_l) - - def getOptionalValue(self, bool hasValue ): - """ - getOptionalValue(self, hasValue: bool ) -> Optional[int] - """ - assert isinstance(hasValue, pybool_t), 'arg hasValue wrong type' - - _r = self.inst.get().getOptionalValue((hasValue)) - if _r.has_value(): - py_result = _r.value() - else: - py_result = None - return py_result - - def unwrapOptional(self, object opt ): - """ - unwrapOptional(self, opt: Optional[int] ) -> int - """ - assert (opt is None or isinstance(opt, int)), 'arg opt wrong type' - cdef libcpp_optional[int] v0 - if opt is not None: - v0 = libcpp_optional[int](opt) - cdef int _r = self.inst.get().unwrapOptional(v0) - py_result = _r - return py_result - - def getStringViewLength(self, bytes sv ): - """ - getStringViewLength(self, sv: bytes ) -> int - """ - assert isinstance(sv, (bytes, str)), 'arg sv wrong type' - cdef bytes v0 - if isinstance(sv, str): - v0 = sv.encode('utf-8') - else: - v0 = sv - cdef size_t _r = self.inst.get().getStringViewLength((v0)) - py_result = _r - return py_result - - def stringViewToString(self, bytes sv ): - """ - stringViewToString(self, sv: bytes ) -> bytes - """ - assert isinstance(sv, (bytes, str)), 'arg sv wrong type' - cdef bytes v0 - if isinstance(sv, str): - v0 = sv.encode('utf-8') - else: - v0 = sv - cdef libcpp_string _r = self.inst.get().stringViewToString((v0)) - py_result = _r - return py_result diff --git a/tests/test_files/generated/.gitkeep b/tests/test_files/generated/.gitkeep new file mode 100644 index 00000000..dc82bfc8 --- /dev/null +++ b/tests/test_files/generated/.gitkeep @@ -0,0 +1,2 @@ +# This directory contains generated .pyx and .pyi files from autowrap tests +# The contents are gitignored but we keep the directory structure diff --git a/tests/test_files/generated/addons/.gitkeep b/tests/test_files/generated/addons/.gitkeep new file mode 100644 index 00000000..a58b0e6a --- /dev/null +++ b/tests/test_files/generated/addons/.gitkeep @@ -0,0 +1 @@ +# Generated addon files go here diff --git a/tests/test_files/generated/numpy_vector/.gitkeep b/tests/test_files/generated/numpy_vector/.gitkeep new file mode 100644 index 00000000..f419c280 --- /dev/null +++ b/tests/test_files/generated/numpy_vector/.gitkeep @@ -0,0 +1 @@ +# Generated numpy_vector wrapper files go here diff --git a/tests/test_files/inherited.pyx b/tests/test_files/inherited.pyx deleted file mode 100644 index 118bdd60..00000000 --- a/tests/test_files/inherited.pyx +++ /dev/null @@ -1,242 +0,0 @@ -#Generated with autowrap 0.24.0 and Cython (Parser) 3.2.1 -#cython: c_string_encoding=ascii -#cython: embedsignature=False -from enum import Enum as _PyEnum -from cpython cimport Py_buffer -from cpython cimport bool as pybool_t -from libcpp.string cimport string as libcpp_string -from libcpp.string cimport string as libcpp_utf8_string -from libcpp.string cimport string as libcpp_utf8_output_string -from libcpp.set cimport set as libcpp_set -from libcpp.vector cimport vector as libcpp_vector -from libcpp.pair cimport pair as libcpp_pair -from libcpp.map cimport map as libcpp_map -from libcpp.unordered_map cimport unordered_map as libcpp_unordered_map -from libcpp.unordered_set cimport unordered_set as libcpp_unordered_set -from libcpp.deque cimport deque as libcpp_deque -from libcpp.list cimport list as libcpp_list -from libcpp.optional cimport optional as libcpp_optional -from libcpp.string_view cimport string_view as libcpp_string_view -from libcpp cimport bool -from libc.string cimport const_char -from cython.operator cimport dereference as deref, preincrement as inc, address as address -from AutowrapRefHolder cimport AutowrapRefHolder -from AutowrapPtrHolder cimport AutowrapPtrHolder -from AutowrapConstPtrHolder cimport AutowrapConstPtrHolder -from libcpp.memory cimport shared_ptr -from inherited cimport Base as _Base -from inherited cimport Base as _Base -from inherited cimport BaseZ as _BaseZ -from inherited cimport Inherited as _Inherited -from inherited cimport InheritedTwo as _InheritedTwo - -cdef extern from "autowrap_tools.hpp": - char * _cast_const_away(char *) - -cdef class BaseDouble: - """ - Cython implementation of _Base[double] - """ - - cdef shared_ptr[_Base[double]] inst - - def __dealloc__(self): - self.inst.reset() - - - property a: - def __set__(self, double a): - - self.inst.get().a = (a) - - - def __get__(self): - cdef double _r = self.inst.get().a - py_result = _r - return py_result - - def __init__(self): - """ - __init__(self) -> None - """ - self.inst = shared_ptr[_Base[double]](new _Base[double]()) - - def foo(self): - """ - foo(self) -> float - """ - cdef double _r = self.inst.get().foo() - py_result = _r - return py_result - -cdef class BaseInt: - """ - Cython implementation of _Base[int] - """ - - cdef shared_ptr[_Base[int]] inst - - def __dealloc__(self): - self.inst.reset() - - - property a: - def __set__(self, int a): - - self.inst.get().a = (a) - - - def __get__(self): - cdef int _r = self.inst.get().a - py_result = _r - return py_result - - def __init__(self): - """ - __init__(self) -> None - """ - self.inst = shared_ptr[_Base[int]](new _Base[int]()) - - def foo(self): - """ - foo(self) -> int - """ - cdef int _r = self.inst.get().foo() - py_result = _r - return py_result - -cdef class BaseZ: - """ - Cython implementation of _BaseZ - """ - - cdef shared_ptr[_BaseZ] inst - - def __dealloc__(self): - self.inst.reset() - - - property a: - def __set__(self, int a): - - self.inst.get().a = (a) - - - def __get__(self): - cdef int _r = self.inst.get().a - py_result = _r - return py_result - - def __init__(self): - """ - __init__(self) -> None - """ - self.inst = shared_ptr[_BaseZ](new _BaseZ()) - - def bar(self): - """ - bar(self) -> int - """ - cdef int _r = self.inst.get().bar() - py_result = _r - return py_result - -cdef class InheritedInt: - """ - Cython implementation of _Inherited[int] - -- Inherits from ['Base[A]', 'BaseZ'] - """ - - cdef shared_ptr[_Inherited[int]] inst - - def __dealloc__(self): - self.inst.reset() - - - def __init__(self): - """ - __init__(self) -> None - """ - self.inst = shared_ptr[_Inherited[int]](new _Inherited[int]()) - - def getBase(self): - """ - getBase(self) -> int - """ - cdef int _r = self.inst.get().getBase() - py_result = _r - return py_result - - def getBaseZ(self): - """ - getBaseZ(self) -> int - """ - cdef int _r = self.inst.get().getBaseZ() - py_result = _r - return py_result - - def foo(self): - """ - foo(self) -> int - """ - cdef int _r = self.inst.get().foo() - py_result = _r - return py_result - - def bar(self): - """ - bar(self) -> int - """ - cdef int _r = self.inst.get().bar() - py_result = _r - return py_result - -cdef class InheritedIntDbl: - """ - Cython implementation of _InheritedTwo[int,double] - -- Inherits from ['BaseZ'] - """ - - cdef shared_ptr[_InheritedTwo[int,double]] inst - - def __dealloc__(self): - self.inst.reset() - - - def __init__(self): - """ - __init__(self) -> None - """ - self.inst = shared_ptr[_InheritedTwo[int,double]](new _InheritedTwo[int,double]()) - - def getBase(self): - """ - getBase(self) -> int - """ - cdef int _r = self.inst.get().getBase() - py_result = _r - return py_result - - def getBaseB(self): - """ - getBaseB(self) -> float - """ - cdef double _r = self.inst.get().getBaseB() - py_result = _r - return py_result - - def getBaseZ(self): - """ - getBaseZ(self) -> int - """ - cdef int _r = self.inst.get().getBaseZ() - py_result = _r - return py_result - - def bar(self): - """ - bar(self) -> int - """ - cdef int _r = self.inst.get().bar() - py_result = _r - return py_result diff --git a/tests/test_files/int_container_class.pyx b/tests/test_files/int_container_class.pyx deleted file mode 100644 index f5dbaec4..00000000 --- a/tests/test_files/int_container_class.pyx +++ /dev/null @@ -1,51 +0,0 @@ -#cython: language_level=3 -from int_container_class cimport X as X_, XContainer as XContainer_ -from cython.operator import dereference as deref - - -cdef class Xint: - - cdef X_[int] * inst - - def __cinit__(self): - self.inst = NULL - - def __init__(self, *v): - if len(v): - if isinstance(v[0], int): - self._set_(new X_[int](v[0])) - - def __add__(Xint self, Xint other): - cdef X_[int] * arg0 = self.inst - cdef X_[int] * arg1 = other.inst - cdef rv = newX(new X_[int](arg0[0]+arg1[0])) - return rv - - - def getValue(self): - return self.inst[0] - - - cdef _set_(self, X_[int] * x): - if self.inst != NULL: - del self.inst - self.inst = x - -cdef newX(X_[int] * rv): - cdef Xint rr = Xint() - rr._set_(rv) - return rr - -cdef class XContainerInt: - - cdef XContainer_[int] * inst - - def __cinit__(self): - self.inst = new XContainer_[int]() - - def push_back(self, Xint o): - self.inst.push_back(deref(o.inst)) - - def size(self): - return self.inst.size() - diff --git a/tests/test_files/itertest.pyx b/tests/test_files/itertest.pyx deleted file mode 100644 index 5ab17fbf..00000000 --- a/tests/test_files/itertest.pyx +++ /dev/null @@ -1,61 +0,0 @@ -#cython: language_level=3 -from cython.operator cimport dereference as deref, preincrement as preinc -from libcpp.list cimport list as cpplist - - - -cdef class IterTest: - - cdef cpplist[int] * inst - - def __cinit__(self): - self.inst = NULL - - def __dealloc__(self): - print("dealloc called") - if self.inst != NULL: - del self.inst - - cdef _init(self, cpplist[int] * inst): - self.inst = inst # new cpplist[int]() - - def add(self, int i): - self.inst.push_back(i) - - def __iter__(self): - assert self.inst != NULL - - it = self.inst.begin() - while it != self.inst.end(): - yield deref(it) - preinc(it) - -cdef inline create(list li): - result = IterTest() - result._init(new cpplist[int]()) - for i in li: - result.add(i) - return result - - -cdef inline conv1(cpplist[int] & ii): - return [ i for i in ii ] - -cdef inline conv2(list ii): - cdef cpplist[int] rv - for i in ii: - rv.push_back( i) - return rv - -def run(list x): - cdef cpplist[int] xv = conv2(x) - xv.reverse() - return conv1(xv) - -def run2(list x): - return create(x) - - - - - diff --git a/tests/test_files/numpy_vector/numpy_vector_test.hpp b/tests/test_files/numpy_vector/numpy_vector_test.hpp new file mode 100644 index 00000000..d99015f9 --- /dev/null +++ b/tests/test_files/numpy_vector/numpy_vector_test.hpp @@ -0,0 +1,78 @@ +#pragma once +#include + +class NumpyVectorTest +{ +public: + NumpyVectorTest() {} + + // Test simple vector output (const ref - should copy) + const std::vector& getConstRefVector() { + static std::vector data = {1.0, 2.0, 3.0, 4.0, 5.0}; + return data; + } + + // Test simple vector output (non-const ref - should be view) + std::vector& getMutableRefVector() { + static std::vector data = {10.0, 20.0, 30.0}; + return data; + } + + // Test simple vector output (value - should copy) + std::vector getValueVector(size_t size) { + std::vector result; + for (size_t i = 0; i < size; i++) { + result.push_back(static_cast(i) * 2.0); + } + return result; + } + + // Test simple vector input + double sumVector(const std::vector& data) { + double sum = 0.0; + for (double val : data) { + sum += val; + } + return sum; + } + + // Test different numeric types + int sumIntVector(const std::vector& data) { + int sum = 0; + for (int val : data) { + sum += val; + } + return sum; + } + + std::vector createFloatVector(size_t size) { + std::vector result; + for (size_t i = 0; i < size; i++) { + result.push_back(static_cast(i) + 0.5f); + } + return result; + } + + // Test nested vectors (2D arrays) + std::vector> create2DVector(size_t rows, size_t cols) { + std::vector> result; + for (size_t i = 0; i < rows; i++) { + std::vector row; + for (size_t j = 0; j < cols; j++) { + row.push_back(static_cast(i * cols + j)); + } + result.push_back(row); + } + return result; + } + + double sum2DVector(const std::vector>& data) { + double sum = 0.0; + for (const auto& row : data) { + for (double val : row) { + sum += val; + } + } + return sum; + } +}; diff --git a/tests/test_files/numpy_vector/numpy_vector_test.pxd b/tests/test_files/numpy_vector/numpy_vector_test.pxd new file mode 100644 index 00000000..1a7c5770 --- /dev/null +++ b/tests/test_files/numpy_vector/numpy_vector_test.pxd @@ -0,0 +1,21 @@ +from libcpp.vector cimport vector as libcpp_vector_as_np + +cdef extern from "numpy_vector_test.hpp": + cdef cppclass NumpyVectorTest: + NumpyVectorTest() + + # Test vector outputs with different qualifiers + const libcpp_vector_as_np[double]& getConstRefVector() # const ref -> copy + libcpp_vector_as_np[double]& getMutableRefVector() # non-const ref -> view + libcpp_vector_as_np[double] getValueVector(size_t size) # value -> copy + + # Test simple vector input + double sumVector(libcpp_vector_as_np[double] data) + + # Test different numeric types + int sumIntVector(libcpp_vector_as_np[int] data) + libcpp_vector_as_np[float] createFloatVector(size_t size) + + # Test nested vectors (2D arrays) + libcpp_vector_as_np[libcpp_vector_as_np[double]] create2DVector(size_t rows, size_t cols) + double sum2DVector(libcpp_vector_as_np[libcpp_vector_as_np[double]] data) diff --git a/tests/test_files/numpy_vector/numpy_vector_wrapper.pyi b/tests/test_files/numpy_vector/numpy_vector_wrapper.pyi new file mode 100644 index 00000000..50b6b21b --- /dev/null +++ b/tests/test_files/numpy_vector/numpy_vector_wrapper.pyi @@ -0,0 +1,67 @@ +from __future__ import annotations +from typing import overload, Any, List, Dict, Tuple, Set, Sequence, Union + +from enum import IntEnum as _PyEnum + + + + +class NumpyVectorTest: + """ + Cython implementation of _NumpyVectorTest + """ + + def __init__(self) -> None: + """ + Cython signature: void NumpyVectorTest() + """ + ... + + def getConstRefVector(self) -> numpy.ndarray[numpy.float64_t, ndim=1]: + """ + Cython signature: const libcpp_vector_as_np[double] & getConstRefVector() + """ + ... + + def getMutableRefVector(self) -> numpy.ndarray[numpy.float64_t, ndim=1]: + """ + Cython signature: libcpp_vector_as_np[double] & getMutableRefVector() + """ + ... + + def getValueVector(self, size: int ) -> numpy.ndarray[numpy.float64_t, ndim=1]: + """ + Cython signature: libcpp_vector_as_np[double] getValueVector(size_t size) + """ + ... + + def sumVector(self, data: numpy.ndarray[numpy.float64_t, ndim=1] ) -> float: + """ + Cython signature: double sumVector(libcpp_vector_as_np[double] data) + """ + ... + + def sumIntVector(self, data: numpy.ndarray[numpy.int32_t, ndim=1] ) -> int: + """ + Cython signature: int sumIntVector(libcpp_vector_as_np[int] data) + """ + ... + + def createFloatVector(self, size: int ) -> numpy.ndarray[numpy.float32_t, ndim=1]: + """ + Cython signature: libcpp_vector_as_np[float] createFloatVector(size_t size) + """ + ... + + def create2DVector(self, rows: int , cols: int ) -> numpy.ndarray[numpy.float64_t, ndim=2]: + """ + Cython signature: libcpp_vector_as_np[libcpp_vector_as_np[double]] create2DVector(size_t rows, size_t cols) + """ + ... + + def sum2DVector(self, data: numpy.ndarray[numpy.float64_t, ndim=2] ) -> float: + """ + Cython signature: double sum2DVector(libcpp_vector_as_np[libcpp_vector_as_np[double]] data) + """ + ... + diff --git a/tests/test_main.py b/tests/test_main.py index 46b810c7..5f1140ca 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -38,7 +38,7 @@ def test_from_command_line(): args = [ "pxds/*.pxd", "--out", - "out.pyx", + "generated/out.pyx", "--addons=/addons", "--converters=converters", ] @@ -68,9 +68,9 @@ def test_run(): converters = [script_dir + "/test_files/converters"] extra_includes = [script_dir + "/test_files/includes"] - includes = run(pxds, addons, converters, script_dir + "/test_files/out.pyx", extra_includes) + includes = run(pxds, addons, converters, script_dir + "/test_files/generated/out.pyx", extra_includes) - mod = compile_and_import("out", [script_dir + "/test_files/out.cpp"], includes) + mod = compile_and_import("out", [script_dir + "/test_files/generated/out.cpp"], includes) ih = mod.IntHolder() ih.set_(3) diff --git a/tests/test_numpy_vector_converter.py b/tests/test_numpy_vector_converter.py new file mode 100644 index 00000000..f2c0440b --- /dev/null +++ b/tests/test_numpy_vector_converter.py @@ -0,0 +1,286 @@ +""" +Tests for libcpp_vector_as_np conversion provider. + +This test verifies that the StdVectorAsNumpyConverter correctly handles: +- Vector outputs with const ref (copy) +- Vector outputs with non-const ref (view) +- Vector outputs with value (copy) +- Vector inputs (temporary C++ vector created) +- Different numeric types (double, int, float) +- Nested vectors (2D arrays) +- Fast memcpy for efficient data transfer +""" +from __future__ import print_function +from __future__ import absolute_import + +import pytest +import os + +import autowrap + +test_files = os.path.join(os.path.dirname(__file__), "test_files", "numpy_vector") + + +@pytest.fixture(scope="module") +def numpy_vector_module(): + """Compile and import the numpy_vector_test module.""" + import numpy + target = os.path.join(test_files, "..", "generated", "numpy_vector", "numpy_vector_wrapper.pyx") + + # Parse the declarations + decls, instance_map = autowrap.parse( + ["numpy_vector_test.pxd"], + root=test_files + ) + + # Generate code with numpy enabled + include_dirs = autowrap.generate_code( + decls, + instance_map, + target=target, + debug=True, + include_numpy=True + ) + + # Add numpy include directories + include_dirs.append(numpy.get_include()) + + module = autowrap.Utils.compile_and_import( + "numpy_vector_wrapper", + [target], + include_dirs, + ) + return module + + +class TestVectorOutputs: + """Tests for vector outputs with different qualifiers.""" + + def test_const_ref_output_is_readonly_view(self, numpy_vector_module): + """Const ref should create a readonly view (zero-copy).""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + result = t.getConstRefVector() + assert isinstance(result, np.ndarray) + assert result.shape == (5,) + assert np.allclose(result, [1.0, 2.0, 3.0, 4.0, 5.0]) + + # Array should be readonly + assert not result.flags.writeable + + # Try to modify - should fail + with pytest.raises(ValueError, match="read-only"): + result[0] = 999.0 + + # Check base attribute - should be the C++ object (self) to keep it alive + assert result.base is not None + assert result.base is t, f"Expected .base to be the owner object, got {type(result.base).__name__}" + + def test_mutable_ref_output_is_view(self, numpy_vector_module): + """Non-const ref should create a writable view (zero-copy).""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + result = t.getMutableRefVector() + assert isinstance(result, np.ndarray) + assert result.shape == (3,) + assert np.allclose(result, [10.0, 20.0, 30.0]) + + # Array should be writable + assert result.flags.writeable + + # Check base attribute - should be the C++ object (self) to keep it alive + assert result.base is not None + assert result.base is t, f"Expected .base to be the owner object, got {type(result.base).__name__}" + + # Modify array - SHOULD affect C++ data since it's a view + result[0] = 999.0 + result2 = t.getMutableRefVector() + assert result2[0] == 999.0 # C++ data was modified! + + def test_value_output_is_copy(self, numpy_vector_module): + """Value return should create a copy (Python owns data).""" + import numpy as np + import weakref + import gc + m = numpy_vector_module + t = m.NumpyVectorTest() + + result = t.getValueVector(5) + assert isinstance(result, np.ndarray) + assert result.shape == (5,) + assert np.allclose(result, [0.0, 2.0, 4.0, 6.0, 8.0]) + + # Modify array - safe since Python owns this data + result[0] = 999.0 + assert result[0] == 999.0 + + # Check base attribute - ArrayWrapper keeps the data alive + # The buffer protocol should set the wrapper as the base + assert result.base is not None + # For now, buffer protocol creates a memoryview base, which keeps the ArrayWrapper alive + # This is acceptable as long as lifetime management works correctly + assert result.base is not None, "Array base should not be None" + + def test_value_output_lifetime_management(self, numpy_vector_module): + """Test that ArrayWrapper stays alive and keeps data valid after function returns.""" + import numpy as np + import weakref + import gc + import sys + m = numpy_vector_module + t = m.NumpyVectorTest() + + # Get array from value return + result = t.getValueVector(5) + assert isinstance(result, np.ndarray) + assert result.shape == (5,) + original_values = result.copy() + assert np.allclose(original_values, [0.0, 2.0, 4.0, 6.0, 8.0]) + + # The array should have a base object (memoryview) that keeps ArrayWrapper alive + assert result.base is not None, "Array must have a base to keep wrapper alive" + + # Get reference to the base object (memoryview) to verify it stays alive + base_obj = result.base + + # Create weak reference to track if wrapper gets garbage collected prematurely + # The base (memoryview) should keep a reference to the ArrayWrapper + base_ref = weakref.ref(base_obj) + + # Force garbage collection to test lifetime management + gc.collect() + + # The base should still be alive because the array references it + assert base_ref() is not None, "Base (memoryview) should still be alive" + + # Data should still be valid (no use-after-free) + assert np.allclose(result, original_values), "Data should remain valid after GC" + + # Modify the data to verify it's still accessible + result[0] = 42.0 + assert result[0] == 42.0, "Should be able to modify data" + + # Get reference count of base object + # The array holds a reference, so refcount should be at least 2 (our var + array.base) + base_refcount = sys.getrefcount(base_obj) + assert base_refcount >= 2, f"Base refcount should be >= 2, got {base_refcount}" + + # Delete our local reference to base_obj + del base_obj + gc.collect() + + # The array should still work because it keeps its own reference to base + assert np.allclose(result[[1,2,3,4]], [2.0, 4.0, 6.0, 8.0]), "Data still valid after deleting local base ref" + + # The weak ref should still be alive because array.base still references it + assert base_ref() is not None, "Base should still be alive through array.base" + + +class TestVectorInputs: + """Tests for vector inputs.""" + + def test_sum_vector(self, numpy_vector_module): + """Test passing a 1D numpy array to C++.""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + data = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + result = t.sumVector(data) + assert result == 15.0 + + def test_sum_empty_vector(self, numpy_vector_module): + """Test passing an empty numpy array.""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + data = np.array([]) + result = t.sumVector(data) + assert result == 0.0 + + def test_sum_requires_numpy_array(self, numpy_vector_module): + """Test that passing a Python list raises TypeError (numpy array required).""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + data = [1.0, 2.0, 3.0] + # Should raise TypeError because only numpy arrays are accepted + with pytest.raises(TypeError): + t.sumVector(data) + + +class TestDifferentNumericTypes: + """Tests for different numeric types (int, float, double).""" + + def test_int_vector(self, numpy_vector_module): + """Test integer vectors.""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + data = np.array([1, 2, 3, 4, 5], dtype=np.int32) + result = t.sumIntVector(data) + assert result == 15 + + def test_float_vector(self, numpy_vector_module): + """Test float vectors.""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + result = t.createFloatVector(3) + assert isinstance(result, np.ndarray) + assert result.dtype == np.float32 + assert np.allclose(result, [0.5, 1.5, 2.5]) + + +class TestNestedVectors: + """Tests for nested vectors (2D arrays).""" + + def test_create_2d_vector(self, numpy_vector_module): + """Test receiving a 2D numpy array from C++.""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + result = t.create2DVector(3, 4) + assert isinstance(result, np.ndarray) + assert result.shape == (3, 4) + expected = np.array([ + [0.0, 1.0, 2.0, 3.0], + [4.0, 5.0, 6.0, 7.0], + [8.0, 9.0, 10.0, 11.0] + ]) + assert np.allclose(result, expected) + + def test_sum_2d_vector(self, numpy_vector_module): + """Test passing a 2D numpy array to C++.""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + data = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) + result = t.sum2DVector(data) + assert result == 21.0 + + +class TestPerformance: + """Tests for performance and large arrays.""" + + def test_large_vector_with_memcpy(self, numpy_vector_module): + """Test with a larger vector to verify memcpy is used.""" + import numpy as np + m = numpy_vector_module + t = m.NumpyVectorTest() + + # Create a large array + data = np.arange(10000, dtype=np.float64) + result = t.sumVector(data) + expected = np.sum(data) + assert np.isclose(result, expected) diff --git a/tests/test_wrap_len.py b/tests/test_wrap_len.py index b2127d98..b4495880 100644 --- a/tests/test_wrap_len.py +++ b/tests/test_wrap_len.py @@ -31,7 +31,7 @@ @pytest.fixture(scope="module") def wrap_len_module(): """Compile and import the wrap_len_test module.""" - target = os.path.join(test_files, "wrap_len_wrapper.pyx") + target = os.path.join(test_files, "generated", "wrap_len_wrapper.pyx") include_dirs = autowrap.parse_and_generate_code( ["wrap_len_test.pxd"], root=test_files, target=target, debug=True diff --git a/tests/test_wrapped_containers.py b/tests/test_wrapped_containers.py index 09bf8378..d5239a39 100644 --- a/tests/test_wrapped_containers.py +++ b/tests/test_wrapped_containers.py @@ -28,7 +28,7 @@ def wrapped_container_module(): """Compile and import the wrapped_container_test module.""" # Note: output file must have different name than input .pxd to avoid # Cython "redeclared" errors when both files are in the include path - target = os.path.join(test_files, "wrapped_container_wrapper.pyx") + target = os.path.join(test_files, "generated", "wrapped_container_wrapper.pyx") include_dirs = autowrap.parse_and_generate_code( ["wrapped_container_test.pxd"], root=test_files, target=target, debug=True