Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/_pytest/pathlib.py: 19%

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

510 statements  

1from __future__ import annotations 

2 

3from collections.abc import Callable 

4from collections.abc import Iterable 

5from collections.abc import Iterator 

6import contextlib 

7from enum import Enum 

8from errno import EBADF 

9from errno import ELOOP 

10from errno import ENOENT 

11from errno import ENOTDIR 

12import fnmatch 

13from functools import partial 

14from importlib.machinery import ModuleSpec 

15from importlib.machinery import PathFinder 

16import importlib.util 

17import itertools 

18import os 

19from os.path import expanduser 

20from os.path import expandvars 

21from os.path import isabs 

22from os.path import sep 

23from pathlib import Path 

24from pathlib import PurePath 

25from posixpath import sep as posix_sep 

26import shutil 

27import stat 

28import sys 

29import types 

30from types import ModuleType 

31from typing import Any 

32from typing import TypeVar 

33import uuid 

34import warnings 

35 

36from _pytest.compat import assert_never 

37from _pytest.outcomes import skip 

38from _pytest.warning_types import PytestWarning 

39 

40 

41if sys.version_info < (3, 11): 

42 from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader 

43else: 

44 from importlib.machinery import NamespaceLoader 

45 

46LOCK_TIMEOUT = 60 * 60 * 24 * 3 

47 

48_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath) 

49 

50# The following function, variables and comments were 

51# copied from cpython 3.9 Lib/pathlib.py file. 

52 

53# EBADF - guard against macOS `stat` throwing EBADF 

54_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP) 

55 

56_IGNORED_WINERRORS = ( 

57 21, # ERROR_NOT_READY - drive exists but is not accessible 

58 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself 

59) 

60 

61 

62def _ignore_error(exception: Exception) -> bool: 

63 return ( 

64 getattr(exception, "errno", None) in _IGNORED_ERRORS 

65 or getattr(exception, "winerror", None) in _IGNORED_WINERRORS 

66 ) 

67 

68 

69def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: 

70 return path.joinpath(".lock") 

71 

72 

73def _chmod_rwx(p: str) -> bool: 

74 """Grant owner sufficient permissions for deletion. 

75 

76 Directories get ``S_IRWXU`` (read+write+exec for traversal). 

77 Regular files get ``S_IRUSR | S_IWUSR`` only, to avoid making 

78 non-executable files executable as a side effect. 

79 

80 Returns True if permissions were actually changed, False if they were 

81 already sufficient or couldn't be changed. 

82 """ 

83 try: 

84 old_mode = os.stat(p).st_mode 

85 except OSError: 

86 # Path may have been removed concurrently, or be inaccessible. 

87 return False 

88 perm_mode = stat.S_IMODE(old_mode) 

89 bits = stat.S_IRWXU if stat.S_ISDIR(old_mode) else stat.S_IRUSR | stat.S_IWUSR 

90 new_mode = perm_mode | bits 

91 if perm_mode == new_mode: 

92 return False 

93 try: 

94 os.chmod(p, new_mode) 

95 except OSError: 

96 return False 

97 return True 

98 

99 

100def on_rm_rf_error( 

101 func: Callable[..., Any] | None, 

102 path: str, 

103 excinfo: BaseException 

104 | tuple[type[BaseException], BaseException, types.TracebackType | None], 

105 *, 

106 start_path: Path, 

107) -> bool: 

108 """Handle known read-only errors during rmtree. 

109 

110 The returned value is used only by our own tests. 

111 """ 

112 if isinstance(excinfo, BaseException): 

113 exc = excinfo 

114 else: 

115 exc = excinfo[1] 

116 

117 # Another process removed the file in the middle of the "rm_rf" (xdist for example). 

118 # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018 

119 if isinstance(exc, FileNotFoundError): 

120 return False 

121 

122 if not isinstance(exc, PermissionError): 

123 warnings.warn( 

124 PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}") 

125 ) 

126 return False 

127 

128 p = Path(path) 

129 

130 if func in (os.open, os.scandir): 

131 # Directory traversal failed (e.g. missing S_IXUSR). Fix permissions 

132 # on the path and its parent (bounded by start_path), then remove it 

133 # ourselves since rmtree skips entries after the error handler returns. 

134 # See: https://github.com/pytest-dev/pytest/issues/7940 

135 parent_changed = False 

136 parent = p.parent 

137 # Never chmod outside the tree rooted at start_path. 

138 if parent not in (p, start_path): 

139 parent_changed = _chmod_rwx(str(parent)) 

140 path_changed = _chmod_rwx(str(p)) 

141 if not (parent_changed or path_changed): 

142 return False 

143 if p.is_dir(): 

144 rm_rf(p) 

145 else: 

146 try: 

147 os.unlink(str(p)) 

148 except OSError: 

149 return False 

150 return True 

151 

152 if func not in (os.rmdir, os.remove, os.unlink): 

