Skip to content
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

ENH: Nditer as context manager #9998

Merged
merged 3 commits into from Apr 21, 2018
Merged

Conversation

mattip
Copy link
Member

@mattip mattip commented Nov 10, 2017

Resolves issue #9714 by enabling use of nditer as a context manager. Code like this:

a = np.arange(24, dtype='f8').reshape(2, 3, 4).T
it = np.nditer(a, [], [['readwrite', 'updateifcopy']],
            casting='same_kind', op_dtypes=[np.dtype('f4')])
# Check that UPDATEIFCOPY is activated
it.operands[0][2, 1, 1] = -12.5
assert  a[2, 1, 1] != -12.5
it = None                     # magic!!!
assert a[2, 1, 1] == -12.5

now becomes

a = np.arange(24, dtype='f8').reshape(2, 3, 4).T
with np.nditer(a, [], [['readwrite', 'updateifcopy']],
            casting='same_kind', op_dtypes=[np.dtype('f4')]) as it:
    # Check that UPDATEIFCOPY is activated
    it.operands[0][2, 1, 1] = -12.5
    assert  a[2, 1, 1] != -12.5
assert a[2, 1, 1] == -12.5

No more need to do the it = None assignment to write the data back to the original array. Tests were adjusted. In addition, DeprecationWarning will be raised if a nditer should be using this code pattern and is not, or if the nditer it is used outside the context manager.

This also allows completing the confusing part of pull request #9639, and now any call to PyArray_SetUpdateIfCopyBase will emit a DeprecationWarning.

I am not really happy with the way this affects nested_iters, since it returns a tuple there is no convienient way to handle the context manager there, that is why the test tweak is in a seperate commit.

@ahaldane
Copy link
Member

ahaldane commented Nov 10, 2017

Is it possible to create a simple custom context manager for nested_iters?
Something like:

class nested_iters(object):
    def __init__(self, args):
        self.mgr1 = nditer(args, 1)
        self.mgr2 = nditer(args, 2)

    def __enter__(self):
        return (self.mgr1.__enter__(), self.mgr2.__enter__())

    def __exit__(self, type, value, tb):
        self.mgr1.__exit__()
        self.mgr2.__exit__()

It may also be easier do this in python instead of trying to modify NpyIter_NestedIters in C - I'm not sure any C-level control is needed, though I'm not too familiar with the code. Probably it could be stuck into numpy/core/numeric.py. It doesn't look like NpyIter_NestedIters is part of the C-api, so we wouldn't even need a C interface. (If it is, we can put the python definition in numpy/core/_internals.py and call it from C with npy_cache_import).

(incidentally, nested_iters is not documented in the docs, we should fix that some day)

@ghost
Copy link

ghost commented Nov 11, 2017

IHMO just reimplement nested for python 3 and return that:

try:
    from contextlib import nested  # Python 2
except ImportError:
    from contextlib import ExitStack, contextmanager

    @contextmanager
    def nested(*contexts):
        """
        Reimplementation of nested in python 3.
        """
        with ExitStack() as stack:
            for ctx in contexts:
                stack.enter_context(ctx)
            yield contexts

@ghost
Copy link

ghost commented Nov 11, 2017

Rare case where Python 2 has a better API.

@@ -2143,6 +2144,7 @@ array_assign_subscript(PyArrayObject *self, PyObject *ind, PyObject *op)
return -1;

success:
PyArray_ResolveWritebackIfCopy(view);
Copy link
Member

Choose a reason for hiding this comment

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

Is this related to np.nditer? (edit: I see, separate commit)

Copy link
Member

Choose a reason for hiding this comment

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

Sorry if this is a naive question, I'm just coming back to this:

Why is this line needed? Doesn't the context manager exit take care of any needed writebacks? Furthermore, let's say that within the context manager the user creates a view of the array they got from nditer, and writes to the view (Thus calling Writeback) and then the context manager exits, doesn't that double-call the writeback code?

This PR segfaults on the following for me:

a = np.arange(24, dtype='f8').reshape(2, 3, 4).T
with np.nditer(a, [],[['readwrite', 'updateifcopy']],casting='same_kind',op_dtypes=[np.dtype('f4')]) as i:
    i.operands[0][...] = 3
    i.operands[0][...] = 14

which I suspect is related to my comment, but I haven't checked carefully.

Copy link
Member Author

Choose a reason for hiding this comment

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

good catch. Added your test and fixed in c6a1a2c6d09. The context manager does indeed clean up, but this function can possibly allocate a new ndarray with writebackifcopy semantics that need to be cleaned up. The fix was to call the resolve function iff a new view is created

