Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/_py/path.py: 18%

946 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1"""local path implementation.""" 

2from __future__ import annotations 

3 

4import atexit 

5import fnmatch 

6import importlib.util 

7import io 

8import os 

9import posixpath 

10import sys 

11import uuid 

12import warnings 

13from contextlib import contextmanager 

14from os.path import abspath 

15from os.path import dirname 

16from os.path import exists 

17from os.path import isabs 

18from os.path import isdir 

19from os.path import isfile 

20from os.path import islink 

21from os.path import normpath 

22from stat import S_ISDIR 

23from stat import S_ISLNK 

24from stat import S_ISREG 

25from typing import Any 

26from typing import Callable 

27from typing import overload 

28from typing import TYPE_CHECKING 

29 

30from . import error 

31 

32if TYPE_CHECKING: 

33 from typing import Literal 

34 

35# Moved from local.py. 

36iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt") 

37 

38 

39class Checkers: 

40 _depend_on_existence = "exists", "link", "dir", "file" 

41 

42 def __init__(self, path): 

43 self.path = path 

44 

45 def dotfile(self): 

46 return self.path.basename.startswith(".") 

47 

48 def ext(self, arg): 

49 if not arg.startswith("."): 

50 arg = "." + arg 

51 return self.path.ext == arg 

52 

53 def basename(self, arg): 

54 return self.path.basename == arg 

55 

56 def basestarts(self, arg): 

57 return self.path.basename.startswith(arg) 

58 

59 def relto(self, arg): 

60 return self.path.relto(arg) 

61 

62 def fnmatch(self, arg): 

63 return self.path.fnmatch(arg) 

64 

65 def endswith(self, arg): 

66 return str(self.path).endswith(arg) 

67 

68 def _evaluate(self, kw): 

69 from .._code.source import getrawcode 

70 

71 for name, value in kw.items(): 

72 invert = False 

73 meth = None 

74 try: 

75 meth = getattr(self, name) 

76 except AttributeError: 

77 if name[:3] == "not": 

78 invert = True 

79 try: 

80 meth = getattr(self, name[3:]) 

81 except AttributeError: 

82 pass 

83 if meth is None: 

84 raise TypeError(f"no {name!r} checker available for {self.path!r}") 

85 try: 

86 if getrawcode(meth).co_argcount > 1: 

87 if (not meth(value)) ^ invert: 

88 return False 

89 else: 

90 if bool(value) ^ bool(meth()) ^ invert: 

91 return False 

92 except (error.ENOENT, error.ENOTDIR, error.EBUSY): 

93 # EBUSY feels not entirely correct, 

94 # but its kind of necessary since ENOMEDIUM 

95 # is not accessible in python 

96 for name in self._depend_on_existence: 

97 if name in kw: 

98 if kw.get(name): 

99 return False 

100 name = "not" + name 

101 if name in kw: 

102 if not kw.get(name): 

103 return False 

104 return True 

105 

106 _statcache: Stat 

107 

108 def _stat(self) -> Stat: 

109 try: 

110 return self._statcache 

111 except AttributeError: 

112 try: 

113 self._statcache = self.path.stat() 

114 except error.ELOOP: 

115 self._statcache = self.path.lstat() 

116 return self._statcache 

117 

118 def dir(self): 

119 return S_ISDIR(self._stat().mode) 

120 

121 def file(self): 

122 return S_ISREG(self._stat().mode) 

123 

124 def exists(self): 

125 return self._stat() 

126 

127 def link(self): 

128 st = self.path.lstat() 

129 return S_ISLNK(st.mode) 

130 

131 

132class NeverRaised(Exception): 

133 pass 

134 

135 

136class Visitor: 

137 def __init__(self, fil, rec, ignore, bf, sort): 

138 if isinstance(fil, str): 

139 fil = FNMatcher(fil) 

140 if isinstance(rec, str): 

141 self.rec: Callable[[LocalPath], bool] = FNMatcher(rec) 

142 elif not hasattr(rec, "__call__") and rec: 

143 self.rec = lambda path: True 

144 else: 

145 self.rec = rec 

146 self.fil = fil 

147 self.ignore = ignore 

148 self.breadthfirst = bf 

149 self.optsort = sort and sorted or (lambda x: x) 

150 

151 def gen(self, path): 

152 try: 

153 entries = path.listdir() 

154 except self.ignore: 

155 return 

156 rec = self.rec 

157 dirs = self.optsort( 

158 [p for p in entries if p.check(dir=1) and (rec is None or rec(p))] 

159 ) 

160 if not self.breadthfirst: 

161 for subdir in dirs: 

162 for p in self.gen(subdir): 

163 yield p 

164 for p in self.optsort(entries): 

165 if self.fil is None or self.fil(p): 

166 yield p 

167 if self.breadthfirst: 

168 for subdir in dirs: 

169 for p in self.gen(subdir): 

170 yield p 

171 

172 

173class FNMatcher: 

174 def __init__(self, pattern): 

175 self.pattern = pattern 

176 

177 def __call__(self, path): 

178 pattern = self.pattern 

179 

180 if ( 

181 pattern.find(path.sep) == -1 

182 and iswin32 

183 and pattern.find(posixpath.sep) != -1 

184 ): 

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

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

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

188 pattern = pattern.replace(posixpath.sep, path.sep) 

189 

190 if pattern.find(path.sep) == -1: 

191 name = path.basename 

192 else: 

193 name = str(path) # path.strpath # XXX svn? 

194 if not os.path.isabs(pattern): 

195 pattern = "*" + path.sep + pattern 

196 return fnmatch.fnmatch(name, pattern) 

197 

198 

199def map_as_list(func, iter): 

200 return list(map(func, iter)) 

201 

202 

203class Stat: 

204 if TYPE_CHECKING: 

205 

206 @property 

207 def size(self) -> int: 

208 ... 

209 

210 @property 

211 def mtime(self) -> float: 

212 ... 

213 

214 def __getattr__(self, name: str) -> Any: 

