Skip to content

WIP: Remove fragile use of __array_interface__ in ctypeslib.as_array #10970

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

Merged
merged 4 commits into from
Jun 7, 2018

Conversation

eric-wieser
Copy link
Member

At this point, we can rely on PEP3118 support within ctypes and numpy

Fixes gh-10968
Fixes gh-2671
Closes gh-6214 (commit is cherry-picked, thanks @tynn!)

'data': (ct.addressof(self), False),
}

simple_type.__array_interface__ = property(__array_interface__)
Copy link
Member Author

Choose a reason for hiding this comment

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

Monkey-patching ctypes was undocumented, and has confused me in the past!

# Prep that numerical ctypes types:
for types, code in simple_types:
for tp in types:
prep_simple(tp, "%c%d" % (code, ct.sizeof(tp)))
Copy link
Member Author

Choose a reason for hiding this comment

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

This is already encoded in the implementation of dtype.str.__get__

@eric-wieser
Copy link
Member Author

eric-wieser commented Apr 25, 2018

While we're talking about PEP3118 and ctypes - could I ask someone who's worked with PEP3118 (@ahaldane?) to give a review to python/cpython#5561 and python/cpython#5576? It would be great if numpy would work with ctype structs as well, and those PRs have been sitting idle for a while


def as_array(obj, shape=None):
def as_array(obj, shape=()):
Copy link
Member

Choose a reason for hiding this comment

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

it would be safer to have shape=None and in the body of the code if shape is None: shape = ()
While in the current version shape is read-only, this might future-proof the function

Copy link
Member Author

Choose a reason for hiding this comment

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

Safer from what perspective?

Copy link
Member

Choose a reason for hiding this comment

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

whoops, shape is a zero-length-tuple so immutable. Never mind, sorry for the noise.

Copy link
Member Author

Choose a reason for hiding this comment

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

You've made me realize that defaulting to None and erroring is probably closer to the old behaviour

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to reject shape=None

@eric-wieser eric-wieser changed the title BUG: Remove fragile use of __array_interface__ in ctypeslib.as_array BUG/WIP: Remove fragile use of __array_interface__ in ctypeslib.as_array Apr 25, 2018
@eric-wieser eric-wieser changed the title BUG/WIP: Remove fragile use of __array_interface__ in ctypeslib.as_array WIP: Remove fragile use of __array_interface__ in ctypeslib.as_array Apr 26, 2018
@eric-wieser
Copy link
Member Author

I suspect this fixes gh-6511 too

@charris
Copy link
Member

charris commented May 3, 2018

I'm hoping that this also fixes the daily wheels builds that have been broken since #10968 went in.

________________ TestNewBufferProtocol.test_error_too_many_dims ________________
self = <numpy.core.tests.test_multiarray.TestNewBufferProtocol object at 0x7f02878c8210>
    def test_error_too_many_dims(self):
        # construct a memoryview with 33 dimensions
        m = memoryview(self.c_u8_33d())
        assert_equal(m.ndim, 33)
    
        assert_raises_regex(
            RuntimeError, "ndim",
>           np.array, m)
m          = <memory at 0x7f028afb6cc8>
self       = <numpy.core.tests.test_multiarray.TestNewBufferProtocol object at 0x7f02878c8210>
/venv/local/lib/python2.7/site-packages/numpy/core/tests/test_multiarray.py:6543: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
self = <numpy.testing._private.utils._Dummy testMethod=nop>
expected_exception = <type 'exceptions.RuntimeError'>, expected_regexp = 'ndim'
callable_obj = <built-in function array>, args = (<memory at 0x7f028afb6cc8>,)
kwargs = {}
context = <unittest.case._AssertRaisesContext object at 0x7f02878c82d0>
    def assertRaisesRegexp(self, expected_exception, expected_regexp,
                           callable_obj=None, *args, **kwargs):
        """Asserts that the message in a raised exception matches a regexp.
    
            Args:
                expected_exception: Exception class expected to be raised.
                expected_regexp: Regexp (re pattern object or string) expected
                        to be found in error message.
                callable_obj: Function to be called.
                args: Extra args.
                kwargs: Extra kwargs.
            """
        context = _AssertRaisesContext(expected_exception, self, expected_regexp)
        if callable_obj is None:
            return context
        with context:
>           callable_obj(*args, **kwargs)
E           ValueError: '(1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)<B

@eric-wieser
Copy link
Member Author

eric-wieser commented May 4, 2018

It looks like you've truncated that error message by at least one character (a '?). I'd hope the error message has more context than just the format itself.

I don't predict this patch will fix that. Strange that it works fine on travis, but not the wheel builds.

@charris
Copy link
Member

charris commented May 4, 2018

Also works on Mac and Windows, only manylinux1 on Python 2.7 is a problem. See at https://travis-ci.org/MacPython/numpy-wheels/jobs/374373146.

Yeah, I kind of thought this PR wouldn't help, but wanted to get the ball rolling ...

@eric-wieser
Copy link
Member Author

@charris: First step would be to not replace the error message coming from the internal PEP3118 parser with that less useful one.

@eric-wieser
Copy link
Member Author

In light of #11115, I now worry that #10968 and this PR are relying on stable ctypes implementations that did not exist in non-recent versions of python 2.7. Do we have a stance on which python 2.7 versions are supported?

