Skip to content

Conversation

@jcusick13
Copy link
Contributor

@jcusick13 jcusick13 commented Jul 16, 2022

Closes #8706, #21883

Addresses concerns raised in the two linked issues by:

  • Casting NULL objects to Py_None within _deepcopy_call to avoid issues within CPython's deepcopy
  • Setting _deepcopy_call to return an int code for error/success, which is then checked in array_deepcopy

Copy link
Member

@HaoZeke HaoZeke left a comment

Choose a reason for hiding this comment

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

Will let @seberg be the last word but this seems solid. Thanks!

@jcusick13
Copy link
Contributor Author

@seberg I realized when I sat back down to work on this, I wasn't exactly clear of how to setup a test for the fail case. I'm stumbling a little bit on how to create an object that will fail when copy.deepcopy is called specifically from within PyObject_CallFunctionObjArgs. I originally tried making a dummy class within the test but I was only able to make the high level .__deepcopy__ behavior fail, which isn't exactly what we're looking for.

def test__deepcopy__catches_failure(self):
    class MyArray(np.ndarray):
         def __deepcopy__(self, *args, **kwargs):
             raise Exception

    a = MyArray([1, 2, 3], dtype='O')
    a.__deepcopy__({})  # Raises an `Exception` defined above, not one from within `_deepcopy_call` C code

I also tried mocking out deepcopy itself but also didn't seem to get all the way there. It wasn't clear to me what to assert for in the case when the underlying deepcopy fails.

def test__deepcopy__catches_failure(self):
    a = np.array([1, 2, 3], dtype='O')

    with patch.object(copy, 'deepcopy', side_effect=None):
        b = a.__deepcopy__({})
      
        # What would a valid assertion be here?
        assert (b == None).all()

Do you have any suggestions or examples from other places in the code of how to do this?

@seberg
Copy link
Member

seberg commented Jul 17, 2022

@jcusick13 oh, you have to put hte bad object into the array:

    class MyObj(np.ndarray):
         def __deepcopy__(self, *args, **kwargs):
             raise Exception

    arr = np.array([MyObj()])
    arr.__deepcopy__({})  # or copy.deepcopy(arr), I guess

@jcusick13
Copy link
Contributor Author

Thanks @seberg. I added the suggested changes but am still a little bit confused. From looking at the docs for PyObject_CallFunctionObjArgs, I'm expecting errors to be caught within that function and then returned as NULL

Return the result of the call on success, or raise an exception and return NULL on failure.

though it seems like the Exception raised from the bad object just bubbles all the way back up to the surface rather than being caught within the error handling of _deepcopy_call:

/* call deepcopy on this argument */
res = PyObject_CallFunctionObjArgs(deepcopy, itemp, visit, NULL);
Py_DECREF(itemp);
if (res == NULL) {
    return -1;
}

@seberg
Copy link
Member

seberg commented Jul 17, 2022

How exceptions work (typically), is that you return NULL indicating that an error occurred (somewhere). Python then sets the global error state that you could check for example with PyErr_Occured(). So NULL result means "error", and the error itself is stored elsewhere. Was that your question?

@jcusick13
Copy link
Contributor Author

Ah thanks for the explanation @seberg. I think my final point of confusion is if we then expect a failure within this function

res = PyObject_CallFunctionObjArgs(deepcopy, itemp, visit, NULL);

to raise a Python Exception or if it should return NULL, which would then be handled more gracefully but the error handling within _deepcopy_call?

Mainly, it's not clear to me what to check during the test for the fail case. What should I be looking to create an assert for in the end of the test for this behavior?

raise Exception

arr = np.array([1, MyObj(), 3], dtype='O')
arr.__deepcopy__({})
Copy link
Member

Choose a reason for hiding this comment

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

This should float up the exception, so you could not use Exception above maybe (maybe AssertionError), not that it matters much. What matters is wrapping this into:

with pytest.raises(Exception):  # (or a more exact Exception)
    arr.__deepcopy__({})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it! I think I was just overthinking things a bit. Just made the changes now; appreciate the help 🙌

@seberg
Copy link
Member

seberg commented Jul 17, 2022

Thanks also for that last error test! Hope to see you around :). Lets put this in, it looks nice to me.

@seberg seberg merged commit 0762342 into numpy:main Jul 17, 2022
@jcusick13 jcusick13 deleted the 21883/deepcopy-exception branch July 18, 2022 00:05
@charris charris added the 09 - Backport-Candidate PRs tagged should be backported label Jul 22, 2022
charris pushed a commit to charris/numpy that referenced this pull request Jul 24, 2022
Addresses concerns raised in the two linked issues by:

Casting NULL objects to Py_None within _deepcopy_call to avoid issues within CPython's deepcopy
Setting _deepcopy_call to return an int code for error/success, which is then checked in array_deepcopy

Closes numpygh-8706, numpygh-21883
@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Jul 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Second contribution

Development

Successfully merging this pull request may close these issues.

_deepcopy_call needs better error handling.

4 participants