Skip to content

BUG: Fix pickle and memoryview for datetime64, timedelta64 scalars - #11694

Merged
eric-wieser merged 2 commits into
numpy:masterfrom
mattip:master
Aug 19, 2018
Merged

eric-wieser merged 2 commits into
numpy:masterfrom
mattip:master

Conversation

@mattip

@mattip mattip commented Aug 9, 2018 •

Copy link
Copy Markdown
Member

Fixes #11656, which is a regression after #10564.

Adds tests for pickling datetime64 and timedelta64 scalars. Pickling arrays of these dtypes works but pickling scalars fails.

The problem seems to be in the new code for gentype_getbuffer, which creates a PEP 3118 compliant buffer. The old code was:

int gentype_getbuffer(PyObject *self, Py_buffer *view, int flags)
{
    Py_ssize_t len;
    void *buf;
    /* FIXME: XXX: the format is not implemented! -- this needs more work */
    len = gentype_getreadbuf(self, 0, &buf);
    return PyBuffer_FillInfo(view, self, buf, len, 1, flags);
}

so now I need to find what is happening in that code that is not happening in the new code.

So far I only found where we override an original exception with a new one. Now memoryview(np.datetime64('2016-01-01')) raises an exception with the same message as `memoryview(np.array(100, 'M8[D]').

Edit: python2 - python3 confusion

Comment thread numpy/core/src/multiarray/buffer.c Outdated
info = _buffer_get_info(self);
if (info == NULL) {
PyErr_SetString(PyExc_BufferError,
if (!PyErr_Occurred()) {

@eric-wieser eric-wieser Aug 9, 2018 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this if can ever be hit (at least, after gh-11695)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Try setting a breakpoint there and calling memoryview(np.datetime64('2016-01-01')) or more to the point of the issue pickle.dumps(np.datetime64('2016-01-01')). The call chain is gettype_getbuffer -> _buffer_get_info -> _buffer_info_new -> _buffer_format_string -> switch statement -> missing case marked with XXX -> raising error with "cannot include dtype '%c' in a buffer".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To be clear - I think _buffer_get_info is well behaved, and will always set an exception if it returns NULL.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ok

@eric-wieser

eric-wieser commented Aug 9, 2018 •

Copy link
Copy Markdown
Member

In case it helps, this was the old behavior

def as_dict(m):
	return dict(shape=m.shape, itemsize=m.itemsize, ndim=m.ndim, format=m.format)
>>> as_dict(memoryview(np.datetime64('2016-01-01')))
{'itemsize': 1, 'ndim': 1, 'shape': (8,), 'format': 'B'}

@mattip

mattip commented Aug 9, 2018

Copy link
Copy Markdown
Member Author

The memoryview info for datetime64 and timedelta64 uses B which is a single byte, so the code needs to special case the memoryview buffer generation. The dtype.itemsize == 8, dtype.shape == () becomes the memoryview mv.itemsize == 1, mv.shape == (8,0). If the dtype used `L' or 'l', the code would be cleaner.

@mattip mattip changed the title WIP: BUG: pickle and memoryview for datetime64, timedelta64 scalars BUG: pickle and memoryview for datetime64, timedelta64 scalars Aug 9, 2018
@mattip

mattip commented Aug 9, 2018

Copy link
Copy Markdown
Member Author

I cleared the WIP in the title, this seems to work now.

Comment thread numpy/core/tests/test_regression.py Outdated
dt1 = np.datetime64('2016-01-01')
dt2 = np.datetime64('2017-01-01')
expected = {'strides': (1,), 'itemsize': 1, 'ndim': 1,
'shape': (8,), 'format': 'B'}

@eric-wieser eric-wieser Aug 10, 2018 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One interesting idea would be to set the format to T{8B:value:T{}:__numpy_dtype_m8[N]:} so that we can actually round-trip this

Comment thread numpy/core/tests/test_regression.py Outdated
assert_equal(res, expected)
assert_equal(v.tolist(), lst)

lst = [110, 1, 0, 0, 0, 0, 0, 0]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this might be making assumptions about endianness

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

removed

Comment thread numpy/core/tests/test_regression.py Outdated
assert_(base <= sys.getrefcount(s))

@pytest.mark.skipif(sys.version_info < (3, 0), reason="Python3 only")
def test_datetime_memoryview(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this test might be better alongside wherever the other memoryview tests are

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

moved

Comment thread numpy/core/tests/test_scalarbuffer.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

NameError: lst

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

gaa

Comment thread numpy/core/src/multiarray/buffer.c Outdated
case NPY_UBYTE: if (_append_char(str, 'B')) return -1; break;
case NPY_UBYTE:
case NPY_DATETIME:
case NPY_TIMEDELTA: if (_append_char(str, 'B')) return -1; break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it be legal to set the format to 8B instead, and leave the shape as ()?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why not 'Q' then, since we call these datetime64 and timedelta64, the 64 bit is explicit. This would mean that d=np.ones((3), 'M8[D]'); np.array(memoryview(d)) would return a uint64 array, rather that a 3 by 8 'uint8' one.

@eric-wieser

Copy link
Copy Markdown
Member

Can you add a test for memoryview of datetime arrays?

@mattip
mattip force-pushed the master branch 2 times, most recently from c38cd2b to 63e44e3 Compare August 13, 2018 23:50
@mattip

mattip commented Aug 13, 2018

Copy link
Copy Markdown
Member Author

Can you add a test for memoryview of datetime arrays?

Added tests for both scalars and arrays. After changing the format string to 'Q' I could remove the special casing in memoryview(scalar) added in the earlier commits. Also now np.asarray(memoryview(d)) works where it used to raise an error (also improved the error message).

I tried to hit the "Sanity check" errors in tests. Should I go to C to create non-compliant buffers?

@mattip

mattip commented Aug 14, 2018

Copy link
Copy Markdown
Member Author

Would this be easier to review if I move the last commit 63e44e3 to a separate PR? Then we could get this into 1.15.1 without changing the buffer format from B. Note various things would fail, but they failed previously (all on python3) where

m=memoryview(np.array((3), 'M8[D]'))
d = memoryview(np.datetime64('2001-01-01'))
a = np.array(m)
b = np.array(d)
version m succeeds? d succeeds? a succeeds? b succeeds?
1.13.0 no yes (not relevant) yes
1.14.0 no yes (not relevant) yes
1.15.0 no no (not relevant) (not relevant)
895a2a8 (format=B) yes yes no yes
63e44e3 (format=Q) yes yes yes yes

@charris

charris commented Aug 15, 2018

Copy link
Copy Markdown
Member

Does the 'Q' format need a prefix, '=' perhaps, to indicate standard size (8) and avoid compiler dependence? I'm thinking of https://docs.python.org/2/library/struct.html.

@eric-wieser

eric-wieser commented Aug 15, 2018 •

Copy link
Copy Markdown
Member

Then we could get this into 1.15.1 without changing the buffer format from B

I agree - let's leave the buffer format as it was in 1.14.x for the 1.15.x series - we can change it in 1.16

What do you mean by "a doesn't succeed"?

"Item size computed from the PEP 3118 buffer format "
"string does not match the actual item size.");
"Item size %d for PEP 3118 buffer format "
"string %s does not match the dtype %c item size %d.",

