From e7a4bb554edb72fc6619d23241d59162d06f249a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 19 Feb 2019 08:30:15 +0200 Subject: bpo-35798: Add test.support.check_syntax_warning(). (#11895) It checks that a SyntaxWarning is raised when compile specified statement, that it is raised only once, that it is converted to a SyntaxError when raised as exception, and that both warning and exception objects have corresponding attributes. --- Doc/library/test.rst | 21 +++++++++++-- Lib/test/support/__init__.py | 29 ++++++++++++++++++ Lib/test/test_grammar.py | 32 +++++++------------- Lib/test/test_string_literals.py | 34 +++------------------- .../Tests/2019-02-16-15-19-31.bpo-35798.JF16MP.rst | 1 + 5 files changed, 62 insertions(+), 55 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2019-02-16-15-19-31.bpo-35798.JF16MP.rst diff --git a/Doc/library/test.rst b/Doc/library/test.rst index 6a10bab..b5057c0 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -936,9 +936,24 @@ The :mod:`test.support` module defines the following functions: Test for syntax errors in *statement* by attempting to compile *statement*. *testcase* is the :mod:`unittest` instance for the test. *errtext* is the - text of the error raised by :exc:`SyntaxError`. If *lineno* is not None, - compares to the line of the :exc:`SyntaxError`. If *offset* is not None, - compares to the offset of the :exc:`SyntaxError`. + regular expression which should match the string representation of the + raised :exc:`SyntaxError`. If *lineno* is not ``None``, compares to + the line of the exception. If *offset* is not ``None``, compares to + the offset of the exception. + + +.. function:: check_syntax_warning(testcase, statement, errtext='', *, lineno=1, offset=None) + + Test for syntax warning in *statement* by attempting to compile *statement*. + Test also that the :exc:`SyntaxWarning` is emitted only once, and that it + will be converted to a :exc:`SyntaxError` when turned into error. + *testcase* is the :mod:`unittest` instance for the test. *errtext* is the + regular expression which should match the string representation of the + emitted :exc:`SyntaxWarning` and raised :exc:`SyntaxError`. If *lineno* + is not ``None``, compares to the line of the warning and exception. + If *offset* is not ``None``, compares to the offset of the exception. + + .. versionadded:: 3.8 .. function:: open_urlresource(url, *args, **kw) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 436f648..9b75a21 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -86,6 +86,7 @@ __all__ = [ # unittest "is_resource_enabled", "requires", "requires_freebsd_version", "requires_linux_version", "requires_mac_ver", "check_syntax_error", + "check_syntax_warning", "TransientResource", "time_out", "socket_peer_reset", "ioerror_peer_reset", "transient_internet", "BasicTestRunner", "run_unittest", "run_doctest", "skip_unless_symlink", "requires_gzip", "requires_bz2", "requires_lzma", @@ -1113,6 +1114,7 @@ def make_bad_fd(): file.close() unlink(TESTFN) + def check_syntax_error(testcase, statement, errtext='', *, lineno=None, offset=None): with testcase.assertRaisesRegex(SyntaxError, errtext) as cm: compile(statement, '', 'exec') @@ -1124,6 +1126,33 @@ def check_syntax_error(testcase, statement, errtext='', *, lineno=None, offset=N if offset is not None: testcase.assertEqual(err.offset, offset) +def check_syntax_warning(testcase, statement, errtext='', *, lineno=1, offset=None): + # Test also that a warning is emitted only once. + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always', SyntaxWarning) + compile(statement, '', 'exec') + testcase.assertEqual(len(warns), 1, warns) + + warn, = warns + testcase.assertTrue(issubclass(warn.category, SyntaxWarning), warn.category) + if errtext: + testcase.assertRegex(str(warn.message), errtext) + testcase.assertEqual(warn.filename, '') + testcase.assertIsNotNone(warn.lineno) + if lineno is not None: + testcase.assertEqual(warn.lineno, lineno) + + # SyntaxWarning should be converted to SyntaxError when raised, + # since the latter contains more information and provides better + # error report. + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('error', SyntaxWarning) + check_syntax_error(testcase, statement, errtext, + lineno=lineno, offset=offset) + # No warnings are leaked when a SyntaxError is raised. + testcase.assertEqual(warns, []) + + def open_urlresource(url, *args, **kw): import urllib.request, urllib.parse diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index 2ee38f0..6d7d554 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -1,7 +1,7 @@ # Python test set -- part 1, grammar. # This just tests whether the parser accepts them all. -from test.support import check_syntax_error +from test.support import check_syntax_error, check_syntax_warning import inspect import unittest import sys @@ -101,7 +101,7 @@ INVALID_UNDERSCORE_LITERALS = [ class TokenTests(unittest.TestCase): - check_syntax_error = check_syntax_error + from test.support import check_syntax_error def test_backslash(self): # Backslash means line continuation: @@ -276,7 +276,7 @@ class CNS: class GrammarTests(unittest.TestCase): - check_syntax_error = check_syntax_error + from test.support import check_syntax_error, check_syntax_warning # single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE # XXX can't test in a script -- this rule is only used when interactive @@ -1109,12 +1109,10 @@ class GrammarTests(unittest.TestCase): else: self.fail("AssertionError not raised by 'assert False'") - with self.assertWarnsRegex(SyntaxWarning, 'assertion is always true'): - compile('assert(x, "msg")', '', 'exec') + self.check_syntax_warning('assert(x, "msg")', + 'assertion is always true') with warnings.catch_warnings(): - warnings.filterwarnings('error', category=SyntaxWarning) - with self.assertRaisesRegex(SyntaxError, 'assertion is always true'): - compile('assert(x, "msg")', '', 'exec') + warnings.simplefilter('error', SyntaxWarning) compile('assert x, "msg"', '', 'exec') @@ -1243,12 +1241,7 @@ class GrammarTests(unittest.TestCase): def test_comparison_is_literal(self): def check(test, msg='"is" with a literal'): - with self.assertWarnsRegex(SyntaxWarning, msg): - compile(test, '', 'exec') - with warnings.catch_warnings(): - warnings.filterwarnings('error', category=SyntaxWarning) - with self.assertRaisesRegex(SyntaxError, msg): - compile(test, '', 'exec') + self.check_syntax_warning(test, msg) check('x is 1') check('x is "thing"') @@ -1257,7 +1250,7 @@ class GrammarTests(unittest.TestCase): check('x is not 1', '"is not" with a literal') with warnings.catch_warnings(): - warnings.filterwarnings('error', category=SyntaxWarning) + warnings.simplefilter('error', SyntaxWarning) compile('x is None', '', 'exec') compile('x is False', '', 'exec') compile('x is True', '', 'exec') @@ -1265,12 +1258,7 @@ class GrammarTests(unittest.TestCase): def test_warn_missed_comma(self): def check(test): - with self.assertWarnsRegex(SyntaxWarning, msg): - compile(test, '', 'exec') - with warnings.catch_warnings(): - warnings.filterwarnings('error', category=SyntaxWarning) - with self.assertRaisesRegex(SyntaxError, msg): - compile(test, '', 'exec') + self.check_syntax_warning(test, msg) msg=r'is not callable; perhaps you missed a comma\?' check('[(1, 2) (3, 4)]') @@ -1342,7 +1330,7 @@ class GrammarTests(unittest.TestCase): check('[[1, 2] [...]]') with warnings.catch_warnings(): - warnings.filterwarnings('error', category=SyntaxWarning) + warnings.simplefilter('error', SyntaxWarning) compile('[(lambda x, y: x) (3, 4)]', '', 'exec') compile('[[1, 2] [i]]', '', 'exec') compile('[[1, 2] [0]]', '', 'exec') diff --git a/Lib/test/test_string_literals.py b/Lib/test/test_string_literals.py index 635ba57..048f40d 100644 --- a/Lib/test/test_string_literals.py +++ b/Lib/test/test_string_literals.py @@ -63,6 +63,8 @@ def byte(i): class TestLiterals(unittest.TestCase): + from test.support import check_syntax_warning + def setUp(self): self.save_path = sys.path[:] self.tmpdir = tempfile.mkdtemp() @@ -112,21 +114,7 @@ class TestLiterals(unittest.TestCase): with self.assertWarns(SyntaxWarning): self.assertEqual(eval(r"'\%c'" % b), '\\' + chr(b)) - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always', category=SyntaxWarning) - eval("'''\n\\z'''") - self.assertEqual(len(w), 1) - self.assertEqual(w[0].filename, '') - self.assertEqual(w[0].lineno, 1) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('error', category=SyntaxWarning) - with self.assertRaises(SyntaxError) as cm: - eval("'''\n\\z'''") - exc = cm.exception - self.assertEqual(w, []) - self.assertEqual(exc.filename, '') - self.assertEqual(exc.lineno, 1) + self.check_syntax_warning("'''\n\\z'''") def test_eval_str_raw(self): self.assertEqual(eval(""" r'x' """), 'x') @@ -161,21 +149,7 @@ class TestLiterals(unittest.TestCase): with self.assertWarns(SyntaxWarning): self.assertEqual(eval(r"b'\%c'" % b), b'\\' + bytes([b])) - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always', category=SyntaxWarning) - eval("b'''\n\\z'''") - self.assertEqual(len(w), 1) - self.assertEqual(w[0].filename, '') - self.assertEqual(w[0].lineno, 1) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('error', category=SyntaxWarning) - with self.assertRaises(SyntaxError) as cm: - eval("b'''\n\\z'''") - exc = cm.exception - self.assertEqual(w, []) - self.assertEqual(exc.filename, '') - self.assertEqual(exc.lineno, 1) + self.check_syntax_warning("b'''\n\\z'''") def test_eval_bytes_raw(self): self.assertEqual(eval(""" br'x' """), b'x') diff --git a/Misc/NEWS.d/next/Tests/2019-02-16-15-19-31.bpo-35798.JF16MP.rst b/Misc/NEWS.d/next/Tests/2019-02-16-15-19-31.bpo-35798.JF16MP.rst new file mode 100644 index 0000000..e320466 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2019-02-16-15-19-31.bpo-35798.JF16MP.rst @@ -0,0 +1 @@ +Added :func:`test.support.check_syntax_warning`. -- cgit v0.12