@charris
Copy link
Member

charris commented May 20, 2018

Do we have a stance on which python 2.7 versions are supported?

No, but maybe we should. Python 2.7.7 came out about 4 years ago and these days it is pretty easy to upgrade. Be worth raising the issue on the list. There were several ctypes fixes in the recently released Python 2.7.15, so if we do pick a version, it should only be new enough that our tests pass and otherwise recommend that folks stay current.

return {
_dtype(ctype).str: ctype
for ctype in simple_types
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this would read better if the } was at the end of the previous line. This looks too much like C.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe do the dictionary creation separately from the return statement.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we have different stylistic opinions here. How about I just put it on one line, since it fits nicely anyway?

'data': (ct.addressof(contents), False),
'shape': shape}
def _ctype_ndarray(el_type, shape):
""" Create an ndarray of the given element type and shape """
Copy link
Member

Choose a reason for hiding this comment

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

What is el_type? A complete docstring would help here.


def _get_typecodes():
""" Get a dictionary mapping __array_interface__ formats to ctypes types """
Copy link
Member

Choose a reason for hiding this comment

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

Maybe """Return ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Done


################################################################
# public functions
if ctypes is not None:
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we still need this. Much of the code here is ancient and predates the inclusion of ctypes into the standard library in Python 2.5.

The numpy array shares the memory with the ctypes object.

The size parameter must be given if converting from a ctypes POINTER.
The size parameter is ignored if converting from a ctypes array
The shape parameter must be given if converting from a ctypes POINTER.
Copy link
Member

Choose a reason for hiding this comment

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

A standard docstring would help here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm going to argue this is out of scope for this patch

prep_pointer(obj, shape)
else:
prep_array(tp)
if isinstance(obj, ctypes._Pointer):
Copy link
Member

Choose a reason for hiding this comment

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

Is using private class safe?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not a huge fan of using attribute testing here, which will give a false-positive for np.recarray with the wrong dtype. I suppose I could switch it back to how the detection was done before, if you'd prefer that.

@@ -21,11 +21,12 @@
except ImportError:
_HAS_CTYPE = False


@pytest.mark.skipif(not _HAS_CTYPE,
Copy link
Member

Choose a reason for hiding this comment

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

See above, ctypes should be available in the Python versions we support.

Copy link
Member Author

Choose a reason for hiding this comment

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

Discussed below

@@ -113,3 +111,59 @@ def test_cache(self):
a1 = ndpointer(dtype=np.float64)
a2 = ndpointer(dtype=np.float64)
assert_(a1 == a2)


@pytest.mark.skipif(not _HAS_CTYPE,

This comment was marked as resolved.

class TestAsArray(object):
def test_array(self):
from ctypes import c_int
at = c_int * 2
Copy link
Member

Choose a reason for hiding this comment

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

at could be more descriptive.

Copy link
Member Author

Choose a reason for hiding this comment

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

Now pair_t

reason="ctypes not available on this python installation")
class TestAsArray(object):
def test_array(self):
from ctypes import c_int
Copy link
Member

Choose a reason for hiding this comment

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

Blank line after imports is standard.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

assert_array_equal(a, np.array([[1, 2], [3, 4], [5, 6]]))

def test_pointer(self):
from ctypes import c_int, cast, POINTER
Copy link
Member

Choose a reason for hiding this comment

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

See above.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

assert_raises(TypeError, as_array, p)

def test_struct_array_pointer(self):
from ctypes import c_int16, Structure, pointer
Copy link
Member

Choose a reason for hiding this comment

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

See above.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@charris
Copy link
Member

charris commented May 20, 2018

The presence of ctypes since Python 2.5 can be assumed. The correctness of ctypes is something else :)

@eric-wieser
Copy link
Member Author

The presence of ctypes since Python 2.5 can be assumed.

I disagree - we've had bug reports about this in the past. It seems that python can be built without ctypes on some platforms?

@eric-wieser
Copy link
Member Author

See this comment for a summary of how to end up without ctypes

@charris
Copy link
Member

charris commented May 21, 2018

Hmm, it actually seems reasonable that ctypes may not be available on all platforms/environments. It certainly doesn't hurt to deal with that case.

@eric-wieser
Copy link
Member Author

Some nits addressed. I think this does not change the patch-version-worry introduced by #10882

Everything behaves a lot better if we let the array constructor handle it, which will use the ctypes PEP3118 support.

Bugs this fixes:

* Stale state being attached to pointer objects (fixes numpygh-2671, closes numpygh-6214)
* Weird failure modes on structured arrays (fixes-10978)
* A regression in numpygh-10882 (fixes numpygh-10968)
@eric-wieser eric-wieser force-pushed the cut-down-ctypeslib branch from f6a6009 to 8a8be50 Compare May 26, 2018 08:23
@charris charris merged commit bfaaf9d into numpy:master Jun 7, 2018
@charris
Copy link
Member

charris commented Jun 7, 2018

Thanks Eric.

@charris charris changed the title WIP: Remove fragile use of __array_interface__ in ctypeslib.as_array WIP: Remove fragile use of __array_interface__ in ctypeslib.as_array Jun 16, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants