Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/libcst/metadata/name_provider.py: 39%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

75 statements  

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. 

5 

6import dataclasses 

7from pathlib import Path 

8from typing import Any, Collection, List, Mapping, Optional, Union 

9 

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) 

19 

20 

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``:: 

30 

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 } 

51 

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

55 

56 METADATA_DEPENDENCIES = (ScopeProvider,) 

57 

58 def visit_Module(self, node: cst.Module) -> Optional[bool]: 

59 visitor = QualifiedNameVisitor(self) 

60 node.visit(visitor) 

61 

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) 

72 

73 

74class QualifiedNameVisitor(cst.CSTVisitor): 

75 def __init__(self, provider: "QualifiedNameProvider") -> None: 

76 self.provider: QualifiedNameProvider = provider 

77 

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 

88 

89 

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. 

95 

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. 

100 

101 Example usage:: 

102 

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>)}} 

108 

109 """ 

110 

111 METADATA_DEPENDENCIES = (QualifiedNameProvider,) 

112 

113 @classmethod 

114 def gen_cache( 

115 cls, 

116 root_path: Path, 

117 paths: List[str], 

118 *, 

119 use_pyproject_toml: bool = False, 

120 **kwargs: Any, 

121 ) -> Mapping[str, ModuleNameAndPackage]: 

122 cache = { 

123 path: calculate_module_and_package( 

124 root_path, path, use_pyproject_toml=use_pyproject_toml 

125 ) 

126 for path in paths 

127 } 

128 return cache 

129 

130 def __init__(self, cache: ModuleNameAndPackage) -> None: 

131 super().__init__(cache) 

132 self.module_name: str = cache.name 

133 self.package_name: str = cache.package 

134 

135 def visit_Module(self, node: cst.Module) -> bool: 

136 visitor = FullyQualifiedNameVisitor(self, self.module_name, self.package_name) 

137 node.visit(visitor) 

138 self.set_metadata( 

139 node, 

140 {QualifiedName(name=self.module_name, source=QualifiedNameSource.LOCAL)}, 

141 ) 

142 return True 

143 

144 

145class FullyQualifiedNameVisitor(cst.CSTVisitor): 

146 @staticmethod 

147 def _fully_qualify_local(module_name: str, package_name: str, name: str) -> str: 

148 abs_name = name.lstrip(".") 

149 num_dots = len(name) - len(abs_name) 

150 # handle relative import 

151 if num_dots > 0: 

152 name = abs_name 

153 # see importlib._bootstrap._resolve_name 

154 # https://github.com/python/cpython/blob/3.10/Lib/importlib/_bootstrap.py#L902 

155 bits = package_name.rsplit(".", num_dots - 1) 

156 if len(bits) < num_dots: 

157 raise ImportError("attempted relative import beyond top-level package") 

158 module_name = bits[0] 

159 

160 return f"{module_name}.{name}" 

161 

162 @staticmethod 

163 def _fully_qualify( 

164 module_name: str, package_name: str, qname: QualifiedName 

165 ) -> QualifiedName: 

166 if qname.source == QualifiedNameSource.BUILTIN: 

167 # builtins are already fully qualified 

168 return qname 

169 name = qname.name 

170 if qname.source == QualifiedNameSource.IMPORT and not name.startswith("."): 

171 # non-relative imports are already fully qualified 

172 return qname 

173 new_name = FullyQualifiedNameVisitor._fully_qualify_local( 

174 module_name, package_name, qname.name 

175 ) 

176 return dataclasses.replace(qname, name=new_name) 

177 

178 def __init__( 

179 self, provider: FullyQualifiedNameProvider, module_name: str, package_name: str 

180 ) -> None: 

181 self.module_name = module_name 

182 self.package_name = package_name 

183 self.provider = provider 

184 

185 def on_visit(self, node: cst.CSTNode) -> bool: 

186 qnames = self.provider.get_metadata(QualifiedNameProvider, node) 

187 if qnames is not None: 

188 self.provider.set_metadata( 

189 node, 

190 { 

191 FullyQualifiedNameVisitor._fully_qualify( 

192 self.module_name, self.package_name, qname 

193 ) 

194 for qname in qnames 

195 }, 

196 ) 

197 return True