-
-
Notifications
You must be signed in to change notification settings - Fork 10.8k
BUG/ENH: Fix use of ndpointer in return values #12431
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,7 +55,9 @@ | |
'c_intp', 'as_ctypes', 'as_array'] | ||
|
||
import os | ||
from numpy import integer, ndarray, dtype as _dtype, deprecate, array | ||
from numpy import ( | ||
integer, ndarray, dtype as _dtype, deprecate, array, frombuffer | ||
) | ||
from numpy.core.multiarray import _flagdict, flagsobj | ||
|
||
try: | ||
|
@@ -175,24 +177,6 @@ def _flags_fromnum(num): | |
|
||
|
||
class _ndptr(_ndptr_base): | ||
|
||
def _check_retval_(self): | ||
"""This method is called when this class is used as the .restype | ||
attribute for a shared-library function. It constructs a numpy | ||
array from a void pointer.""" | ||
return array(self) | ||
|
||
@property | ||
def __array_interface__(self): | ||
return {'descr': self._dtype_.descr, | ||
'__ref': self, | ||
'strides': None, | ||
'shape': self._shape_, | ||
'version': 3, | ||
'typestr': self._dtype_.descr[0][1], | ||
'data': (self.value, False), | ||
} | ||
|
||
@classmethod | ||
def from_param(cls, obj): | ||
if not isinstance(obj, ndarray): | ||
|
@@ -213,6 +197,34 @@ def from_param(cls, obj): | |
return obj.ctypes | ||
|
||
|
||
class _concrete_ndptr(_ndptr): | ||
""" | ||
Like _ndptr, but with `_shape_` and `_dtype_` specified. | ||
|
||
Notably, this means the pointer has enough information to reconstruct | ||
the array, which is not generally true. | ||
""" | ||
def _check_retval_(self): | ||
""" | ||
This method is called when this class is used as the .restype | ||
attribute for a shared-library function, to automatically wrap the | ||
pointer into an array. | ||
""" | ||
return self.contents | ||
|
||
@property | ||
def contents(self): | ||
""" | ||
Get an ndarray viewing the data pointed to by this pointer. | ||
|
||
This mirrors the `contents` attribute of a normal ctypes pointer | ||
""" | ||
full_dtype = _dtype((self._dtype_, self._shape_)) | ||
full_ctype = ctypes.c_char * full_dtype.itemsize | ||
buffer = ctypes.cast(self, ctypes.POINTER(full_ctype)).contents | ||
return frombuffer(buffer, dtype=full_dtype).squeeze(axis=0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is sort of frustrating - there ought to be a better way to construct an array from a pointer without having to go through an intermediate byte array. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's why the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the goal of full_ctypes is to get an object supporting the buffer protocol that we can pass to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting thought. I'd mostly be concerned if memory or speed was a problem with the current approach. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memory shouldn't be a concern - this doesn't make any copies of the underlying data - at most we're churning around a bunch of metadata. |
||
|
||
|
||
# Factory for an array-checking class with from_param defined for | ||
# use with ctypes argtypes mechanism | ||
_pointer_type_cache = {} | ||
|
@@ -320,7 +332,12 @@ def ndpointer(dtype=None, ndim=None, shape=None, flags=None): | |
if flags is not None: | ||
name += "_"+"_".join(flags) | ||
|
||
klass = type("ndpointer_%s"%name, (_ndptr,), | ||
if dtype is not None and shape is not None: | ||
base = _concrete_ndptr | ||
else: | ||
base = _ndptr | ||
|
||
klass = type("ndpointer_%s"%name, (base,), | ||
{"_dtype_": dtype, | ||
"_shape_" : shape, | ||
"_ndim_" : ndim, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,20 +9,30 @@ | |
from numpy.testing import assert_, assert_array_equal, assert_raises, assert_equal | ||
|
||
try: | ||
import ctypes | ||
except ImportError: | ||
ctypes = None | ||
else: | ||
cdll = None | ||
test_cdll = None | ||
if hasattr(sys, 'gettotalrefcount'): | ||
try: | ||
cdll = load_library('_multiarray_umath_d', np.core._multiarray_umath.__file__) | ||
except OSError: | ||
pass | ||
try: | ||
test_cdll = load_library('_multiarray_tests', np.core._multiarray_tests.__file__) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Long lines here and above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missed this one. It's not clear to me why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you looking to fix this, or are you going to try the cpython approach and remove an argument? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the lines in this section were long before this patch, I'd be inclined leave things as they are, and not spend more CI cycles on whitespace changes :). In a future patch I might try using CDLL directly here and seeing if any CI breaks. |
||
except OSError: | ||
pass | ||
if cdll is None: | ||
cdll = load_library('_multiarray_umath', np.core._multiarray_umath.__file__) | ||
_HAS_CTYPE = True | ||
except ImportError: | ||
_HAS_CTYPE = False | ||
if test_cdll is None: | ||
test_cdll = load_library('_multiarray_tests', np.core._multiarray_tests.__file__) | ||
|
||
c_forward_pointer = test_cdll.forward_pointer | ||
|
||
|
||
@pytest.mark.skipif(not _HAS_CTYPE, | ||
@pytest.mark.skipif(ctypes is None, | ||
reason="ctypes not available in this python") | ||
@pytest.mark.skipif(sys.platform == 'cygwin', | ||
reason="Known to fail on cygwin") | ||
|
@@ -117,8 +127,63 @@ def test_cache(self): | |
assert_(ndpointer(shape=2) is not ndpointer(ndim=2)) | ||
assert_(ndpointer(ndim=2) is not ndpointer(shape=2)) | ||
|
||
@pytest.mark.skipif(ctypes is None, | ||
reason="ctypes not available on this python installation") | ||
class TestNdpointerCFunc(object): | ||
def test_arguments(self): | ||
""" Test that arguments are coerced from arrays """ | ||
c_forward_pointer.restype = ctypes.c_void_p | ||
c_forward_pointer.argtypes = (ndpointer(ndim=2),) | ||
|
||
c_forward_pointer(np.zeros((2, 3))) | ||
# too many dimensions | ||
assert_raises( | ||
ctypes.ArgumentError, c_forward_pointer, np.zeros((2, 3, 4))) | ||
|
||
@pytest.mark.parametrize( | ||
'dt', [ | ||
float, | ||
np.dtype(dict( | ||
formats=['<i4', '<i4'], | ||
names=['a', 'b'], | ||
offsets=[0, 2], | ||
itemsize=6 | ||
)) | ||
], ids=[ | ||
'float', | ||
'overlapping-fields' | ||
] | ||
) | ||
def test_return(self, dt): | ||
""" Test that return values are coerced to arrays """ | ||
arr = np.zeros((2, 3), dt) | ||
ptr_type = ndpointer(shape=arr.shape, dtype=arr.dtype) | ||
|
||
c_forward_pointer.restype = ptr_type | ||
c_forward_pointer.argtypes = (ptr_type,) | ||
|
||
# check that the arrays are equivalent views on the same data | ||
arr2 = c_forward_pointer(arr) | ||
assert_equal(arr2.dtype, arr.dtype) | ||
assert_equal(arr2.shape, arr.shape) | ||
assert_equal( | ||
arr2.__array_interface__['data'], | ||
arr.__array_interface__['data'] | ||
) | ||
|
||
def test_vague_return_value(self): | ||
""" Test that vague ndpointer return values do not promote to arrays """ | ||
arr = np.zeros((2, 3)) | ||
ptr_type = ndpointer(dtype=arr.dtype) | ||
|
||
c_forward_pointer.restype = ptr_type | ||
c_forward_pointer.argtypes = (ptr_type,) | ||
|
||
ret = c_forward_pointer(arr) | ||
assert_(isinstance(ret, ptr_type)) | ||
|
||
|
||
@pytest.mark.skipif(not _HAS_CTYPE, | ||
@pytest.mark.skipif(ctypes is None, | ||
reason="ctypes not available on this python installation") | ||
class TestAsArray(object): | ||
def test_array(self): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the one hand, we could try to restore this - on the other, it didn't make a massive amount of sense anyway.