koichi12 commited on
Commit
87489a9
·
verified ·
1 Parent(s): 95b994f

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. .venv/lib/python3.11/site-packages/_pytest/__init__.py +13 -0
  3. .venv/lib/python3.11/site-packages/_pytest/__pycache__/cacheprovider.cpython-311.pyc +0 -0
  4. .venv/lib/python3.11/site-packages/_pytest/__pycache__/doctest.cpython-311.pyc +0 -0
  5. .venv/lib/python3.11/site-packages/_pytest/__pycache__/unraisableexception.cpython-311.pyc +0 -0
  6. .venv/lib/python3.11/site-packages/_pytest/_code/__init__.py +26 -0
  7. .venv/lib/python3.11/site-packages/_pytest/_code/__pycache__/__init__.cpython-311.pyc +0 -0
  8. .venv/lib/python3.11/site-packages/_pytest/_code/__pycache__/code.cpython-311.pyc +0 -0
  9. .venv/lib/python3.11/site-packages/_pytest/_code/__pycache__/source.cpython-311.pyc +0 -0
  10. .venv/lib/python3.11/site-packages/_pytest/_code/code.py +1409 -0
  11. .venv/lib/python3.11/site-packages/_pytest/_code/source.py +215 -0
  12. .venv/lib/python3.11/site-packages/_pytest/_io/__init__.py +10 -0
  13. .venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/__init__.cpython-311.pyc +0 -0
  14. .venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/pprint.cpython-311.pyc +0 -0
  15. .venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/saferepr.cpython-311.pyc +0 -0
  16. .venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/terminalwriter.cpython-311.pyc +0 -0
  17. .venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/wcwidth.cpython-311.pyc +0 -0
  18. .venv/lib/python3.11/site-packages/_pytest/_io/pprint.py +673 -0
  19. .venv/lib/python3.11/site-packages/_pytest/_io/saferepr.py +130 -0
  20. .venv/lib/python3.11/site-packages/_pytest/_io/terminalwriter.py +274 -0
  21. .venv/lib/python3.11/site-packages/_pytest/_io/wcwidth.py +57 -0
  22. .venv/lib/python3.11/site-packages/_pytest/_version.py +16 -0
  23. .venv/lib/python3.11/site-packages/_pytest/assertion/__init__.py +192 -0
  24. .venv/lib/python3.11/site-packages/_pytest/assertion/__pycache__/__init__.cpython-311.pyc +0 -0
  25. .venv/lib/python3.11/site-packages/_pytest/assertion/__pycache__/rewrite.cpython-311.pyc +0 -0
  26. .venv/lib/python3.11/site-packages/_pytest/assertion/__pycache__/truncate.cpython-311.pyc +0 -0
  27. .venv/lib/python3.11/site-packages/_pytest/assertion/__pycache__/util.cpython-311.pyc +0 -0
  28. .venv/lib/python3.11/site-packages/_pytest/assertion/rewrite.py +1211 -0
  29. .venv/lib/python3.11/site-packages/_pytest/assertion/truncate.py +117 -0
  30. .venv/lib/python3.11/site-packages/_pytest/assertion/util.py +609 -0
  31. .venv/lib/python3.11/site-packages/_pytest/capture.py +1087 -0
  32. .venv/lib/python3.11/site-packages/_pytest/debugging.py +385 -0
  33. .venv/lib/python3.11/site-packages/_pytest/deprecated.py +91 -0
  34. .venv/lib/python3.11/site-packages/_pytest/faulthandler.py +105 -0
  35. .venv/lib/python3.11/site-packages/_pytest/fixtures.py +1932 -0
  36. .venv/lib/python3.11/site-packages/_pytest/freeze_support.py +45 -0
  37. .venv/lib/python3.11/site-packages/_pytest/junitxml.py +697 -0
  38. .venv/lib/python3.11/site-packages/_pytest/main.py +1072 -0
  39. .venv/lib/python3.11/site-packages/_pytest/mark/__init__.py +292 -0
  40. .venv/lib/python3.11/site-packages/_pytest/mark/__pycache__/__init__.cpython-311.pyc +0 -0
  41. .venv/lib/python3.11/site-packages/_pytest/mark/__pycache__/expression.cpython-311.pyc +0 -0
  42. .venv/lib/python3.11/site-packages/_pytest/mark/__pycache__/structures.cpython-311.pyc +0 -0
  43. .venv/lib/python3.11/site-packages/_pytest/mark/expression.py +333 -0
  44. .venv/lib/python3.11/site-packages/_pytest/mark/structures.py +615 -0
  45. .venv/lib/python3.11/site-packages/_pytest/monkeypatch.py +415 -0
  46. .venv/lib/python3.11/site-packages/_pytest/outcomes.py +318 -0
  47. .venv/lib/python3.11/site-packages/_pytest/pastebin.py +113 -0
  48. .venv/lib/python3.11/site-packages/_pytest/pathlib.py +1038 -0
  49. .venv/lib/python3.11/site-packages/_pytest/py.typed +0 -0
  50. .venv/lib/python3.11/site-packages/_pytest/pytester.py +1766 -0
.gitattributes CHANGED
@@ -181,3 +181,5 @@ tuning-competition-baseline/.venv/lib/python3.11/site-packages/torch/_inductor/_
181
  .venv/lib/python3.11/site-packages/ray/rllib/env/__pycache__/multi_agent_episode.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
182
  .venv/lib/python3.11/site-packages/ray/tune/execution/__pycache__/tune_controller.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
 
 
 
181
  .venv/lib/python3.11/site-packages/ray/rllib/env/__pycache__/multi_agent_episode.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
182
  .venv/lib/python3.11/site-packages/ray/tune/execution/__pycache__/tune_controller.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
.venv/lib/python3.11/site-packages/_pytest/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+
4
+ __all__ = ["__version__", "version_tuple"]
5
+
6
+ try:
7
+ from ._version import version as __version__
8
+ from ._version import version_tuple
9
+ except ImportError: # pragma: no cover
10
+ # broken installation, we don't even try
11
+ # unknown only works because we do poor mans version compare
12
+ __version__ = "unknown"
13
+ version_tuple = (0, 0, "unknown")
.venv/lib/python3.11/site-packages/_pytest/__pycache__/cacheprovider.cpython-311.pyc ADDED
Binary file (34.2 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/__pycache__/doctest.cpython-311.pyc ADDED
Binary file (37.1 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/__pycache__/unraisableexception.cpython-311.pyc ADDED
Binary file (5.41 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/_code/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Python inspection/code generation API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .code import Code
6
+ from .code import ExceptionInfo
7
+ from .code import filter_traceback
8
+ from .code import Frame
9
+ from .code import getfslineno
10
+ from .code import Traceback
11
+ from .code import TracebackEntry
12
+ from .source import getrawcode
13
+ from .source import Source
14
+
15
+
16
+ __all__ = [
17
+ "Code",
18
+ "ExceptionInfo",
19
+ "filter_traceback",
20
+ "Frame",
21
+ "getfslineno",
22
+ "getrawcode",
23
+ "Traceback",
24
+ "TracebackEntry",
25
+ "Source",
26
+ ]
.venv/lib/python3.11/site-packages/_pytest/_code/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (842 Bytes). View file
 
.venv/lib/python3.11/site-packages/_pytest/_code/__pycache__/code.cpython-311.pyc ADDED
Binary file (68.1 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/_code/__pycache__/source.cpython-311.pyc ADDED
Binary file (12.7 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/_code/code.py ADDED
@@ -0,0 +1,1409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ import dataclasses
6
+ import inspect
7
+ from inspect import CO_VARARGS
8
+ from inspect import CO_VARKEYWORDS
9
+ from io import StringIO
10
+ import os
11
+ from pathlib import Path
12
+ import re
13
+ import sys
14
+ import traceback
15
+ from traceback import format_exception_only
16
+ from types import CodeType
17
+ from types import FrameType
18
+ from types import TracebackType
19
+ from typing import Any
20
+ from typing import Callable
21
+ from typing import ClassVar
22
+ from typing import Final
23
+ from typing import final
24
+ from typing import Generic
25
+ from typing import Iterable
26
+ from typing import List
27
+ from typing import Literal
28
+ from typing import Mapping
29
+ from typing import overload
30
+ from typing import Pattern
31
+ from typing import Sequence
32
+ from typing import SupportsIndex
33
+ from typing import Tuple
34
+ from typing import Type
35
+ from typing import TypeVar
36
+ from typing import Union
37
+
38
+ import pluggy
39
+
40
+ import _pytest
41
+ from _pytest._code.source import findsource
42
+ from _pytest._code.source import getrawcode
43
+ from _pytest._code.source import getstatementrange_ast
44
+ from _pytest._code.source import Source
45
+ from _pytest._io import TerminalWriter
46
+ from _pytest._io.saferepr import safeformat
47
+ from _pytest._io.saferepr import saferepr
48
+ from _pytest.compat import get_real_func
49
+ from _pytest.deprecated import check_ispytest
50
+ from _pytest.pathlib import absolutepath
51
+ from _pytest.pathlib import bestrelpath
52
+
53
+
54
+ if sys.version_info < (3, 11):
55
+ from exceptiongroup import BaseExceptionGroup
56
+
57
+ TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
58
+
59
+ EXCEPTION_OR_MORE = Union[Type[BaseException], Tuple[Type[BaseException], ...]]
60
+
61
+
62
+ class Code:
63
+ """Wrapper around Python code objects."""
64
+
65
+ __slots__ = ("raw",)
66
+
67
+ def __init__(self, obj: CodeType) -> None:
68
+ self.raw = obj
69
+
70
+ @classmethod
71
+ def from_function(cls, obj: object) -> Code:
72
+ return cls(getrawcode(obj))
73
+
74
+ def __eq__(self, other):
75
+ return self.raw == other.raw
76
+
77
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
78
+ __hash__ = None # type: ignore
79
+
80
+ @property
81
+ def firstlineno(self) -> int:
82
+ return self.raw.co_firstlineno - 1
83
+
84
+ @property
85
+ def name(self) -> str:
86
+ return self.raw.co_name
87
+
88
+ @property
89
+ def path(self) -> Path | str:
90
+ """Return a path object pointing to source code, or an ``str`` in
91
+ case of ``OSError`` / non-existing file."""
92
+ if not self.raw.co_filename:
93
+ return ""
94
+ try:
95
+ p = absolutepath(self.raw.co_filename)
96
+ # maybe don't try this checking
97
+ if not p.exists():
98
+ raise OSError("path check failed.")
99
+ return p
100
+ except OSError:
101
+ # XXX maybe try harder like the weird logic
102
+ # in the standard lib [linecache.updatecache] does?
103
+ return self.raw.co_filename
104
+
105
+ @property
106
+ def fullsource(self) -> Source | None:
107
+ """Return a _pytest._code.Source object for the full source file of the code."""
108
+ full, _ = findsource(self.raw)
109
+ return full
110
+
111
+ def source(self) -> Source:
112
+ """Return a _pytest._code.Source object for the code object's source only."""
113
+ # return source only for that part of code
114
+ return Source(self.raw)
115
+
116
+ def getargs(self, var: bool = False) -> tuple[str, ...]:
117
+ """Return a tuple with the argument names for the code object.
118
+
119
+ If 'var' is set True also return the names of the variable and
120
+ keyword arguments when present.
121
+ """
122
+ # Handy shortcut for getting args.
123
+ raw = self.raw
124
+ argcount = raw.co_argcount
125
+ if var:
126
+ argcount += raw.co_flags & CO_VARARGS
127
+ argcount += raw.co_flags & CO_VARKEYWORDS
128
+ return raw.co_varnames[:argcount]
129
+
130
+
131
+ class Frame:
132
+ """Wrapper around a Python frame holding f_locals and f_globals
133
+ in which expressions can be evaluated."""
134
+
135
+ __slots__ = ("raw",)
136
+
137
+ def __init__(self, frame: FrameType) -> None:
138
+ self.raw = frame
139
+
140
+ @property
141
+ def lineno(self) -> int:
142
+ return self.raw.f_lineno - 1
143
+
144
+ @property
145
+ def f_globals(self) -> dict[str, Any]:
146
+ return self.raw.f_globals
147
+
148
+ @property
149
+ def f_locals(self) -> dict[str, Any]:
150
+ return self.raw.f_locals
151
+
152
+ @property
153
+ def code(self) -> Code:
154
+ return Code(self.raw.f_code)
155
+
156
+ @property
157
+ def statement(self) -> Source:
158
+ """Statement this frame is at."""
159
+ if self.code.fullsource is None:
160
+ return Source("")
161
+ return self.code.fullsource.getstatement(self.lineno)
162
+
163
+ def eval(self, code, **vars):
164
+ """Evaluate 'code' in the frame.
165
+
166
+ 'vars' are optional additional local variables.
167
+
168
+ Returns the result of the evaluation.
169
+ """
170
+ f_locals = self.f_locals.copy()
171
+ f_locals.update(vars)
172
+ return eval(code, self.f_globals, f_locals)
173
+
174
+ def repr(self, object: object) -> str:
175
+ """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
176
+ return saferepr(object)
177
+
178
+ def getargs(self, var: bool = False):
179
+ """Return a list of tuples (name, value) for all arguments.
180
+
181
+ If 'var' is set True, also include the variable and keyword arguments
182
+ when present.
183
+ """
184
+ retval = []
185
+ for arg in self.code.getargs(var):
186
+ try:
187
+ retval.append((arg, self.f_locals[arg]))
188
+ except KeyError:
189
+ pass # this can occur when using Psyco
190
+ return retval
191
+
192
+
193
+ class TracebackEntry:
194
+ """A single entry in a Traceback."""
195
+
196
+ __slots__ = ("_rawentry", "_repr_style")
197
+
198
+ def __init__(
199
+ self,
200
+ rawentry: TracebackType,
201
+ repr_style: Literal["short", "long"] | None = None,
202
+ ) -> None:
203
+ self._rawentry: Final = rawentry
204
+ self._repr_style: Final = repr_style
205
+
206
+ def with_repr_style(
207
+ self, repr_style: Literal["short", "long"] | None
208
+ ) -> TracebackEntry:
209
+ return TracebackEntry(self._rawentry, repr_style)
210
+
211
+ @property
212
+ def lineno(self) -> int:
213
+ return self._rawentry.tb_lineno - 1
214
+
215
+ @property
216
+ def frame(self) -> Frame:
217
+ return Frame(self._rawentry.tb_frame)
218
+
219
+ @property
220
+ def relline(self) -> int:
221
+ return self.lineno - self.frame.code.firstlineno
222
+
223
+ def __repr__(self) -> str:
224
+ return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
225
+
226
+ @property
227
+ def statement(self) -> Source:
228
+ """_pytest._code.Source object for the current statement."""
229
+ source = self.frame.code.fullsource
230
+ assert source is not None
231
+ return source.getstatement(self.lineno)
232
+
233
+ @property
234
+ def path(self) -> Path | str:
235
+ """Path to the source code."""
236
+ return self.frame.code.path
237
+
238
+ @property
239
+ def locals(self) -> dict[str, Any]:
240
+ """Locals of underlying frame."""
241
+ return self.frame.f_locals
242
+
243
+ def getfirstlinesource(self) -> int:
244
+ return self.frame.code.firstlineno
245
+
246
+ def getsource(
247
+ self, astcache: dict[str | Path, ast.AST] | None = None
248
+ ) -> Source | None:
249
+ """Return failing source code."""
250
+ # we use the passed in astcache to not reparse asttrees
251
+ # within exception info printing
252
+ source = self.frame.code.fullsource
253
+ if source is None:
254
+ return None
255
+ key = astnode = None
256
+ if astcache is not None:
257
+ key = self.frame.code.path
258
+ if key is not None:
259
+ astnode = astcache.get(key, None)
260
+ start = self.getfirstlinesource()
261
+ try:
262
+ astnode, _, end = getstatementrange_ast(
263
+ self.lineno, source, astnode=astnode
264
+ )
265
+ except SyntaxError:
266
+ end = self.lineno + 1
267
+ else:
268
+ if key is not None and astcache is not None:
269
+ astcache[key] = astnode
270
+ return source[start:end]
271
+
272
+ source = property(getsource)
273
+
274
+ def ishidden(self, excinfo: ExceptionInfo[BaseException] | None) -> bool:
275
+ """Return True if the current frame has a var __tracebackhide__
276
+ resolving to True.
277
+
278
+ If __tracebackhide__ is a callable, it gets called with the
279
+ ExceptionInfo instance and can decide whether to hide the traceback.
280
+
281
+ Mostly for internal use.
282
+ """
283
+ tbh: bool | Callable[[ExceptionInfo[BaseException] | None], bool] = False
284
+ for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
285
+ # in normal cases, f_locals and f_globals are dictionaries
286
+ # however via `exec(...)` / `eval(...)` they can be other types
287
+ # (even incorrect types!).
288
+ # as such, we suppress all exceptions while accessing __tracebackhide__
289
+ try:
290
+ tbh = maybe_ns_dct["__tracebackhide__"]
291
+ except Exception:
292
+ pass
293
+ else:
294
+ break
295
+ if tbh and callable(tbh):
296
+ return tbh(excinfo)
297
+ return tbh
298
+
299
+ def __str__(self) -> str:
300
+ name = self.frame.code.name
301
+ try:
302
+ line = str(self.statement).lstrip()
303
+ except KeyboardInterrupt:
304
+ raise
305
+ except BaseException:
306
+ line = "???"
307
+ # This output does not quite match Python's repr for traceback entries,
308
+ # but changing it to do so would break certain plugins. See
309
+ # https://github.com/pytest-dev/pytest/pull/7535/ for details.
310
+ return " File %r:%d in %s\n %s\n" % (
311
+ str(self.path),
312
+ self.lineno + 1,
313
+ name,
314
+ line,
315
+ )
316
+
317
+ @property
318
+ def name(self) -> str:
319
+ """co_name of underlying code."""
320
+ return self.frame.code.raw.co_name
321
+
322
+
323
+ class Traceback(List[TracebackEntry]):
324
+ """Traceback objects encapsulate and offer higher level access to Traceback entries."""
325
+
326
+ def __init__(
327
+ self,
328
+ tb: TracebackType | Iterable[TracebackEntry],
329
+ ) -> None:
330
+ """Initialize from given python traceback object and ExceptionInfo."""
331
+ if isinstance(tb, TracebackType):
332
+
333
+ def f(cur: TracebackType) -> Iterable[TracebackEntry]:
334
+ cur_: TracebackType | None = cur
335
+ while cur_ is not None:
336
+ yield TracebackEntry(cur_)
337
+ cur_ = cur_.tb_next
338
+
339
+ super().__init__(f(tb))
340
+ else:
341
+ super().__init__(tb)
342
+
343
+ def cut(
344
+ self,
345
+ path: os.PathLike[str] | str | None = None,
346
+ lineno: int | None = None,
347
+ firstlineno: int | None = None,
348
+ excludepath: os.PathLike[str] | None = None,
349
+ ) -> Traceback:
350
+ """Return a Traceback instance wrapping part of this Traceback.
351
+
352
+ By providing any combination of path, lineno and firstlineno, the
353
+ first frame to start the to-be-returned traceback is determined.
354
+
355
+ This allows cutting the first part of a Traceback instance e.g.
356
+ for formatting reasons (removing some uninteresting bits that deal
357
+ with handling of the exception/traceback).
358
+ """
359
+ path_ = None if path is None else os.fspath(path)
360
+ excludepath_ = None if excludepath is None else os.fspath(excludepath)
361
+ for x in self:
362
+ code = x.frame.code
363
+ codepath = code.path
364
+ if path is not None and str(codepath) != path_:
365
+ continue
366
+ if (
367
+ excludepath is not None
368
+ and isinstance(codepath, Path)
369
+ and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
370
+ ):
371
+ continue
372
+ if lineno is not None and x.lineno != lineno:
373
+ continue
374
+ if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
375
+ continue
376
+ return Traceback(x._rawentry)
377
+ return self
378
+
379
+ @overload
380
+ def __getitem__(self, key: SupportsIndex) -> TracebackEntry: ...
381
+
382
+ @overload
383
+ def __getitem__(self, key: slice) -> Traceback: ...
384
+
385
+ def __getitem__(self, key: SupportsIndex | slice) -> TracebackEntry | Traceback:
386
+ if isinstance(key, slice):
387
+ return self.__class__(super().__getitem__(key))
388
+ else:
389
+ return super().__getitem__(key)
390
+
391
+ def filter(
392
+ self,
393
+ excinfo_or_fn: ExceptionInfo[BaseException] | Callable[[TracebackEntry], bool],
394
+ /,
395
+ ) -> Traceback:
396
+ """Return a Traceback instance with certain items removed.
397
+
398
+ If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
399
+ which are hidden (see ishidden() above).
400
+
401
+ Otherwise, the filter is a function that gets a single argument, a
402
+ ``TracebackEntry`` instance, and should return True when the item should
403
+ be added to the ``Traceback``, False when not.
404
+ """
405
+ if isinstance(excinfo_or_fn, ExceptionInfo):
406
+ fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731
407
+ else:
408
+ fn = excinfo_or_fn
409
+ return Traceback(filter(fn, self))
410
+
411
+ def recursionindex(self) -> int | None:
412
+ """Return the index of the frame/TracebackEntry where recursion originates if
413
+ appropriate, None if no recursion occurred."""
414
+ cache: dict[tuple[Any, int, int], list[dict[str, Any]]] = {}
415
+ for i, entry in enumerate(self):
416
+ # id for the code.raw is needed to work around
417
+ # the strange metaprogramming in the decorator lib from pypi
418
+ # which generates code objects that have hash/value equality
419
+ # XXX needs a test
420
+ key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
421
+ values = cache.setdefault(key, [])
422
+ # Since Python 3.13 f_locals is a proxy, freeze it.
423
+ loc = dict(entry.frame.f_locals)
424
+ if values:
425
+ for otherloc in values:
426
+ if otherloc == loc:
427
+ return i
428
+ values.append(loc)
429
+ return None
430
+
431
+
432
+ E = TypeVar("E", bound=BaseException, covariant=True)
433
+
434
+
435
+ @final
436
+ @dataclasses.dataclass
437
+ class ExceptionInfo(Generic[E]):
438
+ """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
439
+
440
+ _assert_start_repr: ClassVar = "AssertionError('assert "
441
+
442
+ _excinfo: tuple[type[E], E, TracebackType] | None
443
+ _striptext: str
444
+ _traceback: Traceback | None
445
+
446
+ def __init__(
447
+ self,
448
+ excinfo: tuple[type[E], E, TracebackType] | None,
449
+ striptext: str = "",
450
+ traceback: Traceback | None = None,
451
+ *,
452
+ _ispytest: bool = False,
453
+ ) -> None:
454
+ check_ispytest(_ispytest)
455
+ self._excinfo = excinfo
456
+ self._striptext = striptext
457
+ self._traceback = traceback
458
+
459
+ @classmethod
460
+ def from_exception(
461
+ cls,
462
+ # Ignoring error: "Cannot use a covariant type variable as a parameter".
463
+ # This is OK to ignore because this class is (conceptually) readonly.
464
+ # See https://github.com/python/mypy/issues/7049.
465
+ exception: E, # type: ignore[misc]
466
+ exprinfo: str | None = None,
467
+ ) -> ExceptionInfo[E]:
468
+ """Return an ExceptionInfo for an existing exception.
469
+
470
+ The exception must have a non-``None`` ``__traceback__`` attribute,
471
+ otherwise this function fails with an assertion error. This means that
472
+ the exception must have been raised, or added a traceback with the
473
+ :py:meth:`~BaseException.with_traceback()` method.
474
+
475
+ :param exprinfo:
476
+ A text string helping to determine if we should strip
477
+ ``AssertionError`` from the output. Defaults to the exception
478
+ message/``__str__()``.
479
+
480
+ .. versionadded:: 7.4
481
+ """
482
+ assert exception.__traceback__, (
483
+ "Exceptions passed to ExcInfo.from_exception(...)"
484
+ " must have a non-None __traceback__."
485
+ )
486
+ exc_info = (type(exception), exception, exception.__traceback__)
487
+ return cls.from_exc_info(exc_info, exprinfo)
488
+
489
+ @classmethod
490
+ def from_exc_info(
491
+ cls,
492
+ exc_info: tuple[type[E], E, TracebackType],
493
+ exprinfo: str | None = None,
494
+ ) -> ExceptionInfo[E]:
495
+ """Like :func:`from_exception`, but using old-style exc_info tuple."""
496
+ _striptext = ""
497
+ if exprinfo is None and isinstance(exc_info[1], AssertionError):
498
+ exprinfo = getattr(exc_info[1], "msg", None)
499
+ if exprinfo is None:
500
+ exprinfo = saferepr(exc_info[1])
501
+ if exprinfo and exprinfo.startswith(cls._assert_start_repr):
502
+ _striptext = "AssertionError: "
503
+
504
+ return cls(exc_info, _striptext, _ispytest=True)
505
+
506
+ @classmethod
507
+ def from_current(cls, exprinfo: str | None = None) -> ExceptionInfo[BaseException]:
508
+ """Return an ExceptionInfo matching the current traceback.
509
+
510
+ .. warning::
511
+
512
+ Experimental API
513
+
514
+ :param exprinfo:
515
+ A text string helping to determine if we should strip
516
+ ``AssertionError`` from the output. Defaults to the exception
517
+ message/``__str__()``.
518
+ """
519
+ tup = sys.exc_info()
520
+ assert tup[0] is not None, "no current exception"
521
+ assert tup[1] is not None, "no current exception"
522
+ assert tup[2] is not None, "no current exception"
523
+ exc_info = (tup[0], tup[1], tup[2])
524
+ return ExceptionInfo.from_exc_info(exc_info, exprinfo)
525
+
526
+ @classmethod
527
+ def for_later(cls) -> ExceptionInfo[E]:
528
+ """Return an unfilled ExceptionInfo."""
529
+ return cls(None, _ispytest=True)
530
+
531
+ def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None:
532
+ """Fill an unfilled ExceptionInfo created with ``for_later()``."""
533
+ assert self._excinfo is None, "ExceptionInfo was already filled"
534
+ self._excinfo = exc_info
535
+
536
+ @property
537
+ def type(self) -> type[E]:
538
+ """The exception class."""
539
+ assert (
540
+ self._excinfo is not None
541
+ ), ".type can only be used after the context manager exits"
542
+ return self._excinfo[0]
543
+
544
+ @property
545
+ def value(self) -> E:
546
+ """The exception value."""
547
+ assert (
548
+ self._excinfo is not None
549
+ ), ".value can only be used after the context manager exits"
550
+ return self._excinfo[1]
551
+
552
+ @property
553
+ def tb(self) -> TracebackType:
554
+ """The exception raw traceback."""
555
+ assert (
556
+ self._excinfo is not None
557
+ ), ".tb can only be used after the context manager exits"
558
+ return self._excinfo[2]
559
+
560
+ @property
561
+ def typename(self) -> str:
562
+ """The type name of the exception."""
563
+ assert (
564
+ self._excinfo is not None
565
+ ), ".typename can only be used after the context manager exits"
566
+ return self.type.__name__
567
+
568
+ @property
569
+ def traceback(self) -> Traceback:
570
+ """The traceback."""
571
+ if self._traceback is None:
572
+ self._traceback = Traceback(self.tb)
573
+ return self._traceback
574
+
575
+ @traceback.setter
576
+ def traceback(self, value: Traceback) -> None:
577
+ self._traceback = value
578
+
579
+ def __repr__(self) -> str:
580
+ if self._excinfo is None:
581
+ return "<ExceptionInfo for raises contextmanager>"
582
+ return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
583
+
584
+ def exconly(self, tryshort: bool = False) -> str:
585
+ """Return the exception as a string.
586
+
587
+ When 'tryshort' resolves to True, and the exception is an
588
+ AssertionError, only the actual exception part of the exception
589
+ representation is returned (so 'AssertionError: ' is removed from
590
+ the beginning).
591
+ """
592
+ lines = format_exception_only(self.type, self.value)
593
+ text = "".join(lines)
594
+ text = text.rstrip()
595
+ if tryshort:
596
+ if text.startswith(self._striptext):
597
+ text = text[len(self._striptext) :]
598
+ return text
599
+
600
+ def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool:
601
+ """Return True if the exception is an instance of exc.
602
+
603
+ Consider using ``isinstance(excinfo.value, exc)`` instead.
604
+ """
605
+ return isinstance(self.value, exc)
606
+
607
+ def _getreprcrash(self) -> ReprFileLocation | None:
608
+ # Find last non-hidden traceback entry that led to the exception of the
609
+ # traceback, or None if all hidden.
610
+ for i in range(-1, -len(self.traceback) - 1, -1):
611
+ entry = self.traceback[i]
612
+ if not entry.ishidden(self):
613
+ path, lineno = entry.frame.code.raw.co_filename, entry.lineno
614
+ exconly = self.exconly(tryshort=True)
615
+ return ReprFileLocation(path, lineno + 1, exconly)
616
+ return None
617
+
618
+ def getrepr(
619
+ self,
620
+ showlocals: bool = False,
621
+ style: TracebackStyle = "long",
622
+ abspath: bool = False,
623
+ tbfilter: bool
624
+ | Callable[[ExceptionInfo[BaseException]], _pytest._code.code.Traceback] = True,
625
+ funcargs: bool = False,
626
+ truncate_locals: bool = True,
627
+ truncate_args: bool = True,
628
+ chain: bool = True,
629
+ ) -> ReprExceptionInfo | ExceptionChainRepr:
630
+ """Return str()able representation of this exception info.
631
+
632
+ :param bool showlocals:
633
+ Show locals per traceback entry.
634
+ Ignored if ``style=="native"``.
635
+
636
+ :param str style:
637
+ long|short|line|no|native|value traceback style.
638
+
639
+ :param bool abspath:
640
+ If paths should be changed to absolute or left unchanged.
641
+
642
+ :param tbfilter:
643
+ A filter for traceback entries.
644
+
645
+ * If false, don't hide any entries.
646
+ * If true, hide internal entries and entries that contain a local
647
+ variable ``__tracebackhide__ = True``.
648
+ * If a callable, delegates the filtering to the callable.
649
+
650
+ Ignored if ``style`` is ``"native"``.
651
+
652
+ :param bool funcargs:
653
+ Show fixtures ("funcargs" for legacy purposes) per traceback entry.
654
+
655
+ :param bool truncate_locals:
656
+ With ``showlocals==True``, make sure locals can be safely represented as strings.
657
+
658
+ :param bool truncate_args:
659
+ With ``showargs==True``, make sure args can be safely represented as strings.
660
+
661
+ :param bool chain:
662
+ If chained exceptions in Python 3 should be shown.
663
+
664
+ .. versionchanged:: 3.9
665
+
666
+ Added the ``chain`` parameter.
667
+ """
668
+ if style == "native":
669
+ return ReprExceptionInfo(
670
+ reprtraceback=ReprTracebackNative(
671
+ traceback.format_exception(
672
+ self.type,
673
+ self.value,
674
+ self.traceback[0]._rawentry if self.traceback else None,
675
+ )
676
+ ),
677
+ reprcrash=self._getreprcrash(),
678
+ )
679
+
680
+ fmt = FormattedExcinfo(
681
+ showlocals=showlocals,
682
+ style=style,
683
+ abspath=abspath,
684
+ tbfilter=tbfilter,
685
+ funcargs=funcargs,
686
+ truncate_locals=truncate_locals,
687
+ truncate_args=truncate_args,
688
+ chain=chain,
689
+ )
690
+ return fmt.repr_excinfo(self)
691
+
692
+ def _stringify_exception(self, exc: BaseException) -> str:
693
+ try:
694
+ notes = getattr(exc, "__notes__", [])
695
+ except KeyError:
696
+ # Workaround for https://github.com/python/cpython/issues/98778 on
697
+ # Python <= 3.9, and some 3.10 and 3.11 patch versions.
698
+ HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
699
+ if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
700
+ notes = []
701
+ else:
702
+ raise
703
+
704
+ return "\n".join(
705
+ [
706
+ str(exc),
707
+ *notes,
708
+ ]
709
+ )
710
+
711
+ def match(self, regexp: str | Pattern[str]) -> Literal[True]:
712
+ """Check whether the regular expression `regexp` matches the string
713
+ representation of the exception using :func:`python:re.search`.
714
+
715
+ If it matches `True` is returned, otherwise an `AssertionError` is raised.
716
+ """
717
+ __tracebackhide__ = True
718
+ value = self._stringify_exception(self.value)
719
+ msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
720
+ if regexp == value:
721
+ msg += "\n Did you mean to `re.escape()` the regex?"
722
+ assert re.search(regexp, value), msg
723
+ # Return True to allow for "assert excinfo.match()".
724
+ return True
725
+
726
+ def _group_contains(
727
+ self,
728
+ exc_group: BaseExceptionGroup[BaseException],
729
+ expected_exception: EXCEPTION_OR_MORE,
730
+ match: str | Pattern[str] | None,
731
+ target_depth: int | None = None,
732
+ current_depth: int = 1,
733
+ ) -> bool:
734
+ """Return `True` if a `BaseExceptionGroup` contains a matching exception."""
735
+ if (target_depth is not None) and (current_depth > target_depth):
736
+ # already descended past the target depth
737
+ return False
738
+ for exc in exc_group.exceptions:
739
+ if isinstance(exc, BaseExceptionGroup):
740
+ if self._group_contains(
741
+ exc, expected_exception, match, target_depth, current_depth + 1
742
+ ):
743
+ return True
744
+ if (target_depth is not None) and (current_depth != target_depth):
745
+ # not at the target depth, no match
746
+ continue
747
+ if not isinstance(exc, expected_exception):
748
+ continue
749
+ if match is not None:
750
+ value = self._stringify_exception(exc)
751
+ if not re.search(match, value):
752
+ continue
753
+ return True
754
+ return False
755
+
756
+ def group_contains(
757
+ self,
758
+ expected_exception: EXCEPTION_OR_MORE,
759
+ *,
760
+ match: str | Pattern[str] | None = None,
761
+ depth: int | None = None,
762
+ ) -> bool:
763
+ """Check whether a captured exception group contains a matching exception.
764
+
765
+ :param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
766
+ The expected exception type, or a tuple if one of multiple possible
767
+ exception types are expected.
768
+
769
+ :param str | Pattern[str] | None match:
770
+ If specified, a string containing a regular expression,
771
+ or a regular expression object, that is tested against the string
772
+ representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
773
+ using :func:`re.search`.
774
+
775
+ To match a literal string that may contain :ref:`special characters
776
+ <re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
777
+
778
+ :param Optional[int] depth:
779
+ If `None`, will search for a matching exception at any nesting depth.
780
+ If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
781
+ the exceptions contained within the topmost exception group).
782
+
783
+ .. versionadded:: 8.0
784
+ """
785
+ msg = "Captured exception is not an instance of `BaseExceptionGroup`"
786
+ assert isinstance(self.value, BaseExceptionGroup), msg
787
+ msg = "`depth` must be >= 1 if specified"
788
+ assert (depth is None) or (depth >= 1), msg
789
+ return self._group_contains(self.value, expected_exception, match, depth)
790
+
791
+
792
+ @dataclasses.dataclass
793
+ class FormattedExcinfo:
794
+ """Presenting information about failing Functions and Generators."""
795
+
796
+ # for traceback entries
797
+ flow_marker: ClassVar = ">"
798
+ fail_marker: ClassVar = "E"
799
+
800
+ showlocals: bool = False
801
+ style: TracebackStyle = "long"
802
+ abspath: bool = True
803
+ tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True
804
+ funcargs: bool = False
805
+ truncate_locals: bool = True
806
+ truncate_args: bool = True
807
+ chain: bool = True
808
+ astcache: dict[str | Path, ast.AST] = dataclasses.field(
809
+ default_factory=dict, init=False, repr=False
810
+ )
811
+
812
+ def _getindent(self, source: Source) -> int:
813
+ # Figure out indent for the given source.
814
+ try:
815
+ s = str(source.getstatement(len(source) - 1))
816
+ except KeyboardInterrupt:
817
+ raise
818
+ except BaseException:
819
+ try:
820
+ s = str(source[-1])
821
+ except KeyboardInterrupt:
822
+ raise
823
+ except BaseException:
824
+ return 0
825
+ return 4 + (len(s) - len(s.lstrip()))
826
+
827
+ def _getentrysource(self, entry: TracebackEntry) -> Source | None:
828
+ source = entry.getsource(self.astcache)
829
+ if source is not None:
830
+ source = source.deindent()
831
+ return source
832
+
833
+ def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None:
834
+ if self.funcargs:
835
+ args = []
836
+ for argname, argvalue in entry.frame.getargs(var=True):
837
+ if self.truncate_args:
838
+ str_repr = saferepr(argvalue)
839
+ else:
840
+ str_repr = saferepr(argvalue, maxsize=None)
841
+ args.append((argname, str_repr))
842
+ return ReprFuncArgs(args)
843
+ return None
844
+
845
+ def get_source(
846
+ self,
847
+ source: Source | None,
848
+ line_index: int = -1,
849
+ excinfo: ExceptionInfo[BaseException] | None = None,
850
+ short: bool = False,
851
+ ) -> list[str]:
852
+ """Return formatted and marked up source lines."""
853
+ lines = []
854
+ if source is not None and line_index < 0:
855
+ line_index += len(source)
856
+ if source is None or line_index >= len(source.lines) or line_index < 0:
857
+ # `line_index` could still be outside `range(len(source.lines))` if
858
+ # we're processing AST with pathological position attributes.
859
+ source = Source("???")
860
+ line_index = 0
861
+ space_prefix = " "
862
+ if short:
863
+ lines.append(space_prefix + source.lines[line_index].strip())
864
+ else:
865
+ for line in source.lines[:line_index]:
866
+ lines.append(space_prefix + line)
867
+ lines.append(self.flow_marker + " " + source.lines[line_index])
868
+ for line in source.lines[line_index + 1 :]:
869
+ lines.append(space_prefix + line)
870
+ if excinfo is not None:
871
+ indent = 4 if short else self._getindent(source)
872
+ lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
873
+ return lines
874
+
875
+ def get_exconly(
876
+ self,
877
+ excinfo: ExceptionInfo[BaseException],
878
+ indent: int = 4,
879
+ markall: bool = False,
880
+ ) -> list[str]:
881
+ lines = []
882
+ indentstr = " " * indent
883
+ # Get the real exception information out.
884
+ exlines = excinfo.exconly(tryshort=True).split("\n")
885
+ failindent = self.fail_marker + indentstr[1:]
886
+ for line in exlines:
887
+ lines.append(failindent + line)
888
+ if not markall:
889
+ failindent = indentstr
890
+ return lines
891
+
892
+ def repr_locals(self, locals: Mapping[str, object]) -> ReprLocals | None:
893
+ if self.showlocals:
894
+ lines = []
895
+ keys = [loc for loc in locals if loc[0] != "@"]
896
+ keys.sort()
897
+ for name in keys:
898
+ value = locals[name]
899
+ if name == "__builtins__":
900
+ lines.append("__builtins__ = <builtins>")
901
+ else:
902
+ # This formatting could all be handled by the
903
+ # _repr() function, which is only reprlib.Repr in
904
+ # disguise, so is very configurable.
905
+ if self.truncate_locals:
906
+ str_repr = saferepr(value)
907
+ else:
908
+ str_repr = safeformat(value)
909
+ # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
910
+ lines.append(f"{name:<10} = {str_repr}")
911
+ # else:
912
+ # self._line("%-10s =\\" % (name,))
913
+ # # XXX
914
+ # pprint.pprint(value, stream=self.excinfowriter)
915
+ return ReprLocals(lines)
916
+ return None
917
+
918
+ def repr_traceback_entry(
919
+ self,
920
+ entry: TracebackEntry | None,
921
+ excinfo: ExceptionInfo[BaseException] | None = None,
922
+ ) -> ReprEntry:
923
+ lines: list[str] = []
924
+ style = (
925
+ entry._repr_style
926
+ if entry is not None and entry._repr_style is not None
927
+ else self.style
928
+ )
929
+ if style in ("short", "long") and entry is not None:
930
+ source = self._getentrysource(entry)
931
+ if source is None:
932
+ source = Source("???")
933
+ line_index = 0
934
+ else:
935
+ line_index = entry.lineno - entry.getfirstlinesource()
936
+ short = style == "short"
937
+ reprargs = self.repr_args(entry) if not short else None
938
+ s = self.get_source(source, line_index, excinfo, short=short)
939
+ lines.extend(s)
940
+ if short:
941
+ message = f"in {entry.name}"
942
+ else:
943
+ message = excinfo and excinfo.typename or ""
944
+ entry_path = entry.path
945
+ path = self._makepath(entry_path)
946
+ reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
947
+ localsrepr = self.repr_locals(entry.locals)
948
+ return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
949
+ elif style == "value":
950
+ if excinfo:
951
+ lines.extend(str(excinfo.value).split("\n"))
952
+ return ReprEntry(lines, None, None, None, style)
953
+ else:
954
+ if excinfo:
955
+ lines.extend(self.get_exconly(excinfo, indent=4))
956
+ return ReprEntry(lines, None, None, None, style)
957
+
958
+ def _makepath(self, path: Path | str) -> str:
959
+ if not self.abspath and isinstance(path, Path):
960
+ try:
961
+ np = bestrelpath(Path.cwd(), path)
962
+ except OSError:
963
+ return str(path)
964
+ if len(np) < len(str(path)):
965
+ return np
966
+ return str(path)
967
+
968
+ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
969
+ traceback = excinfo.traceback
970
+ if callable(self.tbfilter):
971
+ traceback = self.tbfilter(excinfo)
972
+ elif self.tbfilter:
973
+ traceback = traceback.filter(excinfo)
974
+
975
+ if isinstance(excinfo.value, RecursionError):
976
+ traceback, extraline = self._truncate_recursive_traceback(traceback)
977
+ else:
978
+ extraline = None
979
+
980
+ if not traceback:
981
+ if extraline is None:
982
+ extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
983
+ entries = [self.repr_traceback_entry(None, excinfo)]
984
+ return ReprTraceback(entries, extraline, style=self.style)
985
+
986
+ last = traceback[-1]
987
+ if self.style == "value":
988
+ entries = [self.repr_traceback_entry(last, excinfo)]
989
+ return ReprTraceback(entries, None, style=self.style)
990
+
991
+ entries = [
992
+ self.repr_traceback_entry(entry, excinfo if last == entry else None)
993
+ for entry in traceback
994
+ ]
995
+ return ReprTraceback(entries, extraline, style=self.style)
996
+
997
+ def _truncate_recursive_traceback(
998
+ self, traceback: Traceback
999
+ ) -> tuple[Traceback, str | None]:
1000
+ """Truncate the given recursive traceback trying to find the starting
1001
+ point of the recursion.
1002
+
1003
+ The detection is done by going through each traceback entry and
1004
+ finding the point in which the locals of the frame are equal to the
1005
+ locals of a previous frame (see ``recursionindex()``).
1006
+
1007
+ Handle the situation where the recursion process might raise an
1008
+ exception (for example comparing numpy arrays using equality raises a
1009
+ TypeError), in which case we do our best to warn the user of the
1010
+ error and show a limited traceback.
1011
+ """
1012
+ try:
1013
+ recursionindex = traceback.recursionindex()
1014
+ except Exception as e:
1015
+ max_frames = 10
1016
+ extraline: str | None = (
1017
+ "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
1018
+ " The following exception happened when comparing locals in the stack frame:\n"
1019
+ f" {type(e).__name__}: {e!s}\n"
1020
+ f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
1021
+ )
1022
+ # Type ignored because adding two instances of a List subtype
1023
+ # currently incorrectly has type List instead of the subtype.
1024
+ traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
1025
+ else:
1026
+ if recursionindex is not None:
1027
+ extraline = "!!! Recursion detected (same locals & position)"
1028
+ traceback = traceback[: recursionindex + 1]
1029
+ else:
1030
+ extraline = None
1031
+
1032
+ return traceback, extraline
1033
+
1034
+ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainRepr:
1035
+ repr_chain: list[tuple[ReprTraceback, ReprFileLocation | None, str | None]] = []
1036
+ e: BaseException | None = excinfo.value
1037
+ excinfo_: ExceptionInfo[BaseException] | None = excinfo
1038
+ descr = None
1039
+ seen: set[int] = set()
1040
+ while e is not None and id(e) not in seen:
1041
+ seen.add(id(e))
1042
+
1043
+ if excinfo_:
1044
+ # Fall back to native traceback as a temporary workaround until
1045
+ # full support for exception groups added to ExceptionInfo.
1046
+ # See https://github.com/pytest-dev/pytest/issues/9159
1047
+ if isinstance(e, BaseExceptionGroup):
1048
+ reprtraceback: ReprTracebackNative | ReprTraceback = (
1049
+ ReprTracebackNative(
1050
+ traceback.format_exception(
1051
+ type(excinfo_.value),
1052
+ excinfo_.value,
1053
+ excinfo_.traceback[0]._rawentry,
1054
+ )
1055
+ )
1056
+ )
1057
+ else:
1058
+ reprtraceback = self.repr_traceback(excinfo_)
1059
+ reprcrash = excinfo_._getreprcrash()
1060
+ else:
1061
+ # Fallback to native repr if the exception doesn't have a traceback:
1062
+ # ExceptionInfo objects require a full traceback to work.
1063
+ reprtraceback = ReprTracebackNative(
1064
+ traceback.format_exception(type(e), e, None)
1065
+ )
1066
+ reprcrash = None
1067
+ repr_chain += [(reprtraceback, reprcrash, descr)]
1068
+
1069
+ if e.__cause__ is not None and self.chain:
1070
+ e = e.__cause__
1071
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
1072
+ descr = "The above exception was the direct cause of the following exception:"
1073
+ elif (
1074
+ e.__context__ is not None and not e.__suppress_context__ and self.chain
1075
+ ):
1076
+ e = e.__context__
1077
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
1078
+ descr = "During handling of the above exception, another exception occurred:"
1079
+ else:
1080
+ e = None
1081
+ repr_chain.reverse()
1082
+ return ExceptionChainRepr(repr_chain)
1083
+
1084
+
1085
+ @dataclasses.dataclass(eq=False)
1086
+ class TerminalRepr:
1087
+ def __str__(self) -> str:
1088
+ # FYI this is called from pytest-xdist's serialization of exception
1089
+ # information.
1090
+ io = StringIO()
1091
+ tw = TerminalWriter(file=io)
1092
+ self.toterminal(tw)
1093
+ return io.getvalue().strip()
1094
+
1095
+ def __repr__(self) -> str:
1096
+ return f"<{self.__class__} instance at {id(self):0x}>"
1097
+
1098
+ def toterminal(self, tw: TerminalWriter) -> None:
1099
+ raise NotImplementedError()
1100
+
1101
+
1102
+ # This class is abstract -- only subclasses are instantiated.
1103
+ @dataclasses.dataclass(eq=False)
1104
+ class ExceptionRepr(TerminalRepr):
1105
+ # Provided by subclasses.
1106
+ reprtraceback: ReprTraceback
1107
+ reprcrash: ReprFileLocation | None
1108
+ sections: list[tuple[str, str, str]] = dataclasses.field(
1109
+ init=False, default_factory=list
1110
+ )
1111
+
1112
+ def addsection(self, name: str, content: str, sep: str = "-") -> None:
1113
+ self.sections.append((name, content, sep))
1114
+
1115
+ def toterminal(self, tw: TerminalWriter) -> None:
1116
+ for name, content, sep in self.sections:
1117
+ tw.sep(sep, name)
1118
+ tw.line(content)
1119
+
1120
+
1121
+ @dataclasses.dataclass(eq=False)
1122
+ class ExceptionChainRepr(ExceptionRepr):
1123
+ chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]]
1124
+
1125
+ def __init__(
1126
+ self,
1127
+ chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]],
1128
+ ) -> None:
1129
+ # reprcrash and reprtraceback of the outermost (the newest) exception
1130
+ # in the chain.
1131
+ super().__init__(
1132
+ reprtraceback=chain[-1][0],
1133
+ reprcrash=chain[-1][1],
1134
+ )
1135
+ self.chain = chain
1136
+
1137
+ def toterminal(self, tw: TerminalWriter) -> None:
1138
+ for element in self.chain:
1139
+ element[0].toterminal(tw)
1140
+ if element[2] is not None:
1141
+ tw.line("")
1142
+ tw.line(element[2], yellow=True)
1143
+ super().toterminal(tw)
1144
+
1145
+
1146
+ @dataclasses.dataclass(eq=False)
1147
+ class ReprExceptionInfo(ExceptionRepr):
1148
+ reprtraceback: ReprTraceback
1149
+ reprcrash: ReprFileLocation | None
1150
+
1151
+ def toterminal(self, tw: TerminalWriter) -> None:
1152
+ self.reprtraceback.toterminal(tw)
1153
+ super().toterminal(tw)
1154
+
1155
+
1156
+ @dataclasses.dataclass(eq=False)
1157
+ class ReprTraceback(TerminalRepr):
1158
+ reprentries: Sequence[ReprEntry | ReprEntryNative]
1159
+ extraline: str | None
1160
+ style: TracebackStyle
1161
+
1162
+ entrysep: ClassVar = "_ "
1163
+
1164
+ def toterminal(self, tw: TerminalWriter) -> None:
1165
+ # The entries might have different styles.
1166
+ for i, entry in enumerate(self.reprentries):
1167
+ if entry.style == "long":
1168
+ tw.line("")
1169
+ entry.toterminal(tw)
1170
+ if i < len(self.reprentries) - 1:
1171
+ next_entry = self.reprentries[i + 1]
1172
+ if (
1173
+ entry.style == "long"
1174
+ or entry.style == "short"
1175
+ and next_entry.style == "long"
1176
+ ):
1177
+ tw.sep(self.entrysep)
1178
+
1179
+ if self.extraline:
1180
+ tw.line(self.extraline)
1181
+
1182
+
1183
+ class ReprTracebackNative(ReprTraceback):
1184
+ def __init__(self, tblines: Sequence[str]) -> None:
1185
+ self.reprentries = [ReprEntryNative(tblines)]
1186
+ self.extraline = None
1187
+ self.style = "native"
1188
+
1189
+
1190
+ @dataclasses.dataclass(eq=False)
1191
+ class ReprEntryNative(TerminalRepr):
1192
+ lines: Sequence[str]
1193
+
1194
+ style: ClassVar[TracebackStyle] = "native"
1195
+
1196
+ def toterminal(self, tw: TerminalWriter) -> None:
1197
+ tw.write("".join(self.lines))
1198
+
1199
+
1200
+ @dataclasses.dataclass(eq=False)
1201
+ class ReprEntry(TerminalRepr):
1202
+ lines: Sequence[str]
1203
+ reprfuncargs: ReprFuncArgs | None
1204
+ reprlocals: ReprLocals | None
1205
+ reprfileloc: ReprFileLocation | None
1206
+ style: TracebackStyle
1207
+
1208
+ def _write_entry_lines(self, tw: TerminalWriter) -> None:
1209
+ """Write the source code portions of a list of traceback entries with syntax highlighting.
1210
+
1211
+ Usually entries are lines like these:
1212
+
1213
+ " x = 1"
1214
+ "> assert x == 2"
1215
+ "E assert 1 == 2"
1216
+
1217
+ This function takes care of rendering the "source" portions of it (the lines without
1218
+ the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
1219
+ character, as doing so might break line continuations.
1220
+ """
1221
+ if not self.lines:
1222
+ return
1223
+
1224
+ if self.style == "value":
1225
+ # Using tw.write instead of tw.line for testing purposes due to TWMock implementation;
1226
+ # lines written with TWMock.line and TWMock._write_source cannot be distinguished
1227
+ # from each other, whereas lines written with TWMock.write are marked with TWMock.WRITE
1228
+ for line in self.lines:
1229
+ tw.write(line)
1230
+ tw.write("\n")
1231
+ return
1232
+
1233
+ # separate indents and source lines that are not failures: we want to
1234
+ # highlight the code but not the indentation, which may contain markers
1235
+ # such as "> assert 0"
1236
+ fail_marker = f"{FormattedExcinfo.fail_marker} "
1237
+ indent_size = len(fail_marker)
1238
+ indents: list[str] = []
1239
+ source_lines: list[str] = []
1240
+ failure_lines: list[str] = []
1241
+ for index, line in enumerate(self.lines):
1242
+ is_failure_line = line.startswith(fail_marker)
1243
+ if is_failure_line:
1244
+ # from this point on all lines are considered part of the failure
1245
+ failure_lines.extend(self.lines[index:])
1246
+ break
1247
+ else:
1248
+ indents.append(line[:indent_size])
1249
+ source_lines.append(line[indent_size:])
1250
+
1251
+ tw._write_source(source_lines, indents)
1252
+
1253
+ # failure lines are always completely red and bold
1254
+ for line in failure_lines:
1255
+ tw.line(line, bold=True, red=True)
1256
+
1257
+ def toterminal(self, tw: TerminalWriter) -> None:
1258
+ if self.style == "short":
1259
+ if self.reprfileloc:
1260
+ self.reprfileloc.toterminal(tw)
1261
+ self._write_entry_lines(tw)
1262
+ if self.reprlocals:
1263
+ self.reprlocals.toterminal(tw, indent=" " * 8)
1264
+ return
1265
+
1266
+ if self.reprfuncargs:
1267
+ self.reprfuncargs.toterminal(tw)
1268
+
1269
+ self._write_entry_lines(tw)
1270
+
1271
+ if self.reprlocals:
1272
+ tw.line("")
1273
+ self.reprlocals.toterminal(tw)
1274
+ if self.reprfileloc:
1275
+ if self.lines:
1276
+ tw.line("")
1277
+ self.reprfileloc.toterminal(tw)
1278
+
1279
+ def __str__(self) -> str:
1280
+ return "{}\n{}\n{}".format(
1281
+ "\n".join(self.lines), self.reprlocals, self.reprfileloc
1282
+ )
1283
+
1284
+
1285
+ @dataclasses.dataclass(eq=False)
1286
+ class ReprFileLocation(TerminalRepr):
1287
+ path: str
1288
+ lineno: int
1289
+ message: str
1290
+
1291
+ def __post_init__(self) -> None:
1292
+ self.path = str(self.path)
1293
+
1294
+ def toterminal(self, tw: TerminalWriter) -> None:
1295
+ # Filename and lineno output for each entry, using an output format
1296
+ # that most editors understand.
1297
+ msg = self.message
1298
+ i = msg.find("\n")
1299
+ if i != -1:
1300
+ msg = msg[:i]
1301
+ tw.write(self.path, bold=True, red=True)
1302
+ tw.line(f":{self.lineno}: {msg}")
1303
+
1304
+
1305
+ @dataclasses.dataclass(eq=False)
1306
+ class ReprLocals(TerminalRepr):
1307
+ lines: Sequence[str]
1308
+
1309
+ def toterminal(self, tw: TerminalWriter, indent="") -> None:
1310
+ for line in self.lines:
1311
+ tw.line(indent + line)
1312
+
1313
+
1314
+ @dataclasses.dataclass(eq=False)
1315
+ class ReprFuncArgs(TerminalRepr):
1316
+ args: Sequence[tuple[str, object]]
1317
+
1318
+ def toterminal(self, tw: TerminalWriter) -> None:
1319
+ if self.args:
1320
+ linesofar = ""
1321
+ for name, value in self.args:
1322
+ ns = f"{name} = {value}"
1323
+ if len(ns) + len(linesofar) + 2 > tw.fullwidth:
1324
+ if linesofar:
1325
+ tw.line(linesofar)
1326
+ linesofar = ns
1327
+ else:
1328
+ if linesofar:
1329
+ linesofar += ", " + ns
1330
+ else:
1331
+ linesofar = ns
1332
+ if linesofar:
1333
+ tw.line(linesofar)
1334
+ tw.line("")
1335
+
1336
+
1337
+ def getfslineno(obj: object) -> tuple[str | Path, int]:
1338
+ """Return source location (path, lineno) for the given object.
1339
+
1340
+ If the source cannot be determined return ("", -1).
1341
+
1342
+ The line number is 0-based.
1343
+ """
1344
+ # xxx let decorators etc specify a sane ordering
1345
+ # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
1346
+ # in 6ec13a2b9. It ("place_as") appears to be something very custom.
1347
+ obj = get_real_func(obj)
1348
+ if hasattr(obj, "place_as"):
1349
+ obj = obj.place_as
1350
+
1351
+ try:
1352
+ code = Code.from_function(obj)
1353
+ except TypeError:
1354
+ try:
1355
+ fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
1356
+ except TypeError:
1357
+ return "", -1
1358
+
1359
+ fspath = fn and absolutepath(fn) or ""
1360
+ lineno = -1
1361
+ if fspath:
1362
+ try:
1363
+ _, lineno = findsource(obj)
1364
+ except OSError:
1365
+ pass
1366
+ return fspath, lineno
1367
+
1368
+ return code.path, code.firstlineno
1369
+
1370
+
1371
+ # Relative paths that we use to filter traceback entries from appearing to the user;
1372
+ # see filter_traceback.
1373
+ # note: if we need to add more paths than what we have now we should probably use a list
1374
+ # for better maintenance.
1375
+
1376
+ _PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
1377
+ # pluggy is either a package or a single module depending on the version
1378
+ if _PLUGGY_DIR.name == "__init__.py":
1379
+ _PLUGGY_DIR = _PLUGGY_DIR.parent
1380
+ _PYTEST_DIR = Path(_pytest.__file__).parent
1381
+
1382
+
1383
+ def filter_traceback(entry: TracebackEntry) -> bool:
1384
+ """Return True if a TracebackEntry instance should be included in tracebacks.
1385
+
1386
+ We hide traceback entries of:
1387
+
1388
+ * dynamically generated code (no code to show up for it);
1389
+ * internal traceback from pytest or its internal libraries, py and pluggy.
1390
+ """
1391
+ # entry.path might sometimes return a str object when the entry
1392
+ # points to dynamically generated code.
1393
+ # See https://bitbucket.org/pytest-dev/py/issues/71.
1394
+ raw_filename = entry.frame.code.raw.co_filename
1395
+ is_generated = "<" in raw_filename and ">" in raw_filename
1396
+ if is_generated:
1397
+ return False
1398
+
1399
+ # entry.path might point to a non-existing file, in which case it will
1400
+ # also return a str object. See #1133.
1401
+ p = Path(entry.path)
1402
+
1403
+ parents = p.parents
1404
+ if _PLUGGY_DIR in parents:
1405
+ return False
1406
+ if _PYTEST_DIR in parents:
1407
+ return False
1408
+
1409
+ return True
.venv/lib/python3.11/site-packages/_pytest/_code/source.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from bisect import bisect_right
6
+ import inspect
7
+ import textwrap
8
+ import tokenize
9
+ import types
10
+ from typing import Iterable
11
+ from typing import Iterator
12
+ from typing import overload
13
+ import warnings
14
+
15
+
16
+ class Source:
17
+ """An immutable object holding a source code fragment.
18
+
19
+ When using Source(...), the source lines are deindented.
20
+ """
21
+
22
+ def __init__(self, obj: object = None) -> None:
23
+ if not obj:
24
+ self.lines: list[str] = []
25
+ elif isinstance(obj, Source):
26
+ self.lines = obj.lines
27
+ elif isinstance(obj, (tuple, list)):
28
+ self.lines = deindent(x.rstrip("\n") for x in obj)
29
+ elif isinstance(obj, str):
30
+ self.lines = deindent(obj.split("\n"))
31
+ else:
32
+ try:
33
+ rawcode = getrawcode(obj)
34
+ src = inspect.getsource(rawcode)
35
+ except TypeError:
36
+ src = inspect.getsource(obj) # type: ignore[arg-type]
37
+ self.lines = deindent(src.split("\n"))
38
+
39
+ def __eq__(self, other: object) -> bool:
40
+ if not isinstance(other, Source):
41
+ return NotImplemented
42
+ return self.lines == other.lines
43
+
44
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
45
+ __hash__ = None # type: ignore
46
+
47
+ @overload
48
+ def __getitem__(self, key: int) -> str: ...
49
+
50
+ @overload
51
+ def __getitem__(self, key: slice) -> Source: ...
52
+
53
+ def __getitem__(self, key: int | slice) -> str | Source:
54
+ if isinstance(key, int):
55
+ return self.lines[key]
56
+ else:
57
+ if key.step not in (None, 1):
58
+ raise IndexError("cannot slice a Source with a step")
59
+ newsource = Source()
60
+ newsource.lines = self.lines[key.start : key.stop]
61
+ return newsource
62
+
63
+ def __iter__(self) -> Iterator[str]:
64
+ return iter(self.lines)
65
+
66
+ def __len__(self) -> int:
67
+ return len(self.lines)
68
+
69
+ def strip(self) -> Source:
70
+ """Return new Source object with trailing and leading blank lines removed."""
71
+ start, end = 0, len(self)
72
+ while start < end and not self.lines[start].strip():
73
+ start += 1
74
+ while end > start and not self.lines[end - 1].strip():
75
+ end -= 1
76
+ source = Source()
77
+ source.lines[:] = self.lines[start:end]
78
+ return source
79
+
80
+ def indent(self, indent: str = " " * 4) -> Source:
81
+ """Return a copy of the source object with all lines indented by the
82
+ given indent-string."""
83
+ newsource = Source()
84
+ newsource.lines = [(indent + line) for line in self.lines]
85
+ return newsource
86
+
87
+ def getstatement(self, lineno: int) -> Source:
88
+ """Return Source statement which contains the given linenumber
89
+ (counted from 0)."""
90
+ start, end = self.getstatementrange(lineno)
91
+ return self[start:end]
92
+
93
+ def getstatementrange(self, lineno: int) -> tuple[int, int]:
94
+ """Return (start, end) tuple which spans the minimal statement region
95
+ which containing the given lineno."""
96
+ if not (0 <= lineno < len(self)):
97
+ raise IndexError("lineno out of range")
98
+ ast, start, end = getstatementrange_ast(lineno, self)
99
+ return start, end
100
+
101
+ def deindent(self) -> Source:
102
+ """Return a new Source object deindented."""
103
+ newsource = Source()
104
+ newsource.lines[:] = deindent(self.lines)
105
+ return newsource
106
+
107
+ def __str__(self) -> str:
108
+ return "\n".join(self.lines)
109
+
110
+
111
+ #
112
+ # helper functions
113
+ #
114
+
115
+
116
+ def findsource(obj) -> tuple[Source | None, int]:
117
+ try:
118
+ sourcelines, lineno = inspect.findsource(obj)
119
+ except Exception:
120
+ return None, -1
121
+ source = Source()
122
+ source.lines = [line.rstrip() for line in sourcelines]
123
+ return source, lineno
124
+
125
+
126
+ def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
127
+ """Return code object for given function."""
128
+ try:
129
+ return obj.__code__ # type: ignore[attr-defined,no-any-return]
130
+ except AttributeError:
131
+ pass
132
+ if trycall:
133
+ call = getattr(obj, "__call__", None)
134
+ if call and not isinstance(obj, type):
135
+ return getrawcode(call, trycall=False)
136
+ raise TypeError(f"could not get code object for {obj!r}")
137
+
138
+
139
+ def deindent(lines: Iterable[str]) -> list[str]:
140
+ return textwrap.dedent("\n".join(lines)).splitlines()
141
+
142
+
143
+ def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None]:
144
+ # Flatten all statements and except handlers into one lineno-list.
145
+ # AST's line numbers start indexing at 1.
146
+ values: list[int] = []
147
+ for x in ast.walk(node):
148
+ if isinstance(x, (ast.stmt, ast.ExceptHandler)):
149
+ # The lineno points to the class/def, so need to include the decorators.
150
+ if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
151
+ for d in x.decorator_list:
152
+ values.append(d.lineno - 1)
153
+ values.append(x.lineno - 1)
154
+ for name in ("finalbody", "orelse"):
155
+ val: list[ast.stmt] | None = getattr(x, name, None)
156
+ if val:
157
+ # Treat the finally/orelse part as its own statement.
158
+ values.append(val[0].lineno - 1 - 1)
159
+ values.sort()
160
+ insert_index = bisect_right(values, lineno)
161
+ start = values[insert_index - 1]
162
+ if insert_index >= len(values):
163
+ end = None
164
+ else:
165
+ end = values[insert_index]
166
+ return start, end
167
+
168
+
169
+ def getstatementrange_ast(
170
+ lineno: int,
171
+ source: Source,
172
+ assertion: bool = False,
173
+ astnode: ast.AST | None = None,
174
+ ) -> tuple[ast.AST, int, int]:
175
+ if astnode is None:
176
+ content = str(source)
177
+ # See #4260:
178
+ # Don't produce duplicate warnings when compiling source to find AST.
179
+ with warnings.catch_warnings():
180
+ warnings.simplefilter("ignore")
181
+ astnode = ast.parse(content, "source", "exec")
182
+
183
+ start, end = get_statement_startend2(lineno, astnode)
184
+ # We need to correct the end:
185
+ # - ast-parsing strips comments
186
+ # - there might be empty lines
187
+ # - we might have lesser indented code blocks at the end
188
+ if end is None:
189
+ end = len(source.lines)
190
+
191
+ if end > start + 1:
192
+ # Make sure we don't span differently indented code blocks
193
+ # by using the BlockFinder helper used which inspect.getsource() uses itself.
194
+ block_finder = inspect.BlockFinder()
195
+ # If we start with an indented line, put blockfinder to "started" mode.
196
+ block_finder.started = (
197
+ bool(source.lines[start]) and source.lines[start][0].isspace()
198
+ )
199
+ it = ((x + "\n") for x in source.lines[start:end])
200
+ try:
201
+ for tok in tokenize.generate_tokens(lambda: next(it)):
202
+ block_finder.tokeneater(*tok)
203
+ except (inspect.EndOfBlock, IndentationError):
204
+ end = block_finder.last + start
205
+ except Exception:
206
+ pass
207
+
208
+ # The end might still point to a comment or empty line, correct it.
209
+ while end:
210
+ line = source.lines[end - 1].lstrip()
211
+ if line.startswith("#") or not line:
212
+ end -= 1
213
+ else:
214
+ break
215
+ return astnode, start, end
.venv/lib/python3.11/site-packages/_pytest/_io/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from .terminalwriter import get_terminal_width
4
+ from .terminalwriter import TerminalWriter
5
+
6
+
7
+ __all__ = [
8
+ "TerminalWriter",
9
+ "get_terminal_width",
10
+ ]
.venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (415 Bytes). View file
 
.venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/pprint.cpython-311.pyc ADDED
Binary file (25 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/saferepr.cpython-311.pyc ADDED
Binary file (6.66 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/terminalwriter.cpython-311.pyc ADDED
Binary file (12.8 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/_io/__pycache__/wcwidth.cpython-311.pyc ADDED
Binary file (1.94 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/_io/pprint.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ # This module was imported from the cpython standard library
3
+ # (https://github.com/python/cpython/) at commit
4
+ # c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
5
+ #
6
+ #
7
+ # Original Author: Fred L. Drake, Jr.
8
+ # fdrake@acm.org
9
+ #
10
+ # This is a simple little module I wrote to make life easier. I didn't
11
+ # see anything quite like it in the library, though I may have overlooked
12
+ # something. I wrote this when I was trying to read some heavily nested
13
+ # tuples with fairly non-descriptive content. This is modeled very much
14
+ # after Lisp/Scheme - style pretty-printing of lists. If you find it
15
+ # useful, thank small children who sleep at night.
16
+ from __future__ import annotations
17
+
18
+ import collections as _collections
19
+ import dataclasses as _dataclasses
20
+ from io import StringIO as _StringIO
21
+ import re
22
+ import types as _types
23
+ from typing import Any
24
+ from typing import Callable
25
+ from typing import IO
26
+ from typing import Iterator
27
+
28
+
29
+ class _safe_key:
30
+ """Helper function for key functions when sorting unorderable objects.
31
+
32
+ The wrapped-object will fallback to a Py2.x style comparison for
33
+ unorderable types (sorting first comparing the type name and then by
34
+ the obj ids). Does not work recursively, so dict.items() must have
35
+ _safe_key applied to both the key and the value.
36
+
37
+ """
38
+
39
+ __slots__ = ["obj"]
40
+
41
+ def __init__(self, obj):
42
+ self.obj = obj
43
+
44
+ def __lt__(self, other):
45
+ try:
46
+ return self.obj < other.obj
47
+ except TypeError:
48
+ return (str(type(self.obj)), id(self.obj)) < (
49
+ str(type(other.obj)),
50
+ id(other.obj),
51
+ )
52
+
53
+
54
+ def _safe_tuple(t):
55
+ """Helper function for comparing 2-tuples"""
56
+ return _safe_key(t[0]), _safe_key(t[1])
57
+
58
+
59
+ class PrettyPrinter:
60
+ def __init__(
61
+ self,
62
+ indent: int = 4,
63
+ width: int = 80,
64
+ depth: int | None = None,
65
+ ) -> None:
66
+ """Handle pretty printing operations onto a stream using a set of
67
+ configured parameters.
68
+
69
+ indent
70
+ Number of spaces to indent for each level of nesting.
71
+
72
+ width
73
+ Attempted maximum number of columns in the output.
74
+
75
+ depth
76
+ The maximum depth to print out nested structures.
77
+
78
+ """
79
+ if indent < 0:
80
+ raise ValueError("indent must be >= 0")
81
+ if depth is not None and depth <= 0:
82
+ raise ValueError("depth must be > 0")
83
+ if not width:
84
+ raise ValueError("width must be != 0")
85
+ self._depth = depth
86
+ self._indent_per_level = indent
87
+ self._width = width
88
+
89
+ def pformat(self, object: Any) -> str:
90
+ sio = _StringIO()
91
+ self._format(object, sio, 0, 0, set(), 0)
92
+ return sio.getvalue()
93
+
94
+ def _format(
95
+ self,
96
+ object: Any,
97
+ stream: IO[str],
98
+ indent: int,
99
+ allowance: int,
100
+ context: set[int],
101
+ level: int,
102
+ ) -> None:
103
+ objid = id(object)
104
+ if objid in context:
105
+ stream.write(_recursion(object))
106
+ return
107
+
108
+ p = self._dispatch.get(type(object).__repr__, None)
109
+ if p is not None:
110
+ context.add(objid)
111
+ p(self, object, stream, indent, allowance, context, level + 1)
112
+ context.remove(objid)
113
+ elif (
114
+ _dataclasses.is_dataclass(object) # type:ignore[unreachable]
115
+ and not isinstance(object, type)
116
+ and object.__dataclass_params__.repr
117
+ and
118
+ # Check dataclass has generated repr method.
119
+ hasattr(object.__repr__, "__wrapped__")
120
+ and "__create_fn__" in object.__repr__.__wrapped__.__qualname__
121
+ ):
122
+ context.add(objid) # type:ignore[unreachable]
123
+ self._pprint_dataclass(
124
+ object, stream, indent, allowance, context, level + 1
125
+ )
126
+ context.remove(objid)
127
+ else:
128
+ stream.write(self._repr(object, context, level))
129
+
130
+ def _pprint_dataclass(
131
+ self,
132
+ object: Any,
133
+ stream: IO[str],
134
+ indent: int,
135
+ allowance: int,
136
+ context: set[int],
137
+ level: int,
138
+ ) -> None:
139
+ cls_name = object.__class__.__name__
140
+ items = [
141
+ (f.name, getattr(object, f.name))
142
+ for f in _dataclasses.fields(object)
143
+ if f.repr
144
+ ]
145
+ stream.write(cls_name + "(")
146
+ self._format_namespace_items(items, stream, indent, allowance, context, level)
147
+ stream.write(")")
148
+
149
+ _dispatch: dict[
150
+ Callable[..., str],
151
+ Callable[[PrettyPrinter, Any, IO[str], int, int, set[int], int], None],
152
+ ] = {}
153
+
154
+ def _pprint_dict(
155
+ self,
156
+ object: Any,
157
+ stream: IO[str],
158
+ indent: int,
159
+ allowance: int,
160
+ context: set[int],
161
+ level: int,
162
+ ) -> None:
163
+ write = stream.write
164
+ write("{")
165
+ items = sorted(object.items(), key=_safe_tuple)
166
+ self._format_dict_items(items, stream, indent, allowance, context, level)
167
+ write("}")
168
+
169
+ _dispatch[dict.__repr__] = _pprint_dict
170
+
171
+ def _pprint_ordered_dict(
172
+ self,
173
+ object: Any,
174
+ stream: IO[str],
175
+ indent: int,
176
+ allowance: int,
177
+ context: set[int],
178
+ level: int,
179
+ ) -> None:
180
+ if not len(object):
181
+ stream.write(repr(object))
182
+ return
183
+ cls = object.__class__
184
+ stream.write(cls.__name__ + "(")
185
+ self._pprint_dict(object, stream, indent, allowance, context, level)
186
+ stream.write(")")
187
+
188
+ _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
189
+
190
+ def _pprint_list(
191
+ self,
192
+ object: Any,
193
+ stream: IO[str],
194
+ indent: int,
195
+ allowance: int,
196
+ context: set[int],
197
+ level: int,
198
+ ) -> None:
199
+ stream.write("[")
200
+ self._format_items(object, stream, indent, allowance, context, level)
201
+ stream.write("]")
202
+
203
+ _dispatch[list.__repr__] = _pprint_list
204
+
205
+ def _pprint_tuple(
206
+ self,
207
+ object: Any,
208
+ stream: IO[str],
209
+ indent: int,
210
+ allowance: int,
211
+ context: set[int],
212
+ level: int,
213
+ ) -> None:
214
+ stream.write("(")
215
+ self._format_items(object, stream, indent, allowance, context, level)
216
+ stream.write(")")
217
+
218
+ _dispatch[tuple.__repr__] = _pprint_tuple
219
+
220
+ def _pprint_set(
221
+ self,
222
+ object: Any,
223
+ stream: IO[str],
224
+ indent: int,
225
+ allowance: int,
226
+ context: set[int],
227
+ level: int,
228
+ ) -> None:
229
+ if not len(object):
230
+ stream.write(repr(object))
231
+ return
232
+ typ = object.__class__
233
+ if typ is set:
234
+ stream.write("{")
235
+ endchar = "}"
236
+ else:
237
+ stream.write(typ.__name__ + "({")
238
+ endchar = "})"
239
+ object = sorted(object, key=_safe_key)
240
+ self._format_items(object, stream, indent, allowance, context, level)
241
+ stream.write(endchar)
242
+
243
+ _dispatch[set.__repr__] = _pprint_set
244
+ _dispatch[frozenset.__repr__] = _pprint_set
245
+
246
+ def _pprint_str(
247
+ self,
248
+ object: Any,
249
+ stream: IO[str],
250
+ indent: int,
251
+ allowance: int,
252
+ context: set[int],
253
+ level: int,
254
+ ) -> None:
255
+ write = stream.write
256
+ if not len(object):
257
+ write(repr(object))
258
+ return
259
+ chunks = []
260
+ lines = object.splitlines(True)
261
+ if level == 1:
262
+ indent += 1
263
+ allowance += 1
264
+ max_width1 = max_width = self._width - indent
265
+ for i, line in enumerate(lines):
266
+ rep = repr(line)
267
+ if i == len(lines) - 1:
268
+ max_width1 -= allowance
269
+ if len(rep) <= max_width1:
270
+ chunks.append(rep)
271
+ else:
272
+ # A list of alternating (non-space, space) strings
273
+ parts = re.findall(r"\S*\s*", line)
274
+ assert parts
275
+ assert not parts[-1]
276
+ parts.pop() # drop empty last part
277
+ max_width2 = max_width
278
+ current = ""
279
+ for j, part in enumerate(parts):
280
+ candidate = current + part
281
+ if j == len(parts) - 1 and i == len(lines) - 1:
282
+ max_width2 -= allowance
283
+ if len(repr(candidate)) > max_width2:
284
+ if current:
285
+ chunks.append(repr(current))
286
+ current = part
287
+ else:
288
+ current = candidate
289
+ if current:
290
+ chunks.append(repr(current))
291
+ if len(chunks) == 1:
292
+ write(rep)
293
+ return
294
+ if level == 1:
295
+ write("(")
296
+ for i, rep in enumerate(chunks):
297
+ if i > 0:
298
+ write("\n" + " " * indent)
299
+ write(rep)
300
+ if level == 1:
301
+ write(")")
302
+
303
+ _dispatch[str.__repr__] = _pprint_str
304
+
305
+ def _pprint_bytes(
306
+ self,
307
+ object: Any,
308
+ stream: IO[str],
309
+ indent: int,
310
+ allowance: int,
311
+ context: set[int],
312
+ level: int,
313
+ ) -> None:
314
+ write = stream.write
315
+ if len(object) <= 4:
316
+ write(repr(object))
317
+ return
318
+ parens = level == 1
319
+ if parens:
320
+ indent += 1
321
+ allowance += 1
322
+ write("(")
323
+ delim = ""
324
+ for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
325
+ write(delim)
326
+ write(rep)
327
+ if not delim:
328
+ delim = "\n" + " " * indent
329
+ if parens:
330
+ write(")")
331
+
332
+ _dispatch[bytes.__repr__] = _pprint_bytes
333
+
334
+ def _pprint_bytearray(
335
+ self,
336
+ object: Any,
337
+ stream: IO[str],
338
+ indent: int,
339
+ allowance: int,
340
+ context: set[int],
341
+ level: int,
342
+ ) -> None:
343
+ write = stream.write
344
+ write("bytearray(")
345
+ self._pprint_bytes(
346
+ bytes(object), stream, indent + 10, allowance + 1, context, level + 1
347
+ )
348
+ write(")")
349
+
350
+ _dispatch[bytearray.__repr__] = _pprint_bytearray
351
+
352
+ def _pprint_mappingproxy(
353
+ self,
354
+ object: Any,
355
+ stream: IO[str],
356
+ indent: int,
357
+ allowance: int,
358
+ context: set[int],
359
+ level: int,
360
+ ) -> None:
361
+ stream.write("mappingproxy(")
362
+ self._format(object.copy(), stream, indent, allowance, context, level)
363
+ stream.write(")")
364
+
365
+ _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
366
+
367
+ def _pprint_simplenamespace(
368
+ self,
369
+ object: Any,
370
+ stream: IO[str],
371
+ indent: int,
372
+ allowance: int,
373
+ context: set[int],
374
+ level: int,
375
+ ) -> None:
376
+ if type(object) is _types.SimpleNamespace:
377
+ # The SimpleNamespace repr is "namespace" instead of the class
378
+ # name, so we do the same here. For subclasses; use the class name.
379
+ cls_name = "namespace"
380
+ else:
381
+ cls_name = object.__class__.__name__
382
+ items = object.__dict__.items()
383
+ stream.write(cls_name + "(")
384
+ self._format_namespace_items(items, stream, indent, allowance, context, level)
385
+ stream.write(")")
386
+
387
+ _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
388
+
389
+ def _format_dict_items(
390
+ self,
391
+ items: list[tuple[Any, Any]],
392
+ stream: IO[str],
393
+ indent: int,
394
+ allowance: int,
395
+ context: set[int],
396
+ level: int,
397
+ ) -> None:
398
+ if not items:
399
+ return
400
+
401
+ write = stream.write
402
+ item_indent = indent + self._indent_per_level
403
+ delimnl = "\n" + " " * item_indent
404
+ for key, ent in items:
405
+ write(delimnl)
406
+ write(self._repr(key, context, level))
407
+ write(": ")
408
+ self._format(ent, stream, item_indent, 1, context, level)
409
+ write(",")
410
+
411
+ write("\n" + " " * indent)
412
+
413
+ def _format_namespace_items(
414
+ self,
415
+ items: list[tuple[Any, Any]],
416
+ stream: IO[str],
417
+ indent: int,
418
+ allowance: int,
419
+ context: set[int],
420
+ level: int,
421
+ ) -> None:
422
+ if not items:
423
+ return
424
+
425
+ write = stream.write
426
+ item_indent = indent + self._indent_per_level
427
+ delimnl = "\n" + " " * item_indent
428
+ for key, ent in items:
429
+ write(delimnl)
430
+ write(key)
431
+ write("=")
432
+ if id(ent) in context:
433
+ # Special-case representation of recursion to match standard
434
+ # recursive dataclass repr.
435
+ write("...")
436
+ else:
437
+ self._format(
438
+ ent,
439
+ stream,
440
+ item_indent + len(key) + 1,
441
+ 1,
442
+ context,
443
+ level,
444
+ )
445
+
446
+ write(",")
447
+
448
+ write("\n" + " " * indent)
449
+
450
+ def _format_items(
451
+ self,
452
+ items: list[Any],
453
+ stream: IO[str],
454
+ indent: int,
455
+ allowance: int,
456
+ context: set[int],
457
+ level: int,
458
+ ) -> None:
459
+ if not items:
460
+ return
461
+
462
+ write = stream.write
463
+ item_indent = indent + self._indent_per_level
464
+ delimnl = "\n" + " " * item_indent
465
+
466
+ for item in items:
467
+ write(delimnl)
468
+ self._format(item, stream, item_indent, 1, context, level)
469
+ write(",")
470
+
471
+ write("\n" + " " * indent)
472
+
473
+ def _repr(self, object: Any, context: set[int], level: int) -> str:
474
+ return self._safe_repr(object, context.copy(), self._depth, level)
475
+
476
+ def _pprint_default_dict(
477
+ self,
478
+ object: Any,
479
+ stream: IO[str],
480
+ indent: int,
481
+ allowance: int,
482
+ context: set[int],
483
+ level: int,
484
+ ) -> None:
485
+ rdf = self._repr(object.default_factory, context, level)
486
+ stream.write(f"{object.__class__.__name__}({rdf}, ")
487
+ self._pprint_dict(object, stream, indent, allowance, context, level)
488
+ stream.write(")")
489
+
490
+ _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
491
+
492
+ def _pprint_counter(
493
+ self,
494
+ object: Any,
495
+ stream: IO[str],
496
+ indent: int,
497
+ allowance: int,
498
+ context: set[int],
499
+ level: int,
500
+ ) -> None:
501
+ stream.write(object.__class__.__name__ + "(")
502
+
503
+ if object:
504
+ stream.write("{")
505
+ items = object.most_common()
506
+ self._format_dict_items(items, stream, indent, allowance, context, level)
507
+ stream.write("}")
508
+
509
+ stream.write(")")
510
+
511
+ _dispatch[_collections.Counter.__repr__] = _pprint_counter
512
+
513
+ def _pprint_chain_map(
514
+ self,
515
+ object: Any,
516
+ stream: IO[str],
517
+ indent: int,
518
+ allowance: int,
519
+ context: set[int],
520
+ level: int,
521
+ ) -> None:
522
+ if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
523
+ stream.write(repr(object))
524
+ return
525
+
526
+ stream.write(object.__class__.__name__ + "(")
527
+ self._format_items(object.maps, stream, indent, allowance, context, level)
528
+ stream.write(")")
529
+
530
+ _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
531
+
532
+ def _pprint_deque(
533
+ self,
534
+ object: Any,
535
+ stream: IO[str],
536
+ indent: int,
537
+ allowance: int,
538
+ context: set[int],
539
+ level: int,
540
+ ) -> None:
541
+ stream.write(object.__class__.__name__ + "(")
542
+ if object.maxlen is not None:
543
+ stream.write("maxlen=%d, " % object.maxlen)
544
+ stream.write("[")
545
+
546
+ self._format_items(object, stream, indent, allowance + 1, context, level)
547
+ stream.write("])")
548
+
549
+ _dispatch[_collections.deque.__repr__] = _pprint_deque
550
+
551
+ def _pprint_user_dict(
552
+ self,
553
+ object: Any,
554
+ stream: IO[str],
555
+ indent: int,
556
+ allowance: int,
557
+ context: set[int],
558
+ level: int,
559
+ ) -> None:
560
+ self._format(object.data, stream, indent, allowance, context, level - 1)
561
+
562
+ _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
563
+
564
+ def _pprint_user_list(
565
+ self,
566
+ object: Any,
567
+ stream: IO[str],
568
+ indent: int,
569
+ allowance: int,
570
+ context: set[int],
571
+ level: int,
572
+ ) -> None:
573
+ self._format(object.data, stream, indent, allowance, context, level - 1)
574
+
575
+ _dispatch[_collections.UserList.__repr__] = _pprint_user_list
576
+
577
+ def _pprint_user_string(
578
+ self,
579
+ object: Any,
580
+ stream: IO[str],
581
+ indent: int,
582
+ allowance: int,
583
+ context: set[int],
584
+ level: int,
585
+ ) -> None:
586
+ self._format(object.data, stream, indent, allowance, context, level - 1)
587
+
588
+ _dispatch[_collections.UserString.__repr__] = _pprint_user_string
589
+
590
+ def _safe_repr(
591
+ self, object: Any, context: set[int], maxlevels: int | None, level: int
592
+ ) -> str:
593
+ typ = type(object)
594
+ if typ in _builtin_scalars:
595
+ return repr(object)
596
+
597
+ r = getattr(typ, "__repr__", None)
598
+
599
+ if issubclass(typ, dict) and r is dict.__repr__:
600
+ if not object:
601
+ return "{}"
602
+ objid = id(object)
603
+ if maxlevels and level >= maxlevels:
604
+ return "{...}"
605
+ if objid in context:
606
+ return _recursion(object)
607
+ context.add(objid)
608
+ components: list[str] = []
609
+ append = components.append
610
+ level += 1
611
+ for k, v in sorted(object.items(), key=_safe_tuple):
612
+ krepr = self._safe_repr(k, context, maxlevels, level)
613
+ vrepr = self._safe_repr(v, context, maxlevels, level)
614
+ append(f"{krepr}: {vrepr}")
615
+ context.remove(objid)
616
+ return "{{{}}}".format(", ".join(components))
617
+
618
+ if (issubclass(typ, list) and r is list.__repr__) or (
619
+ issubclass(typ, tuple) and r is tuple.__repr__
620
+ ):
621
+ if issubclass(typ, list):
622
+ if not object:
623
+ return "[]"
624
+ format = "[%s]"
625
+ elif len(object) == 1:
626
+ format = "(%s,)"
627
+ else:
628
+ if not object:
629
+ return "()"
630
+ format = "(%s)"
631
+ objid = id(object)
632
+ if maxlevels and level >= maxlevels:
633
+ return format % "..."
634
+ if objid in context:
635
+ return _recursion(object)
636
+ context.add(objid)
637
+ components = []
638
+ append = components.append
639
+ level += 1
640
+ for o in object:
641
+ orepr = self._safe_repr(o, context, maxlevels, level)
642
+ append(orepr)
643
+ context.remove(objid)
644
+ return format % ", ".join(components)
645
+
646
+ return repr(object)
647
+
648
+
649
+ _builtin_scalars = frozenset(
650
+ {str, bytes, bytearray, float, complex, bool, type(None), int}
651
+ )
652
+
653
+
654
+ def _recursion(object: Any) -> str:
655
+ return f"<Recursion on {type(object).__name__} with id={id(object)}>"
656
+
657
+
658
+ def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]:
659
+ current = b""
660
+ last = len(object) // 4 * 4
661
+ for i in range(0, len(object), 4):
662
+ part = object[i : i + 4]
663
+ candidate = current + part
664
+ if i == last:
665
+ width -= allowance
666
+ if len(repr(candidate)) > width:
667
+ if current:
668
+ yield repr(current)
669
+ current = part
670
+ else:
671
+ current = candidate
672
+ if current:
673
+ yield repr(current)
.venv/lib/python3.11/site-packages/_pytest/_io/saferepr.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import pprint
4
+ import reprlib
5
+
6
+
7
+ def _try_repr_or_str(obj: object) -> str:
8
+ try:
9
+ return repr(obj)
10
+ except (KeyboardInterrupt, SystemExit):
11
+ raise
12
+ except BaseException:
13
+ return f'{type(obj).__name__}("{obj}")'
14
+
15
+
16
+ def _format_repr_exception(exc: BaseException, obj: object) -> str:
17
+ try:
18
+ exc_info = _try_repr_or_str(exc)
19
+ except (KeyboardInterrupt, SystemExit):
20
+ raise
21
+ except BaseException as inner_exc:
22
+ exc_info = f"unpresentable exception ({_try_repr_or_str(inner_exc)})"
23
+ return (
24
+ f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
25
+ )
26
+
27
+
28
+ def _ellipsize(s: str, maxsize: int) -> str:
29
+ if len(s) > maxsize:
30
+ i = max(0, (maxsize - 3) // 2)
31
+ j = max(0, maxsize - 3 - i)
32
+ return s[:i] + "..." + s[len(s) - j :]
33
+ return s
34
+
35
+
36
+ class SafeRepr(reprlib.Repr):
37
+ """
38
+ repr.Repr that limits the resulting size of repr() and includes
39
+ information on exceptions raised during the call.
40
+ """
41
+
42
+ def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None:
43
+ """
44
+ :param maxsize:
45
+ If not None, will truncate the resulting repr to that specific size, using ellipsis
46
+ somewhere in the middle to hide the extra text.
47
+ If None, will not impose any size limits on the returning repr.
48
+ """
49
+ super().__init__()
50
+ # ``maxstring`` is used by the superclass, and needs to be an int; using a
51
+ # very large number in case maxsize is None, meaning we want to disable
52
+ # truncation.
53
+ self.maxstring = maxsize if maxsize is not None else 1_000_000_000
54
+ self.maxsize = maxsize
55
+ self.use_ascii = use_ascii
56
+
57
+ def repr(self, x: object) -> str:
58
+ try:
59
+ if self.use_ascii:
60
+ s = ascii(x)
61
+ else:
62
+ s = super().repr(x)
63
+ except (KeyboardInterrupt, SystemExit):
64
+ raise
65
+ except BaseException as exc:
66
+ s = _format_repr_exception(exc, x)
67
+ if self.maxsize is not None:
68
+ s = _ellipsize(s, self.maxsize)
69
+ return s
70
+
71
+ def repr_instance(self, x: object, level: int) -> str:
72
+ try:
73
+ s = repr(x)
74
+ except (KeyboardInterrupt, SystemExit):
75
+ raise
76
+ except BaseException as exc:
77
+ s = _format_repr_exception(exc, x)
78
+ if self.maxsize is not None:
79
+ s = _ellipsize(s, self.maxsize)
80
+ return s
81
+
82
+
83
+ def safeformat(obj: object) -> str:
84
+ """Return a pretty printed string for the given object.
85
+
86
+ Failing __repr__ functions of user instances will be represented
87
+ with a short exception info.
88
+ """
89
+ try:
90
+ return pprint.pformat(obj)
91
+ except Exception as exc:
92
+ return _format_repr_exception(exc, obj)
93
+
94
+
95
+ # Maximum size of overall repr of objects to display during assertion errors.
96
+ DEFAULT_REPR_MAX_SIZE = 240
97
+
98
+
99
+ def saferepr(
100
+ obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
101
+ ) -> str:
102
+ """Return a size-limited safe repr-string for the given object.
103
+
104
+ Failing __repr__ functions of user instances will be represented
105
+ with a short exception info and 'saferepr' generally takes
106
+ care to never raise exceptions itself.
107
+
108
+ This function is a wrapper around the Repr/reprlib functionality of the
109
+ stdlib.
110
+ """
111
+ return SafeRepr(maxsize, use_ascii).repr(obj)
112
+
113
+
114
+ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
115
+ """Return an unlimited-size safe repr-string for the given object.
116
+
117
+ As with saferepr, failing __repr__ functions of user instances
118
+ will be represented with a short exception info.
119
+
120
+ This function is a wrapper around simple repr.
121
+
122
+ Note: a cleaner solution would be to alter ``saferepr``this way
123
+ when maxsize=None, but that might affect some other code.
124
+ """
125
+ try:
126
+ if use_ascii:
127
+ return ascii(obj)
128
+ return repr(obj)
129
+ except Exception as exc:
130
+ return _format_repr_exception(exc, obj)
.venv/lib/python3.11/site-packages/_pytest/_io/terminalwriter.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper functions for writing to terminals and files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import sys
8
+ from typing import final
9
+ from typing import Literal
10
+ from typing import Sequence
11
+ from typing import TextIO
12
+ from typing import TYPE_CHECKING
13
+
14
+ from ..compat import assert_never
15
+ from .wcwidth import wcswidth
16
+
17
+
18
+ if TYPE_CHECKING:
19
+ from pygments.formatter import Formatter
20
+ from pygments.lexer import Lexer
21
+
22
+
23
+ # This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
24
+
25
+
26
+ def get_terminal_width() -> int:
27
+ width, _ = shutil.get_terminal_size(fallback=(80, 24))
28
+
29
+ # The Windows get_terminal_size may be bogus, let's sanify a bit.
30
+ if width < 40:
31
+ width = 80
32
+
33
+ return width
34
+
35
+
36
+ def should_do_markup(file: TextIO) -> bool:
37
+ if os.environ.get("PY_COLORS") == "1":
38
+ return True
39
+ if os.environ.get("PY_COLORS") == "0":
40
+ return False
41
+ if os.environ.get("NO_COLOR"):
42
+ return False
43
+ if os.environ.get("FORCE_COLOR"):
44
+ return True
45
+ return (
46
+ hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb"
47
+ )
48
+
49
+
50
+ @final
51
+ class TerminalWriter:
52
+ _esctable = dict(
53
+ black=30,
54
+ red=31,
55
+ green=32,
56
+ yellow=33,
57
+ blue=34,
58
+ purple=35,
59
+ cyan=36,
60
+ white=37,
61
+ Black=40,
62
+ Red=41,
63
+ Green=42,
64
+ Yellow=43,
65
+ Blue=44,
66
+ Purple=45,
67
+ Cyan=46,
68
+ White=47,
69
+ bold=1,
70
+ light=2,
71
+ blink=5,
72
+ invert=7,
73
+ )
74
+
75
+ def __init__(self, file: TextIO | None = None) -> None:
76
+ if file is None:
77
+ file = sys.stdout
78
+ if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32":
79
+ try:
80
+ import colorama
81
+ except ImportError:
82
+ pass
83
+ else:
84
+ file = colorama.AnsiToWin32(file).stream
85
+ assert file is not None
86
+ self._file = file
87
+ self.hasmarkup = should_do_markup(file)
88
+ self._current_line = ""
89
+ self._terminal_width: int | None = None
90
+ self.code_highlight = True
91
+
92
+ @property
93
+ def fullwidth(self) -> int:
94
+ if self._terminal_width is not None:
95
+ return self._terminal_width
96
+ return get_terminal_width()
97
+
98
+ @fullwidth.setter
99
+ def fullwidth(self, value: int) -> None:
100
+ self._terminal_width = value
101
+
102
+ @property
103
+ def width_of_current_line(self) -> int:
104
+ """Return an estimate of the width so far in the current line."""
105
+ return wcswidth(self._current_line)
106
+
107
+ def markup(self, text: str, **markup: bool) -> str:
108
+ for name in markup:
109
+ if name not in self._esctable:
110
+ raise ValueError(f"unknown markup: {name!r}")
111
+ if self.hasmarkup:
112
+ esc = [self._esctable[name] for name, on in markup.items() if on]
113
+ if esc:
114
+ text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m"
115
+ return text
116
+
117
+ def sep(
118
+ self,
119
+ sepchar: str,
120
+ title: str | None = None,
121
+ fullwidth: int | None = None,
122
+ **markup: bool,
123
+ ) -> None:
124
+ if fullwidth is None:
125
+ fullwidth = self.fullwidth
126
+ # The goal is to have the line be as long as possible
127
+ # under the condition that len(line) <= fullwidth.
128
+ if sys.platform == "win32":
129
+ # If we print in the last column on windows we are on a
130
+ # new line but there is no way to verify/neutralize this
131
+ # (we may not know the exact line width).
132
+ # So let's be defensive to avoid empty lines in the output.
133
+ fullwidth -= 1
134
+ if title is not None:
135
+ # we want 2 + 2*len(fill) + len(title) <= fullwidth
136
+ # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth
137
+ # 2*len(sepchar)*N <= fullwidth - len(title) - 2
138
+ # N <= (fullwidth - len(title) - 2) // (2*len(sepchar))
139
+ N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1)
140
+ fill = sepchar * N
141
+ line = f"{fill} {title} {fill}"
142
+ else:
143
+ # we want len(sepchar)*N <= fullwidth
144
+ # i.e. N <= fullwidth // len(sepchar)
145
+ line = sepchar * (fullwidth // len(sepchar))
146
+ # In some situations there is room for an extra sepchar at the right,
147
+ # in particular if we consider that with a sepchar like "_ " the
148
+ # trailing space is not important at the end of the line.
149
+ if len(line) + len(sepchar.rstrip()) <= fullwidth:
150
+ line += sepchar.rstrip()
151
+
152
+ self.line(line, **markup)
153
+
154
+ def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None:
155
+ if msg:
156
+ current_line = msg.rsplit("\n", 1)[-1]
157
+ if "\n" in msg:
158
+ self._current_line = current_line
159
+ else:
160
+ self._current_line += current_line
161
+
162
+ msg = self.markup(msg, **markup)
163
+
164
+ try:
165
+ self._file.write(msg)
166
+ except UnicodeEncodeError:
167
+ # Some environments don't support printing general Unicode
168
+ # strings, due to misconfiguration or otherwise; in that case,
169
+ # print the string escaped to ASCII.
170
+ # When the Unicode situation improves we should consider
171
+ # letting the error propagate instead of masking it (see #7475
172
+ # for one brief attempt).
173
+ msg = msg.encode("unicode-escape").decode("ascii")
174
+ self._file.write(msg)
175
+
176
+ if flush:
177
+ self.flush()
178
+
179
+ def line(self, s: str = "", **markup: bool) -> None:
180
+ self.write(s, **markup)
181
+ self.write("\n")
182
+
183
+ def flush(self) -> None:
184
+ self._file.flush()
185
+
186
+ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None:
187
+ """Write lines of source code possibly highlighted.
188
+
189
+ Keeping this private for now because the API is clunky. We should discuss how
190
+ to evolve the terminal writer so we can have more precise color support, for example
191
+ being able to write part of a line in one color and the rest in another, and so on.
192
+ """
193
+ if indents and len(indents) != len(lines):
194
+ raise ValueError(
195
+ f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
196
+ )
197
+ if not indents:
198
+ indents = [""] * len(lines)
199
+ source = "\n".join(lines)
200
+ new_lines = self._highlight(source).splitlines()
201
+ for indent, new_line in zip(indents, new_lines):
202
+ self.line(indent + new_line)
203
+
204
+ def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer | None:
205
+ try:
206
+ if lexer == "python":
207
+ from pygments.lexers.python import PythonLexer
208
+
209
+ return PythonLexer()
210
+ elif lexer == "diff":
211
+ from pygments.lexers.diff import DiffLexer
212
+
213
+ return DiffLexer()
214
+ else:
215
+ assert_never(lexer)
216
+ except ModuleNotFoundError:
217
+ return None
218
+
219
+ def _get_pygments_formatter(self) -> Formatter | None:
220
+ try:
221
+ import pygments.util
222
+ except ModuleNotFoundError:
223
+ return None
224
+
225
+ from _pytest.config.exceptions import UsageError
226
+
227
+ theme = os.getenv("PYTEST_THEME")
228
+ theme_mode = os.getenv("PYTEST_THEME_MODE", "dark")
229
+
230
+ try:
231
+ from pygments.formatters.terminal import TerminalFormatter
232
+
233
+ return TerminalFormatter(bg=theme_mode, style=theme)
234
+
235
+ except pygments.util.ClassNotFound as e:
236
+ raise UsageError(
237
+ f"PYTEST_THEME environment variable has an invalid value: '{theme}'. "
238
+ "Hint: See available pygments styles with `pygmentize -L styles`."
239
+ ) from e
240
+ except pygments.util.OptionError as e:
241
+ raise UsageError(
242
+ f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. "
243
+ "The allowed values are 'dark' (default) and 'light'."
244
+ ) from e
245
+
246
+ def _highlight(
247
+ self, source: str, lexer: Literal["diff", "python"] = "python"
248
+ ) -> str:
249
+ """Highlight the given source if we have markup support."""
250
+ if not source or not self.hasmarkup or not self.code_highlight:
251
+ return source
252
+
253
+ pygments_lexer = self._get_pygments_lexer(lexer)
254
+ if pygments_lexer is None:
255
+ return source
256
+
257
+ pygments_formatter = self._get_pygments_formatter()
258
+ if pygments_formatter is None:
259
+ return source
260
+
261
+ from pygments import highlight
262
+
263
+ highlighted: str = highlight(source, pygments_lexer, pygments_formatter)
264
+ # pygments terminal formatter may add a newline when there wasn't one.
265
+ # We don't want this, remove.
266
+ if highlighted[-1] == "\n" and source[-1] != "\n":
267
+ highlighted = highlighted[:-1]
268
+
269
+ # Some lexers will not set the initial color explicitly
270
+ # which may lead to the previous color being propagated to the
271
+ # start of the expression, so reset first.
272
+ highlighted = "\x1b[0m" + highlighted
273
+
274
+ return highlighted
.venv/lib/python3.11/site-packages/_pytest/_io/wcwidth.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ import unicodedata
5
+
6
+
7
+ @lru_cache(100)
8
+ def wcwidth(c: str) -> int:
9
+ """Determine how many columns are needed to display a character in a terminal.
10
+
11
+ Returns -1 if the character is not printable.
12
+ Returns 0, 1 or 2 for other characters.
13
+ """
14
+ o = ord(c)
15
+
16
+ # ASCII fast path.
17
+ if 0x20 <= o < 0x07F:
18
+ return 1
19
+
20
+ # Some Cf/Zp/Zl characters which should be zero-width.
21
+ if (
22
+ o == 0x0000
23
+ or 0x200B <= o <= 0x200F
24
+ or 0x2028 <= o <= 0x202E
25
+ or 0x2060 <= o <= 0x2063
26
+ ):
27
+ return 0
28
+
29
+ category = unicodedata.category(c)
30
+
31
+ # Control characters.
32
+ if category == "Cc":
33
+ return -1
34
+
35
+ # Combining characters with zero width.
36
+ if category in ("Me", "Mn"):
37
+ return 0
38
+
39
+ # Full/Wide east asian characters.
40
+ if unicodedata.east_asian_width(c) in ("F", "W"):
41
+ return 2
42
+
43
+ return 1
44
+
45
+
46
+ def wcswidth(s: str) -> int:
47
+ """Determine how many columns are needed to display a string in a terminal.
48
+
49
+ Returns -1 if the string contains non-printable characters.
50
+ """
51
+ width = 0
52
+ for c in unicodedata.normalize("NFC", s):
53
+ wc = wcwidth(c)
54
+ if wc < 0:
55
+ return -1
56
+ width += wc
57
+ return width
.venv/lib/python3.11/site-packages/_pytest/_version.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '8.3.4'
16
+ __version_tuple__ = version_tuple = (8, 3, 4)
.venv/lib/python3.11/site-packages/_pytest/assertion/__init__.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Support for presenting detailed information in failing assertions."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from typing import Any
8
+ from typing import Generator
9
+ from typing import TYPE_CHECKING
10
+
11
+ from _pytest.assertion import rewrite
12
+ from _pytest.assertion import truncate
13
+ from _pytest.assertion import util
14
+ from _pytest.assertion.rewrite import assertstate_key
15
+ from _pytest.config import Config
16
+ from _pytest.config import hookimpl
17
+ from _pytest.config.argparsing import Parser
18
+ from _pytest.nodes import Item
19
+
20
+
21
+ if TYPE_CHECKING:
22
+ from _pytest.main import Session
23
+
24
+
25
+ def pytest_addoption(parser: Parser) -> None:
26
+ group = parser.getgroup("debugconfig")
27
+ group.addoption(
28
+ "--assert",
29
+ action="store",
30
+ dest="assertmode",
31
+ choices=("rewrite", "plain"),
32
+ default="rewrite",
33
+ metavar="MODE",
34
+ help=(
35
+ "Control assertion debugging tools.\n"
36
+ "'plain' performs no assertion debugging.\n"
37
+ "'rewrite' (the default) rewrites assert statements in test modules"
38
+ " on import to provide assert expression information."
39
+ ),
40
+ )
41
+ parser.addini(
42
+ "enable_assertion_pass_hook",
43
+ type="bool",
44
+ default=False,
45
+ help="Enables the pytest_assertion_pass hook. "
46
+ "Make sure to delete any previously generated pyc cache files.",
47
+ )
48
+ Config._add_verbosity_ini(
49
+ parser,
50
+ Config.VERBOSITY_ASSERTIONS,
51
+ help=(
52
+ "Specify a verbosity level for assertions, overriding the main level. "
53
+ "Higher levels will provide more detailed explanation when an assertion fails."
54
+ ),
55
+ )
56
+
57
+
58
+ def register_assert_rewrite(*names: str) -> None:
59
+ """Register one or more module names to be rewritten on import.
60
+
61
+ This function will make sure that this module or all modules inside
62
+ the package will get their assert statements rewritten.
63
+ Thus you should make sure to call this before the module is
64
+ actually imported, usually in your __init__.py if you are a plugin
65
+ using a package.
66
+
67
+ :param names: The module names to register.
68
+ """
69
+ for name in names:
70
+ if not isinstance(name, str):
71
+ msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
72
+ raise TypeError(msg.format(repr(names)))
73
+ for hook in sys.meta_path:
74
+ if isinstance(hook, rewrite.AssertionRewritingHook):
75
+ importhook = hook
76
+ break
77
+ else:
78
+ # TODO(typing): Add a protocol for mark_rewrite() and use it
79
+ # for importhook and for PytestPluginManager.rewrite_hook.
80
+ importhook = DummyRewriteHook() # type: ignore
81
+ importhook.mark_rewrite(*names)
82
+
83
+
84
+ class DummyRewriteHook:
85
+ """A no-op import hook for when rewriting is disabled."""
86
+
87
+ def mark_rewrite(self, *names: str) -> None:
88
+ pass
89
+
90
+
91
+ class AssertionState:
92
+ """State for the assertion plugin."""
93
+
94
+ def __init__(self, config: Config, mode) -> None:
95
+ self.mode = mode
96
+ self.trace = config.trace.root.get("assertion")
97
+ self.hook: rewrite.AssertionRewritingHook | None = None
98
+
99
+
100
+ def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
101
+ """Try to install the rewrite hook, raise SystemError if it fails."""
102
+ config.stash[assertstate_key] = AssertionState(config, "rewrite")
103
+ config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
104
+ sys.meta_path.insert(0, hook)
105
+ config.stash[assertstate_key].trace("installed rewrite import hook")
106
+
107
+ def undo() -> None:
108
+ hook = config.stash[assertstate_key].hook
109
+ if hook is not None and hook in sys.meta_path:
110
+ sys.meta_path.remove(hook)
111
+
112
+ config.add_cleanup(undo)
113
+ return hook
114
+
115
+
116
+ def pytest_collection(session: Session) -> None:
117
+ # This hook is only called when test modules are collected
118
+ # so for example not in the managing process of pytest-xdist
119
+ # (which does not collect test modules).
120
+ assertstate = session.config.stash.get(assertstate_key, None)
121
+ if assertstate:
122
+ if assertstate.hook is not None:
123
+ assertstate.hook.set_session(session)
124
+
125
+
126
+ @hookimpl(wrapper=True, tryfirst=True)
127
+ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
128
+ """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
129
+
130
+ The rewrite module will use util._reprcompare if it exists to use custom
131
+ reporting via the pytest_assertrepr_compare hook. This sets up this custom
132
+ comparison for the test.
133
+ """
134
+ ihook = item.ihook
135
+
136
+ def callbinrepr(op, left: object, right: object) -> str | None:
137
+ """Call the pytest_assertrepr_compare hook and prepare the result.
138
+
139
+ This uses the first result from the hook and then ensures the
140
+ following:
141
+ * Overly verbose explanations are truncated unless configured otherwise
142
+ (eg. if running in verbose mode).
143
+ * Embedded newlines are escaped to help util.format_explanation()
144
+ later.
145
+ * If the rewrite mode is used embedded %-characters are replaced
146
+ to protect later % formatting.
147
+
148
+ The result can be formatted by util.format_explanation() for
149
+ pretty printing.
150
+ """
151
+ hook_result = ihook.pytest_assertrepr_compare(
152
+ config=item.config, op=op, left=left, right=right
153
+ )
154
+ for new_expl in hook_result:
155
+ if new_expl:
156
+ new_expl = truncate.truncate_if_required(new_expl, item)
157
+ new_expl = [line.replace("\n", "\\n") for line in new_expl]
158
+ res = "\n~".join(new_expl)
159
+ if item.config.getvalue("assertmode") == "rewrite":
160
+ res = res.replace("%", "%%")
161
+ return res
162
+ return None
163
+
164
+ saved_assert_hooks = util._reprcompare, util._assertion_pass
165
+ util._reprcompare = callbinrepr
166
+ util._config = item.config
167
+
168
+ if ihook.pytest_assertion_pass.get_hookimpls():
169
+
170
+ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
171
+ ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl)
172
+
173
+ util._assertion_pass = call_assertion_pass_hook
174
+
175
+ try:
176
+ return (yield)
177
+ finally:
178
+ util._reprcompare, util._assertion_pass = saved_assert_hooks
179
+ util._config = None
180
+
181
+
182
+ def pytest_sessionfinish(session: Session) -> None:
183
+ assertstate = session.config.stash.get(assertstate_key, None)
184
+ if assertstate:
185
+ if assertstate.hook is not None:
186
+ assertstate.hook.set_session(None)
187
+
188
+
189
+ def pytest_assertrepr_compare(
190
+ config: Config, op: str, left: Any, right: Any
191
+ ) -> list[str] | None:
192
+ return util.assertrepr_compare(config=config, op=op, left=left, right=right)
.venv/lib/python3.11/site-packages/_pytest/assertion/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (10.5 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/assertion/__pycache__/rewrite.cpython-311.pyc ADDED
Binary file (66.4 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/assertion/__pycache__/truncate.cpython-311.pyc ADDED
Binary file (4.1 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/assertion/__pycache__/util.cpython-311.pyc ADDED
Binary file (28.2 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/assertion/rewrite.py ADDED
@@ -0,0 +1,1211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rewrite assertion AST to produce nice error messages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from collections import defaultdict
7
+ import errno
8
+ import functools
9
+ import importlib.abc
10
+ import importlib.machinery
11
+ import importlib.util
12
+ import io
13
+ import itertools
14
+ import marshal
15
+ import os
16
+ from pathlib import Path
17
+ from pathlib import PurePath
18
+ import struct
19
+ import sys
20
+ import tokenize
21
+ import types
22
+ from typing import Callable
23
+ from typing import IO
24
+ from typing import Iterable
25
+ from typing import Iterator
26
+ from typing import Sequence
27
+ from typing import TYPE_CHECKING
28
+
29
+ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
30
+ from _pytest._io.saferepr import saferepr
31
+ from _pytest._version import version
32
+ from _pytest.assertion import util
33
+ from _pytest.config import Config
34
+ from _pytest.main import Session
35
+ from _pytest.pathlib import absolutepath
36
+ from _pytest.pathlib import fnmatch_ex
37
+ from _pytest.stash import StashKey
38
+
39
+
40
+ # fmt: off
41
+ from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
42
+ # fmt:on
43
+
44
+ if TYPE_CHECKING:
45
+ from _pytest.assertion import AssertionState
46
+
47
+
48
+ class Sentinel:
49
+ pass
50
+
51
+
52
+ assertstate_key = StashKey["AssertionState"]()
53
+
54
+ # pytest caches rewritten pycs in pycache dirs
55
+ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
56
+ PYC_EXT = ".py" + (__debug__ and "c" or "o")
57
+ PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
58
+
59
+ # Special marker that denotes we have just left a scope definition
60
+ _SCOPE_END_MARKER = Sentinel()
61
+
62
+
63
+ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
64
+ """PEP302/PEP451 import hook which rewrites asserts."""
65
+
66
+ def __init__(self, config: Config) -> None:
67
+ self.config = config
68
+ try:
69
+ self.fnpats = config.getini("python_files")
70
+ except ValueError:
71
+ self.fnpats = ["test_*.py", "*_test.py"]
72
+ self.session: Session | None = None
73
+ self._rewritten_names: dict[str, Path] = {}
74
+ self._must_rewrite: set[str] = set()
75
+ # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
76
+ # which might result in infinite recursion (#3506)
77
+ self._writing_pyc = False
78
+ self._basenames_to_check_rewrite = {"conftest"}
79
+ self._marked_for_rewrite_cache: dict[str, bool] = {}
80
+ self._session_paths_checked = False
81
+
82
+ def set_session(self, session: Session | None) -> None:
83
+ self.session = session
84
+ self._session_paths_checked = False
85
+
86
+ # Indirection so we can mock calls to find_spec originated from the hook during testing
87
+ _find_spec = importlib.machinery.PathFinder.find_spec
88
+
89
+ def find_spec(
90
+ self,
91
+ name: str,
92
+ path: Sequence[str | bytes] | None = None,
93
+ target: types.ModuleType | None = None,
94
+ ) -> importlib.machinery.ModuleSpec | None:
95
+ if self._writing_pyc:
96
+ return None
97
+ state = self.config.stash[assertstate_key]
98
+ if self._early_rewrite_bailout(name, state):
99
+ return None
100
+ state.trace(f"find_module called for: {name}")
101
+
102
+ # Type ignored because mypy is confused about the `self` binding here.
103
+ spec = self._find_spec(name, path) # type: ignore
104
+
105
+ if spec is None and path is not None:
106
+ # With --import-mode=importlib, PathFinder cannot find spec without modifying `sys.path`,
107
+ # causing inability to assert rewriting (#12659).
108
+ # At this point, try using the file path to find the module spec.
109
+ for _path_str in path:
110
+ spec = importlib.util.spec_from_file_location(name, _path_str)
111
+ if spec is not None:
112
+ break
113
+
114
+ if (
115
+ # the import machinery could not find a file to import
116
+ spec is None
117
+ # this is a namespace package (without `__init__.py`)
118
+ # there's nothing to rewrite there
119
+ or spec.origin is None
120
+ # we can only rewrite source files
121
+ or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
122
+ # if the file doesn't exist, we can't rewrite it
123
+ or not os.path.exists(spec.origin)
124
+ ):
125
+ return None
126
+ else:
127
+ fn = spec.origin
128
+
129
+ if not self._should_rewrite(name, fn, state):
130
+ return None
131
+
132
+ return importlib.util.spec_from_file_location(
133
+ name,
134
+ fn,
135
+ loader=self,
136
+ submodule_search_locations=spec.submodule_search_locations,
137
+ )
138
+
139
+ def create_module(
140
+ self, spec: importlib.machinery.ModuleSpec
141
+ ) -> types.ModuleType | None:
142
+ return None # default behaviour is fine
143
+
144
+ def exec_module(self, module: types.ModuleType) -> None:
145
+ assert module.__spec__ is not None
146
+ assert module.__spec__.origin is not None
147
+ fn = Path(module.__spec__.origin)
148
+ state = self.config.stash[assertstate_key]
149
+
150
+ self._rewritten_names[module.__name__] = fn
151
+
152
+ # The requested module looks like a test file, so rewrite it. This is
153
+ # the most magical part of the process: load the source, rewrite the
154
+ # asserts, and load the rewritten source. We also cache the rewritten
155
+ # module code in a special pyc. We must be aware of the possibility of
156
+ # concurrent pytest processes rewriting and loading pycs. To avoid
157
+ # tricky race conditions, we maintain the following invariant: The
158
+ # cached pyc is always a complete, valid pyc. Operations on it must be
159
+ # atomic. POSIX's atomic rename comes in handy.
160
+ write = not sys.dont_write_bytecode
161
+ cache_dir = get_cache_dir(fn)
162
+ if write:
163
+ ok = try_makedirs(cache_dir)
164
+ if not ok:
165
+ write = False
166
+ state.trace(f"read only directory: {cache_dir}")
167
+
168
+ cache_name = fn.name[:-3] + PYC_TAIL
169
+ pyc = cache_dir / cache_name
170
+ # Notice that even if we're in a read-only directory, I'm going
171
+ # to check for a cached pyc. This may not be optimal...
172
+ co = _read_pyc(fn, pyc, state.trace)
173
+ if co is None:
174
+ state.trace(f"rewriting {fn!r}")
175
+ source_stat, co = _rewrite_test(fn, self.config)
176
+ if write:
177
+ self._writing_pyc = True
178
+ try:
179
+ _write_pyc(state, co, source_stat, pyc)
180
+ finally:
181
+ self._writing_pyc = False
182
+ else:
183
+ state.trace(f"found cached rewritten pyc for {fn}")
184
+ exec(co, module.__dict__)
185
+
186
+ def _early_rewrite_bailout(self, name: str, state: AssertionState) -> bool:
187
+ """A fast way to get out of rewriting modules.
188
+
189
+ Profiling has shown that the call to PathFinder.find_spec (inside of
190
+ the find_spec from this class) is a major slowdown, so, this method
191
+ tries to filter what we're sure won't be rewritten before getting to
192
+ it.
193
+ """
194
+ if self.session is not None and not self._session_paths_checked:
195
+ self._session_paths_checked = True
196
+ for initial_path in self.session._initialpaths:
197
+ # Make something as c:/projects/my_project/path.py ->
198
+ # ['c:', 'projects', 'my_project', 'path.py']
199
+ parts = str(initial_path).split(os.sep)
200
+ # add 'path' to basenames to be checked.
201
+ self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
202
+
203
+ # Note: conftest already by default in _basenames_to_check_rewrite.
204
+ parts = name.split(".")
205
+ if parts[-1] in self._basenames_to_check_rewrite:
206
+ return False
207
+
208
+ # For matching the name it must be as if it was a filename.
209
+ path = PurePath(*parts).with_suffix(".py")
210
+
211
+ for pat in self.fnpats:
212
+ # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
213
+ # on the name alone because we need to match against the full path
214
+ if os.path.dirname(pat):
215
+ return False
216
+ if fnmatch_ex(pat, path):
217
+ return False
218
+
219
+ if self._is_marked_for_rewrite(name, state):
220
+ return False
221
+
222
+ state.trace(f"early skip of rewriting module: {name}")
223
+ return True
224
+
225
+ def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool:
226
+ # always rewrite conftest files
227
+ if os.path.basename(fn) == "conftest.py":
228
+ state.trace(f"rewriting conftest file: {fn!r}")
229
+ return True
230
+
231
+ if self.session is not None:
232
+ if self.session.isinitpath(absolutepath(fn)):
233
+ state.trace(f"matched test file (was specified on cmdline): {fn!r}")
234
+ return True
235
+
236
+ # modules not passed explicitly on the command line are only
237
+ # rewritten if they match the naming convention for test files
238
+ fn_path = PurePath(fn)
239
+ for pat in self.fnpats:
240
+ if fnmatch_ex(pat, fn_path):
241
+ state.trace(f"matched test file {fn!r}")
242
+ return True
243
+
244
+ return self._is_marked_for_rewrite(name, state)
245
+
246
+ def _is_marked_for_rewrite(self, name: str, state: AssertionState) -> bool:
247
+ try:
248
+ return self._marked_for_rewrite_cache[name]
249
+ except KeyError:
250
+ for marked in self._must_rewrite:
251
+ if name == marked or name.startswith(marked + "."):
252
+ state.trace(f"matched marked file {name!r} (from {marked!r})")
253
+ self._marked_for_rewrite_cache[name] = True
254
+ return True
255
+
256
+ self._marked_for_rewrite_cache[name] = False
257
+ return False
258
+
259
+ def mark_rewrite(self, *names: str) -> None:
260
+ """Mark import names as needing to be rewritten.
261
+
262
+ The named module or package as well as any nested modules will
263
+ be rewritten on import.
264
+ """
265
+ already_imported = (
266
+ set(names).intersection(sys.modules).difference(self._rewritten_names)
267
+ )
268
+ for name in already_imported:
269
+ mod = sys.modules[name]
270
+ if not AssertionRewriter.is_rewrite_disabled(
271
+ mod.__doc__ or ""
272
+ ) and not isinstance(mod.__loader__, type(self)):
273
+ self._warn_already_imported(name)
274
+ self._must_rewrite.update(names)
275
+ self._marked_for_rewrite_cache.clear()
276
+
277
+ def _warn_already_imported(self, name: str) -> None:
278
+ from _pytest.warning_types import PytestAssertRewriteWarning
279
+
280
+ self.config.issue_config_time_warning(
281
+ PytestAssertRewriteWarning(
282
+ f"Module already imported so cannot be rewritten: {name}"
283
+ ),
284
+ stacklevel=5,
285
+ )
286
+
287
+ def get_data(self, pathname: str | bytes) -> bytes:
288
+ """Optional PEP302 get_data API."""
289
+ with open(pathname, "rb") as f:
290
+ return f.read()
291
+
292
+ if sys.version_info >= (3, 10):
293
+ if sys.version_info >= (3, 12):
294
+ from importlib.resources.abc import TraversableResources
295
+ else:
296
+ from importlib.abc import TraversableResources
297
+
298
+ def get_resource_reader(self, name: str) -> TraversableResources:
299
+ if sys.version_info < (3, 11):
300
+ from importlib.readers import FileReader
301
+ else:
302
+ from importlib.resources.readers import FileReader
303
+
304
+ return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
305
+
306
+
307
+ def _write_pyc_fp(
308
+ fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType
309
+ ) -> None:
310
+ # Technically, we don't have to have the same pyc format as
311
+ # (C)Python, since these "pycs" should never be seen by builtin
312
+ # import. However, there's little reason to deviate.
313
+ fp.write(importlib.util.MAGIC_NUMBER)
314
+ # https://www.python.org/dev/peps/pep-0552/
315
+ flags = b"\x00\x00\x00\x00"
316
+ fp.write(flags)
317
+ # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
318
+ mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
319
+ size = source_stat.st_size & 0xFFFFFFFF
320
+ # "<LL" stands for 2 unsigned longs, little-endian.
321
+ fp.write(struct.pack("<LL", mtime, size))
322
+ fp.write(marshal.dumps(co))
323
+
324
+
325
+ def _write_pyc(
326
+ state: AssertionState,
327
+ co: types.CodeType,
328
+ source_stat: os.stat_result,
329
+ pyc: Path,
330
+ ) -> bool:
331
+ proc_pyc = f"{pyc}.{os.getpid()}"
332
+ try:
333
+ with open(proc_pyc, "wb") as fp:
334
+ _write_pyc_fp(fp, source_stat, co)
335
+ except OSError as e:
336
+ state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
337
+ return False
338
+
339
+ try:
340
+ os.replace(proc_pyc, pyc)
341
+ except OSError as e:
342
+ state.trace(f"error writing pyc file at {pyc}: {e}")
343
+ # we ignore any failure to write the cache file
344
+ # there are many reasons, permission-denied, pycache dir being a
345
+ # file etc.
346
+ return False
347
+ return True
348
+
349
+
350
+ def _rewrite_test(fn: Path, config: Config) -> tuple[os.stat_result, types.CodeType]:
351
+ """Read and rewrite *fn* and return the code object."""
352
+ stat = os.stat(fn)
353
+ source = fn.read_bytes()
354
+ strfn = str(fn)
355
+ tree = ast.parse(source, filename=strfn)
356
+ rewrite_asserts(tree, source, strfn, config)
357
+ co = compile(tree, strfn, "exec", dont_inherit=True)
358
+ return stat, co
359
+
360
+
361
+ def _read_pyc(
362
+ source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None
363
+ ) -> types.CodeType | None:
364
+ """Possibly read a pytest pyc containing rewritten code.
365
+
366
+ Return rewritten code if successful or None if not.
367
+ """
368
+ try:
369
+ fp = open(pyc, "rb")
370
+ except OSError:
371
+ return None
372
+ with fp:
373
+ try:
374
+ stat_result = os.stat(source)
375
+ mtime = int(stat_result.st_mtime)
376
+ size = stat_result.st_size
377
+ data = fp.read(16)
378
+ except OSError as e:
379
+ trace(f"_read_pyc({source}): OSError {e}")
380
+ return None
381
+ # Check for invalid or out of date pyc file.
382
+ if len(data) != (16):
383
+ trace(f"_read_pyc({source}): invalid pyc (too short)")
384
+ return None
385
+ if data[:4] != importlib.util.MAGIC_NUMBER:
386
+ trace(f"_read_pyc({source}): invalid pyc (bad magic number)")
387
+ return None
388
+ if data[4:8] != b"\x00\x00\x00\x00":
389
+ trace(f"_read_pyc({source}): invalid pyc (unsupported flags)")
390
+ return None
391
+ mtime_data = data[8:12]
392
+ if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
393
+ trace(f"_read_pyc({source}): out of date")
394
+ return None
395
+ size_data = data[12:16]
396
+ if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
397
+ trace(f"_read_pyc({source}): invalid pyc (incorrect size)")
398
+ return None
399
+ try:
400
+ co = marshal.load(fp)
401
+ except Exception as e:
402
+ trace(f"_read_pyc({source}): marshal.load error {e}")
403
+ return None
404
+ if not isinstance(co, types.CodeType):
405
+ trace(f"_read_pyc({source}): not a code object")
406
+ return None
407
+ return co
408
+
409
+
410
+ def rewrite_asserts(
411
+ mod: ast.Module,
412
+ source: bytes,
413
+ module_path: str | None = None,
414
+ config: Config | None = None,
415
+ ) -> None:
416
+ """Rewrite the assert statements in mod."""
417
+ AssertionRewriter(module_path, config, source).run(mod)
418
+
419
+
420
+ def _saferepr(obj: object) -> str:
421
+ r"""Get a safe repr of an object for assertion error messages.
422
+
423
+ The assertion formatting (util.format_explanation()) requires
424
+ newlines to be escaped since they are a special character for it.
425
+ Normally assertion.util.format_explanation() does this but for a
426
+ custom repr it is possible to contain one of the special escape
427
+ sequences, especially '\n{' and '\n}' are likely to be present in
428
+ JSON reprs.
429
+ """
430
+ if isinstance(obj, types.MethodType):
431
+ # for bound methods, skip redundant <bound method ...> information
432
+ return obj.__name__
433
+
434
+ maxsize = _get_maxsize_for_saferepr(util._config)
435
+ return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
436
+
437
+
438
+ def _get_maxsize_for_saferepr(config: Config | None) -> int | None:
439
+ """Get `maxsize` configuration for saferepr based on the given config object."""
440
+ if config is None:
441
+ verbosity = 0
442
+ else:
443
+ verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
444
+ if verbosity >= 2:
445
+ return None
446
+ if verbosity >= 1:
447
+ return DEFAULT_REPR_MAX_SIZE * 10
448
+ return DEFAULT_REPR_MAX_SIZE
449
+
450
+
451
+ def _format_assertmsg(obj: object) -> str:
452
+ r"""Format the custom assertion message given.
453
+
454
+ For strings this simply replaces newlines with '\n~' so that
455
+ util.format_explanation() will preserve them instead of escaping
456
+ newlines. For other objects saferepr() is used first.
457
+ """
458
+ # reprlib appears to have a bug which means that if a string
459
+ # contains a newline it gets escaped, however if an object has a
460
+ # .__repr__() which contains newlines it does not get escaped.
461
+ # However in either case we want to preserve the newline.
462
+ replaces = [("\n", "\n~"), ("%", "%%")]
463
+ if not isinstance(obj, str):
464
+ obj = saferepr(obj, _get_maxsize_for_saferepr(util._config))
465
+ replaces.append(("\\n", "\n~"))
466
+
467
+ for r1, r2 in replaces:
468
+ obj = obj.replace(r1, r2)
469
+
470
+ return obj
471
+
472
+
473
+ def _should_repr_global_name(obj: object) -> bool:
474
+ if callable(obj):
475
+ return False
476
+
477
+ try:
478
+ return not hasattr(obj, "__name__")
479
+ except Exception:
480
+ return True
481
+
482
+
483
+ def _format_boolop(explanations: Iterable[str], is_or: bool) -> str:
484
+ explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
485
+ return explanation.replace("%", "%%")
486
+
487
+
488
+ def _call_reprcompare(
489
+ ops: Sequence[str],
490
+ results: Sequence[bool],
491
+ expls: Sequence[str],
492
+ each_obj: Sequence[object],
493
+ ) -> str:
494
+ for i, res, expl in zip(range(len(ops)), results, expls):
495
+ try:
496
+ done = not res
497
+ except Exception:
498
+ done = True
499
+ if done:
500
+ break
501
+ if util._reprcompare is not None:
502
+ custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
503
+ if custom is not None:
504
+ return custom
505
+ return expl
506
+
507
+
508
+ def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
509
+ if util._assertion_pass is not None:
510
+ util._assertion_pass(lineno, orig, expl)
511
+
512
+
513
+ def _check_if_assertion_pass_impl() -> bool:
514
+ """Check if any plugins implement the pytest_assertion_pass hook
515
+ in order not to generate explanation unnecessarily (might be expensive)."""
516
+ return True if util._assertion_pass else False
517
+
518
+
519
+ UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
520
+
521
+ BINOP_MAP = {
522
+ ast.BitOr: "|",
523
+ ast.BitXor: "^",
524
+ ast.BitAnd: "&",
525
+ ast.LShift: "<<",
526
+ ast.RShift: ">>",
527
+ ast.Add: "+",
528
+ ast.Sub: "-",
529
+ ast.Mult: "*",
530
+ ast.Div: "/",
531
+ ast.FloorDiv: "//",
532
+ ast.Mod: "%%", # escaped for string formatting
533
+ ast.Eq: "==",
534
+ ast.NotEq: "!=",
535
+ ast.Lt: "<",
536
+ ast.LtE: "<=",
537
+ ast.Gt: ">",
538
+ ast.GtE: ">=",
539
+ ast.Pow: "**",
540
+ ast.Is: "is",
541
+ ast.IsNot: "is not",
542
+ ast.In: "in",
543
+ ast.NotIn: "not in",
544
+ ast.MatMult: "@",
545
+ }
546
+
547
+
548
+ def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
549
+ """Recursively yield node and all its children in depth-first order."""
550
+ yield node
551
+ for child in ast.iter_child_nodes(node):
552
+ yield from traverse_node(child)
553
+
554
+
555
+ @functools.lru_cache(maxsize=1)
556
+ def _get_assertion_exprs(src: bytes) -> dict[int, str]:
557
+ """Return a mapping from {lineno: "assertion test expression"}."""
558
+ ret: dict[int, str] = {}
559
+
560
+ depth = 0
561
+ lines: list[str] = []
562
+ assert_lineno: int | None = None
563
+ seen_lines: set[int] = set()
564
+
565
+ def _write_and_reset() -> None:
566
+ nonlocal depth, lines, assert_lineno, seen_lines
567
+ assert assert_lineno is not None
568
+ ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\")
569
+ depth = 0
570
+ lines = []
571
+ assert_lineno = None
572
+ seen_lines = set()
573
+
574
+ tokens = tokenize.tokenize(io.BytesIO(src).readline)
575
+ for tp, source, (lineno, offset), _, line in tokens:
576
+ if tp == tokenize.NAME and source == "assert":
577
+ assert_lineno = lineno
578
+ elif assert_lineno is not None:
579
+ # keep track of depth for the assert-message `,` lookup
580
+ if tp == tokenize.OP and source in "([{":
581
+ depth += 1
582
+ elif tp == tokenize.OP and source in ")]}":
583
+ depth -= 1
584
+
585
+ if not lines:
586
+ lines.append(line[offset:])
587
+ seen_lines.add(lineno)
588
+ # a non-nested comma separates the expression from the message
589
+ elif depth == 0 and tp == tokenize.OP and source == ",":
590
+ # one line assert with message
591
+ if lineno in seen_lines and len(lines) == 1:
592
+ offset_in_trimmed = offset + len(lines[-1]) - len(line)
593
+ lines[-1] = lines[-1][:offset_in_trimmed]
594
+ # multi-line assert with message
595
+ elif lineno in seen_lines:
596
+ lines[-1] = lines[-1][:offset]
597
+ # multi line assert with escaped newline before message
598
+ else:
599
+ lines.append(line[:offset])
600
+ _write_and_reset()
601
+ elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}:
602
+ _write_and_reset()
603
+ elif lines and lineno not in seen_lines:
604
+ lines.append(line)
605
+ seen_lines.add(lineno)
606
+
607
+ return ret
608
+
609
+
610
+ class AssertionRewriter(ast.NodeVisitor):
611
+ """Assertion rewriting implementation.
612
+
613
+ The main entrypoint is to call .run() with an ast.Module instance,
614
+ this will then find all the assert statements and rewrite them to
615
+ provide intermediate values and a detailed assertion error. See
616
+ http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
617
+ for an overview of how this works.
618
+
619
+ The entry point here is .run() which will iterate over all the
620
+ statements in an ast.Module and for each ast.Assert statement it
621
+ finds call .visit() with it. Then .visit_Assert() takes over and
622
+ is responsible for creating new ast statements to replace the
623
+ original assert statement: it rewrites the test of an assertion
624
+ to provide intermediate values and replace it with an if statement
625
+ which raises an assertion error with a detailed explanation in
626
+ case the expression is false and calls pytest_assertion_pass hook
627
+ if expression is true.
628
+
629
+ For this .visit_Assert() uses the visitor pattern to visit all the
630
+ AST nodes of the ast.Assert.test field, each visit call returning
631
+ an AST node and the corresponding explanation string. During this
632
+ state is kept in several instance attributes:
633
+
634
+ :statements: All the AST statements which will replace the assert
635
+ statement.
636
+
637
+ :variables: This is populated by .variable() with each variable
638
+ used by the statements so that they can all be set to None at
639
+ the end of the statements.
640
+
641
+ :variable_counter: Counter to create new unique variables needed
642
+ by statements. Variables are created using .variable() and
643
+ have the form of "@py_assert0".
644
+
645
+ :expl_stmts: The AST statements which will be executed to get
646
+ data from the assertion. This is the code which will construct
647
+ the detailed assertion message that is used in the AssertionError
648
+ or for the pytest_assertion_pass hook.
649
+
650
+ :explanation_specifiers: A dict filled by .explanation_param()
651
+ with %-formatting placeholders and their corresponding
652
+ expressions to use in the building of an assertion message.
653
+ This is used by .pop_format_context() to build a message.
654
+
655
+ :stack: A stack of the explanation_specifiers dicts maintained by
656
+ .push_format_context() and .pop_format_context() which allows
657
+ to build another %-formatted string while already building one.
658
+
659
+ :scope: A tuple containing the current scope used for variables_overwrite.
660
+
661
+ :variables_overwrite: A dict filled with references to variables
662
+ that change value within an assert. This happens when a variable is
663
+ reassigned with the walrus operator
664
+
665
+ This state, except the variables_overwrite, is reset on every new assert
666
+ statement visited and used by the other visitors.
667
+ """
668
+
669
+ def __init__(
670
+ self, module_path: str | None, config: Config | None, source: bytes
671
+ ) -> None:
672
+ super().__init__()
673
+ self.module_path = module_path
674
+ self.config = config
675
+ if config is not None:
676
+ self.enable_assertion_pass_hook = config.getini(
677
+ "enable_assertion_pass_hook"
678
+ )
679
+ else:
680
+ self.enable_assertion_pass_hook = False
681
+ self.source = source
682
+ self.scope: tuple[ast.AST, ...] = ()
683
+ self.variables_overwrite: defaultdict[tuple[ast.AST, ...], dict[str, str]] = (
684
+ defaultdict(dict)
685
+ )
686
+
687
+ def run(self, mod: ast.Module) -> None:
688
+ """Find all assert statements in *mod* and rewrite them."""
689
+ if not mod.body:
690
+ # Nothing to do.
691
+ return
692
+
693
+ # We'll insert some special imports at the top of the module, but after any
694
+ # docstrings and __future__ imports, so first figure out where that is.
695
+ doc = getattr(mod, "docstring", None)
696
+ expect_docstring = doc is None
697
+ if doc is not None and self.is_rewrite_disabled(doc):
698
+ return
699
+ pos = 0
700
+ item = None
701
+ for item in mod.body:
702
+ if (
703
+ expect_docstring
704
+ and isinstance(item, ast.Expr)
705
+ and isinstance(item.value, ast.Constant)
706
+ and isinstance(item.value.value, str)
707
+ ):
708
+ doc = item.value.value
709
+ if self.is_rewrite_disabled(doc):
710
+ return
711
+ expect_docstring = False
712
+ elif (
713
+ isinstance(item, ast.ImportFrom)
714
+ and item.level == 0
715
+ and item.module == "__future__"
716
+ ):
717
+ pass
718
+ else:
719
+ break
720
+ pos += 1
721
+ # Special case: for a decorated function, set the lineno to that of the
722
+ # first decorator, not the `def`. Issue #4984.
723
+ if isinstance(item, ast.FunctionDef) and item.decorator_list:
724
+ lineno = item.decorator_list[0].lineno
725
+ else:
726
+ lineno = item.lineno
727
+ # Now actually insert the special imports.
728
+ if sys.version_info >= (3, 10):
729
+ aliases = [
730
+ ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0),
731
+ ast.alias(
732
+ "_pytest.assertion.rewrite",
733
+ "@pytest_ar",
734
+ lineno=lineno,
735
+ col_offset=0,
736
+ ),
737
+ ]
738
+ else:
739
+ aliases = [
740
+ ast.alias("builtins", "@py_builtins"),
741
+ ast.alias("_pytest.assertion.rewrite", "@pytest_ar"),
742
+ ]
743
+ imports = [
744
+ ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases
745
+ ]
746
+ mod.body[pos:pos] = imports
747
+
748
+ # Collect asserts.
749
+ self.scope = (mod,)
750
+ nodes: list[ast.AST | Sentinel] = [mod]
751
+ while nodes:
752
+ node = nodes.pop()
753
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
754
+ self.scope = tuple((*self.scope, node))
755
+ nodes.append(_SCOPE_END_MARKER)
756
+ if node == _SCOPE_END_MARKER:
757
+ self.scope = self.scope[:-1]
758
+ continue
759
+ assert isinstance(node, ast.AST)
760
+ for name, field in ast.iter_fields(node):
761
+ if isinstance(field, list):
762
+ new: list[ast.AST] = []
763
+ for i, child in enumerate(field):
764
+ if isinstance(child, ast.Assert):
765
+ # Transform assert.
766
+ new.extend(self.visit(child))
767
+ else:
768
+ new.append(child)
769
+ if isinstance(child, ast.AST):
770
+ nodes.append(child)
771
+ setattr(node, name, new)
772
+ elif (
773
+ isinstance(field, ast.AST)
774
+ # Don't recurse into expressions as they can't contain
775
+ # asserts.
776
+ and not isinstance(field, ast.expr)
777
+ ):
778
+ nodes.append(field)
779
+
780
+ @staticmethod
781
+ def is_rewrite_disabled(docstring: str) -> bool:
782
+ return "PYTEST_DONT_REWRITE" in docstring
783
+
784
+ def variable(self) -> str:
785
+ """Get a new variable."""
786
+ # Use a character invalid in python identifiers to avoid clashing.
787
+ name = "@py_assert" + str(next(self.variable_counter))
788
+ self.variables.append(name)
789
+ return name
790
+
791
+ def assign(self, expr: ast.expr) -> ast.Name:
792
+ """Give *expr* a name."""
793
+ name = self.variable()
794
+ self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
795
+ return ast.copy_location(ast.Name(name, ast.Load()), expr)
796
+
797
+ def display(self, expr: ast.expr) -> ast.expr:
798
+ """Call saferepr on the expression."""
799
+ return self.helper("_saferepr", expr)
800
+
801
+ def helper(self, name: str, *args: ast.expr) -> ast.expr:
802
+ """Call a helper in this module."""
803
+ py_name = ast.Name("@pytest_ar", ast.Load())
804
+ attr = ast.Attribute(py_name, name, ast.Load())
805
+ return ast.Call(attr, list(args), [])
806
+
807
+ def builtin(self, name: str) -> ast.Attribute:
808
+ """Return the builtin called *name*."""
809
+ builtin_name = ast.Name("@py_builtins", ast.Load())
810
+ return ast.Attribute(builtin_name, name, ast.Load())
811
+
812
+ def explanation_param(self, expr: ast.expr) -> str:
813
+ """Return a new named %-formatting placeholder for expr.
814
+
815
+ This creates a %-formatting placeholder for expr in the
816
+ current formatting context, e.g. ``%(py0)s``. The placeholder
817
+ and expr are placed in the current format context so that it
818
+ can be used on the next call to .pop_format_context().
819
+ """
820
+ specifier = "py" + str(next(self.variable_counter))
821
+ self.explanation_specifiers[specifier] = expr
822
+ return "%(" + specifier + ")s"
823
+
824
+ def push_format_context(self) -> None:
825
+ """Create a new formatting context.
826
+
827
+ The format context is used for when an explanation wants to
828
+ have a variable value formatted in the assertion message. In
829
+ this case the value required can be added using
830
+ .explanation_param(). Finally .pop_format_context() is used
831
+ to format a string of %-formatted values as added by
832
+ .explanation_param().
833
+ """
834
+ self.explanation_specifiers: dict[str, ast.expr] = {}
835
+ self.stack.append(self.explanation_specifiers)
836
+
837
+ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
838
+ """Format the %-formatted string with current format context.
839
+
840
+ The expl_expr should be an str ast.expr instance constructed from
841
+ the %-placeholders created by .explanation_param(). This will
842
+ add the required code to format said string to .expl_stmts and
843
+ return the ast.Name instance of the formatted string.
844
+ """
845
+ current = self.stack.pop()
846
+ if self.stack:
847
+ self.explanation_specifiers = self.stack[-1]
848
+ keys: list[ast.expr | None] = [ast.Constant(key) for key in current.keys()]
849
+ format_dict = ast.Dict(keys, list(current.values()))
850
+ form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
851
+ name = "@py_format" + str(next(self.variable_counter))
852
+ if self.enable_assertion_pass_hook:
853
+ self.format_variables.append(name)
854
+ self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form))
855
+ return ast.Name(name, ast.Load())
856
+
857
+ def generic_visit(self, node: ast.AST) -> tuple[ast.Name, str]:
858
+ """Handle expressions we don't have custom code for."""
859
+ assert isinstance(node, ast.expr)
860
+ res = self.assign(node)
861
+ return res, self.explanation_param(self.display(res))
862
+
863
+ def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]:
864
+ """Return the AST statements to replace the ast.Assert instance.
865
+
866
+ This rewrites the test of an assertion to provide
867
+ intermediate values and replace it with an if statement which
868
+ raises an assertion error with a detailed explanation in case
869
+ the expression is false.
870
+ """
871
+ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
872
+ import warnings
873
+
874
+ from _pytest.warning_types import PytestAssertRewriteWarning
875
+
876
+ # TODO: This assert should not be needed.
877
+ assert self.module_path is not None
878
+ warnings.warn_explicit(
879
+ PytestAssertRewriteWarning(
880
+ "assertion is always true, perhaps remove parentheses?"
881
+ ),
882
+ category=None,
883
+ filename=self.module_path,
884
+ lineno=assert_.lineno,
885
+ )
886
+
887
+ self.statements: list[ast.stmt] = []
888
+ self.variables: list[str] = []
889
+ self.variable_counter = itertools.count()
890
+
891
+ if self.enable_assertion_pass_hook:
892
+ self.format_variables: list[str] = []
893
+
894
+ self.stack: list[dict[str, ast.expr]] = []
895
+ self.expl_stmts: list[ast.stmt] = []
896
+ self.push_format_context()
897
+ # Rewrite assert into a bunch of statements.
898
+ top_condition, explanation = self.visit(assert_.test)
899
+
900
+ negation = ast.UnaryOp(ast.Not(), top_condition)
901
+
902
+ if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
903
+ msg = self.pop_format_context(ast.Constant(explanation))
904
+
905
+ # Failed
906
+ if assert_.msg:
907
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
908
+ gluestr = "\n>assert "
909
+ else:
910
+ assertmsg = ast.Constant("")
911
+ gluestr = "assert "
912
+ err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
913
+ err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
914
+ err_name = ast.Name("AssertionError", ast.Load())
915
+ fmt = self.helper("_format_explanation", err_msg)
916
+ exc = ast.Call(err_name, [fmt], [])
917
+ raise_ = ast.Raise(exc, None)
918
+ statements_fail = []
919
+ statements_fail.extend(self.expl_stmts)
920
+ statements_fail.append(raise_)
921
+
922
+ # Passed
923
+ fmt_pass = self.helper("_format_explanation", msg)
924
+ orig = _get_assertion_exprs(self.source)[assert_.lineno]
925
+ hook_call_pass = ast.Expr(
926
+ self.helper(
927
+ "_call_assertion_pass",
928
+ ast.Constant(assert_.lineno),
929
+ ast.Constant(orig),
930
+ fmt_pass,
931
+ )
932
+ )
933
+ # If any hooks implement assert_pass hook
934
+ hook_impl_test = ast.If(
935
+ self.helper("_check_if_assertion_pass_impl"),
936
+ [*self.expl_stmts, hook_call_pass],
937
+ [],
938
+ )
939
+ statements_pass: list[ast.stmt] = [hook_impl_test]
940
+
941
+ # Test for assertion condition
942
+ main_test = ast.If(negation, statements_fail, statements_pass)
943
+ self.statements.append(main_test)
944
+ if self.format_variables:
945
+ variables: list[ast.expr] = [
946
+ ast.Name(name, ast.Store()) for name in self.format_variables
947
+ ]
948
+ clear_format = ast.Assign(variables, ast.Constant(None))
949
+ self.statements.append(clear_format)
950
+
951
+ else: # Original assertion rewriting
952
+ # Create failure message.
953
+ body = self.expl_stmts
954
+ self.statements.append(ast.If(negation, body, []))
955
+ if assert_.msg:
956
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
957
+ explanation = "\n>assert " + explanation
958
+ else:
959
+ assertmsg = ast.Constant("")
960
+ explanation = "assert " + explanation
961
+ template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
962
+ msg = self.pop_format_context(template)
963
+ fmt = self.helper("_format_explanation", msg)
964
+ err_name = ast.Name("AssertionError", ast.Load())
965
+ exc = ast.Call(err_name, [fmt], [])
966
+ raise_ = ast.Raise(exc, None)
967
+
968
+ body.append(raise_)
969
+
970
+ # Clear temporary variables by setting them to None.
971
+ if self.variables:
972
+ variables = [ast.Name(name, ast.Store()) for name in self.variables]
973
+ clear = ast.Assign(variables, ast.Constant(None))
974
+ self.statements.append(clear)
975
+ # Fix locations (line numbers/column offsets).
976
+ for stmt in self.statements:
977
+ for node in traverse_node(stmt):
978
+ if getattr(node, "lineno", None) is None:
979
+ # apply the assertion location to all generated ast nodes without source location
980
+ # and preserve the location of existing nodes or generated nodes with an correct location.
981
+ ast.copy_location(node, assert_)
982
+ return self.statements
983
+
984
+ def visit_NamedExpr(self, name: ast.NamedExpr) -> tuple[ast.NamedExpr, str]:
985
+ # This method handles the 'walrus operator' repr of the target
986
+ # name if it's a local variable or _should_repr_global_name()
987
+ # thinks it's acceptable.
988
+ locs = ast.Call(self.builtin("locals"), [], [])
989
+ target_id = name.target.id
990
+ inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
991
+ dorepr = self.helper("_should_repr_global_name", name)
992
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
993
+ expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
994
+ return name, self.explanation_param(expr)
995
+
996
+ def visit_Name(self, name: ast.Name) -> tuple[ast.Name, str]:
997
+ # Display the repr of the name if it's a local variable or
998
+ # _should_repr_global_name() thinks it's acceptable.
999
+ locs = ast.Call(self.builtin("locals"), [], [])
1000
+ inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs])
1001
+ dorepr = self.helper("_should_repr_global_name", name)
1002
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
1003
+ expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
1004
+ return name, self.explanation_param(expr)
1005
+
1006
+ def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]:
1007
+ res_var = self.variable()
1008
+ expl_list = self.assign(ast.List([], ast.Load()))
1009
+ app = ast.Attribute(expl_list, "append", ast.Load())
1010
+ is_or = int(isinstance(boolop.op, ast.Or))
1011
+ body = save = self.statements
1012
+ fail_save = self.expl_stmts
1013
+ levels = len(boolop.values) - 1
1014
+ self.push_format_context()
1015
+ # Process each operand, short-circuiting if needed.
1016
+ for i, v in enumerate(boolop.values):
1017
+ if i:
1018
+ fail_inner: list[ast.stmt] = []
1019
+ # cond is set in a prior loop iteration below
1020
+ self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
1021
+ self.expl_stmts = fail_inner
1022
+ # Check if the left operand is a ast.NamedExpr and the value has already been visited
1023
+ if (
1024
+ isinstance(v, ast.Compare)
1025
+ and isinstance(v.left, ast.NamedExpr)
1026
+ and v.left.target.id
1027
+ in [
1028
+ ast_expr.id
1029
+ for ast_expr in boolop.values[:i]
1030
+ if hasattr(ast_expr, "id")
1031
+ ]
1032
+ ):
1033
+ pytest_temp = self.variable()
1034
+ self.variables_overwrite[self.scope][v.left.target.id] = v.left # type:ignore[assignment]
1035
+ v.left.target.id = pytest_temp
1036
+ self.push_format_context()
1037
+ res, expl = self.visit(v)
1038
+ body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
1039
+ expl_format = self.pop_format_context(ast.Constant(expl))
1040
+ call = ast.Call(app, [expl_format], [])
1041
+ self.expl_stmts.append(ast.Expr(call))
1042
+ if i < levels:
1043
+ cond: ast.expr = res
1044
+ if is_or:
1045
+ cond = ast.UnaryOp(ast.Not(), cond)
1046
+ inner: list[ast.stmt] = []
1047
+ self.statements.append(ast.If(cond, inner, []))
1048
+ self.statements = body = inner
1049
+ self.statements = save
1050
+ self.expl_stmts = fail_save
1051
+ expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
1052
+ expl = self.pop_format_context(expl_template)
1053
+ return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
1054
+
1055
+ def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]:
1056
+ pattern = UNARY_MAP[unary.op.__class__]
1057
+ operand_res, operand_expl = self.visit(unary.operand)
1058
+ res = self.assign(ast.copy_location(ast.UnaryOp(unary.op, operand_res), unary))
1059
+ return res, pattern % (operand_expl,)
1060
+
1061
+ def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]:
1062
+ symbol = BINOP_MAP[binop.op.__class__]
1063
+ left_expr, left_expl = self.visit(binop.left)
1064
+ right_expr, right_expl = self.visit(binop.right)
1065
+ explanation = f"({left_expl} {symbol} {right_expl})"
1066
+ res = self.assign(
1067
+ ast.copy_location(ast.BinOp(left_expr, binop.op, right_expr), binop)
1068
+ )
1069
+ return res, explanation
1070
+
1071
+ def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]:
1072
+ new_func, func_expl = self.visit(call.func)
1073
+ arg_expls = []
1074
+ new_args = []
1075
+ new_kwargs = []
1076
+ for arg in call.args:
1077
+ if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
1078
+ self.scope, {}
1079
+ ):
1080
+ arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
1081
+ res, expl = self.visit(arg)
1082
+ arg_expls.append(expl)
1083
+ new_args.append(res)
1084
+ for keyword in call.keywords:
1085
+ if isinstance(
1086
+ keyword.value, ast.Name
1087
+ ) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
1088
+ keyword.value = self.variables_overwrite[self.scope][keyword.value.id] # type:ignore[assignment]
1089
+ res, expl = self.visit(keyword.value)
1090
+ new_kwargs.append(ast.keyword(keyword.arg, res))
1091
+ if keyword.arg:
1092
+ arg_expls.append(keyword.arg + "=" + expl)
1093
+ else: # **args have `arg` keywords with an .arg of None
1094
+ arg_expls.append("**" + expl)
1095
+
1096
+ expl = "{}({})".format(func_expl, ", ".join(arg_expls))
1097
+ new_call = ast.copy_location(ast.Call(new_func, new_args, new_kwargs), call)
1098
+ res = self.assign(new_call)
1099
+ res_expl = self.explanation_param(self.display(res))
1100
+ outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
1101
+ return res, outer_expl
1102
+
1103
+ def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]:
1104
+ # A Starred node can appear in a function call.
1105
+ res, expl = self.visit(starred.value)
1106
+ new_starred = ast.Starred(res, starred.ctx)
1107
+ return new_starred, "*" + expl
1108
+
1109
+ def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]:
1110
+ if not isinstance(attr.ctx, ast.Load):
1111
+ return self.generic_visit(attr)
1112
+ value, value_expl = self.visit(attr.value)
1113
+ res = self.assign(
1114
+ ast.copy_location(ast.Attribute(value, attr.attr, ast.Load()), attr)
1115
+ )
1116
+ res_expl = self.explanation_param(self.display(res))
1117
+ pat = "%s\n{%s = %s.%s\n}"
1118
+ expl = pat % (res_expl, res_expl, value_expl, attr.attr)
1119
+ return res, expl
1120
+
1121
+ def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]:
1122
+ self.push_format_context()
1123
+ # We first check if we have overwritten a variable in the previous assert
1124
+ if isinstance(
1125
+ comp.left, ast.Name
1126
+ ) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
1127
+ comp.left = self.variables_overwrite[self.scope][comp.left.id] # type:ignore[assignment]
1128
+ if isinstance(comp.left, ast.NamedExpr):
1129
+ self.variables_overwrite[self.scope][comp.left.target.id] = comp.left # type:ignore[assignment]
1130
+ left_res, left_expl = self.visit(comp.left)
1131
+ if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
1132
+ left_expl = f"({left_expl})"
1133
+ res_variables = [self.variable() for i in range(len(comp.ops))]
1134
+ load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables]
1135
+ store_names = [ast.Name(v, ast.Store()) for v in res_variables]
1136
+ it = zip(range(len(comp.ops)), comp.ops, comp.comparators)
1137
+ expls: list[ast.expr] = []
1138
+ syms: list[ast.expr] = []
1139
+ results = [left_res]
1140
+ for i, op, next_operand in it:
1141
+ if (
1142
+ isinstance(next_operand, ast.NamedExpr)
1143
+ and isinstance(left_res, ast.Name)
1144
+ and next_operand.target.id == left_res.id
1145
+ ):
1146
+ next_operand.target.id = self.variable()
1147
+ self.variables_overwrite[self.scope][left_res.id] = next_operand # type:ignore[assignment]
1148
+ next_res, next_expl = self.visit(next_operand)
1149
+ if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
1150
+ next_expl = f"({next_expl})"
1151
+ results.append(next_res)
1152
+ sym = BINOP_MAP[op.__class__]
1153
+ syms.append(ast.Constant(sym))
1154
+ expl = f"{left_expl} {sym} {next_expl}"
1155
+ expls.append(ast.Constant(expl))
1156
+ res_expr = ast.copy_location(ast.Compare(left_res, [op], [next_res]), comp)
1157
+ self.statements.append(ast.Assign([store_names[i]], res_expr))
1158
+ left_res, left_expl = next_res, next_expl
1159
+ # Use pytest.assertion.util._reprcompare if that's available.
1160
+ expl_call = self.helper(
1161
+ "_call_reprcompare",
1162
+ ast.Tuple(syms, ast.Load()),
1163
+ ast.Tuple(load_names, ast.Load()),
1164
+ ast.Tuple(expls, ast.Load()),
1165
+ ast.Tuple(results, ast.Load()),
1166
+ )
1167
+ if len(comp.ops) > 1:
1168
+ res: ast.expr = ast.BoolOp(ast.And(), load_names)
1169
+ else:
1170
+ res = load_names[0]
1171
+
1172
+ return res, self.explanation_param(self.pop_format_context(expl_call))
1173
+
1174
+
1175
+ def try_makedirs(cache_dir: Path) -> bool:
1176
+ """Attempt to create the given directory and sub-directories exist.
1177
+
1178
+ Returns True if successful or if it already exists.
1179
+ """
1180
+ try:
1181
+ os.makedirs(cache_dir, exist_ok=True)
1182
+ except (FileNotFoundError, NotADirectoryError, FileExistsError):
1183
+ # One of the path components was not a directory:
1184
+ # - we're in a zip file
1185
+ # - it is a file
1186
+ return False
1187
+ except PermissionError:
1188
+ return False
1189
+ except OSError as e:
1190
+ # as of now, EROFS doesn't have an equivalent OSError-subclass
1191
+ #
1192
+ # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not
1193
+ # implemented" for a read-only error
1194
+ if e.errno in {errno.EROFS, errno.ENOSYS}:
1195
+ return False
1196
+ raise
1197
+ return True
1198
+
1199
+
1200
+ def get_cache_dir(file_path: Path) -> Path:
1201
+ """Return the cache directory to write .pyc files for the given .py file path."""
1202
+ if sys.pycache_prefix:
1203
+ # given:
1204
+ # prefix = '/tmp/pycs'
1205
+ # path = '/home/user/proj/test_app.py'
1206
+ # we want:
1207
+ # '/tmp/pycs/home/user/proj'
1208
+ return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1])
1209
+ else:
1210
+ # classic pycache directory
1211
+ return file_path.parent / "__pycache__"
.venv/lib/python3.11/site-packages/_pytest/assertion/truncate.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utilities for truncating assertion output.
2
+
3
+ Current default behaviour is to truncate assertion explanations at
4
+ terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from _pytest.assertion import util
10
+ from _pytest.config import Config
11
+ from _pytest.nodes import Item
12
+
13
+
14
+ DEFAULT_MAX_LINES = 8
15
+ DEFAULT_MAX_CHARS = 8 * 80
16
+ USAGE_MSG = "use '-vv' to show"
17
+
18
+
19
+ def truncate_if_required(
20
+ explanation: list[str], item: Item, max_length: int | None = None
21
+ ) -> list[str]:
22
+ """Truncate this assertion explanation if the given test item is eligible."""
23
+ if _should_truncate_item(item):
24
+ return _truncate_explanation(explanation)
25
+ return explanation
26
+
27
+
28
+ def _should_truncate_item(item: Item) -> bool:
29
+ """Whether or not this test item is eligible for truncation."""
30
+ verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
31
+ return verbose < 2 and not util.running_on_ci()
32
+
33
+
34
+ def _truncate_explanation(
35
+ input_lines: list[str],
36
+ max_lines: int | None = None,
37
+ max_chars: int | None = None,
38
+ ) -> list[str]:
39
+ """Truncate given list of strings that makes up the assertion explanation.
40
+
41
+ Truncates to either 8 lines, or 640 characters - whichever the input reaches
42
+ first, taking the truncation explanation into account. The remaining lines
43
+ will be replaced by a usage message.
44
+ """
45
+ if max_lines is None:
46
+ max_lines = DEFAULT_MAX_LINES
47
+ if max_chars is None:
48
+ max_chars = DEFAULT_MAX_CHARS
49
+
50
+ # Check if truncation required
51
+ input_char_count = len("".join(input_lines))
52
+ # The length of the truncation explanation depends on the number of lines
53
+ # removed but is at least 68 characters:
54
+ # The real value is
55
+ # 64 (for the base message:
56
+ # '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
57
+ # )
58
+ # + 1 (for plural)
59
+ # + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
60
+ # + 3 for the '...' added to the truncated line
61
+ # But if there's more than 100 lines it's very likely that we're going to
62
+ # truncate, so we don't need the exact value using log10.
63
+ tolerable_max_chars = (
64
+ max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
65
+ )
66
+ # The truncation explanation add two lines to the output
67
+ tolerable_max_lines = max_lines + 2
68
+ if (
69
+ len(input_lines) <= tolerable_max_lines
70
+ and input_char_count <= tolerable_max_chars
71
+ ):
72
+ return input_lines
73
+ # Truncate first to max_lines, and then truncate to max_chars if necessary
74
+ truncated_explanation = input_lines[:max_lines]
75
+ truncated_char = True
76
+ # We reevaluate the need to truncate chars following removal of some lines
77
+ if len("".join(truncated_explanation)) > tolerable_max_chars:
78
+ truncated_explanation = _truncate_by_char_count(
79
+ truncated_explanation, max_chars
80
+ )
81
+ else:
82
+ truncated_char = False
83
+
84
+ truncated_line_count = len(input_lines) - len(truncated_explanation)
85
+ if truncated_explanation[-1]:
86
+ # Add ellipsis and take into account part-truncated final line
87
+ truncated_explanation[-1] = truncated_explanation[-1] + "..."
88
+ if truncated_char:
89
+ # It's possible that we did not remove any char from this line
90
+ truncated_line_count += 1
91
+ else:
92
+ # Add proper ellipsis when we were able to fit a full line exactly
93
+ truncated_explanation[-1] = "..."
94
+ return [
95
+ *truncated_explanation,
96
+ "",
97
+ f"...Full output truncated ({truncated_line_count} line"
98
+ f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
99
+ ]
100
+
101
+
102
+ def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]:
103
+ # Find point at which input length exceeds total allowed length
104
+ iterated_char_count = 0
105
+ for iterated_index, input_line in enumerate(input_lines):
106
+ if iterated_char_count + len(input_line) > max_chars:
107
+ break
108
+ iterated_char_count += len(input_line)
109
+
110
+ # Create truncated explanation with modified final line
111
+ truncated_result = input_lines[:iterated_index]
112
+ final_line = input_lines[iterated_index]
113
+ if final_line:
114
+ final_line_truncate_point = max_chars - iterated_char_count
115
+ final_line = final_line[:final_line_truncate_point]
116
+ truncated_result.append(final_line)
117
+ return truncated_result
.venv/lib/python3.11/site-packages/_pytest/assertion/util.py ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Utilities for assertion debugging."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import collections.abc
7
+ import os
8
+ import pprint
9
+ from typing import AbstractSet
10
+ from typing import Any
11
+ from typing import Callable
12
+ from typing import Iterable
13
+ from typing import Literal
14
+ from typing import Mapping
15
+ from typing import Protocol
16
+ from typing import Sequence
17
+ from unicodedata import normalize
18
+
19
+ from _pytest import outcomes
20
+ import _pytest._code
21
+ from _pytest._io.pprint import PrettyPrinter
22
+ from _pytest._io.saferepr import saferepr
23
+ from _pytest._io.saferepr import saferepr_unlimited
24
+ from _pytest.config import Config
25
+
26
+
27
+ # The _reprcompare attribute on the util module is used by the new assertion
28
+ # interpretation code and assertion rewriter to detect this plugin was
29
+ # loaded and in turn call the hooks defined here as part of the
30
+ # DebugInterpreter.
31
+ _reprcompare: Callable[[str, object, object], str | None] | None = None
32
+
33
+ # Works similarly as _reprcompare attribute. Is populated with the hook call
34
+ # when pytest_runtest_setup is called.
35
+ _assertion_pass: Callable[[int, str, str], None] | None = None
36
+
37
+ # Config object which is assigned during pytest_runtest_protocol.
38
+ _config: Config | None = None
39
+
40
+
41
+ class _HighlightFunc(Protocol):
42
+ def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
43
+ """Apply highlighting to the given source."""
44
+
45
+
46
+ def format_explanation(explanation: str) -> str:
47
+ r"""Format an explanation.
48
+
49
+ Normally all embedded newlines are escaped, however there are
50
+ three exceptions: \n{, \n} and \n~. The first two are intended
51
+ cover nested explanations, see function and attribute explanations
52
+ for examples (.visit_Call(), visit_Attribute()). The last one is
53
+ for when one explanation needs to span multiple lines, e.g. when
54
+ displaying diffs.
55
+ """
56
+ lines = _split_explanation(explanation)
57
+ result = _format_lines(lines)
58
+ return "\n".join(result)
59
+
60
+
61
+ def _split_explanation(explanation: str) -> list[str]:
62
+ r"""Return a list of individual lines in the explanation.
63
+
64
+ This will return a list of lines split on '\n{', '\n}' and '\n~'.
65
+ Any other newlines will be escaped and appear in the line as the
66
+ literal '\n' characters.
67
+ """
68
+ raw_lines = (explanation or "").split("\n")
69
+ lines = [raw_lines[0]]
70
+ for values in raw_lines[1:]:
71
+ if values and values[0] in ["{", "}", "~", ">"]:
72
+ lines.append(values)
73
+ else:
74
+ lines[-1] += "\\n" + values
75
+ return lines
76
+
77
+
78
+ def _format_lines(lines: Sequence[str]) -> list[str]:
79
+ """Format the individual lines.
80
+
81
+ This will replace the '{', '}' and '~' characters of our mini formatting
82
+ language with the proper 'where ...', 'and ...' and ' + ...' text, taking
83
+ care of indentation along the way.
84
+
85
+ Return a list of formatted lines.
86
+ """
87
+ result = list(lines[:1])
88
+ stack = [0]
89
+ stackcnt = [0]
90
+ for line in lines[1:]:
91
+ if line.startswith("{"):
92
+ if stackcnt[-1]:
93
+ s = "and "
94
+ else:
95
+ s = "where "
96
+ stack.append(len(result))
97
+ stackcnt[-1] += 1
98
+ stackcnt.append(0)
99
+ result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
100
+ elif line.startswith("}"):
101
+ stack.pop()
102
+ stackcnt.pop()
103
+ result[stack[-1]] += line[1:]
104
+ else:
105
+ assert line[0] in ["~", ">"]
106
+ stack[-1] += 1
107
+ indent = len(stack) if line.startswith("~") else len(stack) - 1
108
+ result.append(" " * indent + line[1:])
109
+ assert len(stack) == 1
110
+ return result
111
+
112
+
113
+ def issequence(x: Any) -> bool:
114
+ return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
115
+
116
+
117
+ def istext(x: Any) -> bool:
118
+ return isinstance(x, str)
119
+
120
+
121
+ def isdict(x: Any) -> bool:
122
+ return isinstance(x, dict)
123
+
124
+
125
+ def isset(x: Any) -> bool:
126
+ return isinstance(x, (set, frozenset))
127
+
128
+
129
+ def isnamedtuple(obj: Any) -> bool:
130
+ return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
131
+
132
+
133
+ def isdatacls(obj: Any) -> bool:
134
+ return getattr(obj, "__dataclass_fields__", None) is not None
135
+
136
+
137
+ def isattrs(obj: Any) -> bool:
138
+ return getattr(obj, "__attrs_attrs__", None) is not None
139
+
140
+
141
+ def isiterable(obj: Any) -> bool:
142
+ try:
143
+ iter(obj)
144
+ return not istext(obj)
145
+ except Exception:
146
+ return False
147
+
148
+
149
+ def has_default_eq(
150
+ obj: object,
151
+ ) -> bool:
152
+ """Check if an instance of an object contains the default eq
153
+
154
+ First, we check if the object's __eq__ attribute has __code__,
155
+ if so, we check the equally of the method code filename (__code__.co_filename)
156
+ to the default one generated by the dataclass and attr module
157
+ for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
158
+ """
159
+ # inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
160
+ if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
161
+ code_filename = obj.__eq__.__code__.co_filename
162
+
163
+ if isattrs(obj):
164
+ return "attrs generated eq" in code_filename
165
+
166
+ return code_filename == "<string>" # data class
167
+ return True
168
+
169
+
170
+ def assertrepr_compare(
171
+ config, op: str, left: Any, right: Any, use_ascii: bool = False
172
+ ) -> list[str] | None:
173
+ """Return specialised explanations for some operators/operands."""
174
+ verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
175
+
176
+ # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
177
+ # See issue #3246.
178
+ use_ascii = (
179
+ isinstance(left, str)
180
+ and isinstance(right, str)
181
+ and normalize("NFD", left) == normalize("NFD", right)
182
+ )
183
+
184
+ if verbose > 1:
185
+ left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
186
+ right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
187
+ else:
188
+ # XXX: "15 chars indentation" is wrong
189
+ # ("E AssertionError: assert "); should use term width.
190
+ maxsize = (
191
+ 80 - 15 - len(op) - 2
192
+ ) // 2 # 15 chars indentation, 1 space around op
193
+
194
+ left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
195
+ right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
196
+
197
+ summary = f"{left_repr} {op} {right_repr}"
198
+ highlighter = config.get_terminal_writer()._highlight
199
+
200
+ explanation = None
201
+ try:
202
+ if op == "==":
203
+ explanation = _compare_eq_any(left, right, highlighter, verbose)
204
+ elif op == "not in":
205
+ if istext(left) and istext(right):
206
+ explanation = _notin_text(left, right, verbose)
207
+ elif op == "!=":
208
+ if isset(left) and isset(right):
209
+ explanation = ["Both sets are equal"]
210
+ elif op == ">=":
211
+ if isset(left) and isset(right):
212
+ explanation = _compare_gte_set(left, right, highlighter, verbose)
213
+ elif op == "<=":
214
+ if isset(left) and isset(right):
215
+ explanation = _compare_lte_set(left, right, highlighter, verbose)
216
+ elif op == ">":
217
+ if isset(left) and isset(right):
218
+ explanation = _compare_gt_set(left, right, highlighter, verbose)
219
+ elif op == "<":
220
+ if isset(left) and isset(right):
221
+ explanation = _compare_lt_set(left, right, highlighter, verbose)
222
+
223
+ except outcomes.Exit:
224
+ raise
225
+ except Exception:
226
+ repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
227
+ explanation = [
228
+ f"(pytest_assertion plugin: representation of details failed: {repr_crash}.",
229
+ " Probably an object has a faulty __repr__.)",
230
+ ]
231
+
232
+ if not explanation:
233
+ return None
234
+
235
+ if explanation[0] != "":
236
+ explanation = ["", *explanation]
237
+ return [summary, *explanation]
238
+
239
+
240
+ def _compare_eq_any(
241
+ left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0
242
+ ) -> list[str]:
243
+ explanation = []
244
+ if istext(left) and istext(right):
245
+ explanation = _diff_text(left, right, verbose)
246
+ else:
247
+ from _pytest.python_api import ApproxBase
248
+
249
+ if isinstance(left, ApproxBase) or isinstance(right, ApproxBase):
250
+ # Although the common order should be obtained == expected, this ensures both ways
251
+ approx_side = left if isinstance(left, ApproxBase) else right
252
+ other_side = right if isinstance(left, ApproxBase) else left
253
+
254
+ explanation = approx_side._repr_compare(other_side)
255
+ elif type(left) is type(right) and (
256
+ isdatacls(left) or isattrs(left) or isnamedtuple(left)
257
+ ):
258
+ # Note: unlike dataclasses/attrs, namedtuples compare only the
259
+ # field values, not the type or field names. But this branch
260
+ # intentionally only handles the same-type case, which was often
261
+ # used in older code bases before dataclasses/attrs were available.
262
+ explanation = _compare_eq_cls(left, right, highlighter, verbose)
263
+ elif issequence(left) and issequence(right):
264
+ explanation = _compare_eq_sequence(left, right, highlighter, verbose)
265
+ elif isset(left) and isset(right):
266
+ explanation = _compare_eq_set(left, right, highlighter, verbose)
267
+ elif isdict(left) and isdict(right):
268
+ explanation = _compare_eq_dict(left, right, highlighter, verbose)
269
+
270
+ if isiterable(left) and isiterable(right):
271
+ expl = _compare_eq_iterable(left, right, highlighter, verbose)
272
+ explanation.extend(expl)
273
+
274
+ return explanation
275
+
276
+
277
+ def _diff_text(left: str, right: str, verbose: int = 0) -> list[str]:
278
+ """Return the explanation for the diff between text.
279
+
280
+ Unless --verbose is used this will skip leading and trailing
281
+ characters which are identical to keep the diff minimal.
282
+ """
283
+ from difflib import ndiff
284
+
285
+ explanation: list[str] = []
286
+
287
+ if verbose < 1:
288
+ i = 0 # just in case left or right has zero length
289
+ for i in range(min(len(left), len(right))):
290
+ if left[i] != right[i]:
291
+ break
292
+ if i > 42:
293
+ i -= 10 # Provide some context
294
+ explanation = [
295
+ f"Skipping {i} identical leading characters in diff, use -v to show"
296
+ ]
297
+ left = left[i:]
298
+ right = right[i:]
299
+ if len(left) == len(right):
300
+ for i in range(len(left)):
301
+ if left[-i] != right[-i]:
302
+ break
303
+ if i > 42:
304
+ i -= 10 # Provide some context
305
+ explanation += [
306
+ f"Skipping {i} identical trailing "
307
+ "characters in diff, use -v to show"
308
+ ]
309
+ left = left[:-i]
310
+ right = right[:-i]
311
+ keepends = True
312
+ if left.isspace() or right.isspace():
313
+ left = repr(str(left))
314
+ right = repr(str(right))
315
+ explanation += ["Strings contain only whitespace, escaping them using repr()"]
316
+ # "right" is the expected base against which we compare "left",
317
+ # see https://github.com/pytest-dev/pytest/issues/3333
318
+ explanation += [
319
+ line.strip("\n")
320
+ for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
321
+ ]
322
+ return explanation
323
+
324
+
325
+ def _compare_eq_iterable(
326
+ left: Iterable[Any],
327
+ right: Iterable[Any],
328
+ highlighter: _HighlightFunc,
329
+ verbose: int = 0,
330
+ ) -> list[str]:
331
+ if verbose <= 0 and not running_on_ci():
332
+ return ["Use -v to get more diff"]
333
+ # dynamic import to speedup pytest
334
+ import difflib
335
+
336
+ left_formatting = PrettyPrinter().pformat(left).splitlines()
337
+ right_formatting = PrettyPrinter().pformat(right).splitlines()
338
+
339
+ explanation = ["", "Full diff:"]
340
+ # "right" is the expected base against which we compare "left",
341
+ # see https://github.com/pytest-dev/pytest/issues/3333
342
+ explanation.extend(
343
+ highlighter(
344
+ "\n".join(
345
+ line.rstrip()
346
+ for line in difflib.ndiff(right_formatting, left_formatting)
347
+ ),
348
+ lexer="diff",
349
+ ).splitlines()
350
+ )
351
+ return explanation
352
+
353
+
354
+ def _compare_eq_sequence(
355
+ left: Sequence[Any],
356
+ right: Sequence[Any],
357
+ highlighter: _HighlightFunc,
358
+ verbose: int = 0,
359
+ ) -> list[str]:
360
+ comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
361
+ explanation: list[str] = []
362
+ len_left = len(left)
363
+ len_right = len(right)
364
+ for i in range(min(len_left, len_right)):
365
+ if left[i] != right[i]:
366
+ if comparing_bytes:
367
+ # when comparing bytes, we want to see their ascii representation
368
+ # instead of their numeric values (#5260)
369
+ # using a slice gives us the ascii representation:
370
+ # >>> s = b'foo'
371
+ # >>> s[0]
372
+ # 102
373
+ # >>> s[0:1]
374
+ # b'f'
375
+ left_value = left[i : i + 1]
376
+ right_value = right[i : i + 1]
377
+ else:
378
+ left_value = left[i]
379
+ right_value = right[i]
380
+
381
+ explanation.append(
382
+ f"At index {i} diff:"
383
+ f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
384
+ )
385
+ break
386
+
387
+ if comparing_bytes:
388
+ # when comparing bytes, it doesn't help to show the "sides contain one or more
389
+ # items" longer explanation, so skip it
390
+
391
+ return explanation
392
+
393
+ len_diff = len_left - len_right
394
+ if len_diff:
395
+ if len_diff > 0:
396
+ dir_with_more = "Left"
397
+ extra = saferepr(left[len_right])
398
+ else:
399
+ len_diff = 0 - len_diff
400
+ dir_with_more = "Right"
401
+ extra = saferepr(right[len_left])
402
+
403
+ if len_diff == 1:
404
+ explanation += [
405
+ f"{dir_with_more} contains one more item: {highlighter(extra)}"
406
+ ]
407
+ else:
408
+ explanation += [
409
+ "%s contains %d more items, first extra item: %s"
410
+ % (dir_with_more, len_diff, highlighter(extra))
411
+ ]
412
+ return explanation
413
+
414
+
415
+ def _compare_eq_set(
416
+ left: AbstractSet[Any],
417
+ right: AbstractSet[Any],
418
+ highlighter: _HighlightFunc,
419
+ verbose: int = 0,
420
+ ) -> list[str]:
421
+ explanation = []
422
+ explanation.extend(_set_one_sided_diff("left", left, right, highlighter))
423
+ explanation.extend(_set_one_sided_diff("right", right, left, highlighter))
424
+ return explanation
425
+
426
+
427
+ def _compare_gt_set(
428
+ left: AbstractSet[Any],
429
+ right: AbstractSet[Any],
430
+ highlighter: _HighlightFunc,
431
+ verbose: int = 0,
432
+ ) -> list[str]:
433
+ explanation = _compare_gte_set(left, right, highlighter)
434
+ if not explanation:
435
+ return ["Both sets are equal"]
436
+ return explanation
437
+
438
+
439
+ def _compare_lt_set(
440
+ left: AbstractSet[Any],
441
+ right: AbstractSet[Any],
442
+ highlighter: _HighlightFunc,
443
+ verbose: int = 0,
444
+ ) -> list[str]:
445
+ explanation = _compare_lte_set(left, right, highlighter)
446
+ if not explanation:
447
+ return ["Both sets are equal"]
448
+ return explanation
449
+
450
+
451
+ def _compare_gte_set(
452
+ left: AbstractSet[Any],
453
+ right: AbstractSet[Any],
454
+ highlighter: _HighlightFunc,
455
+ verbose: int = 0,
456
+ ) -> list[str]:
457
+ return _set_one_sided_diff("right", right, left, highlighter)
458
+
459
+
460
+ def _compare_lte_set(
461
+ left: AbstractSet[Any],
462
+ right: AbstractSet[Any],
463
+ highlighter: _HighlightFunc,
464
+ verbose: int = 0,
465
+ ) -> list[str]:
466
+ return _set_one_sided_diff("left", left, right, highlighter)
467
+
468
+
469
+ def _set_one_sided_diff(
470
+ posn: str,
471
+ set1: AbstractSet[Any],
472
+ set2: AbstractSet[Any],
473
+ highlighter: _HighlightFunc,
474
+ ) -> list[str]:
475
+ explanation = []
476
+ diff = set1 - set2
477
+ if diff:
478
+ explanation.append(f"Extra items in the {posn} set:")
479
+ for item in diff:
480
+ explanation.append(highlighter(saferepr(item)))
481
+ return explanation
482
+
483
+
484
+ def _compare_eq_dict(
485
+ left: Mapping[Any, Any],
486
+ right: Mapping[Any, Any],
487
+ highlighter: _HighlightFunc,
488
+ verbose: int = 0,
489
+ ) -> list[str]:
490
+ explanation: list[str] = []
491
+ set_left = set(left)
492
+ set_right = set(right)
493
+ common = set_left.intersection(set_right)
494
+ same = {k: left[k] for k in common if left[k] == right[k]}
495
+ if same and verbose < 2:
496
+ explanation += [f"Omitting {len(same)} identical items, use -vv to show"]
497
+ elif same:
498
+ explanation += ["Common items:"]
499
+ explanation += highlighter(pprint.pformat(same)).splitlines()
500
+ diff = {k for k in common if left[k] != right[k]}
501
+ if diff:
502
+ explanation += ["Differing items:"]
503
+ for k in diff:
504
+ explanation += [
505
+ highlighter(saferepr({k: left[k]}))
506
+ + " != "
507
+ + highlighter(saferepr({k: right[k]}))
508
+ ]
509
+ extra_left = set_left - set_right
510
+ len_extra_left = len(extra_left)
511
+ if len_extra_left:
512
+ explanation.append(
513
+ "Left contains %d more item%s:"
514
+ % (len_extra_left, "" if len_extra_left == 1 else "s")
515
+ )
516
+ explanation.extend(
517
+ highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines()
518
+ )
519
+ extra_right = set_right - set_left
520
+ len_extra_right = len(extra_right)
521
+ if len_extra_right:
522
+ explanation.append(
523
+ "Right contains %d more item%s:"
524
+ % (len_extra_right, "" if len_extra_right == 1 else "s")
525
+ )
526
+ explanation.extend(
527
+ highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines()
528
+ )
529
+ return explanation
530
+
531
+
532
+ def _compare_eq_cls(
533
+ left: Any, right: Any, highlighter: _HighlightFunc, verbose: int
534
+ ) -> list[str]:
535
+ if not has_default_eq(left):
536
+ return []
537
+ if isdatacls(left):
538
+ import dataclasses
539
+
540
+ all_fields = dataclasses.fields(left)
541
+ fields_to_check = [info.name for info in all_fields if info.compare]
542
+ elif isattrs(left):
543
+ all_fields = left.__attrs_attrs__
544
+ fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
545
+ elif isnamedtuple(left):
546
+ fields_to_check = left._fields
547
+ else:
548
+ assert False
549
+
550
+ indent = " "
551
+ same = []
552
+ diff = []
553
+ for field in fields_to_check:
554
+ if getattr(left, field) == getattr(right, field):
555
+ same.append(field)
556
+ else:
557
+ diff.append(field)
558
+
559
+ explanation = []
560
+ if same or diff:
561
+ explanation += [""]
562
+ if same and verbose < 2:
563
+ explanation.append(f"Omitting {len(same)} identical items, use -vv to show")
564
+ elif same:
565
+ explanation += ["Matching attributes:"]
566
+ explanation += highlighter(pprint.pformat(same)).splitlines()
567
+ if diff:
568
+ explanation += ["Differing attributes:"]
569
+ explanation += highlighter(pprint.pformat(diff)).splitlines()
570
+ for field in diff:
571
+ field_left = getattr(left, field)
572
+ field_right = getattr(right, field)
573
+ explanation += [
574
+ "",
575
+ f"Drill down into differing attribute {field}:",
576
+ f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}",
577
+ ]
578
+ explanation += [
579
+ indent + line
580
+ for line in _compare_eq_any(
581
+ field_left, field_right, highlighter, verbose
582
+ )
583
+ ]
584
+ return explanation
585
+
586
+
587
+ def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]:
588
+ index = text.find(term)
589
+ head = text[:index]
590
+ tail = text[index + len(term) :]
591
+ correct_text = head + tail
592
+ diff = _diff_text(text, correct_text, verbose)
593
+ newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"]
594
+ for line in diff:
595
+ if line.startswith("Skipping"):
596
+ continue
597
+ if line.startswith("- "):
598
+ continue
599
+ if line.startswith("+ "):
600
+ newdiff.append(" " + line[2:])
601
+ else:
602
+ newdiff.append(line)
603
+ return newdiff
604
+
605
+
606
+ def running_on_ci() -> bool:
607
+ """Check if we're currently running on a CI system."""
608
+ env_vars = ["CI", "BUILD_NUMBER"]
609
+ return any(var in os.environ for var in env_vars)
.venv/lib/python3.11/site-packages/_pytest/capture.py ADDED
@@ -0,0 +1,1087 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Per-test stdout/stderr capturing mechanism."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import abc
7
+ import collections
8
+ import contextlib
9
+ import io
10
+ from io import UnsupportedOperation
11
+ import os
12
+ import sys
13
+ from tempfile import TemporaryFile
14
+ from types import TracebackType
15
+ from typing import Any
16
+ from typing import AnyStr
17
+ from typing import BinaryIO
18
+ from typing import Final
19
+ from typing import final
20
+ from typing import Generator
21
+ from typing import Generic
22
+ from typing import Iterable
23
+ from typing import Iterator
24
+ from typing import Literal
25
+ from typing import NamedTuple
26
+ from typing import TextIO
27
+ from typing import TYPE_CHECKING
28
+
29
+
30
+ if TYPE_CHECKING:
31
+ from typing_extensions import Self
32
+
33
+ from _pytest.config import Config
34
+ from _pytest.config import hookimpl
35
+ from _pytest.config.argparsing import Parser
36
+ from _pytest.deprecated import check_ispytest
37
+ from _pytest.fixtures import fixture
38
+ from _pytest.fixtures import SubRequest
39
+ from _pytest.nodes import Collector
40
+ from _pytest.nodes import File
41
+ from _pytest.nodes import Item
42
+ from _pytest.reports import CollectReport
43
+
44
+
45
+ _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
46
+
47
+
48
+ def pytest_addoption(parser: Parser) -> None:
49
+ group = parser.getgroup("general")
50
+ group._addoption(
51
+ "--capture",
52
+ action="store",
53
+ default="fd",
54
+ metavar="method",
55
+ choices=["fd", "sys", "no", "tee-sys"],
56
+ help="Per-test capturing method: one of fd|sys|no|tee-sys",
57
+ )
58
+ group._addoption(
59
+ "-s",
60
+ action="store_const",
61
+ const="no",
62
+ dest="capture",
63
+ help="Shortcut for --capture=no",
64
+ )
65
+
66
+
67
+ def _colorama_workaround() -> None:
68
+ """Ensure colorama is imported so that it attaches to the correct stdio
69
+ handles on Windows.
70
+
71
+ colorama uses the terminal on import time. So if something does the
72
+ first import of colorama while I/O capture is active, colorama will
73
+ fail in various ways.
74
+ """
75
+ if sys.platform.startswith("win32"):
76
+ try:
77
+ import colorama # noqa: F401
78
+ except ImportError:
79
+ pass
80
+
81
+
82
+ def _windowsconsoleio_workaround(stream: TextIO) -> None:
83
+ """Workaround for Windows Unicode console handling.
84
+
85
+ Python 3.6 implemented Unicode console handling for Windows. This works
86
+ by reading/writing to the raw console handle using
87
+ ``{Read,Write}ConsoleW``.
88
+
89
+ The problem is that we are going to ``dup2`` over the stdio file
90
+ descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
91
+ handles used by Python to write to the console. Though there is still some
92
+ weirdness and the console handle seems to only be closed randomly and not
93
+ on the first call to ``CloseHandle``, or maybe it gets reopened with the
94
+ same handle value when we suspend capturing.
95
+
96
+ The workaround in this case will reopen stdio with a different fd which
97
+ also means a different handle by replicating the logic in
98
+ "Py_lifecycle.c:initstdio/create_stdio".
99
+
100
+ :param stream:
101
+ In practice ``sys.stdout`` or ``sys.stderr``, but given
102
+ here as parameter for unittesting purposes.
103
+
104
+ See https://github.com/pytest-dev/py/issues/103.
105
+ """
106
+ if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
107
+ return
108
+
109
+ # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
110
+ if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore]
111
+ return
112
+
113
+ raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
114
+
115
+ if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore]
116
+ return
117
+
118
+ def _reopen_stdio(f, mode):
119
+ if not hasattr(stream.buffer, "raw") and mode[0] == "w":
120
+ buffering = 0
121
+ else:
122
+ buffering = -1
123
+
124
+ return io.TextIOWrapper(
125
+ open(os.dup(f.fileno()), mode, buffering),
126
+ f.encoding,
127
+ f.errors,
128
+ f.newlines,
129
+ f.line_buffering,
130
+ )
131
+
132
+ sys.stdin = _reopen_stdio(sys.stdin, "rb")
133
+ sys.stdout = _reopen_stdio(sys.stdout, "wb")
134
+ sys.stderr = _reopen_stdio(sys.stderr, "wb")
135
+
136
+
137
+ @hookimpl(wrapper=True)
138
+ def pytest_load_initial_conftests(early_config: Config) -> Generator[None]:
139
+ ns = early_config.known_args_namespace
140
+ if ns.capture == "fd":
141
+ _windowsconsoleio_workaround(sys.stdout)
142
+ _colorama_workaround()
143
+ pluginmanager = early_config.pluginmanager
144
+ capman = CaptureManager(ns.capture)
145
+ pluginmanager.register(capman, "capturemanager")
146
+
147
+ # Make sure that capturemanager is properly reset at final shutdown.
148
+ early_config.add_cleanup(capman.stop_global_capturing)
149
+
150
+ # Finally trigger conftest loading but while capturing (issue #93).
151
+ capman.start_global_capturing()
152
+ try:
153
+ try:
154
+ yield
155
+ finally:
156
+ capman.suspend_global_capture()
157
+ except BaseException:
158
+ out, err = capman.read_global_capture()
159
+ sys.stdout.write(out)
160
+ sys.stderr.write(err)
161
+ raise
162
+
163
+
164
+ # IO Helpers.
165
+
166
+
167
+ class EncodedFile(io.TextIOWrapper):
168
+ __slots__ = ()
169
+
170
+ @property
171
+ def name(self) -> str:
172
+ # Ensure that file.name is a string. Workaround for a Python bug
173
+ # fixed in >=3.7.4: https://bugs.python.org/issue36015
174
+ return repr(self.buffer)
175
+
176
+ @property
177
+ def mode(self) -> str:
178
+ # TextIOWrapper doesn't expose a mode, but at least some of our
179
+ # tests check it.
180
+ return self.buffer.mode.replace("b", "")
181
+
182
+
183
+ class CaptureIO(io.TextIOWrapper):
184
+ def __init__(self) -> None:
185
+ super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)
186
+
187
+ def getvalue(self) -> str:
188
+ assert isinstance(self.buffer, io.BytesIO)
189
+ return self.buffer.getvalue().decode("UTF-8")
190
+
191
+
192
+ class TeeCaptureIO(CaptureIO):
193
+ def __init__(self, other: TextIO) -> None:
194
+ self._other = other
195
+ super().__init__()
196
+
197
+ def write(self, s: str) -> int:
198
+ super().write(s)
199
+ return self._other.write(s)
200
+
201
+
202
+ class DontReadFromInput(TextIO):
203
+ @property
204
+ def encoding(self) -> str:
205
+ assert sys.__stdin__ is not None
206
+ return sys.__stdin__.encoding
207
+
208
+ def read(self, size: int = -1) -> str:
209
+ raise OSError(
210
+ "pytest: reading from stdin while output is captured! Consider using `-s`."
211
+ )
212
+
213
+ readline = read
214
+
215
+ def __next__(self) -> str:
216
+ return self.readline()
217
+
218
+ def readlines(self, hint: int | None = -1) -> list[str]:
219
+ raise OSError(
220
+ "pytest: reading from stdin while output is captured! Consider using `-s`."
221
+ )
222
+
223
+ def __iter__(self) -> Iterator[str]:
224
+ return self
225
+
226
+ def fileno(self) -> int:
227
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
228
+
229
+ def flush(self) -> None:
230
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()")
231
+
232
+ def isatty(self) -> bool:
233
+ return False
234
+
235
+ def close(self) -> None:
236
+ pass
237
+
238
+ def readable(self) -> bool:
239
+ return False
240
+
241
+ def seek(self, offset: int, whence: int = 0) -> int:
242
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
243
+
244
+ def seekable(self) -> bool:
245
+ return False
246
+
247
+ def tell(self) -> int:
248
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
249
+
250
+ def truncate(self, size: int | None = None) -> int:
251
+ raise UnsupportedOperation("cannot truncate stdin")
252
+
253
+ def write(self, data: str) -> int:
254
+ raise UnsupportedOperation("cannot write to stdin")
255
+
256
+ def writelines(self, lines: Iterable[str]) -> None:
257
+ raise UnsupportedOperation("Cannot write to stdin")
258
+
259
+ def writable(self) -> bool:
260
+ return False
261
+
262
+ def __enter__(self) -> Self:
263
+ return self
264
+
265
+ def __exit__(
266
+ self,
267
+ type: type[BaseException] | None,
268
+ value: BaseException | None,
269
+ traceback: TracebackType | None,
270
+ ) -> None:
271
+ pass
272
+
273
+ @property
274
+ def buffer(self) -> BinaryIO:
275
+ # The str/bytes doesn't actually matter in this type, so OK to fake.
276
+ return self # type: ignore[return-value]
277
+
278
+
279
+ # Capture classes.
280
+
281
+
282
+ class CaptureBase(abc.ABC, Generic[AnyStr]):
283
+ EMPTY_BUFFER: AnyStr
284
+
285
+ @abc.abstractmethod
286
+ def __init__(self, fd: int) -> None:
287
+ raise NotImplementedError()
288
+
289
+ @abc.abstractmethod
290
+ def start(self) -> None:
291
+ raise NotImplementedError()
292
+
293
+ @abc.abstractmethod
294
+ def done(self) -> None:
295
+ raise NotImplementedError()
296
+
297
+ @abc.abstractmethod
298
+ def suspend(self) -> None:
299
+ raise NotImplementedError()
300
+
301
+ @abc.abstractmethod
302
+ def resume(self) -> None:
303
+ raise NotImplementedError()
304
+
305
+ @abc.abstractmethod
306
+ def writeorg(self, data: AnyStr) -> None:
307
+ raise NotImplementedError()
308
+
309
+ @abc.abstractmethod
310
+ def snap(self) -> AnyStr:
311
+ raise NotImplementedError()
312
+
313
+
314
+ patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
315
+
316
+
317
+ class NoCapture(CaptureBase[str]):
318
+ EMPTY_BUFFER = ""
319
+
320
+ def __init__(self, fd: int) -> None:
321
+ pass
322
+
323
+ def start(self) -> None:
324
+ pass
325
+
326
+ def done(self) -> None:
327
+ pass
328
+
329
+ def suspend(self) -> None:
330
+ pass
331
+
332
+ def resume(self) -> None:
333
+ pass
334
+
335
+ def snap(self) -> str:
336
+ return ""
337
+
338
+ def writeorg(self, data: str) -> None:
339
+ pass
340
+
341
+
342
+ class SysCaptureBase(CaptureBase[AnyStr]):
343
+ def __init__(
344
+ self, fd: int, tmpfile: TextIO | None = None, *, tee: bool = False
345
+ ) -> None:
346
+ name = patchsysdict[fd]
347
+ self._old: TextIO = getattr(sys, name)
348
+ self.name = name
349
+ if tmpfile is None:
350
+ if name == "stdin":
351
+ tmpfile = DontReadFromInput()
352
+ else:
353
+ tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old)
354
+ self.tmpfile = tmpfile
355
+ self._state = "initialized"
356
+
357
+ def repr(self, class_name: str) -> str:
358
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
359
+ class_name,
360
+ self.name,
361
+ hasattr(self, "_old") and repr(self._old) or "<UNSET>",
362
+ self._state,
363
+ self.tmpfile,
364
+ )
365
+
366
+ def __repr__(self) -> str:
367
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
368
+ self.__class__.__name__,
369
+ self.name,
370
+ hasattr(self, "_old") and repr(self._old) or "<UNSET>",
371
+ self._state,
372
+ self.tmpfile,
373
+ )
374
+
375
+ def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
376
+ assert (
377
+ self._state in states
378
+ ), "cannot {} in state {!r}: expected one of {}".format(
379
+ op, self._state, ", ".join(states)
380
+ )
381
+
382
+ def start(self) -> None:
383
+ self._assert_state("start", ("initialized",))
384
+ setattr(sys, self.name, self.tmpfile)
385
+ self._state = "started"
386
+
387
+ def done(self) -> None:
388
+ self._assert_state("done", ("initialized", "started", "suspended", "done"))
389
+ if self._state == "done":
390
+ return
391
+ setattr(sys, self.name, self._old)
392
+ del self._old
393
+ self.tmpfile.close()
394
+ self._state = "done"
395
+
396
+ def suspend(self) -> None:
397
+ self._assert_state("suspend", ("started", "suspended"))
398
+ setattr(sys, self.name, self._old)
399
+ self._state = "suspended"
400
+
401
+ def resume(self) -> None:
402
+ self._assert_state("resume", ("started", "suspended"))
403
+ if self._state == "started":
404
+ return
405
+ setattr(sys, self.name, self.tmpfile)
406
+ self._state = "started"
407
+
408
+
409
+ class SysCaptureBinary(SysCaptureBase[bytes]):
410
+ EMPTY_BUFFER = b""
411
+
412
+ def snap(self) -> bytes:
413
+ self._assert_state("snap", ("started", "suspended"))
414
+ self.tmpfile.seek(0)
415
+ res = self.tmpfile.buffer.read()
416
+ self.tmpfile.seek(0)
417
+ self.tmpfile.truncate()
418
+ return res
419
+
420
+ def writeorg(self, data: bytes) -> None:
421
+ self._assert_state("writeorg", ("started", "suspended"))
422
+ self._old.flush()
423
+ self._old.buffer.write(data)
424
+ self._old.buffer.flush()
425
+
426
+
427
+ class SysCapture(SysCaptureBase[str]):
428
+ EMPTY_BUFFER = ""
429
+
430
+ def snap(self) -> str:
431
+ self._assert_state("snap", ("started", "suspended"))
432
+ assert isinstance(self.tmpfile, CaptureIO)
433
+ res = self.tmpfile.getvalue()
434
+ self.tmpfile.seek(0)
435
+ self.tmpfile.truncate()
436
+ return res
437
+
438
+ def writeorg(self, data: str) -> None:
439
+ self._assert_state("writeorg", ("started", "suspended"))
440
+ self._old.write(data)
441
+ self._old.flush()
442
+
443
+
444
+ class FDCaptureBase(CaptureBase[AnyStr]):
445
+ def __init__(self, targetfd: int) -> None:
446
+ self.targetfd = targetfd
447
+
448
+ try:
449
+ os.fstat(targetfd)
450
+ except OSError:
451
+ # FD capturing is conceptually simple -- create a temporary file,
452
+ # redirect the FD to it, redirect back when done. But when the
453
+ # target FD is invalid it throws a wrench into this lovely scheme.
454
+ #
455
+ # Tests themselves shouldn't care if the FD is valid, FD capturing
456
+ # should work regardless of external circumstances. So falling back
457
+ # to just sys capturing is not a good option.
458
+ #
459
+ # Further complications are the need to support suspend() and the
460
+ # possibility of FD reuse (e.g. the tmpfile getting the very same
461
+ # target FD). The following approach is robust, I believe.
462
+ self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR)
463
+ os.dup2(self.targetfd_invalid, targetfd)
464
+ else:
465
+ self.targetfd_invalid = None
466
+ self.targetfd_save = os.dup(targetfd)
467
+
468
+ if targetfd == 0:
469
+ self.tmpfile = open(os.devnull, encoding="utf-8")
470
+ self.syscapture: CaptureBase[str] = SysCapture(targetfd)
471
+ else:
472
+ self.tmpfile = EncodedFile(
473
+ TemporaryFile(buffering=0),
474
+ encoding="utf-8",
475
+ errors="replace",
476
+ newline="",
477
+ write_through=True,
478
+ )
479
+ if targetfd in patchsysdict:
480
+ self.syscapture = SysCapture(targetfd, self.tmpfile)
481
+ else:
482
+ self.syscapture = NoCapture(targetfd)
483
+
484
+ self._state = "initialized"
485
+
486
+ def __repr__(self) -> str:
487
+ return (
488
+ f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
489
+ f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
490
+ )
491
+
492
+ def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
493
+ assert (
494
+ self._state in states
495
+ ), "cannot {} in state {!r}: expected one of {}".format(
496
+ op, self._state, ", ".join(states)
497
+ )
498
+
499
+ def start(self) -> None:
500
+ """Start capturing on targetfd using memorized tmpfile."""
501
+ self._assert_state("start", ("initialized",))
502
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
503
+ self.syscapture.start()
504
+ self._state = "started"
505
+
506
+ def done(self) -> None:
507
+ """Stop capturing, restore streams, return original capture file,
508
+ seeked to position zero."""
509
+ self._assert_state("done", ("initialized", "started", "suspended", "done"))
510
+ if self._state == "done":
511
+ return
512
+ os.dup2(self.targetfd_save, self.targetfd)
513
+ os.close(self.targetfd_save)
514
+ if self.targetfd_invalid is not None:
515
+ if self.targetfd_invalid != self.targetfd:
516
+ os.close(self.targetfd)
517
+ os.close(self.targetfd_invalid)
518
+ self.syscapture.done()
519
+ self.tmpfile.close()
520
+ self._state = "done"
521
+
522
+ def suspend(self) -> None:
523
+ self._assert_state("suspend", ("started", "suspended"))
524
+ if self._state == "suspended":
525
+ return
526
+ self.syscapture.suspend()
527
+ os.dup2(self.targetfd_save, self.targetfd)
528
+ self._state = "suspended"
529
+
530
+ def resume(self) -> None:
531
+ self._assert_state("resume", ("started", "suspended"))
532
+ if self._state == "started":
533
+ return
534
+ self.syscapture.resume()
535
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
536
+ self._state = "started"
537
+
538
+
539
+ class FDCaptureBinary(FDCaptureBase[bytes]):
540
+ """Capture IO to/from a given OS-level file descriptor.
541
+
542
+ snap() produces `bytes`.
543
+ """
544
+
545
+ EMPTY_BUFFER = b""
546
+
547
+ def snap(self) -> bytes:
548
+ self._assert_state("snap", ("started", "suspended"))
549
+ self.tmpfile.seek(0)
550
+ res = self.tmpfile.buffer.read()
551
+ self.tmpfile.seek(0)
552
+ self.tmpfile.truncate()
553
+ return res
554
+
555
+ def writeorg(self, data: bytes) -> None:
556
+ """Write to original file descriptor."""
557
+ self._assert_state("writeorg", ("started", "suspended"))
558
+ os.write(self.targetfd_save, data)
559
+
560
+
561
+ class FDCapture(FDCaptureBase[str]):
562
+ """Capture IO to/from a given OS-level file descriptor.
563
+
564
+ snap() produces text.
565
+ """
566
+
567
+ EMPTY_BUFFER = ""
568
+
569
+ def snap(self) -> str:
570
+ self._assert_state("snap", ("started", "suspended"))
571
+ self.tmpfile.seek(0)
572
+ res = self.tmpfile.read()
573
+ self.tmpfile.seek(0)
574
+ self.tmpfile.truncate()
575
+ return res
576
+
577
+ def writeorg(self, data: str) -> None:
578
+ """Write to original file descriptor."""
579
+ self._assert_state("writeorg", ("started", "suspended"))
580
+ # XXX use encoding of original stream
581
+ os.write(self.targetfd_save, data.encode("utf-8"))
582
+
583
+
584
+ # MultiCapture
585
+
586
+
587
+ # Generic NamedTuple only supported since Python 3.11.
588
+ if sys.version_info >= (3, 11) or TYPE_CHECKING:
589
+
590
+ @final
591
+ class CaptureResult(NamedTuple, Generic[AnyStr]):
592
+ """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
593
+
594
+ out: AnyStr
595
+ err: AnyStr
596
+
597
+ else:
598
+
599
+ class CaptureResult(
600
+ collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
601
+ Generic[AnyStr],
602
+ ):
603
+ """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
604
+
605
+ __slots__ = ()
606
+
607
+
608
+ class MultiCapture(Generic[AnyStr]):
609
+ _state = None
610
+ _in_suspended = False
611
+
612
+ def __init__(
613
+ self,
614
+ in_: CaptureBase[AnyStr] | None,
615
+ out: CaptureBase[AnyStr] | None,
616
+ err: CaptureBase[AnyStr] | None,
617
+ ) -> None:
618
+ self.in_: CaptureBase[AnyStr] | None = in_
619
+ self.out: CaptureBase[AnyStr] | None = out
620
+ self.err: CaptureBase[AnyStr] | None = err
621
+
622
+ def __repr__(self) -> str:
623
+ return (
624
+ f"<MultiCapture out={self.out!r} err={self.err!r} in_={self.in_!r} "
625
+ f"_state={self._state!r} _in_suspended={self._in_suspended!r}>"
626
+ )
627
+
628
+ def start_capturing(self) -> None:
629
+ self._state = "started"
630
+ if self.in_:
631
+ self.in_.start()
632
+ if self.out:
633
+ self.out.start()
634
+ if self.err:
635
+ self.err.start()
636
+
637
+ def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]:
638
+ """Pop current snapshot out/err capture and flush to orig streams."""
639
+ out, err = self.readouterr()
640
+ if out:
641
+ assert self.out is not None
642
+ self.out.writeorg(out)
643
+ if err:
644
+ assert self.err is not None
645
+ self.err.writeorg(err)
646
+ return out, err
647
+
648
+ def suspend_capturing(self, in_: bool = False) -> None:
649
+ self._state = "suspended"
650
+ if self.out:
651
+ self.out.suspend()
652
+ if self.err:
653
+ self.err.suspend()
654
+ if in_ and self.in_:
655
+ self.in_.suspend()
656
+ self._in_suspended = True
657
+
658
+ def resume_capturing(self) -> None:
659
+ self._state = "started"
660
+ if self.out:
661
+ self.out.resume()
662
+ if self.err:
663
+ self.err.resume()
664
+ if self._in_suspended:
665
+ assert self.in_ is not None
666
+ self.in_.resume()
667
+ self._in_suspended = False
668
+
669
+ def stop_capturing(self) -> None:
670
+ """Stop capturing and reset capturing streams."""
671
+ if self._state == "stopped":
672
+ raise ValueError("was already stopped")
673
+ self._state = "stopped"
674
+ if self.out:
675
+ self.out.done()
676
+ if self.err:
677
+ self.err.done()
678
+ if self.in_:
679
+ self.in_.done()
680
+
681
+ def is_started(self) -> bool:
682
+ """Whether actively capturing -- not suspended or stopped."""
683
+ return self._state == "started"
684
+
685
+ def readouterr(self) -> CaptureResult[AnyStr]:
686
+ out = self.out.snap() if self.out else ""
687
+ err = self.err.snap() if self.err else ""
688
+ # TODO: This type error is real, need to fix.
689
+ return CaptureResult(out, err) # type: ignore[arg-type]
690
+
691
+
692
+ def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
693
+ if method == "fd":
694
+ return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
695
+ elif method == "sys":
696
+ return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2))
697
+ elif method == "no":
698
+ return MultiCapture(in_=None, out=None, err=None)
699
+ elif method == "tee-sys":
700
+ return MultiCapture(
701
+ in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)
702
+ )
703
+ raise ValueError(f"unknown capturing method: {method!r}")
704
+
705
+
706
+ # CaptureManager and CaptureFixture
707
+
708
+
709
+ class CaptureManager:
710
+ """The capture plugin.
711
+
712
+ Manages that the appropriate capture method is enabled/disabled during
713
+ collection and each test phase (setup, call, teardown). After each of
714
+ those points, the captured output is obtained and attached to the
715
+ collection/runtest report.
716
+
717
+ There are two levels of capture:
718
+
719
+ * global: enabled by default and can be suppressed by the ``-s``
720
+ option. This is always enabled/disabled during collection and each test
721
+ phase.
722
+
723
+ * fixture: when a test function or one of its fixture depend on the
724
+ ``capsys`` or ``capfd`` fixtures. In this case special handling is
725
+ needed to ensure the fixtures take precedence over the global capture.
726
+ """
727
+
728
+ def __init__(self, method: _CaptureMethod) -> None:
729
+ self._method: Final = method
730
+ self._global_capturing: MultiCapture[str] | None = None
731
+ self._capture_fixture: CaptureFixture[Any] | None = None
732
+
733
+ def __repr__(self) -> str:
734
+ return (
735
+ f"<CaptureManager _method={self._method!r} _global_capturing={self._global_capturing!r} "
736
+ f"_capture_fixture={self._capture_fixture!r}>"
737
+ )
738
+
739
+ def is_capturing(self) -> str | bool:
740
+ if self.is_globally_capturing():
741
+ return "global"
742
+ if self._capture_fixture:
743
+ return f"fixture {self._capture_fixture.request.fixturename}"
744
+ return False
745
+
746
+ # Global capturing control
747
+
748
+ def is_globally_capturing(self) -> bool:
749
+ return self._method != "no"
750
+
751
+ def start_global_capturing(self) -> None:
752
+ assert self._global_capturing is None
753
+ self._global_capturing = _get_multicapture(self._method)
754
+ self._global_capturing.start_capturing()
755
+
756
+ def stop_global_capturing(self) -> None:
757
+ if self._global_capturing is not None:
758
+ self._global_capturing.pop_outerr_to_orig()
759
+ self._global_capturing.stop_capturing()
760
+ self._global_capturing = None
761
+
762
+ def resume_global_capture(self) -> None:
763
+ # During teardown of the python process, and on rare occasions, capture
764
+ # attributes can be `None` while trying to resume global capture.
765
+ if self._global_capturing is not None:
766
+ self._global_capturing.resume_capturing()
767
+
768
+ def suspend_global_capture(self, in_: bool = False) -> None:
769
+ if self._global_capturing is not None:
770
+ self._global_capturing.suspend_capturing(in_=in_)
771
+
772
+ def suspend(self, in_: bool = False) -> None:
773
+ # Need to undo local capsys-et-al if it exists before disabling global capture.
774
+ self.suspend_fixture()
775
+ self.suspend_global_capture(in_)
776
+
777
+ def resume(self) -> None:
778
+ self.resume_global_capture()
779
+ self.resume_fixture()
780
+
781
+ def read_global_capture(self) -> CaptureResult[str]:
782
+ assert self._global_capturing is not None
783
+ return self._global_capturing.readouterr()
784
+
785
+ # Fixture Control
786
+
787
+ def set_fixture(self, capture_fixture: CaptureFixture[Any]) -> None:
788
+ if self._capture_fixture:
789
+ current_fixture = self._capture_fixture.request.fixturename
790
+ requested_fixture = capture_fixture.request.fixturename
791
+ capture_fixture.request.raiseerror(
792
+ f"cannot use {requested_fixture} and {current_fixture} at the same time"
793
+ )
794
+ self._capture_fixture = capture_fixture
795
+
796
+ def unset_fixture(self) -> None:
797
+ self._capture_fixture = None
798
+
799
+ def activate_fixture(self) -> None:
800
+ """If the current item is using ``capsys`` or ``capfd``, activate
801
+ them so they take precedence over the global capture."""
802
+ if self._capture_fixture:
803
+ self._capture_fixture._start()
804
+
805
+ def deactivate_fixture(self) -> None:
806
+ """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
807
+ if self._capture_fixture:
808
+ self._capture_fixture.close()
809
+
810
+ def suspend_fixture(self) -> None:
811
+ if self._capture_fixture:
812
+ self._capture_fixture._suspend()
813
+
814
+ def resume_fixture(self) -> None:
815
+ if self._capture_fixture:
816
+ self._capture_fixture._resume()
817
+
818
+ # Helper context managers
819
+
820
+ @contextlib.contextmanager
821
+ def global_and_fixture_disabled(self) -> Generator[None]:
822
+ """Context manager to temporarily disable global and current fixture capturing."""
823
+ do_fixture = self._capture_fixture and self._capture_fixture._is_started()
824
+ if do_fixture:
825
+ self.suspend_fixture()
826
+ do_global = self._global_capturing and self._global_capturing.is_started()
827
+ if do_global:
828
+ self.suspend_global_capture()
829
+ try:
830
+ yield
831
+ finally:
832
+ if do_global:
833
+ self.resume_global_capture()
834
+ if do_fixture:
835
+ self.resume_fixture()
836
+
837
+ @contextlib.contextmanager
838
+ def item_capture(self, when: str, item: Item) -> Generator[None]:
839
+ self.resume_global_capture()
840
+ self.activate_fixture()
841
+ try:
842
+ yield
843
+ finally:
844
+ self.deactivate_fixture()
845
+ self.suspend_global_capture(in_=False)
846
+
847
+ out, err = self.read_global_capture()
848
+ item.add_report_section(when, "stdout", out)
849
+ item.add_report_section(when, "stderr", err)
850
+
851
+ # Hooks
852
+
853
+ @hookimpl(wrapper=True)
854
+ def pytest_make_collect_report(
855
+ self, collector: Collector
856
+ ) -> Generator[None, CollectReport, CollectReport]:
857
+ if isinstance(collector, File):
858
+ self.resume_global_capture()
859
+ try:
860
+ rep = yield
861
+ finally:
862
+ self.suspend_global_capture()
863
+ out, err = self.read_global_capture()
864
+ if out:
865
+ rep.sections.append(("Captured stdout", out))
866
+ if err:
867
+ rep.sections.append(("Captured stderr", err))
868
+ else:
869
+ rep = yield
870
+ return rep
871
+
872
+ @hookimpl(wrapper=True)
873
+ def pytest_runtest_setup(self, item: Item) -> Generator[None]:
874
+ with self.item_capture("setup", item):
875
+ return (yield)
876
+
877
+ @hookimpl(wrapper=True)
878
+ def pytest_runtest_call(self, item: Item) -> Generator[None]:
879
+ with self.item_capture("call", item):
880
+ return (yield)
881
+
882
+ @hookimpl(wrapper=True)
883
+ def pytest_runtest_teardown(self, item: Item) -> Generator[None]:
884
+ with self.item_capture("teardown", item):
885
+ return (yield)
886
+
887
+ @hookimpl(tryfirst=True)
888
+ def pytest_keyboard_interrupt(self) -> None:
889
+ self.stop_global_capturing()
890
+
891
+ @hookimpl(tryfirst=True)
892
+ def pytest_internalerror(self) -> None:
893
+ self.stop_global_capturing()
894
+
895
+
896
+ class CaptureFixture(Generic[AnyStr]):
897
+ """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
898
+ :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
899
+
900
+ def __init__(
901
+ self,
902
+ captureclass: type[CaptureBase[AnyStr]],
903
+ request: SubRequest,
904
+ *,
905
+ _ispytest: bool = False,
906
+ ) -> None:
907
+ check_ispytest(_ispytest)
908
+ self.captureclass: type[CaptureBase[AnyStr]] = captureclass
909
+ self.request = request
910
+ self._capture: MultiCapture[AnyStr] | None = None
911
+ self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
912
+ self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
913
+
914
+ def _start(self) -> None:
915
+ if self._capture is None:
916
+ self._capture = MultiCapture(
917
+ in_=None,
918
+ out=self.captureclass(1),
919
+ err=self.captureclass(2),
920
+ )
921
+ self._capture.start_capturing()
922
+
923
+ def close(self) -> None:
924
+ if self._capture is not None:
925
+ out, err = self._capture.pop_outerr_to_orig()
926
+ self._captured_out += out
927
+ self._captured_err += err
928
+ self._capture.stop_capturing()
929
+ self._capture = None
930
+
931
+ def readouterr(self) -> CaptureResult[AnyStr]:
932
+ """Read and return the captured output so far, resetting the internal
933
+ buffer.
934
+
935
+ :returns:
936
+ The captured content as a namedtuple with ``out`` and ``err``
937
+ string attributes.
938
+ """
939
+ captured_out, captured_err = self._captured_out, self._captured_err
940
+ if self._capture is not None:
941
+ out, err = self._capture.readouterr()
942
+ captured_out += out
943
+ captured_err += err
944
+ self._captured_out = self.captureclass.EMPTY_BUFFER
945
+ self._captured_err = self.captureclass.EMPTY_BUFFER
946
+ return CaptureResult(captured_out, captured_err)
947
+
948
+ def _suspend(self) -> None:
949
+ """Suspend this fixture's own capturing temporarily."""
950
+ if self._capture is not None:
951
+ self._capture.suspend_capturing()
952
+
953
+ def _resume(self) -> None:
954
+ """Resume this fixture's own capturing temporarily."""
955
+ if self._capture is not None:
956
+ self._capture.resume_capturing()
957
+
958
+ def _is_started(self) -> bool:
959
+ """Whether actively capturing -- not disabled or closed."""
960
+ if self._capture is not None:
961
+ return self._capture.is_started()
962
+ return False
963
+
964
+ @contextlib.contextmanager
965
+ def disabled(self) -> Generator[None]:
966
+ """Temporarily disable capturing while inside the ``with`` block."""
967
+ capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
968
+ "capturemanager"
969
+ )
970
+ with capmanager.global_and_fixture_disabled():
971
+ yield
972
+
973
+
974
+ # The fixtures.
975
+
976
+
977
+ @fixture
978
+ def capsys(request: SubRequest) -> Generator[CaptureFixture[str]]:
979
+ r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
980
+
981
+ The captured output is made available via ``capsys.readouterr()`` method
982
+ calls, which return a ``(out, err)`` namedtuple.
983
+ ``out`` and ``err`` will be ``text`` objects.
984
+
985
+ Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
986
+
987
+ Example:
988
+
989
+ .. code-block:: python
990
+
991
+ def test_output(capsys):
992
+ print("hello")
993
+ captured = capsys.readouterr()
994
+ assert captured.out == "hello\n"
995
+ """
996
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
997
+ capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
998
+ capman.set_fixture(capture_fixture)
999
+ capture_fixture._start()
1000
+ yield capture_fixture
1001
+ capture_fixture.close()
1002
+ capman.unset_fixture()
1003
+
1004
+
1005
+ @fixture
1006
+ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
1007
+ r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
1008
+
1009
+ The captured output is made available via ``capsysbinary.readouterr()``
1010
+ method calls, which return a ``(out, err)`` namedtuple.
1011
+ ``out`` and ``err`` will be ``bytes`` objects.
1012
+
1013
+ Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
1014
+
1015
+ Example:
1016
+
1017
+ .. code-block:: python
1018
+
1019
+ def test_output(capsysbinary):
1020
+ print("hello")
1021
+ captured = capsysbinary.readouterr()
1022
+ assert captured.out == b"hello\n"
1023
+ """
1024
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1025
+ capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
1026
+ capman.set_fixture(capture_fixture)
1027
+ capture_fixture._start()
1028
+ yield capture_fixture
1029
+ capture_fixture.close()
1030
+ capman.unset_fixture()
1031
+
1032
+
1033
+ @fixture
1034
+ def capfd(request: SubRequest) -> Generator[CaptureFixture[str]]:
1035
+ r"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
1036
+
1037
+ The captured output is made available via ``capfd.readouterr()`` method
1038
+ calls, which return a ``(out, err)`` namedtuple.
1039
+ ``out`` and ``err`` will be ``text`` objects.
1040
+
1041
+ Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
1042
+
1043
+ Example:
1044
+
1045
+ .. code-block:: python
1046
+
1047
+ def test_system_echo(capfd):
1048
+ os.system('echo "hello"')
1049
+ captured = capfd.readouterr()
1050
+ assert captured.out == "hello\n"
1051
+ """
1052
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1053
+ capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
1054
+ capman.set_fixture(capture_fixture)
1055
+ capture_fixture._start()
1056
+ yield capture_fixture
1057
+ capture_fixture.close()
1058
+ capman.unset_fixture()
1059
+
1060
+
1061
+ @fixture
1062
+ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
1063
+ r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
1064
+
1065
+ The captured output is made available via ``capfd.readouterr()`` method
1066
+ calls, which return a ``(out, err)`` namedtuple.
1067
+ ``out`` and ``err`` will be ``byte`` objects.
1068
+
1069
+ Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
1070
+
1071
+ Example:
1072
+
1073
+ .. code-block:: python
1074
+
1075
+ def test_system_echo(capfdbinary):
1076
+ os.system('echo "hello"')
1077
+ captured = capfdbinary.readouterr()
1078
+ assert captured.out == b"hello\n"
1079
+
1080
+ """
1081
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1082
+ capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
1083
+ capman.set_fixture(capture_fixture)
1084
+ capture_fixture._start()
1085
+ yield capture_fixture
1086
+ capture_fixture.close()
1087
+ capman.unset_fixture()
.venv/lib/python3.11/site-packages/_pytest/debugging.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ # ruff: noqa: T100
3
+ """Interactive debugging with PDB, the Python Debugger."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import functools
9
+ import sys
10
+ import types
11
+ from typing import Any
12
+ from typing import Callable
13
+ from typing import Generator
14
+ import unittest
15
+
16
+ from _pytest import outcomes
17
+ from _pytest._code import ExceptionInfo
18
+ from _pytest.capture import CaptureManager
19
+ from _pytest.config import Config
20
+ from _pytest.config import ConftestImportFailure
21
+ from _pytest.config import hookimpl
22
+ from _pytest.config import PytestPluginManager
23
+ from _pytest.config.argparsing import Parser
24
+ from _pytest.config.exceptions import UsageError
25
+ from _pytest.nodes import Node
26
+ from _pytest.reports import BaseReport
27
+ from _pytest.runner import CallInfo
28
+
29
+
30
+ def _validate_usepdb_cls(value: str) -> tuple[str, str]:
31
+ """Validate syntax of --pdbcls option."""
32
+ try:
33
+ modname, classname = value.split(":")
34
+ except ValueError as e:
35
+ raise argparse.ArgumentTypeError(
36
+ f"{value!r} is not in the format 'modname:classname'"
37
+ ) from e
38
+ return (modname, classname)
39
+
40
+
41
+ def pytest_addoption(parser: Parser) -> None:
42
+ group = parser.getgroup("general")
43
+ group._addoption(
44
+ "--pdb",
45
+ dest="usepdb",
46
+ action="store_true",
47
+ help="Start the interactive Python debugger on errors or KeyboardInterrupt",
48
+ )
49
+ group._addoption(
50
+ "--pdbcls",
51
+ dest="usepdb_cls",
52
+ metavar="modulename:classname",
53
+ type=_validate_usepdb_cls,
54
+ help="Specify a custom interactive Python debugger for use with --pdb."
55
+ "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
56
+ )
57
+ group._addoption(
58
+ "--trace",
59
+ dest="trace",
60
+ action="store_true",
61
+ help="Immediately break when running each test",
62
+ )
63
+
64
+
65
+ def pytest_configure(config: Config) -> None:
66
+ import pdb
67
+
68
+ if config.getvalue("trace"):
69
+ config.pluginmanager.register(PdbTrace(), "pdbtrace")
70
+ if config.getvalue("usepdb"):
71
+ config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
72
+
73
+ pytestPDB._saved.append(
74
+ (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config)
75
+ )
76
+ pdb.set_trace = pytestPDB.set_trace
77
+ pytestPDB._pluginmanager = config.pluginmanager
78
+ pytestPDB._config = config
79
+
80
+ # NOTE: not using pytest_unconfigure, since it might get called although
81
+ # pytest_configure was not (if another plugin raises UsageError).
82
+ def fin() -> None:
83
+ (
84
+ pdb.set_trace,
85
+ pytestPDB._pluginmanager,
86
+ pytestPDB._config,
87
+ ) = pytestPDB._saved.pop()
88
+
89
+ config.add_cleanup(fin)
90
+
91
+
92
+ class pytestPDB:
93
+ """Pseudo PDB that defers to the real pdb."""
94
+
95
+ _pluginmanager: PytestPluginManager | None = None
96
+ _config: Config | None = None
97
+ _saved: list[
98
+ tuple[Callable[..., None], PytestPluginManager | None, Config | None]
99
+ ] = []
100
+ _recursive_debug = 0
101
+ _wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None
102
+
103
+ @classmethod
104
+ def _is_capturing(cls, capman: CaptureManager | None) -> str | bool:
105
+ if capman:
106
+ return capman.is_capturing()
107
+ return False
108
+
109
+ @classmethod
110
+ def _import_pdb_cls(cls, capman: CaptureManager | None):
111
+ if not cls._config:
112
+ import pdb
113
+
114
+ # Happens when using pytest.set_trace outside of a test.
115
+ return pdb.Pdb
116
+
117
+ usepdb_cls = cls._config.getvalue("usepdb_cls")
118
+
119
+ if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
120
+ return cls._wrapped_pdb_cls[1]
121
+
122
+ if usepdb_cls:
123
+ modname, classname = usepdb_cls
124
+
125
+ try:
126
+ __import__(modname)
127
+ mod = sys.modules[modname]
128
+
129
+ # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
130
+ parts = classname.split(".")
131
+ pdb_cls = getattr(mod, parts[0])
132
+ for part in parts[1:]:
133
+ pdb_cls = getattr(pdb_cls, part)
134
+ except Exception as exc:
135
+ value = ":".join((modname, classname))
136
+ raise UsageError(
137
+ f"--pdbcls: could not import {value!r}: {exc}"
138
+ ) from exc
139
+ else:
140
+ import pdb
141
+
142
+ pdb_cls = pdb.Pdb
143
+
144
+ wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
145
+ cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
146
+ return wrapped_cls
147
+
148
+ @classmethod
149
+ def _get_pdb_wrapper_class(cls, pdb_cls, capman: CaptureManager | None):
150
+ import _pytest.config
151
+
152
+ class PytestPdbWrapper(pdb_cls):
153
+ _pytest_capman = capman
154
+ _continued = False
155
+
156
+ def do_debug(self, arg):
157
+ cls._recursive_debug += 1
158
+ ret = super().do_debug(arg)
159
+ cls._recursive_debug -= 1
160
+ return ret
161
+
162
+ def do_continue(self, arg):
163
+ ret = super().do_continue(arg)
164
+ if cls._recursive_debug == 0:
165
+ assert cls._config is not None
166
+ tw = _pytest.config.create_terminal_writer(cls._config)
167
+ tw.line()
168
+
169
+ capman = self._pytest_capman
170
+ capturing = pytestPDB._is_capturing(capman)
171
+ if capturing:
172
+ if capturing == "global":
173
+ tw.sep(">", "PDB continue (IO-capturing resumed)")
174
+ else:
175
+ tw.sep(
176
+ ">",
177
+ f"PDB continue (IO-capturing resumed for {capturing})",
178
+ )
179
+ assert capman is not None
180
+ capman.resume()
181
+ else:
182
+ tw.sep(">", "PDB continue")
183
+ assert cls._pluginmanager is not None
184
+ cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
185
+ self._continued = True
186
+ return ret
187
+
188
+ do_c = do_cont = do_continue
189
+
190
+ def do_quit(self, arg):
191
+ """Raise Exit outcome when quit command is used in pdb.
192
+
193
+ This is a bit of a hack - it would be better if BdbQuit
194
+ could be handled, but this would require to wrap the
195
+ whole pytest run, and adjust the report etc.
196
+ """
197
+ ret = super().do_quit(arg)
198
+
199
+ if cls._recursive_debug == 0:
200
+ outcomes.exit("Quitting debugger")
201
+
202
+ return ret
203
+
204
+ do_q = do_quit
205
+ do_exit = do_quit
206
+
207
+ def setup(self, f, tb):
208
+ """Suspend on setup().
209
+
210
+ Needed after do_continue resumed, and entering another
211
+ breakpoint again.
212
+ """
213
+ ret = super().setup(f, tb)
214
+ if not ret and self._continued:
215
+ # pdb.setup() returns True if the command wants to exit
216
+ # from the interaction: do not suspend capturing then.
217
+ if self._pytest_capman:
218
+ self._pytest_capman.suspend_global_capture(in_=True)
219
+ return ret
220
+
221
+ def get_stack(self, f, t):
222
+ stack, i = super().get_stack(f, t)
223
+ if f is None:
224
+ # Find last non-hidden frame.
225
+ i = max(0, len(stack) - 1)
226
+ while i and stack[i][0].f_locals.get("__tracebackhide__", False):
227
+ i -= 1
228
+ return stack, i
229
+
230
+ return PytestPdbWrapper
231
+
232
+ @classmethod
233
+ def _init_pdb(cls, method, *args, **kwargs):
234
+ """Initialize PDB debugging, dropping any IO capturing."""
235
+ import _pytest.config
236
+
237
+ if cls._pluginmanager is None:
238
+ capman: CaptureManager | None = None
239
+ else:
240
+ capman = cls._pluginmanager.getplugin("capturemanager")
241
+ if capman:
242
+ capman.suspend(in_=True)
243
+
244
+ if cls._config:
245
+ tw = _pytest.config.create_terminal_writer(cls._config)
246
+ tw.line()
247
+
248
+ if cls._recursive_debug == 0:
249
+ # Handle header similar to pdb.set_trace in py37+.
250
+ header = kwargs.pop("header", None)
251
+ if header is not None:
252
+ tw.sep(">", header)
253
+ else:
254
+ capturing = cls._is_capturing(capman)
255
+ if capturing == "global":
256
+ tw.sep(">", f"PDB {method} (IO-capturing turned off)")
257
+ elif capturing:
258
+ tw.sep(
259
+ ">",
260
+ f"PDB {method} (IO-capturing turned off for {capturing})",
261
+ )
262
+ else:
263
+ tw.sep(">", f"PDB {method}")
264
+
265
+ _pdb = cls._import_pdb_cls(capman)(**kwargs)
266
+
267
+ if cls._pluginmanager:
268
+ cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
269
+ return _pdb
270
+
271
+ @classmethod
272
+ def set_trace(cls, *args, **kwargs) -> None:
273
+ """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
274
+ frame = sys._getframe().f_back
275
+ _pdb = cls._init_pdb("set_trace", *args, **kwargs)
276
+ _pdb.set_trace(frame)
277
+
278
+
279
+ class PdbInvoke:
280
+ def pytest_exception_interact(
281
+ self, node: Node, call: CallInfo[Any], report: BaseReport
282
+ ) -> None:
283
+ capman = node.config.pluginmanager.getplugin("capturemanager")
284
+ if capman:
285
+ capman.suspend_global_capture(in_=True)
286
+ out, err = capman.read_global_capture()
287
+ sys.stdout.write(out)
288
+ sys.stdout.write(err)
289
+ assert call.excinfo is not None
290
+
291
+ if not isinstance(call.excinfo.value, unittest.SkipTest):
292
+ _enter_pdb(node, call.excinfo, report)
293
+
294
+ def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
295
+ tb = _postmortem_traceback(excinfo)
296
+ post_mortem(tb)
297
+
298
+
299
+ class PdbTrace:
300
+ @hookimpl(wrapper=True)
301
+ def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
302
+ wrap_pytest_function_for_tracing(pyfuncitem)
303
+ return (yield)
304
+
305
+
306
+ def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
307
+ """Change the Python function object of the given Function item by a
308
+ wrapper which actually enters pdb before calling the python function
309
+ itself, effectively leaving the user in the pdb prompt in the first
310
+ statement of the function."""
311
+ _pdb = pytestPDB._init_pdb("runcall")
312
+ testfunction = pyfuncitem.obj
313
+
314
+ # we can't just return `partial(pdb.runcall, testfunction)` because (on
315
+ # python < 3.7.4) runcall's first param is `func`, which means we'd get
316
+ # an exception if one of the kwargs to testfunction was called `func`.
317
+ @functools.wraps(testfunction)
318
+ def wrapper(*args, **kwargs) -> None:
319
+ func = functools.partial(testfunction, *args, **kwargs)
320
+ _pdb.runcall(func)
321
+
322
+ pyfuncitem.obj = wrapper
323
+
324
+
325
+ def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None:
326
+ """Wrap the given pytestfunct item for tracing support if --trace was given in
327
+ the command line."""
328
+ if pyfuncitem.config.getvalue("trace"):
329
+ wrap_pytest_function_for_tracing(pyfuncitem)
330
+
331
+
332
+ def _enter_pdb(
333
+ node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport
334
+ ) -> BaseReport:
335
+ # XXX we reuse the TerminalReporter's terminalwriter
336
+ # because this seems to avoid some encoding related troubles
337
+ # for not completely clear reasons.
338
+ tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
339
+ tw.line()
340
+
341
+ showcapture = node.config.option.showcapture
342
+
343
+ for sectionname, content in (
344
+ ("stdout", rep.capstdout),
345
+ ("stderr", rep.capstderr),
346
+ ("log", rep.caplog),
347
+ ):
348
+ if showcapture in (sectionname, "all") and content:
349
+ tw.sep(">", "captured " + sectionname)
350
+ if content[-1:] == "\n":
351
+ content = content[:-1]
352
+ tw.line(content)
353
+
354
+ tw.sep(">", "traceback")
355
+ rep.toterminal(tw)
356
+ tw.sep(">", "entering PDB")
357
+ tb = _postmortem_traceback(excinfo)
358
+ rep._pdbshown = True # type: ignore[attr-defined]
359
+ post_mortem(tb)
360
+ return rep
361
+
362
+
363
+ def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType:
364
+ from doctest import UnexpectedException
365
+
366
+ if isinstance(excinfo.value, UnexpectedException):
367
+ # A doctest.UnexpectedException is not useful for post_mortem.
368
+ # Use the underlying exception instead:
369
+ return excinfo.value.exc_info[2]
370
+ elif isinstance(excinfo.value, ConftestImportFailure):
371
+ # A config.ConftestImportFailure is not useful for post_mortem.
372
+ # Use the underlying exception instead:
373
+ assert excinfo.value.cause.__traceback__ is not None
374
+ return excinfo.value.cause.__traceback__
375
+ else:
376
+ assert excinfo._excinfo is not None
377
+ return excinfo._excinfo[2]
378
+
379
+
380
+ def post_mortem(t: types.TracebackType) -> None:
381
+ p = pytestPDB._init_pdb("post_mortem")
382
+ p.reset()
383
+ p.interaction(None, t)
384
+ if p.quitting:
385
+ outcomes.exit("Quitting debugger")
.venv/lib/python3.11/site-packages/_pytest/deprecated.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deprecation messages and bits of code used elsewhere in the codebase that
2
+ is planned to be removed in the next pytest release.
3
+
4
+ Keeping it in a central location makes it easy to track what is deprecated and should
5
+ be removed when the time comes.
6
+
7
+ All constants defined in this module should be either instances of
8
+ :class:`PytestWarning`, or :class:`UnformattedWarning`
9
+ in case of warnings which need to format their messages.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from warnings import warn
15
+
16
+ from _pytest.warning_types import PytestDeprecationWarning
17
+ from _pytest.warning_types import PytestRemovedIn9Warning
18
+ from _pytest.warning_types import UnformattedWarning
19
+
20
+
21
+ # set of plugins which have been integrated into the core; we use this list to ignore
22
+ # them during registration to avoid conflicts
23
+ DEPRECATED_EXTERNAL_PLUGINS = {
24
+ "pytest_catchlog",
25
+ "pytest_capturelog",
26
+ "pytest_faulthandler",
27
+ }
28
+
29
+
30
+ # This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
31
+ # * If you're in the future: "could have been".
32
+ YIELD_FIXTURE = PytestDeprecationWarning(
33
+ "@pytest.yield_fixture is deprecated.\n"
34
+ "Use @pytest.fixture instead; they are the same."
35
+ )
36
+
37
+ # This deprecation is never really meant to be removed.
38
+ PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
39
+
40
+
41
+ HOOK_LEGACY_PATH_ARG = UnformattedWarning(
42
+ PytestRemovedIn9Warning,
43
+ "The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n"
44
+ "see https://docs.pytest.org/en/latest/deprecations.html"
45
+ "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
46
+ )
47
+
48
+ NODE_CTOR_FSPATH_ARG = UnformattedWarning(
49
+ PytestRemovedIn9Warning,
50
+ "The (fspath: py.path.local) argument to {node_type_name} is deprecated. "
51
+ "Please use the (path: pathlib.Path) argument instead.\n"
52
+ "See https://docs.pytest.org/en/latest/deprecations.html"
53
+ "#fspath-argument-for-node-constructors-replaced-with-pathlib-path",
54
+ )
55
+
56
+ HOOK_LEGACY_MARKING = UnformattedWarning(
57
+ PytestDeprecationWarning,
58
+ "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
59
+ "Please use the pytest.hook{type}({hook_opts}) decorator instead\n"
60
+ " to configure the hooks.\n"
61
+ " See https://docs.pytest.org/en/latest/deprecations.html"
62
+ "#configuring-hook-specs-impls-using-markers",
63
+ )
64
+
65
+ MARKED_FIXTURE = PytestRemovedIn9Warning(
66
+ "Marks applied to fixtures have no effect\n"
67
+ "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
68
+ )
69
+
70
+ # You want to make some `__init__` or function "private".
71
+ #
72
+ # def my_private_function(some, args):
73
+ # ...
74
+ #
75
+ # Do this:
76
+ #
77
+ # def my_private_function(some, args, *, _ispytest: bool = False):
78
+ # check_ispytest(_ispytest)
79
+ # ...
80
+ #
81
+ # Change all internal/allowed calls to
82
+ #
83
+ # my_private_function(some, args, _ispytest=True)
84
+ #
85
+ # All other calls will get the default _ispytest=False and trigger
86
+ # the warning (possibly error in the future).
87
+
88
+
89
+ def check_ispytest(ispytest: bool) -> None:
90
+ if not ispytest:
91
+ warn(PRIVATE, stacklevel=3)
.venv/lib/python3.11/site-packages/_pytest/faulthandler.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import Generator
6
+
7
+ from _pytest.config import Config
8
+ from _pytest.config.argparsing import Parser
9
+ from _pytest.nodes import Item
10
+ from _pytest.stash import StashKey
11
+ import pytest
12
+
13
+
14
+ fault_handler_original_stderr_fd_key = StashKey[int]()
15
+ fault_handler_stderr_fd_key = StashKey[int]()
16
+
17
+
18
+ def pytest_addoption(parser: Parser) -> None:
19
+ help = (
20
+ "Dump the traceback of all threads if a test takes "
21
+ "more than TIMEOUT seconds to finish"
22
+ )
23
+ parser.addini("faulthandler_timeout", help, default=0.0)
24
+
25
+
26
+ def pytest_configure(config: Config) -> None:
27
+ import faulthandler
28
+
29
+ # at teardown we want to restore the original faulthandler fileno
30
+ # but faulthandler has no api to return the original fileno
31
+ # so here we stash the stderr fileno to be used at teardown
32
+ # sys.stderr and sys.__stderr__ may be closed or patched during the session
33
+ # so we can't rely on their values being good at that point (#11572).
34
+ stderr_fileno = get_stderr_fileno()
35
+ if faulthandler.is_enabled():
36
+ config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno
37
+ config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno)
38
+ faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
39
+
40
+
41
+ def pytest_unconfigure(config: Config) -> None:
42
+ import faulthandler
43
+
44
+ faulthandler.disable()
45
+ # Close the dup file installed during pytest_configure.
46
+ if fault_handler_stderr_fd_key in config.stash:
47
+ os.close(config.stash[fault_handler_stderr_fd_key])
48
+ del config.stash[fault_handler_stderr_fd_key]
49
+ # Re-enable the faulthandler if it was originally enabled.
50
+ if fault_handler_original_stderr_fd_key in config.stash:
51
+ faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
52
+ del config.stash[fault_handler_original_stderr_fd_key]
53
+
54
+
55
+ def get_stderr_fileno() -> int:
56
+ try:
57
+ fileno = sys.stderr.fileno()
58
+ # The Twisted Logger will return an invalid file descriptor since it is not backed
59
+ # by an FD. So, let's also forward this to the same code path as with pytest-xdist.
60
+ if fileno == -1:
61
+ raise AttributeError()
62
+ return fileno
63
+ except (AttributeError, ValueError):
64
+ # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
65
+ # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
66
+ # This is potentially dangerous, but the best we can do.
67
+ assert sys.__stderr__ is not None
68
+ return sys.__stderr__.fileno()
69
+
70
+
71
+ def get_timeout_config_value(config: Config) -> float:
72
+ return float(config.getini("faulthandler_timeout") or 0.0)
73
+
74
+
75
+ @pytest.hookimpl(wrapper=True, trylast=True)
76
+ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
77
+ timeout = get_timeout_config_value(item.config)
78
+ if timeout > 0:
79
+ import faulthandler
80
+
81
+ stderr = item.config.stash[fault_handler_stderr_fd_key]
82
+ faulthandler.dump_traceback_later(timeout, file=stderr)
83
+ try:
84
+ return (yield)
85
+ finally:
86
+ faulthandler.cancel_dump_traceback_later()
87
+ else:
88
+ return (yield)
89
+
90
+
91
+ @pytest.hookimpl(tryfirst=True)
92
+ def pytest_enter_pdb() -> None:
93
+ """Cancel any traceback dumping due to timeout before entering pdb."""
94
+ import faulthandler
95
+
96
+ faulthandler.cancel_dump_traceback_later()
97
+
98
+
99
+ @pytest.hookimpl(tryfirst=True)
100
+ def pytest_exception_interact() -> None:
101
+ """Cancel any traceback dumping due to an interactive exception being
102
+ raised."""
103
+ import faulthandler
104
+
105
+ faulthandler.cancel_dump_traceback_later()
.venv/lib/python3.11/site-packages/_pytest/fixtures.py ADDED
@@ -0,0 +1,1932 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import abc
5
+ from collections import defaultdict
6
+ from collections import deque
7
+ import dataclasses
8
+ import functools
9
+ import inspect
10
+ import os
11
+ from pathlib import Path
12
+ import sys
13
+ import types
14
+ from typing import AbstractSet
15
+ from typing import Any
16
+ from typing import Callable
17
+ from typing import cast
18
+ from typing import Dict
19
+ from typing import Final
20
+ from typing import final
21
+ from typing import Generator
22
+ from typing import Generic
23
+ from typing import Iterable
24
+ from typing import Iterator
25
+ from typing import Mapping
26
+ from typing import MutableMapping
27
+ from typing import NoReturn
28
+ from typing import Optional
29
+ from typing import OrderedDict
30
+ from typing import overload
31
+ from typing import Sequence
32
+ from typing import Tuple
33
+ from typing import TYPE_CHECKING
34
+ from typing import TypeVar
35
+ from typing import Union
36
+ import warnings
37
+
38
+ import _pytest
39
+ from _pytest import nodes
40
+ from _pytest._code import getfslineno
41
+ from _pytest._code import Source
42
+ from _pytest._code.code import FormattedExcinfo
43
+ from _pytest._code.code import TerminalRepr
44
+ from _pytest._io import TerminalWriter
45
+ from _pytest.compat import _PytestWrapper
46
+ from _pytest.compat import assert_never
47
+ from _pytest.compat import get_real_func
48
+ from _pytest.compat import get_real_method
49
+ from _pytest.compat import getfuncargnames
50
+ from _pytest.compat import getimfunc
51
+ from _pytest.compat import getlocation
52
+ from _pytest.compat import is_generator
53
+ from _pytest.compat import NOTSET
54
+ from _pytest.compat import NotSetType
55
+ from _pytest.compat import safe_getattr
56
+ from _pytest.compat import safe_isclass
57
+ from _pytest.config import _PluggyPlugin
58
+ from _pytest.config import Config
59
+ from _pytest.config import ExitCode
60
+ from _pytest.config.argparsing import Parser
61
+ from _pytest.deprecated import check_ispytest
62
+ from _pytest.deprecated import MARKED_FIXTURE
63
+ from _pytest.deprecated import YIELD_FIXTURE
64
+ from _pytest.main import Session
65
+ from _pytest.mark import Mark
66
+ from _pytest.mark import ParameterSet
67
+ from _pytest.mark.structures import MarkDecorator
68
+ from _pytest.outcomes import fail
69
+ from _pytest.outcomes import skip
70
+ from _pytest.outcomes import TEST_OUTCOME
71
+ from _pytest.pathlib import absolutepath
72
+ from _pytest.pathlib import bestrelpath
73
+ from _pytest.scope import _ScopeName
74
+ from _pytest.scope import HIGH_SCOPES
75
+ from _pytest.scope import Scope
76
+
77
+
78
+ if sys.version_info < (3, 11):
79
+ from exceptiongroup import BaseExceptionGroup
80
+
81
+
82
+ if TYPE_CHECKING:
83
+ from _pytest.python import CallSpec2
84
+ from _pytest.python import Function
85
+ from _pytest.python import Metafunc
86
+
87
+
88
+ # The value of the fixture -- return/yield of the fixture function (type variable).
89
+ FixtureValue = TypeVar("FixtureValue")
90
+ # The type of the fixture function (type variable).
91
+ FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object])
92
+ # The type of a fixture function (type alias generic in fixture value).
93
+ _FixtureFunc = Union[
94
+ Callable[..., FixtureValue], Callable[..., Generator[FixtureValue, None, None]]
95
+ ]
96
+ # The type of FixtureDef.cached_result (type alias generic in fixture value).
97
+ _FixtureCachedResult = Union[
98
+ Tuple[
99
+ # The result.
100
+ FixtureValue,
101
+ # Cache key.
102
+ object,
103
+ None,
104
+ ],
105
+ Tuple[
106
+ None,
107
+ # Cache key.
108
+ object,
109
+ # The exception and the original traceback.
110
+ Tuple[BaseException, Optional[types.TracebackType]],
111
+ ],
112
+ ]
113
+
114
+
115
+ @dataclasses.dataclass(frozen=True)
116
+ class PseudoFixtureDef(Generic[FixtureValue]):
117
+ cached_result: _FixtureCachedResult[FixtureValue]
118
+ _scope: Scope
119
+
120
+
121
+ def pytest_sessionstart(session: Session) -> None:
122
+ session._fixturemanager = FixtureManager(session)
123
+
124
+
125
+ def get_scope_package(
126
+ node: nodes.Item,
127
+ fixturedef: FixtureDef[object],
128
+ ) -> nodes.Node | None:
129
+ from _pytest.python import Package
130
+
131
+ for parent in node.iter_parents():
132
+ if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid:
133
+ return parent
134
+ return node.session
135
+
136
+
137
+ def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None:
138
+ import _pytest.python
139
+
140
+ if scope is Scope.Function:
141
+ # Type ignored because this is actually safe, see:
142
+ # https://github.com/python/mypy/issues/4717
143
+ return node.getparent(nodes.Item) # type: ignore[type-abstract]
144
+ elif scope is Scope.Class:
145
+ return node.getparent(_pytest.python.Class)
146
+ elif scope is Scope.Module:
147
+ return node.getparent(_pytest.python.Module)
148
+ elif scope is Scope.Package:
149
+ return node.getparent(_pytest.python.Package)
150
+ elif scope is Scope.Session:
151
+ return node.getparent(_pytest.main.Session)
152
+ else:
153
+ assert_never(scope)
154
+
155
+
156
+ def getfixturemarker(obj: object) -> FixtureFunctionMarker | None:
157
+ """Return fixturemarker or None if it doesn't exist or raised
158
+ exceptions."""
159
+ return cast(
160
+ Optional[FixtureFunctionMarker],
161
+ safe_getattr(obj, "_pytestfixturefunction", None),
162
+ )
163
+
164
+
165
+ # Algorithm for sorting on a per-parametrized resource setup basis.
166
+ # It is called for Session scope first and performs sorting
167
+ # down to the lower scopes such as to minimize number of "high scope"
168
+ # setups and teardowns.
169
+
170
+
171
+ @dataclasses.dataclass(frozen=True)
172
+ class FixtureArgKey:
173
+ argname: str
174
+ param_index: int
175
+ scoped_item_path: Path | None
176
+ item_cls: type | None
177
+
178
+
179
+ _V = TypeVar("_V")
180
+ OrderedSet = Dict[_V, None]
181
+
182
+
183
+ def get_parametrized_fixture_argkeys(
184
+ item: nodes.Item, scope: Scope
185
+ ) -> Iterator[FixtureArgKey]:
186
+ """Return list of keys for all parametrized arguments which match
187
+ the specified scope."""
188
+ assert scope is not Scope.Function
189
+
190
+ try:
191
+ callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
192
+ except AttributeError:
193
+ return
194
+
195
+ item_cls = None
196
+ if scope is Scope.Session:
197
+ scoped_item_path = None
198
+ elif scope is Scope.Package:
199
+ # Package key = module's directory.
200
+ scoped_item_path = item.path.parent
201
+ elif scope is Scope.Module:
202
+ scoped_item_path = item.path
203
+ elif scope is Scope.Class:
204
+ scoped_item_path = item.path
205
+ item_cls = item.cls # type: ignore[attr-defined]
206
+ else:
207
+ assert_never(scope)
208
+
209
+ for argname in callspec.indices:
210
+ if callspec._arg2scope[argname] != scope:
211
+ continue
212
+ param_index = callspec.indices[argname]
213
+ yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
214
+
215
+
216
+ def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]:
217
+ argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {}
218
+ items_by_argkey: dict[
219
+ Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
220
+ ] = {}
221
+ for scope in HIGH_SCOPES:
222
+ scoped_argkeys_by_item = argkeys_by_item[scope] = {}
223
+ scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
224
+ for item in items:
225
+ argkeys = dict.fromkeys(get_parametrized_fixture_argkeys(item, scope))
226
+ if argkeys:
227
+ scoped_argkeys_by_item[item] = argkeys
228
+ for argkey in argkeys:
229
+ scoped_items_by_argkey[argkey][item] = None
230
+
231
+ items_set = dict.fromkeys(items)
232
+ return list(
233
+ reorder_items_atscope(
234
+ items_set, argkeys_by_item, items_by_argkey, Scope.Session
235
+ )
236
+ )
237
+
238
+
239
+ def reorder_items_atscope(
240
+ items: OrderedSet[nodes.Item],
241
+ argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[FixtureArgKey]]],
242
+ items_by_argkey: Mapping[
243
+ Scope, Mapping[FixtureArgKey, OrderedDict[nodes.Item, None]]
244
+ ],
245
+ scope: Scope,
246
+ ) -> OrderedSet[nodes.Item]:
247
+ if scope is Scope.Function or len(items) < 3:
248
+ return items
249
+
250
+ scoped_items_by_argkey = items_by_argkey[scope]
251
+ scoped_argkeys_by_item = argkeys_by_item[scope]
252
+
253
+ ignore: set[FixtureArgKey] = set()
254
+ items_deque = deque(items)
255
+ items_done: OrderedSet[nodes.Item] = {}
256
+ while items_deque:
257
+ no_argkey_items: OrderedSet[nodes.Item] = {}
258
+ slicing_argkey = None
259
+ while items_deque:
260
+ item = items_deque.popleft()
261
+ if item in items_done or item in no_argkey_items:
262
+ continue
263
+ argkeys = dict.fromkeys(
264
+ k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore
265
+ )
266
+ if not argkeys:
267
+ no_argkey_items[item] = None
268
+ else:
269
+ slicing_argkey, _ = argkeys.popitem()
270
+ # We don't have to remove relevant items from later in the
271
+ # deque because they'll just be ignored.
272
+ matching_items = [
273
+ i for i in scoped_items_by_argkey[slicing_argkey] if i in items
274
+ ]
275
+ for i in reversed(matching_items):
276
+ items_deque.appendleft(i)
277
+ # Fix items_by_argkey order.
278
+ for other_scope in HIGH_SCOPES:
279
+ other_scoped_items_by_argkey = items_by_argkey[other_scope]
280
+ for argkey in argkeys_by_item[other_scope].get(i, ()):
281
+ other_scoped_items_by_argkey[argkey][i] = None
282
+ other_scoped_items_by_argkey[argkey].move_to_end(
283
+ i, last=False
284
+ )
285
+ break
286
+ if no_argkey_items:
287
+ reordered_no_argkey_items = reorder_items_atscope(
288
+ no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower()
289
+ )
290
+ items_done.update(reordered_no_argkey_items)
291
+ if slicing_argkey is not None:
292
+ ignore.add(slicing_argkey)
293
+ return items_done
294
+
295
+
296
+ @dataclasses.dataclass(frozen=True)
297
+ class FuncFixtureInfo:
298
+ """Fixture-related information for a fixture-requesting item (e.g. test
299
+ function).
300
+
301
+ This is used to examine the fixtures which an item requests statically
302
+ (known during collection). This includes autouse fixtures, fixtures
303
+ requested by the `usefixtures` marker, fixtures requested in the function
304
+ parameters, and the transitive closure of these.
305
+
306
+ An item may also request fixtures dynamically (using `request.getfixturevalue`);
307
+ these are not reflected here.
308
+ """
309
+
310
+ __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
311
+
312
+ # Fixture names that the item requests directly by function parameters.
313
+ argnames: tuple[str, ...]
314
+ # Fixture names that the item immediately requires. These include
315
+ # argnames + fixture names specified via usefixtures and via autouse=True in
316
+ # fixture definitions.
317
+ initialnames: tuple[str, ...]
318
+ # The transitive closure of the fixture names that the item requires.
319
+ # Note: can't include dynamic dependencies (`request.getfixturevalue` calls).
320
+ names_closure: list[str]
321
+ # A map from a fixture name in the transitive closure to the FixtureDefs
322
+ # matching the name which are applicable to this function.
323
+ # There may be multiple overriding fixtures with the same name. The
324
+ # sequence is ordered from furthest to closes to the function.
325
+ name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]]
326
+
327
+ def prune_dependency_tree(self) -> None:
328
+ """Recompute names_closure from initialnames and name2fixturedefs.
329
+
330
+ Can only reduce names_closure, which means that the new closure will
331
+ always be a subset of the old one. The order is preserved.
332
+
333
+ This method is needed because direct parametrization may shadow some
334
+ of the fixtures that were included in the originally built dependency
335
+ tree. In this way the dependency tree can get pruned, and the closure
336
+ of argnames may get reduced.
337
+ """
338
+ closure: set[str] = set()
339
+ working_set = set(self.initialnames)
340
+ while working_set:
341
+ argname = working_set.pop()
342
+ # Argname may be something not included in the original names_closure,
343
+ # in which case we ignore it. This currently happens with pseudo
344
+ # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
345
+ # So they introduce the new dependency 'request' which might have
346
+ # been missing in the original tree (closure).
347
+ if argname not in closure and argname in self.names_closure:
348
+ closure.add(argname)
349
+ if argname in self.name2fixturedefs:
350
+ working_set.update(self.name2fixturedefs[argname][-1].argnames)
351
+
352
+ self.names_closure[:] = sorted(closure, key=self.names_closure.index)
353
+
354
+
355
+ class FixtureRequest(abc.ABC):
356
+ """The type of the ``request`` fixture.
357
+
358
+ A request object gives access to the requesting test context and has a
359
+ ``param`` attribute in case the fixture is parametrized.
360
+ """
361
+
362
+ def __init__(
363
+ self,
364
+ pyfuncitem: Function,
365
+ fixturename: str | None,
366
+ arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]],
367
+ fixture_defs: dict[str, FixtureDef[Any]],
368
+ *,
369
+ _ispytest: bool = False,
370
+ ) -> None:
371
+ check_ispytest(_ispytest)
372
+ #: Fixture for which this request is being performed.
373
+ self.fixturename: Final = fixturename
374
+ self._pyfuncitem: Final = pyfuncitem
375
+ # The FixtureDefs for each fixture name requested by this item.
376
+ # Starts from the statically-known fixturedefs resolved during
377
+ # collection. Dynamically requested fixtures (using
378
+ # `request.getfixturevalue("foo")`) are added dynamically.
379
+ self._arg2fixturedefs: Final = arg2fixturedefs
380
+ # The evaluated argnames so far, mapping to the FixtureDef they resolved
381
+ # to.
382
+ self._fixture_defs: Final = fixture_defs
383
+ # Notes on the type of `param`:
384
+ # -`request.param` is only defined in parametrized fixtures, and will raise
385
+ # AttributeError otherwise. Python typing has no notion of "undefined", so
386
+ # this cannot be reflected in the type.
387
+ # - Technically `param` is only (possibly) defined on SubRequest, not
388
+ # FixtureRequest, but the typing of that is still in flux so this cheats.
389
+ # - In the future we might consider using a generic for the param type, but
390
+ # for now just using Any.
391
+ self.param: Any
392
+
393
+ @property
394
+ def _fixturemanager(self) -> FixtureManager:
395
+ return self._pyfuncitem.session._fixturemanager
396
+
397
+ @property
398
+ @abc.abstractmethod
399
+ def _scope(self) -> Scope:
400
+ raise NotImplementedError()
401
+
402
+ @property
403
+ def scope(self) -> _ScopeName:
404
+ """Scope string, one of "function", "class", "module", "package", "session"."""
405
+ return self._scope.value
406
+
407
+ @abc.abstractmethod
408
+ def _check_scope(
409
+ self,
410
+ requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
411
+ requested_scope: Scope,
412
+ ) -> None:
413
+ raise NotImplementedError()
414
+
415
+ @property
416
+ def fixturenames(self) -> list[str]:
417
+ """Names of all active fixtures in this request."""
418
+ result = list(self._pyfuncitem.fixturenames)
419
+ result.extend(set(self._fixture_defs).difference(result))
420
+ return result
421
+
422
+ @property
423
+ @abc.abstractmethod
424
+ def node(self):
425
+ """Underlying collection node (depends on current request scope)."""
426
+ raise NotImplementedError()
427
+
428
+ @property
429
+ def config(self) -> Config:
430
+ """The pytest config object associated with this request."""
431
+ return self._pyfuncitem.config
432
+
433
+ @property
434
+ def function(self):
435
+ """Test function object if the request has a per-function scope."""
436
+ if self.scope != "function":
437
+ raise AttributeError(
438
+ f"function not available in {self.scope}-scoped context"
439
+ )
440
+ return self._pyfuncitem.obj
441
+
442
+ @property
443
+ def cls(self):
444
+ """Class (can be None) where the test function was collected."""
445
+ if self.scope not in ("class", "function"):
446
+ raise AttributeError(f"cls not available in {self.scope}-scoped context")
447
+ clscol = self._pyfuncitem.getparent(_pytest.python.Class)
448
+ if clscol:
449
+ return clscol.obj
450
+
451
+ @property
452
+ def instance(self):
453
+ """Instance (can be None) on which test function was collected."""
454
+ if self.scope != "function":
455
+ return None
456
+ return getattr(self._pyfuncitem, "instance", None)
457
+
458
+ @property
459
+ def module(self):
460
+ """Python module object where the test function was collected."""
461
+ if self.scope not in ("function", "class", "module"):
462
+ raise AttributeError(f"module not available in {self.scope}-scoped context")
463
+ mod = self._pyfuncitem.getparent(_pytest.python.Module)
464
+ assert mod is not None
465
+ return mod.obj
466
+
467
+ @property
468
+ def path(self) -> Path:
469
+ """Path where the test function was collected."""
470
+ if self.scope not in ("function", "class", "module", "package"):
471
+ raise AttributeError(f"path not available in {self.scope}-scoped context")
472
+ return self._pyfuncitem.path
473
+
474
+ @property
475
+ def keywords(self) -> MutableMapping[str, Any]:
476
+ """Keywords/markers dictionary for the underlying node."""
477
+ node: nodes.Node = self.node
478
+ return node.keywords
479
+
480
+ @property
481
+ def session(self) -> Session:
482
+ """Pytest session object."""
483
+ return self._pyfuncitem.session
484
+
485
+ @abc.abstractmethod
486
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
487
+ """Add finalizer/teardown function to be called without arguments after
488
+ the last test within the requesting test context finished execution."""
489
+ raise NotImplementedError()
490
+
491
+ def applymarker(self, marker: str | MarkDecorator) -> None:
492
+ """Apply a marker to a single test function invocation.
493
+
494
+ This method is useful if you don't want to have a keyword/marker
495
+ on all function invocations.
496
+
497
+ :param marker:
498
+ An object created by a call to ``pytest.mark.NAME(...)``.
499
+ """
500
+ self.node.add_marker(marker)
501
+
502
+ def raiseerror(self, msg: str | None) -> NoReturn:
503
+ """Raise a FixtureLookupError exception.
504
+
505
+ :param msg:
506
+ An optional custom error message.
507
+ """
508
+ raise FixtureLookupError(None, self, msg)
509
+
510
+ def getfixturevalue(self, argname: str) -> Any:
511
+ """Dynamically run a named fixture function.
512
+
513
+ Declaring fixtures via function argument is recommended where possible.
514
+ But if you can only decide whether to use another fixture at test
515
+ setup time, you may use this function to retrieve it inside a fixture
516
+ or test function body.
517
+
518
+ This method can be used during the test setup phase or the test run
519
+ phase, but during the test teardown phase a fixture's value may not
520
+ be available.
521
+
522
+ :param argname:
523
+ The fixture name.
524
+ :raises pytest.FixtureLookupError:
525
+ If the given fixture could not be found.
526
+ """
527
+ # Note that in addition to the use case described in the docstring,
528
+ # getfixturevalue() is also called by pytest itself during item and fixture
529
+ # setup to evaluate the fixtures that are requested statically
530
+ # (using function parameters, autouse, etc).
531
+
532
+ fixturedef = self._get_active_fixturedef(argname)
533
+ assert fixturedef.cached_result is not None, (
534
+ f'The fixture value for "{argname}" is not available. '
535
+ "This can happen when the fixture has already been torn down."
536
+ )
537
+ return fixturedef.cached_result[0]
538
+
539
+ def _iter_chain(self) -> Iterator[SubRequest]:
540
+ """Yield all SubRequests in the chain, from self up.
541
+
542
+ Note: does *not* yield the TopRequest.
543
+ """
544
+ current = self
545
+ while isinstance(current, SubRequest):
546
+ yield current
547
+ current = current._parent_request
548
+
549
+ def _get_active_fixturedef(
550
+ self, argname: str
551
+ ) -> FixtureDef[object] | PseudoFixtureDef[object]:
552
+ if argname == "request":
553
+ cached_result = (self, [0], None)
554
+ return PseudoFixtureDef(cached_result, Scope.Function)
555
+
556
+ # If we already finished computing a fixture by this name in this item,
557
+ # return it.
558
+ fixturedef = self._fixture_defs.get(argname)
559
+ if fixturedef is not None:
560
+ self._check_scope(fixturedef, fixturedef._scope)
561
+ return fixturedef
562
+
563
+ # Find the appropriate fixturedef.
564
+ fixturedefs = self._arg2fixturedefs.get(argname, None)
565
+ if fixturedefs is None:
566
+ # We arrive here because of a dynamic call to
567
+ # getfixturevalue(argname) which was naturally
568
+ # not known at parsing/collection time.
569
+ fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
570
+ if fixturedefs is not None:
571
+ self._arg2fixturedefs[argname] = fixturedefs
572
+ # No fixtures defined with this name.
573
+ if fixturedefs is None:
574
+ raise FixtureLookupError(argname, self)
575
+ # The are no fixtures with this name applicable for the function.
576
+ if not fixturedefs:
577
+ raise FixtureLookupError(argname, self)
578
+ # A fixture may override another fixture with the same name, e.g. a
579
+ # fixture in a module can override a fixture in a conftest, a fixture in
580
+ # a class can override a fixture in the module, and so on.
581
+ # An overriding fixture can request its own name (possibly indirectly);
582
+ # in this case it gets the value of the fixture it overrides, one level
583
+ # up.
584
+ # Check how many `argname`s deep we are, and take the next one.
585
+ # `fixturedefs` is sorted from furthest to closest, so use negative
586
+ # indexing to go in reverse.
587
+ index = -1
588
+ for request in self._iter_chain():
589
+ if request.fixturename == argname:
590
+ index -= 1
591
+ # If already consumed all of the available levels, fail.
592
+ if -index > len(fixturedefs):
593
+ raise FixtureLookupError(argname, self)
594
+ fixturedef = fixturedefs[index]
595
+
596
+ # Prepare a SubRequest object for calling the fixture.
597
+ try:
598
+ callspec = self._pyfuncitem.callspec
599
+ except AttributeError:
600
+ callspec = None
601
+ if callspec is not None and argname in callspec.params:
602
+ param = callspec.params[argname]
603
+ param_index = callspec.indices[argname]
604
+ # The parametrize invocation scope overrides the fixture's scope.
605
+ scope = callspec._arg2scope[argname]
606
+ else:
607
+ param = NOTSET
608
+ param_index = 0
609
+ scope = fixturedef._scope
610
+ self._check_fixturedef_without_param(fixturedef)
611
+ self._check_scope(fixturedef, scope)
612
+ subrequest = SubRequest(
613
+ self, scope, param, param_index, fixturedef, _ispytest=True
614
+ )
615
+
616
+ # Make sure the fixture value is cached, running it if it isn't
617
+ fixturedef.execute(request=subrequest)
618
+
619
+ self._fixture_defs[argname] = fixturedef
620
+ return fixturedef
621
+
622
+ def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None:
623
+ """Check that this request is allowed to execute this fixturedef without
624
+ a param."""
625
+ funcitem = self._pyfuncitem
626
+ has_params = fixturedef.params is not None
627
+ fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
628
+ if has_params and fixtures_not_supported:
629
+ msg = (
630
+ f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
631
+ f"Node id: {funcitem.nodeid}\n"
632
+ f"Function type: {type(funcitem).__name__}"
633
+ )
634
+ fail(msg, pytrace=False)
635
+ if has_params:
636
+ frame = inspect.stack()[3]
637
+ frameinfo = inspect.getframeinfo(frame[0])
638
+ source_path = absolutepath(frameinfo.filename)
639
+ source_lineno = frameinfo.lineno
640
+ try:
641
+ source_path_str = str(source_path.relative_to(funcitem.config.rootpath))
642
+ except ValueError:
643
+ source_path_str = str(source_path)
644
+ location = getlocation(fixturedef.func, funcitem.config.rootpath)
645
+ msg = (
646
+ "The requested fixture has no parameter defined for test:\n"
647
+ f" {funcitem.nodeid}\n\n"
648
+ f"Requested fixture '{fixturedef.argname}' defined in:\n"
649
+ f"{location}\n\n"
650
+ f"Requested here:\n"
651
+ f"{source_path_str}:{source_lineno}"
652
+ )
653
+ fail(msg, pytrace=False)
654
+
655
+ def _get_fixturestack(self) -> list[FixtureDef[Any]]:
656
+ values = [request._fixturedef for request in self._iter_chain()]
657
+ values.reverse()
658
+ return values
659
+
660
+
661
+ @final
662
+ class TopRequest(FixtureRequest):
663
+ """The type of the ``request`` fixture in a test function."""
664
+
665
+ def __init__(self, pyfuncitem: Function, *, _ispytest: bool = False) -> None:
666
+ super().__init__(
667
+ fixturename=None,
668
+ pyfuncitem=pyfuncitem,
669
+ arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
670
+ fixture_defs={},
671
+ _ispytest=_ispytest,
672
+ )
673
+
674
+ @property
675
+ def _scope(self) -> Scope:
676
+ return Scope.Function
677
+
678
+ def _check_scope(
679
+ self,
680
+ requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
681
+ requested_scope: Scope,
682
+ ) -> None:
683
+ # TopRequest always has function scope so always valid.
684
+ pass
685
+
686
+ @property
687
+ def node(self):
688
+ return self._pyfuncitem
689
+
690
+ def __repr__(self) -> str:
691
+ return f"<FixtureRequest for {self.node!r}>"
692
+
693
+ def _fillfixtures(self) -> None:
694
+ item = self._pyfuncitem
695
+ for argname in item.fixturenames:
696
+ if argname not in item.funcargs:
697
+ item.funcargs[argname] = self.getfixturevalue(argname)
698
+
699
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
700
+ self.node.addfinalizer(finalizer)
701
+
702
+
703
+ @final
704
+ class SubRequest(FixtureRequest):
705
+ """The type of the ``request`` fixture in a fixture function requested
706
+ (transitively) by a test function."""
707
+
708
+ def __init__(
709
+ self,
710
+ request: FixtureRequest,
711
+ scope: Scope,
712
+ param: Any,
713
+ param_index: int,
714
+ fixturedef: FixtureDef[object],
715
+ *,
716
+ _ispytest: bool = False,
717
+ ) -> None:
718
+ super().__init__(
719
+ pyfuncitem=request._pyfuncitem,
720
+ fixturename=fixturedef.argname,
721
+ fixture_defs=request._fixture_defs,
722
+ arg2fixturedefs=request._arg2fixturedefs,
723
+ _ispytest=_ispytest,
724
+ )
725
+ self._parent_request: Final[FixtureRequest] = request
726
+ self._scope_field: Final = scope
727
+ self._fixturedef: Final[FixtureDef[object]] = fixturedef
728
+ if param is not NOTSET:
729
+ self.param = param
730
+ self.param_index: Final = param_index
731
+
732
+ def __repr__(self) -> str:
733
+ return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
734
+
735
+ @property
736
+ def _scope(self) -> Scope:
737
+ return self._scope_field
738
+
739
+ @property
740
+ def node(self):
741
+ scope = self._scope
742
+ if scope is Scope.Function:
743
+ # This might also be a non-function Item despite its attribute name.
744
+ node: nodes.Node | None = self._pyfuncitem
745
+ elif scope is Scope.Package:
746
+ node = get_scope_package(self._pyfuncitem, self._fixturedef)
747
+ else:
748
+ node = get_scope_node(self._pyfuncitem, scope)
749
+ if node is None and scope is Scope.Class:
750
+ # Fallback to function item itself.
751
+ node = self._pyfuncitem
752
+ assert node, f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}'
753
+ return node
754
+
755
+ def _check_scope(
756
+ self,
757
+ requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
758
+ requested_scope: Scope,
759
+ ) -> None:
760
+ if isinstance(requested_fixturedef, PseudoFixtureDef):
761
+ return
762
+ if self._scope > requested_scope:
763
+ # Try to report something helpful.
764
+ argname = requested_fixturedef.argname
765
+ fixture_stack = "\n".join(
766
+ self._format_fixturedef_line(fixturedef)
767
+ for fixturedef in self._get_fixturestack()
768
+ )
769
+ requested_fixture = self._format_fixturedef_line(requested_fixturedef)
770
+ fail(
771
+ f"ScopeMismatch: You tried to access the {requested_scope.value} scoped "
772
+ f"fixture {argname} with a {self._scope.value} scoped request object. "
773
+ f"Requesting fixture stack:\n{fixture_stack}\n"
774
+ f"Requested fixture:\n{requested_fixture}",
775
+ pytrace=False,
776
+ )
777
+
778
+ def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str:
779
+ factory = fixturedef.func
780
+ path, lineno = getfslineno(factory)
781
+ if isinstance(path, Path):
782
+ path = bestrelpath(self._pyfuncitem.session.path, path)
783
+ signature = inspect.signature(factory)
784
+ return f"{path}:{lineno + 1}: def {factory.__name__}{signature}"
785
+
786
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
787
+ self._fixturedef.addfinalizer(finalizer)
788
+
789
+
790
+ @final
791
+ class FixtureLookupError(LookupError):
792
+ """Could not return a requested fixture (missing or invalid)."""
793
+
794
+ def __init__(
795
+ self, argname: str | None, request: FixtureRequest, msg: str | None = None
796
+ ) -> None:
797
+ self.argname = argname
798
+ self.request = request
799
+ self.fixturestack = request._get_fixturestack()
800
+ self.msg = msg
801
+
802
+ def formatrepr(self) -> FixtureLookupErrorRepr:
803
+ tblines: list[str] = []
804
+ addline = tblines.append
805
+ stack = [self.request._pyfuncitem.obj]
806
+ stack.extend(map(lambda x: x.func, self.fixturestack))
807
+ msg = self.msg
808
+ if msg is not None:
809
+ # The last fixture raise an error, let's present
810
+ # it at the requesting side.
811
+ stack = stack[:-1]
812
+ for function in stack:
813
+ fspath, lineno = getfslineno(function)
814
+ try:
815
+ lines, _ = inspect.getsourcelines(get_real_func(function))
816
+ except (OSError, IndexError, TypeError):
817
+ error_msg = "file %s, line %s: source code not available"
818
+ addline(error_msg % (fspath, lineno + 1))
819
+ else:
820
+ addline(f"file {fspath}, line {lineno + 1}")
821
+ for i, line in enumerate(lines):
822
+ line = line.rstrip()
823
+ addline(" " + line)
824
+ if line.lstrip().startswith("def"):
825
+ break
826
+
827
+ if msg is None:
828
+ fm = self.request._fixturemanager
829
+ available = set()
830
+ parent = self.request._pyfuncitem.parent
831
+ assert parent is not None
832
+ for name, fixturedefs in fm._arg2fixturedefs.items():
833
+ faclist = list(fm._matchfactories(fixturedefs, parent))
834
+ if faclist:
835
+ available.add(name)
836
+ if self.argname in available:
837
+ msg = (
838
+ f" recursive dependency involving fixture '{self.argname}' detected"
839
+ )
840
+ else:
841
+ msg = f"fixture '{self.argname}' not found"
842
+ msg += "\n available fixtures: {}".format(", ".join(sorted(available)))
843
+ msg += "\n use 'pytest --fixtures [testpath]' for help on them."
844
+
845
+ return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
846
+
847
+
848
+ class FixtureLookupErrorRepr(TerminalRepr):
849
+ def __init__(
850
+ self,
851
+ filename: str | os.PathLike[str],
852
+ firstlineno: int,
853
+ tblines: Sequence[str],
854
+ errorstring: str,
855
+ argname: str | None,
856
+ ) -> None:
857
+ self.tblines = tblines
858
+ self.errorstring = errorstring
859
+ self.filename = filename
860
+ self.firstlineno = firstlineno
861
+ self.argname = argname
862
+
863
+ def toterminal(self, tw: TerminalWriter) -> None:
864
+ # tw.line("FixtureLookupError: %s" %(self.argname), red=True)
865
+ for tbline in self.tblines:
866
+ tw.line(tbline.rstrip())
867
+ lines = self.errorstring.split("\n")
868
+ if lines:
869
+ tw.line(
870
+ f"{FormattedExcinfo.fail_marker} {lines[0].strip()}",
871
+ red=True,
872
+ )
873
+ for line in lines[1:]:
874
+ tw.line(
875
+ f"{FormattedExcinfo.flow_marker} {line.strip()}",
876
+ red=True,
877
+ )
878
+ tw.line()
879
+ tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1))
880
+
881
+
882
+ def call_fixture_func(
883
+ fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
884
+ ) -> FixtureValue:
885
+ if is_generator(fixturefunc):
886
+ fixturefunc = cast(
887
+ Callable[..., Generator[FixtureValue, None, None]], fixturefunc
888
+ )
889
+ generator = fixturefunc(**kwargs)
890
+ try:
891
+ fixture_result = next(generator)
892
+ except StopIteration:
893
+ raise ValueError(f"{request.fixturename} did not yield a value") from None
894
+ finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
895
+ request.addfinalizer(finalizer)
896
+ else:
897
+ fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
898
+ fixture_result = fixturefunc(**kwargs)
899
+ return fixture_result
900
+
901
+
902
+ def _teardown_yield_fixture(fixturefunc, it) -> None:
903
+ """Execute the teardown of a fixture function by advancing the iterator
904
+ after the yield and ensure the iteration ends (if not it means there is
905
+ more than one yield in the function)."""
906
+ try:
907
+ next(it)
908
+ except StopIteration:
909
+ pass
910
+ else:
911
+ fs, lineno = getfslineno(fixturefunc)
912
+ fail(
913
+ f"fixture function has more than one 'yield':\n\n"
914
+ f"{Source(fixturefunc).indent()}\n"
915
+ f"{fs}:{lineno + 1}",
916
+ pytrace=False,
917
+ )
918
+
919
+
920
+ def _eval_scope_callable(
921
+ scope_callable: Callable[[str, Config], _ScopeName],
922
+ fixture_name: str,
923
+ config: Config,
924
+ ) -> _ScopeName:
925
+ try:
926
+ # Type ignored because there is no typing mechanism to specify
927
+ # keyword arguments, currently.
928
+ result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
929
+ except Exception as e:
930
+ raise TypeError(
931
+ f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n"
932
+ "Expected a function with the signature (*, fixture_name, config)"
933
+ ) from e
934
+ if not isinstance(result, str):
935
+ fail(
936
+ f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n"
937
+ f"{result!r}",
938
+ pytrace=False,
939
+ )
940
+ return result
941
+
942
+
943
+ @final
944
+ class FixtureDef(Generic[FixtureValue]):
945
+ """A container for a fixture definition.
946
+
947
+ Note: At this time, only explicitly documented fields and methods are
948
+ considered public stable API.
949
+ """
950
+
951
+ def __init__(
952
+ self,
953
+ config: Config,
954
+ baseid: str | None,
955
+ argname: str,
956
+ func: _FixtureFunc[FixtureValue],
957
+ scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None,
958
+ params: Sequence[object] | None,
959
+ ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
960
+ *,
961
+ _ispytest: bool = False,
962
+ ) -> None:
963
+ check_ispytest(_ispytest)
964
+ # The "base" node ID for the fixture.
965
+ #
966
+ # This is a node ID prefix. A fixture is only available to a node (e.g.
967
+ # a `Function` item) if the fixture's baseid is a nodeid of a parent of
968
+ # node.
969
+ #
970
+ # For a fixture found in a Collector's object (e.g. a `Module`s module,
971
+ # a `Class`'s class), the baseid is the Collector's nodeid.
972
+ #
973
+ # For a fixture found in a conftest plugin, the baseid is the conftest's
974
+ # directory path relative to the rootdir.
975
+ #
976
+ # For other plugins, the baseid is the empty string (always matches).
977
+ self.baseid: Final = baseid or ""
978
+ # Whether the fixture was found from a node or a conftest in the
979
+ # collection tree. Will be false for fixtures defined in non-conftest
980
+ # plugins.
981
+ self.has_location: Final = baseid is not None
982
+ # The fixture factory function.
983
+ self.func: Final = func
984
+ # The name by which the fixture may be requested.
985
+ self.argname: Final = argname
986
+ if scope is None:
987
+ scope = Scope.Function
988
+ elif callable(scope):
989
+ scope = _eval_scope_callable(scope, argname, config)
990
+ if isinstance(scope, str):
991
+ scope = Scope.from_user(
992
+ scope, descr=f"Fixture '{func.__name__}'", where=baseid
993
+ )
994
+ self._scope: Final = scope
995
+ # If the fixture is directly parametrized, the parameter values.
996
+ self.params: Final = params
997
+ # If the fixture is directly parametrized, a tuple of explicit IDs to
998
+ # assign to the parameter values, or a callable to generate an ID given
999
+ # a parameter value.
1000
+ self.ids: Final = ids
1001
+ # The names requested by the fixtures.
1002
+ self.argnames: Final = getfuncargnames(func, name=argname)
1003
+ # If the fixture was executed, the current value of the fixture.
1004
+ # Can change if the fixture is executed with different parameters.
1005
+ self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
1006
+ self._finalizers: Final[list[Callable[[], object]]] = []
1007
+
1008
+ @property
1009
+ def scope(self) -> _ScopeName:
1010
+ """Scope string, one of "function", "class", "module", "package", "session"."""
1011
+ return self._scope.value
1012
+
1013
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
1014
+ self._finalizers.append(finalizer)
1015
+
1016
+ def finish(self, request: SubRequest) -> None:
1017
+ exceptions: list[BaseException] = []
1018
+ while self._finalizers:
1019
+ fin = self._finalizers.pop()
1020
+ try:
1021
+ fin()
1022
+ except BaseException as e:
1023
+ exceptions.append(e)
1024
+ node = request.node
1025
+ node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
1026
+ # Even if finalization fails, we invalidate the cached fixture
1027
+ # value and remove all finalizers because they may be bound methods
1028
+ # which will keep instances alive.
1029
+ self.cached_result = None
1030
+ self._finalizers.clear()
1031
+ if len(exceptions) == 1:
1032
+ raise exceptions[0]
1033
+ elif len(exceptions) > 1:
1034
+ msg = f'errors while tearing down fixture "{self.argname}" of {node}'
1035
+ raise BaseExceptionGroup(msg, exceptions[::-1])
1036
+
1037
+ def execute(self, request: SubRequest) -> FixtureValue:
1038
+ """Return the value of this fixture, executing it if not cached."""
1039
+ # Ensure that the dependent fixtures requested by this fixture are loaded.
1040
+ # This needs to be done before checking if we have a cached value, since
1041
+ # if a dependent fixture has their cache invalidated, e.g. due to
1042
+ # parametrization, they finalize themselves and fixtures depending on it
1043
+ # (which will likely include this fixture) setting `self.cached_result = None`.
1044
+ # See #4871
1045
+ requested_fixtures_that_should_finalize_us = []
1046
+ for argname in self.argnames:
1047
+ fixturedef = request._get_active_fixturedef(argname)
1048
+ # Saves requested fixtures in a list so we later can add our finalizer
1049
+ # to them, ensuring that if a requested fixture gets torn down we get torn
1050
+ # down first. This is generally handled by SetupState, but still currently
1051
+ # needed when this fixture is not parametrized but depends on a parametrized
1052
+ # fixture.
1053
+ if not isinstance(fixturedef, PseudoFixtureDef):
1054
+ requested_fixtures_that_should_finalize_us.append(fixturedef)
1055
+
1056
+ # Check for (and return) cached value/exception.
1057
+ if self.cached_result is not None:
1058
+ request_cache_key = self.cache_key(request)
1059
+ cache_key = self.cached_result[1]
1060
+ try:
1061
+ # Attempt to make a normal == check: this might fail for objects
1062
+ # which do not implement the standard comparison (like numpy arrays -- #6497).
1063
+ cache_hit = bool(request_cache_key == cache_key)
1064
+ except (ValueError, RuntimeError):
1065
+ # If the comparison raises, use 'is' as fallback.
1066
+ cache_hit = request_cache_key is cache_key
1067
+
1068
+ if cache_hit:
1069
+ if self.cached_result[2] is not None:
1070
+ exc, exc_tb = self.cached_result[2]
1071
+ raise exc.with_traceback(exc_tb)
1072
+ else:
1073
+ result = self.cached_result[0]
1074
+ return result
1075
+ # We have a previous but differently parametrized fixture instance
1076
+ # so we need to tear it down before creating a new one.
1077
+ self.finish(request)
1078
+ assert self.cached_result is None
1079
+
1080
+ # Add finalizer to requested fixtures we saved previously.
1081
+ # We make sure to do this after checking for cached value to avoid
1082
+ # adding our finalizer multiple times. (#12135)
1083
+ finalizer = functools.partial(self.finish, request=request)
1084
+ for parent_fixture in requested_fixtures_that_should_finalize_us:
1085
+ parent_fixture.addfinalizer(finalizer)
1086
+
1087
+ ihook = request.node.ihook
1088
+ try:
1089
+ # Setup the fixture, run the code in it, and cache the value
1090
+ # in self.cached_result
1091
+ result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
1092
+ finally:
1093
+ # schedule our finalizer, even if the setup failed
1094
+ request.node.addfinalizer(finalizer)
1095
+
1096
+ return result
1097
+
1098
+ def cache_key(self, request: SubRequest) -> object:
1099
+ return getattr(request, "param", None)
1100
+
1101
+ def __repr__(self) -> str:
1102
+ return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"
1103
+
1104
+
1105
+ def resolve_fixture_function(
1106
+ fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
1107
+ ) -> _FixtureFunc[FixtureValue]:
1108
+ """Get the actual callable that can be called to obtain the fixture
1109
+ value."""
1110
+ fixturefunc = fixturedef.func
1111
+ # The fixture function needs to be bound to the actual
1112
+ # request.instance so that code working with "fixturedef" behaves
1113
+ # as expected.
1114
+ instance = request.instance
1115
+ if instance is not None:
1116
+ # Handle the case where fixture is defined not in a test class, but some other class
1117
+ # (for example a plugin class with a fixture), see #2270.
1118
+ if hasattr(fixturefunc, "__self__") and not isinstance(
1119
+ instance,
1120
+ fixturefunc.__self__.__class__,
1121
+ ):
1122
+ return fixturefunc
1123
+ fixturefunc = getimfunc(fixturedef.func)
1124
+ if fixturefunc != fixturedef.func:
1125
+ fixturefunc = fixturefunc.__get__(instance)
1126
+ return fixturefunc
1127
+
1128
+
1129
+ def pytest_fixture_setup(
1130
+ fixturedef: FixtureDef[FixtureValue], request: SubRequest
1131
+ ) -> FixtureValue:
1132
+ """Execution of fixture setup."""
1133
+ kwargs = {}
1134
+ for argname in fixturedef.argnames:
1135
+ kwargs[argname] = request.getfixturevalue(argname)
1136
+
1137
+ fixturefunc = resolve_fixture_function(fixturedef, request)
1138
+ my_cache_key = fixturedef.cache_key(request)
1139
+ try:
1140
+ result = call_fixture_func(fixturefunc, request, kwargs)
1141
+ except TEST_OUTCOME as e:
1142
+ if isinstance(e, skip.Exception):
1143
+ # The test requested a fixture which caused a skip.
1144
+ # Don't show the fixture as the skip location, as then the user
1145
+ # wouldn't know which test skipped.
1146
+ e._use_item_location = True
1147
+ fixturedef.cached_result = (None, my_cache_key, (e, e.__traceback__))
1148
+ raise
1149
+ fixturedef.cached_result = (result, my_cache_key, None)
1150
+ return result
1151
+
1152
+
1153
+ def wrap_function_to_error_out_if_called_directly(
1154
+ function: FixtureFunction,
1155
+ fixture_marker: FixtureFunctionMarker,
1156
+ ) -> FixtureFunction:
1157
+ """Wrap the given fixture function so we can raise an error about it being called directly,
1158
+ instead of used as an argument in a test function."""
1159
+ name = fixture_marker.name or function.__name__
1160
+ message = (
1161
+ f'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
1162
+ "but are created automatically when test functions request them as parameters.\n"
1163
+ "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n"
1164
+ "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code."
1165
+ )
1166
+
1167
+ @functools.wraps(function)
1168
+ def result(*args, **kwargs):
1169
+ fail(message, pytrace=False)
1170
+
1171
+ # Keep reference to the original function in our own custom attribute so we don't unwrap
1172
+ # further than this point and lose useful wrappings like @mock.patch (#3774).
1173
+ result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
1174
+
1175
+ return cast(FixtureFunction, result)
1176
+
1177
+
1178
+ @final
1179
+ @dataclasses.dataclass(frozen=True)
1180
+ class FixtureFunctionMarker:
1181
+ scope: _ScopeName | Callable[[str, Config], _ScopeName]
1182
+ params: tuple[object, ...] | None
1183
+ autouse: bool = False
1184
+ ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None
1185
+ name: str | None = None
1186
+
1187
+ _ispytest: dataclasses.InitVar[bool] = False
1188
+
1189
+ def __post_init__(self, _ispytest: bool) -> None:
1190
+ check_ispytest(_ispytest)
1191
+
1192
+ def __call__(self, function: FixtureFunction) -> FixtureFunction:
1193
+ if inspect.isclass(function):
1194
+ raise ValueError("class fixtures not supported (maybe in the future)")
1195
+
1196
+ if getattr(function, "_pytestfixturefunction", False):
1197
+ raise ValueError(
1198
+ f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}"
1199
+ )
1200
+
1201
+ if hasattr(function, "pytestmark"):
1202
+ warnings.warn(MARKED_FIXTURE, stacklevel=2)
1203
+
1204
+ function = wrap_function_to_error_out_if_called_directly(function, self)
1205
+
1206
+ name = self.name or function.__name__
1207
+ if name == "request":
1208
+ location = getlocation(function)
1209
+ fail(
1210
+ f"'request' is a reserved word for fixtures, use another name:\n {location}",
1211
+ pytrace=False,
1212
+ )
1213
+
1214
+ # Type ignored because https://github.com/python/mypy/issues/2087.
1215
+ function._pytestfixturefunction = self # type: ignore[attr-defined]
1216
+ return function
1217
+
1218
+
1219
+ @overload
1220
+ def fixture(
1221
+ fixture_function: FixtureFunction,
1222
+ *,
1223
+ scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
1224
+ params: Iterable[object] | None = ...,
1225
+ autouse: bool = ...,
1226
+ ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
1227
+ name: str | None = ...,
1228
+ ) -> FixtureFunction: ...
1229
+
1230
+
1231
+ @overload
1232
+ def fixture(
1233
+ fixture_function: None = ...,
1234
+ *,
1235
+ scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
1236
+ params: Iterable[object] | None = ...,
1237
+ autouse: bool = ...,
1238
+ ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
1239
+ name: str | None = None,
1240
+ ) -> FixtureFunctionMarker: ...
1241
+
1242
+
1243
+ def fixture(
1244
+ fixture_function: FixtureFunction | None = None,
1245
+ *,
1246
+ scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function",
1247
+ params: Iterable[object] | None = None,
1248
+ autouse: bool = False,
1249
+ ids: Sequence[object | None] | Callable[[Any], object | None] | None = None,
1250
+ name: str | None = None,
1251
+ ) -> FixtureFunctionMarker | FixtureFunction:
1252
+ """Decorator to mark a fixture factory function.
1253
+
1254
+ This decorator can be used, with or without parameters, to define a
1255
+ fixture function.
1256
+
1257
+ The name of the fixture function can later be referenced to cause its
1258
+ invocation ahead of running tests: test modules or classes can use the
1259
+ ``pytest.mark.usefixtures(fixturename)`` marker.
1260
+
1261
+ Test functions can directly use fixture names as input arguments in which
1262
+ case the fixture instance returned from the fixture function will be
1263
+ injected.
1264
+
1265
+ Fixtures can provide their values to test functions using ``return`` or
1266
+ ``yield`` statements. When using ``yield`` the code block after the
1267
+ ``yield`` statement is executed as teardown code regardless of the test
1268
+ outcome, and must yield exactly once.
1269
+
1270
+ :param scope:
1271
+ The scope for which this fixture is shared; one of ``"function"``
1272
+ (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``.
1273
+
1274
+ This parameter may also be a callable which receives ``(fixture_name, config)``
1275
+ as parameters, and must return a ``str`` with one of the values mentioned above.
1276
+
1277
+ See :ref:`dynamic scope` in the docs for more information.
1278
+
1279
+ :param params:
1280
+ An optional list of parameters which will cause multiple invocations
1281
+ of the fixture function and all of the tests using it. The current
1282
+ parameter is available in ``request.param``.
1283
+
1284
+ :param autouse:
1285
+ If True, the fixture func is activated for all tests that can see it.
1286
+ If False (the default), an explicit reference is needed to activate
1287
+ the fixture.
1288
+
1289
+ :param ids:
1290
+ Sequence of ids each corresponding to the params so that they are
1291
+ part of the test id. If no ids are provided they will be generated
1292
+ automatically from the params.
1293
+
1294
+ :param name:
1295
+ The name of the fixture. This defaults to the name of the decorated
1296
+ function. If a fixture is used in the same module in which it is
1297
+ defined, the function name of the fixture will be shadowed by the
1298
+ function arg that requests the fixture; one way to resolve this is to
1299
+ name the decorated function ``fixture_<fixturename>`` and then use
1300
+ ``@pytest.fixture(name='<fixturename>')``.
1301
+ """
1302
+ fixture_marker = FixtureFunctionMarker(
1303
+ scope=scope,
1304
+ params=tuple(params) if params is not None else None,
1305
+ autouse=autouse,
1306
+ ids=None if ids is None else ids if callable(ids) else tuple(ids),
1307
+ name=name,
1308
+ _ispytest=True,
1309
+ )
1310
+
1311
+ # Direct decoration.
1312
+ if fixture_function:
1313
+ return fixture_marker(fixture_function)
1314
+
1315
+ return fixture_marker
1316
+
1317
+
1318
+ def yield_fixture(
1319
+ fixture_function=None,
1320
+ *args,
1321
+ scope="function",
1322
+ params=None,
1323
+ autouse=False,
1324
+ ids=None,
1325
+ name=None,
1326
+ ):
1327
+ """(Return a) decorator to mark a yield-fixture factory function.
1328
+
1329
+ .. deprecated:: 3.0
1330
+ Use :py:func:`pytest.fixture` directly instead.
1331
+ """
1332
+ warnings.warn(YIELD_FIXTURE, stacklevel=2)
1333
+ return fixture(
1334
+ fixture_function,
1335
+ *args,
1336
+ scope=scope,
1337
+ params=params,
1338
+ autouse=autouse,
1339
+ ids=ids,
1340
+ name=name,
1341
+ )
1342
+
1343
+
1344
+ @fixture(scope="session")
1345
+ def pytestconfig(request: FixtureRequest) -> Config:
1346
+ """Session-scoped fixture that returns the session's :class:`pytest.Config`
1347
+ object.
1348
+
1349
+ Example::
1350
+
1351
+ def test_foo(pytestconfig):
1352
+ if pytestconfig.get_verbosity() > 0:
1353
+ ...
1354
+
1355
+ """
1356
+ return request.config
1357
+
1358
+
1359
+ def pytest_addoption(parser: Parser) -> None:
1360
+ parser.addini(
1361
+ "usefixtures",
1362
+ type="args",
1363
+ default=[],
1364
+ help="List of default fixtures to be used with this project",
1365
+ )
1366
+ group = parser.getgroup("general")
1367
+ group.addoption(
1368
+ "--fixtures",
1369
+ "--funcargs",
1370
+ action="store_true",
1371
+ dest="showfixtures",
1372
+ default=False,
1373
+ help="Show available fixtures, sorted by plugin appearance "
1374
+ "(fixtures with leading '_' are only shown with '-v')",
1375
+ )
1376
+ group.addoption(
1377
+ "--fixtures-per-test",
1378
+ action="store_true",
1379
+ dest="show_fixtures_per_test",
1380
+ default=False,
1381
+ help="Show fixtures per test",
1382
+ )
1383
+
1384
+
1385
+ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
1386
+ if config.option.showfixtures:
1387
+ showfixtures(config)
1388
+ return 0
1389
+ if config.option.show_fixtures_per_test:
1390
+ show_fixtures_per_test(config)
1391
+ return 0
1392
+ return None
1393
+
1394
+
1395
+ def _get_direct_parametrize_args(node: nodes.Node) -> set[str]:
1396
+ """Return all direct parametrization arguments of a node, so we don't
1397
+ mistake them for fixtures.
1398
+
1399
+ Check https://github.com/pytest-dev/pytest/issues/5036.
1400
+
1401
+ These things are done later as well when dealing with parametrization
1402
+ so this could be improved.
1403
+ """
1404
+ parametrize_argnames: set[str] = set()
1405
+ for marker in node.iter_markers(name="parametrize"):
1406
+ if not marker.kwargs.get("indirect", False):
1407
+ p_argnames, _ = ParameterSet._parse_parametrize_args(
1408
+ *marker.args, **marker.kwargs
1409
+ )
1410
+ parametrize_argnames.update(p_argnames)
1411
+ return parametrize_argnames
1412
+
1413
+
1414
+ def deduplicate_names(*seqs: Iterable[str]) -> tuple[str, ...]:
1415
+ """De-duplicate the sequence of names while keeping the original order."""
1416
+ # Ideally we would use a set, but it does not preserve insertion order.
1417
+ return tuple(dict.fromkeys(name for seq in seqs for name in seq))
1418
+
1419
+
1420
+ class FixtureManager:
1421
+ """pytest fixture definitions and information is stored and managed
1422
+ from this class.
1423
+
1424
+ During collection fm.parsefactories() is called multiple times to parse
1425
+ fixture function definitions into FixtureDef objects and internal
1426
+ data structures.
1427
+
1428
+ During collection of test functions, metafunc-mechanics instantiate
1429
+ a FuncFixtureInfo object which is cached per node/func-name.
1430
+ This FuncFixtureInfo object is later retrieved by Function nodes
1431
+ which themselves offer a fixturenames attribute.
1432
+
1433
+ The FuncFixtureInfo object holds information about fixtures and FixtureDefs
1434
+ relevant for a particular function. An initial list of fixtures is
1435
+ assembled like this:
1436
+
1437
+ - ini-defined usefixtures
1438
+ - autouse-marked fixtures along the collection chain up from the function
1439
+ - usefixtures markers at module/class/function level
1440
+ - test function funcargs
1441
+
1442
+ Subsequently the funcfixtureinfo.fixturenames attribute is computed
1443
+ as the closure of the fixtures needed to setup the initial fixtures,
1444
+ i.e. fixtures needed by fixture functions themselves are appended
1445
+ to the fixturenames list.
1446
+
1447
+ Upon the test-setup phases all fixturenames are instantiated, retrieved
1448
+ by a lookup of their FuncFixtureInfo.
1449
+ """
1450
+
1451
+ def __init__(self, session: Session) -> None:
1452
+ self.session = session
1453
+ self.config: Config = session.config
1454
+ # Maps a fixture name (argname) to all of the FixtureDefs in the test
1455
+ # suite/plugins defined with this name. Populated by parsefactories().
1456
+ # TODO: The order of the FixtureDefs list of each arg is significant,
1457
+ # explain.
1458
+ self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {}
1459
+ self._holderobjseen: Final[set[object]] = set()
1460
+ # A mapping from a nodeid to a list of autouse fixtures it defines.
1461
+ self._nodeid_autousenames: Final[dict[str, list[str]]] = {
1462
+ "": self.config.getini("usefixtures"),
1463
+ }
1464
+ session.config.pluginmanager.register(self, "funcmanage")
1465
+
1466
+ def getfixtureinfo(
1467
+ self,
1468
+ node: nodes.Item,
1469
+ func: Callable[..., object] | None,
1470
+ cls: type | None,
1471
+ ) -> FuncFixtureInfo:
1472
+ """Calculate the :class:`FuncFixtureInfo` for an item.
1473
+
1474
+ If ``func`` is None, or if the item sets an attribute
1475
+ ``nofuncargs = True``, then ``func`` is not examined at all.
1476
+
1477
+ :param node:
1478
+ The item requesting the fixtures.
1479
+ :param func:
1480
+ The item's function.
1481
+ :param cls:
1482
+ If the function is a method, the method's class.
1483
+ """
1484
+ if func is not None and not getattr(node, "nofuncargs", False):
1485
+ argnames = getfuncargnames(func, name=node.name, cls=cls)
1486
+ else:
1487
+ argnames = ()
1488
+ usefixturesnames = self._getusefixturesnames(node)
1489
+ autousenames = self._getautousenames(node)
1490
+ initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
1491
+
1492
+ direct_parametrize_args = _get_direct_parametrize_args(node)
1493
+
1494
+ names_closure, arg2fixturedefs = self.getfixtureclosure(
1495
+ parentnode=node,
1496
+ initialnames=initialnames,
1497
+ ignore_args=direct_parametrize_args,
1498
+ )
1499
+
1500
+ return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
1501
+
1502
+ def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None:
1503
+ # Fixtures defined in conftest plugins are only visible to within the
1504
+ # conftest's directory. This is unlike fixtures in non-conftest plugins
1505
+ # which have global visibility. So for conftests, construct the base
1506
+ # nodeid from the plugin name (which is the conftest path).
1507
+ if plugin_name and plugin_name.endswith("conftest.py"):
1508
+ # Note: we explicitly do *not* use `plugin.__file__` here -- The
1509
+ # difference is that plugin_name has the correct capitalization on
1510
+ # case-insensitive systems (Windows) and other normalization issues
1511
+ # (issue #11816).
1512
+ conftestpath = absolutepath(plugin_name)
1513
+ try:
1514
+ nodeid = str(conftestpath.parent.relative_to(self.config.rootpath))
1515
+ except ValueError:
1516
+ nodeid = ""
1517
+ if nodeid == ".":
1518
+ nodeid = ""
1519
+ if os.sep != nodes.SEP:
1520
+ nodeid = nodeid.replace(os.sep, nodes.SEP)
1521
+ else:
1522
+ nodeid = None
1523
+
1524
+ self.parsefactories(plugin, nodeid)
1525
+
1526
+ def _getautousenames(self, node: nodes.Node) -> Iterator[str]:
1527
+ """Return the names of autouse fixtures applicable to node."""
1528
+ for parentnode in node.listchain():
1529
+ basenames = self._nodeid_autousenames.get(parentnode.nodeid)
1530
+ if basenames:
1531
+ yield from basenames
1532
+
1533
+ def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]:
1534
+ """Return the names of usefixtures fixtures applicable to node."""
1535
+ for mark in node.iter_markers(name="usefixtures"):
1536
+ yield from mark.args
1537
+
1538
+ def getfixtureclosure(
1539
+ self,
1540
+ parentnode: nodes.Node,
1541
+ initialnames: tuple[str, ...],
1542
+ ignore_args: AbstractSet[str],
1543
+ ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]:
1544
+ # Collect the closure of all fixtures, starting with the given
1545
+ # fixturenames as the initial set. As we have to visit all
1546
+ # factory definitions anyway, we also return an arg2fixturedefs
1547
+ # mapping so that the caller can reuse it and does not have
1548
+ # to re-discover fixturedefs again for each fixturename
1549
+ # (discovering matching fixtures for a given name/node is expensive).
1550
+
1551
+ fixturenames_closure = list(initialnames)
1552
+
1553
+ arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {}
1554
+ lastlen = -1
1555
+ while lastlen != len(fixturenames_closure):
1556
+ lastlen = len(fixturenames_closure)
1557
+ for argname in fixturenames_closure:
1558
+ if argname in ignore_args:
1559
+ continue
1560
+ if argname in arg2fixturedefs:
1561
+ continue
1562
+ fixturedefs = self.getfixturedefs(argname, parentnode)
1563
+ if fixturedefs:
1564
+ arg2fixturedefs[argname] = fixturedefs
1565
+ for arg in fixturedefs[-1].argnames:
1566
+ if arg not in fixturenames_closure:
1567
+ fixturenames_closure.append(arg)
1568
+
1569
+ def sort_by_scope(arg_name: str) -> Scope:
1570
+ try:
1571
+ fixturedefs = arg2fixturedefs[arg_name]
1572
+ except KeyError:
1573
+ return Scope.Function
1574
+ else:
1575
+ return fixturedefs[-1]._scope
1576
+
1577
+ fixturenames_closure.sort(key=sort_by_scope, reverse=True)
1578
+ return fixturenames_closure, arg2fixturedefs
1579
+
1580
+ def pytest_generate_tests(self, metafunc: Metafunc) -> None:
1581
+ """Generate new tests based on parametrized fixtures used by the given metafunc"""
1582
+
1583
+ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
1584
+ args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs)
1585
+ return args
1586
+
1587
+ for argname in metafunc.fixturenames:
1588
+ # Get the FixtureDefs for the argname.
1589
+ fixture_defs = metafunc._arg2fixturedefs.get(argname)
1590
+ if not fixture_defs:
1591
+ # Will raise FixtureLookupError at setup time if not parametrized somewhere
1592
+ # else (e.g @pytest.mark.parametrize)
1593
+ continue
1594
+
1595
+ # If the test itself parametrizes using this argname, give it
1596
+ # precedence.
1597
+ if any(
1598
+ argname in get_parametrize_mark_argnames(mark)
1599
+ for mark in metafunc.definition.iter_markers("parametrize")
1600
+ ):
1601
+ continue
1602
+
1603
+ # In the common case we only look at the fixture def with the
1604
+ # closest scope (last in the list). But if the fixture overrides
1605
+ # another fixture, while requesting the super fixture, keep going
1606
+ # in case the super fixture is parametrized (#1953).
1607
+ for fixturedef in reversed(fixture_defs):
1608
+ # Fixture is parametrized, apply it and stop.
1609
+ if fixturedef.params is not None:
1610
+ metafunc.parametrize(
1611
+ argname,
1612
+ fixturedef.params,
1613
+ indirect=True,
1614
+ scope=fixturedef.scope,
1615
+ ids=fixturedef.ids,
1616
+ )
1617
+ break
1618
+
1619
+ # Not requesting the overridden super fixture, stop.
1620
+ if argname not in fixturedef.argnames:
1621
+ break
1622
+
1623
+ # Try next super fixture, if any.
1624
+
1625
+ def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> None:
1626
+ # Separate parametrized setups.
1627
+ items[:] = reorder_items(items)
1628
+
1629
+ def _register_fixture(
1630
+ self,
1631
+ *,
1632
+ name: str,
1633
+ func: _FixtureFunc[object],
1634
+ nodeid: str | None,
1635
+ scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function",
1636
+ params: Sequence[object] | None = None,
1637
+ ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
1638
+ autouse: bool = False,
1639
+ ) -> None:
1640
+ """Register a fixture
1641
+
1642
+ :param name:
1643
+ The fixture's name.
1644
+ :param func:
1645
+ The fixture's implementation function.
1646
+ :param nodeid:
1647
+ The visibility of the fixture. The fixture will be available to the
1648
+ node with this nodeid and its children in the collection tree.
1649
+ None means that the fixture is visible to the entire collection tree,
1650
+ e.g. a fixture defined for general use in a plugin.
1651
+ :param scope:
1652
+ The fixture's scope.
1653
+ :param params:
1654
+ The fixture's parametrization params.
1655
+ :param ids:
1656
+ The fixture's IDs.
1657
+ :param autouse:
1658
+ Whether this is an autouse fixture.
1659
+ """
1660
+ fixture_def = FixtureDef(
1661
+ config=self.config,
1662
+ baseid=nodeid,
1663
+ argname=name,
1664
+ func=func,
1665
+ scope=scope,
1666
+ params=params,
1667
+ ids=ids,
1668
+ _ispytest=True,
1669
+ )
1670
+
1671
+ faclist = self._arg2fixturedefs.setdefault(name, [])
1672
+ if fixture_def.has_location:
1673
+ faclist.append(fixture_def)
1674
+ else:
1675
+ # fixturedefs with no location are at the front
1676
+ # so this inserts the current fixturedef after the
1677
+ # existing fixturedefs from external plugins but
1678
+ # before the fixturedefs provided in conftests.
1679
+ i = len([f for f in faclist if not f.has_location])
1680
+ faclist.insert(i, fixture_def)
1681
+ if autouse:
1682
+ self._nodeid_autousenames.setdefault(nodeid or "", []).append(name)
1683
+
1684
+ @overload
1685
+ def parsefactories(
1686
+ self,
1687
+ node_or_obj: nodes.Node,
1688
+ ) -> None:
1689
+ raise NotImplementedError()
1690
+
1691
+ @overload
1692
+ def parsefactories(
1693
+ self,
1694
+ node_or_obj: object,
1695
+ nodeid: str | None,
1696
+ ) -> None:
1697
+ raise NotImplementedError()
1698
+
1699
+ def parsefactories(
1700
+ self,
1701
+ node_or_obj: nodes.Node | object,
1702
+ nodeid: str | NotSetType | None = NOTSET,
1703
+ ) -> None:
1704
+ """Collect fixtures from a collection node or object.
1705
+
1706
+ Found fixtures are parsed into `FixtureDef`s and saved.
1707
+
1708
+ If `node_or_object` is a collection node (with an underlying Python
1709
+ object), the node's object is traversed and the node's nodeid is used to
1710
+ determine the fixtures' visibility. `nodeid` must not be specified in
1711
+ this case.
1712
+
1713
+ If `node_or_object` is an object (e.g. a plugin), the object is
1714
+ traversed and the given `nodeid` is used to determine the fixtures'
1715
+ visibility. `nodeid` must be specified in this case; None and "" mean
1716
+ total visibility.
1717
+ """
1718
+ if nodeid is not NOTSET:
1719
+ holderobj = node_or_obj
1720
+ else:
1721
+ assert isinstance(node_or_obj, nodes.Node)
1722
+ holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
1723
+ assert isinstance(node_or_obj.nodeid, str)
1724
+ nodeid = node_or_obj.nodeid
1725
+ if holderobj in self._holderobjseen:
1726
+ return
1727
+
1728
+ # Avoid accessing `@property` (and other descriptors) when iterating fixtures.
1729
+ if not safe_isclass(holderobj) and not isinstance(holderobj, types.ModuleType):
1730
+ holderobj_tp: object = type(holderobj)
1731
+ else:
1732
+ holderobj_tp = holderobj
1733
+
1734
+ self._holderobjseen.add(holderobj)
1735
+ for name in dir(holderobj):
1736
+ # The attribute can be an arbitrary descriptor, so the attribute
1737
+ # access below can raise. safe_getattr() ignores such exceptions.
1738
+ obj_ub = safe_getattr(holderobj_tp, name, None)
1739
+ marker = getfixturemarker(obj_ub)
1740
+ if not isinstance(marker, FixtureFunctionMarker):
1741
+ # Magic globals with __getattr__ might have got us a wrong
1742
+ # fixture attribute.
1743
+ continue
1744
+
1745
+ # OK we know it is a fixture -- now safe to look up on the _instance_.
1746
+ obj = getattr(holderobj, name)
1747
+
1748
+ if marker.name:
1749
+ name = marker.name
1750
+
1751
+ # During fixture definition we wrap the original fixture function
1752
+ # to issue a warning if called directly, so here we unwrap it in
1753
+ # order to not emit the warning when pytest itself calls the
1754
+ # fixture function.
1755
+ func = get_real_method(obj, holderobj)
1756
+
1757
+ self._register_fixture(
1758
+ name=name,
1759
+ nodeid=nodeid,
1760
+ func=func,
1761
+ scope=marker.scope,
1762
+ params=marker.params,
1763
+ ids=marker.ids,
1764
+ autouse=marker.autouse,
1765
+ )
1766
+
1767
+ def getfixturedefs(
1768
+ self, argname: str, node: nodes.Node
1769
+ ) -> Sequence[FixtureDef[Any]] | None:
1770
+ """Get FixtureDefs for a fixture name which are applicable
1771
+ to a given node.
1772
+
1773
+ Returns None if there are no fixtures at all defined with the given
1774
+ name. (This is different from the case in which there are fixtures
1775
+ with the given name, but none applicable to the node. In this case,
1776
+ an empty result is returned).
1777
+
1778
+ :param argname: Name of the fixture to search for.
1779
+ :param node: The requesting Node.
1780
+ """
1781
+ try:
1782
+ fixturedefs = self._arg2fixturedefs[argname]
1783
+ except KeyError:
1784
+ return None
1785
+ return tuple(self._matchfactories(fixturedefs, node))
1786
+
1787
+ def _matchfactories(
1788
+ self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
1789
+ ) -> Iterator[FixtureDef[Any]]:
1790
+ parentnodeids = {n.nodeid for n in node.iter_parents()}
1791
+ for fixturedef in fixturedefs:
1792
+ if fixturedef.baseid in parentnodeids:
1793
+ yield fixturedef
1794
+
1795
+
1796
+ def show_fixtures_per_test(config: Config) -> int | ExitCode:
1797
+ from _pytest.main import wrap_session
1798
+
1799
+ return wrap_session(config, _show_fixtures_per_test)
1800
+
1801
+
1802
+ _PYTEST_DIR = Path(_pytest.__file__).parent
1803
+
1804
+
1805
+ def _pretty_fixture_path(invocation_dir: Path, func) -> str:
1806
+ loc = Path(getlocation(func, invocation_dir))
1807
+ prefix = Path("...", "_pytest")
1808
+ try:
1809
+ return str(prefix / loc.relative_to(_PYTEST_DIR))
1810
+ except ValueError:
1811
+ return bestrelpath(invocation_dir, loc)
1812
+
1813
+
1814
+ def _show_fixtures_per_test(config: Config, session: Session) -> None:
1815
+ import _pytest.config
1816
+
1817
+ session.perform_collect()
1818
+ invocation_dir = config.invocation_params.dir
1819
+ tw = _pytest.config.create_terminal_writer(config)
1820
+ verbose = config.get_verbosity()
1821
+
1822
+ def get_best_relpath(func) -> str:
1823
+ loc = getlocation(func, invocation_dir)
1824
+ return bestrelpath(invocation_dir, Path(loc))
1825
+
1826
+ def write_fixture(fixture_def: FixtureDef[object]) -> None:
1827
+ argname = fixture_def.argname
1828
+ if verbose <= 0 and argname.startswith("_"):
1829
+ return
1830
+ prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
1831
+ tw.write(f"{argname}", green=True)
1832
+ tw.write(f" -- {prettypath}", yellow=True)
1833
+ tw.write("\n")
1834
+ fixture_doc = inspect.getdoc(fixture_def.func)
1835
+ if fixture_doc:
1836
+ write_docstring(
1837
+ tw,
1838
+ fixture_doc.split("\n\n", maxsplit=1)[0]
1839
+ if verbose <= 0
1840
+ else fixture_doc,
1841
+ )
1842
+ else:
1843
+ tw.line(" no docstring available", red=True)
1844
+
1845
+ def write_item(item: nodes.Item) -> None:
1846
+ # Not all items have _fixtureinfo attribute.
1847
+ info: FuncFixtureInfo | None = getattr(item, "_fixtureinfo", None)
1848
+ if info is None or not info.name2fixturedefs:
1849
+ # This test item does not use any fixtures.
1850
+ return
1851
+ tw.line()
1852
+ tw.sep("-", f"fixtures used by {item.name}")
1853
+ # TODO: Fix this type ignore.
1854
+ tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
1855
+ # dict key not used in loop but needed for sorting.
1856
+ for _, fixturedefs in sorted(info.name2fixturedefs.items()):
1857
+ assert fixturedefs is not None
1858
+ if not fixturedefs:
1859
+ continue
1860
+ # Last item is expected to be the one used by the test item.
1861
+ write_fixture(fixturedefs[-1])
1862
+
1863
+ for session_item in session.items:
1864
+ write_item(session_item)
1865
+
1866
+
1867
+ def showfixtures(config: Config) -> int | ExitCode:
1868
+ from _pytest.main import wrap_session
1869
+
1870
+ return wrap_session(config, _showfixtures_main)
1871
+
1872
+
1873
+ def _showfixtures_main(config: Config, session: Session) -> None:
1874
+ import _pytest.config
1875
+
1876
+ session.perform_collect()
1877
+ invocation_dir = config.invocation_params.dir
1878
+ tw = _pytest.config.create_terminal_writer(config)
1879
+ verbose = config.get_verbosity()
1880
+
1881
+ fm = session._fixturemanager
1882
+
1883
+ available = []
1884
+ seen: set[tuple[str, str]] = set()
1885
+
1886
+ for argname, fixturedefs in fm._arg2fixturedefs.items():
1887
+ assert fixturedefs is not None
1888
+ if not fixturedefs:
1889
+ continue
1890
+ for fixturedef in fixturedefs:
1891
+ loc = getlocation(fixturedef.func, invocation_dir)
1892
+ if (fixturedef.argname, loc) in seen:
1893
+ continue
1894
+ seen.add((fixturedef.argname, loc))
1895
+ available.append(
1896
+ (
1897
+ len(fixturedef.baseid),
1898
+ fixturedef.func.__module__,
1899
+ _pretty_fixture_path(invocation_dir, fixturedef.func),
1900
+ fixturedef.argname,
1901
+ fixturedef,
1902
+ )
1903
+ )
1904
+
1905
+ available.sort()
1906
+ currentmodule = None
1907
+ for baseid, module, prettypath, argname, fixturedef in available:
1908
+ if currentmodule != module:
1909
+ if not module.startswith("_pytest."):
1910
+ tw.line()
1911
+ tw.sep("-", f"fixtures defined from {module}")
1912
+ currentmodule = module
1913
+ if verbose <= 0 and argname.startswith("_"):
1914
+ continue
1915
+ tw.write(f"{argname}", green=True)
1916
+ if fixturedef.scope != "function":
1917
+ tw.write(f" [{fixturedef.scope} scope]", cyan=True)
1918
+ tw.write(f" -- {prettypath}", yellow=True)
1919
+ tw.write("\n")
1920
+ doc = inspect.getdoc(fixturedef.func)
1921
+ if doc:
1922
+ write_docstring(
1923
+ tw, doc.split("\n\n", maxsplit=1)[0] if verbose <= 0 else doc
1924
+ )
1925
+ else:
1926
+ tw.line(" no docstring available", red=True)
1927
+ tw.line()
1928
+
1929
+
1930
+ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
1931
+ for line in doc.split("\n"):
1932
+ tw.line(indent + line)
.venv/lib/python3.11/site-packages/_pytest/freeze_support.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Provides a function to report all internal modules for using freezing
2
+ tools."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import types
7
+ from typing import Iterator
8
+
9
+
10
+ def freeze_includes() -> list[str]:
11
+ """Return a list of module names used by pytest that should be
12
+ included by cx_freeze."""
13
+ import _pytest
14
+
15
+ result = list(_iter_all_modules(_pytest))
16
+ return result
17
+
18
+
19
+ def _iter_all_modules(
20
+ package: str | types.ModuleType,
21
+ prefix: str = "",
22
+ ) -> Iterator[str]:
23
+ """Iterate over the names of all modules that can be found in the given
24
+ package, recursively.
25
+
26
+ >>> import _pytest
27
+ >>> list(_iter_all_modules(_pytest))
28
+ ['_pytest._argcomplete', '_pytest._code.code', ...]
29
+ """
30
+ import os
31
+ import pkgutil
32
+
33
+ if isinstance(package, str):
34
+ path = package
35
+ else:
36
+ # Type ignored because typeshed doesn't define ModuleType.__path__
37
+ # (only defined on packages).
38
+ package_path = package.__path__
39
+ path, prefix = package_path[0], package.__name__ + "."
40
+ for _, name, is_package in pkgutil.iter_modules([path]):
41
+ if is_package:
42
+ for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."):
43
+ yield prefix + m
44
+ else:
45
+ yield prefix + name
.venv/lib/python3.11/site-packages/_pytest/junitxml.py ADDED
@@ -0,0 +1,697 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Report test results in JUnit-XML format, for use with Jenkins and build
3
+ integration servers.
4
+
5
+ Based on initial code from Ross Lawley.
6
+
7
+ Output conforms to
8
+ https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from datetime import datetime
14
+ from datetime import timezone
15
+ import functools
16
+ import os
17
+ import platform
18
+ import re
19
+ from typing import Callable
20
+ from typing import Match
21
+ import xml.etree.ElementTree as ET
22
+
23
+ from _pytest import nodes
24
+ from _pytest import timing
25
+ from _pytest._code.code import ExceptionRepr
26
+ from _pytest._code.code import ReprFileLocation
27
+ from _pytest.config import Config
28
+ from _pytest.config import filename_arg
29
+ from _pytest.config.argparsing import Parser
30
+ from _pytest.fixtures import FixtureRequest
31
+ from _pytest.reports import TestReport
32
+ from _pytest.stash import StashKey
33
+ from _pytest.terminal import TerminalReporter
34
+ import pytest
35
+
36
+
37
+ xml_key = StashKey["LogXML"]()
38
+
39
+
40
+ def bin_xml_escape(arg: object) -> str:
41
+ r"""Visually escape invalid XML characters.
42
+
43
+ For example, transforms
44
+ 'hello\aworld\b'
45
+ into
46
+ 'hello#x07world#x08'
47
+ Note that the #xABs are *not* XML escapes - missing the ampersand &#xAB.
48
+ The idea is to escape visually for the user rather than for XML itself.
49
+ """
50
+
51
+ def repl(matchobj: Match[str]) -> str:
52
+ i = ord(matchobj.group())
53
+ if i <= 0xFF:
54
+ return f"#x{i:02X}"
55
+ else:
56
+ return f"#x{i:04X}"
57
+
58
+ # The spec range of valid chars is:
59
+ # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
60
+ # For an unknown(?) reason, we disallow #x7F (DEL) as well.
61
+ illegal_xml_re = (
62
+ "[^\u0009\u000a\u000d\u0020-\u007e\u0080-\ud7ff\ue000-\ufffd\u10000-\u10ffff]"
63
+ )
64
+ return re.sub(illegal_xml_re, repl, str(arg))
65
+
66
+
67
+ def merge_family(left, right) -> None:
68
+ result = {}
69
+ for kl, vl in left.items():
70
+ for kr, vr in right.items():
71
+ if not isinstance(vl, list):
72
+ raise TypeError(type(vl))
73
+ result[kl] = vl + vr
74
+ left.update(result)
75
+
76
+
77
+ families = {}
78
+ families["_base"] = {"testcase": ["classname", "name"]}
79
+ families["_base_legacy"] = {"testcase": ["file", "line", "url"]}
80
+
81
+ # xUnit 1.x inherits legacy attributes.
82
+ families["xunit1"] = families["_base"].copy()
83
+ merge_family(families["xunit1"], families["_base_legacy"])
84
+
85
+ # xUnit 2.x uses strict base attributes.
86
+ families["xunit2"] = families["_base"]
87
+
88
+
89
+ class _NodeReporter:
90
+ def __init__(self, nodeid: str | TestReport, xml: LogXML) -> None:
91
+ self.id = nodeid
92
+ self.xml = xml
93
+ self.add_stats = self.xml.add_stats
94
+ self.family = self.xml.family
95
+ self.duration = 0.0
96
+ self.properties: list[tuple[str, str]] = []
97
+ self.nodes: list[ET.Element] = []
98
+ self.attrs: dict[str, str] = {}
99
+
100
+ def append(self, node: ET.Element) -> None:
101
+ self.xml.add_stats(node.tag)
102
+ self.nodes.append(node)
103
+
104
+ def add_property(self, name: str, value: object) -> None:
105
+ self.properties.append((str(name), bin_xml_escape(value)))
106
+
107
+ def add_attribute(self, name: str, value: object) -> None:
108
+ self.attrs[str(name)] = bin_xml_escape(value)
109
+
110
+ def make_properties_node(self) -> ET.Element | None:
111
+ """Return a Junit node containing custom properties, if any."""
112
+ if self.properties:
113
+ properties = ET.Element("properties")
114
+ for name, value in self.properties:
115
+ properties.append(ET.Element("property", name=name, value=value))
116
+ return properties
117
+ return None
118
+
119
+ def record_testreport(self, testreport: TestReport) -> None:
120
+ names = mangle_test_address(testreport.nodeid)
121
+ existing_attrs = self.attrs
122
+ classnames = names[:-1]
123
+ if self.xml.prefix:
124
+ classnames.insert(0, self.xml.prefix)
125
+ attrs: dict[str, str] = {
126
+ "classname": ".".join(classnames),
127
+ "name": bin_xml_escape(names[-1]),
128
+ "file": testreport.location[0],
129
+ }
130
+ if testreport.location[1] is not None:
131
+ attrs["line"] = str(testreport.location[1])
132
+ if hasattr(testreport, "url"):
133
+ attrs["url"] = testreport.url
134
+ self.attrs = attrs
135
+ self.attrs.update(existing_attrs) # Restore any user-defined attributes.
136
+
137
+ # Preserve legacy testcase behavior.
138
+ if self.family == "xunit1":
139
+ return
140
+
141
+ # Filter out attributes not permitted by this test family.
142
+ # Including custom attributes because they are not valid here.
143
+ temp_attrs = {}
144
+ for key in self.attrs:
145
+ if key in families[self.family]["testcase"]:
146
+ temp_attrs[key] = self.attrs[key]
147
+ self.attrs = temp_attrs
148
+
149
+ def to_xml(self) -> ET.Element:
150
+ testcase = ET.Element("testcase", self.attrs, time=f"{self.duration:.3f}")
151
+ properties = self.make_properties_node()
152
+ if properties is not None:
153
+ testcase.append(properties)
154
+ testcase.extend(self.nodes)
155
+ return testcase
156
+
157
+ def _add_simple(self, tag: str, message: str, data: str | None = None) -> None:
158
+ node = ET.Element(tag, message=message)
159
+ node.text = bin_xml_escape(data)
160
+ self.append(node)
161
+
162
+ def write_captured_output(self, report: TestReport) -> None:
163
+ if not self.xml.log_passing_tests and report.passed:
164
+ return
165
+
166
+ content_out = report.capstdout
167
+ content_log = report.caplog
168
+ content_err = report.capstderr
169
+ if self.xml.logging == "no":
170
+ return
171
+ content_all = ""
172
+ if self.xml.logging in ["log", "all"]:
173
+ content_all = self._prepare_content(content_log, " Captured Log ")
174
+ if self.xml.logging in ["system-out", "out-err", "all"]:
175
+ content_all += self._prepare_content(content_out, " Captured Out ")
176
+ self._write_content(report, content_all, "system-out")
177
+ content_all = ""
178
+ if self.xml.logging in ["system-err", "out-err", "all"]:
179
+ content_all += self._prepare_content(content_err, " Captured Err ")
180
+ self._write_content(report, content_all, "system-err")
181
+ content_all = ""
182
+ if content_all:
183
+ self._write_content(report, content_all, "system-out")
184
+
185
+ def _prepare_content(self, content: str, header: str) -> str:
186
+ return "\n".join([header.center(80, "-"), content, ""])
187
+
188
+ def _write_content(self, report: TestReport, content: str, jheader: str) -> None:
189
+ tag = ET.Element(jheader)
190
+ tag.text = bin_xml_escape(content)
191
+ self.append(tag)
192
+
193
+ def append_pass(self, report: TestReport) -> None:
194
+ self.add_stats("passed")
195
+
196
+ def append_failure(self, report: TestReport) -> None:
197
+ # msg = str(report.longrepr.reprtraceback.extraline)
198
+ if hasattr(report, "wasxfail"):
199
+ self._add_simple("skipped", "xfail-marked test passes unexpectedly")
200
+ else:
201
+ assert report.longrepr is not None
202
+ reprcrash: ReprFileLocation | None = getattr(
203
+ report.longrepr, "reprcrash", None
204
+ )
205
+ if reprcrash is not None:
206
+ message = reprcrash.message
207
+ else:
208
+ message = str(report.longrepr)
209
+ message = bin_xml_escape(message)
210
+ self._add_simple("failure", message, str(report.longrepr))
211
+
212
+ def append_collect_error(self, report: TestReport) -> None:
213
+ # msg = str(report.longrepr.reprtraceback.extraline)
214
+ assert report.longrepr is not None
215
+ self._add_simple("error", "collection failure", str(report.longrepr))
216
+
217
+ def append_collect_skipped(self, report: TestReport) -> None:
218
+ self._add_simple("skipped", "collection skipped", str(report.longrepr))
219
+
220
+ def append_error(self, report: TestReport) -> None:
221
+ assert report.longrepr is not None
222
+ reprcrash: ReprFileLocation | None = getattr(report.longrepr, "reprcrash", None)
223
+ if reprcrash is not None:
224
+ reason = reprcrash.message
225
+ else:
226
+ reason = str(report.longrepr)
227
+
228
+ if report.when == "teardown":
229
+ msg = f'failed on teardown with "{reason}"'
230
+ else:
231
+ msg = f'failed on setup with "{reason}"'
232
+ self._add_simple("error", bin_xml_escape(msg), str(report.longrepr))
233
+
234
+ def append_skipped(self, report: TestReport) -> None:
235
+ if hasattr(report, "wasxfail"):
236
+ xfailreason = report.wasxfail
237
+ if xfailreason.startswith("reason: "):
238
+ xfailreason = xfailreason[8:]
239
+ xfailreason = bin_xml_escape(xfailreason)
240
+ skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason)
241
+ self.append(skipped)
242
+ else:
243
+ assert isinstance(report.longrepr, tuple)
244
+ filename, lineno, skipreason = report.longrepr
245
+ if skipreason.startswith("Skipped: "):
246
+ skipreason = skipreason[9:]
247
+ details = f"{filename}:{lineno}: {skipreason}"
248
+
249
+ skipped = ET.Element(
250
+ "skipped", type="pytest.skip", message=bin_xml_escape(skipreason)
251
+ )
252
+ skipped.text = bin_xml_escape(details)
253
+ self.append(skipped)
254
+ self.write_captured_output(report)
255
+
256
+ def finalize(self) -> None:
257
+ data = self.to_xml()
258
+ self.__dict__.clear()
259
+ # Type ignored because mypy doesn't like overriding a method.
260
+ # Also the return value doesn't match...
261
+ self.to_xml = lambda: data # type: ignore[method-assign]
262
+
263
+
264
+ def _warn_incompatibility_with_xunit2(
265
+ request: FixtureRequest, fixture_name: str
266
+ ) -> None:
267
+ """Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions."""
268
+ from _pytest.warning_types import PytestWarning
269
+
270
+ xml = request.config.stash.get(xml_key, None)
271
+ if xml is not None and xml.family not in ("xunit1", "legacy"):
272
+ request.node.warn(
273
+ PytestWarning(
274
+ f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')"
275
+ )
276
+ )
277
+
278
+
279
+ @pytest.fixture
280
+ def record_property(request: FixtureRequest) -> Callable[[str, object], None]:
281
+ """Add extra properties to the calling test.
282
+
283
+ User properties become part of the test report and are available to the
284
+ configured reporters, like JUnit XML.
285
+
286
+ The fixture is callable with ``name, value``. The value is automatically
287
+ XML-encoded.
288
+
289
+ Example::
290
+
291
+ def test_function(record_property):
292
+ record_property("example_key", 1)
293
+ """
294
+ _warn_incompatibility_with_xunit2(request, "record_property")
295
+
296
+ def append_property(name: str, value: object) -> None:
297
+ request.node.user_properties.append((name, value))
298
+
299
+ return append_property
300
+
301
+
302
+ @pytest.fixture
303
+ def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], None]:
304
+ """Add extra xml attributes to the tag for the calling test.
305
+
306
+ The fixture is callable with ``name, value``. The value is
307
+ automatically XML-encoded.
308
+ """
309
+ from _pytest.warning_types import PytestExperimentalApiWarning
310
+
311
+ request.node.warn(
312
+ PytestExperimentalApiWarning("record_xml_attribute is an experimental feature")
313
+ )
314
+
315
+ _warn_incompatibility_with_xunit2(request, "record_xml_attribute")
316
+
317
+ # Declare noop
318
+ def add_attr_noop(name: str, value: object) -> None:
319
+ pass
320
+
321
+ attr_func = add_attr_noop
322
+
323
+ xml = request.config.stash.get(xml_key, None)
324
+ if xml is not None:
325
+ node_reporter = xml.node_reporter(request.node.nodeid)
326
+ attr_func = node_reporter.add_attribute
327
+
328
+ return attr_func
329
+
330
+
331
+ def _check_record_param_type(param: str, v: str) -> None:
332
+ """Used by record_testsuite_property to check that the given parameter name is of the proper
333
+ type."""
334
+ __tracebackhide__ = True
335
+ if not isinstance(v, str):
336
+ msg = "{param} parameter needs to be a string, but {g} given" # type: ignore[unreachable]
337
+ raise TypeError(msg.format(param=param, g=type(v).__name__))
338
+
339
+
340
+ @pytest.fixture(scope="session")
341
+ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]:
342
+ """Record a new ``<property>`` tag as child of the root ``<testsuite>``.
343
+
344
+ This is suitable to writing global information regarding the entire test
345
+ suite, and is compatible with ``xunit2`` JUnit family.
346
+
347
+ This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
348
+
349
+ .. code-block:: python
350
+
351
+ def test_foo(record_testsuite_property):
352
+ record_testsuite_property("ARCH", "PPC")
353
+ record_testsuite_property("STORAGE_TYPE", "CEPH")
354
+
355
+ :param name:
356
+ The property name.
357
+ :param value:
358
+ The property value. Will be converted to a string.
359
+
360
+ .. warning::
361
+
362
+ Currently this fixture **does not work** with the
363
+ `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
364
+ :issue:`7767` for details.
365
+ """
366
+ __tracebackhide__ = True
367
+
368
+ def record_func(name: str, value: object) -> None:
369
+ """No-op function in case --junit-xml was not passed in the command-line."""
370
+ __tracebackhide__ = True
371
+ _check_record_param_type("name", name)
372
+
373
+ xml = request.config.stash.get(xml_key, None)
374
+ if xml is not None:
375
+ record_func = xml.add_global_property
376
+ return record_func
377
+
378
+
379
+ def pytest_addoption(parser: Parser) -> None:
380
+ group = parser.getgroup("terminal reporting")
381
+ group.addoption(
382
+ "--junitxml",
383
+ "--junit-xml",
384
+ action="store",
385
+ dest="xmlpath",
386
+ metavar="path",
387
+ type=functools.partial(filename_arg, optname="--junitxml"),
388
+ default=None,
389
+ help="Create junit-xml style report file at given path",
390
+ )
391
+ group.addoption(
392
+ "--junitprefix",
393
+ "--junit-prefix",
394
+ action="store",
395
+ metavar="str",
396
+ default=None,
397
+ help="Prepend prefix to classnames in junit-xml output",
398
+ )
399
+ parser.addini(
400
+ "junit_suite_name", "Test suite name for JUnit report", default="pytest"
401
+ )
402
+ parser.addini(
403
+ "junit_logging",
404
+ "Write captured log messages to JUnit report: "
405
+ "one of no|log|system-out|system-err|out-err|all",
406
+ default="no",
407
+ )
408
+ parser.addini(
409
+ "junit_log_passing_tests",
410
+ "Capture log information for passing tests to JUnit report: ",
411
+ type="bool",
412
+ default=True,
413
+ )
414
+ parser.addini(
415
+ "junit_duration_report",
416
+ "Duration time to report: one of total|call",
417
+ default="total",
418
+ ) # choices=['total', 'call'])
419
+ parser.addini(
420
+ "junit_family",
421
+ "Emit XML for schema: one of legacy|xunit1|xunit2",
422
+ default="xunit2",
423
+ )
424
+
425
+
426
+ def pytest_configure(config: Config) -> None:
427
+ xmlpath = config.option.xmlpath
428
+ # Prevent opening xmllog on worker nodes (xdist).
429
+ if xmlpath and not hasattr(config, "workerinput"):
430
+ junit_family = config.getini("junit_family")
431
+ config.stash[xml_key] = LogXML(
432
+ xmlpath,
433
+ config.option.junitprefix,
434
+ config.getini("junit_suite_name"),
435
+ config.getini("junit_logging"),
436
+ config.getini("junit_duration_report"),
437
+ junit_family,
438
+ config.getini("junit_log_passing_tests"),
439
+ )
440
+ config.pluginmanager.register(config.stash[xml_key])
441
+
442
+
443
+ def pytest_unconfigure(config: Config) -> None:
444
+ xml = config.stash.get(xml_key, None)
445
+ if xml:
446
+ del config.stash[xml_key]
447
+ config.pluginmanager.unregister(xml)
448
+
449
+
450
+ def mangle_test_address(address: str) -> list[str]:
451
+ path, possible_open_bracket, params = address.partition("[")
452
+ names = path.split("::")
453
+ # Convert file path to dotted path.
454
+ names[0] = names[0].replace(nodes.SEP, ".")
455
+ names[0] = re.sub(r"\.py$", "", names[0])
456
+ # Put any params back.
457
+ names[-1] += possible_open_bracket + params
458
+ return names
459
+
460
+
461
+ class LogXML:
462
+ def __init__(
463
+ self,
464
+ logfile,
465
+ prefix: str | None,
466
+ suite_name: str = "pytest",
467
+ logging: str = "no",
468
+ report_duration: str = "total",
469
+ family="xunit1",
470
+ log_passing_tests: bool = True,
471
+ ) -> None:
472
+ logfile = os.path.expanduser(os.path.expandvars(logfile))
473
+ self.logfile = os.path.normpath(os.path.abspath(logfile))
474
+ self.prefix = prefix
475
+ self.suite_name = suite_name
476
+ self.logging = logging
477
+ self.log_passing_tests = log_passing_tests
478
+ self.report_duration = report_duration
479
+ self.family = family
480
+ self.stats: dict[str, int] = dict.fromkeys(
481
+ ["error", "passed", "failure", "skipped"], 0
482
+ )
483
+ self.node_reporters: dict[tuple[str | TestReport, object], _NodeReporter] = {}
484
+ self.node_reporters_ordered: list[_NodeReporter] = []
485
+ self.global_properties: list[tuple[str, str]] = []
486
+
487
+ # List of reports that failed on call but teardown is pending.
488
+ self.open_reports: list[TestReport] = []
489
+ self.cnt_double_fail_tests = 0
490
+
491
+ # Replaces convenience family with real family.
492
+ if self.family == "legacy":
493
+ self.family = "xunit1"
494
+
495
+ def finalize(self, report: TestReport) -> None:
496
+ nodeid = getattr(report, "nodeid", report)
497
+ # Local hack to handle xdist report order.
498
+ workernode = getattr(report, "node", None)
499
+ reporter = self.node_reporters.pop((nodeid, workernode))
500
+
501
+ for propname, propvalue in report.user_properties:
502
+ reporter.add_property(propname, str(propvalue))
503
+
504
+ if reporter is not None:
505
+ reporter.finalize()
506
+
507
+ def node_reporter(self, report: TestReport | str) -> _NodeReporter:
508
+ nodeid: str | TestReport = getattr(report, "nodeid", report)
509
+ # Local hack to handle xdist report order.
510
+ workernode = getattr(report, "node", None)
511
+
512
+ key = nodeid, workernode
513
+
514
+ if key in self.node_reporters:
515
+ # TODO: breaks for --dist=each
516
+ return self.node_reporters[key]
517
+
518
+ reporter = _NodeReporter(nodeid, self)
519
+
520
+ self.node_reporters[key] = reporter
521
+ self.node_reporters_ordered.append(reporter)
522
+
523
+ return reporter
524
+
525
+ def add_stats(self, key: str) -> None:
526
+ if key in self.stats:
527
+ self.stats[key] += 1
528
+
529
+ def _opentestcase(self, report: TestReport) -> _NodeReporter:
530
+ reporter = self.node_reporter(report)
531
+ reporter.record_testreport(report)
532
+ return reporter
533
+
534
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
535
+ """Handle a setup/call/teardown report, generating the appropriate
536
+ XML tags as necessary.
537
+
538
+ Note: due to plugins like xdist, this hook may be called in interlaced
539
+ order with reports from other nodes. For example:
540
+
541
+ Usual call order:
542
+ -> setup node1
543
+ -> call node1
544
+ -> teardown node1
545
+ -> setup node2
546
+ -> call node2
547
+ -> teardown node2
548
+
549
+ Possible call order in xdist:
550
+ -> setup node1
551
+ -> call node1
552
+ -> setup node2
553
+ -> call node2
554
+ -> teardown node2
555
+ -> teardown node1
556
+ """
557
+ close_report = None
558
+ if report.passed:
559
+ if report.when == "call": # ignore setup/teardown
560
+ reporter = self._opentestcase(report)
561
+ reporter.append_pass(report)
562
+ elif report.failed:
563
+ if report.when == "teardown":
564
+ # The following vars are needed when xdist plugin is used.
565
+ report_wid = getattr(report, "worker_id", None)
566
+ report_ii = getattr(report, "item_index", None)
567
+ close_report = next(
568
+ (
569
+ rep
570
+ for rep in self.open_reports
571
+ if (
572
+ rep.nodeid == report.nodeid
573
+ and getattr(rep, "item_index", None) == report_ii
574
+ and getattr(rep, "worker_id", None) == report_wid
575
+ )
576
+ ),
577
+ None,
578
+ )
579
+ if close_report:
580
+ # We need to open new testcase in case we have failure in
581
+ # call and error in teardown in order to follow junit
582
+ # schema.
583
+ self.finalize(close_report)
584
+ self.cnt_double_fail_tests += 1
585
+ reporter = self._opentestcase(report)
586
+ if report.when == "call":
587
+ reporter.append_failure(report)
588
+ self.open_reports.append(report)
589
+ if not self.log_passing_tests:
590
+ reporter.write_captured_output(report)
591
+ else:
592
+ reporter.append_error(report)
593
+ elif report.skipped:
594
+ reporter = self._opentestcase(report)
595
+ reporter.append_skipped(report)
596
+ self.update_testcase_duration(report)
597
+ if report.when == "teardown":
598
+ reporter = self._opentestcase(report)
599
+ reporter.write_captured_output(report)
600
+
601
+ self.finalize(report)
602
+ report_wid = getattr(report, "worker_id", None)
603
+ report_ii = getattr(report, "item_index", None)
604
+ close_report = next(
605
+ (
606
+ rep
607
+ for rep in self.open_reports
608
+ if (
609
+ rep.nodeid == report.nodeid
610
+ and getattr(rep, "item_index", None) == report_ii
611
+ and getattr(rep, "worker_id", None) == report_wid
612
+ )
613
+ ),
614
+ None,
615
+ )
616
+ if close_report:
617
+ self.open_reports.remove(close_report)
618
+
619
+ def update_testcase_duration(self, report: TestReport) -> None:
620
+ """Accumulate total duration for nodeid from given report and update
621
+ the Junit.testcase with the new total if already created."""
622
+ if self.report_duration in {"total", report.when}:
623
+ reporter = self.node_reporter(report)
624
+ reporter.duration += getattr(report, "duration", 0.0)
625
+
626
+ def pytest_collectreport(self, report: TestReport) -> None:
627
+ if not report.passed:
628
+ reporter = self._opentestcase(report)
629
+ if report.failed:
630
+ reporter.append_collect_error(report)
631
+ else:
632
+ reporter.append_collect_skipped(report)
633
+
634
+ def pytest_internalerror(self, excrepr: ExceptionRepr) -> None:
635
+ reporter = self.node_reporter("internal")
636
+ reporter.attrs.update(classname="pytest", name="internal")
637
+ reporter._add_simple("error", "internal error", str(excrepr))
638
+
639
+ def pytest_sessionstart(self) -> None:
640
+ self.suite_start_time = timing.time()
641
+
642
+ def pytest_sessionfinish(self) -> None:
643
+ dirname = os.path.dirname(os.path.abspath(self.logfile))
644
+ # exist_ok avoids filesystem race conditions between checking path existence and requesting creation
645
+ os.makedirs(dirname, exist_ok=True)
646
+
647
+ with open(self.logfile, "w", encoding="utf-8") as logfile:
648
+ suite_stop_time = timing.time()
649
+ suite_time_delta = suite_stop_time - self.suite_start_time
650
+
651
+ numtests = (
652
+ self.stats["passed"]
653
+ + self.stats["failure"]
654
+ + self.stats["skipped"]
655
+ + self.stats["error"]
656
+ - self.cnt_double_fail_tests
657
+ )
658
+ logfile.write('<?xml version="1.0" encoding="utf-8"?>')
659
+
660
+ suite_node = ET.Element(
661
+ "testsuite",
662
+ name=self.suite_name,
663
+ errors=str(self.stats["error"]),
664
+ failures=str(self.stats["failure"]),
665
+ skipped=str(self.stats["skipped"]),
666
+ tests=str(numtests),
667
+ time=f"{suite_time_delta:.3f}",
668
+ timestamp=datetime.fromtimestamp(self.suite_start_time, timezone.utc)
669
+ .astimezone()
670
+ .isoformat(),
671
+ hostname=platform.node(),
672
+ )
673
+ global_properties = self._get_global_properties_node()
674
+ if global_properties is not None:
675
+ suite_node.append(global_properties)
676
+ for node_reporter in self.node_reporters_ordered:
677
+ suite_node.append(node_reporter.to_xml())
678
+ testsuites = ET.Element("testsuites")
679
+ testsuites.append(suite_node)
680
+ logfile.write(ET.tostring(testsuites, encoding="unicode"))
681
+
682
+ def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
683
+ terminalreporter.write_sep("-", f"generated xml file: {self.logfile}")
684
+
685
+ def add_global_property(self, name: str, value: object) -> None:
686
+ __tracebackhide__ = True
687
+ _check_record_param_type("name", name)
688
+ self.global_properties.append((name, bin_xml_escape(value)))
689
+
690
+ def _get_global_properties_node(self) -> ET.Element | None:
691
+ """Return a Junit node containing custom properties, if any."""
692
+ if self.global_properties:
693
+ properties = ET.Element("properties")
694
+ for name, value in self.global_properties:
695
+ properties.append(ET.Element("property", name=name, value=value))
696
+ return properties
697
+ return None
.venv/lib/python3.11/site-packages/_pytest/main.py ADDED
@@ -0,0 +1,1072 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core implementation of the testing process: init, session, runtest loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import dataclasses
7
+ import fnmatch
8
+ import functools
9
+ import importlib
10
+ import importlib.util
11
+ import os
12
+ from pathlib import Path
13
+ import sys
14
+ from typing import AbstractSet
15
+ from typing import Callable
16
+ from typing import Dict
17
+ from typing import final
18
+ from typing import Iterable
19
+ from typing import Iterator
20
+ from typing import Literal
21
+ from typing import overload
22
+ from typing import Sequence
23
+ from typing import TYPE_CHECKING
24
+ import warnings
25
+
26
+ import pluggy
27
+
28
+ from _pytest import nodes
29
+ import _pytest._code
30
+ from _pytest.config import Config
31
+ from _pytest.config import directory_arg
32
+ from _pytest.config import ExitCode
33
+ from _pytest.config import hookimpl
34
+ from _pytest.config import PytestPluginManager
35
+ from _pytest.config import UsageError
36
+ from _pytest.config.argparsing import Parser
37
+ from _pytest.config.compat import PathAwareHookProxy
38
+ from _pytest.outcomes import exit
39
+ from _pytest.pathlib import absolutepath
40
+ from _pytest.pathlib import bestrelpath
41
+ from _pytest.pathlib import fnmatch_ex
42
+ from _pytest.pathlib import safe_exists
43
+ from _pytest.pathlib import scandir
44
+ from _pytest.reports import CollectReport
45
+ from _pytest.reports import TestReport
46
+ from _pytest.runner import collect_one_node
47
+ from _pytest.runner import SetupState
48
+ from _pytest.warning_types import PytestWarning
49
+
50
+
51
+ if TYPE_CHECKING:
52
+ from typing_extensions import Self
53
+
54
+ from _pytest.fixtures import FixtureManager
55
+
56
+
57
+ def pytest_addoption(parser: Parser) -> None:
58
+ parser.addini(
59
+ "norecursedirs",
60
+ "Directory patterns to avoid for recursion",
61
+ type="args",
62
+ default=[
63
+ "*.egg",
64
+ ".*",
65
+ "_darcs",
66
+ "build",
67
+ "CVS",
68
+ "dist",
69
+ "node_modules",
70
+ "venv",
71
+ "{arch}",
72
+ ],
73
+ )
74
+ parser.addini(
75
+ "testpaths",
76
+ "Directories to search for tests when no files or directories are given on the "
77
+ "command line",
78
+ type="args",
79
+ default=[],
80
+ )
81
+ group = parser.getgroup("general", "Running and selection options")
82
+ group._addoption(
83
+ "-x",
84
+ "--exitfirst",
85
+ action="store_const",
86
+ dest="maxfail",
87
+ const=1,
88
+ help="Exit instantly on first error or failed test",
89
+ )
90
+ group = parser.getgroup("pytest-warnings")
91
+ group.addoption(
92
+ "-W",
93
+ "--pythonwarnings",
94
+ action="append",
95
+ help="Set which warnings to report, see -W option of Python itself",
96
+ )
97
+ parser.addini(
98
+ "filterwarnings",
99
+ type="linelist",
100
+ help="Each line specifies a pattern for "
101
+ "warnings.filterwarnings. "
102
+ "Processed after -W/--pythonwarnings.",
103
+ )
104
+ group._addoption(
105
+ "--maxfail",
106
+ metavar="num",
107
+ action="store",
108
+ type=int,
109
+ dest="maxfail",
110
+ default=0,
111
+ help="Exit after first num failures or errors",
112
+ )
113
+ group._addoption(
114
+ "--strict-config",
115
+ action="store_true",
116
+ help="Any warnings encountered while parsing the `pytest` section of the "
117
+ "configuration file raise errors",
118
+ )
119
+ group._addoption(
120
+ "--strict-markers",
121
+ action="store_true",
122
+ help="Markers not registered in the `markers` section of the configuration "
123
+ "file raise errors",
124
+ )
125
+ group._addoption(
126
+ "--strict",
127
+ action="store_true",
128
+ help="(Deprecated) alias to --strict-markers",
129
+ )
130
+ group._addoption(
131
+ "-c",
132
+ "--config-file",
133
+ metavar="FILE",
134
+ type=str,
135
+ dest="inifilename",
136
+ help="Load configuration from `FILE` instead of trying to locate one of the "
137
+ "implicit configuration files.",
138
+ )
139
+ group._addoption(
140
+ "--continue-on-collection-errors",
141
+ action="store_true",
142
+ default=False,
143
+ dest="continue_on_collection_errors",
144
+ help="Force test execution even if collection errors occur",
145
+ )
146
+ group._addoption(
147
+ "--rootdir",
148
+ action="store",
149
+ dest="rootdir",
150
+ help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', "
151
+ "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: "
152
+ "'$HOME/root_dir'.",
153
+ )
154
+
155
+ group = parser.getgroup("collect", "collection")
156
+ group.addoption(
157
+ "--collectonly",
158
+ "--collect-only",
159
+ "--co",
160
+ action="store_true",
161
+ help="Only collect tests, don't execute them",
162
+ )
163
+ group.addoption(
164
+ "--pyargs",
165
+ action="store_true",
166
+ help="Try to interpret all arguments as Python packages",
167
+ )
168
+ group.addoption(
169
+ "--ignore",
170
+ action="append",
171
+ metavar="path",
172
+ help="Ignore path during collection (multi-allowed)",
173
+ )
174
+ group.addoption(
175
+ "--ignore-glob",
176
+ action="append",
177
+ metavar="path",
178
+ help="Ignore path pattern during collection (multi-allowed)",
179
+ )
180
+ group.addoption(
181
+ "--deselect",
182
+ action="append",
183
+ metavar="nodeid_prefix",
184
+ help="Deselect item (via node id prefix) during collection (multi-allowed)",
185
+ )
186
+ group.addoption(
187
+ "--confcutdir",
188
+ dest="confcutdir",
189
+ default=None,
190
+ metavar="dir",
191
+ type=functools.partial(directory_arg, optname="--confcutdir"),
192
+ help="Only load conftest.py's relative to specified dir",
193
+ )
194
+ group.addoption(
195
+ "--noconftest",
196
+ action="store_true",
197
+ dest="noconftest",
198
+ default=False,
199
+ help="Don't load any conftest.py files",
200
+ )
201
+ group.addoption(
202
+ "--keepduplicates",
203
+ "--keep-duplicates",
204
+ action="store_true",
205
+ dest="keepduplicates",
206
+ default=False,
207
+ help="Keep duplicate tests",
208
+ )
209
+ group.addoption(
210
+ "--collect-in-virtualenv",
211
+ action="store_true",
212
+ dest="collect_in_virtualenv",
213
+ default=False,
214
+ help="Don't ignore tests in a local virtualenv directory",
215
+ )
216
+ group.addoption(
217
+ "--import-mode",
218
+ default="prepend",
219
+ choices=["prepend", "append", "importlib"],
220
+ dest="importmode",
221
+ help="Prepend/append to sys.path when importing test modules and conftest "
222
+ "files. Default: prepend.",
223
+ )
224
+ parser.addini(
225
+ "consider_namespace_packages",
226
+ type="bool",
227
+ default=False,
228
+ help="Consider namespace packages when resolving module names during import",
229
+ )
230
+
231
+ group = parser.getgroup("debugconfig", "test session debugging and configuration")
232
+ group.addoption(
233
+ "--basetemp",
234
+ dest="basetemp",
235
+ default=None,
236
+ type=validate_basetemp,
237
+ metavar="dir",
238
+ help=(
239
+ "Base temporary directory for this test run. "
240
+ "(Warning: this directory is removed if it exists.)"
241
+ ),
242
+ )
243
+
244
+
245
+ def validate_basetemp(path: str) -> str:
246
+ # GH 7119
247
+ msg = "basetemp must not be empty, the current working directory or any parent directory of it"
248
+
249
+ # empty path
250
+ if not path:
251
+ raise argparse.ArgumentTypeError(msg)
252
+
253
+ def is_ancestor(base: Path, query: Path) -> bool:
254
+ """Return whether query is an ancestor of base."""
255
+ if base == query:
256
+ return True
257
+ return query in base.parents
258
+
259
+ # check if path is an ancestor of cwd
260
+ if is_ancestor(Path.cwd(), Path(path).absolute()):
261
+ raise argparse.ArgumentTypeError(msg)
262
+
263
+ # check symlinks for ancestors
264
+ if is_ancestor(Path.cwd().resolve(), Path(path).resolve()):
265
+ raise argparse.ArgumentTypeError(msg)
266
+
267
+ return path
268
+
269
+
270
+ def wrap_session(
271
+ config: Config, doit: Callable[[Config, Session], int | ExitCode | None]
272
+ ) -> int | ExitCode:
273
+ """Skeleton command line program."""
274
+ session = Session.from_config(config)
275
+ session.exitstatus = ExitCode.OK
276
+ initstate = 0
277
+ try:
278
+ try:
279
+ config._do_configure()
280
+ initstate = 1
281
+ config.hook.pytest_sessionstart(session=session)
282
+ initstate = 2
283
+ session.exitstatus = doit(config, session) or 0
284
+ except UsageError:
285
+ session.exitstatus = ExitCode.USAGE_ERROR
286
+ raise
287
+ except Failed:
288
+ session.exitstatus = ExitCode.TESTS_FAILED
289
+ except (KeyboardInterrupt, exit.Exception):
290
+ excinfo = _pytest._code.ExceptionInfo.from_current()
291
+ exitstatus: int | ExitCode = ExitCode.INTERRUPTED
292
+ if isinstance(excinfo.value, exit.Exception):
293
+ if excinfo.value.returncode is not None:
294
+ exitstatus = excinfo.value.returncode
295
+ if initstate < 2:
296
+ sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n")
297
+ config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
298
+ session.exitstatus = exitstatus
299
+ except BaseException:
300
+ session.exitstatus = ExitCode.INTERNAL_ERROR
301
+ excinfo = _pytest._code.ExceptionInfo.from_current()
302
+ try:
303
+ config.notify_exception(excinfo, config.option)
304
+ except exit.Exception as exc:
305
+ if exc.returncode is not None:
306
+ session.exitstatus = exc.returncode
307
+ sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
308
+ else:
309
+ if isinstance(excinfo.value, SystemExit):
310
+ sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
311
+
312
+ finally:
313
+ # Explicitly break reference cycle.
314
+ excinfo = None # type: ignore
315
+ os.chdir(session.startpath)
316
+ if initstate >= 2:
317
+ try:
318
+ config.hook.pytest_sessionfinish(
319
+ session=session, exitstatus=session.exitstatus
320
+ )
321
+ except exit.Exception as exc:
322
+ if exc.returncode is not None:
323
+ session.exitstatus = exc.returncode
324
+ sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
325
+ config._ensure_unconfigure()
326
+ return session.exitstatus
327
+
328
+
329
+ def pytest_cmdline_main(config: Config) -> int | ExitCode:
330
+ return wrap_session(config, _main)
331
+
332
+
333
+ def _main(config: Config, session: Session) -> int | ExitCode | None:
334
+ """Default command line protocol for initialization, session,
335
+ running tests and reporting."""
336
+ config.hook.pytest_collection(session=session)
337
+ config.hook.pytest_runtestloop(session=session)
338
+
339
+ if session.testsfailed:
340
+ return ExitCode.TESTS_FAILED
341
+ elif session.testscollected == 0:
342
+ return ExitCode.NO_TESTS_COLLECTED
343
+ return None
344
+
345
+
346
+ def pytest_collection(session: Session) -> None:
347
+ session.perform_collect()
348
+
349
+
350
+ def pytest_runtestloop(session: Session) -> bool:
351
+ if session.testsfailed and not session.config.option.continue_on_collection_errors:
352
+ raise session.Interrupted(
353
+ "%d error%s during collection"
354
+ % (session.testsfailed, "s" if session.testsfailed != 1 else "")
355
+ )
356
+
357
+ if session.config.option.collectonly:
358
+ return True
359
+
360
+ for i, item in enumerate(session.items):
361
+ nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
362
+ item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
363
+ if session.shouldfail:
364
+ raise session.Failed(session.shouldfail)
365
+ if session.shouldstop:
366
+ raise session.Interrupted(session.shouldstop)
367
+ return True
368
+
369
+
370
+ def _in_venv(path: Path) -> bool:
371
+ """Attempt to detect if ``path`` is the root of a Virtual Environment by
372
+ checking for the existence of the pyvenv.cfg file.
373
+
374
+ [https://peps.python.org/pep-0405/]
375
+
376
+ For regression protection we also check for conda environments that do not include pyenv.cfg yet --
377
+ https://github.com/conda/conda/issues/13337 is the conda issue tracking adding pyenv.cfg.
378
+
379
+ Checking for the `conda-meta/history` file per https://github.com/pytest-dev/pytest/issues/12652#issuecomment-2246336902.
380
+
381
+ """
382
+ try:
383
+ return (
384
+ path.joinpath("pyvenv.cfg").is_file()
385
+ or path.joinpath("conda-meta", "history").is_file()
386
+ )
387
+ except OSError:
388
+ return False
389
+
390
+
391
+ def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None:
392
+ if collection_path.name == "__pycache__":
393
+ return True
394
+
395
+ ignore_paths = config._getconftest_pathlist(
396
+ "collect_ignore", path=collection_path.parent
397
+ )
398
+ ignore_paths = ignore_paths or []
399
+ excludeopt = config.getoption("ignore")
400
+ if excludeopt:
401
+ ignore_paths.extend(absolutepath(x) for x in excludeopt)
402
+
403
+ if collection_path in ignore_paths:
404
+ return True
405
+
406
+ ignore_globs = config._getconftest_pathlist(
407
+ "collect_ignore_glob", path=collection_path.parent
408
+ )
409
+ ignore_globs = ignore_globs or []
410
+ excludeglobopt = config.getoption("ignore_glob")
411
+ if excludeglobopt:
412
+ ignore_globs.extend(absolutepath(x) for x in excludeglobopt)
413
+
414
+ if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs):
415
+ return True
416
+
417
+ allow_in_venv = config.getoption("collect_in_virtualenv")
418
+ if not allow_in_venv and _in_venv(collection_path):
419
+ return True
420
+
421
+ if collection_path.is_dir():
422
+ norecursepatterns = config.getini("norecursedirs")
423
+ if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
424
+ return True
425
+
426
+ return None
427
+
428
+
429
+ def pytest_collect_directory(
430
+ path: Path, parent: nodes.Collector
431
+ ) -> nodes.Collector | None:
432
+ return Dir.from_parent(parent, path=path)
433
+
434
+
435
+ def pytest_collection_modifyitems(items: list[nodes.Item], config: Config) -> None:
436
+ deselect_prefixes = tuple(config.getoption("deselect") or [])
437
+ if not deselect_prefixes:
438
+ return
439
+
440
+ remaining = []
441
+ deselected = []
442
+ for colitem in items:
443
+ if colitem.nodeid.startswith(deselect_prefixes):
444
+ deselected.append(colitem)
445
+ else:
446
+ remaining.append(colitem)
447
+
448
+ if deselected:
449
+ config.hook.pytest_deselected(items=deselected)
450
+ items[:] = remaining
451
+
452
+
453
+ class FSHookProxy:
454
+ def __init__(
455
+ self,
456
+ pm: PytestPluginManager,
457
+ remove_mods: AbstractSet[object],
458
+ ) -> None:
459
+ self.pm = pm
460
+ self.remove_mods = remove_mods
461
+
462
+ def __getattr__(self, name: str) -> pluggy.HookCaller:
463
+ x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
464
+ self.__dict__[name] = x
465
+ return x
466
+
467
+
468
+ class Interrupted(KeyboardInterrupt):
469
+ """Signals that the test run was interrupted."""
470
+
471
+ __module__ = "builtins" # For py3.
472
+
473
+
474
+ class Failed(Exception):
475
+ """Signals a stop as failed test run."""
476
+
477
+
478
+ @dataclasses.dataclass
479
+ class _bestrelpath_cache(Dict[Path, str]):
480
+ __slots__ = ("path",)
481
+
482
+ path: Path
483
+
484
+ def __missing__(self, path: Path) -> str:
485
+ r = bestrelpath(self.path, path)
486
+ self[path] = r
487
+ return r
488
+
489
+
490
+ @final
491
+ class Dir(nodes.Directory):
492
+ """Collector of files in a file system directory.
493
+
494
+ .. versionadded:: 8.0
495
+
496
+ .. note::
497
+
498
+ Python directories with an `__init__.py` file are instead collected by
499
+ :class:`~pytest.Package` by default. Both are :class:`~pytest.Directory`
500
+ collectors.
501
+ """
502
+
503
+ @classmethod
504
+ def from_parent( # type: ignore[override]
505
+ cls,
506
+ parent: nodes.Collector,
507
+ *,
508
+ path: Path,
509
+ ) -> Self:
510
+ """The public constructor.
511
+
512
+ :param parent: The parent collector of this Dir.
513
+ :param path: The directory's path.
514
+ :type path: pathlib.Path
515
+ """
516
+ return super().from_parent(parent=parent, path=path)
517
+
518
+ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
519
+ config = self.config
520
+ col: nodes.Collector | None
521
+ cols: Sequence[nodes.Collector]
522
+ ihook = self.ihook
523
+ for direntry in scandir(self.path):
524
+ if direntry.is_dir():
525
+ path = Path(direntry.path)
526
+ if not self.session.isinitpath(path, with_parents=True):
527
+ if ihook.pytest_ignore_collect(collection_path=path, config=config):
528
+ continue
529
+ col = ihook.pytest_collect_directory(path=path, parent=self)
530
+ if col is not None:
531
+ yield col
532
+
533
+ elif direntry.is_file():
534
+ path = Path(direntry.path)
535
+ if not self.session.isinitpath(path):
536
+ if ihook.pytest_ignore_collect(collection_path=path, config=config):
537
+ continue
538
+ cols = ihook.pytest_collect_file(file_path=path, parent=self)
539
+ yield from cols
540
+
541
+
542
+ @final
543
+ class Session(nodes.Collector):
544
+ """The root of the collection tree.
545
+
546
+ ``Session`` collects the initial paths given as arguments to pytest.
547
+ """
548
+
549
+ Interrupted = Interrupted
550
+ Failed = Failed
551
+ # Set on the session by runner.pytest_sessionstart.
552
+ _setupstate: SetupState
553
+ # Set on the session by fixtures.pytest_sessionstart.
554
+ _fixturemanager: FixtureManager
555
+ exitstatus: int | ExitCode
556
+
557
+ def __init__(self, config: Config) -> None:
558
+ super().__init__(
559
+ name="",
560
+ path=config.rootpath,
561
+ fspath=None,
562
+ parent=None,
563
+ config=config,
564
+ session=self,
565
+ nodeid="",
566
+ )
567
+ self.testsfailed = 0
568
+ self.testscollected = 0
569
+ self._shouldstop: bool | str = False
570
+ self._shouldfail: bool | str = False
571
+ self.trace = config.trace.root.get("collection")
572
+ self._initialpaths: frozenset[Path] = frozenset()
573
+ self._initialpaths_with_parents: frozenset[Path] = frozenset()
574
+ self._notfound: list[tuple[str, Sequence[nodes.Collector]]] = []
575
+ self._initial_parts: list[CollectionArgument] = []
576
+ self._collection_cache: dict[nodes.Collector, CollectReport] = {}
577
+ self.items: list[nodes.Item] = []
578
+
579
+ self._bestrelpathcache: dict[Path, str] = _bestrelpath_cache(config.rootpath)
580
+
581
+ self.config.pluginmanager.register(self, name="session")
582
+
583
+ @classmethod
584
+ def from_config(cls, config: Config) -> Session:
585
+ session: Session = cls._create(config=config)
586
+ return session
587
+
588
+ def __repr__(self) -> str:
589
+ return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % (
590
+ self.__class__.__name__,
591
+ self.name,
592
+ getattr(self, "exitstatus", "<UNSET>"),
593
+ self.testsfailed,
594
+ self.testscollected,
595
+ )
596
+
597
+ @property
598
+ def shouldstop(self) -> bool | str:
599
+ return self._shouldstop
600
+
601
+ @shouldstop.setter
602
+ def shouldstop(self, value: bool | str) -> None:
603
+ # The runner checks shouldfail and assumes that if it is set we are
604
+ # definitely stopping, so prevent unsetting it.
605
+ if value is False and self._shouldstop:
606
+ warnings.warn(
607
+ PytestWarning(
608
+ "session.shouldstop cannot be unset after it has been set; ignoring."
609
+ ),
610
+ stacklevel=2,
611
+ )
612
+ return
613
+ self._shouldstop = value
614
+
615
+ @property
616
+ def shouldfail(self) -> bool | str:
617
+ return self._shouldfail
618
+
619
+ @shouldfail.setter
620
+ def shouldfail(self, value: bool | str) -> None:
621
+ # The runner checks shouldfail and assumes that if it is set we are
622
+ # definitely stopping, so prevent unsetting it.
623
+ if value is False and self._shouldfail:
624
+ warnings.warn(
625
+ PytestWarning(
626
+ "session.shouldfail cannot be unset after it has been set; ignoring."
627
+ ),
628
+ stacklevel=2,
629
+ )
630
+ return
631
+ self._shouldfail = value
632
+
633
+ @property
634
+ def startpath(self) -> Path:
635
+ """The path from which pytest was invoked.
636
+
637
+ .. versionadded:: 7.0.0
638
+ """
639
+ return self.config.invocation_params.dir
640
+
641
+ def _node_location_to_relpath(self, node_path: Path) -> str:
642
+ # bestrelpath is a quite slow function.
643
+ return self._bestrelpathcache[node_path]
644
+
645
+ @hookimpl(tryfirst=True)
646
+ def pytest_collectstart(self) -> None:
647
+ if self.shouldfail:
648
+ raise self.Failed(self.shouldfail)
649
+ if self.shouldstop:
650
+ raise self.Interrupted(self.shouldstop)
651
+
652
+ @hookimpl(tryfirst=True)
653
+ def pytest_runtest_logreport(self, report: TestReport | CollectReport) -> None:
654
+ if report.failed and not hasattr(report, "wasxfail"):
655
+ self.testsfailed += 1
656
+ maxfail = self.config.getvalue("maxfail")
657
+ if maxfail and self.testsfailed >= maxfail:
658
+ self.shouldfail = "stopping after %d failures" % (self.testsfailed)
659
+
660
+ pytest_collectreport = pytest_runtest_logreport
661
+
662
+ def isinitpath(
663
+ self,
664
+ path: str | os.PathLike[str],
665
+ *,
666
+ with_parents: bool = False,
667
+ ) -> bool:
668
+ """Is path an initial path?
669
+
670
+ An initial path is a path explicitly given to pytest on the command
671
+ line.
672
+
673
+ :param with_parents:
674
+ If set, also return True if the path is a parent of an initial path.
675
+
676
+ .. versionchanged:: 8.0
677
+ Added the ``with_parents`` parameter.
678
+ """
679
+ # Optimization: Path(Path(...)) is much slower than isinstance.
680
+ path_ = path if isinstance(path, Path) else Path(path)
681
+ if with_parents:
682
+ return path_ in self._initialpaths_with_parents
683
+ else:
684
+ return path_ in self._initialpaths
685
+
686
+ def gethookproxy(self, fspath: os.PathLike[str]) -> pluggy.HookRelay:
687
+ # Optimization: Path(Path(...)) is much slower than isinstance.
688
+ path = fspath if isinstance(fspath, Path) else Path(fspath)
689
+ pm = self.config.pluginmanager
690
+ # Check if we have the common case of running
691
+ # hooks with all conftest.py files.
692
+ my_conftestmodules = pm._getconftestmodules(path)
693
+ remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
694
+ proxy: pluggy.HookRelay
695
+ if remove_mods:
696
+ # One or more conftests are not in use at this path.
697
+ proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment]
698
+ else:
699
+ # All plugins are active for this fspath.
700
+ proxy = self.config.hook
701
+ return proxy
702
+
703
+ def _collect_path(
704
+ self,
705
+ path: Path,
706
+ path_cache: dict[Path, Sequence[nodes.Collector]],
707
+ ) -> Sequence[nodes.Collector]:
708
+ """Create a Collector for the given path.
709
+
710
+ `path_cache` makes it so the same Collectors are returned for the same
711
+ path.
712
+ """
713
+ if path in path_cache:
714
+ return path_cache[path]
715
+
716
+ if path.is_dir():
717
+ ihook = self.gethookproxy(path.parent)
718
+ col: nodes.Collector | None = ihook.pytest_collect_directory(
719
+ path=path, parent=self
720
+ )
721
+ cols: Sequence[nodes.Collector] = (col,) if col is not None else ()
722
+
723
+ elif path.is_file():
724
+ ihook = self.gethookproxy(path)
725
+ cols = ihook.pytest_collect_file(file_path=path, parent=self)
726
+
727
+ else:
728
+ # Broken symlink or invalid/missing file.
729
+ cols = ()
730
+
731
+ path_cache[path] = cols
732
+ return cols
733
+
734
+ @overload
735
+ def perform_collect(
736
+ self, args: Sequence[str] | None = ..., genitems: Literal[True] = ...
737
+ ) -> Sequence[nodes.Item]: ...
738
+
739
+ @overload
740
+ def perform_collect(
741
+ self, args: Sequence[str] | None = ..., genitems: bool = ...
742
+ ) -> Sequence[nodes.Item | nodes.Collector]: ...
743
+
744
+ def perform_collect(
745
+ self, args: Sequence[str] | None = None, genitems: bool = True
746
+ ) -> Sequence[nodes.Item | nodes.Collector]:
747
+ """Perform the collection phase for this session.
748
+
749
+ This is called by the default :hook:`pytest_collection` hook
750
+ implementation; see the documentation of this hook for more details.
751
+ For testing purposes, it may also be called directly on a fresh
752
+ ``Session``.
753
+
754
+ This function normally recursively expands any collectors collected
755
+ from the session to their items, and only items are returned. For
756
+ testing purposes, this may be suppressed by passing ``genitems=False``,
757
+ in which case the return value contains these collectors unexpanded,
758
+ and ``session.items`` is empty.
759
+ """
760
+ if args is None:
761
+ args = self.config.args
762
+
763
+ self.trace("perform_collect", self, args)
764
+ self.trace.root.indent += 1
765
+
766
+ hook = self.config.hook
767
+
768
+ self._notfound = []
769
+ self._initial_parts = []
770
+ self._collection_cache = {}
771
+ self.items = []
772
+ items: Sequence[nodes.Item | nodes.Collector] = self.items
773
+ try:
774
+ initialpaths: list[Path] = []
775
+ initialpaths_with_parents: list[Path] = []
776
+ for arg in args:
777
+ collection_argument = resolve_collection_argument(
778
+ self.config.invocation_params.dir,
779
+ arg,
780
+ as_pypath=self.config.option.pyargs,
781
+ )
782
+ self._initial_parts.append(collection_argument)
783
+ initialpaths.append(collection_argument.path)
784
+ initialpaths_with_parents.append(collection_argument.path)
785
+ initialpaths_with_parents.extend(collection_argument.path.parents)
786
+ self._initialpaths = frozenset(initialpaths)
787
+ self._initialpaths_with_parents = frozenset(initialpaths_with_parents)
788
+
789
+ rep = collect_one_node(self)
790
+ self.ihook.pytest_collectreport(report=rep)
791
+ self.trace.root.indent -= 1
792
+ if self._notfound:
793
+ errors = []
794
+ for arg, collectors in self._notfound:
795
+ if collectors:
796
+ errors.append(
797
+ f"not found: {arg}\n(no match in any of {collectors!r})"
798
+ )
799
+ else:
800
+ errors.append(f"found no collectors for {arg}")
801
+
802
+ raise UsageError(*errors)
803
+
804
+ if not genitems:
805
+ items = rep.result
806
+ else:
807
+ if rep.passed:
808
+ for node in rep.result:
809
+ self.items.extend(self.genitems(node))
810
+
811
+ self.config.pluginmanager.check_pending()
812
+ hook.pytest_collection_modifyitems(
813
+ session=self, config=self.config, items=items
814
+ )
815
+ finally:
816
+ self._notfound = []
817
+ self._initial_parts = []
818
+ self._collection_cache = {}
819
+ hook.pytest_collection_finish(session=self)
820
+
821
+ if genitems:
822
+ self.testscollected = len(items)
823
+
824
+ return items
825
+
826
+ def _collect_one_node(
827
+ self,
828
+ node: nodes.Collector,
829
+ handle_dupes: bool = True,
830
+ ) -> tuple[CollectReport, bool]:
831
+ if node in self._collection_cache and handle_dupes:
832
+ rep = self._collection_cache[node]
833
+ return rep, True
834
+ else:
835
+ rep = collect_one_node(node)
836
+ self._collection_cache[node] = rep
837
+ return rep, False
838
+
839
+ def collect(self) -> Iterator[nodes.Item | nodes.Collector]:
840
+ # This is a cache for the root directories of the initial paths.
841
+ # We can't use collection_cache for Session because of its special
842
+ # role as the bootstrapping collector.
843
+ path_cache: dict[Path, Sequence[nodes.Collector]] = {}
844
+
845
+ pm = self.config.pluginmanager
846
+
847
+ for collection_argument in self._initial_parts:
848
+ self.trace("processing argument", collection_argument)
849
+ self.trace.root.indent += 1
850
+
851
+ argpath = collection_argument.path
852
+ names = collection_argument.parts
853
+ module_name = collection_argument.module_name
854
+
855
+ # resolve_collection_argument() ensures this.
856
+ if argpath.is_dir():
857
+ assert not names, f"invalid arg {(argpath, names)!r}"
858
+
859
+ paths = [argpath]
860
+ # Add relevant parents of the path, from the root, e.g.
861
+ # /a/b/c.py -> [/, /a, /a/b, /a/b/c.py]
862
+ if module_name is None:
863
+ # Paths outside of the confcutdir should not be considered.
864
+ for path in argpath.parents:
865
+ if not pm._is_in_confcutdir(path):
866
+ break
867
+ paths.insert(0, path)
868
+ else:
869
+ # For --pyargs arguments, only consider paths matching the module
870
+ # name. Paths beyond the package hierarchy are not included.
871
+ module_name_parts = module_name.split(".")
872
+ for i, path in enumerate(argpath.parents, 2):
873
+ if i > len(module_name_parts) or path.stem != module_name_parts[-i]:
874
+ break
875
+ paths.insert(0, path)
876
+
877
+ # Start going over the parts from the root, collecting each level
878
+ # and discarding all nodes which don't match the level's part.
879
+ any_matched_in_initial_part = False
880
+ notfound_collectors = []
881
+ work: list[tuple[nodes.Collector | nodes.Item, list[Path | str]]] = [
882
+ (self, [*paths, *names])
883
+ ]
884
+ while work:
885
+ matchnode, matchparts = work.pop()
886
+
887
+ # Pop'd all of the parts, this is a match.
888
+ if not matchparts:
889
+ yield matchnode
890
+ any_matched_in_initial_part = True
891
+ continue
892
+
893
+ # Should have been matched by now, discard.
894
+ if not isinstance(matchnode, nodes.Collector):
895
+ continue
896
+
897
+ # Collect this level of matching.
898
+ # Collecting Session (self) is done directly to avoid endless
899
+ # recursion to this function.
900
+ subnodes: Sequence[nodes.Collector | nodes.Item]
901
+ if isinstance(matchnode, Session):
902
+ assert isinstance(matchparts[0], Path)
903
+ subnodes = matchnode._collect_path(matchparts[0], path_cache)
904
+ else:
905
+ # For backward compat, files given directly multiple
906
+ # times on the command line should not be deduplicated.
907
+ handle_dupes = not (
908
+ len(matchparts) == 1
909
+ and isinstance(matchparts[0], Path)
910
+ and matchparts[0].is_file()
911
+ )
912
+ rep, duplicate = self._collect_one_node(matchnode, handle_dupes)
913
+ if not duplicate and not rep.passed:
914
+ # Report collection failures here to avoid failing to
915
+ # run some test specified in the command line because
916
+ # the module could not be imported (#134).
917
+ matchnode.ihook.pytest_collectreport(report=rep)
918
+ if not rep.passed:
919
+ continue
920
+ subnodes = rep.result
921
+
922
+ # Prune this level.
923
+ any_matched_in_collector = False
924
+ for node in reversed(subnodes):
925
+ # Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`.
926
+ if isinstance(matchparts[0], Path):
927
+ is_match = node.path == matchparts[0]
928
+ if sys.platform == "win32" and not is_match:
929
+ # In case the file paths do not match, fallback to samefile() to
930
+ # account for short-paths on Windows (#11895).
931
+ same_file = os.path.samefile(node.path, matchparts[0])
932
+ # We don't want to match links to the current node,
933
+ # otherwise we would match the same file more than once (#12039).
934
+ is_match = same_file and (
935
+ os.path.islink(node.path)
936
+ == os.path.islink(matchparts[0])
937
+ )
938
+
939
+ # Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
940
+ else:
941
+ # TODO: Remove parametrized workaround once collection structure contains
942
+ # parametrization.
943
+ is_match = (
944
+ node.name == matchparts[0]
945
+ or node.name.split("[")[0] == matchparts[0]
946
+ )
947
+ if is_match:
948
+ work.append((node, matchparts[1:]))
949
+ any_matched_in_collector = True
950
+
951
+ if not any_matched_in_collector:
952
+ notfound_collectors.append(matchnode)
953
+
954
+ if not any_matched_in_initial_part:
955
+ report_arg = "::".join((str(argpath), *names))
956
+ self._notfound.append((report_arg, notfound_collectors))
957
+
958
+ self.trace.root.indent -= 1
959
+
960
+ def genitems(self, node: nodes.Item | nodes.Collector) -> Iterator[nodes.Item]:
961
+ self.trace("genitems", node)
962
+ if isinstance(node, nodes.Item):
963
+ node.ihook.pytest_itemcollected(item=node)
964
+ yield node
965
+ else:
966
+ assert isinstance(node, nodes.Collector)
967
+ keepduplicates = self.config.getoption("keepduplicates")
968
+ # For backward compat, dedup only applies to files.
969
+ handle_dupes = not (keepduplicates and isinstance(node, nodes.File))
970
+ rep, duplicate = self._collect_one_node(node, handle_dupes)
971
+ if duplicate and not keepduplicates:
972
+ return
973
+ if rep.passed:
974
+ for subnode in rep.result:
975
+ yield from self.genitems(subnode)
976
+ if not duplicate:
977
+ node.ihook.pytest_collectreport(report=rep)
978
+
979
+
980
+ def search_pypath(module_name: str) -> str | None:
981
+ """Search sys.path for the given a dotted module name, and return its file
982
+ system path if found."""
983
+ try:
984
+ spec = importlib.util.find_spec(module_name)
985
+ # AttributeError: looks like package module, but actually filename
986
+ # ImportError: module does not exist
987
+ # ValueError: not a module name
988
+ except (AttributeError, ImportError, ValueError):
989
+ return None
990
+ if spec is None or spec.origin is None or spec.origin == "namespace":
991
+ return None
992
+ elif spec.submodule_search_locations:
993
+ return os.path.dirname(spec.origin)
994
+ else:
995
+ return spec.origin
996
+
997
+
998
+ @dataclasses.dataclass(frozen=True)
999
+ class CollectionArgument:
1000
+ """A resolved collection argument."""
1001
+
1002
+ path: Path
1003
+ parts: Sequence[str]
1004
+ module_name: str | None
1005
+
1006
+
1007
+ def resolve_collection_argument(
1008
+ invocation_path: Path, arg: str, *, as_pypath: bool = False
1009
+ ) -> CollectionArgument:
1010
+ """Parse path arguments optionally containing selection parts and return (fspath, names).
1011
+
1012
+ Command-line arguments can point to files and/or directories, and optionally contain
1013
+ parts for specific tests selection, for example:
1014
+
1015
+ "pkg/tests/test_foo.py::TestClass::test_foo"
1016
+
1017
+ This function ensures the path exists, and returns a resolved `CollectionArgument`:
1018
+
1019
+ CollectionArgument(
1020
+ path=Path("/full/path/to/pkg/tests/test_foo.py"),
1021
+ parts=["TestClass", "test_foo"],
1022
+ module_name=None,
1023
+ )
1024
+
1025
+ When as_pypath is True, expects that the command-line argument actually contains
1026
+ module paths instead of file-system paths:
1027
+
1028
+ "pkg.tests.test_foo::TestClass::test_foo"
1029
+
1030
+ In which case we search sys.path for a matching module, and then return the *path* to the
1031
+ found module, which may look like this:
1032
+
1033
+ CollectionArgument(
1034
+ path=Path("/home/u/myvenv/lib/site-packages/pkg/tests/test_foo.py"),
1035
+ parts=["TestClass", "test_foo"],
1036
+ module_name="pkg.tests.test_foo",
1037
+ )
1038
+
1039
+ If the path doesn't exist, raise UsageError.
1040
+ If the path is a directory and selection parts are present, raise UsageError.
1041
+ """
1042
+ base, squacket, rest = str(arg).partition("[")
1043
+ strpath, *parts = base.split("::")
1044
+ if parts:
1045
+ parts[-1] = f"{parts[-1]}{squacket}{rest}"
1046
+ module_name = None
1047
+ if as_pypath:
1048
+ pyarg_strpath = search_pypath(strpath)
1049
+ if pyarg_strpath is not None:
1050
+ module_name = strpath
1051
+ strpath = pyarg_strpath
1052
+ fspath = invocation_path / strpath
1053
+ fspath = absolutepath(fspath)
1054
+ if not safe_exists(fspath):
1055
+ msg = (
1056
+ "module or package not found: {arg} (missing __init__.py?)"
1057
+ if as_pypath
1058
+ else "file or directory not found: {arg}"
1059
+ )
1060
+ raise UsageError(msg.format(arg=arg))
1061
+ if parts and fspath.is_dir():
1062
+ msg = (
1063
+ "package argument cannot contain :: selection parts: {arg}"
1064
+ if as_pypath
1065
+ else "directory argument cannot contain :: selection parts: {arg}"
1066
+ )
1067
+ raise UsageError(msg.format(arg=arg))
1068
+ return CollectionArgument(
1069
+ path=fspath,
1070
+ parts=parts,
1071
+ module_name=module_name,
1072
+ )
.venv/lib/python3.11/site-packages/_pytest/mark/__init__.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generic mechanism for marking and selecting python functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import collections
6
+ import dataclasses
7
+ from typing import AbstractSet
8
+ from typing import Collection
9
+ from typing import Iterable
10
+ from typing import Optional
11
+ from typing import TYPE_CHECKING
12
+
13
+ from .expression import Expression
14
+ from .expression import ParseError
15
+ from .structures import EMPTY_PARAMETERSET_OPTION
16
+ from .structures import get_empty_parameterset_mark
17
+ from .structures import Mark
18
+ from .structures import MARK_GEN
19
+ from .structures import MarkDecorator
20
+ from .structures import MarkGenerator
21
+ from .structures import ParameterSet
22
+ from _pytest.config import Config
23
+ from _pytest.config import ExitCode
24
+ from _pytest.config import hookimpl
25
+ from _pytest.config import UsageError
26
+ from _pytest.config.argparsing import NOT_SET
27
+ from _pytest.config.argparsing import Parser
28
+ from _pytest.stash import StashKey
29
+
30
+
31
+ if TYPE_CHECKING:
32
+ from _pytest.nodes import Item
33
+
34
+
35
+ __all__ = [
36
+ "MARK_GEN",
37
+ "Mark",
38
+ "MarkDecorator",
39
+ "MarkGenerator",
40
+ "ParameterSet",
41
+ "get_empty_parameterset_mark",
42
+ ]
43
+
44
+
45
+ old_mark_config_key = StashKey[Optional[Config]]()
46
+
47
+
48
+ def param(
49
+ *values: object,
50
+ marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
51
+ id: str | None = None,
52
+ ) -> ParameterSet:
53
+ """Specify a parameter in `pytest.mark.parametrize`_ calls or
54
+ :ref:`parametrized fixtures <fixture-parametrize-marks>`.
55
+
56
+ .. code-block:: python
57
+
58
+ @pytest.mark.parametrize(
59
+ "test_input,expected",
60
+ [
61
+ ("3+5", 8),
62
+ pytest.param("6*9", 42, marks=pytest.mark.xfail),
63
+ ],
64
+ )
65
+ def test_eval(test_input, expected):
66
+ assert eval(test_input) == expected
67
+
68
+ :param values: Variable args of the values of the parameter set, in order.
69
+ :param marks: A single mark or a list of marks to be applied to this parameter set.
70
+ :param id: The id to attribute to this parameter set.
71
+ """
72
+ return ParameterSet.param(*values, marks=marks, id=id)
73
+
74
+
75
+ def pytest_addoption(parser: Parser) -> None:
76
+ group = parser.getgroup("general")
77
+ group._addoption(
78
+ "-k",
79
+ action="store",
80
+ dest="keyword",
81
+ default="",
82
+ metavar="EXPRESSION",
83
+ help="Only run tests which match the given substring expression. "
84
+ "An expression is a Python evaluable expression "
85
+ "where all names are substring-matched against test names "
86
+ "and their parent classes. Example: -k 'test_method or test_"
87
+ "other' matches all test functions and classes whose name "
88
+ "contains 'test_method' or 'test_other', while -k 'not test_method' "
89
+ "matches those that don't contain 'test_method' in their names. "
90
+ "-k 'not test_method and not test_other' will eliminate the matches. "
91
+ "Additionally keywords are matched to classes and functions "
92
+ "containing extra names in their 'extra_keyword_matches' set, "
93
+ "as well as functions which have names assigned directly to them. "
94
+ "The matching is case-insensitive.",
95
+ )
96
+
97
+ group._addoption(
98
+ "-m",
99
+ action="store",
100
+ dest="markexpr",
101
+ default="",
102
+ metavar="MARKEXPR",
103
+ help="Only run tests matching given mark expression. "
104
+ "For example: -m 'mark1 and not mark2'.",
105
+ )
106
+
107
+ group.addoption(
108
+ "--markers",
109
+ action="store_true",
110
+ help="show markers (builtin, plugin and per-project ones).",
111
+ )
112
+
113
+ parser.addini("markers", "Register new markers for test functions", "linelist")
114
+ parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")
115
+
116
+
117
+ @hookimpl(tryfirst=True)
118
+ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
119
+ import _pytest.config
120
+
121
+ if config.option.markers:
122
+ config._do_configure()
123
+ tw = _pytest.config.create_terminal_writer(config)
124
+ for line in config.getini("markers"):
125
+ parts = line.split(":", 1)
126
+ name = parts[0]
127
+ rest = parts[1] if len(parts) == 2 else ""
128
+ tw.write(f"@pytest.mark.{name}:", bold=True)
129
+ tw.line(rest)
130
+ tw.line()
131
+ config._ensure_unconfigure()
132
+ return 0
133
+
134
+ return None
135
+
136
+
137
+ @dataclasses.dataclass
138
+ class KeywordMatcher:
139
+ """A matcher for keywords.
140
+
141
+ Given a list of names, matches any substring of one of these names. The
142
+ string inclusion check is case-insensitive.
143
+
144
+ Will match on the name of colitem, including the names of its parents.
145
+ Only matches names of items which are either a :class:`Class` or a
146
+ :class:`Function`.
147
+
148
+ Additionally, matches on names in the 'extra_keyword_matches' set of
149
+ any item, as well as names directly assigned to test functions.
150
+ """
151
+
152
+ __slots__ = ("_names",)
153
+
154
+ _names: AbstractSet[str]
155
+
156
+ @classmethod
157
+ def from_item(cls, item: Item) -> KeywordMatcher:
158
+ mapped_names = set()
159
+
160
+ # Add the names of the current item and any parent items,
161
+ # except the Session and root Directory's which are not
162
+ # interesting for matching.
163
+ import pytest
164
+
165
+ for node in item.listchain():
166
+ if isinstance(node, pytest.Session):
167
+ continue
168
+ if isinstance(node, pytest.Directory) and isinstance(
169
+ node.parent, pytest.Session
170
+ ):
171
+ continue
172
+ mapped_names.add(node.name)
173
+
174
+ # Add the names added as extra keywords to current or parent items.
175
+ mapped_names.update(item.listextrakeywords())
176
+
177
+ # Add the names attached to the current function through direct assignment.
178
+ function_obj = getattr(item, "function", None)
179
+ if function_obj:
180
+ mapped_names.update(function_obj.__dict__)
181
+
182
+ # Add the markers to the keywords as we no longer handle them correctly.
183
+ mapped_names.update(mark.name for mark in item.iter_markers())
184
+
185
+ return cls(mapped_names)
186
+
187
+ def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool:
188
+ if kwargs:
189
+ raise UsageError("Keyword expressions do not support call parameters.")
190
+ subname = subname.lower()
191
+ names = (name.lower() for name in self._names)
192
+
193
+ for name in names:
194
+ if subname in name:
195
+ return True
196
+ return False
197
+
198
+
199
+ def deselect_by_keyword(items: list[Item], config: Config) -> None:
200
+ keywordexpr = config.option.keyword.lstrip()
201
+ if not keywordexpr:
202
+ return
203
+
204
+ expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'")
205
+
206
+ remaining = []
207
+ deselected = []
208
+ for colitem in items:
209
+ if not expr.evaluate(KeywordMatcher.from_item(colitem)):
210
+ deselected.append(colitem)
211
+ else:
212
+ remaining.append(colitem)
213
+
214
+ if deselected:
215
+ config.hook.pytest_deselected(items=deselected)
216
+ items[:] = remaining
217
+
218
+
219
+ @dataclasses.dataclass
220
+ class MarkMatcher:
221
+ """A matcher for markers which are present.
222
+
223
+ Tries to match on any marker names, attached to the given colitem.
224
+ """
225
+
226
+ __slots__ = ("own_mark_name_mapping",)
227
+
228
+ own_mark_name_mapping: dict[str, list[Mark]]
229
+
230
+ @classmethod
231
+ def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher:
232
+ mark_name_mapping = collections.defaultdict(list)
233
+ for mark in markers:
234
+ mark_name_mapping[mark.name].append(mark)
235
+ return cls(mark_name_mapping)
236
+
237
+ def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool:
238
+ if not (matches := self.own_mark_name_mapping.get(name, [])):
239
+ return False
240
+
241
+ for mark in matches:
242
+ if all(mark.kwargs.get(k, NOT_SET) == v for k, v in kwargs.items()):
243
+ return True
244
+
245
+ return False
246
+
247
+
248
+ def deselect_by_mark(items: list[Item], config: Config) -> None:
249
+ matchexpr = config.option.markexpr
250
+ if not matchexpr:
251
+ return
252
+
253
+ expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'")
254
+ remaining: list[Item] = []
255
+ deselected: list[Item] = []
256
+ for item in items:
257
+ if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())):
258
+ remaining.append(item)
259
+ else:
260
+ deselected.append(item)
261
+ if deselected:
262
+ config.hook.pytest_deselected(items=deselected)
263
+ items[:] = remaining
264
+
265
+
266
+ def _parse_expression(expr: str, exc_message: str) -> Expression:
267
+ try:
268
+ return Expression.compile(expr)
269
+ except ParseError as e:
270
+ raise UsageError(f"{exc_message}: {expr}: {e}") from None
271
+
272
+
273
+ def pytest_collection_modifyitems(items: list[Item], config: Config) -> None:
274
+ deselect_by_keyword(items, config)
275
+ deselect_by_mark(items, config)
276
+
277
+
278
+ def pytest_configure(config: Config) -> None:
279
+ config.stash[old_mark_config_key] = MARK_GEN._config
280
+ MARK_GEN._config = config
281
+
282
+ empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
283
+
284
+ if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
285
+ raise UsageError(
286
+ f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect"
287
+ f" but it is {empty_parameterset!r}"
288
+ )
289
+
290
+
291
+ def pytest_unconfigure(config: Config) -> None:
292
+ MARK_GEN._config = config.stash.get(old_mark_config_key, None)
.venv/lib/python3.11/site-packages/_pytest/mark/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (14.2 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/mark/__pycache__/expression.cpython-311.pyc ADDED
Binary file (18.3 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/mark/__pycache__/structures.cpython-311.pyc ADDED
Binary file (28.8 kB). View file
 
.venv/lib/python3.11/site-packages/_pytest/mark/expression.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r"""Evaluate match expressions, as used by `-k` and `-m`.
2
+
3
+ The grammar is:
4
+
5
+ expression: expr? EOF
6
+ expr: and_expr ('or' and_expr)*
7
+ and_expr: not_expr ('and' not_expr)*
8
+ not_expr: 'not' not_expr | '(' expr ')' | ident kwargs?
9
+
10
+ ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
11
+ kwargs: ('(' name '=' value ( ', ' name '=' value )* ')')
12
+ name: a valid ident, but not a reserved keyword
13
+ value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None'
14
+
15
+ The semantics are:
16
+
17
+ - Empty expression evaluates to False.
18
+ - ident evaluates to True or False according to a provided matcher function.
19
+ - or/and/not evaluate according to the usual boolean semantics.
20
+ - ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import ast
26
+ import dataclasses
27
+ import enum
28
+ import keyword
29
+ import re
30
+ import types
31
+ from typing import Iterator
32
+ from typing import Literal
33
+ from typing import Mapping
34
+ from typing import NoReturn
35
+ from typing import overload
36
+ from typing import Protocol
37
+ from typing import Sequence
38
+
39
+
40
+ __all__ = [
41
+ "Expression",
42
+ "ParseError",
43
+ ]
44
+
45
+
46
+ class TokenType(enum.Enum):
47
+ LPAREN = "left parenthesis"
48
+ RPAREN = "right parenthesis"
49
+ OR = "or"
50
+ AND = "and"
51
+ NOT = "not"
52
+ IDENT = "identifier"
53
+ EOF = "end of input"
54
+ EQUAL = "="
55
+ STRING = "string literal"
56
+ COMMA = ","
57
+
58
+
59
+ @dataclasses.dataclass(frozen=True)
60
+ class Token:
61
+ __slots__ = ("type", "value", "pos")
62
+ type: TokenType
63
+ value: str
64
+ pos: int
65
+
66
+
67
+ class ParseError(Exception):
68
+ """The expression contains invalid syntax.
69
+
70
+ :param column: The column in the line where the error occurred (1-based).
71
+ :param message: A description of the error.
72
+ """
73
+
74
+ def __init__(self, column: int, message: str) -> None:
75
+ self.column = column
76
+ self.message = message
77
+
78
+ def __str__(self) -> str:
79
+ return f"at column {self.column}: {self.message}"
80
+
81
+
82
+ class Scanner:
83
+ __slots__ = ("tokens", "current")
84
+
85
+ def __init__(self, input: str) -> None:
86
+ self.tokens = self.lex(input)
87
+ self.current = next(self.tokens)
88
+
89
+ def lex(self, input: str) -> Iterator[Token]:
90
+ pos = 0
91
+ while pos < len(input):
92
+ if input[pos] in (" ", "\t"):
93
+ pos += 1
94
+ elif input[pos] == "(":
95
+ yield Token(TokenType.LPAREN, "(", pos)
96
+ pos += 1
97
+ elif input[pos] == ")":
98
+ yield Token(TokenType.RPAREN, ")", pos)
99
+ pos += 1
100
+ elif input[pos] == "=":
101
+ yield Token(TokenType.EQUAL, "=", pos)
102
+ pos += 1
103
+ elif input[pos] == ",":
104
+ yield Token(TokenType.COMMA, ",", pos)
105
+ pos += 1
106
+ elif (quote_char := input[pos]) in ("'", '"'):
107
+ end_quote_pos = input.find(quote_char, pos + 1)
108
+ if end_quote_pos == -1:
109
+ raise ParseError(
110
+ pos + 1,
111
+ f'closing quote "{quote_char}" is missing',
112
+ )
113
+ value = input[pos : end_quote_pos + 1]
114
+ if (backslash_pos := input.find("\\")) != -1:
115
+ raise ParseError(
116
+ backslash_pos + 1,
117
+ r'escaping with "\" not supported in marker expression',
118
+ )
119
+ yield Token(TokenType.STRING, value, pos)
120
+ pos += len(value)
121
+ else:
122
+ match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
123
+ if match:
124
+ value = match.group(0)
125
+ if value == "or":
126
+ yield Token(TokenType.OR, value, pos)
127
+ elif value == "and":
128
+ yield Token(TokenType.AND, value, pos)
129
+ elif value == "not":
130
+ yield Token(TokenType.NOT, value, pos)
131
+ else:
132
+ yield Token(TokenType.IDENT, value, pos)
133
+ pos += len(value)
134
+ else:
135
+ raise ParseError(
136
+ pos + 1,
137
+ f'unexpected character "{input[pos]}"',
138
+ )
139
+ yield Token(TokenType.EOF, "", pos)
140
+
141
+ @overload
142
+ def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ...
143
+
144
+ @overload
145
+ def accept(
146
+ self, type: TokenType, *, reject: Literal[False] = False
147
+ ) -> Token | None: ...
148
+
149
+ def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
150
+ if self.current.type is type:
151
+ token = self.current
152
+ if token.type is not TokenType.EOF:
153
+ self.current = next(self.tokens)
154
+ return token
155
+ if reject:
156
+ self.reject((type,))
157
+ return None
158
+
159
+ def reject(self, expected: Sequence[TokenType]) -> NoReturn:
160
+ raise ParseError(
161
+ self.current.pos + 1,
162
+ "expected {}; got {}".format(
163
+ " OR ".join(type.value for type in expected),
164
+ self.current.type.value,
165
+ ),
166
+ )
167
+
168
+
169
+ # True, False and None are legal match expression identifiers,
170
+ # but illegal as Python identifiers. To fix this, this prefix
171
+ # is added to identifiers in the conversion to Python AST.
172
+ IDENT_PREFIX = "$"
173
+
174
+
175
+ def expression(s: Scanner) -> ast.Expression:
176
+ if s.accept(TokenType.EOF):
177
+ ret: ast.expr = ast.Constant(False)
178
+ else:
179
+ ret = expr(s)
180
+ s.accept(TokenType.EOF, reject=True)
181
+ return ast.fix_missing_locations(ast.Expression(ret))
182
+
183
+
184
+ def expr(s: Scanner) -> ast.expr:
185
+ ret = and_expr(s)
186
+ while s.accept(TokenType.OR):
187
+ rhs = and_expr(s)
188
+ ret = ast.BoolOp(ast.Or(), [ret, rhs])
189
+ return ret
190
+
191
+
192
+ def and_expr(s: Scanner) -> ast.expr:
193
+ ret = not_expr(s)
194
+ while s.accept(TokenType.AND):
195
+ rhs = not_expr(s)
196
+ ret = ast.BoolOp(ast.And(), [ret, rhs])
197
+ return ret
198
+
199
+
200
+ def not_expr(s: Scanner) -> ast.expr:
201
+ if s.accept(TokenType.NOT):
202
+ return ast.UnaryOp(ast.Not(), not_expr(s))
203
+ if s.accept(TokenType.LPAREN):
204
+ ret = expr(s)
205
+ s.accept(TokenType.RPAREN, reject=True)
206
+ return ret
207
+ ident = s.accept(TokenType.IDENT)
208
+ if ident:
209
+ name = ast.Name(IDENT_PREFIX + ident.value, ast.Load())
210
+ if s.accept(TokenType.LPAREN):
211
+ ret = ast.Call(func=name, args=[], keywords=all_kwargs(s))
212
+ s.accept(TokenType.RPAREN, reject=True)
213
+ else:
214
+ ret = name
215
+ return ret
216
+
217
+ s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
218
+
219
+
220
+ BUILTIN_MATCHERS = {"True": True, "False": False, "None": None}
221
+
222
+
223
+ def single_kwarg(s: Scanner) -> ast.keyword:
224
+ keyword_name = s.accept(TokenType.IDENT, reject=True)
225
+ if not keyword_name.value.isidentifier():
226
+ raise ParseError(
227
+ keyword_name.pos + 1,
228
+ f"not a valid python identifier {keyword_name.value}",
229
+ )
230
+ if keyword.iskeyword(keyword_name.value):
231
+ raise ParseError(
232
+ keyword_name.pos + 1,
233
+ f"unexpected reserved python keyword `{keyword_name.value}`",
234
+ )
235
+ s.accept(TokenType.EQUAL, reject=True)
236
+
237
+ if value_token := s.accept(TokenType.STRING):
238
+ value: str | int | bool | None = value_token.value[1:-1] # strip quotes
239
+ else:
240
+ value_token = s.accept(TokenType.IDENT, reject=True)
241
+ if (
242
+ (number := value_token.value).isdigit()
243
+ or number.startswith("-")
244
+ and number[1:].isdigit()
245
+ ):
246
+ value = int(number)
247
+ elif value_token.value in BUILTIN_MATCHERS:
248
+ value = BUILTIN_MATCHERS[value_token.value]
249
+ else:
250
+ raise ParseError(
251
+ value_token.pos + 1,
252
+ f'unexpected character/s "{value_token.value}"',
253
+ )
254
+
255
+ ret = ast.keyword(keyword_name.value, ast.Constant(value))
256
+ return ret
257
+
258
+
259
+ def all_kwargs(s: Scanner) -> list[ast.keyword]:
260
+ ret = [single_kwarg(s)]
261
+ while s.accept(TokenType.COMMA):
262
+ ret.append(single_kwarg(s))
263
+ return ret
264
+
265
+
266
+ class MatcherCall(Protocol):
267
+ def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: ...
268
+
269
+
270
+ @dataclasses.dataclass
271
+ class MatcherNameAdapter:
272
+ matcher: MatcherCall
273
+ name: str
274
+
275
+ def __bool__(self) -> bool:
276
+ return self.matcher(self.name)
277
+
278
+ def __call__(self, **kwargs: str | int | bool | None) -> bool:
279
+ return self.matcher(self.name, **kwargs)
280
+
281
+
282
+ class MatcherAdapter(Mapping[str, MatcherNameAdapter]):
283
+ """Adapts a matcher function to a locals mapping as required by eval()."""
284
+
285
+ def __init__(self, matcher: MatcherCall) -> None:
286
+ self.matcher = matcher
287
+
288
+ def __getitem__(self, key: str) -> MatcherNameAdapter:
289
+ return MatcherNameAdapter(matcher=self.matcher, name=key[len(IDENT_PREFIX) :])
290
+
291
+ def __iter__(self) -> Iterator[str]:
292
+ raise NotImplementedError()
293
+
294
+ def __len__(self) -> int:
295
+ raise NotImplementedError()
296
+
297
+
298
+ class Expression:
299
+ """A compiled match expression as used by -k and -m.
300
+
301
+ The expression can be evaluated against different matchers.
302
+ """
303
+
304
+ __slots__ = ("code",)
305
+
306
+ def __init__(self, code: types.CodeType) -> None:
307
+ self.code = code
308
+
309
+ @classmethod
310
+ def compile(self, input: str) -> Expression:
311
+ """Compile a match expression.
312
+
313
+ :param input: The input expression - one line.
314
+ """
315
+ astexpr = expression(Scanner(input))
316
+ code: types.CodeType = compile(
317
+ astexpr,
318
+ filename="<pytest match expression>",
319
+ mode="eval",
320
+ )
321
+ return Expression(code)
322
+
323
+ def evaluate(self, matcher: MatcherCall) -> bool:
324
+ """Evaluate the match expression.
325
+
326
+ :param matcher:
327
+ Given an identifier, should return whether it matches or not.
328
+ Should be prepared to handle arbitrary strings as input.
329
+
330
+ :returns: Whether the expression matches or not.
331
+ """
332
+ ret: bool = bool(eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher)))
333
+ return ret
.venv/lib/python3.11/site-packages/_pytest/mark/structures.py ADDED
@@ -0,0 +1,615 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import collections.abc
5
+ import dataclasses
6
+ import inspect
7
+ from typing import Any
8
+ from typing import Callable
9
+ from typing import Collection
10
+ from typing import final
11
+ from typing import Iterable
12
+ from typing import Iterator
13
+ from typing import Mapping
14
+ from typing import MutableMapping
15
+ from typing import NamedTuple
16
+ from typing import overload
17
+ from typing import Sequence
18
+ from typing import TYPE_CHECKING
19
+ from typing import TypeVar
20
+ from typing import Union
21
+ import warnings
22
+
23
+ from .._code import getfslineno
24
+ from ..compat import ascii_escaped
25
+ from ..compat import NOTSET
26
+ from ..compat import NotSetType
27
+ from _pytest.config import Config
28
+ from _pytest.deprecated import check_ispytest
29
+ from _pytest.deprecated import MARKED_FIXTURE
30
+ from _pytest.outcomes import fail
31
+ from _pytest.scope import _ScopeName
32
+ from _pytest.warning_types import PytestUnknownMarkWarning
33
+
34
+
35
+ if TYPE_CHECKING:
36
+ from ..nodes import Node
37
+
38
+
39
+ EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
40
+
41
+
42
+ def istestfunc(func) -> bool:
43
+ return callable(func) and getattr(func, "__name__", "<lambda>") != "<lambda>"
44
+
45
+
46
+ def get_empty_parameterset_mark(
47
+ config: Config, argnames: Sequence[str], func
48
+ ) -> MarkDecorator:
49
+ from ..nodes import Collector
50
+
51
+ fs, lineno = getfslineno(func)
52
+ reason = "got empty parameter set %r, function %s at %s:%d" % (
53
+ argnames,
54
+ func.__name__,
55
+ fs,
56
+ lineno,
57
+ )
58
+
59
+ requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
60
+ if requested_mark in ("", None, "skip"):
61
+ mark = MARK_GEN.skip(reason=reason)
62
+ elif requested_mark == "xfail":
63
+ mark = MARK_GEN.xfail(reason=reason, run=False)
64
+ elif requested_mark == "fail_at_collect":
65
+ f_name = func.__name__
66
+ _, lineno = getfslineno(func)
67
+ raise Collector.CollectError(
68
+ "Empty parameter set in '%s' at line %d" % (f_name, lineno + 1)
69
+ )
70
+ else:
71
+ raise LookupError(requested_mark)
72
+ return mark
73
+
74
+
75
+ class ParameterSet(NamedTuple):
76
+ values: Sequence[object | NotSetType]
77
+ marks: Collection[MarkDecorator | Mark]
78
+ id: str | None
79
+
80
+ @classmethod
81
+ def param(
82
+ cls,
83
+ *values: object,
84
+ marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
85
+ id: str | None = None,
86
+ ) -> ParameterSet:
87
+ if isinstance(marks, MarkDecorator):
88
+ marks = (marks,)
89
+ else:
90
+ assert isinstance(marks, collections.abc.Collection)
91
+
92
+ if id is not None:
93
+ if not isinstance(id, str):
94
+ raise TypeError(f"Expected id to be a string, got {type(id)}: {id!r}")
95
+ id = ascii_escaped(id)
96
+ return cls(values, marks, id)
97
+
98
+ @classmethod
99
+ def extract_from(
100
+ cls,
101
+ parameterset: ParameterSet | Sequence[object] | object,
102
+ force_tuple: bool = False,
103
+ ) -> ParameterSet:
104
+ """Extract from an object or objects.
105
+
106
+ :param parameterset:
107
+ A legacy style parameterset that may or may not be a tuple,
108
+ and may or may not be wrapped into a mess of mark objects.
109
+
110
+ :param force_tuple:
111
+ Enforce tuple wrapping so single argument tuple values
112
+ don't get decomposed and break tests.
113
+ """
114
+ if isinstance(parameterset, cls):
115
+ return parameterset
116
+ if force_tuple:
117
+ return cls.param(parameterset)
118
+ else:
119
+ # TODO: Refactor to fix this type-ignore. Currently the following
120
+ # passes type-checking but crashes:
121
+ #
122
+ # @pytest.mark.parametrize(('x', 'y'), [1, 2])
123
+ # def test_foo(x, y): pass
124
+ return cls(parameterset, marks=[], id=None) # type: ignore[arg-type]
125
+
126
+ @staticmethod
127
+ def _parse_parametrize_args(
128
+ argnames: str | Sequence[str],
129
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
130
+ *args,
131
+ **kwargs,
132
+ ) -> tuple[Sequence[str], bool]:
133
+ if isinstance(argnames, str):
134
+ argnames = [x.strip() for x in argnames.split(",") if x.strip()]
135
+ force_tuple = len(argnames) == 1
136
+ else:
137
+ force_tuple = False
138
+ return argnames, force_tuple
139
+
140
+ @staticmethod
141
+ def _parse_parametrize_parameters(
142
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
143
+ force_tuple: bool,
144
+ ) -> list[ParameterSet]:
145
+ return [
146
+ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
147
+ ]
148
+
149
+ @classmethod
150
+ def _for_parametrize(
151
+ cls,
152
+ argnames: str | Sequence[str],
153
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
154
+ func,
155
+ config: Config,
156
+ nodeid: str,
157
+ ) -> tuple[Sequence[str], list[ParameterSet]]:
158
+ argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
159
+ parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
160
+ del argvalues
161
+
162
+ if parameters:
163
+ # Check all parameter sets have the correct number of values.
164
+ for param in parameters:
165
+ if len(param.values) != len(argnames):
166
+ msg = (
167
+ '{nodeid}: in "parametrize" the number of names ({names_len}):\n'
168
+ " {names}\n"
169
+ "must be equal to the number of values ({values_len}):\n"
170
+ " {values}"
171
+ )
172
+ fail(
173
+ msg.format(
174
+ nodeid=nodeid,
175
+ values=param.values,
176
+ names=argnames,
177
+ names_len=len(argnames),
178
+ values_len=len(param.values),
179
+ ),
180
+ pytrace=False,
181
+ )
182
+ else:
183
+ # Empty parameter set (likely computed at runtime): create a single
184
+ # parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
185
+ mark = get_empty_parameterset_mark(config, argnames, func)
186
+ parameters.append(
187
+ ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
188
+ )
189
+ return argnames, parameters
190
+
191
+
192
+ @final
193
+ @dataclasses.dataclass(frozen=True)
194
+ class Mark:
195
+ """A pytest mark."""
196
+
197
+ #: Name of the mark.
198
+ name: str
199
+ #: Positional arguments of the mark decorator.
200
+ args: tuple[Any, ...]
201
+ #: Keyword arguments of the mark decorator.
202
+ kwargs: Mapping[str, Any]
203
+
204
+ #: Source Mark for ids with parametrize Marks.
205
+ _param_ids_from: Mark | None = dataclasses.field(default=None, repr=False)
206
+ #: Resolved/generated ids with parametrize Marks.
207
+ _param_ids_generated: Sequence[str] | None = dataclasses.field(
208
+ default=None, repr=False
209
+ )
210
+
211
+ def __init__(
212
+ self,
213
+ name: str,
214
+ args: tuple[Any, ...],
215
+ kwargs: Mapping[str, Any],
216
+ param_ids_from: Mark | None = None,
217
+ param_ids_generated: Sequence[str] | None = None,
218
+ *,
219
+ _ispytest: bool = False,
220
+ ) -> None:
221
+ """:meta private:"""
222
+ check_ispytest(_ispytest)
223
+ # Weirdness to bypass frozen=True.
224
+ object.__setattr__(self, "name", name)
225
+ object.__setattr__(self, "args", args)
226
+ object.__setattr__(self, "kwargs", kwargs)
227
+ object.__setattr__(self, "_param_ids_from", param_ids_from)
228
+ object.__setattr__(self, "_param_ids_generated", param_ids_generated)
229
+
230
+ def _has_param_ids(self) -> bool:
231
+ return "ids" in self.kwargs or len(self.args) >= 4
232
+
233
+ def combined_with(self, other: Mark) -> Mark:
234
+ """Return a new Mark which is a combination of this
235
+ Mark and another Mark.
236
+
237
+ Combines by appending args and merging kwargs.
238
+
239
+ :param Mark other: The mark to combine with.
240
+ :rtype: Mark
241
+ """
242
+ assert self.name == other.name
243
+
244
+ # Remember source of ids with parametrize Marks.
245
+ param_ids_from: Mark | None = None
246
+ if self.name == "parametrize":
247
+ if other._has_param_ids():
248
+ param_ids_from = other
249
+ elif self._has_param_ids():
250
+ param_ids_from = self
251
+
252
+ return Mark(
253
+ self.name,
254
+ self.args + other.args,
255
+ dict(self.kwargs, **other.kwargs),
256
+ param_ids_from=param_ids_from,
257
+ _ispytest=True,
258
+ )
259
+
260
+
261
+ # A generic parameter designating an object to which a Mark may
262
+ # be applied -- a test function (callable) or class.
263
+ # Note: a lambda is not allowed, but this can't be represented.
264
+ Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
265
+
266
+
267
+ @dataclasses.dataclass
268
+ class MarkDecorator:
269
+ """A decorator for applying a mark on test functions and classes.
270
+
271
+ ``MarkDecorators`` are created with ``pytest.mark``::
272
+
273
+ mark1 = pytest.mark.NAME # Simple MarkDecorator
274
+ mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
275
+
276
+ and can then be applied as decorators to test functions::
277
+
278
+ @mark2
279
+ def test_function():
280
+ pass
281
+
282
+ When a ``MarkDecorator`` is called, it does the following:
283
+
284
+ 1. If called with a single class as its only positional argument and no
285
+ additional keyword arguments, it attaches the mark to the class so it
286
+ gets applied automatically to all test cases found in that class.
287
+
288
+ 2. If called with a single function as its only positional argument and
289
+ no additional keyword arguments, it attaches the mark to the function,
290
+ containing all the arguments already stored internally in the
291
+ ``MarkDecorator``.
292
+
293
+ 3. When called in any other case, it returns a new ``MarkDecorator``
294
+ instance with the original ``MarkDecorator``'s content updated with
295
+ the arguments passed to this call.
296
+
297
+ Note: The rules above prevent a ``MarkDecorator`` from storing only a
298
+ single function or class reference as its positional argument with no
299
+ additional keyword or positional arguments. You can work around this by
300
+ using `with_args()`.
301
+ """
302
+
303
+ mark: Mark
304
+
305
+ def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:
306
+ """:meta private:"""
307
+ check_ispytest(_ispytest)
308
+ self.mark = mark
309
+
310
+ @property
311
+ def name(self) -> str:
312
+ """Alias for mark.name."""
313
+ return self.mark.name
314
+
315
+ @property
316
+ def args(self) -> tuple[Any, ...]:
317
+ """Alias for mark.args."""
318
+ return self.mark.args
319
+
320
+ @property
321
+ def kwargs(self) -> Mapping[str, Any]:
322
+ """Alias for mark.kwargs."""
323
+ return self.mark.kwargs
324
+
325
+ @property
326
+ def markname(self) -> str:
327
+ """:meta private:"""
328
+ return self.name # for backward-compat (2.4.1 had this attr)
329
+
330
+ def with_args(self, *args: object, **kwargs: object) -> MarkDecorator:
331
+ """Return a MarkDecorator with extra arguments added.
332
+
333
+ Unlike calling the MarkDecorator, with_args() can be used even
334
+ if the sole argument is a callable/class.
335
+ """
336
+ mark = Mark(self.name, args, kwargs, _ispytest=True)
337
+ return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)
338
+
339
+ # Type ignored because the overloads overlap with an incompatible
340
+ # return type. Not much we can do about that. Thankfully mypy picks
341
+ # the first match so it works out even if we break the rules.
342
+ @overload
343
+ def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap]
344
+ pass
345
+
346
+ @overload
347
+ def __call__(self, *args: object, **kwargs: object) -> MarkDecorator:
348
+ pass
349
+
350
+ def __call__(self, *args: object, **kwargs: object):
351
+ """Call the MarkDecorator."""
352
+ if args and not kwargs:
353
+ func = args[0]
354
+ is_class = inspect.isclass(func)
355
+ if len(args) == 1 and (istestfunc(func) or is_class):
356
+ store_mark(func, self.mark, stacklevel=3)
357
+ return func
358
+ return self.with_args(*args, **kwargs)
359
+
360
+
361
+ def get_unpacked_marks(
362
+ obj: object | type,
363
+ *,
364
+ consider_mro: bool = True,
365
+ ) -> list[Mark]:
366
+ """Obtain the unpacked marks that are stored on an object.
367
+
368
+ If obj is a class and consider_mro is true, return marks applied to
369
+ this class and all of its super-classes in MRO order. If consider_mro
370
+ is false, only return marks applied directly to this class.
371
+ """
372
+ if isinstance(obj, type):
373
+ if not consider_mro:
374
+ mark_lists = [obj.__dict__.get("pytestmark", [])]
375
+ else:
376
+ mark_lists = [
377
+ x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
378
+ ]
379
+ mark_list = []
380
+ for item in mark_lists:
381
+ if isinstance(item, list):
382
+ mark_list.extend(item)
383
+ else:
384
+ mark_list.append(item)
385
+ else:
386
+ mark_attribute = getattr(obj, "pytestmark", [])
387
+ if isinstance(mark_attribute, list):
388
+ mark_list = mark_attribute
389
+ else:
390
+ mark_list = [mark_attribute]
391
+ return list(normalize_mark_list(mark_list))
392
+
393
+
394
+ def normalize_mark_list(
395
+ mark_list: Iterable[Mark | MarkDecorator],
396
+ ) -> Iterable[Mark]:
397
+ """
398
+ Normalize an iterable of Mark or MarkDecorator objects into a list of marks
399
+ by retrieving the `mark` attribute on MarkDecorator instances.
400
+
401
+ :param mark_list: marks to normalize
402
+ :returns: A new list of the extracted Mark objects
403
+ """
404
+ for mark in mark_list:
405
+ mark_obj = getattr(mark, "mark", mark)
406
+ if not isinstance(mark_obj, Mark):
407
+ raise TypeError(f"got {mark_obj!r} instead of Mark")
408
+ yield mark_obj
409
+
410
+
411
+ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
412
+ """Store a Mark on an object.
413
+
414
+ This is used to implement the Mark declarations/decorators correctly.
415
+ """
416
+ assert isinstance(mark, Mark), mark
417
+
418
+ from ..fixtures import getfixturemarker
419
+
420
+ if getfixturemarker(obj) is not None:
421
+ warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel)
422
+
423
+ # Always reassign name to avoid updating pytestmark in a reference that
424
+ # was only borrowed.
425
+ obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
426
+
427
+
428
+ # Typing for builtin pytest marks. This is cheating; it gives builtin marks
429
+ # special privilege, and breaks modularity. But practicality beats purity...
430
+ if TYPE_CHECKING:
431
+
432
+ class _SkipMarkDecorator(MarkDecorator):
433
+ @overload # type: ignore[override,no-overload-impl]
434
+ def __call__(self, arg: Markable) -> Markable: ...
435
+
436
+ @overload
437
+ def __call__(self, reason: str = ...) -> MarkDecorator: ...
438
+
439
+ class _SkipifMarkDecorator(MarkDecorator):
440
+ def __call__( # type: ignore[override]
441
+ self,
442
+ condition: str | bool = ...,
443
+ *conditions: str | bool,
444
+ reason: str = ...,
445
+ ) -> MarkDecorator: ...
446
+
447
+ class _XfailMarkDecorator(MarkDecorator):
448
+ @overload # type: ignore[override,no-overload-impl]
449
+ def __call__(self, arg: Markable) -> Markable: ...
450
+
451
+ @overload
452
+ def __call__(
453
+ self,
454
+ condition: str | bool = False,
455
+ *conditions: str | bool,
456
+ reason: str = ...,
457
+ run: bool = ...,
458
+ raises: None | type[BaseException] | tuple[type[BaseException], ...] = ...,
459
+ strict: bool = ...,
460
+ ) -> MarkDecorator: ...
461
+
462
+ class _ParametrizeMarkDecorator(MarkDecorator):
463
+ def __call__( # type: ignore[override]
464
+ self,
465
+ argnames: str | Sequence[str],
466
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
467
+ *,
468
+ indirect: bool | Sequence[str] = ...,
469
+ ids: Iterable[None | str | float | int | bool]
470
+ | Callable[[Any], object | None]
471
+ | None = ...,
472
+ scope: _ScopeName | None = ...,
473
+ ) -> MarkDecorator: ...
474
+
475
+ class _UsefixturesMarkDecorator(MarkDecorator):
476
+ def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
477
+ ...
478
+
479
+ class _FilterwarningsMarkDecorator(MarkDecorator):
480
+ def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override]
481
+ ...
482
+
483
+
484
+ @final
485
+ class MarkGenerator:
486
+ """Factory for :class:`MarkDecorator` objects - exposed as
487
+ a ``pytest.mark`` singleton instance.
488
+
489
+ Example::
490
+
491
+ import pytest
492
+
493
+
494
+ @pytest.mark.slowtest
495
+ def test_function():
496
+ pass
497
+
498
+ applies a 'slowtest' :class:`Mark` on ``test_function``.
499
+ """
500
+
501
+ # See TYPE_CHECKING above.
502
+ if TYPE_CHECKING:
503
+ skip: _SkipMarkDecorator
504
+ skipif: _SkipifMarkDecorator
505
+ xfail: _XfailMarkDecorator
506
+ parametrize: _ParametrizeMarkDecorator
507
+ usefixtures: _UsefixturesMarkDecorator
508
+ filterwarnings: _FilterwarningsMarkDecorator
509
+
510
+ def __init__(self, *, _ispytest: bool = False) -> None:
511
+ check_ispytest(_ispytest)
512
+ self._config: Config | None = None
513
+ self._markers: set[str] = set()
514
+
515
+ def __getattr__(self, name: str) -> MarkDecorator:
516
+ """Generate a new :class:`MarkDecorator` with the given name."""
517
+ if name[0] == "_":
518
+ raise AttributeError("Marker name must NOT start with underscore")
519
+
520
+ if self._config is not None:
521
+ # We store a set of markers as a performance optimisation - if a mark
522
+ # name is in the set we definitely know it, but a mark may be known and
523
+ # not in the set. We therefore start by updating the set!
524
+ if name not in self._markers:
525
+ for line in self._config.getini("markers"):
526
+ # example lines: "skipif(condition): skip the given test if..."
527
+ # or "hypothesis: tests which use Hypothesis", so to get the
528
+ # marker name we split on both `:` and `(`.
529
+ marker = line.split(":")[0].split("(")[0].strip()
530
+ self._markers.add(marker)
531
+
532
+ # If the name is not in the set of known marks after updating,
533
+ # then it really is time to issue a warning or an error.
534
+ if name not in self._markers:
535
+ if self._config.option.strict_markers or self._config.option.strict:
536
+ fail(
537
+ f"{name!r} not found in `markers` configuration option",
538
+ pytrace=False,
539
+ )
540
+
541
+ # Raise a specific error for common misspellings of "parametrize".
542
+ if name in ["parameterize", "parametrise", "parameterise"]:
543
+ __tracebackhide__ = True
544
+ fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
545
+
546
+ warnings.warn(
547
+ f"Unknown pytest.mark.{name} - is this a typo? You can register "
548
+ "custom marks to avoid this warning - for details, see "
549
+ "https://docs.pytest.org/en/stable/how-to/mark.html",
550
+ PytestUnknownMarkWarning,
551
+ 2,
552
+ )
553
+
554
+ return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
555
+
556
+
557
+ MARK_GEN = MarkGenerator(_ispytest=True)
558
+
559
+
560
+ @final
561
+ class NodeKeywords(MutableMapping[str, Any]):
562
+ __slots__ = ("node", "parent", "_markers")
563
+
564
+ def __init__(self, node: Node) -> None:
565
+ self.node = node
566
+ self.parent = node.parent
567
+ self._markers = {node.name: True}
568
+
569
+ def __getitem__(self, key: str) -> Any:
570
+ try:
571
+ return self._markers[key]
572
+ except KeyError:
573
+ if self.parent is None:
574
+ raise
575
+ return self.parent.keywords[key]
576
+
577
+ def __setitem__(self, key: str, value: Any) -> None:
578
+ self._markers[key] = value
579
+
580
+ # Note: we could've avoided explicitly implementing some of the methods
581
+ # below and use the collections.abc fallback, but that would be slow.
582
+
583
+ def __contains__(self, key: object) -> bool:
584
+ return (
585
+ key in self._markers
586
+ or self.parent is not None
587
+ and key in self.parent.keywords
588
+ )
589
+
590
+ def update( # type: ignore[override]
591
+ self,
592
+ other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (),
593
+ **kwds: Any,
594
+ ) -> None:
595
+ self._markers.update(other)
596
+ self._markers.update(kwds)
597
+
598
+ def __delitem__(self, key: str) -> None:
599
+ raise ValueError("cannot delete key in keywords dict")
600
+
601
+ def __iter__(self) -> Iterator[str]:
602
+ # Doesn't need to be fast.
603
+ yield from self._markers
604
+ if self.parent is not None:
605
+ for keyword in self.parent.keywords:
606
+ # self._marks and self.parent.keywords can have duplicates.
607
+ if keyword not in self._markers:
608
+ yield keyword
609
+
610
+ def __len__(self) -> int:
611
+ # Doesn't need to be fast.
612
+ return sum(1 for keyword in self)
613
+
614
+ def __repr__(self) -> str:
615
+ return f"<NodeKeywords for node {self.node}>"
.venv/lib/python3.11/site-packages/_pytest/monkeypatch.py ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Monkeypatching and mocking functionality."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from contextlib import contextmanager
7
+ import os
8
+ import re
9
+ import sys
10
+ from typing import Any
11
+ from typing import final
12
+ from typing import Generator
13
+ from typing import Mapping
14
+ from typing import MutableMapping
15
+ from typing import overload
16
+ from typing import TypeVar
17
+ import warnings
18
+
19
+ from _pytest.fixtures import fixture
20
+ from _pytest.warning_types import PytestWarning
21
+
22
+
23
+ RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
24
+
25
+
26
+ K = TypeVar("K")
27
+ V = TypeVar("V")
28
+
29
+
30
+ @fixture
31
+ def monkeypatch() -> Generator[MonkeyPatch]:
32
+ """A convenient fixture for monkey-patching.
33
+
34
+ The fixture provides these methods to modify objects, dictionaries, or
35
+ :data:`os.environ`:
36
+
37
+ * :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>`
38
+ * :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>`
39
+ * :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>`
40
+ * :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>`
41
+ * :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>`
42
+ * :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>`
43
+ * :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>`
44
+ * :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>`
45
+ * :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>`
46
+
47
+ All modifications will be undone after the requesting test function or
48
+ fixture has finished. The ``raising`` parameter determines if a :class:`KeyError`
49
+ or :class:`AttributeError` will be raised if the set/deletion operation does not have the
50
+ specified target.
51
+
52
+ To undo modifications done by the fixture in a contained scope,
53
+ use :meth:`context() <pytest.MonkeyPatch.context>`.
54
+ """
55
+ mpatch = MonkeyPatch()
56
+ yield mpatch
57
+ mpatch.undo()
58
+
59
+
60
+ def resolve(name: str) -> object:
61
+ # Simplified from zope.dottedname.
62
+ parts = name.split(".")
63
+
64
+ used = parts.pop(0)
65
+ found: object = __import__(used)
66
+ for part in parts:
67
+ used += "." + part
68
+ try:
69
+ found = getattr(found, part)
70
+ except AttributeError:
71
+ pass
72
+ else:
73
+ continue
74
+ # We use explicit un-nesting of the handling block in order
75
+ # to avoid nested exceptions.
76
+ try:
77
+ __import__(used)
78
+ except ImportError as ex:
79
+ expected = str(ex).split()[-1]
80
+ if expected == used:
81
+ raise
82
+ else:
83
+ raise ImportError(f"import error in {used}: {ex}") from ex
84
+ found = annotated_getattr(found, part, used)
85
+ return found
86
+
87
+
88
+ def annotated_getattr(obj: object, name: str, ann: str) -> object:
89
+ try:
90
+ obj = getattr(obj, name)
91
+ except AttributeError as e:
92
+ raise AttributeError(
93
+ f"{type(obj).__name__!r} object at {ann} has no attribute {name!r}"
94
+ ) from e
95
+ return obj
96
+
97
+
98
+ def derive_importpath(import_path: str, raising: bool) -> tuple[str, object]:
99
+ if not isinstance(import_path, str) or "." not in import_path:
100
+ raise TypeError(f"must be absolute import path string, not {import_path!r}")
101
+ module, attr = import_path.rsplit(".", 1)
102
+ target = resolve(module)
103
+ if raising:
104
+ annotated_getattr(target, attr, ann=module)
105
+ return attr, target
106
+
107
+
108
+ class Notset:
109
+ def __repr__(self) -> str:
110
+ return "<notset>"
111
+
112
+
113
+ notset = Notset()
114
+
115
+
116
+ @final
117
+ class MonkeyPatch:
118
+ """Helper to conveniently monkeypatch attributes/items/environment
119
+ variables/syspath.
120
+
121
+ Returned by the :fixture:`monkeypatch` fixture.
122
+
123
+ .. versionchanged:: 6.2
124
+ Can now also be used directly as `pytest.MonkeyPatch()`, for when
125
+ the fixture is not available. In this case, use
126
+ :meth:`with MonkeyPatch.context() as mp: <context>` or remember to call
127
+ :meth:`undo` explicitly.
128
+ """
129
+
130
+ def __init__(self) -> None:
131
+ self._setattr: list[tuple[object, str, object]] = []
132
+ self._setitem: list[tuple[Mapping[Any, Any], object, object]] = []
133
+ self._cwd: str | None = None
134
+ self._savesyspath: list[str] | None = None
135
+
136
+ @classmethod
137
+ @contextmanager
138
+ def context(cls) -> Generator[MonkeyPatch]:
139
+ """Context manager that returns a new :class:`MonkeyPatch` object
140
+ which undoes any patching done inside the ``with`` block upon exit.
141
+
142
+ Example:
143
+
144
+ .. code-block:: python
145
+
146
+ import functools
147
+
148
+
149
+ def test_partial(monkeypatch):
150
+ with monkeypatch.context() as m:
151
+ m.setattr(functools, "partial", 3)
152
+
153
+ Useful in situations where it is desired to undo some patches before the test ends,
154
+ such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
155
+ of this see :issue:`3290`).
156
+ """
157
+ m = cls()
158
+ try:
159
+ yield m
160
+ finally:
161
+ m.undo()
162
+
163
+ @overload
164
+ def setattr(
165
+ self,
166
+ target: str,
167
+ name: object,
168
+ value: Notset = ...,
169
+ raising: bool = ...,
170
+ ) -> None: ...
171
+
172
+ @overload
173
+ def setattr(
174
+ self,
175
+ target: object,
176
+ name: str,
177
+ value: object,
178
+ raising: bool = ...,
179
+ ) -> None: ...
180
+
181
+ def setattr(
182
+ self,
183
+ target: str | object,
184
+ name: object | str,
185
+ value: object = notset,
186
+ raising: bool = True,
187
+ ) -> None:
188
+ """
189
+ Set attribute value on target, memorizing the old value.
190
+
191
+ For example:
192
+
193
+ .. code-block:: python
194
+
195
+ import os
196
+
197
+ monkeypatch.setattr(os, "getcwd", lambda: "/")
198
+
199
+ The code above replaces the :func:`os.getcwd` function by a ``lambda`` which
200
+ always returns ``"/"``.
201
+
202
+ For convenience, you can specify a string as ``target`` which
203
+ will be interpreted as a dotted import path, with the last part
204
+ being the attribute name:
205
+
206
+ .. code-block:: python
207
+
208
+ monkeypatch.setattr("os.getcwd", lambda: "/")
209
+
210
+ Raises :class:`AttributeError` if the attribute does not exist, unless
211
+ ``raising`` is set to False.
212
+
213
+ **Where to patch**
214
+
215
+ ``monkeypatch.setattr`` works by (temporarily) changing the object that a name points to with another one.
216
+ There can be many names pointing to any individual object, so for patching to work you must ensure
217
+ that you patch the name used by the system under test.
218
+
219
+ See the section :ref:`Where to patch <python:where-to-patch>` in the :mod:`unittest.mock`
220
+ docs for a complete explanation, which is meant for :func:`unittest.mock.patch` but
221
+ applies to ``monkeypatch.setattr`` as well.
222
+ """
223
+ __tracebackhide__ = True
224
+ import inspect
225
+
226
+ if isinstance(value, Notset):
227
+ if not isinstance(target, str):
228
+ raise TypeError(
229
+ "use setattr(target, name, value) or "
230
+ "setattr(target, value) with target being a dotted "
231
+ "import string"
232
+ )
233
+ value = name
234
+ name, target = derive_importpath(target, raising)
235
+ else:
236
+ if not isinstance(name, str):
237
+ raise TypeError(
238
+ "use setattr(target, name, value) with name being a string or "
239
+ "setattr(target, value) with target being a dotted "
240
+ "import string"
241
+ )
242
+
243
+ oldval = getattr(target, name, notset)
244
+ if raising and oldval is notset:
245
+ raise AttributeError(f"{target!r} has no attribute {name!r}")
246
+
247
+ # avoid class descriptors like staticmethod/classmethod
248
+ if inspect.isclass(target):
249
+ oldval = target.__dict__.get(name, notset)
250
+ self._setattr.append((target, name, oldval))
251
+ setattr(target, name, value)
252
+
253
+ def delattr(
254
+ self,
255
+ target: object | str,
256
+ name: str | Notset = notset,
257
+ raising: bool = True,
258
+ ) -> None:
259
+ """Delete attribute ``name`` from ``target``.
260
+
261
+ If no ``name`` is specified and ``target`` is a string
262
+ it will be interpreted as a dotted import path with the
263
+ last part being the attribute name.
264
+
265
+ Raises AttributeError it the attribute does not exist, unless
266
+ ``raising`` is set to False.
267
+ """
268
+ __tracebackhide__ = True
269
+ import inspect
270
+
271
+ if isinstance(name, Notset):
272
+ if not isinstance(target, str):
273
+ raise TypeError(
274
+ "use delattr(target, name) or "
275
+ "delattr(target) with target being a dotted "
276
+ "import string"
277
+ )
278
+ name, target = derive_importpath(target, raising)
279
+
280
+ if not hasattr(target, name):
281
+ if raising:
282
+ raise AttributeError(name)
283
+ else:
284
+ oldval = getattr(target, name, notset)
285
+ # Avoid class descriptors like staticmethod/classmethod.
286
+ if inspect.isclass(target):
287
+ oldval = target.__dict__.get(name, notset)
288
+ self._setattr.append((target, name, oldval))
289
+ delattr(target, name)
290
+
291
+ def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
292
+ """Set dictionary entry ``name`` to value."""
293
+ self._setitem.append((dic, name, dic.get(name, notset)))
294
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
295
+ dic[name] = value # type: ignore[index]
296
+
297
+ def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
298
+ """Delete ``name`` from dict.
299
+
300
+ Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
301
+ False.
302
+ """
303
+ if name not in dic:
304
+ if raising:
305
+ raise KeyError(name)
306
+ else:
307
+ self._setitem.append((dic, name, dic.get(name, notset)))
308
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
309
+ del dic[name] # type: ignore[attr-defined]
310
+
311
+ def setenv(self, name: str, value: str, prepend: str | None = None) -> None:
312
+ """Set environment variable ``name`` to ``value``.
313
+
314
+ If ``prepend`` is a character, read the current environment variable
315
+ value and prepend the ``value`` adjoined with the ``prepend``
316
+ character.
317
+ """
318
+ if not isinstance(value, str):
319
+ warnings.warn( # type: ignore[unreachable]
320
+ PytestWarning(
321
+ f"Value of environment variable {name} type should be str, but got "
322
+ f"{value!r} (type: {type(value).__name__}); converted to str implicitly"
323
+ ),
324
+ stacklevel=2,
325
+ )
326
+ value = str(value)
327
+ if prepend and name in os.environ:
328
+ value = value + prepend + os.environ[name]
329
+ self.setitem(os.environ, name, value)
330
+
331
+ def delenv(self, name: str, raising: bool = True) -> None:
332
+ """Delete ``name`` from the environment.
333
+
334
+ Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
335
+ False.
336
+ """
337
+ environ: MutableMapping[str, str] = os.environ
338
+ self.delitem(environ, name, raising=raising)
339
+
340
+ def syspath_prepend(self, path) -> None:
341
+ """Prepend ``path`` to ``sys.path`` list of import locations."""
342
+ if self._savesyspath is None:
343
+ self._savesyspath = sys.path[:]
344
+ sys.path.insert(0, str(path))
345
+
346
+ # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
347
+ # this is only needed when pkg_resources was already loaded by the namespace package
348
+ if "pkg_resources" in sys.modules:
349
+ from pkg_resources import fixup_namespace_packages
350
+
351
+ fixup_namespace_packages(str(path))
352
+
353
+ # A call to syspathinsert() usually means that the caller wants to
354
+ # import some dynamically created files, thus with python3 we
355
+ # invalidate its import caches.
356
+ # This is especially important when any namespace package is in use,
357
+ # since then the mtime based FileFinder cache (that gets created in
358
+ # this case already) gets not invalidated when writing the new files
359
+ # quickly afterwards.
360
+ from importlib import invalidate_caches
361
+
362
+ invalidate_caches()
363
+
364
+ def chdir(self, path: str | os.PathLike[str]) -> None:
365
+ """Change the current working directory to the specified path.
366
+
367
+ :param path:
368
+ The path to change into.
369
+ """
370
+ if self._cwd is None:
371
+ self._cwd = os.getcwd()
372
+ os.chdir(path)
373
+
374
+ def undo(self) -> None:
375
+ """Undo previous changes.
376
+
377
+ This call consumes the undo stack. Calling it a second time has no
378
+ effect unless you do more monkeypatching after the undo call.
379
+
380
+ There is generally no need to call `undo()`, since it is
381
+ called automatically during tear-down.
382
+
383
+ .. note::
384
+ The same `monkeypatch` fixture is used across a
385
+ single test function invocation. If `monkeypatch` is used both by
386
+ the test function itself and one of the test fixtures,
387
+ calling `undo()` will undo all of the changes made in
388
+ both functions.
389
+
390
+ Prefer to use :meth:`context() <pytest.MonkeyPatch.context>` instead.
391
+ """
392
+ for obj, name, value in reversed(self._setattr):
393
+ if value is not notset:
394
+ setattr(obj, name, value)
395
+ else:
396
+ delattr(obj, name)
397
+ self._setattr[:] = []
398
+ for dictionary, key, value in reversed(self._setitem):
399
+ if value is notset:
400
+ try:
401
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
402
+ del dictionary[key] # type: ignore[attr-defined]
403
+ except KeyError:
404
+ pass # Was already deleted, so we have the desired state.
405
+ else:
406
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
407
+ dictionary[key] = value # type: ignore[index]
408
+ self._setitem[:] = []
409
+ if self._savesyspath is not None:
410
+ sys.path[:] = self._savesyspath
411
+ self._savesyspath = None
412
+
413
+ if self._cwd is not None:
414
+ os.chdir(self._cwd)
415
+ self._cwd = None
.venv/lib/python3.11/site-packages/_pytest/outcomes.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exception classes and constants handling test outcomes as well as
2
+ functions creating them."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from typing import Any
8
+ from typing import Callable
9
+ from typing import cast
10
+ from typing import NoReturn
11
+ from typing import Protocol
12
+ from typing import Type
13
+ from typing import TypeVar
14
+
15
+ from .warning_types import PytestDeprecationWarning
16
+
17
+
18
+ class OutcomeException(BaseException):
19
+ """OutcomeException and its subclass instances indicate and contain info
20
+ about test and collection outcomes."""
21
+
22
+ def __init__(self, msg: str | None = None, pytrace: bool = True) -> None:
23
+ if msg is not None and not isinstance(msg, str):
24
+ error_msg = ( # type: ignore[unreachable]
25
+ "{} expected string as 'msg' parameter, got '{}' instead.\n"
26
+ "Perhaps you meant to use a mark?"
27
+ )
28
+ raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__))
29
+ super().__init__(msg)
30
+ self.msg = msg
31
+ self.pytrace = pytrace
32
+
33
+ def __repr__(self) -> str:
34
+ if self.msg is not None:
35
+ return self.msg
36
+ return f"<{self.__class__.__name__} instance>"
37
+
38
+ __str__ = __repr__
39
+
40
+
41
+ TEST_OUTCOME = (OutcomeException, Exception)
42
+
43
+
44
+ class Skipped(OutcomeException):
45
+ # XXX hackish: on 3k we fake to live in the builtins
46
+ # in order to have Skipped exception printing shorter/nicer
47
+ __module__ = "builtins"
48
+
49
+ def __init__(
50
+ self,
51
+ msg: str | None = None,
52
+ pytrace: bool = True,
53
+ allow_module_level: bool = False,
54
+ *,
55
+ _use_item_location: bool = False,
56
+ ) -> None:
57
+ super().__init__(msg=msg, pytrace=pytrace)
58
+ self.allow_module_level = allow_module_level
59
+ # If true, the skip location is reported as the item's location,
60
+ # instead of the place that raises the exception/calls skip().
61
+ self._use_item_location = _use_item_location
62
+
63
+
64
+ class Failed(OutcomeException):
65
+ """Raised from an explicit call to pytest.fail()."""
66
+
67
+ __module__ = "builtins"
68
+
69
+
70
+ class Exit(Exception):
71
+ """Raised for immediate program exits (no tracebacks/summaries)."""
72
+
73
+ def __init__(
74
+ self, msg: str = "unknown reason", returncode: int | None = None
75
+ ) -> None:
76
+ self.msg = msg
77
+ self.returncode = returncode
78
+ super().__init__(msg)
79
+
80
+
81
+ # Elaborate hack to work around https://github.com/python/mypy/issues/2087.
82
+ # Ideally would just be `exit.Exception = Exit` etc.
83
+
84
+ _F = TypeVar("_F", bound=Callable[..., object])
85
+ _ET = TypeVar("_ET", bound=Type[BaseException])
86
+
87
+
88
+ class _WithException(Protocol[_F, _ET]):
89
+ Exception: _ET
90
+ __call__: _F
91
+
92
+
93
+ def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]:
94
+ def decorate(func: _F) -> _WithException[_F, _ET]:
95
+ func_with_exception = cast(_WithException[_F, _ET], func)
96
+ func_with_exception.Exception = exception_type
97
+ return func_with_exception
98
+
99
+ return decorate
100
+
101
+
102
+ # Exposed helper methods.
103
+
104
+
105
+ @_with_exception(Exit)
106
+ def exit(
107
+ reason: str = "",
108
+ returncode: int | None = None,
109
+ ) -> NoReturn:
110
+ """Exit testing process.
111
+
112
+ :param reason:
113
+ The message to show as the reason for exiting pytest. reason has a default value
114
+ only because `msg` is deprecated.
115
+
116
+ :param returncode:
117
+ Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
118
+
119
+ :raises pytest.exit.Exception:
120
+ The exception that is raised.
121
+ """
122
+ __tracebackhide__ = True
123
+ raise Exit(reason, returncode)
124
+
125
+
126
+ @_with_exception(Skipped)
127
+ def skip(
128
+ reason: str = "",
129
+ *,
130
+ allow_module_level: bool = False,
131
+ ) -> NoReturn:
132
+ """Skip an executing test with the given message.
133
+
134
+ This function should be called only during testing (setup, call or teardown) or
135
+ during collection by using the ``allow_module_level`` flag. This function can
136
+ be called in doctests as well.
137
+
138
+ :param reason:
139
+ The message to show the user as reason for the skip.
140
+
141
+ :param allow_module_level:
142
+ Allows this function to be called at module level.
143
+ Raising the skip exception at module level will stop
144
+ the execution of the module and prevent the collection of all tests in the module,
145
+ even those defined before the `skip` call.
146
+
147
+ Defaults to False.
148
+
149
+ :raises pytest.skip.Exception:
150
+ The exception that is raised.
151
+
152
+ .. note::
153
+ It is better to use the :ref:`pytest.mark.skipif ref` marker when
154
+ possible to declare a test to be skipped under certain conditions
155
+ like mismatching platforms or dependencies.
156
+ Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`)
157
+ to skip a doctest statically.
158
+ """
159
+ __tracebackhide__ = True
160
+ raise Skipped(msg=reason, allow_module_level=allow_module_level)
161
+
162
+
163
+ @_with_exception(Failed)
164
+ def fail(reason: str = "", pytrace: bool = True) -> NoReturn:
165
+ """Explicitly fail an executing test with the given message.
166
+
167
+ :param reason:
168
+ The message to show the user as reason for the failure.
169
+
170
+ :param pytrace:
171
+ If False, msg represents the full failure information and no
172
+ python traceback will be reported.
173
+
174
+ :raises pytest.fail.Exception:
175
+ The exception that is raised.
176
+ """
177
+ __tracebackhide__ = True
178
+ raise Failed(msg=reason, pytrace=pytrace)
179
+
180
+
181
+ class XFailed(Failed):
182
+ """Raised from an explicit call to pytest.xfail()."""
183
+
184
+
185
+ @_with_exception(XFailed)
186
+ def xfail(reason: str = "") -> NoReturn:
187
+ """Imperatively xfail an executing test or setup function with the given reason.
188
+
189
+ This function should be called only during testing (setup, call or teardown).
190
+
191
+ No other code is executed after using ``xfail()`` (it is implemented
192
+ internally by raising an exception).
193
+
194
+ :param reason:
195
+ The message to show the user as reason for the xfail.
196
+
197
+ .. note::
198
+ It is better to use the :ref:`pytest.mark.xfail ref` marker when
199
+ possible to declare a test to be xfailed under certain conditions
200
+ like known bugs or missing features.
201
+
202
+ :raises pytest.xfail.Exception:
203
+ The exception that is raised.
204
+ """
205
+ __tracebackhide__ = True
206
+ raise XFailed(reason)
207
+
208
+
209
+ def importorskip(
210
+ modname: str,
211
+ minversion: str | None = None,
212
+ reason: str | None = None,
213
+ *,
214
+ exc_type: type[ImportError] | None = None,
215
+ ) -> Any:
216
+ """Import and return the requested module ``modname``, or skip the
217
+ current test if the module cannot be imported.
218
+
219
+ :param modname:
220
+ The name of the module to import.
221
+ :param minversion:
222
+ If given, the imported module's ``__version__`` attribute must be at
223
+ least this minimal version, otherwise the test is still skipped.
224
+ :param reason:
225
+ If given, this reason is shown as the message when the module cannot
226
+ be imported.
227
+ :param exc_type:
228
+ The exception that should be captured in order to skip modules.
229
+ Must be :py:class:`ImportError` or a subclass.
230
+
231
+ If the module can be imported but raises :class:`ImportError`, pytest will
232
+ issue a warning to the user, as often users expect the module not to be
233
+ found (which would raise :class:`ModuleNotFoundError` instead).
234
+
235
+ This warning can be suppressed by passing ``exc_type=ImportError`` explicitly.
236
+
237
+ See :ref:`import-or-skip-import-error` for details.
238
+
239
+
240
+ :returns:
241
+ The imported module. This should be assigned to its canonical name.
242
+
243
+ :raises pytest.skip.Exception:
244
+ If the module cannot be imported.
245
+
246
+ Example::
247
+
248
+ docutils = pytest.importorskip("docutils")
249
+
250
+ .. versionadded:: 8.2
251
+
252
+ The ``exc_type`` parameter.
253
+ """
254
+ import warnings
255
+
256
+ __tracebackhide__ = True
257
+ compile(modname, "", "eval") # to catch syntaxerrors
258
+
259
+ # Until pytest 9.1, we will warn the user if we catch ImportError (instead of ModuleNotFoundError),
260
+ # as this might be hiding an installation/environment problem, which is not usually what is intended
261
+ # when using importorskip() (#11523).
262
+ # In 9.1, to keep the function signature compatible, we just change the code below to:
263
+ # 1. Use `exc_type = ModuleNotFoundError` if `exc_type` is not given.
264
+ # 2. Remove `warn_on_import` and the warning handling.
265
+ if exc_type is None:
266
+ exc_type = ImportError
267
+ warn_on_import_error = True
268
+ else:
269
+ warn_on_import_error = False
270
+
271
+ skipped: Skipped | None = None
272
+ warning: Warning | None = None
273
+
274
+ with warnings.catch_warnings():
275
+ # Make sure to ignore ImportWarnings that might happen because
276
+ # of existing directories with the same name we're trying to
277
+ # import but without a __init__.py file.
278
+ warnings.simplefilter("ignore")
279
+
280
+ try:
281
+ __import__(modname)
282
+ except exc_type as exc:
283
+ # Do not raise or issue warnings inside the catch_warnings() block.
284
+ if reason is None:
285
+ reason = f"could not import {modname!r}: {exc}"
286
+ skipped = Skipped(reason, allow_module_level=True)
287
+
288
+ if warn_on_import_error and not isinstance(exc, ModuleNotFoundError):
289
+ lines = [
290
+ "",
291
+ f"Module '{modname}' was found, but when imported by pytest it raised:",
292
+ f" {exc!r}",
293
+ "In pytest 9.1 this warning will become an error by default.",
294
+ "You can fix the underlying problem, or alternatively overwrite this behavior and silence this "
295
+ "warning by passing exc_type=ImportError explicitly.",
296
+ "See https://docs.pytest.org/en/stable/deprecations.html#pytest-importorskip-default-behavior-regarding-importerror",
297
+ ]
298
+ warning = PytestDeprecationWarning("\n".join(lines))
299
+
300
+ if warning:
301
+ warnings.warn(warning, stacklevel=2)
302
+ if skipped:
303
+ raise skipped
304
+
305
+ mod = sys.modules[modname]
306
+ if minversion is None:
307
+ return mod
308
+ verattr = getattr(mod, "__version__", None)
309
+ if minversion is not None:
310
+ # Imported lazily to improve start-up time.
311
+ from packaging.version import Version
312
+
313
+ if verattr is None or Version(verattr) < Version(minversion):
314
+ raise Skipped(
315
+ f"module {modname!r} has __version__ {verattr!r}, required is: {minversion!r}",
316
+ allow_module_level=True,
317
+ )
318
+ return mod
.venv/lib/python3.11/site-packages/_pytest/pastebin.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Submit failure or test session information to a pastebin service."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from io import StringIO
7
+ import tempfile
8
+ from typing import IO
9
+
10
+ from _pytest.config import Config
11
+ from _pytest.config import create_terminal_writer
12
+ from _pytest.config.argparsing import Parser
13
+ from _pytest.stash import StashKey
14
+ from _pytest.terminal import TerminalReporter
15
+ import pytest
16
+
17
+
18
+ pastebinfile_key = StashKey[IO[bytes]]()
19
+
20
+
21
+ def pytest_addoption(parser: Parser) -> None:
22
+ group = parser.getgroup("terminal reporting")
23
+ group._addoption(
24
+ "--pastebin",
25
+ metavar="mode",
26
+ action="store",
27
+ dest="pastebin",
28
+ default=None,
29
+ choices=["failed", "all"],
30
+ help="Send failed|all info to bpaste.net pastebin service",
31
+ )
32
+
33
+
34
+ @pytest.hookimpl(trylast=True)
35
+ def pytest_configure(config: Config) -> None:
36
+ if config.option.pastebin == "all":
37
+ tr = config.pluginmanager.getplugin("terminalreporter")
38
+ # If no terminal reporter plugin is present, nothing we can do here;
39
+ # this can happen when this function executes in a worker node
40
+ # when using pytest-xdist, for example.
41
+ if tr is not None:
42
+ # pastebin file will be UTF-8 encoded binary file.
43
+ config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b")
44
+ oldwrite = tr._tw.write
45
+
46
+ def tee_write(s, **kwargs):
47
+ oldwrite(s, **kwargs)
48
+ if isinstance(s, str):
49
+ s = s.encode("utf-8")
50
+ config.stash[pastebinfile_key].write(s)
51
+
52
+ tr._tw.write = tee_write
53
+
54
+
55
+ def pytest_unconfigure(config: Config) -> None:
56
+ if pastebinfile_key in config.stash:
57
+ pastebinfile = config.stash[pastebinfile_key]
58
+ # Get terminal contents and delete file.
59
+ pastebinfile.seek(0)
60
+ sessionlog = pastebinfile.read()
61
+ pastebinfile.close()
62
+ del config.stash[pastebinfile_key]
63
+ # Undo our patching in the terminal reporter.
64
+ tr = config.pluginmanager.getplugin("terminalreporter")
65
+ del tr._tw.__dict__["write"]
66
+ # Write summary.
67
+ tr.write_sep("=", "Sending information to Paste Service")
68
+ pastebinurl = create_new_paste(sessionlog)
69
+ tr.write_line(f"pastebin session-log: {pastebinurl}\n")
70
+
71
+
72
+ def create_new_paste(contents: str | bytes) -> str:
73
+ """Create a new paste using the bpaste.net service.
74
+
75
+ :contents: Paste contents string.
76
+ :returns: URL to the pasted contents, or an error message.
77
+ """
78
+ import re
79
+ from urllib.parse import urlencode
80
+ from urllib.request import urlopen
81
+
82
+ params = {"code": contents, "lexer": "text", "expiry": "1week"}
83
+ url = "https://bpa.st"
84
+ try:
85
+ response: str = (
86
+ urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
87
+ )
88
+ except OSError as exc_info: # urllib errors
89
+ return f"bad response: {exc_info}"
90
+ m = re.search(r'href="/raw/(\w+)"', response)
91
+ if m:
92
+ return f"{url}/show/{m.group(1)}"
93
+ else:
94
+ return "bad response: invalid format ('" + response + "')"
95
+
96
+
97
+ def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
98
+ if terminalreporter.config.option.pastebin != "failed":
99
+ return
100
+ if "failed" in terminalreporter.stats:
101
+ terminalreporter.write_sep("=", "Sending information to Paste Service")
102
+ for rep in terminalreporter.stats["failed"]:
103
+ try:
104
+ msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc
105
+ except AttributeError:
106
+ msg = terminalreporter._getfailureheadline(rep)
107
+ file = StringIO()
108
+ tw = create_terminal_writer(terminalreporter.config, file)
109
+ rep.toterminal(tw)
110
+ s = file.getvalue()
111
+ assert len(s)
112
+ pastebinurl = create_new_paste(s)
113
+ terminalreporter.write_line(f"{msg} --> {pastebinurl}")
.venv/lib/python3.11/site-packages/_pytest/pathlib.py ADDED
@@ -0,0 +1,1038 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import contextlib
5
+ from enum import Enum
6
+ from errno import EBADF
7
+ from errno import ELOOP
8
+ from errno import ENOENT
9
+ from errno import ENOTDIR
10
+ import fnmatch
11
+ from functools import partial
12
+ from importlib.machinery import ModuleSpec
13
+ from importlib.machinery import PathFinder
14
+ import importlib.util
15
+ import itertools
16
+ import os
17
+ from os.path import expanduser
18
+ from os.path import expandvars
19
+ from os.path import isabs
20
+ from os.path import sep
21
+ from pathlib import Path
22
+ from pathlib import PurePath
23
+ from posixpath import sep as posix_sep
24
+ import shutil
25
+ import sys
26
+ import types
27
+ from types import ModuleType
28
+ from typing import Any
29
+ from typing import Callable
30
+ from typing import Iterable
31
+ from typing import Iterator
32
+ from typing import TypeVar
33
+ import uuid
34
+ import warnings
35
+
36
+ from _pytest.compat import assert_never
37
+ from _pytest.outcomes import skip
38
+ from _pytest.warning_types import PytestWarning
39
+
40
+
41
+ if sys.version_info < (3, 11):
42
+ from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader
43
+ else:
44
+ from importlib.machinery import NamespaceLoader
45
+
46
+ LOCK_TIMEOUT = 60 * 60 * 24 * 3
47
+
48
+ _AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
49
+
50
+ # The following function, variables and comments were
51
+ # copied from cpython 3.9 Lib/pathlib.py file.
52
+
53
+ # EBADF - guard against macOS `stat` throwing EBADF
54
+ _IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP)
55
+
56
+ _IGNORED_WINERRORS = (
57
+ 21, # ERROR_NOT_READY - drive exists but is not accessible
58
+ 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself
59
+ )
60
+
61
+
62
+ def _ignore_error(exception: Exception) -> bool:
63
+ return (
64
+ getattr(exception, "errno", None) in _IGNORED_ERRORS
65
+ or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
66
+ )
67
+
68
+
69
+ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
70
+ return path.joinpath(".lock")
71
+
72
+
73
+ def on_rm_rf_error(
74
+ func: Callable[..., Any] | None,
75
+ path: str,
76
+ excinfo: BaseException
77
+ | tuple[type[BaseException], BaseException, types.TracebackType | None],
78
+ *,
79
+ start_path: Path,
80
+ ) -> bool:
81
+ """Handle known read-only errors during rmtree.
82
+
83
+ The returned value is used only by our own tests.
84
+ """
85
+ if isinstance(excinfo, BaseException):
86
+ exc = excinfo
87
+ else:
88
+ exc = excinfo[1]
89
+
90
+ # Another process removed the file in the middle of the "rm_rf" (xdist for example).
91
+ # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
92
+ if isinstance(exc, FileNotFoundError):
93
+ return False
94
+
95
+ if not isinstance(exc, PermissionError):
96
+ warnings.warn(
97
+ PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
98
+ )
99
+ return False
100
+
101
+ if func not in (os.rmdir, os.remove, os.unlink):
102
+ if func not in (os.open,):
103
+ warnings.warn(
104
+ PytestWarning(
105
+ f"(rm_rf) unknown function {func} when removing {path}:\n{type(exc)}: {exc}"
106
+ )
107
+ )
108
+ return False
109
+
110
+ # Chmod + retry.
111
+ import stat
112
+
113
+ def chmod_rw(p: str) -> None:
114
+ mode = os.stat(p).st_mode
115
+ os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR)
116
+
117
+ # For files, we need to recursively go upwards in the directories to
118
+ # ensure they all are also writable.
119
+ p = Path(path)
120
+ if p.is_file():
121
+ for parent in p.parents:
122
+ chmod_rw(str(parent))
123
+ # Stop when we reach the original path passed to rm_rf.
124
+ if parent == start_path:
125
+ break
126
+ chmod_rw(str(path))
127
+
128
+ func(path)
129
+ return True
130
+
131
+
132
+ def ensure_extended_length_path(path: Path) -> Path:
133
+ """Get the extended-length version of a path (Windows).
134
+
135
+ On Windows, by default, the maximum length of a path (MAX_PATH) is 260
136
+ characters, and operations on paths longer than that fail. But it is possible
137
+ to overcome this by converting the path to "extended-length" form before
138
+ performing the operation:
139
+ https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
140
+
141
+ On Windows, this function returns the extended-length absolute version of path.
142
+ On other platforms it returns path unchanged.
143
+ """
144
+ if sys.platform.startswith("win32"):
145
+ path = path.resolve()
146
+ path = Path(get_extended_length_path_str(str(path)))
147
+ return path
148
+
149
+
150
+ def get_extended_length_path_str(path: str) -> str:
151
+ """Convert a path to a Windows extended length path."""
152
+ long_path_prefix = "\\\\?\\"
153
+ unc_long_path_prefix = "\\\\?\\UNC\\"
154
+ if path.startswith((long_path_prefix, unc_long_path_prefix)):
155
+ return path
156
+ # UNC
157
+ if path.startswith("\\\\"):
158
+ return unc_long_path_prefix + path[2:]
159
+ return long_path_prefix + path
160
+
161
+
162
+ def rm_rf(path: Path) -> None:
163
+ """Remove the path contents recursively, even if some elements
164
+ are read-only."""
165
+ path = ensure_extended_length_path(path)
166
+ onerror = partial(on_rm_rf_error, start_path=path)
167
+ if sys.version_info >= (3, 12):
168
+ shutil.rmtree(str(path), onexc=onerror)
169
+ else:
170
+ shutil.rmtree(str(path), onerror=onerror)
171
+
172
+
173
+ def find_prefixed(root: Path, prefix: str) -> Iterator[os.DirEntry[str]]:
174
+ """Find all elements in root that begin with the prefix, case-insensitive."""
175
+ l_prefix = prefix.lower()
176
+ for x in os.scandir(root):
177
+ if x.name.lower().startswith(l_prefix):
178
+ yield x
179
+
180
+
181
+ def extract_suffixes(iter: Iterable[os.DirEntry[str]], prefix: str) -> Iterator[str]:
182
+ """Return the parts of the paths following the prefix.
183
+
184
+ :param iter: Iterator over path names.
185
+ :param prefix: Expected prefix of the path names.
186
+ """
187
+ p_len = len(prefix)
188
+ for entry in iter:
189
+ yield entry.name[p_len:]
190
+
191
+
192
+ def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
193
+ """Combine find_prefixes and extract_suffixes."""
194
+ return extract_suffixes(find_prefixed(root, prefix), prefix)
195
+
196
+
197
+ def parse_num(maybe_num: str) -> int:
198
+ """Parse number path suffixes, returns -1 on error."""
199
+ try:
200
+ return int(maybe_num)
201
+ except ValueError:
202
+ return -1
203
+
204
+
205
+ def _force_symlink(root: Path, target: str | PurePath, link_to: str | Path) -> None:
206
+ """Helper to create the current symlink.
207
+
208
+ It's full of race conditions that are reasonably OK to ignore
209
+ for the context of best effort linking to the latest test run.
210
+
211
+ The presumption being that in case of much parallelism
212
+ the inaccuracy is going to be acceptable.
213
+ """
214
+ current_symlink = root.joinpath(target)
215
+ try:
216
+ current_symlink.unlink()
217
+ except OSError:
218
+ pass
219
+ try:
220
+ current_symlink.symlink_to(link_to)
221
+ except Exception:
222
+ pass
223
+
224
+
225
+ def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path:
226
+ """Create a directory with an increased number as suffix for the given prefix."""
227
+ for i in range(10):
228
+ # try up to 10 times to create the folder
229
+ max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
230
+ new_number = max_existing + 1
231
+ new_path = root.joinpath(f"{prefix}{new_number}")
232
+ try:
233
+ new_path.mkdir(mode=mode)
234
+ except Exception:
235
+ pass
236
+ else:
237
+ _force_symlink(root, prefix + "current", new_path)
238
+ return new_path
239
+ else:
240
+ raise OSError(
241
+ "could not create numbered dir with prefix "
242
+ f"{prefix} in {root} after 10 tries"
243
+ )
244
+
245
+
246
+ def create_cleanup_lock(p: Path) -> Path:
247
+ """Create a lock to prevent premature folder cleanup."""
248
+ lock_path = get_lock_path(p)
249
+ try:
250
+ fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
251
+ except FileExistsError as e:
252
+ raise OSError(f"cannot create lockfile in {p}") from e
253
+ else:
254
+ pid = os.getpid()
255
+ spid = str(pid).encode()
256
+ os.write(fd, spid)
257
+ os.close(fd)
258
+ if not lock_path.is_file():
259
+ raise OSError("lock path got renamed after successful creation")
260
+ return lock_path
261
+
262
+
263
+ def register_cleanup_lock_removal(
264
+ lock_path: Path, register: Any = atexit.register
265
+ ) -> Any:
266
+ """Register a cleanup function for removing a lock, by default on atexit."""
267
+ pid = os.getpid()
268
+
269
+ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:
270
+ current_pid = os.getpid()
271
+ if current_pid != original_pid:
272
+ # fork
273
+ return
274
+ try:
275
+ lock_path.unlink()
276
+ except OSError:
277
+ pass
278
+
279
+ return register(cleanup_on_exit)
280
+
281
+
282
+ def maybe_delete_a_numbered_dir(path: Path) -> None:
283
+ """Remove a numbered directory if its lock can be obtained and it does
284
+ not seem to be in use."""
285
+ path = ensure_extended_length_path(path)
286
+ lock_path = None
287
+ try:
288
+ lock_path = create_cleanup_lock(path)
289
+ parent = path.parent
290
+
291
+ garbage = parent.joinpath(f"garbage-{uuid.uuid4()}")
292
+ path.rename(garbage)
293
+ rm_rf(garbage)
294
+ except OSError:
295
+ # known races:
296
+ # * other process did a cleanup at the same time
297
+ # * deletable folder was found
298
+ # * process cwd (Windows)
299
+ return
300
+ finally:
301
+ # If we created the lock, ensure we remove it even if we failed
302
+ # to properly remove the numbered dir.
303
+ if lock_path is not None:
304
+ try:
305
+ lock_path.unlink()
306
+ except OSError:
307
+ pass
308
+
309
+
310
+ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool:
311
+ """Check if `path` is deletable based on whether the lock file is expired."""
312
+ if path.is_symlink():
313
+ return False
314
+ lock = get_lock_path(path)
315
+ try:
316
+ if not lock.is_file():
317
+ return True
318
+ except OSError:
319
+ # we might not have access to the lock file at all, in this case assume
320
+ # we don't have access to the entire directory (#7491).
321
+ return False
322
+ try:
323
+ lock_time = lock.stat().st_mtime
324
+ except Exception:
325
+ return False
326
+ else:
327
+ if lock_time < consider_lock_dead_if_created_before:
328
+ # We want to ignore any errors while trying to remove the lock such as:
329
+ # - PermissionDenied, like the file permissions have changed since the lock creation;
330
+ # - FileNotFoundError, in case another pytest process got here first;
331
+ # and any other cause of failure.
332
+ with contextlib.suppress(OSError):
333
+ lock.unlink()
334
+ return True
335
+ return False
336
+
337
+
338
+ def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None:
339
+ """Try to cleanup a folder if we can ensure it's deletable."""
340
+ if ensure_deletable(path, consider_lock_dead_if_created_before):
341
+ maybe_delete_a_numbered_dir(path)
342
+
343
+
344
+ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
345
+ """List candidates for numbered directories to be removed - follows py.path."""
346
+ max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
347
+ max_delete = max_existing - keep
348
+ entries = find_prefixed(root, prefix)
349
+ entries, entries2 = itertools.tee(entries)
350
+ numbers = map(parse_num, extract_suffixes(entries2, prefix))
351
+ for entry, number in zip(entries, numbers):
352
+ if number <= max_delete:
353
+ yield Path(entry)
354
+
355
+
356
+ def cleanup_dead_symlinks(root: Path) -> None:
357
+ for left_dir in root.iterdir():
358
+ if left_dir.is_symlink():
359
+ if not left_dir.resolve().exists():
360
+ left_dir.unlink()
361
+
362
+
363
+ def cleanup_numbered_dir(
364
+ root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
365
+ ) -> None:
366
+ """Cleanup for lock driven numbered directories."""
367
+ if not root.exists():
368
+ return
369
+ for path in cleanup_candidates(root, prefix, keep):
370
+ try_cleanup(path, consider_lock_dead_if_created_before)
371
+ for path in root.glob("garbage-*"):
372
+ try_cleanup(path, consider_lock_dead_if_created_before)
373
+
374
+ cleanup_dead_symlinks(root)
375
+
376
+
377
+ def make_numbered_dir_with_cleanup(
378
+ root: Path,
379
+ prefix: str,
380
+ keep: int,
381
+ lock_timeout: float,
382
+ mode: int,
383
+ ) -> Path:
384
+ """Create a numbered dir with a cleanup lock and remove old ones."""
385
+ e = None
386
+ for i in range(10):
387
+ try:
388
+ p = make_numbered_dir(root, prefix, mode)
389
+ # Only lock the current dir when keep is not 0
390
+ if keep != 0:
391
+ lock_path = create_cleanup_lock(p)
392
+ register_cleanup_lock_removal(lock_path)
393
+ except Exception as exc:
394
+ e = exc
395
+ else:
396
+ consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout
397
+ # Register a cleanup for program exit
398
+ atexit.register(
399
+ cleanup_numbered_dir,
400
+ root,
401
+ prefix,
402
+ keep,
403
+ consider_lock_dead_if_created_before,
404
+ )
405
+ return p
406
+ assert e is not None
407
+ raise e
408
+
409
+
410
+ def resolve_from_str(input: str, rootpath: Path) -> Path:
411
+ input = expanduser(input)
412
+ input = expandvars(input)
413
+ if isabs(input):
414
+ return Path(input)
415
+ else:
416
+ return rootpath.joinpath(input)
417
+
418
+
419
+ def fnmatch_ex(pattern: str, path: str | os.PathLike[str]) -> bool:
420
+ """A port of FNMatcher from py.path.common which works with PurePath() instances.
421
+
422
+ The difference between this algorithm and PurePath.match() is that the
423
+ latter matches "**" glob expressions for each part of the path, while
424
+ this algorithm uses the whole path instead.
425
+
426
+ For example:
427
+ "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py"
428
+ with this algorithm, but not with PurePath.match().
429
+
430
+ This algorithm was ported to keep backward-compatibility with existing
431
+ settings which assume paths match according this logic.
432
+
433
+ References:
434
+ * https://bugs.python.org/issue29249
435
+ * https://bugs.python.org/issue34731
436
+ """
437
+ path = PurePath(path)
438
+ iswin32 = sys.platform.startswith("win")
439
+
440
+ if iswin32 and sep not in pattern and posix_sep in pattern:
441
+ # Running on Windows, the pattern has no Windows path separators,
442
+ # and the pattern has one or more Posix path separators. Replace
443
+ # the Posix path separators with the Windows path separator.
444
+ pattern = pattern.replace(posix_sep, sep)
445
+
446
+ if sep not in pattern:
447
+ name = path.name
448
+ else:
449
+ name = str(path)
450
+ if path.is_absolute() and not os.path.isabs(pattern):
451
+ pattern = f"*{os.sep}{pattern}"
452
+ return fnmatch.fnmatch(name, pattern)
453
+
454
+
455
+ def parts(s: str) -> set[str]:
456
+ parts = s.split(sep)
457
+ return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
458
+
459
+
460
+ def symlink_or_skip(
461
+ src: os.PathLike[str] | str,
462
+ dst: os.PathLike[str] | str,
463
+ **kwargs: Any,
464
+ ) -> None:
465
+ """Make a symlink, or skip the test in case symlinks are not supported."""
466
+ try:
467
+ os.symlink(src, dst, **kwargs)
468
+ except OSError as e:
469
+ skip(f"symlinks not supported: {e}")
470
+
471
+
472
+ class ImportMode(Enum):
473
+ """Possible values for `mode` parameter of `import_path`."""
474
+
475
+ prepend = "prepend"
476
+ append = "append"
477
+ importlib = "importlib"
478
+
479
+
480
+ class ImportPathMismatchError(ImportError):
481
+ """Raised on import_path() if there is a mismatch of __file__'s.
482
+
483
+ This can happen when `import_path` is called multiple times with different filenames that has
484
+ the same basename but reside in packages
485
+ (for example "/tests1/test_foo.py" and "/tests2/test_foo.py").
486
+ """
487
+
488
+
489
+ def import_path(
490
+ path: str | os.PathLike[str],
491
+ *,
492
+ mode: str | ImportMode = ImportMode.prepend,
493
+ root: Path,
494
+ consider_namespace_packages: bool,
495
+ ) -> ModuleType:
496
+ """
497
+ Import and return a module from the given path, which can be a file (a module) or
498
+ a directory (a package).
499
+
500
+ :param path:
501
+ Path to the file to import.
502
+
503
+ :param mode:
504
+ Controls the underlying import mechanism that will be used:
505
+
506
+ * ImportMode.prepend: the directory containing the module (or package, taking
507
+ `__init__.py` files into account) will be put at the *start* of `sys.path` before
508
+ being imported with `importlib.import_module`.
509
+
510
+ * ImportMode.append: same as `prepend`, but the directory will be appended
511
+ to the end of `sys.path`, if not already in `sys.path`.
512
+
513
+ * ImportMode.importlib: uses more fine control mechanisms provided by `importlib`
514
+ to import the module, which avoids having to muck with `sys.path` at all. It effectively
515
+ allows having same-named test modules in different places.
516
+
517
+ :param root:
518
+ Used as an anchor when mode == ImportMode.importlib to obtain
519
+ a unique name for the module being imported so it can safely be stored
520
+ into ``sys.modules``.
521
+
522
+ :param consider_namespace_packages:
523
+ If True, consider namespace packages when resolving module names.
524
+
525
+ :raises ImportPathMismatchError:
526
+ If after importing the given `path` and the module `__file__`
527
+ are different. Only raised in `prepend` and `append` modes.
528
+ """
529
+ path = Path(path)
530
+ mode = ImportMode(mode)
531
+
532
+ if not path.exists():
533
+ raise ImportError(path)
534
+
535
+ if mode is ImportMode.importlib:
536
+ # Try to import this module using the standard import mechanisms, but
537
+ # without touching sys.path.
538
+ try:
539
+ pkg_root, module_name = resolve_pkg_root_and_module_name(
540
+ path, consider_namespace_packages=consider_namespace_packages
541
+ )
542
+ except CouldNotResolvePathError:
543
+ pass
544
+ else:
545
+ # If the given module name is already in sys.modules, do not import it again.
546
+ with contextlib.suppress(KeyError):
547
+ return sys.modules[module_name]
548
+
549
+ mod = _import_module_using_spec(
550
+ module_name, path, pkg_root, insert_modules=False
551
+ )
552
+ if mod is not None:
553
+ return mod
554
+
555
+ # Could not import the module with the current sys.path, so we fall back
556
+ # to importing the file as a single module, not being a part of a package.
557
+ module_name = module_name_from_path(path, root)
558
+ with contextlib.suppress(KeyError):
559
+ return sys.modules[module_name]
560
+
561
+ mod = _import_module_using_spec(
562
+ module_name, path, path.parent, insert_modules=True
563
+ )
564
+ if mod is None:
565
+ raise ImportError(f"Can't find module {module_name} at location {path}")
566
+ return mod
567
+
568
+ try:
569
+ pkg_root, module_name = resolve_pkg_root_and_module_name(
570
+ path, consider_namespace_packages=consider_namespace_packages
571
+ )
572
+ except CouldNotResolvePathError:
573
+ pkg_root, module_name = path.parent, path.stem
574
+
575
+ # Change sys.path permanently: restoring it at the end of this function would cause surprising
576
+ # problems because of delayed imports: for example, a conftest.py file imported by this function
577
+ # might have local imports, which would fail at runtime if we restored sys.path.
578
+ if mode is ImportMode.append:
579
+ if str(pkg_root) not in sys.path:
580
+ sys.path.append(str(pkg_root))
581
+ elif mode is ImportMode.prepend:
582
+ if str(pkg_root) != sys.path[0]:
583
+ sys.path.insert(0, str(pkg_root))
584
+ else:
585
+ assert_never(mode)
586
+
587
+ importlib.import_module(module_name)
588
+
589
+ mod = sys.modules[module_name]
590
+ if path.name == "__init__.py":
591
+ return mod
592
+
593
+ ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "")
594
+ if ignore != "1":
595
+ module_file = mod.__file__
596
+ if module_file is None:
597
+ raise ImportPathMismatchError(module_name, module_file, path)
598
+
599
+ if module_file.endswith((".pyc", ".pyo")):
600
+ module_file = module_file[:-1]
601
+ if module_file.endswith(os.sep + "__init__.py"):
602
+ module_file = module_file[: -(len(os.sep + "__init__.py"))]
603
+
604
+ try:
605
+ is_same = _is_same(str(path), module_file)
606
+ except FileNotFoundError:
607
+ is_same = False
608
+
609
+ if not is_same:
610
+ raise ImportPathMismatchError(module_name, module_file, path)
611
+
612
+ return mod
613
+
614
+
615
+ def _import_module_using_spec(
616
+ module_name: str, module_path: Path, module_location: Path, *, insert_modules: bool
617
+ ) -> ModuleType | None:
618
+ """
619
+ Tries to import a module by its canonical name, path, and its parent location.
620
+
621
+ :param module_name:
622
+ The expected module name, will become the key of `sys.modules`.
623
+
624
+ :param module_path:
625
+ The file path of the module, for example `/foo/bar/test_demo.py`.
626
+ If module is a package, pass the path to the `__init__.py` of the package.
627
+ If module is a namespace package, pass directory path.
628
+
629
+ :param module_location:
630
+ The parent location of the module.
631
+ If module is a package, pass the directory containing the `__init__.py` file.
632
+
633
+ :param insert_modules:
634
+ If True, will call `insert_missing_modules` to create empty intermediate modules
635
+ with made-up module names (when importing test files not reachable from `sys.path`).
636
+
637
+ Example 1 of parent_module_*:
638
+
639
+ module_name: "a.b.c.demo"
640
+ module_path: Path("a/b/c/demo.py")
641
+ module_location: Path("a/b/c/")
642
+ if "a.b.c" is package ("a/b/c/__init__.py" exists), then
643
+ parent_module_name: "a.b.c"
644
+ parent_module_path: Path("a/b/c/__init__.py")
645
+ parent_module_location: Path("a/b/c/")
646
+ else:
647
+ parent_module_name: "a.b.c"
648
+ parent_module_path: Path("a/b/c")
649
+ parent_module_location: Path("a/b/")
650
+
651
+ Example 2 of parent_module_*:
652
+
653
+ module_name: "a.b.c"
654
+ module_path: Path("a/b/c/__init__.py")
655
+ module_location: Path("a/b/c/")
656
+ if "a.b" is package ("a/b/__init__.py" exists), then
657
+ parent_module_name: "a.b"
658
+ parent_module_path: Path("a/b/__init__.py")
659
+ parent_module_location: Path("a/b/")
660
+ else:
661
+ parent_module_name: "a.b"
662
+ parent_module_path: Path("a/b/")
663
+ parent_module_location: Path("a/")
664
+ """
665
+ # Attempt to import the parent module, seems is our responsibility:
666
+ # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
667
+ parent_module_name, _, name = module_name.rpartition(".")
668
+ parent_module: ModuleType | None = None
669
+ if parent_module_name:
670
+ parent_module = sys.modules.get(parent_module_name)
671
+ if parent_module is None:
672
+ # Get parent_location based on location, get parent_path based on path.
673
+ if module_path.name == "__init__.py":
674
+ # If the current module is in a package,
675
+ # need to leave the package first and then enter the parent module.
676
+ parent_module_path = module_path.parent.parent
677
+ else:
678
+ parent_module_path = module_path.parent
679
+
680
+ if (parent_module_path / "__init__.py").is_file():
681
+ # If the parent module is a package, loading by __init__.py file.
682
+ parent_module_path = parent_module_path / "__init__.py"
683
+
684
+ parent_module = _import_module_using_spec(
685
+ parent_module_name,
686
+ parent_module_path,
687
+ parent_module_path.parent,
688
+ insert_modules=insert_modules,
689
+ )
690
+
691
+ # Checking with sys.meta_path first in case one of its hooks can import this module,
692
+ # such as our own assertion-rewrite hook.
693
+ for meta_importer in sys.meta_path:
694
+ spec = meta_importer.find_spec(
695
+ module_name, [str(module_location), str(module_path)]
696
+ )
697
+ if spec_matches_module_path(spec, module_path):
698
+ break
699
+ else:
700
+ loader = None
701
+ if module_path.is_dir():
702
+ # The `spec_from_file_location` matches a loader based on the file extension by default.
703
+ # For a namespace package, need to manually specify a loader.
704
+ loader = NamespaceLoader(name, module_path, PathFinder())
705
+
706
+ spec = importlib.util.spec_from_file_location(
707
+ module_name, str(module_path), loader=loader
708
+ )
709
+
710
+ if spec_matches_module_path(spec, module_path):
711
+ assert spec is not None
712
+ # Find spec and import this module.
713
+ mod = importlib.util.module_from_spec(spec)
714
+ sys.modules[module_name] = mod
715
+ spec.loader.exec_module(mod) # type: ignore[union-attr]
716
+
717
+ # Set this module as an attribute of the parent module (#12194).
718
+ if parent_module is not None:
719
+ setattr(parent_module, name, mod)
720
+
721
+ if insert_modules:
722
+ insert_missing_modules(sys.modules, module_name)
723
+ return mod
724
+
725
+ return None
726
+
727
+
728
+ def spec_matches_module_path(module_spec: ModuleSpec | None, module_path: Path) -> bool:
729
+ """Return true if the given ModuleSpec can be used to import the given module path."""
730
+ if module_spec is None:
731
+ return False
732
+
733
+ if module_spec.origin:
734
+ return Path(module_spec.origin) == module_path
735
+
736
+ # Compare the path with the `module_spec.submodule_Search_Locations` in case
737
+ # the module is part of a namespace package.
738
+ # https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.submodule_search_locations
739
+ if module_spec.submodule_search_locations: # can be None.
740
+ for path in module_spec.submodule_search_locations:
741
+ if Path(path) == module_path:
742
+ return True
743
+
744
+ return False
745
+
746
+
747
+ # Implement a special _is_same function on Windows which returns True if the two filenames
748
+ # compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
749
+ if sys.platform.startswith("win"):
750
+
751
+ def _is_same(f1: str, f2: str) -> bool:
752
+ return Path(f1) == Path(f2) or os.path.samefile(f1, f2)
753
+
754
+ else:
755
+
756
+ def _is_same(f1: str, f2: str) -> bool:
757
+ return os.path.samefile(f1, f2)
758
+
759
+
760
+ def module_name_from_path(path: Path, root: Path) -> str:
761
+ """
762
+ Return a dotted module name based on the given path, anchored on root.
763
+
764
+ For example: path="projects/src/tests/test_foo.py" and root="/projects", the
765
+ resulting module name will be "src.tests.test_foo".
766
+ """
767
+ path = path.with_suffix("")
768
+ try:
769
+ relative_path = path.relative_to(root)
770
+ except ValueError:
771
+ # If we can't get a relative path to root, use the full path, except
772
+ # for the first part ("d:\\" or "/" depending on the platform, for example).
773
+ path_parts = path.parts[1:]
774
+ else:
775
+ # Use the parts for the relative path to the root path.
776
+ path_parts = relative_path.parts
777
+
778
+ # Module name for packages do not contain the __init__ file, unless
779
+ # the `__init__.py` file is at the root.
780
+ if len(path_parts) >= 2 and path_parts[-1] == "__init__":
781
+ path_parts = path_parts[:-1]
782
+
783
+ # Module names cannot contain ".", normalize them to "_". This prevents
784
+ # a directory having a "." in the name (".env.310" for example) causing extra intermediate modules.
785
+ # Also, important to replace "." at the start of paths, as those are considered relative imports.
786
+ path_parts = tuple(x.replace(".", "_") for x in path_parts)
787
+
788
+ return ".".join(path_parts)
789
+
790
+
791
+ def insert_missing_modules(modules: dict[str, ModuleType], module_name: str) -> None:
792
+ """
793
+ Used by ``import_path`` to create intermediate modules when using mode=importlib.
794
+
795
+ When we want to import a module as "src.tests.test_foo" for example, we need
796
+ to create empty modules "src" and "src.tests" after inserting "src.tests.test_foo",
797
+ otherwise "src.tests.test_foo" is not importable by ``__import__``.
798
+ """
799
+ module_parts = module_name.split(".")
800
+ while module_name:
801
+ parent_module_name, _, child_name = module_name.rpartition(".")
802
+ if parent_module_name:
803
+ parent_module = modules.get(parent_module_name)
804
+ if parent_module is None:
805
+ try:
806
+ # If sys.meta_path is empty, calling import_module will issue
807
+ # a warning and raise ModuleNotFoundError. To avoid the
808
+ # warning, we check sys.meta_path explicitly and raise the error
809
+ # ourselves to fall back to creating a dummy module.
810
+ if not sys.meta_path:
811
+ raise ModuleNotFoundError
812
+ parent_module = importlib.import_module(parent_module_name)
813
+ except ModuleNotFoundError:
814
+ parent_module = ModuleType(
815
+ module_name,
816
+ doc="Empty module created by pytest's importmode=importlib.",
817
+ )
818
+ modules[parent_module_name] = parent_module
819
+
820
+ # Add child attribute to the parent that can reference the child
821
+ # modules.
822
+ if not hasattr(parent_module, child_name):
823
+ setattr(parent_module, child_name, modules[module_name])
824
+
825
+ module_parts.pop(-1)
826
+ module_name = ".".join(module_parts)
827
+
828
+
829
+ def resolve_package_path(path: Path) -> Path | None:
830
+ """Return the Python package path by looking for the last
831
+ directory upwards which still contains an __init__.py.
832
+
833
+ Returns None if it cannot be determined.
834
+ """
835
+ result = None
836
+ for parent in itertools.chain((path,), path.parents):
837
+ if parent.is_dir():
838
+ if not (parent / "__init__.py").is_file():
839
+ break
840
+ if not parent.name.isidentifier():
841
+ break
842
+ result = parent
843
+ return result
844
+
845
+
846
+ def resolve_pkg_root_and_module_name(
847
+ path: Path, *, consider_namespace_packages: bool = False
848
+ ) -> tuple[Path, str]:
849
+ """
850
+ Return the path to the directory of the root package that contains the
851
+ given Python file, and its module name:
852
+
853
+ src/
854
+ app/
855
+ __init__.py
856
+ core/
857
+ __init__.py
858
+ models.py
859
+
860
+ Passing the full path to `models.py` will yield Path("src") and "app.core.models".
861
+
862
+ If consider_namespace_packages is True, then we additionally check upwards in the hierarchy
863
+ for namespace packages:
864
+
865
+ https://packaging.python.org/en/latest/guides/packaging-namespace-packages
866
+
867
+ Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files).
868
+ """
869
+ pkg_root: Path | None = None
870
+ pkg_path = resolve_package_path(path)
871
+ if pkg_path is not None:
872
+ pkg_root = pkg_path.parent
873
+ if consider_namespace_packages:
874
+ start = pkg_root if pkg_root is not None else path.parent
875
+ for candidate in (start, *start.parents):
876
+ module_name = compute_module_name(candidate, path)
877
+ if module_name and is_importable(module_name, path):
878
+ # Point the pkg_root to the root of the namespace package.
879
+ pkg_root = candidate
880
+ break
881
+
882
+ if pkg_root is not None:
883
+ module_name = compute_module_name(pkg_root, path)
884
+ if module_name:
885
+ return pkg_root, module_name
886
+
887
+ raise CouldNotResolvePathError(f"Could not resolve for {path}")
888
+
889
+
890
+ def is_importable(module_name: str, module_path: Path) -> bool:
891
+ """
892
+ Return if the given module path could be imported normally by Python, akin to the user
893
+ entering the REPL and importing the corresponding module name directly, and corresponds
894
+ to the module_path specified.
895
+
896
+ :param module_name:
897
+ Full module name that we want to check if is importable.
898
+ For example, "app.models".
899
+
900
+ :param module_path:
901
+ Full path to the python module/package we want to check if is importable.
902
+ For example, "/projects/src/app/models.py".
903
+ """
904
+ try:
905
+ # Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through
906
+ # sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``).
907
+ # Using importlib.util.find_spec() is different, it gives the same results as trying to import
908
+ # the module normally in the REPL.
909
+ spec = importlib.util.find_spec(module_name)
910
+ except (ImportError, ValueError, ImportWarning):
911
+ return False
912
+ else:
913
+ return spec_matches_module_path(spec, module_path)
914
+
915
+
916
+ def compute_module_name(root: Path, module_path: Path) -> str | None:
917
+ """Compute a module name based on a path and a root anchor."""
918
+ try:
919
+ path_without_suffix = module_path.with_suffix("")
920
+ except ValueError:
921
+ # Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter).
922
+ return None
923
+
924
+ try:
925
+ relative = path_without_suffix.relative_to(root)
926
+ except ValueError: # pragma: no cover
927
+ return None
928
+ names = list(relative.parts)
929
+ if not names:
930
+ return None
931
+ if names[-1] == "__init__":
932
+ names.pop()
933
+ return ".".join(names)
934
+
935
+
936
+ class CouldNotResolvePathError(Exception):
937
+ """Custom exception raised by resolve_pkg_root_and_module_name."""
938
+
939
+
940
+ def scandir(
941
+ path: str | os.PathLike[str],
942
+ sort_key: Callable[[os.DirEntry[str]], object] = lambda entry: entry.name,
943
+ ) -> list[os.DirEntry[str]]:
944
+ """Scan a directory recursively, in breadth-first order.
945
+
946
+ The returned entries are sorted according to the given key.
947
+ The default is to sort by name.
948
+ """
949
+ entries = []
950
+ with os.scandir(path) as s:
951
+ # Skip entries with symlink loops and other brokenness, so the caller
952
+ # doesn't have to deal with it.
953
+ for entry in s:
954
+ try:
955
+ entry.is_file()
956
+ except OSError as err:
957
+ if _ignore_error(err):
958
+ continue
959
+ raise
960
+ entries.append(entry)
961
+ entries.sort(key=sort_key) # type: ignore[arg-type]
962
+ return entries
963
+
964
+
965
+ def visit(
966
+ path: str | os.PathLike[str], recurse: Callable[[os.DirEntry[str]], bool]
967
+ ) -> Iterator[os.DirEntry[str]]:
968
+ """Walk a directory recursively, in breadth-first order.
969
+
970
+ The `recurse` predicate determines whether a directory is recursed.
971
+
972
+ Entries at each directory level are sorted.
973
+ """
974
+ entries = scandir(path)
975
+ yield from entries
976
+ for entry in entries:
977
+ if entry.is_dir() and recurse(entry):
978
+ yield from visit(entry.path, recurse)
979
+
980
+
981
+ def absolutepath(path: str | os.PathLike[str]) -> Path:
982
+ """Convert a path to an absolute path using os.path.abspath.
983
+
984
+ Prefer this over Path.resolve() (see #6523).
985
+ Prefer this over Path.absolute() (not public, doesn't normalize).
986
+ """
987
+ return Path(os.path.abspath(path))
988
+
989
+
990
+ def commonpath(path1: Path, path2: Path) -> Path | None:
991
+ """Return the common part shared with the other path, or None if there is
992
+ no common part.
993
+
994
+ If one path is relative and one is absolute, returns None.
995
+ """
996
+ try:
997
+ return Path(os.path.commonpath((str(path1), str(path2))))
998
+ except ValueError:
999
+ return None
1000
+
1001
+
1002
+ def bestrelpath(directory: Path, dest: Path) -> str:
1003
+ """Return a string which is a relative path from directory to dest such
1004
+ that directory/bestrelpath == dest.
1005
+
1006
+ The paths must be either both absolute or both relative.
1007
+
1008
+ If no such path can be determined, returns dest.
1009
+ """
1010
+ assert isinstance(directory, Path)
1011
+ assert isinstance(dest, Path)
1012
+ if dest == directory:
1013
+ return os.curdir
1014
+ # Find the longest common directory.
1015
+ base = commonpath(directory, dest)
1016
+ # Can be the case on Windows for two absolute paths on different drives.
1017
+ # Can be the case for two relative paths without common prefix.
1018
+ # Can be the case for a relative path and an absolute path.
1019
+ if not base:
1020
+ return str(dest)
1021
+ reldirectory = directory.relative_to(base)
1022
+ reldest = dest.relative_to(base)
1023
+ return os.path.join(
1024
+ # Back from directory to base.
1025
+ *([os.pardir] * len(reldirectory.parts)),
1026
+ # Forward from base to dest.
1027
+ *reldest.parts,
1028
+ )
1029
+
1030
+
1031
+ def safe_exists(p: Path) -> bool:
1032
+ """Like Path.exists(), but account for input arguments that might be too long (#11394)."""
1033
+ try:
1034
+ return p.exists()
1035
+ except (ValueError, OSError):
1036
+ # ValueError: stat: path too long for Windows
1037
+ # OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
1038
+ return False
.venv/lib/python3.11/site-packages/_pytest/py.typed ADDED
File without changes
.venv/lib/python3.11/site-packages/_pytest/pytester.py ADDED
@@ -0,0 +1,1766 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """(Disabled by default) support for testing pytest and pytest plugins.
3
+
4
+ PYTEST_DONT_REWRITE
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import collections.abc
10
+ import contextlib
11
+ from fnmatch import fnmatch
12
+ import gc
13
+ import importlib
14
+ from io import StringIO
15
+ import locale
16
+ import os
17
+ from pathlib import Path
18
+ import platform
19
+ import re
20
+ import shutil
21
+ import subprocess
22
+ import sys
23
+ import traceback
24
+ from typing import Any
25
+ from typing import Callable
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 Literal
32
+ from typing import overload
33
+ from typing import Sequence
34
+ from typing import TextIO
35
+ from typing import TYPE_CHECKING
36
+ from weakref import WeakKeyDictionary
37
+
38
+ from iniconfig import IniConfig
39
+ from iniconfig import SectionWrapper
40
+
41
+ from _pytest import timing
42
+ from _pytest._code import Source
43
+ from _pytest.capture import _get_multicapture
44
+ from _pytest.compat import NOTSET
45
+ from _pytest.compat import NotSetType
46
+ from _pytest.config import _PluggyPlugin
47
+ from _pytest.config import Config
48
+ from _pytest.config import ExitCode
49
+ from _pytest.config import hookimpl
50
+ from _pytest.config import main
51
+ from _pytest.config import PytestPluginManager
52
+ from _pytest.config.argparsing import Parser
53
+ from _pytest.deprecated import check_ispytest
54
+ from _pytest.fixtures import fixture
55
+ from _pytest.fixtures import FixtureRequest
56
+ from _pytest.main import Session
57
+ from _pytest.monkeypatch import MonkeyPatch
58
+ from _pytest.nodes import Collector
59
+ from _pytest.nodes import Item
60
+ from _pytest.outcomes import fail
61
+ from _pytest.outcomes import importorskip
62
+ from _pytest.outcomes import skip
63
+ from _pytest.pathlib import bestrelpath
64
+ from _pytest.pathlib import make_numbered_dir
65
+ from _pytest.reports import CollectReport
66
+ from _pytest.reports import TestReport
67
+ from _pytest.tmpdir import TempPathFactory
68
+ from _pytest.warning_types import PytestWarning
69
+
70
+
71
+ if TYPE_CHECKING:
72
+ import pexpect
73
+
74
+
75
+ pytest_plugins = ["pytester_assertions"]
76
+
77
+
78
+ IGNORE_PAM = [ # filenames added when obtaining details about the current user
79
+ "/var/lib/sss/mc/passwd"
80
+ ]
81
+
82
+
83
+ def pytest_addoption(parser: Parser) -> None:
84
+ parser.addoption(
85
+ "--lsof",
86
+ action="store_true",
87
+ dest="lsof",
88
+ default=False,
89
+ help="Run FD checks if lsof is available",
90
+ )
91
+
92
+ parser.addoption(
93
+ "--runpytest",
94
+ default="inprocess",
95
+ dest="runpytest",
96
+ choices=("inprocess", "subprocess"),
97
+ help=(
98
+ "Run pytest sub runs in tests using an 'inprocess' "
99
+ "or 'subprocess' (python -m main) method"
100
+ ),
101
+ )
102
+
103
+ parser.addini(
104
+ "pytester_example_dir", help="Directory to take the pytester example files from"
105
+ )
106
+
107
+
108
+ def pytest_configure(config: Config) -> None:
109
+ if config.getvalue("lsof"):
110
+ checker = LsofFdLeakChecker()
111
+ if checker.matching_platform():
112
+ config.pluginmanager.register(checker)
113
+
114
+ config.addinivalue_line(
115
+ "markers",
116
+ "pytester_example_path(*path_segments): join the given path "
117
+ "segments to `pytester_example_dir` for this test.",
118
+ )
119
+
120
+
121
+ class LsofFdLeakChecker:
122
+ def get_open_files(self) -> list[tuple[str, str]]:
123
+ if sys.version_info >= (3, 11):
124
+ # New in Python 3.11, ignores utf-8 mode
125
+ encoding = locale.getencoding()
126
+ else:
127
+ encoding = locale.getpreferredencoding(False)
128
+ out = subprocess.run(
129
+ ("lsof", "-Ffn0", "-p", str(os.getpid())),
130
+ stdout=subprocess.PIPE,
131
+ stderr=subprocess.DEVNULL,
132
+ check=True,
133
+ text=True,
134
+ encoding=encoding,
135
+ ).stdout
136
+
137
+ def isopen(line: str) -> bool:
138
+ return line.startswith("f") and (
139
+ "deleted" not in line
140
+ and "mem" not in line
141
+ and "txt" not in line
142
+ and "cwd" not in line
143
+ )
144
+
145
+ open_files = []
146
+
147
+ for line in out.split("\n"):
148
+ if isopen(line):
149
+ fields = line.split("\0")
150
+ fd = fields[0][1:]
151
+ filename = fields[1][1:]
152
+ if filename in IGNORE_PAM:
153
+ continue
154
+ if filename.startswith("/"):
155
+ open_files.append((fd, filename))
156
+
157
+ return open_files
158
+
159
+ def matching_platform(self) -> bool:
160
+ try:
161
+ subprocess.run(("lsof", "-v"), check=True)
162
+ except (OSError, subprocess.CalledProcessError):
163
+ return False
164
+ else:
165
+ return True
166
+
167
+ @hookimpl(wrapper=True, tryfirst=True)
168
+ def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]:
169
+ lines1 = self.get_open_files()
170
+ try:
171
+ return (yield)
172
+ finally:
173
+ if hasattr(sys, "pypy_version_info"):
174
+ gc.collect()
175
+ lines2 = self.get_open_files()
176
+
177
+ new_fds = {t[0] for t in lines2} - {t[0] for t in lines1}
178
+ leaked_files = [t for t in lines2 if t[0] in new_fds]
179
+ if leaked_files:
180
+ error = [
181
+ f"***** {len(leaked_files)} FD leakage detected",
182
+ *(str(f) for f in leaked_files),
183
+ "*** Before:",
184
+ *(str(f) for f in lines1),
185
+ "*** After:",
186
+ *(str(f) for f in lines2),
187
+ f"***** {len(leaked_files)} FD leakage detected",
188
+ "*** function {}:{}: {} ".format(*item.location),
189
+ "See issue #2366",
190
+ ]
191
+ item.warn(PytestWarning("\n".join(error)))
192
+
193
+
194
+ # used at least by pytest-xdist plugin
195
+
196
+
197
+ @fixture
198
+ def _pytest(request: FixtureRequest) -> PytestArg:
199
+ """Return a helper which offers a gethookrecorder(hook) method which
200
+ returns a HookRecorder instance which helps to make assertions about called
201
+ hooks."""
202
+ return PytestArg(request)
203
+
204
+
205
+ class PytestArg:
206
+ def __init__(self, request: FixtureRequest) -> None:
207
+ self._request = request
208
+
209
+ def gethookrecorder(self, hook) -> HookRecorder:
210
+ hookrecorder = HookRecorder(hook._pm)
211
+ self._request.addfinalizer(hookrecorder.finish_recording)
212
+ return hookrecorder
213
+
214
+
215
+ def get_public_names(values: Iterable[str]) -> list[str]:
216
+ """Only return names from iterator values without a leading underscore."""
217
+ return [x for x in values if x[0] != "_"]
218
+
219
+
220
+ @final
221
+ class RecordedHookCall:
222
+ """A recorded call to a hook.
223
+
224
+ The arguments to the hook call are set as attributes.
225
+ For example:
226
+
227
+ .. code-block:: python
228
+
229
+ calls = hook_recorder.getcalls("pytest_runtest_setup")
230
+ # Suppose pytest_runtest_setup was called once with `item=an_item`.
231
+ assert calls[0].item is an_item
232
+ """
233
+
234
+ def __init__(self, name: str, kwargs) -> None:
235
+ self.__dict__.update(kwargs)
236
+ self._name = name
237
+
238
+ def __repr__(self) -> str:
239
+ d = self.__dict__.copy()
240
+ del d["_name"]
241
+ return f"<RecordedHookCall {self._name!r}(**{d!r})>"
242
+
243
+ if TYPE_CHECKING:
244
+ # The class has undetermined attributes, this tells mypy about it.
245
+ def __getattr__(self, key: str): ...
246
+
247
+
248
+ @final
249
+ class HookRecorder:
250
+ """Record all hooks called in a plugin manager.
251
+
252
+ Hook recorders are created by :class:`Pytester`.
253
+
254
+ This wraps all the hook calls in the plugin manager, recording each call
255
+ before propagating the normal calls.
256
+ """
257
+
258
+ def __init__(
259
+ self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False
260
+ ) -> None:
261
+ check_ispytest(_ispytest)
262
+
263
+ self._pluginmanager = pluginmanager
264
+ self.calls: list[RecordedHookCall] = []
265
+ self.ret: int | ExitCode | None = None
266
+
267
+ def before(hook_name: str, hook_impls, kwargs) -> None:
268
+ self.calls.append(RecordedHookCall(hook_name, kwargs))
269
+
270
+ def after(outcome, hook_name: str, hook_impls, kwargs) -> None:
271
+ pass
272
+
273
+ self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after)
274
+
275
+ def finish_recording(self) -> None:
276
+ self._undo_wrapping()
277
+
278
+ def getcalls(self, names: str | Iterable[str]) -> list[RecordedHookCall]:
279
+ """Get all recorded calls to hooks with the given names (or name)."""
280
+ if isinstance(names, str):
281
+ names = names.split()
282
+ return [call for call in self.calls if call._name in names]
283
+
284
+ def assert_contains(self, entries: Sequence[tuple[str, str]]) -> None:
285
+ __tracebackhide__ = True
286
+ i = 0
287
+ entries = list(entries)
288
+ # Since Python 3.13, f_locals is not a dict, but eval requires a dict.
289
+ backlocals = dict(sys._getframe(1).f_locals)
290
+ while entries:
291
+ name, check = entries.pop(0)
292
+ for ind, call in enumerate(self.calls[i:]):
293
+ if call._name == name:
294
+ print("NAMEMATCH", name, call)
295
+ if eval(check, backlocals, call.__dict__):
296
+ print("CHECKERMATCH", repr(check), "->", call)
297
+ else:
298
+ print("NOCHECKERMATCH", repr(check), "-", call)
299
+ continue
300
+ i += ind + 1
301
+ break
302
+ print("NONAMEMATCH", name, "with", call)
303
+ else:
304
+ fail(f"could not find {name!r} check {check!r}")
305
+
306
+ def popcall(self, name: str) -> RecordedHookCall:
307
+ __tracebackhide__ = True
308
+ for i, call in enumerate(self.calls):
309
+ if call._name == name:
310
+ del self.calls[i]
311
+ return call
312
+ lines = [f"could not find call {name!r}, in:"]
313
+ lines.extend([f" {x}" for x in self.calls])
314
+ fail("\n".join(lines))
315
+
316
+ def getcall(self, name: str) -> RecordedHookCall:
317
+ values = self.getcalls(name)
318
+ assert len(values) == 1, (name, values)
319
+ return values[0]
320
+
321
+ # functionality for test reports
322
+
323
+ @overload
324
+ def getreports(
325
+ self,
326
+ names: Literal["pytest_collectreport"],
327
+ ) -> Sequence[CollectReport]: ...
328
+
329
+ @overload
330
+ def getreports(
331
+ self,
332
+ names: Literal["pytest_runtest_logreport"],
333
+ ) -> Sequence[TestReport]: ...
334
+
335
+ @overload
336
+ def getreports(
337
+ self,
338
+ names: str | Iterable[str] = (
339
+ "pytest_collectreport",
340
+ "pytest_runtest_logreport",
341
+ ),
342
+ ) -> Sequence[CollectReport | TestReport]: ...
343
+
344
+ def getreports(
345
+ self,
346
+ names: str | Iterable[str] = (
347
+ "pytest_collectreport",
348
+ "pytest_runtest_logreport",
349
+ ),
350
+ ) -> Sequence[CollectReport | TestReport]:
351
+ return [x.report for x in self.getcalls(names)]
352
+
353
+ def matchreport(
354
+ self,
355
+ inamepart: str = "",
356
+ names: str | Iterable[str] = (
357
+ "pytest_runtest_logreport",
358
+ "pytest_collectreport",
359
+ ),
360
+ when: str | None = None,
361
+ ) -> CollectReport | TestReport:
362
+ """Return a testreport whose dotted import path matches."""
363
+ values = []
364
+ for rep in self.getreports(names=names):
365
+ if not when and rep.when != "call" and rep.passed:
366
+ # setup/teardown passing reports - let's ignore those
367
+ continue
368
+ if when and rep.when != when:
369
+ continue
370
+ if not inamepart or inamepart in rep.nodeid.split("::"):
371
+ values.append(rep)
372
+ if not values:
373
+ raise ValueError(
374
+ f"could not find test report matching {inamepart!r}: "
375
+ "no test reports at all!"
376
+ )
377
+ if len(values) > 1:
378
+ raise ValueError(
379
+ f"found 2 or more testreports matching {inamepart!r}: {values}"
380
+ )
381
+ return values[0]
382
+
383
+ @overload
384
+ def getfailures(
385
+ self,
386
+ names: Literal["pytest_collectreport"],
387
+ ) -> Sequence[CollectReport]: ...
388
+
389
+ @overload
390
+ def getfailures(
391
+ self,
392
+ names: Literal["pytest_runtest_logreport"],
393
+ ) -> Sequence[TestReport]: ...
394
+
395
+ @overload
396
+ def getfailures(
397
+ self,
398
+ names: str | Iterable[str] = (
399
+ "pytest_collectreport",
400
+ "pytest_runtest_logreport",
401
+ ),
402
+ ) -> Sequence[CollectReport | TestReport]: ...
403
+
404
+ def getfailures(
405
+ self,
406
+ names: str | Iterable[str] = (
407
+ "pytest_collectreport",
408
+ "pytest_runtest_logreport",
409
+ ),
410
+ ) -> Sequence[CollectReport | TestReport]:
411
+ return [rep for rep in self.getreports(names) if rep.failed]
412
+
413
+ def getfailedcollections(self) -> Sequence[CollectReport]:
414
+ return self.getfailures("pytest_collectreport")
415
+
416
+ def listoutcomes(
417
+ self,
418
+ ) -> tuple[
419
+ Sequence[TestReport],
420
+ Sequence[CollectReport | TestReport],
421
+ Sequence[CollectReport | TestReport],
422
+ ]:
423
+ passed = []
424
+ skipped = []
425
+ failed = []
426
+ for rep in self.getreports(
427
+ ("pytest_collectreport", "pytest_runtest_logreport")
428
+ ):
429
+ if rep.passed:
430
+ if rep.when == "call":
431
+ assert isinstance(rep, TestReport)
432
+ passed.append(rep)
433
+ elif rep.skipped:
434
+ skipped.append(rep)
435
+ else:
436
+ assert rep.failed, f"Unexpected outcome: {rep!r}"
437
+ failed.append(rep)
438
+ return passed, skipped, failed
439
+
440
+ def countoutcomes(self) -> list[int]:
441
+ return [len(x) for x in self.listoutcomes()]
442
+
443
+ def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None:
444
+ __tracebackhide__ = True
445
+ from _pytest.pytester_assertions import assertoutcome
446
+
447
+ outcomes = self.listoutcomes()
448
+ assertoutcome(
449
+ outcomes,
450
+ passed=passed,
451
+ skipped=skipped,
452
+ failed=failed,
453
+ )
454
+
455
+ def clear(self) -> None:
456
+ self.calls[:] = []
457
+
458
+
459
+ @fixture
460
+ def linecomp() -> LineComp:
461
+ """A :class: `LineComp` instance for checking that an input linearly
462
+ contains a sequence of strings."""
463
+ return LineComp()
464
+
465
+
466
+ @fixture(name="LineMatcher")
467
+ def LineMatcher_fixture(request: FixtureRequest) -> type[LineMatcher]:
468
+ """A reference to the :class: `LineMatcher`.
469
+
470
+ This is instantiable with a list of lines (without their trailing newlines).
471
+ This is useful for testing large texts, such as the output of commands.
472
+ """
473
+ return LineMatcher
474
+
475
+
476
+ @fixture
477
+ def pytester(
478
+ request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch
479
+ ) -> Pytester:
480
+ """
481
+ Facilities to write tests/configuration files, execute pytest in isolation, and match
482
+ against expected output, perfect for black-box testing of pytest plugins.
483
+
484
+ It attempts to isolate the test run from external factors as much as possible, modifying
485
+ the current working directory to ``path`` and environment variables during initialization.
486
+
487
+ It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path`
488
+ fixture but provides methods which aid in testing pytest itself.
489
+ """
490
+ return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True)
491
+
492
+
493
+ @fixture
494
+ def _sys_snapshot() -> Generator[None]:
495
+ snappaths = SysPathsSnapshot()
496
+ snapmods = SysModulesSnapshot()
497
+ yield
498
+ snapmods.restore()
499
+ snappaths.restore()
500
+
501
+
502
+ @fixture
503
+ def _config_for_test() -> Generator[Config]:
504
+ from _pytest.config import get_config
505
+
506
+ config = get_config()
507
+ yield config
508
+ config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles.
509
+
510
+
511
+ # Regex to match the session duration string in the summary: "74.34s".
512
+ rex_session_duration = re.compile(r"\d+\.\d\ds")
513
+ # Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped".
514
+ rex_outcome = re.compile(r"(\d+) (\w+)")
515
+
516
+
517
+ @final
518
+ class RunResult:
519
+ """The result of running a command from :class:`~pytest.Pytester`."""
520
+
521
+ def __init__(
522
+ self,
523
+ ret: int | ExitCode,
524
+ outlines: list[str],
525
+ errlines: list[str],
526
+ duration: float,
527
+ ) -> None:
528
+ try:
529
+ self.ret: int | ExitCode = ExitCode(ret)
530
+ """The return value."""
531
+ except ValueError:
532
+ self.ret = ret
533
+ self.outlines = outlines
534
+ """List of lines captured from stdout."""
535
+ self.errlines = errlines
536
+ """List of lines captured from stderr."""
537
+ self.stdout = LineMatcher(outlines)
538
+ """:class:`~pytest.LineMatcher` of stdout.
539
+
540
+ Use e.g. :func:`str(stdout) <pytest.LineMatcher.__str__()>` to reconstruct stdout, or the commonly used
541
+ :func:`stdout.fnmatch_lines() <pytest.LineMatcher.fnmatch_lines()>` method.
542
+ """
543
+ self.stderr = LineMatcher(errlines)
544
+ """:class:`~pytest.LineMatcher` of stderr."""
545
+ self.duration = duration
546
+ """Duration in seconds."""
547
+
548
+ def __repr__(self) -> str:
549
+ return (
550
+ "<RunResult ret=%s len(stdout.lines)=%d len(stderr.lines)=%d duration=%.2fs>"
551
+ % (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration)
552
+ )
553
+
554
+ def parseoutcomes(self) -> dict[str, int]:
555
+ """Return a dictionary of outcome noun -> count from parsing the terminal
556
+ output that the test process produced.
557
+
558
+ The returned nouns will always be in plural form::
559
+
560
+ ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
561
+
562
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
563
+ """
564
+ return self.parse_summary_nouns(self.outlines)
565
+
566
+ @classmethod
567
+ def parse_summary_nouns(cls, lines) -> dict[str, int]:
568
+ """Extract the nouns from a pytest terminal summary line.
569
+
570
+ It always returns the plural noun for consistency::
571
+
572
+ ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
573
+
574
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
575
+ """
576
+ for line in reversed(lines):
577
+ if rex_session_duration.search(line):
578
+ outcomes = rex_outcome.findall(line)
579
+ ret = {noun: int(count) for (count, noun) in outcomes}
580
+ break
581
+ else:
582
+ raise ValueError("Pytest terminal summary report not found")
583
+
584
+ to_plural = {
585
+ "warning": "warnings",
586
+ "error": "errors",
587
+ }
588
+ return {to_plural.get(k, k): v for k, v in ret.items()}
589
+
590
+ def assert_outcomes(
591
+ self,
592
+ passed: int = 0,
593
+ skipped: int = 0,
594
+ failed: int = 0,
595
+ errors: int = 0,
596
+ xpassed: int = 0,
597
+ xfailed: int = 0,
598
+ warnings: int | None = None,
599
+ deselected: int | None = None,
600
+ ) -> None:
601
+ """
602
+ Assert that the specified outcomes appear with the respective
603
+ numbers (0 means it didn't occur) in the text output from a test run.
604
+
605
+ ``warnings`` and ``deselected`` are only checked if not None.
606
+ """
607
+ __tracebackhide__ = True
608
+ from _pytest.pytester_assertions import assert_outcomes
609
+
610
+ outcomes = self.parseoutcomes()
611
+ assert_outcomes(
612
+ outcomes,
613
+ passed=passed,
614
+ skipped=skipped,
615
+ failed=failed,
616
+ errors=errors,
617
+ xpassed=xpassed,
618
+ xfailed=xfailed,
619
+ warnings=warnings,
620
+ deselected=deselected,
621
+ )
622
+
623
+
624
+ class SysModulesSnapshot:
625
+ def __init__(self, preserve: Callable[[str], bool] | None = None) -> None:
626
+ self.__preserve = preserve
627
+ self.__saved = dict(sys.modules)
628
+
629
+ def restore(self) -> None:
630
+ if self.__preserve:
631
+ self.__saved.update(
632
+ (k, m) for k, m in sys.modules.items() if self.__preserve(k)
633
+ )
634
+ sys.modules.clear()
635
+ sys.modules.update(self.__saved)
636
+
637
+
638
+ class SysPathsSnapshot:
639
+ def __init__(self) -> None:
640
+ self.__saved = list(sys.path), list(sys.meta_path)
641
+
642
+ def restore(self) -> None:
643
+ sys.path[:], sys.meta_path[:] = self.__saved
644
+
645
+
646
+ @final
647
+ class Pytester:
648
+ """
649
+ Facilities to write tests/configuration files, execute pytest in isolation, and match
650
+ against expected output, perfect for black-box testing of pytest plugins.
651
+
652
+ It attempts to isolate the test run from external factors as much as possible, modifying
653
+ the current working directory to :attr:`path` and environment variables during initialization.
654
+ """
655
+
656
+ __test__ = False
657
+
658
+ CLOSE_STDIN: Final = NOTSET
659
+
660
+ class TimeoutExpired(Exception):
661
+ pass
662
+
663
+ def __init__(
664
+ self,
665
+ request: FixtureRequest,
666
+ tmp_path_factory: TempPathFactory,
667
+ monkeypatch: MonkeyPatch,
668
+ *,
669
+ _ispytest: bool = False,
670
+ ) -> None:
671
+ check_ispytest(_ispytest)
672
+ self._request = request
673
+ self._mod_collections: WeakKeyDictionary[Collector, list[Item | Collector]] = (
674
+ WeakKeyDictionary()
675
+ )
676
+ if request.function:
677
+ name: str = request.function.__name__
678
+ else:
679
+ name = request.node.name
680
+ self._name = name
681
+ self._path: Path = tmp_path_factory.mktemp(name, numbered=True)
682
+ #: A list of plugins to use with :py:meth:`parseconfig` and
683
+ #: :py:meth:`runpytest`. Initially this is an empty list but plugins can
684
+ #: be added to the list. The type of items to add to the list depends on
685
+ #: the method using them so refer to them for details.
686
+ self.plugins: list[str | _PluggyPlugin] = []
687
+ self._sys_path_snapshot = SysPathsSnapshot()
688
+ self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
689
+ self._request.addfinalizer(self._finalize)
690
+ self._method = self._request.config.getoption("--runpytest")
691
+ self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True)
692
+
693
+ self._monkeypatch = mp = monkeypatch
694
+ self.chdir()
695
+ mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
696
+ # Ensure no unexpected caching via tox.
697
+ mp.delenv("TOX_ENV_DIR", raising=False)
698
+ # Discard outer pytest options.
699
+ mp.delenv("PYTEST_ADDOPTS", raising=False)
700
+ # Ensure no user config is used.
701
+ tmphome = str(self.path)
702
+ mp.setenv("HOME", tmphome)
703
+ mp.setenv("USERPROFILE", tmphome)
704
+ # Do not use colors for inner runs by default.
705
+ mp.setenv("PY_COLORS", "0")
706
+
707
+ @property
708
+ def path(self) -> Path:
709
+ """Temporary directory path used to create files/run tests from, etc."""
710
+ return self._path
711
+
712
+ def __repr__(self) -> str:
713
+ return f"<Pytester {self.path!r}>"
714
+
715
+ def _finalize(self) -> None:
716
+ """
717
+ Clean up global state artifacts.
718
+
719
+ Some methods modify the global interpreter state and this tries to
720
+ clean this up. It does not remove the temporary directory however so
721
+ it can be looked at after the test run has finished.
722
+ """
723
+ self._sys_modules_snapshot.restore()
724
+ self._sys_path_snapshot.restore()
725
+
726
+ def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
727
+ # Some zope modules used by twisted-related tests keep internal state
728
+ # and can't be deleted; we had some trouble in the past with
729
+ # `zope.interface` for example.
730
+ #
731
+ # Preserve readline due to https://bugs.python.org/issue41033.
732
+ # pexpect issues a SIGWINCH.
733
+ def preserve_module(name):
734
+ return name.startswith(("zope", "readline"))
735
+
736
+ return SysModulesSnapshot(preserve=preserve_module)
737
+
738
+ def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
739
+ """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
740
+ pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined]
741
+ self._request.addfinalizer(reprec.finish_recording)
742
+ return reprec
743
+
744
+ def chdir(self) -> None:
745
+ """Cd into the temporary directory.
746
+
747
+ This is done automatically upon instantiation.
748
+ """
749
+ self._monkeypatch.chdir(self.path)
750
+
751
+ def _makefile(
752
+ self,
753
+ ext: str,
754
+ lines: Sequence[Any | bytes],
755
+ files: dict[str, str],
756
+ encoding: str = "utf-8",
757
+ ) -> Path:
758
+ items = list(files.items())
759
+
760
+ if ext is None:
761
+ raise TypeError("ext must not be None")
762
+
763
+ if ext and not ext.startswith("."):
764
+ raise ValueError(
765
+ f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"
766
+ )
767
+
768
+ def to_text(s: Any | bytes) -> str:
769
+ return s.decode(encoding) if isinstance(s, bytes) else str(s)
770
+
771
+ if lines:
772
+ source = "\n".join(to_text(x) for x in lines)
773
+ basename = self._name
774
+ items.insert(0, (basename, source))
775
+
776
+ ret = None
777
+ for basename, value in items:
778
+ p = self.path.joinpath(basename).with_suffix(ext)
779
+ p.parent.mkdir(parents=True, exist_ok=True)
780
+ source_ = Source(value)
781
+ source = "\n".join(to_text(line) for line in source_.lines)
782
+ p.write_text(source.strip(), encoding=encoding)
783
+ if ret is None:
784
+ ret = p
785
+ assert ret is not None
786
+ return ret
787
+
788
+ def makefile(self, ext: str, *args: str, **kwargs: str) -> Path:
789
+ r"""Create new text file(s) in the test directory.
790
+
791
+ :param ext:
792
+ The extension the file(s) should use, including the dot, e.g. `.py`.
793
+ :param args:
794
+ All args are treated as strings and joined using newlines.
795
+ The result is written as contents to the file. The name of the
796
+ file is based on the test function requesting this fixture.
797
+ :param kwargs:
798
+ Each keyword is the name of a file, while the value of it will
799
+ be written as contents of the file.
800
+ :returns:
801
+ The first created file.
802
+
803
+ Examples:
804
+
805
+ .. code-block:: python
806
+
807
+ pytester.makefile(".txt", "line1", "line2")
808
+
809
+ pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n")
810
+
811
+ To create binary files, use :meth:`pathlib.Path.write_bytes` directly:
812
+
813
+ .. code-block:: python
814
+
815
+ filename = pytester.path.joinpath("foo.bin")
816
+ filename.write_bytes(b"...")
817
+ """
818
+ return self._makefile(ext, args, kwargs)
819
+
820
+ def makeconftest(self, source: str) -> Path:
821
+ """Write a conftest.py file.
822
+
823
+ :param source: The contents.
824
+ :returns: The conftest.py file.
825
+ """
826
+ return self.makepyfile(conftest=source)
827
+
828
+ def makeini(self, source: str) -> Path:
829
+ """Write a tox.ini file.
830
+
831
+ :param source: The contents.
832
+ :returns: The tox.ini file.
833
+ """
834
+ return self.makefile(".ini", tox=source)
835
+
836
+ def getinicfg(self, source: str) -> SectionWrapper:
837
+ """Return the pytest section from the tox.ini config file."""
838
+ p = self.makeini(source)
839
+ return IniConfig(str(p))["pytest"]
840
+
841
+ def makepyprojecttoml(self, source: str) -> Path:
842
+ """Write a pyproject.toml file.
843
+
844
+ :param source: The contents.
845
+ :returns: The pyproject.ini file.
846
+
847
+ .. versionadded:: 6.0
848
+ """
849
+ return self.makefile(".toml", pyproject=source)
850
+
851
+ def makepyfile(self, *args, **kwargs) -> Path:
852
+ r"""Shortcut for .makefile() with a .py extension.
853
+
854
+ Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
855
+ existing files.
856
+
857
+ Examples:
858
+
859
+ .. code-block:: python
860
+
861
+ def test_something(pytester):
862
+ # Initial file is created test_something.py.
863
+ pytester.makepyfile("foobar")
864
+ # To create multiple files, pass kwargs accordingly.
865
+ pytester.makepyfile(custom="foobar")
866
+ # At this point, both 'test_something.py' & 'custom.py' exist in the test directory.
867
+
868
+ """
869
+ return self._makefile(".py", args, kwargs)
870
+
871
+ def maketxtfile(self, *args, **kwargs) -> Path:
872
+ r"""Shortcut for .makefile() with a .txt extension.
873
+
874
+ Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
875
+ existing files.
876
+
877
+ Examples:
878
+
879
+ .. code-block:: python
880
+
881
+ def test_something(pytester):
882
+ # Initial file is created test_something.txt.
883
+ pytester.maketxtfile("foobar")
884
+ # To create multiple files, pass kwargs accordingly.
885
+ pytester.maketxtfile(custom="foobar")
886
+ # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory.
887
+
888
+ """
889
+ return self._makefile(".txt", args, kwargs)
890
+
891
+ def syspathinsert(self, path: str | os.PathLike[str] | None = None) -> None:
892
+ """Prepend a directory to sys.path, defaults to :attr:`path`.
893
+
894
+ This is undone automatically when this object dies at the end of each
895
+ test.
896
+
897
+ :param path:
898
+ The path.
899
+ """
900
+ if path is None:
901
+ path = self.path
902
+
903
+ self._monkeypatch.syspath_prepend(str(path))
904
+
905
+ def mkdir(self, name: str | os.PathLike[str]) -> Path:
906
+ """Create a new (sub)directory.
907
+
908
+ :param name:
909
+ The name of the directory, relative to the pytester path.
910
+ :returns:
911
+ The created directory.
912
+ :rtype: pathlib.Path
913
+ """
914
+ p = self.path / name
915
+ p.mkdir()
916
+ return p
917
+
918
+ def mkpydir(self, name: str | os.PathLike[str]) -> Path:
919
+ """Create a new python package.
920
+
921
+ This creates a (sub)directory with an empty ``__init__.py`` file so it
922
+ gets recognised as a Python package.
923
+ """
924
+ p = self.path / name
925
+ p.mkdir()
926
+ p.joinpath("__init__.py").touch()
927
+ return p
928
+
929
+ def copy_example(self, name: str | None = None) -> Path:
930
+ """Copy file from project's directory into the testdir.
931
+
932
+ :param name:
933
+ The name of the file to copy.
934
+ :return:
935
+ Path to the copied directory (inside ``self.path``).
936
+ :rtype: pathlib.Path
937
+ """
938
+ example_dir_ = self._request.config.getini("pytester_example_dir")
939
+ if example_dir_ is None:
940
+ raise ValueError("pytester_example_dir is unset, can't copy examples")
941
+ example_dir: Path = self._request.config.rootpath / example_dir_
942
+
943
+ for extra_element in self._request.node.iter_markers("pytester_example_path"):
944
+ assert extra_element.args
945
+ example_dir = example_dir.joinpath(*extra_element.args)
946
+
947
+ if name is None:
948
+ func_name = self._name
949
+ maybe_dir = example_dir / func_name
950
+ maybe_file = example_dir / (func_name + ".py")
951
+
952
+ if maybe_dir.is_dir():
953
+ example_path = maybe_dir
954
+ elif maybe_file.is_file():
955
+ example_path = maybe_file
956
+ else:
957
+ raise LookupError(
958
+ f"{func_name} can't be found as module or package in {example_dir}"
959
+ )
960
+ else:
961
+ example_path = example_dir.joinpath(name)
962
+
963
+ if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
964
+ shutil.copytree(example_path, self.path, symlinks=True, dirs_exist_ok=True)
965
+ return self.path
966
+ elif example_path.is_file():
967
+ result = self.path.joinpath(example_path.name)
968
+ shutil.copy(example_path, result)
969
+ return result
970
+ else:
971
+ raise LookupError(
972
+ f'example "{example_path}" is not found as a file or directory'
973
+ )
974
+
975
+ def getnode(self, config: Config, arg: str | os.PathLike[str]) -> Collector | Item:
976
+ """Get the collection node of a file.
977
+
978
+ :param config:
979
+ A pytest config.
980
+ See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it.
981
+ :param arg:
982
+ Path to the file.
983
+ :returns:
984
+ The node.
985
+ """
986
+ session = Session.from_config(config)
987
+ assert "::" not in str(arg)
988
+ p = Path(os.path.abspath(arg))
989
+ config.hook.pytest_sessionstart(session=session)
990
+ res = session.perform_collect([str(p)], genitems=False)[0]
991
+ config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
992
+ return res
993
+
994
+ def getpathnode(self, path: str | os.PathLike[str]) -> Collector | Item:
995
+ """Return the collection node of a file.
996
+
997
+ This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
998
+ create the (configured) pytest Config instance.
999
+
1000
+ :param path:
1001
+ Path to the file.
1002
+ :returns:
1003
+ The node.
1004
+ """
1005
+ path = Path(path)
1006
+ config = self.parseconfigure(path)
1007
+ session = Session.from_config(config)
1008
+ x = bestrelpath(session.path, path)
1009
+ config.hook.pytest_sessionstart(session=session)
1010
+ res = session.perform_collect([x], genitems=False)[0]
1011
+ config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
1012
+ return res
1013
+
1014
+ def genitems(self, colitems: Sequence[Item | Collector]) -> list[Item]:
1015
+ """Generate all test items from a collection node.
1016
+
1017
+ This recurses into the collection node and returns a list of all the
1018
+ test items contained within.
1019
+
1020
+ :param colitems:
1021
+ The collection nodes.
1022
+ :returns:
1023
+ The collected items.
1024
+ """
1025
+ session = colitems[0].session
1026
+ result: list[Item] = []
1027
+ for colitem in colitems:
1028
+ result.extend(session.genitems(colitem))
1029
+ return result
1030
+
1031
+ def runitem(self, source: str) -> Any:
1032
+ """Run the "test_func" Item.
1033
+
1034
+ The calling test instance (class containing the test method) must
1035
+ provide a ``.getrunner()`` method which should return a runner which
1036
+ can run the test protocol for a single item, e.g.
1037
+ ``_pytest.runner.runtestprotocol``.
1038
+ """
1039
+ # used from runner functional tests
1040
+ item = self.getitem(source)
1041
+ # the test class where we are called from wants to provide the runner
1042
+ testclassinstance = self._request.instance
1043
+ runner = testclassinstance.getrunner()
1044
+ return runner(item)
1045
+
1046
+ def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder:
1047
+ """Run a test module in process using ``pytest.main()``.
1048
+
1049
+ This run writes "source" into a temporary file and runs
1050
+ ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
1051
+ for the result.
1052
+
1053
+ :param source: The source code of the test module.
1054
+ :param cmdlineargs: Any extra command line arguments to use.
1055
+ """
1056
+ p = self.makepyfile(source)
1057
+ values = [*list(cmdlineargs), p]
1058
+ return self.inline_run(*values)
1059
+
1060
+ def inline_genitems(self, *args) -> tuple[list[Item], HookRecorder]:
1061
+ """Run ``pytest.main(['--collect-only'])`` in-process.
1062
+
1063
+ Runs the :py:func:`pytest.main` function to run all of pytest inside
1064
+ the test process itself like :py:meth:`inline_run`, but returns a
1065
+ tuple of the collected items and a :py:class:`HookRecorder` instance.
1066
+ """
1067
+ rec = self.inline_run("--collect-only", *args)
1068
+ items = [x.item for x in rec.getcalls("pytest_itemcollected")]
1069
+ return items, rec
1070
+
1071
+ def inline_run(
1072
+ self,
1073
+ *args: str | os.PathLike[str],
1074
+ plugins=(),
1075
+ no_reraise_ctrlc: bool = False,
1076
+ ) -> HookRecorder:
1077
+ """Run ``pytest.main()`` in-process, returning a HookRecorder.
1078
+
1079
+ Runs the :py:func:`pytest.main` function to run all of pytest inside
1080
+ the test process itself. This means it can return a
1081
+ :py:class:`HookRecorder` instance which gives more detailed results
1082
+ from that run than can be done by matching stdout/stderr from
1083
+ :py:meth:`runpytest`.
1084
+
1085
+ :param args:
1086
+ Command line arguments to pass to :py:func:`pytest.main`.
1087
+ :param plugins:
1088
+ Extra plugin instances the ``pytest.main()`` instance should use.
1089
+ :param no_reraise_ctrlc:
1090
+ Typically we reraise keyboard interrupts from the child run. If
1091
+ True, the KeyboardInterrupt exception is captured.
1092
+ """
1093
+ # (maybe a cpython bug?) the importlib cache sometimes isn't updated
1094
+ # properly between file creation and inline_run (especially if imports
1095
+ # are interspersed with file creation)
1096
+ importlib.invalidate_caches()
1097
+
1098
+ plugins = list(plugins)
1099
+ finalizers = []
1100
+ try:
1101
+ # Any sys.module or sys.path changes done while running pytest
1102
+ # inline should be reverted after the test run completes to avoid
1103
+ # clashing with later inline tests run within the same pytest test,
1104
+ # e.g. just because they use matching test module names.
1105
+ finalizers.append(self.__take_sys_modules_snapshot().restore)
1106
+ finalizers.append(SysPathsSnapshot().restore)
1107
+
1108
+ # Important note:
1109
+ # - our tests should not leave any other references/registrations
1110
+ # laying around other than possibly loaded test modules
1111
+ # referenced from sys.modules, as nothing will clean those up
1112
+ # automatically
1113
+
1114
+ rec = []
1115
+
1116
+ class Collect:
1117
+ def pytest_configure(x, config: Config) -> None:
1118
+ rec.append(self.make_hook_recorder(config.pluginmanager))
1119
+
1120
+ plugins.append(Collect())
1121
+ ret = main([str(x) for x in args], plugins=plugins)
1122
+ if len(rec) == 1:
1123
+ reprec = rec.pop()
1124
+ else:
1125
+
1126
+ class reprec: # type: ignore
1127
+ pass
1128
+
1129
+ reprec.ret = ret
1130
+
1131
+ # Typically we reraise keyboard interrupts from the child run
1132
+ # because it's our user requesting interruption of the testing.
1133
+ if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc:
1134
+ calls = reprec.getcalls("pytest_keyboard_interrupt")
1135
+ if calls and calls[-1].excinfo.type == KeyboardInterrupt:
1136
+ raise KeyboardInterrupt()
1137
+ return reprec
1138
+ finally:
1139
+ for finalizer in finalizers:
1140
+ finalizer()
1141
+
1142
+ def runpytest_inprocess(
1143
+ self, *args: str | os.PathLike[str], **kwargs: Any
1144
+ ) -> RunResult:
1145
+ """Return result of running pytest in-process, providing a similar
1146
+ interface to what self.runpytest() provides."""
1147
+ syspathinsert = kwargs.pop("syspathinsert", False)
1148
+
1149
+ if syspathinsert:
1150
+ self.syspathinsert()
1151
+ now = timing.time()
1152
+ capture = _get_multicapture("sys")
1153
+ capture.start_capturing()
1154
+ try:
1155
+ try:
1156
+ reprec = self.inline_run(*args, **kwargs)
1157
+ except SystemExit as e:
1158
+ ret = e.args[0]
1159
+ try:
1160
+ ret = ExitCode(e.args[0])
1161
+ except ValueError:
1162
+ pass
1163
+
1164
+ class reprec: # type: ignore
1165
+ ret = ret
1166
+
1167
+ except Exception:
1168
+ traceback.print_exc()
1169
+
1170
+ class reprec: # type: ignore
1171
+ ret = ExitCode(3)
1172
+
1173
+ finally:
1174
+ out, err = capture.readouterr()
1175
+ capture.stop_capturing()
1176
+ sys.stdout.write(out)
1177
+ sys.stderr.write(err)
1178
+
1179
+ assert reprec.ret is not None
1180
+ res = RunResult(
1181
+ reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now
1182
+ )
1183
+ res.reprec = reprec # type: ignore
1184
+ return res
1185
+
1186
+ def runpytest(self, *args: str | os.PathLike[str], **kwargs: Any) -> RunResult:
1187
+ """Run pytest inline or in a subprocess, depending on the command line
1188
+ option "--runpytest" and return a :py:class:`~pytest.RunResult`."""
1189
+ new_args = self._ensure_basetemp(args)
1190
+ if self._method == "inprocess":
1191
+ return self.runpytest_inprocess(*new_args, **kwargs)
1192
+ elif self._method == "subprocess":
1193
+ return self.runpytest_subprocess(*new_args, **kwargs)
1194
+ raise RuntimeError(f"Unrecognized runpytest option: {self._method}")
1195
+
1196
+ def _ensure_basetemp(
1197
+ self, args: Sequence[str | os.PathLike[str]]
1198
+ ) -> list[str | os.PathLike[str]]:
1199
+ new_args = list(args)
1200
+ for x in new_args:
1201
+ if str(x).startswith("--basetemp"):
1202
+ break
1203
+ else:
1204
+ new_args.append(
1205
+ "--basetemp={}".format(self.path.parent.joinpath("basetemp"))
1206
+ )
1207
+ return new_args
1208
+
1209
+ def parseconfig(self, *args: str | os.PathLike[str]) -> Config:
1210
+ """Return a new pytest :class:`pytest.Config` instance from given
1211
+ commandline args.
1212
+
1213
+ This invokes the pytest bootstrapping code in _pytest.config to create a
1214
+ new :py:class:`pytest.PytestPluginManager` and call the
1215
+ :hook:`pytest_cmdline_parse` hook to create a new :class:`pytest.Config`
1216
+ instance.
1217
+
1218
+ If :attr:`plugins` has been populated they should be plugin modules
1219
+ to be registered with the plugin manager.
1220
+ """
1221
+ import _pytest.config
1222
+
1223
+ new_args = self._ensure_basetemp(args)
1224
+ new_args = [str(x) for x in new_args]
1225
+
1226
+ config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type]
1227
+ # we don't know what the test will do with this half-setup config
1228
+ # object and thus we make sure it gets unconfigured properly in any
1229
+ # case (otherwise capturing could still be active, for example)
1230
+ self._request.addfinalizer(config._ensure_unconfigure)
1231
+ return config
1232
+
1233
+ def parseconfigure(self, *args: str | os.PathLike[str]) -> Config:
1234
+ """Return a new pytest configured Config instance.
1235
+
1236
+ Returns a new :py:class:`pytest.Config` instance like
1237
+ :py:meth:`parseconfig`, but also calls the :hook:`pytest_configure`
1238
+ hook.
1239
+ """
1240
+ config = self.parseconfig(*args)
1241
+ config._do_configure()
1242
+ return config
1243
+
1244
+ def getitem(
1245
+ self, source: str | os.PathLike[str], funcname: str = "test_func"
1246
+ ) -> Item:
1247
+ """Return the test item for a test function.
1248
+
1249
+ Writes the source to a python file and runs pytest's collection on
1250
+ the resulting module, returning the test item for the requested
1251
+ function name.
1252
+
1253
+ :param source:
1254
+ The module source.
1255
+ :param funcname:
1256
+ The name of the test function for which to return a test item.
1257
+ :returns:
1258
+ The test item.
1259
+ """
1260
+ items = self.getitems(source)
1261
+ for item in items:
1262
+ if item.name == funcname:
1263
+ return item
1264
+ assert 0, f"{funcname!r} item not found in module:\n{source}\nitems: {items}"
1265
+
1266
+ def getitems(self, source: str | os.PathLike[str]) -> list[Item]:
1267
+ """Return all test items collected from the module.
1268
+
1269
+ Writes the source to a Python file and runs pytest's collection on
1270
+ the resulting module, returning all test items contained within.
1271
+ """
1272
+ modcol = self.getmodulecol(source)
1273
+ return self.genitems([modcol])
1274
+
1275
+ def getmodulecol(
1276
+ self,
1277
+ source: str | os.PathLike[str],
1278
+ configargs=(),
1279
+ *,
1280
+ withinit: bool = False,
1281
+ ):
1282
+ """Return the module collection node for ``source``.
1283
+
1284
+ Writes ``source`` to a file using :py:meth:`makepyfile` and then
1285
+ runs the pytest collection on it, returning the collection node for the
1286
+ test module.
1287
+
1288
+ :param source:
1289
+ The source code of the module to collect.
1290
+
1291
+ :param configargs:
1292
+ Any extra arguments to pass to :py:meth:`parseconfigure`.
1293
+
1294
+ :param withinit:
1295
+ Whether to also write an ``__init__.py`` file to the same
1296
+ directory to ensure it is a package.
1297
+ """
1298
+ if isinstance(source, os.PathLike):
1299
+ path = self.path.joinpath(source)
1300
+ assert not withinit, "not supported for paths"
1301
+ else:
1302
+ kw = {self._name: str(source)}
1303
+ path = self.makepyfile(**kw)
1304
+ if withinit:
1305
+ self.makepyfile(__init__="#")
1306
+ self.config = config = self.parseconfigure(path, *configargs)
1307
+ return self.getnode(config, path)
1308
+
1309
+ def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None:
1310
+ """Return the collection node for name from the module collection.
1311
+
1312
+ Searches a module collection node for a collection node matching the
1313
+ given name.
1314
+
1315
+ :param modcol: A module collection node; see :py:meth:`getmodulecol`.
1316
+ :param name: The name of the node to return.
1317
+ """
1318
+ if modcol not in self._mod_collections:
1319
+ self._mod_collections[modcol] = list(modcol.collect())
1320
+ for colitem in self._mod_collections[modcol]:
1321
+ if colitem.name == name:
1322
+ return colitem
1323
+ return None
1324
+
1325
+ def popen(
1326
+ self,
1327
+ cmdargs: Sequence[str | os.PathLike[str]],
1328
+ stdout: int | TextIO = subprocess.PIPE,
1329
+ stderr: int | TextIO = subprocess.PIPE,
1330
+ stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN,
1331
+ **kw,
1332
+ ):
1333
+ """Invoke :py:class:`subprocess.Popen`.
1334
+
1335
+ Calls :py:class:`subprocess.Popen` making sure the current working
1336
+ directory is in ``PYTHONPATH``.
1337
+
1338
+ You probably want to use :py:meth:`run` instead.
1339
+ """
1340
+ env = os.environ.copy()
1341
+ env["PYTHONPATH"] = os.pathsep.join(
1342
+ filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
1343
+ )
1344
+ kw["env"] = env
1345
+
1346
+ if stdin is self.CLOSE_STDIN:
1347
+ kw["stdin"] = subprocess.PIPE
1348
+ elif isinstance(stdin, bytes):
1349
+ kw["stdin"] = subprocess.PIPE
1350
+ else:
1351
+ kw["stdin"] = stdin
1352
+
1353
+ popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw)
1354
+ if stdin is self.CLOSE_STDIN:
1355
+ assert popen.stdin is not None
1356
+ popen.stdin.close()
1357
+ elif isinstance(stdin, bytes):
1358
+ assert popen.stdin is not None
1359
+ popen.stdin.write(stdin)
1360
+
1361
+ return popen
1362
+
1363
+ def run(
1364
+ self,
1365
+ *cmdargs: str | os.PathLike[str],
1366
+ timeout: float | None = None,
1367
+ stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN,
1368
+ ) -> RunResult:
1369
+ """Run a command with arguments.
1370
+
1371
+ Run a process using :py:class:`subprocess.Popen` saving the stdout and
1372
+ stderr.
1373
+
1374
+ :param cmdargs:
1375
+ The sequence of arguments to pass to :py:class:`subprocess.Popen`,
1376
+ with path-like objects being converted to :py:class:`str`
1377
+ automatically.
1378
+ :param timeout:
1379
+ The period in seconds after which to timeout and raise
1380
+ :py:class:`Pytester.TimeoutExpired`.
1381
+ :param stdin:
1382
+ Optional standard input.
1383
+
1384
+ - If it is ``CLOSE_STDIN`` (Default), then this method calls
1385
+ :py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and
1386
+ the standard input is closed immediately after the new command is
1387
+ started.
1388
+
1389
+ - If it is of type :py:class:`bytes`, these bytes are sent to the
1390
+ standard input of the command.
1391
+
1392
+ - Otherwise, it is passed through to :py:class:`subprocess.Popen`.
1393
+ For further information in this case, consult the document of the
1394
+ ``stdin`` parameter in :py:class:`subprocess.Popen`.
1395
+ :type stdin: _pytest.compat.NotSetType | bytes | IO[Any] | int
1396
+ :returns:
1397
+ The result.
1398
+
1399
+ """
1400
+ __tracebackhide__ = True
1401
+
1402
+ cmdargs = tuple(os.fspath(arg) for arg in cmdargs)
1403
+ p1 = self.path.joinpath("stdout")
1404
+ p2 = self.path.joinpath("stderr")
1405
+ print("running:", *cmdargs)
1406
+ print(" in:", Path.cwd())
1407
+
1408
+ with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2:
1409
+ now = timing.time()
1410
+ popen = self.popen(
1411
+ cmdargs,
1412
+ stdin=stdin,
1413
+ stdout=f1,
1414
+ stderr=f2,
1415
+ close_fds=(sys.platform != "win32"),
1416
+ )
1417
+ if popen.stdin is not None:
1418
+ popen.stdin.close()
1419
+
1420
+ def handle_timeout() -> None:
1421
+ __tracebackhide__ = True
1422
+
1423
+ timeout_message = f"{timeout} second timeout expired running: {cmdargs}"
1424
+
1425
+ popen.kill()
1426
+ popen.wait()
1427
+ raise self.TimeoutExpired(timeout_message)
1428
+
1429
+ if timeout is None:
1430
+ ret = popen.wait()
1431
+ else:
1432
+ try:
1433
+ ret = popen.wait(timeout)
1434
+ except subprocess.TimeoutExpired:
1435
+ handle_timeout()
1436
+
1437
+ with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2:
1438
+ out = f1.read().splitlines()
1439
+ err = f2.read().splitlines()
1440
+
1441
+ self._dump_lines(out, sys.stdout)
1442
+ self._dump_lines(err, sys.stderr)
1443
+
1444
+ with contextlib.suppress(ValueError):
1445
+ ret = ExitCode(ret)
1446
+ return RunResult(ret, out, err, timing.time() - now)
1447
+
1448
+ def _dump_lines(self, lines, fp):
1449
+ try:
1450
+ for line in lines:
1451
+ print(line, file=fp)
1452
+ except UnicodeEncodeError:
1453
+ print(f"couldn't print to {fp} because of encoding")
1454
+
1455
+ def _getpytestargs(self) -> tuple[str, ...]:
1456
+ return sys.executable, "-mpytest"
1457
+
1458
+ def runpython(self, script: os.PathLike[str]) -> RunResult:
1459
+ """Run a python script using sys.executable as interpreter."""
1460
+ return self.run(sys.executable, script)
1461
+
1462
+ def runpython_c(self, command: str) -> RunResult:
1463
+ """Run ``python -c "command"``."""
1464
+ return self.run(sys.executable, "-c", command)
1465
+
1466
+ def runpytest_subprocess(
1467
+ self, *args: str | os.PathLike[str], timeout: float | None = None
1468
+ ) -> RunResult:
1469
+ """Run pytest as a subprocess with given arguments.
1470
+
1471
+ Any plugins added to the :py:attr:`plugins` list will be added using the
1472
+ ``-p`` command line option. Additionally ``--basetemp`` is used to put
1473
+ any temporary files and directories in a numbered directory prefixed
1474
+ with "runpytest-" to not conflict with the normal numbered pytest
1475
+ location for temporary files and directories.
1476
+
1477
+ :param args:
1478
+ The sequence of arguments to pass to the pytest subprocess.
1479
+ :param timeout:
1480
+ The period in seconds after which to timeout and raise
1481
+ :py:class:`Pytester.TimeoutExpired`.
1482
+ :returns:
1483
+ The result.
1484
+ """
1485
+ __tracebackhide__ = True
1486
+ p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
1487
+ args = (f"--basetemp={p}", *args)
1488
+ plugins = [x for x in self.plugins if isinstance(x, str)]
1489
+ if plugins:
1490
+ args = ("-p", plugins[0], *args)
1491
+ args = self._getpytestargs() + args
1492
+ return self.run(*args, timeout=timeout)
1493
+
1494
+ def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn:
1495
+ """Run pytest using pexpect.
1496
+
1497
+ This makes sure to use the right pytest and sets up the temporary
1498
+ directory locations.
1499
+
1500
+ The pexpect child is returned.
1501
+ """
1502
+ basetemp = self.path / "temp-pexpect"
1503
+ basetemp.mkdir(mode=0o700)
1504
+ invoke = " ".join(map(str, self._getpytestargs()))
1505
+ cmd = f"{invoke} --basetemp={basetemp} {string}"
1506
+ return self.spawn(cmd, expect_timeout=expect_timeout)
1507
+
1508
+ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn:
1509
+ """Run a command using pexpect.
1510
+
1511
+ The pexpect child is returned.
1512
+ """
1513
+ pexpect = importorskip("pexpect", "3.0")
1514
+ if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
1515
+ skip("pypy-64 bit not supported")
1516
+ if not hasattr(pexpect, "spawn"):
1517
+ skip("pexpect.spawn not available")
1518
+ logfile = self.path.joinpath("spawn.out").open("wb")
1519
+
1520
+ child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout)
1521
+ self._request.addfinalizer(logfile.close)
1522
+ return child
1523
+
1524
+
1525
+ class LineComp:
1526
+ def __init__(self) -> None:
1527
+ self.stringio = StringIO()
1528
+ """:class:`python:io.StringIO()` instance used for input."""
1529
+
1530
+ def assert_contains_lines(self, lines2: Sequence[str]) -> None:
1531
+ """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value.
1532
+
1533
+ Lines are matched using :func:`LineMatcher.fnmatch_lines <pytest.LineMatcher.fnmatch_lines>`.
1534
+ """
1535
+ __tracebackhide__ = True
1536
+ val = self.stringio.getvalue()
1537
+ self.stringio.truncate(0)
1538
+ self.stringio.seek(0)
1539
+ lines1 = val.split("\n")
1540
+ LineMatcher(lines1).fnmatch_lines(lines2)
1541
+
1542
+
1543
+ class LineMatcher:
1544
+ """Flexible matching of text.
1545
+
1546
+ This is a convenience class to test large texts like the output of
1547
+ commands.
1548
+
1549
+ The constructor takes a list of lines without their trailing newlines, i.e.
1550
+ ``text.splitlines()``.
1551
+ """
1552
+
1553
+ def __init__(self, lines: list[str]) -> None:
1554
+ self.lines = lines
1555
+ self._log_output: list[str] = []
1556
+
1557
+ def __str__(self) -> str:
1558
+ """Return the entire original text.
1559
+
1560
+ .. versionadded:: 6.2
1561
+ You can use :meth:`str` in older versions.
1562
+ """
1563
+ return "\n".join(self.lines)
1564
+
1565
+ def _getlines(self, lines2: str | Sequence[str] | Source) -> Sequence[str]:
1566
+ if isinstance(lines2, str):
1567
+ lines2 = Source(lines2)
1568
+ if isinstance(lines2, Source):
1569
+ lines2 = lines2.strip().lines
1570
+ return lines2
1571
+
1572
+ def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
1573
+ """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`)."""
1574
+ __tracebackhide__ = True
1575
+ self._match_lines_random(lines2, fnmatch)
1576
+
1577
+ def re_match_lines_random(self, lines2: Sequence[str]) -> None:
1578
+ """Check lines exist in the output in any order (using :func:`python:re.match`)."""
1579
+ __tracebackhide__ = True
1580
+ self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
1581
+
1582
+ def _match_lines_random(
1583
+ self, lines2: Sequence[str], match_func: Callable[[str, str], bool]
1584
+ ) -> None:
1585
+ __tracebackhide__ = True
1586
+ lines2 = self._getlines(lines2)
1587
+ for line in lines2:
1588
+ for x in self.lines:
1589
+ if line == x or match_func(x, line):
1590
+ self._log("matched: ", repr(line))
1591
+ break
1592
+ else:
1593
+ msg = f"line {line!r} not found in output"
1594
+ self._log(msg)
1595
+ self._fail(msg)
1596
+
1597
+ def get_lines_after(self, fnline: str) -> Sequence[str]:
1598
+ """Return all lines following the given line in the text.
1599
+
1600
+ The given line can contain glob wildcards.
1601
+ """
1602
+ for i, line in enumerate(self.lines):
1603
+ if fnline == line or fnmatch(line, fnline):
1604
+ return self.lines[i + 1 :]
1605
+ raise ValueError(f"line {fnline!r} not found in output")
1606
+
1607
+ def _log(self, *args) -> None:
1608
+ self._log_output.append(" ".join(str(x) for x in args))
1609
+
1610
+ @property
1611
+ def _log_text(self) -> str:
1612
+ return "\n".join(self._log_output)
1613
+
1614
+ def fnmatch_lines(
1615
+ self, lines2: Sequence[str], *, consecutive: bool = False
1616
+ ) -> None:
1617
+ """Check lines exist in the output (using :func:`python:fnmatch.fnmatch`).
1618
+
1619
+ The argument is a list of lines which have to match and can use glob
1620
+ wildcards. If they do not match a pytest.fail() is called. The
1621
+ matches and non-matches are also shown as part of the error message.
1622
+
1623
+ :param lines2: String patterns to match.
1624
+ :param consecutive: Match lines consecutively?
1625
+ """
1626
+ __tracebackhide__ = True
1627
+ self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
1628
+
1629
+ def re_match_lines(
1630
+ self, lines2: Sequence[str], *, consecutive: bool = False
1631
+ ) -> None:
1632
+ """Check lines exist in the output (using :func:`python:re.match`).
1633
+
1634
+ The argument is a list of lines which have to match using ``re.match``.
1635
+ If they do not match a pytest.fail() is called.
1636
+
1637
+ The matches and non-matches are also shown as part of the error message.
1638
+
1639
+ :param lines2: string patterns to match.
1640
+ :param consecutive: match lines consecutively?
1641
+ """
1642
+ __tracebackhide__ = True
1643
+ self._match_lines(
1644
+ lines2,
1645
+ lambda name, pat: bool(re.match(pat, name)),
1646
+ "re.match",
1647
+ consecutive=consecutive,
1648
+ )
1649
+
1650
+ def _match_lines(
1651
+ self,
1652
+ lines2: Sequence[str],
1653
+ match_func: Callable[[str, str], bool],
1654
+ match_nickname: str,
1655
+ *,
1656
+ consecutive: bool = False,
1657
+ ) -> None:
1658
+ """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
1659
+
1660
+ :param Sequence[str] lines2:
1661
+ List of string patterns to match. The actual format depends on
1662
+ ``match_func``.
1663
+ :param match_func:
1664
+ A callable ``match_func(line, pattern)`` where line is the
1665
+ captured line from stdout/stderr and pattern is the matching
1666
+ pattern.
1667
+ :param str match_nickname:
1668
+ The nickname for the match function that will be logged to stdout
1669
+ when a match occurs.
1670
+ :param consecutive:
1671
+ Match lines consecutively?
1672
+ """
1673
+ if not isinstance(lines2, collections.abc.Sequence):
1674
+ raise TypeError(f"invalid type for lines2: {type(lines2).__name__}")
1675
+ lines2 = self._getlines(lines2)
1676
+ lines1 = self.lines[:]
1677
+ extralines = []
1678
+ __tracebackhide__ = True
1679
+ wnick = len(match_nickname) + 1
1680
+ started = False
1681
+ for line in lines2:
1682
+ nomatchprinted = False
1683
+ while lines1:
1684
+ nextline = lines1.pop(0)
1685
+ if line == nextline:
1686
+ self._log("exact match:", repr(line))
1687
+ started = True
1688
+ break
1689
+ elif match_func(nextline, line):
1690
+ self._log(f"{match_nickname}:", repr(line))
1691
+ self._log(
1692
+ "{:>{width}}".format("with:", width=wnick), repr(nextline)
1693
+ )
1694
+ started = True
1695
+ break
1696
+ else:
1697
+ if consecutive and started:
1698
+ msg = f"no consecutive match: {line!r}"
1699
+ self._log(msg)
1700
+ self._log(
1701
+ "{:>{width}}".format("with:", width=wnick), repr(nextline)
1702
+ )
1703
+ self._fail(msg)
1704
+ if not nomatchprinted:
1705
+ self._log(
1706
+ "{:>{width}}".format("nomatch:", width=wnick), repr(line)
1707
+ )
1708
+ nomatchprinted = True
1709
+ self._log("{:>{width}}".format("and:", width=wnick), repr(nextline))
1710
+ extralines.append(nextline)
1711
+ else:
1712
+ msg = f"remains unmatched: {line!r}"
1713
+ self._log(msg)
1714
+ self._fail(msg)
1715
+ self._log_output = []
1716
+
1717
+ def no_fnmatch_line(self, pat: str) -> None:
1718
+ """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
1719
+
1720
+ :param str pat: The pattern to match lines.
1721
+ """
1722
+ __tracebackhide__ = True
1723
+ self._no_match_line(pat, fnmatch, "fnmatch")
1724
+
1725
+ def no_re_match_line(self, pat: str) -> None:
1726
+ """Ensure captured lines do not match the given pattern, using ``re.match``.
1727
+
1728
+ :param str pat: The regular expression to match lines.
1729
+ """
1730
+ __tracebackhide__ = True
1731
+ self._no_match_line(
1732
+ pat, lambda name, pat: bool(re.match(pat, name)), "re.match"
1733
+ )
1734
+
1735
+ def _no_match_line(
1736
+ self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
1737
+ ) -> None:
1738
+ """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``.
1739
+
1740
+ :param str pat: The pattern to match lines.
1741
+ """
1742
+ __tracebackhide__ = True
1743
+ nomatch_printed = False
1744
+ wnick = len(match_nickname) + 1
1745
+ for line in self.lines:
1746
+ if match_func(line, pat):
1747
+ msg = f"{match_nickname}: {pat!r}"
1748
+ self._log(msg)
1749
+ self._log("{:>{width}}".format("with:", width=wnick), repr(line))
1750
+ self._fail(msg)
1751
+ else:
1752
+ if not nomatch_printed:
1753
+ self._log("{:>{width}}".format("nomatch:", width=wnick), repr(pat))
1754
+ nomatch_printed = True
1755
+ self._log("{:>{width}}".format("and:", width=wnick), repr(line))
1756
+ self._log_output = []
1757
+
1758
+ def _fail(self, msg: str) -> None:
1759
+ __tracebackhide__ = True
1760
+ log_text = self._log_text
1761
+ self._log_output = []
1762
+ fail(log_text)
1763
+
1764
+ def str(self) -> str:
1765
+ """Return the entire original text."""
1766
+ return str(self)