153 warnings.warn( 

154 PytestWarning( 

155 f"(rm_rf) unknown function {func} when removing {path}:\n{type(exc)}: {exc}" 

156 ) 

157 ) 

158 return False 

159 

160 # Chmod + retry. 

161 # For files, we need to recursively go upwards in the directories to 

162 # ensure they all are also accessible and writable. 

163 if p.is_file(): 

164 for parent in p.parents: # pragma: no branch 

165 _chmod_rwx(str(parent)) 

166 # Stop when we reach the original path passed to rm_rf. 

167 if parent == start_path: 

168 break 

169 _chmod_rwx(str(path)) 

170 

171 func(path) 

172 return True 

173 

174 

175def ensure_extended_length_path(path: Path) -> Path: 

176 """Get the extended-length version of a path (Windows). 

177 

178 On Windows, by default, the maximum length of a path (MAX_PATH) is 260 

179 characters, and operations on paths longer than that fail. But it is possible 

180 to overcome this by converting the path to "extended-length" form before 

181 performing the operation: 

182 https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation 

183 

184 On Windows, this function returns the extended-length absolute version of path. 

185 On other platforms it returns path unchanged. 

186 """ 

187 if sys.platform.startswith("win32"): 

188 path = path.resolve() 

189 path = Path(get_extended_length_path_str(str(path))) 

190 return path 

191 

192 

193def get_extended_length_path_str(path: str) -> str: 

194 """Convert a path to a Windows extended length path.""" 

195 long_path_prefix = "\\\\?\\" 

196 unc_long_path_prefix = "\\\\?\\UNC\\" 

197 if path.startswith((long_path_prefix, unc_long_path_prefix)): 

198 return path 

199 # UNC 

200 if path.startswith("\\\\"): 

201 return unc_long_path_prefix + path[2:] 

202 return long_path_prefix + path 

203 

204 

205def rm_rf(path: Path) -> None: 

206 """Remove the path contents recursively, even if some elements 

207 are read-only.""" 

208 path = ensure_extended_length_path(path) 

209 onerror = partial(on_rm_rf_error, start_path=path) 

210 if sys.version_info >= (3, 12): 

211 shutil.rmtree(str(path), onexc=onerror) 

212 else: 

213 shutil.rmtree(str(path), onerror=onerror) 

214 

215 

216def find_prefixed(root: Path, prefix: str) -> Iterator[os.DirEntry[str]]: 

217 """Find all elements in root that begin with the prefix, case-insensitive.""" 

218 l_prefix = prefix.lower() 

219 for x in os.scandir(root): 

220 if x.name.lower().startswith(l_prefix): 

221 yield x 

222 

223 

224def extract_suffixes(iter: Iterable[os.DirEntry[str]], prefix: str) -> Iterator[str]: 

225 """Return the parts of the paths following the prefix. 

226 

227 :param iter: Iterator over path names. 

228 :param prefix: Expected prefix of the path names. 

229 """ 

230 p_len = len(prefix) 

231 for entry in iter: 

232 yield entry.name[p_len:] 

233 

234 

235def find_suffixes(root: Path, prefix: str) -> Iterator[str]: 

236 """Combine find_prefixes and extract_suffixes.""" 

237 return extract_suffixes(find_prefixed(root, prefix), prefix) 

238 

239 

240def parse_num(maybe_num: str) -> int: 

241 """Parse number path suffixes, returns -1 on error.""" 

242 try: 

243 return int(maybe_num) 

244 except ValueError: 

245 return -1 

246 

247 

248def _force_symlink(root: Path, target: str | PurePath, link_to: str | Path) -> None: 

249 """Helper to create the current symlink. 

250 

251 It's full of race conditions that are reasonably OK to ignore 

252 for the context of best effort linking to the latest test run. 

253 

254 The presumption being that in case of much parallelism 

255 the inaccuracy is going to be acceptable. 

256 """ 

257 current_symlink = root.joinpath(target) 

258 try: 

259 current_symlink.unlink() 

260 except OSError: 

261 pass 

262 try: 

263 current_symlink.symlink_to(link_to) 

264 except Exception: 

265 pass 

266 

267 

268def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path: 

269 """Create a directory with an increased number as suffix for the given prefix.""" 

270 for i in range(10): 

271 # try up to 10 times to create the directory 

272 max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) 

273 new_number = max_existing + 1 

274 new_path = root.joinpath(f"{prefix}{new_number}") 

275 try: 

276 new_path.mkdir(mode=mode) 

277 except Exception: 

278 pass 

279 else: 

280 _force_symlink(root, prefix + "current", new_path) 

281 return new_path 

282 else: 

283 raise OSError( 

284 "could not create numbered dir with prefix " 

285 f"{prefix} in {root} after 10 tries" 

286 ) 

287 

288 

289def create_cleanup_lock(p: Path) -> Path: 

290 """Create a lock to prevent premature directory cleanup.""" 

291 lock_path = get_lock_path(p) 

292 try: 

