-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
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
Conversation
CC: @pitrou This is an initial working implementation of the feature to discuss the design and other aspects. |
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.
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
.
Include/memoryobject.h
Outdated
@@ -56,6 +56,7 @@ typedef struct { | |||
|
|||
typedef struct { | |||
PyObject_VAR_HEAD | |||
int readonly; |
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.
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") |
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.
Even if it's not writable, you should still be able to cast to readonly=True
.
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.
Should I test both (writable and not writable) separately?
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.
I did that adding a explicit test for readonly=False
and readonly buffer. (Raises when casting a readonly buffer to a writable one).
Objects/memoryobject.c
Outdated
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, |
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.
I wonder if it makes sense for the readonly
argument to be keyword-only. @skrah may have an opinion on this.
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.
Yes, intuitively I think it should be keyword-only.
Objects/memoryobject.c
Outdated
@@ -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)){ |
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.
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).
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 agree with all of Antoine's comments. The flag should just set |
I have made the requested changes; please review again |
Thanks for making the requested changes! @pitrou: please review the changes made to this pull request. |
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.
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]) |
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.
The signature here should be cast(format[, shape[, readonly]])
(note the additional square brackets around the "readonly" parameter, since it's optional).
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.
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`. |
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.
Judging by the code, it's TypeError not ValueError.
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.
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): |
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.
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) |
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.
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) |
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.
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): |
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.
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") |
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.
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."); |
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.
Nit: this line needs indenting.
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 |
As I mentioned in my review above, there is a fundamental limitation that this PR is hitting, and it's that currently To witness it without Numpy installed, CPython has a private type
And the issue is similar if using non-byte formats and trying to cast to an identical format:
For this PR to be truly useful, |
@pitrou Should we close this PR then? |
I don't think so. I think that supporting the case where |
@pitrou On the contrary, I'm more than happy to continue! Just wanted to understand what is the status of this PR. Thanks! |
Perhaps we can make I'm not sure about our policy of making a required parameter not-required. |
I think it's ok to make |
I think we can't change the behavior that Initially the point of Allowing the same shape would be one way out, but I'm not complete happy with writing Perhaps
|
While |
@@ -3624,13 +3624,15 @@ copying. | |||
|
|||
.. versionadded:: 3.2 | |||
|
|||
.. method:: cast(format[, shape]) | |||
.. method:: cast(format[, shape, readonly]) |
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.
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 |
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.
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`. |
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.
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?
Okay, let's use Regarding Martin's points: Probably While we're at it, |
It strikes me that perhaps we're asking too much to the |
I like that, probably converting to readonly will be used separately from casting most of the time anyway.
|
See PR #6466 for the |
@pablogsal, yes, I think so. Sorry for leading you into a dead end :-) |
@pitrou No problem at all :) |
https://bugs.python.org/issue33176