/* iter must used as a context manager if writebackifcopy semantics used */
#define ITR_INSIDE 1<<0
#define ITR_HAS_WRITEBACKIFCOPY 1<<1
#define ITR_EXITED 1<<2
Copy link
Member

Choose a reason for hiding this comment

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

Why not an enum?

Copy link
Member

Choose a reason for hiding this comment

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

Or even just a char for each flag - we don't care about efficiency here.

Perhaps

char has_writebackifcopy;
enum {
   CONTEXT_BEFORE,
   CONTEXT_INSIDE,
   CONTEXT_AFTER
} context_state;

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 in 9a45b3d055

@@ -2331,6 +2381,39 @@ npyiter_ass_subscript(NewNpyArrayIterObject *self, PyObject *op,
return -1;
}

static PyObject *
npyiter_self(NewNpyArrayIterObject *self)
Copy link
Member

Choose a reason for hiding this comment

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

Would be better as npyiter_enter since this also sets flags.

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 in 9a45b3d055

PyErr_SetString(PyExc_ValueError, "operation on non-initialized iterator");
return NULL;
}
self->managed &= ITR_INSIDE;
Copy link
Member

Choose a reason for hiding this comment

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

Should this be |=?

Copy link
Member Author

Choose a reason for hiding this comment

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

refactored in 9a45b3d055

if (PyArray_ResolveWritebackIfCopy(*object) < 0)
return NULL;
}
self->managed = ITR_EXITED;
Copy link
Member

Choose a reason for hiding this comment

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

Why does this clear ITR_HAS_WRITEBACKIFCOPY?

Copy link

Choose a reason for hiding this comment

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

IIUC, PyArray_ResolveWritebackIfCopy is called, so there's no need to worry about writing back.

Copy link
Member

Choose a reason for hiding this comment

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

So what does the following do ?

it = np.nditer(...)
with it:
    next(it)
with it:
   # do stuff

It seems to me that we'll forget that we were supposed to be in a context manager the second time around

Copy link

Choose a reason for hiding this comment

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

That's currently undefined behavior.

Copy link

Choose a reason for hiding this comment

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

Obviously that should be fixed by moving the constructor logic into __enter__.

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO calling __enter__ the second time should raise an exception since writebackifcopy resolution would have already been triggered. Is it common to try to re-enter a context manager? In my limited experience I have not seen code like that

Copy link
Member

Choose a reason for hiding this comment

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

Raising an exception sounds reasonable to me - as long as it doesn't fail silently

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 raises an error, tested in 9a45b3d055

object = NpyIter_GetOperandArray(iter);
for(iop = 0; iop < nop; ++iop, ++object) {
if (PyArray_ResolveWritebackIfCopy(*object) < 0)
return NULL;
Copy link
Member

Choose a reason for hiding this comment

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

Braces and/or indentation needed 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.

fixed in 9a45b3d055

}

static PyObject *
npyiter_exit(PyObject *f, PyObject *args)
Copy link
Member

Choose a reason for hiding this comment

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

Why not declare f as NewNpyArrayIterObject like you do in npy_iter_self?

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 in 9a45b3d055

PyErr_SetString(PyExc_ValueError,
"Iterator has exited its context");
return NULL;
}
Copy link
Member

Choose a reason for hiding this comment

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

Why is this desirable? How do operands interact with WRITEBACK_IF_COPY? Do they contain the temporary, or the target of the writeback?

Copy link

Choose a reason for hiding this comment

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

IIUC, you must call PyArray_ResolveWritebackIfCopy only once. I think operands contain the temporary copy, they operate on it, and then when the context has exited, the results are written back. Obviously they cannot be written back if the context has already exited.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not only that, but once they have been written back, the temporary scratch buffer is removed. Now the whole reason for the writeback semantics have been undone, for instance the operand dtype conflicts with the underlying base array's dtype. Reading from the operand will most likely retrieve the wrong value, or writing to it will corrupt the data.

Copy link

Choose a reason for hiding this comment

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

Would be ideal to recreate the scratch buffer in every __enter__. But fixing the deprecation is a higher priority IMHO.

Copy link
Member

Choose a reason for hiding this comment

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

So to be clear, operands contains the temporary scratch buffer, not the original array?

Copy link

Choose a reason for hiding this comment

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

I think operands contain the temporary copy

Copy link
Member Author

Choose a reason for hiding this comment

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