215 return getattr(self._osstatresult, "st_" + name) 

216 

217 def __init__(self, path, osstatresult): 

218 self.path = path 

219 self._osstatresult = osstatresult 

220 

221 @property 

222 def owner(self): 

223 if iswin32: 

224 raise NotImplementedError("XXX win32") 

225 import pwd 

226 

227 entry = error.checked_call(pwd.getpwuid, self.uid) 

228 return entry[0] 

229 

230 @property 

231 def group(self): 

232 """Return group name of file.""" 

233 if iswin32: 

234 raise NotImplementedError("XXX win32") 

235 import grp 

236 

237 entry = error.checked_call(grp.getgrgid, self.gid) 

238 return entry[0] 

239 

240 def isdir(self): 

241 return S_ISDIR(self._osstatresult.st_mode) 

242 

243 def isfile(self): 

244 return S_ISREG(self._osstatresult.st_mode) 

245 

246 def islink(self): 

247 self.path.lstat() 

248 return S_ISLNK(self._osstatresult.st_mode) 

249 

250 

251def getuserid(user): 

252 import pwd 

253 

254 if not isinstance(user, int): 

255 user = pwd.getpwnam(user)[2] 

256 return user 

257 

258 

259def getgroupid(group): 

260 import grp 

261 

262 if not isinstance(group, int): 

263 group = grp.getgrnam(group)[2] 

264 return group 

265 

266 

267class LocalPath: 

268 """Object oriented interface to os.path and other local filesystem 

269 related information. 

270 """ 

271 

272 class ImportMismatchError(ImportError): 

273 """raised on pyimport() if there is a mismatch of __file__'s""" 

274 

275 sep = os.sep 

276 

277 def __init__(self, path=None, expanduser=False): 

278 """Initialize and return a local Path instance. 

279 

280 Path can be relative to the current directory. 

281 If path is None it defaults to the current working directory. 

282 If expanduser is True, tilde-expansion is performed. 

283 Note that Path instances always carry an absolute path. 

284 Note also that passing in a local path object will simply return 

285 the exact same path object. Use new() to get a new copy. 

286 """ 

287 if path is None: 

288 self.strpath = error.checked_call(os.getcwd) 

289 else: 

290 try: 

291 path = os.fspath(path) 

292 except TypeError: 

293 raise ValueError( 

294 "can only pass None, Path instances " 

295 "or non-empty strings to LocalPath" 

296 ) 

297 if expanduser: 

298 path = os.path.expanduser(path) 

299 self.strpath = abspath(path) 

300 

301 if sys.platform != "win32": 

302 

303 def chown(self, user, group, rec=0): 

304 """Change ownership to the given user and group. 

305 user and group may be specified by a number or 

306 by a name. if rec is True change ownership 

307 recursively. 

308 """ 

309 uid = getuserid(user) 

310 gid = getgroupid(group) 

311 if rec: 

312 for x in self.visit(rec=lambda x: x.check(link=0)): 

313 if x.check(link=0): 

314 error.checked_call(os.chown, str(x), uid, gid) 

315 error.checked_call(os.chown, str(self), uid, gid) 

316 

317 def readlink(self) -> str: 

318 """Return value of a symbolic link.""" 

319 # https://github.com/python/mypy/issues/12278 

320 return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value] 

321 

322 def mklinkto(self, oldname): 

323 """Posix style hard link to another name.""" 

324 error.checked_call(os.link, str(oldname), str(self)) 

325 

326 def mksymlinkto(self, value, absolute=1): 

327 """Create a symbolic link with the given value (pointing to another name).""" 

328 if absolute: 

329 error.checked_call(os.symlink, str(value), self.strpath) 

330 else: 

331 base = self.common(value) 

332 # with posix local paths '/' is always a common base 

333 relsource = self.__class__(value).relto(base) 

334 reldest = self.relto(base) 

335 n = reldest.count(self.sep) 

336 target = self.sep.join(("..",) * n + (relsource,)) 

337 error.checked_call(os.symlink, target, self.strpath) 

338 

339 def __div__(self, other): 

340 return self.join(os.fspath(other)) 

341 

342 __truediv__ = __div__ # py3k 

343 

344 @property 

345 def basename(self): 

346 """Basename part of path.""" 

347 return self._getbyspec("basename")[0] 

348 

349 @property 

350 def dirname(self): 

351 """Dirname part of path.""" 

352 return self._getbyspec("dirname")[0] 

353 

354 @property 

355 def purebasename(self): 

356 """Pure base name of the path.""" 

357 return self._getbyspec("purebasename")[0] 

358 

359 @property 

360 def ext(self): 

361 """Extension of the path (including the '.').""" 

362 return self._getbyspec("ext")[0] 

363 

364 def read_binary(self): 

365 """Read and return a bytestring from reading the path.""" 

366 with self.open("rb") as f: 

367 return f.read() 

368 

369 def read_text(self, encoding): 

370 """Read and return a Unicode string from reading the path.""" 

371 with self.open("r", encoding=encoding) as f: 

372 return f.read() 

373 

374 def read(self, mode="r"): 

375 """Read and return a bytestring from reading the path.""" 

376 with self.open(mode) as f: 

377 return f.read() 

378 

379 def readlines(self, cr=1): 

380 """Read and return a list of lines from the path. if cr is False, the 

381 newline will be removed from the end of each line.""" 

382 mode = "r" 

383 

384 if not cr: 

385 content = self.read(mode) 

386 return content.split("\n") 

387 else: 

388 f = self.open(mode) 

389 try: 

390 return f.readlines() 

391 finally: 

392 f.close() 

393 

394 def load(self): 

395 """(deprecated) return object unpickled from self.read()""" 

396 f = self.open("rb") 

397 try: 

398 import pickle 

399 

400 return error.checked_call(pickle.load, f) 

401 finally: 

402 f.close() 

403 

404 def move(self, target): 

405 """Move this path to target.""" 

406 if target.relto(self): 

