def test_lambda_metadata_matcher(self) -> None: # Match on qualified name provider module = cst.parse_module( "from typing import List\n\ndef foo() -> None: pass\n") wrapper = cst.MetadataWrapper(module) functiondef = cst.ensure_type(wrapper.module.body[1], cst.FunctionDef) self.assertTrue( matches( functiondef, m.FunctionDef(name=m.MatchMetadataIfTrue( meta.QualifiedNameProvider, lambda qualnames: any(n.name in {"foo", "bar", "baz"} for n in qualnames), )), metadata_resolver=wrapper, )) self.assertFalse( matches( functiondef, m.FunctionDef(name=m.MatchMetadataIfTrue( meta.QualifiedNameProvider, lambda qualnames: any(n.name in {"bar", "baz"} for n in qualnames), )), metadata_resolver=wrapper, ))
def test_replace_metadata(self) -> None: def _rename_foo( node: cst.CSTNode, extraction: Dict[str, Union[cst.CSTNode, Sequence[cst.CSTNode]]], ) -> cst.CSTNode: return cst.ensure_type(node, cst.Name).with_changes(value="replaced") original = cst.parse_module( "foo: int = 37\ndef bar(foo: int) -> int:\n return foo\n\nbiz: int = bar(42)\n" ) wrapper = cst.MetadataWrapper(original) replaced = cst.ensure_type( m.replace( wrapper, m.Name(metadata=m.MatchMetadataIfTrue( meta.QualifiedNameProvider, lambda qualnames: any(n.name == "foo" for n in qualnames), )), _rename_foo, ), cst.Module, ).code self.assertEqual( replaced, "replaced: int = 37\ndef bar(foo: int) -> int:\n return foo\n\nbiz: int = bar(42)\n", )
def handle_any_string( self, node: Union[cst.SimpleString, cst.ConcatenatedString]) -> None: value = node.evaluated_value if value is None: return mod = cst.parse_module(value) extracted_nodes = m.extractall( mod, m.Name( value=m.SaveMatchedNode(m.DoNotCare(), "name"), metadata=m.MatchMetadataIfTrue( cst.metadata.ParentNodeProvider, lambda parent: not isinstance(parent, cst.Attribute), ), ) | m.SaveMatchedNode(m.Attribute(), "attribute"), metadata_resolver=MetadataWrapper(mod, unsafe_skip_copy=True), ) names = { cast(str, values["name"]) for values in extracted_nodes if "name" in values } | { name for values in extracted_nodes if "attribute" in values for name, _ in cst.metadata.scope_provider._gen_dotted_names( cast(cst.Attribute, values["attribute"])) } self.names.update(names)
class StripStringsCommand(VisitorBasedCodemodCommand): DESCRIPTION: str = ( "Converts string type annotations to 3.7-compatible forward references." ) METADATA_DEPENDENCIES = (QualifiedNameProvider, ) # We want to gate the SimpleString visitor below to only SimpleStrings inside # an Annotation. @m.call_if_inside(m.Annotation()) # We also want to gate the SimpleString visitor below to ensure that we don't # erroneously strip strings from a Literal. @m.call_if_not_inside( m.Subscript( # We could match on value=m.Name("Literal") here, but then we might miss # instances where people are importing typing_extensions directly, or # importing Literal as an alias. value=m.MatchMetadataIfTrue( QualifiedNameProvider, lambda qualnames: any(qualname.name == "typing_extensions.Literal" for qualname in qualnames), ))) def leave_SimpleString( self, original_node: libcst.SimpleString, updated_node: libcst.SimpleString ) -> Union[libcst.SimpleString, libcst.BaseExpression]: AddImportsVisitor.add_needed_import(self.context, "__future__", "annotations") # Just use LibCST to evaluate the expression itself, and insert that as the # annotation. return parse_expression(updated_node.evaluated_value, config=self.module.config_for_parsing)
def match_qualname(name): # We use the metadata to get qualname instead of matching directly on function # name, because this handles some scope and "from x import y as z" issues. return m.MatchMetadataIfTrue( cst.metadata.QualifiedNameProvider, # If there are multiple possible qualnames, e.g. due to conditional imports, # be conservative. Better to leave the user to fix a few things by hand than # to break their code while attempting to refactor it! lambda qualnames: all(n.name == name for n in qualnames), )
def visit_Subscript(self, node: cst.Subscript) -> None: if not self.has_future_annotations_import: return if self.in_annotation: if m.matches( node, m.Subscript(metadata=m.MatchMetadataIfTrue( QualifiedNameProvider, lambda qualnames: any(n.name == "typing_extensions.Literal" for n in qualnames), )), metadata_resolver=self.context.wrapper, ): self.in_literal.add(node)
def leave_Module(self, original_node: cst.Module, updated_node: cst.Module) -> cst.Module: # Somewhat contrived scenario to test codepaths. return cst.ensure_type( self.replace( original_node, m.Name(metadata=m.MatchMetadataIfTrue( meta.QualifiedNameProvider, lambda qualnames: any(n.name == "foo" for n in qualnames), )), _rename_foo, ), cst.Module, )
# This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. from typing import Set, Union, cast import libcst as cst import libcst.matchers as m from libcst.codemod._context import CodemodContext from libcst.codemod._visitor import ContextAwareVisitor from libcst.metadata import MetadataWrapper, QualifiedNameProvider FUNCS_CONSIDERED_AS_STRING_ANNOTATIONS = {"typing.TypeVar"} ANNOTATION_MATCHER: m.BaseMatcherNode = m.Annotation() | m.Call( metadata=m.MatchMetadataIfTrue( QualifiedNameProvider, lambda qualnames: any(qn.name in FUNCS_CONSIDERED_AS_STRING_ANNOTATIONS for qn in qualnames), )) class GatherNamesFromStringAnnotationsVisitor(ContextAwareVisitor): """ Collects all names from string literals used for typing purposes. This includes annotations like ``foo: "SomeType"``, and parameters to special functions related to typing (currently only `typing.TypeVar`). After visiting, a set of all found names will be available on the ``names`` attribute of this visitor. """ METADATA_DEPENDENCIES = (QualifiedNameProvider, )