Nanthasit commited on
Commit
0b1bd98
·
verified ·
1 Parent(s): 6918355

Remove accidentally uploaded coder-readme.md (part 2)

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 +6 -0
  2. .venv/bin/python3 +3 -0
  3. .venv/bin/python3.14 +3 -0
  4. .venv/bin/ruff +3 -0
  5. .venv/bin/sakthai +10 -0
  6. .venv/bin/stubgen +10 -0
  7. .venv/bin/stubtest +10 -0
  8. .venv/bin/websockets +10 -0
  9. .venv/lib/python3.14/site-packages/08ae81f72d5a2b5fa9e0__mypyc.cpython-314-x86_64-linux-gnu.so +3 -0
  10. .venv/lib/python3.14/site-packages/81d243bd2c585b0f4821__mypyc.cpython-314-x86_64-linux-gnu.so +3 -0
  11. .venv/lib/python3.14/site-packages/__editable__.sakthai_agent-2.0.0.pth +3 -0
  12. .venv/lib/python3.14/site-packages/_cffi_backend.cpython-314-x86_64-linux-gnu.so +3 -0
  13. .venv/lib/python3.14/site-packages/_hypothesis_ftz_detector.py +159 -0
  14. .venv/lib/python3.14/site-packages/_hypothesis_globals.py +28 -0
  15. .venv/lib/python3.14/site-packages/_hypothesis_pytestplugin.py +492 -0
  16. .venv/lib/python3.14/site-packages/_pytest/__init__.py +13 -0
  17. .venv/lib/python3.14/site-packages/_pytest/_argcomplete.py +117 -0
  18. .venv/lib/python3.14/site-packages/_pytest/_code/__init__.py +26 -0
  19. .venv/lib/python3.14/site-packages/_pytest/_code/code.py +1632 -0
  20. .venv/lib/python3.14/site-packages/_pytest/_code/source.py +228 -0
  21. .venv/lib/python3.14/site-packages/_pytest/_io/__init__.py +10 -0
  22. .venv/lib/python3.14/site-packages/_pytest/_io/pprint.py +673 -0
  23. .venv/lib/python3.14/site-packages/_pytest/_io/saferepr.py +155 -0
  24. .venv/lib/python3.14/site-packages/_pytest/_io/terminalwriter.py +258 -0
  25. .venv/lib/python3.14/site-packages/_pytest/_io/wcwidth.py +57 -0
  26. .venv/lib/python3.14/site-packages/_pytest/_py/__init__.py +0 -0
  27. .venv/lib/python3.14/site-packages/_pytest/_py/error.py +119 -0
  28. .venv/lib/python3.14/site-packages/_pytest/_py/path.py +1475 -0
  29. .venv/lib/python3.14/site-packages/_pytest/_version.py +24 -0
  30. .venv/lib/python3.14/site-packages/_pytest/assertion/__init__.py +236 -0
  31. .venv/lib/python3.14/site-packages/_pytest/assertion/_compare_any.py +131 -0
  32. .venv/lib/python3.14/site-packages/_pytest/assertion/_compare_mapping.py +48 -0
  33. .venv/lib/python3.14/site-packages/_pytest/assertion/_compare_sequence.py +90 -0
  34. .venv/lib/python3.14/site-packages/_pytest/assertion/_compare_set.py +101 -0
  35. .venv/lib/python3.14/site-packages/_pytest/assertion/_guards.py +60 -0
  36. .venv/lib/python3.14/site-packages/_pytest/assertion/_typing.py +12 -0
  37. .venv/lib/python3.14/site-packages/_pytest/assertion/compare_text.py +104 -0
  38. .venv/lib/python3.14/site-packages/_pytest/assertion/highlight.py +11 -0
  39. .venv/lib/python3.14/site-packages/_pytest/assertion/rewrite.py +1193 -0
  40. .venv/lib/python3.14/site-packages/_pytest/assertion/truncate.py +128 -0
  41. .venv/lib/python3.14/site-packages/_pytest/assertion/util.py +215 -0
  42. .venv/lib/python3.14/site-packages/_pytest/cacheprovider.py +640 -0
  43. .venv/lib/python3.14/site-packages/_pytest/capture.py +1151 -0
  44. .venv/lib/python3.14/site-packages/_pytest/compat.py +329 -0
  45. .venv/lib/python3.14/site-packages/_pytest/config/__init__.py +2246 -0
  46. .venv/lib/python3.14/site-packages/_pytest/config/argparsing.py +503 -0
  47. .venv/lib/python3.14/site-packages/_pytest/config/exceptions.py +15 -0
  48. .venv/lib/python3.14/site-packages/_pytest/config/findpaths.py +350 -0
  49. .venv/lib/python3.14/site-packages/_pytest/debugging.py +404 -0
  50. .venv/lib/python3.14/site-packages/_pytest/deprecated.py +139 -0
.gitattributes CHANGED
@@ -35,3 +35,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  gguf/sakthai-coder-q4_k_m.gguf filter=lfs diff=lfs merge=lfs -text
37
  .venv/bin/python filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  gguf/sakthai-coder-q4_k_m.gguf filter=lfs diff=lfs merge=lfs -text
37
  .venv/bin/python filter=lfs diff=lfs merge=lfs -text
38
+ .venv/bin/python3 filter=lfs diff=lfs merge=lfs -text
39
+ .venv/bin/python3.14 filter=lfs diff=lfs merge=lfs -text
40
+ .venv/bin/ruff filter=lfs diff=lfs merge=lfs -text
41
+ .venv/lib/python3.14/site-packages/08ae81f72d5a2b5fa9e0__mypyc.cpython-314-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
42
+ .venv/lib/python3.14/site-packages/81d243bd2c585b0f4821__mypyc.cpython-314-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
43
+ .venv/lib/python3.14/site-packages/_cffi_backend.cpython-314-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
.venv/bin/python3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3033d7dd6bc07b45575eeb52dd886c05391f2464e4c51c9cac5e63226d41c9db
3
+ size 32306368
.venv/bin/python3.14 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3033d7dd6bc07b45575eeb52dd886c05391f2464e4c51c9cac5e63226d41c9db
3
+ size 32306368
.venv/bin/ruff ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64aae5e444938e33121c3b940dff9b3d8ef8fc2a88c477e7f3a4fae2584a8fe8
3
+ size 27832160
.venv/bin/sakthai ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/opt/data/Sak-Family-Agent/.venv/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from sakthai.cli import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
.venv/bin/stubgen ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/opt/data/sakthai-skills-repo/.venv/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from mypy.stubgen import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
.venv/bin/stubtest ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/opt/data/sakthai-skills-repo/.venv/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from mypy.stubtest import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
.venv/bin/websockets ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/opt/data/sakthai-skills-repo/.venv/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from websockets.cli import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
.venv/lib/python3.14/site-packages/08ae81f72d5a2b5fa9e0__mypyc.cpython-314-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:984c73beda1391a9b2cb7d423d8f7c1736bcafb8625fced8089eecb90892426b
3
+ size 36745144
.venv/lib/python3.14/site-packages/81d243bd2c585b0f4821__mypyc.cpython-314-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:128abe84767022efa02b02c588bd2ec1955c5aaa22f6fdc655ae690f9592dec1
3
+ size 433360
.venv/lib/python3.14/site-packages/__editable__.sakthai_agent-2.0.0.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:12aca8d236f52b1e052f8b809515f049c3e162c5f343a250bd259d3c8e2c4e49
3
+ size 44
.venv/lib/python3.14/site-packages/_cffi_backend.cpython-314-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:202efbf6eaa01ba2dcf833edf7c6bf9449c599c38348169a583864b796029d87
3
+ size 344664
.venv/lib/python3.14/site-packages/_hypothesis_ftz_detector.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is part of Hypothesis, which may be found at
2
+ # https://github.com/HypothesisWorks/hypothesis/
3
+ #
4
+ # Copyright the Hypothesis Authors.
5
+ # Individual contributors are listed in AUTHORS.rst and the git log.
6
+ #
7
+ # This Source Code Form is subject to the terms of the Mozilla Public License,
8
+ # v. 2.0. If a copy of the MPL was not distributed with this file, You can
9
+ # obtain one at https://mozilla.org/MPL/2.0/.
10
+
11
+ """
12
+ This is a toolkit for determining which module set the "flush to zero" flag.
13
+
14
+ For details, see the docstring and comments in `identify_ftz_culprit()`. This module
15
+ is defined outside the main Hypothesis namespace so that we can avoid triggering
16
+ import of Hypothesis itself from each subprocess which must import the worker function.
17
+ """
18
+
19
+ import importlib
20
+ import sys
21
+ from collections.abc import Callable
22
+ from typing import TYPE_CHECKING, TypeAlias
23
+
24
+ if TYPE_CHECKING:
25
+ from multiprocessing import Queue
26
+
27
+ FTZCulprits: TypeAlias = tuple[bool | None, set[str]]
28
+
29
+
30
+ KNOWN_EVER_CULPRITS = (
31
+ # https://moyix.blogspot.com/2022/09/someones-been-messing-with-my-subnormals.html
32
+ # fmt: off
33
+ "archive-pdf-tools", "bgfx-python", "bicleaner-ai-glove", "BTrees", "cadbiom",
34
+ "ctranslate2", "dyNET", "dyNET38", "gevent", "glove-python-binary", "higra",
35
+ "hybridq", "ikomia", "ioh", "jij-cimod", "lavavu", "lavavu-osmesa", "MulticoreTSNE",
36
+ "neural-compressor", "nwhy", "openjij", "openturns", "perfmetrics", "pHashPy",
37
+ "pyace-lite", "pyapr", "pycompadre", "pycompadre-serial", "PyKEP", "pykep",
38
+ "pylimer-tools", "pyqubo", "pyscf", "PyTAT", "python-prtree", "qiskit-aer",
39
+ "qiskit-aer-gpu", "RelStorage", "sail-ml", "segmentation", "sente", "sinr",
40
+ "snapml", "superman", "symengine", "systran-align", "texture-tool", "tsne-mp",
41
+ "xcsf",
42
+ # fmt: on
43
+ )
44
+
45
+
46
+ def flush_to_zero() -> bool:
47
+ # If this subnormal number compares equal to zero we have a problem
48
+ return 2.0**-1073 == 0
49
+
50
+
51
+ def run_in_process(fn: Callable[..., FTZCulprits], *args: object) -> FTZCulprits:
52
+ import multiprocessing as mp
53
+
54
+ mp.set_start_method("spawn", force=True)
55
+ q: Queue[FTZCulprits] = mp.Queue()
56
+ p = mp.Process(target=target, args=(q, fn, *args))
57
+ p.start()
58
+ retval = q.get()
59
+ p.join()
60
+ return retval
61
+
62
+
63
+ def target(
64
+ q: "Queue[FTZCulprits]", fn: Callable[..., FTZCulprits], *args: object
65
+ ) -> None:
66
+ q.put(fn(*args))
67
+
68
+
69
+ def always_imported_modules() -> FTZCulprits:
70
+ return flush_to_zero(), set(sys.modules)
71
+
72
+
73
+ def modules_imported_by(mod: str) -> FTZCulprits:
74
+ """Return the set of modules imported transitively by mod."""
75
+ before = set(sys.modules)
76
+ try:
77
+ importlib.import_module(mod)
78
+ except Exception:
79
+ return None, set()
80
+ imports = set(sys.modules) - before
81
+ return flush_to_zero(), imports
82
+
83
+
84
+ # We don't want to redo all the expensive process-spawning checks when we've already
85
+ # done them, so we cache known-good packages and a known-FTZ result if we have one.
86
+ KNOWN_FTZ: str | None = None
87
+ CHECKED_CACHE: set[str] = set()
88
+
89
+
90
+ def identify_ftz_culprits() -> str:
91
+ """Find the modules in sys.modules which cause "mod" to be imported."""
92
+ # If we've run this function before, return the same result.
93
+ global KNOWN_FTZ
94
+ if KNOWN_FTZ:
95
+ return KNOWN_FTZ
96
+ # Start by determining our baseline: the FTZ and sys.modules state in a fresh
97
+ # process which has only imported this module and nothing else.
98
+ always_enables_ftz, always_imports = run_in_process(always_imported_modules)
99
+ if always_enables_ftz:
100
+ raise RuntimeError("Python is always in FTZ mode, even without imports!")
101
+ CHECKED_CACHE.update(always_imports)
102
+
103
+ # Next, we'll search through sys.modules looking for a package (or packages) such
104
+ # that importing them in a new process sets the FTZ state. As a heuristic, we'll
105
+ # start with packages known to have ever enabled FTZ, then top-level packages as
106
+ # a way to eliminate large fractions of the search space relatively quickly.
107
+ def key(name: str) -> tuple[bool, int, str]:
108
+ """Prefer known-FTZ modules, then top-level packages, then alphabetical."""
109
+ return (name not in KNOWN_EVER_CULPRITS, name.count("."), name)
110
+
111
+ # We'll track the set of modules to be checked, and those which do trigger FTZ.
112
+ candidates = set(sys.modules) - CHECKED_CACHE
113
+ triggering_modules = {}
114
+ while candidates:
115
+ mod = min(candidates, key=key)
116
+ candidates.discard(mod)
117
+ enables_ftz, imports = run_in_process(modules_imported_by, mod)
118
+ imports -= CHECKED_CACHE
119
+ if enables_ftz:
120
+ triggering_modules[mod] = imports
121
+ candidates &= imports
122
+ else:
123
+ candidates -= imports
124
+ CHECKED_CACHE.update(imports)
125
+
126
+ # We only want to report the 'top level' packages which enable FTZ - for example,
127
+ # if the enabling code is in `a.b`, and `a` in turn imports `a.b`, we prefer to
128
+ # report `a`. On the other hand, if `a` does _not_ import `a.b`, as is the case
129
+ # for `hypothesis.extra.*` modules, then `a` will not be in `triggering_modules`
130
+ # and we'll report `a.b` here instead.
131
+ prefixes = tuple(n + "." for n in triggering_modules)
132
+ result = {k for k in triggering_modules if not k.startswith(prefixes)}
133
+
134
+ # Suppose that `bar` enables FTZ, and `foo` imports `bar`. At this point we're
135
+ # tracking both, but only want to report the latter.
136
+ for a in sorted(result):
137
+ for b in sorted(result):
138
+ if a in triggering_modules[b] and b not in triggering_modules[a]:
139
+ result.discard(b)
140
+
141
+ # There may be a cyclic dependency which that didn't handle, or simply two
142
+ # separate modules which both enable FTZ. We already gave up comprehensive
143
+ # reporting for speed above (`candidates &= imports`), so we'll also buy
144
+ # simpler reporting by arbitrarily selecting the alphabetically first package.
145
+ KNOWN_FTZ = min(result) # Cache the result - it's likely this will trigger again!
146
+ return KNOWN_FTZ
147
+
148
+
149
+ if __name__ == "__main__":
150
+ # This would be really really annoying to write automated tests for, so I've
151
+ # done some manual exploratory testing: `pip install grequests gevent==21.12.0`,
152
+ # and call print() as desired to observe behavior.
153
+ import grequests # noqa
154
+
155
+ # To test without skipping to a known answer, uncomment the following line and
156
+ # change the last element of key from `name` to `-len(name)` so that we check
157
+ # grequests before gevent.
158
+ # KNOWN_EVER_CULPRITS = [c for c in KNOWN_EVER_CULPRITS if c != "gevent"]
159
+ print(identify_ftz_culprits()) # noqa: T201
.venv/lib/python3.14/site-packages/_hypothesis_globals.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is part of Hypothesis, which may be found at
2
+ # https://github.com/HypothesisWorks/hypothesis/
3
+ #
4
+ # Copyright the Hypothesis Authors.
5
+ # Individual contributors are listed in AUTHORS.rst and the git log.
6
+ #
7
+ # This Source Code Form is subject to the terms of the Mozilla Public License,
8
+ # v. 2.0. If a copy of the MPL was not distributed with this file, You can
9
+ # obtain one at https://mozilla.org/MPL/2.0/.
10
+
11
+ """
12
+ Module for globals shared between plugin(s) and the main hypothesis module, without
13
+ depending on either. This file should have no imports outside of stdlib.
14
+ """
15
+
16
+ import os
17
+
18
+ in_initialization = 1
19
+ """If >0, indicates that hypothesis is still initializing (importing or loading
20
+ the test environment). `import hypothesis` will cause this number to be decremented,
21
+ and the pytest plugin increments at load time, then decrements it just before each test
22
+ session starts. However, this leads to a hole in coverage if another pytest plugin
23
+ imports hypothesis before our plugin is loaded. HYPOTHESIS_EXTEND_INITIALIZATION may
24
+ be set to pre-increment the value on behalf of _hypothesis_pytestplugin, plugging the
25
+ hole."""
26
+
27
+ if os.environ.get("HYPOTHESIS_EXTEND_INITIALIZATION"):
28
+ in_initialization += 1
.venv/lib/python3.14/site-packages/_hypothesis_pytestplugin.py ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is part of Hypothesis, which may be found at
2
+ # https://github.com/HypothesisWorks/hypothesis/
3
+ #
4
+ # Copyright the Hypothesis Authors.
5
+ # Individual contributors are listed in AUTHORS.rst and the git log.
6
+ #
7
+ # This Source Code Form is subject to the terms of the Mozilla Public License,
8
+ # v. 2.0. If a copy of the MPL was not distributed with this file, You can
9
+ # obtain one at https://mozilla.org/MPL/2.0/.
10
+
11
+ """
12
+ The pytest plugin for Hypothesis.
13
+
14
+ We move this from the old location at `hypothesis.extra.pytestplugin` so that it
15
+ can be loaded by Pytest without importing Hypothesis. In turn, this means that
16
+ Hypothesis will not load our own third-party plugins (with associated side-effects)
17
+ unless and until the user explicitly runs `import hypothesis`.
18
+
19
+ See https://github.com/HypothesisWorks/hypothesis/issues/3140 for details.
20
+ """
21
+
22
+ import base64
23
+ import json
24
+ import os
25
+ import sys
26
+ import warnings
27
+ from fnmatch import fnmatch
28
+ from inspect import signature
29
+
30
+ import _hypothesis_globals
31
+ import pytest
32
+
33
+ try:
34
+ from _pytest.junitxml import xml_key
35
+ except ImportError:
36
+ xml_key = "_xml" # type: ignore
37
+
38
+ LOAD_PROFILE_OPTION = "--hypothesis-profile"
39
+ VERBOSITY_OPTION = "--hypothesis-verbosity"
40
+ PRINT_STATISTICS_OPTION = "--hypothesis-show-statistics"
41
+ SEED_OPTION = "--hypothesis-seed"
42
+ EXPLAIN_OPTION = "--hypothesis-explain"
43
+
44
+ _VERBOSITY_NAMES = ["quiet", "normal", "verbose", "debug"]
45
+ _ALL_OPTIONS = [
46
+ LOAD_PROFILE_OPTION,
47
+ VERBOSITY_OPTION,
48
+ PRINT_STATISTICS_OPTION,
49
+ SEED_OPTION,
50
+ EXPLAIN_OPTION,
51
+ ]
52
+ STATS_KEY = "_hypothesis_stats"
53
+ FAILING_EXAMPLES_KEY = "_hypothesis_failing_examples"
54
+
55
+
56
+ class StoringReporter:
57
+ def __init__(self, config):
58
+ assert "hypothesis" in sys.modules
59
+ from hypothesis.reporting import default
60
+
61
+ self.report = default
62
+ self.config = config
63
+ self.results = []
64
+
65
+ def __call__(self, msg):
66
+ if self.config.getoption("capture", "fd") == "no":
67
+ self.report(msg)
68
+ if not isinstance(msg, str):
69
+ msg = repr(msg)
70
+ self.results.append(msg)
71
+
72
+
73
+ # Avoiding distutils.version.LooseVersion due to
74
+ # https://github.com/HypothesisWorks/hypothesis/issues/2490
75
+ if tuple(map(int, pytest.__version__.split(".")[:2])) < (4, 6): # pragma: no cover
76
+ import warnings
77
+
78
+ PYTEST_TOO_OLD_MESSAGE = """
79
+ You are using pytest version %s. Hypothesis tests work with any test
80
+ runner, but our pytest plugin requires pytest 4.6 or newer.
81
+ Note that the pytest developers no longer support your version either!
82
+ Disabling the Hypothesis pytest plugin...
83
+ """
84
+ warnings.warn(PYTEST_TOO_OLD_MESSAGE % (pytest.__version__,), stacklevel=1)
85
+
86
+ else:
87
+ # Restart side-effect detection as early as possible, to maximize coverage. We
88
+ # need balanced increment/decrement in configure/sessionstart to support nested
89
+ # pytest (e.g. runpytest_inprocess), so this early increment in effect replaces
90
+ # the first one in pytest_configure.
91
+ if not os.environ.get("HYPOTHESIS_EXTEND_INITIALIZATION"):
92
+ _hypothesis_globals.in_initialization += 1
93
+ if "hypothesis" in sys.modules:
94
+ # Some other plugin has imported hypothesis, so we'll check if there
95
+ # have been undetected side-effects and warn if so.
96
+ from hypothesis.configuration import notice_initialization_restarted
97
+
98
+ notice_initialization_restarted()
99
+
100
+ def pytest_addoption(parser):
101
+ group = parser.getgroup("hypothesis", "Hypothesis")
102
+ group.addoption(
103
+ LOAD_PROFILE_OPTION,
104
+ action="store",
105
+ help="Load in a registered hypothesis.settings profile",
106
+ )
107
+ group.addoption(
108
+ VERBOSITY_OPTION,
109
+ action="store",
110
+ choices=_VERBOSITY_NAMES,
111
+ help="Override profile with verbosity setting specified",
112
+ )
113
+ group.addoption(
114
+ PRINT_STATISTICS_OPTION,
115
+ action="store_true",
116
+ help="Configure when statistics are printed",
117
+ default=False,
118
+ )
119
+ group.addoption(
120
+ SEED_OPTION,
121
+ action="store",
122
+ help="Set a seed to use for all Hypothesis tests",
123
+ )
124
+ group.addoption(
125
+ EXPLAIN_OPTION,
126
+ action="store_true",
127
+ help="Enable the `explain` phase for failing Hypothesis tests",
128
+ default=False,
129
+ )
130
+
131
+ def _any_hypothesis_option(config):
132
+ return bool(any(config.getoption(opt) for opt in _ALL_OPTIONS))
133
+
134
+ def pytest_report_header(config):
135
+ if not (
136
+ config.option.verbose >= 1
137
+ or "hypothesis" in sys.modules
138
+ or _any_hypothesis_option(config)
139
+ ):
140
+ return None
141
+
142
+ from hypothesis import Verbosity, settings
143
+
144
+ if config.option.verbose < 1 and settings.default.verbosity < Verbosity.verbose:
145
+ return None
146
+ settings_str = settings.default.show_changed()
147
+ if settings_str != "":
148
+ settings_str = f" -> {settings_str}"
149
+ return (
150
+ f"hypothesis profile {settings.get_current_profile_name()!r}{settings_str}"
151
+ )
152
+
153
+ def pytest_configure(config):
154
+ config.addinivalue_line("markers", "hypothesis: Tests which use hypothesis.")
155
+ if not _any_hypothesis_option(config):
156
+ return
157
+ from hypothesis import Phase, Verbosity, core, settings
158
+
159
+ profile = config.getoption(LOAD_PROFILE_OPTION)
160
+ if profile:
161
+ settings.load_profile(profile)
162
+ verbosity_name = config.getoption(VERBOSITY_OPTION)
163
+ if verbosity_name and verbosity_name != settings.default.verbosity.name:
164
+ verbosity_value = Verbosity[verbosity_name]
165
+ name = (
166
+ f"{settings.get_current_profile_name()}-with-{verbosity_name}-verbosity"
167
+ )
168
+ # register_profile creates a new profile, exactly like the current one,
169
+ # with the extra values given (in this case 'verbosity')
170
+ settings.register_profile(name, verbosity=verbosity_value)
171
+ settings.load_profile(name)
172
+ if (
173
+ config.getoption(EXPLAIN_OPTION)
174
+ and Phase.explain not in settings.default.phases
175
+ ):
176
+ name = f"{settings.get_current_profile_name()}-with-explain-phase"
177
+ phases = (*settings.default.phases, Phase.explain)
178
+ settings.register_profile(name, phases=phases)
179
+ settings.load_profile(name)
180
+
181
+ seed = config.getoption(SEED_OPTION)
182
+ if seed is not None:
183
+ try:
184
+ seed = int(seed)
185
+ except ValueError:
186
+ pass
187
+ core.global_force_seed = seed
188
+
189
+ @pytest.hookimpl(hookwrapper=True)
190
+ def pytest_runtest_call(item):
191
+ __tracebackhide__ = True
192
+ if not (hasattr(item, "obj") and "hypothesis" in sys.modules):
193
+ yield
194
+ return
195
+
196
+ from hypothesis import core, is_hypothesis_test
197
+
198
+ # See https://github.com/pytest-dev/pytest/issues/9159
199
+ core.pytest_shows_exceptiongroups = (
200
+ getattr(pytest, "version_tuple", ())[:2] >= (7, 2)
201
+ or item.config.getoption("tbstyle", "auto") == "native"
202
+ )
203
+ core.running_under_pytest = True
204
+
205
+ if not is_hypothesis_test(item.obj):
206
+ # If @given was not applied, check whether other hypothesis
207
+ # decorators were applied, and raise an error if they were.
208
+ # We add this frame of indirection to enable __tracebackhide__.
209
+ def raise_hypothesis_usage_error(msg):
210
+ raise InvalidArgument(msg)
211
+
212
+ if getattr(item.obj, "is_hypothesis_strategy_function", False):
213
+ from hypothesis.errors import InvalidArgument
214
+
215
+ raise_hypothesis_usage_error(
216
+ f"{item.nodeid} is a function that returns a Hypothesis strategy, "
217
+ "but pytest has collected it as a test function. This is useless "
218
+ "as the function body will never be executed. To define a test "
219
+ "function, use @given instead of @composite."
220
+ )
221
+ message = "Using `@%s` on a test without `@given` is completely pointless."
222
+ for name, attribute in [
223
+ ("example", "hypothesis_explicit_examples"),
224
+ ("seed", "_hypothesis_internal_use_seed"),
225
+ ("settings", "_hypothesis_internal_settings_applied"),
226
+ ("reproduce_example", "_hypothesis_internal_use_reproduce_failure"),
227
+ ]:
228
+ if hasattr(item.obj, attribute):
229
+ from hypothesis.errors import InvalidArgument
230
+
231
+ raise_hypothesis_usage_error(message % (name,))
232
+ yield
233
+ return
234
+
235
+ from hypothesis import HealthCheck, settings as Settings
236
+ from hypothesis.internal.escalation import current_pytest_item
237
+ from hypothesis.internal.healthcheck import fail_health_check
238
+ from hypothesis.reporting import with_reporter
239
+ from hypothesis.statistics import collector, describe_statistics
240
+
241
+ # Retrieve the settings for this test from the test object, which
242
+ # is normally a Hypothesis wrapped_test wrapper. If this doesn't
243
+ # work, the test object is probably something weird
244
+ # (e.g a stateful test wrapper), so we skip the function-scoped
245
+ # fixture check.
246
+ settings = getattr(
247
+ item.obj, "_hypothesis_internal_use_settings", Settings.default
248
+ )
249
+
250
+ # Check for suspicious use of function-scoped fixtures, but only
251
+ # if the corresponding health check is not suppressed.
252
+ fixture_params = False
253
+ if not set(settings.suppress_health_check).issuperset(
254
+ {HealthCheck.function_scoped_fixture, HealthCheck.differing_executors}
255
+ ):
256
+ # Warn about function-scoped fixtures, excluding autouse fixtures because
257
+ # the advice is probably not actionable and the status quo seems OK...
258
+ # See https://github.com/HypothesisWorks/hypothesis/issues/377 for detail.
259
+ argnames = None
260
+ for fx_defs in item._request._fixturemanager.getfixtureinfo(
261
+ node=item, func=item.function, cls=None
262
+ ).name2fixturedefs.values():
263
+ if argnames is None:
264
+ argnames = frozenset(signature(item.function).parameters)
265
+ for fx in fx_defs:
266
+ fixture_params |= bool(fx.params)
267
+ if fx.argname not in argnames:
268
+ continue
269
+ active_fx = item._request._get_active_fixturedef(fx.argname)
270
+ if active_fx.scope == "function":
271
+ fail_health_check(
272
+ settings,
273
+ f"{item.nodeid!r} uses a function-scoped fixture {fx.argname!r}."
274
+ "\n\n"
275
+ "Function-scoped fixtures are not reset between inputs "
276
+ "generated by `@given(...)`, which is often surprising and "
277
+ "can cause subtle test bugs."
278
+ "\n\n"
279
+ "If you were expecting the fixture to run separately "
280
+ "for each generated input, then unfortunately you "
281
+ "will need to find a different way to achieve your "
282
+ "goal (for example, replacing the fixture with a similar "
283
+ "context manager inside of the test)."
284
+ "\n\n"
285
+ "If you are confident that your test will work correctly "
286
+ "even though the fixture is not reset between generated "
287
+ "inputs, you can suppress this health check with "
288
+ "@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]). "
289
+ "See "
290
+ "https://hypothesis.readthedocs.io/en/latest/reference/api.html#hypothesis.HealthCheck "
291
+ "for details.",
292
+ HealthCheck.function_scoped_fixture,
293
+ )
294
+
295
+ if fixture_params or (item.get_closest_marker("parametrize") is not None):
296
+ # Disable the differing_executors health check due to false alarms:
297
+ # see https://github.com/HypothesisWorks/hypothesis/issues/3733
298
+ fn = getattr(item.obj, "__func__", item.obj)
299
+ fn._hypothesis_internal_use_settings = Settings(
300
+ parent=settings,
301
+ suppress_health_check={HealthCheck.differing_executors}
302
+ | set(settings.suppress_health_check),
303
+ )
304
+
305
+ # Give every parametrized test invocation a unique database key
306
+ key = item.nodeid.encode()
307
+ item.obj.hypothesis.inner_test._hypothesis_internal_add_digest = key
308
+
309
+ store = StoringReporter(item.config)
310
+
311
+ def note_statistics(stats):
312
+ stats["nodeid"] = item.nodeid
313
+ item.hypothesis_statistics = describe_statistics(stats)
314
+
315
+ with (
316
+ collector.with_value(note_statistics),
317
+ with_reporter(store),
318
+ current_pytest_item.with_value(item),
319
+ ):
320
+ yield
321
+
322
+ if store.results:
323
+ item.hypothesis_report_information = "\n".join(store.results)
324
+
325
+ def _stash_get(config, key, default):
326
+ if hasattr(config, "stash"):
327
+ # pytest 7
328
+ return config.stash.get(key, default)
329
+ elif hasattr(config, "_store"):
330
+ # pytest 5.4
331
+ return config._store.get(key, default)
332
+ else:
333
+ return getattr(config, key, default)
334
+
335
+ @pytest.hookimpl(hookwrapper=True)
336
+ def pytest_runtest_makereport(item, call):
337
+ report = (yield).get_result()
338
+ if hasattr(item, "hypothesis_report_information"):
339
+ report.sections.append(("Hypothesis", item.hypothesis_report_information))
340
+ if report.when != "teardown":
341
+ return
342
+
343
+ terminalreporter = item.config.pluginmanager.getplugin("terminalreporter")
344
+
345
+ if hasattr(item, "hypothesis_statistics"):
346
+ stats = item.hypothesis_statistics
347
+ stats_base64 = base64.b64encode(stats.encode()).decode()
348
+
349
+ name = "hypothesis-statistics-" + item.nodeid
350
+
351
+ # Include hypothesis information to the junit XML report.
352
+ #
353
+ # Note that when `pytest-xdist` is enabled, `xml_key` is not present in the
354
+ # stash, so we don't add anything to the junit XML report in that scenario.
355
+ # https://github.com/pytest-dev/pytest/issues/7767#issuecomment-1082436256
356
+ xml = _stash_get(item.config, xml_key, None)
357
+ if xml:
358
+ xml.add_global_property(name, stats_base64)
359
+
360
+ # If there's a terminal report, include our summary stats for each test
361
+ if terminalreporter is not None:
362
+ report.__dict__[STATS_KEY] = stats
363
+
364
+ # If there's an HTML report, include our summary stats for each test
365
+ pytest_html = item.config.pluginmanager.getplugin("html")
366
+ if pytest_html is not None: # pragma: no cover
367
+ report.extra = [
368
+ *getattr(report, "extra", []),
369
+ pytest_html.extras.text(stats, name="Hypothesis stats"),
370
+ ]
371
+
372
+ # This doesn't intrinsically have anything to do with the terminalreporter;
373
+ # we're just cargo-culting a way to get strings back to a single function
374
+ # even if the test were distributed with pytest-xdist.
375
+ failing_examples = getattr(item, FAILING_EXAMPLES_KEY, None)
376
+ if failing_examples and terminalreporter is not None:
377
+ try:
378
+ from hypothesis.extra._patching import FAIL_MSG, get_patch_for
379
+ except ImportError:
380
+ return
381
+ # We'll save this as a triple of [filename, hunk_before, hunk_after].
382
+ triple = get_patch_for(item.obj, [(x, FAIL_MSG) for x in failing_examples])
383
+ if triple is not None:
384
+ report.__dict__[FAILING_EXAMPLES_KEY] = json.dumps(triple)
385
+
386
+ def pytest_terminal_summary(terminalreporter):
387
+ failing_examples = []
388
+ print_stats = terminalreporter.config.getoption(PRINT_STATISTICS_OPTION)
389
+ if print_stats:
390
+ terminalreporter.section("Hypothesis Statistics")
391
+ for reports in terminalreporter.stats.values():
392
+ for report in reports:
393
+ stats = report.__dict__.get(STATS_KEY)
394
+ if stats and print_stats:
395
+ terminalreporter.write_line(stats + "\n\n")
396
+ examples = report.__dict__.get(FAILING_EXAMPLES_KEY)
397
+ if examples:
398
+ failing_examples.append(json.loads(examples))
399
+
400
+ from hypothesis.internal.observability import _WROTE_TO
401
+
402
+ if _WROTE_TO:
403
+ terminalreporter.section("Hypothesis")
404
+ for fname in sorted(_WROTE_TO):
405
+ terminalreporter.write_line(f"observations written to {fname}")
406
+
407
+ if failing_examples:
408
+ # This must have been imported already to write the failing examples
409
+ from hypothesis.extra._patching import gc_patches, make_patch, save_patch
410
+
411
+ patch = make_patch(failing_examples)
412
+ try:
413
+ gc_patches()
414
+ fname = save_patch(patch)
415
+ except Exception:
416
+ # fail gracefully if we hit any filesystem or permissions problems
417
+ return
418
+ if not _WROTE_TO:
419
+ terminalreporter.section("Hypothesis")
420
+ terminalreporter.write_line(
421
+ f"`git apply {fname}` to add failing examples to your code."
422
+ )
423
+
424
+ def pytest_collection_modifyitems(items):
425
+ if "hypothesis" not in sys.modules:
426
+ return
427
+
428
+ from hypothesis import is_hypothesis_test
429
+
430
+ has_hypothesis_tests = False
431
+ for item in items:
432
+ if isinstance(item, pytest.Function) and is_hypothesis_test(item.obj):
433
+ item.add_marker("hypothesis")
434
+ has_hypothesis_tests = True
435
+
436
+ if has_hypothesis_tests:
437
+ # Collect local constants now, during test collection, so that this
438
+ # time is not attributed to whatever test happens to run first.
439
+ # (see https://github.com/HypothesisWorks/hypothesis/issues/4627)
440
+ # We might miss weird wrapped uses (if there are no other PBTs!),
441
+ # in order to avoid the latency from inefficient 3rd party plugins.
442
+ from hypothesis.internal.conjecture.providers import _get_local_constants
443
+
444
+ _get_local_constants()
445
+
446
+ def pytest_sessionstart(session):
447
+ # Note: may be called multiple times, so we can go negative
448
+ _hypothesis_globals.in_initialization -= 1
449
+
450
+ # Monkeypatch some internals to prevent applying @pytest.fixture() to a
451
+ # function which has already been decorated with @hypothesis.given().
452
+ # (the reverse case is already an explicit error in Hypothesis)
453
+ # We do this here so that it catches people on old Pytest versions too.
454
+ from _pytest import fixtures
455
+
456
+ def _ban_given_call(self, function):
457
+ if "hypothesis" in sys.modules:
458
+ from hypothesis import is_hypothesis_test
459
+
460
+ if is_hypothesis_test(function):
461
+ raise RuntimeError(
462
+ f"Can't apply @pytest.fixture() to {function.__name__} because "
463
+ "it is already decorated with @hypothesis.given()"
464
+ )
465
+ return _orig_call(self, function)
466
+
467
+ _orig_call = fixtures.FixtureFunctionMarker.__call__
468
+ fixtures.FixtureFunctionMarker.__call__ = _ban_given_call # type: ignore
469
+
470
+ if int(pytest.__version__.split(".")[0]) >= 7: # pragma: no branch
471
+ # Hook has had this signature since Pytest 7.0, so skip on older versions
472
+
473
+ def pytest_ignore_collect(collection_path, config):
474
+ # Detect, warn about, and mititgate certain misconfigurations;
475
+ # this is mostly educational but can also speed up collection.
476
+ if (
477
+ (name := collection_path.name) == ".hypothesis"
478
+ and collection_path.is_dir()
479
+ and not any(fnmatch(name, p) for p in config.getini("norecursedirs"))
480
+ ):
481
+ warnings.warn(
482
+ "Skipping collection of '.hypothesis' directory - this usually "
483
+ "means you've explicitly set the `norecursedirs` pytest config "
484
+ "option, replacing rather than extending the default ignores.",
485
+ stacklevel=1,
486
+ )
487
+ return True
488
+ return None # let other hooks decide
489
+
490
+
491
+ def load():
492
+ """Required for `pluggy` to load a plugin from setuptools entrypoints."""
.venv/lib/python3.14/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.14/site-packages/_pytest/_argcomplete.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Allow bash-completion for argparse with argcomplete if installed.
2
+
3
+ Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
4
+ to find the magic string, so _ARGCOMPLETE env. var is never set, and
5
+ this does not need special code).
6
+
7
+ Function try_argcomplete(parser) should be called directly before
8
+ the call to ArgumentParser.parse_args().
9
+
10
+ The filescompleter is what you normally would use on the positional
11
+ arguments specification, in order to get "dirname/" after "dirn<TAB>"
12
+ instead of the default "dirname ":
13
+
14
+ optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
15
+
16
+ Other, application specific, completers should go in the file
17
+ doing the add_argument calls as they need to be specified as .completer
18
+ attributes as well. (If argcomplete is not installed, the function the
19
+ attribute points to will not be used).
20
+
21
+ SPEEDUP
22
+ =======
23
+
24
+ The generic argcomplete script for bash-completion
25
+ (/etc/bash_completion.d/python-argcomplete.sh)
26
+ uses a python program to determine startup script generated by pip.
27
+ You can speed up completion somewhat by changing this script to include
28
+ # PYTHON_ARGCOMPLETE_OK
29
+ so the python-argcomplete-check-easy-install-script does not
30
+ need to be called to find the entry point of the code and see if that is
31
+ marked with PYTHON_ARGCOMPLETE_OK.
32
+
33
+ INSTALL/DEBUGGING
34
+ =================
35
+
36
+ To include this support in another application that has setup.py generated
37
+ scripts:
38
+
39
+ - Add the line:
40
+ # PYTHON_ARGCOMPLETE_OK
41
+ near the top of the main python entry point.
42
+
43
+ - Include in the file calling parse_args():
44
+ from _argcomplete import try_argcomplete, filescompleter
45
+ Call try_argcomplete just before parse_args(), and optionally add
46
+ filescompleter to the positional arguments' add_argument().
47
+
48
+ If things do not work right away:
49
+
50
+ - Switch on argcomplete debugging with (also helpful when doing custom
51
+ completers):
52
+ export _ARC_DEBUG=1
53
+
54
+ - Run:
55
+ python-argcomplete-check-easy-install-script $(which appname)
56
+ echo $?
57
+ will echo 0 if the magic line has been found, 1 if not.
58
+
59
+ - Sometimes it helps to find early on errors using:
60
+ _ARGCOMPLETE=1 _ARC_DEBUG=1 appname
61
+ which should throw a KeyError: 'COMPLINE' (which is properly set by the
62
+ global argcomplete script).
63
+ """
64
+
65
+ from __future__ import annotations
66
+
67
+ import argparse
68
+ from glob import glob
69
+ import os
70
+ import sys
71
+ from typing import Any
72
+
73
+
74
+ class FastFilesCompleter:
75
+ """Fast file completer class."""
76
+
77
+ def __init__(self, directories: bool = True) -> None:
78
+ self.directories = directories
79
+
80
+ def __call__(self, prefix: str, **kwargs: Any) -> list[str]:
81
+ # Only called on non option completions.
82
+ if os.sep in prefix[1:]:
83
+ prefix_dir = len(os.path.dirname(prefix) + os.sep)
84
+ else:
85
+ prefix_dir = 0
86
+ completion = []
87
+ globbed = []
88
+ if "*" not in prefix and "?" not in prefix:
89
+ # We are on unix, otherwise no bash.
90
+ if not prefix or prefix[-1] == os.sep:
91
+ globbed.extend(glob(prefix + ".*"))
92
+ prefix += "*"
93
+ globbed.extend(glob(prefix))
94
+ for x in sorted(globbed):
95
+ if os.path.isdir(x):
96
+ x += "/"
97
+ # Append stripping the prefix (like bash, not like compgen).
98
+ completion.append(x[prefix_dir:])
99
+ return completion
100
+
101
+
102
+ if os.environ.get("_ARGCOMPLETE"):
103
+ try:
104
+ import argcomplete.completers
105
+ except ImportError:
106
+ sys.exit(-1)
107
+ filescompleter: FastFilesCompleter | None = FastFilesCompleter()
108
+
109
+ def try_argcomplete(parser: argparse.ArgumentParser) -> None:
110
+ argcomplete.autocomplete(parser, always_complete_options=False)
111
+
112
+ else:
113
+
114
+ def try_argcomplete(parser: argparse.ArgumentParser) -> None:
115
+ pass
116
+
117
+ filescompleter = None
.venv/lib/python3.14/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
+ "Frame",
20
+ "Source",
21
+ "Traceback",
22
+ "TracebackEntry",
23
+ "filter_traceback",
24
+ "getfslineno",
25
+ "getrawcode",
26
+ ]
.venv/lib/python3.14/site-packages/_pytest/_code/code.py ADDED
@@ -0,0 +1,1632 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from collections.abc import Callable
6
+ from collections.abc import Iterable
7
+ from collections.abc import Mapping
8
+ from collections.abc import Sequence
9
+ import dataclasses
10
+ import inspect
11
+ from io import StringIO
12
+ import os
13
+ from pathlib import Path
14
+ import re
15
+ import sys
16
+ from traceback import extract_tb
17
+ from traceback import format_exception
18
+ from traceback import format_exception_only
19
+ from traceback import FrameSummary
20
+ from types import CodeType
21
+ from types import FrameType
22
+ from types import TracebackType
23
+ from typing import Any
24
+ from typing import ClassVar
25
+ from typing import Final
26
+ from typing import final
27
+ from typing import Generic
28
+ from typing import Literal
29
+ from typing import overload
30
+ from typing import SupportsIndex
31
+ from typing import TypeAlias
32
+ from typing import TypeVar
33
+
34
+ import pluggy
35
+
36
+ import _pytest
37
+ from _pytest._code.source import findsource
38
+ from _pytest._code.source import getrawcode
39
+ from _pytest._code.source import getstatementrange_ast
40
+ from _pytest._code.source import Source
41
+ from _pytest._io import TerminalWriter
42
+ from _pytest._io.saferepr import safeformat
43
+ from _pytest._io.saferepr import saferepr
44
+ from _pytest.compat import get_real_func
45
+ from _pytest.deprecated import check_ispytest
46
+ from _pytest.pathlib import absolutepath
47
+ from _pytest.pathlib import bestrelpath
48
+
49
+
50
+ if sys.version_info < (3, 11):
51
+ from exceptiongroup import BaseExceptionGroup
52
+
53
+ TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
54
+
55
+ EXCEPTION_OR_MORE = type[BaseException] | tuple[type[BaseException], ...]
56
+
57
+
58
+ class Code:
59
+ """Wrapper around Python code objects."""
60
+
61
+ __slots__ = ("raw",)
62
+
63
+ def __init__(self, obj: CodeType) -> None:
64
+ self.raw = obj
65
+
66
+ @classmethod
67
+ def from_function(cls, obj: object) -> Code:
68
+ return cls(getrawcode(obj))
69
+
70
+ def __eq__(self, other):
71
+ return self.raw == other.raw
72
+
73
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
74
+ __hash__ = None # type: ignore
75
+
76
+ @property
77
+ def firstlineno(self) -> int:
78
+ return self.raw.co_firstlineno - 1
79
+
80
+ @property
81
+ def name(self) -> str:
82
+ return self.raw.co_name
83
+
84
+ @property
85
+ def path(self) -> Path | str:
86
+ """Return a path object pointing to source code, or an ``str`` in
87
+ case of ``OSError`` / non-existing file."""
88
+ filename = inspect.getfile(self.raw)
89
+ if not filename:
90
+ return ""
91
+ try:
92
+ p = absolutepath(filename)
93
+ # maybe don't try this checking
94
+ if not p.exists():
95
+ raise OSError("path check failed.")
96
+ return p
97
+ except OSError:
98
+ # XXX maybe try harder like the weird logic
99
+ # in the standard lib [linecache.updatecache] does?
100
+ return filename
101
+
102
+ @property
103
+ def fullsource(self) -> Source | None:
104
+ """Return a _pytest._code.Source object for the full source file of the code."""
105
+ full, _ = findsource(self.raw)
106
+ return full
107
+
108
+ def source(self) -> Source:
109
+ """Return a _pytest._code.Source object for the code object's source only."""
110
+ # return source only for that part of code
111
+ return Source(self.raw)
112
+
113
+ def getargs(self, var: bool = False) -> tuple[str, ...]:
114
+ """Return a tuple with the argument names for the code object.
115
+
116
+ If 'var' is set True also return the names of the variable and
117
+ keyword arguments when present.
118
+ """
119
+ # inspect.getargs merges positional and kwonly into a single list;
120
+ # co_argcount is needed to exclude kwonly when var=False.
121
+ args, varargs, varkw = inspect.getargs(self.raw)
122
+ if not var:
123
+ return tuple(args[: self.raw.co_argcount])
124
+ result = list(args)
125
+ if varargs is not None:
126
+ result.append(varargs)
127
+ if varkw is not None:
128
+ result.append(varkw)
129
+ return tuple(result)
130
+
131
+
132
+ class Frame:
133
+ """Wrapper around a Python frame holding f_locals and f_globals
134
+ in which expressions can be evaluated."""
135
+
136
+ __slots__ = ("raw",)
137
+
138
+ def __init__(self, frame: FrameType) -> None:
139
+ self.raw = frame
140
+
141
+ @property
142
+ def lineno(self) -> int:
143
+ return self.raw.f_lineno - 1
144
+
145
+ @property
146
+ def f_globals(self) -> dict[str, Any]:
147
+ return self.raw.f_globals
148
+
149
+ @property
150
+ def f_locals(self) -> dict[str, Any]:
151
+ return self.raw.f_locals
152
+
153
+ @property
154
+ def code(self) -> Code:
155
+ return Code(self.raw.f_code)
156
+
157
+ @property
158
+ def statement(self) -> Source:
159
+ """Statement this frame is at."""
160
+ if self.code.fullsource is None:
161
+ return Source("")
162
+ return self.code.fullsource.getstatement(self.lineno)
163
+
164
+ def eval(self, code, **vars):
165
+ """Evaluate 'code' in the frame.
166
+
167
+ 'vars' are optional additional local variables.
168
+
169
+ Returns the result of the evaluation.
170
+ """
171
+ f_locals = self.f_locals.copy()
172
+ f_locals.update(vars)
173
+ return eval(code, self.f_globals, f_locals)
174
+
175
+ def repr(self, object: object) -> str:
176
+ """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
177
+ return saferepr(object)
178
+
179
+ def getargs(self, var: bool = False):
180
+ """Return a list of tuples (name, value) for all arguments.
181
+
182
+ If 'var' is set True, also include the variable and keyword arguments
183
+ when present.
184
+ """
185
+ retval = []
186
+ for arg in self.code.getargs(var):
187
+ try:
188
+ retval.append((arg, self.f_locals[arg]))
189
+ except KeyError:
190
+ pass # this can occur when using Psyco
191
+ return retval
192
+
193
+
194
+ class TracebackEntry:
195
+ """A single entry in a Traceback."""
196
+
197
+ __slots__ = ("_rawentry", "_repr_style")
198
+
199
+ def __init__(
200
+ self,
201
+ rawentry: TracebackType,
202
+ repr_style: Literal["short", "long"] | None = None,
203
+ ) -> None:
204
+ self._rawentry: Final = rawentry
205
+ self._repr_style: Final = repr_style
206
+
207
+ def with_repr_style(
208
+ self, repr_style: Literal["short", "long"] | None
209
+ ) -> TracebackEntry:
210
+ return TracebackEntry(self._rawentry, repr_style)
211
+
212
+ @property
213
+ def lineno(self) -> int:
214
+ return self._rawentry.tb_lineno - 1
215
+
216
+ def get_python_framesummary(self) -> FrameSummary:
217
+ # Python's built-in traceback module implements all the nitty gritty
218
+ # details to get column numbers of out frames.
219
+ stack_summary = extract_tb(self._rawentry, limit=1)
220
+ return stack_summary[0]
221
+
222
+ # Column and end line numbers introduced in python 3.11
223
+ if sys.version_info < (3, 11):
224
+
225
+ @property
226
+ def end_lineno_relative(self) -> int | None:
227
+ return None
228
+
229
+ @property
230
+ def colno(self) -> int | None:
231
+ return None
232
+
233
+ @property
234
+ def end_colno(self) -> int | None:
235
+ return None
236
+ else:
237
+
238
+ @property
239
+ def end_lineno_relative(self) -> int | None:
240
+ frame_summary = self.get_python_framesummary()
241
+ if frame_summary.end_lineno is None: # pragma: no cover
242
+ return None
243
+ return frame_summary.end_lineno - 1 - self.frame.code.firstlineno
244
+
245
+ @property
246
+ def colno(self) -> int | None:
247
+ """Starting byte offset of the expression in the traceback entry."""
248
+ return self.get_python_framesummary().colno
249
+
250
+ @property
251
+ def end_colno(self) -> int | None:
252
+ """Ending byte offset of the expression in the traceback entry."""
253
+ return self.get_python_framesummary().end_colno
254
+
255
+ @property
256
+ def frame(self) -> Frame:
257
+ return Frame(self._rawentry.tb_frame)
258
+
259
+ @property
260
+ def relline(self) -> int:
261
+ return self.lineno - self.frame.code.firstlineno
262
+
263
+ def __repr__(self) -> str:
264
+ return f"<TracebackEntry {self.frame.code.path}:{self.lineno + 1}>"
265
+
266
+ @property
267
+ def statement(self) -> Source:
268
+ """_pytest._code.Source object for the current statement."""
269
+ source = self.frame.code.fullsource
270
+ assert source is not None
271
+ return source.getstatement(self.lineno)
272
+
273
+ @property
274
+ def path(self) -> Path | str:
275
+ """Path to the source code."""
276
+ return self.frame.code.path
277
+
278
+ @property
279
+ def locals(self) -> dict[str, Any]:
280
+ """Locals of underlying frame."""
281
+ return self.frame.f_locals
282
+
283
+ def getfirstlinesource(self) -> int:
284
+ return self.frame.code.firstlineno
285
+
286
+ def getsource(
287
+ self, astcache: dict[str | Path, ast.AST] | None = None
288
+ ) -> Source | None:
289
+ """Return failing source code."""
290
+ # we use the passed in astcache to not reparse asttrees
291
+ # within exception info printing
292
+ source = self.frame.code.fullsource
293
+ if source is None:
294
+ return None
295
+ key = astnode = None
296
+ if astcache is not None:
297
+ key = self.frame.code.path
298
+ if key is not None:
299
+ astnode = astcache.get(key, None)
300
+ start = self.getfirstlinesource()
301
+ try:
302
+ astnode, _, end = getstatementrange_ast(
303
+ self.lineno, source, astnode=astnode
304
+ )
305
+ except SyntaxError:
306
+ end = self.lineno + 1
307
+ else:
308
+ if key is not None and astcache is not None:
309
+ astcache[key] = astnode
310
+ return source[start:end]
311
+
312
+ source = property(getsource)
313
+
314
+ def ishidden(self, excinfo: ExceptionInfo[BaseException] | None) -> bool:
315
+ """Return True if the current frame has a var __tracebackhide__
316
+ resolving to True.
317
+
318
+ If __tracebackhide__ is a callable, it gets called with the
319
+ ExceptionInfo instance and can decide whether to hide the traceback.
320
+
321
+ Mostly for internal use.
322
+ """
323
+ tbh: bool | Callable[[ExceptionInfo[BaseException] | None], bool] = False
324
+ for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
325
+ # in normal cases, f_locals and f_globals are dictionaries
326
+ # however via `exec(...)` / `eval(...)` they can be other types
327
+ # (even incorrect types!).
328
+ # as such, we suppress all exceptions while accessing __tracebackhide__
329
+ try:
330
+ tbh = maybe_ns_dct["__tracebackhide__"]
331
+ except Exception:
332
+ pass
333
+ else:
334
+ break
335
+ if tbh and callable(tbh):
336
+ return tbh(excinfo)
337
+ return tbh
338
+
339
+ def __str__(self) -> str:
340
+ name = self.frame.code.name
341
+ try:
342
+ line = str(self.statement).lstrip()
343
+ except KeyboardInterrupt:
344
+ raise
345
+ except BaseException:
346
+ line = "???"
347
+ # This output does not quite match Python's repr for traceback entries,
348
+ # but changing it to do so would break certain plugins. See
349
+ # https://github.com/pytest-dev/pytest/pull/7535/ for details.
350
+ return f" File '{self.path}':{self.lineno + 1} in {name}\n {line}\n"
351
+
352
+ @property
353
+ def name(self) -> str:
354
+ """co_name of underlying code."""
355
+ return self.frame.code.raw.co_name
356
+
357
+
358
+ class Traceback(list[TracebackEntry]):
359
+ """Traceback objects encapsulate and offer higher level access to Traceback entries."""
360
+
361
+ def __init__(
362
+ self,
363
+ tb: TracebackType | Iterable[TracebackEntry],
364
+ ) -> None:
365
+ """Initialize from given python traceback object and ExceptionInfo."""
366
+ if isinstance(tb, TracebackType):
367
+
368
+ def f(cur: TracebackType) -> Iterable[TracebackEntry]:
369
+ cur_: TracebackType | None = cur
370
+ while cur_ is not None:
371
+ yield TracebackEntry(cur_)
372
+ cur_ = cur_.tb_next
373
+
374
+ super().__init__(f(tb))
375
+ else:
376
+ super().__init__(tb)
377
+
378
+ def cut(
379
+ self,
380
+ path: os.PathLike[str] | str | None = None,
381
+ lineno: int | None = None,
382
+ firstlineno: int | None = None,
383
+ excludepath: os.PathLike[str] | None = None,
384
+ ) -> Traceback:
385
+ """Return a Traceback instance wrapping part of this Traceback.
386
+
387
+ By providing any combination of path, lineno and firstlineno, the
388
+ first frame to start the to-be-returned traceback is determined.
389
+
390
+ This allows cutting the first part of a Traceback instance e.g.
391
+ for formatting reasons (removing some uninteresting bits that deal
392
+ with handling of the exception/traceback).
393
+ """
394
+ path_ = None if path is None else os.fspath(path)
395
+ excludepath_ = None if excludepath is None else os.fspath(excludepath)
396
+ for x in self:
397
+ code = x.frame.code
398
+ codepath = code.path
399
+ if path is not None and str(codepath) != path_:
400
+ continue
401
+ if (
402
+ excludepath is not None
403
+ and isinstance(codepath, Path)
404
+ and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
405
+ ):
406
+ continue
407
+ if lineno is not None and x.lineno != lineno:
408
+ continue
409
+ if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
410
+ continue
411
+ return Traceback(x._rawentry)
412
+ return self
413
+
414
+ @overload
415
+ def __getitem__(self, key: SupportsIndex) -> TracebackEntry: ...
416
+
417
+ @overload
418
+ def __getitem__(self, key: slice) -> Traceback: ...
419
+
420
+ def __getitem__(self, key: SupportsIndex | slice) -> TracebackEntry | Traceback:
421
+ if isinstance(key, slice):
422
+ return self.__class__(super().__getitem__(key))
423
+ else:
424
+ return super().__getitem__(key)
425
+
426
+ def filter(
427
+ self,
428
+ excinfo_or_fn: ExceptionInfo[BaseException] | Callable[[TracebackEntry], bool],
429
+ /,
430
+ ) -> Traceback:
431
+ """Return a Traceback instance with certain items removed.
432
+
433
+ If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
434
+ which are hidden (see ishidden() above).
435
+
436
+ Otherwise, the filter is a function that gets a single argument, a
437
+ ``TracebackEntry`` instance, and should return True when the item should
438
+ be added to the ``Traceback``, False when not.
439
+ """
440
+ if isinstance(excinfo_or_fn, ExceptionInfo):
441
+ fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731
442
+ else:
443
+ fn = excinfo_or_fn
444
+ return Traceback(filter(fn, self))
445
+
446
+ def recursionindex(self) -> int | None:
447
+ """Return the index of the frame/TracebackEntry where recursion originates if
448
+ appropriate, None if no recursion occurred."""
449
+ cache: dict[tuple[Any, int, int], list[dict[str, Any]]] = {}
450
+ for i, entry in enumerate(self):
451
+ # id for the code.raw is needed to work around
452
+ # the strange metaprogramming in the decorator lib from pypi
453
+ # which generates code objects that have hash/value equality
454
+ # XXX needs a test
455
+ key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
456
+ values = cache.setdefault(key, [])
457
+ # Since Python 3.13 f_locals is a proxy, freeze it.
458
+ loc = dict(entry.frame.f_locals)
459
+ if values:
460
+ for otherloc in values:
461
+ if otherloc == loc:
462
+ return i
463
+ values.append(loc)
464
+ return None
465
+
466
+
467
+ def stringify_exception(
468
+ exc: BaseException, include_subexception_msg: bool = True
469
+ ) -> str:
470
+ try:
471
+ notes = getattr(exc, "__notes__", [])
472
+ except KeyError:
473
+ # Workaround for https://github.com/python/cpython/issues/98778 on
474
+ # some 3.10 and 3.11 patch versions.
475
+ HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
476
+ if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
477
+ notes = []
478
+ else: # pragma: no cover
479
+ # exception not related to above bug, reraise
480
+ raise
481
+ if not include_subexception_msg and isinstance(exc, BaseExceptionGroup):
482
+ message = exc.message
483
+ else:
484
+ message = str(exc)
485
+
486
+ return "\n".join(
487
+ [
488
+ message,
489
+ *notes,
490
+ ]
491
+ )
492
+
493
+
494
+ E = TypeVar("E", bound=BaseException, covariant=True)
495
+
496
+
497
+ @final
498
+ @dataclasses.dataclass
499
+ class ExceptionInfo(Generic[E]):
500
+ """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
501
+
502
+ _assert_start_repr: ClassVar = "AssertionError('assert "
503
+
504
+ _excinfo: tuple[type[E], E, TracebackType] | None
505
+ _striptext: str
506
+ _traceback: Traceback | None
507
+
508
+ def __init__(
509
+ self,
510
+ excinfo: tuple[type[E], E, TracebackType] | None,
511
+ striptext: str = "",
512
+ traceback: Traceback | None = None,
513
+ *,
514
+ _ispytest: bool = False,
515
+ ) -> None:
516
+ check_ispytest(_ispytest)
517
+ self._excinfo = excinfo
518
+ self._striptext = striptext
519
+ self._traceback = traceback
520
+
521
+ @classmethod
522
+ def from_exception(
523
+ cls,
524
+ # Ignoring error: "Cannot use a covariant type variable as a parameter".
525
+ # This is OK to ignore because this class is (conceptually) readonly.
526
+ # See https://github.com/python/mypy/issues/7049.
527
+ exception: E, # type: ignore[misc]
528
+ exprinfo: str | None = None,
529
+ ) -> ExceptionInfo[E]:
530
+ """Return an ExceptionInfo for an existing exception.
531
+
532
+ The exception must have a non-``None`` ``__traceback__`` attribute,
533
+ otherwise this function fails with an assertion error. This means that
534
+ the exception must have been raised, or added a traceback with the
535
+ :py:meth:`~BaseException.with_traceback()` method.
536
+
537
+ :param exprinfo:
538
+ A text string helping to determine if we should strip
539
+ ``AssertionError`` from the output. Defaults to the exception
540
+ message/``__str__()``.
541
+
542
+ .. versionadded:: 7.4
543
+ """
544
+ assert exception.__traceback__, (
545
+ "Exceptions passed to ExcInfo.from_exception(...)"
546
+ " must have a non-None __traceback__."
547
+ )
548
+ exc_info = (type(exception), exception, exception.__traceback__)
549
+ return cls.from_exc_info(exc_info, exprinfo)
550
+
551
+ @classmethod
552
+ def from_exc_info(
553
+ cls,
554
+ exc_info: tuple[type[E], E, TracebackType],
555
+ exprinfo: str | None = None,
556
+ ) -> ExceptionInfo[E]:
557
+ """Like :func:`from_exception`, but using old-style exc_info tuple."""
558
+ _striptext = ""
559
+ if exprinfo is None and isinstance(exc_info[1], AssertionError):
560
+ exprinfo = getattr(exc_info[1], "msg", None)
561
+ if exprinfo is None:
562
+ exprinfo = saferepr(exc_info[1])
563
+ if exprinfo and exprinfo.startswith(cls._assert_start_repr):
564
+ _striptext = "AssertionError: "
565
+
566
+ return cls(exc_info, _striptext, _ispytest=True)
567
+
568
+ @classmethod
569
+ def from_current(cls, exprinfo: str | None = None) -> ExceptionInfo[BaseException]:
570
+ """Return an ExceptionInfo matching the current traceback.
571
+
572
+ .. warning::
573
+
574
+ Experimental API
575
+
576
+ :param exprinfo:
577
+ A text string helping to determine if we should strip
578
+ ``AssertionError`` from the output. Defaults to the exception
579
+ message/``__str__()``.
580
+ """
581
+ tup = sys.exc_info()
582
+ assert tup[0] is not None, "no current exception"
583
+ assert tup[1] is not None, "no current exception"
584
+ assert tup[2] is not None, "no current exception"
585
+ exc_info = (tup[0], tup[1], tup[2])
586
+ return ExceptionInfo.from_exc_info(exc_info, exprinfo)
587
+
588
+ @classmethod
589
+ def for_later(cls) -> ExceptionInfo[E]:
590
+ """Return an unfilled ExceptionInfo."""
591
+ return cls(None, _ispytest=True)
592
+
593
+ def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None:
594
+ """Fill an unfilled ExceptionInfo created with ``for_later()``."""
595
+ assert self._excinfo is None, "ExceptionInfo was already filled"
596
+ self._excinfo = exc_info
597
+
598
+ @property
599
+ def type(self) -> type[E]:
600
+ """The exception class."""
601
+ assert self._excinfo is not None, (
602
+ ".type can only be used after the context manager exits"
603
+ )
604
+ return self._excinfo[0]
605
+
606
+ @property
607
+ def value(self) -> E:
608
+ """The exception value."""
609
+ assert self._excinfo is not None, (
610
+ ".value can only be used after the context manager exits"
611
+ )
612
+ return self._excinfo[1]
613
+
614
+ @property
615
+ def tb(self) -> TracebackType:
616
+ """The exception raw traceback."""
617
+ assert self._excinfo is not None, (
618
+ ".tb can only be used after the context manager exits"
619
+ )
620
+ return self._excinfo[2]
621
+
622
+ @property
623
+ def typename(self) -> str:
624
+ """The type name of the exception."""
625
+ assert self._excinfo is not None, (
626
+ ".typename can only be used after the context manager exits"
627
+ )
628
+ return self.type.__name__
629
+
630
+ @property
631
+ def traceback(self) -> Traceback:
632
+ """The traceback."""
633
+ if self._traceback is None:
634
+ self._traceback = Traceback(self.tb)
635
+ return self._traceback
636
+
637
+ @traceback.setter
638
+ def traceback(self, value: Traceback) -> None:
639
+ self._traceback = value
640
+
641
+ def __repr__(self) -> str:
642
+ if self._excinfo is None:
643
+ return "<ExceptionInfo for raises contextmanager>"
644
+ return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
645
+
646
+ def exconly(self, tryshort: bool = False) -> str:
647
+ """Return the exception as a string.
648
+
649
+ This is usually a single line "<exception type>: <exception str>", but
650
+ may also include additional lines for the exception notes, and detailed
651
+ information for SyntaxError's.
652
+
653
+ :param tryshort:
654
+ If true, and the exception is an AssertionError, strip
655
+ 'AssertionError: ' from the beginning.
656
+ """
657
+
658
+ def _get_single_subexc(
659
+ eg: BaseExceptionGroup[BaseException],
660
+ ) -> BaseException | None:
661
+ if len(eg.exceptions) != 1:
662
+ return None
663
+ if isinstance(e := eg.exceptions[0], BaseExceptionGroup):
664
+ return _get_single_subexc(e)
665
+ return e
666
+
667
+ if (
668
+ tryshort
669
+ and isinstance(self.value, BaseExceptionGroup)
670
+ and (subexc := _get_single_subexc(self.value)) is not None
671
+ ):
672
+ return f"{subexc!r} [single exception in {type(self.value).__name__}]"
673
+
674
+ lines = format_exception_only(self.value)
675
+ # The lines already include line separators.
676
+ text = "".join(lines)
677
+ text = text.rstrip()
678
+ if tryshort:
679
+ if text.startswith(self._striptext):
680
+ text = text[len(self._striptext) :]
681
+ return text
682
+
683
+ def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool:
684
+ """Return True if the exception is an instance of exc.
685
+
686
+ Consider using ``isinstance(excinfo.value, exc)`` instead.
687
+ """
688
+ return isinstance(self.value, exc)
689
+
690
+ def _getreprcrash(self) -> ReprFileLocation | None:
691
+ # Find last non-hidden traceback entry that led to the exception of the
692
+ # traceback, or None if all hidden.
693
+ for i in range(-1, -len(self.traceback) - 1, -1):
694
+ entry = self.traceback[i]
695
+ if not entry.ishidden(self):
696
+ path, lineno = entry.frame.code.raw.co_filename, entry.lineno
697
+ exconly = self.exconly(tryshort=True)
698
+ return ReprFileLocation(path, lineno + 1, exconly)
699
+ return None
700
+
701
+ def getrepr(
702
+ self,
703
+ showlocals: bool = False,
704
+ style: TracebackStyle = "long",
705
+ abspath: bool = False,
706
+ tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True,
707
+ funcargs: bool = False,
708
+ truncate_locals: bool = True,
709
+ truncate_args: bool = True,
710
+ chain: bool = True,
711
+ ) -> ReprExceptionInfo | ExceptionChainRepr:
712
+ """Return str()able representation of this exception info.
713
+
714
+ The formatting parameters are ineffective if ``style="native"``,
715
+ since in this case the native formatting is used.
716
+
717
+ :param bool showlocals:
718
+ Show locals per traceback entry.
719
+
720
+ :param str style:
721
+ long|short|line|no|native|value traceback style.
722
+
723
+ :param bool abspath:
724
+ If paths should be changed to absolute or left unchanged.
725
+
726
+ :param tbfilter:
727
+ A filter for traceback entries.
728
+
729
+ * If false, don't hide any entries.
730
+ * If true, hide internal entries and entries that contain a local
731
+ variable ``__tracebackhide__ = True``.
732
+ * If a callable, delegates the filtering to the callable.
733
+
734
+ :param bool funcargs:
735
+ Show function arguments per traceback entry.
736
+
737
+ :param bool truncate_locals:
738
+ Whether to show a size-limited `repr()` of locals, or a full
739
+ pretty-printing.
740
+
741
+ :param bool truncate_args:
742
+ Whether to show a size-limited truncated `repr()` of function
743
+ arguments, or a full pretty-printing.
744
+
745
+ :param bool chain:
746
+ If chained exceptions should be shown.
747
+
748
+ .. versionchanged:: 3.9
749
+
750
+ Added the ``chain`` parameter.
751
+ """
752
+ if style == "native":
753
+ return ReprExceptionInfo(
754
+ reprtraceback=ReprTracebackNative(
755
+ format_exception(
756
+ self.type,
757
+ self.value,
758
+ self.traceback[0]._rawentry if self.traceback else None,
759
+ )
760
+ ),
761
+ reprcrash=self._getreprcrash(),
762
+ )
763
+
764
+ fmt = ExceptionInfoFormatter(
765
+ showlocals=showlocals,
766
+ style=style,
767
+ abspath=abspath,
768
+ tbfilter=tbfilter,
769
+ funcargs=funcargs,
770
+ truncate_locals=truncate_locals,
771
+ truncate_args=truncate_args,
772
+ chain=chain,
773
+ )
774
+ return fmt.repr_excinfo(self)
775
+
776
+ def match(self, regexp: str | re.Pattern[str]) -> Literal[True]:
777
+ """Check whether the regular expression `regexp` matches the string
778
+ representation of the exception using :func:`python:re.search`.
779
+
780
+ If it matches `True` is returned, otherwise an `AssertionError` is raised.
781
+ """
782
+ __tracebackhide__ = True
783
+ value = stringify_exception(self.value)
784
+ msg = (
785
+ f"Regex pattern did not match.\n"
786
+ f" Expected regex: {regexp!r}\n"
787
+ f" Actual message: {value!r}"
788
+ )
789
+ if regexp == value:
790
+ msg += "\n Did you mean to `re.escape()` the regex?"
791
+ assert re.search(regexp, value), msg
792
+ # Return True to allow for "assert excinfo.match()".
793
+ return True
794
+
795
+ def _group_contains(
796
+ self,
797
+ exc_group: BaseExceptionGroup[BaseException],
798
+ expected_exception: EXCEPTION_OR_MORE,
799
+ match: str | re.Pattern[str] | None,
800
+ target_depth: int | None = None,
801
+ current_depth: int = 1,
802
+ ) -> bool:
803
+ """Return `True` if a `BaseExceptionGroup` contains a matching exception."""
804
+ if (target_depth is not None) and (current_depth > target_depth):
805
+ # already descended past the target depth
806
+ return False
807
+ for exc in exc_group.exceptions:
808
+ if isinstance(exc, BaseExceptionGroup):
809
+ if self._group_contains(
810
+ exc, expected_exception, match, target_depth, current_depth + 1
811
+ ):
812
+ return True
813
+ if (target_depth is not None) and (current_depth != target_depth):
814
+ # not at the target depth, no match
815
+ continue
816
+ if not isinstance(exc, expected_exception):
817
+ continue
818
+ if match is not None:
819
+ value = stringify_exception(exc)
820
+ if not re.search(match, value):
821
+ continue
822
+ return True
823
+ return False
824
+
825
+ def group_contains(
826
+ self,
827
+ expected_exception: EXCEPTION_OR_MORE,
828
+ *,
829
+ match: str | re.Pattern[str] | None = None,
830
+ depth: int | None = None,
831
+ ) -> bool:
832
+ """Check whether a captured exception group contains a matching exception.
833
+
834
+ :param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
835
+ The expected exception type, or a tuple if one of multiple possible
836
+ exception types are expected.
837
+
838
+ :param str | re.Pattern[str] | None match:
839
+ If specified, a string containing a regular expression,
840
+ or a regular expression object, that is tested against the string
841
+ representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
842
+ using :func:`re.search`.
843
+
844
+ To match a literal string that may contain :ref:`special characters
845
+ <re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
846
+
847
+ :param Optional[int] depth:
848
+ If `None`, will search for a matching exception at any nesting depth.
849
+ If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
850
+ the exceptions contained within the topmost exception group).
851
+
852
+ .. versionadded:: 8.0
853
+
854
+ .. warning::
855
+ This helper makes it easy to check for the presence of specific exceptions,
856
+ but it is very bad for checking that the group does *not* contain
857
+ *any other exceptions*.
858
+ You should instead consider using :class:`pytest.RaisesGroup`
859
+
860
+ """
861
+ msg = "Captured exception is not an instance of `BaseExceptionGroup`"
862
+ assert isinstance(self.value, BaseExceptionGroup), msg
863
+ msg = "`depth` must be >= 1 if specified"
864
+ assert (depth is None) or (depth >= 1), msg
865
+ return self._group_contains(self.value, expected_exception, match, depth)
866
+
867
+
868
+ # Type alias for the `tbfilter` setting:
869
+ # bool: If True, it should be filtered using Traceback.filter()
870
+ # callable: A callable that takes an ExceptionInfo and returns the filtered traceback.
871
+ TracebackFilter: TypeAlias = bool | Callable[[ExceptionInfo[BaseException]], Traceback]
872
+
873
+
874
+ @dataclasses.dataclass
875
+ class ExceptionInfoFormatter:
876
+ """Helper object to format ExceptionInfo's and individual exception parts
877
+ into TerminalRepr's.
878
+
879
+ See :func:`ExceptionInfo.getrepr` for parameters.
880
+ """
881
+
882
+ # for traceback entries
883
+ flow_marker: ClassVar = ">"
884
+ fail_marker: ClassVar = "E"
885
+
886
+ showlocals: bool = False
887
+ # Note: "native" is handled outside of ExceptionInfoFormatter.
888
+ style: TracebackStyle = "long"
889
+ abspath: bool = True
890
+ tbfilter: TracebackFilter = True
891
+ funcargs: bool = False
892
+ truncate_locals: bool = True
893
+ truncate_args: bool = True
894
+ chain: bool = True
895
+
896
+ astcache: dict[str | Path, ast.AST] = dataclasses.field(
897
+ default_factory=dict, init=False, repr=False
898
+ )
899
+
900
+ def _getindent(self, source: Source) -> int:
901
+ # Figure out indent for the given source.
902
+ try:
903
+ s = str(source.getstatement(len(source) - 1))
904
+ except KeyboardInterrupt:
905
+ raise
906
+ except BaseException:
907
+ try:
908
+ s = str(source[-1])
909
+ except KeyboardInterrupt:
910
+ raise
911
+ except BaseException:
912
+ return 0
913
+ return 4 + (len(s) - len(s.lstrip()))
914
+
915
+ def _getentrysource(self, entry: TracebackEntry) -> Source | None:
916
+ source = entry.getsource(self.astcache)
917
+ if source is not None:
918
+ source = source.deindent()
919
+ return source
920
+
921
+ def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None:
922
+ if self.funcargs:
923
+ args = []
924
+ for argname, argvalue in entry.frame.getargs(var=True):
925
+ if self.truncate_args:
926
+ str_repr = saferepr(argvalue)
927
+ else:
928
+ str_repr = saferepr(argvalue, maxsize=None)
929
+ args.append((argname, str_repr))
930
+ return ReprFuncArgs(args)
931
+ return None
932
+
933
+ def get_source(
934
+ self,
935
+ source: Source | None,
936
+ line_index: int = -1,
937
+ excinfo: ExceptionInfo[BaseException] | None = None,
938
+ short: bool = False,
939
+ end_line_index: int | None = None,
940
+ colno: int | None = None,
941
+ end_colno: int | None = None,
942
+ ) -> list[str]:
943
+ """Return formatted and marked up source lines."""
944
+ lines = []
945
+ if source is not None and line_index < 0:
946
+ line_index += len(source)
947
+ if source is None or line_index >= len(source.lines) or line_index < 0:
948
+ # `line_index` could still be outside `range(len(source.lines))` if
949
+ # we're processing AST with pathological position attributes.
950
+ source = Source("???")
951
+ line_index = 0
952
+ space_prefix = " "
953
+ if short:
954
+ lines.append(space_prefix + source.lines[line_index].strip())
955
+ lines.extend(
956
+ self.get_highlight_arrows_for_line(
957
+ raw_line=source.raw_lines[line_index],
958
+ line=source.lines[line_index].strip(),
959
+ lineno=line_index,
960
+ end_lineno=end_line_index,
961
+ colno=colno,
962
+ end_colno=end_colno,
963
+ )
964
+ )
965
+ else:
966
+ for line in source.lines[:line_index]:
967
+ lines.append(space_prefix + line)
968
+ lines.append(self.flow_marker + " " + source.lines[line_index])
969
+ lines.extend(
970
+ self.get_highlight_arrows_for_line(
971
+ raw_line=source.raw_lines[line_index],
972
+ line=source.lines[line_index],
973
+ lineno=line_index,
974
+ end_lineno=end_line_index,
975
+ colno=colno,
976
+ end_colno=end_colno,
977
+ )
978
+ )
979
+ for line in source.lines[line_index + 1 :]:
980
+ lines.append(space_prefix + line)
981
+ if excinfo is not None:
982
+ indent = 4 if short else self._getindent(source)
983
+ lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
984
+ return lines
985
+
986
+ def get_highlight_arrows_for_line(
987
+ self,
988
+ line: str,
989
+ raw_line: str,
990
+ lineno: int | None,
991
+ end_lineno: int | None,
992
+ colno: int | None,
993
+ end_colno: int | None,
994
+ ) -> list[str]:
995
+ """Return characters highlighting a source line.
996
+
997
+ Example with colno and end_colno pointing to the bar expression:
998
+ "foo() + bar()"
999
+ returns " ^^^^^"
1000
+ """
1001
+ if lineno != end_lineno:
1002
+ # Don't handle expressions that span multiple lines.
1003
+ return []
1004
+ if colno is None or end_colno is None:
1005
+ # Can't do anything without column information.
1006
+ return []
1007
+
1008
+ num_stripped_chars = len(raw_line) - len(line)
1009
+
1010
+ start_char_offset = _byte_offset_to_character_offset(raw_line, colno)
1011
+ end_char_offset = _byte_offset_to_character_offset(raw_line, end_colno)
1012
+ num_carets = end_char_offset - start_char_offset
1013
+ # If the highlight would span the whole line, it is redundant, don't
1014
+ # show it.
1015
+ if num_carets >= len(line.strip()):
1016
+ return []
1017
+
1018
+ highlights = " "
1019
+ highlights += " " * (start_char_offset - num_stripped_chars + 1)
1020
+ highlights += "^" * num_carets
1021
+ return [highlights]
1022
+
1023
+ def get_exconly(
1024
+ self,
1025
+ excinfo: ExceptionInfo[BaseException],
1026
+ indent: int = 4,
1027
+ markall: bool = False,
1028
+ ) -> list[str]:
1029
+ lines = []
1030
+ indentstr = " " * indent
1031
+ # Get the real exception information out.
1032
+ exlines = excinfo.exconly(tryshort=True).split("\n")
1033
+ failindent = self.fail_marker + indentstr[1:]
1034
+ for line in exlines:
1035
+ lines.append(failindent + line)
1036
+ if not markall:
1037
+ failindent = indentstr
1038
+ return lines
1039
+
1040
+ def repr_locals(self, locals: Mapping[str, object]) -> ReprLocals | None:
1041
+ if self.showlocals:
1042
+ lines = []
1043
+ # Variables starting with `@` are helpers injected by assertion
1044
+ # rewriting, not user variables, so hide them.
1045
+ keys = [loc for loc in locals if loc[0] != "@"]
1046
+ keys.sort()
1047
+ for name in keys:
1048
+ value = locals[name]
1049
+ if name == "__builtins__":
1050
+ lines.append("__builtins__ = <builtins>")
1051
+ else:
1052
+ # This formatting could all be handled by the
1053
+ # _repr() function, which is only reprlib.Repr in
1054
+ # disguise, so is very configurable.
1055
+ if self.truncate_locals:
1056
+ str_repr = saferepr(value)
1057
+ else:
1058
+ str_repr = safeformat(value)
1059
+ # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
1060
+ lines.append(f"{name:<10} = {str_repr}")
1061
+ # else:
1062
+ # self._line("%-10s =\\" % (name,))
1063
+ # # XXX
1064
+ # pprint.pprint(value, stream=self.excinfowriter)
1065
+ return ReprLocals(lines)
1066
+ return None
1067
+
1068
+ def repr_traceback_entry(
1069
+ self,
1070
+ entry: TracebackEntry | None,
1071
+ excinfo: ExceptionInfo[BaseException] | None = None,
1072
+ ) -> ReprEntry:
1073
+ lines: list[str] = []
1074
+ style = (
1075
+ entry._repr_style
1076
+ if entry is not None and entry._repr_style is not None
1077
+ else self.style
1078
+ )
1079
+ if style in ("short", "long") and entry is not None:
1080
+ source = self._getentrysource(entry)
1081
+ if source is None:
1082
+ source = Source("???")
1083
+ line_index = 0
1084
+ end_line_index, colno, end_colno = None, None, None
1085
+ else:
1086
+ line_index = entry.relline
1087
+ end_line_index = entry.end_lineno_relative
1088
+ colno = entry.colno
1089
+ end_colno = entry.end_colno
1090
+ short = style == "short"
1091
+ reprargs = self.repr_args(entry) if not short else None
1092
+ s = self.get_source(
1093
+ source=source,
1094
+ line_index=line_index,
1095
+ excinfo=excinfo,
1096
+ short=short,
1097
+ end_line_index=end_line_index,
1098
+ colno=colno,
1099
+ end_colno=end_colno,
1100
+ )
1101
+ lines.extend(s)
1102
+ if short:
1103
+ message = f"in {entry.name}"
1104
+ else:
1105
+ message = (excinfo and excinfo.typename) or ""
1106
+ entry_path = entry.path
1107
+ path = self._makepath(entry_path)
1108
+ reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
1109
+ localsrepr = self.repr_locals(entry.locals)
1110
+ return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
1111
+ elif style == "value":
1112
+ if excinfo:
1113
+ lines.extend(str(excinfo.value).split("\n"))
1114
+ return ReprEntry(lines, None, None, None, style)
1115
+ else:
1116
+ if excinfo:
1117
+ lines.extend(self.get_exconly(excinfo, indent=4))
1118
+ return ReprEntry(lines, None, None, None, style)
1119
+
1120
+ def _makepath(self, path: Path | str) -> str:
1121
+ if not self.abspath and isinstance(path, Path):
1122
+ try:
1123
+ np = bestrelpath(Path.cwd(), path)
1124
+ except OSError:
1125
+ return str(path)
1126
+ if len(np) < len(str(path)):
1127
+ return np
1128
+ return str(path)
1129
+
1130
+ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
1131
+ traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
1132
+
1133
+ if isinstance(excinfo.value, RecursionError):
1134
+ traceback, extraline = self._truncate_recursive_traceback(traceback)
1135
+ else:
1136
+ extraline = None
1137
+
1138
+ if not traceback:
1139
+ if extraline is None:
1140
+ extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
1141
+ entries = [self.repr_traceback_entry(None, excinfo)]
1142
+ return ReprTraceback(entries, extraline, style=self.style)
1143
+
1144
+ last = traceback[-1]
1145
+ if self.style == "value":
1146
+ entries = [self.repr_traceback_entry(last, excinfo)]
1147
+ return ReprTraceback(entries, None, style=self.style)
1148
+
1149
+ entries = [
1150
+ self.repr_traceback_entry(entry, excinfo if last == entry else None)
1151
+ for entry in traceback
1152
+ ]
1153
+ return ReprTraceback(entries, extraline, style=self.style)
1154
+
1155
+ def _truncate_recursive_traceback(
1156
+ self, traceback: Traceback
1157
+ ) -> tuple[Traceback, str | None]:
1158
+ """Truncate the given recursive traceback trying to find the starting
1159
+ point of the recursion.
1160
+
1161
+ The detection is done by going through each traceback entry and
1162
+ finding the point in which the locals of the frame are equal to the
1163
+ locals of a previous frame (see ``recursionindex()``).
1164
+
1165
+ Handle the situation where the recursion process might raise an
1166
+ exception (for example comparing numpy arrays using equality raises a
1167
+ TypeError), in which case we do our best to warn the user of the
1168
+ error and show a limited traceback.
1169
+ """
1170
+ try:
1171
+ recursionindex = traceback.recursionindex()
1172
+ except Exception as e:
1173
+ max_frames = 10
1174
+ extraline: str | None = (
1175
+ "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
1176
+ " The following exception happened when comparing locals in the stack frame:\n"
1177
+ f" {type(e).__name__}: {e!s}\n"
1178
+ f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
1179
+ )
1180
+ # Type ignored because adding two instances of a List subtype
1181
+ # currently incorrectly has type List instead of the subtype.
1182
+ traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
1183
+ else:
1184
+ if recursionindex is not None:
1185
+ extraline = "!!! Recursion detected (same locals & position)"
1186
+ traceback = traceback[: recursionindex + 1]
1187
+ else:
1188
+ extraline = None
1189
+
1190
+ return traceback, extraline
1191
+
1192
+ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainRepr:
1193
+ repr_chain: list[tuple[ReprTraceback, ReprFileLocation | None, str | None]] = []
1194
+ e: BaseException | None = excinfo.value
1195
+ excinfo_: ExceptionInfo[BaseException] | None = excinfo
1196
+ description = None
1197
+ seen: set[int] = set()
1198
+ while e is not None and id(e) not in seen:
1199
+ seen.add(id(e))
1200
+
1201
+ if excinfo_:
1202
+ # Fall back to native traceback as a temporary workaround until
1203
+ # full support for exception groups added to ExceptionInfo.
1204
+ # See https://github.com/pytest-dev/pytest/issues/9159
1205
+ reprtraceback: ReprTraceback | ReprTracebackNative
1206
+ if isinstance(e, BaseExceptionGroup):
1207
+ # don't filter any sub-exceptions since they shouldn't have any internal frames
1208
+ traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
1209
+ extraline = (
1210
+ "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
1211
+ if not traceback
1212
+ else None
1213
+ )
1214
+ reprtraceback = ReprTracebackNative(
1215
+ format_exception(
1216
+ type(excinfo.value),
1217
+ excinfo.value,
1218
+ traceback[0]._rawentry if traceback else None,
1219
+ ),
1220
+ extraline=extraline,
1221
+ )
1222
+
1223
+ else:
1224
+ reprtraceback = self.repr_traceback(excinfo_)
1225
+ reprcrash = excinfo_._getreprcrash()
1226
+ else:
1227
+ # Fallback to native repr if the exception doesn't have a traceback:
1228
+ # ExceptionInfo objects require a full traceback to work.
1229
+ reprtraceback = ReprTracebackNative(format_exception(type(e), e, None))
1230
+ reprcrash = None
1231
+ repr_chain.append((reprtraceback, reprcrash, description))
1232
+
1233
+ if e.__cause__ is not None and self.chain:
1234
+ e = e.__cause__
1235
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
1236
+ description = "The above exception was the direct cause of the following exception:"
1237
+ elif (
1238
+ e.__context__ is not None and not e.__suppress_context__ and self.chain
1239
+ ):
1240
+ e = e.__context__
1241
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
1242
+ description = "During handling of the above exception, another exception occurred:"
1243
+ else:
1244
+ e = None
1245
+ repr_chain.reverse()
1246
+ return ExceptionChainRepr(repr_chain)
1247
+
1248
+
1249
+ @dataclasses.dataclass(eq=False)
1250
+ class TerminalRepr:
1251
+ """Base class for terminal representations -- pieces of data that display
1252
+ themselves to a terminal."""
1253
+
1254
+ def __str__(self) -> str:
1255
+ # FYI this is called from pytest-xdist's serialization of exception
1256
+ # information.
1257
+ io = StringIO()
1258
+ tw = TerminalWriter(file=io)
1259
+ self.toterminal(tw)
1260
+ return io.getvalue().strip()
1261
+
1262
+ def __repr__(self) -> str:
1263
+ return f"<{self.__class__} instance at {id(self):0x}>"
1264
+
1265
+ def toterminal(self, tw: TerminalWriter) -> None:
1266
+ raise NotImplementedError()
1267
+
1268
+
1269
+ @dataclasses.dataclass(eq=False)
1270
+ class ExceptionRepr(TerminalRepr):
1271
+ """Base class for exception terminal representations.
1272
+
1273
+ The representation generally contains:
1274
+ - The exception traceback (`reprtraceback`)
1275
+ - The exception message and location (`reprcrash`)
1276
+ - Separated, titled sections with additional data (pytest core doesn't use
1277
+ this currently).
1278
+ """
1279
+
1280
+ reprtraceback: ReprTraceback
1281
+ reprcrash: ReprFileLocation | None
1282
+ sections: list[tuple[str, str, str]] = dataclasses.field(
1283
+ init=False, default_factory=list
1284
+ )
1285
+
1286
+ def addsection(self, name: str, content: str, sep: str = "-") -> None:
1287
+ self.sections.append((name, content, sep))
1288
+
1289
+ def toterminal(self, tw: TerminalWriter) -> None:
1290
+ for name, content, sep in self.sections:
1291
+ tw.sep(sep, name)
1292
+ tw.line(content)
1293
+
1294
+
1295
+ @dataclasses.dataclass(eq=False)
1296
+ class ExceptionChainRepr(ExceptionRepr):
1297
+ """A chain of exceptions, separated by descriptions (e.g. "The above
1298
+ exception was the direct cause of the following exception")."""
1299
+
1300
+ chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]]
1301
+
1302
+ def __init__(
1303
+ self,
1304
+ chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]],
1305
+ ) -> None:
1306
+ # reprcrash and reprtraceback of the outermost (the newest) exception
1307
+ # in the chain.
1308
+ super().__init__(
1309
+ reprtraceback=chain[-1][0],
1310
+ reprcrash=chain[-1][1],
1311
+ )
1312
+ self.chain = chain
1313
+
1314
+ def toterminal(self, tw: TerminalWriter) -> None:
1315
+ for reprtraceback, reprcrash, description in self.chain:
1316
+ reprtraceback.toterminal(tw)
1317
+ if description is not None:
1318
+ tw.line("")
1319
+ tw.line(description, yellow=True)
1320
+ super().toterminal(tw)
1321
+
1322
+
1323
+ @dataclasses.dataclass(eq=False)
1324
+ class ReprExceptionInfo(ExceptionRepr):
1325
+ """A single exception with optional extra details (function arguments,
1326
+ function locals, file location) and possible extra line and sections emitted
1327
+ at the end."""
1328
+
1329
+ def toterminal(self, tw: TerminalWriter) -> None:
1330
+ self.reprtraceback.toterminal(tw)
1331
+ super().toterminal(tw)
1332
+
1333
+
1334
+ @dataclasses.dataclass(eq=False)
1335
+ class ReprTraceback(TerminalRepr):
1336
+ """A traceback with optional extra details (function arguments, function
1337
+ locals, file location) and possible extra line emitted at the end."""
1338
+
1339
+ reprentries: Sequence[ReprEntry | ReprEntryNative]
1340
+ extraline: str | None
1341
+ style: TracebackStyle
1342
+
1343
+ entrysep: ClassVar = "_ "
1344
+
1345
+ def toterminal(self, tw: TerminalWriter) -> None:
1346
+ # The entries might have different styles.
1347
+ for i, entry in enumerate(self.reprentries):
1348
+ if entry.style == "long":
1349
+ tw.line("")
1350
+ entry.toterminal(tw)
1351
+ if i < len(self.reprentries) - 1:
1352
+ next_entry = self.reprentries[i + 1]
1353
+ if entry.style == "long" or (
1354
+ entry.style == "short" and next_entry.style == "long"
1355
+ ):
1356
+ tw.sep(self.entrysep)
1357
+
1358
+ if self.extraline:
1359
+ tw.line(self.extraline)
1360
+
1361
+
1362
+ class ReprTracebackNative(ReprTraceback):
1363
+ """A traceback in native style.
1364
+
1365
+ The lines are emitted as is; uses a single entry for the entire native
1366
+ traceback.
1367
+ """
1368
+
1369
+ def __init__(self, tblines: Sequence[str], *, extraline: str | None = None) -> None:
1370
+ self.reprentries = [ReprEntryNative(tblines)]
1371
+ self.extraline = extraline
1372
+ self.style = "native"
1373
+
1374
+
1375
+ @dataclasses.dataclass(eq=False)
1376
+ class ReprEntryNative(TerminalRepr):
1377
+ """An entry in a traceback in native style.
1378
+
1379
+ Emits the lines as is. The lines are assumed to include the line separators.
1380
+
1381
+ [Note that we currently use a single "entry" for the entire native
1382
+ traceback, so this is a bit misleading, but there's no point trying to parse
1383
+ or split a native traceback.]
1384
+ """
1385
+
1386
+ lines: Sequence[str]
1387
+
1388
+ style: ClassVar[TracebackStyle] = "native"
1389
+
1390
+ def toterminal(self, tw: TerminalWriter) -> None:
1391
+ tw.write("".join(self.lines))
1392
+
1393
+
1394
+ @dataclasses.dataclass(eq=False)
1395
+ class ReprEntry(TerminalRepr):
1396
+ """An entry in a traceback with possible extra details (function arguments,
1397
+ function locals, source snippet, function's file and line)."""
1398
+
1399
+ lines: Sequence[str]
1400
+ reprfuncargs: ReprFuncArgs | None
1401
+ reprlocals: ReprLocals | None
1402
+ reprfileloc: ReprFileLocation | None
1403
+ style: TracebackStyle
1404
+
1405
+ def _write_entry_lines(self, tw: TerminalWriter) -> None:
1406
+ """Write the source code portions of a list of traceback entries with syntax highlighting.
1407
+
1408
+ Usually entries are lines like these:
1409
+
1410
+ " x = 1"
1411
+ "> assert x == 2"
1412
+ "E assert 1 == 2"
1413
+
1414
+ This function takes care of rendering the "source" portions of it (the lines without
1415
+ the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
1416
+ character, as doing so might break line continuations.
1417
+ """
1418
+ if not self.lines:
1419
+ return
1420
+
1421
+ if self.style == "value":
1422
+ # Using tw.write instead of tw.line for testing purposes due to TWMock implementation;
1423
+ # lines written with TWMock.line and TWMock._write_source cannot be distinguished
1424
+ # from each other, whereas lines written with TWMock.write are marked with TWMock.WRITE
1425
+ for line in self.lines:
1426
+ tw.write(line)
1427
+ tw.write("\n")
1428
+ return
1429
+
1430
+ # separate indents and source lines that are not failures: we want to
1431
+ # highlight the code but not the indentation, which may contain markers
1432
+ # such as "> assert 0"
1433
+ fail_marker = f"{ExceptionInfoFormatter.fail_marker} "
1434
+ indent_size = len(fail_marker)
1435
+ indents: list[str] = []
1436
+ source_lines: list[str] = []
1437
+ failure_lines: list[str] = []
1438
+ for index, line in enumerate(self.lines):
1439
+ is_failure_line = line.startswith(fail_marker)
1440
+ if is_failure_line:
1441
+ # from this point on all lines are considered part of the failure
1442
+ failure_lines.extend(self.lines[index:])
1443
+ break
1444
+ else:
1445
+ indents.append(line[:indent_size])
1446
+ source_lines.append(line[indent_size:])
1447
+
1448
+ tw._write_source(source_lines, indents)
1449
+
1450
+ # failure lines are always completely red and bold
1451
+ for line in failure_lines:
1452
+ tw.line(line, bold=True, red=True)
1453
+
1454
+ def toterminal(self, tw: TerminalWriter) -> None:
1455
+ if self.style == "short":
1456
+ if self.reprfileloc:
1457
+ self.reprfileloc.toterminal(tw)
1458
+ self._write_entry_lines(tw)
1459
+ if self.reprlocals:
1460
+ self.reprlocals.toterminal(tw, indent=" " * 8)
1461
+ return
1462
+
1463
+ if self.reprfuncargs:
1464
+ self.reprfuncargs.toterminal(tw)
1465
+
1466
+ self._write_entry_lines(tw)
1467
+
1468
+ if self.reprlocals:
1469
+ tw.line("")
1470
+ self.reprlocals.toterminal(tw)
1471
+ if self.reprfileloc:
1472
+ if self.lines:
1473
+ tw.line("")
1474
+ self.reprfileloc.toterminal(tw)
1475
+
1476
+ def __str__(self) -> str:
1477
+ return "{}\n{}\n{}".format(
1478
+ "\n".join(self.lines), self.reprlocals, self.reprfileloc
1479
+ )
1480
+
1481
+
1482
+ @dataclasses.dataclass(eq=False)
1483
+ class ReprFileLocation(TerminalRepr):
1484
+ """A message at a file location, using the `<path>:<lineno>: <message>`
1485
+ format that most editors understand.
1486
+
1487
+ Only the first line of the message is emitted.
1488
+ """
1489
+
1490
+ path: str
1491
+ lineno: int
1492
+ message: str
1493
+
1494
+ def __post_init__(self) -> None:
1495
+ self.path = str(self.path)
1496
+
1497
+ def toterminal(self, tw: TerminalWriter) -> None:
1498
+ msg = self.message
1499
+ i = msg.find("\n")
1500
+ if i != -1:
1501
+ msg = msg[:i]
1502
+ tw.write(self.path, bold=True, red=True)
1503
+ tw.line(f":{self.lineno}: {msg}")
1504
+
1505
+
1506
+ @dataclasses.dataclass(eq=False)
1507
+ class ReprLocals(TerminalRepr):
1508
+ """Function local variables (pre-formatted)."""
1509
+
1510
+ lines: Sequence[str]
1511
+
1512
+ def toterminal(self, tw: TerminalWriter, indent: str = "") -> None:
1513
+ for line in self.lines:
1514
+ tw.line(indent + line)
1515
+
1516
+
1517
+ @dataclasses.dataclass(eq=False)
1518
+ class ReprFuncArgs(TerminalRepr):
1519
+ """Function arguments - name = value, comma separated."""
1520
+
1521
+ args: Sequence[tuple[str, object]]
1522
+
1523
+ def toterminal(self, tw: TerminalWriter) -> None:
1524
+ if self.args:
1525
+ linesofar = ""
1526
+ for name, value in self.args:
1527
+ ns = f"{name} = {value}"
1528
+ if len(ns) + len(linesofar) + 2 > tw.fullwidth:
1529
+ if linesofar:
1530
+ tw.line(linesofar)
1531
+ linesofar = ns
1532
+ else:
1533
+ if linesofar:
1534
+ linesofar += ", " + ns
1535
+ else:
1536
+ linesofar = ns
1537
+ if linesofar:
1538
+ tw.line(linesofar)
1539
+ tw.line("")
1540
+
1541
+
1542
+ def getfslineno(obj: object) -> tuple[str | Path, int]:
1543
+ """Return source location (path, lineno) for the given object.
1544
+
1545
+ If the source cannot be determined return ("", -1).
1546
+
1547
+ The line number is 0-based.
1548
+ """
1549
+ # xxx let decorators etc specify a sane ordering
1550
+ # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
1551
+ # in 6ec13a2b9. It ("place_as") appears to be something very custom.
1552
+ obj = get_real_func(obj)
1553
+ if hasattr(obj, "place_as"):
1554
+ obj = obj.place_as
1555
+
1556
+ try:
1557
+ code = Code.from_function(obj)
1558
+ except TypeError:
1559
+ try:
1560
+ fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
1561
+ except TypeError:
1562
+ return "", -1
1563
+
1564
+ fspath = (fn and absolutepath(fn)) or ""
1565
+ lineno = -1
1566
+ if fspath:
1567
+ try:
1568
+ _, lineno = findsource(obj)
1569
+ except OSError:
1570
+ pass
1571
+ return fspath, lineno
1572
+
1573
+ return code.path, code.firstlineno
1574
+
1575
+
1576
+ def _byte_offset_to_character_offset(str, offset):
1577
+ """Converts a byte based offset in a string to a code-point."""
1578
+ as_utf8 = str.encode("utf-8")
1579
+ return len(as_utf8[:offset].decode("utf-8", errors="replace"))
1580
+
1581
+
1582
+ # Relative paths that we use to filter traceback entries from appearing to the user;
1583
+ # see filter_traceback.
1584
+ # note: if we need to add more paths than what we have now we should probably use a list
1585
+ # for better maintenance.
1586
+
1587
+ _PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
1588
+ # pluggy is either a package or a single module depending on the version
1589
+ if _PLUGGY_DIR.name == "__init__.py":
1590
+ _PLUGGY_DIR = _PLUGGY_DIR.parent
1591
+ _PYTEST_DIR = Path(_pytest.__file__).parent
1592
+
1593
+
1594
+ def filter_traceback(entry: TracebackEntry) -> bool:
1595
+ """Return True if a TracebackEntry instance should be included in tracebacks.
1596
+
1597
+ We hide traceback entries of:
1598
+
1599
+ * dynamically generated code (no code to show up for it);
1600
+ * internal traceback from pytest or its internal libraries, py and pluggy.
1601
+ """
1602
+ # entry.path might sometimes return a str object when the entry
1603
+ # points to dynamically generated code.
1604
+ # See https://bitbucket.org/pytest-dev/py/issues/71.
1605
+ raw_filename = entry.frame.code.raw.co_filename
1606
+ is_generated = "<" in raw_filename and ">" in raw_filename
1607
+ if is_generated:
1608
+ return False
1609
+
1610
+ # entry.path might point to a non-existing file, in which case it will
1611
+ # also return a str object. See #1133.
1612
+ p = Path(entry.path)
1613
+
1614
+ parents = p.parents
1615
+ if _PLUGGY_DIR in parents:
1616
+ return False
1617
+ if _PYTEST_DIR in parents:
1618
+ return False
1619
+
1620
+ return True
1621
+
1622
+
1623
+ def filter_excinfo_traceback(
1624
+ tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException]
1625
+ ) -> Traceback:
1626
+ """Filter the exception traceback in ``excinfo`` according to ``tbfilter``."""
1627
+ if callable(tbfilter):
1628
+ return tbfilter(excinfo)
1629
+ elif tbfilter:
1630
+ return excinfo.traceback.filter(excinfo)
1631
+ else:
1632
+ return excinfo.traceback
.venv/lib/python3.14/site-packages/_pytest/_code/source.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from bisect import bisect_right
6
+ from collections.abc import Iterable
7
+ from collections.abc import Iterator
8
+ import inspect
9
+ import textwrap
10
+ import tokenize
11
+ import types
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
+ self.raw_lines: list[str] = []
26
+ elif isinstance(obj, Source):
27
+ self.lines = obj.lines
28
+ self.raw_lines = obj.raw_lines
29
+ elif isinstance(obj, tuple | list):
30
+ self.lines = deindent(x.rstrip("\n") for x in obj)
31
+ self.raw_lines = list(x.rstrip("\n") for x in obj)
32
+ elif isinstance(obj, str):
33
+ self.lines = deindent(obj.split("\n"))
34
+ self.raw_lines = obj.split("\n")
35
+ else:
36
+ try:
37
+ rawcode = getrawcode(obj)
38
+ src = inspect.getsource(rawcode)
39
+ except TypeError:
40
+ src = inspect.getsource(obj) # type: ignore[arg-type]
41
+ self.lines = deindent(src.split("\n"))
42
+ self.raw_lines = src.split("\n")
43
+
44
+ def __eq__(self, other: object) -> bool:
45
+ if not isinstance(other, Source):
46
+ return NotImplemented
47
+ return self.lines == other.lines
48
+
49
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
50
+ __hash__ = None # type: ignore
51
+
52
+ @overload
53
+ def __getitem__(self, key: int) -> str: ...
54
+
55
+ @overload
56
+ def __getitem__(self, key: slice) -> Source: ...
57
+
58
+ def __getitem__(self, key: int | slice) -> str | Source:
59
+ if isinstance(key, int):
60
+ return self.lines[key]
61
+ else:
62
+ if key.step not in (None, 1):
63
+ raise IndexError("cannot slice a Source with a step")
64
+ newsource = Source()
65
+ newsource.lines = self.lines[key.start : key.stop]
66
+ newsource.raw_lines = self.raw_lines[key.start : key.stop]
67
+ return newsource
68
+
69
+ def __iter__(self) -> Iterator[str]:
70
+ return iter(self.lines)
71
+
72
+ def __len__(self) -> int:
73
+ return len(self.lines)
74
+
75
+ def strip(self) -> Source:
76
+ """Return new Source object with trailing and leading blank lines removed."""
77
+ start, end = 0, len(self)
78
+ while start < end and not self.lines[start].strip():
79
+ start += 1
80
+ while end > start and not self.lines[end - 1].strip():
81
+ end -= 1
82
+ source = Source()
83
+ source.raw_lines = self.raw_lines
84
+ source.lines[:] = self.lines[start:end]
85
+ return source
86
+
87
+ def indent(self, indent: str = " " * 4) -> Source:
88
+ """Return a copy of the source object with all lines indented by the
89
+ given indent-string."""
90
+ newsource = Source()
91
+ newsource.raw_lines = self.raw_lines
92
+ newsource.lines = [(indent + line) for line in self.lines]
93
+ return newsource
94
+
95
+ def getstatement(self, lineno: int) -> Source:
96
+ """Return Source statement which contains the given linenumber
97
+ (counted from 0)."""
98
+ start, end = self.getstatementrange(lineno)
99
+ return self[start:end]
100
+
101
+ def getstatementrange(self, lineno: int) -> tuple[int, int]:
102
+ """Return (start, end) tuple which spans the minimal statement region
103
+ which containing the given lineno."""
104
+ if not (0 <= lineno < len(self)):
105
+ raise IndexError("lineno out of range")
106
+ _ast, start, end = getstatementrange_ast(lineno, self)
107
+ return start, end
108
+
109
+ def deindent(self) -> Source:
110
+ """Return a new Source object deindented."""
111
+ newsource = Source()
112
+ newsource.lines[:] = deindent(self.lines)
113
+ newsource.raw_lines = self.raw_lines
114
+ return newsource
115
+
116
+ def __str__(self) -> str:
117
+ return "\n".join(self.lines)
118
+
119
+
120
+ #
121
+ # helper functions
122
+ #
123
+
124
+
125
+ def findsource(obj) -> tuple[Source | None, int]:
126
+ try:
127
+ sourcelines, lineno = inspect.findsource(obj)
128
+ except Exception:
129
+ return None, -1
130
+ source = Source()
131
+ source.lines = [line.rstrip() for line in sourcelines]
132
+ source.raw_lines = sourcelines
133
+ return source, lineno
134
+
135
+
136
+ def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
137
+ """Return code object for given function."""
138
+ try:
139
+ return obj.__code__ # type: ignore[attr-defined,no-any-return]
140
+ except AttributeError:
141
+ pass
142
+ if trycall:
143
+ call = getattr(obj, "__call__", None)
144
+ if call and not isinstance(obj, type):
145
+ return getrawcode(call, trycall=False)
146
+ raise TypeError(f"could not get code object for {obj!r}")
147
+
148
+
149
+ def deindent(lines: Iterable[str]) -> list[str]:
150
+ return textwrap.dedent("\n".join(lines)).splitlines()
151
+
152
+
153
+ def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None]:
154
+ # Flatten all statements and except handlers into one lineno-list.
155
+ # AST's line numbers start indexing at 1.
156
+ values: list[int] = []
157
+ for x in ast.walk(node):
158
+ if isinstance(x, ast.stmt | ast.ExceptHandler):
159
+ # The lineno points to the class/def, so need to include the decorators.
160
+ if isinstance(x, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef):
161
+ for d in x.decorator_list:
162
+ values.append(d.lineno - 1)
163
+ values.append(x.lineno - 1)
164
+ for name in ("finalbody", "orelse"):
165
+ val: list[ast.stmt] | None = getattr(x, name, None)
166
+ if val:
167
+ # Treat the finally/orelse part as its own statement.
168
+ values.append(val[0].lineno - 1 - 1)
169
+ values.sort()
170
+ insert_index = bisect_right(values, lineno)
171
+ if insert_index == 0:
172
+ return 0, None
173
+ start = values[insert_index - 1]
174
+ if insert_index >= len(values):
175
+ end = None
176
+ else:
177
+ end = values[insert_index]
178
+ return start, end
179
+
180
+
181
+ def getstatementrange_ast(
182
+ lineno: int,
183
+ source: Source,
184
+ assertion: bool = False,
185
+ astnode: ast.AST | None = None,
186
+ ) -> tuple[ast.AST, int, int]:
187
+ if astnode is None:
188
+ content = str(source)
189
+ # See #4260:
190
+ # Don't produce duplicate warnings when compiling source to find AST.
191
+ with warnings.catch_warnings():
192
+ warnings.simplefilter("ignore")
193
+ astnode = ast.parse(content, "source", "exec")
194
+
195
+ start, end = get_statement_startend2(lineno, astnode)
196
+ # We need to correct the end:
197
+ # - ast-parsing strips comments
198
+ # - there might be empty lines
199
+ # - we might have lesser indented code blocks at the end
200
+ if end is None:
201
+ end = len(source.lines)
202
+
203
+ if end > start + 1:
204
+ # Make sure we don't span differently indented code blocks
205
+ # by using the BlockFinder helper used which inspect.getsource() uses itself.
206
+ block_finder = inspect.BlockFinder()
207
+ # If we start with an indented line, put blockfinder to "started" mode.
208
+ block_finder.started = (
209
+ bool(source.lines[start]) and source.lines[start][0].isspace()
210
+ )
211
+ it = ((x + "\n") for x in source.lines[start:end])
212
+ try:
213
+ for tok in tokenize.generate_tokens(lambda: next(it)):
214
+ block_finder.tokeneater(*tok)
215
+ except (inspect.EndOfBlock, IndentationError):
216
+ end = block_finder.last + start
217
+ except Exception:
218
+ pass
219
+
220
+ # The end might still point to a comment or empty line, correct it.
221
+ end = min(end, len(source.lines))
222
+ while end:
223
+ line = source.lines[end - 1].lstrip()
224
+ if line.startswith("#") or not line:
225
+ end -= 1
226
+ else:
227
+ break
228
+ return astnode, start, end
.venv/lib/python3.14/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.14/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
+ from collections.abc import Callable
20
+ from collections.abc import Iterator
21
+ import dataclasses as _dataclasses
22
+ from io import StringIO as _StringIO
23
+ import re
24
+ import types as _types
25
+ from typing import Any
26
+ from typing import IO
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)
115
+ and not isinstance(object, type)
116
+ and object.__dataclass_params__.repr # type:ignore[attr-defined]
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)
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 = object.items()
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(f"maxlen={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 object.items():
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.14/site-packages/_pytest/_io/saferepr.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from itertools import islice
4
+ import pprint
5
+ import reprlib
6
+
7
+
8
+ def _try_repr_or_str(obj: object) -> str:
9
+ try:
10
+ return repr(obj)
11
+ except (KeyboardInterrupt, SystemExit):
12
+ raise
13
+ except BaseException:
14
+ return f'{type(obj).__name__}("{obj}")'
15
+
16
+
17
+ def _format_repr_exception(exc: BaseException, obj: object) -> str:
18
+ try:
19
+ exc_info = _try_repr_or_str(exc)
20
+ except (KeyboardInterrupt, SystemExit):
21
+ raise
22
+ except BaseException as inner_exc:
23
+ exc_info = f"unpresentable exception ({_try_repr_or_str(inner_exc)})"
24
+ return (
25
+ f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
26
+ )
27
+
28
+
29
+ def _ellipsize(s: str, maxsize: int) -> str:
30
+ if len(s) > maxsize:
31
+ i = max(0, (maxsize - 3) // 2)
32
+ j = max(0, maxsize - 3 - i)
33
+ return s[:i] + "..." + s[len(s) - j :]
34
+ return s
35
+
36
+
37
+ class SafeRepr(reprlib.Repr):
38
+ """
39
+ repr.Repr that limits the resulting size of repr() and includes
40
+ information on exceptions raised during the call.
41
+ """
42
+
43
+ def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None:
44
+ """
45
+ :param maxsize:
46
+ If not None, will truncate the resulting repr to that specific size, using ellipsis
47
+ somewhere in the middle to hide the extra text.
48
+ If None, will not impose any size limits on the returning repr.
49
+ """
50
+ super().__init__()
51
+ # ``maxstring`` is used by the superclass, and needs to be an int; using a
52
+ # very large number in case maxsize is None, meaning we want to disable
53
+ # truncation.
54
+ self.maxstring = maxsize if maxsize is not None else 1_000_000_000
55
+ self.maxsize = maxsize
56
+ self.use_ascii = use_ascii
57
+
58
+ def repr(self, x: object) -> str:
59
+ try:
60
+ if self.use_ascii:
61
+ s = ascii(x)
62
+ else:
63
+ s = super().repr(x)
64
+ except (KeyboardInterrupt, SystemExit):
65
+ raise
66
+ except BaseException as exc:
67
+ s = _format_repr_exception(exc, x)
68
+ if self.maxsize is not None:
69
+ s = _ellipsize(s, self.maxsize)
70
+ return s
71
+
72
+ def repr_instance(self, x: object, level: int) -> str:
73
+ try:
74
+ s = repr(x)
75
+ except (KeyboardInterrupt, SystemExit):
76
+ raise
77
+ except BaseException as exc:
78
+ s = _format_repr_exception(exc, x)
79
+ if self.maxsize is not None:
80
+ s = _ellipsize(s, self.maxsize)
81
+
82
+ return s
83
+
84
+ def repr_dict(self, x: dict[object, object], level: int) -> str:
85
+ """Represent a dict while preserving its insertion order.
86
+
87
+ Differs from ``reprlib.Repr.repr_dict`` by iterating directly over ``x``
88
+ rather than using the stdlib's sorting helper.
89
+ """
90
+ fillvalue = "..."
91
+ n = len(x)
92
+ if n == 0:
93
+ return "{}"
94
+ if level <= 0:
95
+ return "{" + fillvalue + "}"
96
+ newlevel = level - 1
97
+ repr1 = self.repr1
98
+ pieces = []
99
+ for key in islice(x, self.maxdict):
100
+ keyrepr = repr1(key, newlevel)
101
+ valrepr = repr1(x[key], newlevel)
102
+ pieces.append(f"{keyrepr}: {valrepr}")
103
+ if n > self.maxdict:
104
+ pieces.append(fillvalue)
105
+ return "{" + ", ".join(pieces) + "}"
106
+
107
+
108
+ def safeformat(obj: object) -> str:
109
+ """Return a pretty printed string for the given object.
110
+
111
+ Failing __repr__ functions of user instances will be represented
112
+ with a short exception info.
113
+ """
114
+ try:
115
+ return pprint.pformat(obj)
116
+ except Exception as exc:
117
+ return _format_repr_exception(exc, obj)
118
+
119
+
120
+ # Maximum size of overall repr of objects to display during assertion errors.
121
+ DEFAULT_REPR_MAX_SIZE = 240
122
+
123
+
124
+ def saferepr(
125
+ obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
126
+ ) -> str:
127
+ """Return a size-limited safe repr-string for the given object.
128
+
129
+ Failing __repr__ functions of user instances will be represented
130
+ with a short exception info and 'saferepr' generally takes
131
+ care to never raise exceptions itself.
132
+
133
+ This function is a wrapper around the Repr/reprlib functionality of the
134
+ stdlib.
135
+ """
136
+ return SafeRepr(maxsize, use_ascii).repr(obj)
137
+
138
+
139
+ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
140
+ """Return an unlimited-size safe repr-string for the given object.
141
+
142
+ As with saferepr, failing __repr__ functions of user instances
143
+ will be represented with a short exception info.
144
+
145
+ This function is a wrapper around simple repr.
146
+
147
+ Note: a cleaner solution would be to alter ``saferepr``this way
148
+ when maxsize=None, but that might affect some other code.
149
+ """
150
+ try:
151
+ if use_ascii:
152
+ return ascii(obj)
153
+ return repr(obj)
154
+ except Exception as exc:
155
+ return _format_repr_exception(exc, obj)
.venv/lib/python3.14/site-packages/_pytest/_io/terminalwriter.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper functions for writing to terminals and files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ import os
7
+ import shutil
8
+ import sys
9
+ from typing import final
10
+ from typing import Literal
11
+ from typing import TextIO
12
+
13
+ import pygments
14
+ from pygments.formatters.terminal import TerminalFormatter
15
+ from pygments.lexer import Lexer
16
+ from pygments.lexers.diff import DiffLexer
17
+ from pygments.lexers.python import PythonLexer
18
+
19
+ from ..compat import assert_never
20
+ from .wcwidth import wcswidth
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
+ self.write_raw(msg, flush=flush)
165
+
166
+ def write_raw(self, msg: str, *, flush: bool = False) -> None:
167
+ try:
168
+ self._file.write(msg)
169
+ except UnicodeEncodeError:
170
+ # Some environments don't support printing general Unicode
171
+ # strings, due to misconfiguration or otherwise; in that case,
172
+ # print the string escaped to ASCII.
173
+ # When the Unicode situation improves we should consider
174
+ # letting the error propagate instead of masking it (see #7475
175
+ # for one brief attempt).
176
+ msg = msg.encode("unicode-escape").decode("ascii")
177
+ self._file.write(msg)
178
+
179
+ if flush:
180
+ self.flush()
181
+
182
+ def line(self, s: str = "", **markup: bool) -> None:
183
+ self.write(s, **markup)
184
+ self.write("\n")
185
+
186
+ def flush(self) -> None:
187
+ self._file.flush()
188
+
189
+ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None:
190
+ """Write lines of source code possibly highlighted.
191
+
192
+ Keeping this private for now because the API is clunky. We should discuss how
193
+ to evolve the terminal writer so we can have more precise color support, for example
194
+ being able to write part of a line in one color and the rest in another, and so on.
195
+ """
196
+ if indents and len(indents) != len(lines):
197
+ raise ValueError(
198
+ f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
199
+ )
200
+ if not indents:
201
+ indents = [""] * len(lines)
202
+ source = "\n".join(lines)
203
+ new_lines = self._highlight(source).splitlines()
204
+ # Would be better to strict=True but that fails some CI jobs.
205
+ for indent, new_line in zip(indents, new_lines, strict=False):
206
+ self.line(indent + new_line)
207
+
208
+ def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer:
209
+ if lexer == "python":
210
+ return PythonLexer()
211
+ elif lexer == "diff":
212
+ return DiffLexer()
213
+ else:
214
+ assert_never(lexer)
215
+
216
+ def _get_pygments_formatter(self) -> TerminalFormatter:
217
+ from _pytest.config.exceptions import UsageError
218
+
219
+ theme = os.getenv("PYTEST_THEME")
220
+ theme_mode = os.getenv("PYTEST_THEME_MODE", "dark")
221
+
222
+ try:
223
+ return TerminalFormatter(bg=theme_mode, style=theme)
224
+ except pygments.util.ClassNotFound as e:
225
+ raise UsageError(
226
+ f"PYTEST_THEME environment variable has an invalid value: '{theme}'. "
227
+ "Hint: See available pygments styles with `pygmentize -L styles`."
228
+ ) from e
229
+ except pygments.util.OptionError as e:
230
+ raise UsageError(
231
+ f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. "
232
+ "The allowed values are 'dark' (default) and 'light'."
233
+ ) from e
234
+
235
+ def _highlight(
236
+ self, source: str, lexer: Literal["diff", "python"] = "python"
237
+ ) -> str:
238
+ """Highlight the given source if we have markup support."""
239
+ if not source or not self.hasmarkup or not self.code_highlight:
240
+ return source
241
+
242
+ pygments_lexer = self._get_pygments_lexer(lexer)
243
+ pygments_formatter = self._get_pygments_formatter()
244
+
245
+ highlighted: str = pygments.highlight(
246
+ source, pygments_lexer, pygments_formatter
247
+ )
248
+ # pygments terminal formatter may add a newline when there wasn't one.
249
+ # We don't want this, remove.
250
+ if highlighted[-1] == "\n" and source[-1] != "\n":
251
+ highlighted = highlighted[:-1]
252
+
253
+ # Some lexers will not set the initial color explicitly
254
+ # which may lead to the previous color being propagated to the
255
+ # start of the expression, so reset first.
256
+ highlighted = "\x1b[0m" + highlighted
257
+
258
+ return highlighted
.venv/lib/python3.14/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.14/site-packages/_pytest/_py/__init__.py ADDED
File without changes
.venv/lib/python3.14/site-packages/_pytest/_py/error.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """create errno-specific classes for IO or os calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ import errno
7
+ import os
8
+ import sys
9
+ from typing import TYPE_CHECKING
10
+ from typing import TypeVar
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from typing_extensions import ParamSpec
15
+
16
+ P = ParamSpec("P")
17
+
18
+ R = TypeVar("R")
19
+
20
+
21
+ class Error(EnvironmentError):
22
+ def __repr__(self) -> str:
23
+ return "{}.{} {!r}: {} ".format(
24
+ self.__class__.__module__,
25
+ self.__class__.__name__,
26
+ self.__class__.__doc__,
27
+ " ".join(map(str, self.args)),
28
+ # repr(self.args)
29
+ )
30
+
31
+ def __str__(self) -> str:
32
+ s = "[{}]: {}".format(
33
+ self.__class__.__doc__,
34
+ " ".join(map(str, self.args)),
35
+ )
36
+ return s
37
+
38
+
39
+ _winerrnomap = {
40
+ 2: errno.ENOENT,
41
+ 3: errno.ENOENT,
42
+ 17: errno.EEXIST,
43
+ 18: errno.EXDEV,
44
+ 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable
45
+ 22: errno.ENOTDIR,
46
+ 20: errno.ENOTDIR,
47
+ 267: errno.ENOTDIR,
48
+ 5: errno.EACCES, # anything better?
49
+ }
50
+
51
+
52
+ class ErrorMaker:
53
+ """lazily provides Exception classes for each possible POSIX errno
54
+ (as defined per the 'errno' module). All such instances
55
+ subclass EnvironmentError.
56
+ """
57
+
58
+ _errno2class: dict[int, type[Error]] = {}
59
+
60
+ def __getattr__(self, name: str) -> type[Error]:
61
+ if name[0] == "_":
62
+ raise AttributeError(name)
63
+ eno = getattr(errno, name)
64
+ cls = self._geterrnoclass(eno)
65
+ setattr(self, name, cls)
66
+ return cls
67
+
68
+ def _geterrnoclass(self, eno: int) -> type[Error]:
69
+ try:
70
+ return self._errno2class[eno]
71
+ except KeyError:
72
+ clsname = errno.errorcode.get(eno, f"UnknownErrno{eno}")
73
+ errorcls = type(
74
+ clsname,
75
+ (Error,),
76
+ {"__module__": "py.error", "__doc__": os.strerror(eno)},
77
+ )
78
+ self._errno2class[eno] = errorcls
79
+ return errorcls
80
+
81
+ def checked_call(
82
+ self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
83
+ ) -> R:
84
+ """Call a function and raise an errno-exception if applicable."""
85
+ __tracebackhide__ = True
86
+ try:
87
+ return func(*args, **kwargs)
88
+ except Error:
89
+ raise
90
+ except OSError as value:
91
+ if not hasattr(value, "errno"):
92
+ raise
93
+ if sys.platform == "win32":
94
+ try:
95
+ # error: Invalid index type "Optional[int]" for "dict[int, int]"; expected type "int" [index]
96
+ # OK to ignore because we catch the KeyError below.
97
+ cls = self._geterrnoclass(_winerrnomap[value.errno]) # type:ignore[index]
98
+ except KeyError:
99
+ raise value
100
+ else:
101
+ # we are not on Windows, or we got a proper OSError
102
+ if value.errno is None:
103
+ cls = type(
104
+ "UnknownErrnoNone",
105
+ (Error,),
106
+ {"__module__": "py.error", "__doc__": None},
107
+ )
108
+ else:
109
+ cls = self._geterrnoclass(value.errno)
110
+
111
+ raise cls(f"{func.__name__}{args!r}")
112
+
113
+
114
+ _error_maker = ErrorMaker()
115
+ checked_call = _error_maker.checked_call
116
+
117
+
118
+ def __getattr__(attr: str) -> type[Error]:
119
+ return getattr(_error_maker, attr) # type: ignore[no-any-return]
.venv/lib/python3.14/site-packages/_pytest/_py/path.py ADDED
@@ -0,0 +1,1475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """local path implementation."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import atexit
7
+ from collections.abc import Callable
8
+ from contextlib import contextmanager
9
+ import fnmatch
10
+ import importlib.util
11
+ import io
12
+ import os
13
+ from os.path import abspath
14
+ from os.path import dirname
15
+ from os.path import exists
16
+ from os.path import isabs
17
+ from os.path import isdir
18
+ from os.path import isfile
19
+ from os.path import islink
20
+ from os.path import normpath
21
+ import posixpath
22
+ from stat import S_ISDIR
23
+ from stat import S_ISLNK
24
+ from stat import S_ISREG
25
+ import sys
26
+ from typing import Any
27
+ from typing import cast
28
+ from typing import Literal
29
+ from typing import overload
30
+ from typing import TYPE_CHECKING
31
+ import uuid
32
+ import warnings
33
+
34
+ from . import error
35
+
36
+
37
+ # Moved from local.py.
38
+ iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
39
+
40
+
41
+ class Checkers:
42
+ _depend_on_existence = "exists", "link", "dir", "file"
43
+
44
+ def __init__(self, path):
45
+ self.path = path
46
+
47
+ def dotfile(self):
48
+ return self.path.basename.startswith(".")
49
+
50
+ def ext(self, arg):
51
+ if not arg.startswith("."):
52
+ arg = "." + arg
53
+ return self.path.ext == arg
54
+
55
+ def basename(self, arg):
56
+ return self.path.basename == arg
57
+
58
+ def basestarts(self, arg):
59
+ return self.path.basename.startswith(arg)
60
+
61
+ def relto(self, arg):
62
+ return self.path.relto(arg)
63
+
64
+ def fnmatch(self, arg):
65
+ return self.path.fnmatch(arg)
66
+
67
+ def endswith(self, arg):
68
+ return str(self.path).endswith(arg)
69
+
70
+ def _evaluate(self, kw):
71
+ from .._code.source import getrawcode
72
+
73
+ for name, value in kw.items():
74
+ invert = False
75
+ meth = None
76
+ try:
77
+ meth = getattr(self, name)
78
+ except AttributeError:
79
+ if name[:3] == "not":
80
+ invert = True
81
+ try:
82
+ meth = getattr(self, name[3:])
83
+ except AttributeError:
84
+ pass
85
+ if meth is None:
86
+ raise TypeError(f"no {name!r} checker available for {self.path!r}")
87
+ try:
88
+ if getrawcode(meth).co_argcount > 1:
89
+ if (not meth(value)) ^ invert:
90
+ return False
91
+ else:
92
+ if bool(value) ^ bool(meth()) ^ invert:
93
+ return False
94
+ except (error.ENOENT, error.ENOTDIR, error.EBUSY):
95
+ # EBUSY feels not entirely correct,
96
+ # but its kind of necessary since ENOMEDIUM
97
+ # is not accessible in python
98
+ for name in self._depend_on_existence:
99
+ if name in kw:
100
+ if kw.get(name):
101
+ return False
102
+ name = "not" + name
103
+ if name in kw:
104
+ if not kw.get(name):
105
+ return False
106
+ return True
107
+
108
+ _statcache: Stat
109
+
110
+ def _stat(self) -> Stat:
111
+ try:
112
+ return self._statcache
113
+ except AttributeError:
114
+ try:
115
+ self._statcache = self.path.stat()
116
+ except error.ELOOP:
117
+ self._statcache = self.path.lstat()
118
+ return self._statcache
119
+
120
+ def dir(self):
121
+ return S_ISDIR(self._stat().mode)
122
+
123
+ def file(self):
124
+ return S_ISREG(self._stat().mode)
125
+
126
+ def exists(self):
127
+ return self._stat()
128
+
129
+ def link(self):
130
+ st = self.path.lstat()
131
+ return S_ISLNK(st.mode)
132
+
133
+
134
+ class NeverRaised(Exception):
135
+ pass
136
+
137
+
138
+ class Visitor:
139
+ def __init__(self, fil, rec, ignore, bf, sort):
140
+ if isinstance(fil, (str, bytes)):
141
+ fil = FNMatcher(fil)
142
+ if isinstance(rec, str):
143
+ self.rec: Callable[[LocalPath], bool] = FNMatcher(rec)
144
+ elif not hasattr(rec, "__call__") and rec:
145
+ self.rec = lambda path: True
146
+ else:
147
+ self.rec = rec
148
+ self.fil = fil
149
+ self.ignore = ignore
150
+ self.breadthfirst = bf
151
+ self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
152
+
153
+ def gen(self, path):
154
+ try:
155
+ entries = path.listdir()
156
+ except self.ignore:
157
+ return
158
+ rec = self.rec
159
+ dirs = self.optsort(
160
+ [p for p in entries if p.check(dir=1) and (rec is None or rec(p))]
161
+ )
162
+ if not self.breadthfirst:
163
+ for subdir in dirs:
164
+ yield from self.gen(subdir)
165
+ for p in self.optsort(entries):
166
+ if self.fil is None or self.fil(p):
167
+ yield p
168
+ if self.breadthfirst:
169
+ for subdir in dirs:
170
+ yield from self.gen(subdir)
171
+
172
+
173
+ class FNMatcher:
174
+ def __init__(self, pattern):
175
+ self.pattern = pattern
176
+
177
+ def __call__(self, path):
178
+ pattern = self.pattern
179
+
180
+ if (
181
+ pattern.find(path.sep) == -1
182
+ and iswin32
183
+ and pattern.find(posixpath.sep) != -1
184
+ ):
185
+ # Running on Windows, the pattern has no Windows path separators,
186
+ # and the pattern has one or more Posix path separators. Replace
187
+ # the Posix path separators with the Windows path separator.
188
+ pattern = pattern.replace(posixpath.sep, path.sep)
189
+
190
+ if pattern.find(path.sep) == -1:
191
+ name = path.basename
192
+ else:
193
+ name = str(path) # path.strpath # XXX svn?
194
+ if not os.path.isabs(pattern):
195
+ pattern = "*" + path.sep + pattern
196
+ return fnmatch.fnmatch(name, pattern)
197
+
198
+
199
+ def map_as_list(func, iter):
200
+ return list(map(func, iter))
201
+
202
+
203
+ class Stat:
204
+ if TYPE_CHECKING:
205
+
206
+ @property
207
+ def size(self) -> int: ...
208
+
209
+ @property
210
+ def mtime(self) -> float: ...
211
+
212
+ def __getattr__(self, name: str) -> Any:
213
+ return getattr(self._osstatresult, "st_" + name)
214
+
215
+ def __init__(self, path, osstatresult):
216
+ self.path = path
217
+ self._osstatresult = osstatresult
218
+
219
+ @property
220
+ def owner(self):
221
+ if iswin32:
222
+ raise NotImplementedError("XXX win32")
223
+ import pwd
224
+
225
+ entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore]
226
+ return entry[0]
227
+
228
+ @property
229
+ def group(self):
230
+ """Return group name of file."""
231
+ if iswin32:
232
+ raise NotImplementedError("XXX win32")
233
+ import grp
234
+
235
+ entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore]
236
+ return entry[0]
237
+
238
+ def isdir(self):
239
+ return S_ISDIR(self._osstatresult.st_mode)
240
+
241
+ def isfile(self):
242
+ return S_ISREG(self._osstatresult.st_mode)
243
+
244
+ def islink(self):
245
+ self.path.lstat()
246
+ return S_ISLNK(self._osstatresult.st_mode)
247
+
248
+
249
+ def getuserid(user):
250
+ import pwd
251
+
252
+ if not isinstance(user, int):
253
+ user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore]
254
+ return user
255
+
256
+
257
+ def getgroupid(group):
258
+ import grp
259
+
260
+ if not isinstance(group, int):
261
+ group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore]
262
+ return group
263
+
264
+
265
+ class LocalPath:
266
+ """Object oriented interface to os.path and other local filesystem
267
+ related information.
268
+ """
269
+
270
+ class ImportMismatchError(ImportError):
271
+ """raised on pyimport() if there is a mismatch of __file__'s"""
272
+
273
+ sep = os.sep
274
+
275
+ def __init__(self, path=None, expanduser=False):
276
+ """Initialize and return a local Path instance.
277
+
278
+ Path can be relative to the current directory.
279
+ If path is None it defaults to the current working directory.
280
+ If expanduser is True, tilde-expansion is performed.
281
+ Note that Path instances always carry an absolute path.
282
+ Note also that passing in a local path object will simply return
283
+ the exact same path object. Use new() to get a new copy.
284
+ """
285
+ if path is None:
286
+ self.strpath = error.checked_call(os.getcwd)
287
+ else:
288
+ try:
289
+ path = os.fspath(path)
290
+ except TypeError:
291
+ raise ValueError(
292
+ "can only pass None, Path instances "
293
+ "or non-empty strings to LocalPath"
294
+ )
295
+ if expanduser:
296
+ path = os.path.expanduser(path)
297
+ self.strpath = abspath(path)
298
+
299
+ if sys.platform != "win32":
300
+
301
+ def chown(self, user, group, rec=0):
302
+ """Change ownership to the given user and group.
303
+ user and group may be specified by a number or
304
+ by a name. if rec is True change ownership
305
+ recursively.
306
+ """
307
+ uid = getuserid(user)
308
+ gid = getgroupid(group)
309
+ if rec:
310
+ for x in self.visit(rec=lambda x: x.check(link=0)):
311
+ if x.check(link=0):
312
+ error.checked_call(os.chown, str(x), uid, gid)
313
+ error.checked_call(os.chown, str(self), uid, gid)
314
+
315
+ def readlink(self) -> str:
316
+ """Return value of a symbolic link."""
317
+ # https://github.com/python/mypy/issues/12278
318
+ return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore]
319
+
320
+ def mklinkto(self, oldname):
321
+ """Posix style hard link to another name."""
322
+ error.checked_call(os.link, str(oldname), str(self))
323
+
324
+ def mksymlinkto(self, value, absolute=1):
325
+ """Create a symbolic link with the given value (pointing to another name)."""
326
+ if absolute:
327
+ error.checked_call(os.symlink, str(value), self.strpath)
328
+ else:
329
+ base = self.common(value)
330
+ # with posix local paths '/' is always a common base
331
+ relsource = self.__class__(value).relto(base)
332
+ reldest = self.relto(base)
333
+ n = reldest.count(self.sep)
334
+ target = self.sep.join(("..",) * n + (relsource,))
335
+ error.checked_call(os.symlink, target, self.strpath)
336
+
337
+ def __div__(self, other):
338
+ return self.join(os.fspath(other))
339
+
340
+ __truediv__ = __div__ # py3k
341
+
342
+ @property
343
+ def basename(self):
344
+ """Basename part of path."""
345
+ return self._getbyspec("basename")[0]
346
+
347
+ @property
348
+ def dirname(self):
349
+ """Dirname part of path."""
350
+ return self._getbyspec("dirname")[0]
351
+
352
+ @property
353
+ def purebasename(self):
354
+ """Pure base name of the path."""
355
+ return self._getbyspec("purebasename")[0]
356
+
357
+ @property
358
+ def ext(self):
359
+ """Extension of the path (including the '.')."""
360
+ return self._getbyspec("ext")[0]
361
+
362
+ def read_binary(self):
363
+ """Read and return a bytestring from reading the path."""
364
+ with self.open("rb") as f:
365
+ return f.read()
366
+
367
+ def read_text(self, encoding):
368
+ """Read and return a Unicode string from reading the path."""
369
+ with self.open("r", encoding=encoding) as f:
370
+ return f.read()
371
+
372
+ def read(self, mode="r"):
373
+ """Read and return a bytestring from reading the path."""
374
+ with self.open(mode) as f:
375
+ return f.read()
376
+
377
+ def readlines(self, cr=1):
378
+ """Read and return a list of lines from the path. if cr is False, the
379
+ newline will be removed from the end of each line."""
380
+ mode = "r"
381
+
382
+ if not cr:
383
+ content = self.read(mode)
384
+ return content.split("\n")
385
+ else:
386
+ f = self.open(mode)
387
+ try:
388
+ return f.readlines()
389
+ finally:
390
+ f.close()
391
+
392
+ def load(self):
393
+ """(deprecated) return object unpickled from self.read()"""
394
+ f = self.open("rb")
395
+ try:
396
+ import pickle
397
+
398
+ return error.checked_call(pickle.load, f)
399
+ finally:
400
+ f.close()
401
+
402
+ def move(self, target):
403
+ """Move this path to target."""
404
+ if target.relto(self):
405
+ raise error.EINVAL(target, "cannot move path into a subdirectory of itself")
406
+ try:
407
+ self.rename(target)
408
+ except error.EXDEV: # invalid cross-device link
409
+ self.copy(target)
410
+ self.remove()
411
+
412
+ def fnmatch(self, pattern):
413
+ """Return true if the basename/fullname matches the glob-'pattern'.
414
+
415
+ valid pattern characters::
416
+
417
+ * matches everything
418
+ ? matches any single character
419
+ [seq] matches any character in seq
420
+ [!seq] matches any char not in seq
421
+
422
+ If the pattern contains a path-separator then the full path
423
+ is used for pattern matching and a '*' is prepended to the
424
+ pattern.
425
+
426
+ if the pattern doesn't contain a path-separator the pattern
427
+ is only matched against the basename.
428
+ """
429
+ return FNMatcher(pattern)(self)
430
+
431
+ def relto(self, relpath):
432
+ """Return a string which is the relative part of the path
433
+ to the given 'relpath'.
434
+ """
435
+ if not isinstance(relpath, str | LocalPath):
436
+ raise TypeError(f"{relpath!r}: not a string or path object")
437
+ strrelpath = str(relpath)
438
+ if strrelpath and strrelpath[-1] != self.sep:
439
+ strrelpath += self.sep
440
+ # assert strrelpath[-1] == self.sep
441
+ # assert strrelpath[-2] != self.sep
442
+ strself = self.strpath
443
+ if sys.platform == "win32" or getattr(os, "_name", None) == "nt":
444
+ if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)):
445
+ return strself[len(strrelpath) :]
446
+ elif strself.startswith(strrelpath):
447
+ return strself[len(strrelpath) :]
448
+ return ""
449
+
450
+ def ensure_dir(self, *args):
451
+ """Ensure the path joined with args is a directory."""
452
+ return self.ensure(*args, dir=True)
453
+
454
+ def bestrelpath(self, dest):
455
+ """Return a string which is a relative path from self
456
+ (assumed to be a directory) to dest such that
457
+ self.join(bestrelpath) == dest and if not such
458
+ path can be determined return dest.
459
+ """
460
+ try:
461
+ if self == dest:
462
+ return os.curdir
463
+ base = self.common(dest)
464
+ if not base: # can be the case on windows
465
+ return str(dest)
466
+ self2base = self.relto(base)
467
+ reldest = dest.relto(base)
468
+ if self2base:
469
+ n = self2base.count(self.sep) + 1
470
+ else:
471
+ n = 0
472
+ lst = [os.pardir] * n
473
+ if reldest:
474
+ lst.append(reldest)
475
+ target = dest.sep.join(lst)
476
+ return target
477
+ except AttributeError:
478
+ return str(dest)
479
+
480
+ def exists(self):
481
+ return self.check()
482
+
483
+ def isdir(self):
484
+ return self.check(dir=1)
485
+
486
+ def isfile(self):
487
+ return self.check(file=1)
488
+
489
+ def parts(self, reverse=False):
490
+ """Return a root-first list of all ancestor directories
491
+ plus the path itself.
492
+ """
493
+ current = self
494
+ lst = [self]
495
+ while 1:
496
+ last = current
497
+ current = current.dirpath()
498
+ if last == current:
499
+ break
500
+ lst.append(current)
501
+ if not reverse:
502
+ lst.reverse()
503
+ return lst
504
+
505
+ def common(self, other):
506
+ """Return the common part shared with the other path
507
+ or None if there is no common part.
508
+ """
509
+ last = None
510
+ for x, y in zip(self.parts(), other.parts()):
511
+ if x != y:
512
+ return last
513
+ last = x
514
+ return last
515
+
516
+ def __add__(self, other):
517
+ """Return new path object with 'other' added to the basename"""
518
+ return self.new(basename=self.basename + str(other))
519
+
520
+ def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False):
521
+ """Yields all paths below the current one
522
+
523
+ fil is a filter (glob pattern or callable), if not matching the
524
+ path will not be yielded, defaulting to None (everything is
525
+ returned)
526
+
527
+ rec is a filter (glob pattern or callable) that controls whether
528
+ a node is descended, defaulting to None
529
+
530
+ ignore is an Exception class that is ignoredwhen calling dirlist()
531
+ on any of the paths (by default, all exceptions are reported)
532
+
533
+ bf if True will cause a breadthfirst search instead of the
534
+ default depthfirst. Default: False
535
+
536
+ sort if True will sort entries within each directory level.
537
+ """
538
+ yield from Visitor(fil, rec, ignore, bf, sort).gen(self)
539
+
540
+ def _sortlist(self, res, sort):
541
+ if sort:
542
+ if hasattr(sort, "__call__"):
543
+ warnings.warn(
544
+ DeprecationWarning(
545
+ "listdir(sort=callable) is deprecated and breaks on python3"
546
+ ),
547
+ stacklevel=3,
548
+ )
549
+ res.sort(sort)
550
+ else:
551
+ res.sort()
552
+
553
+ def __fspath__(self):
554
+ return self.strpath
555
+
556
+ def __hash__(self):
557
+ s = self.strpath
558
+ if iswin32:
559
+ s = s.lower()
560
+ return hash(s)
561
+
562
+ def __eq__(self, other):
563
+ s1 = os.fspath(self)
564
+ try:
565
+ s2 = os.fspath(other)
566
+ except TypeError:
567
+ return False
568
+ if iswin32:
569
+ s1 = s1.lower()
570
+ try:
571
+ s2 = s2.lower()
572
+ except AttributeError:
573
+ return False
574
+ return s1 == s2
575
+
576
+ def __ne__(self, other):
577
+ return not (self == other)
578
+
579
+ def __lt__(self, other):
580
+ return os.fspath(self) < os.fspath(other)
581
+
582
+ def __gt__(self, other):
583
+ return os.fspath(self) > os.fspath(other)
584
+
585
+ def samefile(self, other):
586
+ """Return True if 'other' references the same file as 'self'."""
587
+ other = os.fspath(other)
588
+ if not isabs(other):
589
+ other = abspath(other)
590
+ if self == other:
591
+ return True
592
+ if not hasattr(os.path, "samefile"):
593
+ return False
594
+ return error.checked_call(os.path.samefile, self.strpath, other)
595
+
596
+ def remove(self, rec=1, ignore_errors=False):
597
+ """Remove a file or directory (or a directory tree if rec=1).
598
+ if ignore_errors is True, errors while removing directories will
599
+ be ignored.
600
+ """
601
+ if self.check(dir=1, link=0):
602
+ if rec:
603
+ # force remove of readonly files on windows
604
+ if iswin32:
605
+ self.chmod(0o700, rec=1)
606
+ import shutil
607
+
608
+ error.checked_call(
609
+ shutil.rmtree, self.strpath, ignore_errors=ignore_errors
610
+ )
611
+ else:
612
+ error.checked_call(os.rmdir, self.strpath)
613
+ else:
614
+ if iswin32:
615
+ self.chmod(0o700)
616
+ error.checked_call(os.remove, self.strpath)
617
+
618
+ def computehash(self, hashtype="md5", chunksize=524288):
619
+ """Return hexdigest of hashvalue for this file."""
620
+ try:
621
+ try:
622
+ import hashlib as mod
623
+ except ImportError:
624
+ if hashtype == "sha1":
625
+ hashtype = "sha"
626
+ mod = __import__(hashtype)
627
+ hash = getattr(mod, hashtype)()
628
+ except (AttributeError, ImportError):
629
+ raise ValueError(f"Don't know how to compute {hashtype!r} hash")
630
+ f = self.open("rb")
631
+ try:
632
+ while 1:
633
+ buf = f.read(chunksize)
634
+ if not buf:
635
+ return hash.hexdigest()
636
+ hash.update(buf)
637
+ finally:
638
+ f.close()
639
+
640
+ def new(self, **kw):
641
+ """Create a modified version of this path.
642
+ the following keyword arguments modify various path parts::
643
+
644
+ a:/some/path/to/a/file.ext
645
+ xx drive
646
+ xxxxxxxxxxxxxxxxx dirname
647
+ xxxxxxxx basename
648
+ xxxx purebasename
649
+ xxx ext
650
+ """
651
+ obj = object.__new__(self.__class__)
652
+ if not kw:
653
+ obj.strpath = self.strpath
654
+ return obj
655
+ drive, dirname, _basename, purebasename, ext = self._getbyspec(
656
+ "drive,dirname,basename,purebasename,ext"
657
+ )
658
+ if "basename" in kw:
659
+ if "purebasename" in kw or "ext" in kw:
660
+ raise ValueError(f"invalid specification {kw!r}")
661
+ else:
662
+ pb = kw.setdefault("purebasename", purebasename)
663
+ try:
664
+ ext = kw["ext"]
665
+ except KeyError:
666
+ pass
667
+ else:
668
+ if ext and not ext.startswith("."):
669
+ ext = "." + ext
670
+ kw["basename"] = pb + ext
671
+
672
+ if "dirname" in kw and not kw["dirname"]:
673
+ kw["dirname"] = drive
674
+ else:
675
+ kw.setdefault("dirname", dirname)
676
+ kw.setdefault("sep", self.sep)
677
+ obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
678
+ return obj
679
+
680
+ def _getbyspec(self, spec: str) -> list[str]:
681
+ """See new for what 'spec' can be."""
682
+ res = []
683
+ parts = self.strpath.split(self.sep)
684
+
685
+ args = filter(None, spec.split(","))
686
+ for name in args:
687
+ if name == "drive":
688
+ res.append(parts[0])
689
+ elif name == "dirname":
690
+ res.append(self.sep.join(parts[:-1]))
691
+ else:
692
+ basename = parts[-1]
693
+ if name == "basename":
694
+ res.append(basename)
695
+ else:
696
+ i = basename.rfind(".")
697
+ if i == -1:
698
+ purebasename, ext = basename, ""
699
+ else:
700
+ purebasename, ext = basename[:i], basename[i:]
701
+ if name == "purebasename":
702
+ res.append(purebasename)
703
+ elif name == "ext":
704
+ res.append(ext)
705
+ else:
706
+ raise ValueError(f"invalid part specification {name!r}")
707
+ return res
708
+
709
+ def dirpath(self, *args, **kwargs):
710
+ """Return the directory path joined with any given path arguments."""
711
+ if not kwargs:
712
+ path = object.__new__(self.__class__)
713
+ path.strpath = dirname(self.strpath)
714
+ if args:
715
+ path = path.join(*args)
716
+ return path
717
+ return self.new(basename="").join(*args, **kwargs)
718
+
719
+ def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath:
720
+ """Return a new path by appending all 'args' as path
721
+ components. if abs=1 is used restart from root if any
722
+ of the args is an absolute path.
723
+ """
724
+ sep = self.sep
725
+ strargs = [os.fspath(arg) for arg in args]
726
+ strpath = self.strpath
727
+ if abs:
728
+ newargs: list[str] = []
729
+ for arg in reversed(strargs):
730
+ if isabs(arg):
731
+ strpath = arg
732
+ strargs = newargs
733
+ break
734
+ newargs.insert(0, arg)
735
+ # special case for when we have e.g. strpath == "/"
736
+ actual_sep = "" if strpath.endswith(sep) else sep
737
+ for arg in strargs:
738
+ arg = arg.strip(sep)
739
+ if iswin32:
740
+ # allow unix style paths even on windows.
741
+ arg = arg.strip("/")
742
+ arg = arg.replace("/", sep)
743
+ strpath = strpath + actual_sep + arg
744
+ actual_sep = sep
745
+ obj = object.__new__(self.__class__)
746
+ obj.strpath = normpath(strpath)
747
+ return obj
748
+
749
+ def open(self, mode="r", ensure=False, encoding=None):
750
+ """Return an opened file with the given mode.
751
+
752
+ If ensure is True, create parent directories if needed.
753
+ """
754
+ if ensure:
755
+ self.dirpath().ensure(dir=1)
756
+ if encoding:
757
+ return error.checked_call(
758
+ io.open,
759
+ self.strpath,
760
+ mode,
761
+ encoding=encoding,
762
+ )
763
+ return error.checked_call(open, self.strpath, mode)
764
+
765
+ def _fastjoin(self, name):
766
+ child = object.__new__(self.__class__)
767
+ child.strpath = self.strpath + self.sep + name
768
+ return child
769
+
770
+ def islink(self):
771
+ return islink(self.strpath)
772
+
773
+ def check(self, **kw):
774
+ """Check a path for existence and properties.
775
+
776
+ Without arguments, return True if the path exists, otherwise False.
777
+
778
+ valid checkers::
779
+
780
+ file = 1 # is a file
781
+ file = 0 # is not a file (may not even exist)
782
+ dir = 1 # is a dir
783
+ link = 1 # is a link
784
+ exists = 1 # exists
785
+
786
+ You can specify multiple checker definitions, for example::
787
+
788
+ path.check(file=1, link=1) # a link pointing to a file
789
+ """
790
+ if not kw:
791
+ return exists(self.strpath)
792
+ if len(kw) == 1:
793
+ if "dir" in kw:
794
+ return not kw["dir"] ^ isdir(self.strpath)
795
+ if "file" in kw:
796
+ return not kw["file"] ^ isfile(self.strpath)
797
+ if not kw:
798
+ kw = {"exists": 1}
799
+ return Checkers(self)._evaluate(kw)
800
+
801
+ _patternchars = set("*?[" + os.sep)
802
+
803
+ def listdir(self, fil=None, sort=None):
804
+ """List directory contents, possibly filter by the given fil func
805
+ and possibly sorted.
806
+ """
807
+ if fil is None and sort is None:
808
+ names = error.checked_call(os.listdir, self.strpath)
809
+ return map_as_list(self._fastjoin, names)
810
+ if isinstance(fil, str):
811
+ if not self._patternchars.intersection(fil):
812
+ child = self._fastjoin(fil)
813
+ if exists(child.strpath):
814
+ return [child]
815
+ return []
816
+ fil = FNMatcher(fil)
817
+ names = error.checked_call(os.listdir, self.strpath)
818
+ res = []
819
+ for name in names:
820
+ child = self._fastjoin(name)
821
+ if fil is None or fil(child):
822
+ res.append(child)
823
+ self._sortlist(res, sort)
824
+ return res
825
+
826
+ def size(self) -> int:
827
+ """Return size of the underlying file object"""
828
+ return self.stat().size
829
+
830
+ def mtime(self) -> float:
831
+ """Return last modification time of the path."""
832
+ return self.stat().mtime
833
+
834
+ def copy(self, target, mode=False, stat=False):
835
+ """Copy path to target.
836
+
837
+ If mode is True, will copy permission from path to target.
838
+ If stat is True, copy permission, last modification
839
+ time, last access time, and flags from path to target.
840
+ """
841
+ if self.check(file=1):
842
+ if target.check(dir=1):
843
+ target = target.join(self.basename)
844
+ assert self != target
845
+ copychunked(self, target)
846
+ if mode:
847
+ copymode(self.strpath, target.strpath)
848
+ if stat:
849
+ copystat(self, target)
850
+ else:
851
+
852
+ def rec(p):
853
+ return p.check(link=0)
854
+
855
+ for x in self.visit(rec=rec):
856
+ relpath = x.relto(self)
857
+ newx = target.join(relpath)
858
+ newx.dirpath().ensure(dir=1)
859
+ if x.check(link=1):
860
+ newx.mksymlinkto(x.readlink())
861
+ continue
862
+ elif x.check(file=1):
863
+ copychunked(x, newx)
864
+ elif x.check(dir=1):
865
+ newx.ensure(dir=1)
866
+ if mode:
867
+ copymode(x.strpath, newx.strpath)
868
+ if stat:
869
+ copystat(x, newx)
870
+
871
+ def rename(self, target):
872
+ """Rename this path to target."""
873
+ target = os.fspath(target)
874
+ return error.checked_call(os.rename, self.strpath, target)
875
+
876
+ def dump(self, obj, bin=1):
877
+ """Pickle object into path location"""
878
+ f = self.open("wb")
879
+ import pickle
880
+
881
+ try:
882
+ error.checked_call(pickle.dump, obj, f, bin)
883
+ finally:
884
+ f.close()
885
+
886
+ def mkdir(self, *args):
887
+ """Create & return the directory joined with args."""
888
+ p = self.join(*args)
889
+ error.checked_call(os.mkdir, os.fspath(p))
890
+ return p
891
+
892
+ def write_binary(self, data, ensure=False):
893
+ """Write binary data into path. If ensure is True create
894
+ missing parent directories.
895
+ """
896
+ if ensure:
897
+ self.dirpath().ensure(dir=1)
898
+ with self.open("wb") as f:
899
+ f.write(data)
900
+
901
+ def write_text(self, data, encoding, ensure=False):
902
+ """Write text data into path using the specified encoding.
903
+ If ensure is True create missing parent directories.
904
+ """
905
+ if ensure:
906
+ self.dirpath().ensure(dir=1)
907
+ with self.open("w", encoding=encoding) as f:
908
+ f.write(data)
909
+
910
+ def write(self, data, mode="w", ensure=False):
911
+ """Write data into path. If ensure is True create
912
+ missing parent directories.
913
+ """
914
+ if ensure:
915
+ self.dirpath().ensure(dir=1)
916
+ if "b" in mode:
917
+ if not isinstance(data, bytes):
918
+ raise ValueError("can only process bytes")
919
+ else:
920
+ if not isinstance(data, str):
921
+ if not isinstance(data, bytes):
922
+ data = str(data)
923
+ else:
924
+ data = data.decode(sys.getdefaultencoding())
925
+ f = self.open(mode)
926
+ try:
927
+ f.write(data)
928
+ finally:
929
+ f.close()
930
+
931
+ def _ensuredirs(self):
932
+ parent = self.dirpath()
933
+ if parent == self:
934
+ return self
935
+ if parent.check(dir=0):
936
+ parent._ensuredirs()
937
+ if self.check(dir=0):
938
+ try:
939
+ self.mkdir()
940
+ except error.EEXIST:
941
+ # race condition: file/dir created by another thread/process.
942
+ # complain if it is not a dir
943
+ if self.check(dir=0):
944
+ raise
945
+ return self
946
+
947
+ def ensure(self, *args, **kwargs):
948
+ """Ensure that an args-joined path exists (by default as
949
+ a file). if you specify a keyword argument 'dir=True'
950
+ then the path is forced to be a directory path.
951
+ """
952
+ p = self.join(*args)
953
+ if kwargs.get("dir", 0):
954
+ return p._ensuredirs()
955
+ else:
956
+ p.dirpath()._ensuredirs()
957
+ if not p.check(file=1):
958
+ p.open("wb").close()
959
+ return p
960
+
961
+ @overload
962
+ def stat(self, raising: Literal[True] = ...) -> Stat: ...
963
+
964
+ @overload
965
+ def stat(self, raising: Literal[False]) -> Stat | None: ...
966
+
967
+ def stat(self, raising: bool = True) -> Stat | None:
968
+ """Return an os.stat() tuple."""
969
+ if raising:
970
+ return Stat(self, error.checked_call(os.stat, self.strpath))
971
+ try:
972
+ return Stat(self, os.stat(self.strpath))
973
+ except KeyboardInterrupt:
974
+ raise
975
+ except Exception:
976
+ return None
977
+
978
+ def lstat(self) -> Stat:
979
+ """Return an os.lstat() tuple."""
980
+ return Stat(self, error.checked_call(os.lstat, self.strpath))
981
+
982
+ def setmtime(self, mtime=None):
983
+ """Set modification time for the given path. if 'mtime' is None
984
+ (the default) then the file's mtime is set to current time.
985
+
986
+ Note that the resolution for 'mtime' is platform dependent.
987
+ """
988
+ if mtime is None:
989
+ return error.checked_call(os.utime, self.strpath, mtime)
990
+ try:
991
+ return error.checked_call(os.utime, self.strpath, (-1, mtime))
992
+ except error.EINVAL:
993
+ return error.checked_call(os.utime, self.strpath, (self.atime(), mtime))
994
+
995
+ def chdir(self):
996
+ """Change directory to self and return old current directory"""
997
+ try:
998
+ old = self.__class__()
999
+ except error.ENOENT:
1000
+ old = None
1001
+ error.checked_call(os.chdir, self.strpath)
1002
+ return old
1003
+
1004
+ @contextmanager
1005
+ def as_cwd(self):
1006
+ """
1007
+ Return a context manager, which changes to the path's dir during the
1008
+ managed "with" context.
1009
+ On __enter__ it returns the old dir, which might be ``None``.
1010
+ """
1011
+ old = self.chdir()
1012
+ try:
1013
+ yield old
1014
+ finally:
1015
+ if old is not None:
1016
+ old.chdir()
1017
+
1018
+ def realpath(self):
1019
+ """Return a new path which contains no symbolic links."""
1020
+ return self.__class__(os.path.realpath(self.strpath))
1021
+
1022
+ def atime(self):
1023
+ """Return last access time of the path."""
1024
+ return self.stat().atime
1025
+
1026
+ def __repr__(self):
1027
+ return f"local({self.strpath!r})"
1028
+
1029
+ def __str__(self):
1030
+ """Return string representation of the Path."""
1031
+ return self.strpath
1032
+
1033
+ def chmod(self, mode, rec=0):
1034
+ """Change permissions to the given mode. If mode is an
1035
+ integer it directly encodes the os-specific modes.
1036
+ if rec is True perform recursively.
1037
+ """
1038
+ if not isinstance(mode, int):
1039
+ raise TypeError(f"mode {mode!r} must be an integer")
1040
+ if rec:
1041
+ for x in self.visit(rec=rec):
1042
+ error.checked_call(os.chmod, str(x), mode)
1043
+ error.checked_call(os.chmod, self.strpath, mode)
1044
+
1045
+ def pypkgpath(self):
1046
+ """Return the Python package path by looking for the last
1047
+ directory upwards which still contains an __init__.py.
1048
+ Return None if a pkgpath cannot be determined.
1049
+ """
1050
+ pkgpath = None
1051
+ for parent in self.parts(reverse=True):
1052
+ if parent.isdir():
1053
+ if not parent.join("__init__.py").exists():
1054
+ break
1055
+ if not isimportable(parent.basename):
1056
+ break
1057
+ pkgpath = parent
1058
+ return pkgpath
1059
+
1060
+ def _ensuresyspath(self, ensuremode, path):
1061
+ if ensuremode:
1062
+ s = str(path)
1063
+ if ensuremode == "append":
1064
+ if s not in sys.path:
1065
+ sys.path.append(s)
1066
+ else:
1067
+ if s != sys.path[0]:
1068
+ sys.path.insert(0, s)
1069
+
1070
+ def pyimport(self, modname=None, ensuresyspath=True):
1071
+ """Return path as an imported python module.
1072
+
1073
+ If modname is None, look for the containing package
1074
+ and construct an according module name.
1075
+ The module will be put/looked up in sys.modules.
1076
+ if ensuresyspath is True then the root dir for importing
1077
+ the file (taking __init__.py files into account) will
1078
+ be prepended to sys.path if it isn't there already.
1079
+ If ensuresyspath=="append" the root dir will be appended
1080
+ if it isn't already contained in sys.path.
1081
+ if ensuresyspath is False no modification of syspath happens.
1082
+
1083
+ Special value of ensuresyspath=="importlib" is intended
1084
+ purely for using in pytest, it is capable only of importing
1085
+ separate .py files outside packages, e.g. for test suite
1086
+ without any __init__.py file. It effectively allows having
1087
+ same-named test modules in different places and offers
1088
+ mild opt-in via this option. Note that it works only in
1089
+ recent versions of python.
1090
+ """
1091
+ if not self.check():
1092
+ raise error.ENOENT(self)
1093
+
1094
+ if ensuresyspath == "importlib":
1095
+ if modname is None:
1096
+ modname = self.purebasename
1097
+ spec = importlib.util.spec_from_file_location(modname, str(self))
1098
+ if spec is None or spec.loader is None:
1099
+ raise ImportError(f"Can't find module {modname} at location {self!s}")
1100
+ mod = importlib.util.module_from_spec(spec)
1101
+ spec.loader.exec_module(mod)
1102
+ return mod
1103
+
1104
+ pkgpath = None
1105
+ if modname is None:
1106
+ pkgpath = self.pypkgpath()
1107
+ if pkgpath is not None:
1108
+ pkgroot = pkgpath.dirpath()
1109
+ names = self.new(ext="").relto(pkgroot).split(self.sep)
1110
+ if names[-1] == "__init__":
1111
+ names.pop()
1112
+ modname = ".".join(names)
1113
+ else:
1114
+ pkgroot = self.dirpath()
1115
+ modname = self.purebasename
1116
+
1117
+ self._ensuresyspath(ensuresyspath, pkgroot)
1118
+ __import__(modname)
1119
+ mod = sys.modules[modname]
1120
+ if self.basename == "__init__.py":
1121
+ return mod # we don't check anything as we might
1122
+ # be in a namespace package ... too icky to check
1123
+ modfile = mod.__file__
1124
+ assert modfile is not None
1125
+ if modfile[-4:] in (".pyc", ".pyo"):
1126
+ modfile = modfile[:-1]
1127
+ elif modfile.endswith("$py.class"):
1128
+ modfile = modfile[:-9] + ".py"
1129
+ if modfile.endswith(os.sep + "__init__.py"):
1130
+ if self.basename != "__init__.py":
1131
+ modfile = modfile[:-12]
1132
+ try:
1133
+ issame = self.samefile(modfile)
1134
+ except error.ENOENT:
1135
+ issame = False
1136
+ if not issame:
1137
+ ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH")
1138
+ if ignore != "1":
1139
+ raise self.ImportMismatchError(modname, modfile, self)
1140
+ return mod
1141
+ else:
1142
+ try:
1143
+ return sys.modules[modname]
1144
+ except KeyError:
1145
+ # we have a custom modname, do a pseudo-import
1146
+ import types
1147
+
1148
+ mod = types.ModuleType(modname)
1149
+ mod.__file__ = str(self)
1150
+ sys.modules[modname] = mod
1151
+ try:
1152
+ with open(str(self), "rb") as f:
1153
+ exec(f.read(), mod.__dict__)
1154
+ except BaseException:
1155
+ del sys.modules[modname]
1156
+ raise
1157
+ return mod
1158
+
1159
+ def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str:
1160
+ """Return stdout text from executing a system child process,
1161
+ where the 'self' path points to executable.
1162
+ The process is directly invoked and not through a system shell.
1163
+ """
1164
+ from subprocess import PIPE
1165
+ from subprocess import Popen
1166
+
1167
+ popen_opts.pop("stdout", None)
1168
+ popen_opts.pop("stderr", None)
1169
+ proc = Popen(
1170
+ [str(self)] + [str(arg) for arg in argv],
1171
+ **popen_opts,
1172
+ stdout=PIPE,
1173
+ stderr=PIPE,
1174
+ )
1175
+ stdout: str | bytes
1176
+ stdout, stderr = proc.communicate()
1177
+ ret = proc.wait()
1178
+ if isinstance(stdout, bytes):
1179
+ stdout = stdout.decode(sys.getdefaultencoding())
1180
+ if ret != 0:
1181
+ if isinstance(stderr, bytes):
1182
+ stderr = stderr.decode(sys.getdefaultencoding())
1183
+ raise RuntimeError(
1184
+ ret,
1185
+ ret,
1186
+ str(self),
1187
+ stdout,
1188
+ stderr,
1189
+ )
1190
+ return stdout
1191
+
1192
+ @classmethod
1193
+ def sysfind(cls, name, checker=None, paths=None):
1194
+ """Return a path object found by looking at the systems
1195
+ underlying PATH specification. If the checker is not None
1196
+ it will be invoked to filter matching paths. If a binary
1197
+ cannot be found, None is returned
1198
+ Note: This is probably not working on plain win32 systems
1199
+ but may work on cygwin.
1200
+ """
1201
+ if isabs(name):
1202
+ p = local(name)
1203
+ if p.check(file=1):
1204
+ return p
1205
+ else:
1206
+ if paths is None:
1207
+ if iswin32:
1208
+ paths = os.environ["Path"].split(";")
1209
+ if "" not in paths and "." not in paths:
1210
+ paths.append(".")
1211
+ try:
1212
+ systemroot = os.environ["SYSTEMROOT"]
1213
+ except KeyError:
1214
+ pass
1215
+ else:
1216
+ paths = [
1217
+ path.replace("%SystemRoot%", systemroot) for path in paths
1218
+ ]
1219
+ else:
1220
+ paths = os.environ["PATH"].split(":")
1221
+ tryadd = []
1222
+ if iswin32:
1223
+ tryadd += os.environ["PATHEXT"].split(os.pathsep)
1224
+ tryadd.append("")
1225
+
1226
+ for x in paths:
1227
+ for addext in tryadd:
1228
+ p = local(x).join(name, abs=True) + addext
1229
+ try:
1230
+ if p.check(file=1):
1231
+ if checker:
1232
+ if not checker(p):
1233
+ continue
1234
+ return p
1235
+ except error.EACCES:
1236
+ pass
1237
+ return None
1238
+
1239
+ @classmethod
1240
+ def _gethomedir(cls):
1241
+ try:
1242
+ x = os.environ["HOME"]
1243
+ except KeyError:
1244
+ try:
1245
+ x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"]
1246
+ except KeyError:
1247
+ return None
1248
+ return cls(x)
1249
+
1250
+ # """
1251
+ # special class constructors for local filesystem paths
1252
+ # """
1253
+ @classmethod
1254
+ def get_temproot(cls):
1255
+ """Return the system's temporary directory
1256
+ (where tempfiles are usually created in)
1257
+ """
1258
+ import tempfile
1259
+
1260
+ return local(tempfile.gettempdir())
1261
+
1262
+ @classmethod
1263
+ def mkdtemp(cls, rootdir=None):
1264
+ """Return a Path object pointing to a fresh new temporary directory
1265
+ (which we created ourselves).
1266
+ """
1267
+ import tempfile
1268
+
1269
+ if rootdir is None:
1270
+ rootdir = cls.get_temproot()
1271
+ path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir))
1272
+ return cls(path)
1273
+
1274
+ @classmethod
1275
+ def make_numbered_dir(
1276
+ cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800
1277
+ ): # two days
1278
+ """Return unique directory with a number greater than the current
1279
+ maximum one. The number is assumed to start directly after prefix.
1280
+ if keep is true directories with a number less than (maxnum-keep)
1281
+ will be removed. If .lock files are used (lock_timeout non-zero),
1282
+ algorithm is multi-process safe.
1283
+ """
1284
+ if rootdir is None:
1285
+ rootdir = cls.get_temproot()
1286
+
1287
+ nprefix = prefix.lower()
1288
+
1289
+ def parse_num(path):
1290
+ """Parse the number out of a path (if it matches the prefix)"""
1291
+ nbasename = path.basename.lower()
1292
+ if nbasename.startswith(nprefix):
1293
+ try:
1294
+ return int(nbasename[len(nprefix) :])
1295
+ except ValueError:
1296
+ pass
1297
+
1298
+ def create_lockfile(path):
1299
+ """Exclusively create lockfile. Throws when failed"""
1300
+ mypid = os.getpid()
1301
+ lockfile = path.join(".lock")
1302
+ if hasattr(lockfile, "mksymlinkto"):
1303
+ lockfile.mksymlinkto(str(mypid))
1304
+ else:
1305
+ fd = error.checked_call(
1306
+ os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644
1307
+ )
1308
+ with os.fdopen(fd, "w") as f:
1309
+ f.write(str(mypid))
1310
+ return lockfile
1311
+
1312
+ def atexit_remove_lockfile(lockfile):
1313
+ """Ensure lockfile is removed at process exit"""
1314
+ mypid = os.getpid()
1315
+
1316
+ def try_remove_lockfile():
1317
+ # in a fork() situation, only the last process should
1318
+ # remove the .lock, otherwise the other processes run the
1319
+ # risk of seeing their temporary dir disappear. For now
1320
+ # we remove the .lock in the parent only (i.e. we assume
1321
+ # that the children finish before the parent).
1322
+ if os.getpid() != mypid:
1323
+ return
1324
+ try:
1325
+ lockfile.remove()
1326
+ except error.Error:
1327
+ pass
1328
+
1329
+ atexit.register(try_remove_lockfile)
1330
+
1331
+ # compute the maximum number currently in use with the prefix
1332
+ lastmax = None
1333
+ while True:
1334
+ maxnum = -1
1335
+ for path in rootdir.listdir():
1336
+ num = parse_num(path)
1337
+ if num is not None:
1338
+ maxnum = max(maxnum, num)
1339
+
1340
+ # make the new directory
1341
+ try:
1342
+ udir = rootdir.mkdir(prefix + str(maxnum + 1))
1343
+ if lock_timeout:
1344
+ lockfile = create_lockfile(udir)
1345
+ atexit_remove_lockfile(lockfile)
1346
+ except (error.EEXIST, error.ENOENT, error.EBUSY):
1347
+ # race condition (1): another thread/process created the dir
1348
+ # in the meantime - try again
1349
+ # race condition (2): another thread/process spuriously acquired
1350
+ # lock treating empty directory as candidate
1351
+ # for removal - try again
1352
+ # race condition (3): another thread/process tried to create the lock at
1353
+ # the same time (happened in Python 3.3 on Windows)
1354
+ # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa
1355
+ if lastmax == maxnum:
1356
+ raise
1357
+ lastmax = maxnum
1358
+ continue
1359
+ break
1360
+
1361
+ def get_mtime(path):
1362
+ """Read file modification time"""
1363
+ try:
1364
+ return path.lstat().mtime
1365
+ except error.Error:
1366
+ pass
1367
+
1368
+ garbage_prefix = prefix + "garbage-"
1369
+
1370
+ def is_garbage(path):
1371
+ """Check if path denotes directory scheduled for removal"""
1372
+ bn = path.basename
1373
+ return bn.startswith(garbage_prefix)
1374
+
1375
+ # prune old directories
1376
+ udir_time = get_mtime(udir)
1377
+ if keep and udir_time:
1378
+ for path in rootdir.listdir():
1379
+ num = parse_num(path)
1380
+ if num is not None and num <= (maxnum - keep):
1381
+ try:
1382
+ # try acquiring lock to remove directory as exclusive user
1383
+ if lock_timeout:
1384
+ create_lockfile(path)
1385
+ except (error.EEXIST, error.ENOENT, error.EBUSY):
1386
+ path_time = get_mtime(path)
1387
+ if not path_time:
1388
+ # assume directory doesn't exist now
1389
+ continue
1390
+ if abs(udir_time - path_time) < lock_timeout:
1391
+ # assume directory with lockfile exists
1392
+ # and lock timeout hasn't expired yet
1393
+ continue
1394
+
1395
+ # path dir locked for exclusive use
1396
+ # and scheduled for removal to avoid another thread/process
1397
+ # treating it as a new directory or removal candidate
1398
+ garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4()))
1399
+ try:
1400
+ path.rename(garbage_path)
1401
+ garbage_path.remove(rec=1)
1402
+ except KeyboardInterrupt:
1403
+ raise
1404
+ except Exception: # this might be error.Error, WindowsError ...
1405
+ pass
1406
+ if is_garbage(path):
1407
+ try:
1408
+ path.remove(rec=1)
1409
+ except KeyboardInterrupt:
1410
+ raise
1411
+ except Exception: # this might be error.Error, WindowsError ...
1412
+ pass
1413
+
1414
+ # make link...
1415
+ try:
1416
+ username = os.environ["USER"] # linux, et al
1417
+ except KeyError:
1418
+ try:
1419
+ username = os.environ["USERNAME"] # windows
1420
+ except KeyError:
1421
+ username = "current"
1422
+
1423
+ src = str(udir)
1424
+ dest = src[: src.rfind("-")] + "-" + username
1425
+ try:
1426
+ os.unlink(dest)
1427
+ except OSError:
1428
+ pass
1429
+ try:
1430
+ os.symlink(src, dest)
1431
+ except (OSError, AttributeError, NotImplementedError):
1432
+ pass
1433
+
1434
+ return udir
1435
+
1436
+
1437
+ def copymode(src, dest):
1438
+ """Copy permission from src to dst."""
1439
+ import shutil
1440
+
1441
+ shutil.copymode(src, dest)
1442
+
1443
+
1444
+ def copystat(src, dest):
1445
+ """Copy permission, last modification time,
1446
+ last access time, and flags from src to dst."""
1447
+ import shutil
1448
+
1449
+ shutil.copystat(str(src), str(dest))
1450
+
1451
+
1452
+ def copychunked(src, dest):
1453
+ chunksize = 524288 # half a meg of bytes
1454
+ fsrc = src.open("rb")
1455
+ try:
1456
+ fdest = dest.open("wb")
1457
+ try:
1458
+ while 1:
1459
+ buf = fsrc.read(chunksize)
1460
+ if not buf:
1461
+ break
1462
+ fdest.write(buf)
1463
+ finally:
1464
+ fdest.close()
1465
+ finally:
1466
+ fsrc.close()
1467
+
1468
+
1469
+ def isimportable(name):
1470
+ if name and (name[0].isalpha() or name[0] == "_"):
1471
+ name = name.replace("_", "")
1472
+ return not name or name.isalnum()
1473
+
1474
+
1475
+ local = LocalPath
.venv/lib/python3.14/site-packages/_pytest/_version.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '9.1.1'
22
+ __version_tuple__ = version_tuple = (9, 1, 1)
23
+
24
+ __commit_id__ = commit_id = None
.venv/lib/python3.14/site-packages/_pytest/assertion/__init__.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Support for presenting detailed information in failing assertions."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Generator
7
+ import sys
8
+ from typing import Any
9
+ from typing import Protocol
10
+ from typing import TYPE_CHECKING
11
+
12
+ from _pytest.assertion import rewrite
13
+ from _pytest.assertion import truncate
14
+ from _pytest.assertion import util
15
+ from _pytest.assertion.rewrite import assertstate_key
16
+ from _pytest.config import Config
17
+ from _pytest.config import hookimpl
18
+ from _pytest.config.argparsing import Parser
19
+ from _pytest.nodes import Item
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from _pytest.main import Session
24
+
25
+
26
+ def pytest_addoption(parser: Parser) -> None:
27
+ group = parser.getgroup("debugconfig")
28
+ group.addoption(
29
+ "--assert",
30
+ action="store",
31
+ dest="assertmode",
32
+ choices=("rewrite", "plain"),
33
+ default="rewrite",
34
+ metavar="MODE",
35
+ help=(
36
+ "Control assertion debugging tools.\n"
37
+ "'plain' performs no assertion debugging.\n"
38
+ "'rewrite' (the default) rewrites assert statements in test modules"
39
+ " on import to provide assert expression information."
40
+ ),
41
+ )
42
+ parser.addini(
43
+ "enable_assertion_pass_hook",
44
+ type="bool",
45
+ default=False,
46
+ help="Enables the pytest_assertion_pass hook. "
47
+ "Make sure to delete any previously generated pyc cache files.",
48
+ )
49
+
50
+ parser.addini(
51
+ "truncation_limit_lines",
52
+ default=None,
53
+ help="Set threshold of LINES after which truncation will take effect",
54
+ )
55
+ parser.addini(
56
+ "truncation_limit_chars",
57
+ default=None,
58
+ help=("Set threshold of CHARS after which truncation will take effect"),
59
+ )
60
+ parser.addini(
61
+ "assertion_text_diff_style",
62
+ default=util.ASSERTION_TEXT_DIFF_STYLE_NDIFF,
63
+ help=(
64
+ "Choose how pytest renders diffs for string equality assertions: "
65
+ f"{util.ASSERTION_TEXT_DIFF_STYLE_NDIFF} or "
66
+ f"{util.ASSERTION_TEXT_DIFF_STYLE_BLOCK}"
67
+ ),
68
+ )
69
+
70
+ Config._add_verbosity_ini(
71
+ parser,
72
+ Config.VERBOSITY_ASSERTIONS,
73
+ help=(
74
+ "Specify a verbosity level for assertions, overriding the main level. "
75
+ "Higher levels will provide more detailed explanation when an assertion fails."
76
+ ),
77
+ )
78
+
79
+
80
+ def pytest_configure(config: Config) -> None:
81
+ util.validate_assertion_text_diff_style(config)
82
+
83
+
84
+ def register_assert_rewrite(*names: str) -> None:
85
+ """Register one or more module names to be rewritten on import.
86
+
87
+ This function will make sure that this module or all modules inside
88
+ the package will get their assert statements rewritten.
89
+ Thus you should make sure to call this before the module is
90
+ actually imported, usually in your __init__.py if you are a plugin
91
+ using a package.
92
+
93
+ :param names: The module names to register.
94
+ """
95
+ for name in names:
96
+ if not isinstance(name, str):
97
+ msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
98
+ raise TypeError(msg.format(repr(names)))
99
+ rewrite_hook: RewriteHook
100
+ for hook in sys.meta_path:
101
+ if isinstance(hook, rewrite.AssertionRewritingHook):
102
+ rewrite_hook = hook
103
+ break
104
+ else:
105
+ rewrite_hook = DummyRewriteHook()
106
+ rewrite_hook.mark_rewrite(*names)
107
+
108
+
109
+ class RewriteHook(Protocol):
110
+ def mark_rewrite(self, *names: str) -> None: ...
111
+
112
+
113
+ class DummyRewriteHook:
114
+ """A no-op import hook for when rewriting is disabled."""
115
+
116
+ def mark_rewrite(self, *names: str) -> None:
117
+ pass
118
+
119
+
120
+ class AssertionState:
121
+ """State for the assertion plugin."""
122
+
123
+ def __init__(self, config: Config, mode) -> None:
124
+ self.mode = mode
125
+ self.trace = config.trace.root.get("assertion")
126
+ self.hook: rewrite.AssertionRewritingHook | None = None
127
+
128
+
129
+ def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
130
+ """Try to install the rewrite hook, raise SystemError if it fails."""
131
+ config.stash[assertstate_key] = AssertionState(config, "rewrite")
132
+ config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
133
+ sys.meta_path.insert(0, hook)
134
+ config.stash[assertstate_key].trace("installed rewrite import hook")
135
+
136
+ def undo() -> None:
137
+ hook = config.stash[assertstate_key].hook
138
+ if hook is not None and hook in sys.meta_path:
139
+ sys.meta_path.remove(hook)
140
+
141
+ config.add_cleanup(undo)
142
+ return hook
143
+
144
+
145
+ def pytest_collection(session: Session) -> None:
146
+ # This hook is only called when test modules are collected
147
+ # so for example not in the managing process of pytest-xdist
148
+ # (which does not collect test modules).
149
+ assertstate = session.config.stash.get(assertstate_key, None)
150
+ if assertstate:
151
+ if assertstate.hook is not None:
152
+ assertstate.hook.set_session(session)
153
+
154
+
155
+ @hookimpl(wrapper=True, tryfirst=True)
156
+ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
157
+ """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
158
+
159
+ The rewrite module will use util._reprcompare if it exists to use custom
160
+ reporting via the pytest_assertrepr_compare hook. This sets up this custom
161
+ comparison for the test.
162
+ """
163
+ ihook = item.ihook
164
+
165
+ def callbinrepr(op, left: object, right: object) -> str | None:
166
+ """Call the pytest_assertrepr_compare hook and prepare the result.
167
+
168
+ This uses the first result from the hook and then ensures the
169
+ following:
170
+ * Overly verbose explanations are truncated unless configured otherwise
171
+ (eg. if running in verbose mode).
172
+ * Embedded newlines are escaped to help util.format_explanation()
173
+ later.
174
+ * If the rewrite mode is used embedded %-characters are replaced
175
+ to protect later % formatting.
176
+
177
+ The result can be formatted by util.format_explanation() for
178
+ pretty printing.
179
+ """
180
+ hook_result = ihook.pytest_assertrepr_compare(
181
+ config=item.config, op=op, left=left, right=right
182
+ )
183
+ for new_expl in hook_result:
184
+ if new_expl:
185
+ new_expl = truncate.truncate_if_required(new_expl, item)
186
+ new_expl = [line.replace("\n", "\\n") for line in new_expl]
187
+ res = "\n~".join(new_expl)
188
+ if item.config.getvalue("assertmode") == "rewrite":
189
+ res = res.replace("%", "%%")
190
+ return res
191
+ return None
192
+
193
+ saved_assert_hooks = util._reprcompare, util._assertion_pass
194
+ util._reprcompare = callbinrepr
195
+ util._config = item.config
196
+
197
+ if ihook.pytest_assertion_pass.get_hookimpls():
198
+
199
+ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
200
+ ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl)
201
+
202
+ util._assertion_pass = call_assertion_pass_hook
203
+
204
+ try:
205
+ return (yield)
206
+ finally:
207
+ util._reprcompare, util._assertion_pass = saved_assert_hooks
208
+ util._config = None
209
+
210
+
211
+ def pytest_sessionfinish(session: Session) -> None:
212
+ assertstate = session.config.stash.get(assertstate_key, None)
213
+ if assertstate:
214
+ if assertstate.hook is not None:
215
+ assertstate.hook.set_session(None)
216
+
217
+
218
+ def pytest_assertrepr_compare(
219
+ config: Config, op: str, left: Any, right: Any
220
+ ) -> list[str] | None:
221
+ if config.pluginmanager.has_plugin("terminalreporter"):
222
+ highlighter = config.get_terminal_writer()._highlight
223
+ else:
224
+ # Keep it plaintext when not using terminalrepoterer (#14377).
225
+ highlighter = util.dummy_highlighter
226
+ explanation = list(
227
+ util.assertrepr_compare(
228
+ op=op,
229
+ left=left,
230
+ right=right,
231
+ verbose=config.get_verbosity(Config.VERBOSITY_ASSERTIONS),
232
+ highlighter=highlighter,
233
+ assertion_text_diff_style=util.get_assertion_text_diff_style(config),
234
+ )
235
+ )
236
+ return explanation or None
.venv/lib/python3.14/site-packages/_pytest/assertion/_compare_any.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ import dataclasses
5
+ import pprint
6
+
7
+ from _pytest.assertion._compare_mapping import _compare_eq_mapping
8
+ from _pytest.assertion._compare_sequence import _compare_eq_iterable
9
+ from _pytest.assertion._compare_sequence import _compare_eq_sequence
10
+ from _pytest.assertion._compare_set import _compare_eq_set
11
+ from _pytest.assertion._guards import has_default_eq
12
+ from _pytest.assertion._guards import isattrs
13
+ from _pytest.assertion._guards import isdatacls
14
+ from _pytest.assertion._guards import isiterable
15
+ from _pytest.assertion._guards import ismapping
16
+ from _pytest.assertion._guards import isnamedtuple
17
+ from _pytest.assertion._guards import issequence
18
+ from _pytest.assertion._guards import isset
19
+ from _pytest.assertion._guards import istext
20
+ from _pytest.assertion._typing import _AssertionTextDiffStyle
21
+ from _pytest.assertion._typing import _HighlightFunc
22
+ from _pytest.assertion.compare_text import _compare_eq_text
23
+
24
+
25
+ def _compare_eq_any(
26
+ left: object,
27
+ right: object,
28
+ highlighter: _HighlightFunc,
29
+ verbose: int,
30
+ assertion_text_diff_style: _AssertionTextDiffStyle,
31
+ ) -> Iterator[str]:
32
+ """Yield the per-line explanation for ``left == right`` (without summary).
33
+
34
+ Yields nothing when no specialised explanation applies, so consumers
35
+ can stream the output and bail out early (e.g. for truncation) without
36
+ materialising the entire diff first.
37
+ """
38
+ if istext(left) and istext(right):
39
+ yield from _compare_eq_text(
40
+ left,
41
+ right,
42
+ highlighter,
43
+ verbose,
44
+ assertion_text_diff_style,
45
+ )
46
+ else:
47
+ from _pytest.python_api import ApproxBase
48
+
49
+ # Although the common order should be obtained == approx(...), allow both ways.
50
+ if isinstance(right, ApproxBase):
51
+ yield from right._repr_compare(left)
52
+ elif isinstance(left, ApproxBase):
53
+ yield from left._repr_compare(right)
54
+ elif type(left) is type(right) and (
55
+ isdatacls(left) or isattrs(left) or isnamedtuple(left)
56
+ ):
57
+ # Note: unlike dataclasses/attrs, namedtuples compare only the
58
+ # field values, not the type or field names. But this branch
59
+ # intentionally only handles the same-type case, which was often
60
+ # used in older code bases before dataclasses/attrs were available.
61
+ yield from _compare_eq_cls(
62
+ left,
63
+ right,
64
+ highlighter,
65
+ verbose,
66
+ assertion_text_diff_style,
67
+ )
68
+ elif issequence(left) and issequence(right):
69
+ yield from _compare_eq_sequence(left, right, highlighter, verbose)
70
+ elif isset(left) and isset(right):
71
+ yield from _compare_eq_set(left, right, highlighter, verbose)
72
+ elif ismapping(left) and ismapping(right):
73
+ yield from _compare_eq_mapping(left, right, highlighter, verbose)
74
+
75
+ if isiterable(left) and isiterable(right):
76
+ yield from _compare_eq_iterable(left, right, highlighter, verbose)
77
+
78
+
79
+ def _compare_eq_cls(
80
+ left: object,
81
+ right: object,
82
+ highlighter: _HighlightFunc,
83
+ verbose: int,
84
+ assertion_text_diff_style: _AssertionTextDiffStyle,
85
+ ) -> Iterator[str]:
86
+ if not has_default_eq(left):
87
+ return
88
+ if isdatacls(left):
89
+ all_fields = dataclasses.fields(left)
90
+ fields_to_check = [info.name for info in all_fields if info.compare]
91
+ elif isattrs(left):
92
+ all_fields = left.__attrs_attrs__ # type: ignore[attr-defined]
93
+ fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
94
+ elif isnamedtuple(left):
95
+ fields_to_check = left._fields # type: ignore[attr-defined]
96
+ else:
97
+ assert False
98
+
99
+ indent = " "
100
+ same = []
101
+ diff = []
102
+ for field in fields_to_check:
103
+ if getattr(left, field) == getattr(right, field):
104
+ same.append(field)
105
+ else:
106
+ diff.append(field)
107
+
108
+ if same or diff:
109
+ yield ""
110
+ if same and verbose < 2:
111
+ yield f"Omitting {len(same)} identical items, use -vv to show"
112
+ elif same:
113
+ yield "Matching attributes:"
114
+ yield from highlighter(pprint.pformat(same)).splitlines()
115
+ if diff:
116
+ yield "Differing attributes:"
117
+ yield from highlighter(pprint.pformat(diff)).splitlines()
118
+ for field in diff:
119
+ field_left = getattr(left, field)
120
+ field_right = getattr(right, field)
121
+ yield ""
122
+ yield f"Drill down into differing attribute {field}:"
123
+ yield f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}"
124
+ for line in _compare_eq_any(
125
+ field_left,
126
+ field_right,
127
+ highlighter,
128
+ verbose,
129
+ assertion_text_diff_style,
130
+ ):
131
+ yield indent + line
.venv/lib/python3.14/site-packages/_pytest/assertion/_compare_mapping.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from collections.abc import Mapping
5
+ import pprint
6
+
7
+ from _pytest._io.saferepr import saferepr
8
+ from _pytest.assertion._typing import _HighlightFunc
9
+
10
+
11
+ def _compare_eq_mapping(
12
+ left: Mapping[object, object],
13
+ right: Mapping[object, object],
14
+ highlighter: _HighlightFunc,
15
+ verbose: int = 0,
16
+ ) -> Iterator[str]:
17
+ set_left = set(left)
18
+ set_right = set(right)
19
+ common = set_left.intersection(set_right)
20
+ same = {k: left[k] for k in common if left[k] == right[k]}
21
+ if same and verbose < 2:
22
+ yield f"Omitting {len(same)} identical items, use -vv to show"
23
+ elif same:
24
+ yield "Common items:"
25
+ yield from highlighter(pprint.pformat(same)).splitlines()
26
+ diff = {k for k in common if left[k] != right[k]}
27
+ if diff:
28
+ yield "Differing items:"
29
+ for k in diff:
30
+ yield (
31
+ highlighter(saferepr({k: left[k]}))
32
+ + " != "
33
+ + highlighter(saferepr({k: right[k]}))
34
+ )
35
+ extra_left = set_left - set_right
36
+ len_extra_left = len(extra_left)
37
+ if len_extra_left:
38
+ yield f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:"
39
+ yield from highlighter(
40
+ pprint.pformat({k: left[k] for k in extra_left})
41
+ ).splitlines()
42
+ extra_right = set_right - set_left
43
+ len_extra_right = len(extra_right)
44
+ if len_extra_right:
45
+ yield f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:"
46
+ yield from highlighter(
47
+ pprint.pformat({k: right[k] for k in extra_right})
48
+ ).splitlines()
.venv/lib/python3.14/site-packages/_pytest/assertion/_compare_sequence.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from collections.abc import Iterator
5
+ from collections.abc import Sequence
6
+
7
+ from _pytest._io.pprint import PrettyPrinter
8
+ from _pytest._io.saferepr import saferepr
9
+ from _pytest.assertion._typing import _HighlightFunc
10
+ from _pytest.compat import running_on_ci
11
+
12
+
13
+ def _compare_eq_iterable(
14
+ left: Iterable[object],
15
+ right: Iterable[object],
16
+ highlighter: _HighlightFunc,
17
+ verbose: int = 0,
18
+ ) -> Iterator[str]:
19
+ if verbose <= 0 and not running_on_ci():
20
+ yield "Use -v to get more diff"
21
+ return
22
+ # dynamic import to speedup pytest
23
+ import difflib
24
+
25
+ left_formatting = PrettyPrinter().pformat(left).splitlines()
26
+ right_formatting = PrettyPrinter().pformat(right).splitlines()
27
+
28
+ yield ""
29
+ yield "Full diff:"
30
+ # "right" is the expected base against which we compare "left",
31
+ # see https://github.com/pytest-dev/pytest/issues/3333
32
+ yield from highlighter(
33
+ "\n".join(
34
+ line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
35
+ ),
36
+ lexer="diff",
37
+ ).splitlines()
38
+
39
+
40
+ def _compare_eq_sequence(
41
+ left: Sequence[object],
42
+ right: Sequence[object],
43
+ highlighter: _HighlightFunc,
44
+ verbose: int = 0,
45
+ ) -> Iterator[str]:
46
+ comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
47
+ len_left = len(left)
48
+ len_right = len(right)
49
+ for i in range(min(len_left, len_right)):
50
+ if left[i] != right[i]:
51
+ if comparing_bytes:
52
+ # when comparing bytes, we want to see their ascii representation
53
+ # instead of their numeric values (#5260)
54
+ # using a slice gives us the ascii representation:
55
+ # >>> s = b'foo'
56
+ # >>> s[0]
57
+ # 102
58
+ # >>> s[0:1]
59
+ # b'f'
60
+ left_value: object = left[i : i + 1]
61
+ right_value: object = right[i : i + 1]
62
+ else:
63
+ left_value = left[i]
64
+ right_value = right[i]
65
+
66
+ yield (
67
+ f"At index {i} diff:"
68
+ f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
69
+ )
70
+ break
71
+
72
+ if comparing_bytes:
73
+ # when comparing bytes, it doesn't help to show the "sides contain one or more
74
+ # items" longer explanation, so skip it
75
+ return
76
+
77
+ len_diff = len_left - len_right
78
+ if len_diff:
79
+ if len_diff > 0:
80
+ dir_with_more = "Left"
81
+ extra = saferepr(left[len_right])
82
+ else:
83
+ len_diff = 0 - len_diff
84
+ dir_with_more = "Right"
85
+ extra = saferepr(right[len_left])
86
+
87
+ if len_diff == 1:
88
+ yield f"{dir_with_more} contains one more item: {highlighter(extra)}"
89
+ else:
90
+ yield f"{dir_with_more} contains {len_diff} more items, first extra item: {highlighter(extra)}"
.venv/lib/python3.14/site-packages/_pytest/assertion/_compare_set.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from collections.abc import Iterator
5
+ from collections.abc import Set as AbstractSet
6
+ from typing import TypeAlias
7
+
8
+ from _pytest._io.saferepr import saferepr
9
+ from _pytest.assertion._typing import _HighlightFunc
10
+
11
+
12
+ def _set_one_sided_diff(
13
+ posn: str,
14
+ set1: AbstractSet[object],
15
+ set2: AbstractSet[object],
16
+ highlighter: _HighlightFunc,
17
+ ) -> Iterator[str]:
18
+ diff = set1 - set2
19
+ if diff:
20
+ yield f"Extra items in the {posn} set:"
21
+ for item in diff:
22
+ yield highlighter(saferepr(item))
23
+
24
+
25
+ def _compare_eq_set(
26
+ left: AbstractSet[object],
27
+ right: AbstractSet[object],
28
+ highlighter: _HighlightFunc,
29
+ verbose: int = 0,
30
+ ) -> Iterator[str]:
31
+ yield from _set_one_sided_diff("left", left, right, highlighter)
32
+ yield from _set_one_sided_diff("right", right, left, highlighter)
33
+
34
+
35
+ def _compare_gte_set(
36
+ left: AbstractSet[object],
37
+ right: AbstractSet[object],
38
+ highlighter: _HighlightFunc,
39
+ verbose: int = 0,
40
+ ) -> Iterator[str]:
41
+ yield from _set_one_sided_diff("right", right, left, highlighter)
42
+
43
+
44
+ def _compare_lte_set(
45
+ left: AbstractSet[object],
46
+ right: AbstractSet[object],
47
+ highlighter: _HighlightFunc,
48
+ verbose: int = 0,
49
+ ) -> Iterator[str]:
50
+ yield from _set_one_sided_diff("left", left, right, highlighter)
51
+
52
+
53
+ def _compare_gt_set(
54
+ left: AbstractSet[object],
55
+ right: AbstractSet[object],
56
+ highlighter: _HighlightFunc,
57
+ verbose: int = 0,
58
+ ) -> Iterator[str]:
59
+ if left == right:
60
+ yield "Both sets are equal"
61
+ else:
62
+ yield from _set_one_sided_diff("right", right, left, highlighter)
63
+
64
+
65
+ def _compare_lt_set(
66
+ left: AbstractSet[object],
67
+ right: AbstractSet[object],
68
+ highlighter: _HighlightFunc,
69
+ verbose: int = 0,
70
+ ) -> Iterator[str]:
71
+ if left == right:
72
+ yield "Both sets are equal"
73
+ else:
74
+ yield from _set_one_sided_diff("left", left, right, highlighter)
75
+
76
+
77
+ SetComparisonFunction: TypeAlias = Callable[
78
+ [AbstractSet[object], AbstractSet[object], _HighlightFunc, int],
79
+ Iterator[str],
80
+ ]
81
+
82
+
83
+ def _both_sets_are_equal(
84
+ left: AbstractSet[object],
85
+ right: AbstractSet[object],
86
+ highlighter: _HighlightFunc,
87
+ verbose: int = 0,
88
+ ) -> Iterator[str]:
89
+ yield "Both sets are equal"
90
+
91
+
92
+ SET_COMPARISON_FUNCTIONS: dict[str, SetComparisonFunction] = {
93
+ # == can't be done here without a prior refactor because there's an additional
94
+ # explanation for iterable in _compare_eq_any
95
+ # "==": _compare_eq_set,
96
+ "!=": _both_sets_are_equal,
97
+ ">=": _compare_gte_set,
98
+ "<=": _compare_lte_set,
99
+ ">": _compare_gt_set,
100
+ "<": _compare_lt_set,
101
+ }
.venv/lib/python3.14/site-packages/_pytest/assertion/_guards.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import collections.abc
4
+ from collections.abc import Mapping
5
+ import dataclasses
6
+ from typing import TypeGuard
7
+
8
+
9
+ def issequence(x: object) -> TypeGuard[collections.abc.Sequence[object]]:
10
+ return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
11
+
12
+
13
+ def istext(x: object) -> TypeGuard[str]:
14
+ return isinstance(x, str)
15
+
16
+
17
+ def ismapping(x: object) -> TypeGuard[Mapping[object, object]]:
18
+ return isinstance(x, Mapping)
19
+
20
+
21
+ def isset(x: object) -> TypeGuard[set[object] | frozenset[object]]:
22
+ return isinstance(x, set | frozenset)
23
+
24
+
25
+ def isnamedtuple(obj: object) -> bool:
26
+ return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
27
+
28
+
29
+ isdatacls = dataclasses.is_dataclass
30
+
31
+
32
+ def isattrs(obj: object) -> bool:
33
+ return getattr(obj, "__attrs_attrs__", None) is not None
34
+
35
+
36
+ def isiterable(obj: object) -> TypeGuard[collections.abc.Iterable[object]]:
37
+ try:
38
+ iter(obj) # type: ignore[call-overload]
39
+ return not istext(obj)
40
+ except Exception:
41
+ return False
42
+
43
+
44
+ def has_default_eq(obj: object) -> bool:
45
+ """Check if an instance of an object contains the default eq
46
+
47
+ First, we check if the object's __eq__ attribute has __code__,
48
+ if so, we check the equally of the method code filename (__code__.co_filename)
49
+ to the default one generated by the dataclass and attr module
50
+ for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
51
+ """
52
+ # inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
53
+ if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
54
+ code_filename = obj.__eq__.__code__.co_filename
55
+
56
+ if isattrs(obj):
57
+ return "attrs generated " in code_filename
58
+
59
+ return code_filename == "<string>" # data class
60
+ return True
.venv/lib/python3.14/site-packages/_pytest/assertion/_typing.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+ from typing import Protocol
5
+
6
+
7
+ _AssertionTextDiffStyle = Literal["ndiff", "block"]
8
+
9
+
10
+ class _HighlightFunc(Protocol): # noqa: PYI046
11
+ def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
12
+ """Apply highlighting to the given source."""
.venv/lib/python3.14/site-packages/_pytest/assertion/compare_text.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+
5
+ from _pytest._io.saferepr import saferepr
6
+ from _pytest.assertion._typing import _AssertionTextDiffStyle
7
+ from _pytest.assertion._typing import _HighlightFunc
8
+ from _pytest.assertion.highlight import dummy_highlighter
9
+ from _pytest.compat import assert_never
10
+
11
+
12
+ def _compare_eq_text(
13
+ left: str,
14
+ right: str,
15
+ highlighter: _HighlightFunc,
16
+ verbose: int,
17
+ assertion_text_diff_style: _AssertionTextDiffStyle,
18
+ ) -> Iterator[str]:
19
+ match assertion_text_diff_style:
20
+ case "block":
21
+ yield from _diff_text_block(left, right)
22
+ case "ndiff":
23
+ yield from _diff_text(left, right, highlighter, verbose)
24
+ case unreachable:
25
+ assert_never(unreachable)
26
+
27
+
28
+ def _diff_text_block(left: str, right: str) -> Iterator[str]:
29
+ yield "Left:"
30
+ yield from _format_text_block_lines(left)
31
+ yield ""
32
+ yield "Right:"
33
+ yield from _format_text_block_lines(right)
34
+
35
+
36
+ def _format_text_block_lines(text: str) -> Iterator[str]:
37
+ for line in text.split("\n"):
38
+ yield f" {line}"
39
+
40
+
41
+ def _diff_text(
42
+ left: str, right: str, highlighter: _HighlightFunc, verbose: int = 0
43
+ ) -> Iterator[str]:
44
+ """Yield the explanation for the diff between text.
45
+
46
+ Unless --verbose is used this will skip leading and trailing
47
+ characters which are identical to keep the diff minimal.
48
+ """
49
+ from difflib import ndiff
50
+
51
+ if verbose < 1:
52
+ i = 0 # just in case left or right has zero length
53
+ for i in range(min(len(left), len(right))):
54
+ if left[i] != right[i]:
55
+ break
56
+ if i > 42:
57
+ i -= 10 # Provide some context
58
+ yield f"Skipping {i} identical leading characters in diff, use -v to show"
59
+ left = left[i:]
60
+ right = right[i:]
61
+ if len(left) == len(right):
62
+ for i in range(len(left)):
63
+ if left[-i] != right[-i]:
64
+ break
65
+ if i > 42:
66
+ i -= 10 # Provide some context
67
+ yield (
68
+ f"Skipping {i} identical trailing "
69
+ "characters in diff, use -v to show"
70
+ )
71
+ left = left[:-i]
72
+ right = right[:-i]
73
+ keepends = True
74
+ if left.isspace() or right.isspace():
75
+ left = repr(str(left))
76
+ right = repr(str(right))
77
+ yield "Strings contain only whitespace, escaping them using repr()"
78
+ # "right" is the expected base against which we compare "left",
79
+ # see https://github.com/pytest-dev/pytest/issues/3333
80
+ yield from highlighter(
81
+ "\n".join(
82
+ line.strip("\n")
83
+ for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
84
+ ),
85
+ lexer="diff",
86
+ ).splitlines()
87
+
88
+
89
+ def _notin_text(term: str, text: str, verbose: int = 0) -> Iterator[str]:
90
+ index = text.find(term)
91
+ head = text[:index]
92
+ tail = text[index + len(term) :]
93
+ correct_text = head + tail
94
+ diff = _diff_text(text, correct_text, dummy_highlighter, verbose)
95
+ yield f"{saferepr(term, maxsize=42)} is contained here:"
96
+ for line in diff:
97
+ if line.startswith("Skipping"):
98
+ continue
99
+ if line.startswith("- "):
100
+ continue
101
+ if line.startswith("+ "):
102
+ yield " " + line[2:]
103
+ else:
104
+ yield line
.venv/lib/python3.14/site-packages/_pytest/assertion/highlight.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+
6
+ def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python") -> str:
7
+ """Dummy highlighter that returns the text unprocessed.
8
+
9
+ Needed for _notin_text, as the diff gets post-processed to only show the "+" part.
10
+ """
11
+ return source
.venv/lib/python3.14/site-packages/_pytest/assertion/rewrite.py ADDED
@@ -0,0 +1,1193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from collections.abc import Callable
8
+ from collections.abc import Iterable
9
+ from collections.abc import Iterator
10
+ from collections.abc import Sequence
11
+ import errno
12
+ import functools
13
+ import importlib.abc
14
+ import importlib.machinery
15
+ import importlib.util
16
+ import io
17
+ import itertools
18
+ import marshal
19
+ import os
20
+ from pathlib import Path
21
+ from pathlib import PurePath
22
+ import struct
23
+ import sys
24
+ import tokenize
25
+ import types
26
+ from typing import IO
27
+ from typing import TYPE_CHECKING
28
+
29
+
30
+ if sys.version_info >= (3, 12):
31
+ from importlib.resources.abc import TraversableResources
32
+ else:
33
+ from importlib.abc import TraversableResources
34
+ if sys.version_info < (3, 11):
35
+ from importlib.readers import FileReader
36
+ else:
37
+ from importlib.resources.readers import FileReader
38
+
39
+
40
+ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
41
+ from _pytest._io.saferepr import saferepr
42
+ from _pytest._io.saferepr import saferepr_unlimited
43
+ from _pytest._version import version
44
+ from _pytest.assertion import util
45
+ from _pytest.config import Config
46
+ from _pytest.fixtures import FixtureFunctionDefinition
47
+ from _pytest.main import Session
48
+ from _pytest.pathlib import absolutepath
49
+ from _pytest.pathlib import fnmatch_ex
50
+ from _pytest.stash import StashKey
51
+
52
+
53
+ # fmt: off
54
+ from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
55
+ # fmt:on
56
+
57
+ if TYPE_CHECKING:
58
+ from _pytest.assertion import AssertionState
59
+
60
+
61
+ class Sentinel:
62
+ pass
63
+
64
+
65
+ assertstate_key = StashKey["AssertionState"]()
66
+
67
+ # pytest caches rewritten pycs in pycache dirs
68
+ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
69
+ PYC_EXT = ".py" + ((__debug__ and "c") or "o")
70
+ PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
71
+
72
+ # Special marker that denotes we have just left a scope definition
73
+ _SCOPE_END_MARKER = Sentinel()
74
+
75
+
76
+ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
77
+ """PEP302/PEP451 import hook which rewrites asserts."""
78
+
79
+ def __init__(self, config: Config) -> None:
80
+ self.config = config
81
+ try:
82
+ self.fnpats = config.getini("python_files")
83
+ except ValueError:
84
+ self.fnpats = ["test_*.py", "*_test.py"]
85
+ self.session: Session | None = None
86
+ self._rewritten_names: dict[str, Path] = {}
87
+ self._must_rewrite: set[str] = set()
88
+ # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
89
+ # which might result in infinite recursion (#3506)
90
+ self._writing_pyc = False
91
+ self._basenames_to_check_rewrite = {"conftest"}
92
+ self._marked_for_rewrite_cache: dict[str, bool] = {}
93
+ self._session_paths_checked = False
94
+
95
+ def set_session(self, session: Session | None) -> None:
96
+ self.session = session
97
+ self._session_paths_checked = False
98
+
99
+ # Indirection so we can mock calls to find_spec originated from the hook during testing
100
+ _find_spec = importlib.machinery.PathFinder.find_spec
101
+
102
+ def find_spec(
103
+ self,
104
+ name: str,
105
+ path: Sequence[str | bytes] | None = None,
106
+ target: types.ModuleType | None = None,
107
+ ) -> importlib.machinery.ModuleSpec | None:
108
+ if self._writing_pyc:
109
+ return None
110
+ state = self.config.stash[assertstate_key]
111
+ if self._early_rewrite_bailout(name, state):
112
+ return None
113
+ state.trace(f"find_module called for: {name}")
114
+
115
+ # Type ignored because mypy is confused about the `self` binding here.
116
+ spec = self._find_spec(name, path) # type: ignore
117
+
118
+ if (
119
+ # the import machinery could not find a file to import
120
+ spec is None
121
+ # this is a namespace package (without `__init__.py`)
122
+ # there's nothing to rewrite there
123
+ or spec.origin is None
124
+ # we can only rewrite source files
125
+ or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
126
+ # if the file doesn't exist, we can't rewrite it
127
+ or not os.path.exists(spec.origin)
128
+ ):
129
+ return None
130
+ else:
131
+ fn = spec.origin
132
+
133
+ if not self._should_rewrite(name, fn, state):
134
+ return None
135
+
136
+ return importlib.util.spec_from_file_location(
137
+ name,
138
+ fn,
139
+ loader=self,
140
+ submodule_search_locations=spec.submodule_search_locations,
141
+ )
142
+
143
+ def create_module(
144
+ self, spec: importlib.machinery.ModuleSpec
145
+ ) -> types.ModuleType | None:
146
+ return None # default behaviour is fine
147
+
148
+ def exec_module(self, module: types.ModuleType) -> None:
149
+ assert module.__spec__ is not None
150
+ assert module.__spec__.origin is not None
151
+ fn = Path(module.__spec__.origin)
152
+ state = self.config.stash[assertstate_key]
153
+
154
+ self._rewritten_names[module.__name__] = fn
155
+
156
+ # The requested module looks like a test file, so rewrite it. This is
157
+ # the most magical part of the process: load the source, rewrite the
158
+ # asserts, and load the rewritten source. We also cache the rewritten
159
+ # module code in a special pyc. We must be aware of the possibility of
160
+ # concurrent pytest processes rewriting and loading pycs. To avoid
161
+ # tricky race conditions, we maintain the following invariant: The
162
+ # cached pyc is always a complete, valid pyc. Operations on it must be
163
+ # atomic. POSIX's atomic rename comes in handy.
164
+ write = not sys.dont_write_bytecode
165
+ cache_dir = get_cache_dir(fn)
166
+ if write:
167
+ ok = try_makedirs(cache_dir)
168
+ if not ok:
169
+ write = False
170
+ state.trace(f"read only directory: {cache_dir}")
171
+
172
+ cache_name = fn.name[:-3] + PYC_TAIL
173
+ pyc = cache_dir / cache_name
174
+ # Notice that even if we're in a read-only directory, I'm going
175
+ # to check for a cached pyc. This may not be optimal...
176
+ co = _read_pyc(fn, pyc, state.trace)
177
+ if co is None:
178
+ state.trace(f"rewriting {fn!r}")
179
+ source_stat, co = _rewrite_test(fn, self.config)
180
+ if write:
181
+ self._writing_pyc = True
182
+ try:
183
+ _write_pyc(state, co, source_stat, pyc)
184
+ finally:
185
+ self._writing_pyc = False
186
+ else:
187
+ state.trace(f"found cached rewritten pyc for {fn}")
188
+ exec(co, module.__dict__)
189
+
190
+ def _early_rewrite_bailout(self, name: str, state: AssertionState) -> bool:
191
+ """A fast way to get out of rewriting modules.
192
+
193
+ Profiling has shown that the call to PathFinder.find_spec (inside of
194
+ the find_spec from this class) is a major slowdown, so, this method
195
+ tries to filter what we're sure won't be rewritten before getting to
196
+ it.
197
+ """
198
+ if self.session is not None and not self._session_paths_checked:
199
+ self._session_paths_checked = True
200
+ for initial_path in self.session._initialpaths:
201
+ # Make something as c:/projects/my_project/path.py ->
202
+ # ['c:', 'projects', 'my_project', 'path.py']
203
+ parts = str(initial_path).split(os.sep)
204
+ # add 'path' to basenames to be checked.
205
+ self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
206
+
207
+ # Note: conftest already by default in _basenames_to_check_rewrite.
208
+ parts = name.split(".")
209
+ if parts[-1] in self._basenames_to_check_rewrite:
210
+ return False
211
+
212
+ # For matching the name it must be as if it was a filename.
213
+ path = PurePath(*parts).with_suffix(".py")
214
+
215
+ for pat in self.fnpats:
216
+ # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
217
+ # on the name alone because we need to match against the full path
218
+ if os.path.dirname(pat):
219
+ return False
220
+ if fnmatch_ex(pat, path):
221
+ return False
222
+
223
+ if self._is_marked_for_rewrite(name, state):
224
+ return False
225
+
226
+ state.trace(f"early skip of rewriting module: {name}")
227
+ return True
228
+
229
+ def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool:
230
+ # always rewrite conftest files
231
+ if os.path.basename(fn) == "conftest.py":
232
+ state.trace(f"rewriting conftest file: {fn!r}")
233
+ return True
234
+
235
+ if self.session is not None:
236
+ if self.session.isinitpath(absolutepath(fn)):
237
+ state.trace(f"matched test file (was specified on cmdline): {fn!r}")
238
+ return True
239
+
240
+ # modules not passed explicitly on the command line are only
241
+ # rewritten if they match the naming convention for test files
242
+ fn_path = PurePath(fn)
243
+ for pat in self.fnpats:
244
+ if fnmatch_ex(pat, fn_path):
245
+ state.trace(f"matched test file {fn!r}")
246
+ return True
247
+
248
+ return self._is_marked_for_rewrite(name, state)
249
+
250
+ def _is_marked_for_rewrite(self, name: str, state: AssertionState) -> bool:
251
+ try:
252
+ return self._marked_for_rewrite_cache[name]
253
+ except KeyError:
254
+ for marked in self._must_rewrite:
255
+ if name == marked or name.startswith(marked + "."):
256
+ state.trace(f"matched marked file {name!r} (from {marked!r})")
257
+ self._marked_for_rewrite_cache[name] = True
258
+ return True
259
+
260
+ self._marked_for_rewrite_cache[name] = False
261
+ return False
262
+
263
+ def mark_rewrite(self, *names: str) -> None:
264
+ """Mark import names as needing to be rewritten.
265
+
266
+ The named module or package as well as any nested modules will
267
+ be rewritten on import.
268
+ """
269
+ already_imported = (
270
+ set(names).intersection(sys.modules).difference(self._rewritten_names)
271
+ )
272
+ for name in already_imported:
273
+ mod = sys.modules[name]
274
+ if not AssertionRewriter.is_rewrite_disabled(
275
+ mod.__doc__ or ""
276
+ ) and not isinstance(mod.__loader__, type(self)):
277
+ self._warn_already_imported(name)
278
+ self._must_rewrite.update(names)
279
+ self._marked_for_rewrite_cache.clear()
280
+
281
+ def _warn_already_imported(self, name: str) -> None:
282
+ from _pytest.warning_types import PytestAssertRewriteWarning
283
+
284
+ self.config.issue_config_time_warning(
285
+ PytestAssertRewriteWarning(
286
+ f"Module already imported so cannot be rewritten; {name}"
287
+ ),
288
+ stacklevel=5,
289
+ )
290
+
291
+ def get_data(self, pathname: str | bytes) -> bytes:
292
+ """Optional PEP302 get_data API."""
293
+ with open(pathname, "rb") as f:
294
+ return f.read()
295
+
296
+ def get_resource_reader(self, name: str) -> TraversableResources:
297
+ return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) # type: ignore[arg-type]
298
+
299
+
300
+ def _write_pyc_fp(
301
+ fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType
302
+ ) -> None:
303
+ # Technically, we don't have to have the same pyc format as
304
+ # (C)Python, since these "pycs" should never be seen by builtin
305
+ # import. However, there's little reason to deviate.
306
+ fp.write(importlib.util.MAGIC_NUMBER)
307
+ # https://www.python.org/dev/peps/pep-0552/
308
+ flags = b"\x00\x00\x00\x00"
309
+ fp.write(flags)
310
+ # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
311
+ mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
312
+ size = source_stat.st_size & 0xFFFFFFFF
313
+ # "<LL" stands for 2 unsigned longs, little-endian.
314
+ fp.write(struct.pack("<LL", mtime, size))
315
+ fp.write(marshal.dumps(co))
316
+
317
+
318
+ def _write_pyc(
319
+ state: AssertionState,
320
+ co: types.CodeType,
321
+ source_stat: os.stat_result,
322
+ pyc: Path,
323
+ ) -> bool:
324
+ proc_pyc = f"{pyc}.{os.getpid()}"
325
+ try:
326
+ with open(proc_pyc, "wb") as fp:
327
+ _write_pyc_fp(fp, source_stat, co)
328
+ except OSError as e:
329
+ state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
330
+ return False
331
+
332
+ try:
333
+ os.replace(proc_pyc, pyc)
334
+ except OSError as e:
335
+ state.trace(f"error writing pyc file at {pyc}: {e}")
336
+ # we ignore any failure to write the cache file
337
+ # there are many reasons, permission-denied, pycache dir being a
338
+ # file etc.
339
+ return False
340
+ return True
341
+
342
+
343
+ def _rewrite_test(fn: Path, config: Config) -> tuple[os.stat_result, types.CodeType]:
344
+ """Read and rewrite *fn* and return the code object."""
345
+ stat = os.stat(fn)
346
+ source = fn.read_bytes()
347
+ strfn = str(fn)
348
+ tree = ast.parse(source, filename=strfn)
349
+ rewrite_asserts(tree, source, strfn, config)
350
+ co = compile(tree, strfn, "exec", dont_inherit=True)
351
+ return stat, co
352
+
353
+
354
+ def _read_pyc(
355
+ source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None
356
+ ) -> types.CodeType | None:
357
+ """Possibly read a pytest pyc containing rewritten code.
358
+
359
+ Return rewritten code if successful or None if not.
360
+ """
361
+ try:
362
+ fp = open(pyc, "rb")
363
+ except OSError:
364
+ return None
365
+ with fp:
366
+ try:
367
+ stat_result = os.stat(source)
368
+ mtime = int(stat_result.st_mtime)
369
+ size = stat_result.st_size
370
+ data = fp.read(16)
371
+ except OSError as e:
372
+ trace(f"_read_pyc({source}): OSError {e}")
373
+ return None
374
+ # Check for invalid or out of date pyc file.
375
+ if len(data) != (16):
376
+ trace(f"_read_pyc({source}): invalid pyc (too short)")
377
+ return None
378
+ if data[:4] != importlib.util.MAGIC_NUMBER:
379
+ trace(f"_read_pyc({source}): invalid pyc (bad magic number)")
380
+ return None
381
+ if data[4:8] != b"\x00\x00\x00\x00":
382
+ trace(f"_read_pyc({source}): invalid pyc (unsupported flags)")
383
+ return None
384
+ mtime_data = data[8:12]
385
+ if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
386
+ trace(f"_read_pyc({source}): out of date")
387
+ return None
388
+ size_data = data[12:16]
389
+ if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
390
+ trace(f"_read_pyc({source}): invalid pyc (incorrect size)")
391
+ return None
392
+ try:
393
+ co = marshal.load(fp)
394
+ except Exception as e:
395
+ trace(f"_read_pyc({source}): marshal.load error {e}")
396
+ return None
397
+ if not isinstance(co, types.CodeType):
398
+ trace(f"_read_pyc({source}): not a code object")
399
+ return None
400
+ return co
401
+
402
+
403
+ def rewrite_asserts(
404
+ mod: ast.Module,
405
+ source: bytes,
406
+ module_path: str | None = None,
407
+ config: Config | None = None,
408
+ ) -> None:
409
+ """Rewrite the assert statements in mod."""
410
+ AssertionRewriter(module_path, config, source).run(mod)
411
+
412
+
413
+ def _saferepr(obj: object) -> str:
414
+ r"""Get a safe repr of an object for assertion error messages.
415
+
416
+ The assertion formatting (util.format_explanation()) requires
417
+ newlines to be escaped since they are a special character for it.
418
+ Normally assertion.util.format_explanation() does this but for a
419
+ custom repr it is possible to contain one of the special escape
420
+ sequences, especially '\n{' and '\n}' are likely to be present in
421
+ JSON reprs.
422
+ """
423
+ if isinstance(obj, types.MethodType):
424
+ # for bound methods, skip redundant <bound method ...> information
425
+ return obj.__name__
426
+
427
+ maxsize = _get_maxsize_for_saferepr(util._config)
428
+ if not maxsize:
429
+ return saferepr_unlimited(obj).replace("\n", "\\n")
430
+ return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
431
+
432
+
433
+ def _get_maxsize_for_saferepr(config: Config | None) -> int | None:
434
+ """Get `maxsize` configuration for saferepr based on the given config object."""
435
+ if config is None:
436
+ verbosity = 0
437
+ else:
438
+ verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
439
+ if verbosity >= 2:
440
+ return None
441
+ if verbosity >= 1:
442
+ return DEFAULT_REPR_MAX_SIZE * 10
443
+ return DEFAULT_REPR_MAX_SIZE
444
+
445
+
446
+ def _format_assertmsg(obj: object) -> str:
447
+ r"""Format the custom assertion message given.
448
+
449
+ For strings this simply replaces newlines with '\n~' so that
450
+ util.format_explanation() will preserve them instead of escaping
451
+ newlines. For other objects saferepr() is used first.
452
+ """
453
+ # reprlib appears to have a bug which means that if a string
454
+ # contains a newline it gets escaped, however if an object has a
455
+ # .__repr__() which contains newlines it does not get escaped.
456
+ # However in either case we want to preserve the newline.
457
+ replaces = [("\n", "\n~"), ("%", "%%")]
458
+ if not isinstance(obj, str):
459
+ obj = saferepr(obj, _get_maxsize_for_saferepr(util._config))
460
+ replaces.append(("\\n", "\n~"))
461
+
462
+ for r1, r2 in replaces:
463
+ obj = obj.replace(r1, r2)
464
+
465
+ return obj
466
+
467
+
468
+ def _should_repr_global_name(obj: object) -> bool:
469
+ if callable(obj):
470
+ # For pytest fixtures the __repr__ method provides more information than the function name.
471
+ return isinstance(obj, FixtureFunctionDefinition)
472
+
473
+ try:
474
+ return not hasattr(obj, "__name__")
475
+ except Exception:
476
+ return True
477
+
478
+
479
+ def _format_boolop(explanations: Iterable[str], is_or: bool) -> str:
480
+ explanation = "(" + ((is_or and " or ") or " and ").join(explanations) + ")"
481
+ return explanation.replace("%", "%%")
482
+
483
+
484
+ def _call_reprcompare(
485
+ ops: Sequence[str],
486
+ results: Sequence[bool],
487
+ expls: Sequence[str],
488
+ each_obj: Sequence[object],
489
+ ) -> str:
490
+ for i, res, expl in zip(range(len(ops)), results, expls, strict=True):
491
+ try:
492
+ done = not res
493
+ except Exception:
494
+ done = True
495
+ if done:
496
+ break
497
+ if util._reprcompare is not None:
498
+ custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
499
+ if custom is not None:
500
+ return custom
501
+ return expl
502
+
503
+
504
+ def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
505
+ if util._assertion_pass is not None:
506
+ util._assertion_pass(lineno, orig, expl)
507
+
508
+
509
+ def _check_if_assertion_pass_impl() -> bool:
510
+ """Check if any plugins implement the pytest_assertion_pass hook
511
+ in order not to generate explanation unnecessarily (might be expensive)."""
512
+ return True if util._assertion_pass else False
513
+
514
+
515
+ UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
516
+
517
+ BINOP_MAP = {
518
+ ast.BitOr: "|",
519
+ ast.BitXor: "^",
520
+ ast.BitAnd: "&",
521
+ ast.LShift: "<<",
522
+ ast.RShift: ">>",
523
+ ast.Add: "+",
524
+ ast.Sub: "-",
525
+ ast.Mult: "*",
526
+ ast.Div: "/",
527
+ ast.FloorDiv: "//",
528
+ ast.Mod: "%%", # escaped for string formatting
529
+ ast.Eq: "==",
530
+ ast.NotEq: "!=",
531
+ ast.Lt: "<",
532
+ ast.LtE: "<=",
533
+ ast.Gt: ">",
534
+ ast.GtE: ">=",
535
+ ast.Pow: "**",
536
+ ast.Is: "is",
537
+ ast.IsNot: "is not",
538
+ ast.In: "in",
539
+ ast.NotIn: "not in",
540
+ ast.MatMult: "@",
541
+ }
542
+
543
+
544
+ def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
545
+ """Recursively yield node and all its children in depth-first order."""
546
+ yield node
547
+ for child in ast.iter_child_nodes(node):
548
+ yield from traverse_node(child)
549
+
550
+
551
+ @functools.lru_cache(maxsize=1)
552
+ def _get_assertion_exprs(src: bytes) -> dict[int, str]:
553
+ """Return a mapping from {lineno: "assertion test expression"}."""
554
+ ret: dict[int, str] = {}
555
+
556
+ depth = 0
557
+ lines: list[str] = []
558
+ assert_lineno: int | None = None
559
+ seen_lines: set[int] = set()
560
+
561
+ def _write_and_reset() -> None:
562
+ nonlocal depth, lines, assert_lineno, seen_lines
563
+ assert assert_lineno is not None
564
+ ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\")
565
+ depth = 0
566
+ lines = []
567
+ assert_lineno = None
568
+ seen_lines = set()
569
+
570
+ tokens = tokenize.tokenize(io.BytesIO(src).readline)
571
+ for tp, source, (lineno, offset), _, line in tokens:
572
+ if tp == tokenize.NAME and source == "assert":
573
+ assert_lineno = lineno
574
+ elif assert_lineno is not None:
575
+ # keep track of depth for the assert-message `,` lookup
576
+ if tp == tokenize.OP and source in "([{":
577
+ depth += 1
578
+ elif tp == tokenize.OP and source in ")]}":
579
+ depth -= 1
580
+
581
+ if not lines:
582
+ lines.append(line[offset:])
583
+ seen_lines.add(lineno)
584
+ # a non-nested comma separates the expression from the message
585
+ elif depth == 0 and tp == tokenize.OP and source == ",":
586
+ # one line assert with message
587
+ if lineno in seen_lines and len(lines) == 1:
588
+ offset_in_trimmed = offset + len(lines[-1]) - len(line)
589
+ lines[-1] = lines[-1][:offset_in_trimmed]
590
+ # multi-line assert with message
591
+ elif lineno in seen_lines:
592
+ lines[-1] = lines[-1][:offset]
593
+ # multi line assert with escaped newline before message
594
+ else:
595
+ lines.append(line[:offset])
596
+ _write_and_reset()
597
+ elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}:
598
+ _write_and_reset()
599
+ elif lines and lineno not in seen_lines:
600
+ lines.append(line)
601
+ seen_lines.add(lineno)
602
+
603
+ return ret
604
+
605
+
606
+ class AssertionRewriter(ast.NodeVisitor):
607
+ """Assertion rewriting implementation.
608
+
609
+ The main entrypoint is to call .run() with an ast.Module instance,
610
+ this will then find all the assert statements and rewrite them to
611
+ provide intermediate values and a detailed assertion error. See
612
+ http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
613
+ for an overview of how this works.
614
+
615
+ The entry point here is .run() which will iterate over all the
616
+ statements in an ast.Module and for each ast.Assert statement it
617
+ finds call .visit() with it. Then .visit_Assert() takes over and
618
+ is responsible for creating new ast statements to replace the
619
+ original assert statement: it rewrites the test of an assertion
620
+ to provide intermediate values and replace it with an if statement
621
+ which raises an assertion error with a detailed explanation in
622
+ case the expression is false and calls pytest_assertion_pass hook
623
+ if expression is true.
624
+
625
+ For this .visit_Assert() uses the visitor pattern to visit all the
626
+ AST nodes of the ast.Assert.test field, each visit call returning
627
+ an AST node and the corresponding explanation string. During this
628
+ state is kept in several instance attributes:
629
+
630
+ :statements: All the AST statements which will replace the assert
631
+ statement.
632
+
633
+ :variables: This is populated by .variable() with each variable
634
+ used by the statements so that they can all be set to None at
635
+ the end of the statements.
636
+
637
+ :variable_counter: Counter to create new unique variables needed
638
+ by statements. Variables are created using .variable() and
639
+ have the form of "@py_assert0".
640
+
641
+ :expl_stmts: The AST statements which will be executed to get
642
+ data from the assertion. This is the code which will construct
643
+ the detailed assertion message that is used in the AssertionError
644
+ or for the pytest_assertion_pass hook.
645
+
646
+ :explanation_specifiers: A dict filled by .explanation_param()
647
+ with %-formatting placeholders and their corresponding
648
+ expressions to use in the building of an assertion message.
649
+ This is used by .pop_format_context() to build a message.
650
+
651
+ :stack: A stack of the explanation_specifiers dicts maintained by
652
+ .push_format_context() and .pop_format_context() which allows
653
+ to build another %-formatted string while already building one.
654
+
655
+ :scope: A tuple containing the current scope used for variables_overwrite.
656
+
657
+ :variables_overwrite: A dict filled with references to variables
658
+ that change value within an assert. This happens when a variable is
659
+ reassigned with the walrus operator
660
+
661
+ This state, except the variables_overwrite, is reset on every new assert
662
+ statement visited and used by the other visitors.
663
+ """
664
+
665
+ def __init__(
666
+ self, module_path: str | None, config: Config | None, source: bytes
667
+ ) -> None:
668
+ super().__init__()
669
+ self.module_path = module_path
670
+ self.config = config
671
+ if config is not None:
672
+ self.enable_assertion_pass_hook = config.getini(
673
+ "enable_assertion_pass_hook"
674
+ )
675
+ else:
676
+ self.enable_assertion_pass_hook = False
677
+ self.source = source
678
+ self.scope: tuple[ast.AST, ...] = ()
679
+ self.variables_overwrite: defaultdict[tuple[ast.AST, ...], dict[str, str]] = (
680
+ defaultdict(dict)
681
+ )
682
+
683
+ def run(self, mod: ast.Module) -> None:
684
+ """Find all assert statements in *mod* and rewrite them."""
685
+ if not mod.body:
686
+ # Nothing to do.
687
+ return
688
+
689
+ # We'll insert some special imports at the top of the module, but after any
690
+ # docstrings and __future__ imports, so first figure out where that is.
691
+ doc = getattr(mod, "docstring", None)
692
+ expect_docstring = doc is None
693
+ if doc is not None and self.is_rewrite_disabled(doc):
694
+ return
695
+ pos = 0
696
+ for item in mod.body:
697
+ match item:
698
+ case ast.Expr(value=ast.Constant(value=str() as doc)) if (
699
+ expect_docstring
700
+ ):
701
+ if self.is_rewrite_disabled(doc):
702
+ return
703
+ expect_docstring = False
704
+ case ast.ImportFrom(level=0, module="__future__"):
705
+ pass
706
+ case _:
707
+ break
708
+ pos += 1
709
+ # Special case: for a decorated function, set the lineno to that of the
710
+ # first decorator, not the `def`. Issue #4984.
711
+ if isinstance(item, ast.FunctionDef) and item.decorator_list:
712
+ lineno = item.decorator_list[0].lineno
713
+ else:
714
+ lineno = item.lineno
715
+ # Now actually insert the special imports.
716
+ aliases = [
717
+ ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0),
718
+ ast.alias(
719
+ "_pytest.assertion.rewrite",
720
+ "@pytest_ar",
721
+ lineno=lineno,
722
+ col_offset=0,
723
+ ),
724
+ ]
725
+ imports = [
726
+ ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases
727
+ ]
728
+ mod.body[pos:pos] = imports
729
+
730
+ # Collect asserts.
731
+ self.scope = (mod,)
732
+ nodes: list[ast.AST | Sentinel] = [mod]
733
+ while nodes:
734
+ node = nodes.pop()
735
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
736
+ self.scope = tuple((*self.scope, node))
737
+ nodes.append(_SCOPE_END_MARKER)
738
+ if node == _SCOPE_END_MARKER:
739
+ self.scope = self.scope[:-1]
740
+ continue
741
+ assert isinstance(node, ast.AST)
742
+ for name, field in ast.iter_fields(node):
743
+ if isinstance(field, list):
744
+ new: list[ast.AST] = []
745
+ for i, child in enumerate(field):
746
+ if isinstance(child, ast.Assert):
747
+ # Transform assert.
748
+ new.extend(self.visit(child))
749
+ else:
750
+ new.append(child)
751
+ if isinstance(child, ast.AST):
752
+ nodes.append(child)
753
+ setattr(node, name, new)
754
+ elif (
755
+ isinstance(field, ast.AST)
756
+ # Don't recurse into expressions as they can't contain
757
+ # asserts.
758
+ and not isinstance(field, ast.expr)
759
+ ):
760
+ nodes.append(field)
761
+
762
+ @staticmethod
763
+ def is_rewrite_disabled(docstring: str) -> bool:
764
+ return "PYTEST_DONT_REWRITE" in docstring
765
+
766
+ def variable(self) -> str:
767
+ """Get a new variable."""
768
+ # Use a character invalid in python identifiers to avoid clashing.
769
+ name = "@py_assert" + str(next(self.variable_counter))
770
+ self.variables.append(name)
771
+ return name
772
+
773
+ def assign(self, expr: ast.expr) -> ast.Name:
774
+ """Give *expr* a name."""
775
+ name = self.variable()
776
+ self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
777
+ return ast.copy_location(ast.Name(name, ast.Load()), expr)
778
+
779
+ def display(self, expr: ast.expr) -> ast.expr:
780
+ """Call saferepr on the expression."""
781
+ return self.helper("_saferepr", expr)
782
+
783
+ def helper(self, name: str, *args: ast.expr) -> ast.expr:
784
+ """Call a helper in this module."""
785
+ py_name = ast.Name("@pytest_ar", ast.Load())
786
+ attr = ast.Attribute(py_name, name, ast.Load())
787
+ return ast.Call(attr, list(args), [])
788
+
789
+ def builtin(self, name: str) -> ast.Attribute:
790
+ """Return the builtin called *name*."""
791
+ builtin_name = ast.Name("@py_builtins", ast.Load())
792
+ return ast.Attribute(builtin_name, name, ast.Load())
793
+
794
+ def explanation_param(self, expr: ast.expr) -> str:
795
+ """Return a new named %-formatting placeholder for expr.
796
+
797
+ This creates a %-formatting placeholder for expr in the
798
+ current formatting context, e.g. ``%(py0)s``. The placeholder
799
+ and expr are placed in the current format context so that it
800
+ can be used on the next call to .pop_format_context().
801
+ """
802
+ specifier = "py" + str(next(self.variable_counter))
803
+ self.explanation_specifiers[specifier] = expr
804
+ return "%(" + specifier + ")s"
805
+
806
+ def push_format_context(self) -> None:
807
+ """Create a new formatting context.
808
+
809
+ The format context is used for when an explanation wants to
810
+ have a variable value formatted in the assertion message. In
811
+ this case the value required can be added using
812
+ .explanation_param(). Finally .pop_format_context() is used
813
+ to format a string of %-formatted values as added by
814
+ .explanation_param().
815
+ """
816
+ self.explanation_specifiers: dict[str, ast.expr] = {}
817
+ self.stack.append(self.explanation_specifiers)
818
+
819
+ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
820
+ """Format the %-formatted string with current format context.
821
+
822
+ The expl_expr should be an str ast.expr instance constructed from
823
+ the %-placeholders created by .explanation_param(). This will
824
+ add the required code to format said string to .expl_stmts and
825
+ return the ast.Name instance of the formatted string.
826
+ """
827
+ current = self.stack.pop()
828
+ if self.stack:
829
+ self.explanation_specifiers = self.stack[-1]
830
+ keys: list[ast.expr | None] = [ast.Constant(key) for key in current.keys()]
831
+ format_dict = ast.Dict(keys, list(current.values()))
832
+ form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
833
+ name = "@py_format" + str(next(self.variable_counter))
834
+ if self.enable_assertion_pass_hook:
835
+ self.format_variables.append(name)
836
+ self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form))
837
+ return ast.Name(name, ast.Load())
838
+
839
+ def generic_visit(self, node: ast.AST) -> tuple[ast.Name, str]:
840
+ """Handle expressions we don't have custom code for."""
841
+ assert isinstance(node, ast.expr)
842
+ res = self.assign(node)
843
+ return res, self.explanation_param(self.display(res))
844
+
845
+ def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]:
846
+ """Return the AST statements to replace the ast.Assert instance.
847
+
848
+ This rewrites the test of an assertion to provide
849
+ intermediate values and replace it with an if statement which
850
+ raises an assertion error with a detailed explanation in case
851
+ the expression is false.
852
+ """
853
+ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
854
+ import warnings
855
+
856
+ from _pytest.warning_types import PytestAssertRewriteWarning
857
+
858
+ # TODO: This assert should not be needed.
859
+ assert self.module_path is not None
860
+ warnings.warn_explicit(
861
+ PytestAssertRewriteWarning(
862
+ "assertion is always true, perhaps remove parentheses?"
863
+ ),
864
+ category=None,
865
+ filename=self.module_path,
866
+ lineno=assert_.lineno,
867
+ )
868
+
869
+ self.statements: list[ast.stmt] = []
870
+ self.variables: list[str] = []
871
+ self.variable_counter = itertools.count()
872
+
873
+ if self.enable_assertion_pass_hook:
874
+ self.format_variables: list[str] = []
875
+
876
+ self.stack: list[dict[str, ast.expr]] = []
877
+ self.expl_stmts: list[ast.stmt] = []
878
+ self.push_format_context()
879
+ # Rewrite assert into a bunch of statements.
880
+ top_condition, explanation = self.visit(assert_.test)
881
+
882
+ negation = ast.UnaryOp(ast.Not(), top_condition)
883
+
884
+ if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
885
+ msg = self.pop_format_context(ast.Constant(explanation))
886
+
887
+ # Failed
888
+ if assert_.msg:
889
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
890
+ gluestr = "\n>assert "
891
+ else:
892
+ assertmsg = ast.Constant("")
893
+ gluestr = "assert "
894
+ err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
895
+ err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
896
+ err_name = ast.Name("AssertionError", ast.Load())
897
+ fmt = self.helper("_format_explanation", err_msg)
898
+ exc = ast.Call(err_name, [fmt], [])
899
+ raise_ = ast.Raise(exc, None)
900
+ statements_fail = []
901
+ statements_fail.extend(self.expl_stmts)
902
+ statements_fail.append(raise_)
903
+
904
+ # Passed
905
+ fmt_pass = self.helper("_format_explanation", msg)
906
+ orig = _get_assertion_exprs(self.source)[assert_.lineno]
907
+ hook_call_pass = ast.Expr(
908
+ self.helper(
909
+ "_call_assertion_pass",
910
+ ast.Constant(assert_.lineno),
911
+ ast.Constant(orig),
912
+ fmt_pass,
913
+ )
914
+ )
915
+ # If any hooks implement assert_pass hook
916
+ hook_impl_test = ast.If(
917
+ self.helper("_check_if_assertion_pass_impl"),
918
+ [*self.expl_stmts, hook_call_pass],
919
+ [],
920
+ )
921
+ statements_pass: list[ast.stmt] = [hook_impl_test]
922
+
923
+ # Test for assertion condition
924
+ main_test = ast.If(negation, statements_fail, statements_pass)
925
+ self.statements.append(main_test)
926
+ if self.format_variables:
927
+ variables: list[ast.expr] = [
928
+ ast.Name(name, ast.Store()) for name in self.format_variables
929
+ ]
930
+ clear_format = ast.Assign(variables, ast.Constant(None))
931
+ self.statements.append(clear_format)
932
+
933
+ else: # Original assertion rewriting
934
+ # Create failure message.
935
+ body = self.expl_stmts
936
+ self.statements.append(ast.If(negation, body, []))
937
+ if assert_.msg:
938
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
939
+ explanation = "\n>assert " + explanation
940
+ else:
941
+ assertmsg = ast.Constant("")
942
+ explanation = "assert " + explanation
943
+ template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
944
+ msg = self.pop_format_context(template)
945
+ fmt = self.helper("_format_explanation", msg)
946
+ err_name = ast.Name("AssertionError", ast.Load())
947
+ exc = ast.Call(err_name, [fmt], [])
948
+ raise_ = ast.Raise(exc, None)
949
+
950
+ body.append(raise_)
951
+
952
+ # Clear temporary variables by setting them to None.
953
+ if self.variables:
954
+ variables = [ast.Name(name, ast.Store()) for name in self.variables]
955
+ clear = ast.Assign(variables, ast.Constant(None))
956
+ self.statements.append(clear)
957
+ # Fix locations (line numbers/column offsets).
958
+ for stmt in self.statements:
959
+ for node in traverse_node(stmt):
960
+ if getattr(node, "lineno", None) is None:
961
+ # apply the assertion location to all generated ast nodes without source location
962
+ # and preserve the location of existing nodes or generated nodes with an correct location.
963
+ ast.copy_location(node, assert_)
964
+ return self.statements
965
+
966
+ def visit_NamedExpr(self, name: ast.NamedExpr) -> tuple[ast.NamedExpr, str]:
967
+ # This method handles the 'walrus operator' repr of the target
968
+ # name if it's a local variable or _should_repr_global_name()
969
+ # thinks it's acceptable.
970
+ locs = ast.Call(self.builtin("locals"), [], [])
971
+ target_id = name.target.id
972
+ inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
973
+ dorepr = self.helper("_should_repr_global_name", name)
974
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
975
+ expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
976
+ return name, self.explanation_param(expr)
977
+
978
+ def visit_Name(self, name: ast.Name) -> tuple[ast.Name, str]:
979
+ # Display the repr of the name if it's a local variable or
980
+ # _should_repr_global_name() thinks it's acceptable.
981
+ locs = ast.Call(self.builtin("locals"), [], [])
982
+ inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs])
983
+ dorepr = self.helper("_should_repr_global_name", name)
984
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
985
+ expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
986
+ return name, self.explanation_param(expr)
987
+
988
+ def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]:
989
+ res_var = self.variable()
990
+ expl_list = self.assign(ast.List([], ast.Load()))
991
+ app = ast.Attribute(expl_list, "append", ast.Load())
992
+ is_or = int(isinstance(boolop.op, ast.Or))
993
+ body = save = self.statements
994
+ fail_save = self.expl_stmts
995
+ levels = len(boolop.values) - 1
996
+ self.push_format_context()
997
+ # Process each operand, short-circuiting if needed.
998
+ for i, v in enumerate(boolop.values):
999
+ if i:
1000
+ fail_inner: list[ast.stmt] = []
1001
+ # cond is set in a prior loop iteration below
1002
+ self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
1003
+ self.expl_stmts = fail_inner
1004
+ match v:
1005
+ # Check if the left operand is an ast.NamedExpr and the value has already been visited
1006
+ case ast.Compare(
1007
+ left=ast.NamedExpr(target=ast.Name(id=target_id))
1008
+ ) if target_id in [
1009
+ e.id for e in boolop.values[:i] if hasattr(e, "id")
1010
+ ]:
1011
+ pytest_temp = self.variable()
1012
+ self.variables_overwrite[self.scope][target_id] = v.left # type:ignore[assignment]
1013
+ # mypy's false positive, we're checking that the 'target' attribute exists.
1014
+ v.left.target.id = pytest_temp # type:ignore[attr-defined]
1015
+ self.push_format_context()
1016
+ res, expl = self.visit(v)
1017
+ body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
1018
+ expl_format = self.pop_format_context(ast.Constant(expl))
1019
+ call = ast.Call(app, [expl_format], [])
1020
+ self.expl_stmts.append(ast.Expr(call))
1021
+ if i < levels:
1022
+ cond: ast.expr = res
1023
+ if is_or:
1024
+ cond = ast.UnaryOp(ast.Not(), cond)
1025
+ inner: list[ast.stmt] = []
1026
+ self.statements.append(ast.If(cond, inner, []))
1027
+ self.statements = body = inner
1028
+ self.statements = save
1029
+ self.expl_stmts = fail_save
1030
+ expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
1031
+ expl = self.pop_format_context(expl_template)
1032
+ return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
1033
+
1034
+ def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]:
1035
+ pattern = UNARY_MAP[unary.op.__class__]
1036
+ operand_res, operand_expl = self.visit(unary.operand)
1037
+ res = self.assign(ast.copy_location(ast.UnaryOp(unary.op, operand_res), unary))
1038
+ return res, pattern % (operand_expl,)
1039
+
1040
+ def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]:
1041
+ symbol = BINOP_MAP[binop.op.__class__]
1042
+ left_expr, left_expl = self.visit(binop.left)
1043
+ right_expr, right_expl = self.visit(binop.right)
1044
+ explanation = f"({left_expl} {symbol} {right_expl})"
1045
+ res = self.assign(
1046
+ ast.copy_location(ast.BinOp(left_expr, binop.op, right_expr), binop)
1047
+ )
1048
+ return res, explanation
1049
+
1050
+ def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]:
1051
+ new_func, func_expl = self.visit(call.func)
1052
+ arg_expls = []
1053
+ new_args = []
1054
+ new_kwargs = []
1055
+ for arg in call.args:
1056
+ if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
1057
+ self.scope, {}
1058
+ ):
1059
+ arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
1060
+ res, expl = self.visit(arg)
1061
+ arg_expls.append(expl)
1062
+ new_args.append(res)
1063
+ for keyword in call.keywords:
1064
+ match keyword.value:
1065
+ case ast.Name(id=id) if id in self.variables_overwrite.get(
1066
+ self.scope, {}
1067
+ ):
1068
+ keyword.value = self.variables_overwrite[self.scope][id] # type:ignore[assignment]
1069
+ res, expl = self.visit(keyword.value)
1070
+ new_kwargs.append(ast.keyword(keyword.arg, res))
1071
+ if keyword.arg:
1072
+ arg_expls.append(keyword.arg + "=" + expl)
1073
+ else: # **args have `arg` keywords with an .arg of None
1074
+ arg_expls.append("**" + expl)
1075
+
1076
+ expl = "{}({})".format(func_expl, ", ".join(arg_expls))
1077
+ new_call = ast.copy_location(ast.Call(new_func, new_args, new_kwargs), call)
1078
+ res = self.assign(new_call)
1079
+ res_expl = self.explanation_param(self.display(res))
1080
+ outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
1081
+ return res, outer_expl
1082
+
1083
+ def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]:
1084
+ # A Starred node can appear in a function call.
1085
+ res, expl = self.visit(starred.value)
1086
+ new_starred = ast.Starred(res, starred.ctx)
1087
+ return new_starred, "*" + expl
1088
+
1089
+ def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]:
1090
+ if not isinstance(attr.ctx, ast.Load):
1091
+ return self.generic_visit(attr)
1092
+ value, value_expl = self.visit(attr.value)
1093
+ res = self.assign(
1094
+ ast.copy_location(ast.Attribute(value, attr.attr, ast.Load()), attr)
1095
+ )
1096
+ res_expl = self.explanation_param(self.display(res))
1097
+ pat = "%s\n{%s = %s.%s\n}"
1098
+ expl = pat % (res_expl, res_expl, value_expl, attr.attr)
1099
+ return res, expl
1100
+
1101
+ def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]:
1102
+ self.push_format_context()
1103
+ # We first check if we have overwritten a variable in the previous assert
1104
+ match comp.left:
1105
+ case ast.Name(id=name_id) if name_id in self.variables_overwrite.get(
1106
+ self.scope, {}
1107
+ ):
1108
+ comp.left = self.variables_overwrite[self.scope][name_id] # type: ignore[assignment]
1109
+ case ast.NamedExpr(target=ast.Name(id=target_id)):
1110
+ self.variables_overwrite[self.scope][target_id] = comp.left # type: ignore[assignment]
1111
+ left_res, left_expl = self.visit(comp.left)
1112
+ if isinstance(comp.left, ast.Compare | ast.BoolOp):
1113
+ left_expl = f"({left_expl})"
1114
+ res_variables = [self.variable() for i in range(len(comp.ops))]
1115
+ load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables]
1116
+ store_names = [ast.Name(v, ast.Store()) for v in res_variables]
1117
+ it = zip(range(len(comp.ops)), comp.ops, comp.comparators, strict=True)
1118
+ expls: list[ast.expr] = []
1119
+ syms: list[ast.expr] = []
1120
+ results = [left_res]
1121
+ for i, op, next_operand in it:
1122
+ match (next_operand, left_res):
1123
+ case (
1124
+ ast.NamedExpr(target=ast.Name(id=target_id)),
1125
+ ast.Name(id=name_id),
1126
+ ) if target_id == name_id:
1127
+ next_operand.target.id = self.variable()
1128
+ self.variables_overwrite[self.scope][name_id] = next_operand # type: ignore[assignment]
1129
+
1130
+ next_res, next_expl = self.visit(next_operand)
1131
+ if isinstance(next_operand, ast.Compare | ast.BoolOp):
1132
+ next_expl = f"({next_expl})"
1133
+ results.append(next_res)
1134
+ sym = BINOP_MAP[op.__class__]
1135
+ syms.append(ast.Constant(sym))
1136
+ expl = f"{left_expl} {sym} {next_expl}"
1137
+ expls.append(ast.Constant(expl))
1138
+ res_expr = ast.copy_location(ast.Compare(left_res, [op], [next_res]), comp)
1139
+ self.statements.append(ast.Assign([store_names[i]], res_expr))
1140
+ left_res, left_expl = next_res, next_expl
1141
+ # Use pytest.assertion.util._reprcompare if that's available.
1142
+ expl_call = self.helper(
1143
+ "_call_reprcompare",
1144
+ ast.Tuple(syms, ast.Load()),
1145
+ ast.Tuple(load_names, ast.Load()),
1146
+ ast.Tuple(expls, ast.Load()),
1147
+ ast.Tuple(results, ast.Load()),
1148
+ )
1149
+ if len(comp.ops) > 1:
1150
+ res: ast.expr = ast.BoolOp(ast.And(), load_names)
1151
+ else:
1152
+ res = load_names[0]
1153
+
1154
+ return res, self.explanation_param(self.pop_format_context(expl_call))
1155
+
1156
+
1157
+ def try_makedirs(cache_dir: Path) -> bool:
1158
+ """Attempt to create the given directory and sub-directories exist.
1159
+
1160
+ Returns True if successful or if it already exists.
1161
+ """
1162
+ try:
1163
+ os.makedirs(cache_dir, exist_ok=True)
1164
+ except (FileNotFoundError, NotADirectoryError, FileExistsError):
1165
+ # One of the path components was not a directory:
1166
+ # - we're in a zip file
1167
+ # - it is a file
1168
+ return False
1169
+ except PermissionError:
1170
+ return False
1171
+ except OSError as e:
1172
+ # as of now, EROFS doesn't have an equivalent OSError-subclass
1173
+ #
1174
+ # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not
1175
+ # implemented" for a read-only error
1176
+ if e.errno in {errno.EROFS, errno.ENOSYS}:
1177
+ return False
1178
+ raise
1179
+ return True
1180
+
1181
+
1182
+ def get_cache_dir(file_path: Path) -> Path:
1183
+ """Return the cache directory to write .pyc files for the given .py file path."""
1184
+ if sys.pycache_prefix:
1185
+ # given:
1186
+ # prefix = '/tmp/pycs'
1187
+ # path = '/home/user/proj/test_app.py'
1188
+ # we want:
1189
+ # '/tmp/pycs/home/user/proj'
1190
+ return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1])
1191
+ else:
1192
+ # classic pycache directory
1193
+ return file_path.parent / "__pycache__"
.venv/lib/python3.14/site-packages/_pytest/assertion/truncate.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.compat import running_on_ci
10
+ from _pytest.config import Config
11
+ from _pytest.nodes import Item
12
+
13
+
14
+ DEFAULT_MAX_LINES = 8
15
+ DEFAULT_MAX_CHARS = DEFAULT_MAX_LINES * 80
16
+ USAGE_MSG = "use '-vv' to show"
17
+
18
+
19
+ def truncate_if_required(explanation: list[str], item: Item) -> list[str]:
20
+ """Truncate this assertion explanation if the given test item is eligible."""
21
+ should_truncate, max_lines, max_chars = _get_truncation_parameters(item)
22
+ if should_truncate:
23
+ return _truncate_explanation(
24
+ explanation,
25
+ max_lines=max_lines,
26
+ max_chars=max_chars,
27
+ )
28
+ return explanation
29
+
30
+
31
+ def _get_truncation_parameters(item: Item) -> tuple[bool, int, int]:
32
+ """Return the truncation parameters related to the given item, as (should truncate, max lines, max chars)."""
33
+ # We do not need to truncate if one of conditions is met:
34
+ # 1. Verbosity level is 2 or more;
35
+ # 2. Test is being run in CI environment;
36
+ # 3. Both truncation_limit_lines and truncation_limit_chars
37
+ # .ini parameters are set to 0 explicitly.
38
+ max_lines = item.config.getini("truncation_limit_lines")
39
+ max_lines = int(max_lines if max_lines is not None else DEFAULT_MAX_LINES)
40
+
41
+ max_chars = item.config.getini("truncation_limit_chars")
42
+ max_chars = int(max_chars if max_chars is not None else DEFAULT_MAX_CHARS)
43
+
44
+ verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
45
+
46
+ should_truncate = verbose < 2 and not running_on_ci()
47
+ should_truncate = should_truncate and (max_lines > 0 or max_chars > 0)
48
+
49
+ return should_truncate, max_lines, max_chars
50
+
51
+
52
+ def _truncate_explanation(
53
+ input_lines: list[str],
54
+ max_lines: int,
55
+ max_chars: int,
56
+ ) -> list[str]:
57
+ """Truncate given list of strings that makes up the assertion explanation.
58
+
59
+ Truncates to either max_lines, or max_chars - whichever the input reaches
60
+ first, taking the truncation explanation into account. The remaining lines
61
+ will be replaced by a usage message.
62
+
63
+ If max_chars=0, no truncation by character count is performed.
64
+ If max_lines=0, no truncation by line count is performed.
65
+
66
+ When this function is launched we know max_lines > 0 or max_chars > 0
67
+ because _get_truncation_parameters was called first.
68
+ """
69
+ # The length of the truncation explanation depends on the number of lines
70
+ # removed but is at least 68 characters:
71
+ # The real value is
72
+ # 64 (for the base message:
73
+ # '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
74
+ # )
75
+ # + 1 (for plural)
76
+ # + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
77
+ # + 3 for the '...' added to the truncated line
78
+ # But if there's more than 100 lines it's very likely that we're going to
79
+ # truncate, so we don't need the exact value using log10.
80
+ tolerable_max_chars = (
81
+ max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
82
+ )
83
+ # The truncation explanation add two lines to the output
84
+ if max_lines == 0 or len(input_lines) <= max_lines + 2:
85
+ if max_chars == 0 or sum(len(s) for s in input_lines) <= tolerable_max_chars:
86
+ return input_lines
87
+ truncated_explanation = input_lines
88
+ else:
89
+ # Truncate first to max_lines, and then truncate to max_chars if necessary
90
+ truncated_explanation = input_lines[:max_lines]
91
+ # We reevaluate the need to truncate chars following removal of some lines
92
+ need_to_truncate_char = (
93
+ max_chars > 0
94
+ and sum(len(e) for e in truncated_explanation) > tolerable_max_chars
95
+ )
96
+ if need_to_truncate_char:
97
+ truncated_explanation = _truncate_by_char_count(
98
+ truncated_explanation, max_chars
99
+ )
100
+ # Something was truncated, adding '...' at the end to show that
101
+ truncated_explanation[-1] += "..."
102
+ truncated_line_count = (
103
+ len(input_lines) - len(truncated_explanation) + int(need_to_truncate_char)
104
+ )
105
+ return [
106
+ *truncated_explanation,
107
+ "",
108
+ f"...Full output truncated ({truncated_line_count} line"
109
+ f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
110
+ ]
111
+
112
+
113
+ def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]:
114
+ # Find point at which input length exceeds total allowed length
115
+ iterated_char_count = 0
116
+ for iterated_index, input_line in enumerate(input_lines):
117
+ if iterated_char_count + len(input_line) > max_chars:
118
+ break
119
+ iterated_char_count += len(input_line)
120
+
121
+ # Create truncated explanation with modified final line
122
+ truncated_result = input_lines[:iterated_index]
123
+ final_line = input_lines[iterated_index]
124
+ if final_line:
125
+ final_line_truncate_point = max_chars - iterated_char_count
126
+ final_line = final_line[:final_line_truncate_point]
127
+ truncated_result.append(final_line)
128
+ return truncated_result
.venv/lib/python3.14/site-packages/_pytest/assertion/util.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Utilities for assertion debugging."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable
7
+ from collections.abc import Iterator
8
+ from collections.abc import Sequence
9
+ from typing import Literal
10
+ from unicodedata import normalize
11
+
12
+ from _pytest import outcomes
13
+ import _pytest._code
14
+ from _pytest._io.saferepr import saferepr
15
+ from _pytest._io.saferepr import saferepr_unlimited
16
+ from _pytest.assertion._compare_any import _compare_eq_any
17
+ from _pytest.assertion._compare_set import SET_COMPARISON_FUNCTIONS
18
+ from _pytest.assertion._guards import isset
19
+ from _pytest.assertion._guards import istext
20
+ from _pytest.assertion._typing import _AssertionTextDiffStyle
21
+ from _pytest.assertion._typing import _HighlightFunc
22
+ from _pytest.assertion.compare_text import _notin_text
23
+ from _pytest.assertion.highlight import dummy_highlighter as dummy_highlighter
24
+ from _pytest.config import Config
25
+ from _pytest.config import UsageError
26
+
27
+
28
+ # The _reprcompare attribute on the util module is used by the new assertion
29
+ # interpretation code and assertion rewriter to detect this plugin was
30
+ # loaded and in turn call the hooks defined here as part of the
31
+ # DebugInterpreter.
32
+ _reprcompare: Callable[[str, object, object], str | None] | None = None
33
+
34
+ # Works similarly as _reprcompare attribute. Is populated with the hook call
35
+ # when pytest_runtest_setup is called.
36
+ _assertion_pass: Callable[[int, str, str], None] | None = None
37
+
38
+ # Config object which is assigned during pytest_runtest_protocol.
39
+ _config: Config | None = None
40
+
41
+ ASSERTION_TEXT_DIFF_STYLE_INI = "assertion_text_diff_style"
42
+ ASSERTION_TEXT_DIFF_STYLE_NDIFF: Literal["ndiff"] = "ndiff"
43
+ ASSERTION_TEXT_DIFF_STYLE_BLOCK: Literal["block"] = "block"
44
+ ASSERTION_TEXT_DIFF_STYLE_CHOICES = (
45
+ ASSERTION_TEXT_DIFF_STYLE_NDIFF,
46
+ ASSERTION_TEXT_DIFF_STYLE_BLOCK,
47
+ )
48
+
49
+
50
+ def get_assertion_text_diff_style(config: Config) -> _AssertionTextDiffStyle:
51
+ style = str(config.getini(ASSERTION_TEXT_DIFF_STYLE_INI))
52
+ match style:
53
+ case "ndiff" | "block":
54
+ return style
55
+ case _:
56
+ choices = ", ".join(
57
+ repr(choice) for choice in ASSERTION_TEXT_DIFF_STYLE_CHOICES
58
+ )
59
+ raise UsageError(
60
+ f"{ASSERTION_TEXT_DIFF_STYLE_INI} must be one of {choices}; got {style!r}"
61
+ )
62
+
63
+
64
+ def validate_assertion_text_diff_style(config: Config) -> None:
65
+ get_assertion_text_diff_style(config)
66
+
67
+
68
+ def format_explanation(explanation: str) -> str:
69
+ r"""Format an explanation.
70
+
71
+ Normally all embedded newlines are escaped, however there are
72
+ three exceptions: \n{, \n} and \n~. The first two are intended
73
+ cover nested explanations, see function and attribute explanations
74
+ for examples (.visit_Call(), visit_Attribute()). The last one is
75
+ for when one explanation needs to span multiple lines, e.g. when
76
+ displaying diffs.
77
+ """
78
+ lines = _split_explanation(explanation)
79
+ result = _format_lines(lines)
80
+ return "\n".join(result)
81
+
82
+
83
+ def _split_explanation(explanation: str) -> list[str]:
84
+ r"""Return a list of individual lines in the explanation.
85
+
86
+ This will return a list of lines split on '\n{', '\n}' and '\n~'.
87
+ Any other newlines will be escaped and appear in the line as the
88
+ literal '\n' characters.
89
+ """
90
+ raw_lines = (explanation or "").split("\n")
91
+ lines = [raw_lines[0]]
92
+ for values in raw_lines[1:]:
93
+ if values and values[0] in ["{", "}", "~", ">"]:
94
+ lines.append(values)
95
+ else:
96
+ lines[-1] += "\\n" + values
97
+ return lines
98
+
99
+
100
+ def _format_lines(lines: Sequence[str]) -> list[str]:
101
+ """Format the individual lines.
102
+
103
+ This will replace the '{', '}' and '~' characters of our mini formatting
104
+ language with the proper 'where ...', 'and ...' and ' + ...' text, taking
105
+ care of indentation along the way.
106
+
107
+ Return a list of formatted lines.
108
+ """
109
+ result = list(lines[:1])
110
+ stack = [0]
111
+ stackcnt = [0]
112
+ for line in lines[1:]:
113
+ if line.startswith("{"):
114
+ if stackcnt[-1]:
115
+ s = "and "
116
+ else:
117
+ s = "where "
118
+ stack.append(len(result))
119
+ stackcnt[-1] += 1
120
+ stackcnt.append(0)
121
+ result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
122
+ elif line.startswith("}"):
123
+ stack.pop()
124
+ stackcnt.pop()
125
+ result[stack[-1]] += line[1:]
126
+ else:
127
+ assert line[0] in ["~", ">"]
128
+ stack[-1] += 1
129
+ indent = len(stack) if line.startswith("~") else len(stack) - 1
130
+ result.append(" " * indent + line[1:])
131
+ assert len(stack) == 1
132
+ return result
133
+
134
+
135
+ def assertrepr_compare(
136
+ op: str,
137
+ left: object,
138
+ right: object,
139
+ *,
140
+ verbose: int,
141
+ highlighter: _HighlightFunc,
142
+ assertion_text_diff_style: _AssertionTextDiffStyle,
143
+ ) -> Iterator[str]:
144
+ """Yield specialised explanations for some operators/operands.
145
+
146
+ The first line yielded is always the summary (``left op right``);
147
+ subsequent lines are the detailed explanation. Yields nothing when no
148
+ specialised explanation applies, which lets consumers map an empty
149
+ iterator to "no explanation" without materialising anything.
150
+
151
+ The iterator is lazy on purpose: a streaming consumer can stop pulling
152
+ lines as soon as it has enough to show, so an enormous diff doesn't
153
+ have to be built in full just to be thrown away.
154
+ """
155
+ # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
156
+ # See issue #3246.
157
+ use_ascii = (
158
+ isinstance(left, str)
159
+ and isinstance(right, str)
160
+ and normalize("NFD", left) == normalize("NFD", right)
161
+ )
162
+
163
+ if verbose > 1:
164
+ left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
165
+ right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
166
+ else:
167
+ # XXX: "15 chars indentation" is wrong
168
+ # ("E AssertionError: assert "); should use term width.
169
+ maxsize = (
170
+ 80 - 15 - len(op) - 2
171
+ ) // 2 # 15 chars indentation, 1 space around op
172
+
173
+ left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
174
+ right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
175
+
176
+ summary = f"{left_repr} {op} {right_repr}"
177
+
178
+ try:
179
+ if op == "==":
180
+ source = _compare_eq_any(
181
+ left,
182
+ right,
183
+ highlighter,
184
+ verbose,
185
+ assertion_text_diff_style,
186
+ )
187
+ elif op == "not in" and istext(left) and istext(right):
188
+ source = _notin_text(left, right, verbose)
189
+ elif op in {"!=", ">=", "<=", ">", "<"} and isset(left) and isset(right):
190
+ source = SET_COMPARISON_FUNCTIONS[op](left, right, highlighter, verbose)
191
+ else:
192
+ source = iter(())
193
+
194
+ # Only yield the summary if there is a detailed explanation.
195
+ # Make sure there's a separating empty line after the summary.
196
+ summary_yielded = False
197
+ for line in source:
198
+ if not summary_yielded:
199
+ yield summary
200
+ if line != "":
201
+ yield ""
202
+ summary_yielded = True
203
+ yield line
204
+ except outcomes.Exit:
205
+ raise
206
+ except Exception:
207
+ repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
208
+ if not summary_yielded:
209
+ yield summary
210
+ yield ""
211
+ summary_yielded = True
212
+ yield (
213
+ f"(pytest_assertion plugin: representation of details failed: {repr_crash}."
214
+ )
215
+ yield " Probably an object has a faulty __repr__.)"
.venv/lib/python3.14/site-packages/_pytest/cacheprovider.py ADDED
@@ -0,0 +1,640 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Implementation of the cache provider."""
3
+
4
+ # This plugin was not named "cache" to avoid conflicts with the external
5
+ # pytest-cache version.
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Generator
9
+ from collections.abc import Iterable
10
+ import dataclasses
11
+ import errno
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ import shutil
16
+ import tempfile
17
+ from typing import final
18
+
19
+ from .pathlib import resolve_from_str
20
+ from .pathlib import rm_rf
21
+ from .reports import CollectReport
22
+ from _pytest import nodes
23
+ from _pytest._io import TerminalWriter
24
+ from _pytest.config import Config
25
+ from _pytest.config import ExitCode
26
+ from _pytest.config import hookimpl
27
+ from _pytest.config.argparsing import Parser
28
+ from _pytest.deprecated import check_ispytest
29
+ from _pytest.fixtures import fixture
30
+ from _pytest.fixtures import FixtureRequest
31
+ from _pytest.main import Session
32
+ from _pytest.nodes import Directory
33
+ from _pytest.nodes import File
34
+ from _pytest.reports import TestReport
35
+
36
+
37
+ CACHEDIR_FILES: dict[str, bytes] = {
38
+ "README.md": b"""\
39
+ # pytest cache directory #
40
+
41
+ This directory contains data from the pytest's cache plugin,
42
+ which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
43
+
44
+ **Do not** commit this to version control.
45
+
46
+ See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
47
+ """,
48
+ ".gitignore": b"# Created by pytest automatically.\n*\n",
49
+ "CACHEDIR.TAG": b"""\
50
+ Signature: 8a477f597d28d172789f06886806bc55
51
+ # This file is a cache directory tag created by pytest.
52
+ # For information about cache directory tags, see:
53
+ # https://bford.info/cachedir/spec.html
54
+ """,
55
+ }
56
+
57
+
58
+ def _make_cachedir(target: Path) -> None:
59
+ """Create the pytest cache directory atomically with supporting files.
60
+
61
+ Creates a temporary directory with README.md, .gitignore, and CACHEDIR.TAG,
62
+ then atomically renames it to the target location. If another process wins
63
+ the race, the temporary directory is cleaned up.
64
+ """
65
+ target.parent.mkdir(parents=True, exist_ok=True)
66
+ path = Path(tempfile.mkdtemp(prefix="pytest-cache-files-", dir=target.parent))
67
+ try:
68
+ # Reset permissions to the default, see #12308.
69
+ # Note: there's no way to get the current umask atomically, eek.
70
+ umask = os.umask(0o022)
71
+ os.umask(umask)
72
+ path.chmod(0o777 - umask)
73
+
74
+ for name, content in CACHEDIR_FILES.items():
75
+ path.joinpath(name).write_bytes(content)
76
+
77
+ path.rename(target)
78
+ except OSError as e:
79
+ # If 2 concurrent pytests both race to the rename, the loser
80
+ # gets "Directory not empty" from the rename. In this case,
81
+ # everything is handled so just continue after cleanup.
82
+ # On Windows, the error is a FileExistsError which translates to EEXIST.
83
+ if e.errno not in (errno.ENOTEMPTY, errno.EEXIST):
84
+ raise
85
+ finally:
86
+ shutil.rmtree(path, ignore_errors=True)
87
+
88
+
89
+ @final
90
+ @dataclasses.dataclass
91
+ class Cache:
92
+ """Instance of the `cache` fixture."""
93
+
94
+ _cachedir: Path = dataclasses.field(repr=False)
95
+ _config: Config = dataclasses.field(repr=False)
96
+
97
+ # Sub-directory under cache-dir for directories created by `mkdir()`.
98
+ _CACHE_PREFIX_DIRS = "d"
99
+
100
+ # Sub-directory under cache-dir for values created by `set()`.
101
+ _CACHE_PREFIX_VALUES = "v"
102
+
103
+ def __init__(
104
+ self, cachedir: Path, config: Config, *, _ispytest: bool = False
105
+ ) -> None:
106
+ check_ispytest(_ispytest)
107
+ self._cachedir = cachedir
108
+ self._config = config
109
+
110
+ @classmethod
111
+ def for_config(cls, config: Config, *, _ispytest: bool = False) -> Cache:
112
+ """Create the Cache instance for a Config.
113
+
114
+ :meta private:
115
+ """
116
+ check_ispytest(_ispytest)
117
+ cachedir = cls.cache_dir_from_config(config, _ispytest=True)
118
+ if config.getoption("cacheclear") and cachedir.is_dir():
119
+ cls.clear_cache(cachedir, _ispytest=True)
120
+ return cls(cachedir, config, _ispytest=True)
121
+
122
+ @classmethod
123
+ def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None:
124
+ """Clear the sub-directories used to hold cached directories and values.
125
+
126
+ :meta private:
127
+ """
128
+ check_ispytest(_ispytest)
129
+ for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
130
+ d = cachedir / prefix
131
+ if d.is_dir():
132
+ rm_rf(d)
133
+
134
+ @staticmethod
135
+ def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path:
136
+ """Get the path to the cache directory for a Config.
137
+
138
+ :meta private:
139
+ """
140
+ check_ispytest(_ispytest)
141
+ return resolve_from_str(config.getini("cache_dir"), config.rootpath)
142
+
143
+ def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
144
+ """Issue a cache warning.
145
+
146
+ :meta private:
147
+ """
148
+ check_ispytest(_ispytest)
149
+ import warnings
150
+
151
+ from _pytest.warning_types import PytestCacheWarning
152
+
153
+ warnings.warn(
154
+ PytestCacheWarning(fmt.format(**args) if args else fmt),
155
+ self._config.hook,
156
+ stacklevel=3,
157
+ )
158
+
159
+ def _mkdir(self, path: Path) -> None:
160
+ self._ensure_cache_dir_and_supporting_files()
161
+ path.mkdir(exist_ok=True, parents=True)
162
+
163
+ def mkdir(self, name: str) -> Path:
164
+ """Return a directory path object with the given name.
165
+
166
+ If the directory does not yet exist, it will be created. You can use
167
+ it to manage files to e.g. store/retrieve database dumps across test
168
+ sessions.
169
+
170
+ .. versionadded:: 7.0
171
+
172
+ :param name:
173
+ Must be a string not containing a ``/`` separator.
174
+ Make sure the name contains your plugin or application
175
+ identifiers to prevent clashes with other cache users.
176
+ """
177
+ path = Path(name)
178
+ if len(path.parts) > 1:
179
+ raise ValueError("name is not allowed to contain path separators")
180
+ res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
181
+ self._mkdir(res)
182
+ return res
183
+
184
+ def _getvaluepath(self, key: str) -> Path:
185
+ return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
186
+
187
+ def get(self, key: str, default):
188
+ """Return the cached value for the given key.
189
+
190
+ If no value was yet cached or the value cannot be read, the specified
191
+ default is returned.
192
+
193
+ :param key:
194
+ Must be a ``/`` separated value. Usually the first
195
+ name is the name of your plugin or your application.
196
+ :param default:
197
+ The value to return in case of a cache-miss or invalid cache value.
198
+ """
199
+ path = self._getvaluepath(key)
200
+ try:
201
+ with path.open("r", encoding="UTF-8") as f:
202
+ return json.load(f)
203
+ except (ValueError, OSError):
204
+ return default
205
+
206
+ def set(self, key: str, value: object) -> None:
207
+ """Save value for the given key.
208
+
209
+ :param key:
210
+ Must be a ``/`` separated value. Usually the first
211
+ name is the name of your plugin or your application.
212
+ :param value:
213
+ Must be of any combination of basic python types,
214
+ including nested types like lists of dictionaries.
215
+ """
216
+ path = self._getvaluepath(key)
217
+ try:
218
+ self._mkdir(path.parent)
219
+ except OSError as exc:
220
+ self.warn(
221
+ f"could not create cache path {path}: {exc}",
222
+ _ispytest=True,
223
+ )
224
+ return
225
+ data = json.dumps(value, ensure_ascii=False, indent=2)
226
+ try:
227
+ f = path.open("w", encoding="UTF-8")
228
+ except OSError as exc:
229
+ self.warn(
230
+ f"cache could not write path {path}: {exc}",
231
+ _ispytest=True,
232
+ )
233
+ else:
234
+ with f:
235
+ f.write(data)
236
+
237
+ def _ensure_cache_dir_and_supporting_files(self) -> None:
238
+ """Create the cache dir and its supporting files."""
239
+ if not self._cachedir.is_dir():
240
+ _make_cachedir(self._cachedir)
241
+
242
+
243
+ class LFPluginCollWrapper:
244
+ def __init__(self, lfplugin: LFPlugin) -> None:
245
+ self.lfplugin = lfplugin
246
+ self._collected_at_least_one_failure = False
247
+
248
+ @hookimpl(wrapper=True)
249
+ def pytest_make_collect_report(
250
+ self, collector: nodes.Collector
251
+ ) -> Generator[None, CollectReport, CollectReport]:
252
+ res = yield
253
+ if isinstance(collector, Session | Directory):
254
+ # Sort any lf-paths to the beginning.
255
+ lf_paths = self.lfplugin._last_failed_paths
256
+
257
+ # Use stable sort to prioritize last failed.
258
+ def sort_key(node: nodes.Item | nodes.Collector) -> bool:
259
+ return node.path in lf_paths
260
+
261
+ res.result = sorted(
262
+ res.result,
263
+ key=sort_key,
264
+ reverse=True,
265
+ )
266
+
267
+ elif isinstance(collector, File):
268
+ if collector.path in self.lfplugin._last_failed_paths:
269
+ result = res.result
270
+ lastfailed = self.lfplugin.lastfailed
271
+
272
+ # Only filter with known failures.
273
+ if not self._collected_at_least_one_failure:
274
+ if not any(x.nodeid in lastfailed for x in result):
275
+ return res
276
+ self.lfplugin.config.pluginmanager.register(
277
+ LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
278
+ )
279
+ self._collected_at_least_one_failure = True
280
+
281
+ session = collector.session
282
+ result[:] = [
283
+ x
284
+ for x in result
285
+ if x.nodeid in lastfailed
286
+ # Include any passed arguments (not trivial to filter).
287
+ or session.isinitpath(x.path)
288
+ # Keep all sub-collectors.
289
+ or isinstance(x, nodes.Collector)
290
+ ]
291
+
292
+ return res
293
+
294
+
295
+ class LFPluginCollSkipfiles:
296
+ def __init__(self, lfplugin: LFPlugin) -> None:
297
+ self.lfplugin = lfplugin
298
+
299
+ @hookimpl
300
+ def pytest_make_collect_report(
301
+ self, collector: nodes.Collector
302
+ ) -> CollectReport | None:
303
+ if isinstance(collector, File):
304
+ if collector.path not in self.lfplugin._last_failed_paths:
305
+ self.lfplugin._skipped_files += 1
306
+
307
+ return CollectReport(
308
+ collector.nodeid, "passed", longrepr=None, result=[]
309
+ )
310
+ return None
311
+
312
+
313
+ class LFPlugin:
314
+ """Plugin which implements the --lf (run last-failing) option."""
315
+
316
+ def __init__(self, config: Config) -> None:
317
+ self.config = config
318
+ active_keys = "lf", "failedfirst"
319
+ self.active = any(config.getoption(key) for key in active_keys)
320
+ assert config.cache
321
+ self.lastfailed: dict[str, bool] = config.cache.get("cache/lastfailed", {})
322
+ self._previously_failed_count: int | None = None
323
+ self._report_status: str | None = None
324
+ self._skipped_files = 0 # count skipped files during collection due to --lf
325
+
326
+ if config.getoption("lf"):
327
+ self._last_failed_paths = self.get_last_failed_paths()
328
+ config.pluginmanager.register(
329
+ LFPluginCollWrapper(self), "lfplugin-collwrapper"
330
+ )
331
+
332
+ def get_last_failed_paths(self) -> set[Path]:
333
+ """Return a set with all Paths of the previously failed nodeids and
334
+ their parents."""
335
+ rootpath = self.config.rootpath
336
+ result = set()
337
+ for nodeid in self.lastfailed:
338
+ path = rootpath / nodeid.split("::")[0]
339
+ result.add(path)
340
+ result.update(path.parents)
341
+ return {x for x in result if x.exists()}
342
+
343
+ def pytest_report_collectionfinish(self) -> str | None:
344
+ if self.active and self.config.get_verbosity() >= 0:
345
+ return f"run-last-failure: {self._report_status}"
346
+ return None
347
+
348
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
349
+ if (report.when == "call" and report.passed) or report.skipped:
350
+ self.lastfailed.pop(report.nodeid, None)
351
+ elif report.failed:
352
+ self.lastfailed[report.nodeid] = True
353
+
354
+ def pytest_collectreport(self, report: CollectReport) -> None:
355
+ passed = report.outcome in ("passed", "skipped")
356
+ if passed:
357
+ if report.nodeid in self.lastfailed:
358
+ self.lastfailed.pop(report.nodeid)
359
+ self.lastfailed.update((item.nodeid, True) for item in report.result)
360
+ else:
361
+ self.lastfailed[report.nodeid] = True
362
+
363
+ @hookimpl(wrapper=True, tryfirst=True)
364
+ def pytest_collection_modifyitems(
365
+ self, config: Config, items: list[nodes.Item]
366
+ ) -> Generator[None]:
367
+ res = yield
368
+
369
+ if not self.active:
370
+ return res
371
+
372
+ if self.lastfailed:
373
+ previously_failed = []
374
+ previously_passed = []
375
+ for item in items:
376
+ if item.nodeid in self.lastfailed:
377
+ previously_failed.append(item)
378
+ else:
379
+ previously_passed.append(item)
380
+ self._previously_failed_count = len(previously_failed)
381
+
382
+ if not previously_failed:
383
+ # Running a subset of all tests with recorded failures
384
+ # only outside of it.
385
+ self._report_status = (
386
+ f"{len(self.lastfailed)} known failures not in selected tests"
387
+ )
388
+ else:
389
+ if self.config.getoption("lf"):
390
+ items[:] = previously_failed
391
+ config.hook.pytest_deselected(items=previously_passed)
392
+ else: # --failedfirst
393
+ items[:] = previously_failed + previously_passed
394
+
395
+ noun = "failure" if self._previously_failed_count == 1 else "failures"
396
+ suffix = " first" if self.config.getoption("failedfirst") else ""
397
+ self._report_status = (
398
+ f"rerun previous {self._previously_failed_count} {noun}{suffix}"
399
+ )
400
+
401
+ if self._skipped_files > 0:
402
+ files_noun = "file" if self._skipped_files == 1 else "files"
403
+ self._report_status += f" (skipped {self._skipped_files} {files_noun})"
404
+ else:
405
+ self._report_status = "no previously failed tests, "
406
+ if self.config.getoption("last_failed_no_failures") == "none":
407
+ self._report_status += "deselecting all items."
408
+ config.hook.pytest_deselected(items=items[:])
409
+ items[:] = []
410
+ else:
411
+ self._report_status += "not deselecting items."
412
+
413
+ return res
414
+
415
+ def pytest_sessionfinish(self, session: Session) -> None:
416
+ config = self.config
417
+ if config.getoption("cacheshow") or hasattr(config, "workerinput"):
418
+ return
419
+
420
+ assert config.cache is not None
421
+ saved_lastfailed = config.cache.get("cache/lastfailed", {})
422
+ if saved_lastfailed != self.lastfailed:
423
+ config.cache.set("cache/lastfailed", self.lastfailed)
424
+
425
+
426
+ class NFPlugin:
427
+ """Plugin which implements the --nf (run new-first) option."""
428
+
429
+ def __init__(self, config: Config) -> None:
430
+ self.config = config
431
+ self.active = config.option.newfirst
432
+ assert config.cache is not None
433
+ self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
434
+
435
+ @hookimpl(wrapper=True, tryfirst=True)
436
+ def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> Generator[None]:
437
+ res = yield
438
+
439
+ if self.active:
440
+ new_items: dict[str, nodes.Item] = {}
441
+ other_items: dict[str, nodes.Item] = {}
442
+ for item in items:
443
+ if item.nodeid not in self.cached_nodeids:
444
+ new_items[item.nodeid] = item
445
+ else:
446
+ other_items[item.nodeid] = item
447
+
448
+ items[:] = self._get_increasing_order(
449
+ new_items.values()
450
+ ) + self._get_increasing_order(other_items.values())
451
+ self.cached_nodeids.update(new_items)
452
+ else:
453
+ self.cached_nodeids.update(item.nodeid for item in items)
454
+
455
+ return res
456
+
457
+ def _get_increasing_order(self, items: Iterable[nodes.Item]) -> list[nodes.Item]:
458
+ return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True)
459
+
460
+ def pytest_sessionfinish(self) -> None:
461
+ config = self.config
462
+ if config.getoption("cacheshow") or hasattr(config, "workerinput"):
463
+ return
464
+
465
+ if config.getoption("collectonly"):
466
+ return
467
+
468
+ assert config.cache is not None
469
+ config.cache.set("cache/nodeids", sorted(self.cached_nodeids))
470
+
471
+
472
+ def pytest_addoption(parser: Parser) -> None:
473
+ """Add command-line options for cache functionality.
474
+
475
+ :param parser: Parser object to add command-line options to.
476
+ """
477
+ group = parser.getgroup("general")
478
+ group.addoption(
479
+ "--lf",
480
+ "--last-failed",
481
+ action="store_true",
482
+ dest="lf",
483
+ help="Rerun only the tests that failed at the last run (or all if none failed)",
484
+ )
485
+ group.addoption(
486
+ "--ff",
487
+ "--failed-first",
488
+ action="store_true",
489
+ dest="failedfirst",
490
+ help="Run all tests, but run the last failures first. "
491
+ "This may re-order tests and thus lead to "
492
+ "repeated fixture setup/teardown.",
493
+ )
494
+ group.addoption(
495
+ "--nf",
496
+ "--new-first",
497
+ action="store_true",
498
+ dest="newfirst",
499
+ help="Run tests from new files first, then the rest of the tests "
500
+ "sorted by file mtime",
501
+ )
502
+ group.addoption(
503
+ "--cache-show",
504
+ action="append",
505
+ nargs="?",
506
+ dest="cacheshow",
507
+ help=(
508
+ "Show cache contents, don't perform collection or tests. "
509
+ "Optional argument: glob (default: '*')."
510
+ ),
511
+ )
512
+ group.addoption(
513
+ "--cache-clear",
514
+ action="store_true",
515
+ dest="cacheclear",
516
+ help="Remove all cache contents at start of test run",
517
+ )
518
+ cache_dir_default = ".pytest_cache"
519
+ if "TOX_ENV_DIR" in os.environ:
520
+ cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default)
521
+ parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path")
522
+ group.addoption(
523
+ "--lfnf",
524
+ "--last-failed-no-failures",
525
+ action="store",
526
+ dest="last_failed_no_failures",
527
+ choices=("all", "none"),
528
+ default="all",
529
+ help="With ``--lf``, determines whether to execute tests when there "
530
+ "are no previously (known) failures or when no "
531
+ "cached ``lastfailed`` data was found. "
532
+ "``all`` (the default) runs the full test suite again. "
533
+ "``none`` just emits a message about no known failures and exits successfully.",
534
+ )
535
+
536
+
537
+ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
538
+ if config.option.cacheshow and not config.option.help:
539
+ from _pytest.main import wrap_session
540
+
541
+ return wrap_session(config, cacheshow)
542
+ return None
543
+
544
+
545
+ @hookimpl(tryfirst=True)
546
+ def pytest_configure(config: Config) -> None:
547
+ """Configure cache system and register related plugins.
548
+
549
+ Creates the Cache instance and registers the last-failed (LFPlugin)
550
+ and new-first (NFPlugin) plugins with the plugin manager.
551
+
552
+ :param config: pytest configuration object.
553
+ """
554
+ config.cache = Cache.for_config(config, _ispytest=True)
555
+ config.pluginmanager.register(LFPlugin(config), "lfplugin")
556
+ config.pluginmanager.register(NFPlugin(config), "nfplugin")
557
+
558
+
559
+ @fixture
560
+ def cache(request: FixtureRequest) -> Cache:
561
+ """Return a cache object that can persist state between testing sessions.
562
+
563
+ cache.get(key, default)
564
+ cache.set(key, value)
565
+
566
+ Keys must be ``/`` separated strings, where the first part is usually the
567
+ name of your plugin or application to avoid clashes with other cache users.
568
+
569
+ Values can be any object handled by the json stdlib module.
570
+ """
571
+ assert request.config.cache is not None
572
+ return request.config.cache
573
+
574
+
575
+ def pytest_report_header(config: Config) -> str | None:
576
+ """Display cachedir with --cache-show and if non-default."""
577
+ if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache":
578
+ assert config.cache is not None
579
+ cachedir = config.cache._cachedir
580
+ # TODO: evaluate generating upward relative paths
581
+ # starting with .., ../.. if sensible
582
+
583
+ try:
584
+ displaypath = cachedir.relative_to(config.rootpath)
585
+ except ValueError:
586
+ displaypath = cachedir
587
+ return f"cachedir: {displaypath}"
588
+ return None
589
+
590
+
591
+ def cacheshow(config: Config, session: Session) -> int:
592
+ """Display cache contents when --cache-show is used.
593
+
594
+ Shows cached values and directories matching the specified glob pattern
595
+ (default: '*'). Displays cache location, cached test results, and
596
+ any cached directories created by plugins.
597
+
598
+ :param config: pytest configuration object.
599
+ :param session: pytest session object.
600
+ :returns: Exit code (0 for success).
601
+ """
602
+ from pprint import pformat
603
+
604
+ assert config.cache is not None
605
+
606
+ tw = TerminalWriter()
607
+ tw.line("cachedir: " + str(config.cache._cachedir))
608
+ if not config.cache._cachedir.is_dir():
609
+ tw.line("cache is empty")
610
+ return 0
611
+
612
+ glob = config.option.cacheshow[0]
613
+ if glob is None:
614
+ glob = "*"
615
+
616
+ dummy = object()
617
+ basedir = config.cache._cachedir
618
+ vdir = basedir / Cache._CACHE_PREFIX_VALUES
619
+ tw.sep("-", f"cache values for {glob!r}")
620
+ for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()):
621
+ key = str(valpath.relative_to(vdir))
622
+ val = config.cache.get(key, dummy)
623
+ if val is dummy:
624
+ tw.line(f"{key} contains unreadable content, will be ignored")
625
+ else:
626
+ tw.line(f"{key} contains:")
627
+ for line in pformat(val).splitlines():
628
+ tw.line(" " + line)
629
+
630
+ ddir = basedir / Cache._CACHE_PREFIX_DIRS
631
+ if ddir.is_dir():
632
+ contents = sorted(ddir.rglob(glob))
633
+ tw.sep("-", f"cache directories for {glob!r}")
634
+ for p in contents:
635
+ # if p.is_dir():
636
+ # print("%s/" % p.relative_to(basedir))
637
+ if p.is_file():
638
+ key = str(p.relative_to(basedir))
639
+ tw.line(f"{key} is a file of length {p.stat().st_size}")
640
+ return 0
.venv/lib/python3.14/site-packages/_pytest/capture.py ADDED
@@ -0,0 +1,1151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from collections.abc import Generator
9
+ from collections.abc import Iterable
10
+ from collections.abc import Iterator
11
+ import contextlib
12
+ import io
13
+ from io import UnsupportedOperation
14
+ import os
15
+ import sys
16
+ from tempfile import TemporaryFile
17
+ from types import TracebackType
18
+ from typing import Any
19
+ from typing import AnyStr
20
+ from typing import BinaryIO
21
+ from typing import cast
22
+ from typing import Final
23
+ from typing import final
24
+ from typing import Generic
25
+ from typing import Literal
26
+ from typing import NamedTuple
27
+ from typing import TextIO
28
+ from typing import TYPE_CHECKING
29
+
30
+
31
+ if TYPE_CHECKING:
32
+ from typing_extensions import Self
33
+
34
+ from _pytest.config import Config
35
+ from _pytest.config import hookimpl
36
+ from _pytest.config.argparsing import Parser
37
+ from _pytest.deprecated import check_ispytest
38
+ from _pytest.fixtures import fixture
39
+ from _pytest.fixtures import SubRequest
40
+ from _pytest.nodes import Collector
41
+ from _pytest.nodes import File
42
+ from _pytest.nodes import Item
43
+ from _pytest.reports import CollectReport
44
+
45
+
46
+ _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
47
+
48
+
49
+ def pytest_addoption(parser: Parser) -> None:
50
+ group = parser.getgroup("general")
51
+ group.addoption(
52
+ "--capture",
53
+ action="store",
54
+ default="fd",
55
+ metavar="method",
56
+ choices=["fd", "sys", "no", "tee-sys"],
57
+ help="Per-test capturing method: one of fd|sys|no|tee-sys",
58
+ )
59
+ group._addoption( # private to use reserved lower-case short option
60
+ "-s",
61
+ action="store_const",
62
+ const="no",
63
+ dest="capture",
64
+ help="Shortcut for --capture=no",
65
+ )
66
+
67
+
68
+ def _colorama_workaround() -> None:
69
+ """Ensure colorama is imported so that it attaches to the correct stdio
70
+ handles on Windows.
71
+
72
+ colorama uses the terminal on import time. So if something does the
73
+ first import of colorama while I/O capture is active, colorama will
74
+ fail in various ways.
75
+ """
76
+ if sys.platform.startswith("win32"):
77
+ try:
78
+ import colorama # noqa: F401
79
+ except ImportError:
80
+ pass
81
+
82
+
83
+ def _readline_workaround() -> None:
84
+ """Ensure readline is imported early so it attaches to the correct stdio handles.
85
+
86
+ This isn't a problem with the default GNU readline implementation, but in
87
+ some configurations, Python uses libedit instead (on macOS, and for prebuilt
88
+ binaries such as used by uv).
89
+
90
+ In theory this is only needed if readline.backend == "libedit", but the
91
+ workaround consists of importing readline here, so we already worked around
92
+ the issue by the time we could check if we need to.
93
+ """
94
+ try:
95
+ import readline # noqa: F401
96
+ except ImportError:
97
+ pass
98
+
99
+
100
+ def _windowsconsoleio_workaround(stream: TextIO) -> None:
101
+ """Workaround for Windows Unicode console handling.
102
+
103
+ Python 3.6 implemented Unicode console handling for Windows. This works
104
+ by reading/writing to the raw console handle using
105
+ ``{Read,Write}ConsoleW``.
106
+
107
+ The problem is that we are going to ``dup2`` over the stdio file
108
+ descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
109
+ handles used by Python to write to the console. Though there is still some
110
+ weirdness and the console handle seems to only be closed randomly and not
111
+ on the first call to ``CloseHandle``, or maybe it gets reopened with the
112
+ same handle value when we suspend capturing.
113
+
114
+ The workaround in this case will reopen stdio with a different fd which
115
+ also means a different handle by replicating the logic in
116
+ "Py_lifecycle.c:initstdio/create_stdio".
117
+
118
+ :param stream:
119
+ In practice ``sys.stdout`` or ``sys.stderr``, but given
120
+ here as parameter for unittesting purposes.
121
+
122
+ See https://github.com/pytest-dev/py/issues/103.
123
+ """
124
+ if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
125
+ return
126
+
127
+ # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
128
+ if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore]
129
+ return
130
+
131
+ raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
132
+
133
+ if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore]
134
+ return
135
+
136
+ def _reopen_stdio(f, mode):
137
+ if not hasattr(stream.buffer, "raw") and mode[0] == "w":
138
+ buffering = 0
139
+ else:
140
+ buffering = -1
141
+
142
+ return io.TextIOWrapper(
143
+ open(os.dup(f.fileno()), mode, buffering),
144
+ f.encoding,
145
+ f.errors,
146
+ f.newlines,
147
+ f.line_buffering,
148
+ )
149
+
150
+ sys.stdin = _reopen_stdio(sys.stdin, "rb")
151
+ sys.stdout = _reopen_stdio(sys.stdout, "wb")
152
+ sys.stderr = _reopen_stdio(sys.stderr, "wb")
153
+
154
+
155
+ @hookimpl(wrapper=True)
156
+ def pytest_load_initial_conftests(early_config: Config) -> Generator[None]:
157
+ ns = early_config.known_args_namespace
158
+ if ns.capture == "fd":
159
+ _windowsconsoleio_workaround(sys.stdout)
160
+ _colorama_workaround()
161
+ _readline_workaround()
162
+ pluginmanager = early_config.pluginmanager
163
+ capman = CaptureManager(ns.capture)
164
+ pluginmanager.register(capman, "capturemanager")
165
+
166
+ # Make sure that capturemanager is properly reset at final shutdown.
167
+ early_config.add_cleanup(capman.stop_global_capturing)
168
+
169
+ # Finally trigger conftest loading but while capturing (issue #93).
170
+ capman.start_global_capturing()
171
+ try:
172
+ try:
173
+ yield
174
+ finally:
175
+ capman.suspend_global_capture()
176
+ except BaseException:
177
+ out, err = capman.read_global_capture()
178
+ sys.stdout.write(out)
179
+ sys.stderr.write(err)
180
+ raise
181
+
182
+
183
+ # IO Helpers.
184
+
185
+
186
+ class EncodedFile(io.TextIOWrapper):
187
+ __slots__ = ()
188
+
189
+ @property
190
+ def name(self) -> str:
191
+ # Ensure that file.name is a string. Workaround for a Python bug
192
+ # fixed in >=3.7.4: https://bugs.python.org/issue36015
193
+ return repr(self.buffer)
194
+
195
+ @property
196
+ def mode(self) -> str:
197
+ # TextIOWrapper doesn't expose a mode, but at least some of our
198
+ # tests check it.
199
+ assert hasattr(self.buffer, "mode")
200
+ return cast(str, self.buffer.mode.replace("b", ""))
201
+
202
+
203
+ class CaptureIO(io.TextIOWrapper):
204
+ def __init__(self) -> None:
205
+ super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)
206
+
207
+ def getvalue(self) -> str:
208
+ assert isinstance(self.buffer, io.BytesIO)
209
+ return self.buffer.getvalue().decode("UTF-8")
210
+
211
+
212
+ class TeeCaptureIO(CaptureIO):
213
+ def __init__(self, other: TextIO) -> None:
214
+ self._other = other
215
+ super().__init__()
216
+
217
+ def write(self, s: str) -> int:
218
+ super().write(s)
219
+ return self._other.write(s)
220
+
221
+
222
+ class DontReadFromInput(TextIO):
223
+ @property
224
+ def encoding(self) -> str:
225
+ assert sys.__stdin__ is not None
226
+ return sys.__stdin__.encoding
227
+
228
+ def read(self, size: int = -1) -> str:
229
+ raise OSError(
230
+ "pytest: reading from stdin while output is captured! Consider using `-s`."
231
+ )
232
+
233
+ readline = read
234
+
235
+ def __next__(self) -> str:
236
+ return self.readline()
237
+
238
+ def readlines(self, hint: int | None = -1) -> list[str]:
239
+ raise OSError(
240
+ "pytest: reading from stdin while output is captured! Consider using `-s`."
241
+ )
242
+
243
+ def __iter__(self) -> Iterator[str]:
244
+ return self
245
+
246
+ def fileno(self) -> int:
247
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
248
+
249
+ def flush(self) -> None:
250
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()")
251
+
252
+ def isatty(self) -> bool:
253
+ return False
254
+
255
+ def close(self) -> None:
256
+ pass
257
+
258
+ def readable(self) -> bool:
259
+ return False
260
+
261
+ def seek(self, offset: int, whence: int = 0) -> int:
262
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
263
+
264
+ def seekable(self) -> bool:
265
+ return False
266
+
267
+ def tell(self) -> int:
268
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
269
+
270
+ def truncate(self, size: int | None = None) -> int:
271
+ raise UnsupportedOperation("cannot truncate stdin")
272
+
273
+ def write(self, data: str) -> int:
274
+ raise UnsupportedOperation("cannot write to stdin")
275
+
276
+ def writelines(self, lines: Iterable[str]) -> None:
277
+ raise UnsupportedOperation("Cannot write to stdin")
278
+
279
+ def writable(self) -> bool:
280
+ return False
281
+
282
+ def __enter__(self) -> Self:
283
+ return self
284
+
285
+ def __exit__(
286
+ self,
287
+ type: type[BaseException] | None,
288
+ value: BaseException | None,
289
+ traceback: TracebackType | None,
290
+ ) -> None:
291
+ pass
292
+
293
+ @property
294
+ def buffer(self) -> BinaryIO:
295
+ # The str/bytes doesn't actually matter in this type, so OK to fake.
296
+ return self # type: ignore[return-value]
297
+
298
+
299
+ # Capture classes.
300
+
301
+
302
+ class CaptureBase(abc.ABC, Generic[AnyStr]):
303
+ EMPTY_BUFFER: AnyStr
304
+
305
+ @abc.abstractmethod
306
+ def __init__(self, fd: int) -> None:
307
+ raise NotImplementedError()
308
+
309
+ @abc.abstractmethod
310
+ def start(self) -> None:
311
+ raise NotImplementedError()
312
+
313
+ @abc.abstractmethod
314
+ def done(self) -> None:
315
+ raise NotImplementedError()
316
+
317
+ @abc.abstractmethod
318
+ def suspend(self) -> None:
319
+ raise NotImplementedError()
320
+
321
+ @abc.abstractmethod
322
+ def resume(self) -> None:
323
+ raise NotImplementedError()
324
+
325
+ @abc.abstractmethod
326
+ def writeorg(self, data: AnyStr) -> None:
327
+ raise NotImplementedError()
328
+
329
+ @abc.abstractmethod
330
+ def snap(self) -> AnyStr:
331
+ raise NotImplementedError()
332
+
333
+
334
+ patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
335
+
336
+
337
+ class NoCapture(CaptureBase[str]):
338
+ EMPTY_BUFFER = ""
339
+
340
+ def __init__(self, fd: int) -> None:
341
+ pass
342
+
343
+ def start(self) -> None:
344
+ pass
345
+
346
+ def done(self) -> None:
347
+ pass
348
+
349
+ def suspend(self) -> None:
350
+ pass
351
+
352
+ def resume(self) -> None:
353
+ pass
354
+
355
+ def snap(self) -> str:
356
+ return ""
357
+
358
+ def writeorg(self, data: str) -> None:
359
+ pass
360
+
361
+
362
+ class SysCaptureBase(CaptureBase[AnyStr]):
363
+ def __init__(
364
+ self, fd: int, tmpfile: TextIO | None = None, *, tee: bool = False
365
+ ) -> None:
366
+ name = patchsysdict[fd]
367
+ self._old: TextIO = getattr(sys, name)
368
+ self.name = name
369
+ if tmpfile is None:
370
+ if name == "stdin":
371
+ tmpfile = DontReadFromInput()
372
+ else:
373
+ tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old)
374
+ self.tmpfile = tmpfile
375
+ self._state = "initialized"
376
+
377
+ def repr(self, class_name: str) -> str:
378
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
379
+ class_name,
380
+ self.name,
381
+ (hasattr(self, "_old") and repr(self._old)) or "<UNSET>",
382
+ self._state,
383
+ self.tmpfile,
384
+ )
385
+
386
+ def __repr__(self) -> str:
387
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
388
+ self.__class__.__name__,
389
+ self.name,
390
+ (hasattr(self, "_old") and repr(self._old)) or "<UNSET>",
391
+ self._state,
392
+ self.tmpfile,
393
+ )
394
+
395
+ def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
396
+ assert self._state in states, (
397
+ "cannot {} in state {!r}: expected one of {}".format(
398
+ op, self._state, ", ".join(states)
399
+ )
400
+ )
401
+
402
+ def start(self) -> None:
403
+ self._assert_state("start", ("initialized",))
404
+ setattr(sys, self.name, self.tmpfile)
405
+ self._state = "started"
406
+
407
+ def done(self) -> None:
408
+ self._assert_state("done", ("initialized", "started", "suspended", "done"))
409
+ if self._state == "done":
410
+ return
411
+ setattr(sys, self.name, self._old)
412
+ del self._old
413
+ self.tmpfile.close()
414
+ self._state = "done"
415
+
416
+ def suspend(self) -> None:
417
+ self._assert_state("suspend", ("started", "suspended"))
418
+ setattr(sys, self.name, self._old)
419
+ self._state = "suspended"
420
+
421
+ def resume(self) -> None:
422
+ self._assert_state("resume", ("started", "suspended"))
423
+ if self._state == "started":
424
+ return
425
+ setattr(sys, self.name, self.tmpfile)
426
+ self._state = "started"
427
+
428
+
429
+ class SysCaptureBinary(SysCaptureBase[bytes]):
430
+ EMPTY_BUFFER = b""
431
+
432
+ def snap(self) -> bytes:
433
+ self._assert_state("snap", ("started", "suspended"))
434
+ self.tmpfile.seek(0)
435
+ res = self.tmpfile.buffer.read()
436
+ self.tmpfile.seek(0)
437
+ self.tmpfile.truncate()
438
+ return res
439
+
440
+ def writeorg(self, data: bytes) -> None:
441
+ self._assert_state("writeorg", ("started", "suspended"))
442
+ self._old.flush()
443
+ self._old.buffer.write(data)
444
+ self._old.buffer.flush()
445
+
446
+
447
+ class SysCapture(SysCaptureBase[str]):
448
+ EMPTY_BUFFER = ""
449
+
450
+ def snap(self) -> str:
451
+ self._assert_state("snap", ("started", "suspended"))
452
+ assert isinstance(self.tmpfile, CaptureIO)
453
+ res = self.tmpfile.getvalue()
454
+ self.tmpfile.seek(0)
455
+ self.tmpfile.truncate()
456
+ return res
457
+
458
+ def writeorg(self, data: str) -> None:
459
+ self._assert_state("writeorg", ("started", "suspended"))
460
+ self._old.write(data)
461
+ self._old.flush()
462
+
463
+
464
+ class FDCaptureBase(CaptureBase[AnyStr]):
465
+ def __init__(self, targetfd: int) -> None:
466
+ self.targetfd = targetfd
467
+
468
+ try:
469
+ os.fstat(targetfd)
470
+ except OSError:
471
+ # FD capturing is conceptually simple -- create a temporary file,
472
+ # redirect the FD to it, redirect back when done. But when the
473
+ # target FD is invalid it throws a wrench into this lovely scheme.
474
+ #
475
+ # Tests themselves shouldn't care if the FD is valid, FD capturing
476
+ # should work regardless of external circumstances. So falling back
477
+ # to just sys capturing is not a good option.
478
+ #
479
+ # Further complications are the need to support suspend() and the
480
+ # possibility of FD reuse (e.g. the tmpfile getting the very same
481
+ # target FD). The following approach is robust, I believe.
482
+ self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR)
483
+ os.dup2(self.targetfd_invalid, targetfd)
484
+ else:
485
+ self.targetfd_invalid = None
486
+ self.targetfd_save = os.dup(targetfd)
487
+
488
+ if targetfd == 0:
489
+ self.tmpfile = open(os.devnull, encoding="utf-8")
490
+ self.syscapture: CaptureBase[str] = SysCapture(targetfd)
491
+ else:
492
+ self.tmpfile = EncodedFile(
493
+ TemporaryFile(buffering=0),
494
+ encoding="utf-8",
495
+ errors="replace",
496
+ newline="",
497
+ write_through=True,
498
+ )
499
+ if targetfd in patchsysdict:
500
+ self.syscapture = SysCapture(targetfd, self.tmpfile)
501
+ else:
502
+ self.syscapture = NoCapture(targetfd)
503
+
504
+ self._state = "initialized"
505
+
506
+ def __repr__(self) -> str:
507
+ return (
508
+ f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
509
+ f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
510
+ )
511
+
512
+ def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
513
+ assert self._state in states, (
514
+ "cannot {} in state {!r}: expected one of {}".format(
515
+ op, self._state, ", ".join(states)
516
+ )
517
+ )
518
+
519
+ def start(self) -> None:
520
+ """Start capturing on targetfd using memorized tmpfile."""
521
+ self._assert_state("start", ("initialized",))
522
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
523
+ self.syscapture.start()
524
+ self._state = "started"
525
+
526
+ def done(self) -> None:
527
+ """Stop capturing, restore streams, return original capture file,
528
+ seeked to position zero."""
529
+ self._assert_state("done", ("initialized", "started", "suspended", "done"))
530
+ if self._state == "done":
531
+ return
532
+ os.dup2(self.targetfd_save, self.targetfd)
533
+ os.close(self.targetfd_save)
534
+ if self.targetfd_invalid is not None:
535
+ if self.targetfd_invalid != self.targetfd:
536
+ os.close(self.targetfd)
537
+ os.close(self.targetfd_invalid)
538
+ self.syscapture.done()
539
+ self.tmpfile.close()
540
+ self._state = "done"
541
+
542
+ def suspend(self) -> None:
543
+ self._assert_state("suspend", ("started", "suspended"))
544
+ if self._state == "suspended":
545
+ return
546
+ self.syscapture.suspend()
547
+ os.dup2(self.targetfd_save, self.targetfd)
548
+ self._state = "suspended"
549
+
550
+ def resume(self) -> None:
551
+ self._assert_state("resume", ("started", "suspended"))
552
+ if self._state == "started":
553
+ return
554
+ self.syscapture.resume()
555
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
556
+ self._state = "started"
557
+
558
+
559
+ class FDCaptureBinary(FDCaptureBase[bytes]):
560
+ """Capture IO to/from a given OS-level file descriptor.
561
+
562
+ snap() produces `bytes`.
563
+ """
564
+
565
+ EMPTY_BUFFER = b""
566
+
567
+ def snap(self) -> bytes:
568
+ self._assert_state("snap", ("started", "suspended"))
569
+ self.tmpfile.seek(0)
570
+ res = self.tmpfile.buffer.read()
571
+ self.tmpfile.seek(0)
572
+ self.tmpfile.truncate()
573
+ return res # type: ignore[return-value]
574
+
575
+ def writeorg(self, data: bytes) -> None:
576
+ """Write to original file descriptor."""
577
+ self._assert_state("writeorg", ("started", "suspended"))
578
+ os.write(self.targetfd_save, data)
579
+
580
+
581
+ class FDCapture(FDCaptureBase[str]):
582
+ """Capture IO to/from a given OS-level file descriptor.
583
+
584
+ snap() produces text.
585
+ """
586
+
587
+ EMPTY_BUFFER = ""
588
+
589
+ def snap(self) -> str:
590
+ self._assert_state("snap", ("started", "suspended"))
591
+ self.tmpfile.seek(0)
592
+ res = self.tmpfile.read()
593
+ self.tmpfile.seek(0)
594
+ self.tmpfile.truncate()
595
+ return res
596
+
597
+ def writeorg(self, data: str) -> None:
598
+ """Write to original file descriptor."""
599
+ self._assert_state("writeorg", ("started", "suspended"))
600
+ # XXX use encoding of original stream
601
+ os.write(self.targetfd_save, data.encode("utf-8"))
602
+
603
+
604
+ # MultiCapture
605
+
606
+
607
+ # Generic NamedTuple only supported since Python 3.11.
608
+ if sys.version_info >= (3, 11) or TYPE_CHECKING:
609
+
610
+ @final
611
+ class CaptureResult(NamedTuple, Generic[AnyStr]):
612
+ """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
613
+
614
+ out: AnyStr
615
+ err: AnyStr
616
+
617
+ else:
618
+
619
+ class CaptureResult(
620
+ collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
621
+ Generic[AnyStr],
622
+ ):
623
+ """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
624
+
625
+ __slots__ = ()
626
+
627
+
628
+ class MultiCapture(Generic[AnyStr]):
629
+ _state = None
630
+ _in_suspended = False
631
+
632
+ def __init__(
633
+ self,
634
+ in_: CaptureBase[AnyStr] | None,
635
+ out: CaptureBase[AnyStr] | None,
636
+ err: CaptureBase[AnyStr] | None,
637
+ ) -> None:
638
+ self.in_: CaptureBase[AnyStr] | None = in_
639
+ self.out: CaptureBase[AnyStr] | None = out
640
+ self.err: CaptureBase[AnyStr] | None = err
641
+
642
+ def __repr__(self) -> str:
643
+ return (
644
+ f"<MultiCapture out={self.out!r} err={self.err!r} in_={self.in_!r} "
645
+ f"_state={self._state!r} _in_suspended={self._in_suspended!r}>"
646
+ )
647
+
648
+ def start_capturing(self) -> None:
649
+ self._state = "started"
650
+ if self.in_:
651
+ self.in_.start()
652
+ if self.out:
653
+ self.out.start()
654
+ if self.err:
655
+ self.err.start()
656
+
657
+ def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]:
658
+ """Pop current snapshot out/err capture and flush to orig streams."""
659
+ out, err = self.readouterr()
660
+ if out:
661
+ assert self.out is not None
662
+ self.out.writeorg(out)
663
+ if err:
664
+ assert self.err is not None
665
+ self.err.writeorg(err)
666
+ return out, err
667
+
668
+ def suspend_capturing(self, in_: bool = False) -> None:
669
+ self._state = "suspended"
670
+ if self.out:
671
+ self.out.suspend()
672
+ if self.err:
673
+ self.err.suspend()
674
+ if in_ and self.in_:
675
+ self.in_.suspend()
676
+ self._in_suspended = True
677
+
678
+ def resume_capturing(self) -> None:
679
+ self._state = "started"
680
+ if self.out:
681
+ self.out.resume()
682
+ if self.err:
683
+ self.err.resume()
684
+ if self._in_suspended:
685
+ assert self.in_ is not None
686
+ self.in_.resume()
687
+ self._in_suspended = False
688
+
689
+ def stop_capturing(self) -> None:
690
+ """Stop capturing and reset capturing streams."""
691
+ if self._state == "stopped":
692
+ raise ValueError("was already stopped")
693
+ self._state = "stopped"
694
+ if self.out:
695
+ self.out.done()
696
+ if self.err:
697
+ self.err.done()
698
+ if self.in_:
699
+ self.in_.done()
700
+
701
+ def is_started(self) -> bool:
702
+ """Whether actively capturing -- not suspended or stopped."""
703
+ return self._state == "started"
704
+
705
+ def readouterr(self) -> CaptureResult[AnyStr]:
706
+ out = self.out.snap() if self.out else ""
707
+ err = self.err.snap() if self.err else ""
708
+ # TODO: This type error is real, need to fix.
709
+ return CaptureResult(out, err) # type: ignore[arg-type]
710
+
711
+
712
+ def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
713
+ if method == "fd":
714
+ return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
715
+ elif method == "sys":
716
+ return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2))
717
+ elif method == "no":
718
+ return MultiCapture(in_=None, out=None, err=None)
719
+ elif method == "tee-sys":
720
+ return MultiCapture(
721
+ in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)
722
+ )
723
+ raise ValueError(f"unknown capturing method: {method!r}")
724
+
725
+
726
+ # CaptureManager and CaptureFixture
727
+
728
+
729
+ class CaptureManager:
730
+ """The capture plugin.
731
+
732
+ Manages that the appropriate capture method is enabled/disabled during
733
+ collection and each test phase (setup, call, teardown). After each of
734
+ those points, the captured output is obtained and attached to the
735
+ collection/runtest report.
736
+
737
+ There are two levels of capture:
738
+
739
+ * global: enabled by default and can be suppressed by the ``-s``
740
+ option. This is always enabled/disabled during collection and each test
741
+ phase.
742
+
743
+ * fixture: when a test function or one of its fixture depend on the
744
+ ``capsys`` or ``capfd`` fixtures. In this case special handling is
745
+ needed to ensure the fixtures take precedence over the global capture.
746
+ """
747
+
748
+ def __init__(self, method: _CaptureMethod) -> None:
749
+ self._method: Final = method
750
+ self._global_capturing: MultiCapture[str] | None = None
751
+ self._capture_fixture: CaptureFixture[Any] | None = None
752
+
753
+ def __repr__(self) -> str:
754
+ return (
755
+ f"<CaptureManager _method={self._method!r} _global_capturing={self._global_capturing!r} "
756
+ f"_capture_fixture={self._capture_fixture!r}>"
757
+ )
758
+
759
+ def is_capturing(self) -> str | bool:
760
+ if self.is_globally_capturing():
761
+ return "global"
762
+ if self._capture_fixture:
763
+ return f"fixture {self._capture_fixture.request.fixturename}"
764
+ return False
765
+
766
+ # Global capturing control
767
+
768
+ def is_globally_capturing(self) -> bool:
769
+ return self._method != "no"
770
+
771
+ def start_global_capturing(self) -> None:
772
+ assert self._global_capturing is None
773
+ self._global_capturing = _get_multicapture(self._method)
774
+ self._global_capturing.start_capturing()
775
+
776
+ def stop_global_capturing(self) -> None:
777
+ if self._global_capturing is not None:
778
+ self._global_capturing.pop_outerr_to_orig()
779
+ self._global_capturing.stop_capturing()
780
+ self._global_capturing = None
781
+
782
+ def resume_global_capture(self) -> None:
783
+ # During teardown of the python process, and on rare occasions, capture
784
+ # attributes can be `None` while trying to resume global capture.
785
+ if self._global_capturing is not None:
786
+ self._global_capturing.resume_capturing()
787
+
788
+ def suspend_global_capture(self, in_: bool = False) -> None:
789
+ if self._global_capturing is not None:
790
+ self._global_capturing.suspend_capturing(in_=in_)
791
+
792
+ def suspend(self, in_: bool = False) -> None:
793
+ # Need to undo local capsys-et-al if it exists before disabling global capture.
794
+ self.suspend_fixture()
795
+ self.suspend_global_capture(in_)
796
+
797
+ def resume(self) -> None:
798
+ self.resume_global_capture()
799
+ self.resume_fixture()
800
+
801
+ def read_global_capture(self) -> CaptureResult[str]:
802
+ assert self._global_capturing is not None
803
+ return self._global_capturing.readouterr()
804
+
805
+ # Fixture Control
806
+
807
+ def set_fixture(self, capture_fixture: CaptureFixture[Any]) -> None:
808
+ if self._capture_fixture:
809
+ current_fixture = self._capture_fixture.request.fixturename
810
+ requested_fixture = capture_fixture.request.fixturename
811
+ capture_fixture.request.raiseerror(
812
+ f"cannot use {requested_fixture} and {current_fixture} at the same time"
813
+ )
814
+ self._capture_fixture = capture_fixture
815
+
816
+ def unset_fixture(self) -> None:
817
+ self._capture_fixture = None
818
+
819
+ def activate_fixture(self) -> None:
820
+ """If the current item is using ``capsys`` or ``capfd``, activate
821
+ them so they take precedence over the global capture."""
822
+ if self._capture_fixture:
823
+ self._capture_fixture._start()
824
+
825
+ def deactivate_fixture(self) -> None:
826
+ """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
827
+ if self._capture_fixture:
828
+ self._capture_fixture.close()
829
+
830
+ def suspend_fixture(self) -> None:
831
+ if self._capture_fixture:
832
+ self._capture_fixture._suspend()
833
+
834
+ def resume_fixture(self) -> None:
835
+ if self._capture_fixture:
836
+ self._capture_fixture._resume()
837
+
838
+ # Helper context managers
839
+
840
+ @contextlib.contextmanager
841
+ def global_and_fixture_disabled(self) -> Generator[None]:
842
+ """Context manager to temporarily disable global and current fixture capturing."""
843
+ do_fixture = self._capture_fixture and self._capture_fixture._is_started()
844
+ if do_fixture:
845
+ self.suspend_fixture()
846
+ do_global = self._global_capturing and self._global_capturing.is_started()
847
+ if do_global:
848
+ self.suspend_global_capture()
849
+ try:
850
+ yield
851
+ finally:
852
+ if do_global:
853
+ self.resume_global_capture()
854
+ if do_fixture:
855
+ self.resume_fixture()
856
+
857
+ @contextlib.contextmanager
858
+ def item_capture(self, when: str, item: Item) -> Generator[None]:
859
+ self.resume_global_capture()
860
+ self.activate_fixture()
861
+ try:
862
+ yield
863
+ finally:
864
+ self.deactivate_fixture()
865
+ self.suspend_global_capture(in_=False)
866
+
867
+ out, err = self.read_global_capture()
868
+ item.add_report_section(when, "stdout", out)
869
+ item.add_report_section(when, "stderr", err)
870
+
871
+ # Hooks
872
+
873
+ @hookimpl(wrapper=True)
874
+ def pytest_make_collect_report(
875
+ self, collector: Collector
876
+ ) -> Generator[None, CollectReport, CollectReport]:
877
+ if isinstance(collector, File):
878
+ self.resume_global_capture()
879
+ try:
880
+ rep = yield
881
+ finally:
882
+ self.suspend_global_capture()
883
+ out, err = self.read_global_capture()
884
+ if out:
885
+ rep.sections.append(("Captured stdout", out))
886
+ if err:
887
+ rep.sections.append(("Captured stderr", err))
888
+ else:
889
+ rep = yield
890
+ return rep
891
+
892
+ @hookimpl(wrapper=True)
893
+ def pytest_runtest_setup(self, item: Item) -> Generator[None]:
894
+ with self.item_capture("setup", item):
895
+ return (yield)
896
+
897
+ @hookimpl(wrapper=True)
898
+ def pytest_runtest_call(self, item: Item) -> Generator[None]:
899
+ with self.item_capture("call", item):
900
+ return (yield)
901
+
902
+ @hookimpl(wrapper=True)
903
+ def pytest_runtest_teardown(self, item: Item) -> Generator[None]:
904
+ with self.item_capture("teardown", item):
905
+ return (yield)
906
+
907
+ @hookimpl(tryfirst=True)
908
+ def pytest_keyboard_interrupt(self) -> None:
909
+ self.stop_global_capturing()
910
+
911
+ @hookimpl(tryfirst=True)
912
+ def pytest_internalerror(self) -> None:
913
+ self.stop_global_capturing()
914
+
915
+
916
+ class CaptureFixture(Generic[AnyStr]):
917
+ """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
918
+ :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
919
+
920
+ def __init__(
921
+ self,
922
+ captureclass: type[CaptureBase[AnyStr]],
923
+ request: SubRequest,
924
+ *,
925
+ config: dict[str, Any] | None = None,
926
+ _ispytest: bool = False,
927
+ ) -> None:
928
+ check_ispytest(_ispytest)
929
+ self.captureclass: type[CaptureBase[AnyStr]] = captureclass
930
+ self.request = request
931
+ self._config = config if config else {}
932
+ self._capture: MultiCapture[AnyStr] | None = None
933
+ self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
934
+ self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
935
+
936
+ def _start(self) -> None:
937
+ if self._capture is None:
938
+ self._capture = MultiCapture(
939
+ in_=None,
940
+ out=self.captureclass(1, **self._config),
941
+ err=self.captureclass(2, **self._config),
942
+ )
943
+ self._capture.start_capturing()
944
+
945
+ def close(self) -> None:
946
+ if self._capture is not None:
947
+ if self._config.get("tee"):
948
+ # When tee is enabled, output was already written to the
949
+ # original stream in real-time by TeeCaptureIO. Using
950
+ # pop_outerr_to_orig() would write it a second time via
951
+ # writeorg(), causing doubled output (see #13784).
952
+ out, err = self._capture.readouterr()
953
+ else:
954
+ out, err = self._capture.pop_outerr_to_orig()
955
+ self._captured_out += out
956
+ self._captured_err += err
957
+ self._capture.stop_capturing()
958
+ self._capture = None
959
+
960
+ def readouterr(self) -> CaptureResult[AnyStr]:
961
+ """Read and return the captured output so far, resetting the internal
962
+ buffer.
963
+
964
+ :returns:
965
+ The captured content as a namedtuple with ``out`` and ``err``
966
+ string attributes.
967
+ """
968
+ captured_out, captured_err = self._captured_out, self._captured_err
969
+ if self._capture is not None:
970
+ out, err = self._capture.readouterr()
971
+ captured_out += out
972
+ captured_err += err
973
+ self._captured_out = self.captureclass.EMPTY_BUFFER
974
+ self._captured_err = self.captureclass.EMPTY_BUFFER
975
+ return CaptureResult(captured_out, captured_err)
976
+
977
+ def _suspend(self) -> None:
978
+ """Suspend this fixture's own capturing temporarily."""
979
+ if self._capture is not None:
980
+ self._capture.suspend_capturing()
981
+
982
+ def _resume(self) -> None:
983
+ """Resume this fixture's own capturing temporarily."""
984
+ if self._capture is not None:
985
+ self._capture.resume_capturing()
986
+
987
+ def _is_started(self) -> bool:
988
+ """Whether actively capturing -- not disabled or closed."""
989
+ if self._capture is not None:
990
+ return self._capture.is_started()
991
+ return False
992
+
993
+ @contextlib.contextmanager
994
+ def disabled(self) -> Generator[None]:
995
+ """Temporarily disable capturing while inside the ``with`` block."""
996
+ capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
997
+ "capturemanager"
998
+ )
999
+ with capmanager.global_and_fixture_disabled():
1000
+ yield
1001
+
1002
+
1003
+ # The fixtures.
1004
+
1005
+
1006
+ @fixture
1007
+ def capsys(request: SubRequest) -> Generator[CaptureFixture[str]]:
1008
+ r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
1009
+
1010
+ The captured output is made available via ``capsys.readouterr()`` method
1011
+ calls, which return a ``(out, err)`` namedtuple.
1012
+ ``out`` and ``err`` will be ``text`` objects.
1013
+
1014
+ Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
1015
+
1016
+ Example:
1017
+
1018
+ .. code-block:: python
1019
+
1020
+ def test_output(capsys):
1021
+ print("hello")
1022
+ captured = capsys.readouterr()
1023
+ assert captured.out == "hello\n"
1024
+ """
1025
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1026
+ capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
1027
+ capman.set_fixture(capture_fixture)
1028
+ capture_fixture._start()
1029
+ yield capture_fixture
1030
+ capture_fixture.close()
1031
+ capman.unset_fixture()
1032
+
1033
+
1034
+ @fixture
1035
+ def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]:
1036
+ r"""Enable simultaneous text capturing and pass-through of writes
1037
+ to ``sys.stdout`` and ``sys.stderr`` as defined by ``--capture=``.
1038
+
1039
+
1040
+ The captured output is made available via ``capteesys.readouterr()`` method
1041
+ calls, which return a ``(out, err)`` namedtuple.
1042
+ ``out`` and ``err`` will be ``text`` objects.
1043
+
1044
+ The output is also passed-through, allowing it to be "live-printed",
1045
+ reported, or both as defined by ``--capture=``.
1046
+
1047
+ Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
1048
+
1049
+ Example:
1050
+
1051
+ .. code-block:: python
1052
+
1053
+ def test_output(capteesys):
1054
+ print("hello")
1055
+ captured = capteesys.readouterr()
1056
+ assert captured.out == "hello\n"
1057
+ """
1058
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1059
+ capture_fixture = CaptureFixture(
1060
+ SysCapture, request, config=dict(tee=True), _ispytest=True
1061
+ )
1062
+ capman.set_fixture(capture_fixture)
1063
+ capture_fixture._start()
1064
+ yield capture_fixture
1065
+ capture_fixture.close()
1066
+ capman.unset_fixture()
1067
+
1068
+
1069
+ @fixture
1070
+ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
1071
+ r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
1072
+
1073
+ The captured output is made available via ``capsysbinary.readouterr()``
1074
+ method calls, which return a ``(out, err)`` namedtuple.
1075
+ ``out`` and ``err`` will be ``bytes`` objects.
1076
+
1077
+ Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
1078
+
1079
+ Example:
1080
+
1081
+ .. code-block:: python
1082
+
1083
+ def test_output(capsysbinary):
1084
+ print("hello")
1085
+ captured = capsysbinary.readouterr()
1086
+ assert captured.out == b"hello\n"
1087
+ """
1088
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1089
+ capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
1090
+ capman.set_fixture(capture_fixture)
1091
+ capture_fixture._start()
1092
+ yield capture_fixture
1093
+ capture_fixture.close()
1094
+ capman.unset_fixture()
1095
+
1096
+
1097
+ @fixture
1098
+ def capfd(request: SubRequest) -> Generator[CaptureFixture[str]]:
1099
+ r"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
1100
+
1101
+ The captured output is made available via ``capfd.readouterr()`` method
1102
+ calls, which return a ``(out, err)`` namedtuple.
1103
+ ``out`` and ``err`` will be ``text`` objects.
1104
+
1105
+ Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
1106
+
1107
+ Example:
1108
+
1109
+ .. code-block:: python
1110
+
1111
+ def test_system_echo(capfd):
1112
+ os.system('echo "hello"')
1113
+ captured = capfd.readouterr()
1114
+ assert captured.out == "hello\n"
1115
+ """
1116
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1117
+ capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
1118
+ capman.set_fixture(capture_fixture)
1119
+ capture_fixture._start()
1120
+ yield capture_fixture
1121
+ capture_fixture.close()
1122
+ capman.unset_fixture()
1123
+
1124
+
1125
+ @fixture
1126
+ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
1127
+ r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
1128
+
1129
+ The captured output is made available via ``capfd.readouterr()`` method
1130
+ calls, which return a ``(out, err)`` namedtuple.
1131
+ ``out`` and ``err`` will be ``byte`` objects.
1132
+
1133
+ Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
1134
+
1135
+ Example:
1136
+
1137
+ .. code-block:: python
1138
+
1139
+ def test_system_echo(capfdbinary):
1140
+ os.system('echo "hello"')
1141
+ captured = capfdbinary.readouterr()
1142
+ assert captured.out == b"hello\n"
1143
+
1144
+ """
1145
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1146
+ capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
1147
+ capman.set_fixture(capture_fixture)
1148
+ capture_fixture._start()
1149
+ yield capture_fixture
1150
+ capture_fixture.close()
1151
+ capman.unset_fixture()
.venv/lib/python3.14/site-packages/_pytest/compat.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Python version compatibility code and random general utilities."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable
7
+ import enum
8
+ import functools
9
+ import inspect
10
+ from inspect import Parameter
11
+ from inspect import Signature
12
+ import os
13
+ from pathlib import Path
14
+ import sys
15
+ from typing import Any
16
+ from typing import Final
17
+ from typing import NoReturn
18
+ from typing import TYPE_CHECKING
19
+
20
+ import py
21
+
22
+
23
+ if sys.version_info >= (3, 14):
24
+ from annotationlib import Format
25
+
26
+
27
+ #: constant to prepare valuing pylib path replacements/lazy proxies later on
28
+ # intended for removal in pytest 8.0 or 9.0
29
+
30
+ # fmt: off
31
+ # intentional space to create a fake difference for the verification
32
+ LEGACY_PATH = py.path. local
33
+ # fmt: on
34
+
35
+
36
+ def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
37
+ """Internal wrapper to prepare lazy proxies for legacy_path instances"""
38
+ return LEGACY_PATH(path)
39
+
40
+
41
+ # fmt: off
42
+ # Singleton type for NOTSET, as described in:
43
+ # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
44
+ class NotSetType(enum.Enum):
45
+ token = 0
46
+ NOTSET: Final = NotSetType.token
47
+ # fmt: on
48
+
49
+
50
+ def iscoroutinefunction(func: object) -> bool:
51
+ """Return True if func is a coroutine function (a function defined with async
52
+ def syntax, and doesn't contain yield), or a function decorated with
53
+ @asyncio.coroutine.
54
+
55
+ Note: copied and modified from Python 3.5's builtin coroutines.py to avoid
56
+ importing asyncio directly, which in turns also initializes the "logging"
57
+ module as a side-effect (see issue #8).
58
+ """
59
+ return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
60
+
61
+
62
+ def is_async_function(func: object) -> bool:
63
+ """Return True if the given function seems to be an async function or
64
+ an async generator."""
65
+ return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
66
+
67
+
68
+ def signature(obj: Callable[..., Any]) -> Signature:
69
+ """Return signature without evaluating annotations."""
70
+ if sys.version_info >= (3, 14):
71
+ return inspect.signature(obj, annotation_format=Format.STRING)
72
+ return inspect.signature(obj)
73
+
74
+
75
+ def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
76
+ function = get_real_func(function)
77
+ fn = Path(inspect.getfile(function))
78
+ lineno = function.__code__.co_firstlineno
79
+ if curdir is not None:
80
+ try:
81
+ relfn = fn.relative_to(curdir)
82
+ except ValueError:
83
+ pass
84
+ else:
85
+ return f"{relfn}:{lineno + 1}"
86
+ return f"{fn}:{lineno + 1}"
87
+
88
+
89
+ def num_mock_patch_args(function) -> int:
90
+ """Return number of arguments used up by mock arguments (if any)."""
91
+ patchings = getattr(function, "patchings", None)
92
+ if not patchings:
93
+ return 0
94
+
95
+ mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
96
+ ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
97
+
98
+ return len(
99
+ [
100
+ p
101
+ for p in patchings
102
+ if not p.attribute_name
103
+ and (p.new is mock_sentinel or p.new is ut_mock_sentinel)
104
+ ]
105
+ )
106
+
107
+
108
+ def getfuncargnames(
109
+ function: Callable[..., object],
110
+ *,
111
+ name: str = "",
112
+ cls: type | None = None,
113
+ ) -> tuple[str, ...]:
114
+ """Return the names of a function's mandatory arguments.
115
+
116
+ Should return the names of all function arguments that:
117
+ * Aren't bound to an instance or type as in instance or class methods.
118
+ * Don't have default values.
119
+ * Aren't bound with functools.partial.
120
+ * Aren't replaced with mocks.
121
+
122
+ The cls arguments indicate that the function should be treated as a bound
123
+ method even though it's not unless the function is a static method.
124
+
125
+ The name parameter should be the original name in which the function was collected.
126
+ """
127
+ # TODO(RonnyPfannschmidt): This function should be refactored when we
128
+ # revisit fixtures. The fixture mechanism should ask the node for
129
+ # the fixture names, and not try to obtain directly from the
130
+ # function object well after collection has occurred.
131
+
132
+ # The parameters attribute of a Signature object contains an
133
+ # ordered mapping of parameter names to Parameter instances. This
134
+ # creates a tuple of the names of the parameters that don't have
135
+ # defaults.
136
+ try:
137
+ parameters = signature(function).parameters.values()
138
+ except (ValueError, TypeError) as e:
139
+ from _pytest.outcomes import fail
140
+
141
+ fail(
142
+ f"Could not determine arguments of {function!r}: {e}",
143
+ pytrace=False,
144
+ )
145
+
146
+ arg_names = tuple(
147
+ p.name
148
+ for p in parameters
149
+ if (
150
+ p.kind is Parameter.POSITIONAL_OR_KEYWORD
151
+ or p.kind is Parameter.KEYWORD_ONLY
152
+ )
153
+ and p.default is Parameter.empty
154
+ )
155
+ if not name:
156
+ name = function.__name__
157
+
158
+ # If this function should be treated as a bound method even though
159
+ # it's passed as an unbound method or function, and its first parameter
160
+ # wasn't defined as positional only, remove the first parameter name.
161
+ if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and (
162
+ # Not using `getattr` because we don't want to resolve the staticmethod.
163
+ # Not using `cls.__dict__` because we want to check the entire MRO.
164
+ cls
165
+ and not isinstance(
166
+ inspect.getattr_static(cls, name, default=None), staticmethod
167
+ )
168
+ ):
169
+ arg_names = arg_names[1:]
170
+ # Remove any names that will be replaced with mocks.
171
+ if hasattr(function, "__wrapped__"):
172
+ arg_names = arg_names[num_mock_patch_args(function) :]
173
+ return arg_names
174
+
175
+
176
+ def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
177
+ # Note: this code intentionally mirrors the code at the beginning of
178
+ # getfuncargnames, to get the arguments which were excluded from its result
179
+ # because they had default values.
180
+ return tuple(
181
+ p.name
182
+ for p in signature(function).parameters.values()
183
+ if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
184
+ and p.default is not Parameter.empty
185
+ )
186
+
187
+
188
+ _non_printable_ascii_translate_table = {
189
+ i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
190
+ }
191
+ _non_printable_ascii_translate_table.update(
192
+ {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"}
193
+ )
194
+
195
+
196
+ def ascii_escaped(val: bytes | str) -> str:
197
+ r"""If val is pure ASCII, return it as an str, otherwise, escape
198
+ bytes objects into a sequence of escaped bytes:
199
+
200
+ b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
201
+
202
+ and escapes strings into a sequence of escaped unicode ids, e.g.:
203
+
204
+ r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
205
+
206
+ Note:
207
+ The obvious "v.decode('unicode-escape')" will return
208
+ valid UTF-8 unicode if it finds them in bytes, but we
209
+ want to return escaped bytes for any byte, even if they match
210
+ a UTF-8 string.
211
+ """
212
+ if isinstance(val, bytes):
213
+ ret = val.decode("ascii", "backslashreplace")
214
+ else:
215
+ ret = val.encode("unicode_escape").decode("ascii")
216
+ return ret.translate(_non_printable_ascii_translate_table)
217
+
218
+
219
+ def get_real_func(obj):
220
+ """Get the real function object of the (possibly) wrapped object by
221
+ :func:`functools.wraps`, or :func:`functools.partial`."""
222
+ obj = inspect.unwrap(obj)
223
+
224
+ if isinstance(obj, functools.partial):
225
+ obj = obj.func
226
+ return obj
227
+
228
+
229
+ def getimfunc(func):
230
+ try:
231
+ return func.__func__
232
+ except AttributeError:
233
+ return func
234
+
235
+
236
+ def safe_getattr(object: Any, name: str, default: Any) -> Any:
237
+ """Like getattr but return default upon any Exception or any OutcomeException.
238
+
239
+ Attribute access can potentially fail for 'evil' Python objects.
240
+ See issue #214.
241
+ It catches OutcomeException because of #2490 (issue #580), new outcomes
242
+ are derived from BaseException instead of Exception (for more details
243
+ check #2707).
244
+ """
245
+ from _pytest.outcomes import TEST_OUTCOME
246
+
247
+ try:
248
+ return getattr(object, name, default)
249
+ except TEST_OUTCOME:
250
+ return default
251
+
252
+
253
+ def safe_isclass(obj: object) -> bool:
254
+ """Ignore any exception via isinstance on Python 3."""
255
+ try:
256
+ return inspect.isclass(obj)
257
+ except Exception:
258
+ return False
259
+
260
+
261
+ def get_user_id() -> int | None:
262
+ """Return the current process's real user id or None if it could not be
263
+ determined.
264
+
265
+ :return: The user id or None if it could not be determined.
266
+ """
267
+ # mypy follows the version and platform checking expectation of PEP 484:
268
+ # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
269
+ # Containment checks are too complex for mypy v1.5.0 and cause failure.
270
+ if sys.platform == "win32" or sys.platform == "emscripten":
271
+ # win32 does not have a getuid() function.
272
+ # Emscripten has a return 0 stub.
273
+ return None
274
+ else:
275
+ # On other platforms, a return value of -1 is assumed to indicate that
276
+ # the current process's real user id could not be determined.
277
+ ERROR = -1
278
+ uid = os.getuid()
279
+ return uid if uid != ERROR else None
280
+
281
+
282
+ if sys.version_info >= (3, 11):
283
+ from typing import assert_never
284
+ else:
285
+
286
+ def assert_never(value: NoReturn) -> NoReturn:
287
+ assert False, f"Unhandled value: {value} ({type(value).__name__})"
288
+
289
+
290
+ class CallableBool:
291
+ """
292
+ A bool-like object that can also be called, returning its true/false value.
293
+
294
+ Used for backwards compatibility in cases where something was supposed to be a method
295
+ but was implemented as a simple attribute by mistake (see `TerminalReporter.isatty`).
296
+
297
+ Do not use in new code.
298
+ """
299
+
300
+ def __init__(self, value: bool) -> None:
301
+ self._value = value
302
+
303
+ def __bool__(self) -> bool:
304
+ return self._value
305
+
306
+ def __call__(self) -> bool:
307
+ return self._value
308
+
309
+
310
+ def running_on_ci() -> bool:
311
+ """Check if we're currently running on a CI system."""
312
+ # Only enable CI mode if one of these env variables is defined and non-empty.
313
+ # Note: review `regendoc` tox env in case this list is changed.
314
+ env_vars = ["CI", "BUILD_NUMBER"]
315
+ return any(os.environ.get(var) for var in env_vars)
316
+
317
+
318
+ if sys.version_info >= (3, 13):
319
+ from warnings import deprecated as deprecated
320
+ else:
321
+ if TYPE_CHECKING:
322
+ from typing_extensions import deprecated as deprecated
323
+ else:
324
+
325
+ def deprecated(msg, /, *, category=None, stacklevel=1):
326
+ def decorator(func):
327
+ return func
328
+
329
+ return decorator
.venv/lib/python3.14/site-packages/_pytest/config/__init__.py ADDED
@@ -0,0 +1,2246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Command line options, config-file and conftest.py processing."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import builtins
8
+ import collections.abc
9
+ from collections.abc import Callable
10
+ from collections.abc import Generator
11
+ from collections.abc import Iterable
12
+ from collections.abc import Iterator
13
+ from collections.abc import Mapping
14
+ from collections.abc import MutableMapping
15
+ from collections.abc import Sequence
16
+ import contextlib
17
+ import copy
18
+ import dataclasses
19
+ import enum
20
+ from functools import lru_cache
21
+ import glob
22
+ import importlib
23
+ import importlib.metadata
24
+ import inspect
25
+ import os
26
+ import pathlib
27
+ import re
28
+ import shlex
29
+ import sys
30
+ from textwrap import dedent
31
+ import types
32
+ from types import FunctionType
33
+ from typing import Any
34
+ from typing import cast
35
+ from typing import Final
36
+ from typing import final
37
+ from typing import IO
38
+ from typing import TextIO
39
+ from typing import TYPE_CHECKING
40
+ import warnings
41
+
42
+ from pluggy import HookimplMarker
43
+ from pluggy import HookimplOpts
44
+ from pluggy import HookspecMarker
45
+ from pluggy import HookspecOpts
46
+ from pluggy import PluginManager
47
+
48
+ from .exceptions import PrintHelp as PrintHelp
49
+ from .exceptions import UsageError as UsageError
50
+ from .findpaths import ConfigDict
51
+ from .findpaths import ConfigValue
52
+ from .findpaths import determine_setup
53
+ from .findpaths import parse_override_ini
54
+ from _pytest import __version__
55
+ import _pytest._code
56
+ from _pytest._code import ExceptionInfo
57
+ from _pytest._code import filter_traceback
58
+ from _pytest._code.code import TracebackStyle
59
+ from _pytest._io import TerminalWriter
60
+ from _pytest.compat import assert_never
61
+ from _pytest.compat import deprecated
62
+ from _pytest.compat import NOTSET
63
+ from _pytest.config.argparsing import Argument
64
+ from _pytest.config.argparsing import FILE_OR_DIR
65
+ from _pytest.config.argparsing import Parser
66
+ import _pytest.deprecated
67
+ import _pytest.hookspec
68
+ from _pytest.outcomes import fail
69
+ from _pytest.outcomes import Skipped
70
+ from _pytest.pathlib import absolutepath
71
+ from _pytest.pathlib import bestrelpath
72
+ from _pytest.pathlib import import_path
73
+ from _pytest.pathlib import ImportMode
74
+ from _pytest.pathlib import resolve_package_path
75
+ from _pytest.pathlib import safe_exists
76
+ from _pytest.stash import Stash
77
+ from _pytest.warning_types import PytestConfigWarning
78
+ from _pytest.warning_types import warn_explicit_for
79
+
80
+
81
+ if TYPE_CHECKING:
82
+ from _pytest.assertion.rewrite import AssertionRewritingHook
83
+ from _pytest.cacheprovider import Cache
84
+ from _pytest.terminal import TerminalReporter
85
+
86
+ _PluggyPlugin = object
87
+ """A type to represent plugin objects.
88
+
89
+ Plugins can be any namespace, so we can't narrow it down much, but we use an
90
+ alias to make the intent clear.
91
+
92
+ Ideally this type would be provided by pluggy itself.
93
+ """
94
+
95
+
96
+ hookimpl = HookimplMarker("pytest")
97
+ hookspec = HookspecMarker("pytest")
98
+
99
+
100
+ @final
101
+ class ExitCode(enum.IntEnum):
102
+ """Encodes the valid exit codes by pytest.
103
+
104
+ Currently users and plugins may supply other exit codes as well.
105
+
106
+ .. versionadded:: 5.0
107
+ """
108
+
109
+ #: Tests passed.
110
+ OK = 0
111
+ #: Tests failed.
112
+ TESTS_FAILED = 1
113
+ #: pytest was interrupted.
114
+ INTERRUPTED = 2
115
+ #: An internal error got in the way.
116
+ INTERNAL_ERROR = 3
117
+ #: pytest was misused.
118
+ USAGE_ERROR = 4
119
+ #: pytest couldn't find tests.
120
+ NO_TESTS_COLLECTED = 5
121
+ #: All tests pass, but maximum number of warnings exceeded.
122
+ MAX_WARNINGS_ERROR = 6
123
+
124
+ __module__ = "pytest"
125
+
126
+
127
+ class ConftestImportFailure(Exception):
128
+ def __init__(
129
+ self,
130
+ path: pathlib.Path,
131
+ *,
132
+ cause: Exception,
133
+ ) -> None:
134
+ self.path = path
135
+ self.cause = cause
136
+
137
+ def __str__(self) -> str:
138
+ return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
139
+
140
+
141
+ def filter_traceback_for_conftest_import_failure(
142
+ entry: _pytest._code.TracebackEntry,
143
+ ) -> bool:
144
+ """Filter tracebacks entries which point to pytest internals or importlib.
145
+
146
+ Make a special case for importlib because we use it to import test modules and conftest files
147
+ in _pytest.pathlib.import_path.
148
+ """
149
+ return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
150
+
151
+
152
+ def print_conftest_import_error(e: ConftestImportFailure, file: TextIO) -> None:
153
+ exc_info = ExceptionInfo.from_exception(e.cause)
154
+ tw = TerminalWriter(file)
155
+ tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
156
+ exc_info.traceback = exc_info.traceback.filter(
157
+ filter_traceback_for_conftest_import_failure
158
+ )
159
+ exc_repr = (
160
+ exc_info.getrepr(style="short", chain=False)
161
+ if exc_info.traceback
162
+ else exc_info.exconly()
163
+ )
164
+ formatted_tb = str(exc_repr)
165
+ for line in formatted_tb.splitlines():
166
+ tw.line(line.rstrip(), red=True)
167
+
168
+
169
+ def print_usage_error(e: UsageError, file: TextIO) -> None:
170
+ tw = TerminalWriter(file)
171
+ for msg in e.args:
172
+ tw.line(f"ERROR: {msg}\n", red=True)
173
+
174
+
175
+ def _get_prog_name(argv: Sequence[str]) -> str:
176
+ """Determine the CLI program name from the argument vector.
177
+
178
+ :param argv: The argument vector (typically ``sys.argv``).
179
+ :returns: ``"python -m pytest"`` when invoked via ``python -m``,
180
+ ``"pytest"`` otherwise.
181
+ """
182
+ argv0 = argv[0] if argv else ""
183
+ if os.path.basename(argv0) == "__main__.py":
184
+ return "python -m pytest"
185
+ return "pytest"
186
+
187
+
188
+ def main(
189
+ args: list[str] | os.PathLike[str] | None = None,
190
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
191
+ ) -> int | ExitCode:
192
+ """Perform an in-process test run.
193
+
194
+ :param args:
195
+ List of command line arguments. If `None` or not given, defaults to reading
196
+ arguments directly from the process command line (:data:`sys.argv`).
197
+ :param plugins: List of plugin objects to be auto-registered during initialization.
198
+
199
+ :returns: An exit code.
200
+ """
201
+ return _main(args=args, plugins=plugins, prog="pytest.main()")
202
+
203
+
204
+ def _main(
205
+ *,
206
+ args: list[str] | os.PathLike[str] | None = None,
207
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
208
+ prog: str,
209
+ ) -> int | ExitCode:
210
+ # Handle a single `--version`/`-V` argument early to avoid starting up the entire pytest infrastructure.
211
+ new_args = sys.argv[1:] if args is None else args
212
+ if (
213
+ isinstance(new_args, Sequence)
214
+ and (new_args.count("--version") + new_args.count("-V")) == 1
215
+ ):
216
+ sys.stdout.write(f"pytest {__version__}\n")
217
+ return ExitCode.OK
218
+
219
+ old_pytest_version = os.environ.get("PYTEST_VERSION")
220
+ try:
221
+ os.environ["PYTEST_VERSION"] = __version__
222
+ try:
223
+ config = _prepareconfig(new_args, plugins, prog=prog)
224
+ except ConftestImportFailure as e:
225
+ print_conftest_import_error(e, file=sys.stderr)
226
+ return ExitCode.USAGE_ERROR
227
+
228
+ try:
229
+ ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
230
+ try:
231
+ return ExitCode(ret)
232
+ except ValueError:
233
+ return ret
234
+ finally:
235
+ config._ensure_unconfigure()
236
+ except UsageError as e:
237
+ print_usage_error(e, file=sys.stderr)
238
+ return ExitCode.USAGE_ERROR
239
+ finally:
240
+ if old_pytest_version is None:
241
+ os.environ.pop("PYTEST_VERSION", None)
242
+ else:
243
+ os.environ["PYTEST_VERSION"] = old_pytest_version
244
+
245
+
246
+ def _console_main() -> int:
247
+ """The CLI entry point of pytest (internal).
248
+
249
+ This is the real implementation used by entry points and ``__main__.py``.
250
+ """
251
+ # https://docs.python.org/3/library/signal.html#note-on-sigpipe
252
+ try:
253
+ code = _main(prog=_get_prog_name(sys.argv))
254
+ sys.stdout.flush()
255
+ return code
256
+ except BrokenPipeError:
257
+ # Python flushes standard streams on exit; redirect remaining output
258
+ # to devnull to avoid another BrokenPipeError at shutdown
259
+ devnull = os.open(os.devnull, os.O_WRONLY)
260
+ os.dup2(devnull, sys.stdout.fileno())
261
+ return 1 # Python exits with error code 1 on EPIPE
262
+
263
+
264
+ def console_main() -> int:
265
+ """The CLI entry point of pytest.
266
+
267
+ .. deprecated:: 9.1
268
+ This function is slated for removal in pytest 10.
269
+ It is not meant for programmable use; use :func:`pytest.main` instead.
270
+ """
271
+ import warnings
272
+
273
+ from _pytest.deprecated import CONSOLE_MAIN
274
+
275
+ warnings.warn(CONSOLE_MAIN, stacklevel=2)
276
+ return _console_main()
277
+
278
+
279
+ class cmdline: # compatibility namespace
280
+ main = staticmethod(main)
281
+
282
+
283
+ def filename_arg(path: str, optname: str) -> str:
284
+ """Argparse type validator for filename arguments.
285
+
286
+ :path: Path of filename.
287
+ :optname: Name of the option.
288
+ """
289
+ if os.path.isdir(path):
290
+ raise UsageError(f"{optname} must be a filename, given: {path}")
291
+ return path
292
+
293
+
294
+ def directory_arg(path: str, optname: str) -> str:
295
+ """Argparse type validator for directory arguments.
296
+
297
+ :path: Path of directory.
298
+ :optname: Name of the option.
299
+ """
300
+ if not os.path.isdir(path):
301
+ raise UsageError(f"{optname} must be a directory, given: {path}")
302
+ return path
303
+
304
+
305
+ # Plugins that cannot be disabled via "-p no:X" currently.
306
+ essential_plugins = (
307
+ "mark",
308
+ "main",
309
+ "runner",
310
+ "fixtures",
311
+ "helpconfig", # Provides -p.
312
+ )
313
+
314
+ default_plugins = (
315
+ *essential_plugins,
316
+ "python",
317
+ "terminal",
318
+ "debugging",
319
+ "unittest",
320
+ "capture",
321
+ "skipping",
322
+ "legacypath",
323
+ "tmpdir",
324
+ "monkeypatch",
325
+ "recwarn",
326
+ "pastebin",
327
+ "assertion",
328
+ "junitxml",
329
+ "doctest",
330
+ "cacheprovider",
331
+ "setuponly",
332
+ "setupplan",
333
+ "stepwise",
334
+ "unraisableexception",
335
+ "threadexception",
336
+ "warnings",
337
+ "logging",
338
+ "reports",
339
+ "faulthandler",
340
+ "subtests",
341
+ )
342
+
343
+ builtin_plugins = {
344
+ *default_plugins,
345
+ "pytester",
346
+ "pytester_assertions",
347
+ "terminalprogress",
348
+ }
349
+
350
+
351
+ def get_config(
352
+ args: Iterable[str] | None = None,
353
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
354
+ *,
355
+ prog: str | None = None,
356
+ ) -> Config:
357
+ # Subsequent calls to main will create a fresh instance.
358
+ pluginmanager = PytestPluginManager()
359
+ invocation_params = Config.InvocationParams(
360
+ args=args or (),
361
+ plugins=plugins,
362
+ dir=pathlib.Path.cwd(),
363
+ )
364
+ config = Config(pluginmanager, invocation_params=invocation_params, prog=prog)
365
+
366
+ if invocation_params.args:
367
+ # Handle any "-p no:plugin" args.
368
+ pluginmanager.consider_preparse(invocation_params.args, exclude_only=True)
369
+
370
+ for spec in default_plugins:
371
+ pluginmanager.import_plugin(spec)
372
+
373
+ return config
374
+
375
+
376
+ def get_plugin_manager() -> PytestPluginManager:
377
+ """Obtain a new instance of the
378
+ :py:class:`pytest.PytestPluginManager`, with default plugins
379
+ already loaded.
380
+
381
+ This function can be used by integration with other tools, like hooking
382
+ into pytest to run tests into an IDE.
383
+ """
384
+ return get_config().pluginmanager
385
+
386
+
387
+ def _prepareconfig(
388
+ args: list[str] | os.PathLike[str],
389
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
390
+ *,
391
+ prog: str | None = None,
392
+ ) -> Config:
393
+ if isinstance(args, os.PathLike):
394
+ args = [os.fspath(args)]
395
+ elif not isinstance(args, list):
396
+ msg = ( # type:ignore[unreachable]
397
+ "`args` parameter expected to be a list of strings, got: {!r} (type: {})"
398
+ )
399
+ raise TypeError(msg.format(args, type(args)))
400
+
401
+ initial_config = get_config(args, plugins, prog=prog)
402
+ pluginmanager = initial_config.pluginmanager
403
+ try:
404
+ if plugins:
405
+ for plugin in plugins:
406
+ if isinstance(plugin, str):
407
+ pluginmanager.consider_pluginarg(plugin)
408
+ else:
409
+ pluginmanager.register(plugin)
410
+ config: Config = pluginmanager.hook.pytest_cmdline_parse(
411
+ pluginmanager=pluginmanager, args=args
412
+ )
413
+ return config
414
+ except BaseException:
415
+ initial_config._ensure_unconfigure()
416
+ raise
417
+
418
+
419
+ def _get_directory(path: pathlib.Path) -> pathlib.Path:
420
+ """Get the directory of a path - itself if already a directory."""
421
+ if path.is_file():
422
+ return path.parent
423
+ else:
424
+ return path
425
+
426
+
427
+ def _get_legacy_hook_marks(
428
+ method: Any,
429
+ hook_type: str,
430
+ opt_names: tuple[str, ...],
431
+ ) -> dict[str, bool]:
432
+ if TYPE_CHECKING:
433
+ # abuse typeguard from importlib to avoid massive method type union that's lacking an alias
434
+ assert inspect.isroutine(method)
435
+ known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
436
+ must_warn: list[str] = []
437
+ opts: dict[str, bool] = {}
438
+ for opt_name in opt_names:
439
+ opt_attr = getattr(method, opt_name, AttributeError)
440
+ if opt_attr is not AttributeError:
441
+ must_warn.append(f"{opt_name}={opt_attr}")
442
+ opts[opt_name] = True
443
+ elif opt_name in known_marks:
444
+ must_warn.append(f"{opt_name}=True")
445
+ opts[opt_name] = True
446
+ else:
447
+ opts[opt_name] = False
448
+ if must_warn:
449
+ hook_opts = ", ".join(must_warn)
450
+ message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
451
+ type=hook_type,
452
+ fullname=method.__qualname__,
453
+ hook_opts=hook_opts,
454
+ )
455
+ warn_explicit_for(cast(FunctionType, method), message)
456
+ return opts
457
+
458
+
459
+ @final
460
+ class PytestPluginManager(PluginManager):
461
+ """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
462
+ additional pytest-specific functionality:
463
+
464
+ * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
465
+ ``pytest_plugins`` global variables found in plugins being loaded.
466
+ * ``conftest.py`` loading during start-up.
467
+ """
468
+
469
+ def __init__(self) -> None:
470
+ from _pytest.assertion import DummyRewriteHook
471
+ from _pytest.assertion import RewriteHook
472
+
473
+ super().__init__("pytest")
474
+
475
+ # -- State related to local conftest plugins.
476
+ # All loaded conftest modules.
477
+ self._conftest_plugins: set[types.ModuleType] = set()
478
+ # All conftest modules applicable for a directory.
479
+ # This includes the directory's own conftest modules as well
480
+ # as those of its parent directories.
481
+ self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {}
482
+ # Cutoff directory above which conftests are no longer discovered.
483
+ self._confcutdir: pathlib.Path | None = None
484
+ # If set, conftest loading is skipped.
485
+ self._noconftest = False
486
+
487
+ # _getconftestmodules()'s call to _get_directory() causes a stat
488
+ # storm when it's called potentially thousands of times in a test
489
+ # session (#9478), often with the same path, so cache it.
490
+ self._get_directory = lru_cache(256)(_get_directory)
491
+
492
+ # plugins that were explicitly skipped with pytest.skip
493
+ # list of (module name, skip reason)
494
+ # previously we would issue a warning when a plugin was skipped, but
495
+ # since we refactored warnings as first citizens of Config, they are
496
+ # just stored here to be used later.
497
+ self.skipped_plugins: list[tuple[str, str]] = []
498
+
499
+ self.add_hookspecs(_pytest.hookspec)
500
+ self.register(self)
501
+ if os.environ.get("PYTEST_DEBUG"):
502
+ err: IO[str] = sys.stderr
503
+ encoding: str = getattr(err, "encoding", "utf8")
504
+ try:
505
+ err = open(
506
+ os.dup(err.fileno()),
507
+ mode=err.mode,
508
+ buffering=1,
509
+ encoding=encoding,
510
+ )
511
+ except Exception:
512
+ pass
513
+ self.trace.root.setwriter(err.write)
514
+ self.enable_tracing()
515
+
516
+ # Config._consider_importhook will set a real object if required.
517
+ self.rewrite_hook: RewriteHook = DummyRewriteHook()
518
+ # Used to know when we are importing conftests after the pytest_configure stage.
519
+ self._configured = False
520
+
521
+ def parse_hookimpl_opts(
522
+ self, plugin: _PluggyPlugin, name: str
523
+ ) -> HookimplOpts | None:
524
+ """:meta private:"""
525
+ # pytest hooks are always prefixed with "pytest_",
526
+ # so we avoid accessing possibly non-readable attributes
527
+ # (see issue #1073).
528
+ if not name.startswith("pytest_"):
529
+ return None
530
+ # Ignore names which cannot be hooks.
531
+ if name == "pytest_plugins":
532
+ return None
533
+
534
+ opts = super().parse_hookimpl_opts(plugin, name)
535
+ if opts is not None:
536
+ return opts
537
+
538
+ method = getattr(plugin, name)
539
+ # Consider only actual functions for hooks (#3775).
540
+ if not inspect.isroutine(method):
541
+ return None
542
+ # Collect unmarked hooks as long as they have the `pytest_' prefix.
543
+ legacy = _get_legacy_hook_marks(
544
+ method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
545
+ )
546
+ return cast(HookimplOpts, legacy)
547
+
548
+ def parse_hookspec_opts(self, module_or_class, name: str) -> HookspecOpts | None:
549
+ """:meta private:"""
550
+ opts = super().parse_hookspec_opts(module_or_class, name)
551
+ if opts is None:
552
+ method = getattr(module_or_class, name)
553
+ if name.startswith("pytest_"):
554
+ legacy = _get_legacy_hook_marks(
555
+ method, "spec", ("firstresult", "historic")
556
+ )
557
+ opts = cast(HookspecOpts, legacy)
558
+ return opts
559
+
560
+ def register(self, plugin: _PluggyPlugin, name: str | None = None) -> str | None:
561
+ if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
562
+ warnings.warn(
563
+ PytestConfigWarning(
564
+ "{} plugin has been merged into the core, "
565
+ "please remove it from your requirements.".format(
566
+ name.replace("_", "-")
567
+ )
568
+ )
569
+ )
570
+ return None
571
+ plugin_name = super().register(plugin, name)
572
+ if plugin_name is not None:
573
+ self.hook.pytest_plugin_registered.call_historic(
574
+ kwargs=dict(
575
+ plugin=plugin,
576
+ plugin_name=plugin_name,
577
+ manager=self,
578
+ )
579
+ )
580
+
581
+ if isinstance(plugin, types.ModuleType):
582
+ self.consider_module(plugin)
583
+ return plugin_name
584
+
585
+ def getplugin(self, name: str):
586
+ # Support deprecated naming because plugins (xdist e.g.) use it.
587
+ plugin: _PluggyPlugin | None = self.get_plugin(name)
588
+ return plugin
589
+
590
+ def hasplugin(self, name: str) -> bool:
591
+ """Return whether a plugin with the given name is registered."""
592
+ return bool(self.get_plugin(name))
593
+
594
+ def pytest_configure(self, config: Config) -> None:
595
+ """:meta private:"""
596
+ # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
597
+ # we should remove tryfirst/trylast as markers.
598
+ config.addinivalue_line(
599
+ "markers",
600
+ "tryfirst: mark a hook implementation function such that the "
601
+ "plugin machinery will try to call it first/as early as possible. "
602
+ "DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.",
603
+ )
604
+ config.addinivalue_line(
605
+ "markers",
606
+ "trylast: mark a hook implementation function such that the "
607
+ "plugin machinery will try to call it last/as late as possible. "
608
+ "DEPRECATED, use @pytest.hookimpl(trylast=True) instead.",
609
+ )
610
+ self._configured = True
611
+
612
+ #
613
+ # Internal API for local conftest plugin handling.
614
+ #
615
+ def _set_initial_conftests(
616
+ self,
617
+ args: Sequence[str | pathlib.Path],
618
+ pyargs: bool,
619
+ noconftest: bool,
620
+ rootpath: pathlib.Path,
621
+ confcutdir: pathlib.Path | None,
622
+ invocation_dir: pathlib.Path,
623
+ importmode: ImportMode | str,
624
+ *,
625
+ consider_namespace_packages: bool,
626
+ ) -> None:
627
+ """Load initial conftest files given a preparsed "namespace".
628
+
629
+ As conftest files may add their own command line options which have
630
+ arguments ('--my-opt somepath') we might get some false positives.
631
+ All builtin and 3rd party plugins will have been loaded, however, so
632
+ common options will not confuse our logic here.
633
+ """
634
+ self._confcutdir = (
635
+ absolutepath(invocation_dir / confcutdir) if confcutdir else None
636
+ )
637
+ self._noconftest = noconftest
638
+ self._using_pyargs = pyargs
639
+
640
+ anchors = []
641
+ for initial_path in args:
642
+ path = str(initial_path)
643
+ # remove node-id syntax
644
+ i = path.find("::")
645
+ if i != -1:
646
+ path = path[:i]
647
+ anchor = absolutepath(invocation_dir / path)
648
+ # Ensure we do not break if what appears to be an anchor
649
+ # is in fact a very long option (#10169, #11394).
650
+ if not safe_exists(anchor):
651
+ continue
652
+
653
+ anchors.append(anchor)
654
+ # Let's also consider test* subdirs.
655
+ if anchor.is_dir():
656
+ anchors.extend(x for x in anchor.glob("test*") if x.is_dir())
657
+ if not anchors:
658
+ anchors.append(invocation_dir)
659
+ anchors.extend(x for x in invocation_dir.glob("test*") if x.is_dir())
660
+
661
+ for anchor in anchors:
662
+ self._loadconftestmodules(
663
+ anchor,
664
+ importmode,
665
+ rootpath,
666
+ consider_namespace_packages=consider_namespace_packages,
667
+ )
668
+
669
+ def _is_in_confcutdir(self, path: pathlib.Path) -> bool:
670
+ """Whether to consider the given path to load conftests from."""
671
+ if self._confcutdir is None:
672
+ return True
673
+ # The semantics here are literally:
674
+ # Do not load a conftest if it is found upwards from confcut dir.
675
+ # But this is *not* the same as:
676
+ # Load only conftests from confcutdir or below.
677
+ # At first glance they might seem the same thing, however we do support use cases where
678
+ # we want to load conftests that are not found in confcutdir or below, but are found
679
+ # in completely different directory hierarchies like packages installed
680
+ # in out-of-source trees.
681
+ # (see #9767 for a regression where the logic was inverted).
682
+ return path not in self._confcutdir.parents
683
+
684
+ def _loadconftestmodules(
685
+ self,
686
+ path: pathlib.Path,
687
+ importmode: str | ImportMode,
688
+ rootpath: pathlib.Path,
689
+ *,
690
+ consider_namespace_packages: bool,
691
+ ) -> None:
692
+ if self._noconftest:
693
+ return
694
+
695
+ directory = self._get_directory(path)
696
+
697
+ # Optimization: avoid repeated searches in the same directory.
698
+ # Assumes always called with same importmode and rootpath.
699
+ if directory in self._dirpath2confmods:
700
+ return
701
+
702
+ clist = []
703
+ for parent in reversed((directory, *directory.parents)):
704
+ if self._is_in_confcutdir(parent):
705
+ conftestpath = parent / "conftest.py"
706
+ if conftestpath.is_file():
707
+ mod = self._importconftest(
708
+ conftestpath,
709
+ importmode,
710
+ rootpath,
711
+ consider_namespace_packages=consider_namespace_packages,
712
+ )
713
+ clist.append(mod)
714
+ self._dirpath2confmods[directory] = clist
715
+
716
+ def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]:
717
+ directory = self._get_directory(path)
718
+ return self._dirpath2confmods.get(directory, ())
719
+
720
+ def _rget_with_confmod(
721
+ self,
722
+ name: str,
723
+ path: pathlib.Path,
724
+ ) -> tuple[types.ModuleType, Any]:
725
+ modules = self._getconftestmodules(path)
726
+ for mod in reversed(modules):
727
+ try:
728
+ return mod, getattr(mod, name)
729
+ except AttributeError:
730
+ continue
731
+ raise KeyError(name)
732
+
733
+ def _importconftest(
734
+ self,
735
+ conftestpath: pathlib.Path,
736
+ importmode: str | ImportMode,
737
+ rootpath: pathlib.Path,
738
+ *,
739
+ consider_namespace_packages: bool,
740
+ ) -> types.ModuleType:
741
+ conftestpath_plugin_name = str(conftestpath)
742
+ existing = self.get_plugin(conftestpath_plugin_name)
743
+ if existing is not None:
744
+ return cast(types.ModuleType, existing)
745
+
746
+ # conftest.py files there are not in a Python package all have module
747
+ # name "conftest", and thus conflict with each other. Clear the existing
748
+ # before loading the new one, otherwise the existing one will be
749
+ # returned from the module cache.
750
+ pkgpath = resolve_package_path(conftestpath)
751
+ if pkgpath is None:
752
+ try:
753
+ del sys.modules[conftestpath.stem]
754
+ except KeyError:
755
+ pass
756
+
757
+ try:
758
+ mod = import_path(
759
+ conftestpath,
760
+ mode=importmode,
761
+ root=rootpath,
762
+ consider_namespace_packages=consider_namespace_packages,
763
+ )
764
+ except Exception as e:
765
+ assert e.__traceback__ is not None
766
+ raise ConftestImportFailure(conftestpath, cause=e) from e
767
+
768
+ self._check_non_top_pytest_plugins(mod, conftestpath)
769
+
770
+ self._conftest_plugins.add(mod)
771
+ dirpath = conftestpath.parent
772
+ if dirpath in self._dirpath2confmods:
773
+ for path, mods in self._dirpath2confmods.items():
774
+ if dirpath in path.parents or path == dirpath:
775
+ if mod in mods:
776
+ raise AssertionError(
777
+ f"While trying to load conftest path {conftestpath!s}, "
778
+ f"found that the module {mod} is already loaded with path {mod.__file__}. "
779
+ "This is not supposed to happen. Please report this issue to pytest."
780
+ )
781
+ mods.append(mod)
782
+ self.trace(f"loading conftestmodule {mod!r}")
783
+ self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
784
+ return mod
785
+
786
+ def _check_non_top_pytest_plugins(
787
+ self,
788
+ mod: types.ModuleType,
789
+ conftestpath: pathlib.Path,
790
+ ) -> None:
791
+ if (
792
+ hasattr(mod, "pytest_plugins")
793
+ and self._configured
794
+ and not self._using_pyargs
795
+ ):
796
+ msg = (
797
+ "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n"
798
+ "It affects the entire test suite instead of just below the conftest as expected.\n"
799
+ " {}\n"
800
+ "Please move it to a top level conftest file at the rootdir:\n"
801
+ " {}\n"
802
+ "For more information, visit:\n"
803
+ " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files"
804
+ )
805
+ fail(msg.format(conftestpath, self._confcutdir), pytrace=False)
806
+
807
+ #
808
+ # API for bootstrapping plugin loading
809
+ #
810
+ #
811
+
812
+ def consider_preparse(
813
+ self, args: Sequence[str], *, exclude_only: bool = False
814
+ ) -> None:
815
+ """:meta private:"""
816
+ i = 0
817
+ n = len(args)
818
+ while i < n:
819
+ opt = args[i]
820
+ i += 1
821
+ if isinstance(opt, str):
822
+ if opt == "-p":
823
+ try:
824
+ parg = args[i]
825
+ except IndexError:
826
+ return
827
+ i += 1
828
+ elif opt.startswith("-p"):
829
+ parg = opt[2:]
830
+ else:
831
+ continue
832
+ parg = parg.strip()
833
+ if exclude_only and not parg.startswith("no:"):
834
+ continue
835
+ self.consider_pluginarg(parg)
836
+
837
+ def consider_pluginarg(self, arg: str) -> None:
838
+ """:meta private:"""
839
+ if arg.startswith("no:"):
840
+ name = arg[3:]
841
+ if name in essential_plugins:
842
+ raise UsageError(f"plugin {name} cannot be disabled")
843
+
844
+ if name.endswith("conftest.py"):
845
+ raise UsageError(
846
+ f"Blocking conftest files using -p is not supported: -p no:{name}\n"
847
+ "conftest.py files are not plugins and cannot be disabled via -p.\n"
848
+ )
849
+
850
+ # PR #4304: remove stepwise if cacheprovider is blocked.
851
+ if name == "cacheprovider":
852
+ self.set_blocked("stepwise")
853
+ self.set_blocked("pytest_stepwise")
854
+
855
+ self.set_blocked(name)
856
+ if not name.startswith("pytest_"):
857
+ self.set_blocked("pytest_" + name)
858
+ else:
859
+ name = arg
860
+ # Unblock the plugin.
861
+ self.unblock(name)
862
+ if not name.startswith("pytest_"):
863
+ self.unblock("pytest_" + name)
864
+ self.import_plugin(arg, consider_entry_points=True)
865
+
866
+ def consider_conftest(
867
+ self, conftestmodule: types.ModuleType, registration_name: str
868
+ ) -> None:
869
+ """:meta private:"""
870
+ self.register(conftestmodule, name=registration_name)
871
+
872
+ def consider_env(self) -> None:
873
+ """:meta private:"""
874
+ self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
875
+
876
+ def consider_module(self, mod: types.ModuleType) -> None:
877
+ """:meta private:"""
878
+ self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
879
+
880
+ def _import_plugin_specs(
881
+ self, spec: None | types.ModuleType | str | Sequence[str]
882
+ ) -> None:
883
+ plugins = _get_plugin_specs_as_list(spec)
884
+ for import_spec in plugins:
885
+ self.import_plugin(import_spec)
886
+
887
+ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
888
+ """Import a plugin with ``modname``.
889
+
890
+ If ``consider_entry_points`` is True, entry point names are also
891
+ considered to find a plugin.
892
+ """
893
+ # Most often modname refers to builtin modules, e.g. "pytester",
894
+ # "terminal" or "capture". Those plugins are registered under their
895
+ # basename for historic purposes but must be imported with the
896
+ # _pytest prefix.
897
+ assert isinstance(modname, str), (
898
+ f"module name as text required, got {modname!r}"
899
+ )
900
+ if self.is_blocked(modname) or self.get_plugin(modname) is not None:
901
+ return
902
+
903
+ importspec = "_pytest." + modname if modname in builtin_plugins else modname
904
+ self.rewrite_hook.mark_rewrite(importspec)
905
+
906
+ if consider_entry_points:
907
+ loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
908
+ if loaded:
909
+ return
910
+
911
+ try:
912
+ if sys.version_info >= (3, 11):
913
+ mod = importlib.import_module(importspec)
914
+ else:
915
+ # On Python 3.10, import_module breaks
916
+ # testing/test_config.py::test_disable_plugin_autoload.
917
+ __import__(importspec)
918
+ mod = sys.modules[importspec]
919
+ except ImportError as e:
920
+ raise ImportError(
921
+ f'Error importing plugin "{modname}": {e.args[0]}'
922
+ ).with_traceback(e.__traceback__) from e
923
+
924
+ except Skipped as e:
925
+ self.skipped_plugins.append((modname, e.msg or ""))
926
+ else:
927
+ self.register(mod, modname)
928
+
929
+
930
+ def _get_plugin_specs_as_list(
931
+ specs: None | types.ModuleType | str | Sequence[str],
932
+ ) -> list[str]:
933
+ """Parse a plugins specification into a list of plugin names."""
934
+ # None means empty.
935
+ if specs is None:
936
+ return []
937
+ # Workaround for #3899 - a submodule which happens to be called "pytest_plugins".
938
+ if isinstance(specs, types.ModuleType):
939
+ return []
940
+ # Comma-separated list.
941
+ if isinstance(specs, str):
942
+ return specs.split(",") if specs else []
943
+ # Direct specification.
944
+ if isinstance(specs, collections.abc.Sequence):
945
+ return list(specs)
946
+ raise UsageError(
947
+ f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}"
948
+ )
949
+
950
+
951
+ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
952
+ """Given an iterable of file names in a source distribution, return the "names" that should
953
+ be marked for assertion rewrite.
954
+
955
+ For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
956
+ the assertion rewrite mechanism.
957
+
958
+ This function has to deal with dist-info based distributions and egg based distributions
959
+ (which are still very much in use for "editable" installs).
960
+
961
+ Here are the file names as seen in a dist-info based distribution:
962
+
963
+ pytest_mock/__init__.py
964
+ pytest_mock/_version.py
965
+ pytest_mock/plugin.py
966
+ pytest_mock.egg-info/PKG-INFO
967
+
968
+ Here are the file names as seen in an egg based distribution:
969
+
970
+ src/pytest_mock/__init__.py
971
+ src/pytest_mock/_version.py
972
+ src/pytest_mock/plugin.py
973
+ src/pytest_mock.egg-info/PKG-INFO
974
+ LICENSE
975
+ setup.py
976
+
977
+ We have to take in account those two distribution flavors in order to determine which
978
+ names should be considered for assertion rewriting.
979
+
980
+ More information:
981
+ https://github.com/pytest-dev/pytest-mock/issues/167
982
+ """
983
+ package_files = list(package_files)
984
+ seen_some = False
985
+ for fn in package_files:
986
+ is_simple_module = "/" not in fn and fn.endswith(".py")
987
+ is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
988
+ if is_simple_module:
989
+ module_name, _ = os.path.splitext(fn)
990
+ # we ignore "setup.py" at the root of the distribution
991
+ # as well as editable installation finder modules made by setuptools
992
+ if module_name != "setup" and not module_name.startswith("__editable__"):
993
+ seen_some = True
994
+ yield module_name
995
+ elif is_package:
996
+ package_name = os.path.dirname(fn)
997
+ seen_some = True
998
+ yield package_name
999
+
1000
+ if not seen_some:
1001
+ # At this point we did not find any packages or modules suitable for assertion
1002
+ # rewriting, so we try again by stripping the first path component (to account for
1003
+ # "src" based source trees for example).
1004
+ # This approach lets us have the common case continue to be fast, as egg-distributions
1005
+ # are rarer.
1006
+ new_package_files = []
1007
+ for fn in package_files:
1008
+ parts = fn.split("/")
1009
+ new_fn = "/".join(parts[1:])
1010
+ if new_fn:
1011
+ new_package_files.append(new_fn)
1012
+ if new_package_files:
1013
+ yield from _iter_rewritable_modules(new_package_files)
1014
+
1015
+
1016
+ class _DeprecatedInicfgProxy(MutableMapping[str, Any]):
1017
+ """Compatibility proxy for the deprecated Config.inicfg."""
1018
+
1019
+ __slots__ = ("_config",)
1020
+
1021
+ def __init__(self, config: Config) -> None:
1022
+ self._config = config
1023
+
1024
+ def __getitem__(self, key: str) -> Any:
1025
+ return self._config._inicfg[key].value
1026
+
1027
+ def __setitem__(self, key: str, value: Any) -> None:
1028
+ self._config._inicfg[key] = ConfigValue(value, origin="override", mode="toml")
1029
+
1030
+ def __delitem__(self, key: str) -> None:
1031
+ del self._config._inicfg[key]
1032
+
1033
+ def __iter__(self) -> Iterator[str]:
1034
+ return iter(self._config._inicfg)
1035
+
1036
+ def __len__(self) -> int:
1037
+ return len(self._config._inicfg)
1038
+
1039
+
1040
+ @final
1041
+ class Config:
1042
+ """Access to configuration values, pluginmanager and plugin hooks.
1043
+
1044
+ :param PytestPluginManager pluginmanager:
1045
+ A pytest PluginManager.
1046
+
1047
+ :param InvocationParams invocation_params:
1048
+ Object containing parameters regarding the :func:`pytest.main`
1049
+ invocation.
1050
+ """
1051
+
1052
+ @final
1053
+ @dataclasses.dataclass(frozen=True)
1054
+ class InvocationParams:
1055
+ """Holds parameters passed during :func:`pytest.main`.
1056
+
1057
+ The object attributes are read-only.
1058
+
1059
+ .. versionadded:: 5.1
1060
+
1061
+ .. note::
1062
+
1063
+ Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``
1064
+ configuration option are handled by pytest, not being included in the ``args`` attribute.
1065
+
1066
+ Plugins accessing ``InvocationParams`` must be aware of that.
1067
+ """
1068
+
1069
+ args: tuple[str, ...]
1070
+ """The command-line arguments as passed to :func:`pytest.main`."""
1071
+ plugins: Sequence[str | _PluggyPlugin] | None
1072
+ """Extra plugins, might be `None`."""
1073
+ dir: pathlib.Path
1074
+ """The directory from which :func:`pytest.main` was invoked."""
1075
+
1076
+ def __init__(
1077
+ self,
1078
+ *,
1079
+ args: Iterable[str],
1080
+ plugins: Sequence[str | _PluggyPlugin] | None,
1081
+ dir: pathlib.Path,
1082
+ ) -> None:
1083
+ object.__setattr__(self, "args", tuple(args))
1084
+ object.__setattr__(self, "plugins", plugins)
1085
+ object.__setattr__(self, "dir", dir)
1086
+
1087
+ class ArgsSource(enum.Enum):
1088
+ """Indicates the source of the test arguments.
1089
+
1090
+ .. versionadded:: 7.2
1091
+ """
1092
+
1093
+ #: Command line arguments.
1094
+ ARGS = enum.auto()
1095
+ #: Invocation directory.
1096
+ INVOCATION_DIR = enum.auto()
1097
+ INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
1098
+ #: 'testpaths' configuration value.
1099
+ TESTPATHS = enum.auto()
1100
+
1101
+ # Set by cacheprovider plugin.
1102
+ cache: Cache
1103
+
1104
+ def __init__(
1105
+ self,
1106
+ pluginmanager: PytestPluginManager,
1107
+ *,
1108
+ invocation_params: InvocationParams | None = None,
1109
+ prog: str | None = None,
1110
+ ) -> None:
1111
+ if invocation_params is None:
1112
+ invocation_params = self.InvocationParams(
1113
+ args=(), plugins=None, dir=pathlib.Path.cwd()
1114
+ )
1115
+
1116
+ self.option = argparse.Namespace()
1117
+ """Access to command line option as attributes.
1118
+
1119
+ :type: argparse.Namespace
1120
+ """
1121
+
1122
+ self.invocation_params = invocation_params
1123
+ """The parameters with which pytest was invoked.
1124
+
1125
+ :type: InvocationParams
1126
+ """
1127
+
1128
+ self._parser = Parser(
1129
+ usage=f"%(prog)s [options] [{FILE_OR_DIR}] [{FILE_OR_DIR}] [...]",
1130
+ processopt=self._processopt,
1131
+ prog=prog,
1132
+ _ispytest=True,
1133
+ )
1134
+ self.pluginmanager = pluginmanager
1135
+ """The plugin manager handles plugin registration and hook invocation.
1136
+
1137
+ :type: PytestPluginManager
1138
+ """
1139
+
1140
+ self.stash = Stash()
1141
+ """A place where plugins can store information on the config for their
1142
+ own use.
1143
+
1144
+ :type: Stash
1145
+ """
1146
+ # Deprecated alias. Was never public. Can be removed in a few releases.
1147
+ self._store = self.stash
1148
+
1149
+ self.trace = self.pluginmanager.trace.root.get("config")
1150
+ self.hook = self.pluginmanager.hook
1151
+ self._inicache: dict[str, Any] = {}
1152
+ self._inicfg: ConfigDict = {}
1153
+ self._cleanup_stack = contextlib.ExitStack()
1154
+ self.pluginmanager.register(self, "pytestconfig")
1155
+ self._configured = False
1156
+ self.hook.pytest_addoption.call_historic(
1157
+ kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
1158
+ )
1159
+ self.args_source = Config.ArgsSource.ARGS
1160
+ self.args: list[str] = []
1161
+
1162
+ if TYPE_CHECKING:
1163
+
1164
+ @deprecated(
1165
+ "config.inicfg is deprecated, use config.getini() to access configuration values instead.",
1166
+ )
1167
+ @property
1168
+ def inicfg(self) -> _DeprecatedInicfgProxy:
1169
+ raise NotImplementedError()
1170
+ else:
1171
+
1172
+ @property
1173
+ def inicfg(self) -> _DeprecatedInicfgProxy:
1174
+ warnings.warn(
1175
+ _pytest.deprecated.CONFIG_INICFG,
1176
+ stacklevel=2,
1177
+ )
1178
+ return _DeprecatedInicfgProxy(self)
1179
+
1180
+ @property
1181
+ def rootpath(self) -> pathlib.Path:
1182
+ """The path to the :ref:`rootdir <rootdir>`.
1183
+
1184
+ .. versionadded:: 6.1
1185
+ """
1186
+ return self._rootpath
1187
+
1188
+ @property
1189
+ def inipath(self) -> pathlib.Path | None:
1190
+ """The path to the :ref:`configfile <configfiles>`.
1191
+
1192
+ .. versionadded:: 6.1
1193
+ """
1194
+ return self._inipath
1195
+
1196
+ def add_cleanup(self, func: Callable[[], None]) -> None:
1197
+ """Add a function to be called when the config object gets out of
1198
+ use (usually coinciding with pytest_unconfigure).
1199
+ """
1200
+ self._cleanup_stack.callback(func)
1201
+
1202
+ def _do_configure(self) -> None:
1203
+ assert not self._configured
1204
+ self._configured = True
1205
+ self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
1206
+
1207
+ def _ensure_unconfigure(self) -> None:
1208
+ try:
1209
+ if self._configured:
1210
+ self._configured = False
1211
+ try:
1212
+ self.hook.pytest_unconfigure(config=self)
1213
+ finally:
1214
+ self.hook.pytest_configure._call_history = []
1215
+ finally:
1216
+ try:
1217
+ self._cleanup_stack.close()
1218
+ finally:
1219
+ self._cleanup_stack = contextlib.ExitStack()
1220
+
1221
+ def get_terminal_writer(self) -> TerminalWriter:
1222
+ terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin(
1223
+ "terminalreporter"
1224
+ )
1225
+ assert terminalreporter is not None
1226
+ return terminalreporter._tw
1227
+
1228
+ def pytest_cmdline_parse(
1229
+ self, pluginmanager: PytestPluginManager, args: list[str]
1230
+ ) -> Config:
1231
+ try:
1232
+ self.parse(args)
1233
+ except UsageError:
1234
+ # Handle `--version --version` and `--help` here in a minimal fashion.
1235
+ # This gets done via helpconfig normally, but its
1236
+ # pytest_cmdline_main is not called in case of errors.
1237
+ if getattr(self.option, "version", False) or "--version" in args:
1238
+ from _pytest.helpconfig import show_version_verbose
1239
+
1240
+ # Note that `--version` (single argument) is handled early by `Config.main()`, so the only
1241
+ # way we are reaching this point is via `--version --version`.
1242
+ show_version_verbose(self)
1243
+ elif (
1244
+ getattr(self.option, "help", False) or "--help" in args or "-h" in args
1245
+ ):
1246
+ self._parser.optparser.print_help()
1247
+ sys.stdout.write(
1248
+ "\nNOTE: displaying only minimal help due to UsageError.\n\n"
1249
+ )
1250
+
1251
+ raise
1252
+
1253
+ return self
1254
+
1255
+ def notify_exception(
1256
+ self,
1257
+ excinfo: ExceptionInfo[BaseException],
1258
+ option: argparse.Namespace | None = None,
1259
+ ) -> None:
1260
+ if option and getattr(option, "fulltrace", False):
1261
+ style: TracebackStyle = "long"
1262
+ else:
1263
+ style = "native"
1264
+ excrepr = excinfo.getrepr(
1265
+ funcargs=True, showlocals=getattr(option, "showlocals", False), style=style
1266
+ )
1267
+ res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
1268
+ if not any(res):
1269
+ for line in str(excrepr).split("\n"):
1270
+ sys.stderr.write(f"INTERNALERROR> {line}\n")
1271
+ sys.stderr.flush()
1272
+
1273
+ def cwd_relative_nodeid(self, nodeid: str) -> str:
1274
+ # nodeid's are relative to the rootpath, compute relative to cwd.
1275
+ if self.invocation_params.dir != self.rootpath:
1276
+ base_path_part, *nodeid_part = nodeid.split("::")
1277
+ # Only process path part
1278
+ fullpath = self.rootpath / base_path_part
1279
+ relative_path = bestrelpath(self.invocation_params.dir, fullpath)
1280
+
1281
+ nodeid = "::".join([relative_path, *nodeid_part])
1282
+ return nodeid
1283
+
1284
+ @classmethod
1285
+ def fromdictargs(cls, option_dict: Mapping[str, Any], args: list[str]) -> Config:
1286
+ """Constructor usable for subprocesses."""
1287
+ config = get_config(args)
1288
+ config.option.__dict__.update(option_dict)
1289
+ config.parse(args, addopts=False)
1290
+ for x in config.option.plugins:
1291
+ config.pluginmanager.consider_pluginarg(x)
1292
+ return config
1293
+
1294
+ def _processopt(self, opt: Argument) -> None:
1295
+ if not hasattr(self.option, opt.dest):
1296
+ setattr(self.option, opt.dest, opt.default)
1297
+
1298
+ @hookimpl(trylast=True)
1299
+ def pytest_load_initial_conftests(self, early_config: Config) -> None:
1300
+ # We haven't fully parsed the command line arguments yet, so
1301
+ # early_config.args it not set yet. But we need it for
1302
+ # discovering the initial conftests. So "pre-run" the logic here.
1303
+ # It will be done for real in `parse()`.
1304
+ args, _args_source = early_config._decide_args(
1305
+ args=early_config.known_args_namespace.file_or_dir,
1306
+ pyargs=early_config.known_args_namespace.pyargs,
1307
+ testpaths=early_config.getini("testpaths"),
1308
+ invocation_dir=early_config.invocation_params.dir,
1309
+ rootpath=early_config.rootpath,
1310
+ warn=False,
1311
+ )
1312
+ self.pluginmanager._set_initial_conftests(
1313
+ args=args,
1314
+ pyargs=early_config.known_args_namespace.pyargs,
1315
+ noconftest=early_config.known_args_namespace.noconftest,
1316
+ rootpath=early_config.rootpath,
1317
+ confcutdir=early_config.known_args_namespace.confcutdir,
1318
+ invocation_dir=early_config.invocation_params.dir,
1319
+ importmode=early_config.known_args_namespace.importmode,
1320
+ consider_namespace_packages=early_config.getini(
1321
+ "consider_namespace_packages"
1322
+ ),
1323
+ )
1324
+
1325
+ def _consider_importhook(self) -> None:
1326
+ """Install the PEP 302 import hook if using assertion rewriting.
1327
+
1328
+ Needs to parse the --assert=<mode> option from the commandline
1329
+ and find all the installed plugins to mark them for rewriting
1330
+ by the importhook.
1331
+ """
1332
+ mode = getattr(self.known_args_namespace, "assertmode", "plain")
1333
+
1334
+ disable_autoload = getattr(
1335
+ self.known_args_namespace, "disable_plugin_autoload", False
1336
+ ) or bool(os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"))
1337
+ if mode == "rewrite":
1338
+ import _pytest.assertion
1339
+
1340
+ try:
1341
+ hook = _pytest.assertion.install_importhook(self)
1342
+ except SystemError:
1343
+ mode = "plain"
1344
+ else:
1345
+ self._mark_plugins_for_rewrite(hook, disable_autoload)
1346
+ self._warn_about_missing_assertion(mode)
1347
+
1348
+ def _mark_plugins_for_rewrite(
1349
+ self, hook: AssertionRewritingHook, disable_autoload: bool
1350
+ ) -> None:
1351
+ """Given an importhook, mark for rewrite any top-level
1352
+ modules or packages in the distribution package for
1353
+ all pytest plugins."""
1354
+ self.pluginmanager.rewrite_hook = hook
1355
+
1356
+ if disable_autoload:
1357
+ # We don't autoload from distribution package entry points,
1358
+ # no need to continue.
1359
+ return
1360
+
1361
+ package_files = (
1362
+ str(file)
1363
+ for dist in importlib.metadata.distributions()
1364
+ if any(ep.group == "pytest11" for ep in dist.entry_points)
1365
+ for file in dist.files or []
1366
+ )
1367
+
1368
+ for name in _iter_rewritable_modules(package_files):
1369
+ hook.mark_rewrite(name)
1370
+
1371
+ def _configure_python_path(self) -> None:
1372
+ # `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`
1373
+ for path in reversed(self.getini("pythonpath")):
1374
+ sys.path.insert(0, str(path))
1375
+ self.add_cleanup(self._unconfigure_python_path)
1376
+
1377
+ def _unconfigure_python_path(self) -> None:
1378
+ for path in self.getini("pythonpath"):
1379
+ path_str = str(path)
1380
+ if path_str in sys.path:
1381
+ sys.path.remove(path_str)
1382
+
1383
+ def _validate_args(self, args: list[str], via: str) -> list[str]:
1384
+ """Validate known args."""
1385
+ self._parser.extra_info["config source"] = via
1386
+ try:
1387
+ self._parser.parse_known_and_unknown_args(
1388
+ args, namespace=copy.copy(self.option)
1389
+ )
1390
+ finally:
1391
+ self._parser.extra_info.pop("config source", None)
1392
+
1393
+ return args
1394
+
1395
+ def _decide_args(
1396
+ self,
1397
+ *,
1398
+ args: list[str],
1399
+ pyargs: bool,
1400
+ testpaths: list[str],
1401
+ invocation_dir: pathlib.Path,
1402
+ rootpath: pathlib.Path,
1403
+ warn: bool,
1404
+ ) -> tuple[list[str], ArgsSource]:
1405
+ """Decide the args (initial paths/nodeids) to use given the relevant inputs.
1406
+
1407
+ :param warn: Whether can issue warnings.
1408
+
1409
+ :returns: The args and the args source. Guaranteed to be non-empty.
1410
+ """
1411
+ if args:
1412
+ source = Config.ArgsSource.ARGS
1413
+ result = args
1414
+ else:
1415
+ if invocation_dir == rootpath:
1416
+ source = Config.ArgsSource.TESTPATHS
1417
+ if pyargs:
1418
+ result = testpaths
1419
+ else:
1420
+ result = []
1421
+ for path in testpaths:
1422
+ result.extend(sorted(glob.iglob(path, recursive=True)))
1423
+ if testpaths and not result:
1424
+ if warn:
1425
+ warning_text = (
1426
+ "No files were found in testpaths; "
1427
+ "consider removing or adjusting your testpaths configuration. "
1428
+ "Searching recursively from the current directory instead."
1429
+ )
1430
+ self.issue_config_time_warning(
1431
+ PytestConfigWarning(warning_text), stacklevel=3
1432
+ )
1433
+ else:
1434
+ result = []
1435
+ if not result:
1436
+ source = Config.ArgsSource.INVOCATION_DIR
1437
+ result = [str(invocation_dir)]
1438
+ return result, source
1439
+
1440
+ @hookimpl(wrapper=True)
1441
+ def pytest_collection(self) -> Generator[None, object, object]:
1442
+ # Validate invalid configuration keys after collection is done so we
1443
+ # take in account options added by late-loading conftest files.
1444
+ try:
1445
+ return (yield)
1446
+ finally:
1447
+ self._validate_config_options()
1448
+
1449
+ def _checkversion(self) -> None:
1450
+ import pytest
1451
+
1452
+ minver = self.getini("minversion")
1453
+ if minver:
1454
+ # Imported lazily to improve start-up time.
1455
+ from packaging.version import Version
1456
+
1457
+ if Version(minver) > Version(pytest.__version__):
1458
+ raise pytest.UsageError(
1459
+ f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
1460
+ )
1461
+
1462
+ def _validate_config_options(self) -> None:
1463
+ for key in sorted(self._get_unknown_ini_keys()):
1464
+ self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")
1465
+
1466
+ def _validate_plugins(self) -> None:
1467
+ required_plugins = sorted(self.getini("required_plugins"))
1468
+ if not required_plugins:
1469
+ return
1470
+
1471
+ # Imported lazily to improve start-up time.
1472
+ from packaging.requirements import InvalidRequirement
1473
+ from packaging.requirements import Requirement
1474
+ from packaging.version import Version
1475
+
1476
+ plugin_info = self.pluginmanager.list_plugin_distinfo()
1477
+ plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
1478
+
1479
+ missing_plugins = []
1480
+ for required_plugin in required_plugins:
1481
+ try:
1482
+ req = Requirement(required_plugin)
1483
+ except InvalidRequirement:
1484
+ missing_plugins.append(required_plugin)
1485
+ continue
1486
+
1487
+ if req.name not in plugin_dist_info:
1488
+ missing_plugins.append(required_plugin)
1489
+ elif not req.specifier.contains(
1490
+ Version(plugin_dist_info[req.name]), prereleases=True
1491
+ ):
1492
+ missing_plugins.append(required_plugin)
1493
+
1494
+ if missing_plugins:
1495
+ raise UsageError(
1496
+ "Missing required plugins: {}".format(", ".join(missing_plugins)),
1497
+ )
1498
+
1499
+ def _warn_or_fail_if_strict(self, message: str) -> None:
1500
+ strict_config = self.getini("strict_config")
1501
+ if strict_config is None:
1502
+ strict_config = self.getini("strict")
1503
+ if strict_config:
1504
+ raise UsageError(message)
1505
+
1506
+ self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
1507
+
1508
+ def _get_unknown_ini_keys(self) -> set[str]:
1509
+ known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys()
1510
+ return self._inicfg.keys() - known_keys
1511
+
1512
+ def parse(self, args: list[str], addopts: bool = True) -> None:
1513
+ # Parse given cmdline arguments into this config object.
1514
+ assert self.args == [], (
1515
+ "can only parse cmdline args at most once per Config object"
1516
+ )
1517
+
1518
+ self.hook.pytest_addhooks.call_historic(
1519
+ kwargs=dict(pluginmanager=self.pluginmanager)
1520
+ )
1521
+
1522
+ if addopts:
1523
+ env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
1524
+ if len(env_addopts):
1525
+ args[:] = (
1526
+ self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
1527
+ + args
1528
+ )
1529
+
1530
+ # At this point, self.option contains only defaults from the _processopt
1531
+ # callback.
1532
+ ns = self._parser.parse_known_args(args, namespace=copy.copy(self.option))
1533
+ rootpath, inipath, inicfg, ignored_config_files = determine_setup(
1534
+ inifile=ns.inifilename,
1535
+ override_ini=ns.override_ini,
1536
+ args=ns.file_or_dir,
1537
+ rootdir_cmd_arg=ns.rootdir or None,
1538
+ invocation_dir=self.invocation_params.dir,
1539
+ )
1540
+ self._rootpath = rootpath
1541
+ self._inipath = inipath
1542
+ self._ignored_config_files = ignored_config_files
1543
+ self._inicfg = inicfg
1544
+ self._parser.extra_info["rootdir"] = str(self.rootpath)
1545
+ self._parser.extra_info["inifile"] = str(self.inipath)
1546
+
1547
+ self._parser.addini("addopts", "Extra command line options", "args")
1548
+ self._parser.addini("minversion", "Minimally required pytest version")
1549
+ self._parser.addini(
1550
+ "pythonpath", type="paths", help="Add paths to sys.path", default=[]
1551
+ )
1552
+ self._parser.addini(
1553
+ "required_plugins",
1554
+ "Plugins that must be present for pytest to run",
1555
+ type="args",
1556
+ default=[],
1557
+ )
1558
+
1559
+ if addopts:
1560
+ args[:] = (
1561
+ self._validate_args(self.getini("addopts"), "via addopts config") + args
1562
+ )
1563
+
1564
+ self.known_args_namespace = self._parser.parse_known_args(
1565
+ args, namespace=copy.copy(self.option)
1566
+ )
1567
+ if addopts:
1568
+ # addopts may have added overrides (especially via OverrideIniAction).
1569
+ # The thing can be endlessly circular but we only do one level (#14442).
1570
+ if overrides := parse_override_ini(self.known_args_namespace.override_ini):
1571
+ self._inicfg.update(overrides)
1572
+ self._inicache.clear()
1573
+ self._checkversion()
1574
+ self._consider_importhook()
1575
+ self._configure_python_path()
1576
+ self.pluginmanager.consider_preparse(args, exclude_only=False)
1577
+ if (
1578
+ not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
1579
+ and not self.known_args_namespace.disable_plugin_autoload
1580
+ ):
1581
+ # Autoloading from distribution package entry point has
1582
+ # not been disabled.
1583
+ self.pluginmanager.load_setuptools_entrypoints("pytest11")
1584
+ # Otherwise only plugins explicitly specified in PYTEST_PLUGINS
1585
+ # are going to be loaded.
1586
+ self.pluginmanager.consider_env()
1587
+
1588
+ # Parse again, now including options added in pytest_addoption
1589
+ # by third-party plugins loaded above. This way they're available
1590
+ # on early_config in the pytest_load_initial_conftests hook call below.
1591
+ self.known_args_namespace = self._parser.parse_known_args(
1592
+ args, namespace=copy.copy(self.option)
1593
+ )
1594
+
1595
+ self._validate_plugins()
1596
+ self._warn_about_skipped_plugins()
1597
+
1598
+ if self.known_args_namespace.confcutdir is None:
1599
+ if self.inipath is not None:
1600
+ confcutdir = str(self.inipath.parent)
1601
+ else:
1602
+ confcutdir = str(self.rootpath)
1603
+ self.known_args_namespace.confcutdir = confcutdir
1604
+ try:
1605
+ self.hook.pytest_load_initial_conftests(
1606
+ early_config=self, args=args, parser=self._parser
1607
+ )
1608
+ except ConftestImportFailure as e:
1609
+ if self.known_args_namespace.help or self.known_args_namespace.version:
1610
+ # we don't want to prevent --help/--version to work
1611
+ # so just let it pass and print a warning at the end
1612
+ self.issue_config_time_warning(
1613
+ PytestConfigWarning(f"could not load initial conftests: {e.path}"),
1614
+ stacklevel=2,
1615
+ )
1616
+ else:
1617
+ raise
1618
+
1619
+ try:
1620
+ self._parser.parse(args, namespace=self.option)
1621
+ except PrintHelp:
1622
+ return
1623
+
1624
+ self.args, self.args_source = self._decide_args(
1625
+ args=getattr(self.option, FILE_OR_DIR),
1626
+ pyargs=self.option.pyargs,
1627
+ testpaths=self.getini("testpaths"),
1628
+ invocation_dir=self.invocation_params.dir,
1629
+ rootpath=self.rootpath,
1630
+ warn=True,
1631
+ )
1632
+
1633
+ def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
1634
+ """Issue and handle a warning during the "configure" stage.
1635
+
1636
+ During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
1637
+ function because it is not possible to have hook wrappers around ``pytest_configure``.
1638
+
1639
+ This function is mainly intended for plugins that need to issue warnings during
1640
+ ``pytest_configure`` (or similar stages).
1641
+
1642
+ :param warning: The warning instance.
1643
+ :param stacklevel: stacklevel forwarded to warnings.warn.
1644
+ """
1645
+ if self.pluginmanager.is_blocked("warnings"):
1646
+ return
1647
+
1648
+ cmdline_filters = self.known_args_namespace.pythonwarnings or []
1649
+ config_filters = self.getini("filterwarnings")
1650
+
1651
+ with warnings.catch_warnings(record=True) as records:
1652
+ warnings.simplefilter("always", type(warning))
1653
+ apply_warning_filters(config_filters, cmdline_filters)
1654
+ warnings.warn(warning, stacklevel=stacklevel)
1655
+
1656
+ if records:
1657
+ frame = sys._getframe(stacklevel - 1)
1658
+ location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
1659
+ self.hook.pytest_warning_recorded.call_historic(
1660
+ kwargs=dict(
1661
+ warning_message=records[0],
1662
+ when="config",
1663
+ nodeid="",
1664
+ location=location,
1665
+ )
1666
+ )
1667
+
1668
+ def addinivalue_line(self, name: str, line: str) -> None:
1669
+ """Add a line to a configuration option. The option must have been
1670
+ declared but might not yet be set in which case the line becomes
1671
+ the first line in its value."""
1672
+ x = self.getini(name)
1673
+ assert isinstance(x, list)
1674
+ x.append(line) # modifies the cached list inline
1675
+
1676
+ def getini(self, name: str) -> Any:
1677
+ """Return configuration value the an :ref:`configuration file <configfiles>`.
1678
+
1679
+ If a configuration value is not defined in a
1680
+ :ref:`configuration file <configfiles>`, then the ``default`` value
1681
+ provided while registering the configuration through
1682
+ :func:`parser.addini <pytest.Parser.addini>` will be returned.
1683
+ Please note that you can even provide ``None`` as a valid
1684
+ default value.
1685
+
1686
+ If ``default`` is not provided while registering using
1687
+ :func:`parser.addini <pytest.Parser.addini>`, then a default value
1688
+ based on the ``type`` parameter passed to
1689
+ :func:`parser.addini <pytest.Parser.addini>` will be returned.
1690
+ The default values based on ``type`` are:
1691
+ ``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
1692
+ ``bool`` : ``False``
1693
+ ``string`` : empty string ``""``
1694
+ ``int`` : ``0``
1695
+ ``float`` : ``0.0``
1696
+
1697
+ If neither the ``default`` nor the ``type`` parameter is passed
1698
+ while registering the configuration through
1699
+ :func:`parser.addini <pytest.Parser.addini>`, then the configuration
1700
+ is treated as a string and a default empty string '' is returned.
1701
+
1702
+ If the specified name hasn't been registered through a prior
1703
+ :func:`parser.addini <pytest.Parser.addini>` call (usually from a
1704
+ plugin), a ValueError is raised.
1705
+ """
1706
+ canonical_name = self._parser._ini_aliases.get(name, name)
1707
+ try:
1708
+ return self._inicache[canonical_name]
1709
+ except KeyError:
1710
+ pass
1711
+ self._inicache[canonical_name] = val = self._getini(canonical_name)
1712
+ return val
1713
+
1714
+ # Meant for easy monkeypatching by legacypath plugin.
1715
+ # Can be inlined back (with no cover removed) once legacypath is gone.
1716
+ def _getini_unknown_type(self, name: str, type: str, value: object):
1717
+ msg = (
1718
+ f"Option {name} has unknown configuration type {type} with value {value!r}"
1719
+ )
1720
+ raise ValueError(msg) # pragma: no cover
1721
+
1722
+ def _getini(self, name: str):
1723
+ # If this is an alias, resolve to canonical name.
1724
+ canonical_name = self._parser._ini_aliases.get(name, name)
1725
+
1726
+ try:
1727
+ _description, type, default = self._parser._inidict[canonical_name]
1728
+ except KeyError as e:
1729
+ raise ValueError(f"unknown configuration value: {name!r}") from e
1730
+
1731
+ # Collect all possible values (canonical name + aliases) from _inicfg.
1732
+ # Each candidate is (ConfigValue, is_canonical).
1733
+ candidates = []
1734
+ if canonical_name in self._inicfg:
1735
+ candidates.append((self._inicfg[canonical_name], True))
1736
+ for alias, target in self._parser._ini_aliases.items():
1737
+ if target == canonical_name and alias in self._inicfg:
1738
+ candidates.append((self._inicfg[alias], False))
1739
+
1740
+ if not candidates:
1741
+ return default
1742
+
1743
+ # Pick the best candidate based on precedence:
1744
+ # 1. CLI override takes precedence over file, then
1745
+ # 2. Canonical name takes precedence over alias.
1746
+ selected = max(candidates, key=lambda x: (x[0].origin == "override", x[1]))[0]
1747
+ value = selected.value
1748
+ mode = selected.mode
1749
+
1750
+ if mode == "ini":
1751
+ # In ini mode, values are always str | list[str].
1752
+ assert isinstance(value, (str, list))
1753
+ return self._getini_ini(name, canonical_name, type, value, default)
1754
+ elif mode == "toml":
1755
+ return self._getini_toml(name, canonical_name, type, value, default)
1756
+ else:
1757
+ assert_never(mode)
1758
+
1759
+ def _getini_ini(
1760
+ self,
1761
+ name: str,
1762
+ canonical_name: str,
1763
+ type: str,
1764
+ value: str | list[str],
1765
+ default: Any,
1766
+ ):
1767
+ """Handle config values read in INI mode.
1768
+
1769
+ In INI mode, values are stored as str or list[str] only, and coerced
1770
+ from string based on the registered type.
1771
+ """
1772
+ # Note: some coercions are only required if we are reading from .ini
1773
+ # files, because the file format doesn't contain type information, but
1774
+ # when reading from toml (in ini mode) we will get either str or list of
1775
+ # str values (see load_config_dict_from_file). For example:
1776
+ #
1777
+ # ini:
1778
+ # a_line_list = "tests acceptance"
1779
+ #
1780
+ # in this case, we need to split the string to obtain a list of strings.
1781
+ #
1782
+ # toml (ini mode):
1783
+ # a_line_list = ["tests", "acceptance"]
1784
+ #
1785
+ # in this case, we already have a list ready to use.
1786
+ if type == "paths":
1787
+ dp = (
1788
+ self.inipath.parent
1789
+ if self.inipath is not None
1790
+ else self.invocation_params.dir
1791
+ )
1792
+ input_values = shlex.split(value) if isinstance(value, str) else value
1793
+ return [dp / x for x in input_values]
1794
+ elif type == "args":
1795
+ return shlex.split(value) if isinstance(value, str) else value
1796
+ elif type == "linelist":
1797
+ if isinstance(value, str):
1798
+ return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
1799
+ else:
1800
+ return value
1801
+ elif type == "bool":
1802
+ return _strtobool(str(value).strip())
1803
+ elif type == "string":
1804
+ return value
1805
+ elif type == "int":
1806
+ if not isinstance(value, str):
1807
+ raise TypeError(
1808
+ f"Expected an int string for option {name} of type integer, but got: {value!r}"
1809
+ ) from None
1810
+ return int(value)
1811
+ elif type == "float":
1812
+ if not isinstance(value, str):
1813
+ raise TypeError(
1814
+ f"Expected a float string for option {name} of type float, but got: {value!r}"
1815
+ ) from None
1816
+ return float(value)
1817
+ else:
1818
+ return self._getini_unknown_type(name, type, value)
1819
+
1820
+ def _getini_toml(
1821
+ self,
1822
+ name: str,
1823
+ canonical_name: str,
1824
+ type: str,
1825
+ value: object,
1826
+ default: Any,
1827
+ ):
1828
+ """Handle TOML config values with strict type validation and no coercion.
1829
+
1830
+ In TOML mode, values already have native types from TOML parsing.
1831
+ We validate types match expectations exactly, including list items.
1832
+ """
1833
+ value_type = builtins.type(value).__name__
1834
+ if type == "paths":
1835
+ # Expect a list of strings.
1836
+ if not isinstance(value, list):
1837
+ raise TypeError(
1838
+ f"{self.inipath}: config option '{name}' expects a list for type 'paths', "
1839
+ f"got {value_type}: {value!r}"
1840
+ )
1841
+ for i, item in enumerate(value):
1842
+ if not isinstance(item, str):
1843
+ item_type = builtins.type(item).__name__
1844
+ raise TypeError(
1845
+ f"{self.inipath}: config option '{name}' expects a list of strings, "
1846
+ f"but item at index {i} is {item_type}: {item!r}"
1847
+ )
1848
+ dp = (
1849
+ self.inipath.parent
1850
+ if self.inipath is not None
1851
+ else self.invocation_params.dir
1852
+ )
1853
+ return [dp / x for x in value]
1854
+ elif type in {"args", "linelist"}:
1855
+ # Expect a list of strings.
1856
+ if not isinstance(value, list):
1857
+ raise TypeError(
1858
+ f"{self.inipath}: config option '{name}' expects a list for type '{type}', "
1859
+ f"got {value_type}: {value!r}"
1860
+ )
1861
+ for i, item in enumerate(value):
1862
+ if not isinstance(item, str):
1863
+ item_type = builtins.type(item).__name__
1864
+ raise TypeError(
1865
+ f"{self.inipath}: config option '{name}' expects a list of strings, "
1866
+ f"but item at index {i} is {item_type}: {item!r}"
1867
+ )
1868
+ return list(value)
1869
+ elif type == "bool":
1870
+ # Expect a boolean.
1871
+ if not isinstance(value, bool):
1872
+ raise TypeError(
1873
+ f"{self.inipath}: config option '{name}' expects a bool, "
1874
+ f"got {value_type}: {value!r}"
1875
+ )
1876
+ return value
1877
+ elif type == "int":
1878
+ # Expect an integer (but not bool, which is a subclass of int).
1879
+ if not isinstance(value, int) or isinstance(value, bool):
1880
+ raise TypeError(
1881
+ f"{self.inipath}: config option '{name}' expects an int, "
1882
+ f"got {value_type}: {value!r}"
1883
+ )
1884
+ return value
1885
+ elif type == "float":
1886
+ # Expect a float or integer only.
1887
+ if not isinstance(value, (float, int)) or isinstance(value, bool):
1888
+ raise TypeError(
1889
+ f"{self.inipath}: config option '{name}' expects a float, "
1890
+ f"got {value_type}: {value!r}"
1891
+ )
1892
+ return value
1893
+ elif type == "string":
1894
+ # Expect a string.
1895
+ if not isinstance(value, str):
1896
+ raise TypeError(
1897
+ f"{self.inipath}: config option '{name}' expects a string, "
1898
+ f"got {value_type}: {value!r}"
1899
+ )
1900
+ return value
1901
+ else:
1902
+ return self._getini_unknown_type(name, type, value)
1903
+
1904
+ def _getconftest_pathlist(
1905
+ self, name: str, path: pathlib.Path
1906
+ ) -> list[pathlib.Path] | None:
1907
+ try:
1908
+ mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
1909
+ except KeyError:
1910
+ return None
1911
+ assert mod.__file__ is not None
1912
+ modpath = pathlib.Path(mod.__file__).parent
1913
+ values: list[pathlib.Path] = []
1914
+ for relroot in relroots:
1915
+ if isinstance(relroot, os.PathLike):
1916
+ relroot = pathlib.Path(relroot)
1917
+ else:
1918
+ relroot = relroot.replace("/", os.sep)
1919
+ relroot = absolutepath(modpath / relroot)
1920
+ values.append(relroot)
1921
+ return values
1922
+
1923
+ def getoption(self, name: str, default: Any = NOTSET, skip: bool = False):
1924
+ """Return command line option value.
1925
+
1926
+ :param name: Name of the option. You may also specify
1927
+ the literal ``--OPT`` option instead of the "dest" option name.
1928
+ :param default: Fallback value if no option of that name is **declared** via :hook:`pytest_addoption`.
1929
+ Note this parameter will be ignored when the option is **declared** even if the option's value is ``None``.
1930
+ :param skip: If ``True``, raise :func:`pytest.skip` if option is undeclared or has a ``None`` value.
1931
+ Note that even if ``True``, if a default was specified it will be returned instead of a skip.
1932
+ """
1933
+ name = self._parser._opt2dest.get(name, name)
1934
+ try:
1935
+ val = getattr(self.option, name)
1936
+ if val is None and skip:
1937
+ raise AttributeError(name)
1938
+ return val
1939
+ except AttributeError as e:
1940
+ if default is not NOTSET:
1941
+ return default
1942
+ if skip:
1943
+ import pytest
1944
+
1945
+ pytest.skip(f"no {name!r} option found")
1946
+ raise ValueError(f"no option named {name!r}") from e
1947
+
1948
+ def getvalue(self, name: str, path=None):
1949
+ """Deprecated, use getoption() instead."""
1950
+ return self.getoption(name)
1951
+
1952
+ def getvalueorskip(self, name: str, path=None):
1953
+ """Deprecated, use getoption(skip=True) instead."""
1954
+ return self.getoption(name, skip=True)
1955
+
1956
+ #: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
1957
+ VERBOSITY_ASSERTIONS: Final = "assertions"
1958
+ #: Verbosity type for test case execution (see :confval:`verbosity_test_cases`).
1959
+ VERBOSITY_TEST_CASES: Final = "test_cases"
1960
+ #: Verbosity type for failed subtests (see :confval:`verbosity_subtests`).
1961
+ VERBOSITY_SUBTESTS: Final = "subtests"
1962
+
1963
+ _VERBOSITY_INI_DEFAULT: Final = "auto"
1964
+
1965
+ def get_verbosity(self, verbosity_type: str | None = None) -> int:
1966
+ r"""Retrieve the verbosity level for a fine-grained verbosity type.
1967
+
1968
+ :param verbosity_type: Verbosity type to get level for. If a level is
1969
+ configured for the given type, that value will be returned. If the
1970
+ given type is not a known verbosity type, the global verbosity
1971
+ level will be returned. If the given type is None (default), the
1972
+ global verbosity level will be returned.
1973
+
1974
+ To configure a level for a fine-grained verbosity type, the
1975
+ configuration file should have a setting for the configuration name
1976
+ and a numeric value for the verbosity level. A special value of "auto"
1977
+ can be used to explicitly use the global verbosity level.
1978
+
1979
+ Example:
1980
+
1981
+ .. tab:: toml
1982
+
1983
+ .. code-block:: toml
1984
+
1985
+ [tool.pytest]
1986
+ verbosity_assertions = 2
1987
+
1988
+ .. tab:: ini
1989
+
1990
+ .. code-block:: ini
1991
+
1992
+ [pytest]
1993
+ verbosity_assertions = 2
1994
+
1995
+ .. code-block:: console
1996
+
1997
+ pytest -v
1998
+
1999
+ .. code-block:: python
2000
+
2001
+ print(config.get_verbosity()) # 1
2002
+ print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
2003
+ """
2004
+ global_level = self.getoption("verbose", default=0)
2005
+ assert isinstance(global_level, int)
2006
+ if verbosity_type is None:
2007
+ return global_level
2008
+
2009
+ ini_name = Config._verbosity_ini_name(verbosity_type)
2010
+ if ini_name not in self._parser._inidict:
2011
+ return global_level
2012
+
2013
+ level = self.getini(ini_name)
2014
+ if level == Config._VERBOSITY_INI_DEFAULT:
2015
+ return global_level
2016
+
2017
+ return int(level)
2018
+
2019
+ @staticmethod
2020
+ def _verbosity_ini_name(verbosity_type: str) -> str:
2021
+ return f"verbosity_{verbosity_type}"
2022
+
2023
+ @staticmethod
2024
+ def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None:
2025
+ """Add a output verbosity configuration option for the given output type.
2026
+
2027
+ :param parser: Parser for command line arguments and config-file values.
2028
+ :param verbosity_type: Fine-grained verbosity category.
2029
+ :param help: Description of the output this type controls.
2030
+
2031
+ The value should be retrieved via a call to
2032
+ :py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
2033
+ """
2034
+ parser.addini(
2035
+ Config._verbosity_ini_name(verbosity_type),
2036
+ help=help,
2037
+ type="string",
2038
+ default=Config._VERBOSITY_INI_DEFAULT,
2039
+ )
2040
+
2041
+ def _warn_about_missing_assertion(self, mode: str) -> None:
2042
+ if not _assertion_supported():
2043
+ if mode == "plain":
2044
+ warning_text = (
2045
+ "ASSERTIONS ARE NOT EXECUTED"
2046
+ " and FAILING TESTS WILL PASS. Are you"
2047
+ " using python -O?"
2048
+ )
2049
+ else:
2050
+ warning_text = (
2051
+ "assertions not in test modules or"
2052
+ " plugins will be ignored"
2053
+ " because assert statements are not executed "
2054
+ "by the underlying Python interpreter "
2055
+ "(are you using python -O?)\n"
2056
+ )
2057
+ self.issue_config_time_warning(
2058
+ PytestConfigWarning(warning_text),
2059
+ stacklevel=3,
2060
+ )
2061
+
2062
+ def _warn_about_skipped_plugins(self) -> None:
2063
+ for module_name, msg in self.pluginmanager.skipped_plugins:
2064
+ self.issue_config_time_warning(
2065
+ PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),
2066
+ stacklevel=2,
2067
+ )
2068
+
2069
+
2070
+ def _assertion_supported() -> bool:
2071
+ try:
2072
+ assert False
2073
+ except AssertionError:
2074
+ return True
2075
+ else:
2076
+ return False # type: ignore[unreachable]
2077
+
2078
+
2079
+ def create_terminal_writer(
2080
+ config: Config, file: TextIO | None = None
2081
+ ) -> TerminalWriter:
2082
+ """Create a TerminalWriter instance configured according to the options
2083
+ in the config object.
2084
+
2085
+ Every code which requires a TerminalWriter object and has access to a
2086
+ config object should use this function.
2087
+ """
2088
+ tw = TerminalWriter(file=file)
2089
+
2090
+ if config.option.color == "yes":
2091
+ tw.hasmarkup = True
2092
+ elif config.option.color == "no":
2093
+ tw.hasmarkup = False
2094
+
2095
+ if config.option.code_highlight == "yes":
2096
+ tw.code_highlight = True
2097
+ elif config.option.code_highlight == "no":
2098
+ tw.code_highlight = False
2099
+
2100
+ return tw
2101
+
2102
+
2103
+ def _strtobool(val: str) -> bool:
2104
+ """Convert a string representation of truth to True or False.
2105
+
2106
+ True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
2107
+ are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
2108
+ 'val' is anything else.
2109
+
2110
+ .. note:: Copied from distutils.util.
2111
+ """
2112
+ val = val.lower()
2113
+ if val in ("y", "yes", "t", "true", "on", "1"):
2114
+ return True
2115
+ elif val in ("n", "no", "f", "false", "off", "0"):
2116
+ return False
2117
+ else:
2118
+ raise ValueError(f"invalid truth value {val!r}")
2119
+
2120
+
2121
+ @lru_cache(maxsize=50)
2122
+ def parse_warning_filter(
2123
+ arg: str, *, escape: bool
2124
+ ) -> tuple[warnings._ActionKind, str, type[Warning], str, int]:
2125
+ """Parse a warnings filter string.
2126
+
2127
+ This is copied from warnings._setoption with the following changes:
2128
+
2129
+ * Does not apply the filter.
2130
+ * Escaping is optional.
2131
+ * Raises UsageError so we get nice error messages on failure.
2132
+ """
2133
+ __tracebackhide__ = True
2134
+ error_template = dedent(
2135
+ f"""\
2136
+ while parsing the following warning configuration:
2137
+
2138
+ {arg}
2139
+
2140
+ This error occurred:
2141
+
2142
+ {{error}}
2143
+ """
2144
+ )
2145
+
2146
+ parts = arg.split(":")
2147
+ if len(parts) > 5:
2148
+ doc_url = (
2149
+ "https://docs.python.org/3/library/warnings.html#describing-warning-filters"
2150
+ )
2151
+ error = dedent(
2152
+ f"""\
2153
+ Too many fields ({len(parts)}), expected at most 5 separated by colons:
2154
+
2155
+ action:message:category:module:line
2156
+
2157
+ For more information please consult: {doc_url}
2158
+ """
2159
+ )
2160
+ raise UsageError(error_template.format(error=error))
2161
+
2162
+ while len(parts) < 5:
2163
+ parts.append("")
2164
+ action_, message, category_, module, lineno_ = (s.strip() for s in parts)
2165
+ try:
2166
+ action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined]
2167
+ except warnings._OptionError as e:
2168
+ raise UsageError(error_template.format(error=str(e))) from None
2169
+ try:
2170
+ category: type[Warning] = _resolve_warning_category(category_)
2171
+ except ImportError:
2172
+ raise
2173
+ except Exception:
2174
+ exc_info = ExceptionInfo.from_current()
2175
+ exception_text = exc_info.getrepr(style="native")
2176
+ raise UsageError(error_template.format(error=exception_text)) from None
2177
+ if message and escape:
2178
+ message = re.escape(message)
2179
+ if module and escape:
2180
+ module = re.escape(module) + r"\Z"
2181
+ if lineno_:
2182
+ try:
2183
+ lineno = int(lineno_)
2184
+ if lineno < 0:
2185
+ raise ValueError("number is negative")
2186
+ except ValueError as e:
2187
+ raise UsageError(
2188
+ error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
2189
+ ) from None
2190
+ else:
2191
+ lineno = 0
2192
+ try:
2193
+ re.compile(message)
2194
+ re.compile(module)
2195
+ except re.error as e:
2196
+ raise UsageError(
2197
+ error_template.format(error=f"Invalid regex {e.pattern!r}: {e}")
2198
+ ) from None
2199
+ return action, message, category, module, lineno
2200
+
2201
+
2202
+ def _resolve_warning_category(category: str) -> type[Warning]:
2203
+ """
2204
+ Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
2205
+ propagate so we can get access to their tracebacks (#9218).
2206
+ """
2207
+ __tracebackhide__ = True
2208
+ if not category:
2209
+ return Warning
2210
+
2211
+ if "." not in category:
2212
+ import builtins as m
2213
+
2214
+ klass = category
2215
+ else:
2216
+ module, _, klass = category.rpartition(".")
2217
+ m = importlib.import_module(module)
2218
+ cat = getattr(m, klass)
2219
+ if not issubclass(cat, Warning):
2220
+ raise UsageError(f"{cat} is not a Warning subclass")
2221
+ return cast(type[Warning], cat)
2222
+
2223
+
2224
+ def apply_warning_filters(
2225
+ config_filters: Iterable[str], cmdline_filters: Iterable[str]
2226
+ ) -> None:
2227
+ """Applies pytest-configured filters to the warnings module"""
2228
+ # Filters should have this precedence: cmdline options, config.
2229
+ # Filters should be applied in the inverse order of precedence.
2230
+ for arg in config_filters:
2231
+ try:
2232
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
2233
+ except ImportError as e:
2234
+ warnings.warn(
2235
+ f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning
2236
+ )
2237
+ continue
2238
+
2239
+ for arg in cmdline_filters:
2240
+ try:
2241
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
2242
+ except ImportError as e:
2243
+ warnings.warn(
2244
+ f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning
2245
+ )
2246
+ continue
.venv/lib/python3.14/site-packages/_pytest/config/argparsing.py ADDED
@@ -0,0 +1,503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from collections.abc import Callable
6
+ from collections.abc import Sequence
7
+ import os
8
+ import sys
9
+ import textwrap
10
+ from typing import Any
11
+ from typing import final
12
+ from typing import Literal
13
+ from typing import NoReturn
14
+
15
+ from .exceptions import UsageError
16
+ import _pytest._io
17
+ from _pytest.compat import NOTSET
18
+ from _pytest.deprecated import check_ispytest
19
+
20
+
21
+ FILE_OR_DIR = "file_or_dir"
22
+
23
+
24
+ @final
25
+ class Parser:
26
+ """Parser for command line arguments and config-file values.
27
+
28
+ :ivar extra_info: Dict of generic param -> value to display in case
29
+ there's an error processing the command line arguments.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ usage: str | None = None,
35
+ processopt: Callable[[Argument], None] | None = None,
36
+ *,
37
+ prog: str | None = None,
38
+ _ispytest: bool = False,
39
+ ) -> None:
40
+ check_ispytest(_ispytest)
41
+
42
+ from _pytest._argcomplete import filescompleter
43
+
44
+ self._processopt = processopt
45
+ self.extra_info: dict[str, Any] = {}
46
+ self.optparser = PytestArgumentParser(usage, self.extra_info, prog=prog)
47
+ anonymous_arggroup = self.optparser.add_argument_group("Custom options")
48
+ self._anonymous = OptionGroup(
49
+ anonymous_arggroup, "_anonymous", self, _ispytest=True
50
+ )
51
+ self._groups = [self._anonymous]
52
+ # Maps option strings -> dest, e.g. "-V" and "--version" to "version".
53
+ self._opt2dest: dict[str, str] = {}
54
+ file_or_dir_arg = self.optparser.add_argument(FILE_OR_DIR, nargs="*")
55
+ file_or_dir_arg.completer = filescompleter # type: ignore
56
+
57
+ self._inidict: dict[str, tuple[str, str, Any]] = {}
58
+ # Maps alias -> canonical name.
59
+ self._ini_aliases: dict[str, str] = {}
60
+
61
+ @property
62
+ def prog(self) -> str:
63
+ return self.optparser.prog
64
+
65
+ @prog.setter
66
+ def prog(self, value: str) -> None:
67
+ self.optparser.prog = value
68
+
69
+ def processoption(self, option: Argument) -> None:
70
+ if self._processopt:
71
+ if option.dest:
72
+ self._processopt(option)
73
+
74
+ def getgroup(
75
+ self, name: str, description: str = "", after: str | None = None
76
+ ) -> OptionGroup:
77
+ """Get (or create) a named option Group.
78
+
79
+ :param name: Name of the option group.
80
+ :param description: Long description for --help output.
81
+ :param after: Name of another group, used for ordering --help output.
82
+ :returns: The option group.
83
+
84
+ The returned group object has an ``addoption`` method with the same
85
+ signature as :func:`parser.addoption <pytest.Parser.addoption>` but
86
+ will be shown in the respective group in the output of
87
+ ``pytest --help``.
88
+ """
89
+ for group in self._groups:
90
+ if group.name == name:
91
+ return group
92
+
93
+ arggroup = self.optparser.add_argument_group(description or name)
94
+ group = OptionGroup(arggroup, name, self, _ispytest=True)
95
+ i = 0
96
+ for i, grp in enumerate(self._groups):
97
+ if grp.name == after:
98
+ break
99
+ self._groups.insert(i + 1, group)
100
+ # argparse doesn't provide a way to control `--help` order, so must
101
+ # access its internals ☹.
102
+ self.optparser._action_groups.insert(i + 1, self.optparser._action_groups.pop())
103
+ return group
104
+
105
+ def addoption(self, *opts: str, **attrs: Any) -> None:
106
+ """Register a command line option.
107
+
108
+ :param opts:
109
+ Option names, can be short or long options.
110
+ :param attrs:
111
+ Same attributes as the argparse library's :meth:`add_argument()
112
+ <argparse.ArgumentParser.add_argument>` function accepts.
113
+
114
+ After command line parsing, options are available on the pytest config
115
+ object via ``config.option.NAME`` where ``NAME`` is usually set
116
+ by passing a ``dest`` attribute, for example
117
+ ``addoption("--long", dest="NAME", ...)``.
118
+ """
119
+ self._anonymous.addoption(*opts, **attrs)
120
+
121
+ def parse(
122
+ self,
123
+ args: Sequence[str | os.PathLike[str]],
124
+ namespace: argparse.Namespace | None = None,
125
+ ) -> argparse.Namespace:
126
+ """Parse the arguments.
127
+
128
+ Unlike ``parse_known_args`` and ``parse_known_and_unknown_args``,
129
+ raises PrintHelp on `--help` and UsageError on unknown flags
130
+
131
+ :meta private:
132
+ """
133
+ from _pytest._argcomplete import try_argcomplete
134
+
135
+ try_argcomplete(self.optparser)
136
+ strargs = [os.fspath(x) for x in args]
137
+ if namespace is None:
138
+ namespace = argparse.Namespace()
139
+ try:
140
+ namespace._raise_print_help = True
141
+ return self.optparser.parse_intermixed_args(strargs, namespace=namespace)
142
+ finally:
143
+ del namespace._raise_print_help
144
+
145
+ def parse_known_args(
146
+ self,
147
+ args: Sequence[str | os.PathLike[str]],
148
+ namespace: argparse.Namespace | None = None,
149
+ ) -> argparse.Namespace:
150
+ """Parse the known arguments at this point.
151
+
152
+ :returns: An argparse namespace object.
153
+ """
154
+ return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
155
+
156
+ def parse_known_and_unknown_args(
157
+ self,
158
+ args: Sequence[str | os.PathLike[str]],
159
+ namespace: argparse.Namespace | None = None,
160
+ ) -> tuple[argparse.Namespace, list[str]]:
161
+ """Parse the known arguments at this point, and also return the
162
+ remaining unknown flag arguments.
163
+
164
+ :returns:
165
+ A tuple containing an argparse namespace object for the known
166
+ arguments, and a list of unknown flag arguments.
167
+ """
168
+ strargs = [os.fspath(x) for x in args]
169
+ if sys.version_info < (3, 12, 8) or (3, 13) <= sys.version_info < (3, 13, 1):
170
+ # Older argparse have a bugged parse_known_intermixed_args.
171
+ namespace, unknown = self.optparser.parse_known_args(strargs, namespace)
172
+ assert namespace is not None
173
+ file_or_dir = getattr(namespace, FILE_OR_DIR)
174
+ unknown_flags: list[str] = []
175
+ for arg in unknown:
176
+ (unknown_flags if arg.startswith("-") else file_or_dir).append(arg)
177
+ return namespace, unknown_flags
178
+ else:
179
+ return self.optparser.parse_known_intermixed_args(strargs, namespace)
180
+
181
+ def addini(
182
+ self,
183
+ name: str,
184
+ help: str,
185
+ type: Literal[
186
+ "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
187
+ ]
188
+ | None = None,
189
+ default: Any = NOTSET,
190
+ *,
191
+ aliases: Sequence[str] = (),
192
+ ) -> None:
193
+ """Register a configuration file option.
194
+
195
+ :param name:
196
+ Name of the configuration.
197
+ :param type:
198
+ Type of the configuration. Can be:
199
+
200
+ * ``string``: a string
201
+ * ``bool``: a boolean
202
+ * ``args``: a list of strings, separated as in a shell
203
+ * ``linelist``: a list of strings, separated by line breaks
204
+ * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
205
+ * ``pathlist``: a list of ``py.path``, separated as in a shell
206
+ * ``int``: an integer
207
+ * ``float``: a floating-point number
208
+
209
+ .. versionadded:: 8.4
210
+
211
+ The ``float`` and ``int`` types.
212
+
213
+ For ``paths`` and ``pathlist`` types, they are considered relative to the config-file.
214
+ In case the execution is happening without a config-file defined,
215
+ they will be considered relative to the current working directory (for example with ``--override-ini``).
216
+
217
+ .. versionadded:: 7.0
218
+ The ``paths`` variable type.
219
+
220
+ .. versionadded:: 8.1
221
+ Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of a config-file.
222
+
223
+ Defaults to ``string`` if ``None`` or not passed.
224
+ :param default:
225
+ Default value if no config-file option exists but is queried.
226
+ :param aliases:
227
+ Additional names by which this option can be referenced.
228
+ Aliases resolve to the canonical name.
229
+
230
+ .. versionadded:: 9.0
231
+ The ``aliases`` parameter.
232
+
233
+ The value of configuration keys can be retrieved via a call to
234
+ :py:func:`config.getini(name) <pytest.Config.getini>`.
235
+ """
236
+ assert type in (
237
+ None,
238
+ "string",
239
+ "paths",
240
+ "pathlist",
241
+ "args",
242
+ "linelist",
243
+ "bool",
244
+ "int",
245
+ "float",
246
+ )
247
+ if type is None:
248
+ type = "string"
249
+ if default is NOTSET:
250
+ default = get_ini_default_for_type(type)
251
+
252
+ self._inidict[name] = (help, type, default)
253
+
254
+ for alias in aliases:
255
+ if alias in self._inidict:
256
+ raise ValueError(
257
+ f"alias {alias!r} conflicts with existing configuration option"
258
+ )
259
+ if (already := self._ini_aliases.get(alias)) is not None:
260
+ raise ValueError(f"{alias!r} is already an alias of {already!r}")
261
+ self._ini_aliases[alias] = name
262
+
263
+
264
+ def get_ini_default_for_type(
265
+ type: Literal[
266
+ "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
267
+ ],
268
+ ) -> Any:
269
+ """
270
+ Used by addini to get the default value for a given config option type, when
271
+ default is not supplied.
272
+ """
273
+ if type in ("paths", "pathlist", "args", "linelist"):
274
+ return []
275
+ elif type == "bool":
276
+ return False
277
+ elif type == "int":
278
+ return 0
279
+ elif type == "float":
280
+ return 0.0
281
+ else:
282
+ return ""
283
+
284
+
285
+ class Argument:
286
+ """An option defined in an OptionGroup."""
287
+
288
+ def __init__(self, action: argparse.Action) -> None:
289
+ self._action = action
290
+
291
+ def attrs(self) -> dict[str, Any]:
292
+ return self._action.__dict__
293
+
294
+ def names(self) -> Sequence[str]:
295
+ return self._action.option_strings
296
+
297
+ @property
298
+ def dest(self) -> str:
299
+ return self._action.dest
300
+
301
+ @property
302
+ def default(self) -> Any:
303
+ return self._action.default
304
+
305
+ @property
306
+ def type(self) -> Any | None:
307
+ return self._action.type
308
+
309
+ def __repr__(self) -> str:
310
+ action = getattr(self, "_action", None)
311
+ if action is None:
312
+ return "Argument(<uninitialized>)"
313
+ args: list[str] = []
314
+ args += ["opts: " + repr(self.names())]
315
+ args += ["dest: " + repr(self.dest)]
316
+ if action.type:
317
+ args += ["type: " + repr(self.type)]
318
+ args += ["default: " + repr(self.default)]
319
+ return "Argument({})".format(", ".join(args))
320
+
321
+
322
+ class OptionGroup:
323
+ """A group of options shown in its own section."""
324
+
325
+ def __init__(
326
+ self,
327
+ arggroup: argparse._ArgumentGroup,
328
+ name: str,
329
+ parser: Parser | None,
330
+ _ispytest: bool = False,
331
+ ) -> None:
332
+ check_ispytest(_ispytest)
333
+ self._arggroup = arggroup
334
+ self.name = name
335
+ self.options: list[Argument] = []
336
+ self.parser = parser
337
+
338
+ def addoption(self, *opts: str, **attrs: Any) -> None:
339
+ """Add an option to this group.
340
+
341
+ If a shortened version of a long option is specified, it will
342
+ be suppressed in the help. ``addoption('--twowords', '--two-words')``
343
+ results in help showing ``--two-words`` only, but ``--twowords`` gets
344
+ accepted **and** the automatic destination is in ``args.twowords``.
345
+
346
+ :param opts:
347
+ Option names, can be short or long options.
348
+ Note that lower-case short options (e.g. `-x`) are reserved.
349
+ :param attrs:
350
+ Same attributes as the argparse library's :meth:`add_argument()
351
+ <argparse.ArgumentParser.add_argument>` function accepts.
352
+ """
353
+ conflict = set(opts).intersection(
354
+ name for opt in self.options for name in opt.names()
355
+ )
356
+ if conflict:
357
+ raise ValueError(f"option names {conflict} already added")
358
+ self._addoption_inner(opts, attrs, allow_reserved=False)
359
+
360
+ def _addoption(self, *opts: str, **attrs: Any) -> None:
361
+ """Like addoption(), but also allows registering short lower case options (e.g. -x),
362
+ which are reserved for pytest core."""
363
+ self._addoption_inner(opts, attrs, allow_reserved=True)
364
+
365
+ def _addoption_inner(
366
+ self, opts: tuple[str, ...], attrs: dict[str, Any], allow_reserved: bool
367
+ ) -> None:
368
+ if not allow_reserved:
369
+ for opt in opts:
370
+ if len(opt) >= 2 and opt[0] == "-" and opt[1].islower():
371
+ raise ValueError("lowercase short options are reserved")
372
+
373
+ action = self._arggroup.add_argument(*opts, **attrs)
374
+ option = Argument(action)
375
+ self.options.append(option)
376
+ if self.parser:
377
+ for name in option.names():
378
+ self.parser._opt2dest[name] = option.dest
379
+ self.parser.processoption(option)
380
+
381
+
382
+ class PytestArgumentParser(argparse.ArgumentParser):
383
+ def __init__(
384
+ self,
385
+ usage: str | None,
386
+ extra_info: dict[str, str],
387
+ *,
388
+ prog: str | None = None,
389
+ ) -> None:
390
+ super().__init__(
391
+ usage=usage,
392
+ prog=prog,
393
+ add_help=False,
394
+ formatter_class=DropShorterLongHelpFormatter,
395
+ allow_abbrev=False,
396
+ fromfile_prefix_chars="@",
397
+ )
398
+ # extra_info is a dict of (param -> value) to display if there's
399
+ # an usage error to provide more contextual information to the user.
400
+ self.extra_info = extra_info
401
+
402
+ def error(self, message: str) -> NoReturn:
403
+ """Transform argparse error message into UsageError."""
404
+ # TODO(py313): Replace with `exit_on_error=False`. Note that while it
405
+ # was added in Python 3.9, it was broken until 3.13 (cpython#121018).
406
+ msg = f"{self.prog}: error: {message}"
407
+ if self.extra_info:
408
+ msg += "\n" + "\n".join(
409
+ f" {k}: {v}" for k, v in sorted(self.extra_info.items())
410
+ )
411
+ raise UsageError(self.format_usage() + msg)
412
+
413
+
414
+ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
415
+ """Shorten help for long options that differ only in extra hyphens.
416
+
417
+ - Collapse **long** options that are the same except for extra hyphens.
418
+ - Shortcut if there are only two options and one of them is a short one.
419
+ """
420
+
421
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
422
+ # Use more accurate terminal width.
423
+ if "width" not in kwargs:
424
+ kwargs["width"] = _pytest._io.get_terminal_width()
425
+ super().__init__(*args, **kwargs)
426
+
427
+ def _format_action_invocation(self, action: argparse.Action) -> str:
428
+ orgstr = super()._format_action_invocation(action)
429
+ if orgstr and orgstr[0] != "-": # only optional arguments
430
+ return orgstr
431
+ options = orgstr.split(", ")
432
+ if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
433
+ # a shortcut for '-h, --help' or '--abc', '-a'
434
+ return orgstr
435
+ return_list = []
436
+ short_long: dict[str, str] = {}
437
+ for option in options:
438
+ if len(option) == 2 or option[2] == " ":
439
+ continue
440
+ assert option.startswith("--"), (
441
+ f'long optional argument without "--": [{option}]'
442
+ )
443
+ xxoption = option[2:]
444
+ shortened = xxoption.replace("-", "")
445
+ if shortened not in short_long or len(short_long[shortened]) < len(
446
+ xxoption
447
+ ):
448
+ short_long[shortened] = xxoption
449
+ # now short_long has been filled out to the longest with dashes
450
+ # **and** we keep the right option ordering from add_argument
451
+ for option in options:
452
+ if len(option) == 2 or option[2] == " ":
453
+ return_list.append(option)
454
+ if option[2:] == short_long.get(option.replace("-", "")):
455
+ return_list.append(option.replace(" ", "=", 1))
456
+ return ", ".join(return_list)
457
+
458
+ def _split_lines(self, text: str, width: int) -> list[str]:
459
+ """Wrap lines after splitting on original newlines.
460
+
461
+ This allows to have explicit line breaks in the help text.
462
+ """
463
+ lines = []
464
+ for line in text.splitlines():
465
+ lines.extend(textwrap.wrap(line.strip(), width))
466
+ return lines
467
+
468
+
469
+ class OverrideIniAction(argparse.Action):
470
+ """Custom argparse action that makes a CLI flag equivalent to overriding an
471
+ option, in addition to behaving like `store_true`.
472
+
473
+ This can simplify things since code only needs to inspect the config option
474
+ and not consider the CLI flag.
475
+ """
476
+
477
+ def __init__(
478
+ self,
479
+ option_strings: Sequence[str],
480
+ dest: str,
481
+ nargs: int | str | None = None,
482
+ *args,
483
+ ini_option: str,
484
+ ini_value: str,
485
+ **kwargs,
486
+ ) -> None:
487
+ super().__init__(option_strings, dest, 0, *args, **kwargs)
488
+ self.ini_option = ini_option
489
+ self.ini_value = ini_value
490
+
491
+ def __call__(
492
+ self,
493
+ parser: argparse.ArgumentParser,
494
+ namespace: argparse.Namespace,
495
+ *args,
496
+ **kwargs,
497
+ ) -> None:
498
+ setattr(namespace, self.dest, True)
499
+ current_overrides = getattr(namespace, "override_ini", None)
500
+ if current_overrides is None:
501
+ current_overrides = []
502
+ current_overrides.append(f"{self.ini_option}={self.ini_value}")
503
+ setattr(namespace, "override_ini", current_overrides)
.venv/lib/python3.14/site-packages/_pytest/config/exceptions.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import final
4
+
5
+
6
+ @final
7
+ class UsageError(Exception):
8
+ """Error in pytest usage or invocation."""
9
+
10
+ __module__ = "pytest"
11
+
12
+
13
+ class PrintHelp(Exception):
14
+ """Raised when pytest should print its help to skip the rest of the
15
+ argument parsing and validation."""
.venv/lib/python3.14/site-packages/_pytest/config/findpaths.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass
6
+ from dataclasses import KW_ONLY
7
+ import os
8
+ from pathlib import Path
9
+ import sys
10
+ from typing import Literal
11
+ from typing import TypeAlias
12
+
13
+ import iniconfig
14
+
15
+ from .exceptions import UsageError
16
+ from _pytest.outcomes import fail
17
+ from _pytest.pathlib import absolutepath
18
+ from _pytest.pathlib import commonpath
19
+ from _pytest.pathlib import safe_exists
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ConfigValue:
24
+ """Represents a configuration value with its origin and parsing mode.
25
+
26
+ This allows tracking whether a value came from a configuration file
27
+ or from a CLI override (--override-ini), which is important for
28
+ determining precedence when dealing with ini option aliases.
29
+
30
+ The mode tracks the parsing mode/data model used for the value:
31
+ - "ini": from INI files or [tool.pytest.ini_options], where the only
32
+ supported value types are `str` or `list[str]`.
33
+ - "toml": from TOML files (not in INI mode), where native TOML types
34
+ are preserved.
35
+ """
36
+
37
+ value: object
38
+ _: KW_ONLY
39
+ origin: Literal["file", "override"]
40
+ mode: Literal["ini", "toml"]
41
+
42
+
43
+ ConfigDict: TypeAlias = dict[str, ConfigValue]
44
+
45
+
46
+ def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
47
+ """Parse the given generic '.ini' file using legacy IniConfig parser, returning
48
+ the parsed object.
49
+
50
+ Raise UsageError if the file cannot be parsed.
51
+ """
52
+ try:
53
+ return iniconfig.IniConfig(str(path))
54
+ except iniconfig.ParseError as exc:
55
+ raise UsageError(str(exc)) from exc
56
+
57
+
58
+ def load_config_dict_from_file(
59
+ filepath: Path,
60
+ ) -> ConfigDict | None:
61
+ """Load pytest configuration from the given file path, if supported.
62
+
63
+ Return None if the file does not contain valid pytest configuration.
64
+ """
65
+ # Configuration from ini files are obtained from the [pytest] section, if present.
66
+ if filepath.suffix == ".ini":
67
+ iniconfig = _parse_ini_config(filepath)
68
+
69
+ if "pytest" in iniconfig:
70
+ return {
71
+ k: ConfigValue(v, origin="file", mode="ini")
72
+ for k, v in iniconfig["pytest"].items()
73
+ }
74
+ else:
75
+ # "pytest.ini" files are always the source of configuration, even if empty.
76
+ if filepath.name in {"pytest.ini", ".pytest.ini"}:
77
+ return {}
78
+
79
+ # '.cfg' files are considered if they contain a "[tool:pytest]" section.
80
+ elif filepath.suffix == ".cfg":
81
+ iniconfig = _parse_ini_config(filepath)
82
+
83
+ if "tool:pytest" in iniconfig.sections:
84
+ return {
85
+ k: ConfigValue(v, origin="file", mode="ini")
86
+ for k, v in iniconfig["tool:pytest"].items()
87
+ }
88
+ elif "pytest" in iniconfig.sections:
89
+ # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
90
+ # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
91
+ fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
92
+
93
+ # '.toml' files are considered if they contain a [tool.pytest] table (toml mode)
94
+ # or [tool.pytest.ini_options] table (ini mode) for pyproject.toml,
95
+ # or [pytest] table (toml mode) for pytest.toml/.pytest.toml.
96
+ elif filepath.suffix == ".toml":
97
+ if sys.version_info >= (3, 11):
98
+ import tomllib
99
+ else:
100
+ import tomli as tomllib
101
+
102
+ toml_text = filepath.read_text(encoding="utf-8")
103
+ try:
104
+ config = tomllib.loads(toml_text)
105
+ except tomllib.TOMLDecodeError as exc:
106
+ raise UsageError(f"{filepath}: {exc}") from exc
107
+
108
+ # pytest.toml and .pytest.toml use [pytest] table directly.
109
+ if filepath.name in ("pytest.toml", ".pytest.toml"):
110
+ pytest_config = config.get("pytest", {})
111
+ if pytest_config:
112
+ # TOML mode - preserve native TOML types.
113
+ return {
114
+ k: ConfigValue(v, origin="file", mode="toml")
115
+ for k, v in pytest_config.items()
116
+ }
117
+ # "pytest.toml" files are always the source of configuration, even if empty.
118
+ return {}
119
+
120
+ # pyproject.toml uses [tool.pytest] or [tool.pytest.ini_options].
121
+ else:
122
+ tool_pytest = config.get("tool", {}).get("pytest", {})
123
+
124
+ # Check for toml mode config: [tool.pytest] with content outside of ini_options.
125
+ toml_config = {k: v for k, v in tool_pytest.items() if k != "ini_options"}
126
+ # Check for ini mode config: [tool.pytest.ini_options].
127
+ ini_config = tool_pytest.get("ini_options", None)
128
+
129
+ if toml_config and ini_config:
130
+ raise UsageError(
131
+ f"{filepath}: Cannot use both [tool.pytest] (native TOML types) and "
132
+ "[tool.pytest.ini_options] (string-based INI format) simultaneously. "
133
+ "Please use [tool.pytest] with native TOML types (recommended) "
134
+ "or [tool.pytest.ini_options] for backwards compatibility."
135
+ )
136
+
137
+ if toml_config:
138
+ # TOML mode - preserve native TOML types.
139
+ return {
140
+ k: ConfigValue(v, origin="file", mode="toml")
141
+ for k, v in toml_config.items()
142
+ }
143
+
144
+ elif ini_config is not None:
145
+ # INI mode - TOML supports richer data types than INI files, but we need to
146
+ # convert all scalar values to str for compatibility with the INI system.
147
+ def make_scalar(v: object) -> str | list[str]:
148
+ return v if isinstance(v, list) else str(v)
149
+
150
+ return {
151
+ k: ConfigValue(make_scalar(v), origin="file", mode="ini")
152
+ for k, v in ini_config.items()
153
+ }
154
+
155
+ return None
156
+
157
+
158
+ def locate_config(
159
+ invocation_dir: Path,
160
+ args: Iterable[Path],
161
+ ) -> tuple[Path | None, Path | None, ConfigDict, Sequence[str]]:
162
+ """Search in the list of arguments for a valid ini-file for pytest,
163
+ and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where
164
+ ignored-config-files is a list of config basenames found that contain
165
+ pytest configuration but were ignored."""
166
+ config_names = [
167
+ "pytest.toml",
168
+ ".pytest.toml",
169
+ "pytest.ini",
170
+ ".pytest.ini",
171
+ "pyproject.toml",
172
+ "tox.ini",
173
+ "setup.cfg",
174
+ ]
175
+ args = [x for x in args if not str(x).startswith("-")]
176
+ if not args:
177
+ args = [invocation_dir]
178
+ found_pyproject_toml: Path | None = None
179
+ ignored_config_files: list[str] = []
180
+
181
+ for arg in args:
182
+ argpath = absolutepath(arg)
183
+ for base in (argpath, *argpath.parents):
184
+ for config_name in config_names:
185
+ p = base / config_name
186
+ if p.is_file():
187
+ if p.name == "pyproject.toml" and found_pyproject_toml is None:
188
+ found_pyproject_toml = p
189
+ ini_config = load_config_dict_from_file(p)
190
+ if ini_config is not None:
191
+ index = config_names.index(config_name)
192
+ for remainder in config_names[index + 1 :]:
193
+ p2 = base / remainder
194
+ if (
195
+ p2.is_file()
196
+ and load_config_dict_from_file(p2) is not None
197
+ ):
198
+ ignored_config_files.append(remainder)
199
+ return base, p, ini_config, ignored_config_files
200
+ if found_pyproject_toml is not None:
201
+ return found_pyproject_toml.parent, found_pyproject_toml, {}, []
202
+ return None, None, {}, []
203
+
204
+
205
+ def get_common_ancestor(
206
+ invocation_dir: Path,
207
+ paths: Iterable[Path],
208
+ ) -> Path:
209
+ common_ancestor: Path | None = None
210
+ for path in paths:
211
+ if not path.exists():
212
+ continue
213
+ if common_ancestor is None:
214
+ common_ancestor = path
215
+ else:
216
+ if common_ancestor in path.parents or path == common_ancestor:
217
+ continue
218
+ elif path in common_ancestor.parents:
219
+ common_ancestor = path
220
+ else:
221
+ shared = commonpath(path, common_ancestor)
222
+ if shared is not None:
223
+ common_ancestor = shared
224
+ if common_ancestor is None:
225
+ common_ancestor = invocation_dir
226
+ elif common_ancestor.is_file():
227
+ common_ancestor = common_ancestor.parent
228
+ return common_ancestor
229
+
230
+
231
+ def get_dirs_from_args(args: Iterable[str]) -> list[Path]:
232
+ def is_option(x: str) -> bool:
233
+ return x.startswith("-")
234
+
235
+ def get_file_part_from_node_id(x: str) -> str:
236
+ return x.split("::", maxsplit=1)[0]
237
+
238
+ def get_dir_from_path(path: Path) -> Path:
239
+ if path.is_dir():
240
+ return path
241
+ return path.parent
242
+
243
+ # These look like paths but may not exist
244
+ possible_paths = (
245
+ absolutepath(get_file_part_from_node_id(arg))
246
+ for arg in args
247
+ if not is_option(arg)
248
+ )
249
+
250
+ return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
251
+
252
+
253
+ def parse_override_ini(override_ini: Sequence[str] | None) -> ConfigDict:
254
+ """Parse the -o/--override-ini command line arguments and return the overrides.
255
+
256
+ :raises UsageError:
257
+ If one of the values is malformed.
258
+ """
259
+ overrides = {}
260
+ # override_ini is a list of "ini=value" options.
261
+ # Always use the last item if multiple values are set for same ini-name,
262
+ # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
263
+ for ini_config in override_ini or ():
264
+ try:
265
+ key, user_ini_value = ini_config.split("=", 1)
266
+ except ValueError as e:
267
+ raise UsageError(
268
+ f"-o/--override-ini expects option=value style (got: {ini_config!r})."
269
+ ) from e
270
+ else:
271
+ overrides[key] = ConfigValue(user_ini_value, origin="override", mode="ini")
272
+ return overrides
273
+
274
+
275
+ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
276
+
277
+
278
+ def determine_setup(
279
+ *,
280
+ inifile: str | None,
281
+ override_ini: Sequence[str] | None,
282
+ args: Sequence[str],
283
+ rootdir_cmd_arg: str | None,
284
+ invocation_dir: Path,
285
+ ) -> tuple[Path, Path | None, ConfigDict, Sequence[str]]:
286
+ """Determine the rootdir, inifile and ini configuration values from the
287
+ command line arguments.
288
+
289
+ :param inifile:
290
+ The `--inifile` command line argument, if given.
291
+ :param override_ini:
292
+ The -o/--override-ini command line arguments, if given.
293
+ :param args:
294
+ The free command line arguments.
295
+ :param rootdir_cmd_arg:
296
+ The `--rootdir` command line argument, if given.
297
+ :param invocation_dir:
298
+ The working directory when pytest was invoked.
299
+
300
+ :raises UsageError:
301
+ """
302
+ rootdir = None
303
+ dirs = get_dirs_from_args(args)
304
+ ignored_config_files: Sequence[str] = []
305
+
306
+ if inifile:
307
+ inipath_ = absolutepath(inifile)
308
+ inipath: Path | None = inipath_
309
+ inicfg = load_config_dict_from_file(inipath_) or {}
310
+ if rootdir_cmd_arg is None:
311
+ rootdir = inipath_.parent
312
+ else:
313
+ ancestor = get_common_ancestor(invocation_dir, dirs)
314
+ rootdir, inipath, inicfg, ignored_config_files = locate_config(
315
+ invocation_dir, [ancestor]
316
+ )
317
+ if rootdir is None and rootdir_cmd_arg is None:
318
+ for possible_rootdir in (ancestor, *ancestor.parents):
319
+ if (possible_rootdir / "setup.py").is_file():
320
+ rootdir = possible_rootdir
321
+ break
322
+ else:
323
+ if dirs != [ancestor]:
324
+ rootdir, inipath, inicfg, _ = locate_config(invocation_dir, dirs)
325
+ if rootdir is None:
326
+ rootdir = get_common_ancestor(
327
+ invocation_dir, [invocation_dir, ancestor]
328
+ )
329
+ if is_fs_root(rootdir):
330
+ rootdir = ancestor
331
+ if rootdir_cmd_arg:
332
+ rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
333
+ if not rootdir.is_dir():
334
+ raise UsageError(
335
+ f"Directory '{rootdir}' not found. Check your '--rootdir' option."
336
+ )
337
+
338
+ ini_overrides = parse_override_ini(override_ini)
339
+ inicfg.update(ini_overrides)
340
+
341
+ assert rootdir is not None
342
+ return rootdir, inipath, inicfg, ignored_config_files
343
+
344
+
345
+ def is_fs_root(p: Path) -> bool:
346
+ r"""
347
+ Return True if the given path is pointing to the root of the
348
+ file system ("/" on Unix and "C:\\" on Windows for example).
349
+ """
350
+ return os.path.splitdrive(str(p))[1] == os.sep
.venv/lib/python3.14/site-packages/_pytest/debugging.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from collections.abc import Callable
9
+ from collections.abc import Generator
10
+ import functools
11
+ import importlib
12
+ import sys
13
+ import types
14
+ from typing import Any
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
+ mod = importlib.import_module(modname)
127
+
128
+ # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
129
+ parts = classname.split(".")
130
+ pdb_cls = getattr(mod, parts[0])
131
+ for part in parts[1:]:
132
+ pdb_cls = getattr(pdb_cls, part)
133
+ except Exception as exc:
134
+ value = ":".join((modname, classname))
135
+ raise UsageError(
136
+ f"--pdbcls: could not import {value!r}: {exc}"
137
+ ) from exc
138
+ else:
139
+ import pdb
140
+
141
+ pdb_cls = pdb.Pdb
142
+
143
+ wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
144
+ cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
145
+ return wrapped_cls
146
+
147
+ @classmethod
148
+ def _get_pdb_wrapper_class(cls, pdb_cls, capman: CaptureManager | None):
149
+ import _pytest.config
150
+
151
+ class PytestPdbWrapper(pdb_cls):
152
+ _pytest_capman = capman
153
+ _continued = False
154
+
155
+ def do_debug(self, arg):
156
+ cls._recursive_debug += 1
157
+ ret = super().do_debug(arg)
158
+ cls._recursive_debug -= 1
159
+ return ret
160
+
161
+ if hasattr(pdb_cls, "do_debug"):
162
+ do_debug.__doc__ = pdb_cls.do_debug.__doc__
163
+
164
+ def do_continue(self, arg):
165
+ ret = super().do_continue(arg)
166
+ if cls._recursive_debug == 0:
167
+ assert cls._config is not None
168
+ tw = _pytest.config.create_terminal_writer(cls._config)
169
+ tw.line()
170
+
171
+ capman = self._pytest_capman
172
+ capturing = pytestPDB._is_capturing(capman)
173
+ if capturing:
174
+ if capturing == "global":
175
+ tw.sep(">", "PDB continue (IO-capturing resumed)")
176
+ else:
177
+ tw.sep(
178
+ ">",
179
+ f"PDB continue (IO-capturing resumed for {capturing})",
180
+ )
181
+ assert capman is not None
182
+ capman.resume()
183
+ else:
184
+ tw.sep(">", "PDB continue")
185
+ assert cls._pluginmanager is not None
186
+ cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
187
+ self._continued = True
188
+ return ret
189
+
190
+ if hasattr(pdb_cls, "do_continue"):
191
+ do_continue.__doc__ = pdb_cls.do_continue.__doc__
192
+
193
+ do_c = do_cont = do_continue
194
+
195
+ def do_quit(self, arg):
196
+ # Raise Exit outcome when quit command is used in pdb.
197
+ #
198
+ # This is a bit of a hack - it would be better if BdbQuit
199
+ # could be handled, but this would require to wrap the
200
+ # whole pytest run, and adjust the report etc.
201
+ ret = super().do_quit(arg)
202
+
203
+ if cls._recursive_debug == 0:
204
+ outcomes.exit("Quitting debugger")
205
+
206
+ return ret
207
+
208
+ if hasattr(pdb_cls, "do_quit"):
209
+ do_quit.__doc__ = pdb_cls.do_quit.__doc__
210
+
211
+ do_q = do_quit
212
+ do_exit = do_quit
213
+
214
+ def setup(self, f, tb):
215
+ """Suspend on setup().
216
+
217
+ Needed after do_continue resumed, and entering another
218
+ breakpoint again.
219
+ """
220
+ ret = super().setup(f, tb)
221
+ if not ret and self._continued:
222
+ # pdb.setup() returns True if the command wants to exit
223
+ # from the interaction: do not suspend capturing then.
224
+ if self._pytest_capman:
225
+ self._pytest_capman.suspend_global_capture(in_=True)
226
+ return ret
227
+
228
+ def get_stack(self, f, t):
229
+ stack, i = super().get_stack(f, t)
230
+ if f is None:
231
+ # Find last non-hidden frame.
232
+ i = max(0, len(stack) - 1)
233
+ while i and stack[i][0].f_locals.get("__tracebackhide__", False):
234
+ i -= 1
235
+ return stack, i
236
+
237
+ return PytestPdbWrapper
238
+
239
+ @classmethod
240
+ def _init_pdb(cls, method, *args, **kwargs):
241
+ """Initialize PDB debugging, dropping any IO capturing."""
242
+ import _pytest.config
243
+
244
+ if cls._pluginmanager is None:
245
+ capman: CaptureManager | None = None
246
+ else:
247
+ capman = cls._pluginmanager.getplugin("capturemanager")
248
+ if capman:
249
+ capman.suspend(in_=True)
250
+
251
+ if cls._config:
252
+ tw = _pytest.config.create_terminal_writer(cls._config)
253
+ tw.line()
254
+
255
+ if cls._recursive_debug == 0:
256
+ # Handle header similar to pdb.set_trace in py37+.
257
+ header = kwargs.pop("header", None)
258
+ if header is not None:
259
+ tw.sep(">", header)
260
+ else:
261
+ capturing = cls._is_capturing(capman)
262
+ if capturing == "global":
263
+ tw.sep(">", f"PDB {method} (IO-capturing turned off)")
264
+ elif capturing:
265
+ tw.sep(
266
+ ">",
267
+ f"PDB {method} (IO-capturing turned off for {capturing})",
268
+ )
269
+ else:
270
+ tw.sep(">", f"PDB {method}")
271
+
272
+ _pdb = cls._import_pdb_cls(capman)(**kwargs)
273
+
274
+ if cls._pluginmanager:
275
+ cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
276
+ return _pdb
277
+
278
+ @classmethod
279
+ def set_trace(cls, *args, **kwargs) -> None:
280
+ """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
281
+ frame = sys._getframe().f_back
282
+ _pdb = cls._init_pdb("set_trace", *args, **kwargs)
283
+ _pdb.set_trace(frame)
284
+
285
+
286
+ class PdbInvoke:
287
+ def pytest_exception_interact(
288
+ self, node: Node, call: CallInfo[Any], report: BaseReport
289
+ ) -> None:
290
+ capman = node.config.pluginmanager.getplugin("capturemanager")
291
+ if capman:
292
+ capman.suspend_global_capture(in_=True)
293
+ out, err = capman.read_global_capture()
294
+ sys.stdout.write(out)
295
+ sys.stdout.write(err)
296
+ assert call.excinfo is not None
297
+ _enter_pdb(node, call.excinfo, report)
298
+
299
+ def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
300
+ exc_or_tb = _postmortem_exc_or_tb(excinfo)
301
+ post_mortem(exc_or_tb)
302
+
303
+
304
+ class PdbTrace:
305
+ @hookimpl(wrapper=True)
306
+ def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
307
+ wrap_pytest_function_for_tracing(pyfuncitem)
308
+ return (yield)
309
+
310
+
311
+ def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
312
+ """Change the Python function object of the given Function item by a
313
+ wrapper which actually enters pdb before calling the python function
314
+ itself, effectively leaving the user in the pdb prompt in the first
315
+ statement of the function."""
316
+ _pdb = pytestPDB._init_pdb("runcall")
317
+ testfunction = pyfuncitem.obj
318
+
319
+ # we can't just return `partial(pdb.runcall, testfunction)` because (on
320
+ # python < 3.7.4) runcall's first param is `func`, which means we'd get
321
+ # an exception if one of the kwargs to testfunction was called `func`.
322
+ @functools.wraps(testfunction)
323
+ def wrapper(*args, **kwargs) -> None:
324
+ func = functools.partial(testfunction, *args, **kwargs)
325
+ _pdb.runcall(func)
326
+
327
+ pyfuncitem.obj = wrapper
328
+
329
+
330
+ def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None:
331
+ """Wrap the given pytestfunct item for tracing support if --trace was given in
332
+ the command line."""
333
+ if pyfuncitem.config.getvalue("trace"):
334
+ wrap_pytest_function_for_tracing(pyfuncitem)
335
+
336
+
337
+ def _enter_pdb(
338
+ node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport
339
+ ) -> BaseReport:
340
+ # XXX we reuse the TerminalReporter's terminalwriter
341
+ # because this seems to avoid some encoding related troubles
342
+ # for not completely clear reasons.
343
+ tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
344
+ tw.line()
345
+
346
+ showcapture = node.config.option.showcapture
347
+
348
+ for sectionname, content in (
349
+ ("stdout", rep.capstdout),
350
+ ("stderr", rep.capstderr),
351
+ ("log", rep.caplog),
352
+ ):
353
+ if showcapture in (sectionname, "all") and content:
354
+ tw.sep(">", "captured " + sectionname)
355
+ if content[-1:] == "\n":
356
+ content = content[:-1]
357
+ tw.line(content)
358
+
359
+ tw.sep(">", "traceback")
360
+ rep.toterminal(tw)
361
+ tw.sep(">", "entering PDB")
362
+ tb_or_exc = _postmortem_exc_or_tb(excinfo)
363
+ rep._pdbshown = True # type: ignore[attr-defined]
364
+ post_mortem(tb_or_exc)
365
+ return rep
366
+
367
+
368
+ def _postmortem_exc_or_tb(
369
+ excinfo: ExceptionInfo[BaseException],
370
+ ) -> types.TracebackType | BaseException:
371
+ from doctest import UnexpectedException
372
+
373
+ get_exc = sys.version_info >= (3, 13)
374
+ if isinstance(excinfo.value, UnexpectedException):
375
+ # A doctest.UnexpectedException is not useful for post_mortem.
376
+ # Use the underlying exception instead:
377
+ underlying_exc = excinfo.value
378
+ if get_exc:
379
+ return underlying_exc.exc_info[1]
380
+
381
+ return underlying_exc.exc_info[2]
382
+ elif isinstance(excinfo.value, ConftestImportFailure):
383
+ # A config.ConftestImportFailure is not useful for post_mortem.
384
+ # Use the underlying exception instead:
385
+ cause = excinfo.value.cause
386
+ if get_exc:
387
+ return cause
388
+
389
+ assert cause.__traceback__ is not None
390
+ return cause.__traceback__
391
+ else:
392
+ assert excinfo._excinfo is not None
393
+ if get_exc:
394
+ return excinfo._excinfo[1]
395
+
396
+ return excinfo._excinfo[2]
397
+
398
+
399
+ def post_mortem(tb_or_exc: types.TracebackType | BaseException) -> None:
400
+ p = pytestPDB._init_pdb("post_mortem")
401
+ p.reset()
402
+ p.interaction(None, tb_or_exc)
403
+ if p.quitting:
404
+ outcomes.exit("Quitting debugger")
.venv/lib/python3.14/site-packages/_pytest/deprecated.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 PytestRemovedIn10Warning
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
+ "pytest_subtests",
28
+ }
29
+
30
+
31
+ # This could have been removed pytest 8, but it's harmless and common, so no rush to remove.
32
+ YIELD_FIXTURE = PytestDeprecationWarning(
33
+ "@pytest.yield_fixture is deprecated.\n"
34
+ "Use @pytest.fixture instead; they are the same."
35
+ )
36
+
37
+ CLASS_FIXTURE_INSTANCE_METHOD = PytestRemovedIn10Warning(
38
+ "Class-scoped fixture defined as instance method is deprecated.\n"
39
+ "Instance attributes set in this fixture will NOT be visible to test methods,\n"
40
+ "as each test gets a new instance while the fixture runs only once per class.\n"
41
+ "Use @classmethod decorator and set attributes on cls instead.\n"
42
+ "See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method"
43
+ )
44
+
45
+ # This deprecation is never really meant to be removed.
46
+ PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
47
+
48
+
49
+ HOOK_LEGACY_MARKING = UnformattedWarning(
50
+ PytestRemovedIn10Warning,
51
+ "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
52
+ "Please use the pytest.hook{type}({hook_opts}) decorator instead\n"
53
+ " to configure the hooks.\n"
54
+ " See https://docs.pytest.org/en/latest/deprecations.html"
55
+ "#configuring-hook-specs-impls-using-markers",
56
+ )
57
+
58
+ MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES = PytestRemovedIn10Warning(
59
+ "monkeypatch.syspath_prepend() called with pkg_resources legacy namespace packages detected.\n"
60
+ "Legacy namespace packages (using pkg_resources.declare_namespace) are deprecated.\n"
61
+ "Please use native namespace packages (PEP 420) instead.\n"
62
+ "See https://docs.pytest.org/en/stable/deprecations.html#monkeypatch-fixup-namespace-packages"
63
+ )
64
+
65
+ PARAMETRIZE_NON_COLLECTION_ITERABLE = UnformattedWarning(
66
+ PytestRemovedIn10Warning,
67
+ "Passing a non-Collection iterable to parametrize is deprecated.\n"
68
+ "Test: {nodeid}, argvalues type: {type_name}\n"
69
+ "Please convert to a list or tuple.\n"
70
+ "See https://docs.pytest.org/en/stable/deprecations.html#parametrize-iterators",
71
+ )
72
+
73
+ CONSOLE_MAIN = PytestRemovedIn10Warning(
74
+ "pytest.console_main() is deprecated and will be removed in pytest 10.\n"
75
+ "It was never intended for programmatic use; use pytest.main() instead.\n"
76
+ "See https://docs.pytest.org/en/stable/deprecations.html#console-main"
77
+ )
78
+
79
+ CONFIG_INICFG = PytestRemovedIn10Warning(
80
+ "config.inicfg is deprecated, use config.getini() to access configuration values instead.\n"
81
+ "See https://docs.pytest.org/en/stable/deprecations.html#config-inicfg"
82
+ )
83
+
84
+ FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN = UnformattedWarning(
85
+ PytestRemovedIn10Warning,
86
+ 'Calling request.getfixturevalue("{argname}") during teardown is deprecated.\n'
87
+ "Please request the fixture before teardown begins, either by declaring it in the fixture signature "
88
+ "or by calling request.getfixturevalue() before the fixture yields.\n"
89
+ "See https://docs.pytest.org/en/stable/deprecations.html#dynamic-fixture-request-during-teardown",
90
+ )
91
+
92
+ PASTEBIN = PytestRemovedIn10Warning(
93
+ "The --pastebin option is deprecated. "
94
+ "The functionality is now available in an external plugin package, pytest-pastebin.\n"
95
+ "See https://docs.pytest.org/en/stable/deprecations.html#the-pastebin-option"
96
+ )
97
+
98
+ # You want to make some `__init__` or function "private".
99
+ #
100
+ # def my_private_function(some, args):
101
+ # ...
102
+ #
103
+ # Do this:
104
+ #
105
+ # def my_private_function(some, args, *, _ispytest: bool = False):
106
+ # check_ispytest(_ispytest)
107
+ # ...
108
+ #
109
+ # Change all internal/allowed calls to
110
+ #
111
+ # my_private_function(some, args, _ispytest=True)
112
+ #
113
+ # All other calls will get the default _ispytest=False and trigger
114
+ # the warning (possibly error in the future).
115
+
116
+
117
+ FIXTURE_BASEID_DEPRECATED = PytestRemovedIn10Warning(
118
+ "Passing baseid to FixtureDef is deprecated. Pass node instead for fixture scoping."
119
+ )
120
+
121
+ FIXTURE_NODEID_DEPRECATED = PytestRemovedIn10Warning(
122
+ "Passing nodeid to _register_fixture is deprecated. "
123
+ "Pass node instead for fixture scoping."
124
+ )
125
+
126
+ FIXTUREDEF_HAS_LOCATION_DEPRECATED = PytestRemovedIn10Warning(
127
+ "FixtureDef.has_location is deprecated and will be removed in pytest 10. "
128
+ "See https://docs.pytest.org/en/stable/deprecations.html#fixturedef-has-location-deprecated"
129
+ )
130
+
131
+ PARSEFACTORIES_NODEID_DEPRECATED = PytestRemovedIn10Warning(
132
+ "Passing nodeid string to parsefactories is deprecated. "
133
+ "Use parsefactories(holder=obj, node=node) instead."
134
+ )
135
+
136
+
137
+ def check_ispytest(ispytest: bool) -> None:
138
+ if not ispytest:
139
+ warn(PRIVATE, stacklevel=3)