407 raise error.EINVAL(target, "cannot move path into a subdirectory of itself") 

408 try: 

409 self.rename(target) 

410 except error.EXDEV: # invalid cross-device link 

411 self.copy(target) 

412 self.remove() 

413 

414 def fnmatch(self, pattern): 

415 """Return true if the basename/fullname matches the glob-'pattern'. 

416 

417 valid pattern characters:: 

418 

419 * matches everything 

420 ? matches any single character 

421 [seq] matches any character in seq 

422 [!seq] matches any char not in seq 

423 

424 If the pattern contains a path-separator then the full path 

425 is used for pattern matching and a '*' is prepended to the 

426 pattern. 

427 

428 if the pattern doesn't contain a path-separator the pattern 

429 is only matched against the basename. 

430 """ 

431 return FNMatcher(pattern)(self) 

432 

433 def relto(self, relpath): 

434 """Return a string which is the relative part of the path 

435 to the given 'relpath'. 

436 """ 

437 if not isinstance(relpath, (str, LocalPath)): 

438 raise TypeError(f"{relpath!r}: not a string or path object") 

439 strrelpath = str(relpath) 

440 if strrelpath and strrelpath[-1] != self.sep: 

441 strrelpath += self.sep 

442 # assert strrelpath[-1] == self.sep 

443 # assert strrelpath[-2] != self.sep 

444 strself = self.strpath 

445 if sys.platform == "win32" or getattr(os, "_name", None) == "nt": 

446 if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)): 

447 return strself[len(strrelpath) :] 

448 elif strself.startswith(strrelpath): 

449 return strself[len(strrelpath) :] 

450 return "" 

451 

452 def ensure_dir(self, *args): 

453 """Ensure the path joined with args is a directory.""" 

454 return self.ensure(*args, **{"dir": True}) 

455 

456 def bestrelpath(self, dest): 

457 """Return a string which is a relative path from self 

458 (assumed to be a directory) to dest such that 

459 self.join(bestrelpath) == dest and if not such 

460 path can be determined return dest. 

461 """ 

462 try: 

463 if self == dest: 

464 return os.curdir 

465 base = self.common(dest) 

466 if not base: # can be the case on windows 

467 return str(dest) 

468 self2base = self.relto(base) 

469 reldest = dest.relto(base) 

470 if self2base: 

471 n = self2base.count(self.sep) + 1 

472 else: 

473 n = 0 

474 lst = [os.pardir] * n 

475 if reldest: 

476 lst.append(reldest) 

477 target = dest.sep.join(lst) 

478 return target 

479 except AttributeError: 

480 return str(dest) 

481 

482 def exists(self): 

483 return self.check() 

484 

485 def isdir(self): 

486 return self.check(dir=1) 

487 

488 def isfile(self): 

489 return self.check(file=1) 

490 

491 def parts(self, reverse=False): 

492 """Return a root-first list of all ancestor directories 

493 plus the path itself. 

494 """ 

495 current = self 

496 lst = [self] 

497 while 1: 

498 last = current 

499 current = current.dirpath() 

500 if last == current: 

501 break 

502 lst.append(current) 

503 if not reverse: 

504 lst.reverse() 

505 return lst 

506 

507 def common(self, other): 

508 """Return the common part shared with the other path 

509 or None if there is no common part. 

510 """ 

511 last = None 

512 for x, y in zip(self.parts(), other.parts()): 

513 if x != y: 

514 return last 

515 last = x 

516 return last 

517 

518 def __add__(self, other): 

519 """Return new path object with 'other' added to the basename""" 

520 return self.new(basename=self.basename + str(other)) 

521 

522 def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): 

523 """Yields all paths below the current one 

524 

525 fil is a filter (glob pattern or callable), if not matching the 

526 path will not be yielded, defaulting to None (everything is 

527 returned) 

528 

529 rec is a filter (glob pattern or callable) that controls whether 

530 a node is descended, defaulting to None 

531 

532 ignore is an Exception class that is ignoredwhen calling dirlist() 

533 on any of the paths (by default, all exceptions are reported) 

534 

535 bf if True will cause a breadthfirst search instead of the 

536 default depthfirst. Default: False 

537 

538 sort if True will sort entries within each directory level. 

539 """ 

540 yield from Visitor(fil, rec, ignore, bf, sort).gen(self) 

541 

542 def _sortlist(self, res, sort): 

543 if sort: 

544 if hasattr(sort, "__call__"): 

545 warnings.warn( 

546 DeprecationWarning( 

547 "listdir(sort=callable) is deprecated and breaks on python3" 

548 ), 

549 stacklevel=3, 

550 ) 

551 res.sort(sort) 

552 else: 

553 res.sort() 

554 

555 def __fspath__(self): 

556 return self.strpath 

557 

558 def __hash__(self): 

559 s = self.strpath 

560 if iswin32: 

561 s = s.lower() 

562 return hash(s) 

563 

564 def __eq__(self, other): 

565 s1 = os.fspath(self) 

566 try: 

567 s2 = os.fspath(other) 

568 except TypeError: 

569 return False 

570 if iswin32: 

571 s1 = s1.lower() 

572 try: 

573 s2 = s2.lower() 

574 except AttributeError: 

575 return False 

576 return s1 == s2 

577 

578 def __ne__(self, other): 

579 return not (self == other) 

580 

581 def __lt__(self, other): 

582 return os.fspath(self) < os.fspath(other) 

583 

584 def __gt__(self, other): 

585 return os.fspath(self) > os.fspath(other) 

586 

587 def samefile(self, other): 

588 """Return True if 'other' references the same file as 'self'.""" 

589 other = os.fspath(other) 

590 if not isabs(other): 

591 other = abspath(other) 

592 if self == other: 

593 return True 

594 if not hasattr(os.path, "samefile"): 

595 return False 

596 return error.checked_call(os.path.samefile, self.strpath, other) 

597 

598 def remove(self, rec=1, ignore_errors=False): 

