Skip to content

Commit

Permalink
ENH: Allow compiling compatibly to old NumPy versions
Browse files Browse the repository at this point in the history
The default compiles compatibly with 1.17.x, we allow going back to
1.15 (mainly because it is easy).

There were few additions in this time, a few structs grew and very
few API functions were added.  Added a way to mark API functions
as requiring a specific target version.

If a user wishes to use the *new* API, they have to add the definition:

    #define NPY_TARGET_VERSION NPY_1_22_API_VERSION

Before importing NumPy.  (Our version numbering is a bit funny
I first thought to use a hex version of the main NumPy version,
but since we already have the `NPY_1_22_API_VERSION` defines...)
  • Loading branch information
seberg committed Apr 4, 2023
1 parent 6309cf2 commit 3a81135
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 23 deletions.
4 changes: 3 additions & 1 deletion numpy/core/code_generators/cversions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,7 @@
# Version 16 (NumPy 1.23)
# NonNull attributes removed from numpy_api.py
# Version 16 (NumPy 1.24) No change.
# Version 16 (NumPy 1.25) No change.
0x00000010 = 04a7bf1e65350926a0e528798da263c0

# Version 17 (NumPy 1.25) No actual change.
0x00000011 = ca1aebdad799358149567d9d93cbca09
58 changes: 50 additions & 8 deletions numpy/core/code_generators/genapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import re
import sys
import importlib.util
import textwrap

from os.path import join

Expand Down Expand Up @@ -98,6 +99,33 @@ def _repl(str):
return str.replace('Bool', 'npy_bool')


class MinVersion:
def __init__(self, version):
""" Version should be the normal NumPy version, e.g. "1.25" """
major, minor = version.split(".")
self.version = f"NPY_{major}_{minor}_API_VERSION"

def __str__(self):
# Used by version hashing:
return self.version

def add_guard(self, name, normal_define):
"""Wrap a definition behind a version guard"""
numpy_target_help_pointer = (
"(Old_NumPy_target see also: "
"https://numpy.org/devdocs/dev/depending_on_numpy.html)")

wrap = textwrap.dedent(f"""
#if NPY_FEATURE_VERSION < {self.version}
#define {name} {numpy_target_help_pointer}
#else
{{define}}
#endif""")

# we only insert `define` later to avoid confusing dedent:
return wrap.format(define=normal_define)


class StealRef:
def __init__(self, arg):
self.arg = arg # counting from 1
Expand Down Expand Up @@ -391,7 +419,20 @@ class FunctionApi:
def __init__(self, name, index, annotations, return_type, args, api_name):
self.name = name
self.index = index
self.annotations = annotations

self.min_version = None
self.annotations = []
for annotation in annotations:
# String checks, because manual import breaks isinstance
if type(annotation).__name__ == "StealRef":
self.annotations.append(annotation)
elif type(annotation).__name__ == "MinVersion":
if self.min_version is not None:
raise ValueError("Two minimum versions specified!")
self.min_version = annotation
else:
raise ValueError(f"unknown annotation {annotation}")

self.return_type = return_type
self.args = args
self.api_name = api_name
Expand All @@ -403,13 +444,14 @@ def _argtypes_string(self):
return argstr

def define_from_array_api_string(self):
define = """\
#define %s \\\n (*(%s (*)(%s)) \\
%s[%d])""" % (self.name,
self.return_type,
self._argtypes_string(),
self.api_name,
self.index)
arguments = self._argtypes_string()
define = textwrap.dedent(f"""\
#define {self.name} \\
(*({self.return_type} (*)({arguments})) \\
{self.api_name}[{self.index}])""")

if self.min_version is not None:
define = self.min_version.add_guard(self.name, define)
return define

def array_api_define(self):
Expand Down
12 changes: 6 additions & 6 deletions numpy/core/code_generators/numpy_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
import importlib.util


def get_StealRef():
def get_annotations():
# Convoluted because we can't import from numpy.distutils
# (numpy is not yet built)
genapi_py = os.path.join(os.path.dirname(__file__), 'genapi.py')
spec = importlib.util.spec_from_file_location('conv_template', genapi_py)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.StealRef
return mod.StealRef, mod.MinVersion


StealRef = get_StealRef()
StealRef, MinVersion = get_annotations()
#from code_generators.genapi import StealRef

# index, type
Expand Down Expand Up @@ -367,8 +367,8 @@ def get_StealRef():
'PyArray_ResolveWritebackIfCopy': (302,),
'PyArray_SetWritebackIfCopyBase': (303,),
# End 1.14 API
'PyDataMem_SetHandler': (304,),
'PyDataMem_GetHandler': (305,),
'PyDataMem_SetHandler': (304, MinVersion("1.22")),
'PyDataMem_GetHandler': (305, MinVersion("1.22")),
# End 1.22 API
}

