diff --git a/Lib/contextlib.py b/Lib/contextlib.py index bde2feb..2bf83fb 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -3,6 +3,7 @@ import sys from collections import deque from functools import wraps +from inspect import signature __all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack"] @@ -196,6 +197,25 @@ class ExitStack(object): Cannot suppress exceptions. """ + # Let's check if the callback accepts `*args` and `**kwds`. + # First we get a signature of it, which may fail with a + # TypeError if the object is not callable or with a ValueError + # if 'signature()' does not support it (builtin) + try: + sig = signature(callback) + except ValueError: + # The callback is a built-in function or type, hence + # we can't do any validation + pass + else: + # Second, we try to bind the passed callback arguments + try: + sig.bind(*args, **kwds) + except TypeError: + msg = ('{!r} callback does not accept the passed ' + 'arguments'.format(callback)) + raise TypeError(msg) from None + def _exit_wrapper(exc_type, exc, tb): callback(*args, **kwds) # We changed the signature, so using @wraps is not appropriate, but diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index e52ed91..25a5b9d 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -585,6 +585,16 @@ class TestExitStack(unittest.TestCase): for i in range(10000): stack.callback(int) + def test_callback_incorrect_arguments(self): + with ExitStack() as stack: + with self.assertRaisesRegexp(TypeError, 'the passed arguments'): + stack.callback((lambda a, b: None), 1, 2, 3) + + def test_callback_not_callable(self): + with ExitStack() as stack: + with self.assertRaisesRegexp(TypeError, 'is not a callable'): + stack.callback(42) + def test_instance_bypass(self): class Example(object): pass cm = Example()