599 """Remove a file or directory (or a directory tree if rec=1). 

600 if ignore_errors is True, errors while removing directories will 

601 be ignored. 

602 """ 

603 if self.check(dir=1, link=0): 

604 if rec: 

605 # force remove of readonly files on windows 

606 if iswin32: 

607 self.chmod(0o700, rec=1) 

608 import shutil 

609 

610 error.checked_call( 

611 shutil.rmtree, self.strpath, ignore_errors=ignore_errors 

612 ) 

613 else: 

614 error.checked_call(os.rmdir, self.strpath) 

615 else: 

616 if iswin32: 

617 self.chmod(0o700) 

618 error.checked_call(os.remove, self.strpath) 

619 

620 def computehash(self, hashtype="md5", chunksize=524288): 

621 """Return hexdigest of hashvalue for this file.""" 

622 try: 

623 try: 

624 import hashlib as mod 

625 except ImportError: 

626 if hashtype == "sha1": 

627 hashtype = "sha" 

628 mod = __import__(hashtype) 

629 hash = getattr(mod, hashtype)() 

630 except (AttributeError, ImportError): 

631 raise ValueError(f"Don't know how to compute {hashtype!r} hash") 

632 f = self.open("rb") 

633 try: 

634 while 1: 

635 buf = f.read(chunksize) 

636 if not buf: 

637 return hash.hexdigest() 

638 hash.update(buf) 

639 finally: 

640 f.close() 

641 

642 def new(self, **kw): 

643 """Create a modified version of this path. 

644 the following keyword arguments modify various path parts:: 

645 

646 a:/some/path/to/a/file.ext 

647 xx drive 

648 xxxxxxxxxxxxxxxxx dirname 

649 xxxxxxxx basename 

650 xxxx purebasename 

651 xxx ext 

652 """ 

653 obj = object.__new__(self.__class__) 

654 if not kw: 

655 obj.strpath = self.strpath 

656 return obj 

657 drive, dirname, basename, purebasename, ext = self._getbyspec( 

658 "drive,dirname,basename,purebasename,ext" 

659 ) 

660 if "basename" in kw: 

661 if "purebasename" in kw or "ext" in kw: 

662 raise ValueError("invalid specification %r" % kw) 

663 else: 

664 pb = kw.setdefault("purebasename", purebasename) 

665 try: 

666 ext = kw["ext"] 

667 except KeyError: 

668 pass 

669 else: 

670 if ext and not ext.startswith("."): 

671 ext = "." + ext 

672 kw["basename"] = pb + ext 

673 

674 if "dirname" in kw and not kw["dirname"]: 

675 kw["dirname"] = drive 

676 else: 

677 kw.setdefault("dirname", dirname) 

678 kw.setdefault("sep", self.sep) 

679 obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw) 

680 return obj 

681 

682 def _getbyspec(self, spec: str) -> list[str]: 

683 """See new for what 'spec' can be.""" 

684 res = [] 

685 parts = self.strpath.split(self.sep) 

686 

687 args = filter(None, spec.split(",")) 

688 for name in args: 

689 if name == "drive": 

690 res.append(parts[0]) 

691 elif name == "dirname": 

692 res.append(self.sep.join(parts[:-1])) 

693 else: 

694 basename = parts[-1] 

695 if name == "basename": 

696 res.append(basename) 

697 else: 

698 i = basename.rfind(".") 

699 if i == -1: 

700 purebasename, ext = basename, "" 

701 else: 

702 purebasename, ext = basename[:i], basename[i:] 

703 if name == "purebasename": 

704 res.append(purebasename) 

705 elif name == "ext": 

706 res.append(ext) 

707 else: 

708 raise ValueError("invalid part specification %r" % name) 

709 return res 

710 

711 def dirpath(self, *args, **kwargs): 

712 """Return the directory path joined with any given path arguments.""" 

713 if not kwargs: 

714 path = object.__new__(self.__class__) 

715 path.strpath = dirname(self.strpath) 

716 if args: 

717 path = path.join(*args) 

718 return path 

719 return self.new(basename="").join(*args, **kwargs) 

720 

721 def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath: 

722 """Return a new path by appending all 'args' as path 

723 components. if abs=1 is used restart from root if any 

724 of the args is an absolute path. 

725 """ 

726 sep = self.sep 

727 strargs = [os.fspath(arg) for arg in args] 

728 strpath = self.strpath 

729 if abs: 

730 newargs: list[str] = [] 

731 for arg in reversed(strargs): 

732 if isabs(arg): 

733 strpath = arg 

734 strargs = newargs 

735 break 

736 newargs.insert(0, arg) 

737 # special case for when we have e.g. strpath == "/" 

738 actual_sep = "" if strpath.endswith(sep) else sep 

739 for arg in strargs: 

740 arg = arg.strip(sep) 

741 if iswin32: 

742 # allow unix style paths even on windows. 

743 arg = arg.strip("/") 

744 arg = arg.replace("/", sep) 

745 strpath = strpath + actual_sep + arg 

746 actual_sep = sep 

747 obj = object.__new__(self.__class__) 

748 obj.strpath = normpath(strpath) 

749 return obj 

750 

751 def open(self, mode="r", ensure=False, encoding=None): 

752 """Return an opened file with the given mode. 

753 

754 If ensure is True, create parent directories if needed. 

755 """ 

756 if ensure: 

757 self.dirpath().ensure(dir=1) 

758 if encoding: 

759 return error.checked_call(io.open, self.strpath, mode, encoding=encoding) 

760 return error.checked_call(open, self.strpath, mode) 

761 

762 def _fastjoin(self, name): 

763 child = object.__new__(self.__class__) 

764 child.strpath = self.strpath + self.sep + name 

765 return child 

766 

767 def islink(self): 

768 return islink(self.strpath) 

769 

770 def check(self, **kw): 

