Skip to content

bpo-33176 Add readonly mode to memoryviews #6314

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

Closed
wants to merge 2 commits into from

Conversation

pablogsal
Copy link
Member

@pablogsal pablogsal commented Mar 30, 2018

@pablogsal
Copy link
Member Author

pablogsal commented Mar 30, 2018

CC: @pitrou

This is an initial working implementation of the feature to discuss the design and other aspects.

@pablogsal pablogsal changed the title bpo-44276 Add readonly mode to memoryviews bpo-33176 Add readonly mode to memoryviews Mar 30, 2018
Copy link
Member

@pitrou pitrou left a comment

Choose a reason for hiding this comment

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

Thanks for doing this. I've posted a couple of comments below. You'll also need to update the memoryview.cast documentation in Doc/library/stdtypes.rst.

@@ -56,6 +56,7 @@ typedef struct {

typedef struct {
PyObject_VAR_HEAD
int readonly;
Copy link
Member

Choose a reason for hiding this comment

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

The Py_buffer structure already a readonly field, so you don't need to add this. See https://docs.python.org/3/c-api/buffer.html#buffer-structure

@@ -71,6 +71,21 @@ def setitem(value):
m = None
self.assertEqual(sys.getrefcount(b), oldrefcount)

def test_setitem_readonly_and_writable_buffer(self):
if not self.rw_type:
self.skipTest("no writable type to test")
Copy link
Member

Choose a reason for hiding this comment

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

Even if it's not writable, you should still be able to cast to readonly=True.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should I test both (writable and not writable) separately?

Copy link
Member Author

@pablogsal pablogsal Mar 31, 2018

Choose a reason for hiding this comment

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

I did that adding a explicit test for readonly=False and readonly buffer. (Raises when casting a readonly buffer to a writable one).

PyObject *format;
Py_ssize_t ndim = 1;

CHECK_RELEASED(self);

if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist,
&format, &shape)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist,
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 it makes sense for the readonly argument to be keyword-only. @skrah may have an opinion on this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, intuitively I think it should be keyword-only.

@@ -1365,6 +1367,13 @@ memory_cast(PyMemoryViewObject *self, PyObject *args, PyObject *kwds)
"memoryview: cannot cast view with zeros in shape or strides");
return NULL;
}

