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
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1"""local path implementation."""
2from __future__ import annotations
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
30from . import error
32if TYPE_CHECKING:
33 from typing import Literal
35# Moved from local.py.
36iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
39class Checkers:
40 _depend_on_existence = "exists", "link", "dir", "file"
42 def __init__(self, path):
43 self.path = path
45 def dotfile(self):
46 return self.path.basename.startswith(".")
48 def ext(self, arg):
49 if not arg.startswith("."):
50 arg = "." + arg
51 return self.path.ext == arg
53 def basename(self, arg):
54 return self.path.basename == arg
56 def basestarts(self, arg):
57 return self.path.basename.startswith(arg)
59 def relto(self, arg):
60 return self.path.relto(arg)
62 def fnmatch(self, arg):
63 return self.path.fnmatch(arg)
65 def endswith(self, arg):
66 return str(self.path).endswith(arg)
68 def _evaluate(self, kw):
69 from .._code.source import getrawcode
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
106 _statcache: Stat
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
118 def dir(self):
119 return S_ISDIR(self._stat().mode)
121 def file(self):
122 return S_ISREG(self._stat().mode)
124 def exists(self):
125 return self._stat()
127 def link(self):
128 st = self.path.lstat()
129 return S_ISLNK(st.mode)
132class NeverRaised(Exception):
133 pass
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)
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
173class FNMatcher:
174 def __init__(self, pattern):
175 self.pattern = pattern
177 def __call__(self, path):
178 pattern = self.pattern
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)
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)
199def map_as_list(func, iter):
200 return list(map(func, iter))
203class Stat:
204 if TYPE_CHECKING:
206 @property
207 def size(self) -> int:
208 ...
210 @property
211 def mtime(self) -> float:
212 ...
214 def __getattr__(self, name: str) -> Any:
215 return getattr(self._osstatresult, "st_" + name)
217 def __init__(self, path, osstatresult):
218 self.path = path
219 self._osstatresult = osstatresult
221 @property
222 def owner(self):
223 if iswin32:
224 raise NotImplementedError("XXX win32")
225 import pwd
227 entry = error.checked_call(pwd.getpwuid, self.uid)
228 return entry[0]
230 @property
231 def group(self):
232 """Return group name of file."""
233 if iswin32:
234 raise NotImplementedError("XXX win32")
235 import grp
237 entry = error.checked_call(grp.getgrgid, self.gid)
238 return entry[0]
240 def isdir(self):
241 return S_ISDIR(self._osstatresult.st_mode)
243 def isfile(self):
244 return S_ISREG(self._osstatresult.st_mode)
246 def islink(self):
247 self.path.lstat()
248 return S_ISLNK(self._osstatresult.st_mode)
251def getuserid(user):
252 import pwd
254 if not isinstance(user, int):
255 user = pwd.getpwnam(user)[2]
256 return user
259def getgroupid(group):
260 import grp
262 if not isinstance(group, int):
263 group = grp.getgrnam(group)[2]
264 return group
267class LocalPath:
268 """Object oriented interface to os.path and other local filesystem
269 related information.
270 """
272 class ImportMismatchError(ImportError):
273 """raised on pyimport() if there is a mismatch of __file__'s"""
275 sep = os.sep
277 def __init__(self, path=None, expanduser=False):
278 """Initialize and return a local Path instance.
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)
301 if sys.platform != "win32":
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)
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]
322 def mklinkto(self, oldname):
323 """Posix style hard link to another name."""
324 error.checked_call(os.link, str(oldname), str(self))
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)
339 def __div__(self, other):
340 return self.join(os.fspath(other))
342 __truediv__ = __div__ # py3k
344 @property
345 def basename(self):
346 """Basename part of path."""
347 return self._getbyspec("basename")[0]
349 @property
350 def dirname(self):
351 """Dirname part of path."""
352 return self._getbyspec("dirname")[0]
354 @property
355 def purebasename(self):
356 """Pure base name of the path."""
357 return self._getbyspec("purebasename")[0]
359 @property
360 def ext(self):
361 """Extension of the path (including the '.')."""
362 return self._getbyspec("ext")[0]
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()
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()
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()
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"
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()
394 def load(self):
395 """(deprecated) return object unpickled from self.read()"""
396 f = self.open("rb")
397 try:
398 import pickle
400 return error.checked_call(pickle.load, f)
401 finally:
402 f.close()
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()
414 def fnmatch(self, pattern):
415 """Return true if the basename/fullname matches the glob-'pattern'.
417 valid pattern characters::
419 * matches everything
420 ? matches any single character
421 [seq] matches any character in seq
422 [!seq] matches any char not in seq
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.
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)
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 ""
452 def ensure_dir(self, *args):
453 """Ensure the path joined with args is a directory."""
454 return self.ensure(*args, **{"dir": True})
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)
482 def exists(self):
483 return self.check()
485 def isdir(self):
486 return self.check(dir=1)
488 def isfile(self):
489 return self.check(file=1)
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
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
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))
522 def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False):
523 """Yields all paths below the current one
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)
529 rec is a filter (glob pattern or callable) that controls whether
530 a node is descended, defaulting to None
532 ignore is an Exception class that is ignoredwhen calling dirlist()
533 on any of the paths (by default, all exceptions are reported)
535 bf if True will cause a breadthfirst search instead of the
536 default depthfirst. Default: False
538 sort if True will sort entries within each directory level.
539 """
540 yield from Visitor(fil, rec, ignore, bf, sort).gen(self)
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()
555 def __fspath__(self):
556 return self.strpath
558 def __hash__(self):
559 s = self.strpath
560 if iswin32:
561 s = s.lower()
562 return hash(s)
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
578 def __ne__(self, other):
579 return not (self == other)
581 def __lt__(self, other):
582 return os.fspath(self) < os.fspath(other)
584 def __gt__(self, other):
585 return os.fspath(self) > os.fspath(other)
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)
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
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)
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()
642 def new(self, **kw):
643 """Create a modified version of this path.
644 the following keyword arguments modify various path parts::
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
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
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)
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
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)
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
751 def open(self, mode="r", ensure=False, encoding=None):
752 """Return an opened file with the given mode.
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)
762 def _fastjoin(self, name):
763 child = object.__new__(self.__class__)
764 child.strpath = self.strpath + self.sep + name
765 return child
767 def islink(self):
768 return islink(self.strpath)
770 def check(self, **kw):
771 """Check a path for existence and properties.
773 Without arguments, return True if the path exists, otherwise False.
775 valid checkers::
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
783 You can specify multiple checker definitions, for example::
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)
798 _patternchars = set("*?[" + os.path.sep)
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
823 def size(self) -> int:
824 """Return size of the underlying file object"""
825 return self.stat().size
827 def mtime(self) -> float:
828 """Return last modification time of the path."""
829 return self.stat().mtime
831 def copy(self, target, mode=False, stat=False):
832 """Copy path to target.
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:
849 def rec(p):
850 return p.check(link=0)
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)
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)
873 def dump(self, obj, bin=1):
874 """Pickle object into path location"""
875 f = self.open("wb")
876 import pickle
878 try:
879 error.checked_call(pickle.dump, obj, f, bin)
880 finally:
881 f.close()
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
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)
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)
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()
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
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
958 @overload
959 def stat(self, raising: Literal[True] = ...) -> Stat:
960 ...
962 @overload
963 def stat(self, raising: Literal[False]) -> Stat | None:
964 ...
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
977 def lstat(self) -> Stat:
978 """Return an os.lstat() tuple."""
979 return Stat(self, error.checked_call(os.lstat, self.strpath))
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.
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))
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
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()
1017 def realpath(self):
1018 """Return a new path which contains no symbolic links."""
1019 return self.__class__(os.path.realpath(self.strpath))
1021 def atime(self):
1022 """Return last access time of the path."""
1023 return self.stat().atime
1025 def __repr__(self):
1026 return "local(%r)" % self.strpath
1028 def __str__(self):
1029 """Return string representation of the Path."""
1030 return self.strpath
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)
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
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)
1069 def pyimport(self, modname=None, ensuresyspath=True):
1070 """Return path as an imported python module.
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.
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)
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
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
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
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
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
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
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("")
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
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)
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
1260 return local(tempfile.gettempdir())
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
1269 if rootdir is None:
1270 rootdir = cls.get_temproot()
1271 return cls(error.checked_call(tempfile.mkdtemp, dir=str(rootdir)))
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()
1286 nprefix = prefix.lower()
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
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
1311 def atexit_remove_lockfile(lockfile):
1312 """Ensure lockfile is removed at process exit"""
1313 mypid = os.getpid()
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
1328 atexit.register(try_remove_lockfile)
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)
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
1360 def get_mtime(path):
1361 """Read file modification time"""
1362 try:
1363 return path.lstat().mtime
1364 except error.Error:
1365 pass
1367 garbage_prefix = prefix + "garbage-"
1369 def is_garbage(path):
1370 """Check if path denotes directory scheduled for removal"""
1371 bn = path.basename
1372 return bn.startswith(garbage_prefix)
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
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
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"
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
1433 return udir
1436def copymode(src, dest):
1437 """Copy permission from src to dst."""
1438 import shutil
1440 shutil.copymode(src, dest)
1443def copystat(src, dest):
1444 """Copy permission, last modification time,
1445 last access time, and flags from src to dst."""
1446 import shutil
1448 shutil.copystat(str(src), str(dest))
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()
1468def isimportable(name):
1469 if name and (name[0].isalpha() or name[0] == "_"):
1470 name = name.replace("_", "")
1471 return not name or name.isalnum()
1474local = LocalPath