Expand Down Expand Up @@ -422,7 +422,7 @@ def get_StealRef():
# End 1.7 API
'PyUFunc_RegisterLoopForDescr': (41,),
# End 1.8 API
'PyUFunc_FromFuncAndDataAndSignatureAndIdentity': (42,),
'PyUFunc_FromFuncAndDataAndSignatureAndIdentity': (42, MinVersion("1.16")),
# End 1.16 API
}

Expand Down
4 changes: 4 additions & 0 deletions numpy/core/include/numpy/arrayscalars.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ typedef struct {
/* note that the PyObject_HEAD macro lives right here */
PyUnicodeObject base;
Py_UCS4 *obval;
#if NPY_FEATURE_VERSION >= NPY_1_20_API_VERSION
char *buffer_fmt;
#endif
} PyUnicodeScalarObject;


Expand All @@ -149,7 +151,9 @@ typedef struct {
PyArray_Descr *descr;
int flags;
PyObject *base;
#if NPY_FEATURE_VERSION >= NPY_1_20_API_VERSION
void *_buffer_info; /* private buffer info, tagged to allow warning */
#endif
} PyVoidScalarObject;

/* Macros
Expand Down
16 changes: 11 additions & 5 deletions numpy/core/include/numpy/ndarraytypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -712,11 +712,15 @@ typedef struct tagPyArrayObject_fields {
int flags;
/* For weak references */
PyObject *weakreflist;
#if NPY_FEATURE_VERSION >= NPY_1_20_API_VERSION
void *_buffer_info; /* private buffer info, tagged to allow warning */
#endif
/*
* For malloc/calloc/realloc/free per object
*/
#if NPY_FEATURE_VERSION >= NPY_1_22_API_VERSION
PyObject *mem_handler;
#endif
} PyArrayObject_fields;

/*
Expand Down Expand Up @@ -1654,11 +1658,13 @@ PyArray_CLEARFLAGS(PyArrayObject *arr, int flags)
((PyArrayObject_fields *)arr)->flags &= ~flags;
}

static inline NPY_RETURNS_BORROWED_REF PyObject *
PyArray_HANDLER(PyArrayObject *arr)
{
return ((PyArrayObject_fields *)arr)->mem_handler;
}
#if NPY_FEATURE_VERSION >= NPY_1_22_API_VERSION
static inline NPY_RETURNS_BORROWED_REF PyObject *
PyArray_HANDLER(PyArrayObject *arr)
{
return ((PyArrayObject_fields *)arr)->mem_handler;
}
#endif

#define PyTypeNum_ISBOOL(type) ((type) == NPY_BOOL)

Expand Down
6 changes: 4 additions & 2 deletions numpy/core/include/numpy/ufuncobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ typedef struct _tagPyUFuncObject {
npy_uint32 iter_flags;

/* New in NPY_API_VERSION 0x0000000D and above */

#if NPY_FEATURE_VERSION >= NPY_1_16_API_VERSION
/*
* for each core_num_dim_ix distinct dimension names,
* the possible "frozen" size (-1 if not frozen).
Expand All @@ -211,13 +211,15 @@ typedef struct _tagPyUFuncObject {

/* Identity for reduction, when identity == PyUFunc_IdentityValue */
PyObject *identity_value;
#endif /* NPY_FEATURE_VERSION >= NPY_1_16_API_VERSION */

/* New in NPY_API_VERSION 0x0000000F and above */

#if NPY_FEATURE_VERSION >= NPY_1_22_API_VERSION
/* New private fields related to dispatching */
void *_dispatch_cache;
/* A PyListObject of `(tuple of DTypes, ArrayMethod/Promoter)` */
PyObject *_loops;
#endif
} PyUFuncObject;

#include "arrayobject.h"
Expand Down
7 changes: 6 additions & 1 deletion numpy/core/setup_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
# 0x0000000f - 1.22.x
# 0x00000010 - 1.23.x
# 0x00000010 - 1.24.x
C_API_VERSION = 0x00000010
# 0x00000011 - 1.25.x
C_API_VERSION = 0x00000011

# When compiling against NumPy (downstream libraries), NumPy will by default
# pick an older feature version. For example, for 1.25.x we default to the
Expand Down Expand Up @@ -96,6 +97,10 @@ def check_api_version(apiversion, codegen_dir):
f"checksum in core/codegen_dir/cversions.txt is {api_hash}. If "
"functions were added in the C API, you have to update "
f"C_API_VERSION in {__file__}."
"\n"
"Please make sure that new additions are guarded with "
"MinVersion() to make them unavailable when wishing support "
"for older NumPy versions."
)
raise MismatchCAPIError(msg)

Expand Down

0 comments on commit 3a81135

Please sign in to comment.