293 fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) 

294 except FileExistsError as e: 

295 raise OSError(f"cannot create lockfile in {p}") from e 

296 else: 

297 pid = os.getpid() 

298 spid = str(pid).encode() 

299 os.write(fd, spid) 

300 os.close(fd) 

301 if not lock_path.is_file(): 

302 raise OSError("lock path got renamed after successful creation") 

303 return lock_path 

304 

305 

306def register_cleanup_lock_removal(lock_path: Path, register: Any) -> Any: 

307 """Register a cleanup function for removing a lock.""" 

308 pid = os.getpid() 

309 

310 def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None: 

311 current_pid = os.getpid() 

312 if current_pid != original_pid: 

313 # fork 

314 return 

315 try: 

316 lock_path.unlink() 

317 except OSError: 

318 pass 

319 

320 return register(cleanup_on_exit) 

321 

322 

323def maybe_delete_a_numbered_dir(path: Path) -> None: 

324 """Remove a numbered directory if its lock can be obtained and it does 

325 not seem to be in use.""" 

326 path = ensure_extended_length_path(path) 

327 lock_path = None 

328 try: 

329 lock_path = create_cleanup_lock(path) 

330 parent = path.parent 

331 

332 garbage = parent.joinpath(f"garbage-{uuid.uuid4()}") 

333 path.rename(garbage) 

334 rm_rf(garbage) 

335 except OSError: 

336 # known races: 

337 # * other process did a cleanup at the same time 

338 # * deletable directory was found 

339 # * process cwd (Windows) 

340 return 

341 finally: 

342 # If we created the lock, ensure we remove it even if we failed 

343 # to properly remove the numbered dir. 

344 if lock_path is not None: 

345 try: 

346 lock_path.unlink() 

347 except OSError: 

348 pass 

349 

350 

351def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool: 

352 """Check if `path` is deletable based on whether the lock file is expired.""" 

353 if path.is_symlink(): 

354 return False 

355 lock = get_lock_path(path) 

356 try: 

357 if not lock.is_file(): 

358 return True 

359 except OSError: 

360 # we might not have access to the lock file at all, in this case assume 

361 # we don't have access to the entire directory (#7491). 

362 return False 

363 try: 

364 lock_time = lock.stat().st_mtime 

365 except Exception: 

366 return False 

367 else: 

368 if lock_time < consider_lock_dead_if_created_before: 

369 # We want to ignore any errors while trying to remove the lock such as: 

370 # - PermissionDenied, like the file permissions have changed since the lock creation; 

371 # - FileNotFoundError, in case another pytest process got here first; 

372 # and any other cause of failure. 

373 with contextlib.suppress(OSError): 

374 lock.unlink() 

375 return True 

376 return False 

377 

378 

379def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None: 

380 """Try to cleanup a directory if we can ensure it's deletable.""" 

381 if ensure_deletable(path, consider_lock_dead_if_created_before): 

382 maybe_delete_a_numbered_dir(path) 

383 

384 

385def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]: 

386 """List candidates for numbered directories to be removed - follows py.path.""" 

387 max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) 

388 max_delete = max_existing - keep 

389 entries = find_prefixed(root, prefix) 

390 entries, entries2 = itertools.tee(entries) 

391 numbers = map(parse_num, extract_suffixes(entries2, prefix)) 

392 for entry, number in zip(entries, numbers, strict=True): 

393 if number <= max_delete: 

394 yield Path(entry) 

395 

396 

397def cleanup_dead_symlinks(root: Path) -> None: 

398 for left_dir in root.iterdir(): 

399 if left_dir.is_symlink(): 

400 if not left_dir.resolve().exists(): 

401 left_dir.unlink() 

402 

403 

404def cleanup_numbered_dir( 

405 root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float 

406) -> None: 

407 """Cleanup for lock driven numbered directories.""" 

408 if not root.exists(): 

409 return 

410 for path in cleanup_candidates(root, prefix, keep): 

411 try_cleanup(path, consider_lock_dead_if_created_before) 

412 for path in root.glob("garbage-*"): 

413 try_cleanup(path, consider_lock_dead_if_created_before) 

414 

415 cleanup_dead_symlinks(root) 

416 

417 

418def make_numbered_dir_with_cleanup( 

419 *, 

420 root: Path, 

421 prefix: str, 

422 mode: int, 

423 keep: int, 

424 lock_timeout: float, 

425 register: Any, 

426) -> Path: 

427 """Create a numbered dir and register its cleanup. 

428 

429 Similar to make_numbered_dir, but also maintains a lock file indicating that 

430 the directory is currently in use, and registers the cleanup of the lock and 

431 of stale numbered directories. 

432 

433 :param keep: 

434 The number of sessions to retain the directory. 

435 :param lock_timeout: 

436 In case of a crash, the lock remains "stuck". The timeout is a time 

437 limit after which the lock is considered stale and can be removed. 

438 :param register: 

439 Called as register(cleanup_func, params...). Should schedule to call 

440 passed cleanup functions on session finish. 

441 """ 

