Skip to content

Commit

Permalink
Merge pull request numpy#12593 from eric-wieser/ufunc-exceptions
Browse files Browse the repository at this point in the history
ENH,WIP: Use richer exception types for ufunc type resolution errors
  • Loading branch information
mhvk authored Jan 20, 2019
2 parents a709c6e + 9c2296b commit 99953e4
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 50 deletions.
100 changes: 100 additions & 0 deletions numpy/core/_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
Various richly-typed exceptions, that also help us deal with string formatting
in python where it's easier.
By putting the formatting in `__str__`, we also avoid paying the cost for
users who silence the exceptions.
"""
from numpy.core.overrides import set_module


def _unpack_tuple(tup):
if len(tup) == 1:
return tup[0]
else:
return tup


def _display_as_base(cls):
"""
A decorator that makes an exception class look like its base.
We use this to hide subclasses that are implementation details - the user
should catch the base type, which is what the traceback will show them.
Classes decorated with this decorator are subject to removal without a
deprecation warning.
"""
assert issubclass(cls, Exception)
cls.__name__ = cls.__base__.__name__
cls.__qualname__ = cls.__base__.__qualname__
return cls


class UFuncTypeError(TypeError):
""" Base class for all ufunc exceptions """
def __init__(self, ufunc):
self.ufunc = ufunc


@_display_as_base
class _UFuncNoLoopError(UFuncTypeError):
""" Thrown when a ufunc loop cannot be found """
def __init__(self, ufunc, dtypes):
super().__init__(ufunc)
self.dtypes = tuple(dtypes)

def __str__(self):
return (
"ufunc {!r} did not contain a loop with signature matching types "
"{!r} -> {!r}"
).format(
self.ufunc.__name__,
_unpack_tuple(self.dtypes[:self.ufunc.nin]),
_unpack_tuple(self.dtypes[self.ufunc.nin:])
)


@_display_as_base
class _UFuncCastingError(UFuncTypeError):
def __init__(self, ufunc, casting, from_, to):
super().__init__(ufunc)
self.casting = casting
self.from_ = from_
self.to = to


@_display_as_base
class _UFuncInputCastingError(_UFuncCastingError):
""" Thrown when a ufunc input cannot be casted """
def __init__(self, ufunc, casting, from_, to, i):
super().__init__(ufunc, casting, from_, to)
self.in_i = i

def __str__(self):
# only show the number if more than one input exists
i_str = "{} ".format(self.in_i) if self.ufunc.nin != 1 else ""
return (
"Cannot cast ufunc {!r} input {}from {!r} to {!r} with casting "
"rule {!r}"
).format(
self.ufunc.__name__, i_str, self.from_, self.to, self.casting
)


@_display_as_base
class _UFuncOutputCastingError(_UFuncCastingError):
""" Thrown when a ufunc output cannot be casted """
def __init__(self, ufunc, casting, from_, to, i):
super().__init__(ufunc, casting, from_, to)
self.out_i = i

def __str__(self):
# only show the number if more than one output exists
i_str = "{} ".format(self.out_i) if self.ufunc.nout != 1 else ""
return (
"Cannot cast ufunc {!r} output {}from {!r} to {!r} with casting "
"rule {!r}"
).format(
self.ufunc.__name__, i_str, self.from_, self.to, self.casting
)
199 changes: 150 additions & 49 deletions numpy/core/src/umath/ufunc_type_resolution.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include "npy_config.h"
#include "npy_pycompat.h"
#include "npy_import.h"

#include "numpy/ufuncobject.h"
#include "ufunc_type_resolution.h"
Expand All @@ -27,6 +28,26 @@
#include "cblasfuncs.h"
#endif

static PyObject *
npy_casting_to_py_object(NPY_CASTING casting)
{
switch (casting) {
case NPY_NO_CASTING:
return PyUString_FromString("no");
case NPY_EQUIV_CASTING:
return PyUString_FromString("equiv");
case NPY_SAFE_CASTING:
return PyUString_FromString("safe");
case NPY_SAME_KIND_CASTING:
return PyUString_FromString("same_kind");
case NPY_UNSAFE_CASTING:
return PyUString_FromString("unsafe");
default:
return PyInt_FromLong(casting);
}
}


static const char *
npy_casting_to_string(NPY_CASTING casting)
{
Expand All @@ -46,6 +67,9 @@ npy_casting_to_string(NPY_CASTING casting)
}
}

/**
* Always returns -1 to indicate the exception was raised, for convenience
*/
static int
raise_binary_type_reso_error(PyUFuncObject *ufunc, PyArrayObject **operands) {
PyObject *errmsg;
Expand All @@ -63,6 +87,126 @@ raise_binary_type_reso_error(PyUFuncObject *ufunc, PyArrayObject **operands) {
return -1;
}

/** Helper function to raise UFuncNoLoopError
* Always returns -1 to indicate the exception was raised, for convenience
*/
static int
raise_no_loop_found_error(
PyUFuncObject *ufunc, PyArray_Descr **dtypes, npy_intp n_dtypes)
{
static PyObject *exc_type = NULL;
PyObject *exc_value;
PyObject *dtypes_tup;
npy_intp i;

npy_cache_import(
"numpy.core._exceptions", "_UFuncNoLoopError",
&exc_type);
if (exc_type == NULL) {
return -1;
}

/* convert dtypes to a tuple */
dtypes_tup = PyTuple_New(n_dtypes);
if (dtypes_tup == NULL) {
return -1;
}
for (i = 0; i < n_dtypes; ++i) {
Py_INCREF(dtypes[i]);
PyTuple_SET_ITEM(dtypes_tup, i, (PyObject *)dtypes[i]);
}

/* produce an error object */
exc_value = PyTuple_Pack(2, ufunc, dtypes_tup);
Py_DECREF(dtypes_tup);
if (exc_value == NULL){
return -1;
}
PyErr_SetObject(exc_type, exc_value);
Py_DECREF(exc_value);

return -1;
}

static int
raise_casting_error(
PyObject *exc_type,
PyUFuncObject *ufunc,
NPY_CASTING casting,
PyArray_Descr *from,
PyArray_Descr *to,
npy_intp i)
{
PyObject *exc_value;
PyObject *casting_value;

casting_value = npy_casting_to_py_object(casting);
if (casting_value == NULL) {
return -1;
}

exc_value = Py_BuildValue(
"ONOOi",
ufunc,
casting_value,
(PyObject *)from,
(PyObject *)to,
i
);
if (exc_value == NULL){
return -1;
}
PyErr_SetObject(exc_type, exc_value);
Py_DECREF(exc_value);

return -1;
}

/** Helper function to raise UFuncInputCastingError
* Always returns -1 to indicate the exception was raised, for convenience
*/
static int
raise_input_casting_error(
PyUFuncObject *ufunc,
NPY_CASTING casting,
PyArray_Descr *from,
PyArray_Descr *to,
npy_intp i)
{
static PyObject *exc_type = NULL;
npy_cache_import(
"numpy.core._exceptions", "_UFuncInputCastingError",
&exc_type);
if (exc_type == NULL) {
return -1;
}

return raise_casting_error(exc_type, ufunc, casting, from, to, i);
}


/** Helper function to raise UFuncOutputCastingError
* Always returns -1 to indicate the exception was raised, for convenience
*/
static int
raise_output_casting_error(
PyUFuncObject *ufunc,
NPY_CASTING casting,
PyArray_Descr *from,
PyArray_Descr *to,
npy_intp i)
{
static PyObject *exc_type = NULL;
npy_cache_import(
"numpy.core._exceptions", "_UFuncOutputCastingError",
&exc_type);
if (exc_type == NULL) {
return -1;
}

return raise_casting_error(exc_type, ufunc, casting, from, to, i);
}


/*UFUNC_API
*
Expand All @@ -79,45 +223,18 @@ PyUFunc_ValidateCasting(PyUFuncObject *ufunc,
PyArray_Descr **dtypes)
{
int i, nin = ufunc->nin, nop = nin + ufunc->nout;
const char *ufunc_name = ufunc_get_name_cstr(ufunc);

for (i = 0; i < nop; ++i) {
if (i < nin) {
if (!PyArray_CanCastArrayTo(operands[i], dtypes[i], casting)) {
PyObject *errmsg;
errmsg = PyUString_FromFormat("Cannot cast ufunc %s "
"input from ", ufunc_name);
PyUString_ConcatAndDel(&errmsg,
PyObject_Repr((PyObject *)PyArray_DESCR(operands[i])));
PyUString_ConcatAndDel(&errmsg,
PyUString_FromString(" to "));
PyUString_ConcatAndDel(&errmsg,
PyObject_Repr((PyObject *)dtypes[i]));
PyUString_ConcatAndDel(&errmsg,
PyUString_FromFormat(" with casting rule %s",
npy_casting_to_string(casting)));
PyErr_SetObject(PyExc_TypeError, errmsg);
Py_DECREF(errmsg);
return -1;
return raise_input_casting_error(
ufunc, casting, PyArray_DESCR(operands[i]), dtypes[i], i);
}
} else if (operands[i] != NULL) {
if (!PyArray_CanCastTypeTo(dtypes[i],
PyArray_DESCR(operands[i]), casting)) {
PyObject *errmsg;
errmsg = PyUString_FromFormat("Cannot cast ufunc %s "
"output from ", ufunc_name);
PyUString_ConcatAndDel(&errmsg,
PyObject_Repr((PyObject *)dtypes[i]));
PyUString_ConcatAndDel(&errmsg,
PyUString_FromString(" to "));
PyUString_ConcatAndDel(&errmsg,
PyObject_Repr((PyObject *)PyArray_DESCR(operands[i])));
PyUString_ConcatAndDel(&errmsg,
PyUString_FromFormat(" with casting rule %s",
npy_casting_to_string(casting)));
PyErr_SetObject(PyExc_TypeError, errmsg);
Py_DECREF(errmsg);
return -1;
return raise_output_casting_error(
ufunc, casting, dtypes[i], PyArray_DESCR(operands[i]), i);
}
}
}
Expand Down Expand Up @@ -1382,12 +1499,8 @@ PyUFunc_DefaultLegacyInnerLoopSelector(PyUFuncObject *ufunc,
{
int nargs = ufunc->nargs;
char *types;
const char *ufunc_name;
PyObject *errmsg;
int i, j;

ufunc_name = ufunc_get_name_cstr(ufunc);

/*
* If there are user-loops search them first.
* TODO: There needs to be a loop selection acceleration structure,
Expand Down Expand Up @@ -1422,19 +1535,7 @@ PyUFunc_DefaultLegacyInnerLoopSelector(PyUFuncObject *ufunc,
types += nargs;
}

errmsg = PyUString_FromFormat("ufunc '%s' did not contain a loop "
"with signature matching types ", ufunc_name);
for (i = 0; i < nargs; ++i) {
PyUString_ConcatAndDel(&errmsg,
PyObject_Repr((PyObject *)dtypes[i]));
if (i < nargs - 1) {
PyUString_ConcatAndDel(&errmsg, PyUString_FromString(" "));
}
}
PyErr_SetObject(PyExc_TypeError, errmsg);
Py_DECREF(errmsg);

return -1;
return raise_no_loop_found_error(ufunc, dtypes, nargs);
}

typedef struct {
Expand Down Expand Up @@ -2251,7 +2352,7 @@ type_tuple_type_resolver(PyUFuncObject *self,

/* If no function was found, throw an error */
PyErr_Format(PyExc_TypeError,
"No loop matching the specified signature and casting\n"
"No loop matching the specified signature and casting "
"was found for ufunc %s", ufunc_name);

return -1;
Expand Down
2 changes: 1 addition & 1 deletion numpy/core/tests/test_multiarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -5920,7 +5920,7 @@ def test_out_arg(self):
assert_array_equal(out, tgt, err_msg=msg)

# test out with not allowed type cast (safe casting)
msg = "Cannot cast ufunc matmul output"
msg = "Cannot cast ufunc .* output"
out = np.zeros((5, 2), dtype=np.int32)
assert_raises_regex(TypeError, msg, self.matmul, a, b, out=out)

Expand Down

0 comments on commit 99953e4

Please sign in to comment.