Skip to content

Commit 6907c31

Browse files
committed
Fix how we handle our numpy build interaction.
We were doing some janky thing where we find the installed numpy. Instead we should be using standard python array interop which doesn't depend on the exact version of numpy you have installed. This should make it much more portable.
1 parent 81b856f commit 6907c31

File tree

9 files changed

+448
-224
lines changed

9 files changed

+448
-224
lines changed

Makefile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ TP_BUILD_OPT_LEVEL ?= 2
2626

2727
# location of python include paths
2828
PYINCLUDE = $(shell python3 -c 'import sysconfig; print(sysconfig.get_paths()["include"])')
29-
# location of numpy
30-
NUMPYINCLUDE = $(shell python3 -c 'import pkg_resources; print(pkg_resources.resource_filename("numpy", "core/include"))')
3129
# name of the _types binary, which we can infer from the name of the _ssl binary
3230
TYPES_SO_NAME = $(shell python3 -c 'import _ssl; import os; print(os.path.split(_ssl.__file__)[1].replace("_ssl", "_types"))')
3331
TYPES_O_NAME = $(shell python3 -c 'import _ssl; import os; print(os.path.split(_ssl.__file__)[1].replace("_ssl", "_types")[:-2] + "o")')
@@ -40,7 +38,6 @@ CPP_FLAGS = -std=c++14 -O$(TP_BUILD_OPT_LEVEL) -Wall -pthread -DNDEBUG -g
4038
-Wno-sign-compare -Wno-narrowing -Wno-int-in-bool-context \
4139
-I$(TP_SRC_PATH)/lz4 \
4240
-I$(PYINCLUDE) \
43-
-I$(NUMPYINCLUDE) \
4441

4542

4643
LINKER_FLAGS = -Wl,-O1 \

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[build-system]
22
requires = [
3-
"setuptools",
3+
"setuptools>=64",
44
"wheel",
5-
'oldest-supported-numpy',
65
]
7-
# build-backend = "setuptools.build_meta:__legacy__"
6+
build-backend = "setuptools.build_meta"

setup.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import pkg_resources
1615
import setuptools
17-
18-
from distutils.command.build_ext import build_ext
19-
from distutils.extension import Extension
20-
21-
22-
class TypedPythonBuildExtension(build_ext):
23-
def run(self):
24-
self.include_dirs.append(
25-
pkg_resources.resource_filename('numpy', 'core/include')
26-
)
27-
28-
build_ext.run(self)
16+
from setuptools import Extension
2917

3018

