55
66
77def test_compile_module ():
8+ class ModuleName (str ):
9+ pass
10+
11+ for module in (None , "" , "package.module" , ModuleName ("package.module" ), "\ud800 " ):
12+ code = compile ("value = 42" , "<module-context>" , "exec" , module = module )
13+ assert code .co_filename == "<module-context>"
14+ namespace = {}
15+ exec (code , namespace )
16+ assert namespace ["value" ] == 42
17+
18+ for module in (1 , False , b"package.module" , [], {}, object ()):
19+ with assert_raises (TypeError ) as caught :
20+ compile ("" , "<module-context>" , "exec" , module = module )
21+ assert "argument 'module' must be str or None" in str (caught .exception )
22+
23+ # module is keyword-only; _feature_version stays keyword-only too.
24+ with assert_raises (TypeError ):
25+ compile ("" , "<module-context>" , "exec" , 0 , False , - 1 , "package.module" )
26+
27+ lexical_sources = [
28+ "x = 1or 0" ,
29+ r"'\z'" ,
30+ r"'\400'" ,
31+ r'f"{1:\z}"' ,
32+ r'f"\{1}"' ,
33+ ]
34+ codegen_sources = [
35+ "assert (1, 2)" ,
36+ "1 is 1" ,
37+ "def f():\n try:\n pass\n finally:\n return 1\n " ,
38+ ]
39+
40+ # Default source compilation uses module for matching, while diagnostics retain
41+ # the real source filename. Test both text and bytes compilation.
42+ for source in lexical_sources + codegen_sources :
43+ for input_source in (source , source .encode ()):
44+ with warnings .catch_warnings (record = True ) as caught :
45+ warnings .simplefilter ("error" , SyntaxWarning )
46+ warnings .filterwarnings (
47+ "always" , category = SyntaxWarning , module = r"^package\.module$"
48+ )
49+ compile (
50+ input_source , "<module-context>" , "exec" , module = "package.module"
51+ )
52+ assert len (caught ) == 1 , (source , caught )
53+ assert caught [0 ].filename == "<module-context>"
54+ assert caught [0 ].category is SyntaxWarning
55+ with warnings .catch_warnings ():
56+ warnings .simplefilter ("ignore" , SyntaxWarning )
57+ warnings .filterwarnings (
58+ "error" , category = SyntaxWarning , module = r"^package\.module$"
59+ )
60+ with assert_raises (SyntaxError ):
61+ compile (
62+ input_source ,
63+ "<module-context>" ,
64+ "exec" ,
65+ module = "package.module" ,
66+ )
67+
68+ # Compiling a pre-existing AST must use the module too.
69+ for source in codegen_sources :
70+ tree = ast .parse (source )
71+ with warnings .catch_warnings (record = True ) as caught :
72+ warnings .simplefilter ("error" , SyntaxWarning )
73+ warnings .filterwarnings (
74+ "always" , category = SyntaxWarning , module = r"^package\.module$"
75+ )
76+ compile (tree , "<module-context>" , "exec" , module = "package.module" )
77+ assert len (caught ) == 1 , (source , caught )
78+ assert caught [0 ].filename == "<module-context>"
79+
80+ # A following compile, with omitted or None module, must not inherit the override.
81+ for kwargs in ({}, {"module" : None }):
82+ with warnings .catch_warnings (record = True ) as caught :
83+ warnings .simplefilter ("error" , SyntaxWarning )
84+ warnings .filterwarnings (
85+ "always" , category = SyntaxWarning , module = r"^<module-context>$"
86+ )
87+ compile ("1 is 1" , "<module-context>" , "exec" , ** kwargs )
88+ assert len (caught ) == 1
89+
90+ # Failed parses use the fallback string scanner, which must retain the module.
91+ for source in (r"b'\z' )" , r'f"{1:\z}" )' ):
92+ with warnings .catch_warnings (record = True ) as caught :
93+ warnings .simplefilter ("error" , SyntaxWarning )
94+ warnings .filterwarnings (
95+ "always" , category = SyntaxWarning , module = r"^package\.module$"
96+ )
97+ with assert_raises (SyntaxError ):
98+ compile (source , "<module-context>" , "exec" , module = "package.module" )
99+ assert len (caught ) == 1 , (source , caught )
100+ assert caught [0 ].filename == "<module-context>"
101+
8102 # AST conversion must use the same fallible, source-located warning boundary.
9103 # TYPE_COMMENTS validates through the AST parser before compiling the source;
10104 # the later compiler must not filter or emit the same escape a second time.
11105 for flags in (
12106 ast .PyCF_ONLY_AST ,
13107 ast .PyCF_TYPE_COMMENTS ,
108+ ast .PyCF_ONLY_AST | ast .PyCF_TYPE_COMMENTS ,
109+ ast .PyCF_ONLY_AST | ast .PyCF_OPTIMIZED_AST ,
14110 ):
15111 source = '\n π = f"{1:\\ z}"\n '
16112 for kwargs , expected_module in (
@@ -34,6 +130,27 @@ def match(self, module):
34130 assert modules == [expected_module ], (flags , kwargs , modules )
35131 assert len (caught ) == 1 , (flags , kwargs , caught )
36132 assert (caught [0 ].filename , caught [0 ].lineno ) == ("<module-context>" , 2 )
133+
134+ with warnings .catch_warnings ():
135+ warnings .simplefilter ("ignore" , SyntaxWarning )
136+ warnings .filterwarnings (
137+ "error" , category = SyntaxWarning , module = r"^package\.module$"
138+ )
139+ with assert_raises (SyntaxError ) as caught :
140+ compile (
141+ source , "<module-context>" , "exec" , flags , module = "package.module"
142+ )
143+ error = caught .exception
144+ assert error .filename == "<module-context>"
145+ assert (error .lineno , error .offset , error .end_lineno , error .end_offset ) == (
146+ 2 ,
147+ 10 ,
148+ 2 ,
149+ 12 ,
150+ )
151+ assert error .text == 'π = f"{1:\\ z}"\n '
152+ assert "Such sequences will not work in the future" not in error .msg
153+
37154 # Warning handlers may raise arbitrary exceptions, which must survive intact.
38155 failure = RuntimeError ("warning handler failed" )
39156
@@ -78,7 +195,27 @@ def showwarning(message, category, filename, lineno, *args, **kwargs):
78195 )
79196 assert modules == ["package.outer" , "package.inner" , "package.outer" ], modules
80197 assert locations == [("<outer>" , 1 ), ("<inner>" , 1 ), ("<outer>" , 2 )], locations
81- for flags in (ast .PyCF_ONLY_AST ,):
198+
199+ # These warnings already came from AST conversion. Retain them instead of
200+ # replacing that path with a broader scanner before every AST parse.
201+ for source in ('''rf"{1:{'\\ z'}}"''' , '''f"{1:{t'\\ z'}}"''' ):
202+ for flags in (
203+ ast .PyCF_ONLY_AST ,
204+ ast .PyCF_TYPE_COMMENTS ,
205+ ast .PyCF_ONLY_AST | ast .PyCF_TYPE_COMMENTS ,
206+ ):
207+ with warnings .catch_warnings (record = True ) as caught :
208+ warnings .simplefilter ("error" , SyntaxWarning )
209+ warnings .filterwarnings (
210+ "always" , category = SyntaxWarning , module = r"^package\.module$"
211+ )
212+ compile (
213+ source , "<module-context>" , "exec" , flags , module = "package.module"
214+ )
215+ assert len (caught ) == 1 , (source , flags , caught )
216+ assert (caught [0 ].filename , caught [0 ].lineno ) == ("<module-context>" , 1 )
217+
218+ for flags in (ast .PyCF_ONLY_AST , ast .PyCF_ONLY_AST | ast .PyCF_TYPE_COMMENTS ):
82219 with warnings .catch_warnings (record = True ) as caught :
83220 warnings .simplefilter ("always" , SyntaxWarning )
84221 with assert_raises (SyntaxError ):
@@ -110,6 +247,38 @@ def showwarning(*args, **kwargs):
110247 )
111248 assert caught .exception .lineno == 2
112249 assert "\\ q" in caught .exception .msg
250+
251+ # Raw literal text is not an invalid escape. Nested nonraw expressions still
252+ # retain the diagnostics previously emitted during AST conversion.
253+ for flags in (
254+ ast .PyCF_ONLY_AST ,
255+ ast .PyCF_TYPE_COMMENTS ,
256+ ast .PyCF_ONLY_AST | ast .PyCF_TYPE_COMMENTS ,
257+ ):
258+ for source in (
259+ r'rf"{1:\z}"' ,
260+ r'rf"{1:{2:\z}}"' ,
261+ r'tr"{1:{2:\z}}"' ,
262+ "f\" {1:{r'\\ z'}}\" " ,
263+ ):
264+ with warnings .catch_warnings (record = True ) as caught :
265+ warnings .simplefilter ("error" , SyntaxWarning )
266+ compile (
267+ source , "<module-context>" , "exec" , flags , module = "package.module"
268+ )
269+ assert not caught , (source , flags , caught )
270+ for source in ("rf\" {f'{2:\\ z}'}\" " , "rf\" {1:{'\\ z'}}\" " ):
271+ with warnings .catch_warnings ():
272+ warnings .simplefilter ("error" , SyntaxWarning )
273+ with assert_raises (SyntaxError ):
274+ compile (
275+ source ,
276+ "<module-context>" ,
277+ "exec" ,
278+ flags ,
279+ module = "package.module" ,
280+ )
281+
113282 # Func-type parsing trims its source before converting the parsed nodes.
114283 # Already-detected escapes must still point into the original source.
115284 for prefix in ("" , "\n \n " ):
0 commit comments