diff --git a/.flake8 b/.flake8 index a764ad0b511..84837eef48e 100644 --- a/.flake8 +++ b/.flake8 @@ -305,7 +305,6 @@ exclude = numba/tests/test_nested_calls.py numba/tests/test_chained_assign.py numba/tests/test_withlifting.py - numba/tests/test_errorhandling.py numba/tests/test_parfors.py numba/tests/test_sets.py numba/tests/test_dyn_array.py diff --git a/numba/core/types/functions.py b/numba/core/types/functions.py index d4940852975..922acd78d1a 100644 --- a/numba/core/types/functions.py +++ b/numba/core/types/functions.py @@ -38,7 +38,7 @@ def _wrapper(tmp, indent=0): return textwrap.indent(tmp, ' ' * indent, lambda line: True) _overload_template = ("- Of which {nduplicates} did not match due to:\n" - "Overload in function '{function}': File: {file}: " + "{kind} {inof} function '{function}': File: {file}: " "Line {line}.\n With argument(s): '({args})':") _err_reasons = {} @@ -87,21 +87,6 @@ def add_error(self, calltemplate, matched, error, literal): self._failures[key].append(_FAILURE(calltemplate, matched, error, literal)) - def _get_source_info(self, source_fn): - source_line = "" - if 'builtin_function' in str(type(source_fn)): - source_file = "" - else: - try: - source_file = path.abspath(inspect.getsourcefile(source_fn)) - source_line = inspect.getsourcelines(source_fn)[1] - here = path.abspath(__file__) - common = path.commonpath([here, source_file]) - source_file = source_file.replace(common, 'numba') - except: - source_file = "Unknown" - return source_file, source_line - def format(self): """Return a formatted error message from all the gathered errors. """ @@ -109,12 +94,14 @@ def format(self): argstr = argsnkwargs_to_str(self._args, self._kwargs) ncandidates = sum([len(x) for x in self._failures.values()]) + # sort out a display name for the function tykey = self._function_type.typing_key # most things have __name__ fname = getattr(tykey, '__name__', None) is_external_fn_ptr = isinstance(self._function_type, ExternalFunctionPointer) + if fname is None: if is_external_fn_ptr: fname = "ExternalFunctionPointer" @@ -129,55 +116,87 @@ def format(self): nolitkwargs = {k: unliteral(v) for k, v in self._kwargs.items()} nolitargstr = argsnkwargs_to_str(nolitargs, nolitkwargs) - key = self._function_type.key[0] - fn_name = getattr(key, '__name__', str(key)) - # depth could potentially get massive, so limit it. ldepth = min(max(self._depth, 0), self._max_depth) + def template_info(tp): + src_info = tp.get_template_info() + unknown = "unknown" + source_name = src_info.get('name', unknown) + source_file = src_info.get('filename', unknown) + source_lines = src_info.get('lines', unknown) + source_kind = src_info.get('kind', 'Unknown template') + return source_name, source_file, source_lines, source_kind + for i, (k, err_list) in enumerate(self._failures.items()): err = err_list[0] nduplicates = len(err_list) template, error = err.template, err.error - source_fn = template.key - if isinstance(source_fn, numba.core.extending._Intrinsic): - source_fn = template.key._defn - elif (is_external_fn_ptr and - isinstance(source_fn, numba.core.typing.templates.Signature)): - source_fn = template.__class__ - elif hasattr(template, '_overload_func'): - source_fn = template._overload_func - source_file, source_line = self._get_source_info(source_fn) + ifo = template_info(template) + source_name, source_file, source_lines, source_kind = ifo largstr = argstr if err.literal else nolitargstr - msgbuf.append(_termcolor.errmsg( - _wrapper(_overload_template.format(nduplicates=nduplicates, - function=source_fn.__name__, - file=source_file, - line=source_line, - args=largstr), - ldepth + 1))) - - if isinstance(error, BaseException): - reason = indent + self.format_error(error) - errstr = _err_reasons['specific_error'].format(reason) + + if err.error == "No match.": + err_dict = defaultdict(set) + for errs in err_list: + err_dict[errs.template].add(errs.literal) + # if there's just one template, and it's erroring on + # literal/nonliteral be specific + if len(err_dict) == 1: + template = [_ for _ in err_dict.keys()][0] + source_name, source_file, source_lines, source_kind = \ + template_info(template) + source_lines = source_lines[0] + else: + source_file = "" + source_lines = "N/A" + + msgbuf.append(_termcolor.errmsg( + _wrapper(_overload_template.format(nduplicates=nduplicates, + kind = source_kind.title(), + function=fname, + inof='of', + file=source_file, + line=source_lines, + args=largstr), + ldepth + 1))) + msgbuf.append(_termcolor.highlight(_wrapper(err.error, + ldepth + 2))) else: - errstr = error - # if you are a developer, show the back traces - if config.DEVELOPER_MODE: + # There was at least one match in this failure class, but it + # failed for a specific reason try and report this. + msgbuf.append(_termcolor.errmsg( + _wrapper(_overload_template.format(nduplicates=nduplicates, + kind = source_kind.title(), + function=source_name, + inof='in', + file=source_file, + line=source_lines[0], + args=largstr), + ldepth + 1))) + if isinstance(error, BaseException): - # if the error is an actual exception instance, trace it - bt = traceback.format_exception(type(error), error, - error.__traceback__) + reason = indent + self.format_error(error) + errstr = _err_reasons['specific_error'].format(reason) else: - bt = [""] - bt_as_lines = _bt_as_lines(bt) - nd2indent = '\n{}'.format(2 * indent) - errstr += _termcolor.reset(nd2indent + - nd2indent.join(bt_as_lines)) - msgbuf.append(_termcolor.highlight(_wrapper(errstr, ldepth + 2))) - loc = self.get_loc(template, error) - if loc: - msgbuf.append('{}raised from {}'.format(indent, loc)) + errstr = error + # if you are a developer, show the back traces + if config.DEVELOPER_MODE: + if isinstance(error, BaseException): + # if the error is an actual exception instance, trace it + bt = traceback.format_exception(type(error), error, + error.__traceback__) + else: + bt = [""] + bt_as_lines = _bt_as_lines(bt) + nd2indent = '\n{}'.format(2 * indent) + errstr += _termcolor.reset(nd2indent + + nd2indent.join(bt_as_lines)) + msgbuf.append(_termcolor.highlight(_wrapper(errstr, + ldepth + 2))) + loc = self.get_loc(template, error) + if loc: + msgbuf.append('{}raised from {}'.format(indent, loc)) # the commented bit rewraps each block, may not be helpful?! return _wrapper('\n'.join(msgbuf) + '\n') # , self._scale * ldepth) diff --git a/numba/core/typing/templates.py b/numba/core/typing/templates.py index de3eb261dd2..71e4ed7dbf8 100644 --- a/numba/core/typing/templates.py +++ b/numba/core/typing/templates.py @@ -2,6 +2,7 @@ Define typing templates """ +from abc import ABC, abstractmethod import functools import sys import inspect @@ -246,7 +247,7 @@ def fold_arguments(pysig, args, kws, normal_handler, default_handler, return args -class FunctionTemplate(object): +class FunctionTemplate(ABC): # Set to true to disable unsafe cast. # subclass overide-able unsafe_casting = True @@ -277,6 +278,24 @@ def get_impl_key(self, sig): key = key.im_func return key + @abstractmethod + def get_template_info(self): + """ + Returns a dictionary with information specific to the template that will + govern how error messages are displayed to users. The dictionary must + be of the form: + info = { + 'kind': "unknown", # str: The kind of template, e.g. "Overload" + 'name': "unknown", # str: The name of the source function + 'sig': "unknown", # str: The signature(s) of the source function + 'filename': "unknown", # str: The filename of the source function + 'lines': ("start", "end"), # tuple(int, int): The start and + end line of the source function. + 'docstring': "unknown" # str: The docstring of the source function + } + """ + pass + class AbstractTemplate(FunctionTemplate): """ @@ -310,6 +329,22 @@ def unpack_opt(x): return sig + def get_template_info(self): + impl = getattr(self, "generic") + basepath = os.path.dirname(os.path.dirname(numba.__file__)) + code, firstlineno = inspect.getsourcelines(impl) + path = inspect.getsourcefile(impl) + sig = str(utils.pysignature(impl)) + info = { + 'kind': "overload", + 'name': getattr(impl, '__qualname__', impl.__name__), + 'sig': sig, + 'filename': os.path.relpath(path, start=basepath), + 'lines': (firstlineno, firstlineno + len(code) - 1), + 'docstring': impl.__doc__ + } + return info + class CallableTemplate(FunctionTemplate): """ @@ -369,6 +404,23 @@ def unpack_opt(x): cases = [sig] return self._select(cases, bound.args, bound.kwargs) + def get_template_info(self): + impl = getattr(self, "generic") + basepath = os.path.dirname(os.path.dirname(numba.__file__)) + code, firstlineno = inspect.getsourcelines(impl) + path = inspect.getsourcefile(impl) + sig = str(utils.pysignature(impl)) + info = { + 'kind': "overload", + 'name': getattr(self.key, '__name__', + getattr(impl, '__qualname__', impl.__name__),), + 'sig': sig, + 'filename': os.path.relpath(path, start=basepath), + 'lines': (firstlineno, firstlineno + len(code) - 1), + 'docstring': impl.__doc__ + } + return info + class ConcreteTemplate(FunctionTemplate): """ @@ -380,6 +432,25 @@ def apply(self, args, kws): cases = getattr(self, 'cases') return self._select(cases, args, kws) + def get_template_info(self): + import operator + name = getattr(self.key, '__name__', "unknown") + op_func = getattr(operator, name, None) + + kind = "Type restricted function" + if op_func is not None: + if self.key is op_func: + kind = "operator overload" + info = { + 'kind': kind, + 'name': name, + 'sig': "unknown", + 'filename': "unknown", + 'lines': ("unknown", "unknown"), + 'docstring': "unknown" + } + return info + class _EmptyImplementationEntry(InternalError): def __init__(self, reason): @@ -675,6 +746,22 @@ def get_source_info(cls): } return info + def get_template_info(self): + basepath = os.path.dirname(os.path.dirname(numba.__file__)) + impl = self._overload_func + code, firstlineno = inspect.getsourcelines(impl) + path = inspect.getsourcefile(impl) + sig = str(utils.pysignature(impl)) + info = { + 'kind': "overload", + 'name': getattr(impl, '__qualname__', impl.__name__), + 'sig': sig, + 'filename': os.path.relpath(path, start=basepath), + 'lines': (firstlineno, firstlineno + len(code) - 1), + 'docstring': impl.__doc__ + } + return info + def make_overload_template(func, overload_func, jit_options, strict, inline): @@ -728,6 +815,22 @@ def get_impl_key(self, sig): """ return self._overload_cache[sig.args] + def get_template_info(self): + basepath = os.path.dirname(os.path.dirname(numba.__file__)) + impl = self._definition_func + code, firstlineno = inspect.getsourcelines(impl) + path = inspect.getsourcefile(impl) + sig = str(utils.pysignature(impl)) + info = { + 'kind': "intrinsic", + 'name': getattr(impl, '__qualname__', impl.__name__), + 'sig': sig, + 'filename': os.path.relpath(path, start=basepath), + 'lines': (firstlineno, firstlineno + len(code) - 1), + 'docstring': impl.__doc__ + } + return info + def make_intrinsic_template(handle, defn, name): """ @@ -885,7 +988,7 @@ def make_overload_attribute_template(typ, attr, overload_func, inline, *overload_func*. """ assert isinstance(typ, types.Type) or issubclass(typ, types.Type) - name = "OverloadTemplate_%s_%s" % (typ, attr) + name = "OverloadAttributeTemplate_%s_%s" % (typ, attr) # Note the implementation cache is subclass-specific dct = dict(key=typ, _attr=attr, _impl_cache={}, _inline=staticmethod(InlineOptions(inline)), diff --git a/numba/cuda/compiler.py b/numba/cuda/compiler.py index e2e4d9476bc..5569ac915c7 100644 --- a/numba/cuda/compiler.py +++ b/numba/cuda/compiler.py @@ -1,4 +1,5 @@ import ctypes +import inspect import os import sys @@ -8,7 +9,7 @@ from numba.core import (types, typing, utils, funcdesc, serialize, config, compiler, sigutils) from numba.core.compiler_lock import global_compiler_lock - +import numba from .cudadrv.devices import get_context from .cudadrv import nvvm, driver from .errors import normalize_kernel_dimensions @@ -137,6 +138,8 @@ def __init__(self, pyfunc, debug, inline): self.debug = debug self.inline = inline self._compileinfos = {} + name = getattr(pyfunc, '__name__', 'unknown') + self.__name__ = f"{name} ".format(name) def __reduce__(self): glbls = serialize._get_function_globals_for_reduction(self.py_func) @@ -231,6 +234,21 @@ def generic(self, args, kws): assert not kws return dft.compile(args).signature + def get_template_info(cls): + basepath = os.path.dirname(os.path.dirname(numba.__file__)) + code, firstlineno = inspect.getsourcelines(pyfunc) + path = inspect.getsourcefile(pyfunc) + sig = str(utils.pysignature(pyfunc)) + info = { + 'kind': "overload", + 'name': getattr(cls.key, '__name__', "unknown"), + 'sig': sig, + 'filename': os.path.relpath(path, start=basepath), + 'lines': (firstlineno, firstlineno + len(code) - 1), + 'docstring': pyfunc.__doc__ + } + return info + typingctx = CUDATargetDesc.typingctx typingctx.insert_user_function(dft, device_function_template) return dft diff --git a/numba/cuda/tests/cudapy/test_errors.py b/numba/cuda/tests/cudapy/test_errors.py index cf0c8a27d3e..9d45a9888df 100644 --- a/numba/cuda/tests/cudapy/test_errors.py +++ b/numba/cuda/tests/cudapy/test_errors.py @@ -1,7 +1,8 @@ import numpy as np from numba import cuda -from numba.cuda.testing import unittest, CUDATestCase +from numba.core.errors import TypingError +from numba.cuda.testing import unittest, CUDATestCase, skip_on_cudasim def noop(x): @@ -53,6 +54,26 @@ def test_unconfigured_untyped_cudakernel(self): kernfunc = cuda.jit(noop) self._test_unconfigured(kernfunc) + @skip_on_cudasim('TypingError does not occur on simulator') + def test_typing_error(self): + # see #5860, this is present to catch changes to error reporting + # accidentally breaking the CUDA target + + @cuda.jit(device=True) + def dev_func(x): + return floor(x) # oops, forgot to import `floor`. + + @cuda.jit + def kernel_func(): + dev_func(1.5) + + with self.assertRaises(TypingError) as raises: + kernel_func[1, 1]() + excstr = str(raises.exception) + self.assertIn("Overload in function 'dev_func '", + excstr) + self.assertIn("NameError: name 'floor' is not defined", excstr) + if __name__ == '__main__': unittest.main() diff --git a/numba/tests/test_errorhandling.py b/numba/tests/test_errorhandling.py index 1a1478d2636..1991f16f819 100644 --- a/numba/tests/test_errorhandling.py +++ b/numba/tests/test_errorhandling.py @@ -2,21 +2,20 @@ Unspecified error handling tests """ -import os -from numba import jit, njit, typed, int64, intp, types -from numba.core import errors, utils -from numba.extending import overload, intrinsic import numpy as np +import os - -from numba.core.untyped_passes import (ExtractByteCode, TranslateByteCode, FixupArgs, +from numba import jit, njit, typed, int64, types +from numba.core import errors +import numba.core.typing.cffi_utils as cffi_support +from numba.extending import (overload, intrinsic, overload_method, + overload_attribute) +from numba.core.compiler import CompilerBase +from numba.core.untyped_passes import (TranslateByteCode, FixupArgs, IRProcessing,) - from numba.core.typed_passes import (NopythonTypeInference, DeadCodeElimination, - NativeLowering, IRLegalization, NoPythonBackend) - -from numba.core.compiler_machinery import FunctionPass, PassManager, register_pass +from numba.core.compiler_machinery import PassManager from numba.core.types.functions import _err_reasons as error_reasons from numba.tests.support import skip_parfors_unsupported @@ -26,6 +25,7 @@ _global_list = [1, 2, 3, 4] _global_dict = typed.Dict.empty(int64, int64) + class TestErrorHandlingBeforeLowering(unittest.TestCase): def test_unsupported_make_function_return_inner_func(self): @@ -96,7 +96,6 @@ def foo_docstring(): def test_use_of_ir_unknown_loc(self): # for context see # 3390 - from numba.core.compiler import CompilerBase class TestPipeline(CompilerBase): def define_pipelines(self): name = 'bad_DCE_pipeline' @@ -104,8 +103,8 @@ def define_pipelines(self): pm.add_pass(TranslateByteCode, "analyzing bytecode") pm.add_pass(FixupArgs, "fix up args") pm.add_pass(IRProcessing, "processing IR") - # remove dead before type inference so that the Arg node is removed - # and the location of the arg cannot be found + # remove dead before type inference so that the Arg node is + # removed and the location of the arg cannot be found pm.add_pass(DeadCodeElimination, "DCE") # typing pm.add_pass(NopythonTypeInference, "nopython frontend") @@ -131,7 +130,6 @@ def check_write_to_globals(self, func): for ex in expected: self.assertIn(ex, str(raises.exception)) - def test_handling_of_write_to_reflected_global(self): @njit def foo(): @@ -150,7 +148,7 @@ def foo(): def test_handling_forgotten_numba_internal_import(self): @njit(parallel=True) def foo(): - for i in prange(10): # prange is not imported + for i in prange(10): # noqa: F821 prange is not imported pass with self.assertRaises(errors.TypingError) as raises: @@ -162,7 +160,7 @@ def foo(): def test_handling_unsupported_generator_expression(self): def foo(): - y = (x for x in range(10)) + (x for x in range(10)) expected = "The use of yield in a closure is unsupported." @@ -171,6 +169,17 @@ def foo(): dec(foo)() self.assertIn(expected, str(raises.exception)) + def test_handling_undefined_variable(self): + @njit + def foo(): + return a # noqa: F821 + + expected = "NameError: name 'a' is not defined" + + with self.assertRaises(errors.TypingError) as raises: + foo() + self.assertIn(expected, str(raises.exception)) + class TestConstantInferenceErrorHandling(unittest.TestCase): @@ -234,8 +243,91 @@ def call_foo(): excstr = str(raises.exception) self.assertIn("No match", excstr) - def test_error_in_intrinsic(self): + def test_error_function_source_is_correct(self): + """ Checks that the reported source location for an overload is the + overload implementation source, not the actual function source from the + target library.""" + + @njit + def foo(): + np.linalg.svd("chars") + + with self.assertRaises(errors.TypingError) as raises: + foo() + + excstr = str(raises.exception) + self.assertIn(error_reasons['specific_error'].splitlines()[0], excstr) + expected_file = os.path.join("numba", "np", "linalg.py") + expected = f"Overload in function 'svd_impl': File: {expected_file}:" + self.assertIn(expected.format(expected_file), excstr) + + def test_concrete_template_source(self): + # hits ConcreteTemplate + @njit + def foo(): + return 'a' + 1 + + with self.assertRaises(errors.TypingError) as raises: + foo() + + excstr = str(raises.exception) + + self.assertIn("Operator Overload in function 'add'", excstr) + # there'll be numerous matched templates that don't work + self.assertIn("", excstr) + + def test_abstract_template_source(self): + # hits AbstractTemplate + @njit + def foo(): + return len(1) + + with self.assertRaises(errors.TypingError) as raises: + foo() + + excstr = str(raises.exception) + self.assertIn("Overload of function 'len'", excstr) + + def test_callable_template_source(self): + # hits CallableTemplate + @njit + def foo(): + return np.angle(1) + + with self.assertRaises(errors.TypingError) as raises: + foo() + + excstr = str(raises.exception) + self.assertIn("Overload of function 'angle'", excstr) + + def test_overloadfunction_template_source(self): + # hits _OverloadFunctionTemplate + def bar(x): + pass + + @overload(bar) + def ol_bar(x): + pass + + @njit + def foo(): + return bar(1) + + with self.assertRaises(errors.TypingError) as raises: + foo() + excstr = str(raises.exception) + # there will not be "numerous" matched templates, there's just one, + # the one above, so assert it is reported + self.assertNotIn("", excstr) + expected_file = os.path.join("numba", "tests", + "test_errorhandling.py") + expected_ol = f"Overload of function 'bar': File: {expected_file}:" + self.assertIn(expected_ol.format(expected_file), excstr) + self.assertIn("No match.", excstr) + + def test_intrinsic_template_source(self): + # hits _IntrinsicTemplate given_reason1 = "x must be literal" given_reason2 = "array.ndim must be 1" @@ -265,24 +357,70 @@ def call_intrin(): self.assertIn(error_reasons['specific_error'].splitlines()[0], excstr) self.assertIn(given_reason1, excstr) self.assertIn(given_reason2, excstr) + self.assertIn("Intrinsic in function", excstr) - def test_error_function_source_is_correct(self): - """ Checks that the reported source location for an overload is the - overload implementation source, not the actual function source from the - target library.""" + def test_overloadmethod_template_source(self): + # doesn't hit _OverloadMethodTemplate for source as it's a nested + # exception + @overload_method(types.UnicodeType, 'isnonsense') + def ol_unicode_isnonsense(self): + pass @njit def foo(): - np.linalg.svd("chars") + "abc".isnonsense() with self.assertRaises(errors.TypingError) as raises: foo() excstr = str(raises.exception) - self.assertIn(error_reasons['specific_error'].splitlines()[0], excstr) - expected_file = os.path.join("numba", "np", "linalg.py") - expected = f"Overload in function 'svd_impl': File: {expected_file}:" - self.assertIn(expected.format(expected_file), excstr) + self.assertIn("Overload of function 'ol_unicode_isnonsense'", excstr) + + def test_overloadattribute_template_source(self): + # doesn't hit _OverloadMethodTemplate for source as it's a nested + # exception + @overload_attribute(types.UnicodeType, 'isnonsense') + def ol_unicode_isnonsense(self): + pass + + @njit + def foo(): + "abc".isnonsense + + with self.assertRaises(errors.TypingError) as raises: + foo() + + excstr = str(raises.exception) + self.assertIn("Overload of function 'ol_unicode_isnonsense'", excstr) + + def test_external_function_pointer_template_source(self): + from numba.tests.ctypes_usecases import c_cos + + @njit + def foo(): + c_cos('a') + + with self.assertRaises(errors.TypingError) as raises: + foo() + + excstr = str(raises.exception) + self.assertIn("Type Restricted Function in function 'unknown'", excstr) + + @unittest.skipUnless(cffi_support.SUPPORTED, "CFFI not supported") + def test_cffi_function_pointer_template_source(self): + from numba.tests import cffi_usecases as mod + mod.init() + func = mod.cffi_cos + + @njit + def foo(): + func('a') + + with self.assertRaises(errors.TypingError) as raises: + foo() + + excstr = str(raises.exception) + self.assertIn("Type Restricted Function in function 'unknown'", excstr) if __name__ == '__main__':