Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/astroid/modutils.py: 25%

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

275 statements  

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 

5"""Python modules manipulation utility functions. 

6 

7:type PY_SOURCE_EXTS: tuple(str) 

8:var PY_SOURCE_EXTS: list of possible python source file extension 

9 

10:type STD_LIB_DIRS: set of str 

11:var STD_LIB_DIRS: directories where standard modules are located 

12 

13:type BUILTIN_MODULES: dict 

14:var BUILTIN_MODULES: dictionary with builtin module names has key 

15""" 

16 

17from __future__ import annotations 

18 

19import importlib 

20import importlib.machinery 

21import importlib.util 

22import io 

23import itertools 

24import os 

25import sys 

26import sysconfig 

27import types 

28from collections.abc import Callable, Iterable, Sequence 

29from contextlib import redirect_stderr, redirect_stdout 

30from functools import lru_cache 

31from sys import stdlib_module_names 

32 

33from astroid.const import IS_JYTHON 

34from astroid.interpreter._import import spec, util 

35 

36if sys.platform.startswith("win"): 

37 PY_SOURCE_EXTS = ("py", "pyw", "pyi") 

38 PY_SOURCE_EXTS_STUBS_FIRST = ("pyi", "pyw", "py") 

39 PY_COMPILED_EXTS = ("dll", "pyd") 

40else: 

41 PY_SOURCE_EXTS = ("py", "pyi") 

42 PY_SOURCE_EXTS_STUBS_FIRST = ("pyi", "py") 

43 PY_COMPILED_EXTS = ("so",) 

44 

45 

46# TODO: Adding `platstdlib` is a fix for a workaround in virtualenv. At some point we should 

47# revisit whether this is still necessary. See https://github.com/pylint-dev/astroid/pull/1323. 