@eric-wieser eric-wieser Aug 15, 2018 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Better to use the INTP format macro here (or perhaps %z) for itemsize, rather than casting.

Edit: %zd

@mattip

mattip commented Aug 15, 2018

Copy link
Copy Markdown
Member Author

What do you mean by "a doesn't succeed"?

I mean that executing the line a = np.array(m) raises an exception. Also, even though np.array(memoryview) may not raise, it will not create a datetime dtype, so I should have qualified the "yes" in a and b columns in that table.

@mattip

mattip commented Aug 15, 2018

Copy link
Copy Markdown
Member Author

I backed out the last commit to fix only the regression on pyhon 3 between 1.14 and 1.15

@eric-wieser

Copy link
Copy Markdown
Member

What is the exception that's thrown?

@mattip

mattip commented Aug 17, 2018

Copy link
Copy Markdown
Member Author

added test to gentype_getbuffer for PEP 3118 compliant buffer creation, test now raises instead of passing (as it should)

@mattip
mattip force-pushed the master branch 2 times, most recently from 69f279d to 27cd5a0 Compare August 17, 2018 10:08
@charris

charris commented Aug 17, 2018

Copy link
Copy Markdown
Member

@eric-wieser Look good?

@eric-wieser

Copy link
Copy Markdown
Member

I'm not really a fan of this approach - what we've ended up doing is:

  • Changing an internal function to return an illegal result
  • Added a bunch of special cases to the caller to try and fix it
  • Calling an expensive python-side parser to determine whether our special cases were sufficient

This does at least meet the requirement of not giving broken behavior to the user. I'm not sure if there's an easier way to achieve this.

Comment thread numpy/core/src/multiarray/buffer.c Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can't you just set the format directly here and return early, rather than modifying _buffer_format_string to do something incorrect?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good idea

Comment thread numpy/core/src/multiarray/buffer.c Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment should be explaining that the format is being exposed as B - scalars are always 1 is false - sizeof(B) == 1 is what's relevant here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

removing code, comment expanded above.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Whoops, this is needed. Comment fixed.

Comment thread numpy/core/src/multiarray/buffer.c Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I assume that in the tests this does occur? If so, perhaps say when it occurs in the comment.

Comment thread numpy/core/src/multiarray/buffer.c Outdated

@eric-wieser eric-wieser Aug 18, 2018 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think shape and strides are uninitialized here - they're malloc'd below for some cases.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed to be compliant with _buffer_info_t allocation strategy

@mattip
mattip force-pushed the master branch 2 times, most recently from ce53937 to eb1b209 Compare August 18, 2018 20:25
@mattip

mattip commented Aug 18, 2018

Copy link
Copy Markdown
Member Author

Special-cased memoryview(d) where d is a datetime64 scalar. This now fixes the original issue, adds tests for memoryview and pickle ing datetime64 objects, and never creates non-compliant buffers.

Comment thread numpy/core/src/multiarray/buffer.c Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

malloc can fail - this needs to call PyErr_NoMemory() if so

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

Comment thread numpy/core/src/multiarray/buffer.c Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd prefer this appear after the comment, alongside all the other assigments

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

@eric-wieser eric-wieser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good now, thanks for the persistence. Will merge and squash if you don't want to squash yourself.

Comment thread numpy/core/src/multiarray/buffer.c Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something for another PR: We ought to be checking the return value of _append_char, but we don't

@mattip

mattip commented Aug 19, 2018

Copy link
Copy Markdown
Member Author

sqaushed to two commits.

assert_raises((ValueError, BufferError), memoryview, a)
assert_raises((ValueError, BufferError), memoryview, np.array((3), 'M8[D]'))


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

super-nit: too many blank lines. Not worth fixing though - something to pick up if / when we apply a PEP8-checker

@eric-wieser
eric-wieser merged commit e8d177f into numpy:master Aug 19, 2018
@eric-wieser

Copy link
Copy Markdown
Member

Thanks @mattip!

@charris charris changed the title BUG: pickle and memoryview for datetime64, timedelta64 scalars BUG: Fix pickle and memoryview for datetime64, timedelta64 scalars Aug 19, 2018
@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Aug 19, 2018
@charris charris removed this from the 1.15.1 release milestone Aug 19, 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.

3 participants