diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index a623f3c57ac..191d5b9fdd6 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -860,10 +860,11 @@ Test cases | ` | | | +-----------------------------------------+-----------------------------+---------------+ - All the assert methods (except :meth:`assertRaises`, - :meth:`assertRaisesRegex`, :meth:`assertWarns`, :meth:`assertWarnsRegex`) - accept a *msg* argument that, if specified, is used as the error message on - failure (see also :data:`longMessage`). + All the assert methods accept a *msg* argument that, if specified, is used + as the error message on failure (see also :data:`longMessage`). + Note that the *msg* keyword argument can be passed to :meth:`assertRaises`, + :meth:`assertRaisesRegex`, :meth:`assertWarns`, :meth:`assertWarnsRegex` + only when they are used as a context manager. .. method:: assertEqual(first, second, msg=None) @@ -957,7 +958,7 @@ Test cases +---------------------------------------------------------+--------------------------------------+------------+ .. method:: assertRaises(exception, callable, *args, **kwds) - assertRaises(exception) + assertRaises(exception, msg=None) Test that an exception is raised when *callable* is called with any positional or keyword arguments that are also passed to @@ -966,12 +967,16 @@ Test cases To catch any of a group of exceptions, a tuple containing the exception classes may be passed as *exception*. - If only the *exception* argument is given, returns a context manager so - that the code under test can be written inline rather than as a function:: + If only the *exception* and possibly the *msg* arguments are given, + return a context manager so that the code under test can be written + inline rather than as a function:: with self.assertRaises(SomeException): do_something() + When used as a context manager, :meth:`assertRaises` accepts the + additional keyword argument *msg*. + The context manager will store the caught exception object in its :attr:`exception` attribute. This can be useful if the intention is to perform additional checks on the exception raised:: @@ -988,9 +993,12 @@ Test cases .. versionchanged:: 3.2 Added the :attr:`exception` attribute. + .. versionchanged:: 3.3 + Added the *msg* keyword argument when used as a context manager. + .. method:: assertRaisesRegex(exception, regex, callable, *args, **kwds) - assertRaisesRegex(exception, regex) + assertRaisesRegex(exception, regex, msg=None) Like :meth:`assertRaises` but also tests that *regex* matches on the string representation of the raised exception. *regex* may be @@ -1007,12 +1015,16 @@ Test cases .. versionadded:: 3.1 under the name ``assertRaisesRegexp``. + .. versionchanged:: 3.2 Renamed to :meth:`assertRaisesRegex`. + .. versionchanged:: 3.3 + Added the *msg* keyword argument when used as a context manager. + .. method:: assertWarns(warning, callable, *args, **kwds) - assertWarns(warning) + assertWarns(warning, msg=None) Test that a warning is triggered when *callable* is called with any positional or keyword arguments that are also passed to @@ -1021,12 +1033,16 @@ Test cases To catch any of a group of warnings, a tuple containing the warning classes may be passed as *warnings*. - If only the *warning* argument is given, returns a context manager so - that the code under test can be written inline rather than as a function:: + If only the *warning* and possibly the *msg* arguments are given, + returns a context manager so that the code under test can be written + inline rather than as a function:: with self.assertWarns(SomeWarning): do_something() + When used as a context manager, :meth:`assertRaises` accepts the + additional keyword argument *msg*. + The context manager will store the caught warning object in its :attr:`warning` attribute, and the source line which triggered the warnings in the :attr:`filename` and :attr:`lineno` attributes. @@ -1044,9 +1060,12 @@ Test cases .. versionadded:: 3.2 + .. versionchanged:: 3.3 + Added the *msg* keyword argument when used as a context manager. + .. method:: assertWarnsRegex(warning, regex, callable, *args, **kwds) - assertWarnsRegex(warning, regex) + assertWarnsRegex(warning, regex, msg=None) Like :meth:`assertWarns` but also tests that *regex* matches on the message of the triggered warning. *regex* may be a regular expression @@ -1064,6 +1083,8 @@ Test cases .. versionadded:: 3.2 + .. versionchanged:: 3.3 + Added the *msg* keyword argument when used as a context manager. There are also other methods used to perform more specific checks, such as: diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 881de6ffa5b..5b7c29dd268 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -104,9 +104,9 @@ def expectedFailure(func): class _AssertRaisesBaseContext(object): def __init__(self, expected, test_case, callable_obj=None, - expected_regex=None): + expected_regex=None): self.expected = expected - self.failureException = test_case.failureException + self.test_case = test_case if callable_obj is not None: try: self.obj_name = callable_obj.__name__ @@ -117,6 +117,24 @@ class _AssertRaisesBaseContext(object): if isinstance(expected_regex, (bytes, str)): expected_regex = re.compile(expected_regex) self.expected_regex = expected_regex + self.msg = None + + def _raiseFailure(self, standardMsg): + msg = self.test_case._formatMessage(self.msg, standardMsg) + raise self.test_case.failureException(msg) + + def handle(self, name, callable_obj, args, kwargs): + """ + If callable_obj is None, assertRaises/Warns is being used as a + context manager, so check for a 'msg' kwarg and return self. + If callable_obj is not None, call it passing args and kwargs. + """ + if callable_obj is None: + self.msg = kwargs.pop('msg', None) + return self + with self: + callable_obj(*args, **kwargs) + class _AssertRaisesContext(_AssertRaisesBaseContext): @@ -132,11 +150,10 @@ class _AssertRaisesContext(_AssertRaisesBaseContext): except AttributeError: exc_name = str(self.expected) if self.obj_name: - raise self.failureException("{0} not raised by {1}" - .format(exc_name, self.obj_name)) + self._raiseFailure("{} not raised by {}".format(exc_name, + self.obj_name)) else: - raise self.failureException("{0} not raised" - .format(exc_name)) + self._raiseFailure("{} not raised".format(exc_name)) if not issubclass(exc_type, self.expected): # let unexpected exceptions pass through return False @@ -147,8 +164,8 @@ class _AssertRaisesContext(_AssertRaisesBaseContext): expected_regex = self.expected_regex if not expected_regex.search(str(exc_value)): - raise self.failureException('"%s" does not match "%s"' % - (expected_regex.pattern, str(exc_value))) + self._raiseFailure('"{}" does not match "{}"'.format( + expected_regex.pattern, str(exc_value))) return True @@ -192,14 +209,13 @@ class _AssertWarnsContext(_AssertRaisesBaseContext): return # Now we simply try to choose a helpful failure message if first_matching is not None: - raise self.failureException('"%s" does not match "%s"' % - (self.expected_regex.pattern, str(first_matching))) + self._raiseFailure('"{}" does not match "{}"'.format( + self.expected_regex.pattern, str(first_matching))) if self.obj_name: - raise self.failureException("{0} not triggered by {1}" - .format(exc_name, self.obj_name)) + self._raiseFailure("{} not triggered by {}".format(exc_name, + self.obj_name)) else: - raise self.failureException("{0} not triggered" - .format(exc_name)) + self._raiseFailure("{} not triggered".format(exc_name)) class _TypeEqualityDict(object): @@ -547,7 +563,6 @@ class TestCase(object): except UnicodeDecodeError: return '%s : %s' % (safe_repr(standardMsg), safe_repr(msg)) - def assertRaises(self, excClass, callableObj=None, *args, **kwargs): """Fail unless an exception of class excClass is thrown by callableObj when invoked with arguments args and keyword @@ -562,6 +577,9 @@ class TestCase(object): with self.assertRaises(SomeException): do_something() + An optional keyword argument 'msg' can be provided when assertRaises + is used as a context object. + The context manager keeps a reference to the exception as the 'exception' attribute. This allows you to inspect the exception after the assertion:: @@ -572,25 +590,25 @@ class TestCase(object): self.assertEqual(the_exception.error_code, 3) """ context = _AssertRaisesContext(excClass, self, callableObj) - if callableObj is None: - return context - with context: - callableObj(*args, **kwargs) + return context.handle('assertRaises', callableObj, args, kwargs) def assertWarns(self, expected_warning, callable_obj=None, *args, **kwargs): """Fail unless a warning of class warnClass is triggered - by callableObj when invoked with arguments args and keyword + by callable_obj when invoked with arguments args and keyword arguments kwargs. If a different type of warning is triggered, it will not be handled: depending on the other warning filtering rules in effect, it might be silenced, printed out, or raised as an exception. - If called with callableObj omitted or None, will return a + If called with callable_obj omitted or None, will return a context object used like this:: with self.assertWarns(SomeWarning): do_something() + An optional keyword argument 'msg' can be provided when assertWarns + is used as a context object. + The context manager keeps a reference to the first matching warning as the 'warning' attribute; similarly, the 'filename' and 'lineno' attributes give you information about the line @@ -603,10 +621,7 @@ class TestCase(object): self.assertEqual(the_warning.some_attribute, 147) """ context = _AssertWarnsContext(expected_warning, self, callable_obj) - if callable_obj is None: - return context - with context: - callable_obj(*args, **kwargs) + return context.handle('assertWarns', callable_obj, args, kwargs) def _getAssertEqualityFunc(self, first, second): """Get a detailed comparison function for the types of the two args. @@ -1083,15 +1098,15 @@ class TestCase(object): expected_regex: Regex (re pattern object or string) expected to be found in error message. callable_obj: Function to be called. + msg: Optional message used in case of failure. Can only be used + when assertRaisesRegex is used as a context manager. args: Extra args. kwargs: Extra kwargs. """ context = _AssertRaisesContext(expected_exception, self, callable_obj, expected_regex) - if callable_obj is None: - return context - with context: - callable_obj(*args, **kwargs) + + return context.handle('assertRaisesRegex', callable_obj, args, kwargs) def assertWarnsRegex(self, expected_warning, expected_regex, callable_obj=None, *args, **kwargs): @@ -1105,15 +1120,14 @@ class TestCase(object): expected_regex: Regex (re pattern object or string) expected to be found in error message. callable_obj: Function to be called. + msg: Optional message used in case of failure. Can only be used + when assertWarnsRegex is used as a context manager. args: Extra args. kwargs: Extra kwargs. """ context = _AssertWarnsContext(expected_warning, self, callable_obj, expected_regex) - if callable_obj is None: - return context - with context: - callable_obj(*args, **kwargs) + return context.handle('assertWarnsRegex', callable_obj, args, kwargs) def assertRegex(self, text, expected_regex, msg=None): """Fail the test unless the text matches the regular expression.""" diff --git a/Lib/unittest/test/test_assertions.py b/Lib/unittest/test/test_assertions.py index a1d20ebf2da..d43fe5a601b 100644 --- a/Lib/unittest/test/test_assertions.py +++ b/Lib/unittest/test/test_assertions.py @@ -1,6 +1,7 @@ import datetime import warnings import unittest +from itertools import product class Test_Assertions(unittest.TestCase): @@ -145,6 +146,14 @@ class TestLongMessage(unittest.TestCase): self.testableTrue._formatMessage(one, '\uFFFD') def assertMessages(self, methodName, args, errors): + """ + Check that methodName(*args) raises the correct error messages. + errors should be a list of 4 regex that match the error when: + 1) longMessage = False and no msg passed; + 2) longMessage = False and msg passed; + 3) longMessage = True and no msg passed; + 4) longMessage = True and msg passed; + """ def getMethod(i): useTestableFalse = i < 2 if useTestableFalse: @@ -284,3 +293,67 @@ class TestLongMessage(unittest.TestCase): ["^unexpectedly identical: None$", "^oops$", "^unexpectedly identical: None$", "^unexpectedly identical: None : oops$"]) + + + def assertMessagesCM(self, methodName, args, func, errors): + """ + Check that the correct error messages are raised while executing: + with method(*args): + func() + *errors* should be a list of 4 regex that match the error when: + 1) longMessage = False and no msg passed; + 2) longMessage = False and msg passed; + 3) longMessage = True and no msg passed; + 4) longMessage = True and msg passed; + """ + p = product((self.testableFalse, self.testableTrue), + ({}, {"msg": "oops"})) + for (cls, kwargs), err in zip(p, errors): + method = getattr(cls, methodName) + with self.assertRaisesRegex(cls.failureException, err): + with method(*args, **kwargs) as cm: + func() + + def testAssertRaises(self): + self.assertMessagesCM('assertRaises', (TypeError,), lambda: None, + ['^TypeError not raised$', '^oops$', + '^TypeError not raised$', + '^TypeError not raised : oops$']) + + def testAssertRaisesRegex(self): + # test error not raised + self.assertMessagesCM('assertRaisesRegex', (TypeError, 'unused regex'), + lambda: None, + ['^TypeError not raised$', '^oops$', + '^TypeError not raised$', + '^TypeError not raised : oops$']) + # test error raised but with wrong message + def raise_wrong_message(): + raise TypeError('foo') + self.assertMessagesCM('assertRaisesRegex', (TypeError, 'regex'), + raise_wrong_message, + ['^"regex" does not match "foo"$', '^oops$', + '^"regex" does not match "foo"$', + '^"regex" does not match "foo" : oops$']) + + def testAssertWarns(self): + self.assertMessagesCM('assertWarns', (UserWarning,), lambda: None, + ['^UserWarning not triggered$', '^oops$', + '^UserWarning not triggered$', + '^UserWarning not triggered : oops$']) + + def testAssertWarnsRegex(self): + # test error not raised + self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'unused regex'), + lambda: None, + ['^UserWarning not triggered$', '^oops$', + '^UserWarning not triggered$', + '^UserWarning not triggered : oops$']) + # test warning raised but with wrong message + def raise_wrong_message(): + warnings.warn('foo') + self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'), + raise_wrong_message, + ['^"regex" does not match "foo"$', '^oops$', + '^"regex" does not match "foo"$', + '^"regex" does not match "foo" : oops$']) diff --git a/Misc/ACKS b/Misc/ACKS index 3516b1c3ae8..d296ea1289e 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -269,6 +269,7 @@ Carey Evans Tim Everett Paul Everitt David Everly +Winston Ewert Greg Ewing Martijn Faassen Clovis Fabricio diff --git a/Misc/NEWS b/Misc/NEWS index 5cf9ac60757..b0d683666f0 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -140,6 +140,10 @@ Core and Builtins Library ------- +- Issue #10775: assertRaises, assertRaisesRegex, assertWarns, and + assertWarnsRegex now accept a keyword argument 'msg' when used as context + managers. Initial patch by Winston Ewert. + - Issue #10684: shutil.move used to delete a folder on case insensitive filesystems when the source and destination name where the same except for the case.