771 """Check a path for existence and properties. 

772 

773 Without arguments, return True if the path exists, otherwise False. 

774 

775 valid checkers:: 

776 

777 file=1 # is a file 

778 file=0 # is not a file (may not even exist) 

779 dir=1 # is a dir 

780 link=1 # is a link 

781 exists=1 # exists 

782 

783 You can specify multiple checker definitions, for example:: 

784 

785 path.check(file=1, link=1) # a link pointing to a file 

786 """ 

787 if not kw: 

788 return exists(self.strpath) 

789 if len(kw) == 1: 

790 if "dir" in kw: 

791 return not kw["dir"] ^ isdir(self.strpath) 

792 if "file" in kw: 

793 return not kw["file"] ^ isfile(self.strpath) 

794 if not kw: 

795 kw = {"exists": 1} 

796 return Checkers(self)._evaluate(kw) 

797 

798 _patternchars = set("*?[" + os.path.sep) 

799 

800 def listdir(self, fil=None, sort=None): 

801 """List directory contents, possibly filter by the given fil func 

802 and possibly sorted. 

803 """ 

804 if fil is None and sort is None: 

805 names = error.checked_call(os.listdir, self.strpath) 

806 return map_as_list(self._fastjoin, names) 

807 if isinstance(fil, str): 

808 if not self._patternchars.intersection(fil): 

809 child = self._fastjoin(fil) 

810 if exists(child.strpath): 

811 return [child] 

812 return [] 

813 fil = FNMatcher(fil) 

814 names = error.checked_call(os.listdir, self.strpath) 

815 res = [] 

816 for name in names: 

817 child = self._fastjoin(name) 

818 if fil is None or fil(child): 

819 res.append(child) 

820 self._sortlist(res, sort) 

821 return res 

822 

823 def size(self) -> int: 

824 """Return size of the underlying file object""" 

825 return self.stat().size 

826 

827 def mtime(self) -> float: 

828 """Return last modification time of the path.""" 

829 return self.stat().mtime 

830 

831 def copy(self, target, mode=False, stat=False): 

832 """Copy path to target. 

833 

834 If mode is True, will copy copy permission from path to target. 

835 If stat is True, copy permission, last modification 

836 time, last access time, and flags from path to target. 

837 """ 

838 if self.check(file=1): 

839 if target.check(dir=1): 

840 target = target.join(self.basename) 

841 assert self != target 

842 copychunked(self, target) 

843 if mode: 

844 copymode(self.strpath, target.strpath) 

845 if stat: 

846 copystat(self, target) 

847 else: 

848 

849 def rec(p): 

850 return p.check(link=0) 

851 

852 for x in self.visit(rec=rec): 

853 relpath = x.relto(self) 

854 newx = target.join(relpath) 

855 newx.dirpath().ensure(dir=1) 

856 if x.check(link=1): 

857 newx.mksymlinkto(x.readlink()) 

858 continue 

859 elif x.check(file=1): 

860 copychunked(x, newx) 

861 elif x.check(dir=1): 

862 newx.ensure(dir=1) 

863 if mode: 

864 copymode(x.strpath, newx.strpath) 

865 if stat: 

866 copystat(x, newx) 

867 

868 def rename(self, target): 

869 """Rename this path to target.""" 

870 target = os.fspath(target) 

871 return error.checked_call(os.rename, self.strpath, target) 

872 

873 def dump(self, obj, bin=1): 

874 """Pickle object into path location""" 

875 f = self.open("wb") 

876 import pickle 

877 

878 try: 

879 error.checked_call(pickle.dump, obj, f, bin) 

880 finally: 

881 f.close() 

882 

883 def mkdir(self, *args): 

884 """Create & return the directory joined with args.""" 

885 p = self.join(*args) 

886 error.checked_call(os.mkdir, os.fspath(p)) 

887 return p 

888 

889 def write_binary(self, data, ensure=False): 

890 """Write binary data into path. If ensure is True create 

891 missing parent directories. 

892 """ 

893 if ensure: 

894 self.dirpath().ensure(dir=1) 

895 with self.open("wb") as f: 

896 f.write(data) 

897 

898 def write_text(self, data, encoding, ensure=False): 

899 """Write text data into path using the specified encoding. 

900 If ensure is True create missing parent directories. 

901 """ 

902 if ensure: 

903 self.dirpath().ensure(dir=1) 

904 with self.open("w", encoding=encoding) as f: 

905 f.write(data) 

906 

907 def write(self, data, mode="w", ensure=False): 

908 """Write data into path. If ensure is True create 

909 missing parent directories. 

910 """ 

911 if ensure: 

912 self.dirpath().ensure(dir=1) 

913 if "b" in mode: 

914 if not isinstance(data, bytes): 

915 raise ValueError("can only process bytes") 

916 else: 

917 if not isinstance(data, str): 

918 if not isinstance(data, bytes): 

919 data = str(data) 

920 else: 

921 data = data.decode(sys.getdefaultencoding()) 

922 f = self.open(mode) 

923 try: 

924 f.write(data) 

925 finally: 

926 f.close() 

927 

928 def _ensuredirs(self): 

929 parent = self.dirpath() 

930 if parent == self: 

931 return self 

932 if parent.check(dir=0): 

933 parent._ensuredirs() 

934 if self.check(dir=0): 

935 try: 

936 self.mkdir() 

937 except error.EEXIST: 

938 # race condition: file/dir created by another thread/process. 

939 # complain if it is not a dir 

940 if self.check(dir=0): 

941 raise 

942 return self 

943 

944 def ensure(self, *args, **kwargs): 

945 """Ensure that an args-joined path exists (by default as 

946 a file). if you specify a keyword argument 'dir=True' 

947 then the path is forced to be a directory path. 

948 """ 

949 p = self.join(*args) 

950 if kwargs.get("dir", 0): 

951 return p._ensuredirs() 

952 else: 

953 p.dirpath()._ensuredirs() 

954 if not p.check(file=1): 

955 p.open("w").close() 

956 return p 

957 

958 @overload 

959 def stat(self, raising: Literal[True] = ...) -> Stat: 

960 ... 

961 

962 @overload 

