Skip to content

Commit d75ff65

Browse files
committed
wip
1 parent 0a0d345 commit d75ff65

14 files changed

+374
-695
lines changed

.github/workflows/linux.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ jobs:
5050
working-directory: build
5151
run: cmake --build . --target run_tests_with_junit_report
5252

53-
- name: Run Polars integration tests
53+
- name: Run sparrow integration tests
5454
if: matrix.build_shared == 'ON'
5555
working-directory: build
56-
run: cmake --build . --target run_polars_tests_direct
56+
run: cmake --build . --target run_sparrow_tests_direct
5757

5858
- name: Install
5959
working-directory: build

.github/workflows/osx.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ jobs:
5555
working-directory: build
5656
run: cmake --build . --target run_tests_with_junit_report
5757

58-
- name: Run Polars integration tests
58+
- name: Run Sparrow integration tests
5959
if: matrix.build_shared == 'ON'
6060
working-directory: build
61-
run: cmake --build . --target run_polars_tests_direct
61+
run: cmake --build . --target run_sparrow_tests_direct
6262

6363
- name: Install
6464
working-directory: build

.github/workflows/windows.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ jobs:
5555
run: |
5656
cmake --build . --config ${{ matrix.build_type }} --target run_tests_with_junit_report
5757
58-
- name: Run Polars integration tests
58+
- name: Run Sparrow integration tests
5959
if: matrix.build_shared == 'ON'
6060
working-directory: build
61-
run: cmake --build . --config ${{ matrix.build_type }} --target run_polars_tests_direct
61+
run: cmake --build . --config ${{ matrix.build_type }} --target run_sparrow_tests_direct
6262

6363
- name: Install
6464
working-directory: build

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,12 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${BINARY_BUILD_DIR}")
151151
set(SPARROW_PYCAPSULE_HEADERS
152152
${SPARROW_PYCAPSULE_INCLUDE_DIR}/sparrow-pycapsule/config/sparrow_pycapsule_version.hpp
153153
${SPARROW_PYCAPSULE_INCLUDE_DIR}/sparrow-pycapsule/pycapsule.hpp
154+
${SPARROW_PYCAPSULE_INCLUDE_DIR}/sparrow-pycapsule/sparrow_array_python_class.hpp
154155
)
155156

156157
set(SPARROW_PYCAPSULE_SOURCES
157158
src/pycapsule.cpp
159+
src/sparrow_array_python_class.cpp
158160
)
159161

160162
option(SPARROW_PYCAPSULE_BUILD_SHARED "Build sparrow pycapsule as a shared library" ON)

src/SparrowPythonClass.cpp renamed to src/sparrow_array_python_class.cpp

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#include "sparrow-pycapsule/SparrowPythonClass.hpp"
1+
#include "sparrow-pycapsule/sparrow_array_python_class.hpp"
22

33
#include <new>
44
#include <utility>
@@ -67,6 +67,81 @@ namespace sparrow::pycapsule
6767
return PyLong_FromSize_t(self->arr->size());
6868
}
6969

