Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/_argcomplete.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/_version.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/capture.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/compat.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/debugging.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/deprecated.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/faulthandler.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/fixtures.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/freeze_support.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/helpconfig.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/hookspec.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/junitxml.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/legacypath.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/logging.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/main.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/monkeypatch.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/nodes.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/outcomes.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/pastebin.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/pathlib.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/pytester.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/pytester_assertions.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/python.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/python_api.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/python_path.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/recwarn.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/reports.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/runner.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/scope.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/setuponly.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/setupplan.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/skipping.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/stash.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/stepwise.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/terminal.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/threadexception.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/timing.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/tmpdir.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/unittest.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/warning_types.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/__pycache__/warnings.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/_py/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/_pytest/_py/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/_py/__pycache__/error.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/_py/__pycache__/path.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/_pytest/_py/error.py +111 -0
- .venv/lib/python3.11/site-packages/_pytest/_py/path.py +1475 -0
- .venv/lib/python3.11/site-packages/_pytest/config/__init__.py +1973 -0
.gitattributes
CHANGED
|
@@ -183,3 +183,4 @@ tuning-competition-baseline/.venv/lib/python3.11/site-packages/torch/_inductor/_
|
|
| 183 |
.venv/lib/python3.11/site-packages/ray/thirdparty_files/psutil/_psutil_linux.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 184 |
.venv/lib/python3.11/site-packages/ray/core/src/ray/gcs/gcs_server filter=lfs diff=lfs merge=lfs -text
|
| 185 |
.venv/lib/python3.11/site-packages/ray/thirdparty_files/psutil/__pycache__/_pslinux.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 183 |
.venv/lib/python3.11/site-packages/ray/thirdparty_files/psutil/_psutil_linux.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 184 |
.venv/lib/python3.11/site-packages/ray/core/src/ray/gcs/gcs_server filter=lfs diff=lfs merge=lfs -text
|
| 185 |
.venv/lib/python3.11/site-packages/ray/thirdparty_files/psutil/__pycache__/_pslinux.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 186 |
+
.venv/lib/python3.11/site-packages/ray/scripts/__pycache__/scripts.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (540 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/_argcomplete.cpython-311.pyc
ADDED
|
Binary file (5.28 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/_version.cpython-311.pyc
ADDED
|
Binary file (638 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/capture.cpython-311.pyc
ADDED
|
Binary file (58.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/compat.cpython-311.pyc
ADDED
|
Binary file (14 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/debugging.cpython-311.pyc
ADDED
|
Binary file (19.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/deprecated.cpython-311.pyc
ADDED
|
Binary file (2.81 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/faulthandler.cpython-311.pyc
ADDED
|
Binary file (4.96 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/fixtures.cpython-311.pyc
ADDED
|
Binary file (85 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/freeze_support.cpython-311.pyc
ADDED
|
Binary file (1.99 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/helpconfig.cpython-311.pyc
ADDED
|
Binary file (13.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/hookspec.cpython-311.pyc
ADDED
|
Binary file (46.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/junitxml.cpython-311.pyc
ADDED
|
Binary file (36.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/legacypath.cpython-311.pyc
ADDED
|
Binary file (26.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/logging.cpython-311.pyc
ADDED
|
Binary file (51.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (47 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/monkeypatch.cpython-311.pyc
ADDED
|
Binary file (18 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/nodes.cpython-311.pyc
ADDED
|
Binary file (33.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/outcomes.cpython-311.pyc
ADDED
|
Binary file (13.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/pastebin.cpython-311.pyc
ADDED
|
Binary file (6.28 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/pathlib.cpython-311.pyc
ADDED
|
Binary file (45.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/pytester.cpython-311.pyc
ADDED
|
Binary file (94 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/pytester_assertions.cpython-311.pyc
ADDED
|
Binary file (2.63 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/python.cpython-311.pyc
ADDED
|
Binary file (76.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/python_api.cpython-311.pyc
ADDED
|
Binary file (49.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/python_path.cpython-311.pyc
ADDED
|
Binary file (1.85 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/recwarn.cpython-311.pyc
ADDED
|
Binary file (17.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/reports.cpython-311.pyc
ADDED
|
Binary file (27.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/runner.cpython-311.pyc
ADDED
|
Binary file (25.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/scope.cpython-311.pyc
ADDED
|
Binary file (4.32 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/setuponly.cpython-311.pyc
ADDED
|
Binary file (5.81 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/setupplan.cpython-311.pyc
ADDED
|
Binary file (2.05 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/skipping.cpython-311.pyc
ADDED
|
Binary file (14.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/stash.cpython-311.pyc
ADDED
|
Binary file (4.81 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/stepwise.cpython-311.pyc
ADDED
|
Binary file (5.88 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/terminal.cpython-311.pyc
ADDED
|
Binary file (81.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/threadexception.cpython-311.pyc
ADDED
|
Binary file (5.38 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/timing.cpython-311.pyc
ADDED
|
Binary file (696 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/tmpdir.cpython-311.pyc
ADDED
|
Binary file (14 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/unittest.cpython-311.pyc
ADDED
|
Binary file (21.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/warning_types.cpython-311.pyc
ADDED
|
Binary file (7.86 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/__pycache__/warnings.cpython-311.pyc
ADDED
|
Binary file (7.61 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/_py/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/_pytest/_py/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (184 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/_py/__pycache__/error.cpython-311.pyc
ADDED
|
Binary file (5.19 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/_py/__pycache__/path.cpython-311.pyc
ADDED
|
Binary file (76.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/_pytest/_py/error.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""create errno-specific classes for IO or os calls."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import errno
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from typing import Callable
|
| 9 |
+
from typing import TYPE_CHECKING
|
| 10 |
+
from typing import TypeVar
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
if TYPE_CHECKING:
|
| 14 |
+
from typing_extensions import ParamSpec
|
| 15 |
+
|
| 16 |
+
P = ParamSpec("P")
|
| 17 |
+
|
| 18 |
+
R = TypeVar("R")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class Error(EnvironmentError):
|
| 22 |
+
def __repr__(self) -> str:
|
| 23 |
+
return "{}.{} {!r}: {} ".format(
|
| 24 |
+
self.__class__.__module__,
|
| 25 |
+
self.__class__.__name__,
|
| 26 |
+
self.__class__.__doc__,
|
| 27 |
+
" ".join(map(str, self.args)),
|
| 28 |
+
# repr(self.args)
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
def __str__(self) -> str:
|
| 32 |
+
s = "[{}]: {}".format(
|
| 33 |
+
self.__class__.__doc__,
|
| 34 |
+
" ".join(map(str, self.args)),
|
| 35 |
+
)
|
| 36 |
+
return s
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
_winerrnomap = {
|
| 40 |
+
2: errno.ENOENT,
|
| 41 |
+
3: errno.ENOENT,
|
| 42 |
+
17: errno.EEXIST,
|
| 43 |
+
18: errno.EXDEV,
|
| 44 |
+
13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable
|
| 45 |
+
22: errno.ENOTDIR,
|
| 46 |
+
20: errno.ENOTDIR,
|
| 47 |
+
267: errno.ENOTDIR,
|
| 48 |
+
5: errno.EACCES, # anything better?
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class ErrorMaker:
|
| 53 |
+
"""lazily provides Exception classes for each possible POSIX errno
|
| 54 |
+
(as defined per the 'errno' module). All such instances
|
| 55 |
+
subclass EnvironmentError.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
_errno2class: dict[int, type[Error]] = {}
|
| 59 |
+
|
| 60 |
+
def __getattr__(self, name: str) -> type[Error]:
|
| 61 |
+
if name[0] == "_":
|
| 62 |
+
raise AttributeError(name)
|
| 63 |
+
eno = getattr(errno, name)
|
| 64 |
+
cls = self._geterrnoclass(eno)
|
| 65 |
+
setattr(self, name, cls)
|
| 66 |
+
return cls
|
| 67 |
+
|
| 68 |
+
def _geterrnoclass(self, eno: int) -> type[Error]:
|
| 69 |
+
try:
|
| 70 |
+
return self._errno2class[eno]
|
| 71 |
+
except KeyError:
|
| 72 |
+
clsname = errno.errorcode.get(eno, "UnknownErrno%d" % (eno,))
|
| 73 |
+
errorcls = type(
|
| 74 |
+
clsname,
|
| 75 |
+
(Error,),
|
| 76 |
+
{"__module__": "py.error", "__doc__": os.strerror(eno)},
|
| 77 |
+
)
|
| 78 |
+
self._errno2class[eno] = errorcls
|
| 79 |
+
return errorcls
|
| 80 |
+
|
| 81 |
+
def checked_call(
|
| 82 |
+
self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
|
| 83 |
+
) -> R:
|
| 84 |
+
"""Call a function and raise an errno-exception if applicable."""
|
| 85 |
+
__tracebackhide__ = True
|
| 86 |
+
try:
|
| 87 |
+
return func(*args, **kwargs)
|
| 88 |
+
except Error:
|
| 89 |
+
raise
|
| 90 |
+
except OSError as value:
|
| 91 |
+
if not hasattr(value, "errno"):
|
| 92 |
+
raise
|
| 93 |
+
errno = value.errno
|
| 94 |
+
if sys.platform == "win32":
|
| 95 |
+
try:
|
| 96 |
+
cls = self._geterrnoclass(_winerrnomap[errno])
|
| 97 |
+
except KeyError:
|
| 98 |
+
raise value
|
| 99 |
+
else:
|
| 100 |
+
# we are not on Windows, or we got a proper OSError
|
| 101 |
+
cls = self._geterrnoclass(errno)
|
| 102 |
+
|
| 103 |
+
raise cls(f"{func.__name__}{args!r}")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
_error_maker = ErrorMaker()
|
| 107 |
+
checked_call = _error_maker.checked_call
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def __getattr__(attr: str) -> type[Error]:
|
| 111 |
+
return getattr(_error_maker, attr) # type: ignore[no-any-return]
|
.venv/lib/python3.11/site-packages/_pytest/_py/path.py
ADDED
|
@@ -0,0 +1,1475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
"""local path implementation."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import atexit
|
| 7 |
+
from contextlib import contextmanager
|
| 8 |
+
import fnmatch
|
| 9 |
+
import importlib.util
|
| 10 |
+
import io
|
| 11 |
+
import os
|
| 12 |
+
from os.path import abspath
|
| 13 |
+
from os.path import dirname
|
| 14 |
+
from os.path import exists
|
| 15 |
+
from os.path import isabs
|
| 16 |
+
from os.path import isdir
|
| 17 |
+
from os.path import isfile
|
| 18 |
+
from os.path import islink
|
| 19 |
+
from os.path import normpath
|
| 20 |
+
import posixpath
|
| 21 |
+
from stat import S_ISDIR
|
| 22 |
+
from stat import S_ISLNK
|
| 23 |
+
from stat import S_ISREG
|
| 24 |
+
import sys
|
| 25 |
+
from typing import Any
|
| 26 |
+
from typing import Callable
|
| 27 |
+
from typing import cast
|
| 28 |
+
from typing import Literal
|
| 29 |
+
from typing import overload
|
| 30 |
+
from typing import TYPE_CHECKING
|
| 31 |
+
import uuid
|
| 32 |
+
import warnings
|
| 33 |
+
|
| 34 |
+
from . import error
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# Moved from local.py.
|
| 38 |
+
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Checkers:
|
| 42 |
+
_depend_on_existence = "exists", "link", "dir", "file"
|
| 43 |
+
|
| 44 |
+
def __init__(self, path):
|
| 45 |
+
self.path = path
|
| 46 |
+
|
| 47 |
+
def dotfile(self):
|
| 48 |
+
return self.path.basename.startswith(".")
|
| 49 |
+
|
| 50 |
+
def ext(self, arg):
|
| 51 |
+
if not arg.startswith("."):
|
| 52 |
+
arg = "." + arg
|
| 53 |
+
return self.path.ext == arg
|
| 54 |
+
|
| 55 |
+
def basename(self, arg):
|
| 56 |
+
return self.path.basename == arg
|
| 57 |
+
|
| 58 |
+
def basestarts(self, arg):
|
| 59 |
+
return self.path.basename.startswith(arg)
|
| 60 |
+
|
| 61 |
+
def relto(self, arg):
|
| 62 |
+
return self.path.relto(arg)
|
| 63 |
+
|
| 64 |
+
def fnmatch(self, arg):
|
| 65 |
+
return self.path.fnmatch(arg)
|
| 66 |
+
|
| 67 |
+
def endswith(self, arg):
|
| 68 |
+
return str(self.path).endswith(arg)
|
| 69 |
+
|
| 70 |
+
def _evaluate(self, kw):
|
| 71 |
+
from .._code.source import getrawcode
|
| 72 |
+
|
| 73 |
+
for name, value in kw.items():
|
| 74 |
+
invert = False
|
| 75 |
+
meth = None
|
| 76 |
+
try:
|
| 77 |
+
meth = getattr(self, name)
|
| 78 |
+
except AttributeError:
|
| 79 |
+
if name[:3] == "not":
|
| 80 |
+
invert = True
|
| 81 |
+
try:
|
| 82 |
+
meth = getattr(self, name[3:])
|
| 83 |
+
except AttributeError:
|
| 84 |
+
pass
|
| 85 |
+
if meth is None:
|
| 86 |
+
raise TypeError(f"no {name!r} checker available for {self.path!r}")
|
| 87 |
+
try:
|
| 88 |
+
if getrawcode(meth).co_argcount > 1:
|
| 89 |
+
if (not meth(value)) ^ invert:
|
| 90 |
+
return False
|
| 91 |
+
else:
|
| 92 |
+
if bool(value) ^ bool(meth()) ^ invert:
|
| 93 |
+
return False
|
| 94 |
+
except (error.ENOENT, error.ENOTDIR, error.EBUSY):
|
| 95 |
+
# EBUSY feels not entirely correct,
|
| 96 |
+
# but its kind of necessary since ENOMEDIUM
|
| 97 |
+
# is not accessible in python
|
| 98 |
+
for name in self._depend_on_existence:
|
| 99 |
+
if name in kw:
|
| 100 |
+
if kw.get(name):
|
| 101 |
+
return False
|
| 102 |
+
name = "not" + name
|
| 103 |
+
if name in kw:
|
| 104 |
+
if not kw.get(name):
|
| 105 |
+
return False
|
| 106 |
+
return True
|
| 107 |
+
|
| 108 |
+
_statcache: Stat
|
| 109 |
+
|
| 110 |
+
def _stat(self) -> Stat:
|
| 111 |
+
try:
|
| 112 |
+
return self._statcache
|
| 113 |
+
except AttributeError:
|
| 114 |
+
try:
|
| 115 |
+
self._statcache = self.path.stat()
|
| 116 |
+
except error.ELOOP:
|
| 117 |
+
self._statcache = self.path.lstat()
|
| 118 |
+
return self._statcache
|
| 119 |
+
|
| 120 |
+
def dir(self):
|
| 121 |
+
return S_ISDIR(self._stat().mode)
|
| 122 |
+
|
| 123 |
+
def file(self):
|
| 124 |
+
return S_ISREG(self._stat().mode)
|
| 125 |
+
|
| 126 |
+
def exists(self):
|
| 127 |
+
return self._stat()
|
| 128 |
+
|
| 129 |
+
def link(self):
|
| 130 |
+
st = self.path.lstat()
|
| 131 |
+
return S_ISLNK(st.mode)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class NeverRaised(Exception):
|
| 135 |
+
pass
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
class Visitor:
|
| 139 |
+
def __init__(self, fil, rec, ignore, bf, sort):
|
| 140 |
+
if isinstance(fil, str):
|
| 141 |
+
fil = FNMatcher(fil)
|
| 142 |
+
if isinstance(rec, str):
|
| 143 |
+
self.rec: Callable[[LocalPath], bool] = FNMatcher(rec)
|
| 144 |
+
elif not hasattr(rec, "__call__") and rec:
|
| 145 |
+
self.rec = lambda path: True
|
| 146 |
+
else:
|
| 147 |
+
self.rec = rec
|
| 148 |
+
self.fil = fil
|
| 149 |
+
self.ignore = ignore
|
| 150 |
+
self.breadthfirst = bf
|
| 151 |
+
self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
|
| 152 |
+
|
| 153 |
+
def gen(self, path):
|
| 154 |
+
try:
|
| 155 |
+
entries = path.listdir()
|
| 156 |
+
except self.ignore:
|
| 157 |
+
return
|
| 158 |
+
rec = self.rec
|
| 159 |
+
dirs = self.optsort(
|
| 160 |
+
[p for p in entries if p.check(dir=1) and (rec is None or rec(p))]
|
| 161 |
+
)
|
| 162 |
+
if not self.breadthfirst:
|
| 163 |
+
for subdir in dirs:
|
| 164 |
+
yield from self.gen(subdir)
|
| 165 |
+
for p in self.optsort(entries):
|
| 166 |
+
if self.fil is None or self.fil(p):
|
| 167 |
+
yield p
|
| 168 |
+
if self.breadthfirst:
|
| 169 |
+
for subdir in dirs:
|
| 170 |
+
yield from self.gen(subdir)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class FNMatcher:
|
| 174 |
+
def __init__(self, pattern):
|
| 175 |
+
self.pattern = pattern
|
| 176 |
+
|
| 177 |
+
def __call__(self, path):
|
| 178 |
+
pattern = self.pattern
|
| 179 |
+
|
| 180 |
+
if (
|
| 181 |
+
pattern.find(path.sep) == -1
|
| 182 |
+
and iswin32
|
| 183 |
+
and pattern.find(posixpath.sep) != -1
|
| 184 |
+
):
|
| 185 |
+
# Running on Windows, the pattern has no Windows path separators,
|
| 186 |
+
# and the pattern has one or more Posix path separators. Replace
|
| 187 |
+
# the Posix path separators with the Windows path separator.
|
| 188 |
+
pattern = pattern.replace(posixpath.sep, path.sep)
|
| 189 |
+
|
| 190 |
+
if pattern.find(path.sep) == -1:
|
| 191 |
+
name = path.basename
|
| 192 |
+
else:
|
| 193 |
+
name = str(path) # path.strpath # XXX svn?
|
| 194 |
+
if not os.path.isabs(pattern):
|
| 195 |
+
pattern = "*" + path.sep + pattern
|
| 196 |
+
return fnmatch.fnmatch(name, pattern)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def map_as_list(func, iter):
|
| 200 |
+
return list(map(func, iter))
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class Stat:
|
| 204 |
+
if TYPE_CHECKING:
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def size(self) -> int: ...
|
| 208 |
+
|
| 209 |
+
@property
|
| 210 |
+
def mtime(self) -> float: ...
|
| 211 |
+
|
| 212 |
+
def __getattr__(self, name: str) -> Any:
|
| 213 |
+
return getattr(self._osstatresult, "st_" + name)
|
| 214 |
+
|
| 215 |
+
def __init__(self, path, osstatresult):
|
| 216 |
+
self.path = path
|
| 217 |
+
self._osstatresult = osstatresult
|
| 218 |
+
|
| 219 |
+
@property
|
| 220 |
+
def owner(self):
|
| 221 |
+
if iswin32:
|
| 222 |
+
raise NotImplementedError("XXX win32")
|
| 223 |
+
import pwd
|
| 224 |
+
|
| 225 |
+
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore]
|
| 226 |
+
return entry[0]
|
| 227 |
+
|
| 228 |
+
@property
|
| 229 |
+
def group(self):
|
| 230 |
+
"""Return group name of file."""
|
| 231 |
+
if iswin32:
|
| 232 |
+
raise NotImplementedError("XXX win32")
|
| 233 |
+
import grp
|
| 234 |
+
|
| 235 |
+
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore]
|
| 236 |
+
return entry[0]
|
| 237 |
+
|
| 238 |
+
def isdir(self):
|
| 239 |
+
return S_ISDIR(self._osstatresult.st_mode)
|
| 240 |
+
|
| 241 |
+
def isfile(self):
|
| 242 |
+
return S_ISREG(self._osstatresult.st_mode)
|
| 243 |
+
|
| 244 |
+
def islink(self):
|
| 245 |
+
self.path.lstat()
|
| 246 |
+
return S_ISLNK(self._osstatresult.st_mode)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def getuserid(user):
|
| 250 |
+
import pwd
|
| 251 |
+
|
| 252 |
+
if not isinstance(user, int):
|
| 253 |
+
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore]
|
| 254 |
+
return user
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def getgroupid(group):
|
| 258 |
+
import grp
|
| 259 |
+
|
| 260 |
+
if not isinstance(group, int):
|
| 261 |
+
group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore]
|
| 262 |
+
return group
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
class LocalPath:
|
| 266 |
+
"""Object oriented interface to os.path and other local filesystem
|
| 267 |
+
related information.
|
| 268 |
+
"""
|
| 269 |
+
|
| 270 |
+
class ImportMismatchError(ImportError):
|
| 271 |
+
"""raised on pyimport() if there is a mismatch of __file__'s"""
|
| 272 |
+
|
| 273 |
+
sep = os.sep
|
| 274 |
+
|
| 275 |
+
def __init__(self, path=None, expanduser=False):
|
| 276 |
+
"""Initialize and return a local Path instance.
|
| 277 |
+
|
| 278 |
+
Path can be relative to the current directory.
|
| 279 |
+
If path is None it defaults to the current working directory.
|
| 280 |
+
If expanduser is True, tilde-expansion is performed.
|
| 281 |
+
Note that Path instances always carry an absolute path.
|
| 282 |
+
Note also that passing in a local path object will simply return
|
| 283 |
+
the exact same path object. Use new() to get a new copy.
|
| 284 |
+
"""
|
| 285 |
+
if path is None:
|
| 286 |
+
self.strpath = error.checked_call(os.getcwd)
|
| 287 |
+
else:
|
| 288 |
+
try:
|
| 289 |
+
path = os.fspath(path)
|
| 290 |
+
except TypeError:
|
| 291 |
+
raise ValueError(
|
| 292 |
+
"can only pass None, Path instances "
|
| 293 |
+
"or non-empty strings to LocalPath"
|
| 294 |
+
)
|
| 295 |
+
if expanduser:
|
| 296 |
+
path = os.path.expanduser(path)
|
| 297 |
+
self.strpath = abspath(path)
|
| 298 |
+
|
| 299 |
+
if sys.platform != "win32":
|
| 300 |
+
|
| 301 |
+
def chown(self, user, group, rec=0):
|
| 302 |
+
"""Change ownership to the given user and group.
|
| 303 |
+
user and group may be specified by a number or
|
| 304 |
+
by a name. if rec is True change ownership
|
| 305 |
+
recursively.
|
| 306 |
+
"""
|
| 307 |
+
uid = getuserid(user)
|
| 308 |
+
gid = getgroupid(group)
|
| 309 |
+
if rec:
|
| 310 |
+
for x in self.visit(rec=lambda x: x.check(link=0)):
|
| 311 |
+
if x.check(link=0):
|
| 312 |
+
error.checked_call(os.chown, str(x), uid, gid)
|
| 313 |
+
error.checked_call(os.chown, str(self), uid, gid)
|
| 314 |
+
|
| 315 |
+
def readlink(self) -> str:
|
| 316 |
+
"""Return value of a symbolic link."""
|
| 317 |
+
# https://github.com/python/mypy/issues/12278
|
| 318 |
+
return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore]
|
| 319 |
+
|
| 320 |
+
def mklinkto(self, oldname):
|
| 321 |
+
"""Posix style hard link to another name."""
|
| 322 |
+
error.checked_call(os.link, str(oldname), str(self))
|
| 323 |
+
|
| 324 |
+
def mksymlinkto(self, value, absolute=1):
|
| 325 |
+
"""Create a symbolic link with the given value (pointing to another name)."""
|
| 326 |
+
if absolute:
|
| 327 |
+
error.checked_call(os.symlink, str(value), self.strpath)
|
| 328 |
+
else:
|
| 329 |
+
base = self.common(value)
|
| 330 |
+
# with posix local paths '/' is always a common base
|
| 331 |
+
relsource = self.__class__(value).relto(base)
|
| 332 |
+
reldest = self.relto(base)
|
| 333 |
+
n = reldest.count(self.sep)
|
| 334 |
+
target = self.sep.join(("..",) * n + (relsource,))
|
| 335 |
+
error.checked_call(os.symlink, target, self.strpath)
|
| 336 |
+
|
| 337 |
+
def __div__(self, other):
|
| 338 |
+
return self.join(os.fspath(other))
|
| 339 |
+
|
| 340 |
+
__truediv__ = __div__ # py3k
|
| 341 |
+
|
| 342 |
+
@property
|
| 343 |
+
def basename(self):
|
| 344 |
+
"""Basename part of path."""
|
| 345 |
+
return self._getbyspec("basename")[0]
|
| 346 |
+
|
| 347 |
+
@property
|
| 348 |
+
def dirname(self):
|
| 349 |
+
"""Dirname part of path."""
|
| 350 |
+
return self._getbyspec("dirname")[0]
|
| 351 |
+
|
| 352 |
+
@property
|
| 353 |
+
def purebasename(self):
|
| 354 |
+
"""Pure base name of the path."""
|
| 355 |
+
return self._getbyspec("purebasename")[0]
|
| 356 |
+
|
| 357 |
+
@property
|
| 358 |
+
def ext(self):
|
| 359 |
+
"""Extension of the path (including the '.')."""
|
| 360 |
+
return self._getbyspec("ext")[0]
|
| 361 |
+
|
| 362 |
+
def read_binary(self):
|
| 363 |
+
"""Read and return a bytestring from reading the path."""
|
| 364 |
+
with self.open("rb") as f:
|
| 365 |
+
return f.read()
|
| 366 |
+
|
| 367 |
+
def read_text(self, encoding):
|
| 368 |
+
"""Read and return a Unicode string from reading the path."""
|
| 369 |
+
with self.open("r", encoding=encoding) as f:
|
| 370 |
+
return f.read()
|
| 371 |
+
|
| 372 |
+
def read(self, mode="r"):
|
| 373 |
+
"""Read and return a bytestring from reading the path."""
|
| 374 |
+
with self.open(mode) as f:
|
| 375 |
+
return f.read()
|
| 376 |
+
|
| 377 |
+
def readlines(self, cr=1):
|
| 378 |
+
"""Read and return a list of lines from the path. if cr is False, the
|
| 379 |
+
newline will be removed from the end of each line."""
|
| 380 |
+
mode = "r"
|
| 381 |
+
|
| 382 |
+
if not cr:
|
| 383 |
+
content = self.read(mode)
|
| 384 |
+
return content.split("\n")
|
| 385 |
+
else:
|
| 386 |
+
f = self.open(mode)
|
| 387 |
+
try:
|
| 388 |
+
return f.readlines()
|
| 389 |
+
finally:
|
| 390 |
+
f.close()
|
| 391 |
+
|
| 392 |
+
def load(self):
|
| 393 |
+
"""(deprecated) return object unpickled from self.read()"""
|
| 394 |
+
f = self.open("rb")
|
| 395 |
+
try:
|
| 396 |
+
import pickle
|
| 397 |
+
|
| 398 |
+
return error.checked_call(pickle.load, f)
|
| 399 |
+
finally:
|
| 400 |
+
f.close()
|
| 401 |
+
|
| 402 |
+
def move(self, target):
|
| 403 |
+
"""Move this path to target."""
|
| 404 |
+
if target.relto(self):
|
| 405 |
+
raise error.EINVAL(target, "cannot move path into a subdirectory of itself")
|
| 406 |
+
try:
|
| 407 |
+
self.rename(target)
|
| 408 |
+
except error.EXDEV: # invalid cross-device link
|
| 409 |
+
self.copy(target)
|
| 410 |
+
self.remove()
|
| 411 |
+
|
| 412 |
+
def fnmatch(self, pattern):
|
| 413 |
+
"""Return true if the basename/fullname matches the glob-'pattern'.
|
| 414 |
+
|
| 415 |
+
valid pattern characters::
|
| 416 |
+
|
| 417 |
+
* matches everything
|
| 418 |
+
? matches any single character
|
| 419 |
+
[seq] matches any character in seq
|
| 420 |
+
[!seq] matches any char not in seq
|
| 421 |
+
|
| 422 |
+
If the pattern contains a path-separator then the full path
|
| 423 |
+
is used for pattern matching and a '*' is prepended to the
|
| 424 |
+
pattern.
|
| 425 |
+
|
| 426 |
+
if the pattern doesn't contain a path-separator the pattern
|
| 427 |
+
is only matched against the basename.
|
| 428 |
+
"""
|
| 429 |
+
return FNMatcher(pattern)(self)
|
| 430 |
+
|
| 431 |
+
def relto(self, relpath):
|
| 432 |
+
"""Return a string which is the relative part of the path
|
| 433 |
+
to the given 'relpath'.
|
| 434 |
+
"""
|
| 435 |
+
if not isinstance(relpath, (str, LocalPath)):
|
| 436 |
+
raise TypeError(f"{relpath!r}: not a string or path object")
|
| 437 |
+
strrelpath = str(relpath)
|
| 438 |
+
if strrelpath and strrelpath[-1] != self.sep:
|
| 439 |
+
strrelpath += self.sep
|
| 440 |
+
# assert strrelpath[-1] == self.sep
|
| 441 |
+
# assert strrelpath[-2] != self.sep
|
| 442 |
+
strself = self.strpath
|
| 443 |
+
if sys.platform == "win32" or getattr(os, "_name", None) == "nt":
|
| 444 |
+
if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)):
|
| 445 |
+
return strself[len(strrelpath) :]
|
| 446 |
+
elif strself.startswith(strrelpath):
|
| 447 |
+
return strself[len(strrelpath) :]
|
| 448 |
+
return ""
|
| 449 |
+
|
| 450 |
+
def ensure_dir(self, *args):
|
| 451 |
+
"""Ensure the path joined with args is a directory."""
|
| 452 |
+
return self.ensure(*args, dir=True)
|
| 453 |
+
|
| 454 |
+
def bestrelpath(self, dest):
|
| 455 |
+
"""Return a string which is a relative path from self
|
| 456 |
+
(assumed to be a directory) to dest such that
|
| 457 |
+
self.join(bestrelpath) == dest and if not such
|
| 458 |
+
path can be determined return dest.
|
| 459 |
+
"""
|
| 460 |
+
try:
|
| 461 |
+
if self == dest:
|
| 462 |
+
return os.curdir
|
| 463 |
+
base = self.common(dest)
|
| 464 |
+
if not base: # can be the case on windows
|
| 465 |
+
return str(dest)
|
| 466 |
+
self2base = self.relto(base)
|
| 467 |
+
reldest = dest.relto(base)
|
| 468 |
+
if self2base:
|
| 469 |
+
n = self2base.count(self.sep) + 1
|
| 470 |
+
else:
|
| 471 |
+
n = 0
|
| 472 |
+
lst = [os.pardir] * n
|
| 473 |
+
if reldest:
|
| 474 |
+
lst.append(reldest)
|
| 475 |
+
target = dest.sep.join(lst)
|
| 476 |
+
return target
|
| 477 |
+
except AttributeError:
|
| 478 |
+
return str(dest)
|
| 479 |
+
|
| 480 |
+
def exists(self):
|
| 481 |
+
return self.check()
|
| 482 |
+
|
| 483 |
+
def isdir(self):
|
| 484 |
+
return self.check(dir=1)
|
| 485 |
+
|
| 486 |
+
def isfile(self):
|
| 487 |
+
return self.check(file=1)
|
| 488 |
+
|
| 489 |
+
def parts(self, reverse=False):
|
| 490 |
+
"""Return a root-first list of all ancestor directories
|
| 491 |
+
plus the path itself.
|
| 492 |
+
"""
|
| 493 |
+
current = self
|
| 494 |
+
lst = [self]
|
| 495 |
+
while 1:
|
| 496 |
+
last = current
|
| 497 |
+
current = current.dirpath()
|
| 498 |
+
if last == current:
|
| 499 |
+
break
|
| 500 |
+
lst.append(current)
|
| 501 |
+
if not reverse:
|
| 502 |
+
lst.reverse()
|
| 503 |
+
return lst
|
| 504 |
+
|
| 505 |
+
def common(self, other):
|
| 506 |
+
"""Return the common part shared with the other path
|
| 507 |
+
or None if there is no common part.
|
| 508 |
+
"""
|
| 509 |
+
last = None
|
| 510 |
+
for x, y in zip(self.parts(), other.parts()):
|
| 511 |
+
if x != y:
|
| 512 |
+
return last
|
| 513 |
+
last = x
|
| 514 |
+
return last
|
| 515 |
+
|
| 516 |
+
def __add__(self, other):
|
| 517 |
+
"""Return new path object with 'other' added to the basename"""
|
| 518 |
+
return self.new(basename=self.basename + str(other))
|
| 519 |
+
|
| 520 |
+
def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False):
|
| 521 |
+
"""Yields all paths below the current one
|
| 522 |
+
|
| 523 |
+
fil is a filter (glob pattern or callable), if not matching the
|
| 524 |
+
path will not be yielded, defaulting to None (everything is
|
| 525 |
+
returned)
|
| 526 |
+
|
| 527 |
+
rec is a filter (glob pattern or callable) that controls whether
|
| 528 |
+
a node is descended, defaulting to None
|
| 529 |
+
|
| 530 |
+
ignore is an Exception class that is ignoredwhen calling dirlist()
|
| 531 |
+
on any of the paths (by default, all exceptions are reported)
|
| 532 |
+
|
| 533 |
+
bf if True will cause a breadthfirst search instead of the
|
| 534 |
+
default depthfirst. Default: False
|
| 535 |
+
|
| 536 |
+
sort if True will sort entries within each directory level.
|
| 537 |
+
"""
|
| 538 |
+
yield from Visitor(fil, rec, ignore, bf, sort).gen(self)
|
| 539 |
+
|
| 540 |
+
def _sortlist(self, res, sort):
|
| 541 |
+
if sort:
|
| 542 |
+
if hasattr(sort, "__call__"):
|
| 543 |
+
warnings.warn(
|
| 544 |
+
DeprecationWarning(
|
| 545 |
+
"listdir(sort=callable) is deprecated and breaks on python3"
|
| 546 |
+
),
|
| 547 |
+
stacklevel=3,
|
| 548 |
+
)
|
| 549 |
+
res.sort(sort)
|
| 550 |
+
else:
|
| 551 |
+
res.sort()
|
| 552 |
+
|
| 553 |
+
def __fspath__(self):
|
| 554 |
+
return self.strpath
|
| 555 |
+
|
| 556 |
+
def __hash__(self):
|
| 557 |
+
s = self.strpath
|
| 558 |
+
if iswin32:
|
| 559 |
+
s = s.lower()
|
| 560 |
+
return hash(s)
|
| 561 |
+
|
| 562 |
+
def __eq__(self, other):
|
| 563 |
+
s1 = os.fspath(self)
|
| 564 |
+
try:
|
| 565 |
+
s2 = os.fspath(other)
|
| 566 |
+
except TypeError:
|
| 567 |
+
return False
|
| 568 |
+
if iswin32:
|
| 569 |
+
s1 = s1.lower()
|
| 570 |
+
try:
|
| 571 |
+
s2 = s2.lower()
|
| 572 |
+
except AttributeError:
|
| 573 |
+
return False
|
| 574 |
+
return s1 == s2
|
| 575 |
+
|
| 576 |
+
def __ne__(self, other):
|
| 577 |
+
return not (self == other)
|
| 578 |
+
|
| 579 |
+
def __lt__(self, other):
|
| 580 |
+
return os.fspath(self) < os.fspath(other)
|
| 581 |
+
|
| 582 |
+
def __gt__(self, other):
|
| 583 |
+
return os.fspath(self) > os.fspath(other)
|
| 584 |
+
|
| 585 |
+
def samefile(self, other):
|
| 586 |
+
"""Return True if 'other' references the same file as 'self'."""
|
| 587 |
+
other = os.fspath(other)
|
| 588 |
+
if not isabs(other):
|
| 589 |
+
other = abspath(other)
|
| 590 |
+
if self == other:
|
| 591 |
+
return True
|
| 592 |
+
if not hasattr(os.path, "samefile"):
|
| 593 |
+
return False
|
| 594 |
+
return error.checked_call(os.path.samefile, self.strpath, other)
|
| 595 |
+
|
| 596 |
+
def remove(self, rec=1, ignore_errors=False):
|
| 597 |
+
"""Remove a file or directory (or a directory tree if rec=1).
|
| 598 |
+
if ignore_errors is True, errors while removing directories will
|
| 599 |
+
be ignored.
|
| 600 |
+
"""
|
| 601 |
+
if self.check(dir=1, link=0):
|
| 602 |
+
if rec:
|
| 603 |
+
# force remove of readonly files on windows
|
| 604 |
+
if iswin32:
|
| 605 |
+
self.chmod(0o700, rec=1)
|
| 606 |
+
import shutil
|
| 607 |
+
|
| 608 |
+
error.checked_call(
|
| 609 |
+
shutil.rmtree, self.strpath, ignore_errors=ignore_errors
|
| 610 |
+
)
|
| 611 |
+
else:
|
| 612 |
+
error.checked_call(os.rmdir, self.strpath)
|
| 613 |
+
else:
|
| 614 |
+
if iswin32:
|
| 615 |
+
self.chmod(0o700)
|
| 616 |
+
error.checked_call(os.remove, self.strpath)
|
| 617 |
+
|
| 618 |
+
def computehash(self, hashtype="md5", chunksize=524288):
|
| 619 |
+
"""Return hexdigest of hashvalue for this file."""
|
| 620 |
+
try:
|
| 621 |
+
try:
|
| 622 |
+
import hashlib as mod
|
| 623 |
+
except ImportError:
|
| 624 |
+
if hashtype == "sha1":
|
| 625 |
+
hashtype = "sha"
|
| 626 |
+
mod = __import__(hashtype)
|
| 627 |
+
hash = getattr(mod, hashtype)()
|
| 628 |
+
except (AttributeError, ImportError):
|
| 629 |
+
raise ValueError(f"Don't know how to compute {hashtype!r} hash")
|
| 630 |
+
f = self.open("rb")
|
| 631 |
+
try:
|
| 632 |
+
while 1:
|
| 633 |
+
buf = f.read(chunksize)
|
| 634 |
+
if not buf:
|
| 635 |
+
return hash.hexdigest()
|
| 636 |
+
hash.update(buf)
|
| 637 |
+
finally:
|
| 638 |
+
f.close()
|
| 639 |
+
|
| 640 |
+
def new(self, **kw):
|
| 641 |
+
"""Create a modified version of this path.
|
| 642 |
+
the following keyword arguments modify various path parts::
|
| 643 |
+
|
| 644 |
+
a:/some/path/to/a/file.ext
|
| 645 |
+
xx drive
|
| 646 |
+
xxxxxxxxxxxxxxxxx dirname
|
| 647 |
+
xxxxxxxx basename
|
| 648 |
+
xxxx purebasename
|
| 649 |
+
xxx ext
|
| 650 |
+
"""
|
| 651 |
+
obj = object.__new__(self.__class__)
|
| 652 |
+
if not kw:
|
| 653 |
+
obj.strpath = self.strpath
|
| 654 |
+
return obj
|
| 655 |
+
drive, dirname, basename, purebasename, ext = self._getbyspec(
|
| 656 |
+
"drive,dirname,basename,purebasename,ext"
|
| 657 |
+
)
|
| 658 |
+
if "basename" in kw:
|
| 659 |
+
if "purebasename" in kw or "ext" in kw:
|
| 660 |
+
raise ValueError(f"invalid specification {kw!r}")
|
| 661 |
+
else:
|
| 662 |
+
pb = kw.setdefault("purebasename", purebasename)
|
| 663 |
+
try:
|
| 664 |
+
ext = kw["ext"]
|
| 665 |
+
except KeyError:
|
| 666 |
+
pass
|
| 667 |
+
else:
|
| 668 |
+
if ext and not ext.startswith("."):
|
| 669 |
+
ext = "." + ext
|
| 670 |
+
kw["basename"] = pb + ext
|
| 671 |
+
|
| 672 |
+
if "dirname" in kw and not kw["dirname"]:
|
| 673 |
+
kw["dirname"] = drive
|
| 674 |
+
else:
|
| 675 |
+
kw.setdefault("dirname", dirname)
|
| 676 |
+
kw.setdefault("sep", self.sep)
|
| 677 |
+
obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
|
| 678 |
+
return obj
|
| 679 |
+
|
| 680 |
+
def _getbyspec(self, spec: str) -> list[str]:
|
| 681 |
+
"""See new for what 'spec' can be."""
|
| 682 |
+
res = []
|
| 683 |
+
parts = self.strpath.split(self.sep)
|
| 684 |
+
|
| 685 |
+
args = filter(None, spec.split(","))
|
| 686 |
+
for name in args:
|
| 687 |
+
if name == "drive":
|
| 688 |
+
res.append(parts[0])
|
| 689 |
+
elif name == "dirname":
|
| 690 |
+
res.append(self.sep.join(parts[:-1]))
|
| 691 |
+
else:
|
| 692 |
+
basename = parts[-1]
|
| 693 |
+
if name == "basename":
|
| 694 |
+
res.append(basename)
|
| 695 |
+
else:
|
| 696 |
+
i = basename.rfind(".")
|
| 697 |
+
if i == -1:
|
| 698 |
+
purebasename, ext = basename, ""
|
| 699 |
+
else:
|
| 700 |
+
purebasename, ext = basename[:i], basename[i:]
|
| 701 |
+
if name == "purebasename":
|
| 702 |
+
res.append(purebasename)
|
| 703 |
+
elif name == "ext":
|
| 704 |
+
res.append(ext)
|
| 705 |
+
else:
|
| 706 |
+
raise ValueError(f"invalid part specification {name!r}")
|
| 707 |
+
return res
|
| 708 |
+
|
| 709 |
+
def dirpath(self, *args, **kwargs):
|
| 710 |
+
"""Return the directory path joined with any given path arguments."""
|
| 711 |
+
if not kwargs:
|
| 712 |
+
path = object.__new__(self.__class__)
|
| 713 |
+
path.strpath = dirname(self.strpath)
|
| 714 |
+
if args:
|
| 715 |
+
path = path.join(*args)
|
| 716 |
+
return path
|
| 717 |
+
return self.new(basename="").join(*args, **kwargs)
|
| 718 |
+
|
| 719 |
+
def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath:
|
| 720 |
+
"""Return a new path by appending all 'args' as path
|
| 721 |
+
components. if abs=1 is used restart from root if any
|
| 722 |
+
of the args is an absolute path.
|
| 723 |
+
"""
|
| 724 |
+
sep = self.sep
|
| 725 |
+
strargs = [os.fspath(arg) for arg in args]
|
| 726 |
+
strpath = self.strpath
|
| 727 |
+
if abs:
|
| 728 |
+
newargs: list[str] = []
|
| 729 |
+
for arg in reversed(strargs):
|
| 730 |
+
if isabs(arg):
|
| 731 |
+
strpath = arg
|
| 732 |
+
strargs = newargs
|
| 733 |
+
break
|
| 734 |
+
newargs.insert(0, arg)
|
| 735 |
+
# special case for when we have e.g. strpath == "/"
|
| 736 |
+
actual_sep = "" if strpath.endswith(sep) else sep
|
| 737 |
+
for arg in strargs:
|
| 738 |
+
arg = arg.strip(sep)
|
| 739 |
+
if iswin32:
|
| 740 |
+
# allow unix style paths even on windows.
|
| 741 |
+
arg = arg.strip("/")
|
| 742 |
+
arg = arg.replace("/", sep)
|
| 743 |
+
strpath = strpath + actual_sep + arg
|
| 744 |
+
actual_sep = sep
|
| 745 |
+
obj = object.__new__(self.__class__)
|
| 746 |
+
obj.strpath = normpath(strpath)
|
| 747 |
+
return obj
|
| 748 |
+
|
| 749 |
+
def open(self, mode="r", ensure=False, encoding=None):
|
| 750 |
+
"""Return an opened file with the given mode.
|
| 751 |
+
|
| 752 |
+
If ensure is True, create parent directories if needed.
|
| 753 |
+
"""
|
| 754 |
+
if ensure:
|
| 755 |
+
self.dirpath().ensure(dir=1)
|
| 756 |
+
if encoding:
|
| 757 |
+
return error.checked_call(
|
| 758 |
+
io.open,
|
| 759 |
+
self.strpath,
|
| 760 |
+
mode,
|
| 761 |
+
encoding=encoding,
|
| 762 |
+
)
|
| 763 |
+
return error.checked_call(open, self.strpath, mode)
|
| 764 |
+
|
| 765 |
+
def _fastjoin(self, name):
|
| 766 |
+
child = object.__new__(self.__class__)
|
| 767 |
+
child.strpath = self.strpath + self.sep + name
|
| 768 |
+
return child
|
| 769 |
+
|
| 770 |
+
def islink(self):
|
| 771 |
+
return islink(self.strpath)
|
| 772 |
+
|
| 773 |
+
def check(self, **kw):
|
| 774 |
+
"""Check a path for existence and properties.
|
| 775 |
+
|
| 776 |
+
Without arguments, return True if the path exists, otherwise False.
|
| 777 |
+
|
| 778 |
+
valid checkers::
|
| 779 |
+
|
| 780 |
+
file = 1 # is a file
|
| 781 |
+
file = 0 # is not a file (may not even exist)
|
| 782 |
+
dir = 1 # is a dir
|
| 783 |
+
link = 1 # is a link
|
| 784 |
+
exists = 1 # exists
|
| 785 |
+
|
| 786 |
+
You can specify multiple checker definitions, for example::
|
| 787 |
+
|
| 788 |
+
path.check(file=1, link=1) # a link pointing to a file
|
| 789 |
+
"""
|
| 790 |
+
if not kw:
|
| 791 |
+
return exists(self.strpath)
|
| 792 |
+
if len(kw) == 1:
|
| 793 |
+
if "dir" in kw:
|
| 794 |
+
return not kw["dir"] ^ isdir(self.strpath)
|
| 795 |
+
if "file" in kw:
|
| 796 |
+
return not kw["file"] ^ isfile(self.strpath)
|
| 797 |
+
if not kw:
|
| 798 |
+
kw = {"exists": 1}
|
| 799 |
+
return Checkers(self)._evaluate(kw)
|
| 800 |
+
|
| 801 |
+
_patternchars = set("*?[" + os.sep)
|
| 802 |
+
|
| 803 |
+
def listdir(self, fil=None, sort=None):
|
| 804 |
+
"""List directory contents, possibly filter by the given fil func
|
| 805 |
+
and possibly sorted.
|
| 806 |
+
"""
|
| 807 |
+
if fil is None and sort is None:
|
| 808 |
+
names = error.checked_call(os.listdir, self.strpath)
|
| 809 |
+
return map_as_list(self._fastjoin, names)
|
| 810 |
+
if isinstance(fil, str):
|
| 811 |
+
if not self._patternchars.intersection(fil):
|
| 812 |
+
child = self._fastjoin(fil)
|
| 813 |
+
if exists(child.strpath):
|
| 814 |
+
return [child]
|
| 815 |
+
return []
|
| 816 |
+
fil = FNMatcher(fil)
|
| 817 |
+
names = error.checked_call(os.listdir, self.strpath)
|
| 818 |
+
res = []
|
| 819 |
+
for name in names:
|
| 820 |
+
child = self._fastjoin(name)
|
| 821 |
+
if fil is None or fil(child):
|
| 822 |
+
res.append(child)
|
| 823 |
+
self._sortlist(res, sort)
|
| 824 |
+
return res
|
| 825 |
+
|
| 826 |
+
def size(self) -> int:
|
| 827 |
+
"""Return size of the underlying file object"""
|
| 828 |
+
return self.stat().size
|
| 829 |
+
|
| 830 |
+
def mtime(self) -> float:
|
| 831 |
+
"""Return last modification time of the path."""
|
| 832 |
+
return self.stat().mtime
|
| 833 |
+
|
| 834 |
+
def copy(self, target, mode=False, stat=False):
|
| 835 |
+
"""Copy path to target.
|
| 836 |
+
|
| 837 |
+
If mode is True, will copy permission from path to target.
|
| 838 |
+
If stat is True, copy permission, last modification
|
| 839 |
+
time, last access time, and flags from path to target.
|
| 840 |
+
"""
|
| 841 |
+
if self.check(file=1):
|
| 842 |
+
if target.check(dir=1):
|
| 843 |
+
target = target.join(self.basename)
|
| 844 |
+
assert self != target
|
| 845 |
+
copychunked(self, target)
|
| 846 |
+
if mode:
|
| 847 |
+
copymode(self.strpath, target.strpath)
|
| 848 |
+
if stat:
|
| 849 |
+
copystat(self, target)
|
| 850 |
+
else:
|
| 851 |
+
|
| 852 |
+
def rec(p):
|
| 853 |
+
return p.check(link=0)
|
| 854 |
+
|
| 855 |
+
for x in self.visit(rec=rec):
|
| 856 |
+
relpath = x.relto(self)
|
| 857 |
+
newx = target.join(relpath)
|
| 858 |
+
newx.dirpath().ensure(dir=1)
|
| 859 |
+
if x.check(link=1):
|
| 860 |
+
newx.mksymlinkto(x.readlink())
|
| 861 |
+
continue
|
| 862 |
+
elif x.check(file=1):
|
| 863 |
+
copychunked(x, newx)
|
| 864 |
+
elif x.check(dir=1):
|
| 865 |
+
newx.ensure(dir=1)
|
| 866 |
+
if mode:
|
| 867 |
+
copymode(x.strpath, newx.strpath)
|
| 868 |
+
if stat:
|
| 869 |
+
copystat(x, newx)
|
| 870 |
+
|
| 871 |
+
def rename(self, target):
|
| 872 |
+
"""Rename this path to target."""
|
| 873 |
+
target = os.fspath(target)
|
| 874 |
+
return error.checked_call(os.rename, self.strpath, target)
|
| 875 |
+
|
| 876 |
+
def dump(self, obj, bin=1):
|
| 877 |
+
"""Pickle object into path location"""
|
| 878 |
+
f = self.open("wb")
|
| 879 |
+
import pickle
|
| 880 |
+
|
| 881 |
+
try:
|
| 882 |
+
error.checked_call(pickle.dump, obj, f, bin)
|
| 883 |
+
finally:
|
| 884 |
+
f.close()
|
| 885 |
+
|
| 886 |
+
def mkdir(self, *args):
|
| 887 |
+
"""Create & return the directory joined with args."""
|
| 888 |
+
p = self.join(*args)
|
| 889 |
+
error.checked_call(os.mkdir, os.fspath(p))
|
| 890 |
+
return p
|
| 891 |
+
|
| 892 |
+
def write_binary(self, data, ensure=False):
|
| 893 |
+
"""Write binary data into path. If ensure is True create
|
| 894 |
+
missing parent directories.
|
| 895 |
+
"""
|
| 896 |
+
if ensure:
|
| 897 |
+
self.dirpath().ensure(dir=1)
|
| 898 |
+
with self.open("wb") as f:
|
| 899 |
+
f.write(data)
|
| 900 |
+
|
| 901 |
+
def write_text(self, data, encoding, ensure=False):
|
| 902 |
+
"""Write text data into path using the specified encoding.
|
| 903 |
+
If ensure is True create missing parent directories.
|
| 904 |
+
"""
|
| 905 |
+
if ensure:
|
| 906 |
+
self.dirpath().ensure(dir=1)
|
| 907 |
+
with self.open("w", encoding=encoding) as f:
|
| 908 |
+
f.write(data)
|
| 909 |
+
|
| 910 |
+
def write(self, data, mode="w", ensure=False):
|
| 911 |
+
"""Write data into path. If ensure is True create
|
| 912 |
+
missing parent directories.
|
| 913 |
+
"""
|
| 914 |
+
if ensure:
|
| 915 |
+
self.dirpath().ensure(dir=1)
|
| 916 |
+
if "b" in mode:
|
| 917 |
+
if not isinstance(data, bytes):
|
| 918 |
+
raise ValueError("can only process bytes")
|
| 919 |
+
else:
|
| 920 |
+
if not isinstance(data, str):
|
| 921 |
+
if not isinstance(data, bytes):
|
| 922 |
+
data = str(data)
|
| 923 |
+
else:
|
| 924 |
+
data = data.decode(sys.getdefaultencoding())
|
| 925 |
+
f = self.open(mode)
|
| 926 |
+
try:
|
| 927 |
+
f.write(data)
|
| 928 |
+
finally:
|
| 929 |
+
f.close()
|
| 930 |
+
|
| 931 |
+
def _ensuredirs(self):
|
| 932 |
+
parent = self.dirpath()
|
| 933 |
+
if parent == self:
|
| 934 |
+
return self
|
| 935 |
+
if parent.check(dir=0):
|
| 936 |
+
parent._ensuredirs()
|
| 937 |
+
if self.check(dir=0):
|
| 938 |
+
try:
|
| 939 |
+
self.mkdir()
|
| 940 |
+
except error.EEXIST:
|
| 941 |
+
# race condition: file/dir created by another thread/process.
|
| 942 |
+
# complain if it is not a dir
|
| 943 |
+
if self.check(dir=0):
|
| 944 |
+
raise
|
| 945 |
+
return self
|
| 946 |
+
|
| 947 |
+
def ensure(self, *args, **kwargs):
|
| 948 |
+
"""Ensure that an args-joined path exists (by default as
|
| 949 |
+
a file). if you specify a keyword argument 'dir=True'
|
| 950 |
+
then the path is forced to be a directory path.
|
| 951 |
+
"""
|
| 952 |
+
p = self.join(*args)
|
| 953 |
+
if kwargs.get("dir", 0):
|
| 954 |
+
return p._ensuredirs()
|
| 955 |
+
else:
|
| 956 |
+
p.dirpath()._ensuredirs()
|
| 957 |
+
if not p.check(file=1):
|
| 958 |
+
p.open("wb").close()
|
| 959 |
+
return p
|
| 960 |
+
|
| 961 |
+
@overload
|
| 962 |
+
def stat(self, raising: Literal[True] = ...) -> Stat: ...
|
| 963 |
+
|
| 964 |
+
@overload
|
| 965 |
+
def stat(self, raising: Literal[False]) -> Stat | None: ...
|
| 966 |
+
|
| 967 |
+
def stat(self, raising: bool = True) -> Stat | None:
|
| 968 |
+
"""Return an os.stat() tuple."""
|
| 969 |
+
if raising:
|
| 970 |
+
return Stat(self, error.checked_call(os.stat, self.strpath))
|
| 971 |
+
try:
|
| 972 |
+
return Stat(self, os.stat(self.strpath))
|
| 973 |
+
except KeyboardInterrupt:
|
| 974 |
+
raise
|
| 975 |
+
except Exception:
|
| 976 |
+
return None
|
| 977 |
+
|
| 978 |
+
def lstat(self) -> Stat:
|
| 979 |
+
"""Return an os.lstat() tuple."""
|
| 980 |
+
return Stat(self, error.checked_call(os.lstat, self.strpath))
|
| 981 |
+
|
| 982 |
+
def setmtime(self, mtime=None):
|
| 983 |
+
"""Set modification time for the given path. if 'mtime' is None
|
| 984 |
+
(the default) then the file's mtime is set to current time.
|
| 985 |
+
|
| 986 |
+
Note that the resolution for 'mtime' is platform dependent.
|
| 987 |
+
"""
|
| 988 |
+
if mtime is None:
|
| 989 |
+
return error.checked_call(os.utime, self.strpath, mtime)
|
| 990 |
+
try:
|
| 991 |
+
return error.checked_call(os.utime, self.strpath, (-1, mtime))
|
| 992 |
+
except error.EINVAL:
|
| 993 |
+
return error.checked_call(os.utime, self.strpath, (self.atime(), mtime))
|
| 994 |
+
|
| 995 |
+
def chdir(self):
|
| 996 |
+
"""Change directory to self and return old current directory"""
|
| 997 |
+
try:
|
| 998 |
+
old = self.__class__()
|
| 999 |
+
except error.ENOENT:
|
| 1000 |
+
old = None
|
| 1001 |
+
error.checked_call(os.chdir, self.strpath)
|
| 1002 |
+
return old
|
| 1003 |
+
|
| 1004 |
+
@contextmanager
|
| 1005 |
+
def as_cwd(self):
|
| 1006 |
+
"""
|
| 1007 |
+
Return a context manager, which changes to the path's dir during the
|
| 1008 |
+
managed "with" context.
|
| 1009 |
+
On __enter__ it returns the old dir, which might be ``None``.
|
| 1010 |
+
"""
|
| 1011 |
+
old = self.chdir()
|
| 1012 |
+
try:
|
| 1013 |
+
yield old
|
| 1014 |
+
finally:
|
| 1015 |
+
if old is not None:
|
| 1016 |
+
old.chdir()
|
| 1017 |
+
|
| 1018 |
+
def realpath(self):
|
| 1019 |
+
"""Return a new path which contains no symbolic links."""
|
| 1020 |
+
return self.__class__(os.path.realpath(self.strpath))
|
| 1021 |
+
|
| 1022 |
+
def atime(self):
|
| 1023 |
+
"""Return last access time of the path."""
|
| 1024 |
+
return self.stat().atime
|
| 1025 |
+
|
| 1026 |
+
def __repr__(self):
|
| 1027 |
+
return f"local({self.strpath!r})"
|
| 1028 |
+
|
| 1029 |
+
def __str__(self):
|
| 1030 |
+
"""Return string representation of the Path."""
|
| 1031 |
+
return self.strpath
|
| 1032 |
+
|
| 1033 |
+
def chmod(self, mode, rec=0):
|
| 1034 |
+
"""Change permissions to the given mode. If mode is an
|
| 1035 |
+
integer it directly encodes the os-specific modes.
|
| 1036 |
+
if rec is True perform recursively.
|
| 1037 |
+
"""
|
| 1038 |
+
if not isinstance(mode, int):
|
| 1039 |
+
raise TypeError(f"mode {mode!r} must be an integer")
|
| 1040 |
+
if rec:
|
| 1041 |
+
for x in self.visit(rec=rec):
|
| 1042 |
+
error.checked_call(os.chmod, str(x), mode)
|
| 1043 |
+
error.checked_call(os.chmod, self.strpath, mode)
|
| 1044 |
+
|
| 1045 |
+
def pypkgpath(self):
|
| 1046 |
+
"""Return the Python package path by looking for the last
|
| 1047 |
+
directory upwards which still contains an __init__.py.
|
| 1048 |
+
Return None if a pkgpath cannot be determined.
|
| 1049 |
+
"""
|
| 1050 |
+
pkgpath = None
|
| 1051 |
+
for parent in self.parts(reverse=True):
|
| 1052 |
+
if parent.isdir():
|
| 1053 |
+
if not parent.join("__init__.py").exists():
|
| 1054 |
+
break
|
| 1055 |
+
if not isimportable(parent.basename):
|
| 1056 |
+
break
|
| 1057 |
+
pkgpath = parent
|
| 1058 |
+
return pkgpath
|
| 1059 |
+
|
| 1060 |
+
def _ensuresyspath(self, ensuremode, path):
|
| 1061 |
+
if ensuremode:
|
| 1062 |
+
s = str(path)
|
| 1063 |
+
if ensuremode == "append":
|
| 1064 |
+
if s not in sys.path:
|
| 1065 |
+
sys.path.append(s)
|
| 1066 |
+
else:
|
| 1067 |
+
if s != sys.path[0]:
|
| 1068 |
+
sys.path.insert(0, s)
|
| 1069 |
+
|
| 1070 |
+
def pyimport(self, modname=None, ensuresyspath=True):
|
| 1071 |
+
"""Return path as an imported python module.
|
| 1072 |
+
|
| 1073 |
+
If modname is None, look for the containing package
|
| 1074 |
+
and construct an according module name.
|
| 1075 |
+
The module will be put/looked up in sys.modules.
|
| 1076 |
+
if ensuresyspath is True then the root dir for importing
|
| 1077 |
+
the file (taking __init__.py files into account) will
|
| 1078 |
+
be prepended to sys.path if it isn't there already.
|
| 1079 |
+
If ensuresyspath=="append" the root dir will be appended
|
| 1080 |
+
if it isn't already contained in sys.path.
|
| 1081 |
+
if ensuresyspath is False no modification of syspath happens.
|
| 1082 |
+
|
| 1083 |
+
Special value of ensuresyspath=="importlib" is intended
|
| 1084 |
+
purely for using in pytest, it is capable only of importing
|
| 1085 |
+
separate .py files outside packages, e.g. for test suite
|
| 1086 |
+
without any __init__.py file. It effectively allows having
|
| 1087 |
+
same-named test modules in different places and offers
|
| 1088 |
+
mild opt-in via this option. Note that it works only in
|
| 1089 |
+
recent versions of python.
|
| 1090 |
+
"""
|
| 1091 |
+
if not self.check():
|
| 1092 |
+
raise error.ENOENT(self)
|
| 1093 |
+
|
| 1094 |
+
if ensuresyspath == "importlib":
|
| 1095 |
+
if modname is None:
|
| 1096 |
+
modname = self.purebasename
|
| 1097 |
+
spec = importlib.util.spec_from_file_location(modname, str(self))
|
| 1098 |
+
if spec is None or spec.loader is None:
|
| 1099 |
+
raise ImportError(f"Can't find module {modname} at location {self!s}")
|
| 1100 |
+
mod = importlib.util.module_from_spec(spec)
|
| 1101 |
+
spec.loader.exec_module(mod)
|
| 1102 |
+
return mod
|
| 1103 |
+
|
| 1104 |
+
pkgpath = None
|
| 1105 |
+
if modname is None:
|
| 1106 |
+
pkgpath = self.pypkgpath()
|
| 1107 |
+
if pkgpath is not None:
|
| 1108 |
+
pkgroot = pkgpath.dirpath()
|
| 1109 |
+
names = self.new(ext="").relto(pkgroot).split(self.sep)
|
| 1110 |
+
if names[-1] == "__init__":
|
| 1111 |
+
names.pop()
|
| 1112 |
+
modname = ".".join(names)
|
| 1113 |
+
else:
|
| 1114 |
+
pkgroot = self.dirpath()
|
| 1115 |
+
modname = self.purebasename
|
| 1116 |
+
|
| 1117 |
+
self._ensuresyspath(ensuresyspath, pkgroot)
|
| 1118 |
+
__import__(modname)
|
| 1119 |
+
mod = sys.modules[modname]
|
| 1120 |
+
if self.basename == "__init__.py":
|
| 1121 |
+
return mod # we don't check anything as we might
|
| 1122 |
+
# be in a namespace package ... too icky to check
|
| 1123 |
+
modfile = mod.__file__
|
| 1124 |
+
assert modfile is not None
|
| 1125 |
+
if modfile[-4:] in (".pyc", ".pyo"):
|
| 1126 |
+
modfile = modfile[:-1]
|
| 1127 |
+
elif modfile.endswith("$py.class"):
|
| 1128 |
+
modfile = modfile[:-9] + ".py"
|
| 1129 |
+
if modfile.endswith(os.sep + "__init__.py"):
|
| 1130 |
+
if self.basename != "__init__.py":
|
| 1131 |
+
modfile = modfile[:-12]
|
| 1132 |
+
try:
|
| 1133 |
+
issame = self.samefile(modfile)
|
| 1134 |
+
except error.ENOENT:
|
| 1135 |
+
issame = False
|
| 1136 |
+
if not issame:
|
| 1137 |
+
ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH")
|
| 1138 |
+
if ignore != "1":
|
| 1139 |
+
raise self.ImportMismatchError(modname, modfile, self)
|
| 1140 |
+
return mod
|
| 1141 |
+
else:
|
| 1142 |
+
try:
|
| 1143 |
+
return sys.modules[modname]
|
| 1144 |
+
except KeyError:
|
| 1145 |
+
# we have a custom modname, do a pseudo-import
|
| 1146 |
+
import types
|
| 1147 |
+
|
| 1148 |
+
mod = types.ModuleType(modname)
|
| 1149 |
+
mod.__file__ = str(self)
|
| 1150 |
+
sys.modules[modname] = mod
|
| 1151 |
+
try:
|
| 1152 |
+
with open(str(self), "rb") as f:
|
| 1153 |
+
exec(f.read(), mod.__dict__)
|
| 1154 |
+
except BaseException:
|
| 1155 |
+
del sys.modules[modname]
|
| 1156 |
+
raise
|
| 1157 |
+
return mod
|
| 1158 |
+
|
| 1159 |
+
def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str:
|
| 1160 |
+
"""Return stdout text from executing a system child process,
|
| 1161 |
+
where the 'self' path points to executable.
|
| 1162 |
+
The process is directly invoked and not through a system shell.
|
| 1163 |
+
"""
|
| 1164 |
+
from subprocess import PIPE
|
| 1165 |
+
from subprocess import Popen
|
| 1166 |
+
|
| 1167 |
+
popen_opts.pop("stdout", None)
|
| 1168 |
+
popen_opts.pop("stderr", None)
|
| 1169 |
+
proc = Popen(
|
| 1170 |
+
[str(self)] + [str(arg) for arg in argv],
|
| 1171 |
+
**popen_opts,
|
| 1172 |
+
stdout=PIPE,
|
| 1173 |
+
stderr=PIPE,
|
| 1174 |
+
)
|
| 1175 |
+
stdout: str | bytes
|
| 1176 |
+
stdout, stderr = proc.communicate()
|
| 1177 |
+
ret = proc.wait()
|
| 1178 |
+
if isinstance(stdout, bytes):
|
| 1179 |
+
stdout = stdout.decode(sys.getdefaultencoding())
|
| 1180 |
+
if ret != 0:
|
| 1181 |
+
if isinstance(stderr, bytes):
|
| 1182 |
+
stderr = stderr.decode(sys.getdefaultencoding())
|
| 1183 |
+
raise RuntimeError(
|
| 1184 |
+
ret,
|
| 1185 |
+
ret,
|
| 1186 |
+
str(self),
|
| 1187 |
+
stdout,
|
| 1188 |
+
stderr,
|
| 1189 |
+
)
|
| 1190 |
+
return stdout
|
| 1191 |
+
|
| 1192 |
+
@classmethod
|
| 1193 |
+
def sysfind(cls, name, checker=None, paths=None):
|
| 1194 |
+
"""Return a path object found by looking at the systems
|
| 1195 |
+
underlying PATH specification. If the checker is not None
|
| 1196 |
+
it will be invoked to filter matching paths. If a binary
|
| 1197 |
+
cannot be found, None is returned
|
| 1198 |
+
Note: This is probably not working on plain win32 systems
|
| 1199 |
+
but may work on cygwin.
|
| 1200 |
+
"""
|
| 1201 |
+
if isabs(name):
|
| 1202 |
+
p = local(name)
|
| 1203 |
+
if p.check(file=1):
|
| 1204 |
+
return p
|
| 1205 |
+
else:
|
| 1206 |
+
if paths is None:
|
| 1207 |
+
if iswin32:
|
| 1208 |
+
paths = os.environ["Path"].split(";")
|
| 1209 |
+
if "" not in paths and "." not in paths:
|
| 1210 |
+
paths.append(".")
|
| 1211 |
+
try:
|
| 1212 |
+
systemroot = os.environ["SYSTEMROOT"]
|
| 1213 |
+
except KeyError:
|
| 1214 |
+
pass
|
| 1215 |
+
else:
|
| 1216 |
+
paths = [
|
| 1217 |
+
path.replace("%SystemRoot%", systemroot) for path in paths
|
| 1218 |
+
]
|
| 1219 |
+
else:
|
| 1220 |
+
paths = os.environ["PATH"].split(":")
|
| 1221 |
+
tryadd = []
|
| 1222 |
+
if iswin32:
|
| 1223 |
+
tryadd += os.environ["PATHEXT"].split(os.pathsep)
|
| 1224 |
+
tryadd.append("")
|
| 1225 |
+
|
| 1226 |
+
for x in paths:
|
| 1227 |
+
for addext in tryadd:
|
| 1228 |
+
p = local(x).join(name, abs=True) + addext
|
| 1229 |
+
try:
|
| 1230 |
+
if p.check(file=1):
|
| 1231 |
+
if checker:
|
| 1232 |
+
if not checker(p):
|
| 1233 |
+
continue
|
| 1234 |
+
return p
|
| 1235 |
+
except error.EACCES:
|
| 1236 |
+
pass
|
| 1237 |
+
return None
|
| 1238 |
+
|
| 1239 |
+
@classmethod
|
| 1240 |
+
def _gethomedir(cls):
|
| 1241 |
+
try:
|
| 1242 |
+
x = os.environ["HOME"]
|
| 1243 |
+
except KeyError:
|
| 1244 |
+
try:
|
| 1245 |
+
x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"]
|
| 1246 |
+
except KeyError:
|
| 1247 |
+
return None
|
| 1248 |
+
return cls(x)
|
| 1249 |
+
|
| 1250 |
+
# """
|
| 1251 |
+
# special class constructors for local filesystem paths
|
| 1252 |
+
# """
|
| 1253 |
+
@classmethod
|
| 1254 |
+
def get_temproot(cls):
|
| 1255 |
+
"""Return the system's temporary directory
|
| 1256 |
+
(where tempfiles are usually created in)
|
| 1257 |
+
"""
|
| 1258 |
+
import tempfile
|
| 1259 |
+
|
| 1260 |
+
return local(tempfile.gettempdir())
|
| 1261 |
+
|
| 1262 |
+
@classmethod
|
| 1263 |
+
def mkdtemp(cls, rootdir=None):
|
| 1264 |
+
"""Return a Path object pointing to a fresh new temporary directory
|
| 1265 |
+
(which we created ourselves).
|
| 1266 |
+
"""
|
| 1267 |
+
import tempfile
|
| 1268 |
+
|
| 1269 |
+
if rootdir is None:
|
| 1270 |
+
rootdir = cls.get_temproot()
|
| 1271 |
+
path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir))
|
| 1272 |
+
return cls(path)
|
| 1273 |
+
|
| 1274 |
+
@classmethod
|
| 1275 |
+
def make_numbered_dir(
|
| 1276 |
+
cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800
|
| 1277 |
+
): # two days
|
| 1278 |
+
"""Return unique directory with a number greater than the current
|
| 1279 |
+
maximum one. The number is assumed to start directly after prefix.
|
| 1280 |
+
if keep is true directories with a number less than (maxnum-keep)
|
| 1281 |
+
will be removed. If .lock files are used (lock_timeout non-zero),
|
| 1282 |
+
algorithm is multi-process safe.
|
| 1283 |
+
"""
|
| 1284 |
+
if rootdir is None:
|
| 1285 |
+
rootdir = cls.get_temproot()
|
| 1286 |
+
|
| 1287 |
+
nprefix = prefix.lower()
|
| 1288 |
+
|
| 1289 |
+
def parse_num(path):
|
| 1290 |
+
"""Parse the number out of a path (if it matches the prefix)"""
|
| 1291 |
+
nbasename = path.basename.lower()
|
| 1292 |
+
if nbasename.startswith(nprefix):
|
| 1293 |
+
try:
|
| 1294 |
+
return int(nbasename[len(nprefix) :])
|
| 1295 |
+
except ValueError:
|
| 1296 |
+
pass
|
| 1297 |
+
|
| 1298 |
+
def create_lockfile(path):
|
| 1299 |
+
"""Exclusively create lockfile. Throws when failed"""
|
| 1300 |
+
mypid = os.getpid()
|
| 1301 |
+
lockfile = path.join(".lock")
|
| 1302 |
+
if hasattr(lockfile, "mksymlinkto"):
|
| 1303 |
+
lockfile.mksymlinkto(str(mypid))
|
| 1304 |
+
else:
|
| 1305 |
+
fd = error.checked_call(
|
| 1306 |
+
os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644
|
| 1307 |
+
)
|
| 1308 |
+
with os.fdopen(fd, "w") as f:
|
| 1309 |
+
f.write(str(mypid))
|
| 1310 |
+
return lockfile
|
| 1311 |
+
|
| 1312 |
+
def atexit_remove_lockfile(lockfile):
|
| 1313 |
+
"""Ensure lockfile is removed at process exit"""
|
| 1314 |
+
mypid = os.getpid()
|
| 1315 |
+
|
| 1316 |
+
def try_remove_lockfile():
|
| 1317 |
+
# in a fork() situation, only the last process should
|
| 1318 |
+
# remove the .lock, otherwise the other processes run the
|
| 1319 |
+
# risk of seeing their temporary dir disappear. For now
|
| 1320 |
+
# we remove the .lock in the parent only (i.e. we assume
|
| 1321 |
+
# that the children finish before the parent).
|
| 1322 |
+
if os.getpid() != mypid:
|
| 1323 |
+
return
|
| 1324 |
+
try:
|
| 1325 |
+
lockfile.remove()
|
| 1326 |
+
except error.Error:
|
| 1327 |
+
pass
|
| 1328 |
+
|
| 1329 |
+
atexit.register(try_remove_lockfile)
|
| 1330 |
+
|
| 1331 |
+
# compute the maximum number currently in use with the prefix
|
| 1332 |
+
lastmax = None
|
| 1333 |
+
while True:
|
| 1334 |
+
maxnum = -1
|
| 1335 |
+
for path in rootdir.listdir():
|
| 1336 |
+
num = parse_num(path)
|
| 1337 |
+
if num is not None:
|
| 1338 |
+
maxnum = max(maxnum, num)
|
| 1339 |
+
|
| 1340 |
+
# make the new directory
|
| 1341 |
+
try:
|
| 1342 |
+
udir = rootdir.mkdir(prefix + str(maxnum + 1))
|
| 1343 |
+
if lock_timeout:
|
| 1344 |
+
lockfile = create_lockfile(udir)
|
| 1345 |
+
atexit_remove_lockfile(lockfile)
|
| 1346 |
+
except (error.EEXIST, error.ENOENT, error.EBUSY):
|
| 1347 |
+
# race condition (1): another thread/process created the dir
|
| 1348 |
+
# in the meantime - try again
|
| 1349 |
+
# race condition (2): another thread/process spuriously acquired
|
| 1350 |
+
# lock treating empty directory as candidate
|
| 1351 |
+
# for removal - try again
|
| 1352 |
+
# race condition (3): another thread/process tried to create the lock at
|
| 1353 |
+
# the same time (happened in Python 3.3 on Windows)
|
| 1354 |
+
# https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa
|
| 1355 |
+
if lastmax == maxnum:
|
| 1356 |
+
raise
|
| 1357 |
+
lastmax = maxnum
|
| 1358 |
+
continue
|
| 1359 |
+
break
|
| 1360 |
+
|
| 1361 |
+
def get_mtime(path):
|
| 1362 |
+
"""Read file modification time"""
|
| 1363 |
+
try:
|
| 1364 |
+
return path.lstat().mtime
|
| 1365 |
+
except error.Error:
|
| 1366 |
+
pass
|
| 1367 |
+
|
| 1368 |
+
garbage_prefix = prefix + "garbage-"
|
| 1369 |
+
|
| 1370 |
+
def is_garbage(path):
|
| 1371 |
+
"""Check if path denotes directory scheduled for removal"""
|
| 1372 |
+
bn = path.basename
|
| 1373 |
+
return bn.startswith(garbage_prefix)
|
| 1374 |
+
|
| 1375 |
+
# prune old directories
|
| 1376 |
+
udir_time = get_mtime(udir)
|
| 1377 |
+
if keep and udir_time:
|
| 1378 |
+
for path in rootdir.listdir():
|
| 1379 |
+
num = parse_num(path)
|
| 1380 |
+
if num is not None and num <= (maxnum - keep):
|
| 1381 |
+
try:
|
| 1382 |
+
# try acquiring lock to remove directory as exclusive user
|
| 1383 |
+
if lock_timeout:
|
| 1384 |
+
create_lockfile(path)
|
| 1385 |
+
except (error.EEXIST, error.ENOENT, error.EBUSY):
|
| 1386 |
+
path_time = get_mtime(path)
|
| 1387 |
+
if not path_time:
|
| 1388 |
+
# assume directory doesn't exist now
|
| 1389 |
+
continue
|
| 1390 |
+
if abs(udir_time - path_time) < lock_timeout:
|
| 1391 |
+
# assume directory with lockfile exists
|
| 1392 |
+
# and lock timeout hasn't expired yet
|
| 1393 |
+
continue
|
| 1394 |
+
|
| 1395 |
+
# path dir locked for exclusive use
|
| 1396 |
+
# and scheduled for removal to avoid another thread/process
|
| 1397 |
+
# treating it as a new directory or removal candidate
|
| 1398 |
+
garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4()))
|
| 1399 |
+
try:
|
| 1400 |
+
path.rename(garbage_path)
|
| 1401 |
+
garbage_path.remove(rec=1)
|
| 1402 |
+
except KeyboardInterrupt:
|
| 1403 |
+
raise
|
| 1404 |
+
except Exception: # this might be error.Error, WindowsError ...
|
| 1405 |
+
pass
|
| 1406 |
+
if is_garbage(path):
|
| 1407 |
+
try:
|
| 1408 |
+
path.remove(rec=1)
|
| 1409 |
+
except KeyboardInterrupt:
|
| 1410 |
+
raise
|
| 1411 |
+
except Exception: # this might be error.Error, WindowsError ...
|
| 1412 |
+
pass
|
| 1413 |
+
|
| 1414 |
+
# make link...
|
| 1415 |
+
try:
|
| 1416 |
+
username = os.environ["USER"] # linux, et al
|
| 1417 |
+
except KeyError:
|
| 1418 |
+
try:
|
| 1419 |
+
username = os.environ["USERNAME"] # windows
|
| 1420 |
+
except KeyError:
|
| 1421 |
+
username = "current"
|
| 1422 |
+
|
| 1423 |
+
src = str(udir)
|
| 1424 |
+
dest = src[: src.rfind("-")] + "-" + username
|
| 1425 |
+
try:
|
| 1426 |
+
os.unlink(dest)
|
| 1427 |
+
except OSError:
|
| 1428 |
+
pass
|
| 1429 |
+
try:
|
| 1430 |
+
os.symlink(src, dest)
|
| 1431 |
+
except (OSError, AttributeError, NotImplementedError):
|
| 1432 |
+
pass
|
| 1433 |
+
|
| 1434 |
+
return udir
|
| 1435 |
+
|
| 1436 |
+
|
| 1437 |
+
def copymode(src, dest):
|
| 1438 |
+
"""Copy permission from src to dst."""
|
| 1439 |
+
import shutil
|
| 1440 |
+
|
| 1441 |
+
shutil.copymode(src, dest)
|
| 1442 |
+
|
| 1443 |
+
|
| 1444 |
+
def copystat(src, dest):
|
| 1445 |
+
"""Copy permission, last modification time,
|
| 1446 |
+
last access time, and flags from src to dst."""
|
| 1447 |
+
import shutil
|
| 1448 |
+
|
| 1449 |
+
shutil.copystat(str(src), str(dest))
|
| 1450 |
+
|
| 1451 |
+
|
| 1452 |
+
def copychunked(src, dest):
|
| 1453 |
+
chunksize = 524288 # half a meg of bytes
|
| 1454 |
+
fsrc = src.open("rb")
|
| 1455 |
+
try:
|
| 1456 |
+
fdest = dest.open("wb")
|
| 1457 |
+
try:
|
| 1458 |
+
while 1:
|
| 1459 |
+
buf = fsrc.read(chunksize)
|
| 1460 |
+
if not buf:
|
| 1461 |
+
break
|
| 1462 |
+
fdest.write(buf)
|
| 1463 |
+
finally:
|
| 1464 |
+
fdest.close()
|
| 1465 |
+
finally:
|
| 1466 |
+
fsrc.close()
|
| 1467 |
+
|
| 1468 |
+
|
| 1469 |
+
def isimportable(name):
|
| 1470 |
+
if name and (name[0].isalpha() or name[0] == "_"):
|
| 1471 |
+
name = name.replace("_", "")
|
| 1472 |
+
return not name or name.isalnum()
|
| 1473 |
+
|
| 1474 |
+
|
| 1475 |
+
local = LocalPath
|
.venv/lib/python3.11/site-packages/_pytest/config/__init__.py
ADDED
|
@@ -0,0 +1,1973 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
"""Command line options, ini-file and conftest.py processing."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import collections.abc
|
| 8 |
+
import copy
|
| 9 |
+
import dataclasses
|
| 10 |
+
import enum
|
| 11 |
+
from functools import lru_cache
|
| 12 |
+
import glob
|
| 13 |
+
import importlib.metadata
|
| 14 |
+
import inspect
|
| 15 |
+
import os
|
| 16 |
+
import pathlib
|
| 17 |
+
import re
|
| 18 |
+
import shlex
|
| 19 |
+
import sys
|
| 20 |
+
from textwrap import dedent
|
| 21 |
+
import types
|
| 22 |
+
from types import FunctionType
|
| 23 |
+
from typing import Any
|
| 24 |
+
from typing import Callable
|
| 25 |
+
from typing import cast
|
| 26 |
+
from typing import Final
|
| 27 |
+
from typing import final
|
| 28 |
+
from typing import Generator
|
| 29 |
+
from typing import IO
|
| 30 |
+
from typing import Iterable
|
| 31 |
+
from typing import Iterator
|
| 32 |
+
from typing import Sequence
|
| 33 |
+
from typing import TextIO
|
| 34 |
+
from typing import Type
|
| 35 |
+
from typing import TYPE_CHECKING
|
| 36 |
+
import warnings
|
| 37 |
+
|
| 38 |
+
import pluggy
|
| 39 |
+
from pluggy import HookimplMarker
|
| 40 |
+
from pluggy import HookimplOpts
|
| 41 |
+
from pluggy import HookspecMarker
|
| 42 |
+
from pluggy import HookspecOpts
|
| 43 |
+
from pluggy import PluginManager
|
| 44 |
+
|
| 45 |
+
from .compat import PathAwareHookProxy
|
| 46 |
+
from .exceptions import PrintHelp as PrintHelp
|
| 47 |
+
from .exceptions import UsageError as UsageError
|
| 48 |
+
from .findpaths import determine_setup
|
| 49 |
+
from _pytest import __version__
|
| 50 |
+
import _pytest._code
|
| 51 |
+
from _pytest._code import ExceptionInfo
|
| 52 |
+
from _pytest._code import filter_traceback
|
| 53 |
+
from _pytest._code.code import TracebackStyle
|
| 54 |
+
from _pytest._io import TerminalWriter
|
| 55 |
+
from _pytest.config.argparsing import Argument
|
| 56 |
+
from _pytest.config.argparsing import Parser
|
| 57 |
+
import _pytest.deprecated
|
| 58 |
+
import _pytest.hookspec
|
| 59 |
+
from _pytest.outcomes import fail
|
| 60 |
+
from _pytest.outcomes import Skipped
|
| 61 |
+
from _pytest.pathlib import absolutepath
|
| 62 |
+
from _pytest.pathlib import bestrelpath
|
| 63 |
+
from _pytest.pathlib import import_path
|
| 64 |
+
from _pytest.pathlib import ImportMode
|
| 65 |
+
from _pytest.pathlib import resolve_package_path
|
| 66 |
+
from _pytest.pathlib import safe_exists
|
| 67 |
+
from _pytest.stash import Stash
|
| 68 |
+
from _pytest.warning_types import PytestConfigWarning
|
| 69 |
+
from _pytest.warning_types import warn_explicit_for
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
if TYPE_CHECKING:
|
| 73 |
+
from _pytest.cacheprovider import Cache
|
| 74 |
+
from _pytest.terminal import TerminalReporter
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
_PluggyPlugin = object
|
| 78 |
+
"""A type to represent plugin objects.
|
| 79 |
+
|
| 80 |
+
Plugins can be any namespace, so we can't narrow it down much, but we use an
|
| 81 |
+
alias to make the intent clear.
|
| 82 |
+
|
| 83 |
+
Ideally this type would be provided by pluggy itself.
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
hookimpl = HookimplMarker("pytest")
|
| 88 |
+
hookspec = HookspecMarker("pytest")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@final
|
| 92 |
+
class ExitCode(enum.IntEnum):
|
| 93 |
+
"""Encodes the valid exit codes by pytest.
|
| 94 |
+
|
| 95 |
+
Currently users and plugins may supply other exit codes as well.
|
| 96 |
+
|
| 97 |
+
.. versionadded:: 5.0
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
#: Tests passed.
|
| 101 |
+
OK = 0
|
| 102 |
+
#: Tests failed.
|
| 103 |
+
TESTS_FAILED = 1
|
| 104 |
+
#: pytest was interrupted.
|
| 105 |
+
INTERRUPTED = 2
|
| 106 |
+
#: An internal error got in the way.
|
| 107 |
+
INTERNAL_ERROR = 3
|
| 108 |
+
#: pytest was misused.
|
| 109 |
+
USAGE_ERROR = 4
|
| 110 |
+
#: pytest couldn't find tests.
|
| 111 |
+
NO_TESTS_COLLECTED = 5
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class ConftestImportFailure(Exception):
|
| 115 |
+
def __init__(
|
| 116 |
+
self,
|
| 117 |
+
path: pathlib.Path,
|
| 118 |
+
*,
|
| 119 |
+
cause: Exception,
|
| 120 |
+
) -> None:
|
| 121 |
+
self.path = path
|
| 122 |
+
self.cause = cause
|
| 123 |
+
|
| 124 |
+
def __str__(self) -> str:
|
| 125 |
+
return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def filter_traceback_for_conftest_import_failure(
|
| 129 |
+
entry: _pytest._code.TracebackEntry,
|
| 130 |
+
) -> bool:
|
| 131 |
+
"""Filter tracebacks entries which point to pytest internals or importlib.
|
| 132 |
+
|
| 133 |
+
Make a special case for importlib because we use it to import test modules and conftest files
|
| 134 |
+
in _pytest.pathlib.import_path.
|
| 135 |
+
"""
|
| 136 |
+
return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def main(
|
| 140 |
+
args: list[str] | os.PathLike[str] | None = None,
|
| 141 |
+
plugins: Sequence[str | _PluggyPlugin] | None = None,
|
| 142 |
+
) -> int | ExitCode:
|
| 143 |
+
"""Perform an in-process test run.
|
| 144 |
+
|
| 145 |
+
:param args:
|
| 146 |
+
List of command line arguments. If `None` or not given, defaults to reading
|
| 147 |
+
arguments directly from the process command line (:data:`sys.argv`).
|
| 148 |
+
:param plugins: List of plugin objects to be auto-registered during initialization.
|
| 149 |
+
|
| 150 |
+
:returns: An exit code.
|
| 151 |
+
"""
|
| 152 |
+
old_pytest_version = os.environ.get("PYTEST_VERSION")
|
| 153 |
+
try:
|
| 154 |
+
os.environ["PYTEST_VERSION"] = __version__
|
| 155 |
+
try:
|
| 156 |
+
config = _prepareconfig(args, plugins)
|
| 157 |
+
except ConftestImportFailure as e:
|
| 158 |
+
exc_info = ExceptionInfo.from_exception(e.cause)
|
| 159 |
+
tw = TerminalWriter(sys.stderr)
|
| 160 |
+
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
|
| 161 |
+
exc_info.traceback = exc_info.traceback.filter(
|
| 162 |
+
filter_traceback_for_conftest_import_failure
|
| 163 |
+
)
|
| 164 |
+
exc_repr = (
|
| 165 |
+
exc_info.getrepr(style="short", chain=False)
|
| 166 |
+
if exc_info.traceback
|
| 167 |
+
else exc_info.exconly()
|
| 168 |
+
)
|
| 169 |
+
formatted_tb = str(exc_repr)
|
| 170 |
+
for line in formatted_tb.splitlines():
|
| 171 |
+
tw.line(line.rstrip(), red=True)
|
| 172 |
+
return ExitCode.USAGE_ERROR
|
| 173 |
+
else:
|
| 174 |
+
try:
|
| 175 |
+
ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
|
| 176 |
+
try:
|
| 177 |
+
return ExitCode(ret)
|
| 178 |
+
except ValueError:
|
| 179 |
+
return ret
|
| 180 |
+
finally:
|
| 181 |
+
config._ensure_unconfigure()
|
| 182 |
+
except UsageError as e:
|
| 183 |
+
tw = TerminalWriter(sys.stderr)
|
| 184 |
+
for msg in e.args:
|
| 185 |
+
tw.line(f"ERROR: {msg}\n", red=True)
|
| 186 |
+
return ExitCode.USAGE_ERROR
|
| 187 |
+
finally:
|
| 188 |
+
if old_pytest_version is None:
|
| 189 |
+
os.environ.pop("PYTEST_VERSION", None)
|
| 190 |
+
else:
|
| 191 |
+
os.environ["PYTEST_VERSION"] = old_pytest_version
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def console_main() -> int:
|
| 195 |
+
"""The CLI entry point of pytest.
|
| 196 |
+
|
| 197 |
+
This function is not meant for programmable use; use `main()` instead.
|
| 198 |
+
"""
|
| 199 |
+
# https://docs.python.org/3/library/signal.html#note-on-sigpipe
|
| 200 |
+
try:
|
| 201 |
+
code = main()
|
| 202 |
+
sys.stdout.flush()
|
| 203 |
+
return code
|
| 204 |
+
except BrokenPipeError:
|
| 205 |
+
# Python flushes standard streams on exit; redirect remaining output
|
| 206 |
+
# to devnull to avoid another BrokenPipeError at shutdown
|
| 207 |
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
| 208 |
+
os.dup2(devnull, sys.stdout.fileno())
|
| 209 |
+
return 1 # Python exits with error code 1 on EPIPE
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class cmdline: # compatibility namespace
|
| 213 |
+
main = staticmethod(main)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def filename_arg(path: str, optname: str) -> str:
|
| 217 |
+
"""Argparse type validator for filename arguments.
|
| 218 |
+
|
| 219 |
+
:path: Path of filename.
|
| 220 |
+
:optname: Name of the option.
|
| 221 |
+
"""
|
| 222 |
+
if os.path.isdir(path):
|
| 223 |
+
raise UsageError(f"{optname} must be a filename, given: {path}")
|
| 224 |
+
return path
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def directory_arg(path: str, optname: str) -> str:
|
| 228 |
+
"""Argparse type validator for directory arguments.
|
| 229 |
+
|
| 230 |
+
:path: Path of directory.
|
| 231 |
+
:optname: Name of the option.
|
| 232 |
+
"""
|
| 233 |
+
if not os.path.isdir(path):
|
| 234 |
+
raise UsageError(f"{optname} must be a directory, given: {path}")
|
| 235 |
+
return path
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
# Plugins that cannot be disabled via "-p no:X" currently.
|
| 239 |
+
essential_plugins = (
|
| 240 |
+
"mark",
|
| 241 |
+
"main",
|
| 242 |
+
"runner",
|
| 243 |
+
"fixtures",
|
| 244 |
+
"helpconfig", # Provides -p.
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
default_plugins = (
|
| 248 |
+
*essential_plugins,
|
| 249 |
+
"python",
|
| 250 |
+
"terminal",
|
| 251 |
+
"debugging",
|
| 252 |
+
"unittest",
|
| 253 |
+
"capture",
|
| 254 |
+
"skipping",
|
| 255 |
+
"legacypath",
|
| 256 |
+
"tmpdir",
|
| 257 |
+
"monkeypatch",
|
| 258 |
+
"recwarn",
|
| 259 |
+
"pastebin",
|
| 260 |
+
"assertion",
|
| 261 |
+
"junitxml",
|
| 262 |
+
"doctest",
|
| 263 |
+
"cacheprovider",
|
| 264 |
+
"freeze_support",
|
| 265 |
+
"setuponly",
|
| 266 |
+
"setupplan",
|
| 267 |
+
"stepwise",
|
| 268 |
+
"warnings",
|
| 269 |
+
"logging",
|
| 270 |
+
"reports",
|
| 271 |
+
"python_path",
|
| 272 |
+
"unraisableexception",
|
| 273 |
+
"threadexception",
|
| 274 |
+
"faulthandler",
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
builtin_plugins = set(default_plugins)
|
| 278 |
+
builtin_plugins.add("pytester")
|
| 279 |
+
builtin_plugins.add("pytester_assertions")
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def get_config(
|
| 283 |
+
args: list[str] | None = None,
|
| 284 |
+
plugins: Sequence[str | _PluggyPlugin] | None = None,
|
| 285 |
+
) -> Config:
|
| 286 |
+
# subsequent calls to main will create a fresh instance
|
| 287 |
+
pluginmanager = PytestPluginManager()
|
| 288 |
+
config = Config(
|
| 289 |
+
pluginmanager,
|
| 290 |
+
invocation_params=Config.InvocationParams(
|
| 291 |
+
args=args or (),
|
| 292 |
+
plugins=plugins,
|
| 293 |
+
dir=pathlib.Path.cwd(),
|
| 294 |
+
),
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
if args is not None:
|
| 298 |
+
# Handle any "-p no:plugin" args.
|
| 299 |
+
pluginmanager.consider_preparse(args, exclude_only=True)
|
| 300 |
+
|
| 301 |
+
for spec in default_plugins:
|
| 302 |
+
pluginmanager.import_plugin(spec)
|
| 303 |
+
|
| 304 |
+
return config
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def get_plugin_manager() -> PytestPluginManager:
|
| 308 |
+
"""Obtain a new instance of the
|
| 309 |
+
:py:class:`pytest.PytestPluginManager`, with default plugins
|
| 310 |
+
already loaded.
|
| 311 |
+
|
| 312 |
+
This function can be used by integration with other tools, like hooking
|
| 313 |
+
into pytest to run tests into an IDE.
|
| 314 |
+
"""
|
| 315 |
+
return get_config().pluginmanager
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def _prepareconfig(
|
| 319 |
+
args: list[str] | os.PathLike[str] | None = None,
|
| 320 |
+
plugins: Sequence[str | _PluggyPlugin] | None = None,
|
| 321 |
+
) -> Config:
|
| 322 |
+
if args is None:
|
| 323 |
+
args = sys.argv[1:]
|
| 324 |
+
elif isinstance(args, os.PathLike):
|
| 325 |
+
args = [os.fspath(args)]
|
| 326 |
+
elif not isinstance(args, list):
|
| 327 |
+
msg = ( # type:ignore[unreachable]
|
| 328 |
+
"`args` parameter expected to be a list of strings, got: {!r} (type: {})"
|
| 329 |
+
)
|
| 330 |
+
raise TypeError(msg.format(args, type(args)))
|
| 331 |
+
|
| 332 |
+
config = get_config(args, plugins)
|
| 333 |
+
pluginmanager = config.pluginmanager
|
| 334 |
+
try:
|
| 335 |
+
if plugins:
|
| 336 |
+
for plugin in plugins:
|
| 337 |
+
if isinstance(plugin, str):
|
| 338 |
+
pluginmanager.consider_pluginarg(plugin)
|
| 339 |
+
else:
|
| 340 |
+
pluginmanager.register(plugin)
|
| 341 |
+
config = pluginmanager.hook.pytest_cmdline_parse(
|
| 342 |
+
pluginmanager=pluginmanager, args=args
|
| 343 |
+
)
|
| 344 |
+
return config
|
| 345 |
+
except BaseException:
|
| 346 |
+
config._ensure_unconfigure()
|
| 347 |
+
raise
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def _get_directory(path: pathlib.Path) -> pathlib.Path:
|
| 351 |
+
"""Get the directory of a path - itself if already a directory."""
|
| 352 |
+
if path.is_file():
|
| 353 |
+
return path.parent
|
| 354 |
+
else:
|
| 355 |
+
return path
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def _get_legacy_hook_marks(
|
| 359 |
+
method: Any,
|
| 360 |
+
hook_type: str,
|
| 361 |
+
opt_names: tuple[str, ...],
|
| 362 |
+
) -> dict[str, bool]:
|
| 363 |
+
if TYPE_CHECKING:
|
| 364 |
+
# abuse typeguard from importlib to avoid massive method type union that's lacking an alias
|
| 365 |
+
assert inspect.isroutine(method)
|
| 366 |
+
known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
|
| 367 |
+
must_warn: list[str] = []
|
| 368 |
+
opts: dict[str, bool] = {}
|
| 369 |
+
for opt_name in opt_names:
|
| 370 |
+
opt_attr = getattr(method, opt_name, AttributeError)
|
| 371 |
+
if opt_attr is not AttributeError:
|
| 372 |
+
must_warn.append(f"{opt_name}={opt_attr}")
|
| 373 |
+
opts[opt_name] = True
|
| 374 |
+
elif opt_name in known_marks:
|
| 375 |
+
must_warn.append(f"{opt_name}=True")
|
| 376 |
+
opts[opt_name] = True
|
| 377 |
+
else:
|
| 378 |
+
opts[opt_name] = False
|
| 379 |
+
if must_warn:
|
| 380 |
+
hook_opts = ", ".join(must_warn)
|
| 381 |
+
message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
|
| 382 |
+
type=hook_type,
|
| 383 |
+
fullname=method.__qualname__,
|
| 384 |
+
hook_opts=hook_opts,
|
| 385 |
+
)
|
| 386 |
+
warn_explicit_for(cast(FunctionType, method), message)
|
| 387 |
+
return opts
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@final
|
| 391 |
+
class PytestPluginManager(PluginManager):
|
| 392 |
+
"""A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
|
| 393 |
+
additional pytest-specific functionality:
|
| 394 |
+
|
| 395 |
+
* Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
|
| 396 |
+
``pytest_plugins`` global variables found in plugins being loaded.
|
| 397 |
+
* ``conftest.py`` loading during start-up.
|
| 398 |
+
"""
|
| 399 |
+
|
| 400 |
+
def __init__(self) -> None:
|
| 401 |
+
import _pytest.assertion
|
| 402 |
+
|
| 403 |
+
super().__init__("pytest")
|
| 404 |
+
|
| 405 |
+
# -- State related to local conftest plugins.
|
| 406 |
+
# All loaded conftest modules.
|
| 407 |
+
self._conftest_plugins: set[types.ModuleType] = set()
|
| 408 |
+
# All conftest modules applicable for a directory.
|
| 409 |
+
# This includes the directory's own conftest modules as well
|
| 410 |
+
# as those of its parent directories.
|
| 411 |
+
self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {}
|
| 412 |
+
# Cutoff directory above which conftests are no longer discovered.
|
| 413 |
+
self._confcutdir: pathlib.Path | None = None
|
| 414 |
+
# If set, conftest loading is skipped.
|
| 415 |
+
self._noconftest = False
|
| 416 |
+
|
| 417 |
+
# _getconftestmodules()'s call to _get_directory() causes a stat
|
| 418 |
+
# storm when it's called potentially thousands of times in a test
|
| 419 |
+
# session (#9478), often with the same path, so cache it.
|
| 420 |
+
self._get_directory = lru_cache(256)(_get_directory)
|
| 421 |
+
|
| 422 |
+
# plugins that were explicitly skipped with pytest.skip
|
| 423 |
+
# list of (module name, skip reason)
|
| 424 |
+
# previously we would issue a warning when a plugin was skipped, but
|
| 425 |
+
# since we refactored warnings as first citizens of Config, they are
|
| 426 |
+
# just stored here to be used later.
|
| 427 |
+
self.skipped_plugins: list[tuple[str, str]] = []
|
| 428 |
+
|
| 429 |
+
self.add_hookspecs(_pytest.hookspec)
|
| 430 |
+
self.register(self)
|
| 431 |
+
if os.environ.get("PYTEST_DEBUG"):
|
| 432 |
+
err: IO[str] = sys.stderr
|
| 433 |
+
encoding: str = getattr(err, "encoding", "utf8")
|
| 434 |
+
try:
|
| 435 |
+
err = open(
|
| 436 |
+
os.dup(err.fileno()),
|
| 437 |
+
mode=err.mode,
|
| 438 |
+
buffering=1,
|
| 439 |
+
encoding=encoding,
|
| 440 |
+
)
|
| 441 |
+
except Exception:
|
| 442 |
+
pass
|
| 443 |
+
self.trace.root.setwriter(err.write)
|
| 444 |
+
self.enable_tracing()
|
| 445 |
+
|
| 446 |
+
# Config._consider_importhook will set a real object if required.
|
| 447 |
+
self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
|
| 448 |
+
# Used to know when we are importing conftests after the pytest_configure stage.
|
| 449 |
+
self._configured = False
|
| 450 |
+
|
| 451 |
+
def parse_hookimpl_opts(
|
| 452 |
+
self, plugin: _PluggyPlugin, name: str
|
| 453 |
+
) -> HookimplOpts | None:
|
| 454 |
+
""":meta private:"""
|
| 455 |
+
# pytest hooks are always prefixed with "pytest_",
|
| 456 |
+
# so we avoid accessing possibly non-readable attributes
|
| 457 |
+
# (see issue #1073).
|
| 458 |
+
if not name.startswith("pytest_"):
|
| 459 |
+
return None
|
| 460 |
+
# Ignore names which cannot be hooks.
|
| 461 |
+
if name == "pytest_plugins":
|
| 462 |
+
return None
|
| 463 |
+
|
| 464 |
+
opts = super().parse_hookimpl_opts(plugin, name)
|
| 465 |
+
if opts is not None:
|
| 466 |
+
return opts
|
| 467 |
+
|
| 468 |
+
method = getattr(plugin, name)
|
| 469 |
+
# Consider only actual functions for hooks (#3775).
|
| 470 |
+
if not inspect.isroutine(method):
|
| 471 |
+
return None
|
| 472 |
+
# Collect unmarked hooks as long as they have the `pytest_' prefix.
|
| 473 |
+
return _get_legacy_hook_marks( # type: ignore[return-value]
|
| 474 |
+
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
def parse_hookspec_opts(self, module_or_class, name: str) -> HookspecOpts | None:
|
| 478 |
+
""":meta private:"""
|
| 479 |
+
opts = super().parse_hookspec_opts(module_or_class, name)
|
| 480 |
+
if opts is None:
|
| 481 |
+
method = getattr(module_or_class, name)
|
| 482 |
+
if name.startswith("pytest_"):
|
| 483 |
+
opts = _get_legacy_hook_marks( # type: ignore[assignment]
|
| 484 |
+
method,
|
| 485 |
+
"spec",
|
| 486 |
+
("firstresult", "historic"),
|
| 487 |
+
)
|
| 488 |
+
return opts
|
| 489 |
+
|
| 490 |
+
def register(self, plugin: _PluggyPlugin, name: str | None = None) -> str | None:
|
| 491 |
+
if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
|
| 492 |
+
warnings.warn(
|
| 493 |
+
PytestConfigWarning(
|
| 494 |
+
"{} plugin has been merged into the core, "
|
| 495 |
+
"please remove it from your requirements.".format(
|
| 496 |
+
name.replace("_", "-")
|
| 497 |
+
)
|
| 498 |
+
)
|
| 499 |
+
)
|
| 500 |
+
return None
|
| 501 |
+
plugin_name = super().register(plugin, name)
|
| 502 |
+
if plugin_name is not None:
|
| 503 |
+
self.hook.pytest_plugin_registered.call_historic(
|
| 504 |
+
kwargs=dict(
|
| 505 |
+
plugin=plugin,
|
| 506 |
+
plugin_name=plugin_name,
|
| 507 |
+
manager=self,
|
| 508 |
+
)
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
if isinstance(plugin, types.ModuleType):
|
| 512 |
+
self.consider_module(plugin)
|
| 513 |
+
return plugin_name
|
| 514 |
+
|
| 515 |
+
def getplugin(self, name: str):
|
| 516 |
+
# Support deprecated naming because plugins (xdist e.g.) use it.
|
| 517 |
+
plugin: _PluggyPlugin | None = self.get_plugin(name)
|
| 518 |
+
return plugin
|
| 519 |
+
|
| 520 |
+
def hasplugin(self, name: str) -> bool:
|
| 521 |
+
"""Return whether a plugin with the given name is registered."""
|
| 522 |
+
return bool(self.get_plugin(name))
|
| 523 |
+
|
| 524 |
+
def pytest_configure(self, config: Config) -> None:
|
| 525 |
+
""":meta private:"""
|
| 526 |
+
# XXX now that the pluginmanager exposes hookimpl(tryfirst...)
|
| 527 |
+
# we should remove tryfirst/trylast as markers.
|
| 528 |
+
config.addinivalue_line(
|
| 529 |
+
"markers",
|
| 530 |
+
"tryfirst: mark a hook implementation function such that the "
|
| 531 |
+
"plugin machinery will try to call it first/as early as possible. "
|
| 532 |
+
"DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.",
|
| 533 |
+
)
|
| 534 |
+
config.addinivalue_line(
|
| 535 |
+
"markers",
|
| 536 |
+
"trylast: mark a hook implementation function such that the "
|
| 537 |
+
"plugin machinery will try to call it last/as late as possible. "
|
| 538 |
+
"DEPRECATED, use @pytest.hookimpl(trylast=True) instead.",
|
| 539 |
+
)
|
| 540 |
+
self._configured = True
|
| 541 |
+
|
| 542 |
+
#
|
| 543 |
+
# Internal API for local conftest plugin handling.
|
| 544 |
+
#
|
| 545 |
+
def _set_initial_conftests(
|
| 546 |
+
self,
|
| 547 |
+
args: Sequence[str | pathlib.Path],
|
| 548 |
+
pyargs: bool,
|
| 549 |
+
noconftest: bool,
|
| 550 |
+
rootpath: pathlib.Path,
|
| 551 |
+
confcutdir: pathlib.Path | None,
|
| 552 |
+
invocation_dir: pathlib.Path,
|
| 553 |
+
importmode: ImportMode | str,
|
| 554 |
+
*,
|
| 555 |
+
consider_namespace_packages: bool,
|
| 556 |
+
) -> None:
|
| 557 |
+
"""Load initial conftest files given a preparsed "namespace".
|
| 558 |
+
|
| 559 |
+
As conftest files may add their own command line options which have
|
| 560 |
+
arguments ('--my-opt somepath') we might get some false positives.
|
| 561 |
+
All builtin and 3rd party plugins will have been loaded, however, so
|
| 562 |
+
common options will not confuse our logic here.
|
| 563 |
+
"""
|
| 564 |
+
self._confcutdir = (
|
| 565 |
+
absolutepath(invocation_dir / confcutdir) if confcutdir else None
|
| 566 |
+
)
|
| 567 |
+
self._noconftest = noconftest
|
| 568 |
+
self._using_pyargs = pyargs
|
| 569 |
+
foundanchor = False
|
| 570 |
+
for initial_path in args:
|
| 571 |
+
path = str(initial_path)
|
| 572 |
+
# remove node-id syntax
|
| 573 |
+
i = path.find("::")
|
| 574 |
+
if i != -1:
|
| 575 |
+
path = path[:i]
|
| 576 |
+
anchor = absolutepath(invocation_dir / path)
|
| 577 |
+
|
| 578 |
+
# Ensure we do not break if what appears to be an anchor
|
| 579 |
+
# is in fact a very long option (#10169, #11394).
|
| 580 |
+
if safe_exists(anchor):
|
| 581 |
+
self._try_load_conftest(
|
| 582 |
+
anchor,
|
| 583 |
+
importmode,
|
| 584 |
+
rootpath,
|
| 585 |
+
consider_namespace_packages=consider_namespace_packages,
|
| 586 |
+
)
|
| 587 |
+
foundanchor = True
|
| 588 |
+
if not foundanchor:
|
| 589 |
+
self._try_load_conftest(
|
| 590 |
+
invocation_dir,
|
| 591 |
+
importmode,
|
| 592 |
+
rootpath,
|
| 593 |
+
consider_namespace_packages=consider_namespace_packages,
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
def _is_in_confcutdir(self, path: pathlib.Path) -> bool:
|
| 597 |
+
"""Whether to consider the given path to load conftests from."""
|
| 598 |
+
if self._confcutdir is None:
|
| 599 |
+
return True
|
| 600 |
+
# The semantics here are literally:
|
| 601 |
+
# Do not load a conftest if it is found upwards from confcut dir.
|
| 602 |
+
# But this is *not* the same as:
|
| 603 |
+
# Load only conftests from confcutdir or below.
|
| 604 |
+
# At first glance they might seem the same thing, however we do support use cases where
|
| 605 |
+
# we want to load conftests that are not found in confcutdir or below, but are found
|
| 606 |
+
# in completely different directory hierarchies like packages installed
|
| 607 |
+
# in out-of-source trees.
|
| 608 |
+
# (see #9767 for a regression where the logic was inverted).
|
| 609 |
+
return path not in self._confcutdir.parents
|
| 610 |
+
|
| 611 |
+
def _try_load_conftest(
|
| 612 |
+
self,
|
| 613 |
+
anchor: pathlib.Path,
|
| 614 |
+
importmode: str | ImportMode,
|
| 615 |
+
rootpath: pathlib.Path,
|
| 616 |
+
*,
|
| 617 |
+
consider_namespace_packages: bool,
|
| 618 |
+
) -> None:
|
| 619 |
+
self._loadconftestmodules(
|
| 620 |
+
anchor,
|
| 621 |
+
importmode,
|
| 622 |
+
rootpath,
|
| 623 |
+
consider_namespace_packages=consider_namespace_packages,
|
| 624 |
+
)
|
| 625 |
+
# let's also consider test* subdirs
|
| 626 |
+
if anchor.is_dir():
|
| 627 |
+
for x in anchor.glob("test*"):
|
| 628 |
+
if x.is_dir():
|
| 629 |
+
self._loadconftestmodules(
|
| 630 |
+
x,
|
| 631 |
+
importmode,
|
| 632 |
+
rootpath,
|
| 633 |
+
consider_namespace_packages=consider_namespace_packages,
|
| 634 |
+
)
|
| 635 |
+
|
| 636 |
+
def _loadconftestmodules(
|
| 637 |
+
self,
|
| 638 |
+
path: pathlib.Path,
|
| 639 |
+
importmode: str | ImportMode,
|
| 640 |
+
rootpath: pathlib.Path,
|
| 641 |
+
*,
|
| 642 |
+
consider_namespace_packages: bool,
|
| 643 |
+
) -> None:
|
| 644 |
+
if self._noconftest:
|
| 645 |
+
return
|
| 646 |
+
|
| 647 |
+
directory = self._get_directory(path)
|
| 648 |
+
|
| 649 |
+
# Optimization: avoid repeated searches in the same directory.
|
| 650 |
+
# Assumes always called with same importmode and rootpath.
|
| 651 |
+
if directory in self._dirpath2confmods:
|
| 652 |
+
return
|
| 653 |
+
|
| 654 |
+
clist = []
|
| 655 |
+
for parent in reversed((directory, *directory.parents)):
|
| 656 |
+
if self._is_in_confcutdir(parent):
|
| 657 |
+
conftestpath = parent / "conftest.py"
|
| 658 |
+
if conftestpath.is_file():
|
| 659 |
+
mod = self._importconftest(
|
| 660 |
+
conftestpath,
|
| 661 |
+
importmode,
|
| 662 |
+
rootpath,
|
| 663 |
+
consider_namespace_packages=consider_namespace_packages,
|
| 664 |
+
)
|
| 665 |
+
clist.append(mod)
|
| 666 |
+
self._dirpath2confmods[directory] = clist
|
| 667 |
+
|
| 668 |
+
def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]:
|
| 669 |
+
directory = self._get_directory(path)
|
| 670 |
+
return self._dirpath2confmods.get(directory, ())
|
| 671 |
+
|
| 672 |
+
def _rget_with_confmod(
|
| 673 |
+
self,
|
| 674 |
+
name: str,
|
| 675 |
+
path: pathlib.Path,
|
| 676 |
+
) -> tuple[types.ModuleType, Any]:
|
| 677 |
+
modules = self._getconftestmodules(path)
|
| 678 |
+
for mod in reversed(modules):
|
| 679 |
+
try:
|
| 680 |
+
return mod, getattr(mod, name)
|
| 681 |
+
except AttributeError:
|
| 682 |
+
continue
|
| 683 |
+
raise KeyError(name)
|
| 684 |
+
|
| 685 |
+
def _importconftest(
|
| 686 |
+
self,
|
| 687 |
+
conftestpath: pathlib.Path,
|
| 688 |
+
importmode: str | ImportMode,
|
| 689 |
+
rootpath: pathlib.Path,
|
| 690 |
+
*,
|
| 691 |
+
consider_namespace_packages: bool,
|
| 692 |
+
) -> types.ModuleType:
|
| 693 |
+
conftestpath_plugin_name = str(conftestpath)
|
| 694 |
+
existing = self.get_plugin(conftestpath_plugin_name)
|
| 695 |
+
if existing is not None:
|
| 696 |
+
return cast(types.ModuleType, existing)
|
| 697 |
+
|
| 698 |
+
# conftest.py files there are not in a Python package all have module
|
| 699 |
+
# name "conftest", and thus conflict with each other. Clear the existing
|
| 700 |
+
# before loading the new one, otherwise the existing one will be
|
| 701 |
+
# returned from the module cache.
|
| 702 |
+
pkgpath = resolve_package_path(conftestpath)
|
| 703 |
+
if pkgpath is None:
|
| 704 |
+
try:
|
| 705 |
+
del sys.modules[conftestpath.stem]
|
| 706 |
+
except KeyError:
|
| 707 |
+
pass
|
| 708 |
+
|
| 709 |
+
try:
|
| 710 |
+
mod = import_path(
|
| 711 |
+
conftestpath,
|
| 712 |
+
mode=importmode,
|
| 713 |
+
root=rootpath,
|
| 714 |
+
consider_namespace_packages=consider_namespace_packages,
|
| 715 |
+
)
|
| 716 |
+
except Exception as e:
|
| 717 |
+
assert e.__traceback__ is not None
|
| 718 |
+
raise ConftestImportFailure(conftestpath, cause=e) from e
|
| 719 |
+
|
| 720 |
+
self._check_non_top_pytest_plugins(mod, conftestpath)
|
| 721 |
+
|
| 722 |
+
self._conftest_plugins.add(mod)
|
| 723 |
+
dirpath = conftestpath.parent
|
| 724 |
+
if dirpath in self._dirpath2confmods:
|
| 725 |
+
for path, mods in self._dirpath2confmods.items():
|
| 726 |
+
if dirpath in path.parents or path == dirpath:
|
| 727 |
+
if mod in mods:
|
| 728 |
+
raise AssertionError(
|
| 729 |
+
f"While trying to load conftest path {conftestpath!s}, "
|
| 730 |
+
f"found that the module {mod} is already loaded with path {mod.__file__}. "
|
| 731 |
+
"This is not supposed to happen. Please report this issue to pytest."
|
| 732 |
+
)
|
| 733 |
+
mods.append(mod)
|
| 734 |
+
self.trace(f"loading conftestmodule {mod!r}")
|
| 735 |
+
self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
|
| 736 |
+
return mod
|
| 737 |
+
|
| 738 |
+
def _check_non_top_pytest_plugins(
|
| 739 |
+
self,
|
| 740 |
+
mod: types.ModuleType,
|
| 741 |
+
conftestpath: pathlib.Path,
|
| 742 |
+
) -> None:
|
| 743 |
+
if (
|
| 744 |
+
hasattr(mod, "pytest_plugins")
|
| 745 |
+
and self._configured
|
| 746 |
+
and not self._using_pyargs
|
| 747 |
+
):
|
| 748 |
+
msg = (
|
| 749 |
+
"Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n"
|
| 750 |
+
"It affects the entire test suite instead of just below the conftest as expected.\n"
|
| 751 |
+
" {}\n"
|
| 752 |
+
"Please move it to a top level conftest file at the rootdir:\n"
|
| 753 |
+
" {}\n"
|
| 754 |
+
"For more information, visit:\n"
|
| 755 |
+
" https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files"
|
| 756 |
+
)
|
| 757 |
+
fail(msg.format(conftestpath, self._confcutdir), pytrace=False)
|
| 758 |
+
|
| 759 |
+
#
|
| 760 |
+
# API for bootstrapping plugin loading
|
| 761 |
+
#
|
| 762 |
+
#
|
| 763 |
+
|
| 764 |
+
def consider_preparse(
|
| 765 |
+
self, args: Sequence[str], *, exclude_only: bool = False
|
| 766 |
+
) -> None:
|
| 767 |
+
""":meta private:"""
|
| 768 |
+
i = 0
|
| 769 |
+
n = len(args)
|
| 770 |
+
while i < n:
|
| 771 |
+
opt = args[i]
|
| 772 |
+
i += 1
|
| 773 |
+
if isinstance(opt, str):
|
| 774 |
+
if opt == "-p":
|
| 775 |
+
try:
|
| 776 |
+
parg = args[i]
|
| 777 |
+
except IndexError:
|
| 778 |
+
return
|
| 779 |
+
i += 1
|
| 780 |
+
elif opt.startswith("-p"):
|
| 781 |
+
parg = opt[2:]
|
| 782 |
+
else:
|
| 783 |
+
continue
|
| 784 |
+
parg = parg.strip()
|
| 785 |
+
if exclude_only and not parg.startswith("no:"):
|
| 786 |
+
continue
|
| 787 |
+
self.consider_pluginarg(parg)
|
| 788 |
+
|
| 789 |
+
def consider_pluginarg(self, arg: str) -> None:
|
| 790 |
+
""":meta private:"""
|
| 791 |
+
if arg.startswith("no:"):
|
| 792 |
+
name = arg[3:]
|
| 793 |
+
if name in essential_plugins:
|
| 794 |
+
raise UsageError(f"plugin {name} cannot be disabled")
|
| 795 |
+
|
| 796 |
+
# PR #4304: remove stepwise if cacheprovider is blocked.
|
| 797 |
+
if name == "cacheprovider":
|
| 798 |
+
self.set_blocked("stepwise")
|
| 799 |
+
self.set_blocked("pytest_stepwise")
|
| 800 |
+
|
| 801 |
+
self.set_blocked(name)
|
| 802 |
+
if not name.startswith("pytest_"):
|
| 803 |
+
self.set_blocked("pytest_" + name)
|
| 804 |
+
else:
|
| 805 |
+
name = arg
|
| 806 |
+
# Unblock the plugin.
|
| 807 |
+
self.unblock(name)
|
| 808 |
+
if not name.startswith("pytest_"):
|
| 809 |
+
self.unblock("pytest_" + name)
|
| 810 |
+
self.import_plugin(arg, consider_entry_points=True)
|
| 811 |
+
|
| 812 |
+
def consider_conftest(
|
| 813 |
+
self, conftestmodule: types.ModuleType, registration_name: str
|
| 814 |
+
) -> None:
|
| 815 |
+
""":meta private:"""
|
| 816 |
+
self.register(conftestmodule, name=registration_name)
|
| 817 |
+
|
| 818 |
+
def consider_env(self) -> None:
|
| 819 |
+
""":meta private:"""
|
| 820 |
+
self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
|
| 821 |
+
|
| 822 |
+
def consider_module(self, mod: types.ModuleType) -> None:
|
| 823 |
+
""":meta private:"""
|
| 824 |
+
self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
|
| 825 |
+
|
| 826 |
+
def _import_plugin_specs(
|
| 827 |
+
self, spec: None | types.ModuleType | str | Sequence[str]
|
| 828 |
+
) -> None:
|
| 829 |
+
plugins = _get_plugin_specs_as_list(spec)
|
| 830 |
+
for import_spec in plugins:
|
| 831 |
+
self.import_plugin(import_spec)
|
| 832 |
+
|
| 833 |
+
def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
|
| 834 |
+
"""Import a plugin with ``modname``.
|
| 835 |
+
|
| 836 |
+
If ``consider_entry_points`` is True, entry point names are also
|
| 837 |
+
considered to find a plugin.
|
| 838 |
+
"""
|
| 839 |
+
# Most often modname refers to builtin modules, e.g. "pytester",
|
| 840 |
+
# "terminal" or "capture". Those plugins are registered under their
|
| 841 |
+
# basename for historic purposes but must be imported with the
|
| 842 |
+
# _pytest prefix.
|
| 843 |
+
assert isinstance(
|
| 844 |
+
modname, str
|
| 845 |
+
), f"module name as text required, got {modname!r}"
|
| 846 |
+
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
|
| 847 |
+
return
|
| 848 |
+
|
| 849 |
+
importspec = "_pytest." + modname if modname in builtin_plugins else modname
|
| 850 |
+
self.rewrite_hook.mark_rewrite(importspec)
|
| 851 |
+
|
| 852 |
+
if consider_entry_points:
|
| 853 |
+
loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
|
| 854 |
+
if loaded:
|
| 855 |
+
return
|
| 856 |
+
|
| 857 |
+
try:
|
| 858 |
+
__import__(importspec)
|
| 859 |
+
except ImportError as e:
|
| 860 |
+
raise ImportError(
|
| 861 |
+
f'Error importing plugin "{modname}": {e.args[0]}'
|
| 862 |
+
).with_traceback(e.__traceback__) from e
|
| 863 |
+
|
| 864 |
+
except Skipped as e:
|
| 865 |
+
self.skipped_plugins.append((modname, e.msg or ""))
|
| 866 |
+
else:
|
| 867 |
+
mod = sys.modules[importspec]
|
| 868 |
+
self.register(mod, modname)
|
| 869 |
+
|
| 870 |
+
|
| 871 |
+
def _get_plugin_specs_as_list(
|
| 872 |
+
specs: None | types.ModuleType | str | Sequence[str],
|
| 873 |
+
) -> list[str]:
|
| 874 |
+
"""Parse a plugins specification into a list of plugin names."""
|
| 875 |
+
# None means empty.
|
| 876 |
+
if specs is None:
|
| 877 |
+
return []
|
| 878 |
+
# Workaround for #3899 - a submodule which happens to be called "pytest_plugins".
|
| 879 |
+
if isinstance(specs, types.ModuleType):
|
| 880 |
+
return []
|
| 881 |
+
# Comma-separated list.
|
| 882 |
+
if isinstance(specs, str):
|
| 883 |
+
return specs.split(",") if specs else []
|
| 884 |
+
# Direct specification.
|
| 885 |
+
if isinstance(specs, collections.abc.Sequence):
|
| 886 |
+
return list(specs)
|
| 887 |
+
raise UsageError(
|
| 888 |
+
f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}"
|
| 889 |
+
)
|
| 890 |
+
|
| 891 |
+
|
| 892 |
+
class Notset:
|
| 893 |
+
def __repr__(self):
|
| 894 |
+
return "<NOTSET>"
|
| 895 |
+
|
| 896 |
+
|
| 897 |
+
notset = Notset()
|
| 898 |
+
|
| 899 |
+
|
| 900 |
+
def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
|
| 901 |
+
"""Given an iterable of file names in a source distribution, return the "names" that should
|
| 902 |
+
be marked for assertion rewrite.
|
| 903 |
+
|
| 904 |
+
For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
|
| 905 |
+
the assertion rewrite mechanism.
|
| 906 |
+
|
| 907 |
+
This function has to deal with dist-info based distributions and egg based distributions
|
| 908 |
+
(which are still very much in use for "editable" installs).
|
| 909 |
+
|
| 910 |
+
Here are the file names as seen in a dist-info based distribution:
|
| 911 |
+
|
| 912 |
+
pytest_mock/__init__.py
|
| 913 |
+
pytest_mock/_version.py
|
| 914 |
+
pytest_mock/plugin.py
|
| 915 |
+
pytest_mock.egg-info/PKG-INFO
|
| 916 |
+
|
| 917 |
+
Here are the file names as seen in an egg based distribution:
|
| 918 |
+
|
| 919 |
+
src/pytest_mock/__init__.py
|
| 920 |
+
src/pytest_mock/_version.py
|
| 921 |
+
src/pytest_mock/plugin.py
|
| 922 |
+
src/pytest_mock.egg-info/PKG-INFO
|
| 923 |
+
LICENSE
|
| 924 |
+
setup.py
|
| 925 |
+
|
| 926 |
+
We have to take in account those two distribution flavors in order to determine which
|
| 927 |
+
names should be considered for assertion rewriting.
|
| 928 |
+
|
| 929 |
+
More information:
|
| 930 |
+
https://github.com/pytest-dev/pytest-mock/issues/167
|
| 931 |
+
"""
|
| 932 |
+
package_files = list(package_files)
|
| 933 |
+
seen_some = False
|
| 934 |
+
for fn in package_files:
|
| 935 |
+
is_simple_module = "/" not in fn and fn.endswith(".py")
|
| 936 |
+
is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
|
| 937 |
+
if is_simple_module:
|
| 938 |
+
module_name, _ = os.path.splitext(fn)
|
| 939 |
+
# we ignore "setup.py" at the root of the distribution
|
| 940 |
+
# as well as editable installation finder modules made by setuptools
|
| 941 |
+
if module_name != "setup" and not module_name.startswith("__editable__"):
|
| 942 |
+
seen_some = True
|
| 943 |
+
yield module_name
|
| 944 |
+
elif is_package:
|
| 945 |
+
package_name = os.path.dirname(fn)
|
| 946 |
+
seen_some = True
|
| 947 |
+
yield package_name
|
| 948 |
+
|
| 949 |
+
if not seen_some:
|
| 950 |
+
# At this point we did not find any packages or modules suitable for assertion
|
| 951 |
+
# rewriting, so we try again by stripping the first path component (to account for
|
| 952 |
+
# "src" based source trees for example).
|
| 953 |
+
# This approach lets us have the common case continue to be fast, as egg-distributions
|
| 954 |
+
# are rarer.
|
| 955 |
+
new_package_files = []
|
| 956 |
+
for fn in package_files:
|
| 957 |
+
parts = fn.split("/")
|
| 958 |
+
new_fn = "/".join(parts[1:])
|
| 959 |
+
if new_fn:
|
| 960 |
+
new_package_files.append(new_fn)
|
| 961 |
+
if new_package_files:
|
| 962 |
+
yield from _iter_rewritable_modules(new_package_files)
|
| 963 |
+
|
| 964 |
+
|
| 965 |
+
@final
|
| 966 |
+
class Config:
|
| 967 |
+
"""Access to configuration values, pluginmanager and plugin hooks.
|
| 968 |
+
|
| 969 |
+
:param PytestPluginManager pluginmanager:
|
| 970 |
+
A pytest PluginManager.
|
| 971 |
+
|
| 972 |
+
:param InvocationParams invocation_params:
|
| 973 |
+
Object containing parameters regarding the :func:`pytest.main`
|
| 974 |
+
invocation.
|
| 975 |
+
"""
|
| 976 |
+
|
| 977 |
+
@final
|
| 978 |
+
@dataclasses.dataclass(frozen=True)
|
| 979 |
+
class InvocationParams:
|
| 980 |
+
"""Holds parameters passed during :func:`pytest.main`.
|
| 981 |
+
|
| 982 |
+
The object attributes are read-only.
|
| 983 |
+
|
| 984 |
+
.. versionadded:: 5.1
|
| 985 |
+
|
| 986 |
+
.. note::
|
| 987 |
+
|
| 988 |
+
Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``
|
| 989 |
+
ini option are handled by pytest, not being included in the ``args`` attribute.
|
| 990 |
+
|
| 991 |
+
Plugins accessing ``InvocationParams`` must be aware of that.
|
| 992 |
+
"""
|
| 993 |
+
|
| 994 |
+
args: tuple[str, ...]
|
| 995 |
+
"""The command-line arguments as passed to :func:`pytest.main`."""
|
| 996 |
+
plugins: Sequence[str | _PluggyPlugin] | None
|
| 997 |
+
"""Extra plugins, might be `None`."""
|
| 998 |
+
dir: pathlib.Path
|
| 999 |
+
"""The directory from which :func:`pytest.main` was invoked. :type: pathlib.Path"""
|
| 1000 |
+
|
| 1001 |
+
def __init__(
|
| 1002 |
+
self,
|
| 1003 |
+
*,
|
| 1004 |
+
args: Iterable[str],
|
| 1005 |
+
plugins: Sequence[str | _PluggyPlugin] | None,
|
| 1006 |
+
dir: pathlib.Path,
|
| 1007 |
+
) -> None:
|
| 1008 |
+
object.__setattr__(self, "args", tuple(args))
|
| 1009 |
+
object.__setattr__(self, "plugins", plugins)
|
| 1010 |
+
object.__setattr__(self, "dir", dir)
|
| 1011 |
+
|
| 1012 |
+
class ArgsSource(enum.Enum):
|
| 1013 |
+
"""Indicates the source of the test arguments.
|
| 1014 |
+
|
| 1015 |
+
.. versionadded:: 7.2
|
| 1016 |
+
"""
|
| 1017 |
+
|
| 1018 |
+
#: Command line arguments.
|
| 1019 |
+
ARGS = enum.auto()
|
| 1020 |
+
#: Invocation directory.
|
| 1021 |
+
INVOCATION_DIR = enum.auto()
|
| 1022 |
+
INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
|
| 1023 |
+
#: 'testpaths' configuration value.
|
| 1024 |
+
TESTPATHS = enum.auto()
|
| 1025 |
+
|
| 1026 |
+
# Set by cacheprovider plugin.
|
| 1027 |
+
cache: Cache
|
| 1028 |
+
|
| 1029 |
+
def __init__(
|
| 1030 |
+
self,
|
| 1031 |
+
pluginmanager: PytestPluginManager,
|
| 1032 |
+
*,
|
| 1033 |
+
invocation_params: InvocationParams | None = None,
|
| 1034 |
+
) -> None:
|
| 1035 |
+
from .argparsing import FILE_OR_DIR
|
| 1036 |
+
from .argparsing import Parser
|
| 1037 |
+
|
| 1038 |
+
if invocation_params is None:
|
| 1039 |
+
invocation_params = self.InvocationParams(
|
| 1040 |
+
args=(), plugins=None, dir=pathlib.Path.cwd()
|
| 1041 |
+
)
|
| 1042 |
+
|
| 1043 |
+
self.option = argparse.Namespace()
|
| 1044 |
+
"""Access to command line option as attributes.
|
| 1045 |
+
|
| 1046 |
+
:type: argparse.Namespace
|
| 1047 |
+
"""
|
| 1048 |
+
|
| 1049 |
+
self.invocation_params = invocation_params
|
| 1050 |
+
"""The parameters with which pytest was invoked.
|
| 1051 |
+
|
| 1052 |
+
:type: InvocationParams
|
| 1053 |
+
"""
|
| 1054 |
+
|
| 1055 |
+
_a = FILE_OR_DIR
|
| 1056 |
+
self._parser = Parser(
|
| 1057 |
+
usage=f"%(prog)s [options] [{_a}] [{_a}] [...]",
|
| 1058 |
+
processopt=self._processopt,
|
| 1059 |
+
_ispytest=True,
|
| 1060 |
+
)
|
| 1061 |
+
self.pluginmanager = pluginmanager
|
| 1062 |
+
"""The plugin manager handles plugin registration and hook invocation.
|
| 1063 |
+
|
| 1064 |
+
:type: PytestPluginManager
|
| 1065 |
+
"""
|
| 1066 |
+
|
| 1067 |
+
self.stash = Stash()
|
| 1068 |
+
"""A place where plugins can store information on the config for their
|
| 1069 |
+
own use.
|
| 1070 |
+
|
| 1071 |
+
:type: Stash
|
| 1072 |
+
"""
|
| 1073 |
+
# Deprecated alias. Was never public. Can be removed in a few releases.
|
| 1074 |
+
self._store = self.stash
|
| 1075 |
+
|
| 1076 |
+
self.trace = self.pluginmanager.trace.root.get("config")
|
| 1077 |
+
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
|
| 1078 |
+
self._inicache: dict[str, Any] = {}
|
| 1079 |
+
self._override_ini: Sequence[str] = ()
|
| 1080 |
+
self._opt2dest: dict[str, str] = {}
|
| 1081 |
+
self._cleanup: list[Callable[[], None]] = []
|
| 1082 |
+
self.pluginmanager.register(self, "pytestconfig")
|
| 1083 |
+
self._configured = False
|
| 1084 |
+
self.hook.pytest_addoption.call_historic(
|
| 1085 |
+
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
|
| 1086 |
+
)
|
| 1087 |
+
self.args_source = Config.ArgsSource.ARGS
|
| 1088 |
+
self.args: list[str] = []
|
| 1089 |
+
|
| 1090 |
+
@property
|
| 1091 |
+
def rootpath(self) -> pathlib.Path:
|
| 1092 |
+
"""The path to the :ref:`rootdir <rootdir>`.
|
| 1093 |
+
|
| 1094 |
+
:type: pathlib.Path
|
| 1095 |
+
|
| 1096 |
+
.. versionadded:: 6.1
|
| 1097 |
+
"""
|
| 1098 |
+
return self._rootpath
|
| 1099 |
+
|
| 1100 |
+
@property
|
| 1101 |
+
def inipath(self) -> pathlib.Path | None:
|
| 1102 |
+
"""The path to the :ref:`configfile <configfiles>`.
|
| 1103 |
+
|
| 1104 |
+
.. versionadded:: 6.1
|
| 1105 |
+
"""
|
| 1106 |
+
return self._inipath
|
| 1107 |
+
|
| 1108 |
+
def add_cleanup(self, func: Callable[[], None]) -> None:
|
| 1109 |
+
"""Add a function to be called when the config object gets out of
|
| 1110 |
+
use (usually coinciding with pytest_unconfigure)."""
|
| 1111 |
+
self._cleanup.append(func)
|
| 1112 |
+
|
| 1113 |
+
def _do_configure(self) -> None:
|
| 1114 |
+
assert not self._configured
|
| 1115 |
+
self._configured = True
|
| 1116 |
+
with warnings.catch_warnings():
|
| 1117 |
+
warnings.simplefilter("default")
|
| 1118 |
+
self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
|
| 1119 |
+
|
| 1120 |
+
def _ensure_unconfigure(self) -> None:
|
| 1121 |
+
if self._configured:
|
| 1122 |
+
self._configured = False
|
| 1123 |
+
self.hook.pytest_unconfigure(config=self)
|
| 1124 |
+
self.hook.pytest_configure._call_history = []
|
| 1125 |
+
while self._cleanup:
|
| 1126 |
+
fin = self._cleanup.pop()
|
| 1127 |
+
fin()
|
| 1128 |
+
|
| 1129 |
+
def get_terminal_writer(self) -> TerminalWriter:
|
| 1130 |
+
terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin(
|
| 1131 |
+
"terminalreporter"
|
| 1132 |
+
)
|
| 1133 |
+
assert terminalreporter is not None
|
| 1134 |
+
return terminalreporter._tw
|
| 1135 |
+
|
| 1136 |
+
def pytest_cmdline_parse(
|
| 1137 |
+
self, pluginmanager: PytestPluginManager, args: list[str]
|
| 1138 |
+
) -> Config:
|
| 1139 |
+
try:
|
| 1140 |
+
self.parse(args)
|
| 1141 |
+
except UsageError:
|
| 1142 |
+
# Handle --version and --help here in a minimal fashion.
|
| 1143 |
+
# This gets done via helpconfig normally, but its
|
| 1144 |
+
# pytest_cmdline_main is not called in case of errors.
|
| 1145 |
+
if getattr(self.option, "version", False) or "--version" in args:
|
| 1146 |
+
from _pytest.helpconfig import showversion
|
| 1147 |
+
|
| 1148 |
+
showversion(self)
|
| 1149 |
+
elif (
|
| 1150 |
+
getattr(self.option, "help", False) or "--help" in args or "-h" in args
|
| 1151 |
+
):
|
| 1152 |
+
self._parser._getparser().print_help()
|
| 1153 |
+
sys.stdout.write(
|
| 1154 |
+
"\nNOTE: displaying only minimal help due to UsageError.\n\n"
|
| 1155 |
+
)
|
| 1156 |
+
|
| 1157 |
+
raise
|
| 1158 |
+
|
| 1159 |
+
return self
|
| 1160 |
+
|
| 1161 |
+
def notify_exception(
|
| 1162 |
+
self,
|
| 1163 |
+
excinfo: ExceptionInfo[BaseException],
|
| 1164 |
+
option: argparse.Namespace | None = None,
|
| 1165 |
+
) -> None:
|
| 1166 |
+
if option and getattr(option, "fulltrace", False):
|
| 1167 |
+
style: TracebackStyle = "long"
|
| 1168 |
+
else:
|
| 1169 |
+
style = "native"
|
| 1170 |
+
excrepr = excinfo.getrepr(
|
| 1171 |
+
funcargs=True, showlocals=getattr(option, "showlocals", False), style=style
|
| 1172 |
+
)
|
| 1173 |
+
res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
|
| 1174 |
+
if not any(res):
|
| 1175 |
+
for line in str(excrepr).split("\n"):
|
| 1176 |
+
sys.stderr.write(f"INTERNALERROR> {line}\n")
|
| 1177 |
+
sys.stderr.flush()
|
| 1178 |
+
|
| 1179 |
+
def cwd_relative_nodeid(self, nodeid: str) -> str:
|
| 1180 |
+
# nodeid's are relative to the rootpath, compute relative to cwd.
|
| 1181 |
+
if self.invocation_params.dir != self.rootpath:
|
| 1182 |
+
base_path_part, *nodeid_part = nodeid.split("::")
|
| 1183 |
+
# Only process path part
|
| 1184 |
+
fullpath = self.rootpath / base_path_part
|
| 1185 |
+
relative_path = bestrelpath(self.invocation_params.dir, fullpath)
|
| 1186 |
+
|
| 1187 |
+
nodeid = "::".join([relative_path, *nodeid_part])
|
| 1188 |
+
return nodeid
|
| 1189 |
+
|
| 1190 |
+
@classmethod
|
| 1191 |
+
def fromdictargs(cls, option_dict, args) -> Config:
|
| 1192 |
+
"""Constructor usable for subprocesses."""
|
| 1193 |
+
config = get_config(args)
|
| 1194 |
+
config.option.__dict__.update(option_dict)
|
| 1195 |
+
config.parse(args, addopts=False)
|
| 1196 |
+
for x in config.option.plugins:
|
| 1197 |
+
config.pluginmanager.consider_pluginarg(x)
|
| 1198 |
+
return config
|
| 1199 |
+
|
| 1200 |
+
def _processopt(self, opt: Argument) -> None:
|
| 1201 |
+
for name in opt._short_opts + opt._long_opts:
|
| 1202 |
+
self._opt2dest[name] = opt.dest
|
| 1203 |
+
|
| 1204 |
+
if hasattr(opt, "default"):
|
| 1205 |
+
if not hasattr(self.option, opt.dest):
|
| 1206 |
+
setattr(self.option, opt.dest, opt.default)
|
| 1207 |
+
|
| 1208 |
+
@hookimpl(trylast=True)
|
| 1209 |
+
def pytest_load_initial_conftests(self, early_config: Config) -> None:
|
| 1210 |
+
# We haven't fully parsed the command line arguments yet, so
|
| 1211 |
+
# early_config.args it not set yet. But we need it for
|
| 1212 |
+
# discovering the initial conftests. So "pre-run" the logic here.
|
| 1213 |
+
# It will be done for real in `parse()`.
|
| 1214 |
+
args, args_source = early_config._decide_args(
|
| 1215 |
+
args=early_config.known_args_namespace.file_or_dir,
|
| 1216 |
+
pyargs=early_config.known_args_namespace.pyargs,
|
| 1217 |
+
testpaths=early_config.getini("testpaths"),
|
| 1218 |
+
invocation_dir=early_config.invocation_params.dir,
|
| 1219 |
+
rootpath=early_config.rootpath,
|
| 1220 |
+
warn=False,
|
| 1221 |
+
)
|
| 1222 |
+
self.pluginmanager._set_initial_conftests(
|
| 1223 |
+
args=args,
|
| 1224 |
+
pyargs=early_config.known_args_namespace.pyargs,
|
| 1225 |
+
noconftest=early_config.known_args_namespace.noconftest,
|
| 1226 |
+
rootpath=early_config.rootpath,
|
| 1227 |
+
confcutdir=early_config.known_args_namespace.confcutdir,
|
| 1228 |
+
invocation_dir=early_config.invocation_params.dir,
|
| 1229 |
+
importmode=early_config.known_args_namespace.importmode,
|
| 1230 |
+
consider_namespace_packages=early_config.getini(
|
| 1231 |
+
"consider_namespace_packages"
|
| 1232 |
+
),
|
| 1233 |
+
)
|
| 1234 |
+
|
| 1235 |
+
def _initini(self, args: Sequence[str]) -> None:
|
| 1236 |
+
ns, unknown_args = self._parser.parse_known_and_unknown_args(
|
| 1237 |
+
args, namespace=copy.copy(self.option)
|
| 1238 |
+
)
|
| 1239 |
+
rootpath, inipath, inicfg = determine_setup(
|
| 1240 |
+
inifile=ns.inifilename,
|
| 1241 |
+
args=ns.file_or_dir + unknown_args,
|
| 1242 |
+
rootdir_cmd_arg=ns.rootdir or None,
|
| 1243 |
+
invocation_dir=self.invocation_params.dir,
|
| 1244 |
+
)
|
| 1245 |
+
self._rootpath = rootpath
|
| 1246 |
+
self._inipath = inipath
|
| 1247 |
+
self.inicfg = inicfg
|
| 1248 |
+
self._parser.extra_info["rootdir"] = str(self.rootpath)
|
| 1249 |
+
self._parser.extra_info["inifile"] = str(self.inipath)
|
| 1250 |
+
self._parser.addini("addopts", "Extra command line options", "args")
|
| 1251 |
+
self._parser.addini("minversion", "Minimally required pytest version")
|
| 1252 |
+
self._parser.addini(
|
| 1253 |
+
"required_plugins",
|
| 1254 |
+
"Plugins that must be present for pytest to run",
|
| 1255 |
+
type="args",
|
| 1256 |
+
default=[],
|
| 1257 |
+
)
|
| 1258 |
+
self._override_ini = ns.override_ini or ()
|
| 1259 |
+
|
| 1260 |
+
def _consider_importhook(self, args: Sequence[str]) -> None:
|
| 1261 |
+
"""Install the PEP 302 import hook if using assertion rewriting.
|
| 1262 |
+
|
| 1263 |
+
Needs to parse the --assert=<mode> option from the commandline
|
| 1264 |
+
and find all the installed plugins to mark them for rewriting
|
| 1265 |
+
by the importhook.
|
| 1266 |
+
"""
|
| 1267 |
+
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
|
| 1268 |
+
mode = getattr(ns, "assertmode", "plain")
|
| 1269 |
+
if mode == "rewrite":
|
| 1270 |
+
import _pytest.assertion
|
| 1271 |
+
|
| 1272 |
+
try:
|
| 1273 |
+
hook = _pytest.assertion.install_importhook(self)
|
| 1274 |
+
except SystemError:
|
| 1275 |
+
mode = "plain"
|
| 1276 |
+
else:
|
| 1277 |
+
self._mark_plugins_for_rewrite(hook)
|
| 1278 |
+
self._warn_about_missing_assertion(mode)
|
| 1279 |
+
|
| 1280 |
+
def _mark_plugins_for_rewrite(self, hook) -> None:
|
| 1281 |
+
"""Given an importhook, mark for rewrite any top-level
|
| 1282 |
+
modules or packages in the distribution package for
|
| 1283 |
+
all pytest plugins."""
|
| 1284 |
+
self.pluginmanager.rewrite_hook = hook
|
| 1285 |
+
|
| 1286 |
+
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
| 1287 |
+
# We don't autoload from distribution package entry points,
|
| 1288 |
+
# no need to continue.
|
| 1289 |
+
return
|
| 1290 |
+
|
| 1291 |
+
package_files = (
|
| 1292 |
+
str(file)
|
| 1293 |
+
for dist in importlib.metadata.distributions()
|
| 1294 |
+
if any(ep.group == "pytest11" for ep in dist.entry_points)
|
| 1295 |
+
for file in dist.files or []
|
| 1296 |
+
)
|
| 1297 |
+
|
| 1298 |
+
for name in _iter_rewritable_modules(package_files):
|
| 1299 |
+
hook.mark_rewrite(name)
|
| 1300 |
+
|
| 1301 |
+
def _validate_args(self, args: list[str], via: str) -> list[str]:
|
| 1302 |
+
"""Validate known args."""
|
| 1303 |
+
self._parser._config_source_hint = via # type: ignore
|
| 1304 |
+
try:
|
| 1305 |
+
self._parser.parse_known_and_unknown_args(
|
| 1306 |
+
args, namespace=copy.copy(self.option)
|
| 1307 |
+
)
|
| 1308 |
+
finally:
|
| 1309 |
+
del self._parser._config_source_hint # type: ignore
|
| 1310 |
+
|
| 1311 |
+
return args
|
| 1312 |
+
|
| 1313 |
+
def _decide_args(
|
| 1314 |
+
self,
|
| 1315 |
+
*,
|
| 1316 |
+
args: list[str],
|
| 1317 |
+
pyargs: bool,
|
| 1318 |
+
testpaths: list[str],
|
| 1319 |
+
invocation_dir: pathlib.Path,
|
| 1320 |
+
rootpath: pathlib.Path,
|
| 1321 |
+
warn: bool,
|
| 1322 |
+
) -> tuple[list[str], ArgsSource]:
|
| 1323 |
+
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
| 1324 |
+
|
| 1325 |
+
:param warn: Whether can issue warnings.
|
| 1326 |
+
|
| 1327 |
+
:returns: The args and the args source. Guaranteed to be non-empty.
|
| 1328 |
+
"""
|
| 1329 |
+
if args:
|
| 1330 |
+
source = Config.ArgsSource.ARGS
|
| 1331 |
+
result = args
|
| 1332 |
+
else:
|
| 1333 |
+
if invocation_dir == rootpath:
|
| 1334 |
+
source = Config.ArgsSource.TESTPATHS
|
| 1335 |
+
if pyargs:
|
| 1336 |
+
result = testpaths
|
| 1337 |
+
else:
|
| 1338 |
+
result = []
|
| 1339 |
+
for path in testpaths:
|
| 1340 |
+
result.extend(sorted(glob.iglob(path, recursive=True)))
|
| 1341 |
+
if testpaths and not result:
|
| 1342 |
+
if warn:
|
| 1343 |
+
warning_text = (
|
| 1344 |
+
"No files were found in testpaths; "
|
| 1345 |
+
"consider removing or adjusting your testpaths configuration. "
|
| 1346 |
+
"Searching recursively from the current directory instead."
|
| 1347 |
+
)
|
| 1348 |
+
self.issue_config_time_warning(
|
| 1349 |
+
PytestConfigWarning(warning_text), stacklevel=3
|
| 1350 |
+
)
|
| 1351 |
+
else:
|
| 1352 |
+
result = []
|
| 1353 |
+
if not result:
|
| 1354 |
+
source = Config.ArgsSource.INVOCATION_DIR
|
| 1355 |
+
result = [str(invocation_dir)]
|
| 1356 |
+
return result, source
|
| 1357 |
+
|
| 1358 |
+
def _preparse(self, args: list[str], addopts: bool = True) -> None:
|
| 1359 |
+
if addopts:
|
| 1360 |
+
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
| 1361 |
+
if len(env_addopts):
|
| 1362 |
+
args[:] = (
|
| 1363 |
+
self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
|
| 1364 |
+
+ args
|
| 1365 |
+
)
|
| 1366 |
+
self._initini(args)
|
| 1367 |
+
if addopts:
|
| 1368 |
+
args[:] = (
|
| 1369 |
+
self._validate_args(self.getini("addopts"), "via addopts config") + args
|
| 1370 |
+
)
|
| 1371 |
+
|
| 1372 |
+
self.known_args_namespace = self._parser.parse_known_args(
|
| 1373 |
+
args, namespace=copy.copy(self.option)
|
| 1374 |
+
)
|
| 1375 |
+
self._checkversion()
|
| 1376 |
+
self._consider_importhook(args)
|
| 1377 |
+
self.pluginmanager.consider_preparse(args, exclude_only=False)
|
| 1378 |
+
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
| 1379 |
+
# Don't autoload from distribution package entry point. Only
|
| 1380 |
+
# explicitly specified plugins are going to be loaded.
|
| 1381 |
+
self.pluginmanager.load_setuptools_entrypoints("pytest11")
|
| 1382 |
+
self.pluginmanager.consider_env()
|
| 1383 |
+
|
| 1384 |
+
self.known_args_namespace = self._parser.parse_known_args(
|
| 1385 |
+
args, namespace=copy.copy(self.known_args_namespace)
|
| 1386 |
+
)
|
| 1387 |
+
|
| 1388 |
+
self._validate_plugins()
|
| 1389 |
+
self._warn_about_skipped_plugins()
|
| 1390 |
+
|
| 1391 |
+
if self.known_args_namespace.confcutdir is None:
|
| 1392 |
+
if self.inipath is not None:
|
| 1393 |
+
confcutdir = str(self.inipath.parent)
|
| 1394 |
+
else:
|
| 1395 |
+
confcutdir = str(self.rootpath)
|
| 1396 |
+
self.known_args_namespace.confcutdir = confcutdir
|
| 1397 |
+
try:
|
| 1398 |
+
self.hook.pytest_load_initial_conftests(
|
| 1399 |
+
early_config=self, args=args, parser=self._parser
|
| 1400 |
+
)
|
| 1401 |
+
except ConftestImportFailure as e:
|
| 1402 |
+
if self.known_args_namespace.help or self.known_args_namespace.version:
|
| 1403 |
+
# we don't want to prevent --help/--version to work
|
| 1404 |
+
# so just let is pass and print a warning at the end
|
| 1405 |
+
self.issue_config_time_warning(
|
| 1406 |
+
PytestConfigWarning(f"could not load initial conftests: {e.path}"),
|
| 1407 |
+
stacklevel=2,
|
| 1408 |
+
)
|
| 1409 |
+
else:
|
| 1410 |
+
raise
|
| 1411 |
+
|
| 1412 |
+
@hookimpl(wrapper=True)
|
| 1413 |
+
def pytest_collection(self) -> Generator[None, object, object]:
|
| 1414 |
+
# Validate invalid ini keys after collection is done so we take in account
|
| 1415 |
+
# options added by late-loading conftest files.
|
| 1416 |
+
try:
|
| 1417 |
+
return (yield)
|
| 1418 |
+
finally:
|
| 1419 |
+
self._validate_config_options()
|
| 1420 |
+
|
| 1421 |
+
def _checkversion(self) -> None:
|
| 1422 |
+
import pytest
|
| 1423 |
+
|
| 1424 |
+
minver = self.inicfg.get("minversion", None)
|
| 1425 |
+
if minver:
|
| 1426 |
+
# Imported lazily to improve start-up time.
|
| 1427 |
+
from packaging.version import Version
|
| 1428 |
+
|
| 1429 |
+
if not isinstance(minver, str):
|
| 1430 |
+
raise pytest.UsageError(
|
| 1431 |
+
f"{self.inipath}: 'minversion' must be a single value"
|
| 1432 |
+
)
|
| 1433 |
+
|
| 1434 |
+
if Version(minver) > Version(pytest.__version__):
|
| 1435 |
+
raise pytest.UsageError(
|
| 1436 |
+
f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
|
| 1437 |
+
)
|
| 1438 |
+
|
| 1439 |
+
def _validate_config_options(self) -> None:
|
| 1440 |
+
for key in sorted(self._get_unknown_ini_keys()):
|
| 1441 |
+
self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")
|
| 1442 |
+
|
| 1443 |
+
def _validate_plugins(self) -> None:
|
| 1444 |
+
required_plugins = sorted(self.getini("required_plugins"))
|
| 1445 |
+
if not required_plugins:
|
| 1446 |
+
return
|
| 1447 |
+
|
| 1448 |
+
# Imported lazily to improve start-up time.
|
| 1449 |
+
from packaging.requirements import InvalidRequirement
|
| 1450 |
+
from packaging.requirements import Requirement
|
| 1451 |
+
from packaging.version import Version
|
| 1452 |
+
|
| 1453 |
+
plugin_info = self.pluginmanager.list_plugin_distinfo()
|
| 1454 |
+
plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
|
| 1455 |
+
|
| 1456 |
+
missing_plugins = []
|
| 1457 |
+
for required_plugin in required_plugins:
|
| 1458 |
+
try:
|
| 1459 |
+
req = Requirement(required_plugin)
|
| 1460 |
+
except InvalidRequirement:
|
| 1461 |
+
missing_plugins.append(required_plugin)
|
| 1462 |
+
continue
|
| 1463 |
+
|
| 1464 |
+
if req.name not in plugin_dist_info:
|
| 1465 |
+
missing_plugins.append(required_plugin)
|
| 1466 |
+
elif not req.specifier.contains(
|
| 1467 |
+
Version(plugin_dist_info[req.name]), prereleases=True
|
| 1468 |
+
):
|
| 1469 |
+
missing_plugins.append(required_plugin)
|
| 1470 |
+
|
| 1471 |
+
if missing_plugins:
|
| 1472 |
+
raise UsageError(
|
| 1473 |
+
"Missing required plugins: {}".format(", ".join(missing_plugins)),
|
| 1474 |
+
)
|
| 1475 |
+
|
| 1476 |
+
def _warn_or_fail_if_strict(self, message: str) -> None:
|
| 1477 |
+
if self.known_args_namespace.strict_config:
|
| 1478 |
+
raise UsageError(message)
|
| 1479 |
+
|
| 1480 |
+
self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
|
| 1481 |
+
|
| 1482 |
+
def _get_unknown_ini_keys(self) -> list[str]:
|
| 1483 |
+
parser_inicfg = self._parser._inidict
|
| 1484 |
+
return [name for name in self.inicfg if name not in parser_inicfg]
|
| 1485 |
+
|
| 1486 |
+
def parse(self, args: list[str], addopts: bool = True) -> None:
|
| 1487 |
+
# Parse given cmdline arguments into this config object.
|
| 1488 |
+
assert (
|
| 1489 |
+
self.args == []
|
| 1490 |
+
), "can only parse cmdline args at most once per Config object"
|
| 1491 |
+
self.hook.pytest_addhooks.call_historic(
|
| 1492 |
+
kwargs=dict(pluginmanager=self.pluginmanager)
|
| 1493 |
+
)
|
| 1494 |
+
self._preparse(args, addopts=addopts)
|
| 1495 |
+
self._parser.after_preparse = True # type: ignore
|
| 1496 |
+
try:
|
| 1497 |
+
args = self._parser.parse_setoption(
|
| 1498 |
+
args, self.option, namespace=self.option
|
| 1499 |
+
)
|
| 1500 |
+
self.args, self.args_source = self._decide_args(
|
| 1501 |
+
args=args,
|
| 1502 |
+
pyargs=self.known_args_namespace.pyargs,
|
| 1503 |
+
testpaths=self.getini("testpaths"),
|
| 1504 |
+
invocation_dir=self.invocation_params.dir,
|
| 1505 |
+
rootpath=self.rootpath,
|
| 1506 |
+
warn=True,
|
| 1507 |
+
)
|
| 1508 |
+
except PrintHelp:
|
| 1509 |
+
pass
|
| 1510 |
+
|
| 1511 |
+
def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
|
| 1512 |
+
"""Issue and handle a warning during the "configure" stage.
|
| 1513 |
+
|
| 1514 |
+
During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
|
| 1515 |
+
function because it is not possible to have hook wrappers around ``pytest_configure``.
|
| 1516 |
+
|
| 1517 |
+
This function is mainly intended for plugins that need to issue warnings during
|
| 1518 |
+
``pytest_configure`` (or similar stages).
|
| 1519 |
+
|
| 1520 |
+
:param warning: The warning instance.
|
| 1521 |
+
:param stacklevel: stacklevel forwarded to warnings.warn.
|
| 1522 |
+
"""
|
| 1523 |
+
if self.pluginmanager.is_blocked("warnings"):
|
| 1524 |
+
return
|
| 1525 |
+
|
| 1526 |
+
cmdline_filters = self.known_args_namespace.pythonwarnings or []
|
| 1527 |
+
config_filters = self.getini("filterwarnings")
|
| 1528 |
+
|
| 1529 |
+
with warnings.catch_warnings(record=True) as records:
|
| 1530 |
+
warnings.simplefilter("always", type(warning))
|
| 1531 |
+
apply_warning_filters(config_filters, cmdline_filters)
|
| 1532 |
+
warnings.warn(warning, stacklevel=stacklevel)
|
| 1533 |
+
|
| 1534 |
+
if records:
|
| 1535 |
+
frame = sys._getframe(stacklevel - 1)
|
| 1536 |
+
location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
|
| 1537 |
+
self.hook.pytest_warning_recorded.call_historic(
|
| 1538 |
+
kwargs=dict(
|
| 1539 |
+
warning_message=records[0],
|
| 1540 |
+
when="config",
|
| 1541 |
+
nodeid="",
|
| 1542 |
+
location=location,
|
| 1543 |
+
)
|
| 1544 |
+
)
|
| 1545 |
+
|
| 1546 |
+
def addinivalue_line(self, name: str, line: str) -> None:
|
| 1547 |
+
"""Add a line to an ini-file option. The option must have been
|
| 1548 |
+
declared but might not yet be set in which case the line becomes
|
| 1549 |
+
the first line in its value."""
|
| 1550 |
+
x = self.getini(name)
|
| 1551 |
+
assert isinstance(x, list)
|
| 1552 |
+
x.append(line) # modifies the cached list inline
|
| 1553 |
+
|
| 1554 |
+
def getini(self, name: str):
|
| 1555 |
+
"""Return configuration value from an :ref:`ini file <configfiles>`.
|
| 1556 |
+
|
| 1557 |
+
If a configuration value is not defined in an
|
| 1558 |
+
:ref:`ini file <configfiles>`, then the ``default`` value provided while
|
| 1559 |
+
registering the configuration through
|
| 1560 |
+
:func:`parser.addini <pytest.Parser.addini>` will be returned.
|
| 1561 |
+
Please note that you can even provide ``None`` as a valid
|
| 1562 |
+
default value.
|
| 1563 |
+
|
| 1564 |
+
If ``default`` is not provided while registering using
|
| 1565 |
+
:func:`parser.addini <pytest.Parser.addini>`, then a default value
|
| 1566 |
+
based on the ``type`` parameter passed to
|
| 1567 |
+
:func:`parser.addini <pytest.Parser.addini>` will be returned.
|
| 1568 |
+
The default values based on ``type`` are:
|
| 1569 |
+
``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
|
| 1570 |
+
``bool`` : ``False``
|
| 1571 |
+
``string`` : empty string ``""``
|
| 1572 |
+
|
| 1573 |
+
If neither the ``default`` nor the ``type`` parameter is passed
|
| 1574 |
+
while registering the configuration through
|
| 1575 |
+
:func:`parser.addini <pytest.Parser.addini>`, then the configuration
|
| 1576 |
+
is treated as a string and a default empty string '' is returned.
|
| 1577 |
+
|
| 1578 |
+
If the specified name hasn't been registered through a prior
|
| 1579 |
+
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
|
| 1580 |
+
plugin), a ValueError is raised.
|
| 1581 |
+
"""
|
| 1582 |
+
try:
|
| 1583 |
+
return self._inicache[name]
|
| 1584 |
+
except KeyError:
|
| 1585 |
+
self._inicache[name] = val = self._getini(name)
|
| 1586 |
+
return val
|
| 1587 |
+
|
| 1588 |
+
# Meant for easy monkeypatching by legacypath plugin.
|
| 1589 |
+
# Can be inlined back (with no cover removed) once legacypath is gone.
|
| 1590 |
+
def _getini_unknown_type(self, name: str, type: str, value: str | list[str]):
|
| 1591 |
+
msg = f"unknown configuration type: {type}"
|
| 1592 |
+
raise ValueError(msg, value) # pragma: no cover
|
| 1593 |
+
|
| 1594 |
+
def _getini(self, name: str):
|
| 1595 |
+
try:
|
| 1596 |
+
description, type, default = self._parser._inidict[name]
|
| 1597 |
+
except KeyError as e:
|
| 1598 |
+
raise ValueError(f"unknown configuration value: {name!r}") from e
|
| 1599 |
+
override_value = self._get_override_ini_value(name)
|
| 1600 |
+
if override_value is None:
|
| 1601 |
+
try:
|
| 1602 |
+
value = self.inicfg[name]
|
| 1603 |
+
except KeyError:
|
| 1604 |
+
return default
|
| 1605 |
+
else:
|
| 1606 |
+
value = override_value
|
| 1607 |
+
# Coerce the values based on types.
|
| 1608 |
+
#
|
| 1609 |
+
# Note: some coercions are only required if we are reading from .ini files, because
|
| 1610 |
+
# the file format doesn't contain type information, but when reading from toml we will
|
| 1611 |
+
# get either str or list of str values (see _parse_ini_config_from_pyproject_toml).
|
| 1612 |
+
# For example:
|
| 1613 |
+
#
|
| 1614 |
+
# ini:
|
| 1615 |
+
# a_line_list = "tests acceptance"
|
| 1616 |
+
# in this case, we need to split the string to obtain a list of strings.
|
| 1617 |
+
#
|
| 1618 |
+
# toml:
|
| 1619 |
+
# a_line_list = ["tests", "acceptance"]
|
| 1620 |
+
# in this case, we already have a list ready to use.
|
| 1621 |
+
#
|
| 1622 |
+
if type == "paths":
|
| 1623 |
+
dp = (
|
| 1624 |
+
self.inipath.parent
|
| 1625 |
+
if self.inipath is not None
|
| 1626 |
+
else self.invocation_params.dir
|
| 1627 |
+
)
|
| 1628 |
+
input_values = shlex.split(value) if isinstance(value, str) else value
|
| 1629 |
+
return [dp / x for x in input_values]
|
| 1630 |
+
elif type == "args":
|
| 1631 |
+
return shlex.split(value) if isinstance(value, str) else value
|
| 1632 |
+
elif type == "linelist":
|
| 1633 |
+
if isinstance(value, str):
|
| 1634 |
+
return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
|
| 1635 |
+
else:
|
| 1636 |
+
return value
|
| 1637 |
+
elif type == "bool":
|
| 1638 |
+
return _strtobool(str(value).strip())
|
| 1639 |
+
elif type == "string":
|
| 1640 |
+
return value
|
| 1641 |
+
elif type is None:
|
| 1642 |
+
return value
|
| 1643 |
+
else:
|
| 1644 |
+
return self._getini_unknown_type(name, type, value)
|
| 1645 |
+
|
| 1646 |
+
def _getconftest_pathlist(
|
| 1647 |
+
self, name: str, path: pathlib.Path
|
| 1648 |
+
) -> list[pathlib.Path] | None:
|
| 1649 |
+
try:
|
| 1650 |
+
mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
|
| 1651 |
+
except KeyError:
|
| 1652 |
+
return None
|
| 1653 |
+
assert mod.__file__ is not None
|
| 1654 |
+
modpath = pathlib.Path(mod.__file__).parent
|
| 1655 |
+
values: list[pathlib.Path] = []
|
| 1656 |
+
for relroot in relroots:
|
| 1657 |
+
if isinstance(relroot, os.PathLike):
|
| 1658 |
+
relroot = pathlib.Path(relroot)
|
| 1659 |
+
else:
|
| 1660 |
+
relroot = relroot.replace("/", os.sep)
|
| 1661 |
+
relroot = absolutepath(modpath / relroot)
|
| 1662 |
+
values.append(relroot)
|
| 1663 |
+
return values
|
| 1664 |
+
|
| 1665 |
+
def _get_override_ini_value(self, name: str) -> str | None:
|
| 1666 |
+
value = None
|
| 1667 |
+
# override_ini is a list of "ini=value" options.
|
| 1668 |
+
# Always use the last item if multiple values are set for same ini-name,
|
| 1669 |
+
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
|
| 1670 |
+
for ini_config in self._override_ini:
|
| 1671 |
+
try:
|
| 1672 |
+
key, user_ini_value = ini_config.split("=", 1)
|
| 1673 |
+
except ValueError as e:
|
| 1674 |
+
raise UsageError(
|
| 1675 |
+
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
|
| 1676 |
+
) from e
|
| 1677 |
+
else:
|
| 1678 |
+
if key == name:
|
| 1679 |
+
value = user_ini_value
|
| 1680 |
+
return value
|
| 1681 |
+
|
| 1682 |
+
def getoption(self, name: str, default=notset, skip: bool = False):
|
| 1683 |
+
"""Return command line option value.
|
| 1684 |
+
|
| 1685 |
+
:param name: Name of the option. You may also specify
|
| 1686 |
+
the literal ``--OPT`` option instead of the "dest" option name.
|
| 1687 |
+
:param default: Fallback value if no option of that name is **declared** via :hook:`pytest_addoption`.
|
| 1688 |
+
Note this parameter will be ignored when the option is **declared** even if the option's value is ``None``.
|
| 1689 |
+
:param skip: If ``True``, raise :func:`pytest.skip` if option is undeclared or has a ``None`` value.
|
| 1690 |
+
Note that even if ``True``, if a default was specified it will be returned instead of a skip.
|
| 1691 |
+
"""
|
| 1692 |
+
name = self._opt2dest.get(name, name)
|
| 1693 |
+
try:
|
| 1694 |
+
val = getattr(self.option, name)
|
| 1695 |
+
if val is None and skip:
|
| 1696 |
+
raise AttributeError(name)
|
| 1697 |
+
return val
|
| 1698 |
+
except AttributeError as e:
|
| 1699 |
+
if default is not notset:
|
| 1700 |
+
return default
|
| 1701 |
+
if skip:
|
| 1702 |
+
import pytest
|
| 1703 |
+
|
| 1704 |
+
pytest.skip(f"no {name!r} option found")
|
| 1705 |
+
raise ValueError(f"no option named {name!r}") from e
|
| 1706 |
+
|
| 1707 |
+
def getvalue(self, name: str, path=None):
|
| 1708 |
+
"""Deprecated, use getoption() instead."""
|
| 1709 |
+
return self.getoption(name)
|
| 1710 |
+
|
| 1711 |
+
def getvalueorskip(self, name: str, path=None):
|
| 1712 |
+
"""Deprecated, use getoption(skip=True) instead."""
|
| 1713 |
+
return self.getoption(name, skip=True)
|
| 1714 |
+
|
| 1715 |
+
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
|
| 1716 |
+
VERBOSITY_ASSERTIONS: Final = "assertions"
|
| 1717 |
+
#: Verbosity type for test case execution (see :confval:`verbosity_test_cases`).
|
| 1718 |
+
VERBOSITY_TEST_CASES: Final = "test_cases"
|
| 1719 |
+
_VERBOSITY_INI_DEFAULT: Final = "auto"
|
| 1720 |
+
|
| 1721 |
+
def get_verbosity(self, verbosity_type: str | None = None) -> int:
|
| 1722 |
+
r"""Retrieve the verbosity level for a fine-grained verbosity type.
|
| 1723 |
+
|
| 1724 |
+
:param verbosity_type: Verbosity type to get level for. If a level is
|
| 1725 |
+
configured for the given type, that value will be returned. If the
|
| 1726 |
+
given type is not a known verbosity type, the global verbosity
|
| 1727 |
+
level will be returned. If the given type is None (default), the
|
| 1728 |
+
global verbosity level will be returned.
|
| 1729 |
+
|
| 1730 |
+
To configure a level for a fine-grained verbosity type, the
|
| 1731 |
+
configuration file should have a setting for the configuration name
|
| 1732 |
+
and a numeric value for the verbosity level. A special value of "auto"
|
| 1733 |
+
can be used to explicitly use the global verbosity level.
|
| 1734 |
+
|
| 1735 |
+
Example:
|
| 1736 |
+
|
| 1737 |
+
.. code-block:: ini
|
| 1738 |
+
|
| 1739 |
+
# content of pytest.ini
|
| 1740 |
+
[pytest]
|
| 1741 |
+
verbosity_assertions = 2
|
| 1742 |
+
|
| 1743 |
+
.. code-block:: console
|
| 1744 |
+
|
| 1745 |
+
pytest -v
|
| 1746 |
+
|
| 1747 |
+
.. code-block:: python
|
| 1748 |
+
|
| 1749 |
+
print(config.get_verbosity()) # 1
|
| 1750 |
+
print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
|
| 1751 |
+
"""
|
| 1752 |
+
global_level = self.getoption("verbose", default=0)
|
| 1753 |
+
assert isinstance(global_level, int)
|
| 1754 |
+
if verbosity_type is None:
|
| 1755 |
+
return global_level
|
| 1756 |
+
|
| 1757 |
+
ini_name = Config._verbosity_ini_name(verbosity_type)
|
| 1758 |
+
if ini_name not in self._parser._inidict:
|
| 1759 |
+
return global_level
|
| 1760 |
+
|
| 1761 |
+
level = self.getini(ini_name)
|
| 1762 |
+
if level == Config._VERBOSITY_INI_DEFAULT:
|
| 1763 |
+
return global_level
|
| 1764 |
+
|
| 1765 |
+
return int(level)
|
| 1766 |
+
|
| 1767 |
+
@staticmethod
|
| 1768 |
+
def _verbosity_ini_name(verbosity_type: str) -> str:
|
| 1769 |
+
return f"verbosity_{verbosity_type}"
|
| 1770 |
+
|
| 1771 |
+
@staticmethod
|
| 1772 |
+
def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None:
|
| 1773 |
+
"""Add a output verbosity configuration option for the given output type.
|
| 1774 |
+
|
| 1775 |
+
:param parser: Parser for command line arguments and ini-file values.
|
| 1776 |
+
:param verbosity_type: Fine-grained verbosity category.
|
| 1777 |
+
:param help: Description of the output this type controls.
|
| 1778 |
+
|
| 1779 |
+
The value should be retrieved via a call to
|
| 1780 |
+
:py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
|
| 1781 |
+
"""
|
| 1782 |
+
parser.addini(
|
| 1783 |
+
Config._verbosity_ini_name(verbosity_type),
|
| 1784 |
+
help=help,
|
| 1785 |
+
type="string",
|
| 1786 |
+
default=Config._VERBOSITY_INI_DEFAULT,
|
| 1787 |
+
)
|
| 1788 |
+
|
| 1789 |
+
def _warn_about_missing_assertion(self, mode: str) -> None:
|
| 1790 |
+
if not _assertion_supported():
|
| 1791 |
+
if mode == "plain":
|
| 1792 |
+
warning_text = (
|
| 1793 |
+
"ASSERTIONS ARE NOT EXECUTED"
|
| 1794 |
+
" and FAILING TESTS WILL PASS. Are you"
|
| 1795 |
+
" using python -O?"
|
| 1796 |
+
)
|
| 1797 |
+
else:
|
| 1798 |
+
warning_text = (
|
| 1799 |
+
"assertions not in test modules or"
|
| 1800 |
+
" plugins will be ignored"
|
| 1801 |
+
" because assert statements are not executed "
|
| 1802 |
+
"by the underlying Python interpreter "
|
| 1803 |
+
"(are you using python -O?)\n"
|
| 1804 |
+
)
|
| 1805 |
+
self.issue_config_time_warning(
|
| 1806 |
+
PytestConfigWarning(warning_text),
|
| 1807 |
+
stacklevel=3,
|
| 1808 |
+
)
|
| 1809 |
+
|
| 1810 |
+
def _warn_about_skipped_plugins(self) -> None:
|
| 1811 |
+
for module_name, msg in self.pluginmanager.skipped_plugins:
|
| 1812 |
+
self.issue_config_time_warning(
|
| 1813 |
+
PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),
|
| 1814 |
+
stacklevel=2,
|
| 1815 |
+
)
|
| 1816 |
+
|
| 1817 |
+
|
| 1818 |
+
def _assertion_supported() -> bool:
|
| 1819 |
+
try:
|
| 1820 |
+
assert False
|
| 1821 |
+
except AssertionError:
|
| 1822 |
+
return True
|
| 1823 |
+
else:
|
| 1824 |
+
return False # type: ignore[unreachable]
|
| 1825 |
+
|
| 1826 |
+
|
| 1827 |
+
def create_terminal_writer(
|
| 1828 |
+
config: Config, file: TextIO | None = None
|
| 1829 |
+
) -> TerminalWriter:
|
| 1830 |
+
"""Create a TerminalWriter instance configured according to the options
|
| 1831 |
+
in the config object.
|
| 1832 |
+
|
| 1833 |
+
Every code which requires a TerminalWriter object and has access to a
|
| 1834 |
+
config object should use this function.
|
| 1835 |
+
"""
|
| 1836 |
+
tw = TerminalWriter(file=file)
|
| 1837 |
+
|
| 1838 |
+
if config.option.color == "yes":
|
| 1839 |
+
tw.hasmarkup = True
|
| 1840 |
+
elif config.option.color == "no":
|
| 1841 |
+
tw.hasmarkup = False
|
| 1842 |
+
|
| 1843 |
+
if config.option.code_highlight == "yes":
|
| 1844 |
+
tw.code_highlight = True
|
| 1845 |
+
elif config.option.code_highlight == "no":
|
| 1846 |
+
tw.code_highlight = False
|
| 1847 |
+
|
| 1848 |
+
return tw
|
| 1849 |
+
|
| 1850 |
+
|
| 1851 |
+
def _strtobool(val: str) -> bool:
|
| 1852 |
+
"""Convert a string representation of truth to True or False.
|
| 1853 |
+
|
| 1854 |
+
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
|
| 1855 |
+
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
|
| 1856 |
+
'val' is anything else.
|
| 1857 |
+
|
| 1858 |
+
.. note:: Copied from distutils.util.
|
| 1859 |
+
"""
|
| 1860 |
+
val = val.lower()
|
| 1861 |
+
if val in ("y", "yes", "t", "true", "on", "1"):
|
| 1862 |
+
return True
|
| 1863 |
+
elif val in ("n", "no", "f", "false", "off", "0"):
|
| 1864 |
+
return False
|
| 1865 |
+
else:
|
| 1866 |
+
raise ValueError(f"invalid truth value {val!r}")
|
| 1867 |
+
|
| 1868 |
+
|
| 1869 |
+
@lru_cache(maxsize=50)
|
| 1870 |
+
def parse_warning_filter(
|
| 1871 |
+
arg: str, *, escape: bool
|
| 1872 |
+
) -> tuple[warnings._ActionKind, str, type[Warning], str, int]:
|
| 1873 |
+
"""Parse a warnings filter string.
|
| 1874 |
+
|
| 1875 |
+
This is copied from warnings._setoption with the following changes:
|
| 1876 |
+
|
| 1877 |
+
* Does not apply the filter.
|
| 1878 |
+
* Escaping is optional.
|
| 1879 |
+
* Raises UsageError so we get nice error messages on failure.
|
| 1880 |
+
"""
|
| 1881 |
+
__tracebackhide__ = True
|
| 1882 |
+
error_template = dedent(
|
| 1883 |
+
f"""\
|
| 1884 |
+
while parsing the following warning configuration:
|
| 1885 |
+
|
| 1886 |
+
{arg}
|
| 1887 |
+
|
| 1888 |
+
This error occurred:
|
| 1889 |
+
|
| 1890 |
+
{{error}}
|
| 1891 |
+
"""
|
| 1892 |
+
)
|
| 1893 |
+
|
| 1894 |
+
parts = arg.split(":")
|
| 1895 |
+
if len(parts) > 5:
|
| 1896 |
+
doc_url = (
|
| 1897 |
+
"https://docs.python.org/3/library/warnings.html#describing-warning-filters"
|
| 1898 |
+
)
|
| 1899 |
+
error = dedent(
|
| 1900 |
+
f"""\
|
| 1901 |
+
Too many fields ({len(parts)}), expected at most 5 separated by colons:
|
| 1902 |
+
|
| 1903 |
+
action:message:category:module:line
|
| 1904 |
+
|
| 1905 |
+
For more information please consult: {doc_url}
|
| 1906 |
+
"""
|
| 1907 |
+
)
|
| 1908 |
+
raise UsageError(error_template.format(error=error))
|
| 1909 |
+
|
| 1910 |
+
while len(parts) < 5:
|
| 1911 |
+
parts.append("")
|
| 1912 |
+
action_, message, category_, module, lineno_ = (s.strip() for s in parts)
|
| 1913 |
+
try:
|
| 1914 |
+
action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined]
|
| 1915 |
+
except warnings._OptionError as e:
|
| 1916 |
+
raise UsageError(error_template.format(error=str(e))) from None
|
| 1917 |
+
try:
|
| 1918 |
+
category: type[Warning] = _resolve_warning_category(category_)
|
| 1919 |
+
except Exception:
|
| 1920 |
+
exc_info = ExceptionInfo.from_current()
|
| 1921 |
+
exception_text = exc_info.getrepr(style="native")
|
| 1922 |
+
raise UsageError(error_template.format(error=exception_text)) from None
|
| 1923 |
+
if message and escape:
|
| 1924 |
+
message = re.escape(message)
|
| 1925 |
+
if module and escape:
|
| 1926 |
+
module = re.escape(module) + r"\Z"
|
| 1927 |
+
if lineno_:
|
| 1928 |
+
try:
|
| 1929 |
+
lineno = int(lineno_)
|
| 1930 |
+
if lineno < 0:
|
| 1931 |
+
raise ValueError("number is negative")
|
| 1932 |
+
except ValueError as e:
|
| 1933 |
+
raise UsageError(
|
| 1934 |
+
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
|
| 1935 |
+
) from None
|
| 1936 |
+
else:
|
| 1937 |
+
lineno = 0
|
| 1938 |
+
return action, message, category, module, lineno
|
| 1939 |
+
|
| 1940 |
+
|
| 1941 |
+
def _resolve_warning_category(category: str) -> type[Warning]:
|
| 1942 |
+
"""
|
| 1943 |
+
Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
|
| 1944 |
+
propagate so we can get access to their tracebacks (#9218).
|
| 1945 |
+
"""
|
| 1946 |
+
__tracebackhide__ = True
|
| 1947 |
+
if not category:
|
| 1948 |
+
return Warning
|
| 1949 |
+
|
| 1950 |
+
if "." not in category:
|
| 1951 |
+
import builtins as m
|
| 1952 |
+
|
| 1953 |
+
klass = category
|
| 1954 |
+
else:
|
| 1955 |
+
module, _, klass = category.rpartition(".")
|
| 1956 |
+
m = __import__(module, None, None, [klass])
|
| 1957 |
+
cat = getattr(m, klass)
|
| 1958 |
+
if not issubclass(cat, Warning):
|
| 1959 |
+
raise UsageError(f"{cat} is not a Warning subclass")
|
| 1960 |
+
return cast(Type[Warning], cat)
|
| 1961 |
+
|
| 1962 |
+
|
| 1963 |
+
def apply_warning_filters(
|
| 1964 |
+
config_filters: Iterable[str], cmdline_filters: Iterable[str]
|
| 1965 |
+
) -> None:
|
| 1966 |
+
"""Applies pytest-configured filters to the warnings module"""
|
| 1967 |
+
# Filters should have this precedence: cmdline options, config.
|
| 1968 |
+
# Filters should be applied in the inverse order of precedence.
|
| 1969 |
+
for arg in config_filters:
|
| 1970 |
+
warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
|
| 1971 |
+
|
| 1972 |
+
for arg in cmdline_filters:
|
| 1973 |
+
warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
|