diff options
author | Cheryl Sabella <cheryl.sabella@gmail.com> | 2018-02-22 03:48:36 (GMT) |
---|---|---|
committer | Terry Jan Reedy <tjreedy@udel.edu> | 2018-02-22 03:48:36 (GMT) |
commit | c84cf6c03fce1fb73bfaf91d7909f1c2708f14a2 (patch) | |
tree | 36fc42ddbf22b3c33d7a2451a5c6e28a8e6c449e | |
parent | ba518804bf4c1ea01df5e622b333d3116cbaa3bd (diff) | |
download | cpython-c84cf6c03fce1fb73bfaf91d7909f1c2708f14a2.zip cpython-c84cf6c03fce1fb73bfaf91d7909f1c2708f14a2.tar.gz cpython-c84cf6c03fce1fb73bfaf91d7909f1c2708f14a2.tar.bz2 |
bpo-32874: IDLE: add tests for pyparse (GH-5755)
There are no code changes other than comments and docstrings.
-rw-r--r-- | Lib/idlelib/idle_test/test_pyparse.py | 523 | ||||
-rw-r--r-- | Lib/idlelib/pyparse.py | 161 | ||||
-rw-r--r-- | Misc/NEWS.d/next/IDLE/2018-02-19-10-56-41.bpo-32874.6pZ9Gv.rst | 1 |
3 files changed, 619 insertions, 66 deletions
diff --git a/Lib/idlelib/idle_test/test_pyparse.py b/Lib/idlelib/idle_test/test_pyparse.py new file mode 100644 index 0000000..b84e9f8 --- /dev/null +++ b/Lib/idlelib/idle_test/test_pyparse.py @@ -0,0 +1,523 @@ +"""Unittest for idlelib.pyparse.py.""" + +from collections import namedtuple +import unittest +from idlelib import pyparse + + +class StringTranslatePseudoMappingTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + whitespace_chars = ' \t\n\r' + cls.preserve_dict = {ord(c): ord(c) for c in whitespace_chars} + cls.default = ord('x') + cls.mapping = pyparse.StringTranslatePseudoMapping( + cls.preserve_dict, default_value=ord('x')) + + @classmethod + def tearDownClass(cls): + del cls.preserve_dict, cls.default, cls.mapping + + def test__init__(self): + m = self.mapping + self.assertEqual(m._non_defaults, self.preserve_dict) + self.assertEqual(m._default_value, self.default) + + def test__get_item__(self): + self.assertEqual(self.mapping[ord('\t')], ord('\t')) + self.assertEqual(self.mapping[ord('a')], self.default) + + def test__len__(self): + self.assertEqual(len(self.mapping), len(self.preserve_dict)) + + def test__iter__(self): + count = 0 + for key, value in self.mapping.items(): + self.assertIn(key, self.preserve_dict) + count += 1 + self.assertEqual(count, len(self.mapping)) + + def test_get(self): + self.assertEqual(self.mapping.get(ord('\t')), ord('\t')) + self.assertEqual(self.mapping.get('a'), self.default) + # Default is a parameter, but it isn't used. + self.assertEqual(self.mapping.get('a', default=500), self.default) + + +class PyParseTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.parser = pyparse.Parser(indentwidth=4, tabwidth=4) + + @classmethod + def tearDownClass(cls): + del cls.parser + + def test_init(self): + self.assertEqual(self.parser.indentwidth, 4) + self.assertEqual(self.parser.tabwidth, 4) + + def test_set_str(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + + # Not empty and doesn't end with newline. + with self.assertRaises(AssertionError): + setstr('a') + + tests = ('', + 'a\n') + + for string in tests: + with self.subTest(string=string): + setstr(string) + eq(p.str, string) + eq(p.study_level, 0) + + def test_find_good_parse_start(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + start = p.find_good_parse_start + + # Split def across lines. + setstr('"""This is a module docstring"""\n' + 'class C():\n' + ' def __init__(self, a,\n' + ' b=True):\n' + ' pass\n' + ) + + # No value sent for is_char_in_string(). + self.assertIsNone(start()) + + # Make text look like a string. This returns pos as the start + # position, but it's set to None. + self.assertIsNone(start(is_char_in_string=lambda index: True)) + + # Make all text look like it's not in a string. This means that it + # found a good start position. + eq(start(is_char_in_string=lambda index: False), 44) + + # If the beginning of the def line is not in a string, then it + # returns that as the index. + eq(start(is_char_in_string=lambda index: index > 44), 44) + # If the beginning of the def line is in a string, then it + # looks for a previous index. + eq(start(is_char_in_string=lambda index: index >= 44), 33) + # If everything before the 'def' is in a string, then returns None. + # The non-continuation def line returns 44 (see below). + eq(start(is_char_in_string=lambda index: index < 44), None) + + # Code without extra line break in def line - mostly returns the same + # values. + setstr('"""This is a module docstring"""\n' + 'class C():\n' + ' def __init__(self, a, b=True):\n' + ' pass\n' + ) + eq(start(is_char_in_string=lambda index: False), 44) + eq(start(is_char_in_string=lambda index: index > 44), 44) + eq(start(is_char_in_string=lambda index: index >= 44), 33) + # When the def line isn't split, this returns which doesn't match the + # split line test. + eq(start(is_char_in_string=lambda index: index < 44), 44) + + def test_set_lo(self): + code = ( + '"""This is a module docstring"""\n' + 'class C():\n' + ' def __init__(self, a,\n' + ' b=True):\n' + ' pass\n' + ) + p = self.parser + p.set_str(code) + + # Previous character is not a newline. + with self.assertRaises(AssertionError): + p.set_lo(5) + + # A value of 0 doesn't change self.str. + p.set_lo(0) + self.assertEqual(p.str, code) + + # An index that is preceded by a newline. + p.set_lo(44) + self.assertEqual(p.str, code[44:]) + + def test_tran(self): + self.assertEqual('\t a([{b}])b"c\'d\n'.translate(self.parser._tran), + 'xxx(((x)))x"x\'x\n') + + def test_study1(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + study = p._study1 + + (NONE, BACKSLASH, FIRST, NEXT, BRACKET) = range(5) + TestInfo = namedtuple('TestInfo', ['string', 'goodlines', + 'continuation']) + tests = ( + TestInfo('', [0], NONE), + # Docstrings. + TestInfo('"""This is a complete docstring."""\n', [0, 1], NONE), + TestInfo("'''This is a complete docstring.'''\n", [0, 1], NONE), + TestInfo('"""This is a continued docstring.\n', [0, 1], FIRST), + TestInfo("'''This is a continued docstring.\n", [0, 1], FIRST), + TestInfo('"""Closing quote does not match."\n', [0, 1], FIRST), + TestInfo('"""Bracket in docstring [\n', [0, 1], FIRST), + TestInfo("'''Incomplete two line docstring.\n\n", [0, 2], NEXT), + # Single-quoted strings. + TestInfo('"This is a complete string."\n', [0, 1], NONE), + TestInfo('"This is an incomplete string.\n', [0, 1], NONE), + TestInfo("'This is more incomplete.\n\n", [0, 1, 2], NONE), + # Comment (backslash does not continue comments). + TestInfo('# Comment\\\n', [0, 1], NONE), + # Brackets. + TestInfo('("""Complete string in bracket"""\n', [0, 1], BRACKET), + TestInfo('("""Open string in bracket\n', [0, 1], FIRST), + TestInfo('a = (1 + 2) - 5 *\\\n', [0, 1], BACKSLASH), # No bracket. + TestInfo('\n def function1(self, a,\n b):\n', + [0, 1, 3], NONE), + TestInfo('\n def function1(self, a,\\\n', [0, 1, 2], BRACKET), + TestInfo('\n def function1(self, a,\n', [0, 1, 2], BRACKET), + TestInfo('())\n', [0, 1], NONE), # Extra closer. + TestInfo(')(\n', [0, 1], BRACKET), # Extra closer. + # For the mismatched example, it doesn't look like contination. + TestInfo('{)(]\n', [0, 1], NONE), # Mismatched. + ) + + for test in tests: + with self.subTest(string=test.string): + setstr(test.string) # resets study_level + study() + eq(p.study_level, 1) + eq(p.goodlines, test.goodlines) + eq(p.continuation, test.continuation) + + # Called again, just returns without reprocessing. + self.assertIsNone(study()) + + def test_get_continuation_type(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + gettype = p.get_continuation_type + + (NONE, BACKSLASH, FIRST, NEXT, BRACKET) = range(5) + TestInfo = namedtuple('TestInfo', ['string', 'continuation']) + tests = ( + TestInfo('', NONE), + TestInfo('"""This is a continuation docstring.\n', FIRST), + TestInfo("'''This is a multiline-continued docstring.\n\n", NEXT), + TestInfo('a = (1 + 2) - 5 *\\\n', BACKSLASH), + TestInfo('\n def function1(self, a,\\\n', BRACKET) + ) + + for test in tests: + with self.subTest(string=test.string): + setstr(test.string) + eq(gettype(), test.continuation) + + def test_study2(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + study = p._study2 + + TestInfo = namedtuple('TestInfo', ['string', 'start', 'end', 'lastch', + 'openbracket', 'bracketing']) + tests = ( + TestInfo('', 0, 0, '', None, ((0, 0),)), + TestInfo("'''This is a multiline continutation docstring.\n\n", + 0, 49, "'", None, ((0, 0), (0, 1), (49, 0))), + TestInfo(' # Comment\\\n', + 0, 12, '', None, ((0, 0), (1, 1), (12, 0))), + # A comment without a space is a special case + TestInfo(' #Comment\\\n', + 0, 0, '', None, ((0, 0),)), + # Backslash continuation. + TestInfo('a = (1 + 2) - 5 *\\\n', + 0, 19, '*', None, ((0, 0), (4, 1), (11, 0))), + # Bracket continuation with close. + TestInfo('\n def function1(self, a,\n b):\n', + 1, 48, ':', None, ((1, 0), (17, 1), (46, 0))), + # Bracket continuation with unneeded backslash. + TestInfo('\n def function1(self, a,\\\n', + 1, 28, ',', 17, ((1, 0), (17, 1))), + # Bracket continuation. + TestInfo('\n def function1(self, a,\n', + 1, 27, ',', 17, ((1, 0), (17, 1))), + # Bracket continuation with comment at end of line with text. + TestInfo('\n def function1(self, a, # End of line comment.\n', + 1, 51, ',', 17, ((1, 0), (17, 1), (28, 2), (51, 1))), + # Multi-line statement with comment line in between code lines. + TestInfo(' a = ["first item",\n # Comment line\n "next item",\n', + 0, 55, ',', 6, ((0, 0), (6, 1), (7, 2), (19, 1), + (23, 2), (38, 1), (42, 2), (53, 1))), + TestInfo('())\n', + 0, 4, ')', None, ((0, 0), (0, 1), (2, 0), (3, 0))), + TestInfo(')(\n', 0, 3, '(', 1, ((0, 0), (1, 0), (1, 1))), + # Wrong closers still decrement stack level. + TestInfo('{)(]\n', + 0, 5, ']', None, ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))), + # Character after backslash. + TestInfo(':\\a\n', 0, 4, '\\a', None, ((0, 0),)), + TestInfo('\n', 0, 0, '', None, ((0, 0),)), + ) + + for test in tests: + # There is a bug where this is carried forward from last item. + p.lastopenbracketpos = None + with self.subTest(string=test.string): + setstr(test.string) + study() + eq(p.study_level, 2) + eq(p.stmt_start, test.start) + eq(p.stmt_end, test.end) + eq(p.lastch, test.lastch) + eq(p.lastopenbracketpos, test.openbracket) + eq(p.stmt_bracketing, test.bracketing) + + # Called again, just returns without reprocessing. + self.assertIsNone(study()) + + def test_get_num_lines_in_stmt(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + getlines = p.get_num_lines_in_stmt + + TestInfo = namedtuple('TestInfo', ['string', 'lines']) + tests = ( + TestInfo('[x for x in a]\n', 1), # Closed on one line. + TestInfo('[x\nfor x in a\n', 2), # Not closed. + TestInfo('[x\\\nfor x in a\\\n', 2), # "", uneeded backslashes. + TestInfo('[x\nfor x in a\n]\n', 3), # Closed on multi-line. + TestInfo('\n"""Docstring comment L1"""\nL2\nL3\nL4\n', 1), + TestInfo('\n"""Docstring comment L1\nL2"""\nL3\nL4\n', 1), + TestInfo('\n"""Docstring comment L1\\\nL2\\\nL3\\\nL4\\\n', 4), + TestInfo('\n\n"""Docstring comment L1\\\nL2\\\nL3\\\nL4\\\n"""\n', 5) + ) + + # Blank string doesn't have enough elements in goodlines. + setstr('') + with self.assertRaises(IndexError): + getlines() + + for test in tests: + with self.subTest(string=test.string): + setstr(test.string) + eq(getlines(), test.lines) + + def test_compute_bracket_indent(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + indent = p.compute_bracket_indent + + TestInfo = namedtuple('TestInfo', ['string', 'spaces']) + tests = ( + TestInfo('def function1(self, a,\n', 14), + # Characters after bracket. + TestInfo('\n def function1(self, a,\n', 18), + TestInfo('\n\tdef function1(self, a,\n', 18), + # No characters after bracket. + TestInfo('\n def function1(\n', 8), + TestInfo('\n\tdef function1(\n', 8), + TestInfo('\n def function1( \n', 8), # Ignore extra spaces. + TestInfo('[\n"first item",\n # Comment line\n "next item",\n', 0), + TestInfo('[\n "first item",\n # Comment line\n "next item",\n', 2), + TestInfo('["first item",\n # Comment line\n "next item",\n', 1), + TestInfo('(\n', 4), + TestInfo('(a\n', 1), + ) + + # Must be C_BRACKET continuation type. + setstr('def function1(self, a, b):\n') + with self.assertRaises(AssertionError): + indent() + + for test in tests: + setstr(test.string) + eq(indent(), test.spaces) + + def test_compute_backslash_indent(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + indent = p.compute_backslash_indent + + # Must be C_BACKSLASH continuation type. + errors = (('def function1(self, a, b\\\n'), # Bracket. + (' """ (\\\n'), # Docstring. + ('a = #\\\n'), # Inline comment. + ) + for string in errors: + with self.subTest(string=string): + setstr(string) + with self.assertRaises(AssertionError): + indent() + + TestInfo = namedtuple('TestInfo', ('string', 'spaces')) + tests = (TestInfo('a = (1 + 2) - 5 *\\\n', 4), + TestInfo('a = 1 + 2 - 5 *\\\n', 4), + TestInfo(' a = 1 + 2 - 5 *\\\n', 8), + TestInfo(' a = "spam"\\\n', 6), + TestInfo(' a = \\\n"a"\\\n', 4), + TestInfo(' a = #\\\n"a"\\\n', 5), + TestInfo('a == \\\n', 2), + TestInfo('a != \\\n', 2), + # Difference between containing = and those not. + TestInfo('\\\n', 2), + TestInfo(' \\\n', 6), + TestInfo('\t\\\n', 6), + TestInfo('a\\\n', 3), + TestInfo('{}\\\n', 4), + TestInfo('(1 + 2) - 5 *\\\n', 3), + ) + for test in tests: + with self.subTest(string=test.string): + setstr(test.string) + eq(indent(), test.spaces) + + def test_get_base_indent_string(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + baseindent = p.get_base_indent_string + + TestInfo = namedtuple('TestInfo', ['string', 'indent']) + tests = (TestInfo('', ''), + TestInfo('def a():\n', ''), + TestInfo('\tdef a():\n', '\t'), + TestInfo(' def a():\n', ' '), + TestInfo(' def a(\n', ' '), + TestInfo('\t\n def a(\n', ' '), + TestInfo('\t\n # Comment.\n', ' '), + ) + + for test in tests: + with self.subTest(string=test.string): + setstr(test.string) + eq(baseindent(), test.indent) + + def test_is_block_opener(self): + yes = self.assertTrue + no = self.assertFalse + p = self.parser + setstr = p.set_str + opener = p.is_block_opener + + TestInfo = namedtuple('TestInfo', ['string', 'assert_']) + tests = ( + TestInfo('def a():\n', yes), + TestInfo('\n def function1(self, a,\n b):\n', yes), + TestInfo(':\n', yes), + TestInfo('a:\n', yes), + TestInfo('):\n', yes), + TestInfo('(:\n', yes), + TestInfo('":\n', no), + TestInfo('\n def function1(self, a,\n', no), + TestInfo('def function1(self, a):\n pass\n', no), + TestInfo('# A comment:\n', no), + TestInfo('"""A docstring:\n', no), + TestInfo('"""A docstring:\n', no), + ) + + for test in tests: + with self.subTest(string=test.string): + setstr(test.string) + test.assert_(opener()) + + def test_is_block_closer(self): + yes = self.assertTrue + no = self.assertFalse + p = self.parser + setstr = p.set_str + closer = p.is_block_closer + + TestInfo = namedtuple('TestInfo', ['string', 'assert_']) + tests = ( + TestInfo('return\n', yes), + TestInfo('\tbreak\n', yes), + TestInfo(' continue\n', yes), + TestInfo(' raise\n', yes), + TestInfo('pass \n', yes), + TestInfo('pass\t\n', yes), + TestInfo('return #\n', yes), + TestInfo('raised\n', no), + TestInfo('returning\n', no), + TestInfo('# return\n', no), + TestInfo('"""break\n', no), + TestInfo('"continue\n', no), + TestInfo('def function1(self, a):\n pass\n', yes), + ) + + for test in tests: + with self.subTest(string=test.string): + setstr(test.string) + test.assert_(closer()) + + def test_get_last_open_bracket_pos(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + openbracket = p.get_last_open_bracket_pos + + TestInfo = namedtuple('TestInfo', ['string', 'position']) + tests = ( + TestInfo('', None), + TestInfo('a\n', None), + TestInfo('# (\n', None), + TestInfo('""" (\n', None), + TestInfo('a = (1 + 2) - 5 *\\\n', None), + TestInfo('\n def function1(self, a,\n', 17), + TestInfo('\n def function1(self, a, # End of line comment.\n', 17), + TestInfo('{)(]\n', None), + TestInfo('(((((((((()))))))\n', 2), + TestInfo('(((((((((())\n)))\n))\n', 2), + ) + + for test in tests: + # There is a bug where the value is carried forward from last item. + p.lastopenbracketpos = None + with self.subTest(string=test.string): + setstr(test.string) + eq(openbracket(), test.position) + + def test_get_last_stmt_bracketing(self): + eq = self.assertEqual + p = self.parser + setstr = p.set_str + bracketing = p.get_last_stmt_bracketing + + TestInfo = namedtuple('TestInfo', ['string', 'bracket']) + tests = ( + TestInfo('', ((0, 0),)), + TestInfo('a\n', ((0, 0),)), + TestInfo('()()\n', ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))), + TestInfo('(\n)()\n', ((0, 0), (0, 1), (3, 0), (3, 1), (5, 0))), + TestInfo('()\n()\n', ((3, 0), (3, 1), (5, 0))), + TestInfo('()(\n)\n', ((0, 0), (0, 1), (2, 0), (2, 1), (5, 0))), + TestInfo('(())\n', ((0, 0), (0, 1), (1, 2), (3, 1), (4, 0))), + TestInfo('(\n())\n', ((0, 0), (0, 1), (2, 2), (4, 1), (5, 0))), + # Same as matched test. + TestInfo('{)(]\n', ((0, 0), (0, 1), (2, 0), (2, 1), (4, 0))), + TestInfo('(((())\n', + ((0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (5, 3), (6, 2))), + ) + + for test in tests: + with self.subTest(string=test.string): + setstr(test.string) + eq(bracketing(), test.bracket) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/pyparse.py b/Lib/idlelib/pyparse.py index 536b2d7..72bd9e0 100644 --- a/Lib/idlelib/pyparse.py +++ b/Lib/idlelib/pyparse.py @@ -1,8 +1,20 @@ +"""Define partial Python code Parser used by editor and hyperparser. + +Instances of StringTranslatePseudoMapping are used with str.translate. + +The following bound search and match functions are defined: +_synchre - start of popular statement; +_junkre - whitespace or comment line; +_match_stringre: string, possibly without closer; +_itemre - line that may have bracket structure start; +_closere - line that must be followed by dedent. +_chew_ordinaryre - non-special characters. +""" from collections.abc import Mapping import re import sys -# Reason last stmt is continued (or C_NONE if it's not). +# Reason last statement is continued (or C_NONE if it's not). (C_NONE, C_BACKSLASH, C_STRING_FIRST_LINE, C_STRING_NEXT_LINES, C_BRACKET) = range(5) @@ -10,7 +22,7 @@ if 0: # for throwaway debugging output def dump(*stuff): sys.__stdout__.write(" ".join(map(str, stuff)) + "\n") -# Find what looks like the start of a popular stmt. +# Find what looks like the start of a popular statement. _synchre = re.compile(r""" ^ @@ -70,7 +82,7 @@ _itemre = re.compile(r""" [^\s#\\] # if we match, m.end()-1 is the interesting char """, re.VERBOSE).match -# Match start of stmts that should be followed by a dedent. +# Match start of statements that should be followed by a dedent. _closere = re.compile(r""" \s* @@ -146,19 +158,20 @@ class Parser: self.str = s self.study_level = 0 - # Return index of a good place to begin parsing, as close to the - # end of the string as possible. This will be the start of some - # popular stmt like "if" or "def". Return None if none found: - # the caller should pass more prior context then, if possible, or - # if not (the entire program text up until the point of interest - # has already been tried) pass 0 to set_lo. - # - # This will be reliable iff given a reliable is_char_in_string - # function, meaning that when it says "no", it's absolutely - # guaranteed that the char is not in a string. - def find_good_parse_start(self, is_char_in_string=None, _synchre=_synchre): + """ + Return index of a good place to begin parsing, as close to the + end of the string as possible. This will be the start of some + popular stmt like "if" or "def". Return None if none found: + the caller should pass more prior context then, if possible, or + if not (the entire program text up until the point of interest + has already been tried) pass 0 to set_lo(). + + This will be reliable iff given a reliable is_char_in_string() + function, meaning that when it says "no", it's absolutely + guaranteed that the char is not in a string. + """ str, pos = self.str, None if not is_char_in_string: @@ -173,7 +186,7 @@ class Parser: i = str.rfind(":\n", 0, limit) if i < 0: break - i = str.rfind('\n', 0, i) + 1 # start of colon line + i = str.rfind('\n', 0, i) + 1 # start of colon line (-1+1=0) m = _synchre(str, i, limit) if m and not is_char_in_string(m.start()): pos = m.start() @@ -206,10 +219,11 @@ class Parser: break return pos - # Throw away the start of the string. Intended to be called with - # find_good_parse_start's result. - def set_lo(self, lo): + """ Throw away the start of the string. + + Intended to be called with the result of find_good_parse_start(). + """ assert lo == 0 or self.str[lo-1] == '\n' if lo > 0: self.str = self.str[lo:] @@ -224,11 +238,13 @@ class Parser: _tran.update((ord(c), ord(c)) for c in "\"'\\\n#") _tran = StringTranslatePseudoMapping(_tran, default_value=ord('x')) - # As quickly as humanly possible <wink>, find the line numbers (0- - # based) of the non-continuation lines. - # Creates self.{goodlines, continuation}. - def _study1(self): + """Find the line numbers of non-continuation lines. + + As quickly as humanly possible <wink>, find the line numbers (0- + based) of the non-continuation lines. + Creates self.{goodlines, continuation}. + """ if self.study_level >= 1: return self.study_level = 1 @@ -244,8 +260,8 @@ class Parser: str = str.replace('xx', 'x') str = str.replace('xx', 'x') str = str.replace('\nx', '\n') - # note that replacing x\n with \n would be incorrect, because - # x may be preceded by a backslash + # Replacing x\n with \n would be incorrect because + # x may be preceded by a backslash. # March over the squashed version of the program, accumulating # the line numbers of non-continued stmts, and determining @@ -360,24 +376,25 @@ class Parser: self._study1() return self.continuation - # study1 was sufficient to determine the continuation status, - # but doing more requires looking at every character. study2 - # does this for the last interesting statement in the block. - # Creates: - # self.stmt_start, stmt_end - # slice indices of last interesting stmt - # self.stmt_bracketing - # the bracketing structure of the last interesting stmt; - # for example, for the statement "say(boo) or die", stmt_bracketing - # will be [(0, 0), (3, 1), (8, 0)]. Strings and comments are - # treated as brackets, for the matter. - # self.lastch - # last non-whitespace character before optional trailing - # comment - # self.lastopenbracketpos - # if continuation is C_BRACKET, index of last open bracket - def _study2(self): + """ + study1 was sufficient to determine the continuation status, + but doing more requires looking at every character. study2 + does this for the last interesting statement in the block. + Creates: + self.stmt_start, stmt_end + slice indices of last interesting stmt + self.stmt_bracketing + the bracketing structure of the last interesting stmt; for + example, for the statement "say(boo) or die", + stmt_bracketing will be ((0, 0), (0, 1), (2, 0), (2, 1), + (4, 0)). Strings and comments are treated as brackets, for + the matter. + self.lastch + last interesting character before optional trailing comment + self.lastopenbracketpos + if continuation is C_BRACKET, index of last open bracket + """ if self.study_level >= 2: return self._study1() @@ -385,11 +402,11 @@ class Parser: # Set p and q to slice indices of last interesting stmt. str, goodlines = self.str, self.goodlines - i = len(goodlines) - 1 - p = len(str) # index of newest line + i = len(goodlines) - 1 # Index of newest line. + p = len(str) # End of goodlines[i] while i: assert p - # p is the index of the stmt at line number goodlines[i]. + # Make p be the index of the stmt at line number goodlines[i]. # Move p back to the stmt at line number goodlines[i-1]. q = p for nothing in range(goodlines[i-1], goodlines[i]): @@ -483,10 +500,11 @@ class Parser: self.lastopenbracketpos = stack[-1] self.stmt_bracketing = tuple(bracketing) - # Assuming continuation is C_BRACKET, return the number - # of spaces the next line should be indented. - def compute_bracket_indent(self): + """Return number of spaces the next line should be indented. + + Line continuation must be C_BRACKET. + """ self._study2() assert self.continuation == C_BRACKET j = self.lastopenbracketpos @@ -513,20 +531,22 @@ class Parser: extra = self.indentwidth return len(str[i:j].expandtabs(self.tabwidth)) + extra - # Return number of physical lines in last stmt (whether or not - # it's an interesting stmt! this is intended to be called when - # continuation is C_BACKSLASH). - def get_num_lines_in_stmt(self): + """Return number of physical lines in last stmt. + + The statement doesn't have to be an interesting statement. This is + intended to be called when continuation is C_BACKSLASH. + """ self._study1() goodlines = self.goodlines return goodlines[-1] - goodlines[-2] - # Assuming continuation is C_BACKSLASH, return the number of spaces - # the next line should be indented. Also assuming the new line is - # the first one following the initial line of the stmt. - def compute_backslash_indent(self): + """Return number of spaces the next line should be indented. + + Line continuation must be C_BACKSLASH. Also assume that the new + line is the first one following the initial line of the stmt. + """ self._study2() assert self.continuation == C_BACKSLASH str = self.str @@ -551,6 +571,8 @@ class Parser: elif ch == '"' or ch == "'": i = _match_stringre(str, i, endpos).end() elif ch == '#': + # This line is unreachable because the # makes a comment of + # everything after it. break elif level == 0 and ch == '=' and \ (i == 0 or str[i-1] not in "=<>!") and \ @@ -576,10 +598,10 @@ class Parser: return len(str[self.stmt_start:i].expandtabs(\ self.tabwidth)) + 1 - # Return the leading whitespace on the initial line of the last - # interesting stmt. - def get_base_indent_string(self): + """Return the leading whitespace on the initial line of the last + interesting stmt. + """ self._study2() i, n = self.stmt_start, self.stmt_end j = i @@ -588,30 +610,37 @@ class Parser: j = j + 1 return str[i:j] - # Did the last interesting stmt open a block? - def is_block_opener(self): + "Return True if the last interesting statemtent opens a block." self._study2() return self.lastch == ':' - # Did the last interesting stmt close a block? - def is_block_closer(self): + "Return True if the last interesting statement closes a block." self._study2() return _closere(self.str, self.stmt_start) is not None - # index of last open bracket ({[, or None if none + # XXX - is this used? lastopenbracketpos = None def get_last_open_bracket_pos(self): + "Return index of last open bracket or None." self._study2() return self.lastopenbracketpos - # the structure of the bracketing of the last interesting statement, - # in the format defined in _study2, or None if the text didn't contain - # anything + # XXX - is this used? stmt_bracketing = None def get_last_stmt_bracketing(self): + """Return a tuple of the structure of the bracketing of the last + interesting statement. + + Tuple is in the format defined in _study2(). + """ self._study2() return self.stmt_bracketing + + +if __name__ == '__main__': #pragma: nocover + import unittest + unittest.main('idlelib.idle_test.test_pyparse', verbosity=2) diff --git a/Misc/NEWS.d/next/IDLE/2018-02-19-10-56-41.bpo-32874.6pZ9Gv.rst b/Misc/NEWS.d/next/IDLE/2018-02-19-10-56-41.bpo-32874.6pZ9Gv.rst new file mode 100644 index 0000000..7965531 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2018-02-19-10-56-41.bpo-32874.6pZ9Gv.rst @@ -0,0 +1 @@ +Add tests for pyparse. |