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 8dedf70c..5fa60fa4 100644 --- a/autowrap/CodeGenerator.py +++ b/autowrap/CodeGenerator.py @@ -2096,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 """ ) @@ -2108,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 ea110cda..31dc3fb4 100644 --- a/autowrap/ConversionProvider.py +++ b/autowrap/ConversionProvider.py @@ -1978,8 +1978,11 @@ class StdVectorAsNumpyConverter(TypeConverterBase): to distinguish from the standard list-based vector conversion. Key features: - - For non-const references (&): Returns numpy VIEW on C++ data (no copy) - - For const ref/value returns: Copies data to numpy array (Python owns memory) + - 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 @@ -1997,14 +2000,19 @@ class StdVectorAsNumpyConverter(TypeConverterBase): 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", - "long": "int64", - "unsigned int": "uint32", "bool": "bool_", } @@ -2012,8 +2020,12 @@ class StdVectorAsNumpyConverter(TypeConverterBase): 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", @@ -2161,15 +2173,67 @@ def input_conversion( 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. - For non-const references: Create view (no copy) - For const ref or value: Copy data (Python owns memory) + 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 @@ -2207,27 +2271,70 @@ def output_conversion( 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) - # For now, always copy data to Python (simpler and safer) - # TODO: Implement true zero-copy views for non-const references - # (requires keeping C++ object alive, which is complex) - code = Code().add( - """ - |# Convert C++ vector to numpy array COPY (Python owns data) - |cdef size_t n_$output_py_var = $input_cpp_var.size() - |cdef object $output_py_var = numpy.empty(n_$output_py_var, dtype=numpy.$dtype) - |if n_$output_py_var > 0: - | memcpy(numpy.PyArray_DATA($output_py_var), $input_cpp_var.data(), n_$output_py_var * sizeof($ctype)) - """, - dict( - input_cpp_var=input_cpp_var, - output_py_var=output_py_var, - inner_type=inner_type, - dtype=dtype, - ctype=ctype, - ), - ) - return code + # 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): 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 c3c7b597..283944ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dev = [ "mypy", "build", "twine", + "numpy>=1.20", ] test = [ "pytest>=6.0", 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_wrapper.pyx b/tests/test_files/numpy_vector/numpy_vector_wrapper.pyx deleted file mode 100644 index a23cf226..00000000 --- a/tests/test_files/numpy_vector/numpy_vector_wrapper.pyx +++ /dev/null @@ -1,176 +0,0 @@ -#Generated with autowrap 0.24.1 and Cython (Parser) 3.2.3 -#cython: c_string_encoding=ascii -#cython: embedsignature=False -from enum import IntEnum 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.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 -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, memcpy -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 -cimport numpy as np -import numpy as np -cimport numpy as numpy -import numpy as numpy -from numpy_vector_test cimport NumpyVectorTest as _NumpyVectorTest - -cdef extern from "autowrap_tools.hpp": - char * _cast_const_away(char *) - -cdef class NumpyVectorTest: - """ - Cython implementation of _NumpyVectorTest - """ - - cdef shared_ptr[_NumpyVectorTest] inst - - def __dealloc__(self): - self.inst.reset() - - - def __init__(self): - """ - __init__(self) -> None - """ - self.inst = shared_ptr[_NumpyVectorTest](new _NumpyVectorTest()) - - def getConstRefVector(self): - """ - getConstRefVector(self) -> numpy.ndarray[numpy.float64_t, ndim=1] - """ - _r = self.inst.get().getConstRefVector() - # Convert C++ vector to numpy array COPY (Python owns data) - cdef size_t n_py_result = _r.size() - cdef object py_result = numpy.empty(n_py_result, dtype=numpy.float64) - if n_py_result > 0: - memcpy(numpy.PyArray_DATA(py_result), _r.data(), n_py_result * sizeof(double)) - return py_result - - def getMutableRefVector(self): - """ - getMutableRefVector(self) -> numpy.ndarray[numpy.float64_t, ndim=1] - """ - _r = self.inst.get().getMutableRefVector() - # Convert C++ vector to numpy array COPY (Python owns data) - cdef size_t n_py_result = _r.size() - cdef object py_result = numpy.empty(n_py_result, dtype=numpy.float64) - if n_py_result > 0: - memcpy(numpy.PyArray_DATA(py_result), _r.data(), n_py_result * sizeof(double)) - return py_result - - def getValueVector(self, size ): - """ - getValueVector(self, size: int ) -> numpy.ndarray[numpy.float64_t, ndim=1] - """ - assert isinstance(size, int) and size >= 0, 'arg size wrong type' - - _r = self.inst.get().getValueVector((size)) - # Convert C++ vector to numpy array COPY (Python owns data) - cdef size_t n_py_result = _r.size() - cdef object py_result = numpy.empty(n_py_result, dtype=numpy.float64) - if n_py_result > 0: - memcpy(numpy.PyArray_DATA(py_result), _r.data(), n_py_result * sizeof(double)) - return py_result - - def sumVector(self, numpy.ndarray[numpy.float64_t, ndim=1] data ): - """ - sumVector(self, data: numpy.ndarray[numpy.float64_t, ndim=1] ) -> float - """ - assert isinstance(data, numpy.ndarray), 'arg data wrong type' - # Convert 1D numpy array to C++ vector (fast memcpy) - cdef libcpp_vector[double] * v0 = new libcpp_vector[double]() - cdef size_t n_0 = data.shape[0] - v0.resize(n_0) - if n_0 > 0: - memcpy(v0.data(), numpy.PyArray_DATA(data), n_0 * sizeof(double)) - cdef double _r = self.inst.get().sumVector(deref(v0)) - del v0 - py_result = _r - return py_result - - def sumIntVector(self, numpy.ndarray[numpy.int32_t, ndim=1] data ): - """ - sumIntVector(self, data: numpy.ndarray[numpy.int32_t, ndim=1] ) -> int - """ - assert isinstance(data, numpy.ndarray), 'arg data wrong type' - # Convert 1D numpy array to C++ vector (fast memcpy) - cdef libcpp_vector[int] * v0 = new libcpp_vector[int]() - cdef size_t n_0 = data.shape[0] - v0.resize(n_0) - if n_0 > 0: - memcpy(v0.data(), numpy.PyArray_DATA(data), n_0 * sizeof(int)) - cdef int _r = self.inst.get().sumIntVector(deref(v0)) - del v0 - py_result = _r - return py_result - - def createFloatVector(self, size ): - """ - createFloatVector(self, size: int ) -> numpy.ndarray[numpy.float32_t, ndim=1] - """ - assert isinstance(size, int) and size >= 0, 'arg size wrong type' - - _r = self.inst.get().createFloatVector((size)) - # Convert C++ vector to numpy array COPY (Python owns data) - cdef size_t n_py_result = _r.size() - cdef object py_result = numpy.empty(n_py_result, dtype=numpy.float32) - if n_py_result > 0: - memcpy(numpy.PyArray_DATA(py_result), _r.data(), n_py_result * sizeof(float)) - return py_result - - def create2DVector(self, rows , cols ): - """ - create2DVector(self, rows: int , cols: int ) -> numpy.ndarray[numpy.float64_t, ndim=2] - """ - assert isinstance(rows, int) and rows >= 0, 'arg rows wrong type' - assert isinstance(cols, int) and cols >= 0, 'arg cols wrong type' - - - _r = self.inst.get().create2DVector((rows), (cols)) - # Convert nested C++ vector to 2D numpy array (copy) - cdef size_t n_rows = _r.size() - cdef size_t n_cols = _r[0].size() if n_rows > 0 else 0 - cdef object py_result = numpy.empty((n_rows, n_cols), dtype=numpy.float64) - cdef size_t i, j - cdef double* row_ptr - for i in range(n_rows): - row_ptr = _r[i].data() - for j in range(n_cols): - py_result[i, j] = row_ptr[j] - return py_result - - def sum2DVector(self, numpy.ndarray[numpy.float64_t, ndim=2] data ): - """ - sum2DVector(self, data: numpy.ndarray[numpy.float64_t, ndim=2] ) -> float - """ - assert isinstance(data, numpy.ndarray), 'arg data wrong type' - # Convert 2D numpy array to nested C++ vector - cdef libcpp_vector[libcpp_vector_as_np[double]] * v0 = new libcpp_vector[libcpp_vector_as_np[double]]() - cdef size_t i_0, j_0 - cdef libcpp_vector[double] row_0 - for i_0 in range(data.shape[0]): - row_0 = libcpp_vector[double]() - for j_0 in range(data.shape[1]): - row_0.push_back(data[i_0, j_0]) - v0.push_back(row_0) - cdef double _r = self.inst.get().sum2DVector(deref(v0)) - del v0 - py_result = _r - return py_result 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 index f9b22c96..f2c0440b 100644 --- a/tests/test_numpy_vector_converter.py +++ b/tests/test_numpy_vector_converter.py @@ -25,7 +25,7 @@ def numpy_vector_module(): """Compile and import the numpy_vector_test module.""" import numpy - target = os.path.join(test_files, "numpy_vector_wrapper.pyx") + target = os.path.join(test_files, "..", "generated", "numpy_vector", "numpy_vector_wrapper.pyx") # Parse the declarations decls, instance_map = autowrap.parse( @@ -56,8 +56,8 @@ def numpy_vector_module(): class TestVectorOutputs: """Tests for vector outputs with different qualifiers.""" - def test_const_ref_output_is_copy(self, numpy_vector_module): - """Const ref should create a copy (Python owns data).""" + 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() @@ -67,14 +67,19 @@ def test_const_ref_output_is_copy(self, numpy_vector_module): assert result.shape == (5,) assert np.allclose(result, [1.0, 2.0, 3.0, 4.0, 5.0]) - # Modify array - should not affect C++ data since it's a copy - result[0] = 999.0 - result2 = t.getConstRefVector() - assert result2[0] == 1.0 # Original C++ data unchanged + # 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__}" - @pytest.mark.skip(reason="True zero-copy views for non-const refs require complex lifetime management - not yet implemented") def test_mutable_ref_output_is_view(self, numpy_vector_module): - """Non-const ref should create a view (C++ owns data).""" + """Non-const ref should create a writable view (zero-copy).""" import numpy as np m = numpy_vector_module t = m.NumpyVectorTest() @@ -84,6 +89,13 @@ def test_mutable_ref_output_is_view(self, numpy_vector_module): 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() @@ -92,6 +104,8 @@ def test_mutable_ref_output_is_view(self, numpy_vector_module): 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() @@ -103,6 +117,67 @@ def test_value_output_is_copy(self, numpy_vector_module): # 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: 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