Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/libcst/metadata/name_provider.py: 39%
74 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:43 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:43 +0000
1# Copyright (c) Meta Platforms, Inc. and affiliates.
2#
3# This source code is licensed under the MIT license found in the
4# LICENSE file in the root directory of this source tree.
6import dataclasses
7from pathlib import Path
8from typing import Collection, List, Mapping, Optional, Union
10import libcst as cst
11from libcst._metadata_dependent import LazyValue, MetadataDependent
12from libcst.helpers.module import calculate_module_and_package, ModuleNameAndPackage
13from libcst.metadata.base_provider import BatchableMetadataProvider
14from libcst.metadata.scope_provider import (
15 QualifiedName,
16 QualifiedNameSource,
17 ScopeProvider,
18)
21class QualifiedNameProvider(BatchableMetadataProvider[Collection[QualifiedName]]):
22 """
23 Compute possible qualified names of a variable CSTNode
24 (extends `PEP-3155 <https://www.python.org/dev/peps/pep-3155/>`_).
25 It uses the
26 :func:`~libcst.metadata.Scope.get_qualified_names_for` underlying to get qualified names.
27 Multiple qualified names may be returned, such as when we have conditional imports or an
28 import shadows another. E.g., the provider finds ``a.b``, ``d.e`` and
29 ``f.g`` as possible qualified names of ``c``::
31 >>> wrapper = MetadataWrapper(
32 >>> cst.parse_module(dedent(
33 >>> '''
34 >>> if something:
35 >>> from a import b as c
36 >>> elif otherthing:
37 >>> from d import e as c
38 >>> else:
39 >>> from f import g as c
40 >>> c()
41 >>> '''
42 >>> ))
43 >>> )
44 >>> call = wrapper.module.body[1].body[0].value
45 >>> wrapper.resolve(QualifiedNameProvider)[call],
46 {
47 QualifiedName(name="a.b", source=QualifiedNameSource.IMPORT),
48 QualifiedName(name="d.e", source=QualifiedNameSource.IMPORT),
49 QualifiedName(name="f.g", source=QualifiedNameSource.IMPORT),
50 }
52 For qualified name of a variable in a function or a comprehension, please refer
53 :func:`~libcst.metadata.Scope.get_qualified_names_for` for more detail.
54 """
56 METADATA_DEPENDENCIES = (ScopeProvider,)
58 def visit_Module(self, node: cst.Module) -> Optional[bool]:
59 visitor = QualifiedNameVisitor(self)
60 node.visit(visitor)
62 @staticmethod
63 def has_name(
64 visitor: MetadataDependent, node: cst.CSTNode, name: Union[str, QualifiedName]
65 ) -> bool:
66 """Check if any of qualified name has the str name or :class:`~libcst.metadata.QualifiedName` name."""
67 qualified_names = visitor.get_metadata(QualifiedNameProvider, node, set())
68 if isinstance(name, str):
69 return any(qn.name == name for qn in qualified_names)
70 else:
71 return any(qn == name for qn in qualified_names)
74class QualifiedNameVisitor(cst.CSTVisitor):
75 def __init__(self, provider: "QualifiedNameProvider") -> None:
76 self.provider: QualifiedNameProvider = provider
78 def on_visit(self, node: cst.CSTNode) -> bool:
79 scope = self.provider.get_metadata(ScopeProvider, node, None)
80 if scope:
81 self.provider.set_metadata(
82 node, LazyValue(lambda: scope.get_qualified_names_for(node))
83 )
84 else:
85 self.provider.set_metadata(node, set())
86 super().on_visit(node)
87 return True
90class FullyQualifiedNameProvider(BatchableMetadataProvider[Collection[QualifiedName]]):
91 """
92 Provide fully qualified names for CST nodes. Like :class:`QualifiedNameProvider`,
93 but the provided :class:`QualifiedName` instances have absolute identifier names
94 instead of local to the current module.
96 This provider is initialized with the current module's fully qualified name, and can
97 be used with :class:`~libcst.metadata.FullRepoManager`. The module's fully qualified
98 name itself is stored as a metadata of the :class:`~libcst.Module` node. Compared to
99 :class:`QualifiedNameProvider`, it also resolves relative imports.
101 Example usage::
103 >>> mgr = FullRepoManager(".", {"dir/a.py"}, {FullyQualifiedNameProvider})
104 >>> wrapper = mgr.get_metadata_wrapper_for_path("dir/a.py")
105 >>> fqnames = wrapper.resolve(FullyQualifiedNameProvider)
106 >>> {type(k): v for (k, v) in fqnames.items()}
107 {<class 'libcst._nodes.module.Module'>: {QualifiedName(name='dir.a', source=<QualifiedNameSource.LOCAL: 3>)}}
109 """
111 METADATA_DEPENDENCIES = (QualifiedNameProvider,)
113 @classmethod
114 def gen_cache(
115 cls, root_path: Path, paths: List[str], timeout: Optional[int] = None
116 ) -> Mapping[str, ModuleNameAndPackage]:
117 cache = {path: calculate_module_and_package(root_path, path) for path in paths}
118 return cache
120 def __init__(self, cache: ModuleNameAndPackage) -> None:
121 super().__init__(cache)
122 self.module_name: str = cache.name
123 self.package_name: str = cache.package
125 def visit_Module(self, node: cst.Module) -> bool:
126 visitor = FullyQualifiedNameVisitor(self, self.module_name, self.package_name)
127 node.visit(visitor)
128 self.set_metadata(
129 node,
130 {QualifiedName(name=self.module_name, source=QualifiedNameSource.LOCAL)},
131 )
132 return True
135class FullyQualifiedNameVisitor(cst.CSTVisitor):
136 @staticmethod
137 def _fully_qualify_local(module_name: str, package_name: str, name: str) -> str:
138 abs_name = name.lstrip(".")
139 num_dots = len(name) - len(abs_name)
140 # handle relative import
141 if num_dots > 0:
142 name = abs_name
143 # see importlib._bootstrap._resolve_name
144 # https://github.com/python/cpython/blob/3.10/Lib/importlib/_bootstrap.py#L902
145 bits = package_name.rsplit(".", num_dots - 1)
146 if len(bits) < num_dots:
147 raise ImportError("attempted relative import beyond top-level package")
148 module_name = bits[0]
150 return f"{module_name}.{name}"
152 @staticmethod
153 def _fully_qualify(
154 module_name: str, package_name: str, qname: QualifiedName
155 ) -> QualifiedName:
156 if qname.source == QualifiedNameSource.BUILTIN:
157 # builtins are already fully qualified
158 return qname
159 name = qname.name
160 if qname.source == QualifiedNameSource.IMPORT and not name.startswith("."):
161 # non-relative imports are already fully qualified
162 return qname
163 new_name = FullyQualifiedNameVisitor._fully_qualify_local(
164 module_name, package_name, qname.name
165 )
166 return dataclasses.replace(qname, name=new_name)
168 def __init__(
169 self, provider: FullyQualifiedNameProvider, module_name: str, package_name: str
170 ) -> None:
171 self.module_name = module_name
172 self.package_name = package_name
173 self.provider = provider
175 def on_visit(self, node: cst.CSTNode) -> bool:
176 qnames = self.provider.get_metadata(QualifiedNameProvider, node)
177 if qnames is not None:
178 self.provider.set_metadata(
179 node,
180 {
181 FullyQualifiedNameVisitor._fully_qualify(
182 self.module_name, self.package_name, qname
183 )
184 for qname in qnames
185 },
186 )
187 return True