From 6cc034200cef1d02099e8e0603a119b8bba7a123 Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Tue, 2 Oct 2018 21:06:29 +0200 Subject: [PATCH 1/3] Validate dispatcher functions in array_function_dispatch They should have the same signature as the decorated function. Note: eventually these checks should be optional -- we really only need them to be run as part of NumPy's test suite, not every time numpy is imported. --- numpy/core/overrides.py | 34 ++++++++++++++++++++++++++++++ numpy/core/tests/test_overrides.py | 21 +++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/numpy/core/overrides.py b/numpy/core/overrides.py index 17e3d475f86e..5d1df340d288 100644 --- a/numpy/core/overrides.py +++ b/numpy/core/overrides.py @@ -2,8 +2,11 @@ TODO: rewrite this in C for performance. """ +import collections import functools + from numpy.core.multiarray import ndarray +from numpy.compat._inspect import getargspec _NDARRAY_ARRAY_FUNCTION = ndarray.__array_function__ @@ -107,13 +110,44 @@ def array_function_implementation_or_override( .format(public_api, list(map(type, overloaded_args)))) +ArgSpec = collections.namedtuple('ArgSpec', 'args varargs keywords defaults') + + +def verify_matching_signatures(implementation, dispatcher): + """Verify that a dispatcher function has the right signature.""" + implementation_spec = ArgSpec(*getargspec(implementation)) + dispatcher_spec = ArgSpec(*getargspec(dispatcher)) + + if (implementation_spec.args != dispatcher_spec.args or + implementation_spec.varargs != dispatcher_spec.varargs or + implementation_spec.keywords != dispatcher_spec.keywords or + (bool(implementation_spec.defaults) != + bool(dispatcher_spec.defaults)) or + (implementation_spec.defaults is not None and + len(implementation_spec.defaults) != + len(dispatcher_spec.defaults))): + raise RuntimeError('implementation and dispatcher for %s have ' + 'different function signatures' % implementation) + + if implementation_spec.defaults is not None: + if dispatcher_spec.defaults != (None,) * len(dispatcher_spec.defaults): + raise RuntimeError('dispatcher functions can only use None for ' + 'default argument values') + + def array_function_dispatch(dispatcher): """Decorator for adding dispatch with the __array_function__ protocol.""" def decorator(implementation): + # TODO: only do this check when the appropriate flag is enabled or for + # a dev install. We want this check for testing but don't want to + # slow down all numpy imports. + verify_matching_signatures(implementation, dispatcher) + @functools.wraps(implementation) def public_api(*args, **kwargs): relevant_args = dispatcher(*args, **kwargs) return array_function_implementation_or_override( implementation, public_api, relevant_args, args, kwargs) return public_api + return decorator diff --git a/numpy/core/tests/test_overrides.py b/numpy/core/tests/test_overrides.py index 7f6157a5bf09..b3fa8e827d7b 100644 --- a/numpy/core/tests/test_overrides.py +++ b/numpy/core/tests/test_overrides.py @@ -7,7 +7,8 @@ from numpy.testing import ( assert_, assert_equal, assert_raises, assert_raises_regex) from numpy.core.overrides import ( - get_overloaded_types_and_args, array_function_dispatch) + get_overloaded_types_and_args, array_function_dispatch, + verify_matching_signatures) def _get_overloaded_args(relevant_args): @@ -200,6 +201,24 @@ def __array_function__(self, func, types, args, kwargs): dispatched_one_arg(array) +class TestVerifyMatchingSignatures(object): + + def test_verify_matching_signatures(self): + + verify_matching_signatures(lambda x: 0, lambda x: 0) + verify_matching_signatures(lambda x=None: 0, lambda x=None: 0) + verify_matching_signatures(lambda x=1: 0, lambda x=None: 0) + + with assert_raises(RuntimeError): + verify_matching_signatures(lambda a: 0, lambda b: 0) + with assert_raises(RuntimeError): + verify_matching_signatures(lambda x: 0, lambda x=None: 0) + with assert_raises(RuntimeError): + verify_matching_signatures(lambda x=None: 0, lambda y=None: 0) + with assert_raises(RuntimeError): + verify_matching_signatures(lambda x=1: 0, lambda y=1: 0) + + def _new_duck_type_and_implements(): """Create a duck array type and implements functions.""" HANDLED_FUNCTIONS = {} From 2f1caf2fe941252106f90ea382f1fe16327e5692 Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Sat, 6 Oct 2018 23:04:38 +0200 Subject: [PATCH 2/3] ENH: make signature checking in array_function_dispatch optional --- numpy/core/overrides.py | 5 +++-- numpy/core/tests/test_overrides.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/numpy/core/overrides.py b/numpy/core/overrides.py index 5d1df340d288..9db78985f549 100644 --- a/numpy/core/overrides.py +++ b/numpy/core/overrides.py @@ -135,13 +135,14 @@ def verify_matching_signatures(implementation, dispatcher): 'default argument values') -def array_function_dispatch(dispatcher): +def array_function_dispatch(dispatcher, check_signature=True): """Decorator for adding dispatch with the __array_function__ protocol.""" def decorator(implementation): # TODO: only do this check when the appropriate flag is enabled or for # a dev install. We want this check for testing but don't want to # slow down all numpy imports. - verify_matching_signatures(implementation, dispatcher) + if check_signature: + verify_matching_signatures(implementation, dispatcher) @functools.wraps(implementation) def public_api(*args, **kwargs): diff --git a/numpy/core/tests/test_overrides.py b/numpy/core/tests/test_overrides.py index b3fa8e827d7b..293114f1df64 100644 --- a/numpy/core/tests/test_overrides.py +++ b/numpy/core/tests/test_overrides.py @@ -218,6 +218,18 @@ def test_verify_matching_signatures(self): with assert_raises(RuntimeError): verify_matching_signatures(lambda x=1: 0, lambda y=1: 0) + def test_array_function_dispatch(self): + + with assert_raises(RuntimeError): + @array_function_dispatch(lambda x: (x,)) + def f(y): + pass + + # should not raise + @array_function_dispatch(lambda x: (x,), check_signature=False) + def f(y): + pass + def _new_duck_type_and_implements(): """Create a duck array type and implements functions.""" From 7bd71e515db6622644e21a6e623d9ab5aa5d2720 Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Mon, 8 Oct 2018 10:41:48 -0700 Subject: [PATCH 3/3] Change verify_signature keyword argument to verify --- numpy/core/overrides.py | 4 ++-- numpy/core/tests/test_overrides.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/numpy/core/overrides.py b/numpy/core/overrides.py index 9db78985f549..906292613b2f 100644 --- a/numpy/core/overrides.py +++ b/numpy/core/overrides.py @@ -135,13 +135,13 @@ def verify_matching_signatures(implementation, dispatcher): 'default argument values') -def array_function_dispatch(dispatcher, check_signature=True): +def array_function_dispatch(dispatcher, verify=True): """Decorator for adding dispatch with the __array_function__ protocol.""" def decorator(implementation): # TODO: only do this check when the appropriate flag is enabled or for # a dev install. We want this check for testing but don't want to # slow down all numpy imports. - if check_signature: + if verify: verify_matching_signatures(implementation, dispatcher) @functools.wraps(implementation) diff --git a/numpy/core/tests/test_overrides.py b/numpy/core/tests/test_overrides.py index 293114f1df64..895f221daf0b 100644 --- a/numpy/core/tests/test_overrides.py +++ b/numpy/core/tests/test_overrides.py @@ -226,7 +226,7 @@ def f(y): pass # should not raise - @array_function_dispatch(lambda x: (x,), check_signature=False) + @array_function_dispatch(lambda x: (x,), verify=False) def f(y): pass