Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/astroid/interpreter/_import/spec.py: 33%

193 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:53 +0000

1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html 

2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE 

3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt 

4 

5from __future__ import annotations 

6 

7import abc 

8import enum 

9import importlib 

10import importlib.machinery 

11import importlib.util 

12import os 

13import pathlib 

14import sys 

15import types 

16import warnings 

17import zipimport 

18from collections.abc import Iterator, Sequence 

19from pathlib import Path 

20from typing import Any, Literal, NamedTuple, Protocol 

21 

22from astroid.const import PY310_PLUS 

23from astroid.modutils import EXT_LIB_DIRS 

24 

25from . import util 

26 

27 

28# The MetaPathFinder protocol comes from typeshed, which says: 

29# Intentionally omits one deprecated and one optional method of `importlib.abc.MetaPathFinder` 

30class _MetaPathFinder(Protocol): 

31 def find_spec( 

32 self, 

33 fullname: str, 

34 path: Sequence[str] | None, 

35 target: types.ModuleType | None = ..., 

36 ) -> importlib.machinery.ModuleSpec | None: 

37 ... # pragma: no cover 

38 

39 

40class ModuleType(enum.Enum): 

41 """Python module types used for ModuleSpec.""" 

42 

43 C_BUILTIN = enum.auto() 

44 C_EXTENSION = enum.auto() 

45 PKG_DIRECTORY = enum.auto() 

46 PY_CODERESOURCE = enum.auto() 

47 PY_COMPILED = enum.auto() 

48 PY_FROZEN = enum.auto() 

49 PY_RESOURCE = enum.auto() 

50 PY_SOURCE = enum.auto() 

51 PY_ZIPMODULE = enum.auto() 

52 PY_NAMESPACE = enum.auto() 

53 

54 

55_MetaPathFinderModuleTypes: dict[str, ModuleType] = { 

56 # Finders created by setuptools editable installs 

57 "_EditableFinder": ModuleType.PY_SOURCE, 

58 "_EditableNamespaceFinder": ModuleType.PY_NAMESPACE, 

59 # Finders create by six 

60 "_SixMetaPathImporter": ModuleType.PY_SOURCE, 

61} 

62 

63_EditableFinderClasses: set[str] = { 

64 "_EditableFinder", 

65 "_EditableNamespaceFinder", 

66} 

67 

68 

69class ModuleSpec(NamedTuple): 

70 """Defines a class similar to PEP 420's ModuleSpec. 

71 

72 A module spec defines a name of a module, its type, location 

73 and where submodules can be found, if the module is a package. 

74 """ 

75 

76 name: str 

77 type: ModuleType | None 

78 location: str | None = None 

79 origin: str | None = None 

80 submodule_search_locations: Sequence[str] | None = None 

81 

82 

83class Finder: 

84 """A finder is a class which knows how to find a particular module.""" 

85 

86 def __init__(self, path: Sequence[str] | None = None) -> None: 

87 self._path = path or sys.path 

88 

89 @abc.abstractmethod 

90 def find_module( 

91 self, 

92 modname: str, 

93 module_parts: Sequence[str], 

94 processed: list[str], 

95 submodule_path: Sequence[str] | None, 

96 ) -> ModuleSpec | None: 

97 """Find the given module. 

98 

99 Each finder is responsible for each protocol of finding, as long as 

100 they all return a ModuleSpec. 

101 

102 :param modname: The module which needs to be searched. 

103 :param module_parts: It should be a list of strings, 

104 where each part contributes to the module's 

105 namespace. 

106 :param processed: What parts from the module parts were processed 

107 so far. 

108 :param submodule_path: A list of paths where the module 

109 can be looked into. 

110 :returns: A ModuleSpec, describing how and where the module was found, 

111 None, otherwise. 

112 """ 

113 

114 def contribute_to_path( 

115 self, spec: ModuleSpec, processed: list[str] 

116 ) -> Sequence[str] | None: 

117 """Get a list of extra paths where this finder can search.""" 

118 

119 

120class ImportlibFinder(Finder): 

121 """A finder based on the importlib module.""" 

122 

