File: test_codegen_clean.py

package info (click to toggle)
python-libcst 1.8.6-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,240 kB
  • sloc: python: 78,096; makefile: 15; sh: 2
file content (181 lines) | stat: -rw-r--r-- 7,005 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import difflib
import os
import os.path

import libcst.codegen.gen_matcher_classes as matcher_codegen
import libcst.codegen.gen_type_mapping as type_codegen
import libcst.codegen.gen_visitor_functions as visitor_codegen
from libcst.codegen.generate import clean_generated_code, format_file
from libcst.testing.utils import UnitTest


class TestCodegenClean(UnitTest):
    def assert_code_matches(
        self,
        old_code: str,
        new_code: str,
        module_name: str,
    ) -> None:
        if old_code != new_code:
            diff = difflib.unified_diff(
                old_code.splitlines(keepends=True),
                new_code.splitlines(keepends=True),
                fromfile="old_code",
                tofile="new_code",
            )
            diff_str = "".join(diff)
            self.fail(
                f"{module_name} needs new codegen, see "
                + "`python -m libcst.codegen.generate --help` "
                + "for instructions, or run `python -m libcst.codegen.generate all`. "
                + f"Diff:\n{diff_str}"
            )

    def test_codegen_clean_visitor_functions(self) -> None:
        """
        Verifies that codegen of visitor functions would not result in a
        changed file. If this test fails, please run 'python -m libcst.codegen.generate all'
        to generate new files.
        """
        new_code = clean_generated_code("\n".join(visitor_codegen.generated_code))
        new_file = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "visitor_codegen.deleteme.py"
        )
        with open(new_file, "w") as fp:
            fp.write(new_code)
        try:
            format_file(new_file)
        except Exception:
            # We failed to format, but this is probably due to invalid code that
            # black doesn't like. This test will still fail and report to run codegen.
            pass
        with open(new_file, "r") as fp:
            new_code = fp.read()
        os.remove(new_file)
        with open(
            os.path.join(
                os.path.dirname(os.path.abspath(__file__)), "../../_typed_visitor.py"
            ),
            "r",
        ) as fp:
            old_code = fp.read()

        # Now that we've done simple codegen, verify that it matches.
        self.assert_code_matches(old_code, new_code, "libcst._typed_visitor")

    def test_codegen_clean_matcher_classes(self) -> None:
        """
        Verifies that codegen of matcher classes would not result in a
        changed file. If this test fails, please run 'python -m libcst.codegen.generate all'
        to generate new files.
        """
        new_code = clean_generated_code("\n".join(matcher_codegen.generated_code))
        new_file = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "matcher_codegen.deleteme.py"
        )
        with open(new_file, "w") as fp:
            fp.write(new_code)
        try:
            format_file(new_file)
        except Exception:
            # We failed to format, but this is probably due to invalid code that
            # black doesn't like. This test will still fail and report to run codegen.
            pass
        with open(new_file, "r") as fp:
            new_code = fp.read()
        os.remove(new_file)
        with open(
            os.path.join(
                os.path.dirname(os.path.abspath(__file__)), "../../matchers/__init__.py"
            ),
            "r",
        ) as fp:
            old_code = fp.read()

        # Now that we've done simple codegen, verify that it matches.
        self.assert_code_matches(old_code, new_code, "libcst.matchers.__init__")

    def test_codegen_clean_return_types(self) -> None:
        """
        Verifies that codegen of return types would not result in a
        changed file. If this test fails, please run 'python -m libcst.codegen.generate all'
        to generate new files.
        """
        new_code = clean_generated_code("\n".join(type_codegen.generated_code))
        new_file = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "type_codegen.deleteme.py"
        )
        with open(new_file, "w") as fp:
            fp.write(new_code)
        try:
            format_file(new_file)
        except Exception:
            # We failed to format, but this is probably due to invalid code that
            # black doesn't like. This test will still fail and report to run codegen.
            pass
        with open(new_file, "r") as fp:
            new_code = fp.read()
        os.remove(new_file)
        with open(
            os.path.join(
                os.path.dirname(os.path.abspath(__file__)),
                "../../matchers/_return_types.py",
            ),
            "r",
        ) as fp:
            old_code = fp.read()

        # Now that we've done simple codegen, verify that it matches.
        self.assert_code_matches(old_code, new_code, "libcst.matchers._return_types")

    def test_normalize_unions(self) -> None:
        """
        Verifies that NormalizeUnions correctly converts binary operations with |
        into Union types, with special handling for Optional cases.
        """
        import libcst as cst
        from libcst.codegen.gen_matcher_classes import NormalizeUnions

        def assert_transforms_to(input_code: str, expected_code: str) -> None:
            input_cst = cst.parse_expression(input_code)
            expected_cst = cst.parse_expression(expected_code)

            result = input_cst.visit(NormalizeUnions())
            assert isinstance(
                result, cst.BaseExpression
            ), f"Expected BaseExpression, got {type(result)}"

            result_code = cst.Module(body=()).code_for_node(result)
            expected_code_str = cst.Module(body=()).code_for_node(expected_cst)

            self.assertEqual(
                result_code,
                expected_code_str,
                f"Expected {expected_code_str}, got {result_code}",
            )

        # Test regular union case
        assert_transforms_to("foo | bar | baz", "typing.Union[foo, bar, baz]")

        # Test Optional case (None on right)
        assert_transforms_to("foo | None", "typing.Optional[foo]")

        # Test Optional case (None on left)
        assert_transforms_to("None | foo", "typing.Optional[foo]")

        # Test case with more than 2 operands including None (should remain Union)
        assert_transforms_to("foo | bar | None", "typing.Union[foo, bar, None]")

        # Flatten existing Union types
        assert_transforms_to(
            "typing.Union[foo, typing.Union[bar, baz]]", "typing.Union[foo, bar, baz]"
        )
        # Merge two kinds of union types
        assert_transforms_to(
            "foo | typing.Union[bar, baz]", "typing.Union[foo, bar, baz]"
        )