@@ -1840,6 +1840,11 @@ class TestKeywordTypoSuggestions(unittest.TestCase):
18401840 ("void f():" , "def" ),
18411841 ]
18421842
1843+ def get_syntax_error (self , source , mode = "exec" ):
1844+ with self .assertRaises (SyntaxError ) as caught :
1845+ compile (source , "<test>" , mode )
1846+ return caught .exception
1847+
18431848 def test_keyword_suggestions_from_file (self ):
18441849 with tempfile .TemporaryDirectory () as script_dir :
18451850 for i , (code , expected_kw ) in enumerate (self .TYPO_CASES ):
@@ -1863,9 +1868,8 @@ def test_keyword_suggestions_from_single_input(self):
18631868 for source , keyword in self .TYPO_CASES :
18641869 for ending in ('' , '\n ' ):
18651870 with self .subTest (source = source , ending = ending ):
1866- with self .assertRaises (SyntaxError ) as caught :
1867- compile (source + ending , "<test>" , "single" )
1868- self .assertEqual (caught .exception ._metadata [0 ], keyword )
1871+ error = self .get_syntax_error (source + ending , "single" )
1872+ self .assertEqual (error ._metadata [0 ], keyword )
18691873
18701874 def test_no_keyword_suggestion_for_comma_errors (self ):
18711875 # When the parser identifies a missing comma, don't suggest
@@ -1904,9 +1908,7 @@ def test_parser_suggestions_need_no_formatting_work(self):
19041908 ]
19051909 for source , keyword , lineno , offset in cases :
19061910 with self .subTest (source = source ):
1907- with self .assertRaises (SyntaxError ) as caught :
1908- compile (source , "<test>" , "exec" )
1909- error = caught .exception
1911+ error = self .get_syntax_error (source )
19101912 original = error .args
19111913 with (unittest .mock .patch ("builtins.compile" ) as compile_mock ,
19121914 unittest .mock .patch ("builtins.open" ) as open_mock ):
@@ -1919,28 +1921,88 @@ def test_parser_suggestions_need_no_formatting_work(self):
19191921 self .assertEqual ((exc .lineno , exc .offset ), (str (lineno ), offset ))
19201922 self .assertEqual (error .args , original )
19211923
1924+ @cpython_only
1925+ def test_public_fields_override_keyword_metadata (self ):
1926+ changes = dict (filename = "changed.py" , msg = "custom message" , text = "<redacted>\n " ,
1927+ lineno = 101 , offset = 2 , end_lineno = 102 , end_offset = 3 )
1928+ for field , value in changes .items ():
1929+ with self .subTest (field = field ):
1930+ error = self .get_syntax_error ("impor math" )
1931+ setattr (error , field , value )
1932+ error .args = (error .msg , (error .filename , error .lineno , error .offset ,
1933+ error .text , error .end_lineno , error .end_offset ))
1934+ formatted = traceback .TracebackException .from_exception (error )
1935+ expected = str (value ) if field in ("lineno" , "end_lineno" ) else value
1936+ self .assertEqual (getattr (formatted , field ), expected )
1937+ self .assertNotIn ("Did you mean" , '' .join (formatted .format_exception_only ()))
1938+
1939+ @cpython_only
1940+ def test_keyword_metadata_avoids_custom_methods (self ):
1941+ class CustomString (str ):
1942+ def __eq__ (self , other ):
1943+ raise AssertionError ("metadata called a custom method" )
1944+
1945+ def __str__ (self ):
1946+ raise AssertionError ("metadata called a custom method" )
1947+
1948+ error = self .get_syntax_error ("impor math" )
1949+ suggestion , location , original = error ._metadata
1950+ for metadata in ((), (CustomString (suggestion ), location , original ),
1951+ (suggestion , (CustomString (location [0 ]), * location [1 :]), original ),
1952+ (suggestion , location , (CustomString (original [0 ]), * original [1 :]))):
1953+ with self .subTest (metadata_type = type (metadata [0 ]) if metadata else None ):
1954+ error ._metadata = metadata
1955+ formatted = '' .join (traceback .format_exception_only (error ))
1956+ self .assertNotIn ("Did you mean" , formatted )
1957+
1958+ def test_keyword_hint_in_interactive_interpreter (self ):
1959+ import code
1960+ with captured_output ("stderr" ) as output :
1961+ code .InteractiveInterpreter ().runsource ("impor math" , filename = "user.py" )
1962+ self .assertIn ("Did you mean 'import'" , output .getvalue ())
1963+
1964+ @requires_subprocess ()
1965+ def test_keyword_hint_does_not_reopen_source (self ):
1966+ with tempfile .TemporaryDirectory () as script_dir :
1967+ make_script (script_dir , "sitecustomize" , textwrap .dedent ('''
1968+ import sys
1969+ def audit(event, args):
1970+ if event == "open" and isinstance(args[0], str) and args[0].endswith("script.py"):
1971+ print("source-open")
1972+ sys.addaudithook(audit)
1973+ ''' ))
1974+ counts = []
1975+ cases = [("= math" , None ), ("impor math" , "import" ),
1976+ ("result = (one\n adn two)" , "and" )]
1977+ for source , keyword in cases :
1978+ script = make_script (script_dir , "script" , source )
1979+ _ , output , error = assert_python_failure (script , PYTHONPATH = script_dir )
1980+ counts .append (output .count (b"source-open" ))
1981+ if keyword is not None :
1982+ self .assertIn (f"Did you mean '{ keyword } '" .encode (), error )
1983+ if keyword == "and" :
1984+ self .assertIn (b"line 1\n result = (one\n " , error )
1985+ self .assertGreater (counts [0 ], 0 )
1986+ self .assertEqual (counts , [counts [0 ]] * len (cases ))
1987+
19221988 @cpython_only
19231989 def test_keyword_trials_do_not_repeat_warnings (self ):
19241990 import warnings
19251991 for prefix in ("value = '\\ q'\n " , "from . lazy import x\n " ):
19261992 with self .subTest (prefix = prefix ), warnings .catch_warnings (record = True ) as caught :
19271993 warnings .simplefilter ("always" )
1928- with self .assertRaises (SyntaxError ) as error :
1929- compile (prefix + "impor math" , "<test>" , "exec" )
1994+ error = self .get_syntax_error (prefix + "impor math" )
19301995 self .assertEqual (len (caught ), 1 )
1931- self .assertEqual (error .exception . _metadata [0 ], "import" )
1996+ self .assertEqual (error ._metadata [0 ], "import" )
19321997
19331998 @cpython_only
19341999 def test_keyword_replay_requires_complete_valid_input (self ):
19352000 for source in ("dell 1" , "impor math\n other =" , "whille True:\n pass" ,
19362001 "switch +:\n case:" , "function f(=):\n pass" , "(mat x)" ,
19372002 "impor math\n " + "pass\n " * 20000 ):
19382003 with self .subTest (source = source ):
1939- with self .assertRaises (SyntaxError ) as caught :
1940- compile (source , "<test>" , "exec" )
1941- # Action errors and a cache which stopped before EOF must not
1942- # count as successful repairs.
1943- self .assertIsNone (caught .exception ._metadata )
2004+ error = self .get_syntax_error (source )
2005+ self .assertIsNone (error ._metadata )
19442006
19452007 @cpython_only
19462008 def test_keyword_replay_respects_feature_version (self ):
@@ -1956,9 +2018,8 @@ def test_keyword_trials_reject_invalid_literals(self):
19562018 for mode in ("exec" , "eval" , "single" ):
19572019 source = "npt " + literal
19582020 with self .subTest (source = source , mode = mode ):
1959- with self .assertRaises (SyntaxError ) as caught :
1960- compile (source , "<test>" , mode )
1961- self .assertIsNone (caught .exception ._metadata )
2021+ error = self .get_syntax_error (source , mode )
2022+ self .assertIsNone (error ._metadata )
19622023
19632024 @cpython_only
19642025 @requires_subprocess ()
@@ -2015,17 +2076,15 @@ def test_keyword_trials_numeric_limit(self):
20152076 with self .subTest (digits = digits ):
20162077 source = "x = " + "1" * digits
20172078 compile (source , "<test>" , "exec" )
2018- with self .assertRaises (SyntaxError ) as caught :
2019- compile ("impor math\n " + source , "<test>" , "exec" )
2020- self .assertEqual (caught .exception ._metadata is not None , expected )
2079+ error = self .get_syntax_error ("impor math\n " + source )
2080+ self .assertEqual (error ._metadata is not None , expected )
20212081
20222082 @cpython_only
20232083 def test_keyword_trials_do_not_call_normalizer (self ):
20242084 with unittest .mock .patch ("unicodedata.normalize" ) as normalize :
2025- with self .assertRaises (SyntaxError ) as caught :
2026- compile ("whille True:\n café = 1" , "<test>" , "exec" )
2085+ error = self .get_syntax_error ("whille True:\n café = 1" )
20272086 normalize .assert_not_called ()
2028- self .assertIsNone (caught . exception ._metadata )
2087+ self .assertIsNone (error ._metadata )
20292088
20302089 @cpython_only
20312090 @requires_subprocess ()
@@ -2058,10 +2117,9 @@ def test_keyword_replay_source_limit(self):
20582117 literal = "value = " + repr ("a" * size ) + "\n "
20592118 for source in (literal + "impor math" , "impor math\n " + literal ):
20602119 with self .subTest (size = size , typo_first = source .startswith ("impor" )):
2061- with self .assertRaises (SyntaxError ) as caught :
2062- compile (source , "<test>" , "exec" )
2120+ error = self .get_syntax_error (source )
20632121 expected = size < 256 * 1024
2064- self .assertEqual (caught . exception ._metadata is not None , expected )
2122+ self .assertEqual (error ._metadata is not None , expected )
20652123 script = make_script (script_dir , "large_keyword_typo" , source )
20662124 _ , _ , stderr = assert_python_failure (script )
20672125 self .assertEqual (b"Did you mean 'import'" in stderr , expected )
@@ -2073,10 +2131,9 @@ def test_keyword_replay_work_limit(self):
20732131 # Several candidates must share the budget, including failed passes.
20742132 for count , expected in ((100 , "lambda" ), (2000 , None )):
20752133 source = "x = 1\n " * count + "f(" + "fron, " * 8 + "lamda x: x)"
2076- with self .subTest (count = count ), self .assertRaises (SyntaxError ) as caught :
2077- compile (source , "<test>" , "exec" )
2078- metadata = caught .exception ._metadata
2079- self .assertEqual (metadata [0 ] if metadata else None , expected )
2134+ with self .subTest (count = count ):
2135+ metadata = self .get_syntax_error (source )._metadata
2136+ self .assertEqual (metadata [0 ] if metadata else None , expected )
20802137
20812138@requires_debug_ranges ()
20822139@force_not_colorized_test_class
0 commit comments