123 _SUFFIXES: Sequence[tuple[str, ModuleType]] = ( 

124 [(s, ModuleType.C_EXTENSION) for s in importlib.machinery.EXTENSION_SUFFIXES] 

125 + [(s, ModuleType.PY_SOURCE) for s in importlib.machinery.SOURCE_SUFFIXES] 

126 + [(s, ModuleType.PY_COMPILED) for s in importlib.machinery.BYTECODE_SUFFIXES] 

127 ) 

128 

129 def find_module( 

130 self, 

131 modname: str, 

132 module_parts: Sequence[str], 

133 processed: list[str], 

134 submodule_path: Sequence[str] | None, 

135 ) -> ModuleSpec | None: 

136 if submodule_path is not None: 

137 submodule_path = list(submodule_path) 

138 elif modname in sys.builtin_module_names: 

139 return ModuleSpec( 

140 name=modname, 

141 location=None, 

142 type=ModuleType.C_BUILTIN, 

143 ) 

144 else: 

145 try: 

146 with warnings.catch_warnings(): 

147 warnings.filterwarnings("ignore", category=UserWarning) 

148 spec = importlib.util.find_spec(modname) 

149 if ( 

150 spec 

151 and spec.loader # type: ignore[comparison-overlap] # noqa: E501 

152 is importlib.machinery.FrozenImporter 

153 ): 

154 # No need for BuiltinImporter; builtins handled above 

155 return ModuleSpec( 

156 name=modname, 

157 location=getattr(spec.loader_state, "filename", None), 

158 type=ModuleType.PY_FROZEN, 

159 ) 

160 except ValueError: 

161 pass 

162 submodule_path = sys.path 

163 

164 for entry in submodule_path: 

165 package_directory = os.path.join(entry, modname) 

166 for suffix in (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0]): 

167 package_file_name = "__init__" + suffix 

168 file_path = os.path.join(package_directory, package_file_name) 

169 if os.path.isfile(file_path): 

170 return ModuleSpec( 

171 name=modname, 

172 location=package_directory, 

173 type=ModuleType.PKG_DIRECTORY, 

174 ) 

175 for suffix, type_ in ImportlibFinder._SUFFIXES: 

176 file_name = modname + suffix 

177 file_path = os.path.join(entry, file_name) 

178 if os.path.isfile(file_path): 

179 return ModuleSpec(name=modname, location=file_path, type=type_) 

180 return None 

181 

182 def contribute_to_path( 

183 self, spec: ModuleSpec, processed: list[str] 

184 ) -> Sequence[str] | None: 

185 if spec.location is None: 

186 # Builtin. 

187 return None 

188 

189 if _is_setuptools_namespace(Path(spec.location)): 

190 # extend_path is called, search sys.path for module/packages 

191 # of this name see pkgutil.extend_path documentation 

192 path = [ 

193 os.path.join(p, *processed) 

194 for p in sys.path 

195 if os.path.isdir(os.path.join(p, *processed)) 

196 ] 

197 elif spec.name == "distutils" and not any( 

198 spec.location.lower().startswith(ext_lib_dir.lower()) 

199 for ext_lib_dir in EXT_LIB_DIRS 

200 ): 

201 # virtualenv below 20.0 patches distutils in an unexpected way 

202 # so we just find the location of distutils that will be 

203 # imported to avoid spurious import-error messages 

204 # https://github.com/pylint-dev/pylint/issues/5645 

205 # A regression test to create this scenario exists in release-tests.yml 

206 # and can be triggered manually from GitHub Actions 

207 distutils_spec = importlib.util.find_spec("distutils") 

208 if distutils_spec and distutils_spec.origin: 

209 origin_path = Path( 

210 distutils_spec.origin 

211 ) # e.g. .../distutils/__init__.py 

212 path = [str(origin_path.parent)] # e.g. .../distutils 

213 else: 

214 path = [spec.location] 

215 else: 

216 path = [spec.location] 

217 return path 

218 

219 

220class ExplicitNamespacePackageFinder(ImportlibFinder): 

221 """A finder for the explicit namespace packages.""" 

222 

223 def find_module( 

224 self, 

225 modname: str, 

226 module_parts: Sequence[str], 

227 processed: list[str], 

228 submodule_path: Sequence[str] | None, 

229 ) -> ModuleSpec | None: 

230 if processed: 

231 modname = ".".join([*processed, modname]) 

232 if util.is_namespace(modname) and modname in sys.modules: 

233 submodule_path = sys.modules[modname].__path__ 