if (readonly && !PyBool_Check(readonly)){
Copy link
Member

Choose a reason for hiding this comment

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

Instead of this, just use the p type format in PyArgs_ParseTuple above (see https://docs.python.org/3/c-api/arg.html#other-objects).

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@skrah
Copy link
Contributor

skrah commented Mar 31, 2018

I agree with all of Antoine's comments. The flag should just set view.readonly, the only thing that should be disallowed is casting a readonly view to a writable one.

@pablogsal
Copy link
Member Author

I have made the requested changes; please review again

@bedevere-bot
Copy link

Thanks for making the requested changes!

@pitrou: please review the changes made to this pull request.

Copy link
Member

@pitrou pitrou left a comment

Choose a reason for hiding this comment

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

Thanks for the updates. I have some more minor comments (see below). However, I noticed a more fundamental limitation that I think would deserve fixing for this new feature to be really useful. I'll post separately about it.

@@ -3624,13 +3624,15 @@ copying.

.. versionadded:: 3.2

.. method:: cast(format[, shape])
.. method:: cast(format[, shape, readonly])
Copy link
Member

Choose a reason for hiding this comment

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

The signature here should be cast(format[, shape[, readonly]]) (note the additional square brackets around the "readonly" parameter, since it's optional).

Copy link
Member

Choose a reason for hiding this comment

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

What about something like

cast(format[, shape], *, readonly=<unchanged>)

That implies readonly is a keyword-only parameter, which matches your doc string and tests. Using the keyword notation also reduces the bracket nesting.

and C-contiguous -> 1D.
and C-contiguous -> 1D. If *readonly* is ``True`` the result view will
not be writable. Triying to cast a view with a readonly underlying buffer to
a writable view will raise a :class:`ValueError`.
Copy link
Member

Choose a reason for hiding this comment

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

Judging by the code, it's TypeError not ValueError.

Copy link
Member

Choose a reason for hiding this comment

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

Also you'll need to add a versionchanged entry below, for the added parameter.

@@ -71,6 +71,36 @@ def setitem(value):
m = None
self.assertEqual(sys.getrefcount(b), oldrefcount)

def test_setitem_readonly_and_writable_buffer(self):
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I would call this test test_cast_writable_to_readonly, or something.

b = self.rw_type(self._source)
oldrefcount = sys.getrefcount(b)
m = self._view(b)
readonly_m = m.cast("B", readonly=True)
Copy link
Member

Choose a reason for hiding this comment

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

I would test m.cast(m.format, readonly=True) instead, to make sure that only changing writability is supported.

Same for the other test below.

readonly_m = m.cast("B", readonly=True)
def setitem(value):
readonly_m[0] = value
self.assertRaises(TypeError, setitem, 12)
Copy link
Member

Choose a reason for hiding this comment

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

Also check that readonly_m.readonly is True?

Same for the other test below.

readonly_m = None
self.assertEqual(sys.getrefcount(b), oldrefcount)

def test_setitem_readonly_and_readonly_buffer(self):
Copy link
Member

Choose a reason for hiding this comment

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

Same as above: rename this test_cast_readonly_to_readonly?


def test_setitem_readonly_and_readonly_buffer(self):
if not self.ro_type:
self.skipTest("no writable type to test")
Copy link
Member

Choose a reason for hiding this comment

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

You mean "no readonly type to test".


if (self->view.readonly && !readonly){
PyErr_SetString(PyExc_TypeError,
"memoryview: cannot cast readonly buffer to a writable buffer.");
Copy link
Member

Choose a reason for hiding this comment

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

Nit: this line needs indenting.

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@pitrou
Copy link
Member

pitrou commented Apr 1, 2018

As I mentioned in my review above, there is a fundamental limitation that this PR is hitting, and it's that currently memoryview.cast() doesn't support same-shape casting for non-1D buffers. You would typically notice that if you have a view over a Numpy array.

To witness it without Numpy installed, CPython has a private type _testbuffer.ndarray that emulates a N-dimensional array. Below you can see the issue I'm talking about:

>>> a = _testbuffer.ndarray([1,2,3,4], format='B', shape=(2, 2))
>>> memoryview(a).tolist()
[[1, 2], [3, 4]]
>>> memoryview(a).cast('B').tolist()
[1, 2, 3, 4]
>>> memoryview(a).cast('B', shape=a.shape).tolist()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: memoryview: cast must be 1D -> ND or ND -> 1D

And the issue is similar if using non-byte formats and trying to cast to an identical format:

>>> a = _testbuffer.ndarray([1,2,3,4], format='i', shape=(2, 2))
>>> memoryview(a).cast('i').tolist()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: memoryview: cannot cast between two non-byte formats

For this PR to be truly useful, m.cast(m.format, m.shape, readonly=True) should be supported for any buffer kind. @skrah may have ideas on how to make it work.

@pablogsal
Copy link
Member Author

@pitrou Should we close this PR then?

@pitrou
Copy link
Member

pitrou commented Apr 1, 2018

I don't think so. I think that supporting the case where format and shape are unchanged should be reasonably doable (simply by comparing the values). If you feel you won't be motivated doing so, I (or Stefan) could take over.

@pablogsal
Copy link
Member Author

@pitrou On the contrary, I'm more than happy to continue! Just wanted to understand what is the status of this PR. Thanks!

@skrah
Copy link
Contributor

skrah commented Apr 1, 2018

Perhaps we can make format not required. Then, if readonly is the only parameter, allow an early return with just the readonly field changed.

I'm not sure about our policy of making a required parameter not-required.

@pitrou
Copy link
Member

pitrou commented Apr 1, 2018

I think it's ok to make format optional. There is still the issue that omitting shape converts the memoryview to 1-d, and that passing an unchanged shape for a n-d memoryview raises.

@skrah
Copy link
Contributor

skrah commented Apr 1, 2018

I think we can't change the behavior that to 1D is assumed when shape is missing.

Initially the point of cast() was to cast to bytes, then the reverse operation (casting back to the previous shape) was demanded. :-)

Allowing the same shape would be one way out, but I'm not complete happy with writing m.cast(shape=m.shape, readonly=True).

Perhaps m.cast(keep_shape=True, readonly=True) ?

keep_shape and shape would be mutually exclusive.

@pitrou
Copy link
Member

pitrou commented Apr 1, 2018

While shape=m.shape isn't very satisfying, keep_shape=True doesn't look better to me, so I would vote for just keeping shape.

@@ -3624,13 +3624,15 @@ copying.

.. versionadded:: 3.2

.. method:: cast(format[, shape])
.. method:: cast(format[, shape, readonly])
Copy link
Member

Choose a reason for hiding this comment

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

What about something like

cast(format[, shape], *, readonly=<unchanged>)

That implies readonly is a keyword-only parameter, which matches your doc string and tests. Using the keyword notation also reduces the bracket nesting.


Cast a memoryview to a new format or shape. *shape* defaults to
``[byte_length//new_itemsize]``, which means that the result view
will be one-dimensional. The return value is a new memoryview, but
the buffer itself is not copied. Supported casts are 1D -> C-:term:`contiguous`
and C-contiguous -> 1D.
and C-contiguous -> 1D. If *readonly* is ``True`` the result view will
not be writable. Triying to cast a view with a readonly underlying buffer to
Copy link
Member

Choose a reason for hiding this comment

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

Spelling: Trying

and C-contiguous -> 1D.
and C-contiguous -> 1D. If *readonly* is ``True`` the result view will
not be writable. Triying to cast a view with a readonly underlying buffer to
a writable view will raise a :class:`ValueError`.
Copy link
Member

Choose a reason for hiding this comment

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

What does readonly=False mean, and what is the default behaviour? How do I avoid casting a readonly view to writable? Will I have to add readonly=True to all my casts?

@skrah
Copy link
Contributor

skrah commented Apr 2, 2018

Okay, let's use shape=m.shape then.

Regarding Martin's points: Probably readonly should default to None, meaning unchanged.

While we're at it, shape should also default to None, which (roughly and imperfectly) corresponds to shape==NULL on the C level. At least there's the vague connotation of "casting to bytes" if shape isn't given.

@pitrou
Copy link
Member

pitrou commented Apr 13, 2018

It strikes me that perhaps we're asking too much to the cast() method. Perhaps a separate to_readonly() method would be better? (from an implementation POV, at least, it would be much simpler :-))

@skrah
Copy link
Contributor

skrah commented Apr 13, 2018 via email

@pitrou
Copy link
Member

pitrou commented Apr 13, 2018

See PR #6466 for the toreadonly() method.

@pablogsal
Copy link
Member Author

pablogsal commented Apr 14, 2018

@pitrou @skrah I agree that toreadonly() is more easy to implement and use. Should we close this one as superseded by #6466?

@pitrou
Copy link
Member

pitrou commented Apr 14, 2018

@pablogsal, yes, I think so. Sorry for leading you into a dead end :-)

@pablogsal
Copy link
Member Author

@pitrou No problem at all :)

@pablogsal pablogsal closed this Apr 14, 2018
@pablogsal pablogsal deleted the bpo33176 branch April 14, 2018 17:24
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.

6 participants