@@ -1833,6 +1833,11 @@ class TestKeywordTypoSuggestions(unittest.TestCase):
18331833 ("void f():" , "def" ),
18341834 ]
18351835
1836+ def get_syntax_error (self , source , mode = "exec" ):
1837+ with self .assertRaises (SyntaxError ) as caught :
1838+ compile (source , "<test>" , mode )
1839+ return caught .exception
1840+
18361841 def test_keyword_suggestions_from_file (self ):
18371842 with tempfile .TemporaryDirectory () as script_dir :
18381843 for i , (code , expected_kw ) in enumerate (self .TYPO_CASES ):
@@ -1856,9 +1861,8 @@ def test_keyword_suggestions_from_single_input(self):
18561861 for source , keyword in self .TYPO_CASES :
18571862 for ending in ('' , '\n ' ):
18581863 with self .subTest (source = source , ending = ending ):
1859- with self .assertRaises (SyntaxError ) as caught :
1860- compile (source + ending , "<test>" , "single" )
1861- self .assertEqual (caught .exception ._metadata [0 ], keyword )
1864+ error = self .get_syntax_error (source + ending , "single" )
1865+ self .assertEqual (error ._metadata [0 ], keyword )
18621866
18631867 def test_no_keyword_suggestion_for_comma_errors (self ):
18641868 # When the parser identifies a missing comma, don't suggest
@@ -1897,9 +1901,7 @@ def test_parser_suggestions_need_no_formatting_work(self):
18971901 ]
18981902 for source , keyword , lineno , offset in cases :
18991903 with self .subTest (source = source ):
1900- with self .assertRaises (SyntaxError ) as caught :
1901- compile (source , "<test>" , "exec" )
1902- error = caught .exception
1904+ error = self .get_syntax_error (source )
19031905 original = error .args
19041906 with (unittest .mock .patch ("builtins.compile" ) as compile_mock ,
19051907 unittest .mock .patch ("builtins.open" ) as open_mock ):
@@ -1912,28 +1914,88 @@ def test_parser_suggestions_need_no_formatting_work(self):
19121914 self .assertEqual ((exc .lineno , exc .offset ), (str (lineno ), offset ))
19131915 self .assertEqual (error .args , original )
19141916
1917+ @cpython_only
1918+ def test_public_fields_override_keyword_metadata (self ):
1919+ changes = dict (filename = "changed.py" , msg = "custom message" , text = "<redacted>\n " ,
1920+ lineno = 101 , offset = 2 , end_lineno = 102 , end_offset = 3 )
1921+ for field , value in changes .items ():
1922+ with self .subTest (field = field ):
1923+ error = self .get_syntax_error ("impor math" )
1924+ setattr (error , field , value )
1925+ error .args = (error .msg , (error .filename , error .lineno , error .offset ,
1926+ error .text , error .end_lineno , error .end_offset ))
1927+ formatted = traceback .TracebackException .from_exception (error )
1928+ expected = str (value ) if field in ("lineno" , "end_lineno" ) else value
1929+ self .assertEqual (getattr (formatted , field ), expected )
1930+ self .assertNotIn ("Did you mean" , '' .join (formatted .format_exception_only ()))
1931+
1932+ @cpython_only
1933+ def test_keyword_metadata_avoids_custom_methods (self ):
1934+ class CustomString (str ):
1935+ def __eq__ (self , other ):
1936+ raise AssertionError ("metadata called a custom method" )
1937+
1938+ def __str__ (self ):
1939+ raise AssertionError ("metadata called a custom method" )
1940+
1941+ error = self .get_syntax_error ("impor math" )
1942+ suggestion , location , original = error ._metadata
1943+ for metadata in ((), (CustomString (suggestion ), location , original ),
1944+ (suggestion , (CustomString (location [0 ]), * location [1 :]), original ),
1945+ (suggestion , location , (CustomString (original [0 ]), * original [1 :]))):
1946+ with self .subTest (metadata_type = type (metadata [0 ]) if metadata else None ):
1947+ error ._metadata = metadata
1948+ formatted = '' .join (traceback .format_exception_only (error ))
1949+ self .assertNotIn ("Did you mean" , formatted )
1950+
1951+ def test_keyword_hint_in_interactive_interpreter (self ):
1952+ import code
1953+ with captured_output ("stderr" ) as output :
1954+ code .InteractiveInterpreter ().runsource ("impor math" , filename = "user.py" )
1955+ self .assertIn ("Did you mean 'import'" , output .getvalue ())
1956+
1957+ @requires_subprocess ()
1958+ def test_keyword_hint_does_not_reopen_source (self ):
1959+ with tempfile .TemporaryDirectory () as script_dir :
1960+ make_script (script_dir , "sitecustomize" , textwrap .dedent ('''
1961+ import sys
1962+ def audit(event, args):
1963+ if event == "open" and isinstance(args[0], str) and args[0].endswith("script.py"):
1964+ print("source-open")
1965+ sys.addaudithook(audit)
1966+ ''' ))
1967+ counts = []
1968+ cases = [("= math" , None ), ("impor math" , "import" ),
1969+ ("result = (one\n adn two)" , "and" )]
1970+ for source , keyword in cases :
1971+ script = make_script (script_dir , "script" , source )
1972+ _ , output , error = assert_python_failure (script , PYTHONPATH = script_dir )
1973+ counts .append (output .count (b"source-open" ))
1974+ if keyword is not None :
1975+ self .assertIn (f"Did you mean '{ keyword } '" .encode (), error )
1976+ if keyword == "and" :
1977+ self .assertIn (b"line 1\n result = (one\n " , error )
1978+ self .assertGreater (counts [0 ], 0 )
1979+ self .assertEqual (counts , [counts [0 ]] * len (cases ))
1980+
19151981 @cpython_only
19161982 def test_keyword_trials_do_not_repeat_warnings (self ):
19171983 import warnings
19181984 for prefix in ("value = '\\ q'\n " , "from . lazy import x\n " ):
19191985 with self .subTest (prefix = prefix ), warnings .catch_warnings (record = True ) as caught :
19201986 warnings .simplefilter ("always" )
1921- with self .assertRaises (SyntaxError ) as error :
1922- compile (prefix + "impor math" , "<test>" , "exec" )
1987+ error = self .get_syntax_error (prefix + "impor math" )
19231988 self .assertEqual (len (caught ), 1 )
1924- self .assertEqual (error .exception . _metadata [0 ], "import" )
1989+ self .assertEqual (error ._metadata [0 ], "import" )
19251990
19261991 @cpython_only
19271992 def test_keyword_replay_requires_complete_valid_input (self ):
19281993 for source in ("dell 1" , "impor math\n other =" , "whille True:\n pass" ,
19291994 "switch +:\n case:" , "function f(=):\n pass" , "(mat x)" ,
19301995 "impor math\n " + "pass\n " * 20000 ):
19311996 with self .subTest (source = source ):
1932- with self .assertRaises (SyntaxError ) as caught :
1933- compile (source , "<test>" , "exec" )
1934- # Action errors and a cache which stopped before EOF must not
1935- # count as successful repairs.
1936- self .assertIsNone (caught .exception ._metadata )
1997+ error = self .get_syntax_error (source )
1998+ self .assertIsNone (error ._metadata )
19371999
19382000 @cpython_only
19392001 def test_keyword_replay_respects_feature_version (self ):
@@ -1949,9 +2011,8 @@ def test_keyword_trials_reject_invalid_literals(self):
19492011 for mode in ("exec" , "eval" , "single" ):
19502012 source = "npt " + literal
19512013 with self .subTest (source = source , mode = mode ):
1952- with self .assertRaises (SyntaxError ) as caught :
1953- compile (source , "<test>" , mode )
1954- self .assertIsNone (caught .exception ._metadata )
2014+ error = self .get_syntax_error (source , mode )
2015+ self .assertIsNone (error ._metadata )
19552016
19562017 @cpython_only
19572018 @requires_subprocess ()
@@ -2008,17 +2069,15 @@ def test_keyword_trials_numeric_limit(self):
20082069 with self .subTest (digits = digits ):
20092070 source = "x = " + "1" * digits
20102071 compile (source , "<test>" , "exec" )
2011- with self .assertRaises (SyntaxError ) as caught :
2012- compile ("impor math\n " + source , "<test>" , "exec" )
2013- self .assertEqual (caught .exception ._metadata is not None , expected )
2072+ error = self .get_syntax_error ("impor math\n " + source )
2073+ self .assertEqual (error ._metadata is not None , expected )
20142074
20152075 @cpython_only
20162076 def test_keyword_trials_do_not_call_normalizer (self ):
20172077 with unittest .mock .patch ("unicodedata.normalize" ) as normalize :
2018- with self .assertRaises (SyntaxError ) as caught :
2019- compile ("whille True:\n café = 1" , "<test>" , "exec" )
2078+ error = self .get_syntax_error ("whille True:\n café = 1" )
20202079 normalize .assert_not_called ()
2021- self .assertIsNone (caught . exception ._metadata )
2080+ self .assertIsNone (error ._metadata )
20222081
20232082 @cpython_only
20242083 @requires_subprocess ()
@@ -2051,10 +2110,9 @@ def test_keyword_replay_source_limit(self):
20512110 literal = "value = " + repr ("a" * size ) + "\n "
20522111 for source in (literal + "impor math" , "impor math\n " + literal ):
20532112 with self .subTest (size = size , typo_first = source .startswith ("impor" )):
2054- with self .assertRaises (SyntaxError ) as caught :
2055- compile (source , "<test>" , "exec" )
2113+ error = self .get_syntax_error (source )
20562114 expected = size < 256 * 1024
2057- self .assertEqual (caught . exception ._metadata is not None , expected )
2115+ self .assertEqual (error ._metadata is not None , expected )
20582116 script = make_script (script_dir , "large_keyword_typo" , source )
20592117 _ , _ , stderr = assert_python_failure (script )
20602118 self .assertEqual (b"Did you mean 'import'" in stderr , expected )
@@ -2066,10 +2124,9 @@ def test_keyword_replay_work_limit(self):
20662124 # Several candidates must share the budget, including failed passes.
20672125 for count , expected in ((100 , "lambda" ), (2000 , None )):
20682126 source = "x = 1\n " * count + "f(" + "fron, " * 8 + "lamda x: x)"
2069- with self .subTest (count = count ), self .assertRaises (SyntaxError ) as caught :
2070- compile (source , "<test>" , "exec" )
2071- metadata = caught .exception ._metadata
2072- self .assertEqual (metadata [0 ] if metadata else None , expected )
2127+ with self .subTest (count = count ):
2128+ metadata = self .get_syntax_error (source )._metadata
2129+ self .assertEqual (metadata [0 ] if metadata else None , expected )
20732130
20742131@requires_debug_ranges ()
20752132@force_not_colorized_test_class
0 commit comments