Skip to content

bpo-35798: Add test.support.check_syntax_warning(). #11895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions Doc/library/test.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
# 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",
Expand Down Expand Up @@ -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, '<test string>', 'exec')
Expand All @@ -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, '<testcase>', '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>')
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

Expand Down
32 changes: 10 additions & 22 deletions Lib/test/test_grammar.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -101,7 +101,7 @@

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:
Expand Down Expand Up @@ -276,7 +276,7 @@ def __getitem__(self, item):

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
Expand Down Expand Up @@ -1109,12 +1109,10 @@ def testAssert2(self):
else:
self.fail("AssertionError not raised by 'assert False'")

with self.assertWarnsRegex(SyntaxWarning, 'assertion is always true'):
compile('assert(x, "msg")', '<testcase>', '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")', '<testcase>', 'exec')
warnings.simplefilter('error', SyntaxWarning)
compile('assert x, "msg"', '<testcase>', 'exec')


Expand Down Expand Up @@ -1243,12 +1241,7 @@ def test_comparison(self):

def test_comparison_is_literal(self):
def check(test, msg='"is" with a literal'):
with self.assertWarnsRegex(SyntaxWarning, msg):
compile(test, '<testcase>', 'exec')
with warnings.catch_warnings():
warnings.filterwarnings('error', category=SyntaxWarning)
with self.assertRaisesRegex(SyntaxError, msg):
compile(test, '<testcase>', 'exec')
self.check_syntax_warning(test, msg)

check('x is 1')
check('x is "thing"')
Expand All @@ -1257,20 +1250,15 @@ def check(test, msg='"is" with a literal'):
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', '<testcase>', 'exec')
compile('x is False', '<testcase>', 'exec')
compile('x is True', '<testcase>', 'exec')
compile('x is ...', '<testcase>', 'exec')

def test_warn_missed_comma(self):
def check(test):
with self.assertWarnsRegex(SyntaxWarning, msg):
compile(test, '<testcase>', 'exec')
with warnings.catch_warnings():
warnings.filterwarnings('error', category=SyntaxWarning)
with self.assertRaisesRegex(SyntaxError, msg):
compile(test, '<testcase>', 'exec')
self.check_syntax_warning(test, msg)

msg=r'is not callable; perhaps you missed a comma\?'
check('[(1, 2) (3, 4)]')
Expand Down Expand Up @@ -1342,7 +1330,7 @@ def check(test):
check('[[1, 2] [...]]')

with warnings.catch_warnings():
warnings.filterwarnings('error', category=SyntaxWarning)
warnings.simplefilter('error', SyntaxWarning)
compile('[(lambda x, y: x) (3, 4)]', '<testcase>', 'exec')
compile('[[1, 2] [i]]', '<testcase>', 'exec')
compile('[[1, 2] [0]]', '<testcase>', 'exec')
Expand Down
34 changes: 4 additions & 30 deletions Lib/test/test_string_literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -112,21 +114,7 @@ def test_eval_str_invalid_escape(self):
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, '<string>')
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, '<string>')
self.assertEqual(exc.lineno, 1)
self.check_syntax_warning("'''\n\\z'''")

def test_eval_str_raw(self):
self.assertEqual(eval(""" r'x' """), 'x')
Expand Down Expand Up @@ -161,21 +149,7 @@ def test_eval_bytes_invalid_escape(self):
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, '<string>')
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, '<string>')
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')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added :func:`test.support.check_syntax_warning`.