442 e = None 

443 for i in range(10): 

444 try: 

445 p = make_numbered_dir(root, prefix, mode) 

446 # Only lock the current dir when keep is not 0 

447 if keep != 0: 

448 lock_path = create_cleanup_lock(p) 

449 register_cleanup_lock_removal(lock_path, register) 

450 except Exception as exc: 

451 e = exc 

452 else: 

453 consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout 

454 # Register a cleanup for program exit 

455 register( 

456 cleanup_numbered_dir, 

457 root, 

458 prefix, 

459 keep, 

460 consider_lock_dead_if_created_before, 

461 ) 

462 return p 

463 assert e is not None 

464 raise e 

465 

466 

467def resolve_from_str(input: str, rootpath: Path) -> Path: 

468 input = expanduser(input) 

469 input = expandvars(input) 

470 if isabs(input): 

471 return Path(input) 

472 else: 

473 return rootpath.joinpath(input) 

474 

475 

476def fnmatch_ex(pattern: str, path: str | os.PathLike[str]) -> bool: 

477 """A port of FNMatcher from py.path.common which works with PurePath() instances. 

478 

479 The difference between this algorithm and PurePath.match() is that the 

480 latter matches "**" glob expressions for each part of the path, while 

481 this algorithm uses the whole path instead. 

482 

483 For example: 

484 "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" 

485 with this algorithm, but not with PurePath.match(). 

486 

487 This algorithm was ported to keep backward-compatibility with existing 

488 settings which assume paths match according this logic. 

489 

490 References: 

491 * https://bugs.python.org/issue29249 

492 * https://bugs.python.org/issue34731 

493 """ 

494 path = PurePath(path) 

495 iswin32 = sys.platform.startswith("win") 

496 

497 if iswin32 and sep not in pattern and posix_sep in pattern: 

498 # Running on Windows, the pattern has no Windows path separators, 

499 # and the pattern has one or more Posix path separators. Replace 

500 # the Posix path separators with the Windows path separator. 

501 pattern = pattern.replace(posix_sep, sep) 

502 

503 if sep not in pattern: 

504 name = path.name 

505 else: 

506 name = str(path) 

507 if path.is_absolute() and not os.path.isabs(pattern): 

508 pattern = f"*{os.sep}{pattern}" 

509 return fnmatch.fnmatch(name, pattern) 

510 

511 

512def parts(s: str) -> set[str]: 

513 parts = s.split(sep) 

514 return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} 

515 

516 

517def symlink_or_skip( 

518 src: os.PathLike[str] | str, 

519 dst: os.PathLike[str] | str, 

520 **kwargs: Any, 

521) -> None: 

522 """Make a symlink, or skip the test in case symlinks are not supported.""" 

523 try: 

524 os.symlink(src, dst, **kwargs) 

525 except OSError as e: 

526 skip(f"symlinks not supported: {e}") 

527 

528 

529class ImportMode(Enum): 

530 """Possible values for `mode` parameter of `import_path`.""" 

531 

532 prepend = "prepend" 

533 append = "append" 

534 importlib = "importlib" 

535 

536 

537class ImportPathMismatchError(ImportError): 

538 """Raised on import_path() if there is a mismatch of __file__'s. 

539 

540 This can happen when `import_path` is called multiple times with different filenames that has 

541 the same basename but reside in packages 

542 (for example "/tests1/test_foo.py" and "/tests2/test_foo.py"). 

543 """ 

544 

545 

546def import_path( 

547 path: str | os.PathLike[str], 

548 *, 

549 mode: str | ImportMode = ImportMode.prepend, 

550 root: Path, 

551 consider_namespace_packages: bool, 

552) -> ModuleType: 

553 """ 

554 Import and return a module from the given path, which can be a file (a module) or 

555 a directory (a package). 

556 

557 :param path: 

558 Path to the file to import. 

559 

560 :param mode: 

561 Controls the underlying import mechanism that will be used: 

562 

563 * ImportMode.prepend: the directory containing the module (or package, taking 

564 `__init__.py` files into account) will be put at the *start* of `sys.path` before 

565 being imported with `importlib.import_module`. 

566 

567 * ImportMode.append: same as `prepend`, but the directory will be appended 

568 to the end of `sys.path`, if not already in `sys.path`. 

569 

570 * ImportMode.importlib: uses more fine control mechanisms provided by `importlib` 

571 to import the module, which avoids having to muck with `sys.path` at all. It effectively 

572 allows having same-named test modules in different places. 

573 

574 :param root: 

575 Used as an anchor when mode == ImportMode.importlib to obtain 

576 a unique name for the module being imported so it can safely be stored 

577 into ``sys.modules``. 

578 

579 :param consider_namespace_packages: 

580 If True, consider namespace packages when resolving module names. 

581 

582 :raises ImportPathMismatchError: 

583 If after importing the given `path` and the module `__file__` 

584 are different. Only raised in `prepend` and `append` modes. 

585 """ 