operands are ndarrays that have the original array as a base attribute. Since they have the WRITEBACKIFCOPY flag, they hold another scratch buffer until PyArray_ResolveWritebackIfCopy is called

assert_(not it.iterationneedsapi)
assert_(sys.getrefcount(a) > rc_a)
assert_(sys.getrefcount(dt) > rc_dt)
# 'it' is still alive in this scope, dealloc
Copy link
Member

Choose a reason for hiding this comment

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

del it might be clearer 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.

fixed in 9a45b3d055

@@ -2284,7 +2284,8 @@ def test_iter_nested_iters_dtype_copy():
assert_equal(vals, [[0, 1, 2], [3, 4, 5]])
vals = None

# updateifcopy
# writebackifcopy
# XXX ugly - is there a better way? np.nested_iter returns a tuple
Copy link
Member

Choose a reason for hiding this comment

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

Why not with i, j?

Copy link

Choose a reason for hiding this comment

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

That's one possibility. I think people were looking for a one-line solution and Python 3 may require a two-line solution.

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a good enough understanding of nested_iters to know what makes sense here. Is it documented anywhere? Do the nditers it returns depend on each other? If so, isn't it problematic to construct the inner iter before calling __enter__ on the outer one? (which is exactly the reason that python 3 removed contextlib.nested)

Copy link

Choose a reason for hiding this comment

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

It is part of the public API, but it isn't documented.

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 nice if it was at least internally documented. Right now, I have very little idea of what it does.

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 would prefer to resolve the nested_iter issue in a separate pull request. I have filed issue #10009 a a reminder to do so. AFAICT the c-api function NpyIter_NestedIters is not part of the public API, so we could fix this on the python level only

Copy link
Member Author

Choose a reason for hiding this comment

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

heh, in the end it is enough to do what @eric-wieser suggested in the first comment, simple and clear. I left the issue open since the function is not documented

``dealloc`` call, which let to strange ``i = None`` code after iteration to
magically write the temporary data back to the original source. Now nditers
can be used as context managers, and when writebackoncopy semantics are
triggered they *must* be used as context managers.
Copy link
Member

@njsmith njsmith Nov 12, 2017

Choose a reason for hiding this comment

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

Unfortunately, AFAICT there are circumstances where nditer can use UPDATEIFCOPY even if "updateifcopy" isn't passed, like if you have a writable array that overlaps with another array.

Suggested text:

Under certain conditions, nditer must be used in a context manager
-----------------------------------------------
When using an nditer with the `"writeonly"` or `"readwrite"` flags, there are some
circumstances where nditer doesn't actually give you a view onto the writable
array. Instead, it gives you a copy, and if you make changes to the copy, nditer later
writes those changes back into your actual array. Currently, this writeback depends
on when the array objects are garbage collected, which makes this API error-prone
on CPython and entirely broken on PyPy. Therefore, `nditer` can now be used
as a context manager (`with np.nditer(...) as it: ...`), and in the future this will
be required whenever using `nditer` with writable arrays.

Copy link
Member

Choose a reason for hiding this comment

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

Note also that I'm suggesting here that we actually make the criterion for issuing deprecation warnings (now) and raising an error (later) be no context manager + writable arrays, since that's so much easier to explain to users than the actual edge cases. (And in particular if you have writable arrays then it's easy to get into a situation where in testing you never hit the UPDATEIFCOPY path, but then in production...)

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 tried to couch the wording in a way that did not limit it to the single flag. Requiring any writable nditer to be used inside a context manager pretty much means any nditer must be used that way. Is a change that deep acceptable/desired?

Copy link
Member

Choose a reason for hiding this comment

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

The problem is that anyone who uses writable nditer might need this depending on details of the input arrays memory layout (in particular whether they overlap). I think forcing everyone to change at once with a nice warning message is better than forcing people to change piecemeal as they accidentally hit the edge case one by one.

Fortunately almost no-one uses nditer :-)

Copy link
Member

Choose a reason for hiding this comment

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

I went through all the hits for nditer on searchcode.com, and only found ~15 projects, of which only ~5 use writeonly/readwrite. I'm sure there are others not in their corpus, but we're still only talking about probably ~dozens of pieces of code worldwide that need updating.

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 in 7eb8aed72


``DeprecationWarning`` warnings will be issued on use of
``UPDATEIFCOPY`` or if writeback resolution has not
occurred before calling ``array_dealloc``.
Copy link
Member

Choose a reason for hiding this comment

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