70+
/**
71+
* @brief Constructor for SparrowArray.
72+
*
73+
* Accepts an object implementing __arrow_c_array__ and imports it.
74+
*/
75+
static PyObject* SparrowArray_new(PyTypeObject* type, PyObject* args, PyObject* kwargs)
76+
{
77+
static const char* kwlist[] = {"arrow_array", nullptr};
78+
PyObject* arrow_array = nullptr;
79+
80+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", const_cast<char**>(kwlist), &arrow_array))
81+
{
82+
return nullptr;
83+
}
84+
85+
// Get __arrow_c_array__ method from the input object
86+
PyObject* arrow_c_array_method = PyObject_GetAttrString(arrow_array, "__arrow_c_array__");
87+
if (arrow_c_array_method == nullptr)
88+
{
89+
PyErr_SetString(
90+
PyExc_TypeError,
91+
"Input object must implement __arrow_c_array__ (ArrowArrayExportable protocol)"
92+
);
93+
return nullptr;
94+
}
95+
96+
// Call __arrow_c_array__() to get the capsules
97+
PyObject* capsules = PyObject_CallNoArgs(arrow_c_array_method);
98+
Py_DECREF(arrow_c_array_method);
99+
100+
if (capsules == nullptr)
101+
{
102+
return nullptr;
103+
}
104+
105+
// Unpack the tuple (schema_capsule, array_capsule)
106+
if (!PyTuple_Check(capsules) || PyTuple_Size(capsules) != 2)
107+
{
108+
Py_DECREF(capsules);
109+
PyErr_SetString(PyExc_TypeError, "__arrow_c_array__ must return a tuple of 2 elements");
110+
return nullptr;
111+
}
112+
113+
PyObject* schema_capsule = PyTuple_GetItem(capsules, 0);
114+
PyObject* array_capsule = PyTuple_GetItem(capsules, 1);
115+
116+
try
117+
{
118+
sparrow::array arr = import_array_from_capsules(schema_capsule, array_capsule);
119+
Py_DECREF(capsules);
120+
121+
// Allocate the object
122+
SparrowArrayObject* self = reinterpret_cast<SparrowArrayObject*>(type->tp_alloc(type, 0));
123+
if (self == nullptr)
124+
{
125+
return nullptr;
126+
}
127+
128+
self->arr = new sparrow::array(std::move(arr));
129+
return reinterpret_cast<PyObject*>(self);
130+
}
131+
catch (const std::bad_alloc&)
132+
{
133+
Py_DECREF(capsules);
134+
PyErr_NoMemory();
135+
return nullptr;
136+
}
137+
catch (const std::exception& e)
138+
{
139+
Py_DECREF(capsules);
140+
PyErr_SetString(PyExc_RuntimeError, e.what());
141+
return nullptr;
142+
}
143+
}
144+
70145
static PyMethodDef SparrowArray_methods[] = {
71146
{"__arrow_c_array__",
72147
reinterpret_cast<PyCFunction>(SparrowArray_arrow_c_array),
@@ -99,12 +174,17 @@ namespace sparrow::pycapsule
99174
.tp_dealloc = reinterpret_cast<destructor>(SparrowArray_dealloc),
100175
.tp_flags = Py_TPFLAGS_DEFAULT,
101176
.tp_doc = PyDoc_STR(
102-
"SparrowArray - Arrow array wrapper implementing __arrow_c_array__.\n\n"
177+
"SparrowArray(arrow_array) - Arrow array wrapper implementing __arrow_c_array__.\n\n"
103178
"This class wraps a sparrow array and implements the Arrow PyCapsule\n"
104179
"Interface (ArrowArrayExportable protocol), allowing it to be passed\n"
105-
"directly to libraries like Polars via pl.from_arrow()."
180+
"directly to libraries like Polars via pl.from_arrow().\n\n"
181+
"Parameters\n"
182+
"----------\n"
183+
"arrow_array : ArrowArrayExportable\n"
184+
" An object implementing __arrow_c_array__ (e.g., PyArrow array)."
106185
),
107186
.tp_methods = SparrowArray_methods,
187+
.tp_new = SparrowArray_new,
108188
};
109189

110190
static bool type_initialized = false;

test/CMakeLists.txt

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -85,28 +85,23 @@ add_custom_target(run_tests_with_junit_report
8585

8686
set_target_properties(run_tests_with_junit_report PROPERTIES FOLDER "Tests utilities")
8787

88-
# Polars integration test helper - Native Python Extension Module
89-
# ===============================================================
90-
# This builds a proper Python extension module (.cpython-*.so) that shares
91-
# the same Python runtime as the interpreter, avoiding dual-runtime issues.
88+
Python_add_library(test_sparrow_helper MODULE test_sparrow_helper_module.cpp)
9289

93-
Python_add_library(test_polars_helper MODULE test_polars_helper_module.cpp)
94-
95-
target_link_libraries(test_polars_helper
90+
target_link_libraries(test_sparrow_helper
9691
PRIVATE
9792
sparrow-pycapsule
9893
sparrow::sparrow
9994
)
10095

101-
target_compile_features(test_polars_helper PRIVATE cxx_std_20)
96+
target_compile_features(test_sparrow_helper PRIVATE cxx_std_20)
10297

10398
if(MSVC)
104-
target_compile_options(test_polars_helper PRIVATE /W4)
99+
target_compile_options(test_sparrow_helper PRIVATE /W4)
105100
else()
106-
target_compile_options(test_polars_helper PRIVATE -Wall -Wextra -Wpedantic)
101+
target_compile_options(test_sparrow_helper PRIVATE -Wall -Wextra -Wpedantic)
107102
endif()
108103

109-
set_target_properties(test_polars_helper PROPERTIES
104+
set_target_properties(test_sparrow_helper PROPERTIES
110105
FOLDER tests
111106
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${CMAKE_BUILD_TYPE}
112107
# Python modules must not have a debug suffix - Python won't find them
@@ -120,77 +115,76 @@ find_package(Python COMPONENTS Interpreter QUIET)
120115
if(Python_Interpreter_FOUND)
121116
# Add a test that runs the Python integration script
122117
add_test(
123-
NAME test_polars_integration
124-
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_polars_integration.py
118+
NAME test_sparrow_integration
119+
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_sparrow_integration.py
125120
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
126121
)
127122

128123
# Set environment variables so Python can find the libraries
129124
# Use generator expressions to get the actual library paths from targets
130-
set_tests_properties(test_polars_integration PROPERTIES
131-
ENVIRONMENT "TEST_POLARS_HELPER_LIB_PATH=$<TARGET_FILE:test_polars_helper>;SPARROW_PYCAPSULE_LIB_PATH=$<TARGET_FILE:sparrow-pycapsule>"
125+
set_tests_properties(test_sparrow_integration PROPERTIES
126+
ENVIRONMENT "TEST_SPARROW_HELPER_LIB_PATH=$<TARGET_FILE:test_sparrow_helper>;SPARROW_PYCAPSULE_LIB_PATH=$<TARGET_FILE:sparrow-pycapsule>"
132127
TIMEOUT 300
133-
DEPENDS test_polars_helper
128+
DEPENDS test_sparrow_helper
134129
)
135130

136-
message(STATUS "Added Polars integration test (Python ${Python_VERSION})")
131+
message(STATUS "Added sparrow integration test (Python ${Python_VERSION})")
137132
else()
138-
message(WARNING "Python interpreter not found, skipping Polars integration test")
133+
message(WARNING "Python interpreter not found, skipping sparrow integration test")
139134
endif()
140135

141-
# Custom target to run Polars tests directly (with better output)
142136
if(Python_Interpreter_FOUND)
143-
add_custom_target(run_polars_tests_direct
137+
add_custom_target(run_sparrow_tests_direct
144138
COMMAND ${CMAKE_COMMAND} -E echo "=================================="
145-
COMMAND ${CMAKE_COMMAND} -E echo "Polars Integration Test Runner"
139+
COMMAND ${CMAKE_COMMAND} -E echo "Sparrow Integration Test Runner"
146140
COMMAND ${CMAKE_COMMAND} -E echo "=================================="
147141
COMMAND ${CMAKE_COMMAND} -E echo ""
148142
COMMAND ${CMAKE_COMMAND} -E echo "Checking Python dependencies..."
149143
COMMAND ${Python_EXECUTABLE} -c "import polars" || ${CMAKE_COMMAND} -E cmake_echo_color --red "ERROR: polars not installed. Install with: pip install polars"
150144
COMMAND ${Python_EXECUTABLE} -c "import pyarrow" || ${CMAKE_COMMAND} -E cmake_echo_color --red "ERROR: pyarrow not installed. Install with: pip install pyarrow"
151145
COMMAND ${CMAKE_COMMAND} -E echo ""
152146
COMMAND ${CMAKE_COMMAND} -E echo "Library paths:"
153-
COMMAND ${CMAKE_COMMAND} -E echo " TEST_POLARS_HELPER_LIB_PATH=$<TARGET_FILE:test_polars_helper>"
147+
COMMAND ${CMAKE_COMMAND} -E echo " TEST_SPARROW_HELPER_LIB_PATH=$<TARGET_FILE:test_sparrow_helper>"
154148
COMMAND ${CMAKE_COMMAND} -E echo " SPARROW_PYCAPSULE_LIB_PATH=$<TARGET_FILE:sparrow-pycapsule>"
155149
COMMAND ${CMAKE_COMMAND} -E echo ""
156150
COMMAND ${CMAKE_COMMAND} -E echo "Running tests..."
157151
COMMAND ${CMAKE_COMMAND} -E echo ""
158152
COMMAND ${CMAKE_COMMAND} -E env
159-
"TEST_POLARS_HELPER_LIB_PATH=$<TARGET_FILE:test_polars_helper>"
153+
"TEST_SPARROW_HELPER_LIB_PATH=$<TARGET_FILE:test_sparrow_helper>"
160154
"SPARROW_PYCAPSULE_LIB_PATH=$<TARGET_FILE:sparrow-pycapsule>"
161-
${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_polars_integration.py
155+
${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_sparrow_integration.py
162156
COMMAND ${CMAKE_COMMAND} -E echo ""
163-
DEPENDS test_polars_helper sparrow-pycapsule
157+
DEPENDS test_sparrow_helper sparrow-pycapsule
164158
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
165-
COMMENT "Running Polars integration tests directly"
159+
COMMENT "Running Sparrow integration tests directly"
166160
USES_TERMINAL
167161
)
168162

169-
set_target_properties(run_polars_tests_direct PROPERTIES FOLDER "Tests utilities")
163+
set_target_properties(run_sparrow_tests_direct PROPERTIES FOLDER "Tests utilities")
170164

171-
# Custom target to check Polars dependencies
172-
add_custom_target(check_polars_deps
173-
COMMAND ${CMAKE_COMMAND} -E echo "Checking Polars integration test dependencies..."
165+
# Custom target to check Sparrow dependencies
166+
add_custom_target(check_sparrow_deps
167+
COMMAND ${CMAKE_COMMAND} -E echo "Checking Sparrow integration test dependencies..."
174168
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check_deps.py
175169
COMMAND ${CMAKE_COMMAND} -E echo "Environment variables that will be set:"
176-
COMMAND ${CMAKE_COMMAND} -E echo " TEST_POLARS_HELPER_LIB_PATH=$<TARGET_FILE:test_polars_helper>"
170+
COMMAND ${CMAKE_COMMAND} -E echo " TEST_SPARROW_HELPER_LIB_PATH=$<TARGET_FILE:test_sparrow_helper>"
177171
COMMAND ${CMAKE_COMMAND} -E echo " SPARROW_PYCAPSULE_LIB_PATH=$<TARGET_FILE:sparrow-pycapsule>"
178172
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
179-
COMMENT "Checking Polars test dependencies"
173+
COMMENT "Checking Sparrow test dependencies"
180174
USES_TERMINAL
181175
)
182176

183-
set_target_properties(check_polars_deps PROPERTIES FOLDER "Tests utilities")
177+
set_target_properties(check_sparrow_deps PROPERTIES FOLDER "Tests utilities")
184178

185179
# Minimal library loading test for debugging segfaults
186180
add_custom_target(test_library_load
187181
COMMAND ${CMAKE_COMMAND} -E env
188-
"TEST_POLARS_HELPER_LIB_PATH=$<TARGET_FILE:test_polars_helper>"
182+
"TEST_SPARROW_HELPER_LIB_PATH=$<TARGET_FILE:test_sparrow_helper>"
189183
"SPARROW_PYCAPSULE_LIB_PATH=$<TARGET_FILE:sparrow-pycapsule>"
190184
"PYTHONUNBUFFERED=1"
191185
${Python_EXECUTABLE} -u ${CMAKE_CURRENT_SOURCE_DIR}/test_library_load.py
192186
DEPENDS
193-
test_polars_helper
187+
test_sparrow_helper
194188
sparrow-pycapsule
195189
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
196190
COMMENT "Testing library loading step-by-step"

test/sparrow_helpers.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Python helper module for sparrow-pycapsule integration tests.
3+
4+
This module provides helper functions that wrap the C++ SparrowArray class,
5+
making it easy to create test arrays and perform roundtrip operations.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import sys
11+
import os
12+
from pathlib import Path
13+
from typing import Any, Protocol, Tuple
14+
15+
class ArrowArrayExportable(Protocol):
16+
"""Protocol for objects implementing the Arrow PyCapsule Interface."""
17+
18+
def __arrow_c_array__(
19+
self, requested_schema: Any = None
20+
) -> Tuple[Any, Any]:
21+
"""Export the array as Arrow PyCapsules.
22+
23+
Returns
24+
-------
25+
Tuple[Any, Any]
26+
A tuple of (schema_capsule, array_capsule).
27+
"""
28+
...
29+
30+
31+
class SparrowArrayType(ArrowArrayExportable, Protocol):
32+
"""Type definition for SparrowArray from C++ extension."""
33+
34+
def size(self) -> int:
35+
"""Get the number of elements in the array."""
36+
...
37+
38+
39+
def _setup_module_path() -> None:
40+
"""Add the build directory to Python path so we can import test_sparrow_helper."""
41+
# Check for environment variable first
42+
helper_path = os.environ.get('TEST_SPARROW_HELPER_PATH')
43+
if helper_path:
44+
module_dir = Path(helper_path).parent
45+
if module_dir.exists():
46+
sys.path.insert(0, str(module_dir))
47+
return
48+
49+
# Try to find in build directory
50+
test_dir = Path(__file__).parent
51+
build_dirs = [
52+
test_dir.parent / "build" / "bin" / "Debug",
53+
test_dir.parent / "build" / "bin" / "Release",
54+
test_dir.parent / "build" / "bin",
55+
]
56+
57+
for build_dir in build_dirs:
58+
if build_dir.exists():
59+
sys.path.insert(0, str(build_dir))
60+
return
61+
62+
raise ImportError(
63+
"Could not find test_sparrow_helper module. "
64+
"Build the project first or set TEST_SPARROW_HELPER_PATH."
65+
)
66+
67+
68+
# Set up module path and import the C++ module
69+
_setup_module_path()
70+
71+
# Import the native Python extension module that provides SparrowArray
72+
from test_sparrow_helper import SparrowArray # noqa: E402

0 commit comments

Comments
 (0)