586 path = Path(path) 

587 mode = ImportMode(mode) 

588 

589 if not path.exists(): 

590 raise ImportError(path) 

591 

592 if mode is ImportMode.importlib: 

593 # Try to import this module using the standard import mechanisms, but 

594 # without touching sys.path. 

595 try: 

596 _, module_name = resolve_pkg_root_and_module_name( 

597 path, consider_namespace_packages=consider_namespace_packages 

598 ) 

599 except CouldNotResolvePathError: 

600 pass 

601 else: 

602 # If the given module name is already in sys.modules, do not import it again. 

603 with contextlib.suppress(KeyError): 

604 return sys.modules[module_name] 

605 

606 mod = _import_module_using_spec(module_name, path, insert_modules=False) 

607 if mod is not None: 

608 return mod 

609 

610 # Could not import the module with the current sys.path, so we fall back 

611 # to importing the file as a single module, not being a part of a package. 

612 module_name = module_name_from_path(path, root) 

613 with contextlib.suppress(KeyError): 

614 return sys.modules[module_name] 

615 

616 mod = _import_module_using_spec(module_name, path, insert_modules=True) 

617 if mod is None: 

618 raise ImportError(f"Can't find module {module_name} at location {path}") 

619 return mod 

620 

621 try: 

622 pkg_root, module_name = resolve_pkg_root_and_module_name( 

623 path, consider_namespace_packages=consider_namespace_packages 

624 ) 

625 except CouldNotResolvePathError: 

626 pkg_root, module_name = path.parent, path.stem 

627 

628 # Change sys.path permanently: restoring it at the end of this function would cause surprising 

629 # problems because of delayed imports: for example, a conftest.py file imported by this function 

630 # might have local imports, which would fail at runtime if we restored sys.path. 

631 if mode is ImportMode.append: 

632 if str(pkg_root) not in sys.path: 

633 sys.path.append(str(pkg_root)) 

634 elif mode is ImportMode.prepend: 

635 if str(pkg_root) != sys.path[0]: 

636 sys.path.insert(0, str(pkg_root)) 

637 else: 

638 assert_never(mode) 

639 

640 importlib.import_module(module_name) 

641 

642 mod = sys.modules[module_name] 

643 if path.name == "__init__.py": 

644 return mod 

645 

646 ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "") 

647 if ignore != "1": 

648 module_file = mod.__file__ 

649 if module_file is None: 

650 raise ImportPathMismatchError(module_name, module_file, path) 

651 

652 if module_file.endswith((".pyc", ".pyo")): 

653 module_file = module_file[:-1] 

654 module_file = module_file.removesuffix(os.sep + "__init__.py") 

655 

656 try: 

657 is_same = _is_same(str(path), module_file) 

658 except FileNotFoundError: 

659 is_same = False 

660 

661 if not is_same: 

662 raise ImportPathMismatchError(module_name, module_file, path) 

663 

664 return mod 

665 

666 

667def _import_module_using_spec( 

668 module_name: str, module_path: Path, *, insert_modules: bool 

669) -> ModuleType | None: 

670 """ 

671 Tries to import a module by its canonical name, path, and its parent location. 

672 

673 :param module_name: 

674 The expected module name, will become the key of `sys.modules`. 

675 

676 :param module_path: 

677 The file path of the module, for example `/foo/bar/test_demo.py`. 

678 If module is a package, pass the path to the `__init__.py` of the package. 

679 If module is a namespace package, pass directory path. 

680 

681 :param insert_modules: 

682 If True, will call `insert_missing_modules` to create empty intermediate modules 

683 with made-up module names (when importing test files not reachable from `sys.path`). 

684 

685 Example 1 of parent_module_*: 

686 

687 module_name: "a.b.c.demo" 

688 module_path: Path("a/b/c/demo.py") 

689 if "a.b.c" is package ("a/b/c/__init__.py" exists), then 

690 parent_module_name: "a.b.c" 

691 parent_module_path: Path("a/b/c/__init__.py") 

692 else: 

693 parent_module_name: "a.b.c" 

694 parent_module_path: Path("a/b/c") 

695 

696 Example 2 of parent_module_*: 

697 

698 module_name: "a.b.c" 

699 module_path: Path("a/b/c/__init__.py") 

700 if "a.b" is package ("a/b/__init__.py" exists), then 

701 parent_module_name: "a.b" 

702 parent_module_path: Path("a/b/__init__.py") 

703 else: 

704 parent_module_name: "a.b" 

705 parent_module_path: Path("a/b/") 

706 """ 

707 # Attempt to import the parent module, seems is our responsibility: 

708 # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311 

709 parent_module_name, _, name = module_name.rpartition(".") 

710 parent_module: ModuleType | None = None 

711 if parent_module_name: 

712 parent_module = sys.modules.get(parent_module_name) 