If array_dealloc sees WRITEBACKIFCOPY then that's just a bug, right? Shouldn't we issue a RuntimeWarning or something? (Really it ought to raise an exception, but since you can't do that from array_dealloc a noisy warning is a reasonable alternative.)

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 in 7e5c2a7e6, warning message adjusted to indicate the root cause could be nditer

# define MSG_FromString PyString_FromString
#else
# define MSG_FromString PyUnicode_FromString
#endif
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this just our PyUString_FromString macro?

@@ -2134,6 +2134,7 @@ array_assign_subscript(PyArrayObject *self, PyObject *ind, PyObject *op)

/* Clean up temporary variables and indices */
fail:
PyArray_DiscardWritebackIfCopy(view);
Copy link
Member

Choose a reason for hiding this comment

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

I'm assuming view can be NULL here from the XDECREF below. Is this save if view is NULL?

Copy link
Member Author

Choose a reason for hiding this comment

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

PyArray_DiscardWritebackIfCopy(arr) does check if arr is NULL

for (iop = 0; iop < nop; ++iop) {
if (op_flags[iop] & NPY_ITER_USE_CONTEXT_MANAGER)
self->has_writebackifcopy = 1;
op_flags[iop] &= ~NPY_ITER_USE_CONTEXT_MANAGER;
Copy link
Member

Choose a reason for hiding this comment

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

Why do you clear this?

@@ -1065,6 +1088,12 @@ NpyIter_NestedIters(PyObject *NPY_UNUSED(self),
Py_DECREF(ret);
goto fail;
}
if ((iter->has_writebackifcopy != 0) &&
(iter->managed != CONTEXT_INSIDE)) {
Copy link
Member

Choose a reason for hiding this comment

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

This second condition is always true, right, because there's no way to be inside the context already 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.

gaah, this is a bug. I need to check the operands and set iter->has_writebackifcopy appropriately


if ((self->has_writebackifcopy != 0) &&
(self->managed != CONTEXT_INSIDE)) {
/* 10-Nov-2017 v1.14 */
Copy link
Member

Choose a reason for hiding this comment

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

Nit: elsewhere we write this 2017-11-10 or 2017-Nov-10

nop = NpyIter_GetNOp(iter);
object = NpyIter_GetOperandArray(iter);
for(iop = 0; iop < nop; ++iop, ++object) {
if (PyArray_ResolveWritebackIfCopy(*object) < 0) {
Copy link
Member

Choose a reason for hiding this comment

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

Should this call Resolve even if an error occurs? (args doesn't contain Py_Nones)

Or would it be better to Discard?

Copy link
Member Author

Choose a reason for hiding this comment

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

correct, I did not properly handle the error condition. TODO

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 in 0c3340936f3

@eric-wieser
Copy link
Member

eric-wieser commented Nov 13, 2017

One overall comment - it's very rare to have something in python that is required to be a context manager - for instance, the file objects provide a close method for when you don't use with.

Perhaps we should add commit_writeback and discard_writeback methods, and leave with as just syntactic sugar? I think that would be better because:

  • It's easier to adapt old code for the rare cases when with is messy (ie, those that require ExitStack
  • We don't have a real __enter__ method anyway
  • It makes it closer to the C code that np.nditer is supposed to be a learning tool to help use

@mattip
Copy link
Member Author

mattip commented Nov 13, 2017

I have no strong opinion on commit_writeback and discard_writeback, but then we would need another function to indicate "I promise to call them later", like allow_nditer_outside_context_manager which is a really bad name. This function would set the needs_context_manager flag to 0.

@ahaldane
Copy link
Member

All right. I'm going to do one last pass through the code soon, and then I am aiming to merge sometime in the next few days.

if (op_flags[iop] & NPY_ITER_READWRITE) {
self->needs_context_manager = 1;
}

Copy link
Member

Choose a reason for hiding this comment

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

extra blank line here

Copy link
Member

Choose a reason for hiding this comment

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

also, all lines related to needs_context_manager can be removed now.

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

@ahaldane
Copy link
Member

All right, I did my pass. Just a few nits, and removal of needs_context_manager.

After you fix those last few, I will merge.

@mattip
Copy link
Member Author

mattip commented Apr 21, 2018

Thanks, this fixes #9714 and is the last stage to resolve #7054

@ahaldane
Copy link
Member

All right! Here goes....

Thank you so much for bearing with all the reviews, I think it is quite a nice PR now.

@ahaldane ahaldane merged commit e0b5e87 into numpy:master Apr 21, 2018
@@ -5,7 +5,8 @@

Whenever you change one index, you break the ABI (and the ABI version number
should be incremented). Whenever you add an item to one of the dict, the API
needs to be updated.
needs to be updated in both setup_common.py and by adding an appropriate
entry to cversion.txt (generate the hash via "python cversions.py".
Copy link
Member

Choose a reason for hiding this comment

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

nit: missing paren

Copy link
Member Author

Choose a reason for hiding this comment

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

What is "best practice" when fixing a merged PR, a new PR or modifying this one again?

Copy link
Member

Choose a reason for hiding this comment

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

You can't modify an existing PR once it's merged.

What I normally do is keep working in the same branch, but open a new PR

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 (in doc-nditer PR)

"""
close()

Resolve all writeback semantics in writeable operands.
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps reference the with statement 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.

fixed (in doc-nditer PR)

@@ -93,6 +117,8 @@ using the old API.
C API changes
=============

``NpyIter_Close`` has been added and should be called before
``NpyIter_Dealloc`` to resolve possible writeback-enabled arrays.
Copy link
Member

Choose a reason for hiding this comment

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

Should this be NpyIter_Deallocate?

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 (in doc-nditer PR)

assert_equal(au, [2]*6)

i = None # should not raise a DeprecationWarning
Copy link
Member

Choose a reason for hiding this comment

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

nit: PEP8 wants two spaces before the #. Also, del i might be clearer.

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 (in doc-nditer PR)

#ifdef DEPRECATE_UPDATEIFCOPY
/* TODO: enable this once a solution for UPDATEIFCOPY
* and nditer are resolved, also pending the fix for GH7054
*/
/* 2017-Nov-10 1.14 */
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 needs an additional comment for the non-pypy deprecation

Copy link
Member Author

Choose a reason for hiding this comment

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

What would you suggest? Leaving the "GH7054" issue reference as a hint?

Copy link
Member

Choose a reason for hiding this comment

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

/* 2017-11-10 1.14 (only in pypy) */
/* 2018-04-21 1.15 (all python implementations) */

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

struct NewNpyArrayIterObject_tag {
PyObject_HEAD
/* The iterator */
NpyIter *iter;
/* Flag indicating iteration started/stopped */
char started, finished;
/* iter must used as a context manager if writebackifcopy semantics used */
char managed;
Copy link
Member

Choose a reason for hiding this comment

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

The comment above this line is no longer correct, right?

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

PyErr_SetString(PyExc_ValueError, "cannot reuse iterator after exit");
return NULL;
}
self->managed = CONTEXT_INSIDE;
Copy link
Member

Choose a reason for hiding this comment

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

Should probably also check that you're not re-entering the iterator

Copy link
Member

Choose a reason for hiding this comment

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

Either that, or allow re-entrant withs, but only cleanup on the outermost one.

Copy link
Member Author

Choose a reason for hiding this comment

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

Reentrant is OK (test added), multiple cleanups are harmless. Once the cleanup is finished, we set self->managed = CONTEXT_EXITED so reentering a closed nditer fails (there is a test for that in test_nditer.py, line 2847

Copy link
Member

Choose a reason for hiding this comment

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

I guess the question is, is this the behavior we expect?

with it:
    with it:
        do_stuff_with_it()  # ok
    do_stuff_with(it)  # error: already closed

It does seem to match how contextlib.closing works, so I suppose is consistent.

* Returns 0 on success, -1 on failure
*/
NPY_NO_EXPORT int
NpyIter_Close(NpyIter *iter)
Copy link
Member

Choose a reason for hiding this comment

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

Is it safe to call this multiple times?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes

Copy link
Member Author

Choose a reason for hiding this comment

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

the first call will resolve all writebacks, so if there was a writeback scratch buffer now the iterator operand's base attribute will be None and the operand's data will be independent from the original writeable array data. Subsequent calls will no do anything

@@ -1473,6 +1489,12 @@ static PyObject *npyiter_itviews_get(NewNpyArrayIterObject *self)
return NULL;
}

if (self->managed == CONTEXT_EXITED) {
PyErr_SetString(PyExc_ValueError,
"Iterator is closed");
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this happen after .close() is called, not just after __exit__ is called?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, see new PR for issue #10950

mattip added a commit to mattip/numpy that referenced this pull request Apr 23, 2018
@mattip mattip deleted the nditer-as-context-manager branch April 24, 2018 09:17
ahaldane added a commit that referenced this pull request Apr 27, 2018
DOC: cleanup documentation, continuation of nditer PR #9998
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.

None yet

6 participants