Skip to content

BUG: don't silence __array_wrap__ errors in ufunc.reduce #9022

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

Merged
merged 3 commits into from
Jul 27, 2018

Conversation

eric-wieser
Copy link
Member

Raised by #5819 (comment).

  • Classes that declared __array_wrap__(self, obj, context), with no default argument for context, would find that this method was never called by reduce
  • Any errors that happen inside __array_wrap__ were silenced, and the result downcast to ndarray

@eric-wieser eric-wieser changed the title BUG: don't silence __array_wrap__ errors in reduce BUG: don't silence __array_wrap__ errors in ufunc.reduce Apr 29, 2017
@@ -53,7 +53,8 @@ def __new__(cls, data, metadata=None):
return res

def __array_wrap__(self, obj, context=None):
obj.metadata = self.metadata
if isinstance(obj, MyArray):
obj.metadata = self.metadata
Copy link
Member Author

Choose a reason for hiding this comment

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

This was previously crashing, because obj was ndarray, and did not have the metadata property.

I think there's a bigger issue here in that __array_prepare__ is not called either, but with __array_ufunc__ approaching release, I don't think we need to fix that any more

}
else {
else if (wrap != NULL) {
PyObject *res = PyObject_CallFunctionObjArgs(
Copy link
Member

Choose a reason for hiding this comment

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

Would almost prefer putting the initialization fo res on another line just to have space to put it all on a single line.

else if (wrap != NULL) {
PyObject *res = PyObject_CallFunctionObjArgs(
wrap, ret, Py_None, NULL);
/* Handle __array_wrap__ that does not accept a context argument */
Copy link
Member

Choose a reason for hiding this comment

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

Blank lines after declarations is preferred.

if (res == NULL && PyErr_ExceptionMatches(PyExc_TypeError)) {
PyErr_Clear();
res = PyObject_CallFunctionObjArgs(
wrap, ret, NULL);
Copy link
Member

Choose a reason for hiding this comment

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

This all should fit on one line I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

Again, copied from the other location. I guess I'll fix both

else {
PyObject *wrap;
_find_array_wrap(args, kwds, &wrap, 1, 1);

Copy link
Member

Choose a reason for hiding this comment

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

I'd move this blank line up to after the declaration.

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 above else. I'm curious if the wrap here matters, the return will already have happened if out is not NULL.

Copy link
Member

Choose a reason for hiding this comment

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

NVM, I see that this is to wrap the return when out is missing.

@@ -68,6 +68,9 @@
/********************/

/* ---------------------------------------------------------------- */
static void
_find_array_wrap(PyObject *args, PyObject *kwds,
PyObject **output_wrap, int nin, int nout);
Copy link
Member

Choose a reason for hiding this comment

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

Needs one more space of indentation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just copied from the definition, I think - I'll fix it there too

@charris
Copy link
Member

charris commented May 11, 2017

Couple of style nits. Reading the ufunc code makes my head ache...

@charris
Copy link
Member

charris commented May 30, 2017

Be good to finish this up.

@eric-wieser
Copy link
Member Author

I'll take a look in the next few days once my schedule clears

@eric-wieser eric-wieser force-pushed the reduce-hides-error branch from fbb53d0 to 2122172 Compare June 1, 2017 12:25
PyErr_Clear();
else {
PyObject *wrap;
_find_array_wrap(args, kwds, &wrap, 1, 1);
Copy link
Member Author

Choose a reason for hiding this comment

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

Turns out this doesn't work, as it interprets the axis argument as an out argument

@eric-wieser
Copy link
Member Author

Overhauled, with the first two trivial cleanup commits extracted to #10988

@eric-wieser eric-wieser changed the base branch from master to fwd_port_1.14.3_changelog May 14, 2018 15:42
@eric-wieser eric-wieser changed the base branch from fwd_port_1.14.3_changelog to master May 14, 2018 15:43
@eric-wieser
Copy link
Member Author

I'll see if I can revive this this weekend - seems my latest fixups segfault...

@eric-wieser
Copy link
Member Author

Hopefully fixed by #11296

return (PyObject *)ret;
}
/* Wrap and return the output */
{
Copy link
Member

Choose a reason for hiding this comment

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

I still don't like free floating blocks :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Would you prefer if(true) {? :)

I could extract this to another _wrap_reduction_output helper function, but I think that starts to obscure the goal here.

Usually my goal with these floating blocks is to limit the scope of the variables, to make it clear where they should be used. C99 will remove most of the reason for my use of them

else if (res == Py_None) {
Py_DECREF(res);
if (out != NULL) {
full_args.out = PyTuple_Pack(1, out);
Copy link
Member

Choose a reason for hiding this comment

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

Always one output in reductions?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, reductions only apply to binary input unary output ufuncs

return res;
}
fail:
Py_DECREF(obj);
Copy link
Member

Choose a reason for hiding this comment

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

Don't need to Py_DECREF(wrap)? I see it is done in the one place above, just want to make sure everything that jumps to here has taken care of it.

py_context = Py_BuildValue("OOi",
context->ufunc, args_tup, context->out_i);
Py_DECREF(args_tup);
if (py_context == NULL) {
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 wrap?

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, thanks

PyObject *args_tup;
/* Call the method with appropriate context */
args_tup = _get_wrap_prepare_args(context->args);
if (args_tup == NULL) {
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 wrap?

@eric-wieser eric-wieser force-pushed the reduce-hides-error branch 2 times, most recently from 7546bf7 to f25853a Compare June 11, 2018 06:23
@eric-wieser
Copy link
Member Author

Ok, fixed the missing decref on wrap.

Unfortunately wrap is still rarely leaked on error in the normal ufunc.__call__ case, but that was true before this patch - and I'd prefer to address it in a future PR.

@mhvk
Copy link
Contributor

mhvk commented Jun 11, 2018

I'm sorry I'm late too this, but I'm worried about the change in behaviour for reductions. Is it needed to wrap even output that is provided or output that has the same type as the input? Since we don't pass in a context here, an implementation of __array_wrap__ can do exactly nothing except change the type - which in both those cases would be OK already. My sense would be to fix the bug but not the logic of when __array_wrap__ is called.

@@ -87,6 +94,15 @@ _get_wrap_prepare_args(ufunc_full_args full_args) {
}
}

static void
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe move the functions up instead?

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'd rather keep the diff smaller to make the blame easier

Copy link
Contributor

Choose a reason for hiding this comment

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

Fine to make it another commit; I find these prototypes before the actual code very confusing...

Copy link
Member Author

Choose a reason for hiding this comment

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

Done - I suppose in a standalone commit, a messy diff is fine if it comes with a commit message assuring nothing really changed.

@eric-wieser
Copy link
Member Author

You're right, probably safer not to change the behavior here. I've modified it to keep the __array_wrap__ lookup logic the same, and just tweak the invocation logic.

PyErr_Clear();
}
else if (res == Py_None) {
Py_DECREF(res);
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 think this line was intended to mean a.__array_wrap__ is None, not a.__array_wrap__() is None

@eric-wieser eric-wieser force-pushed the reduce-hides-error branch 2 times, most recently from facd479 to 90d747a Compare June 12, 2018 07:58
…ce as in ufunc.__call__

Previously they were silenced
…es are not needed

The functions themselves have not changed, only moved
Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

Looks nice to me now; only one nit-pick argument which you should feel free to ignore (maybe it is even wrong).

else {
PyObject *res;
PyObject *args_tup;
_ufunc_context context;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just _ufunc_context context = {ufunc, full_args, i}?

else if (res == Py_None) {
Py_DECREF(res);
/* Wrap and return the output */
{
Copy link
Contributor

Choose a reason for hiding this comment

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

I must admit I find this open context ugly too - I'm trained to think I must be within a special block. I'd just move the PyObject *wrap to the top...

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 put the comment before the block to make it clearer that there was no control structure missing. I think that the reduced number of local variable to keep track of is worth having a briefly misleading {. Also, when we adopt C99, cases like this can all be removed with a find and replace.

else {
PyObject *res;
PyObject *args_tup;
context.ufunc = ufunc;
Copy link
Contributor

Choose a reason for hiding this comment

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

Now I think I've made the comment, but perhaps forgot to submit: maybe just define _ufunc_context context = {ufunc, full_args, i};? Or is that incorrect syntax? (My C remains quite hopeless...)

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, that seems reasonable - Definitely correct in some version of C, I think C90 will cope.

Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

Two nitpicks left (one shared by @charris...)

@@ -4555,42 +4631,15 @@ ufunc_generic_call(PyUFuncObject *ufunc, PyObject *args, PyObject *kwds)
/* wrap outputs */
for (i = 0; i < ufunc->nout; i++) {
int j = ufunc->nin+i;
PyObject *wrap = wraparr[i];
_ufunc_context context = {ufunc, full_args, i};
Copy link
Contributor

Choose a reason for hiding this comment

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

Appveyor complains about this line (https://ci.appveyor.com/project/charris/numpy/build/1.0.10809/job/3m1asbd6b1v588ro#L1894):
numpy\core\src\umath\ufunc_object.c(4634) : error C2440: 'initializing' : cannot convert from 'ufunc_full_args' to 'PyObject *'

Does look like the definition is indeed off.

Copy link
Member Author

@eric-wieser eric-wieser Jun 13, 2018

Choose a reason for hiding this comment

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

Looks like a bug in the old msvc needed by python 2.7 and 3.4 - guess I'll revert

@mattip
Copy link
Member

mattip commented Jul 27, 2018

@mhvk is this ready to go?

@mhvk
Copy link
Contributor

mhvk commented Jul 27, 2018

Yes, I approved this quite a while ago, and the nitpicks were discussed. Thanks for checking, @mattip!

@mhvk mhvk merged commit 43bbeef into numpy:master Jul 27, 2018
@charris charris changed the title BUG: don't silence __array_wrap__ errors in ufunc.reduce BUG: don't silence __array_wrap__ errors in ufunc.reduce Nov 10, 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.

4 participants