Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API: Make numpy.h compatible with both NumPy 1.x and 2.x #5050

Merged
merged 25 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3c45949
API: Make `numpy.h` compatible with both NumPy 1.x and 2.x
seberg Mar 6, 2024
9116d69
TST: Update numpy dtype flags test to not covert flags to char
seberg Mar 6, 2024
ea9ba5f
API: Add `numpy2.h` instead and make `numpy.h` safe
seberg Mar 8, 2024
4555280
API: Rather than `numpy2.h` use a define for the user.
seberg Mar 8, 2024
0d585db
Thread `PYBIND11_NUMPY2_SUPPORT` through things and try to adept test…
seberg Mar 11, 2024
0a87436
Small fixups (shouldn't matter)?
seberg Mar 11, 2024
25df74c
Fixup. Does upgrading scipy help? (it shouldn't?)
seberg Mar 11, 2024
f0457a5
Use NumPy 2 nightlies for ubuntu-latest job also
seberg Mar 12, 2024
247d5c3
BUG: Fix numpy.bool check
seberg Mar 12, 2024
5368842
TST: Fix complexwarning
seberg Mar 12, 2024
96157d6
BUG: Fix the fact that only the 50 slot is filled with the copy alias
seberg Mar 12, 2024
6daf11a
TST: One more test tweak
seberg Mar 12, 2024
5cf73bc
TST: Use "long" name for long, since it changed on windows
seberg Mar 12, 2024
a6414ff
TST: Apparently we didn't always have ulong, so just use `L`
seberg Mar 12, 2024
79393f5
TST: Enforce dtype='l' for test as default isn't long anymore on windows
seberg Mar 12, 2024
2514af5
Rename macro and invert logic to PYBIND11_NUMPY_1_ONLY
seberg Mar 16, 2024
242300d
PYBIND11_INTERNAL_NUMPY_1_ONLY_DETECTED
Mar 19, 2024
a6b2e11
Test and code comment expansion
seberg Mar 22, 2024
99b785a
CI: Use pre-releases of numpy/scipy from pip via explicit version
seberg Mar 22, 2024
525310f
CI: NumPy 2 only available on almalinux (as it is Python >=3.9)
seberg Mar 22, 2024
8d718ff
MAINT: Match name more exactly and adopt error phrasing
seberg Mar 22, 2024
e2e923d
MAINT: Pushed early, move helper to be private member
seberg Mar 22, 2024
c4bd0e0
fix error message compilation when using NumPy 1.x-only backcompat
seberg Mar 22, 2024
38f8ead
silence name shadowing warning
seberg Mar 22, 2024
f3af194
chore: minor optimization
henryiii Mar 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,14 @@ jobs:
run: python -m pip install pytest-github-actions-annotate-failures

# First build - C++11 mode and inplace
# More-or-less randomly adding -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON here.
# More-or-less randomly adding -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON here
# (same for PYBIND11_NUMPY2_SUPPORT).
- name: Configure C++11 ${{ matrix.args }}
run: >
cmake -S . -B .
-DPYBIND11_WERROR=ON
-DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON
-DPYBIND11_NUMPY2_SUPPORT=ON
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=11
Expand All @@ -138,11 +140,13 @@ jobs:

# Second build - C++17 mode and in a build directory
# More-or-less randomly adding -DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF here.
# (same for PYBIND11_NUMPY2_SUPPORT, but requires a NumPy 1.x at runtime).
- name: Configure C++17
run: >
cmake -S . -B build2
-DPYBIND11_WERROR=ON
-DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF
-DPYBIND11_NUMPY2_SUPPORT=OFF
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=17
Expand Down Expand Up @@ -895,8 +899,10 @@ jobs:
python-version: ${{ matrix.python }}

- name: Prepare env
# Ensure use of NumPy 2 (via NumPy nightlies but can be changed soon)
run: |
python3 -m pip install -r tests/requirements.txt
python3 -m pip install --upgrade --pre -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy scipy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have an explicit version check here? To make sure that numpy 2 actually gets installed / got installed?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both are on regular PyPI now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, plus I keep forgetting that >=2.0.0b1 works well, so changed. Changed.


- name: Update CMake
uses: jwlawson/[email protected]
Expand All @@ -905,6 +911,7 @@ jobs:
run: >
cmake -S . -B build
-DPYBIND11_WERROR=ON
-DPYBIND11_NUMPY2_SUPPORT=ON
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=20
Expand All @@ -925,6 +932,7 @@ jobs:
run: >
cmake -S . -B build_partial
-DPYBIND11_WERROR=ON
-DPYBIND11_NUMPY2_SUPPORT=ON
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=20
Expand Down
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,17 @@ option(PYBIND11_TEST "Build pybind11 test suite?" ${PYBIND11_MASTER_PROJECT})
option(PYBIND11_NOPYTHON "Disable search for Python" OFF)
option(PYBIND11_SIMPLE_GIL_MANAGEMENT
"Use simpler GIL management logic that does not support disassociation" OFF)
option(PYBIND11_NUMPY2_SUPPORT "Enable compile in a NumPy 2 and 1 compatible way" OFF)
set(PYBIND11_INTERNALS_VERSION
""
CACHE STRING "Override the ABI version, may be used to enable the unstable ABI.")

if(PYBIND11_SIMPLE_GIL_MANAGEMENT)
add_compile_definitions(PYBIND11_SIMPLE_GIL_MANAGEMENT)
endif()
if(PYBIND11_NUMPY2_SUPPORT)
add_compile_definitions(PYBIND11_NUMPY2_SUPPORT)
endif()

cmake_dependent_option(
USE_PYTHON_INCLUDE_DIR
Expand Down
116 changes: 108 additions & 8 deletions include/pybind11/numpy.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
/* This will be true on all flat address space platforms and allows us to reduce the
whole npy_intp / ssize_t / Py_intptr_t business down to just ssize_t for all size
and dimension types (e.g. shape, strides, indexing), instead of inflicting this
upon the library user. */
upon the library user.
Note that NumPy 2 now uses ssize_t for `npy_intp` to simplify this. */
static_assert(sizeof(::pybind11::ssize_t) == sizeof(Py_intptr_t), "ssize_t != Py_intptr_t");
static_assert(std::is_signed<Py_intptr_t>::value, "Py_intptr_t must be signed");
// We now can reinterpret_cast between py::ssize_t and Py_intptr_t (MSVC + PyPy cares)
Expand All @@ -53,7 +54,8 @@ struct handle_type_name<array> {
template <typename type, typename SFINAE = void>
struct npy_format_descriptor;

struct PyArrayDescr_Proxy {
/* NumPy 1 proxy (always includes legacy fields) */
struct PyArrayDescr1_Proxy {
PyObject_HEAD
PyObject *typeobj;
char kind;
Expand All @@ -68,6 +70,43 @@ struct PyArrayDescr_Proxy {
PyObject *names;
};

#ifdef PYBIND11_NUMPY2_SUPPORT
struct PyArrayDescr_Proxy {
PyObject_HEAD
PyObject *typeobj;
char kind;
char type;
char byteorder;
char _former_flags;
int type_num;
/* Additional fields are NumPy version specific. */
};
#else
/* NumPy 1.x only, we can expose all fields */
using PyArrayDescr_Proxy = PyArrayDescr1_Proxy;
#endif

/* NumPy 2 proxy, including legacy fields */
struct PyArrayDescr2_Proxy {
PyObject_HEAD
PyObject *typeobj;
char kind;
char type;
char byteorder;
char _former_flags;
int type_num;
std::uint64_t flags;
ssize_t elsize;
ssize_t alignment;
PyObject *metadata;
Py_hash_t hash;
void *reserved_null[2];
/* The following fields only exist if 0 <= type_num < 2056 */
char *subarray;
PyObject *fields;
PyObject *names;
};

struct PyArray_Proxy {
PyObject_HEAD
char *data;
Expand Down Expand Up @@ -131,6 +170,13 @@ PYBIND11_NOINLINE module_ import_numpy_core_submodule(const char *submodule_name
object numpy_version = numpy_lib.attr("NumpyVersion")(version_string);
int major_version = numpy_version.attr("major").cast<int>();

#ifndef PYBIND11_NUMPY2_SUPPORT
if (major_version >= 2) {
throw std::runtime_error("module compiled without NumPy 2 support. Please define "
"PYBIND11_NUMPY2_SUPPORT before including `numpy2.h` "
"or define it during build to enable NumPy 2 support.");
}
#endif
/* `numpy.core` was renamed to `numpy._core` in NumPy 2.0 as it officially
became a private module. */
std::string numpy_core_path = major_version >= 2 ? "numpy._core" : "numpy.core";
Expand Down Expand Up @@ -203,6 +249,8 @@ struct npy_api {
NPY_ULONG_, NPY_ULONGLONG_, NPY_UINT_),
};

unsigned int PyArray_RUNTIME_VERSION_;

struct PyArray_Dims {
Py_intptr_t *ptr;
int len;
Expand Down Expand Up @@ -241,6 +289,7 @@ struct npy_api {
PyObject *(*PyArray_FromAny_)(PyObject *, PyObject *, int, int, int, PyObject *);
int (*PyArray_DescrConverter_)(PyObject *, PyObject **);
bool (*PyArray_EquivTypes_)(PyObject *, PyObject *);
#ifndef PYBIND11_NUMPY2_SUPPORT
int (*PyArray_GetArrayParamsFromObject_)(PyObject *,
PyObject *,
unsigned char,
Expand All @@ -249,6 +298,7 @@ struct npy_api {
Py_intptr_t *,
PyObject **,
PyObject *);
#endif
PyObject *(*PyArray_Squeeze_)(PyObject *);
// Unused. Not removed because that affects ABI of the class.
int (*PyArray_SetBaseObject_)(PyObject *, PyObject *);
Expand All @@ -275,7 +325,9 @@ struct npy_api {
API_PyArray_View = 137,
API_PyArray_DescrConverter = 174,
API_PyArray_EquivTypes = 182,
#ifndef PYBIND11_NUMPY2_SUPPORT
API_PyArray_GetArrayParamsFromObject = 278,
#endif
API_PyArray_SetBaseObject = 282
};

Expand All @@ -290,7 +342,8 @@ struct npy_api {
npy_api api;
#define DECL_NPY_API(Func) api.Func##_ = (decltype(api.Func##_)) api_ptr[API_##Func];
DECL_NPY_API(PyArray_GetNDArrayCFeatureVersion);
if (api.PyArray_GetNDArrayCFeatureVersion_() < 0x7) {
api.PyArray_RUNTIME_VERSION_ = api.PyArray_GetNDArrayCFeatureVersion_();
if (api.PyArray_RUNTIME_VERSION_ < 0x7) {
pybind11_fail("pybind11 numpy support requires numpy >= 1.7.0");
}
DECL_NPY_API(PyArray_Type);
Expand All @@ -309,7 +362,9 @@ struct npy_api {
DECL_NPY_API(PyArray_View);
DECL_NPY_API(PyArray_DescrConverter);
DECL_NPY_API(PyArray_EquivTypes);
#ifndef PYBIND11_NUMPY2_SUPPORT
DECL_NPY_API(PyArray_GetArrayParamsFromObject);
#endif
DECL_NPY_API(PyArray_SetBaseObject);

#undef DECL_NPY_API
Expand All @@ -331,6 +386,14 @@ inline const PyArrayDescr_Proxy *array_descriptor_proxy(const PyObject *ptr) {
return reinterpret_cast<const PyArrayDescr_Proxy *>(ptr);
}

inline const PyArrayDescr1_Proxy *array_descriptor1_proxy(const PyObject *ptr) {
return reinterpret_cast<const PyArrayDescr1_Proxy *>(ptr);
}

inline const PyArrayDescr2_Proxy *array_descriptor2_proxy(const PyObject *ptr) {
return reinterpret_cast<const PyArrayDescr2_Proxy *>(ptr);
}

inline bool check_flags(const void *ptr, int flag) {
return (flag == (array_proxy(ptr)->flags & flag));
}
Expand Down Expand Up @@ -610,10 +673,31 @@ class dtype : public object {
}

/// Size of the data type in bytes.
#ifndef PYBIND11_NUMPY2_SUPPORT
ssize_t itemsize() const { return detail::array_descriptor_proxy(m_ptr)->elsize; }
#else
ssize_t itemsize() const {
if (detail::npy_api::get().PyArray_RUNTIME_VERSION_ < 0x12) {
return detail::array_descriptor1_proxy(m_ptr)->elsize;
}
return detail::array_descriptor2_proxy(m_ptr)->elsize;
}
#endif

/// Returns true for structured data types.
#ifndef PYBIND11_NUMPY2_SUPPORT
bool has_fields() const { return detail::array_descriptor_proxy(m_ptr)->names != nullptr; }
#else
bool has_fields() const {
if (detail::npy_api::get().PyArray_RUNTIME_VERSION_ < 0x12) {
return detail::array_descriptor1_proxy(m_ptr)->names != nullptr;
}
if (num() < 0 || num() >= 2056) {
henryiii marked this conversation as resolved.
Show resolved Hide resolved
henryiii marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
return detail::array_descriptor2_proxy(m_ptr)->names != nullptr;
}
#endif

/// Single-character code for dtype's kind.
/// For example, floating point types are 'f' and integral types are 'i'.
Expand All @@ -639,11 +723,29 @@ class dtype : public object {
/// Single character for byteorder
char byteorder() const { return detail::array_descriptor_proxy(m_ptr)->byteorder; }

/// Alignment of the data type
/// Alignment of the data type
#ifndef PYBIND11_NUMPY2_SUPPORT
int alignment() const { return detail::array_descriptor_proxy(m_ptr)->alignment; }
#else
ssize_t alignment() const {
if (detail::npy_api::get().PyArray_RUNTIME_VERSION_ < 0x12) {
return detail::array_descriptor1_proxy(m_ptr)->alignment;
}
return detail::array_descriptor2_proxy(m_ptr)->alignment;
}
#endif

/// Flags for the array descriptor
/// Flags for the array descriptor
#ifndef PYBIND11_NUMPY2_SUPPORT
char flags() const { return detail::array_descriptor_proxy(m_ptr)->flags; }
#else
std::uint64_t flags() const {
henryiii marked this conversation as resolved.
Show resolved Hide resolved
if (detail::npy_api::get().PyArray_RUNTIME_VERSION_ < 0x12) {
return (unsigned char) detail::array_descriptor1_proxy(m_ptr)->flags;
}
return detail::array_descriptor2_proxy(m_ptr)->flags;
}
#endif

private:
static object &_dtype_from_pep3118() {
Expand Down Expand Up @@ -810,9 +912,7 @@ class array : public buffer {
}

/// Byte size of a single element
ssize_t itemsize() const {
return detail::array_descriptor_proxy(detail::array_proxy(m_ptr)->descr)->elsize;
}
ssize_t itemsize() const { return dtype().itemsize(); }
henryiii marked this conversation as resolved.
Show resolved Hide resolved

/// Total number of bytes
ssize_t nbytes() const { return size() * itemsize(); }
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,5 @@ def pytest_report_header(config):
f" {pybind11_tests.cpp_std}"
f" {pybind11_tests.PYBIND11_INTERNALS_ID}"
f" PYBIND11_SIMPLE_GIL_MANAGEMENT={pybind11_tests.PYBIND11_SIMPLE_GIL_MANAGEMENT}"
f" PYBIND11_NUMPY2_SUPPORT={pybind11_tests.PYBIND11_NUMPY2_SUPPORT}"
)
6 changes: 6 additions & 0 deletions tests/pybind11_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ PYBIND11_MODULE(pybind11_tests, m) {
#else
false;
#endif
m.attr("PYBIND11_NUMPY2_SUPPORT") =
#if defined(PYBIND11_NUMPY2_SUPPORT)
true;
#else
false;
#endif

bind_ConstructorStats(m);

Expand Down
8 changes: 7 additions & 1 deletion tests/test_numpy_dtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

import env # noqa: F401
from pybind11_tests import PYBIND11_NUMPY2_SUPPORT
from pybind11_tests import numpy_dtypes as m

np = pytest.importorskip("numpy")
Expand Down Expand Up @@ -178,7 +179,12 @@ def test_dtype(simple_dtype):
assert m.test_dtype_num() == [np.dtype(ch).num for ch in expected_chars]
assert m.test_dtype_byteorder() == [np.dtype(ch).byteorder for ch in expected_chars]
assert m.test_dtype_alignment() == [np.dtype(ch).alignment for ch in expected_chars]
assert m.test_dtype_flags() == [chr(np.dtype(ch).flags) for ch in expected_chars]
if PYBIND11_NUMPY2_SUPPORT:
assert m.test_dtype_flags() == [np.dtype(ch).flags for ch in expected_chars]
else:
assert m.test_dtype_flags() == [
chr(np.dtype(ch).flags) for ch in expected_chars
]


def test_recarray(simple_dtype, packed_dtype):
Expand Down
Loading