234 return ModuleSpec( 

235 name=modname, 

236 location="", 

237 origin="namespace", 

238 type=ModuleType.PY_NAMESPACE, 

239 submodule_search_locations=submodule_path, 

240 ) 

241 return None 

242 

243 def contribute_to_path( 

244 self, spec: ModuleSpec, processed: list[str] 

245 ) -> Sequence[str] | None: 

246 return spec.submodule_search_locations 

247 

248 

249class ZipFinder(Finder): 

250 """Finder that knows how to find a module inside zip files.""" 

251 

252 def __init__(self, path: Sequence[str]) -> None: 

253 super().__init__(path) 

254 for entry_path in path: 

255 if entry_path not in sys.path_importer_cache: 

256 try: 

257 sys.path_importer_cache[entry_path] = zipimport.zipimporter( # type: ignore[assignment] 

258 entry_path 

259 ) 

260 except zipimport.ZipImportError: 

261 continue 

262 

263 def find_module( 

264 self, 

265 modname: str, 

266 module_parts: Sequence[str], 

267 processed: list[str], 

268 submodule_path: Sequence[str] | None, 

269 ) -> ModuleSpec | None: 

270 try: 

271 file_type, filename, path = _search_zip(module_parts) 

272 except ImportError: 

273 return None 

274 

275 return ModuleSpec( 

276 name=modname, 

277 location=filename, 

278 origin="egg", 

279 type=file_type, 

280 submodule_search_locations=path, 

281 ) 

282 

283 

284class PathSpecFinder(Finder): 

285 """Finder based on importlib.machinery.PathFinder.""" 

286 

287 def find_module( 

288 self, 

289 modname: str, 

290 module_parts: Sequence[str], 

291 processed: list[str], 

292 submodule_path: Sequence[str] | None, 

293 ) -> ModuleSpec | None: 

294 spec = importlib.machinery.PathFinder.find_spec(modname, path=submodule_path) 

295 if spec is not None: 

296 is_namespace_pkg = spec.origin is None 

297 location = spec.origin if not is_namespace_pkg else None 

298 module_type = ModuleType.PY_NAMESPACE if is_namespace_pkg else None 

299 return ModuleSpec( 

300 name=spec.name, 

301 location=location, 

302 origin=spec.origin, 

303 type=module_type, 

304 submodule_search_locations=list(spec.submodule_search_locations or []), 

305 ) 

306 return spec 

307 

308 def contribute_to_path( 

309 self, spec: ModuleSpec, processed: list[str] 

310 ) -> Sequence[str] | None: 

311 if spec.type == ModuleType.PY_NAMESPACE: 

312 return spec.submodule_search_locations 

313 return None 

314 

315 

316_SPEC_FINDERS = ( 

317 ImportlibFinder, 

318 ZipFinder, 

319 PathSpecFinder, 

320 ExplicitNamespacePackageFinder, 

321) 

322 

323 

324def _is_setuptools_namespace(location: pathlib.Path) -> bool: 

325 try: 

326 with open(location / "__init__.py", "rb") as stream: 

327 data = stream.read(4096) 

328 except OSError: 

329 return False 

330 extend_path = b"pkgutil" in data and b"extend_path" in data 

331 declare_namespace = ( 

332 b"pkg_resources" in data and b"declare_namespace(__name__)" in data 

333 ) 

334 return extend_path or declare_namespace 

335 

336 

337def _get_zipimporters() -> Iterator[tuple[str, zipimport.zipimporter]]: 

338 for filepath, importer in sys.path_importer_cache.items(): 

339 if isinstance(importer, zipimport.zipimporter): 

340 yield filepath, importer 

341 

342 

343def _search_zip( 

344 modpath: Sequence[str], 

345) -> tuple[Literal[ModuleType.PY_ZIPMODULE], str, str]: 

346 for filepath, importer in _get_zipimporters(): 

347 if PY310_PLUS: 

348 found: Any = importer.find_spec(modpath[0]) 

349 else: 

350 found = importer.find_module(modpath[0]) 

351 if found: 

352 if PY310_PLUS: 

353 if not importer.find_spec(os.path.sep.join(modpath)): 

354 raise ImportError( 

355 "No module named %s in %s/%s" 

356 % (".".join(modpath[1:]), filepath, modpath) 

357 ) 