713 # If the parent_module lacks the `__path__` attribute, AttributeError when finding a submodule's spec, 

714 # requiring re-import according to the path. 

715 need_reimport = not hasattr(parent_module, "__path__") 

716 if parent_module is None or need_reimport: 

717 # Get parent_location based on location, get parent_path based on path. 

718 if module_path.name == "__init__.py": 

719 # If the current module is in a package, 

720 # need to leave the package first and then enter the parent module. 

721 parent_module_path = module_path.parent.parent 

722 else: 

723 parent_module_path = module_path.parent 

724 

725 if (parent_module_path / "__init__.py").is_file(): 

726 # If the parent module is a package, loading by __init__.py file. 

727 parent_module_path = parent_module_path / "__init__.py" 

728 

729 parent_module = _import_module_using_spec( 

730 parent_module_name, 

731 parent_module_path, 

732 insert_modules=insert_modules, 

733 ) 

734 

735 # Checking with sys.meta_path first in case one of its hooks can import this module, 

736 # such as our own assertion-rewrite hook. 

737 find_spec_path = [str(module_path.parent)] 

738 for meta_importer in sys.meta_path: 

739 spec = meta_importer.find_spec(module_name, find_spec_path) 

740 

741 if spec_matches_module_path(spec, module_path): 

742 break 

743 else: 

744 loader = None 

745 if module_path.is_dir(): 

746 # The `spec_from_file_location` matches a loader based on the file extension by default. 

747 # For a namespace package, need to manually specify a loader. 

748 loader = NamespaceLoader(name, module_path, PathFinder()) # type: ignore[arg-type] 

749 

750 spec = importlib.util.spec_from_file_location( 

751 module_name, str(module_path), loader=loader 

752 ) 

753 

754 if spec_matches_module_path(spec, module_path): 

755 assert spec is not None 

756 # Find spec and import this module. 

757 mod = importlib.util.module_from_spec(spec) 

758 sys.modules[module_name] = mod 

759 spec.loader.exec_module(mod) # type: ignore[union-attr] 

760 

761 # Set this module as an attribute of the parent module (#12194). 

762 if parent_module is not None: 

763 setattr(parent_module, name, mod) 

764 

765 if insert_modules: 

766 insert_missing_modules(sys.modules, module_name) 

767 return mod 

768 

769 return None 

770 

771 

772def spec_matches_module_path(module_spec: ModuleSpec | None, module_path: Path) -> bool: 

773 """Return true if the given ModuleSpec can be used to import the given module path.""" 

774 if module_spec is None: 

775 return False 

776 

777 if module_spec.origin: 

778 return Path(module_spec.origin) == module_path 

779 

780 # Compare the path with the `module_spec.submodule_Search_Locations` in case 

781 # the module is part of a namespace package. 

782 # https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.submodule_search_locations 

783 if module_spec.submodule_search_locations: # can be None. 

784 for path in module_spec.submodule_search_locations: 

785 if Path(path) == module_path: 

786 return True 

787 

788 return False 

789 

790 

791# Implement a special _is_same function on Windows which returns True if the two filenames 

792# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678). 

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

794 

795 def _is_same(f1: str, f2: str) -> bool: 

796 return Path(f1) == Path(f2) or os.path.samefile(f1, f2) 

797 

798else: 

799 

800 def _is_same(f1: str, f2: str) -> bool: 

801 return os.path.samefile(f1, f2) 

802 

803 

804def module_name_from_path(path: Path, root: Path) -> str: 

805 """ 

806 Return a dotted module name based on the given path, anchored on root. 

807 

808 For example: path="projects/src/tests/test_foo.py" and root="/projects", the 

809 resulting module name will be "src.tests.test_foo". 

810 """ 

811 path = path.with_suffix("") 

812 try: 

813 relative_path = path.relative_to(root) 

814 except ValueError: 

815 # If we can't get a relative path to root, use the full path, except 

816 # for the first part ("d:\\" or "/" depending on the platform, for example). 

817 path_parts = path.parts[1:] 

818 else: 

819 # Use the parts for the relative path to the root path. 

820 path_parts = relative_path.parts 

821 

822 # Module name for packages do not contain the __init__ file, unless 

823 # the `__init__.py` file is at the root. 

824 if len(path_parts) >= 2 and path_parts[-1] == "__init__": 

825 path_parts = path_parts[:-1] 

826 

827 # Module names cannot contain ".", normalize them to "_". This prevents 

828 # a directory having a "." in the name (".env.310" for example) causing extra intermediate modules. 

829 # Also, important to replace "." at the start of paths, as those are considered relative imports. 

830 path_parts = tuple(x.replace(".", "_") for x in path_parts) 

831 

832 return ".".join(path_parts) 

833 

834 

835def insert_missing_modules(modules: dict[str, ModuleType], module_name: str) -> None: 

