MAINT: Add error return to all casting functionality and NpyIter#17029
MAINT: Add error return to all casting functionality and NpyIter#17029mattip merged 4 commits intonumpy:masterfrom
Conversation
01f8b51 to
8a4b362
Compare
af7fc01 to
2b677e7
Compare
There was a problem hiding this comment.
I understand we can do without NpyIter_SafeDealloc. Do we need to export NpyIter_ErrorOccurred or can it remain an internal API?
There was a problem hiding this comment.
I am not sure what to do best. We can definitely do without the first, yes. For the second one we need to figure out what to do with the error which is set on the fly (possibly when the API is not held).
The current solution is to stash the error and raise it later when we (probably) have the API and can return error values, because iternext does not report error returns. I think we will need to stash the error quite often to cleanup correctly, but that does not have to be done by NpyIter itself in principle. It may be possible to just flag an error return, I think this was my first reaction, because it can't be disrupted e.g. by a random PyErr_Clear() call.
The issue is that the iternext functions use 0 to indicate stop-iteration so we can't distinguish an aborted iteration (due to an error) from a finished iteration. That is why I needed to add NpyIter_ErrorOccurred(), there needs to be a way to know that the iteration was aborted when dealloc is not called immediately. That is mostly important for np.nditer (the python API).
As to the NpyIter_SafeDealloc, no I am not sure its the best design. If it seems nicer than the alternative, we could use it privately I suppose. NpyIter_Deallocate may need the Python API, so as I wrote it here, it has to either check PyErr_Occurred() or the caller has to stash the error.
Right now of course, we ignore that this type of thing can happen. In practice NpyIter_Dealloc is simply called with an error already set, and we basically hope it will all work out and the correct error is raised (possibly much belated).
There was a problem hiding this comment.
Hmmm, there is probably a remaining hole here. When the iteration is never finished, either due to an error or because np.nditer is just not iterated to the end, we need to clean up buffers in that case as well. Technically, we should copy the buffer back if (but I guess only if) there was no error. Also an existing issue.
I guess right now we simply, assume that NpyIter_Deallocate can handle such things (its called by np.nditer.__exit__). Probably the Reset functions are in a similar spot.
|
CI failure due to S390x, not related to this PR. |
262cb92 to
82fbf87
Compare
|
OK, I did not get to new tests today, but this now adds only a single function This fixes the following for example: this is a no-error example, examples with error should behave identically. Alternatively shows the same issue with structured: All of these examples leak the numbers from 1 to 8192 (or so), since that is the size of the first buffer chunk. |
|
Not adding the new dealloc function basically means that we call |
|
sorry for the back and forth. I don't particularly like that the NpyIter user (and we) have to call `PyErr_Occurred(), but here is the solution relying on no new API, but rather on that. In particular, I think this means that there is no way to even find out if a casting error occurred without Now, this is reasonable, as long as we are happy with leaving the error set (users can stash it), and the fact that users must use I guess practically, it changes almost nothing except that iteration may get aborted, when before that was not possible. |
|
OK, a bit of proactive thinking would help me here. We don't have to figure out the full picture yet, but we do have two issues:
So who is supposed to check for errors? Especially for floating point errors? Either the iterator does it, or the iterator may need to be able to signal that FPEs should be checked, does that require more API than the existing Ufunc API? And do we live with the fact that there may be no way to know if an error occurred until calling The full picture:
|
NpyIter cannot properly indicate errors right away, defering them in some case until the actual cleanup occurs. to make that slightly with easier, this adds a new API function with (non-standard for NpyIter) error return as -1. A second function is one which only checks whether an error occurred (i.e. `NpyIter_ErrOccurred()`) which is necessary in the (more rare) case the iterator is not deallocated promptly after the iteration is finished.
6a2cb7f to
101192c
Compare
|
OK, marking ready for review. Its messed up, but there is nothing to do about it. I think the tests should cover things fairly well. I would be happy to think about what new API can help to fix the issue, and we may very much have to soon enough, but there is no obvious choice, so I can't just make a call on my own. And if we are unwilling to add new API, this is probably about as good as it can get. The NpyIter changes should probably be looked at carefully, to ensure I did not accidentally modify things seriously. Generally, it seems the whole API might be a bit dubious about partial iterations, I guess its plausible nobody ever used that... |
|
I don't think the codecov failure has too much relevance. Quite a few things simply can't be changed, or are obvious error-path corrections. |
mattip
left a comment
There was a problem hiding this comment.
LGTM. I would like to put this in soon to enable further refactoring.
|
Thanks @seberg |
Comparison result is always the same
Comparison is always false because res <= 0.
Indeed the for(;;) iteration may stop on 3 different breaks:
res = -1;
break;
if (!res) {
break;
if (!res) {
break;
Therefore, after the loop, the only possible values of res are -1 and 0,
and (res > 0) is always false.
This originates in 37dc69c (numpy#17029).
Comparison result is always the same
Comparison is always false because res <= 0.
Indeed the for(;;) iteration may stop on 3 different breaks:
res = -1;
break;
if (!res) {
break;
if (!res) {
break;
Therefore, after the loop, the only possible values for res are -1 and 0,
and (res > 0) is always false.
This originates in 37dc69c (numpy#17029).
Comparison result is always the same
Comparison is always false because res <= 0.
Indeed the for(;;) iteration may stop on 3 different breaks:
res = -1;
break;
if (!res) {
break;
if (!res) {
break;
Therefore, after the loop, the only possible values for res are -1 and 0,
and (res > 0) is always false.
This originates in 37dc69c (numpy#17029).
Comparison result is always the same
Comparison is always false because res <= 0.
Indeed the for(;;) iteration may stop on 3 different breaks:
res = -1;
break;
if (!res) {
break;
if (!res) {
break;
Therefore, after the loop, the only possible values for res are -1 and 0,
and (res > 0) is always false.
This originates in 37dc69c (numpy#17029).
NpyIter cannot properly indicate errors right away, defering them
in some case until the actual cleanup occurs. to make that slightly
with easier, this adds a new API function with (non-standard for
NpyIter) error return as -1.
A second function is one which only checks whether an error
occurred (i.e.
NpyIter_ErrOccurred()) which is necessary in the(more rare) case the iterator is not deallocated promptly after
the iteration is finished.
Well, this was mostly surprisingly straight forward, I am sure there are some stranger fallout in error-case corner cases, but those were always somewhat buggy, so more likely a few got fixed.
What we will have to discuss is the
NpyIterAPI. Since the iteration functions cannot indicate an error, the first place where we can set it for sure is the deallocation (although many places forget to check for error returns). None of that actually makes the situation worse, but it made me feel like adding a slight modification of theNpyIter_Deallocate, as well as a function to check for such an error earlier if desired (needed for the python exposure).At some point, we may want to (or need to) pass the
PyThreadStateto the casting functions as a next step. To do that, we would require new API. I frankly still have no idea whetherPyGILStatefunctions are considered bad, or if they are OK. If they are OK, there is not too much to worry about for now. (I send an email to python c-sig about it).In any case, on this topic: I think we have to do this retrofitting, the main discussion that is needed is the NpyIter modification.
There are currently two tests failing with regards to structured array fields. So I will owe a beer or two to whoever notices the (hopefully stupid) mistake leading to that :/.Marking this as draft, because at least the NpyIter changes should get a test probably.