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]"
)
|