1"""Tests for the unparse.py script in the Tools/parser directory."""
2
3import unittest
4import test.support
5import io
6import os
7import random
8import tokenize
9import ast
10
11from test.test_tools import basepath, toolsdir, skip_if_missing
12
13skip_if_missing()
14
15parser_path = os.path.join(toolsdir, "parser")
16
17with test.support.DirsOnSysPath(parser_path):
18    import unparse
19
20def read_pyfile(filename):
21    """Read and return the contents of a Python source file (as a
22    string), taking into account the file encoding."""
23    with open(filename, "rb") as pyfile:
24        encoding = tokenize.detect_encoding(pyfile.readline)[0]
25    with open(filename, "r", encoding=encoding) as pyfile:
26        source = pyfile.read()
27    return source
28
29for_else = """\
30def f():
31    for x in range(10):
32        break
33    else:
34        y = 2
35    z = 3
36"""
37
38while_else = """\
39def g():
40    while True:
41        break
42    else:
43        y = 2
44    z = 3
45"""
46
47relative_import = """\
48from . import fred
49from .. import barney
50from .australia import shrimp as prawns
51"""
52
53nonlocal_ex = """\
54def f():
55    x = 1
56    def g():
57        nonlocal x
58        x = 2
59        y = 7
60        def h():
61            nonlocal x, y
62"""
63
64# also acts as test for 'except ... as ...'
65raise_from = """\
66try:
67    1 / 0
68except ZeroDivisionError as e:
69    raise ArithmeticError from e
70"""
71
72class_decorator = """\
73@f1(arg)
74@f2
75class Foo: pass
76"""
77
78elif1 = """\
79if cond1:
80    suite1
81elif cond2:
82    suite2
83else:
84    suite3
85"""
86
87elif2 = """\
88if cond1:
89    suite1
90elif cond2:
91    suite2
92"""
93
94try_except_finally = """\
95try:
96    suite1
97except ex1:
98    suite2
99except ex2:
100    suite3
101else:
102    suite4
103finally:
104    suite5
105"""
106
107with_simple = """\
108with f():
109    suite1
110"""
111
112with_as = """\
113with f() as x:
114    suite1
115"""
116
117with_two_items = """\
118with f() as x, g() as y:
119    suite1
120"""
121
122class ASTTestCase(unittest.TestCase):
123    def assertASTEqual(self, ast1, ast2):
124        self.assertEqual(ast.dump(ast1), ast.dump(ast2))
125
126    def check_roundtrip(self, code1, filename="internal"):
127        ast1 = compile(code1, filename, "exec", ast.PyCF_ONLY_AST)
128        unparse_buffer = io.StringIO()
129        unparse.Unparser(ast1, unparse_buffer)
130        code2 = unparse_buffer.getvalue()
131        ast2 = compile(code2, filename, "exec", ast.PyCF_ONLY_AST)
132        self.assertASTEqual(ast1, ast2)
133
134class UnparseTestCase(ASTTestCase):
135    # Tests for specific bugs found in earlier versions of unparse
136
137    def test_fstrings(self):
138        # See issue 25180
139        self.check_roundtrip(r"""f'{f"{0}"*3}'""")
140        self.check_roundtrip(r"""f'{f"{y}"*3}'""")
141
142    def test_strings(self):
143        self.check_roundtrip("u'foo'")
144        self.check_roundtrip("r'foo'")
145        self.check_roundtrip("b'foo'")
146
147    def test_del_statement(self):
148        self.check_roundtrip("del x, y, z")
149
150    def test_shifts(self):
151        self.check_roundtrip("45 << 2")
152        self.check_roundtrip("13 >> 7")
153
154    def test_for_else(self):
155        self.check_roundtrip(for_else)
156
157    def test_while_else(self):
158        self.check_roundtrip(while_else)
159
160    def test_unary_parens(self):
161        self.check_roundtrip("(-1)**7")
162        self.check_roundtrip("(-1.)**8")
163        self.check_roundtrip("(-1j)**6")
164        self.check_roundtrip("not True or False")
165        self.check_roundtrip("True or not False")
166
167    def test_integer_parens(self):
168        self.check_roundtrip("3 .__abs__()")
169
170    def test_huge_float(self):
171        self.check_roundtrip("1e1000")
172        self.check_roundtrip("-1e1000")
173        self.check_roundtrip("1e1000j")
174        self.check_roundtrip("-1e1000j")
175
176    def test_min_int(self):
177        self.check_roundtrip(str(-2**31))
178        self.check_roundtrip(str(-2**63))
179
180    def test_imaginary_literals(self):
181        self.check_roundtrip("7j")
182        self.check_roundtrip("-7j")
183        self.check_roundtrip("0j")
184        self.check_roundtrip("-0j")
185
186    def test_lambda_parentheses(self):
187        self.check_roundtrip("(lambda: int)()")
188
189    def test_chained_comparisons(self):
190        self.check_roundtrip("1 < 4 <= 5")
191        self.check_roundtrip("a is b is c is not d")
192
193    def test_function_arguments(self):
194        self.check_roundtrip("def f(): pass")
195        self.check_roundtrip("def f(a): pass")
196        self.check_roundtrip("def f(b = 2): pass")
197        self.check_roundtrip("def f(a, b): pass")
198        self.check_roundtrip("def f(a, b = 2): pass")
199        self.check_roundtrip("def f(a = 5, b = 2): pass")
200        self.check_roundtrip("def f(*, a = 1, b = 2): pass")
201        self.check_roundtrip("def f(*, a = 1, b): pass")
202        self.check_roundtrip("def f(*, a, b = 2): pass")
203        self.check_roundtrip("def f(a, b = None, *, c, **kwds): pass")
204        self.check_roundtrip("def f(a=2, *args, c=5, d, **kwds): pass")
205        self.check_roundtrip("def f(*args, **kwargs): pass")
206
207    def test_relative_import(self):
208        self.check_roundtrip(relative_import)
209
210    def test_nonlocal(self):
211        self.check_roundtrip(nonlocal_ex)
212
213    def test_raise_from(self):
214        self.check_roundtrip(raise_from)
215
216    def test_bytes(self):
217        self.check_roundtrip("b'123'")
218
219    def test_annotations(self):
220        self.check_roundtrip("def f(a : int): pass")
221        self.check_roundtrip("def f(a: int = 5): pass")
222        self.check_roundtrip("def f(*args: [int]): pass")
223        self.check_roundtrip("def f(**kwargs: dict): pass")
224        self.check_roundtrip("def f() -> None: pass")
225
226    def test_set_literal(self):
227        self.check_roundtrip("{'a', 'b', 'c'}")
228
229    def test_set_comprehension(self):
230        self.check_roundtrip("{x for x in range(5)}")
231
232    def test_dict_comprehension(self):
233        self.check_roundtrip("{x: x*x for x in range(10)}")
234
235    def test_class_decorators(self):
236        self.check_roundtrip(class_decorator)
237
238    def test_class_definition(self):
239        self.check_roundtrip("class A(metaclass=type, *[], **{}): pass")
240
241    def test_elifs(self):
242        self.check_roundtrip(elif1)
243        self.check_roundtrip(elif2)
244
245    def test_try_except_finally(self):
246        self.check_roundtrip(try_except_finally)
247
248    def test_starred_assignment(self):
249        self.check_roundtrip("a, *b, c = seq")
250        self.check_roundtrip("a, (*b, c) = seq")
251        self.check_roundtrip("a, *b[0], c = seq")
252        self.check_roundtrip("a, *(b, c) = seq")
253
254    def test_with_simple(self):
255        self.check_roundtrip(with_simple)
256
257    def test_with_as(self):
258        self.check_roundtrip(with_as)
259
260    def test_with_two_items(self):
261        self.check_roundtrip(with_two_items)
262
263    def test_dict_unpacking_in_dict(self):
264        # See issue 26489
265        self.check_roundtrip(r"""{**{'y': 2}, 'x': 1}""")
266        self.check_roundtrip(r"""{**{'y': 2}, **{'x': 1}}""")
267
268    def test_subscript(self):
269        self.check_roundtrip("a[i]")
270        self.check_roundtrip("a[i,]")
271        self.check_roundtrip("a[i, j]")
272        self.check_roundtrip("a[()]")
273        self.check_roundtrip("a[i:j]")
274        self.check_roundtrip("a[:j]")
275        self.check_roundtrip("a[i:]")
276        self.check_roundtrip("a[i:j:k]")
277        self.check_roundtrip("a[:j:k]")
278        self.check_roundtrip("a[i::k]")
279        self.check_roundtrip("a[i:j,]")
280        self.check_roundtrip("a[i:j, k]")
281
282
283class DirectoryTestCase(ASTTestCase):
284    """Test roundtrip behaviour on all files in Lib and Lib/test."""
285    NAMES = None
286
287    # test directories, relative to the root of the distribution
288    test_directories = 'Lib', os.path.join('Lib', 'test')
289
290    @classmethod
291    def get_names(cls):
292        if cls.NAMES is not None:
293            return cls.NAMES
294
295        names = []
296        for d in cls.test_directories:
297            test_dir = os.path.join(basepath, d)
298            for n in os.listdir(test_dir):
299                if n.endswith('.py') and not n.startswith('bad'):
300                    names.append(os.path.join(test_dir, n))
301
302        # Test limited subset of files unless the 'cpu' resource is specified.
303        if not test.support.is_resource_enabled("cpu"):
304            names = random.sample(names, 10)
305        # bpo-31174: Store the names sample to always test the same files.
306        # It prevents false alarms when hunting reference leaks.
307        cls.NAMES = names
308        return names
309
310    def test_files(self):
311        # get names of files to test
312        names = self.get_names()
313
314        for filename in names:
315            if test.support.verbose:
316                print('Testing %s' % filename)
317
318            # Some f-strings are not correctly round-tripped by
319            #  Tools/parser/unparse.py.  See issue 28002 for details.
320            #  We need to skip files that contain such f-strings.
321            if os.path.basename(filename) in ('test_fstring.py', ):
322                if test.support.verbose:
323                    print(f'Skipping {filename}: see issue 28002')
324                continue
325
326            with self.subTest(filename=filename):
327                source = read_pyfile(filename)
328                self.check_roundtrip(source)
329
330
331if __name__ == '__main__':
332    unittest.main()
333