836 """ 

837 Used by ``import_path`` to create intermediate modules when using mode=importlib. 

838 

839 When we want to import a module as "src.tests.test_foo" for example, we need 

840 to create empty modules "src" and "src.tests" after inserting "src.tests.test_foo", 

841 otherwise "src.tests.test_foo" is not importable by ``__import__``. 

842 """ 

843 module_parts = module_name.split(".") 

844 while module_name: 

845 parent_module_name, _, child_name = module_name.rpartition(".") 

846 if parent_module_name: 

847 parent_module = modules.get(parent_module_name) 

848 if parent_module is None: 

849 try: 

850 # If sys.meta_path is empty, calling import_module will issue 

851 # a warning and raise ModuleNotFoundError. To avoid the 

852 # warning, we check sys.meta_path explicitly and raise the error 

853 # ourselves to fall back to creating a dummy module. 

854 if not sys.meta_path: 

855 raise ModuleNotFoundError 

856 parent_module = importlib.import_module(parent_module_name) 

857 except ModuleNotFoundError: 

858 parent_module = ModuleType( 

859 module_name, 

860 doc="Empty module created by pytest's importmode=importlib.", 

861 ) 

862 modules[parent_module_name] = parent_module 

863 

864 # Add child attribute to the parent that can reference the child 

865 # modules. 

866 if not hasattr(parent_module, child_name): 

867 setattr(parent_module, child_name, modules[module_name]) 

868 

869 module_parts.pop(-1) 

870 module_name = ".".join(module_parts) 

871 

872 

873def resolve_package_path(path: Path) -> Path | None: 

874 """Return the Python package path by looking for the last 

875 directory upwards which still contains an __init__.py. 

876 

877 Returns None if it cannot be determined. 

878 """ 

879 result = None 

880 for parent in itertools.chain((path,), path.parents): 

881 if parent.is_dir(): 

882 if not (parent / "__init__.py").is_file(): 

883 break 

884 if not parent.name.isidentifier(): 

885 break 

886 result = parent 

887 return result 

888 

889 

890def resolve_pkg_root_and_module_name( 

891 path: Path, *, consider_namespace_packages: bool = False 

892) -> tuple[Path, str]: 

893 """ 

894 Return the path to the directory of the root package that contains the 

895 given Python file, and its module name: 

896 

897 src/ 

898 app/ 

899 __init__.py 

900 core/ 

901 __init__.py 

902 models.py 

903 

904 Passing the full path to `models.py` will yield Path("src") and "app.core.models". 

905 

906 If consider_namespace_packages is True, then we additionally check upwards in the hierarchy 

907 for namespace packages: 

908 

909 https://packaging.python.org/en/latest/guides/packaging-namespace-packages 

910 

911 Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files). 

912 """ 

913 pkg_root: Path | None = None 

914 pkg_path = resolve_package_path(path) 

915 if pkg_path is not None: 

916 pkg_root = pkg_path.parent 

917 if consider_namespace_packages: 

918 start = pkg_root if pkg_root is not None else path.parent 

919 for candidate in (start, *start.parents): 

920 module_name = compute_module_name(candidate, path) 

921 if module_name and is_importable(module_name, path): 

922 # Point the pkg_root to the root of the namespace package. 

923 pkg_root = candidate 

924 break 

925 

926 if pkg_root is not None: 

927 module_name = compute_module_name(pkg_root, path) 

928 if module_name: 

929 return pkg_root, module_name 

930 

931 raise CouldNotResolvePathError(f"Could not resolve for {path}") 

932 

933 

934def is_importable(module_name: str, module_path: Path) -> bool: 

935 """ 

936 Return if the given module path could be imported normally by Python, akin to the user 

937 entering the REPL and importing the corresponding module name directly, and corresponds 

938 to the module_path specified. 

939 

940 :param module_name: 

941 Full module name that we want to check if is importable. 

942 For example, "app.models". 

943 

944 :param module_path: 

945 Full path to the python module/package we want to check if is importable. 

946 For example, "/projects/src/app/models.py". 

947 """ 

948 try: 

949 # Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through 

950 # sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``). 

951 # Using importlib.util.find_spec() is different, it gives the same results as trying to import 

952 # the module normally in the REPL. 

953 spec = importlib.util.find_spec(module_name) 

954 except (ImportError, ValueError, ImportWarning): 

955 return False 

956 else: 

957 return spec_matches_module_path(spec, module_path) 

958 

959 

960def compute_module_name(root: Path, module_path: Path) -> str | None: 

961 """Compute a module name based on a path and a root anchor.""" 

962 try: 

963 path_without_suffix = module_path.with_suffix("") 

964 except ValueError: 

965 # Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter). 

966 return None 

967 

968 try: 

969 relative = path_without_suffix.relative_to(root) 

970 except ValueError: # pragma: no cover 

971 return None 

972 names = list(relative.parts) 

973 if not names: 

974 return None 

975 if names[-1] == "__init__": 

976 names.pop() 

977 return ".".join(names) 

978 

979 

980class CouldNotResolvePathError(Exception): 

