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

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

119 

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 

124 

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 

133 

134 

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] 

149 

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

151 

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) 

167 

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 

174 

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