358 elif not importer.find_module(os.path.sep.join(modpath)): 

359 raise ImportError( 

360 "No module named %s in %s/%s" 

361 % (".".join(modpath[1:]), filepath, modpath) 

362 ) 

363 return ( 

364 ModuleType.PY_ZIPMODULE, 

365 os.path.abspath(filepath) + os.path.sep + os.path.sep.join(modpath), 

366 filepath, 

367 ) 

368 raise ImportError(f"No module named {'.'.join(modpath)}") 

369 

370 

371def _find_spec_with_path( 

372 search_path: Sequence[str], 

373 modname: str, 

374 module_parts: list[str], 

375 processed: list[str], 

376 submodule_path: Sequence[str] | None, 

377) -> tuple[Finder | _MetaPathFinder, ModuleSpec]: 

378 for finder in _SPEC_FINDERS: 

379 finder_instance = finder(search_path) 

380 spec = finder_instance.find_module( 

381 modname, module_parts, processed, submodule_path 

382 ) 

383 if spec is None: 

384 continue 

385 return finder_instance, spec 

386 

387 # Support for custom finders 

388 for meta_finder in sys.meta_path: 

389 # See if we support the customer import hook of the meta_finder 

390 meta_finder_name = meta_finder.__class__.__name__ 

391 if meta_finder_name not in _MetaPathFinderModuleTypes: 

392 # Setuptools>62 creates its EditableFinders dynamically and have 

393 # "type" as their __class__.__name__. We check __name__ as well 

394 # to see if we can support the finder. 

395 try: 

396 meta_finder_name = meta_finder.__name__ # type: ignore[attr-defined] 

397 except AttributeError: 

398 continue 

399 if meta_finder_name not in _MetaPathFinderModuleTypes: 

400 continue 

401 

402 module_type = _MetaPathFinderModuleTypes[meta_finder_name] 

403 

404 # Meta path finders are supposed to have a find_spec method since 

405 # Python 3.4. However, some third-party finders do not implement it. 

406 # PEP302 does not refer to find_spec as well. 

407 # See: https://github.com/pylint-dev/astroid/pull/1752/ 

408 if not hasattr(meta_finder, "find_spec"): 

409 continue 

410 

411 spec = meta_finder.find_spec(modname, submodule_path) 

412 if spec: 

413 return ( 

414 meta_finder, 

415 ModuleSpec( 

416 spec.name, 

417 module_type, 

418 spec.origin, 

419 spec.origin, 

420 spec.submodule_search_locations, 

421 ), 

422 ) 

423 

424 raise ImportError(f"No module named {'.'.join(module_parts)}") 

425 

426 

427def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSpec: 

428 """Find a spec for the given module. 

429 

430 :type modpath: list or tuple 

431 :param modpath: 

432 split module's name (i.e name of a module or package split 

433 on '.'), with leading empty strings for explicit relative import 

434 

435 :type path: list or None 

436 :param path: 

437 optional list of path where the module or package should be 

438 searched (use sys.path if nothing or None is given) 

439 

440 :rtype: ModuleSpec 

441 :return: A module spec, which describes how the module was 

442 found and where. 

443 """ 

444 _path = path or sys.path 

445 

446 # Need a copy for not mutating the argument. 

447 modpath = modpath[:] 

448 

449 submodule_path = None 

450 module_parts = modpath[:] 

451 processed: list[str] = [] 

452 

453 while modpath: 

454 modname = modpath.pop(0) 

455 finder, spec = _find_spec_with_path( 

456 _path, modname, module_parts, processed, submodule_path or path 

457 ) 

458 processed.append(modname) 

459 if modpath: 

460 if isinstance(finder, Finder): 

461 submodule_path = finder.contribute_to_path(spec, processed) 

462 # If modname is a package from an editable install, update submodule_path 

463 # so that the next module in the path will be found inside of it using importlib. 

464 # Existence of __name__ is guaranteed by _find_spec_with_path. 

465 elif finder.__name__ in _EditableFinderClasses: # type: ignore[attr-defined] 

466 submodule_path = spec.submodule_search_locations 

467 

468 if spec.type == ModuleType.PKG_DIRECTORY: 

469 spec = spec._replace(submodule_search_locations=submodule_path) 

470 

471 return spec