981 """Custom exception raised by resolve_pkg_root_and_module_name.""" 

982 

983 

984def scandir( 

985 path: str | os.PathLike[str], 

986 sort_key: Callable[[os.DirEntry[str]], object] = lambda entry: entry.name, 

987) -> list[os.DirEntry[str]]: 

988 """Scan a directory recursively, in breadth-first order. 

989 

990 The returned entries are sorted according to the given key. 

991 The default is to sort by name. 

992 If the directory does not exist, return an empty list. 

993 """ 

994 entries = [] 

995 # Attempt to create a scandir iterator for the given path. 

996 try: 

997 scandir_iter = os.scandir(path) 

998 except FileNotFoundError: 

999 # If the directory does not exist, return an empty list. 

1000 return [] 

1001 # Use the scandir iterator in a context manager to ensure it is properly closed. 

1002 with scandir_iter as s: 

1003 for entry in s: 

1004 try: 

1005 entry.is_file() 

1006 except OSError as err: 

1007 if _ignore_error(err): 

1008 continue 

1009 # Reraise non-ignorable errors to avoid hiding issues. 

1010 raise 

1011 entries.append(entry) 

1012 entries.sort(key=sort_key) # type: ignore[arg-type] 

1013 return entries 

1014 

1015 

1016def visit( 

1017 path: str | os.PathLike[str], recurse: Callable[[os.DirEntry[str]], bool] 

1018) -> Iterator[os.DirEntry[str]]: 

1019 """Walk a directory recursively, in breadth-first order. 

1020 

1021 The `recurse` predicate determines whether a directory is recursed. 

1022 

1023 Entries at each directory level are sorted. 

1024 """ 

1025 entries = scandir(path) 

1026 yield from entries 

1027 for entry in entries: 

1028 if entry.is_dir() and recurse(entry): 

1029 yield from visit(entry.path, recurse) 

1030 

1031 

1032def absolutepath(path: str | os.PathLike[str]) -> Path: 

1033 """Convert a path to an absolute path using os.path.abspath. 

1034 

1035 Prefer this over Path.resolve() (see #6523). 

1036 Prefer this over Path.absolute() (not public, doesn't normalize). 

1037 """ 

1038 return Path(os.path.abspath(path)) 

1039 

1040 

1041def commonpath(path1: Path, path2: Path) -> Path | None: 

1042 """Return the common part shared with the other path, or None if there is 

1043 no common part. 

1044 

1045 If one path is relative and one is absolute, returns None. 

1046 """ 

1047 try: 

1048 return Path(os.path.commonpath((str(path1), str(path2)))) 

1049 except ValueError: 

1050 return None 

1051 

1052 

1053def bestrelpath(directory: Path, dest: Path) -> str: 

1054 """Return a string which is a relative path from directory to dest such 

1055 that directory/bestrelpath == dest. 

1056 

1057 The paths must be either both absolute or both relative. 

1058 

1059 If no such path can be determined, returns dest. 

1060 """ 

1061 assert isinstance(directory, Path) 

1062 assert isinstance(dest, Path) 

1063 if dest == directory: 

1064 return os.curdir 

1065 # Find the longest common directory. 

1066 base = commonpath(directory, dest) 

1067 # Can be the case on Windows for two absolute paths on different drives. 

1068 # Can be the case for two relative paths without common prefix. 

1069 # Can be the case for a relative path and an absolute path. 

1070 if not base: 

1071 return str(dest) 

1072 reldirectory = directory.relative_to(base) 

1073 reldest = dest.relative_to(base) 

1074 return os.path.join( 

1075 # Back from directory to base. 

1076 *([os.pardir] * len(reldirectory.parts)), 

1077 # Forward from base to dest. 

1078 *reldest.parts, 

1079 ) 

1080 

1081 

1082def safe_exists(p: Path) -> bool: 

1083 """Like Path.exists(), but account for input arguments that might be too long (#11394).""" 

1084 try: 

1085 return p.exists() 

1086 except (ValueError, OSError): 

1087 # ValueError: stat: path too long for Windows 

1088 # OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect 

1089 return False 

1090 

1091 

1092def samefile_nofollow(p1: Path, p2: Path) -> bool: 

1093 """Test whether two paths reference the same actual file or directory. 

1094 

1095 Unlike Path.samefile(), does not resolve symlinks. 

1096 """ 

1097 s1, s2 = p1.lstat(), p2.lstat() 

1098 # On Windows st_ino is the file ID, which file systems are free to not support, 

1099 # in which case it is 0 for every file -- WinFsp mounts such as sshfs-win are one 

1100 # example. os.path.samestat() would then consider any two files on the volume to 

1101 # be the same (python/cpython#78116), so treat a zero file ID as "unknown" and 

1102 # leave the caller with plain path comparison (#14864). 

1103 if s1.st_ino == 0 or s2.st_ino == 0: 

1104 return False 

1105 return os.path.samestat(s1, s2)