48STD_LIB_DIRS = {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")} 

49 

50if os.name == "nt": 

51 STD_LIB_DIRS.add(os.path.join(sys.prefix, "dlls")) 

52 try: 

53 # real_prefix is defined when running inside virtual environments, 

54 # created with the **virtualenv** library. 

55 # Deprecated in virtualenv==16.7.9 

56 # See: https://github.com/pypa/virtualenv/issues/1622 

57 STD_LIB_DIRS.add(os.path.join(sys.real_prefix, "dlls")) # type: ignore[attr-defined] 

58 except AttributeError: 

59 # sys.base_exec_prefix is always defined, but in a virtual environment 

60 # created with the stdlib **venv** module, it points to the original 

61 # installation, if the virtual env is activated. 

62 try: 

63 STD_LIB_DIRS.add(os.path.join(sys.base_exec_prefix, "dlls")) 

64 except AttributeError: 

65 pass 

66 

67if os.name == "posix": 

68 # Need the real prefix if we're in a virtualenv, otherwise 

69 # the usual one will do. 

70 # Deprecated in virtualenv==16.7.9 

71 # See: https://github.com/pypa/virtualenv/issues/1622 

72 try: 

73 prefix: str = sys.real_prefix # type: ignore[attr-defined] 

74 except AttributeError: 

75 prefix = sys.prefix 

76 

77 def _posix_path(path: str) -> str: 

78 base_python = f"python{sys.version_info.major}.{sys.version_info.minor}" 

79 return os.path.join(prefix, path, base_python) 

80 

81 STD_LIB_DIRS.add(_posix_path("lib")) 

82 if sys.maxsize > 2**32: 

83 # This tries to fix a problem with /usr/lib64 builds, 

84 # where systems are running both 32-bit and 64-bit code 

85 # on the same machine, which reflects into the places where 

86 # standard library could be found. More details can be found 

87 # here http://bugs.python.org/issue1294959. 

88 # An easy reproducing case would be 

89 # https://github.com/pylint-dev/pylint/issues/712#issuecomment-163178753 

90 STD_LIB_DIRS.add(_posix_path("lib64")) 

91 

92EXT_LIB_DIRS = {sysconfig.get_path("purelib"), sysconfig.get_path("platlib")} 

93BUILTIN_MODULES = dict.fromkeys(sys.builtin_module_names, True) 

94 

95 

96class NoSourceFile(Exception): 

97 """Exception raised when we are not able to get a python 

98 source file for a precompiled file. 

99 """ 

100 

101 

102def _normalize_path(path: str) -> str: 

103 """Resolve symlinks in path and convert to absolute path. 

104 

105 Note that environment variables and ~ in the path need to be expanded in 

106 advance. 

107 

108 This can be cached by using _cache_normalize_path. 

109 """ 

110 return os.path.normcase(os.path.realpath(path)) 

111 

112 

113def _path_from_filename(filename: str, is_jython: bool = IS_JYTHON) -> str: 

114 if not is_jython: 

115 return filename 

116 head, has_pyclass, _ = filename.partition("$py.class") 

117 if has_pyclass: 

118 return head + ".py" 

119 return filename 

120 

121 

122def _handle_blacklist( 

123 blacklist: Sequence[str], dirnames: list[str], filenames: list[str] 

124) -> None: 

125 """Remove files/directories in the black list. 

126 

127 dirnames/filenames are usually from os.walk 

128 """ 

129 for norecurs in blacklist: 

130 if norecurs in dirnames: 

131 dirnames.remove(norecurs) 

132 elif norecurs in filenames: 

133 filenames.remove(norecurs) 

134 

135 

136@lru_cache 

137def _cache_normalize_path_(path: str) -> str: 

138 return _normalize_path(path) 

139 

140 

141def _cache_normalize_path(path: str) -> str: 

142 """Normalize path with caching.""" 

143 # _module_file calls abspath on every path in sys.path every time it's 

144 # called; on a larger codebase this easily adds up to half a second just 

145 # assembling path components. This cache alleviates that. 

146 if not path: # don't cache result for '' 

147 return _normalize_path(path) 

148 return _cache_normalize_path_(path) 

149 

150 

151def load_module_from_name(dotted_name: str) -> types.ModuleType: 

152 """Load a Python module from its name. 

153 

154 :type dotted_name: str 

155 :param dotted_name: python name of a module or package 

156 

157 :raise ImportError: if the module or package is not found 

158 

159 :rtype: module 

160 :return: the loaded module 

161 """ 

162 try: 

163 return sys.modules[dotted_name] 

164 except KeyError: 

165 pass 

166 

167 # Capture and log anything emitted during import to avoid 

168 # contaminating JSON reports in pylint 

169 with ( 

170 redirect_stderr(io.StringIO()) as stderr, 

171 redirect_stdout(io.StringIO()) as stdout, 

172 ): 

173 module = importlib.import_module(dotted_name) 

174 

175 stderr_value = stderr.getvalue() 

176 stdout_value = stdout.getvalue() 

177 if stderr_value or stdout_value: 

178 import logging # pylint: disable=import-outside-toplevel 

179 

180 logger = logging.getLogger(__name__) 

181 if stderr_value: 

182 logger.error( 

183 "Captured stderr while importing %s:\n%s", dotted_name, stderr_value 

184 ) 

185 if stdout_value: 

186 logger.info( 

187 "Captured stdout while importing %s:\n%s", dotted_name, stdout_value 

188 ) 

189 

190 return module 

191 

192 

193def load_module_from_modpath(parts: Sequence[str]) -> types.ModuleType: 

194 """Load a python module from its split name. 

195 

196 :param parts: 

197 python name of a module or package split on '.' 

198 

199 :raise ImportError: if the module or package is not found 

200 

201 :return: the loaded module 

202 """ 

203 return load_module_from_name(".".join(parts)) 

204 

205 

206def load_module_from_file(filepath: str) -> types.ModuleType: 

207 """Load a Python module from it's path. 

208 

209 :type filepath: str 

210 :param filepath: path to the python module or package 

211 

212 :raise ImportError: if the module or package is not found 

213 

214 :rtype: module 

215 :return: the loaded module 

216 """ 

217 modpath = modpath_from_file(filepath) 

218 return load_module_from_modpath(modpath) 

219 

220 

221def check_modpath_has_init(path: str, mod_path: list[str]) -> bool: 

222 """Check there are some __init__.py all along the way.""" 

223 modpath: list[str] = [] 

224 for part in mod_path: 

225 modpath.append(part) 

226 path = os.path.join(path, part) 

227 if not _has_init(path): 

228 old_namespace = util.is_namespace(".".join(modpath)) 

229 if not old_namespace: 

230 return False 

231 return True 

232 

233 

234def _is_subpath(path: str, base: str) -> bool: 

235 path = os.path.normcase(os.path.normpath(path)) 

236 base = os.path.normcase(os.path.normpath(base)) 

237 if not path.startswith(base): 

238 return False 

239 return ( 

240 (len(path) == len(base)) 

241 or (path[len(base)] == os.path.sep) 

242 or (base.endswith(os.path.sep) and path[len(base) - 1] == os.path.sep) 

243 ) 

244 

245 

246def _get_relative_base_path(filename: str, path_to_check: str) -> list[str] | None: 

247 """Extracts the relative mod path of the file to import from. 

248 

249 Check if a file is within the passed in path and if so, returns the 

250 relative mod path from the one passed in. 

251 

252 If the filename is no in path_to_check, returns None 

253 

254 Note this function will look for both abs and realpath of the file, 

255 this allows to find the relative base path even if the file is a 

256 symlink of a file in the passed in path 

257 

258 Examples: 

259 _get_relative_base_path("/a/b/c/d.py", "/a/b") -> ["c","d"] 

260 _get_relative_base_path("/a/b/c/d.py", "/dev") -> None 

261 """ 

262 path_to_check = os.path.normcase(os.path.normpath(path_to_check)) 

263 

264 abs_filename = os.path.abspath(filename) 

265 if _is_subpath(abs_filename, path_to_check): 

266 base_path = os.path.splitext(abs_filename)[0] 

267 relative_base_path = base_path[len(path_to_check) :].lstrip(os.path.sep) 

268 return [pkg for pkg in relative_base_path.split(os.sep) if pkg] 

269 

270 real_filename = os.path.realpath(filename) 

271 if _is_subpath(real_filename, path_to_check): 

272 base_path = os.path.splitext(real_filename)[0] 

273 relative_base_path = base_path[len(path_to_check) :].lstrip(os.path.sep) 

274 return [pkg for pkg in relative_base_path.split(os.sep) if pkg] 

275 

276 return None 

277 

278 

279def modpath_from_file_with_callback( 

280 filename: str, 

281 path: list[str] | None = None, 

282 is_package_cb: Callable[[str, list[str]], bool] | None = None, 

283) -> list[str]: 

284 filename = os.path.expanduser(_path_from_filename(filename)) 

285 paths_to_check = sys.path.copy() 

286 if path: 

287 paths_to_check = path + paths_to_check 

288 for pathname in itertools.chain( 

289 paths_to_check, map(_cache_normalize_path, paths_to_check) 

290 ): 

291 if not pathname: 

292 continue 

293 modpath = _get_relative_base_path(filename, pathname) 

294 if not modpath: 

295 continue 

296 assert is_package_cb is not None 

297 if is_package_cb(pathname, modpath[:-1]): 

298 return modpath 

299 

300 raise ImportError( 

301 "Unable to find module for {} in {}".format( 

302 filename, ", \n".join(paths_to_check) 

303 ) 

304 ) 

305 

306 

307def modpath_from_file(filename: str, path: list[str] | None = None) -> list[str]: 

308 """Get the corresponding split module's name from a filename. 

309 

310 This function will return the name of a module or package split on `.`. 

311 

312 :type filename: str 

313 :param filename: file's path for which we want the module's name 

314 

315 :type Optional[List[str]] path: 

316 Optional list of paths where the module or package should be 

317 searched, additionally to sys.path 

318 

319 :raise ImportError: 

320 if the corresponding module's name has not been found 

321 

322 :rtype: list(str) 

323 :return: the corresponding split module's name 

324 """ 

325 return modpath_from_file_with_callback(filename, path, check_modpath_has_init) 

326 

327 

328def file_from_modpath( 

329 modpath: list[str], 

330 path: Sequence[str] | None = None, 

331 context_file: str | None = None, 

332) -> str | None: 

333 return file_info_from_modpath(modpath, path, context_file).location 

334 

335 

336def file_info_from_modpath( 

337 modpath: list[str], 

338 path: Sequence[str] | None = None, 

339 context_file: str | None = None, 

340) -> spec.ModuleSpec: 

341 """Given a mod path (i.e. split module / package name), return the 

342 corresponding file. 

343 

344 Giving priority to source file over precompiled file if it exists. 

345 

346 :param modpath: 

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

348 on '.') 

349 (this means explicit relative imports that start with dots have 

350 empty strings in this list!) 

351 

352 :param path: 

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

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

355 

356 :param context_file: 

357 context file to consider, necessary if the identifier has been 

358 introduced using a relative import unresolvable in the actual 

359 context (i.e. modutils) 

360 

361 :raise ImportError: if there is no such module in the directory 

362 

363 :return: 

364 the path to the module's file or None if it's an integrated 

365 builtin module such as 'sys' 

366 """ 

367 if context_file is not None: 

368 context: str | None = os.path.dirname(context_file) 

369 else: 

370 context = context_file 

371 if modpath[0] == "xml": 

372 # handle _xmlplus 

373 try: 

374 return _spec_from_modpath(["_xmlplus", *modpath[1:]], path, context) 

375 except ImportError: 

376 return _spec_from_modpath(modpath, path, context) 

377 elif modpath == ["os", "path"]: 

378 # FIXME: currently ignoring search_path... 

379 return spec.ModuleSpec( 

380 name="os.path", 

381 location=os.path.__file__, 

382 type=spec.ModuleType.PY_SOURCE, 

383 ) 

384 return _spec_from_modpath(modpath, path, context) 

385 

386 

387def get_module_part(dotted_name: str, context_file: str | None = None) -> str: 

388 """Given a dotted name return the module part of the name : 

389 

390 >>> get_module_part('astroid.as_string.dump') 

391 'astroid.as_string' 

392 

393 :param dotted_name: full name of the identifier we are interested in 

394 

395 :param context_file: 

396 context file to consider, necessary if the identifier has been 

397 introduced using a relative import unresolvable in the actual 

398 context (i.e. modutils) 

399 

400 :raise ImportError: if there is no such module in the directory 

401 

402 :return: 

403 the module part of the name or None if we have not been able at 

404 all to import the given name 

405 

406 XXX: deprecated, since it doesn't handle package precedence over module 

407 (see #10066) 

408 """ 

409 # os.path trick 

410 if dotted_name.startswith("os.path"): 

411 return "os.path" 

412 parts = dotted_name.split(".") 

413 if context_file is not None: 

414 # first check for builtin module which won't be considered latter 

415 # in that case (path != None) 

416 if parts[0] in BUILTIN_MODULES: 

417 if len(parts) > 2: 

418 raise ImportError(dotted_name) 

419 return parts[0] 

420 # don't use += or insert, we want a new list to be created ! 

421 path: list[str] | None = None 

422 starti = 0 

423 if parts[0] == "": 

424 assert ( 

425 context_file is not None 

426 ), "explicit relative import, but no context_file?" 

427 path = [] # prevent resolving the import non-relatively 

428 starti = 1 

429 # for all further dots: change context 

430 while starti < len(parts) and parts[starti] == "": 

431 starti += 1 

432 assert ( 

433 context_file is not None 

434 ), "explicit relative import, but no context_file?" 

435 context_file = os.path.dirname(context_file) 

436 for i in range(starti, len(parts)): 

437 try: 

438 file_from_modpath( 

439 parts[starti : i + 1], path=path, context_file=context_file 

440 ) 

441 except ImportError: 

442 if i < max(1, len(parts) - 2): 

443 raise 

444 return ".".join(parts[:i]) 

445 return dotted_name 

446 

447 

448def get_module_files( 

449 src_directory: str, blacklist: Sequence[str], list_all: bool = False 

450) -> list[str]: 

451 """Given a package directory return a list of all available python 

452 module's files in the package and its subpackages. 

453 

454 :param src_directory: 

455 path of the directory corresponding to the package 

456 

457 :param blacklist: iterable 

458 list of files or directories to ignore. 

459 

460 :param list_all: 

461 get files from all paths, including ones without __init__.py 

462 

463 :return: 

464 the list of all available python module's files in the package and 

465 its subpackages 

466 """ 

467 files: list[str] = [] 

468 for directory, dirnames, filenames in os.walk(src_directory): 

469 if directory in blacklist: 

470 continue 

471 _handle_blacklist(blacklist, dirnames, filenames) 

472 # check for __init__.py 

473 if not list_all and {"__init__.py", "__init__.pyi"}.isdisjoint(filenames): 

474 dirnames[:] = () 

475 continue 

476 for filename in filenames: 

477 if _is_python_file(filename): 

478 src = os.path.join(directory, filename) 

479 files.append(src) 

480 return files 

481 

482 

483def get_source_file( 

484 filename: str, include_no_ext: bool = False, prefer_stubs: bool = False 

485) -> str: 

486 """Given a python module's file name return the matching source file 

487 name (the filename will be returned identically if it's already an 

488 absolute path to a python source file). 

489 

490 :param filename: python module's file name 

491 

492 :raise NoSourceFile: if no source file exists on the file system 

493 

494 :return: the absolute path of the source file if it exists 

495 """ 

496 filename = os.path.abspath(_path_from_filename(filename)) 

497 base, orig_ext = os.path.splitext(filename) 

498 orig_ext = orig_ext.lstrip(".") 

499 if orig_ext not in PY_SOURCE_EXTS and os.path.exists(f"{base}.{orig_ext}"): 

500 return f"{base}.{orig_ext}" 

501 for ext in PY_SOURCE_EXTS_STUBS_FIRST if prefer_stubs else PY_SOURCE_EXTS: 

502 source_path = f"{base}.{ext}" 

503 if os.path.exists(source_path): 

504 return source_path 

505 if include_no_ext and not orig_ext and os.path.exists(base): 

506 return base 

507 raise NoSourceFile(filename) 

508 

509 

510def is_python_source(filename: str | None) -> bool: 

511 """Return: True if the filename is a python source file.""" 

512 if not filename: 

513 return False 

514 return os.path.splitext(filename)[1][1:] in PY_SOURCE_EXTS 

515 

516 

517def is_stdlib_module(modname: str) -> bool: 

518 """Return: True if the modname is in the standard library""" 

519 return modname.split(".")[0] in stdlib_module_names 

520 

521 

522def module_in_path(modname: str, path: str | Iterable[str]) -> bool: 

523 """Try to determine if a module is imported from one of the specified paths 

524 

525 :param modname: name of the module 

526 

527 :param path: paths to consider 

528 

529 :return: 

530 true if the module: 

531 - is located on the path listed in one of the directory in `paths` 

532 """ 

533 

534 modname = modname.split(".")[0] 

535 try: 

536 filename = file_from_modpath([modname]) 

537 except ImportError: 

538 # Import failed, we can't check path if we don't know it 

539 return False 

540 

541 if filename is None: 

542 # No filename likely means it's compiled in, or potentially a namespace 

543 return False 

544 filename = _normalize_path(filename) 

545 

546 if isinstance(path, str): 

547 return filename.startswith(_cache_normalize_path(path)) 

548 

549 return any(filename.startswith(_cache_normalize_path(entry)) for entry in path) 

550 

551 

552def is_relative(modname: str, from_file: str) -> bool: 

553 """Return true if the given module name is relative to the given 

554 file name. 

555 

556 :param modname: name of the module we are interested in 

557 

558 :param from_file: 

559 path of the module from which modname has been imported 

560 

561 :return: 

562 true if the module has been imported relatively to `from_file` 

563 """ 

564 if not os.path.isdir(from_file): 

565 from_file = os.path.dirname(from_file) 

566 if from_file in sys.path: 

567 return False 

568 return bool( 

569 importlib.machinery.PathFinder.find_spec( 

570 modname.split(".", maxsplit=1)[0], [from_file] 

571 ) 

572 ) 

573 

574 

575@lru_cache(maxsize=1024) 

576def cached_os_path_isfile(path: str | os.PathLike[str]) -> bool: 

577 """A cached version of os.path.isfile that helps avoid repetitive I/O""" 

578 return os.path.isfile(path) 

579 

580 

581# internal only functions ##################################################### 

582 

583 

584def _spec_from_modpath( 

585 modpath: list[str], 

586 path: Sequence[str] | None = None, 

587 context: str | None = None, 

588) -> spec.ModuleSpec: 

589 """Given a mod path (i.e. split module / package name), return the 

590 corresponding spec. 

591 

592 this function is used internally, see `file_from_modpath`'s 

593 documentation for more information 

594 """ 

595 assert modpath 

596 location = None 

597 if context is not None: 

598 try: 

599 found_spec = spec.find_spec(modpath, [context]) 

600 location = found_spec.location 

601 except ImportError: 

602 found_spec = spec.find_spec(modpath, path) 

603 location = found_spec.location 

604 else: 

605 found_spec = spec.find_spec(modpath, path) 

606 if found_spec.type == spec.ModuleType.PY_COMPILED: 

607 try: 

608 assert found_spec.location is not None 

609 location = get_source_file(found_spec.location) 

610 return found_spec._replace( 

611 location=location, type=spec.ModuleType.PY_SOURCE 

612 ) 

613 except NoSourceFile: 

614 return found_spec._replace(location=location) 

615 elif found_spec.type == spec.ModuleType.C_BUILTIN: 

616 # integrated builtin module 

617 return found_spec._replace(location=None) 

618 elif found_spec.type == spec.ModuleType.PKG_DIRECTORY: 

619 assert found_spec.location is not None 

620 location = _has_init(found_spec.location) 

621 return found_spec._replace(location=location, type=spec.ModuleType.PY_SOURCE) 

622 return found_spec 

623 

624 

625def _is_python_file(filename: str) -> bool: 

626 """Return true if the given filename should be considered as a python file. 

627 

628 .pyc and .pyo are ignored 

629 """ 

630 return filename.endswith((".py", ".pyi", ".so", ".pyd", ".pyw")) 

631 

632 

633@lru_cache(maxsize=1024) 

634def _has_init(directory: str) -> str | None: 

635 """If the given directory has a valid __init__ file, return its path, 

636 else return None. 

637 """ 

638 mod_or_pack = os.path.join(directory, "__init__") 

639 for ext in (*PY_SOURCE_EXTS, "pyc", "pyo"): 

640 if os.path.exists(mod_or_pack + "." + ext): 

641 return mod_or_pack + "." + ext 

642 return None 

643 

644 

645def is_namespace(specobj: spec.ModuleSpec) -> bool: 

646 return specobj.type == spec.ModuleType.PY_NAMESPACE 

647 

648 

649def is_directory(specobj: spec.ModuleSpec) -> bool: 

650 return specobj.type == spec.ModuleType.PKG_DIRECTORY 

651 

652 

653def is_module_name_part_of_extension_package_whitelist( 

654 module_name: str, package_whitelist: set[str] 

655) -> bool: 

656 """ 

657 Returns True if one part of the module name is in the package whitelist. 

658 

659 >>> is_module_name_part_of_extension_package_whitelist('numpy.core.umath', {'numpy'}) 

660 True 

661 """ 

662 parts = module_name.split(".") 

663 return any( 

664 ".".join(parts[:x]) in package_whitelist for x in range(1, len(parts) + 1) 

665 )