1"""local path implementation."""

2 from __future__ import annotations

3

4 import atexit

5 import fnmatch

6 import importlib.util

7 import io

8 import os

9 import posixpath

10 import sys

11 import uuid

12 import warnings

13 from contextlib import contextmanager

14 from os.path import abspath

15 from os.path import dirname

16 from os.path import exists

17 from os.path import isabs

18 from os.path import isdir

19 from os.path import isfile

20 from os.path import islink

21 from os.path import normpath

22 from stat import S_ISDIR

23 from stat import S_ISLNK

24 from stat import S_ISREG

25 from typing import Any

26 from typing import Callable

27 from typing import overload

28 from typing import TYPE_CHECKING

29

30 from . import error

31

32 if TYPE_CHECKING:

33 from typing import Literal

34

35# Moved from local.py.

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

37

38

39 class 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

132 class NeverRaised(Exception):

133 pass

134

135

136 class 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

173 class 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

199 def map_as_list(func, iter):

200 return list(map(func, iter))

201

202

203 class 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

251 def getuserid(user):

252 import pwd

253

254 if not isinstance(user, int):

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

256 return user

257

258

259 def getgroupid(group):

260 import grp

261

262 if not isinstance(group, int):

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

264 return group

265

266

267 class 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

1436 def copymode(src, dest):

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

1438 import shutil

1439

1440 shutil.copymode(src, dest)

1441

1442

1443 def 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

1451 def 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

1468 def 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

1474 local = LocalPath