963 def stat(self, raising: Literal[False]) -> Stat | None: 

964 ... 

965 

966 def stat(self, raising: bool = True) -> Stat | None: 

967 """Return an os.stat() tuple.""" 

968 if raising: 

969 return Stat(self, error.checked_call(os.stat, self.strpath)) 

970 try: 

971 return Stat(self, os.stat(self.strpath)) 

972 except KeyboardInterrupt: 

973 raise 

974 except Exception: 

975 return None 

976 

977 def lstat(self) -> Stat: 

978 """Return an os.lstat() tuple.""" 

979 return Stat(self, error.checked_call(os.lstat, self.strpath)) 

980 

981 def setmtime(self, mtime=None): 

982 """Set modification time for the given path. if 'mtime' is None 

983 (the default) then the file's mtime is set to current time. 

984 

985 Note that the resolution for 'mtime' is platform dependent. 

986 """ 

987 if mtime is None: 

988 return error.checked_call(os.utime, self.strpath, mtime) 

989 try: 

990 return error.checked_call(os.utime, self.strpath, (-1, mtime)) 

991 except error.EINVAL: 

992 return error.checked_call(os.utime, self.strpath, (self.atime(), mtime)) 

993 

994 def chdir(self): 

995 """Change directory to self and return old current directory""" 

996 try: 

997 old = self.__class__() 

998 except error.ENOENT: 

999 old = None 

1000 error.checked_call(os.chdir, self.strpath) 

1001 return old 

1002 

1003 @contextmanager 

1004 def as_cwd(self): 

1005 """ 

1006 Return a context manager, which changes to the path's dir during the 

1007 managed "with" context. 

1008 On __enter__ it returns the old dir, which might be ``None``. 

1009 """ 

1010 old = self.chdir() 

1011 try: 

1012 yield old 

1013 finally: 

1014 if old is not None: 

1015 old.chdir() 

1016 

1017 def realpath(self): 

1018 """Return a new path which contains no symbolic links.""" 

1019 return self.__class__(os.path.realpath(self.strpath)) 

1020 

1021 def atime(self): 

1022 """Return last access time of the path.""" 

1023 return self.stat().atime 

1024 

1025 def __repr__(self): 

1026 return "local(%r)" % self.strpath 

1027 

1028 def __str__(self): 

1029 """Return string representation of the Path.""" 

1030 return self.strpath 

1031 

1032 def chmod(self, mode, rec=0): 

1033 """Change permissions to the given mode. If mode is an 

1034 integer it directly encodes the os-specific modes. 

1035 if rec is True perform recursively. 

1036 """ 

1037 if not isinstance(mode, int): 

1038 raise TypeError(f"mode {mode!r} must be an integer") 

1039 if rec: 

1040 for x in self.visit(rec=rec): 

1041 error.checked_call(os.chmod, str(x), mode) 

1042 error.checked_call(os.chmod, self.strpath, mode) 

1043 

1044 def pypkgpath(self): 

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

1046 directory upwards which still contains an __init__.py. 

1047 Return None if a pkgpath can not be determined. 

1048 """ 

1049 pkgpath = None 

1050 for parent in self.parts(reverse=True): 

1051 if parent.isdir(): 

1052 if not parent.join("__init__.py").exists(): 

1053 break 

1054 if not isimportable(parent.basename): 

1055 break 

1056 pkgpath = parent 

1057 return pkgpath 

1058 

1059 def _ensuresyspath(self, ensuremode, path): 

1060 if ensuremode: 

1061 s = str(path) 

1062 if ensuremode == "append": 

1063 if s not in sys.path: 

1064 sys.path.append(s) 

1065 else: 

1066 if s != sys.path[0]: 

1067 sys.path.insert(0, s) 

1068 

1069 def pyimport(self, modname=None, ensuresyspath=True): 

1070 """Return path as an imported python module. 

1071 

1072 If modname is None, look for the containing package 

1073 and construct an according module name. 

1074 The module will be put/looked up in sys.modules. 

1075 if ensuresyspath is True then the root dir for importing 

1076 the file (taking __init__.py files into account) will 

1077 be prepended to sys.path if it isn't there already. 

1078 If ensuresyspath=="append" the root dir will be appended 

1079 if it isn't already contained in sys.path. 

1080 if ensuresyspath is False no modification of syspath happens. 

1081 

1082 Special value of ensuresyspath=="importlib" is intended 

1083 purely for using in pytest, it is capable only of importing 

1084 separate .py files outside packages, e.g. for test suite 

1085 without any __init__.py file. It effectively allows having 

1086 same-named test modules in different places and offers 

1087 mild opt-in via this option. Note that it works only in 

1088 recent versions of python. 

