From 19dda7c9bdc8ef71c792e0f77a9595bfad8d9248 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 10:10:49 -0400 Subject: [PATCH 01/20] vendor py.path and py.error --- .pre-commit-config.yaml | 1 - setup.cfg | 3 +- src/_pytest/_py/__init__.py | 0 src/_pytest/_py/error.py | 91 +++ src/_pytest/_py/path.py | 1489 +++++++++++++++++++++++++++++++++++ src/py.py | 10 + 6 files changed, 1592 insertions(+), 2 deletions(-) create mode 100644 src/_pytest/_py/__init__.py create mode 100644 src/_pytest/_py/error.py create mode 100644 src/_pytest/_py/path.py create mode 100644 src/py.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3cc1b1de718..de612d96988 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,7 +65,6 @@ repos: args: [] additional_dependencies: - iniconfig>=1.1.0 - - py>=1.8.2 - attrs>=19.2.0 - packaging - tomli diff --git a/setup.cfg b/setup.cfg index 38f50556c93..39ade4dff4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,16 +36,17 @@ packages = _pytest _pytest._code _pytest._io + _pytest._py _pytest.assertion _pytest.config _pytest.mark pytest +py_modules = py install_requires = attrs>=19.2.0 iniconfig packaging pluggy>=0.12,<2.0 - py>=1.8.2 colorama;sys_platform=="win32" exceptiongroup>=1.0.0rc8;python_version<"3.11" importlib-metadata>=0.12;python_version<"3.8" diff --git a/src/_pytest/_py/__init__.py b/src/_pytest/_py/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/_pytest/_py/error.py b/src/_pytest/_py/error.py new file mode 100644 index 00000000000..e1e3ccd28af --- /dev/null +++ b/src/_pytest/_py/error.py @@ -0,0 +1,91 @@ +""" +create errno-specific classes for IO or os calls. + +""" +from types import ModuleType +import sys, os, errno + +class Error(EnvironmentError): + def __repr__(self): + return "%s.%s %r: %s " %(self.__class__.__module__, + self.__class__.__name__, + self.__class__.__doc__, + " ".join(map(str, self.args)), + #repr(self.args) + ) + + def __str__(self): + s = "[%s]: %s" %(self.__class__.__doc__, + " ".join(map(str, self.args)), + ) + return s + +_winerrnomap = { + 2: errno.ENOENT, + 3: errno.ENOENT, + 17: errno.EEXIST, + 18: errno.EXDEV, + 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable + 22: errno.ENOTDIR, + 20: errno.ENOTDIR, + 267: errno.ENOTDIR, + 5: errno.EACCES, # anything better? +} + +class ErrorMaker(ModuleType): + """ lazily provides Exception classes for each possible POSIX errno + (as defined per the 'errno' module). All such instances + subclass EnvironmentError. + """ + Error = Error + _errno2class = {} + + def __getattr__(self, name): + if name[0] == "_": + raise AttributeError(name) + eno = getattr(errno, name) + cls = self._geterrnoclass(eno) + setattr(self, name, cls) + return cls + + def _geterrnoclass(self, eno): + try: + return self._errno2class[eno] + except KeyError: + clsname = errno.errorcode.get(eno, "UnknownErrno%d" %(eno,)) + errorcls = type(Error)(clsname, (Error,), + {'__module__':'py.error', + '__doc__': os.strerror(eno)}) + self._errno2class[eno] = errorcls + return errorcls + + def checked_call(self, func, *args, **kwargs): + """ call a function and raise an errno-exception if applicable. """ + __tracebackhide__ = True + try: + return func(*args, **kwargs) + except self.Error: + raise + except (OSError, EnvironmentError): + cls, value, tb = sys.exc_info() + if not hasattr(value, 'errno'): + raise + __tracebackhide__ = False + errno = value.errno + try: + if not isinstance(value, WindowsError): + raise NameError + except NameError: + # we are not on Windows, or we got a proper OSError + cls = self._geterrnoclass(errno) + else: + try: + cls = self._geterrnoclass(_winerrnomap[errno]) + except KeyError: + raise value + raise cls("%s%r" % (func.__name__, args)) + __tracebackhide__ = True + + +error = ErrorMaker('_pytest._py.error') +sys.modules[error.__name__] = error diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py new file mode 100644 index 00000000000..e8adbf27a79 --- /dev/null +++ b/src/_pytest/_py/path.py @@ -0,0 +1,1489 @@ +""" +local path implementation. +""" +from __future__ import with_statement + +from contextlib import contextmanager +import sys, os, atexit, io, uuid +from stat import S_ISLNK, S_ISDIR, S_ISREG + +from os.path import abspath, normpath, isabs, exists, isdir, isfile, islink, dirname + +import warnings +import os +import sys +import posixpath +import fnmatch + +from . import error + +# Moved from local.py. +iswin32 = sys.platform == "win32" or (getattr(os, '_name', False) == 'nt') + +try: + # FileNotFoundError might happen in py34, and is not available with py27. + import_errors = (ImportError, FileNotFoundError) +except NameError: + import_errors = (ImportError,) + +try: + from os import fspath +except ImportError: + def fspath(path): + """ + Return the string representation of the path. + If str or bytes is passed in, it is returned unchanged. + This code comes from PEP 519, modified to support earlier versions of + python. + + This is required for python < 3.6. + """ + if isinstance(path, (str, bytes)): + return path + + # Work from the object's type to match method resolution of other magic + # methods. + path_type = type(path) + try: + return path_type.__fspath__(path) + except AttributeError: + if hasattr(path_type, '__fspath__'): + raise + try: + import pathlib + except import_errors: + pass + else: + if isinstance(path, pathlib.PurePath): + return str(path) + + raise TypeError("expected str, bytes or os.PathLike object, not " + + path_type.__name__) + +class Checkers: + _depend_on_existence = 'exists', 'link', 'dir', 'file' + + def __init__(self, path): + self.path = path + + def dir(self): + raise NotImplementedError + + def file(self): + raise NotImplementedError + + def dotfile(self): + return self.path.basename.startswith('.') + + def ext(self, arg): + if not arg.startswith('.'): + arg = '.' + arg + return self.path.ext == arg + + def exists(self): + raise NotImplementedError + + def basename(self, arg): + return self.path.basename == arg + + def basestarts(self, arg): + return self.path.basename.startswith(arg) + + def relto(self, arg): + return self.path.relto(arg) + + def fnmatch(self, arg): + return self.path.fnmatch(arg) + + def endswith(self, arg): + return str(self.path).endswith(arg) + + def _evaluate(self, kw): + for name, value in kw.items(): + invert = False + meth = None + try: + meth = getattr(self, name) + except AttributeError: + if name[:3] == 'not': + invert = True + try: + meth = getattr(self, name[3:]) + except AttributeError: + pass + if meth is None: + raise TypeError( + "no %r checker available for %r" % (name, self.path)) + try: + if py.code.getrawcode(meth).co_argcount > 1: + if (not meth(value)) ^ invert: + return False + else: + if bool(value) ^ bool(meth()) ^ invert: + return False + except (error.ENOENT, error.ENOTDIR, error.EBUSY): + # EBUSY feels not entirely correct, + # but its kind of necessary since ENOMEDIUM + # is not accessible in python + for name in self._depend_on_existence: + if name in kw: + if kw.get(name): + return False + name = 'not' + name + if name in kw: + if not kw.get(name): + return False + return True + +class NeverRaised(Exception): + pass + +class PathBase(object): + """ shared implementation for filesystem path objects.""" + Checkers = Checkers + + def __div__(self, other): + return self.join(fspath(other)) + __truediv__ = __div__ # py3k + + def basename(self): + """ basename part of path. """ + return self._getbyspec('basename')[0] + basename = property(basename, None, None, basename.__doc__) + + def dirname(self): + """ dirname part of path. """ + return self._getbyspec('dirname')[0] + dirname = property(dirname, None, None, dirname.__doc__) + + def purebasename(self): + """ pure base name of the path.""" + return self._getbyspec('purebasename')[0] + purebasename = property(purebasename, None, None, purebasename.__doc__) + + def ext(self): + """ extension of the path (including the '.').""" + return self._getbyspec('ext')[0] + ext = property(ext, None, None, ext.__doc__) + + def dirpath(self, *args, **kwargs): + """ return the directory path joined with any given path arguments. """ + return self.new(basename='').join(*args, **kwargs) + + def read_binary(self): + """ read and return a bytestring from reading the path. """ + with self.open('rb') as f: + return f.read() + + def read_text(self, encoding): + """ read and return a Unicode string from reading the path. """ + with self.open("r", encoding=encoding) as f: + return f.read() + + + def read(self, mode='r'): + """ read and return a bytestring from reading the path. """ + with self.open(mode) as f: + return f.read() + + def readlines(self, cr=1): + """ read and return a list of lines from the path. if cr is False, the +newline will be removed from the end of each line. """ + if sys.version_info < (3, ): + mode = 'rU' + else: # python 3 deprecates mode "U" in favor of "newline" option + mode = 'r' + + if not cr: + content = self.read(mode) + return content.split('\n') + else: + f = self.open(mode) + try: + return f.readlines() + finally: + f.close() + + def load(self): + """ (deprecated) return object unpickled from self.read() """ + f = self.open('rb') + try: + import pickle + return error.checked_call(pickle.load, f) + finally: + f.close() + + def move(self, target): + """ move this path to target. """ + if target.relto(self): + raise error.EINVAL( + target, + "cannot move path into a subdirectory of itself") + try: + self.rename(target) + except error.EXDEV: # invalid cross-device link + self.copy(target) + self.remove() + + def __repr__(self): + """ return a string representation of this path. """ + return repr(str(self)) + + def check(self, **kw): + """ check a path for existence and properties. + + Without arguments, return True if the path exists, otherwise False. + + valid checkers:: + + file=1 # is a file + file=0 # is not a file (may not even exist) + dir=1 # is a dir + link=1 # is a link + exists=1 # exists + + You can specify multiple checker definitions, for example:: + + path.check(file=1, link=1) # a link pointing to a file + """ + if not kw: + kw = {'exists': 1} + return self.Checkers(self)._evaluate(kw) + + def fnmatch(self, pattern): + """return true if the basename/fullname matches the glob-'pattern'. + + valid pattern characters:: + + * matches everything + ? matches any single character + [seq] matches any character in seq + [!seq] matches any char not in seq + + If the pattern contains a path-separator then the full path + is used for pattern matching and a '*' is prepended to the + pattern. + + if the pattern doesn't contain a path-separator the pattern + is only matched against the basename. + """ + return FNMatcher(pattern)(self) + + def relto(self, relpath): + """ return a string which is the relative part of the path + to the given 'relpath'. + """ + if not isinstance(relpath, (str, PathBase)): + raise TypeError("%r: not a string or path object" %(relpath,)) + strrelpath = str(relpath) + if strrelpath and strrelpath[-1] != self.sep: + strrelpath += self.sep + #assert strrelpath[-1] == self.sep + #assert strrelpath[-2] != self.sep + strself = self.strpath + if sys.platform == "win32" or getattr(os, '_name', None) == 'nt': + if os.path.normcase(strself).startswith( + os.path.normcase(strrelpath)): + return strself[len(strrelpath):] + elif strself.startswith(strrelpath): + return strself[len(strrelpath):] + return "" + + def ensure_dir(self, *args): + """ ensure the path joined with args is a directory. """ + return self.ensure(*args, **{"dir": True}) + + def bestrelpath(self, dest): + """ return a string which is a relative path from self + (assumed to be a directory) to dest such that + self.join(bestrelpath) == dest and if not such + path can be determined return dest. + """ + try: + if self == dest: + return os.curdir + base = self.common(dest) + if not base: # can be the case on windows + return str(dest) + self2base = self.relto(base) + reldest = dest.relto(base) + if self2base: + n = self2base.count(self.sep) + 1 + else: + n = 0 + l = [os.pardir] * n + if reldest: + l.append(reldest) + target = dest.sep.join(l) + return target + except AttributeError: + return str(dest) + + def exists(self): + return self.check() + + def isdir(self): + return self.check(dir=1) + + def isfile(self): + return self.check(file=1) + + def parts(self, reverse=False): + """ return a root-first list of all ancestor directories + plus the path itself. + """ + current = self + l = [self] + while 1: + last = current + current = current.dirpath() + if last == current: + break + l.append(current) + if not reverse: + l.reverse() + return l + + def common(self, other): + """ return the common part shared with the other path + or None if there is no common part. + """ + last = None + for x, y in zip(self.parts(), other.parts()): + if x != y: + return last + last = x + return last + + def __add__(self, other): + """ return new path object with 'other' added to the basename""" + return self.new(basename=self.basename+str(other)) + + def __cmp__(self, other): + """ return sort value (-1, 0, +1). """ + try: + return cmp(self.strpath, other.strpath) + except AttributeError: + return cmp(str(self), str(other)) # self.path, other.path) + + def __lt__(self, other): + try: + return self.strpath < other.strpath + except AttributeError: + return str(self) < str(other) + + def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): + """ yields all paths below the current one + + fil is a filter (glob pattern or callable), if not matching the + path will not be yielded, defaulting to None (everything is + returned) + + rec is a filter (glob pattern or callable) that controls whether + a node is descended, defaulting to None + + ignore is an Exception class that is ignoredwhen calling dirlist() + on any of the paths (by default, all exceptions are reported) + + bf if True will cause a breadthfirst search instead of the + default depthfirst. Default: False + + sort if True will sort entries within each directory level. + """ + for x in Visitor(fil, rec, ignore, bf, sort).gen(self): + yield x + + def _sortlist(self, res, sort): + if sort: + if hasattr(sort, '__call__'): + warnings.warn(DeprecationWarning( + "listdir(sort=callable) is deprecated and breaks on python3" + ), stacklevel=3) + res.sort(sort) + else: + res.sort() + + def samefile(self, other): + """ return True if other refers to the same stat object as self. """ + return self.strpath == str(other) + + def __fspath__(self): + return self.strpath + +class Visitor: + def __init__(self, fil, rec, ignore, bf, sort): + if isinstance(fil, py.builtin._basestring): + fil = FNMatcher(fil) + if isinstance(rec, py.builtin._basestring): + self.rec = FNMatcher(rec) + elif not hasattr(rec, '__call__') and rec: + self.rec = lambda path: True + else: + self.rec = rec + self.fil = fil + self.ignore = ignore + self.breadthfirst = bf + self.optsort = sort and sorted or (lambda x: x) + + def gen(self, path): + try: + entries = path.listdir() + except self.ignore: + return + rec = self.rec + dirs = self.optsort([p for p in entries + if p.check(dir=1) and (rec is None or rec(p))]) + if not self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + for p in self.optsort(entries): + if self.fil is None or self.fil(p): + yield p + if self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + +class FNMatcher: + def __init__(self, pattern): + self.pattern = pattern + + def __call__(self, path): + pattern = self.pattern + + if (pattern.find(path.sep) == -1 and + iswin32 and + pattern.find(posixpath.sep) != -1): + # Running on Windows, the pattern has no Windows path separators, + # and the pattern has one or more Posix path separators. Replace + # the Posix path separators with the Windows path separator. + pattern = pattern.replace(posixpath.sep, path.sep) + + if pattern.find(path.sep) == -1: + name = path.basename + else: + name = str(path) # path.strpath # XXX svn? + if not os.path.isabs(pattern): + pattern = '*' + path.sep + pattern + return fnmatch.fnmatch(name, pattern) + + +if sys.version_info > (3,0): + def map_as_list(func, iter): + return list(map(func, iter)) +else: + map_as_list = map + +ALLOW_IMPORTLIB_MODE = sys.version_info > (3,5) +if ALLOW_IMPORTLIB_MODE: + import importlib + + +class Stat(object): + def __getattr__(self, name): + return getattr(self._osstatresult, "st_" + name) + + def __init__(self, path, osstatresult): + self.path = path + self._osstatresult = osstatresult + + @property + def owner(self): + if iswin32: + raise NotImplementedError("XXX win32") + import pwd + entry = error.checked_call(pwd.getpwuid, self.uid) + return entry[0] + + @property + def group(self): + """ return group name of file. """ + if iswin32: + raise NotImplementedError("XXX win32") + import grp + entry = error.checked_call(grp.getgrgid, self.gid) + return entry[0] + + def isdir(self): + return S_ISDIR(self._osstatresult.st_mode) + + def isfile(self): + return S_ISREG(self._osstatresult.st_mode) + + def islink(self): + st = self.path.lstat() + return S_ISLNK(self._osstatresult.st_mode) + +class PosixPath(PathBase): + def chown(self, user, group, rec=0): + """ change ownership to the given user and group. + user and group may be specified by a number or + by a name. if rec is True change ownership + recursively. + """ + uid = getuserid(user) + gid = getgroupid(group) + if rec: + for x in self.visit(rec=lambda x: x.check(link=0)): + if x.check(link=0): + error.checked_call(os.chown, str(x), uid, gid) + error.checked_call(os.chown, str(self), uid, gid) + + def readlink(self): + """ return value of a symbolic link. """ + return error.checked_call(os.readlink, self.strpath) + + def mklinkto(self, oldname): + """ posix style hard link to another name. """ + error.checked_call(os.link, str(oldname), str(self)) + + def mksymlinkto(self, value, absolute=1): + """ create a symbolic link with the given value (pointing to another name). """ + if absolute: + error.checked_call(os.symlink, str(value), self.strpath) + else: + base = self.common(value) + # with posix local paths '/' is always a common base + relsource = self.__class__(value).relto(base) + reldest = self.relto(base) + n = reldest.count(self.sep) + target = self.sep.join(('..', )*n + (relsource, )) + error.checked_call(os.symlink, target, self.strpath) + +def getuserid(user): + import pwd + if not isinstance(user, int): + user = pwd.getpwnam(user)[2] + return user + +def getgroupid(group): + import grp + if not isinstance(group, int): + group = grp.getgrnam(group)[2] + return group + +FSBase = not iswin32 and PosixPath or PathBase + +class LocalPath(FSBase): + """ object oriented interface to os.path and other local filesystem + related information. + """ + class ImportMismatchError(ImportError): + """ raised on pyimport() if there is a mismatch of __file__'s""" + + sep = os.sep + class Checkers(Checkers): + def _stat(self): + try: + return self._statcache + except AttributeError: + try: + self._statcache = self.path.stat() + except error.ELOOP: + self._statcache = self.path.lstat() + return self._statcache + + def dir(self): + return S_ISDIR(self._stat().mode) + + def file(self): + return S_ISREG(self._stat().mode) + + def exists(self): + return self._stat() + + def link(self): + st = self.path.lstat() + return S_ISLNK(st.mode) + + def __init__(self, path=None, expanduser=False): + """ Initialize and return a local Path instance. + + Path can be relative to the current directory. + If path is None it defaults to the current working directory. + If expanduser is True, tilde-expansion is performed. + Note that Path instances always carry an absolute path. + Note also that passing in a local path object will simply return + the exact same path object. Use new() to get a new copy. + """ + if path is None: + self.strpath = error.checked_call(os.getcwd) + else: + try: + path = fspath(path) + except TypeError: + raise ValueError("can only pass None, Path instances " + "or non-empty strings to LocalPath") + if expanduser: + path = os.path.expanduser(path) + self.strpath = abspath(path) + + def __hash__(self): + s = self.strpath + if iswin32: + s = s.lower() + return hash(s) + + def __eq__(self, other): + s1 = fspath(self) + try: + s2 = fspath(other) + except TypeError: + return False + if iswin32: + s1 = s1.lower() + try: + s2 = s2.lower() + except AttributeError: + return False + return s1 == s2 + + def __ne__(self, other): + return not (self == other) + + def __lt__(self, other): + return fspath(self) < fspath(other) + + def __gt__(self, other): + return fspath(self) > fspath(other) + + def samefile(self, other): + """ return True if 'other' references the same file as 'self'. + """ + other = fspath(other) + if not isabs(other): + other = abspath(other) + if self == other: + return True + if not hasattr(os.path, "samefile"): + return False + return error.checked_call( + os.path.samefile, self.strpath, other) + + def remove(self, rec=1, ignore_errors=False): + """ remove a file or directory (or a directory tree if rec=1). + if ignore_errors is True, errors while removing directories will + be ignored. + """ + if self.check(dir=1, link=0): + if rec: + # force remove of readonly files on windows + if iswin32: + self.chmod(0o700, rec=1) + import shutil + error.checked_call( + shutil.rmtree, self.strpath, + ignore_errors=ignore_errors) + else: + error.checked_call(os.rmdir, self.strpath) + else: + if iswin32: + self.chmod(0o700) + error.checked_call(os.remove, self.strpath) + + def computehash(self, hashtype="md5", chunksize=524288): + """ return hexdigest of hashvalue for this file. """ + try: + try: + import hashlib as mod + except ImportError: + if hashtype == "sha1": + hashtype = "sha" + mod = __import__(hashtype) + hash = getattr(mod, hashtype)() + except (AttributeError, ImportError): + raise ValueError("Don't know how to compute %r hash" %(hashtype,)) + f = self.open('rb') + try: + while 1: + buf = f.read(chunksize) + if not buf: + return hash.hexdigest() + hash.update(buf) + finally: + f.close() + + def new(self, **kw): + """ create a modified version of this path. + the following keyword arguments modify various path parts:: + + a:/some/path/to/a/file.ext + xx drive + xxxxxxxxxxxxxxxxx dirname + xxxxxxxx basename + xxxx purebasename + xxx ext + """ + obj = object.__new__(self.__class__) + if not kw: + obj.strpath = self.strpath + return obj + drive, dirname, basename, purebasename,ext = self._getbyspec( + "drive,dirname,basename,purebasename,ext") + if 'basename' in kw: + if 'purebasename' in kw or 'ext' in kw: + raise ValueError("invalid specification %r" % kw) + else: + pb = kw.setdefault('purebasename', purebasename) + try: + ext = kw['ext'] + except KeyError: + pass + else: + if ext and not ext.startswith('.'): + ext = '.' + ext + kw['basename'] = pb + ext + + if ('dirname' in kw and not kw['dirname']): + kw['dirname'] = drive + else: + kw.setdefault('dirname', dirname) + kw.setdefault('sep', self.sep) + obj.strpath = normpath( + "%(dirname)s%(sep)s%(basename)s" % kw) + return obj + + def _getbyspec(self, spec): + """ see new for what 'spec' can be. """ + res = [] + parts = self.strpath.split(self.sep) + + args = filter(None, spec.split(',') ) + append = res.append + for name in args: + if name == 'drive': + append(parts[0]) + elif name == 'dirname': + append(self.sep.join(parts[:-1])) + else: + basename = parts[-1] + if name == 'basename': + append(basename) + else: + i = basename.rfind('.') + if i == -1: + purebasename, ext = basename, '' + else: + purebasename, ext = basename[:i], basename[i:] + if name == 'purebasename': + append(purebasename) + elif name == 'ext': + append(ext) + else: + raise ValueError("invalid part specification %r" % name) + return res + + def dirpath(self, *args, **kwargs): + """ return the directory path joined with any given path arguments. """ + if not kwargs: + path = object.__new__(self.__class__) + path.strpath = dirname(self.strpath) + if args: + path = path.join(*args) + return path + return super(LocalPath, self).dirpath(*args, **kwargs) + + def join(self, *args, **kwargs): + """ return a new path by appending all 'args' as path + components. if abs=1 is used restart from root if any + of the args is an absolute path. + """ + sep = self.sep + strargs = [fspath(arg) for arg in args] + strpath = self.strpath + if kwargs.get('abs'): + newargs = [] + for arg in reversed(strargs): + if isabs(arg): + strpath = arg + strargs = newargs + break + newargs.insert(0, arg) + # special case for when we have e.g. strpath == "/" + actual_sep = "" if strpath.endswith(sep) else sep + for arg in strargs: + arg = arg.strip(sep) + if iswin32: + # allow unix style paths even on windows. + arg = arg.strip('/') + arg = arg.replace('/', sep) + strpath = strpath + actual_sep + arg + actual_sep = sep + obj = object.__new__(self.__class__) + obj.strpath = normpath(strpath) + return obj + + def open(self, mode='r', ensure=False, encoding=None): + """ return an opened file with the given mode. + + If ensure is True, create parent directories if needed. + """ + if ensure: + self.dirpath().ensure(dir=1) + if encoding: + return error.checked_call(io.open, self.strpath, mode, encoding=encoding) + return error.checked_call(open, self.strpath, mode) + + def _fastjoin(self, name): + child = object.__new__(self.__class__) + child.strpath = self.strpath + self.sep + name + return child + + def islink(self): + return islink(self.strpath) + + def check(self, **kw): + if not kw: + return exists(self.strpath) + if len(kw) == 1: + if "dir" in kw: + return not kw["dir"] ^ isdir(self.strpath) + if "file" in kw: + return not kw["file"] ^ isfile(self.strpath) + return super(LocalPath, self).check(**kw) + + _patternchars = set("*?[" + os.path.sep) + def listdir(self, fil=None, sort=None): + """ list directory contents, possibly filter by the given fil func + and possibly sorted. + """ + if fil is None and sort is None: + names = error.checked_call(os.listdir, self.strpath) + return map_as_list(self._fastjoin, names) + if isinstance(fil, py.builtin._basestring): + if not self._patternchars.intersection(fil): + child = self._fastjoin(fil) + if exists(child.strpath): + return [child] + return [] + fil = FNMatcher(fil) + names = error.checked_call(os.listdir, self.strpath) + res = [] + for name in names: + child = self._fastjoin(name) + if fil is None or fil(child): + res.append(child) + self._sortlist(res, sort) + return res + + def size(self): + """ return size of the underlying file object """ + return self.stat().size + + def mtime(self): + """ return last modification time of the path. """ + return self.stat().mtime + + def copy(self, target, mode=False, stat=False): + """ copy path to target. + + If mode is True, will copy copy permission from path to target. + If stat is True, copy permission, last modification + time, last access time, and flags from path to target. + """ + if self.check(file=1): + if target.check(dir=1): + target = target.join(self.basename) + assert self!=target + copychunked(self, target) + if mode: + copymode(self.strpath, target.strpath) + if stat: + copystat(self, target) + else: + def rec(p): + return p.check(link=0) + for x in self.visit(rec=rec): + relpath = x.relto(self) + newx = target.join(relpath) + newx.dirpath().ensure(dir=1) + if x.check(link=1): + newx.mksymlinkto(x.readlink()) + continue + elif x.check(file=1): + copychunked(x, newx) + elif x.check(dir=1): + newx.ensure(dir=1) + if mode: + copymode(x.strpath, newx.strpath) + if stat: + copystat(x, newx) + + def rename(self, target): + """ rename this path to target. """ + target = fspath(target) + return error.checked_call(os.rename, self.strpath, target) + + def dump(self, obj, bin=1): + """ pickle object into path location""" + f = self.open('wb') + import pickle + try: + error.checked_call(pickle.dump, obj, f, bin) + finally: + f.close() + + def mkdir(self, *args): + """ create & return the directory joined with args. """ + p = self.join(*args) + error.checked_call(os.mkdir, fspath(p)) + return p + + def write_binary(self, data, ensure=False): + """ write binary data into path. If ensure is True create + missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + with self.open('wb') as f: + f.write(data) + + def write_text(self, data, encoding, ensure=False): + """ write text data into path using the specified encoding. + If ensure is True create missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + with self.open('w', encoding=encoding) as f: + f.write(data) + + def write(self, data, mode='w', ensure=False): + """ write data into path. If ensure is True create + missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + if 'b' in mode: + if not py.builtin._isbytes(data): + raise ValueError("can only process bytes") + else: + if not py.builtin._istext(data): + if not py.builtin._isbytes(data): + data = str(data) + else: + data = py.builtin._totext(data, sys.getdefaultencoding()) + f = self.open(mode) + try: + f.write(data) + finally: + f.close() + + def _ensuredirs(self): + parent = self.dirpath() + if parent == self: + return self + if parent.check(dir=0): + parent._ensuredirs() + if self.check(dir=0): + try: + self.mkdir() + except error.EEXIST: + # race condition: file/dir created by another thread/process. + # complain if it is not a dir + if self.check(dir=0): + raise + return self + + def ensure(self, *args, **kwargs): + """ ensure that an args-joined path exists (by default as + a file). if you specify a keyword argument 'dir=True' + then the path is forced to be a directory path. + """ + p = self.join(*args) + if kwargs.get('dir', 0): + return p._ensuredirs() + else: + p.dirpath()._ensuredirs() + if not p.check(file=1): + p.open('w').close() + return p + + def stat(self, raising=True): + """ Return an os.stat() tuple. """ + if raising == True: + return Stat(self, error.checked_call(os.stat, self.strpath)) + try: + return Stat(self, os.stat(self.strpath)) + except KeyboardInterrupt: + raise + except Exception: + return None + + def lstat(self): + """ Return an os.lstat() tuple. """ + return Stat(self, error.checked_call(os.lstat, self.strpath)) + + def setmtime(self, mtime=None): + """ set modification time for the given path. if 'mtime' is None + (the default) then the file's mtime is set to current time. + + Note that the resolution for 'mtime' is platform dependent. + """ + if mtime is None: + return error.checked_call(os.utime, self.strpath, mtime) + try: + return error.checked_call(os.utime, self.strpath, (-1, mtime)) + except error.EINVAL: + return error.checked_call(os.utime, self.strpath, (self.atime(), mtime)) + + def chdir(self): + """ change directory to self and return old current directory """ + try: + old = self.__class__() + except error.ENOENT: + old = None + error.checked_call(os.chdir, self.strpath) + return old + + + @contextmanager + def as_cwd(self): + """ + Return a context manager, which changes to the path's dir during the + managed "with" context. + On __enter__ it returns the old dir, which might be ``None``. + """ + old = self.chdir() + try: + yield old + finally: + if old is not None: + old.chdir() + + def realpath(self): + """ return a new path which contains no symbolic links.""" + return self.__class__(os.path.realpath(self.strpath)) + + def atime(self): + """ return last access time of the path. """ + return self.stat().atime + + def __repr__(self): + return 'local(%r)' % self.strpath + + def __str__(self): + """ return string representation of the Path. """ + return self.strpath + + def chmod(self, mode, rec=0): + """ change permissions to the given mode. If mode is an + integer it directly encodes the os-specific modes. + if rec is True perform recursively. + """ + if not isinstance(mode, int): + raise TypeError("mode %r must be an integer" % (mode,)) + if rec: + for x in self.visit(rec=rec): + error.checked_call(os.chmod, str(x), mode) + error.checked_call(os.chmod, self.strpath, mode) + + def pypkgpath(self): + """ return the Python package path by looking for the last + directory upwards which still contains an __init__.py. + Return None if a pkgpath can not be determined. + """ + pkgpath = None + for parent in self.parts(reverse=True): + if parent.isdir(): + if not parent.join('__init__.py').exists(): + break + if not isimportable(parent.basename): + break + pkgpath = parent + return pkgpath + + def _ensuresyspath(self, ensuremode, path): + if ensuremode: + s = str(path) + if ensuremode == "append": + if s not in sys.path: + sys.path.append(s) + else: + if s != sys.path[0]: + sys.path.insert(0, s) + + def pyimport(self, modname=None, ensuresyspath=True): + """ return path as an imported python module. + + If modname is None, look for the containing package + and construct an according module name. + The module will be put/looked up in sys.modules. + if ensuresyspath is True then the root dir for importing + the file (taking __init__.py files into account) will + be prepended to sys.path if it isn't there already. + If ensuresyspath=="append" the root dir will be appended + if it isn't already contained in sys.path. + if ensuresyspath is False no modification of syspath happens. + + Special value of ensuresyspath=="importlib" is intended + purely for using in pytest, it is capable only of importing + separate .py files outside packages, e.g. for test suite + without any __init__.py file. It effectively allows having + same-named test modules in different places and offers + mild opt-in via this option. Note that it works only in + recent versions of python. + """ + if not self.check(): + raise error.ENOENT(self) + + if ensuresyspath == 'importlib': + if modname is None: + modname = self.purebasename + if not ALLOW_IMPORTLIB_MODE: + raise ImportError( + "Can't use importlib due to old version of Python") + spec = importlib.util.spec_from_file_location( + modname, str(self)) + if spec is None: + raise ImportError( + "Can't find module %s at location %s" % + (modname, str(self)) + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + pkgpath = None + if modname is None: + pkgpath = self.pypkgpath() + if pkgpath is not None: + pkgroot = pkgpath.dirpath() + names = self.new(ext="").relto(pkgroot).split(self.sep) + if names[-1] == "__init__": + names.pop() + modname = ".".join(names) + else: + pkgroot = self.dirpath() + modname = self.purebasename + + self._ensuresyspath(ensuresyspath, pkgroot) + __import__(modname) + mod = sys.modules[modname] + if self.basename == "__init__.py": + return mod # we don't check anything as we might + # be in a namespace package ... too icky to check + modfile = mod.__file__ + if modfile[-4:] in ('.pyc', '.pyo'): + modfile = modfile[:-1] + elif modfile.endswith('$py.class'): + modfile = modfile[:-9] + '.py' + if modfile.endswith(os.path.sep + "__init__.py"): + if self.basename != "__init__.py": + modfile = modfile[:-12] + try: + issame = self.samefile(modfile) + except error.ENOENT: + issame = False + if not issame: + ignore = os.getenv('PY_IGNORE_IMPORTMISMATCH') + if ignore != '1': + raise self.ImportMismatchError(modname, modfile, self) + return mod + else: + try: + return sys.modules[modname] + except KeyError: + # we have a custom modname, do a pseudo-import + import types + mod = types.ModuleType(modname) + mod.__file__ = str(self) + sys.modules[modname] = mod + try: + py.builtin.execfile(str(self), mod.__dict__) + except: + del sys.modules[modname] + raise + return mod + + def sysexec(self, *argv, **popen_opts): + """ return stdout text from executing a system child process, + where the 'self' path points to executable. + The process is directly invoked and not through a system shell. + """ + from subprocess import Popen, PIPE + argv = map_as_list(str, argv) + popen_opts['stdout'] = popen_opts['stderr'] = PIPE + proc = Popen([str(self)] + argv, **popen_opts) + stdout, stderr = proc.communicate() + ret = proc.wait() + if py.builtin._isbytes(stdout): + stdout = py.builtin._totext(stdout, sys.getdefaultencoding()) + if ret != 0: + if py.builtin._isbytes(stderr): + stderr = py.builtin._totext(stderr, sys.getdefaultencoding()) + raise py.process.cmdexec.Error(ret, ret, str(self), + stdout, stderr,) + return stdout + + def sysfind(cls, name, checker=None, paths=None): + """ return a path object found by looking at the systems + underlying PATH specification. If the checker is not None + it will be invoked to filter matching paths. If a binary + cannot be found, None is returned + Note: This is probably not working on plain win32 systems + but may work on cygwin. + """ + if isabs(name): + p = py.path.local(name) + if p.check(file=1): + return p + else: + if paths is None: + if iswin32: + paths = os.environ['Path'].split(';') + if '' not in paths and '.' not in paths: + paths.append('.') + try: + systemroot = os.environ['SYSTEMROOT'] + except KeyError: + pass + else: + paths = [path.replace('%SystemRoot%', systemroot) + for path in paths] + else: + paths = os.environ['PATH'].split(':') + tryadd = [] + if iswin32: + tryadd += os.environ['PATHEXT'].split(os.pathsep) + tryadd.append("") + + for x in paths: + for addext in tryadd: + p = py.path.local(x).join(name, abs=True) + addext + try: + if p.check(file=1): + if checker: + if not checker(p): + continue + return p + except error.EACCES: + pass + return None + sysfind = classmethod(sysfind) + + def _gethomedir(cls): + try: + x = os.environ['HOME'] + except KeyError: + try: + x = os.environ["HOMEDRIVE"] + os.environ['HOMEPATH'] + except KeyError: + return None + return cls(x) + _gethomedir = classmethod(_gethomedir) + + # """ + # special class constructors for local filesystem paths + # """ + @classmethod + def get_temproot(cls): + """ return the system's temporary directory + (where tempfiles are usually created in) + """ + import tempfile + return py.path.local(tempfile.gettempdir()) + + @classmethod + def mkdtemp(cls, rootdir=None): + """ return a Path object pointing to a fresh new temporary directory + (which we created ourself). + """ + import tempfile + if rootdir is None: + rootdir = cls.get_temproot() + return cls(error.checked_call(tempfile.mkdtemp, dir=str(rootdir))) + + def make_numbered_dir(cls, prefix='session-', rootdir=None, keep=3, + lock_timeout=172800): # two days + """ return unique directory with a number greater than the current + maximum one. The number is assumed to start directly after prefix. + if keep is true directories with a number less than (maxnum-keep) + will be removed. If .lock files are used (lock_timeout non-zero), + algorithm is multi-process safe. + """ + if rootdir is None: + rootdir = cls.get_temproot() + + nprefix = prefix.lower() + def parse_num(path): + """ parse the number out of a path (if it matches the prefix) """ + nbasename = path.basename.lower() + if nbasename.startswith(nprefix): + try: + return int(nbasename[len(nprefix):]) + except ValueError: + pass + + def create_lockfile(path): + """ exclusively create lockfile. Throws when failed """ + mypid = os.getpid() + lockfile = path.join('.lock') + if hasattr(lockfile, 'mksymlinkto'): + lockfile.mksymlinkto(str(mypid)) + else: + fd = error.checked_call(os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + with os.fdopen(fd, 'w') as f: + f.write(str(mypid)) + return lockfile + + def atexit_remove_lockfile(lockfile): + """ ensure lockfile is removed at process exit """ + mypid = os.getpid() + def try_remove_lockfile(): + # in a fork() situation, only the last process should + # remove the .lock, otherwise the other processes run the + # risk of seeing their temporary dir disappear. For now + # we remove the .lock in the parent only (i.e. we assume + # that the children finish before the parent). + if os.getpid() != mypid: + return + try: + lockfile.remove() + except error.Error: + pass + atexit.register(try_remove_lockfile) + + # compute the maximum number currently in use with the prefix + lastmax = None + while True: + maxnum = -1 + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None: + maxnum = max(maxnum, num) + + # make the new directory + try: + udir = rootdir.mkdir(prefix + str(maxnum+1)) + if lock_timeout: + lockfile = create_lockfile(udir) + atexit_remove_lockfile(lockfile) + except (error.EEXIST, error.ENOENT, error.EBUSY): + # race condition (1): another thread/process created the dir + # in the meantime - try again + # race condition (2): another thread/process spuriously acquired + # lock treating empty directory as candidate + # for removal - try again + # race condition (3): another thread/process tried to create the lock at + # the same time (happened in Python 3.3 on Windows) + # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa + if lastmax == maxnum: + raise + lastmax = maxnum + continue + break + + def get_mtime(path): + """ read file modification time """ + try: + return path.lstat().mtime + except error.Error: + pass + + garbage_prefix = prefix + 'garbage-' + + def is_garbage(path): + """ check if path denotes directory scheduled for removal """ + bn = path.basename + return bn.startswith(garbage_prefix) + + # prune old directories + udir_time = get_mtime(udir) + if keep and udir_time: + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None and num <= (maxnum - keep): + try: + # try acquiring lock to remove directory as exclusive user + if lock_timeout: + create_lockfile(path) + except (error.EEXIST, error.ENOENT, error.EBUSY): + path_time = get_mtime(path) + if not path_time: + # assume directory doesn't exist now + continue + if abs(udir_time - path_time) < lock_timeout: + # assume directory with lockfile exists + # and lock timeout hasn't expired yet + continue + + # path dir locked for exclusive use + # and scheduled for removal to avoid another thread/process + # treating it as a new directory or removal candidate + garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) + try: + path.rename(garbage_path) + garbage_path.remove(rec=1) + except KeyboardInterrupt: + raise + except: # this might be error.Error, WindowsError ... + pass + if is_garbage(path): + try: + path.remove(rec=1) + except KeyboardInterrupt: + raise + except: # this might be error.Error, WindowsError ... + pass + + # make link... + try: + username = os.environ['USER'] #linux, et al + except KeyError: + try: + username = os.environ['USERNAME'] #windows + except KeyError: + username = 'current' + + src = str(udir) + dest = src[:src.rfind('-')] + '-' + username + try: + os.unlink(dest) + except OSError: + pass + try: + os.symlink(src, dest) + except (OSError, AttributeError, NotImplementedError): + pass + + return udir + make_numbered_dir = classmethod(make_numbered_dir) + + +def copymode(src, dest): + """ copy permission from src to dst. """ + import shutil + shutil.copymode(src, dest) + + +def copystat(src, dest): + """ copy permission, last modification time, + last access time, and flags from src to dst.""" + import shutil + shutil.copystat(str(src), str(dest)) + + +def copychunked(src, dest): + chunksize = 524288 # half a meg of bytes + fsrc = src.open('rb') + try: + fdest = dest.open('wb') + try: + while 1: + buf = fsrc.read(chunksize) + if not buf: + break + fdest.write(buf) + finally: + fdest.close() + finally: + fsrc.close() + + +def isimportable(name): + if name and (name[0].isalpha() or name[0] == '_'): + name = name.replace("_", '') + return not name or name.isalnum() + +local = LocalPath diff --git a/src/py.py b/src/py.py new file mode 100644 index 00000000000..c4f049e360f --- /dev/null +++ b/src/py.py @@ -0,0 +1,10 @@ +# shim for pylib going away +# if pylib is installed this file will get skipped +# (`py/__init__.py` has higher precedence) +import sys + +import _pytest._py.error as error +import _pytest._py.path as path + +sys.modules['py.error'] = error +sys.modules['py.path'] = path From 49abbf248524b444e00571eb692e804fc855704d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Oct 2022 14:19:24 +0000 Subject: [PATCH 02/20] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/_py/error.py | 56 ++-- src/_pytest/_py/path.py | 656 +++++++++++++++++++++------------------ src/_pytest/compat.py | 1 + src/py.py | 4 +- 4 files changed, 389 insertions(+), 328 deletions(-) diff --git a/src/_pytest/_py/error.py b/src/_pytest/_py/error.py index e1e3ccd28af..c427ee5f599 100644 --- a/src/_pytest/_py/error.py +++ b/src/_pytest/_py/error.py @@ -2,41 +2,49 @@ create errno-specific classes for IO or os calls. """ +import errno +import os +import sys from types import ModuleType -import sys, os, errno + class Error(EnvironmentError): def __repr__(self): - return "%s.%s %r: %s " %(self.__class__.__module__, - self.__class__.__name__, - self.__class__.__doc__, - " ".join(map(str, self.args)), - #repr(self.args) - ) + return "{}.{} {!r}: {} ".format( + self.__class__.__module__, + self.__class__.__name__, + self.__class__.__doc__, + " ".join(map(str, self.args)), + # repr(self.args) + ) def __str__(self): - s = "[%s]: %s" %(self.__class__.__doc__, - " ".join(map(str, self.args)), - ) + s = "[{}]: {}".format( + self.__class__.__doc__, + " ".join(map(str, self.args)), + ) return s + _winerrnomap = { 2: errno.ENOENT, 3: errno.ENOENT, 17: errno.EEXIST, 18: errno.EXDEV, - 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable + 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable 22: errno.ENOTDIR, 20: errno.ENOTDIR, 267: errno.ENOTDIR, 5: errno.EACCES, # anything better? } + class ErrorMaker(ModuleType): - """ lazily provides Exception classes for each possible POSIX errno - (as defined per the 'errno' module). All such instances - subclass EnvironmentError. + """lazily provides Exception classes for each possible POSIX errno + (as defined per the 'errno' module). All such instances + subclass EnvironmentError. """ + Error = Error _errno2class = {} @@ -52,23 +60,25 @@ def _geterrnoclass(self, eno): try: return self._errno2class[eno] except KeyError: - clsname = errno.errorcode.get(eno, "UnknownErrno%d" %(eno,)) - errorcls = type(Error)(clsname, (Error,), - {'__module__':'py.error', - '__doc__': os.strerror(eno)}) + clsname = errno.errorcode.get(eno, "UnknownErrno%d" % (eno,)) + errorcls = type(Error)( + clsname, + (Error,), + {"__module__": "py.error", "__doc__": os.strerror(eno)}, + ) self._errno2class[eno] = errorcls return errorcls def checked_call(self, func, *args, **kwargs): - """ call a function and raise an errno-exception if applicable. """ + """call a function and raise an errno-exception if applicable.""" __tracebackhide__ = True try: return func(*args, **kwargs) except self.Error: raise - except (OSError, EnvironmentError): + except OSError: cls, value, tb = sys.exc_info() - if not hasattr(value, 'errno'): + if not hasattr(value, "errno"): raise __tracebackhide__ = False errno = value.errno @@ -83,9 +93,9 @@ def checked_call(self, func, *args, **kwargs): cls = self._geterrnoclass(_winerrnomap[errno]) except KeyError: raise value - raise cls("%s%r" % (func.__name__, args)) + raise cls(f"{func.__name__}{args!r}") __tracebackhide__ = True -error = ErrorMaker('_pytest._py.error') +error = ErrorMaker("_pytest._py.error") sys.modules[error.__name__] = error diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index e8adbf27a79..0bf27bcfaf4 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -1,24 +1,31 @@ """ local path implementation. """ -from __future__ import with_statement - -from contextlib import contextmanager -import sys, os, atexit, io, uuid -from stat import S_ISLNK, S_ISDIR, S_ISREG - -from os.path import abspath, normpath, isabs, exists, isdir, isfile, islink, dirname - -import warnings +import atexit +import fnmatch +import io import os -import sys import posixpath -import fnmatch +import sys +import uuid +import warnings +from contextlib import contextmanager +from os.path import abspath +from os.path import dirname +from os.path import exists +from os.path import isabs +from os.path import isdir +from os.path import isfile +from os.path import islink +from os.path import normpath +from stat import S_ISDIR +from stat import S_ISLNK +from stat import S_ISREG from . import error # Moved from local.py. -iswin32 = sys.platform == "win32" or (getattr(os, '_name', False) == 'nt') +iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt") try: # FileNotFoundError might happen in py34, and is not available with py27. @@ -29,6 +36,7 @@ try: from os import fspath except ImportError: + def fspath(path): """ Return the string representation of the path. @@ -47,7 +55,7 @@ def fspath(path): try: return path_type.__fspath__(path) except AttributeError: - if hasattr(path_type, '__fspath__'): + if hasattr(path_type, "__fspath__"): raise try: import pathlib @@ -57,11 +65,13 @@ def fspath(path): if isinstance(path, pathlib.PurePath): return str(path) - raise TypeError("expected str, bytes or os.PathLike object, not " - + path_type.__name__) + raise TypeError( + "expected str, bytes or os.PathLike object, not " + path_type.__name__ + ) + class Checkers: - _depend_on_existence = 'exists', 'link', 'dir', 'file' + _depend_on_existence = "exists", "link", "dir", "file" def __init__(self, path): self.path = path @@ -73,11 +83,11 @@ def file(self): raise NotImplementedError def dotfile(self): - return self.path.basename.startswith('.') + return self.path.basename.startswith(".") def ext(self, arg): - if not arg.startswith('.'): - arg = '.' + arg + if not arg.startswith("."): + arg = "." + arg return self.path.ext == arg def exists(self): @@ -105,15 +115,14 @@ def _evaluate(self, kw): try: meth = getattr(self, name) except AttributeError: - if name[:3] == 'not': + if name[:3] == "not": invert = True try: meth = getattr(self, name[3:]) except AttributeError: pass if meth is None: - raise TypeError( - "no %r checker available for %r" % (name, self.path)) + raise TypeError(f"no {name!r} checker available for {self.path!r}") try: if py.code.getrawcode(meth).co_argcount > 1: if (not meth(value)) ^ invert: @@ -129,74 +138,78 @@ def _evaluate(self, kw): if name in kw: if kw.get(name): return False - name = 'not' + name + name = "not" + name if name in kw: if not kw.get(name): return False return True + class NeverRaised(Exception): pass -class PathBase(object): - """ shared implementation for filesystem path objects.""" + +class PathBase: + """shared implementation for filesystem path objects.""" + Checkers = Checkers def __div__(self, other): return self.join(fspath(other)) - __truediv__ = __div__ # py3k + + __truediv__ = __div__ # py3k def basename(self): - """ basename part of path. """ - return self._getbyspec('basename')[0] + """basename part of path.""" + return self._getbyspec("basename")[0] + basename = property(basename, None, None, basename.__doc__) def dirname(self): - """ dirname part of path. """ - return self._getbyspec('dirname')[0] + """dirname part of path.""" + return self._getbyspec("dirname")[0] + dirname = property(dirname, None, None, dirname.__doc__) def purebasename(self): - """ pure base name of the path.""" - return self._getbyspec('purebasename')[0] + """pure base name of the path.""" + return self._getbyspec("purebasename")[0] + purebasename = property(purebasename, None, None, purebasename.__doc__) def ext(self): - """ extension of the path (including the '.').""" - return self._getbyspec('ext')[0] + """extension of the path (including the '.').""" + return self._getbyspec("ext")[0] + ext = property(ext, None, None, ext.__doc__) def dirpath(self, *args, **kwargs): - """ return the directory path joined with any given path arguments. """ - return self.new(basename='').join(*args, **kwargs) + """return the directory path joined with any given path arguments.""" + return self.new(basename="").join(*args, **kwargs) def read_binary(self): - """ read and return a bytestring from reading the path. """ - with self.open('rb') as f: + """read and return a bytestring from reading the path.""" + with self.open("rb") as f: return f.read() def read_text(self, encoding): - """ read and return a Unicode string from reading the path. """ + """read and return a Unicode string from reading the path.""" with self.open("r", encoding=encoding) as f: return f.read() - - def read(self, mode='r'): - """ read and return a bytestring from reading the path. """ + def read(self, mode="r"): + """read and return a bytestring from reading the path.""" with self.open(mode) as f: return f.read() def readlines(self, cr=1): - """ read and return a list of lines from the path. if cr is False, the -newline will be removed from the end of each line. """ - if sys.version_info < (3, ): - mode = 'rU' - else: # python 3 deprecates mode "U" in favor of "newline" option - mode = 'r' + """read and return a list of lines from the path. if cr is False, the + newline will be removed from the end of each line.""" + mode = "r" if not cr: content = self.read(mode) - return content.split('\n') + return content.split("\n") else: f = self.open(mode) try: @@ -205,20 +218,19 @@ def readlines(self, cr=1): f.close() def load(self): - """ (deprecated) return object unpickled from self.read() """ - f = self.open('rb') + """(deprecated) return object unpickled from self.read()""" + f = self.open("rb") try: import pickle + return error.checked_call(pickle.load, f) finally: f.close() def move(self, target): - """ move this path to target. """ + """move this path to target.""" if target.relto(self): - raise error.EINVAL( - target, - "cannot move path into a subdirectory of itself") + raise error.EINVAL(target, "cannot move path into a subdirectory of itself") try: self.rename(target) except error.EXDEV: # invalid cross-device link @@ -226,28 +238,28 @@ def move(self, target): self.remove() def __repr__(self): - """ return a string representation of this path. """ + """return a string representation of this path.""" return repr(str(self)) def check(self, **kw): - """ check a path for existence and properties. + """check a path for existence and properties. - Without arguments, return True if the path exists, otherwise False. + Without arguments, return True if the path exists, otherwise False. - valid checkers:: + valid checkers:: - file=1 # is a file - file=0 # is not a file (may not even exist) - dir=1 # is a dir - link=1 # is a link - exists=1 # exists + file=1 # is a file + file=0 # is not a file (may not even exist) + dir=1 # is a dir + link=1 # is a link + exists=1 # exists - You can specify multiple checker definitions, for example:: + You can specify multiple checker definitions, for example:: - path.check(file=1, link=1) # a link pointing to a file + path.check(file=1, link=1) # a link pointing to a file """ if not kw: - kw = {'exists': 1} + kw = {"exists": 1} return self.Checkers(self)._evaluate(kw) def fnmatch(self, pattern): @@ -270,34 +282,33 @@ def fnmatch(self, pattern): return FNMatcher(pattern)(self) def relto(self, relpath): - """ return a string which is the relative part of the path + """return a string which is the relative part of the path to the given 'relpath'. """ if not isinstance(relpath, (str, PathBase)): - raise TypeError("%r: not a string or path object" %(relpath,)) + raise TypeError(f"{relpath!r}: not a string or path object") strrelpath = str(relpath) if strrelpath and strrelpath[-1] != self.sep: strrelpath += self.sep - #assert strrelpath[-1] == self.sep - #assert strrelpath[-2] != self.sep + # assert strrelpath[-1] == self.sep + # assert strrelpath[-2] != self.sep strself = self.strpath - if sys.platform == "win32" or getattr(os, '_name', None) == 'nt': - if os.path.normcase(strself).startswith( - os.path.normcase(strrelpath)): - return strself[len(strrelpath):] + if sys.platform == "win32" or getattr(os, "_name", None) == "nt": + if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)): + return strself[len(strrelpath) :] elif strself.startswith(strrelpath): - return strself[len(strrelpath):] + return strself[len(strrelpath) :] return "" def ensure_dir(self, *args): - """ ensure the path joined with args is a directory. """ + """ensure the path joined with args is a directory.""" return self.ensure(*args, **{"dir": True}) def bestrelpath(self, dest): - """ return a string which is a relative path from self - (assumed to be a directory) to dest such that - self.join(bestrelpath) == dest and if not such - path can be determined return dest. + """return a string which is a relative path from self + (assumed to be a directory) to dest such that + self.join(bestrelpath) == dest and if not such + path can be determined return dest. """ try: if self == dest: @@ -329,8 +340,8 @@ def isfile(self): return self.check(file=1) def parts(self, reverse=False): - """ return a root-first list of all ancestor directories - plus the path itself. + """return a root-first list of all ancestor directories + plus the path itself. """ current = self l = [self] @@ -345,8 +356,8 @@ def parts(self, reverse=False): return l def common(self, other): - """ return the common part shared with the other path - or None if there is no common part. + """return the common part shared with the other path + or None if there is no common part. """ last = None for x, y in zip(self.parts(), other.parts()): @@ -356,15 +367,15 @@ def common(self, other): return last def __add__(self, other): - """ return new path object with 'other' added to the basename""" - return self.new(basename=self.basename+str(other)) + """return new path object with 'other' added to the basename""" + return self.new(basename=self.basename + str(other)) def __cmp__(self, other): - """ return sort value (-1, 0, +1). """ + """return sort value (-1, 0, +1).""" try: return cmp(self.strpath, other.strpath) except AttributeError: - return cmp(str(self), str(other)) # self.path, other.path) + return cmp(str(self), str(other)) # self.path, other.path) def __lt__(self, other): try: @@ -373,50 +384,53 @@ def __lt__(self, other): return str(self) < str(other) def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): - """ yields all paths below the current one + """yields all paths below the current one - fil is a filter (glob pattern or callable), if not matching the - path will not be yielded, defaulting to None (everything is - returned) + fil is a filter (glob pattern or callable), if not matching the + path will not be yielded, defaulting to None (everything is + returned) - rec is a filter (glob pattern or callable) that controls whether - a node is descended, defaulting to None + rec is a filter (glob pattern or callable) that controls whether + a node is descended, defaulting to None - ignore is an Exception class that is ignoredwhen calling dirlist() - on any of the paths (by default, all exceptions are reported) + ignore is an Exception class that is ignoredwhen calling dirlist() + on any of the paths (by default, all exceptions are reported) - bf if True will cause a breadthfirst search instead of the - default depthfirst. Default: False + bf if True will cause a breadthfirst search instead of the + default depthfirst. Default: False - sort if True will sort entries within each directory level. + sort if True will sort entries within each directory level. """ - for x in Visitor(fil, rec, ignore, bf, sort).gen(self): - yield x + yield from Visitor(fil, rec, ignore, bf, sort).gen(self) def _sortlist(self, res, sort): if sort: - if hasattr(sort, '__call__'): - warnings.warn(DeprecationWarning( - "listdir(sort=callable) is deprecated and breaks on python3" - ), stacklevel=3) + if hasattr(sort, "__call__"): + warnings.warn( + DeprecationWarning( + "listdir(sort=callable) is deprecated and breaks on python3" + ), + stacklevel=3, + ) res.sort(sort) else: res.sort() def samefile(self, other): - """ return True if other refers to the same stat object as self. """ + """return True if other refers to the same stat object as self.""" return self.strpath == str(other) def __fspath__(self): return self.strpath + class Visitor: def __init__(self, fil, rec, ignore, bf, sort): if isinstance(fil, py.builtin._basestring): fil = FNMatcher(fil) if isinstance(rec, py.builtin._basestring): self.rec = FNMatcher(rec) - elif not hasattr(rec, '__call__') and rec: + elif not hasattr(rec, "__call__") and rec: self.rec = lambda path: True else: self.rec = rec @@ -431,8 +445,9 @@ def gen(self, path): except self.ignore: return rec = self.rec - dirs = self.optsort([p for p in entries - if p.check(dir=1) and (rec is None or rec(p))]) + dirs = self.optsort( + [p for p in entries if p.check(dir=1) and (rec is None or rec(p))] + ) if not self.breadthfirst: for subdir in dirs: for p in self.gen(subdir): @@ -445,6 +460,7 @@ def gen(self, path): for p in self.gen(subdir): yield p + class FNMatcher: def __init__(self, pattern): self.pattern = pattern @@ -452,9 +468,11 @@ def __init__(self, pattern): def __call__(self, path): pattern = self.pattern - if (pattern.find(path.sep) == -1 and - iswin32 and - pattern.find(posixpath.sep) != -1): + if ( + pattern.find(path.sep) == -1 + and iswin32 + and pattern.find(posixpath.sep) != -1 + ): # Running on Windows, the pattern has no Windows path separators, # and the pattern has one or more Posix path separators. Replace # the Posix path separators with the Windows path separator. @@ -463,24 +481,22 @@ def __call__(self, path): if pattern.find(path.sep) == -1: name = path.basename else: - name = str(path) # path.strpath # XXX svn? + name = str(path) # path.strpath # XXX svn? if not os.path.isabs(pattern): - pattern = '*' + path.sep + pattern + pattern = "*" + path.sep + pattern return fnmatch.fnmatch(name, pattern) -if sys.version_info > (3,0): - def map_as_list(func, iter): - return list(map(func, iter)) -else: - map_as_list = map +def map_as_list(func, iter): + return list(map(func, iter)) -ALLOW_IMPORTLIB_MODE = sys.version_info > (3,5) + +ALLOW_IMPORTLIB_MODE = sys.version_info > (3, 5) if ALLOW_IMPORTLIB_MODE: import importlib -class Stat(object): +class Stat: def __getattr__(self, name): return getattr(self._osstatresult, "st_" + name) @@ -493,15 +509,17 @@ def owner(self): if iswin32: raise NotImplementedError("XXX win32") import pwd + entry = error.checked_call(pwd.getpwuid, self.uid) return entry[0] @property def group(self): - """ return group name of file. """ + """return group name of file.""" if iswin32: raise NotImplementedError("XXX win32") import grp + entry = error.checked_call(grp.getgrgid, self.gid) return entry[0] @@ -512,15 +530,16 @@ def isfile(self): return S_ISREG(self._osstatresult.st_mode) def islink(self): - st = self.path.lstat() + self.path.lstat() return S_ISLNK(self._osstatresult.st_mode) + class PosixPath(PathBase): def chown(self, user, group, rec=0): - """ change ownership to the given user and group. - user and group may be specified by a number or - by a name. if rec is True change ownership - recursively. + """change ownership to the given user and group. + user and group may be specified by a number or + by a name. if rec is True change ownership + recursively. """ uid = getuserid(user) gid = getgroupid(group) @@ -531,15 +550,15 @@ def chown(self, user, group, rec=0): error.checked_call(os.chown, str(self), uid, gid) def readlink(self): - """ return value of a symbolic link. """ + """return value of a symbolic link.""" return error.checked_call(os.readlink, self.strpath) def mklinkto(self, oldname): - """ posix style hard link to another name. """ + """posix style hard link to another name.""" error.checked_call(os.link, str(oldname), str(self)) def mksymlinkto(self, value, absolute=1): - """ create a symbolic link with the given value (pointing to another name). """ + """create a symbolic link with the given value (pointing to another name).""" if absolute: error.checked_call(os.symlink, str(value), self.strpath) else: @@ -548,31 +567,39 @@ def mksymlinkto(self, value, absolute=1): relsource = self.__class__(value).relto(base) reldest = self.relto(base) n = reldest.count(self.sep) - target = self.sep.join(('..', )*n + (relsource, )) + target = self.sep.join(("..",) * n + (relsource,)) error.checked_call(os.symlink, target, self.strpath) + def getuserid(user): import pwd + if not isinstance(user, int): user = pwd.getpwnam(user)[2] return user + def getgroupid(group): import grp + if not isinstance(group, int): group = grp.getgrnam(group)[2] return group + FSBase = not iswin32 and PosixPath or PathBase + class LocalPath(FSBase): - """ object oriented interface to os.path and other local filesystem - related information. + """object oriented interface to os.path and other local filesystem + related information. """ + class ImportMismatchError(ImportError): - """ raised on pyimport() if there is a mismatch of __file__'s""" + """raised on pyimport() if there is a mismatch of __file__'s""" sep = os.sep + class Checkers(Checkers): def _stat(self): try: @@ -598,7 +625,7 @@ def link(self): return S_ISLNK(st.mode) def __init__(self, path=None, expanduser=False): - """ Initialize and return a local Path instance. + """Initialize and return a local Path instance. Path can be relative to the current directory. If path is None it defaults to the current working directory. @@ -613,8 +640,10 @@ def __init__(self, path=None, expanduser=False): try: path = fspath(path) except TypeError: - raise ValueError("can only pass None, Path instances " - "or non-empty strings to LocalPath") + raise ValueError( + "can only pass None, Path instances " + "or non-empty strings to LocalPath" + ) if expanduser: path = os.path.expanduser(path) self.strpath = abspath(path) @@ -649,8 +678,7 @@ def __gt__(self, other): return fspath(self) > fspath(other) def samefile(self, other): - """ return True if 'other' references the same file as 'self'. - """ + """return True if 'other' references the same file as 'self'.""" other = fspath(other) if not isabs(other): other = abspath(other) @@ -658,11 +686,10 @@ def samefile(self, other): return True if not hasattr(os.path, "samefile"): return False - return error.checked_call( - os.path.samefile, self.strpath, other) + return error.checked_call(os.path.samefile, self.strpath, other) def remove(self, rec=1, ignore_errors=False): - """ remove a file or directory (or a directory tree if rec=1). + """remove a file or directory (or a directory tree if rec=1). if ignore_errors is True, errors while removing directories will be ignored. """ @@ -672,9 +699,10 @@ def remove(self, rec=1, ignore_errors=False): if iswin32: self.chmod(0o700, rec=1) import shutil + error.checked_call( - shutil.rmtree, self.strpath, - ignore_errors=ignore_errors) + shutil.rmtree, self.strpath, ignore_errors=ignore_errors + ) else: error.checked_call(os.rmdir, self.strpath) else: @@ -683,7 +711,7 @@ def remove(self, rec=1, ignore_errors=False): error.checked_call(os.remove, self.strpath) def computehash(self, hashtype="md5", chunksize=524288): - """ return hexdigest of hashvalue for this file. """ + """return hexdigest of hashvalue for this file.""" try: try: import hashlib as mod @@ -693,8 +721,8 @@ def computehash(self, hashtype="md5", chunksize=524288): mod = __import__(hashtype) hash = getattr(mod, hashtype)() except (AttributeError, ImportError): - raise ValueError("Don't know how to compute %r hash" %(hashtype,)) - f = self.open('rb') + raise ValueError(f"Don't know how to compute {hashtype!r} hash") + f = self.open("rb") try: while 1: buf = f.read(chunksize) @@ -705,94 +733,94 @@ def computehash(self, hashtype="md5", chunksize=524288): f.close() def new(self, **kw): - """ create a modified version of this path. - the following keyword arguments modify various path parts:: - - a:/some/path/to/a/file.ext - xx drive - xxxxxxxxxxxxxxxxx dirname - xxxxxxxx basename - xxxx purebasename - xxx ext + """create a modified version of this path. + the following keyword arguments modify various path parts:: + + a:/some/path/to/a/file.ext + xx drive + xxxxxxxxxxxxxxxxx dirname + xxxxxxxx basename + xxxx purebasename + xxx ext """ obj = object.__new__(self.__class__) if not kw: obj.strpath = self.strpath return obj - drive, dirname, basename, purebasename,ext = self._getbyspec( - "drive,dirname,basename,purebasename,ext") - if 'basename' in kw: - if 'purebasename' in kw or 'ext' in kw: + drive, dirname, basename, purebasename, ext = self._getbyspec( + "drive,dirname,basename,purebasename,ext" + ) + if "basename" in kw: + if "purebasename" in kw or "ext" in kw: raise ValueError("invalid specification %r" % kw) else: - pb = kw.setdefault('purebasename', purebasename) + pb = kw.setdefault("purebasename", purebasename) try: - ext = kw['ext'] + ext = kw["ext"] except KeyError: pass else: - if ext and not ext.startswith('.'): - ext = '.' + ext - kw['basename'] = pb + ext + if ext and not ext.startswith("."): + ext = "." + ext + kw["basename"] = pb + ext - if ('dirname' in kw and not kw['dirname']): - kw['dirname'] = drive + if "dirname" in kw and not kw["dirname"]: + kw["dirname"] = drive else: - kw.setdefault('dirname', dirname) - kw.setdefault('sep', self.sep) - obj.strpath = normpath( - "%(dirname)s%(sep)s%(basename)s" % kw) + kw.setdefault("dirname", dirname) + kw.setdefault("sep", self.sep) + obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw) return obj def _getbyspec(self, spec): - """ see new for what 'spec' can be. """ + """see new for what 'spec' can be.""" res = [] parts = self.strpath.split(self.sep) - args = filter(None, spec.split(',') ) + args = filter(None, spec.split(",")) append = res.append for name in args: - if name == 'drive': + if name == "drive": append(parts[0]) - elif name == 'dirname': + elif name == "dirname": append(self.sep.join(parts[:-1])) else: basename = parts[-1] - if name == 'basename': + if name == "basename": append(basename) else: - i = basename.rfind('.') + i = basename.rfind(".") if i == -1: - purebasename, ext = basename, '' + purebasename, ext = basename, "" else: purebasename, ext = basename[:i], basename[i:] - if name == 'purebasename': + if name == "purebasename": append(purebasename) - elif name == 'ext': + elif name == "ext": append(ext) else: raise ValueError("invalid part specification %r" % name) return res def dirpath(self, *args, **kwargs): - """ return the directory path joined with any given path arguments. """ + """return the directory path joined with any given path arguments.""" if not kwargs: path = object.__new__(self.__class__) path.strpath = dirname(self.strpath) if args: path = path.join(*args) return path - return super(LocalPath, self).dirpath(*args, **kwargs) + return super().dirpath(*args, **kwargs) def join(self, *args, **kwargs): - """ return a new path by appending all 'args' as path + """return a new path by appending all 'args' as path components. if abs=1 is used restart from root if any of the args is an absolute path. """ sep = self.sep strargs = [fspath(arg) for arg in args] strpath = self.strpath - if kwargs.get('abs'): + if kwargs.get("abs"): newargs = [] for arg in reversed(strargs): if isabs(arg): @@ -806,16 +834,16 @@ def join(self, *args, **kwargs): arg = arg.strip(sep) if iswin32: # allow unix style paths even on windows. - arg = arg.strip('/') - arg = arg.replace('/', sep) + arg = arg.strip("/") + arg = arg.replace("/", sep) strpath = strpath + actual_sep + arg actual_sep = sep obj = object.__new__(self.__class__) obj.strpath = normpath(strpath) return obj - def open(self, mode='r', ensure=False, encoding=None): - """ return an opened file with the given mode. + def open(self, mode="r", ensure=False, encoding=None): + """return an opened file with the given mode. If ensure is True, create parent directories if needed. """ @@ -841,12 +869,13 @@ def check(self, **kw): return not kw["dir"] ^ isdir(self.strpath) if "file" in kw: return not kw["file"] ^ isfile(self.strpath) - return super(LocalPath, self).check(**kw) + return super().check(**kw) _patternchars = set("*?[" + os.path.sep) + def listdir(self, fil=None, sort=None): - """ list directory contents, possibly filter by the given fil func - and possibly sorted. + """list directory contents, possibly filter by the given fil func + and possibly sorted. """ if fil is None and sort is None: names = error.checked_call(os.listdir, self.strpath) @@ -868,32 +897,34 @@ def listdir(self, fil=None, sort=None): return res def size(self): - """ return size of the underlying file object """ + """return size of the underlying file object""" return self.stat().size def mtime(self): - """ return last modification time of the path. """ + """return last modification time of the path.""" return self.stat().mtime def copy(self, target, mode=False, stat=False): - """ copy path to target. + """copy path to target. - If mode is True, will copy copy permission from path to target. - If stat is True, copy permission, last modification - time, last access time, and flags from path to target. + If mode is True, will copy copy permission from path to target. + If stat is True, copy permission, last modification + time, last access time, and flags from path to target. """ if self.check(file=1): if target.check(dir=1): target = target.join(self.basename) - assert self!=target + assert self != target copychunked(self, target) if mode: copymode(self.strpath, target.strpath) if stat: copystat(self, target) else: + def rec(p): return p.check(link=0) + for x in self.visit(rec=rec): relpath = x.relto(self) newx = target.join(relpath) @@ -911,50 +942,51 @@ def rec(p): copystat(x, newx) def rename(self, target): - """ rename this path to target. """ + """rename this path to target.""" target = fspath(target) return error.checked_call(os.rename, self.strpath, target) def dump(self, obj, bin=1): - """ pickle object into path location""" - f = self.open('wb') + """pickle object into path location""" + f = self.open("wb") import pickle + try: error.checked_call(pickle.dump, obj, f, bin) finally: f.close() def mkdir(self, *args): - """ create & return the directory joined with args. """ + """create & return the directory joined with args.""" p = self.join(*args) error.checked_call(os.mkdir, fspath(p)) return p def write_binary(self, data, ensure=False): - """ write binary data into path. If ensure is True create + """write binary data into path. If ensure is True create missing parent directories. """ if ensure: self.dirpath().ensure(dir=1) - with self.open('wb') as f: + with self.open("wb") as f: f.write(data) def write_text(self, data, encoding, ensure=False): - """ write text data into path using the specified encoding. + """write text data into path using the specified encoding. If ensure is True create missing parent directories. """ if ensure: self.dirpath().ensure(dir=1) - with self.open('w', encoding=encoding) as f: + with self.open("w", encoding=encoding) as f: f.write(data) - def write(self, data, mode='w', ensure=False): - """ write data into path. If ensure is True create + def write(self, data, mode="w", ensure=False): + """write data into path. If ensure is True create missing parent directories. """ if ensure: self.dirpath().ensure(dir=1) - if 'b' in mode: + if "b" in mode: if not py.builtin._isbytes(data): raise ValueError("can only process bytes") else: @@ -986,21 +1018,21 @@ def _ensuredirs(self): return self def ensure(self, *args, **kwargs): - """ ensure that an args-joined path exists (by default as - a file). if you specify a keyword argument 'dir=True' - then the path is forced to be a directory path. + """ensure that an args-joined path exists (by default as + a file). if you specify a keyword argument 'dir=True' + then the path is forced to be a directory path. """ p = self.join(*args) - if kwargs.get('dir', 0): + if kwargs.get("dir", 0): return p._ensuredirs() else: p.dirpath()._ensuredirs() if not p.check(file=1): - p.open('w').close() + p.open("w").close() return p def stat(self, raising=True): - """ Return an os.stat() tuple. """ + """Return an os.stat() tuple.""" if raising == True: return Stat(self, error.checked_call(os.stat, self.strpath)) try: @@ -1011,11 +1043,11 @@ def stat(self, raising=True): return None def lstat(self): - """ Return an os.lstat() tuple. """ + """Return an os.lstat() tuple.""" return Stat(self, error.checked_call(os.lstat, self.strpath)) def setmtime(self, mtime=None): - """ set modification time for the given path. if 'mtime' is None + """set modification time for the given path. if 'mtime' is None (the default) then the file's mtime is set to current time. Note that the resolution for 'mtime' is platform dependent. @@ -1028,7 +1060,7 @@ def setmtime(self, mtime=None): return error.checked_call(os.utime, self.strpath, (self.atime(), mtime)) def chdir(self): - """ change directory to self and return old current directory """ + """change directory to self and return old current directory""" try: old = self.__class__() except error.ENOENT: @@ -1036,7 +1068,6 @@ def chdir(self): error.checked_call(os.chdir, self.strpath) return old - @contextmanager def as_cwd(self): """ @@ -1052,41 +1083,41 @@ def as_cwd(self): old.chdir() def realpath(self): - """ return a new path which contains no symbolic links.""" + """return a new path which contains no symbolic links.""" return self.__class__(os.path.realpath(self.strpath)) def atime(self): - """ return last access time of the path. """ + """return last access time of the path.""" return self.stat().atime def __repr__(self): - return 'local(%r)' % self.strpath + return "local(%r)" % self.strpath def __str__(self): - """ return string representation of the Path. """ + """return string representation of the Path.""" return self.strpath def chmod(self, mode, rec=0): - """ change permissions to the given mode. If mode is an - integer it directly encodes the os-specific modes. - if rec is True perform recursively. + """change permissions to the given mode. If mode is an + integer it directly encodes the os-specific modes. + if rec is True perform recursively. """ if not isinstance(mode, int): - raise TypeError("mode %r must be an integer" % (mode,)) + raise TypeError(f"mode {mode!r} must be an integer") if rec: for x in self.visit(rec=rec): error.checked_call(os.chmod, str(x), mode) error.checked_call(os.chmod, self.strpath, mode) def pypkgpath(self): - """ return the Python package path by looking for the last + """return the Python package path by looking for the last directory upwards which still contains an __init__.py. Return None if a pkgpath can not be determined. """ pkgpath = None for parent in self.parts(reverse=True): if parent.isdir(): - if not parent.join('__init__.py').exists(): + if not parent.join("__init__.py").exists(): break if not isimportable(parent.basename): break @@ -1104,7 +1135,7 @@ def _ensuresyspath(self, ensuremode, path): sys.path.insert(0, s) def pyimport(self, modname=None, ensuresyspath=True): - """ return path as an imported python module. + """return path as an imported python module. If modname is None, look for the containing package and construct an according module name. @@ -1127,18 +1158,15 @@ def pyimport(self, modname=None, ensuresyspath=True): if not self.check(): raise error.ENOENT(self) - if ensuresyspath == 'importlib': + if ensuresyspath == "importlib": if modname is None: modname = self.purebasename if not ALLOW_IMPORTLIB_MODE: - raise ImportError( - "Can't use importlib due to old version of Python") - spec = importlib.util.spec_from_file_location( - modname, str(self)) + raise ImportError("Can't use importlib due to old version of Python") + spec = importlib.util.spec_from_file_location(modname, str(self)) if spec is None: raise ImportError( - "Can't find module %s at location %s" % - (modname, str(self)) + f"Can't find module {modname} at location {str(self)}" ) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) @@ -1161,13 +1189,13 @@ def pyimport(self, modname=None, ensuresyspath=True): __import__(modname) mod = sys.modules[modname] if self.basename == "__init__.py": - return mod # we don't check anything as we might - # be in a namespace package ... too icky to check + return mod # we don't check anything as we might + # be in a namespace package ... too icky to check modfile = mod.__file__ - if modfile[-4:] in ('.pyc', '.pyo'): + if modfile[-4:] in (".pyc", ".pyo"): modfile = modfile[:-1] - elif modfile.endswith('$py.class'): - modfile = modfile[:-9] + '.py' + elif modfile.endswith("$py.class"): + modfile = modfile[:-9] + ".py" if modfile.endswith(os.path.sep + "__init__.py"): if self.basename != "__init__.py": modfile = modfile[:-12] @@ -1176,8 +1204,8 @@ def pyimport(self, modname=None, ensuresyspath=True): except error.ENOENT: issame = False if not issame: - ignore = os.getenv('PY_IGNORE_IMPORTMISMATCH') - if ignore != '1': + ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH") + if ignore != "1": raise self.ImportMismatchError(modname, modfile, self) return mod else: @@ -1186,6 +1214,7 @@ def pyimport(self, modname=None, ensuresyspath=True): except KeyError: # we have a custom modname, do a pseudo-import import types + mod = types.ModuleType(modname) mod.__file__ = str(self) sys.modules[modname] = mod @@ -1197,13 +1226,14 @@ def pyimport(self, modname=None, ensuresyspath=True): return mod def sysexec(self, *argv, **popen_opts): - """ return stdout text from executing a system child process, - where the 'self' path points to executable. - The process is directly invoked and not through a system shell. + """return stdout text from executing a system child process, + where the 'self' path points to executable. + The process is directly invoked and not through a system shell. """ from subprocess import Popen, PIPE + argv = map_as_list(str, argv) - popen_opts['stdout'] = popen_opts['stderr'] = PIPE + popen_opts["stdout"] = popen_opts["stderr"] = PIPE proc = Popen([str(self)] + argv, **popen_opts) stdout, stderr = proc.communicate() ret = proc.wait() @@ -1212,17 +1242,22 @@ def sysexec(self, *argv, **popen_opts): if ret != 0: if py.builtin._isbytes(stderr): stderr = py.builtin._totext(stderr, sys.getdefaultencoding()) - raise py.process.cmdexec.Error(ret, ret, str(self), - stdout, stderr,) + raise py.process.cmdexec.Error( + ret, + ret, + str(self), + stdout, + stderr, + ) return stdout def sysfind(cls, name, checker=None, paths=None): - """ return a path object found by looking at the systems - underlying PATH specification. If the checker is not None - it will be invoked to filter matching paths. If a binary - cannot be found, None is returned - Note: This is probably not working on plain win32 systems - but may work on cygwin. + """return a path object found by looking at the systems + underlying PATH specification. If the checker is not None + it will be invoked to filter matching paths. If a binary + cannot be found, None is returned + Note: This is probably not working on plain win32 systems + but may work on cygwin. """ if isabs(name): p = py.path.local(name) @@ -1231,21 +1266,22 @@ def sysfind(cls, name, checker=None, paths=None): else: if paths is None: if iswin32: - paths = os.environ['Path'].split(';') - if '' not in paths and '.' not in paths: - paths.append('.') + paths = os.environ["Path"].split(";") + if "" not in paths and "." not in paths: + paths.append(".") try: - systemroot = os.environ['SYSTEMROOT'] + systemroot = os.environ["SYSTEMROOT"] except KeyError: pass else: - paths = [path.replace('%SystemRoot%', systemroot) - for path in paths] + paths = [ + path.replace("%SystemRoot%", systemroot) for path in paths + ] else: - paths = os.environ['PATH'].split(':') + paths = os.environ["PATH"].split(":") tryadd = [] if iswin32: - tryadd += os.environ['PATHEXT'].split(os.pathsep) + tryadd += os.environ["PATHEXT"].split(os.pathsep) tryadd.append("") for x in paths: @@ -1260,17 +1296,19 @@ def sysfind(cls, name, checker=None, paths=None): except error.EACCES: pass return None + sysfind = classmethod(sysfind) def _gethomedir(cls): try: - x = os.environ['HOME'] + x = os.environ["HOME"] except KeyError: try: - x = os.environ["HOMEDRIVE"] + os.environ['HOMEPATH'] + x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"] except KeyError: return None return cls(x) + _gethomedir = classmethod(_gethomedir) # """ @@ -1278,58 +1316,65 @@ def _gethomedir(cls): # """ @classmethod def get_temproot(cls): - """ return the system's temporary directory - (where tempfiles are usually created in) + """return the system's temporary directory + (where tempfiles are usually created in) """ import tempfile + return py.path.local(tempfile.gettempdir()) @classmethod def mkdtemp(cls, rootdir=None): - """ return a Path object pointing to a fresh new temporary directory - (which we created ourself). + """return a Path object pointing to a fresh new temporary directory + (which we created ourself). """ import tempfile + if rootdir is None: rootdir = cls.get_temproot() return cls(error.checked_call(tempfile.mkdtemp, dir=str(rootdir))) - def make_numbered_dir(cls, prefix='session-', rootdir=None, keep=3, - lock_timeout=172800): # two days - """ return unique directory with a number greater than the current - maximum one. The number is assumed to start directly after prefix. - if keep is true directories with a number less than (maxnum-keep) - will be removed. If .lock files are used (lock_timeout non-zero), - algorithm is multi-process safe. + def make_numbered_dir( + cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800 + ): # two days + """return unique directory with a number greater than the current + maximum one. The number is assumed to start directly after prefix. + if keep is true directories with a number less than (maxnum-keep) + will be removed. If .lock files are used (lock_timeout non-zero), + algorithm is multi-process safe. """ if rootdir is None: rootdir = cls.get_temproot() nprefix = prefix.lower() + def parse_num(path): - """ parse the number out of a path (if it matches the prefix) """ + """parse the number out of a path (if it matches the prefix)""" nbasename = path.basename.lower() if nbasename.startswith(nprefix): try: - return int(nbasename[len(nprefix):]) + return int(nbasename[len(nprefix) :]) except ValueError: pass def create_lockfile(path): - """ exclusively create lockfile. Throws when failed """ + """exclusively create lockfile. Throws when failed""" mypid = os.getpid() - lockfile = path.join('.lock') - if hasattr(lockfile, 'mksymlinkto'): + lockfile = path.join(".lock") + if hasattr(lockfile, "mksymlinkto"): lockfile.mksymlinkto(str(mypid)) else: - fd = error.checked_call(os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) - with os.fdopen(fd, 'w') as f: + fd = error.checked_call( + os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644 + ) + with os.fdopen(fd, "w") as f: f.write(str(mypid)) return lockfile def atexit_remove_lockfile(lockfile): - """ ensure lockfile is removed at process exit """ + """ensure lockfile is removed at process exit""" mypid = os.getpid() + def try_remove_lockfile(): # in a fork() situation, only the last process should # remove the .lock, otherwise the other processes run the @@ -1342,6 +1387,7 @@ def try_remove_lockfile(): lockfile.remove() except error.Error: pass + atexit.register(try_remove_lockfile) # compute the maximum number currently in use with the prefix @@ -1355,7 +1401,7 @@ def try_remove_lockfile(): # make the new directory try: - udir = rootdir.mkdir(prefix + str(maxnum+1)) + udir = rootdir.mkdir(prefix + str(maxnum + 1)) if lock_timeout: lockfile = create_lockfile(udir) atexit_remove_lockfile(lockfile) @@ -1375,16 +1421,16 @@ def try_remove_lockfile(): break def get_mtime(path): - """ read file modification time """ + """read file modification time""" try: return path.lstat().mtime except error.Error: pass - garbage_prefix = prefix + 'garbage-' + garbage_prefix = prefix + "garbage-" def is_garbage(path): - """ check if path denotes directory scheduled for removal """ + """check if path denotes directory scheduled for removal""" bn = path.basename return bn.startswith(garbage_prefix) @@ -1417,27 +1463,27 @@ def is_garbage(path): garbage_path.remove(rec=1) except KeyboardInterrupt: raise - except: # this might be error.Error, WindowsError ... + except: # this might be error.Error, WindowsError ... pass if is_garbage(path): try: path.remove(rec=1) except KeyboardInterrupt: raise - except: # this might be error.Error, WindowsError ... + except: # this might be error.Error, WindowsError ... pass # make link... try: - username = os.environ['USER'] #linux, et al + username = os.environ["USER"] # linux, et al except KeyError: try: - username = os.environ['USERNAME'] #windows + username = os.environ["USERNAME"] # windows except KeyError: - username = 'current' + username = "current" - src = str(udir) - dest = src[:src.rfind('-')] + '-' + username + src = str(udir) + dest = src[: src.rfind("-")] + "-" + username try: os.unlink(dest) except OSError: @@ -1448,27 +1494,30 @@ def is_garbage(path): pass return udir + make_numbered_dir = classmethod(make_numbered_dir) def copymode(src, dest): - """ copy permission from src to dst. """ + """copy permission from src to dst.""" import shutil + shutil.copymode(src, dest) def copystat(src, dest): - """ copy permission, last modification time, + """copy permission, last modification time, last access time, and flags from src to dst.""" import shutil + shutil.copystat(str(src), str(dest)) def copychunked(src, dest): chunksize = 524288 # half a meg of bytes - fsrc = src.open('rb') + fsrc = src.open("rb") try: - fdest = dest.open('wb') + fdest = dest.open("wb") try: while 1: buf = fsrc.read(chunksize) @@ -1482,8 +1531,9 @@ def copychunked(src, dest): def isimportable(name): - if name and (name[0].isalpha() or name[0] == '_'): - name = name.replace("_", '') + if name and (name[0].isalpha() or name[0] == "_"): + name = name.replace("_", "") return not name or name.isalnum() + local = LocalPath diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index fab4c31107f..211407b2374 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -18,6 +18,7 @@ from typing import Union import attr + import py # fmt: off diff --git a/src/py.py b/src/py.py index c4f049e360f..7813c9b93cd 100644 --- a/src/py.py +++ b/src/py.py @@ -6,5 +6,5 @@ import _pytest._py.error as error import _pytest._py.path as path -sys.modules['py.error'] = error -sys.modules['py.path'] = path +sys.modules["py.error"] = error +sys.modules["py.path"] = path From 349f4bffa087fa2ebddd375805914a69e8fbbb45 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 10:30:19 -0400 Subject: [PATCH 03/20] use module __getattr__ for py.error to fix doctesting --- src/_pytest/_py/error.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/_pytest/_py/error.py b/src/_pytest/_py/error.py index c427ee5f599..1f33c5aadbe 100644 --- a/src/_pytest/_py/error.py +++ b/src/_pytest/_py/error.py @@ -5,7 +5,6 @@ import errno import os import sys -from types import ModuleType class Error(EnvironmentError): @@ -39,7 +38,7 @@ def __str__(self): } -class ErrorMaker(ModuleType): +class ErrorMaker: """lazily provides Exception classes for each possible POSIX errno (as defined per the 'errno' module). All such instances subclass EnvironmentError. @@ -97,5 +96,8 @@ def checked_call(self, func, *args, **kwargs): __tracebackhide__ = True -error = ErrorMaker("_pytest._py.error") -sys.modules[error.__name__] = error +_error_maker = ErrorMaker() + + +def __getattr__(attr): + return getattr(_error_maker, attr) From 965e942dfb4f68d1e28d6388544ff81247a0a4ef Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 10:56:43 -0400 Subject: [PATCH 04/20] use getrawcode from _pytest._code --- src/_pytest/_py/path.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 0bf27bcfaf4..585edd65d64 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -109,6 +109,8 @@ def endswith(self, arg): return str(self.path).endswith(arg) def _evaluate(self, kw): + from .._code.source import getrawcode + for name, value in kw.items(): invert = False meth = None @@ -124,7 +126,7 @@ def _evaluate(self, kw): if meth is None: raise TypeError(f"no {name!r} checker available for {self.path!r}") try: - if py.code.getrawcode(meth).co_argcount > 1: + if getrawcode(meth).co_argcount > 1: if (not meth(value)) ^ invert: return False else: From a7c1fc204bc40638029a27045ad4f25beec67f5c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 12:03:30 -0400 Subject: [PATCH 05/20] remove other py.* accesses in _pytest._py.path --- src/_pytest/_py/path.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 585edd65d64..a3c72dcbcf7 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -428,9 +428,9 @@ def __fspath__(self): class Visitor: def __init__(self, fil, rec, ignore, bf, sort): - if isinstance(fil, py.builtin._basestring): + if isinstance(fil, str): fil = FNMatcher(fil) - if isinstance(rec, py.builtin._basestring): + if isinstance(rec, str): self.rec = FNMatcher(rec) elif not hasattr(rec, "__call__") and rec: self.rec = lambda path: True @@ -882,7 +882,7 @@ def listdir(self, fil=None, sort=None): if fil is None and sort is None: names = error.checked_call(os.listdir, self.strpath) return map_as_list(self._fastjoin, names) - if isinstance(fil, py.builtin._basestring): + if isinstance(fil, str): if not self._patternchars.intersection(fil): child = self._fastjoin(fil) if exists(child.strpath): @@ -989,14 +989,14 @@ def write(self, data, mode="w", ensure=False): if ensure: self.dirpath().ensure(dir=1) if "b" in mode: - if not py.builtin._isbytes(data): + if not isinstance(data, bytes): raise ValueError("can only process bytes") else: - if not py.builtin._istext(data): - if not py.builtin._isbytes(data): + if not isinstance(data, str): + if not isinstance(data, bytes): data = str(data) else: - data = py.builtin._totext(data, sys.getdefaultencoding()) + data = data.decode(sys.getdefaultencoding()) f = self.open(mode) try: f.write(data) @@ -1221,7 +1221,8 @@ def pyimport(self, modname=None, ensuresyspath=True): mod.__file__ = str(self) sys.modules[modname] = mod try: - py.builtin.execfile(str(self), mod.__dict__) + with open(str(self), "rb") as f: + exec(f.read(), mod.__dict__) except: del sys.modules[modname] raise @@ -1239,12 +1240,12 @@ def sysexec(self, *argv, **popen_opts): proc = Popen([str(self)] + argv, **popen_opts) stdout, stderr = proc.communicate() ret = proc.wait() - if py.builtin._isbytes(stdout): - stdout = py.builtin._totext(stdout, sys.getdefaultencoding()) + if isinstance(stdout, bytes): + stdout = stdout.decode(sys.getdefaultencoding()) if ret != 0: - if py.builtin._isbytes(stderr): - stderr = py.builtin._totext(stderr, sys.getdefaultencoding()) - raise py.process.cmdexec.Error( + if isinstance(stderr, bytes): + stderr = stderr.decode(sys.getdefaultencoding()) + raise RuntimeError( ret, ret, str(self), @@ -1262,7 +1263,7 @@ def sysfind(cls, name, checker=None, paths=None): but may work on cygwin. """ if isabs(name): - p = py.path.local(name) + p = local(name) if p.check(file=1): return p else: @@ -1288,7 +1289,7 @@ def sysfind(cls, name, checker=None, paths=None): for x in paths: for addext in tryadd: - p = py.path.local(x).join(name, abs=True) + addext + p = local(x).join(name, abs=True) + addext try: if p.check(file=1): if checker: @@ -1323,7 +1324,7 @@ def get_temproot(cls): """ import tempfile - return py.path.local(tempfile.gettempdir()) + return local(tempfile.gettempdir()) @classmethod def mkdtemp(cls, rootdir=None): From 8a151774b8c3381bd03d664320574e8a70d7ffda Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 12:51:52 -0400 Subject: [PATCH 06/20] _pytest._py.path: remove fspath compat --- src/_pytest/_py/path.py | 62 +++++++---------------------------------- 1 file changed, 10 insertions(+), 52 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index a3c72dcbcf7..ccddcd38d3d 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -27,48 +27,6 @@ # Moved from local.py. iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt") -try: - # FileNotFoundError might happen in py34, and is not available with py27. - import_errors = (ImportError, FileNotFoundError) -except NameError: - import_errors = (ImportError,) - -try: - from os import fspath -except ImportError: - - def fspath(path): - """ - Return the string representation of the path. - If str or bytes is passed in, it is returned unchanged. - This code comes from PEP 519, modified to support earlier versions of - python. - - This is required for python < 3.6. - """ - if isinstance(path, (str, bytes)): - return path - - # Work from the object's type to match method resolution of other magic - # methods. - path_type = type(path) - try: - return path_type.__fspath__(path) - except AttributeError: - if hasattr(path_type, "__fspath__"): - raise - try: - import pathlib - except import_errors: - pass - else: - if isinstance(path, pathlib.PurePath): - return str(path) - - raise TypeError( - "expected str, bytes or os.PathLike object, not " + path_type.__name__ - ) - class Checkers: _depend_on_existence = "exists", "link", "dir", "file" @@ -157,7 +115,7 @@ class PathBase: Checkers = Checkers def __div__(self, other): - return self.join(fspath(other)) + return self.join(os.fspath(other)) __truediv__ = __div__ # py3k @@ -640,7 +598,7 @@ def __init__(self, path=None, expanduser=False): self.strpath = error.checked_call(os.getcwd) else: try: - path = fspath(path) + path = os.fspath(path) except TypeError: raise ValueError( "can only pass None, Path instances " @@ -657,9 +615,9 @@ def __hash__(self): return hash(s) def __eq__(self, other): - s1 = fspath(self) + s1 = os.fspath(self) try: - s2 = fspath(other) + s2 = os.fspath(other) except TypeError: return False if iswin32: @@ -674,14 +632,14 @@ def __ne__(self, other): return not (self == other) def __lt__(self, other): - return fspath(self) < fspath(other) + return os.fspath(self) < os.fspath(other) def __gt__(self, other): - return fspath(self) > fspath(other) + return os.fspath(self) > os.fspath(other) def samefile(self, other): """return True if 'other' references the same file as 'self'.""" - other = fspath(other) + other = os.fspath(other) if not isabs(other): other = abspath(other) if self == other: @@ -820,7 +778,7 @@ def join(self, *args, **kwargs): of the args is an absolute path. """ sep = self.sep - strargs = [fspath(arg) for arg in args] + strargs = [os.fspath(arg) for arg in args] strpath = self.strpath if kwargs.get("abs"): newargs = [] @@ -945,7 +903,7 @@ def rec(p): def rename(self, target): """rename this path to target.""" - target = fspath(target) + target = os.fspath(target) return error.checked_call(os.rename, self.strpath, target) def dump(self, obj, bin=1): @@ -961,7 +919,7 @@ def dump(self, obj, bin=1): def mkdir(self, *args): """create & return the directory joined with args.""" p = self.join(*args) - error.checked_call(os.mkdir, fspath(p)) + error.checked_call(os.mkdir, os.fspath(p)) return p def write_binary(self, data, ensure=False): From 00e2f1c15ca2f3f54f5736cf7942129ed99fe31d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 12:52:25 -0400 Subject: [PATCH 07/20] _pytest._py.path: remove _cmp compat --- src/_pytest/_py/path.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index ccddcd38d3d..93f5528529a 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -330,13 +330,6 @@ def __add__(self, other): """return new path object with 'other' added to the basename""" return self.new(basename=self.basename + str(other)) - def __cmp__(self, other): - """return sort value (-1, 0, +1).""" - try: - return cmp(self.strpath, other.strpath) - except AttributeError: - return cmp(str(self), str(other)) # self.path, other.path) - def __lt__(self, other): try: return self.strpath < other.strpath From 382209d9e9ea0ffa61a31dcfb1a961ebf8135b3d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 12:53:35 -0400 Subject: [PATCH 08/20] _pytest._py.path: remove decorator compat --- src/_pytest/_py/path.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 93f5528529a..f51e574cef0 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -119,30 +119,26 @@ def __div__(self, other): __truediv__ = __div__ # py3k + @property def basename(self): """basename part of path.""" return self._getbyspec("basename")[0] - basename = property(basename, None, None, basename.__doc__) - + @property def dirname(self): """dirname part of path.""" return self._getbyspec("dirname")[0] - dirname = property(dirname, None, None, dirname.__doc__) - + @property def purebasename(self): """pure base name of the path.""" return self._getbyspec("purebasename")[0] - purebasename = property(purebasename, None, None, purebasename.__doc__) - + @property def ext(self): """extension of the path (including the '.').""" return self._getbyspec("ext")[0] - ext = property(ext, None, None, ext.__doc__) - def dirpath(self, *args, **kwargs): """return the directory path joined with any given path arguments.""" return self.new(basename="").join(*args, **kwargs) @@ -1205,6 +1201,7 @@ def sysexec(self, *argv, **popen_opts): ) return stdout + @classmethod def sysfind(cls, name, checker=None, paths=None): """return a path object found by looking at the systems underlying PATH specification. If the checker is not None @@ -1251,8 +1248,7 @@ def sysfind(cls, name, checker=None, paths=None): pass return None - sysfind = classmethod(sysfind) - + @classmethod def _gethomedir(cls): try: x = os.environ["HOME"] @@ -1263,8 +1259,6 @@ def _gethomedir(cls): return None return cls(x) - _gethomedir = classmethod(_gethomedir) - # """ # special class constructors for local filesystem paths # """ @@ -1288,6 +1282,7 @@ def mkdtemp(cls, rootdir=None): rootdir = cls.get_temproot() return cls(error.checked_call(tempfile.mkdtemp, dir=str(rootdir))) + @classmethod def make_numbered_dir( cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800 ): # two days @@ -1449,8 +1444,6 @@ def is_garbage(path): return udir - make_numbered_dir = classmethod(make_numbered_dir) - def copymode(src, dest): """copy permission from src to dst.""" From eebbfc65c908346df0ff383dff7cad046e3e8616 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 13:08:48 -0400 Subject: [PATCH 09/20] _pytest._py.error: mypy typing --- src/_pytest/_py/error.py | 58 ++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/src/_pytest/_py/error.py b/src/_pytest/_py/error.py index 1f33c5aadbe..0b8f2d535ef 100644 --- a/src/_pytest/_py/error.py +++ b/src/_pytest/_py/error.py @@ -1,14 +1,23 @@ -""" -create errno-specific classes for IO or os calls. +"""create errno-specific classes for IO or os calls.""" +from __future__ import annotations -""" import errno import os import sys +from typing import Callable +from typing import TYPE_CHECKING +from typing import TypeVar + +if TYPE_CHECKING: + from typing_extensions import ParamSpec + + P = ParamSpec("P") + +R = TypeVar("R") class Error(EnvironmentError): - def __repr__(self): + def __repr__(self) -> str: return "{}.{} {!r}: {} ".format( self.__class__.__module__, self.__class__.__name__, @@ -17,7 +26,7 @@ def __repr__(self): # repr(self.args) ) - def __str__(self): + def __str__(self) -> str: s = "[{}]: {}".format( self.__class__.__doc__, " ".join(map(str, self.args)), @@ -44,10 +53,9 @@ class ErrorMaker: subclass EnvironmentError. """ - Error = Error - _errno2class = {} + _errno2class: dict[int, type[Error]] = {} - def __getattr__(self, name): + def __getattr__(self, name: str) -> type[Error]: if name[0] == "_": raise AttributeError(name) eno = getattr(errno, name) @@ -55,12 +63,12 @@ def __getattr__(self, name): setattr(self, name, cls) return cls - def _geterrnoclass(self, eno): + def _geterrnoclass(self, eno: int) -> type[Error]: try: return self._errno2class[eno] except KeyError: clsname = errno.errorcode.get(eno, "UnknownErrno%d" % (eno,)) - errorcls = type(Error)( + errorcls = type( clsname, (Error,), {"__module__": "py.error", "__doc__": os.strerror(eno)}, @@ -68,36 +76,34 @@ def _geterrnoclass(self, eno): self._errno2class[eno] = errorcls return errorcls - def checked_call(self, func, *args, **kwargs): - """call a function and raise an errno-exception if applicable.""" + def checked_call( + self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs + ) -> R: + """Call a function and raise an errno-exception if applicable.""" __tracebackhide__ = True try: return func(*args, **kwargs) - except self.Error: + except Error: raise - except OSError: - cls, value, tb = sys.exc_info() + except OSError as value: if not hasattr(value, "errno"): raise - __tracebackhide__ = False errno = value.errno - try: - if not isinstance(value, WindowsError): - raise NameError - except NameError: - # we are not on Windows, or we got a proper OSError - cls = self._geterrnoclass(errno) - else: + if sys.platform == "win32": try: cls = self._geterrnoclass(_winerrnomap[errno]) except KeyError: raise value + else: + # we are not on Windows, or we got a proper OSError + cls = self._geterrnoclass(errno) + raise cls(f"{func.__name__}{args!r}") - __tracebackhide__ = True _error_maker = ErrorMaker() +checked_call = _error_maker.checked_call -def __getattr__(attr): - return getattr(_error_maker, attr) +def __getattr__(attr: str) -> type[Error]: + return getattr(_error_maker, attr) # type: ignore[no-any-return] From 63c4d45c591e692084ff1031875c4bb00d71c28e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 16:58:16 -0400 Subject: [PATCH 10/20] _pytest._py.path: importlib mode always available --- src/_pytest/_py/path.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index f51e574cef0..3fc0d694be7 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -1,8 +1,11 @@ """ local path implementation. """ +from __future__ import annotations + import atexit import fnmatch +import importlib.util import io import os import posixpath @@ -440,11 +443,6 @@ def map_as_list(func, iter): return list(map(func, iter)) -ALLOW_IMPORTLIB_MODE = sys.version_info > (3, 5) -if ALLOW_IMPORTLIB_MODE: - import importlib - - class Stat: def __getattr__(self, name): return getattr(self._osstatresult, "st_" + name) @@ -1110,8 +1108,6 @@ def pyimport(self, modname=None, ensuresyspath=True): if ensuresyspath == "importlib": if modname is None: modname = self.purebasename - if not ALLOW_IMPORTLIB_MODE: - raise ImportError("Can't use importlib due to old version of Python") spec = importlib.util.spec_from_file_location(modname, str(self)) if spec is None: raise ImportError( From 73349ef3e1896f2cab0192d96126a128d22b6c62 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 17:07:43 -0400 Subject: [PATCH 11/20] _pytest._py.path: flake8 fixes --- src/_pytest/_py/path.py | 158 ++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 80 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 3fc0d694be7..e9b50977ed8 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -1,6 +1,4 @@ -""" -local path implementation. -""" +"""local path implementation.""" from __future__ import annotations import atexit @@ -124,45 +122,45 @@ def __div__(self, other): @property def basename(self): - """basename part of path.""" + """Basename part of path.""" return self._getbyspec("basename")[0] @property def dirname(self): - """dirname part of path.""" + """Dirname part of path.""" return self._getbyspec("dirname")[0] @property def purebasename(self): - """pure base name of the path.""" + """Pure base name of the path.""" return self._getbyspec("purebasename")[0] @property def ext(self): - """extension of the path (including the '.').""" + """Extension of the path (including the '.').""" return self._getbyspec("ext")[0] def dirpath(self, *args, **kwargs): - """return the directory path joined with any given path arguments.""" + """Return the directory path joined with any given path arguments.""" return self.new(basename="").join(*args, **kwargs) def read_binary(self): - """read and return a bytestring from reading the path.""" + """Read and return a bytestring from reading the path.""" with self.open("rb") as f: return f.read() def read_text(self, encoding): - """read and return a Unicode string from reading the path.""" + """Read and return a Unicode string from reading the path.""" with self.open("r", encoding=encoding) as f: return f.read() def read(self, mode="r"): - """read and return a bytestring from reading the path.""" + """Read and return a bytestring from reading the path.""" with self.open(mode) as f: return f.read() def readlines(self, cr=1): - """read and return a list of lines from the path. if cr is False, the + """Read and return a list of lines from the path. if cr is False, the newline will be removed from the end of each line.""" mode = "r" @@ -187,7 +185,7 @@ def load(self): f.close() def move(self, target): - """move this path to target.""" + """Move this path to target.""" if target.relto(self): raise error.EINVAL(target, "cannot move path into a subdirectory of itself") try: @@ -197,11 +195,11 @@ def move(self, target): self.remove() def __repr__(self): - """return a string representation of this path.""" + """Return a string representation of this path.""" return repr(str(self)) def check(self, **kw): - """check a path for existence and properties. + """Check a path for existence and properties. Without arguments, return True if the path exists, otherwise False. @@ -222,7 +220,7 @@ def check(self, **kw): return self.Checkers(self)._evaluate(kw) def fnmatch(self, pattern): - """return true if the basename/fullname matches the glob-'pattern'. + """Return true if the basename/fullname matches the glob-'pattern'. valid pattern characters:: @@ -241,7 +239,7 @@ def fnmatch(self, pattern): return FNMatcher(pattern)(self) def relto(self, relpath): - """return a string which is the relative part of the path + """Return a string which is the relative part of the path to the given 'relpath'. """ if not isinstance(relpath, (str, PathBase)): @@ -260,11 +258,11 @@ def relto(self, relpath): return "" def ensure_dir(self, *args): - """ensure the path joined with args is a directory.""" + """Ensure the path joined with args is a directory.""" return self.ensure(*args, **{"dir": True}) def bestrelpath(self, dest): - """return a string which is a relative path from self + """Return a string which is a relative path from self (assumed to be a directory) to dest such that self.join(bestrelpath) == dest and if not such path can be determined return dest. @@ -281,10 +279,10 @@ def bestrelpath(self, dest): n = self2base.count(self.sep) + 1 else: n = 0 - l = [os.pardir] * n + lst = [os.pardir] * n if reldest: - l.append(reldest) - target = dest.sep.join(l) + lst.append(reldest) + target = dest.sep.join(lst) return target except AttributeError: return str(dest) @@ -299,23 +297,23 @@ def isfile(self): return self.check(file=1) def parts(self, reverse=False): - """return a root-first list of all ancestor directories + """Return a root-first list of all ancestor directories plus the path itself. """ current = self - l = [self] + lst = [self] while 1: last = current current = current.dirpath() if last == current: break - l.append(current) + lst.append(current) if not reverse: - l.reverse() - return l + lst.reverse() + return lst def common(self, other): - """return the common part shared with the other path + """Return the common part shared with the other path or None if there is no common part. """ last = None @@ -326,7 +324,7 @@ def common(self, other): return last def __add__(self, other): - """return new path object with 'other' added to the basename""" + """Return new path object with 'other' added to the basename""" return self.new(basename=self.basename + str(other)) def __lt__(self, other): @@ -336,7 +334,7 @@ def __lt__(self, other): return str(self) < str(other) def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): - """yields all paths below the current one + """Yields all paths below the current one fil is a filter (glob pattern or callable), if not matching the path will not be yielded, defaulting to None (everything is @@ -369,7 +367,7 @@ def _sortlist(self, res, sort): res.sort() def samefile(self, other): - """return True if other refers to the same stat object as self.""" + """Return True if other refers to the same stat object as self.""" return self.strpath == str(other) def __fspath__(self): @@ -462,7 +460,7 @@ def owner(self): @property def group(self): - """return group name of file.""" + """Return group name of file.""" if iswin32: raise NotImplementedError("XXX win32") import grp @@ -483,7 +481,7 @@ def islink(self): class PosixPath(PathBase): def chown(self, user, group, rec=0): - """change ownership to the given user and group. + """Change ownership to the given user and group. user and group may be specified by a number or by a name. if rec is True change ownership recursively. @@ -497,15 +495,15 @@ def chown(self, user, group, rec=0): error.checked_call(os.chown, str(self), uid, gid) def readlink(self): - """return value of a symbolic link.""" + """Return value of a symbolic link.""" return error.checked_call(os.readlink, self.strpath) def mklinkto(self, oldname): - """posix style hard link to another name.""" + """Posix style hard link to another name.""" error.checked_call(os.link, str(oldname), str(self)) def mksymlinkto(self, value, absolute=1): - """create a symbolic link with the given value (pointing to another name).""" + """Create a symbolic link with the given value (pointing to another name).""" if absolute: error.checked_call(os.symlink, str(value), self.strpath) else: @@ -538,7 +536,7 @@ def getgroupid(group): class LocalPath(FSBase): - """object oriented interface to os.path and other local filesystem + """Object oriented interface to os.path and other local filesystem related information. """ @@ -625,7 +623,7 @@ def __gt__(self, other): return os.fspath(self) > os.fspath(other) def samefile(self, other): - """return True if 'other' references the same file as 'self'.""" + """Return True if 'other' references the same file as 'self'.""" other = os.fspath(other) if not isabs(other): other = abspath(other) @@ -636,7 +634,7 @@ def samefile(self, other): return error.checked_call(os.path.samefile, self.strpath, other) def remove(self, rec=1, ignore_errors=False): - """remove a file or directory (or a directory tree if rec=1). + """Remove a file or directory (or a directory tree if rec=1). if ignore_errors is True, errors while removing directories will be ignored. """ @@ -658,7 +656,7 @@ def remove(self, rec=1, ignore_errors=False): error.checked_call(os.remove, self.strpath) def computehash(self, hashtype="md5", chunksize=524288): - """return hexdigest of hashvalue for this file.""" + """Return hexdigest of hashvalue for this file.""" try: try: import hashlib as mod @@ -680,7 +678,7 @@ def computehash(self, hashtype="md5", chunksize=524288): f.close() def new(self, **kw): - """create a modified version of this path. + """Create a modified version of this path. the following keyword arguments modify various path parts:: a:/some/path/to/a/file.ext @@ -720,7 +718,7 @@ def new(self, **kw): return obj def _getbyspec(self, spec): - """see new for what 'spec' can be.""" + """See new for what 'spec' can be.""" res = [] parts = self.strpath.split(self.sep) @@ -750,7 +748,7 @@ def _getbyspec(self, spec): return res def dirpath(self, *args, **kwargs): - """return the directory path joined with any given path arguments.""" + """Return the directory path joined with any given path arguments.""" if not kwargs: path = object.__new__(self.__class__) path.strpath = dirname(self.strpath) @@ -760,7 +758,7 @@ def dirpath(self, *args, **kwargs): return super().dirpath(*args, **kwargs) def join(self, *args, **kwargs): - """return a new path by appending all 'args' as path + """Return a new path by appending all 'args' as path components. if abs=1 is used restart from root if any of the args is an absolute path. """ @@ -790,7 +788,7 @@ def join(self, *args, **kwargs): return obj def open(self, mode="r", ensure=False, encoding=None): - """return an opened file with the given mode. + """Return an opened file with the given mode. If ensure is True, create parent directories if needed. """ @@ -821,7 +819,7 @@ def check(self, **kw): _patternchars = set("*?[" + os.path.sep) def listdir(self, fil=None, sort=None): - """list directory contents, possibly filter by the given fil func + """List directory contents, possibly filter by the given fil func and possibly sorted. """ if fil is None and sort is None: @@ -844,15 +842,15 @@ def listdir(self, fil=None, sort=None): return res def size(self): - """return size of the underlying file object""" + """Return size of the underlying file object""" return self.stat().size def mtime(self): - """return last modification time of the path.""" + """Return last modification time of the path.""" return self.stat().mtime def copy(self, target, mode=False, stat=False): - """copy path to target. + """Copy path to target. If mode is True, will copy copy permission from path to target. If stat is True, copy permission, last modification @@ -889,12 +887,12 @@ def rec(p): copystat(x, newx) def rename(self, target): - """rename this path to target.""" + """Rename this path to target.""" target = os.fspath(target) return error.checked_call(os.rename, self.strpath, target) def dump(self, obj, bin=1): - """pickle object into path location""" + """Pickle object into path location""" f = self.open("wb") import pickle @@ -904,13 +902,13 @@ def dump(self, obj, bin=1): f.close() def mkdir(self, *args): - """create & return the directory joined with args.""" + """Create & return the directory joined with args.""" p = self.join(*args) error.checked_call(os.mkdir, os.fspath(p)) return p def write_binary(self, data, ensure=False): - """write binary data into path. If ensure is True create + """Write binary data into path. If ensure is True create missing parent directories. """ if ensure: @@ -919,7 +917,7 @@ def write_binary(self, data, ensure=False): f.write(data) def write_text(self, data, encoding, ensure=False): - """write text data into path using the specified encoding. + """Write text data into path using the specified encoding. If ensure is True create missing parent directories. """ if ensure: @@ -928,7 +926,7 @@ def write_text(self, data, encoding, ensure=False): f.write(data) def write(self, data, mode="w", ensure=False): - """write data into path. If ensure is True create + """Write data into path. If ensure is True create missing parent directories. """ if ensure: @@ -965,7 +963,7 @@ def _ensuredirs(self): return self def ensure(self, *args, **kwargs): - """ensure that an args-joined path exists (by default as + """Ensure that an args-joined path exists (by default as a file). if you specify a keyword argument 'dir=True' then the path is forced to be a directory path. """ @@ -980,7 +978,7 @@ def ensure(self, *args, **kwargs): def stat(self, raising=True): """Return an os.stat() tuple.""" - if raising == True: + if raising: return Stat(self, error.checked_call(os.stat, self.strpath)) try: return Stat(self, os.stat(self.strpath)) @@ -994,7 +992,7 @@ def lstat(self): return Stat(self, error.checked_call(os.lstat, self.strpath)) def setmtime(self, mtime=None): - """set modification time for the given path. if 'mtime' is None + """Set modification time for the given path. if 'mtime' is None (the default) then the file's mtime is set to current time. Note that the resolution for 'mtime' is platform dependent. @@ -1007,7 +1005,7 @@ def setmtime(self, mtime=None): return error.checked_call(os.utime, self.strpath, (self.atime(), mtime)) def chdir(self): - """change directory to self and return old current directory""" + """Change directory to self and return old current directory""" try: old = self.__class__() except error.ENOENT: @@ -1030,22 +1028,22 @@ def as_cwd(self): old.chdir() def realpath(self): - """return a new path which contains no symbolic links.""" + """Return a new path which contains no symbolic links.""" return self.__class__(os.path.realpath(self.strpath)) def atime(self): - """return last access time of the path.""" + """Return last access time of the path.""" return self.stat().atime def __repr__(self): return "local(%r)" % self.strpath def __str__(self): - """return string representation of the Path.""" + """Return string representation of the Path.""" return self.strpath def chmod(self, mode, rec=0): - """change permissions to the given mode. If mode is an + """Change permissions to the given mode. If mode is an integer it directly encodes the os-specific modes. if rec is True perform recursively. """ @@ -1057,7 +1055,7 @@ def chmod(self, mode, rec=0): error.checked_call(os.chmod, self.strpath, mode) def pypkgpath(self): - """return the Python package path by looking for the last + """Return the Python package path by looking for the last directory upwards which still contains an __init__.py. Return None if a pkgpath can not be determined. """ @@ -1082,7 +1080,7 @@ def _ensuresyspath(self, ensuremode, path): sys.path.insert(0, s) def pyimport(self, modname=None, ensuresyspath=True): - """return path as an imported python module. + """Return path as an imported python module. If modname is None, look for the containing package and construct an according module name. @@ -1166,13 +1164,13 @@ def pyimport(self, modname=None, ensuresyspath=True): try: with open(str(self), "rb") as f: exec(f.read(), mod.__dict__) - except: + except BaseException: del sys.modules[modname] raise return mod def sysexec(self, *argv, **popen_opts): - """return stdout text from executing a system child process, + """Return stdout text from executing a system child process, where the 'self' path points to executable. The process is directly invoked and not through a system shell. """ @@ -1199,7 +1197,7 @@ def sysexec(self, *argv, **popen_opts): @classmethod def sysfind(cls, name, checker=None, paths=None): - """return a path object found by looking at the systems + """Return a path object found by looking at the systems underlying PATH specification. If the checker is not None it will be invoked to filter matching paths. If a binary cannot be found, None is returned @@ -1260,7 +1258,7 @@ def _gethomedir(cls): # """ @classmethod def get_temproot(cls): - """return the system's temporary directory + """Return the system's temporary directory (where tempfiles are usually created in) """ import tempfile @@ -1269,7 +1267,7 @@ def get_temproot(cls): @classmethod def mkdtemp(cls, rootdir=None): - """return a Path object pointing to a fresh new temporary directory + """Return a Path object pointing to a fresh new temporary directory (which we created ourself). """ import tempfile @@ -1282,7 +1280,7 @@ def mkdtemp(cls, rootdir=None): def make_numbered_dir( cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800 ): # two days - """return unique directory with a number greater than the current + """Return unique directory with a number greater than the current maximum one. The number is assumed to start directly after prefix. if keep is true directories with a number less than (maxnum-keep) will be removed. If .lock files are used (lock_timeout non-zero), @@ -1294,7 +1292,7 @@ def make_numbered_dir( nprefix = prefix.lower() def parse_num(path): - """parse the number out of a path (if it matches the prefix)""" + """Parse the number out of a path (if it matches the prefix)""" nbasename = path.basename.lower() if nbasename.startswith(nprefix): try: @@ -1303,7 +1301,7 @@ def parse_num(path): pass def create_lockfile(path): - """exclusively create lockfile. Throws when failed""" + """Exclusively create lockfile. Throws when failed""" mypid = os.getpid() lockfile = path.join(".lock") if hasattr(lockfile, "mksymlinkto"): @@ -1317,7 +1315,7 @@ def create_lockfile(path): return lockfile def atexit_remove_lockfile(lockfile): - """ensure lockfile is removed at process exit""" + """Ensure lockfile is removed at process exit""" mypid = os.getpid() def try_remove_lockfile(): @@ -1366,7 +1364,7 @@ def try_remove_lockfile(): break def get_mtime(path): - """read file modification time""" + """Read file modification time""" try: return path.lstat().mtime except error.Error: @@ -1375,7 +1373,7 @@ def get_mtime(path): garbage_prefix = prefix + "garbage-" def is_garbage(path): - """check if path denotes directory scheduled for removal""" + """Check if path denotes directory scheduled for removal""" bn = path.basename return bn.startswith(garbage_prefix) @@ -1408,14 +1406,14 @@ def is_garbage(path): garbage_path.remove(rec=1) except KeyboardInterrupt: raise - except: # this might be error.Error, WindowsError ... + except Exception: # this might be error.Error, WindowsError ... pass if is_garbage(path): try: path.remove(rec=1) except KeyboardInterrupt: raise - except: # this might be error.Error, WindowsError ... + except Exception: # this might be error.Error, WindowsError ... pass # make link... @@ -1442,14 +1440,14 @@ def is_garbage(path): def copymode(src, dest): - """copy permission from src to dst.""" + """Copy permission from src to dst.""" import shutil shutil.copymode(src, dest) def copystat(src, dest): - """copy permission, last modification time, + """Copy permission, last modification time, last access time, and flags from src to dst.""" import shutil From af078f3a96cab9fbad610bd6c20a256a5c249cf7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 18:37:22 -0400 Subject: [PATCH 12/20] _pytest._py.path: combine Checkers classes --- src/_pytest/_py/path.py | 60 +++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index e9b50977ed8..5cca5c1471f 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -35,12 +35,6 @@ class Checkers: def __init__(self, path): self.path = path - def dir(self): - raise NotImplementedError - - def file(self): - raise NotImplementedError - def dotfile(self): return self.path.basename.startswith(".") @@ -49,9 +43,6 @@ def ext(self, arg): arg = "." + arg return self.path.ext == arg - def exists(self): - raise NotImplementedError - def basename(self, arg): return self.path.basename == arg @@ -105,6 +96,29 @@ def _evaluate(self, kw): return False return True + def _stat(self): + try: + return self._statcache + except AttributeError: + try: + self._statcache = self.path.stat() + except error.ELOOP: + self._statcache = self.path.lstat() + return self._statcache + + def dir(self): + return S_ISDIR(self._stat().mode) + + def file(self): + return S_ISREG(self._stat().mode) + + def exists(self): + return self._stat() + + def link(self): + st = self.path.lstat() + return S_ISLNK(st.mode) + class NeverRaised(Exception): pass @@ -113,8 +127,6 @@ class NeverRaised(Exception): class PathBase: """shared implementation for filesystem path objects.""" - Checkers = Checkers - def __div__(self, other): return self.join(os.fspath(other)) @@ -217,7 +229,7 @@ def check(self, **kw): """ if not kw: kw = {"exists": 1} - return self.Checkers(self)._evaluate(kw) + return Checkers(self)._evaluate(kw) def fnmatch(self, pattern): """Return true if the basename/fullname matches the glob-'pattern'. @@ -545,30 +557,6 @@ class ImportMismatchError(ImportError): sep = os.sep - class Checkers(Checkers): - def _stat(self): - try: - return self._statcache - except AttributeError: - try: - self._statcache = self.path.stat() - except error.ELOOP: - self._statcache = self.path.lstat() - return self._statcache - - def dir(self): - return S_ISDIR(self._stat().mode) - - def file(self): - return S_ISREG(self._stat().mode) - - def exists(self): - return self._stat() - - def link(self): - st = self.path.lstat() - return S_ISLNK(st.mode) - def __init__(self, path=None, expanduser=False): """Initialize and return a local Path instance. From 6660d4552140679f9bd66b8821a1550f4e39ddb6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 18:44:36 -0400 Subject: [PATCH 13/20] _pytest._py.path: combine PosixPath into LocalPath --- src/_pytest/_py/path.py | 79 ++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 5cca5c1471f..378de6967bf 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -491,43 +491,6 @@ def islink(self): return S_ISLNK(self._osstatresult.st_mode) -class PosixPath(PathBase): - def chown(self, user, group, rec=0): - """Change ownership to the given user and group. - user and group may be specified by a number or - by a name. if rec is True change ownership - recursively. - """ - uid = getuserid(user) - gid = getgroupid(group) - if rec: - for x in self.visit(rec=lambda x: x.check(link=0)): - if x.check(link=0): - error.checked_call(os.chown, str(x), uid, gid) - error.checked_call(os.chown, str(self), uid, gid) - - def readlink(self): - """Return value of a symbolic link.""" - return error.checked_call(os.readlink, self.strpath) - - def mklinkto(self, oldname): - """Posix style hard link to another name.""" - error.checked_call(os.link, str(oldname), str(self)) - - def mksymlinkto(self, value, absolute=1): - """Create a symbolic link with the given value (pointing to another name).""" - if absolute: - error.checked_call(os.symlink, str(value), self.strpath) - else: - base = self.common(value) - # with posix local paths '/' is always a common base - relsource = self.__class__(value).relto(base) - reldest = self.relto(base) - n = reldest.count(self.sep) - target = self.sep.join(("..",) * n + (relsource,)) - error.checked_call(os.symlink, target, self.strpath) - - def getuserid(user): import pwd @@ -544,10 +507,7 @@ def getgroupid(group): return group -FSBase = not iswin32 and PosixPath or PathBase - - -class LocalPath(FSBase): +class LocalPath(PathBase): """Object oriented interface to os.path and other local filesystem related information. """ @@ -581,6 +541,43 @@ def __init__(self, path=None, expanduser=False): path = os.path.expanduser(path) self.strpath = abspath(path) + if sys.platform != "win32": + + def chown(self, user, group, rec=0): + """Change ownership to the given user and group. + user and group may be specified by a number or + by a name. if rec is True change ownership + recursively. + """ + uid = getuserid(user) + gid = getgroupid(group) + if rec: + for x in self.visit(rec=lambda x: x.check(link=0)): + if x.check(link=0): + error.checked_call(os.chown, str(x), uid, gid) + error.checked_call(os.chown, str(self), uid, gid) + + def readlink(self): + """Return value of a symbolic link.""" + return error.checked_call(os.readlink, self.strpath) + + def mklinkto(self, oldname): + """Posix style hard link to another name.""" + error.checked_call(os.link, str(oldname), str(self)) + + def mksymlinkto(self, value, absolute=1): + """Create a symbolic link with the given value (pointing to another name).""" + if absolute: + error.checked_call(os.symlink, str(value), self.strpath) + else: + base = self.common(value) + # with posix local paths '/' is always a common base + relsource = self.__class__(value).relto(base) + reldest = self.relto(base) + n = reldest.count(self.sep) + target = self.sep.join(("..",) * n + (relsource,)) + error.checked_call(os.symlink, target, self.strpath) + def __hash__(self): s = self.strpath if iswin32: From ed4c18f686282b4a7603b04cd3e9c1855366202c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 18:50:44 -0400 Subject: [PATCH 14/20] _pytest._py.path: combine PathBase and LocalPath --- src/_pytest/_py/path.py | 449 +++++++++++++++++++--------------------- 1 file changed, 212 insertions(+), 237 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 378de6967bf..2e2189cb3cf 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -124,8 +124,197 @@ class NeverRaised(Exception): pass -class PathBase: - """shared implementation for filesystem path objects.""" +class Visitor: + def __init__(self, fil, rec, ignore, bf, sort): + if isinstance(fil, str): + fil = FNMatcher(fil) + if isinstance(rec, str): + self.rec = FNMatcher(rec) + elif not hasattr(rec, "__call__") and rec: + self.rec = lambda path: True + else: + self.rec = rec + self.fil = fil + self.ignore = ignore + self.breadthfirst = bf + self.optsort = sort and sorted or (lambda x: x) + + def gen(self, path): + try: + entries = path.listdir() + except self.ignore: + return + rec = self.rec + dirs = self.optsort( + [p for p in entries if p.check(dir=1) and (rec is None or rec(p))] + ) + if not self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + for p in self.optsort(entries): + if self.fil is None or self.fil(p): + yield p + if self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + + +class FNMatcher: + def __init__(self, pattern): + self.pattern = pattern + + def __call__(self, path): + pattern = self.pattern + + if ( + pattern.find(path.sep) == -1 + and iswin32 + and pattern.find(posixpath.sep) != -1 + ): + # Running on Windows, the pattern has no Windows path separators, + # and the pattern has one or more Posix path separators. Replace + # the Posix path separators with the Windows path separator. + pattern = pattern.replace(posixpath.sep, path.sep) + + if pattern.find(path.sep) == -1: + name = path.basename + else: + name = str(path) # path.strpath # XXX svn? + if not os.path.isabs(pattern): + pattern = "*" + path.sep + pattern + return fnmatch.fnmatch(name, pattern) + + +def map_as_list(func, iter): + return list(map(func, iter)) + + +class Stat: + def __getattr__(self, name): + return getattr(self._osstatresult, "st_" + name) + + def __init__(self, path, osstatresult): + self.path = path + self._osstatresult = osstatresult + + @property + def owner(self): + if iswin32: + raise NotImplementedError("XXX win32") + import pwd + + entry = error.checked_call(pwd.getpwuid, self.uid) + return entry[0] + + @property + def group(self): + """Return group name of file.""" + if iswin32: + raise NotImplementedError("XXX win32") + import grp + + entry = error.checked_call(grp.getgrgid, self.gid) + return entry[0] + + def isdir(self): + return S_ISDIR(self._osstatresult.st_mode) + + def isfile(self): + return S_ISREG(self._osstatresult.st_mode) + + def islink(self): + self.path.lstat() + return S_ISLNK(self._osstatresult.st_mode) + + +def getuserid(user): + import pwd + + if not isinstance(user, int): + user = pwd.getpwnam(user)[2] + return user + + +def getgroupid(group): + import grp + + if not isinstance(group, int): + group = grp.getgrnam(group)[2] + return group + + +class LocalPath: + """Object oriented interface to os.path and other local filesystem + related information. + """ + + class ImportMismatchError(ImportError): + """raised on pyimport() if there is a mismatch of __file__'s""" + + sep = os.sep + + def __init__(self, path=None, expanduser=False): + """Initialize and return a local Path instance. + + Path can be relative to the current directory. + If path is None it defaults to the current working directory. + If expanduser is True, tilde-expansion is performed. + Note that Path instances always carry an absolute path. + Note also that passing in a local path object will simply return + the exact same path object. Use new() to get a new copy. + """ + if path is None: + self.strpath = error.checked_call(os.getcwd) + else: + try: + path = os.fspath(path) + except TypeError: + raise ValueError( + "can only pass None, Path instances " + "or non-empty strings to LocalPath" + ) + if expanduser: + path = os.path.expanduser(path) + self.strpath = abspath(path) + + if sys.platform != "win32": + + def chown(self, user, group, rec=0): + """Change ownership to the given user and group. + user and group may be specified by a number or + by a name. if rec is True change ownership + recursively. + """ + uid = getuserid(user) + gid = getgroupid(group) + if rec: + for x in self.visit(rec=lambda x: x.check(link=0)): + if x.check(link=0): + error.checked_call(os.chown, str(x), uid, gid) + error.checked_call(os.chown, str(self), uid, gid) + + def readlink(self): + """Return value of a symbolic link.""" + return error.checked_call(os.readlink, self.strpath) + + def mklinkto(self, oldname): + """Posix style hard link to another name.""" + error.checked_call(os.link, str(oldname), str(self)) + + def mksymlinkto(self, value, absolute=1): + """Create a symbolic link with the given value (pointing to another name).""" + if absolute: + error.checked_call(os.symlink, str(value), self.strpath) + else: + base = self.common(value) + # with posix local paths '/' is always a common base + relsource = self.__class__(value).relto(base) + reldest = self.relto(base) + n = reldest.count(self.sep) + target = self.sep.join(("..",) * n + (relsource,)) + error.checked_call(os.symlink, target, self.strpath) def __div__(self, other): return self.join(os.fspath(other)) @@ -152,10 +341,6 @@ def ext(self): """Extension of the path (including the '.').""" return self._getbyspec("ext")[0] - def dirpath(self, *args, **kwargs): - """Return the directory path joined with any given path arguments.""" - return self.new(basename="").join(*args, **kwargs) - def read_binary(self): """Read and return a bytestring from reading the path.""" with self.open("rb") as f: @@ -206,31 +391,6 @@ def move(self, target): self.copy(target) self.remove() - def __repr__(self): - """Return a string representation of this path.""" - return repr(str(self)) - - def check(self, **kw): - """Check a path for existence and properties. - - Without arguments, return True if the path exists, otherwise False. - - valid checkers:: - - file=1 # is a file - file=0 # is not a file (may not even exist) - dir=1 # is a dir - link=1 # is a link - exists=1 # exists - - You can specify multiple checker definitions, for example:: - - path.check(file=1, link=1) # a link pointing to a file - """ - if not kw: - kw = {"exists": 1} - return Checkers(self)._evaluate(kw) - def fnmatch(self, pattern): """Return true if the basename/fullname matches the glob-'pattern'. @@ -254,7 +414,7 @@ def relto(self, relpath): """Return a string which is the relative part of the path to the given 'relpath'. """ - if not isinstance(relpath, (str, PathBase)): + if not isinstance(relpath, (str, LocalPath)): raise TypeError(f"{relpath!r}: not a string or path object") strrelpath = str(relpath) if strrelpath and strrelpath[-1] != self.sep: @@ -339,12 +499,6 @@ def __add__(self, other): """Return new path object with 'other' added to the basename""" return self.new(basename=self.basename + str(other)) - def __lt__(self, other): - try: - return self.strpath < other.strpath - except AttributeError: - return str(self) < str(other) - def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): """Yields all paths below the current one @@ -378,206 +532,9 @@ def _sortlist(self, res, sort): else: res.sort() - def samefile(self, other): - """Return True if other refers to the same stat object as self.""" - return self.strpath == str(other) - def __fspath__(self): return self.strpath - -class Visitor: - def __init__(self, fil, rec, ignore, bf, sort): - if isinstance(fil, str): - fil = FNMatcher(fil) - if isinstance(rec, str): - self.rec = FNMatcher(rec) - elif not hasattr(rec, "__call__") and rec: - self.rec = lambda path: True - else: - self.rec = rec - self.fil = fil - self.ignore = ignore - self.breadthfirst = bf - self.optsort = sort and sorted or (lambda x: x) - - def gen(self, path): - try: - entries = path.listdir() - except self.ignore: - return - rec = self.rec - dirs = self.optsort( - [p for p in entries if p.check(dir=1) and (rec is None or rec(p))] - ) - if not self.breadthfirst: - for subdir in dirs: - for p in self.gen(subdir): - yield p - for p in self.optsort(entries): - if self.fil is None or self.fil(p): - yield p - if self.breadthfirst: - for subdir in dirs: - for p in self.gen(subdir): - yield p - - -class FNMatcher: - def __init__(self, pattern): - self.pattern = pattern - - def __call__(self, path): - pattern = self.pattern - - if ( - pattern.find(path.sep) == -1 - and iswin32 - and pattern.find(posixpath.sep) != -1 - ): - # Running on Windows, the pattern has no Windows path separators, - # and the pattern has one or more Posix path separators. Replace - # the Posix path separators with the Windows path separator. - pattern = pattern.replace(posixpath.sep, path.sep) - - if pattern.find(path.sep) == -1: - name = path.basename - else: - name = str(path) # path.strpath # XXX svn? - if not os.path.isabs(pattern): - pattern = "*" + path.sep + pattern - return fnmatch.fnmatch(name, pattern) - - -def map_as_list(func, iter): - return list(map(func, iter)) - - -class Stat: - def __getattr__(self, name): - return getattr(self._osstatresult, "st_" + name) - - def __init__(self, path, osstatresult): - self.path = path - self._osstatresult = osstatresult - - @property - def owner(self): - if iswin32: - raise NotImplementedError("XXX win32") - import pwd - - entry = error.checked_call(pwd.getpwuid, self.uid) - return entry[0] - - @property - def group(self): - """Return group name of file.""" - if iswin32: - raise NotImplementedError("XXX win32") - import grp - - entry = error.checked_call(grp.getgrgid, self.gid) - return entry[0] - - def isdir(self): - return S_ISDIR(self._osstatresult.st_mode) - - def isfile(self): - return S_ISREG(self._osstatresult.st_mode) - - def islink(self): - self.path.lstat() - return S_ISLNK(self._osstatresult.st_mode) - - -def getuserid(user): - import pwd - - if not isinstance(user, int): - user = pwd.getpwnam(user)[2] - return user - - -def getgroupid(group): - import grp - - if not isinstance(group, int): - group = grp.getgrnam(group)[2] - return group - - -class LocalPath(PathBase): - """Object oriented interface to os.path and other local filesystem - related information. - """ - - class ImportMismatchError(ImportError): - """raised on pyimport() if there is a mismatch of __file__'s""" - - sep = os.sep - - def __init__(self, path=None, expanduser=False): - """Initialize and return a local Path instance. - - Path can be relative to the current directory. - If path is None it defaults to the current working directory. - If expanduser is True, tilde-expansion is performed. - Note that Path instances always carry an absolute path. - Note also that passing in a local path object will simply return - the exact same path object. Use new() to get a new copy. - """ - if path is None: - self.strpath = error.checked_call(os.getcwd) - else: - try: - path = os.fspath(path) - except TypeError: - raise ValueError( - "can only pass None, Path instances " - "or non-empty strings to LocalPath" - ) - if expanduser: - path = os.path.expanduser(path) - self.strpath = abspath(path) - - if sys.platform != "win32": - - def chown(self, user, group, rec=0): - """Change ownership to the given user and group. - user and group may be specified by a number or - by a name. if rec is True change ownership - recursively. - """ - uid = getuserid(user) - gid = getgroupid(group) - if rec: - for x in self.visit(rec=lambda x: x.check(link=0)): - if x.check(link=0): - error.checked_call(os.chown, str(x), uid, gid) - error.checked_call(os.chown, str(self), uid, gid) - - def readlink(self): - """Return value of a symbolic link.""" - return error.checked_call(os.readlink, self.strpath) - - def mklinkto(self, oldname): - """Posix style hard link to another name.""" - error.checked_call(os.link, str(oldname), str(self)) - - def mksymlinkto(self, value, absolute=1): - """Create a symbolic link with the given value (pointing to another name).""" - if absolute: - error.checked_call(os.symlink, str(value), self.strpath) - else: - base = self.common(value) - # with posix local paths '/' is always a common base - relsource = self.__class__(value).relto(base) - reldest = self.relto(base) - n = reldest.count(self.sep) - target = self.sep.join(("..",) * n + (relsource,)) - error.checked_call(os.symlink, target, self.strpath) - def __hash__(self): s = self.strpath if iswin32: @@ -740,7 +697,7 @@ def dirpath(self, *args, **kwargs): if args: path = path.join(*args) return path - return super().dirpath(*args, **kwargs) + return self.new(basename="").join(*args, **kwargs) def join(self, *args, **kwargs): """Return a new path by appending all 'args' as path @@ -792,6 +749,22 @@ def islink(self): return islink(self.strpath) def check(self, **kw): + """Check a path for existence and properties. + + Without arguments, return True if the path exists, otherwise False. + + valid checkers:: + + file=1 # is a file + file=0 # is not a file (may not even exist) + dir=1 # is a dir + link=1 # is a link + exists=1 # exists + + You can specify multiple checker definitions, for example:: + + path.check(file=1, link=1) # a link pointing to a file + """ if not kw: return exists(self.strpath) if len(kw) == 1: @@ -799,7 +772,9 @@ def check(self, **kw): return not kw["dir"] ^ isdir(self.strpath) if "file" in kw: return not kw["file"] ^ isfile(self.strpath) - return super().check(**kw) + if not kw: + kw = {"exists": 1} + return Checkers(self)._evaluate(kw) _patternchars = set("*?[" + os.path.sep) From 59d8f8a22324fa7e816c53c271e4096c44bd536b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 19:26:44 -0400 Subject: [PATCH 15/20] _pytest._py.path: get mypy passing --- src/_pytest/_py/path.py | 82 +++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 2e2189cb3cf..00f1515238b 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -22,9 +22,16 @@ from stat import S_ISDIR from stat import S_ISLNK from stat import S_ISREG +from typing import Any +from typing import Callable +from typing import overload +from typing import TYPE_CHECKING from . import error +if TYPE_CHECKING: + from typing import Literal + # Moved from local.py. iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt") @@ -96,7 +103,9 @@ def _evaluate(self, kw): return False return True - def _stat(self): + _statcache: Stat + + def _stat(self) -> Stat: try: return self._statcache except AttributeError: @@ -129,7 +138,7 @@ def __init__(self, fil, rec, ignore, bf, sort): if isinstance(fil, str): fil = FNMatcher(fil) if isinstance(rec, str): - self.rec = FNMatcher(rec) + self.rec: Callable[[LocalPath], bool] = FNMatcher(rec) elif not hasattr(rec, "__call__") and rec: self.rec = lambda path: True else: @@ -192,7 +201,17 @@ def map_as_list(func, iter): class Stat: - def __getattr__(self, name): + if TYPE_CHECKING: + + @property + def size(self) -> int: + ... + + @property + def mtime(self) -> float: + ... + + def __getattr__(self, name: str) -> Any: return getattr(self._osstatresult, "st_" + name) def __init__(self, path, osstatresult): @@ -295,9 +314,10 @@ def chown(self, user, group, rec=0): error.checked_call(os.chown, str(x), uid, gid) error.checked_call(os.chown, str(self), uid, gid) - def readlink(self): + def readlink(self) -> str: """Return value of a symbolic link.""" - return error.checked_call(os.readlink, self.strpath) + # https://github.com/python/mypy/issues/12278 + return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value] def mklinkto(self, oldname): """Posix style hard link to another name.""" @@ -659,22 +679,21 @@ def new(self, **kw): obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw) return obj - def _getbyspec(self, spec): + def _getbyspec(self, spec: str) -> list[str]: """See new for what 'spec' can be.""" res = [] parts = self.strpath.split(self.sep) args = filter(None, spec.split(",")) - append = res.append for name in args: if name == "drive": - append(parts[0]) + res.append(parts[0]) elif name == "dirname": - append(self.sep.join(parts[:-1])) + res.append(self.sep.join(parts[:-1])) else: basename = parts[-1] if name == "basename": - append(basename) + res.append(basename) else: i = basename.rfind(".") if i == -1: @@ -682,9 +701,9 @@ def _getbyspec(self, spec): else: purebasename, ext = basename[:i], basename[i:] if name == "purebasename": - append(purebasename) + res.append(purebasename) elif name == "ext": - append(ext) + res.append(ext) else: raise ValueError("invalid part specification %r" % name) return res @@ -699,7 +718,7 @@ def dirpath(self, *args, **kwargs): return path return self.new(basename="").join(*args, **kwargs) - def join(self, *args, **kwargs): + def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath: """Return a new path by appending all 'args' as path components. if abs=1 is used restart from root if any of the args is an absolute path. @@ -707,8 +726,8 @@ def join(self, *args, **kwargs): sep = self.sep strargs = [os.fspath(arg) for arg in args] strpath = self.strpath - if kwargs.get("abs"): - newargs = [] + if abs: + newargs: list[str] = [] for arg in reversed(strargs): if isabs(arg): strpath = arg @@ -801,11 +820,11 @@ def listdir(self, fil=None, sort=None): self._sortlist(res, sort) return res - def size(self): + def size(self) -> int: """Return size of the underlying file object""" return self.stat().size - def mtime(self): + def mtime(self) -> float: """Return last modification time of the path.""" return self.stat().mtime @@ -936,7 +955,15 @@ def ensure(self, *args, **kwargs): p.open("w").close() return p - def stat(self, raising=True): + @overload + def stat(self, raising: Literal[True] = ...) -> Stat: + ... + + @overload + def stat(self, raising: Literal[False]) -> Stat | None: + ... + + def stat(self, raising: bool = True) -> Stat | None: """Return an os.stat() tuple.""" if raising: return Stat(self, error.checked_call(os.stat, self.strpath)) @@ -947,7 +974,7 @@ def stat(self, raising=True): except Exception: return None - def lstat(self): + def lstat(self) -> Stat: """Return an os.lstat() tuple.""" return Stat(self, error.checked_call(os.lstat, self.strpath)) @@ -1067,7 +1094,7 @@ def pyimport(self, modname=None, ensuresyspath=True): if modname is None: modname = self.purebasename spec = importlib.util.spec_from_file_location(modname, str(self)) - if spec is None: + if spec is None or spec.loader is None: raise ImportError( f"Can't find module {modname} at location {str(self)}" ) @@ -1095,6 +1122,7 @@ def pyimport(self, modname=None, ensuresyspath=True): return mod # we don't check anything as we might # be in a namespace package ... too icky to check modfile = mod.__file__ + assert modfile is not None if modfile[-4:] in (".pyc", ".pyo"): modfile = modfile[:-1] elif modfile.endswith("$py.class"): @@ -1129,16 +1157,22 @@ def pyimport(self, modname=None, ensuresyspath=True): raise return mod - def sysexec(self, *argv, **popen_opts): + def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str: """Return stdout text from executing a system child process, where the 'self' path points to executable. The process is directly invoked and not through a system shell. """ from subprocess import Popen, PIPE - argv = map_as_list(str, argv) - popen_opts["stdout"] = popen_opts["stderr"] = PIPE - proc = Popen([str(self)] + argv, **popen_opts) + popen_opts.pop("stdout", None) + popen_opts.pop("stderr", None) + proc = Popen( + [str(self)] + [str(arg) for arg in argv], + **popen_opts, + stdout=PIPE, + stderr=PIPE, + ) + stdout: str | bytes stdout, stderr = proc.communicate() ret = proc.wait() if isinstance(stdout, bytes): From 82344ba4f8e6bd49afd52235385a105890dc3d88 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 21:42:11 -0400 Subject: [PATCH 16/20] add py.path.local tests --- testing/_py/test_local.py | 1542 +++++++++++++++++++++++++++++++++++++ 1 file changed, 1542 insertions(+) create mode 100644 testing/_py/test_local.py diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py new file mode 100644 index 00000000000..6e2c44bc2e0 --- /dev/null +++ b/testing/_py/test_local.py @@ -0,0 +1,1542 @@ +import multiprocessing +import os +import sys +import time + +import pytest +from py import error +from py.path import local + + +class CommonFSTests: + def test_constructor_equality(self, path1): + p = path1.__class__(path1) + assert p == path1 + + def test_eq_nonstring(self, path1): + p1 = path1.join("sampledir") + p2 = path1.join("sampledir") + assert p1 == p2 + + def test_new_identical(self, path1): + assert path1 == path1.new() + + def test_join(self, path1): + p = path1.join("sampledir") + strp = str(p) + assert strp.endswith("sampledir") + assert strp.startswith(str(path1)) + + def test_join_normalized(self, path1): + newpath = path1.join(path1.sep + "sampledir") + strp = str(newpath) + assert strp.endswith("sampledir") + assert strp.startswith(str(path1)) + newpath = path1.join((path1.sep * 2) + "sampledir") + strp = str(newpath) + assert strp.endswith("sampledir") + assert strp.startswith(str(path1)) + + def test_join_noargs(self, path1): + newpath = path1.join() + assert path1 == newpath + + def test_add_something(self, path1): + p = path1.join("sample") + p = p + "dir" + assert p.check() + assert p.exists() + assert p.isdir() + assert not p.isfile() + + def test_parts(self, path1): + newpath = path1.join("sampledir", "otherfile") + par = newpath.parts()[-3:] + assert par == [path1, path1.join("sampledir"), newpath] + + revpar = newpath.parts(reverse=True)[:3] + assert revpar == [newpath, path1.join("sampledir"), path1] + + def test_common(self, path1): + other = path1.join("sampledir") + x = other.common(path1) + assert x == path1 + + # def test_parents_nonexisting_file(self, path1): + # newpath = path1 / 'dirnoexist' / 'nonexisting file' + # par = list(newpath.parents()) + # assert par[:2] == [path1 / 'dirnoexist', path1] + + def test_basename_checks(self, path1): + newpath = path1.join("sampledir") + assert newpath.check(basename="sampledir") + assert newpath.check(notbasename="xyz") + assert newpath.basename == "sampledir" + + def test_basename(self, path1): + newpath = path1.join("sampledir") + assert newpath.check(basename="sampledir") + assert newpath.basename, "sampledir" + + def test_dirname(self, path1): + newpath = path1.join("sampledir") + assert newpath.dirname == str(path1) + + def test_dirpath(self, path1): + newpath = path1.join("sampledir") + assert newpath.dirpath() == path1 + + def test_dirpath_with_args(self, path1): + newpath = path1.join("sampledir") + assert newpath.dirpath("x") == path1.join("x") + + def test_newbasename(self, path1): + newpath = path1.join("samplefile") + newbase = newpath.new(basename="samplefile2") + assert newbase.basename == "samplefile2" + assert newbase.dirpath() == newpath.dirpath() + + def test_not_exists(self, path1): + assert not path1.join("does_not_exist").check() + assert path1.join("does_not_exist").check(exists=0) + + def test_exists(self, path1): + assert path1.join("samplefile").check() + assert path1.join("samplefile").check(exists=1) + assert path1.join("samplefile").exists() + assert path1.join("samplefile").isfile() + assert not path1.join("samplefile").isdir() + + def test_dir(self, path1): + # print repr(path1.join("sampledir")) + assert path1.join("sampledir").check(dir=1) + assert path1.join("samplefile").check(notdir=1) + assert not path1.join("samplefile").check(dir=1) + assert path1.join("samplefile").exists() + assert not path1.join("samplefile").isdir() + assert path1.join("samplefile").isfile() + + def test_fnmatch_file(self, path1): + assert path1.join("samplefile").check(fnmatch="s*e") + assert path1.join("samplefile").fnmatch("s*e") + assert not path1.join("samplefile").fnmatch("s*x") + assert not path1.join("samplefile").check(fnmatch="s*x") + + # def test_fnmatch_dir(self, path1): + + # pattern = path1.sep.join(['s*file']) + # sfile = path1.join("samplefile") + # assert sfile.check(fnmatch=pattern) + + def test_relto(self, path1): + p = path1.join("sampledir", "otherfile") + assert p.relto(path1) == p.sep.join(["sampledir", "otherfile"]) + assert p.check(relto=path1) + assert path1.check(notrelto=p) + assert not path1.check(relto=p) + + def test_bestrelpath(self, path1): + curdir = path1 + sep = curdir.sep + s = curdir.bestrelpath(curdir) + assert s == "." + s = curdir.bestrelpath(curdir.join("hello", "world")) + assert s == "hello" + sep + "world" + + s = curdir.bestrelpath(curdir.dirpath().join("sister")) + assert s == ".." + sep + "sister" + assert curdir.bestrelpath(curdir.dirpath()) == ".." + + assert curdir.bestrelpath("hello") == "hello" + + def test_relto_not_relative(self, path1): + l1 = path1.join("bcde") + l2 = path1.join("b") + assert not l1.relto(l2) + assert not l2.relto(l1) + + def test_listdir(self, path1): + p = path1.listdir() + assert path1.join("sampledir") in p + assert path1.join("samplefile") in p + with pytest.raises(error.ENOTDIR): + path1.join("samplefile").listdir() + + def test_listdir_fnmatchstring(self, path1): + p = path1.listdir("s*dir") + assert len(p) + assert p[0], path1.join("sampledir") + + def test_listdir_filter(self, path1): + p = path1.listdir(lambda x: x.check(dir=1)) + assert path1.join("sampledir") in p + assert not path1.join("samplefile") in p + + def test_listdir_sorted(self, path1): + p = path1.listdir(lambda x: x.check(basestarts="sample"), sort=True) + assert path1.join("sampledir") == p[0] + assert path1.join("samplefile") == p[1] + assert path1.join("samplepickle") == p[2] + + def test_visit_nofilter(self, path1): + lst = [] + for i in path1.visit(): + lst.append(i.relto(path1)) + assert "sampledir" in lst + assert path1.sep.join(["sampledir", "otherfile"]) in lst + + def test_visit_norecurse(self, path1): + lst = [] + for i in path1.visit(None, lambda x: x.basename != "sampledir"): + lst.append(i.relto(path1)) + assert "sampledir" in lst + assert not path1.sep.join(["sampledir", "otherfile"]) in lst + + @pytest.mark.parametrize( + "fil", + ["*dir", "*dir", pytest.mark.skip("sys.version_info <" " (3,6)")(b"*dir")], + ) + def test_visit_filterfunc_is_string(self, path1, fil): + lst = [] + for i in path1.visit(fil): + lst.append(i.relto(path1)) + assert len(lst), 2 + assert "sampledir" in lst + assert "otherdir" in lst + + def test_visit_ignore(self, path1): + p = path1.join("nonexisting") + assert list(p.visit(ignore=error.ENOENT)) == [] + + def test_visit_endswith(self, path1): + p = [] + for i in path1.visit(lambda x: x.check(endswith="file")): + p.append(i.relto(path1)) + assert path1.sep.join(["sampledir", "otherfile"]) in p + assert "samplefile" in p + + def test_cmp(self, path1): + path1 = path1.join("samplefile") + path2 = path1.join("samplefile2") + assert (path1 < path2) == ("samplefile" < "samplefile2") + assert not (path1 < path1) + + def test_simple_read(self, path1): + x = path1.join("samplefile").read("r") + assert x == "samplefile\n" + + def test_join_div_operator(self, path1): + newpath = path1 / "/sampledir" / "/test//" + newpath2 = path1.join("sampledir", "test") + assert newpath == newpath2 + + def test_ext(self, path1): + newpath = path1.join("sampledir.ext") + assert newpath.ext == ".ext" + newpath = path1.join("sampledir") + assert not newpath.ext + + def test_purebasename(self, path1): + newpath = path1.join("samplefile.py") + assert newpath.purebasename == "samplefile" + + def test_multiple_parts(self, path1): + newpath = path1.join("samplefile.py") + dirname, purebasename, basename, ext = newpath._getbyspec( + "dirname,purebasename,basename,ext" + ) + assert str(path1).endswith(dirname) # be careful with win32 'drive' + assert purebasename == "samplefile" + assert basename == "samplefile.py" + assert ext == ".py" + + def test_dotted_name_ext(self, path1): + newpath = path1.join("a.b.c") + ext = newpath.ext + assert ext == ".c" + assert newpath.ext == ".c" + + def test_newext(self, path1): + newpath = path1.join("samplefile.py") + newext = newpath.new(ext=".txt") + assert newext.basename == "samplefile.txt" + assert newext.purebasename == "samplefile" + + def test_readlines(self, path1): + fn = path1.join("samplefile") + contents = fn.readlines() + assert contents == ["samplefile\n"] + + def test_readlines_nocr(self, path1): + fn = path1.join("samplefile") + contents = fn.readlines(cr=0) + assert contents == ["samplefile", ""] + + def test_file(self, path1): + assert path1.join("samplefile").check(file=1) + + def test_not_file(self, path1): + assert not path1.join("sampledir").check(file=1) + assert path1.join("sampledir").check(file=0) + + def test_non_existent(self, path1): + assert path1.join("sampledir.nothere").check(dir=0) + assert path1.join("sampledir.nothere").check(file=0) + assert path1.join("sampledir.nothere").check(notfile=1) + assert path1.join("sampledir.nothere").check(notdir=1) + assert path1.join("sampledir.nothere").check(notexists=1) + assert not path1.join("sampledir.nothere").check(notfile=0) + + # pattern = path1.sep.join(['s*file']) + # sfile = path1.join("samplefile") + # assert sfile.check(fnmatch=pattern) + + def test_size(self, path1): + url = path1.join("samplefile") + assert url.size() > len("samplefile") + + def test_mtime(self, path1): + url = path1.join("samplefile") + assert url.mtime() > 0 + + def test_relto_wrong_type(self, path1): + with pytest.raises(TypeError): + path1.relto(42) + + def test_load(self, path1): + p = path1.join("samplepickle") + obj = p.load() + assert type(obj) is dict + assert obj.get("answer", None) == 42 + + def test_visit_filesonly(self, path1): + p = [] + for i in path1.visit(lambda x: x.check(file=1)): + p.append(i.relto(path1)) + assert "sampledir" not in p + assert path1.sep.join(["sampledir", "otherfile"]) in p + + def test_visit_nodotfiles(self, path1): + p = [] + for i in path1.visit(lambda x: x.check(dotfile=0)): + p.append(i.relto(path1)) + assert "sampledir" in p + assert path1.sep.join(["sampledir", "otherfile"]) in p + assert ".dotfile" not in p + + def test_visit_breadthfirst(self, path1): + lst = [] + for i in path1.visit(bf=True): + lst.append(i.relto(path1)) + for i, p in enumerate(lst): + if path1.sep in p: + for j in range(i, len(lst)): + assert path1.sep in lst[j] + break + else: + pytest.fail("huh") + + def test_visit_sort(self, path1): + lst = [] + for i in path1.visit(bf=True, sort=True): + lst.append(i.relto(path1)) + for i, p in enumerate(lst): + if path1.sep in p: + break + assert lst[:i] == sorted(lst[:i]) + assert lst[i:] == sorted(lst[i:]) + + def test_endswith(self, path1): + def chk(p): + return p.check(endswith="pickle") + + assert not chk(path1) + assert not chk(path1.join("samplefile")) + assert chk(path1.join("somepickle")) + + def test_copy_file(self, path1): + otherdir = path1.join("otherdir") + initpy = otherdir.join("__init__.py") + copied = otherdir.join("copied") + initpy.copy(copied) + try: + assert copied.check() + s1 = initpy.read() + s2 = copied.read() + assert s1 == s2 + finally: + if copied.check(): + copied.remove() + + def test_copy_dir(self, path1): + otherdir = path1.join("otherdir") + copied = path1.join("newdir") + try: + otherdir.copy(copied) + assert copied.check(dir=1) + assert copied.join("__init__.py").check(file=1) + s1 = otherdir.join("__init__.py").read() + s2 = copied.join("__init__.py").read() + assert s1 == s2 + finally: + if copied.check(dir=1): + copied.remove(rec=1) + + def test_remove_file(self, path1): + d = path1.ensure("todeleted") + assert d.check() + d.remove() + assert not d.check() + + def test_remove_dir_recursive_by_default(self, path1): + d = path1.ensure("to", "be", "deleted") + assert d.check() + p = path1.join("to") + p.remove() + assert not p.check() + + def test_ensure_dir(self, path1): + b = path1.ensure_dir("001", "002") + assert b.basename == "002" + assert b.isdir() + + def test_mkdir_and_remove(self, path1): + tmpdir = path1 + with pytest.raises(error.EEXIST): + tmpdir.mkdir("sampledir") + new = tmpdir.join("mktest1") + new.mkdir() + assert new.check(dir=1) + new.remove() + + new = tmpdir.mkdir("mktest") + assert new.check(dir=1) + new.remove() + assert tmpdir.join("mktest") == new + + def test_move_file(self, path1): + p = path1.join("samplefile") + newp = p.dirpath("moved_samplefile") + p.move(newp) + try: + assert newp.check(file=1) + assert not p.check() + finally: + dp = newp.dirpath() + if hasattr(dp, "revert"): + dp.revert() + else: + newp.move(p) + assert p.check() + + def test_move_dir(self, path1): + source = path1.join("sampledir") + dest = path1.join("moveddir") + source.move(dest) + assert dest.check(dir=1) + assert dest.join("otherfile").check(file=1) + assert not source.join("sampledir").check() + + def test_fspath_protocol_match_strpath(self, path1): + assert path1.__fspath__() == path1.strpath + + def test_fspath_func_match_strpath(self, path1): + from os import fspath + + assert fspath(path1) == path1.strpath + + @pytest.mark.skip("sys.version_info < (3,6)") + def test_fspath_open(self, path1): + f = path1.join("opentestfile") + open(f) + + @pytest.mark.skip("sys.version_info < (3,6)") + def test_fspath_fsencode(self, path1): + from os import fsencode + + assert fsencode(path1) == fsencode(path1.strpath) + + +def setuptestfs(path): + if path.join("samplefile").check(): + return + # print "setting up test fs for", repr(path) + samplefile = path.ensure("samplefile") + samplefile.write("samplefile\n") + + execfile = path.ensure("execfile") + execfile.write("x=42") + + execfilepy = path.ensure("execfile.py") + execfilepy.write("x=42") + + d = {1: 2, "hello": "world", "answer": 42} + path.ensure("samplepickle").dump(d) + + sampledir = path.ensure("sampledir", dir=1) + sampledir.ensure("otherfile") + + otherdir = path.ensure("otherdir", dir=1) + otherdir.ensure("__init__.py") + + module_a = otherdir.ensure("a.py") + module_a.write("from .b import stuff as result\n") + module_b = otherdir.ensure("b.py") + module_b.write('stuff="got it"\n') + module_c = otherdir.ensure("c.py") + module_c.write( + """import py; +import otherdir.a +value = otherdir.a.result +""" + ) + module_d = otherdir.ensure("d.py") + module_d.write( + """import py; +from otherdir import a +value2 = a.result +""" + ) + + +win32only = pytest.mark.skipif( + "not (sys.platform == 'win32' or getattr(os, '_name', None) == 'nt')" +) +skiponwin32 = pytest.mark.skipif( + "sys.platform == 'win32' or getattr(os, '_name', None) == 'nt'" +) + +ATIME_RESOLUTION = 0.01 + + +@pytest.fixture(scope="session") +def path1(tmpdir_factory): + path = tmpdir_factory.mktemp("path") + setuptestfs(path) + yield path + assert path.join("samplefile").check() + + +@pytest.fixture +def fake_fspath_obj(request): + class FakeFSPathClass: + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + return FakeFSPathClass(os.path.join("this", "is", "a", "fake", "path")) + + +def batch_make_numbered_dirs(rootdir, repeats): + for i in range(repeats): + dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir) + file_ = dir_.join("foo") + file_.write("%s" % i) + actual = int(file_.read()) + assert actual == i, f"int(file_.read()) is {actual} instead of {i}" + dir_.join(".lock").remove(ignore_errors=True) + return True + + +class TestLocalPath(CommonFSTests): + def test_join_normpath(self, tmpdir): + assert tmpdir.join(".") == tmpdir + p = tmpdir.join("../%s" % tmpdir.basename) + assert p == tmpdir + p = tmpdir.join("..//%s/" % tmpdir.basename) + assert p == tmpdir + + @skiponwin32 + def test_dirpath_abs_no_abs(self, tmpdir): + p = tmpdir.join("foo") + assert p.dirpath("/bar") == tmpdir.join("bar") + assert tmpdir.dirpath("/bar", abs=True) == local("/bar") + + def test_gethash(self, tmpdir): + from hashlib import md5 + from hashlib import sha1 as sha + + fn = tmpdir.join("testhashfile") + data = b"hello" + fn.write(data, mode="wb") + assert fn.computehash("md5") == md5(data).hexdigest() + assert fn.computehash("sha1") == sha(data).hexdigest() + with pytest.raises(ValueError): + fn.computehash("asdasd") + + def test_remove_removes_readonly_file(self, tmpdir): + readonly_file = tmpdir.join("readonly").ensure() + readonly_file.chmod(0) + readonly_file.remove() + assert not readonly_file.check(exists=1) + + def test_remove_removes_readonly_dir(self, tmpdir): + readonly_dir = tmpdir.join("readonlydir").ensure(dir=1) + readonly_dir.chmod(int("500", 8)) + readonly_dir.remove() + assert not readonly_dir.check(exists=1) + + def test_remove_removes_dir_and_readonly_file(self, tmpdir): + readonly_dir = tmpdir.join("readonlydir").ensure(dir=1) + readonly_file = readonly_dir.join("readonlyfile").ensure() + readonly_file.chmod(0) + readonly_dir.remove() + assert not readonly_dir.check(exists=1) + + def test_remove_routes_ignore_errors(self, tmpdir, monkeypatch): + lst = [] + monkeypatch.setattr("shutil.rmtree", lambda *args, **kwargs: lst.append(kwargs)) + tmpdir.remove() + assert not lst[0]["ignore_errors"] + for val in (True, False): + lst[:] = [] + tmpdir.remove(ignore_errors=val) + assert lst[0]["ignore_errors"] == val + + def test_initialize_curdir(self): + assert str(local()) == os.getcwd() + + @skiponwin32 + def test_chdir_gone(self, path1): + p = path1.ensure("dir_to_be_removed", dir=1) + p.chdir() + p.remove() + pytest.raises(error.ENOENT, local) + assert path1.chdir() is None + assert os.getcwd() == str(path1) + + with pytest.raises(error.ENOENT): + with p.as_cwd(): + raise NotImplementedError + + @skiponwin32 + def test_chdir_gone_in_as_cwd(self, path1): + p = path1.ensure("dir_to_be_removed", dir=1) + p.chdir() + p.remove() + + with path1.as_cwd() as old: + assert old is None + + def test_as_cwd(self, path1): + dir = path1.ensure("subdir", dir=1) + old = local() + with dir.as_cwd() as x: + assert x == old + assert local() == dir + assert os.getcwd() == str(old) + + def test_as_cwd_exception(self, path1): + old = local() + dir = path1.ensure("subdir", dir=1) + with pytest.raises(ValueError): + with dir.as_cwd(): + raise ValueError() + assert old == local() + + def test_initialize_reldir(self, path1): + with path1.as_cwd(): + p = local("samplefile") + assert p.check() + + def test_tilde_expansion(self, monkeypatch, tmpdir): + monkeypatch.setenv("HOME", str(tmpdir)) + p = local("~", expanduser=True) + assert p == os.path.expanduser("~") + + @pytest.mark.skipif( + not sys.platform.startswith("win32"), reason="case insensitive only on windows" + ) + def test_eq_hash_are_case_insensitive_on_windows(self): + a = local("/some/path") + b = local("/some/PATH") + assert a == b + assert hash(a) == hash(b) + assert a in {b} + assert a in {b: "b"} + + def test_eq_with_strings(self, path1): + path1 = path1.join("sampledir") + path2 = str(path1) + assert path1 == path2 + assert path2 == path1 + path3 = path1.join("samplefile") + assert path3 != path2 + assert path2 != path3 + + def test_eq_with_none(self, path1): + assert path1 != None # noqa: E711 + + def test_eq_non_ascii_unicode(self, path1): + path2 = path1.join("temp") + path3 = path1.join("ação") + path4 = path1.join("ディレクトリ") + + assert path2 != path3 + assert path2 != path4 + assert path4 != path3 + + def test_gt_with_strings(self, path1): + path2 = path1.join("sampledir") + path3 = str(path1.join("ttt")) + assert path3 > path2 + assert path2 < path3 + assert path2 < "ttt" + assert "ttt" > path2 + path4 = path1.join("aaa") + lst = [path2, path4, path3] + assert sorted(lst) == [path4, path2, path3] + + def test_open_and_ensure(self, path1): + p = path1.join("sub1", "sub2", "file") + with p.open("w", ensure=1) as f: + f.write("hello") + assert p.read() == "hello" + + def test_write_and_ensure(self, path1): + p = path1.join("sub1", "sub2", "file") + p.write("hello", ensure=1) + assert p.read() == "hello" + + @pytest.mark.parametrize("bin", (False, True)) + def test_dump(self, tmpdir, bin): + path = tmpdir.join("dumpfile%s" % int(bin)) + try: + d = {"answer": 42} + path.dump(d, bin=bin) + f = path.open("rb+") + import pickle + + dnew = pickle.load(f) + assert d == dnew + finally: + f.close() + + def test_setmtime(self): + import tempfile + import time + + try: + fd, name = tempfile.mkstemp() + os.close(fd) + except AttributeError: + name = tempfile.mktemp() + open(name, "w").close() + try: + mtime = int(time.time()) - 100 + path = local(name) + assert path.mtime() != mtime + path.setmtime(mtime) + assert path.mtime() == mtime + path.setmtime() + assert path.mtime() != mtime + finally: + os.remove(name) + + def test_normpath(self, path1): + new1 = path1.join("/otherdir") + new2 = path1.join("otherdir") + assert str(new1) == str(new2) + + def test_mkdtemp_creation(self): + d = local.mkdtemp() + try: + assert d.check(dir=1) + finally: + d.remove(rec=1) + + def test_tmproot(self): + d = local.mkdtemp() + tmproot = local.get_temproot() + try: + assert d.check(dir=1) + assert d.dirpath() == tmproot + finally: + d.remove(rec=1) + + def test_chdir(self, tmpdir): + old = local() + try: + res = tmpdir.chdir() + assert str(res) == str(old) + assert os.getcwd() == str(tmpdir) + finally: + old.chdir() + + def test_ensure_filepath_withdir(self, tmpdir): + newfile = tmpdir.join("test1", "test") + newfile.ensure() + assert newfile.check(file=1) + newfile.write("42") + newfile.ensure() + s = newfile.read() + assert s == "42" + + def test_ensure_filepath_withoutdir(self, tmpdir): + newfile = tmpdir.join("test1file") + t = newfile.ensure() + assert t == newfile + assert newfile.check(file=1) + + def test_ensure_dirpath(self, tmpdir): + newfile = tmpdir.join("test1", "testfile") + t = newfile.ensure(dir=1) + assert t == newfile + assert newfile.check(dir=1) + + def test_ensure_non_ascii_unicode(self, tmpdir): + newfile = tmpdir.join("ação", "ディレクトリ") + t = newfile.ensure(dir=1) + assert t == newfile + assert newfile.check(dir=1) + + @pytest.mark.xfail(run=False, reason="unreliable est for long filenames") + def test_long_filenames(self, tmpdir): + if sys.platform == "win32": + pytest.skip("win32: work around needed for path length limit") + # see http://codespeak.net/pipermail/py-dev/2008q2/000922.html + + # testing paths > 260 chars (which is Windows' limitation, but + # depending on how the paths are used), but > 4096 (which is the + # Linux' limitation) - the behaviour of paths with names > 4096 chars + # is undetermined + newfilename = "/test" * 60 + l1 = tmpdir.join(newfilename) + l1.ensure(file=True) + l1.write("foo") + l2 = tmpdir.join(newfilename) + assert l2.read() == "foo" + + def test_visit_depth_first(self, tmpdir): + tmpdir.ensure("a", "1") + tmpdir.ensure("b", "2") + p3 = tmpdir.ensure("breadth") + lst = list(tmpdir.visit(lambda x: x.check(file=1))) + assert len(lst) == 3 + # check that breadth comes last + assert lst[2] == p3 + + def test_visit_rec_fnmatch(self, tmpdir): + p1 = tmpdir.ensure("a", "123") + tmpdir.ensure(".b", "345") + lst = list(tmpdir.visit("???", rec="[!.]*")) + assert len(lst) == 1 + # check that breadth comes last + assert lst[0] == p1 + + def test_fnmatch_file_abspath(self, tmpdir): + b = tmpdir.join("a", "b") + assert b.fnmatch(os.sep.join("ab")) + pattern = os.sep.join([str(tmpdir), "*", "b"]) + assert b.fnmatch(pattern) + + def test_sysfind(self): + name = sys.platform == "win32" and "cmd" or "test" + x = local.sysfind(name) + assert x.check(file=1) + assert local.sysfind("jaksdkasldqwe") is None + assert local.sysfind(name, paths=[]) is None + x2 = local.sysfind(name, paths=[x.dirpath()]) + assert x2 == x + + def test_fspath_protocol_other_class(self, fake_fspath_obj): + # py.path is always absolute + py_path = local(fake_fspath_obj) + str_path = fake_fspath_obj.__fspath__() + assert py_path.check(endswith=str_path) + assert py_path.join(fake_fspath_obj).strpath == os.path.join( + py_path.strpath, str_path + ) + + def test_make_numbered_dir_multiprocess_safe(self, tmpdir): + # https://github.com/pytest-dev/py/issues/30 + with multiprocessing.Pool() as pool: + results = [ + pool.apply_async(batch_make_numbered_dirs, [tmpdir, 100]) + for _ in range(20) + ] + for r in results: + assert r.get() + + +class TestExecutionOnWindows: + pytestmark = win32only + + def test_sysfind_bat_exe_before(self, tmpdir, monkeypatch): + monkeypatch.setenv("PATH", str(tmpdir), prepend=os.pathsep) + tmpdir.ensure("hello") + h = tmpdir.ensure("hello.bat") + x = local.sysfind("hello") + assert x == h + + +class TestExecution: + pytestmark = skiponwin32 + + def test_sysfind_no_permisson_ignored(self, monkeypatch, tmpdir): + noperm = tmpdir.ensure("noperm", dir=True) + monkeypatch.setenv("PATH", str(noperm), prepend=":") + noperm.chmod(0) + try: + assert local.sysfind("jaksdkasldqwe") is None + finally: + noperm.chmod(0o644) + + def test_sysfind_absolute(self): + x = local.sysfind("test") + assert x.check(file=1) + y = local.sysfind(str(x)) + assert y.check(file=1) + assert y == x + + def test_sysfind_multiple(self, tmpdir, monkeypatch): + monkeypatch.setenv( + "PATH", "{}:{}".format(tmpdir.ensure("a"), tmpdir.join("b")), prepend=":" + ) + tmpdir.ensure("b", "a") + x = local.sysfind("a", checker=lambda x: x.dirpath().basename == "b") + assert x.basename == "a" + assert x.dirpath().basename == "b" + assert local.sysfind("a", checker=lambda x: None) is None + + def test_sysexec(self): + x = local.sysfind("ls") + out = x.sysexec("-a") + for x in local().listdir(): + assert out.find(x.basename) != -1 + + def test_sysexec_failing(self): + x = local.sysfind("false") + with pytest.raises(RuntimeError): + x.sysexec("aksjdkasjd") + + def test_make_numbered_dir(self, tmpdir): + tmpdir.ensure("base.not_an_int", dir=1) + for i in range(10): + numdir = local.make_numbered_dir( + prefix="base.", rootdir=tmpdir, keep=2, lock_timeout=0 + ) + assert numdir.check() + assert numdir.basename == "base.%d" % i + if i >= 1: + assert numdir.new(ext=str(i - 1)).check() + if i >= 2: + assert numdir.new(ext=str(i - 2)).check() + if i >= 3: + assert not numdir.new(ext=str(i - 3)).check() + + def test_make_numbered_dir_case(self, tmpdir): + """make_numbered_dir does not make assumptions on the underlying + filesystem based on the platform and will assume it _could_ be case + insensitive. + + See issues: + - https://github.com/pytest-dev/pytest/issues/708 + - https://github.com/pytest-dev/pytest/issues/3451 + """ + d1 = local.make_numbered_dir( + prefix="CAse.", + rootdir=tmpdir, + keep=2, + lock_timeout=0, + ) + d2 = local.make_numbered_dir( + prefix="caSE.", + rootdir=tmpdir, + keep=2, + lock_timeout=0, + ) + assert str(d1).lower() != str(d2).lower() + assert str(d2).endswith(".1") + + def test_make_numbered_dir_NotImplemented_Error(self, tmpdir, monkeypatch): + def notimpl(x, y): + raise NotImplementedError(42) + + monkeypatch.setattr(os, "symlink", notimpl) + x = tmpdir.make_numbered_dir(rootdir=tmpdir, lock_timeout=0) + assert x.relto(tmpdir) + assert x.check() + + def test_locked_make_numbered_dir(self, tmpdir): + for i in range(10): + numdir = local.make_numbered_dir(prefix="base2.", rootdir=tmpdir, keep=2) + assert numdir.check() + assert numdir.basename == "base2.%d" % i + for j in range(i): + assert numdir.new(ext=str(j)).check() + + def test_error_preservation(self, path1): + pytest.raises(EnvironmentError, path1.join("qwoeqiwe").mtime) + pytest.raises(EnvironmentError, path1.join("qwoeqiwe").read) + + # def test_parentdirmatch(self): + # local.parentdirmatch('std', startmodule=__name__) + # + + +class TestImport: + def test_pyimport(self, path1): + obj = path1.join("execfile.py").pyimport() + assert obj.x == 42 + assert obj.__name__ == "execfile" + + def test_pyimport_renamed_dir_creates_mismatch(self, tmpdir, monkeypatch): + p = tmpdir.ensure("a", "test_x123.py") + p.pyimport() + tmpdir.join("a").move(tmpdir.join("b")) + with pytest.raises(tmpdir.ImportMismatchError): + tmpdir.join("b", "test_x123.py").pyimport() + + # Errors can be ignored. + monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1") + tmpdir.join("b", "test_x123.py").pyimport() + + # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. + monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "0") + with pytest.raises(tmpdir.ImportMismatchError): + tmpdir.join("b", "test_x123.py").pyimport() + + def test_pyimport_messy_name(self, tmpdir): + # http://bitbucket.org/hpk42/py-trunk/issue/129 + path = tmpdir.ensure("foo__init__.py") + path.pyimport() + + def test_pyimport_dir(self, tmpdir): + p = tmpdir.join("hello_123") + p_init = p.ensure("__init__.py") + m = p.pyimport() + assert m.__name__ == "hello_123" + m = p_init.pyimport() + assert m.__name__ == "hello_123" + + def test_pyimport_execfile_different_name(self, path1): + obj = path1.join("execfile.py").pyimport(modname="0x.y.z") + assert obj.x == 42 + assert obj.__name__ == "0x.y.z" + + def test_pyimport_a(self, path1): + otherdir = path1.join("otherdir") + mod = otherdir.join("a.py").pyimport() + assert mod.result == "got it" + assert mod.__name__ == "otherdir.a" + + def test_pyimport_b(self, path1): + otherdir = path1.join("otherdir") + mod = otherdir.join("b.py").pyimport() + assert mod.stuff == "got it" + assert mod.__name__ == "otherdir.b" + + def test_pyimport_c(self, path1): + otherdir = path1.join("otherdir") + mod = otherdir.join("c.py").pyimport() + assert mod.value == "got it" + + def test_pyimport_d(self, path1): + otherdir = path1.join("otherdir") + mod = otherdir.join("d.py").pyimport() + assert mod.value2 == "got it" + + def test_pyimport_and_import(self, tmpdir): + tmpdir.ensure("xxxpackage", "__init__.py") + mod1path = tmpdir.ensure("xxxpackage", "module1.py") + mod1 = mod1path.pyimport() + assert mod1.__name__ == "xxxpackage.module1" + from xxxpackage import module1 + + assert module1 is mod1 + + def test_pyimport_check_filepath_consistency(self, monkeypatch, tmpdir): + name = "pointsback123" + ModuleType = type(os) + p = tmpdir.ensure(name + ".py") + for ending in (".pyc", "$py.class", ".pyo"): + mod = ModuleType(name) + pseudopath = tmpdir.ensure(name + ending) + mod.__file__ = str(pseudopath) + monkeypatch.setitem(sys.modules, name, mod) + newmod = p.pyimport() + assert mod == newmod + monkeypatch.undo() + mod = ModuleType(name) + pseudopath = tmpdir.ensure(name + "123.py") + mod.__file__ = str(pseudopath) + monkeypatch.setitem(sys.modules, name, mod) + excinfo = pytest.raises(pseudopath.ImportMismatchError, p.pyimport) + modname, modfile, orig = excinfo.value.args + assert modname == name + assert modfile == pseudopath + assert orig == p + assert issubclass(pseudopath.ImportMismatchError, ImportError) + + def test_issue131_pyimport_on__init__(self, tmpdir): + # __init__.py files may be namespace packages, and thus the + # __file__ of an imported module may not be ourselves + # see issue + p1 = tmpdir.ensure("proja", "__init__.py") + p2 = tmpdir.ensure("sub", "proja", "__init__.py") + m1 = p1.pyimport() + m2 = p2.pyimport() + assert m1 == m2 + + def test_ensuresyspath_append(self, tmpdir): + root1 = tmpdir.mkdir("root1") + file1 = root1.ensure("x123.py") + assert str(root1) not in sys.path + file1.pyimport(ensuresyspath="append") + assert str(root1) == sys.path[-1] + assert str(root1) not in sys.path[:-1] + + +class TestImportlibImport: + OPTS = {"ensuresyspath": "importlib"} + + def test_pyimport(self, path1): + obj = path1.join("execfile.py").pyimport(**self.OPTS) + assert obj.x == 42 + assert obj.__name__ == "execfile" + + def test_pyimport_dir_fails(self, tmpdir): + p = tmpdir.join("hello_123") + p.ensure("__init__.py") + with pytest.raises(ImportError): + p.pyimport(**self.OPTS) + + def test_pyimport_execfile_different_name(self, path1): + obj = path1.join("execfile.py").pyimport(modname="0x.y.z", **self.OPTS) + assert obj.x == 42 + assert obj.__name__ == "0x.y.z" + + def test_pyimport_relative_import_fails(self, path1): + otherdir = path1.join("otherdir") + with pytest.raises(ImportError): + otherdir.join("a.py").pyimport(**self.OPTS) + + def test_pyimport_doesnt_use_sys_modules(self, tmpdir): + p = tmpdir.ensure("file738jsk.py") + mod = p.pyimport(**self.OPTS) + assert mod.__name__ == "file738jsk" + assert "file738jsk" not in sys.modules + + +def test_pypkgdir(tmpdir): + pkg = tmpdir.ensure("pkg1", dir=1) + pkg.ensure("__init__.py") + pkg.ensure("subdir/__init__.py") + assert pkg.pypkgpath() == pkg + assert pkg.join("subdir", "__init__.py").pypkgpath() == pkg + + +def test_pypkgdir_unimportable(tmpdir): + pkg = tmpdir.ensure("pkg1-1", dir=1) # unimportable + pkg.ensure("__init__.py") + subdir = pkg.ensure("subdir/__init__.py").dirpath() + assert subdir.pypkgpath() == subdir + assert subdir.ensure("xyz.py").pypkgpath() == subdir + assert not pkg.pypkgpath() + + +def test_isimportable(): + from py.path import isimportable + + assert not isimportable("") + assert isimportable("x") + assert isimportable("x1") + assert isimportable("x_1") + assert isimportable("_") + assert isimportable("_1") + assert not isimportable("x-1") + assert not isimportable("x:1") + + +def test_homedir_from_HOME(monkeypatch): + path = os.getcwd() + monkeypatch.setenv("HOME", path) + assert local._gethomedir() == local(path) + + +def test_homedir_not_exists(monkeypatch): + monkeypatch.delenv("HOME", raising=False) + monkeypatch.delenv("HOMEDRIVE", raising=False) + homedir = local._gethomedir() + assert homedir is None + + +def test_samefile(tmpdir): + assert tmpdir.samefile(tmpdir) + p = tmpdir.ensure("hello") + assert p.samefile(p) + with p.dirpath().as_cwd(): + assert p.samefile(p.basename) + if sys.platform == "win32": + p1 = p.__class__(str(p).lower()) + p2 = p.__class__(str(p).upper()) + assert p1.samefile(p2) + + +@pytest.mark.skipif(not hasattr(os, "symlink"), reason="os.symlink not available") +def test_samefile_symlink(tmpdir): + p1 = tmpdir.ensure("foo.txt") + p2 = tmpdir.join("linked.txt") + try: + os.symlink(str(p1), str(p2)) + except (OSError, NotImplementedError) as e: + # on Windows this might fail if the user doesn't have special symlink permissions + # pypy3 on Windows doesn't implement os.symlink and raises NotImplementedError + pytest.skip(str(e.args[0])) + + assert p1.samefile(p2) + + +def test_listdir_single_arg(tmpdir): + tmpdir.ensure("hello") + assert tmpdir.listdir("hello")[0].basename == "hello" + + +def test_mkdtemp_rootdir(tmpdir): + dtmp = local.mkdtemp(rootdir=tmpdir) + assert tmpdir.listdir() == [dtmp] + + +class TestWINLocalPath: + pytestmark = win32only + + def test_owner_group_not_implemented(self, path1): + with pytest.raises(NotImplementedError): + path1.stat().owner + with pytest.raises(NotImplementedError): + path1.stat().group + + def test_chmod_simple_int(self, path1): + mode = path1.stat().mode + # Ensure that we actually change the mode to something different. + path1.chmod(mode == 0 and 1 or 0) + try: + print(path1.stat().mode) + print(mode) + assert path1.stat().mode != mode + finally: + path1.chmod(mode) + assert path1.stat().mode == mode + + def test_path_comparison_lowercase_mixed(self, path1): + t1 = path1.join("a_path") + t2 = path1.join("A_path") + assert t1 == t1 + assert t1 == t2 + + def test_relto_with_mixed_case(self, path1): + t1 = path1.join("a_path", "fiLe") + t2 = path1.join("A_path") + assert t1.relto(t2) == "fiLe" + + def test_allow_unix_style_paths(self, path1): + t1 = path1.join("a_path") + assert t1 == str(path1) + "\\a_path" + t1 = path1.join("a_path/") + assert t1 == str(path1) + "\\a_path" + t1 = path1.join("dir/a_path") + assert t1 == str(path1) + "\\dir\\a_path" + + def test_sysfind_in_currentdir(self, path1): + cmd = local.sysfind("cmd") + root = cmd.new(dirname="", basename="") # c:\ in most installations + with root.as_cwd(): + x = local.sysfind(cmd.relto(root)) + assert x.check(file=1) + + def test_fnmatch_file_abspath_posix_pattern_on_win32(self, tmpdir): + # path-matching patterns might contain a posix path separator '/' + # Test that we can match that pattern on windows. + import posixpath + + b = tmpdir.join("a", "b") + assert b.fnmatch(posixpath.sep.join("ab")) + pattern = posixpath.sep.join([str(tmpdir), "*", "b"]) + assert b.fnmatch(pattern) + + +class TestPOSIXLocalPath: + pytestmark = skiponwin32 + + def test_hardlink(self, tmpdir): + linkpath = tmpdir.join("test") + filepath = tmpdir.join("file") + filepath.write("Hello") + nlink = filepath.stat().nlink + linkpath.mklinkto(filepath) + assert filepath.stat().nlink == nlink + 1 + + def test_symlink_are_identical(self, tmpdir): + filepath = tmpdir.join("file") + filepath.write("Hello") + linkpath = tmpdir.join("test") + linkpath.mksymlinkto(filepath) + assert linkpath.readlink() == str(filepath) + + def test_symlink_isfile(self, tmpdir): + linkpath = tmpdir.join("test") + filepath = tmpdir.join("file") + filepath.write("") + linkpath.mksymlinkto(filepath) + assert linkpath.check(file=1) + assert not linkpath.check(link=0, file=1) + assert linkpath.islink() + + def test_symlink_relative(self, tmpdir): + linkpath = tmpdir.join("test") + filepath = tmpdir.join("file") + filepath.write("Hello") + linkpath.mksymlinkto(filepath, absolute=False) + assert linkpath.readlink() == "file" + assert filepath.read() == linkpath.read() + + def test_symlink_not_existing(self, tmpdir): + linkpath = tmpdir.join("testnotexisting") + assert not linkpath.check(link=1) + assert linkpath.check(link=0) + + def test_relto_with_root(self, path1, tmpdir): + y = path1.join("x").relto(local("/")) + assert y[0] == str(path1)[1] + + def test_visit_recursive_symlink(self, tmpdir): + linkpath = tmpdir.join("test") + linkpath.mksymlinkto(tmpdir) + visitor = tmpdir.visit(None, lambda x: x.check(link=0)) + assert list(visitor) == [linkpath] + + def test_symlink_isdir(self, tmpdir): + linkpath = tmpdir.join("test") + linkpath.mksymlinkto(tmpdir) + assert linkpath.check(dir=1) + assert not linkpath.check(link=0, dir=1) + + def test_symlink_remove(self, tmpdir): + linkpath = tmpdir.join("test") + linkpath.mksymlinkto(linkpath) # point to itself + assert linkpath.check(link=1) + linkpath.remove() + assert not linkpath.check() + + def test_realpath_file(self, tmpdir): + linkpath = tmpdir.join("test") + filepath = tmpdir.join("file") + filepath.write("") + linkpath.mksymlinkto(filepath) + realpath = linkpath.realpath() + assert realpath.basename == "file" + + def test_owner(self, path1, tmpdir): + from pwd import getpwuid + from grp import getgrgid + + stat = path1.stat() + assert stat.path == path1 + + uid = stat.uid + gid = stat.gid + owner = getpwuid(uid)[0] + group = getgrgid(gid)[0] + + assert uid == stat.uid + assert owner == stat.owner + assert gid == stat.gid + assert group == stat.group + + def test_stat_helpers(self, tmpdir, monkeypatch): + path1 = tmpdir.ensure("file") + stat1 = path1.stat() + stat2 = tmpdir.stat() + assert stat1.isfile() + assert stat2.isdir() + assert not stat1.islink() + assert not stat2.islink() + + def test_stat_non_raising(self, tmpdir): + path1 = tmpdir.join("file") + pytest.raises(error.ENOENT, lambda: path1.stat()) + res = path1.stat(raising=False) + assert res is None + + def test_atime(self, tmpdir): + import time + + path = tmpdir.ensure("samplefile") + now = time.time() + atime1 = path.atime() + # we could wait here but timer resolution is very + # system dependent + path.read() + time.sleep(ATIME_RESOLUTION) + atime2 = path.atime() + time.sleep(ATIME_RESOLUTION) + duration = time.time() - now + assert (atime2 - atime1) <= duration + + def test_commondir(self, path1): + # XXX This is here in local until we find a way to implement this + # using the subversion command line api. + p1 = path1.join("something") + p2 = path1.join("otherthing") + assert p1.common(p2) == path1 + assert p2.common(p1) == path1 + + def test_commondir_nocommon(self, path1): + # XXX This is here in local until we find a way to implement this + # using the subversion command line api. + p1 = path1.join("something") + p2 = local(path1.sep + "blabla") + assert p1.common(p2) == "/" + + def test_join_to_root(self, path1): + root = path1.parts()[0] + assert len(str(root)) == 1 + assert str(root.join("a")) == "/a" + + def test_join_root_to_root_with_no_abs(self, path1): + nroot = path1.join("/") + assert str(path1) == str(nroot) + assert path1 == nroot + + def test_chmod_simple_int(self, path1): + mode = path1.stat().mode + path1.chmod(int(mode / 2)) + try: + assert path1.stat().mode != mode + finally: + path1.chmod(mode) + assert path1.stat().mode == mode + + def test_chmod_rec_int(self, path1): + # XXX fragile test + def recfilter(x): + return x.check(dotfile=0, link=0) + + oldmodes = {} + for x in path1.visit(rec=recfilter): + oldmodes[x] = x.stat().mode + path1.chmod(int("772", 8), rec=recfilter) + try: + for x in path1.visit(rec=recfilter): + assert x.stat().mode & int("777", 8) == int("772", 8) + finally: + for x, y in oldmodes.items(): + x.chmod(y) + + def test_copy_archiving(self, tmpdir): + unicode_fn = "something-\342\200\223.txt" + f = tmpdir.ensure("a", unicode_fn) + a = f.dirpath() + oldmode = f.stat().mode + newmode = oldmode ^ 1 + f.chmod(newmode) + b = tmpdir.join("b") + a.copy(b, mode=True) + assert b.join(f.basename).stat().mode == newmode + + def test_copy_stat_file(self, tmpdir): + src = tmpdir.ensure("src") + dst = tmpdir.join("dst") + # a small delay before the copy + time.sleep(ATIME_RESOLUTION) + src.copy(dst, stat=True) + oldstat = src.stat() + newstat = dst.stat() + assert oldstat.mode == newstat.mode + assert (dst.atime() - src.atime()) < ATIME_RESOLUTION + assert (dst.mtime() - src.mtime()) < ATIME_RESOLUTION + + def test_copy_stat_dir(self, tmpdir): + test_files = ["a", "b", "c"] + src = tmpdir.join("src") + for f in test_files: + src.join(f).write(f, ensure=True) + dst = tmpdir.join("dst") + # a small delay before the copy + time.sleep(ATIME_RESOLUTION) + src.copy(dst, stat=True) + for f in test_files: + oldstat = src.join(f).stat() + newstat = dst.join(f).stat() + assert (newstat.atime - oldstat.atime) < ATIME_RESOLUTION + assert (newstat.mtime - oldstat.mtime) < ATIME_RESOLUTION + assert oldstat.mode == newstat.mode + + def test_chown_identity(self, path1): + owner = path1.stat().owner + group = path1.stat().group + path1.chown(owner, group) + + def test_chown_dangling_link(self, path1): + owner = path1.stat().owner + group = path1.stat().group + x = path1.join("hello") + x.mksymlinkto("qlwkejqwlek") + try: + path1.chown(owner, group, rec=1) + finally: + x.remove(rec=0) + + def test_chown_identity_rec_mayfail(self, path1): + owner = path1.stat().owner + group = path1.stat().group + path1.chown(owner, group) + + +class TestUnicodePy2Py3: + def test_join_ensure(self, tmpdir, monkeypatch): + if sys.version_info >= (3, 0) and "LANG" not in os.environ: + pytest.skip("cannot run test without locale") + x = local(tmpdir.strpath) + part = "hällo" + y = x.ensure(part) + assert x.join(part) == y + + def test_listdir(self, tmpdir): + if sys.version_info >= (3, 0) and "LANG" not in os.environ: + pytest.skip("cannot run test without locale") + x = local(tmpdir.strpath) + part = "hällo" + y = x.ensure(part) + assert x.listdir(part)[0] == y + + @pytest.mark.xfail(reason="changing read/write might break existing usages") + def test_read_write(self, tmpdir): + x = tmpdir.join("hello") + part = "hällo" + x.write(part) + assert x.read() == part + x.write(part.encode(sys.getdefaultencoding())) + assert x.read() == part.encode(sys.getdefaultencoding()) + + +class TestBinaryAndTextMethods: + def test_read_binwrite(self, tmpdir): + x = tmpdir.join("hello") + part = "hällo" + part_utf8 = part.encode("utf8") + x.write_binary(part_utf8) + assert x.read_binary() == part_utf8 + s = x.read_text(encoding="utf8") + assert s == part + assert isinstance(s, str) + + def test_read_textwrite(self, tmpdir): + x = tmpdir.join("hello") + part = "hällo" + part_utf8 = part.encode("utf8") + x.write_text(part, encoding="utf8") + assert x.read_binary() == part_utf8 + assert x.read_text(encoding="utf8") == part + + def test_default_encoding(self, tmpdir): + x = tmpdir.join("hello") + # Can't use UTF8 as the default encoding (ASCII) doesn't support it + part = "hello" + x.write_text(part, "ascii") + s = x.read_text("ascii") + assert s == part + assert type(s) == type(part) From dc0cb0d149c03767b762c8e80ed2a1337aaf6161 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 22:07:53 -0400 Subject: [PATCH 17/20] fix test pollution of sys.modules --- testing/_py/test_local.py | 7 +++++++ testing/test_pathlib.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index 6e2c44bc2e0..ceacd70ae2e 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -2,6 +2,7 @@ import os import sys import time +from unittest import mock import pytest from py import error @@ -978,6 +979,12 @@ def test_error_preservation(self, path1): class TestImport: + @pytest.fixture(autouse=True) + def preserve_sys(self): + with mock.patch.dict(sys.modules): + with mock.patch.object(sys, "path", list(sys.path)): + yield + def test_pyimport(self, path1): obj = path1.join("execfile.py").pyimport() assert obj.x == 42 diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index c901dc6f435..577c7749fd9 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -91,6 +91,12 @@ def path1(self, tmp_path_factory: TempPathFactory) -> Generator[Path, None, None yield path assert path.joinpath("samplefile").exists() + @pytest.fixture(autouse=True) + def preserve_sys(self): + with unittest.mock.patch.dict(sys.modules): + with unittest.mock.patch.object(sys, "path", list(sys.path)): + yield + def setuptestfs(self, path: Path) -> None: # print "setting up test fs for", repr(path) samplefile = path / "samplefile" From 02a9371259f651f17f38fc9f406eb43a0fcf2e2d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 19 Oct 2022 22:28:51 -0400 Subject: [PATCH 18/20] adjust tests if py library is installed --- testing/_py/test_local.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index ceacd70ae2e..31c10b16021 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -909,8 +909,12 @@ def test_sysexec(self): assert out.find(x.basename) != -1 def test_sysexec_failing(self): + try: + from py._process.cmdexec import ExecutionFailed # py library + except ImportError: + ExecutionFailed = RuntimeError # py vendored x = local.sysfind("false") - with pytest.raises(RuntimeError): + with pytest.raises(ExecutionFailed): x.sysexec("aksjdkasjd") def test_make_numbered_dir(self, tmpdir): @@ -1146,7 +1150,10 @@ def test_pypkgdir_unimportable(tmpdir): def test_isimportable(): - from py.path import isimportable + try: + from py.path import isimportable # py vendored version + except ImportError: + from py._path.local import isimportable # py library assert not isimportable("") assert isimportable("x") From 508be0b2bfdafeff31ed33e3d3403aff1252a87f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 20 Oct 2022 17:15:57 -0400 Subject: [PATCH 19/20] add -pylib tox environment --- .github/workflows/test.yml | 4 ++-- tox.ini | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f3e7e535eb..b33c88b9f67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,7 +66,7 @@ jobs: - name: "windows-py37-pluggy" python: "3.7" os: windows-latest - tox_env: "py37-pluggymain-xdist" + tox_env: "py37-pluggymain-pylib-xdist" - name: "windows-py38" python: "3.8" os: windows-latest @@ -93,7 +93,7 @@ jobs: - name: "ubuntu-py37-pluggy" python: "3.7" os: ubuntu-latest - tox_env: "py37-pluggymain-xdist" + tox_env: "py37-pluggymain-pylib-xdist" - name: "ubuntu-py37-freeze" python: "3.7" os: ubuntu-latest diff --git a/tox.ini b/tox.ini index f1ab4b815a5..f04242c5c43 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ envlist = py310 py311 pypy3 - py37-{pexpect,xdist,unittestextras,numpy,pluggymain} + py37-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib} doctesting plugins py37-freeze @@ -54,6 +54,7 @@ deps = numpy: numpy>=1.19.4 pexpect: pexpect>=4.8.0 pluggymain: pluggy @ git+https://github.com/pytest-dev/pluggy.git + pylib: py>=1.8.2 unittestextras: twisted unittestextras: asynctest xdist: pytest-xdist>=2.1.0 From d543a45a6802defbafcff259bdc235af76f7af3a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 21 Oct 2022 12:46:15 -0400 Subject: [PATCH 20/20] add deprecation changelog for py library vendoring --- changelog/10396.deprecation.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/10396.deprecation.rst diff --git a/changelog/10396.deprecation.rst b/changelog/10396.deprecation.rst new file mode 100644 index 00000000000..84461e82ede --- /dev/null +++ b/changelog/10396.deprecation.rst @@ -0,0 +1 @@ +pytest no longer depends on the ``py`` library. ``pytest`` provides a vendored copy of ``py.error`` and ``py.path`` modules but will use the ``py`` library if it is installed. If you need other ``py.*`` modules, continue to install the deprecated ``py`` library separately, otherwise it can usually be removed as a dependency.