3119
extra_compile_args = [
@@ -68,7 +56,6 @@ def run(self):
6856
author_email='braxton.mckee@gmail.com',
6957
url='https://github.com/aprioriinvestments/typed_python',
7058
packages=setuptools.find_packages(),
71-
cmdclass={'build_ext': TypedPythonBuildExtension},
7259
ext_modules=ext_modules,
7360
install_requires=INSTALL_REQUIRES,
7461

typed_python/NumpyInterop.hpp

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
/******************************************************************************
2+
Copyright 2017-2024 typed_python Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
******************************************************************************/
16+
17+
#pragma once
18+
19+
#include <Python.h>
20+
#include <string>
21+
#include <cstring>
22+
#include "Type.hpp"
23+
24+
// Numpy interop without compile-time numpy dependency.
25+
//
26+
// Instead of #include <numpy/arrayobject.h>, we use:
27+
// - The Python buffer protocol (PEP 3118) to read/write array data
28+
// - Runtime-cached numpy type objects for scalar type detection
29+
// - Runtime calls to numpy.empty() for array creation
30+
//
31+
// If numpy is not installed, all detection functions return false and
32+
// array creation returns nullptr with a Python error set.
33+
34+
namespace NumpyInterop {
35+
36+
// Cached numpy type objects, populated at module init time.
37+
struct NumpyTypeCache {
38+
bool initialized = false;
39+
bool available = false;
40+
41+
PyObject* numpy_module = nullptr;
42+
43+
// Scalar type objects
44+
PyTypeObject* bool_type = nullptr;
45+
PyTypeObject* float16_type = nullptr;
46+
PyTypeObject* float32_type = nullptr;
47+
PyTypeObject* float64_type = nullptr;
48+
PyTypeObject* longdouble_type = nullptr;
49+
50+
PyTypeObject* int8_type = nullptr;
51+
PyTypeObject* int16_type = nullptr;
52+
PyTypeObject* int32_type = nullptr;
53+
PyTypeObject* int64_type = nullptr;
54+
// numpy 'long' maps to C long, which is platform-dependent
55+
PyTypeObject* long_type = nullptr;
56+
PyTypeObject* longlong_type = nullptr;
57+
58+
PyTypeObject* uint8_type = nullptr;
59+
PyTypeObject* uint16_type = nullptr;
60+
PyTypeObject* uint32_type = nullptr;
61+
PyTypeObject* uint64_type = nullptr;
62+
PyTypeObject* ulong_type = nullptr;
63+
PyTypeObject* ulonglong_type = nullptr;
64+
65+
// The ndarray type itself
66+
PyTypeObject* ndarray_type = nullptr;
67+
};
68+
69+
inline NumpyTypeCache& getCache() {
70+
static NumpyTypeCache cache;
71+
return cache;
72+
}
73+
74+
// Attempt to cache a type from numpy module. Returns nullptr if not found.
75+
inline PyTypeObject* cacheType(PyObject* mod, const char* name) {
76+
PyObject* obj = PyObject_GetAttrString(mod, name);
77+
if (!obj) {
78+
PyErr_Clear();
79+
return nullptr;
80+
}
81+
if (PyType_Check(obj)) {
82+
return (PyTypeObject*)obj; // keeps a reference
83+
}
84+
Py_DECREF(obj);
85+
return nullptr;
86+
}
87+
88+
// Initialize the numpy type cache. Call once at module init.
89+
// Safe to call if numpy is not installed.
90+
inline void init() {
91+
NumpyTypeCache& c = getCache();
92+
if (c.initialized) return;
93+
c.initialized = true;
94+
95+
c.numpy_module = PyImport_ImportModule("numpy");
96+
if (!c.numpy_module) {
97+
PyErr_Clear();
98+
c.available = false;
99+
return;
100+
}
101+
c.available = true;
102+
103+
c.ndarray_type = cacheType(c.numpy_module, "ndarray");
104+
105+
c.bool_type = cacheType(c.numpy_module, "bool_");
106+
c.float16_type = cacheType(c.numpy_module, "float16");
107+
c.float32_type = cacheType(c.numpy_module, "float32");
108+
c.float64_type = cacheType(c.numpy_module, "float64");
109+
c.longdouble_type = cacheType(c.numpy_module, "longdouble");
110+
111+
c.int8_type = cacheType(c.numpy_module, "int8");
112+
c.int16_type = cacheType(c.numpy_module, "int16");
113+
c.int32_type = cacheType(c.numpy_module, "int32");
114+
c.int64_type = cacheType(c.numpy_module, "int64");
115+
c.long_type = cacheType(c.numpy_module, "long");
116+
c.longlong_type = cacheType(c.numpy_module, "longlong");
117+
118+
c.uint8_type = cacheType(c.numpy_module, "uint8");
119+
c.uint16_type = cacheType(c.numpy_module, "uint16");
120+
c.uint32_type = cacheType(c.numpy_module, "uint32");
121+
c.uint64_type = cacheType(c.numpy_module, "uint64");
122+
c.ulong_type = cacheType(c.numpy_module, "ulong");
123+
c.ulonglong_type = cacheType(c.numpy_module, "ulonglong");
124+
}
125+
126+
// Check if an object is a numpy ndarray.
127+
inline bool isNumpyArray(PyObject* obj) {
128+
NumpyTypeCache& c = getCache();
129+
if (!c.available || !c.ndarray_type) return false;
130+
return PyObject_IsInstance(obj, (PyObject*)c.ndarray_type) == 1;
131+
}
132+
133+
// Check if a type is a numpy float scalar type.
134+
inline bool isNumpyFloatType(PyTypeObject* t) {
135+
NumpyTypeCache& c = getCache();
136+
if (!c.available) return false;
137+
return (
138+
t == c.float16_type
139+
|| t == c.float32_type
140+
|| t == c.float64_type
141+
|| t == c.longdouble_type
142+
);
143+
}
144+
145+
// Check if a type is a numpy integer scalar type.
146+
inline bool isNumpyIntType(PyTypeObject* t) {
147+
NumpyTypeCache& c = getCache();
148+
if (!c.available) return false;
149+
return (
150+
t == c.int8_type
151+
|| t == c.int16_type
152+
|| t == c.int32_type
153+
|| t == c.int64_type
154+
|| t == c.long_type
155+
|| t == c.longlong_type
156+
|| t == c.uint8_type
157+
|| t == c.uint16_type
158+
|| t == c.uint32_type
159+
|| t == c.uint64_type
160+
|| t == c.ulong_type
161+
|| t == c.ulonglong_type
162+
);
163+
}
164+
165+
// Check if a type is any numpy scalar type (bool, float, or int).
166+
inline bool isNumpyScalarType(PyTypeObject* t) {
167+
NumpyTypeCache& c = getCache();
168+
if (!c.available) return false;
169+
return t == c.bool_type || isNumpyFloatType(t) || isNumpyIntType(t);
170+
}
171+
172+
// Map a numpy scalar type to the best typed_python type category.
173+
inline Type::TypeCategory numpyScalarTypeToBestCategory(PyTypeObject* t) {
174+
NumpyTypeCache& c = getCache();
175+
if (t == c.bool_type) { return Type::TypeCategory::catBool; }
176+
if (t == c.float16_type) { return Type::TypeCategory::catFloat32; }
177+
if (t == c.float32_type) { return Type::TypeCategory::catFloat32; }
178+
if (t == c.float64_type) { return Type::TypeCategory::catFloat64; }
179+
if (t == c.longdouble_type) { return Type::TypeCategory::catFloat64; }
180+
if (t == c.int8_type) { return Type::TypeCategory::catInt8; }
181+
if (t == c.int16_type) { return Type::TypeCategory::catInt16; }
182+
if (t == c.int32_type) { return Type::TypeCategory::catInt32; }
183+
if (t == c.long_type) {
184+
return sizeof(long) == 8 ? Type::TypeCategory::catInt64 : Type::TypeCategory::catInt32;
185+
}
186+
if (t == c.int64_type) { return Type::TypeCategory::catInt64; }
187+
if (t == c.longlong_type) { return Type::TypeCategory::catInt64; }
188+
if (t == c.uint8_type) { return Type::TypeCategory::catUInt8; }
189+
if (t == c.uint16_type) { return Type::TypeCategory::catUInt16; }
190+
if (t == c.uint32_type) { return Type::TypeCategory::catUInt32; }
191+
if (t == c.ulong_type) {
192+
return sizeof(long) == 8 ? Type::TypeCategory::catUInt64 : Type::TypeCategory::catUInt32;
193+
}
194+
if (t == c.uint64_type) { return Type::TypeCategory::catUInt64; }
195+
if (t == c.ulonglong_type) { return Type::TypeCategory::catUInt64; }
196+
197+
throw std::runtime_error("Type is not a numpy type.");
198+
}
199+
200+
// Buffer protocol element type enum (replaces NPY_* constants)
201+
enum class BufferDtype {
202+
Unknown,
203+
Bool,
204+
Int8, Int16, Int32, Int64,
205+
UInt8, UInt16, UInt32, UInt64,
206+
Float32, Float64
207+
};
208+
209+
// Map a buffer protocol format character to our dtype enum.
210+
// See PEP 3118 / struct module format strings.
211+
inline BufferDtype formatCharToDtype(char fmt) {
212+
switch (fmt) {
213+
case '?': return BufferDtype::Bool;
214+
case 'b': return BufferDtype::Int8;
215+
case 'h': return BufferDtype::Int16;
216+
case 'i': return BufferDtype::Int32;
217+
case 'q': return BufferDtype::Int64;
218+
case 'B': return BufferDtype::UInt8;
219+
case 'H': return BufferDtype::UInt16;
220+
case 'I': return BufferDtype::UInt32;
221+
case 'Q': return BufferDtype::UInt64;
222+
case 'f': return BufferDtype::Float32;
223+
case 'd': return BufferDtype::Float64;
224+
case 'l': // C long - platform dependent
225+
return sizeof(long) == 8 ? BufferDtype::Int64 : BufferDtype::Int32;
226+
case 'L': // C unsigned long
227+
return sizeof(unsigned long) == 8 ? BufferDtype::UInt64 : BufferDtype::UInt32;
228+
case 'n': // ssize_t
229+
return sizeof(Py_ssize_t) == 8 ? BufferDtype::Int64 : BufferDtype::Int32;
230+
case 'N': // size_t
231+
return sizeof(size_t) == 8 ? BufferDtype::UInt64 : BufferDtype::UInt32;
232+
default: return BufferDtype::Unknown;
233+
}
234+
}
235+
236+
// Parse a buffer format string to a dtype. Handles optional byte-order prefix
237+
// (e.g. "<d", "=i", "@f") and numpy's format strings.
238+
inline BufferDtype parseBufferFormat(const char* format) {
239+
if (!format || !format[0]) return BufferDtype::Unknown;
240+
241+
const char* p = format;
242+
// Skip byte-order/alignment prefix characters
243+
if (*p == '@' || *p == '=' || *p == '<' || *p == '>' || *p == '!') {
244+
p++;
245+
}
246+
if (!*p) return BufferDtype::Unknown;
247+
// Should be a single format char
248+
if (p[1] != '\0') return BufferDtype::Unknown;
249+
return formatCharToDtype(*p);
250+
}
251+
252+
// Create a numpy array of given size and dtype string.
253+
// dtype_str should be a numpy dtype name like "float64", "int32", etc.
254+
// Returns a new reference, or nullptr with Python error set.
255+
inline PyObject* createNumpyArray(Py_ssize_t size, const char* dtype_str) {
256+
NumpyTypeCache& c = getCache();
257+
if (!c.available || !c.numpy_module) {
258+
PyErr_SetString(PyExc_ImportError, "numpy is not available");
259+
return nullptr;
260+
}
261+
262+
PyObject* empty_func = PyObject_GetAttrString(c.numpy_module, "empty");
263+
if (!empty_func) return nullptr;
264+
265+
PyObject* shape = PyLong_FromSsize_t(size);
266+
PyObject* dtype = PyObject_GetAttrString(c.numpy_module, dtype_str);
267+
if (!dtype) {
268+
Py_DECREF(empty_func);
269+
Py_DECREF(shape);
270+
return nullptr;
271+
}
272+
273+
PyObject* args = PyTuple_Pack(1, shape);
274+
PyObject* kwargs = PyDict_New();
275+
PyDict_SetItemString(kwargs, "dtype", dtype);
276+
277+
PyObject* result = PyObject_Call(empty_func, args, kwargs);
278+
279+
Py_DECREF(empty_func);
280+
Py_DECREF(shape);
281+
Py_DECREF(dtype);
282+
Py_DECREF(args);
283+
Py_DECREF(kwargs);
284+
285+
return result;
286+
}
287+
288+
// RAII wrapper for Py_buffer
289+
struct ScopedBuffer {
290+
Py_buffer view;
291+
bool valid;
292+
293+
ScopedBuffer(PyObject* obj, int flags = PyBUF_FORMAT | PyBUF_STRIDES) {
294+
valid = (PyObject_GetBuffer(obj, &view, flags) == 0);
295+
}
296+
297+
~ScopedBuffer() {
298+
if (valid) PyBuffer_Release(&view);
299+
}
300+
301+
ScopedBuffer(const ScopedBuffer&) = delete;
302+
ScopedBuffer& operator=(const ScopedBuffer&) = delete;
303+
};
304+
305+
} // namespace NumpyInterop

typed_python/PyInstance.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
#include <Python.h>
1818
#include <pystate.h>
1919

20-
#include <numpy/arrayobject.h>
2120
#include <type_traits>
2221

2322
#include "AllTypes.hpp"

0 commit comments

Comments
 (0)