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 all 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
13 changes: 12 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_NUMPY_1_ONLY, but requires a NumPy 1.x at runtime).
- name: Configure C++11 ${{ matrix.args }}
run: >
cmake -S . -B .
-DPYBIND11_WERROR=ON
-DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON
-DPYBIND11_NUMPY_1_ONLY=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_NUMPY_1_ONLY, 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_NUMPY_1_ONLY=ON
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=17
Expand Down Expand Up @@ -660,6 +664,11 @@ jobs:
run: |
python3 -m pip install cmake -r tests/requirements.txt

- name: Ensure NumPy 2 is used (required Python >= 3.9)
if: matrix.container == 'almalinux:9'
run: |
python3 -m pip install 'numpy>=2.0.0b1' 'scipy>=1.13.0rc1'

- name: Configure
shell: bash
run: >
Expand Down Expand Up @@ -895,8 +904,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 'numpy>=2.0.0b1' 'scipy>=1.13.0rc1'

- name: Update CMake
uses: jwlawson/[email protected]
Expand Down
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,18 @@ 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_NUMPY_1_ONLY
"Disable NumPy 2 support to avoid changes to previous pybind11 versions." 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_NUMPY_1_ONLY)
add_compile_definitions(PYBIND11_NUMPY_1_ONLY)
endif()

cmake_dependent_option(
USE_PYTHON_INCLUDE_DIR
Expand Down
14 changes: 12 additions & 2 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,9 @@ class type_caster<bool> {
value = false;
return true;
}
if (convert || (std::strcmp("numpy.bool_", Py_TYPE(src.ptr())->tp_name) == 0)) {
// (allow non-implicit conversion for numpy booleans)
if (convert || is_numpy_bool(src)) {
// (allow non-implicit conversion for numpy booleans), use strncmp
// since NumPy 1.x had an additional trailing underscore.

Py_ssize_t res = -1;
if (src.is_none()) {
Expand Down Expand Up @@ -360,6 +361,15 @@ class type_caster<bool> {
return handle(src ? Py_True : Py_False).inc_ref();
}
PYBIND11_TYPE_CASTER(bool, const_name("bool"));

private:
// Test if an object is a NumPy boolean (without fetching the type).
static inline bool is_numpy_bool(handle object) {
const char *type_name = Py_TYPE(object.ptr())->tp_name;
// Name changed to `numpy.bool` in NumPy 2, `numpy.bool_` is needed for 1.x support
return std::strcmp("numpy.bool", type_name) == 0
|| std::strcmp("numpy.bool_", type_name) == 0;
}
};

// Helper class for UTF-{8,16,32} C++ stl strings:
Expand Down
4 changes: 4 additions & 0 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ PYBIND11_WARNING_DISABLE_MSVC(4505)
# undef copysign
#endif

#if defined(PYBIND11_NUMPY_1_ONLY)
# define PYBIND11_INTERNAL_NUMPY_1_ONLY_DETECTED
#endif

#if defined(PYPY_VERSION) && !defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
# define PYBIND11_SIMPLE_GIL_MANAGEMENT
#endif
Expand Down
125 changes: 116 additions & 9 deletions include/pybind11/numpy.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@
#include <utility>
#include <vector>

#if defined(PYBIND11_NUMPY_1_ONLY) && !defined(PYBIND11_INTERNAL_NUMPY_1_ONLY_DETECTED)
# error PYBIND11_NUMPY_1_ONLY must be defined before any pybind11 header is included.
#endif

/* 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 +58,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 +74,43 @@ struct PyArrayDescr_Proxy {
PyObject *names;
};

#ifndef PYBIND11_NUMPY_1_ONLY
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 +174,14 @@ 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>();

#ifdef PYBIND11_NUMPY_1_ONLY
if (major_version >= 2) {
throw std::runtime_error(
"This extension was built with PYBIND11_NUMPY_1_ONLY defined, "
"but NumPy 2 is used in this process. For NumPy2 compatibility, "
"this extension needs to be rebuilt without the PYBIND11_NUMPY_1_ONLY define.");
}
#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 +254,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 +294,7 @@ struct npy_api {
PyObject *(*PyArray_FromAny_)(PyObject *, PyObject *, int, int, int, PyObject *);
int (*PyArray_DescrConverter_)(PyObject *, PyObject **);
bool (*PyArray_EquivTypes_)(PyObject *, PyObject *);
#ifdef PYBIND11_NUMPY_1_ONLY
int (*PyArray_GetArrayParamsFromObject_)(PyObject *,
PyObject *,
unsigned char,
Expand All @@ -249,6 +303,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 @@ -266,7 +321,8 @@ struct npy_api {
API_PyArray_DescrFromScalar = 57,
API_PyArray_FromAny = 69,
API_PyArray_Resize = 80,
API_PyArray_CopyInto = 82,
// CopyInto was slot 82 and 50 was effectively an alias. NumPy 2 removed 82.
API_PyArray_CopyInto = 50,
henryiii marked this conversation as resolved.
Show resolved Hide resolved
API_PyArray_NewCopy = 85,
API_PyArray_NewFromDescr = 94,
API_PyArray_DescrNewFromType = 96,
Expand All @@ -275,7 +331,9 @@ struct npy_api {
API_PyArray_View = 137,
API_PyArray_DescrConverter = 174,
API_PyArray_EquivTypes = 182,
#ifdef PYBIND11_NUMPY_1_ONLY
API_PyArray_GetArrayParamsFromObject = 278,
#endif
API_PyArray_SetBaseObject = 282
};

Expand All @@ -290,7 +348,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 +368,9 @@ struct npy_api {
DECL_NPY_API(PyArray_View);
DECL_NPY_API(PyArray_DescrConverter);
DECL_NPY_API(PyArray_EquivTypes);
#ifdef PYBIND11_NUMPY_1_ONLY
DECL_NPY_API(PyArray_GetArrayParamsFromObject);
#endif
DECL_NPY_API(PyArray_SetBaseObject);

#undef DECL_NPY_API
Expand All @@ -331,6 +392,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 +679,32 @@ class dtype : public object {
}

/// Size of the data type in bytes.
#ifdef PYBIND11_NUMPY_1_ONLY
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.
#ifdef PYBIND11_NUMPY_1_ONLY
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;
}
const auto *proxy = detail::array_descriptor2_proxy(m_ptr);
if (proxy->type_num < 0 || proxy->type_num >= 2056) {
return false;
}
return proxy->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 +730,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
#ifdef PYBIND11_NUMPY_1_ONLY
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
#ifdef PYBIND11_NUMPY_1_ONLY
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 +919,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_NUMPY_1_ONLY={pybind11_tests.PYBIND11_NUMPY_1_ONLY}"
)
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_NUMPY_1_ONLY") =
#if defined(PYBIND11_NUMPY_1_ONLY)
true;
#else
false;
#endif

bind_ConstructorStats(m);

Expand Down
4 changes: 3 additions & 1 deletion tests/test_eigen_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,9 @@ def test_both_ref_mutators():
def test_nocopy_wrapper():
# get_elem requires a column-contiguous matrix reference, but should be
# callable with other types of matrix (via copying):
int_matrix_colmajor = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], order="F")
int_matrix_colmajor = np.array(
[[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype="l", order="F"
)
henryiii marked this conversation as resolved.
Show resolved Hide resolved
dbl_matrix_colmajor = np.array(
int_matrix_colmajor, dtype="double", order="F", copy=True
)
Expand Down
Loading
Loading