1089 """ 

1090 if not self.check(): 

1091 raise error.ENOENT(self) 

1092 

1093 if ensuresyspath == "importlib": 

1094 if modname is None: 

1095 modname = self.purebasename 

1096 spec = importlib.util.spec_from_file_location(modname, str(self)) 

1097 if spec is None or spec.loader is None: 

1098 raise ImportError( 

1099 f"Can't find module {modname} at location {str(self)}" 

1100 ) 

1101 mod = importlib.util.module_from_spec(spec) 

1102 spec.loader.exec_module(mod) 

1103 return mod 

1104 

1105 pkgpath = None 

1106 if modname is None: 

1107 pkgpath = self.pypkgpath() 

1108 if pkgpath is not None: 

1109 pkgroot = pkgpath.dirpath() 

1110 names = self.new(ext="").relto(pkgroot).split(self.sep) 

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

1112 names.pop() 

1113 modname = ".".join(names) 

1114 else: 

1115 pkgroot = self.dirpath() 

1116 modname = self.purebasename 

1117 

1118 self._ensuresyspath(ensuresyspath, pkgroot) 

1119 __import__(modname) 

1120 mod = sys.modules[modname] 

1121 if self.basename == "__init__.py": 

1122 return mod # we don't check anything as we might 

1123 # be in a namespace package ... too icky to check 

1124 modfile = mod.__file__ 

1125 assert modfile is not None 

1126 if modfile[-4:] in (".pyc", ".pyo"): 

1127 modfile = modfile[:-1] 

1128 elif modfile.endswith("$py.class"): 

1129 modfile = modfile[:-9] + ".py" 

1130 if modfile.endswith(os.path.sep + "__init__.py"): 

1131 if self.basename != "__init__.py": 

1132 modfile = modfile[:-12] 

1133 try: 

1134 issame = self.samefile(modfile) 

1135 except error.ENOENT: 

1136 issame = False 

1137 if not issame: 

1138 ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH") 

1139 if ignore != "1": 

1140 raise self.ImportMismatchError(modname, modfile, self) 

1141 return mod 

1142 else: 

1143 try: 

1144 return sys.modules[modname] 

1145 except KeyError: 

1146 # we have a custom modname, do a pseudo-import 

1147 import types 

1148 

1149 mod = types.ModuleType(modname) 

1150 mod.__file__ = str(self) 

1151 sys.modules[modname] = mod 

1152 try: 

1153 with open(str(self), "rb") as f: 

1154 exec(f.read(), mod.__dict__) 

1155 except BaseException: 

1156 del sys.modules[modname] 

1157 raise 

1158 return mod 

1159 

1160 def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str: 

1161 """Return stdout text from executing a system child process, 

1162 where the 'self' path points to executable. 

1163 The process is directly invoked and not through a system shell. 

1164 """ 

1165 from subprocess import Popen, PIPE 

1166 

1167 popen_opts.pop("stdout", None) 

1168 popen_opts.pop("stderr", None) 

1169 proc = Popen( 

1170 [str(self)] + [str(arg) for arg in argv], 

1171 **popen_opts, 

1172 stdout=PIPE, 

1173 stderr=PIPE, 

1174 ) 

1175 stdout: str | bytes 

1176 stdout, stderr = proc.communicate() 

1177 ret = proc.wait() 

1178 if isinstance(stdout, bytes): 

1179 stdout = stdout.decode(sys.getdefaultencoding()) 

1180 if ret != 0: 

1181 if isinstance(stderr, bytes): 

1182 stderr = stderr.decode(sys.getdefaultencoding()) 

1183 raise RuntimeError( 

1184 ret, 

1185 ret, 

1186 str(self), 

1187 stdout, 

1188 stderr, 

1189 ) 

1190 return stdout 

1191 

1192 @classmethod 

1193 def sysfind(cls, name, checker=None, paths=None): 

1194 """Return a path object found by looking at the systems 

1195 underlying PATH specification. If the checker is not None 

1196 it will be invoked to filter matching paths. If a binary 

1197 cannot be found, None is returned 

1198 Note: This is probably not working on plain win32 systems 

1199 but may work on cygwin. 

1200 """ 

1201 if isabs(name): 

1202 p = local(name) 

1203 if p.check(file=1): 

1204 return p 

1205 else: 

1206 if paths is None: 

1207 if iswin32: 

1208 paths = os.environ["Path"].split(";") 

1209 if "" not in paths and "." not in paths: 

1210 paths.append(".") 

1211 try: 

1212 systemroot = os.environ["SYSTEMROOT"] 

1213 except KeyError: 

1214 pass 

1215 else: 

1216 paths = [ 

1217 path.replace("%SystemRoot%", systemroot) for path in paths 

1218 ] 

1219 else: 

1220 paths = os.environ["PATH"].split(":") 

1221 tryadd = [] 

1222 if iswin32: 

1223 tryadd += os.environ["PATHEXT"].split(os.pathsep) 

1224 tryadd.append("") 

1225 

1226 for x in paths: 

1227 for addext in tryadd: 

1228 p = local(x).join(name, abs=True) + addext 

1229 try: 

1230 if p.check(file=1): 

1231 if checker: 

1232 if not checker(p): 

1233 continue 

1234 return p 

1235 except error.EACCES: 

1236 pass 

1237 return None 

1238 

1239 @classmethod 

1240 def _gethomedir(cls): 

1241 try: 

1242 x = os.environ["HOME"] 

1243 except KeyError: 

1244 try: 

1245 x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"] 

1246 except KeyError: 

1247 return None 

1248 return cls(x) 

1249 

1250 # """ 

1251 # special class constructors for local filesystem paths 

1252 # """ 

1253 @classmethod 

1254 def get_temproot(cls): 

1255 """Return the system's temporary directory 

1256 (where tempfiles are usually created in) 

1257 """ 

1258 import tempfile 

1259 

1260 return local(tempfile.gettempdir()) 

1261 

1262 @classmethod 

1263 def mkdtemp(cls, rootdir=None): 

1264 """Return a Path object pointing to a fresh new temporary directory 

1265 (which we created ourself). 

1266 """ 

1267 import tempfile 

1268 

1269 if rootdir is None: 

1270 rootdir = cls.get_temproot() 

1271 return cls(error.checked_call(tempfile.mkdtemp, dir=str(rootdir))) 

1272 

1273 @classmethod 

1274 def make_numbered_dir( 

1275 cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800 

1276 ): # two days 

1277 """Return unique directory with a number greater than the current 

1278 maximum one. The number is assumed to start directly after prefix. 

1279 if keep is true directories with a number less than (maxnum-keep) 

1280 will be removed. If .lock files are used (lock_timeout non-zero), 

1281 algorithm is multi-process safe. 

1282 """ 

1283 if rootdir is None: 

1284 rootdir = cls.get_temproot() 

1285 

1286 nprefix = prefix.lower() 

1287 

1288 def parse_num(path): 

1289 """Parse the number out of a path (if it matches the prefix)""" 

1290 nbasename = path.basename.lower() 

1291 if nbasename.startswith(nprefix): 

1292 try: 

1293 return int(nbasename[len(nprefix) :]) 

1294 except ValueError: 

1295 pass 

1296 

1297 def create_lockfile(path): 

1298 """Exclusively create lockfile. Throws when failed""" 

1299 mypid = os.getpid() 

1300 lockfile = path.join(".lock") 

1301 if hasattr(lockfile, "mksymlinkto"): 

1302 lockfile.mksymlinkto(str(mypid)) 

1303 else: 

1304 fd = error.checked_call( 

1305 os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644 

1306 ) 

1307 with os.fdopen(fd, "w") as f: 

1308 f.write(str(mypid)) 

1309 return lockfile 

1310 

1311 def atexit_remove_lockfile(lockfile): 

1312 """Ensure lockfile is removed at process exit""" 

1313 mypid = os.getpid() 

1314 

1315 def try_remove_lockfile(): 

1316 # in a fork() situation, only the last process should 

1317 # remove the .lock, otherwise the other processes run the 

1318 # risk of seeing their temporary dir disappear. For now 

1319 # we remove the .lock in the parent only (i.e. we assume 

1320 # that the children finish before the parent). 

1321 if os.getpid() != mypid: 

1322 return 

1323 try: 

1324 lockfile.remove() 

1325 except error.Error: 

1326 pass 

1327 

1328 atexit.register(try_remove_lockfile) 

1329 

1330 # compute the maximum number currently in use with the prefix 

1331 lastmax = None 

1332 while True: 

1333 maxnum = -1 

1334 for path in rootdir.listdir(): 

1335 num = parse_num(path) 

1336 if num is not None: 

1337 maxnum = max(maxnum, num) 

1338 

1339 # make the new directory 

1340 try: 

1341 udir = rootdir.mkdir(prefix + str(maxnum + 1)) 

1342 if lock_timeout: 

1343 lockfile = create_lockfile(udir) 

1344 atexit_remove_lockfile(lockfile) 

1345 except (error.EEXIST, error.ENOENT, error.EBUSY): 

1346 # race condition (1): another thread/process created the dir 

1347 # in the meantime - try again 

1348 # race condition (2): another thread/process spuriously acquired 

1349 # lock treating empty directory as candidate 

1350 # for removal - try again 

1351 # race condition (3): another thread/process tried to create the lock at 

1352 # the same time (happened in Python 3.3 on Windows) 

1353 # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa 

1354 if lastmax == maxnum: 

1355 raise 

1356 lastmax = maxnum 

1357 continue 

1358 break 

1359 

1360 def get_mtime(path): 

1361 """Read file modification time""" 

1362 try: 

1363 return path.lstat().mtime 

1364 except error.Error: 

1365 pass 

1366 

1367 garbage_prefix = prefix + "garbage-" 

1368 

1369 def is_garbage(path): 

1370 """Check if path denotes directory scheduled for removal""" 

1371 bn = path.basename 

1372 return bn.startswith(garbage_prefix) 

1373 

1374 # prune old directories 

1375 udir_time = get_mtime(udir) 

1376 if keep and udir_time: 

1377 for path in rootdir.listdir(): 

1378 num = parse_num(path) 

1379 if num is not None and num <= (maxnum - keep): 

1380 try: 

1381 # try acquiring lock to remove directory as exclusive user 

1382 if lock_timeout: 

1383 create_lockfile(path) 

1384 except (error.EEXIST, error.ENOENT, error.EBUSY): 

1385 path_time = get_mtime(path) 

1386 if not path_time: 

1387 # assume directory doesn't exist now 

1388 continue 

1389 if abs(udir_time - path_time) < lock_timeout: 

1390 # assume directory with lockfile exists 

1391 # and lock timeout hasn't expired yet 

1392 continue 

1393 

1394 # path dir locked for exclusive use 

1395 # and scheduled for removal to avoid another thread/process 

1396 # treating it as a new directory or removal candidate 

1397 garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) 

1398 try: 

1399 path.rename(garbage_path) 

1400 garbage_path.remove(rec=1) 

1401 except KeyboardInterrupt: 

1402 raise 

1403 except Exception: # this might be error.Error, WindowsError ... 

1404 pass 

1405 if is_garbage(path): 

1406 try: 

1407 path.remove(rec=1) 

1408 except KeyboardInterrupt: 

1409 raise 

1410 except Exception: # this might be error.Error, WindowsError ... 

1411 pass 

1412 

1413 # make link... 

1414 try: 

1415 username = os.environ["USER"] # linux, et al 

1416 except KeyError: 

1417 try: 

1418 username = os.environ["USERNAME"] # windows 

1419 except KeyError: 

1420 username = "current" 

1421 

1422 src = str(udir) 

1423 dest = src[: src.rfind("-")] + "-" + username 

1424 try: 

1425 os.unlink(dest) 

1426 except OSError: 

1427 pass 

1428 try: 

1429 os.symlink(src, dest) 

1430 except (OSError, AttributeError, NotImplementedError): 

1431 pass 

1432 

1433 return udir 

1434 

1435 

1436def copymode(src, dest): 

1437 """Copy permission from src to dst.""" 

1438 import shutil 

1439 

1440 shutil.copymode(src, dest) 

1441 

1442 

1443def copystat(src, dest): 

1444 """Copy permission, last modification time, 

1445 last access time, and flags from src to dst.""" 

1446 import shutil 

1447 

1448 shutil.copystat(str(src), str(dest)) 

1449 

1450 

1451def copychunked(src, dest): 

1452 chunksize = 524288 # half a meg of bytes 

1453 fsrc = src.open("rb") 

1454 try: 

1455 fdest = dest.open("wb") 

1456 try: 

1457 while 1: 

1458 buf = fsrc.read(chunksize) 

1459 if not buf: 

1460 break 

1461 fdest.write(buf) 

1462 finally: 

1463 fdest.close() 

1464 finally: 

1465 fsrc.close() 

1466 

1467 

1468def isimportable(name): 

1469 if name and (name[0].isalpha() or name[0] == "_"): 

1470 name = name.replace("_", "") 

1471 return not name or name.isalnum() 

1472 

1473 

1474local = LocalPath