JustinTX commited on
Commit
4ecae3d
·
verified ·
1 Parent(s): acd32b1

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +84 -0
  2. ccevolve/dataclaw_export.jsonl +3 -0
  3. docs/code_evolution_history_panes.mp4 +3 -0
  4. docs/conceptual.png +3 -0
  5. docs/webui.png +3 -0
  6. my/ablation_study_summary.png +3 -0
  7. my/aux_7vs4_comparison.png +3 -0
  8. my/auxiliary_ablation_plots.png +3 -0
  9. my/auxiliary_metric_correlations.png +3 -0
  10. my/correlation_evolution_over_time.png +3 -0
  11. my/correlation_explanation.png +3 -0
  12. my/refined_aux_comparison.png +3 -0
  13. py311/bin/python +3 -0
  14. py311/bin/python3 +3 -0
  15. py311/bin/python3.11 +3 -0
  16. py311/bin/ty +3 -0
  17. py311/lib/python3.11/site-packages/30fcd23745efe32ce681__mypyc.cpython-311-x86_64-linux-gnu.so +3 -0
  18. py311/lib/python3.11/site-packages/Levenshtein/levenshtein_cpp.cpython-311-x86_64-linux-gnu.so +3 -0
  19. py311/lib/python3.11/site-packages/PIL/_imaging.cpython-311-x86_64-linux-gnu.so +3 -0
  20. py311/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so +3 -0
  21. py311/lib/python3.11/site-packages/PIL/_imagingft.cpython-311-x86_64-linux-gnu.so +3 -0
  22. py311/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so +3 -0
  23. py311/lib/python3.11/site-packages/__editable__.shinka-0.0.1.pth +3 -0
  24. py311/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc +3 -0
  25. py311/lib/python3.11/site-packages/_pytest/_code/__init__.py +26 -0
  26. py311/lib/python3.11/site-packages/_pytest/_code/code.py +1565 -0
  27. py311/lib/python3.11/site-packages/_pytest/_code/source.py +225 -0
  28. py311/lib/python3.11/site-packages/_pytest/_io/__init__.py +10 -0
  29. py311/lib/python3.11/site-packages/_pytest/_io/pprint.py +673 -0
  30. py311/lib/python3.11/site-packages/_pytest/_io/saferepr.py +130 -0
  31. py311/lib/python3.11/site-packages/_pytest/_io/terminalwriter.py +258 -0
  32. py311/lib/python3.11/site-packages/_pytest/_io/wcwidth.py +57 -0
  33. py311/lib/python3.11/site-packages/_pytest/_py/__init__.py +0 -0
  34. py311/lib/python3.11/site-packages/_pytest/_py/error.py +119 -0
  35. py311/lib/python3.11/site-packages/_pytest/_py/path.py +1475 -0
  36. py311/lib/python3.11/site-packages/_pytest/assertion/__init__.py +208 -0
  37. py311/lib/python3.11/site-packages/_pytest/assertion/rewrite.py +1202 -0
  38. py311/lib/python3.11/site-packages/_pytest/assertion/truncate.py +137 -0
  39. py311/lib/python3.11/site-packages/_pytest/assertion/util.py +615 -0
  40. py311/lib/python3.11/site-packages/_pytest/config/__init__.py +2197 -0
  41. py311/lib/python3.11/site-packages/_pytest/config/argparsing.py +578 -0
  42. py311/lib/python3.11/site-packages/_pytest/config/compat.py +85 -0
  43. py311/lib/python3.11/site-packages/_pytest/config/exceptions.py +15 -0
  44. py311/lib/python3.11/site-packages/_pytest/config/findpaths.py +350 -0
  45. py311/lib/python3.11/site-packages/_pytest/mark/__init__.py +301 -0
  46. py311/lib/python3.11/site-packages/_pytest/mark/expression.py +353 -0
  47. py311/lib/python3.11/site-packages/_pytest/mark/structures.py +664 -0
  48. py311/lib/python3.11/site-packages/_virtualenv.pth +3 -0
  49. py311/lib/python3.11/site-packages/aiohttp-3.13.3.dist-info/licenses/LICENSE.txt +13 -0
  50. py311/lib/python3.11/site-packages/aiohttp-3.13.3.dist-info/licenses/vendor/llhttp/LICENSE +22 -0
.gitattributes CHANGED
@@ -33,3 +33,87 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ my/correlation_evolution_over_time.png filter=lfs diff=lfs merge=lfs -text
37
+ my/auxiliary_ablation_plots.png filter=lfs diff=lfs merge=lfs -text
38
+ my/refined_aux_comparison.png filter=lfs diff=lfs merge=lfs -text
39
+ my/ablation_study_summary.png filter=lfs diff=lfs merge=lfs -text
40
+ my/aux_7vs4_comparison.png filter=lfs diff=lfs merge=lfs -text
41
+ my/correlation_explanation.png filter=lfs diff=lfs merge=lfs -text
42
+ docs/webui.png filter=lfs diff=lfs merge=lfs -text
43
+ docs/conceptual.png filter=lfs diff=lfs merge=lfs -text
44
+ my/auxiliary_metric_correlations.png filter=lfs diff=lfs merge=lfs -text
45
+ shinka/favicon.png filter=lfs diff=lfs merge=lfs -text
46
+ docs/code_evolution_history_panes.mp4 filter=lfs diff=lfs merge=lfs -text
47
+ ccevolve/dataclaw_export.jsonl filter=lfs diff=lfs merge=lfs -text
48
+ py311/lib/python3.11/site-packages/pillow.libs/libpng16-d00bd151.so.16.49.0 filter=lfs diff=lfs merge=lfs -text
49
+ py311/lib/python3.11/site-packages/30fcd23745efe32ce681__mypyc.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
50
+ py311/lib/python3.11/site-packages/pillow.libs/libharfbuzz-fe5b8f8d.so.0.61121.0 filter=lfs diff=lfs merge=lfs -text
51
+ py311/lib/python3.11/site-packages/pillow.libs/liblzma-64b7ab39.so.5.8.1 filter=lfs diff=lfs merge=lfs -text
52
+ py311/lib/python3.11/site-packages/pillow.libs/libxcb-64009ff3.so.1.1.0 filter=lfs diff=lfs merge=lfs -text
53
+ py311/lib/python3.11/site-packages/pillow.libs/liblcms2-cc10e42f.so.2.0.17 filter=lfs diff=lfs merge=lfs -text
54
+ py311/lib/python3.11/site-packages/pillow.libs/libjpeg-8a13c6e0.so.62.4.0 filter=lfs diff=lfs merge=lfs -text
55
+ py311/lib/python3.11/site-packages/pillow.libs/libtiff-13a02c81.so.6.1.0 filter=lfs diff=lfs merge=lfs -text
56
+ py311/bin/python3 filter=lfs diff=lfs merge=lfs -text
57
+ py311/lib/python3.11/site-packages/scipy.libs/libquadmath-828275a7.so.0.0.0 filter=lfs diff=lfs merge=lfs -text
58
+ py311/lib/python3.11/site-packages/scipy.libs/libgfortran-8f1e9814.so.5.0.0 filter=lfs diff=lfs merge=lfs -text
59
+ py311/lib/python3.11/site-packages/scipy.libs/libquadmath-96973f99-934c22de.so.0.0.0 filter=lfs diff=lfs merge=lfs -text
60
+ py311/lib/python3.11/site-packages/scipy.libs/libgfortran-040039e1-0352e75f.so.5.0.0 filter=lfs diff=lfs merge=lfs -text
61
+ py311/lib/python3.11/site-packages/matplotlib/_image.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
62
+ py311/lib/python3.11/site-packages/matplotlib/_qhull.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
63
+ py311/lib/python3.11/site-packages/pillow.libs/libopenjp2-56811f71.so.2.5.3 filter=lfs diff=lfs merge=lfs -text
64
+ py311/bin/python3.11 filter=lfs diff=lfs merge=lfs -text
65
+ py311/lib/python3.11/site-packages/pillow.libs/libwebp-5f0275c0.so.7.1.10 filter=lfs diff=lfs merge=lfs -text
66
+ py311/lib/python3.11/site-packages/pillow.libs/libfreetype-083ff72c.so.6.20.2 filter=lfs diff=lfs merge=lfs -text
67
+ py311/lib/python3.11/site-packages/pillow.libs/libavif-01e67780.so.16.3.0 filter=lfs diff=lfs merge=lfs -text
68
+ py311/lib/python3.11/site-packages/pillow.libs/libbrotlicommon-c55a5f7a.so.1.1.0 filter=lfs diff=lfs merge=lfs -text
69
+ py311/bin/ty filter=lfs diff=lfs merge=lfs -text
70
+ py311/bin/python filter=lfs diff=lfs merge=lfs -text
71
+ py311/lib/python3.11/site-packages/matplotlib/_path.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
72
+ py311/lib/python3.11/site-packages/matplotlib/_tri.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
73
+ py311/lib/python3.11/site-packages/matplotlib/ft2font.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
74
+ py311/lib/python3.11/site-packages/matplotlib/_c_internal_utils.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
75
+ py311/lib/python3.11/site-packages/contourpy/_contourpy.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
76
+ py311/lib/python3.11/site-packages/numpy.libs/libgfortran-040039e1-0352e75f.so.5.0.0 filter=lfs diff=lfs merge=lfs -text
77
+ py311/lib/python3.11/site-packages/numpy.libs/libquadmath-96973f99-934c22de.so.0.0.0 filter=lfs diff=lfs merge=lfs -text
78
+ py311/lib/python3.11/site-packages/scipy.libs/libscipy_openblas-6cdc3b4a.so filter=lfs diff=lfs merge=lfs -text
79
+ py311/lib/python3.11/site-packages/distlib/t64.exe filter=lfs diff=lfs merge=lfs -text
80
+ py311/lib/python3.11/site-packages/distlib/w64-arm.exe filter=lfs diff=lfs merge=lfs -text
81
+ py311/lib/python3.11/site-packages/distlib/w64.exe filter=lfs diff=lfs merge=lfs -text
82
+ py311/lib/python3.11/site-packages/distlib/t64-arm.exe filter=lfs diff=lfs merge=lfs -text
83
+ py311/lib/python3.11/site-packages/rapidfuzz/fuzz_cpp_avx2.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
84
+ py311/lib/python3.11/site-packages/rapidfuzz/fuzz_cpp.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
85
+ py311/lib/python3.11/site-packages/rapidfuzz/process_cpp_impl.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
86
+ py311/lib/python3.11/site-packages/numpy.libs/libscipy_openblas64_-fdde5778.so filter=lfs diff=lfs merge=lfs -text
87
+ py311/lib/python3.11/site-packages/rapidfuzz/utils_cpp.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
88
+ py311/lib/python3.11/site-packages/propcache/_helpers_c.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
89
+ py311/lib/python3.11/site-packages/multidict/_multidict.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
90
+ py311/lib/python3.11/site-packages/jiter/jiter.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
91
+ py311/lib/python3.11/site-packages/kiwisolver/_cext.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
92
+ py311/lib/python3.11/site-packages/charset_normalizer/md__mypyc.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
93
+ py311/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
94
+ py311/lib/python3.11/site-packages/scikit_learn.libs/libgomp-e985bcbb.so.1.0.0 filter=lfs diff=lfs merge=lfs -text
95
+ py311/lib/python3.11/site-packages/yarl/_quoting_c.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
96
+ py311/lib/python3.11/site-packages/yaml/_yaml.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
97
+ py311/lib/python3.11/site-packages/scipy/_cyutility.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
98
+ py311/lib/python3.11/site-packages/sklearn/_isotonic.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
99
+ py311/lib/python3.11/site-packages/sklearn/_cyutility.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
100
+ py311/lib/python3.11/site-packages/aiohttp/_http_parser.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
101
+ py311/lib/python3.11/site-packages/aiohttp/_http_writer.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
102
+ py311/lib/python3.11/site-packages/PIL/_imagingft.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
103
+ py311/lib/python3.11/site-packages/PIL/_imaging.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
104
+ py311/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
105
+ py311/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
106
+ py311/lib/python3.11/site-packages/frozenlist/_frozenlist.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
107
+ py311/lib/python3.11/site-packages/pydantic_core/_pydantic_core.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
108
+ py311/lib/python3.11/site-packages/Levenshtein/levenshtein_cpp.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
109
+ py311/lib/python3.11/site-packages/pydantic_core/__pycache__/core_schema.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
110
+ py311/lib/python3.11/site-packages/grpc/_cython/cygrpc.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
111
+ py311/lib/python3.11/site-packages/attr/__pycache__/_make.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
112
+ py311/lib/python3.11/site-packages/fontTools/varLib/iup.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
113
+ py311/lib/python3.11/site-packages/fontTools/misc/bezierTools.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
114
+ py311/lib/python3.11/site-packages/fontTools/cu2qu/cu2qu.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
115
+ py311/lib/python3.11/site-packages/fontTools/qu2cu/qu2cu.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
116
+ py311/lib/python3.11/site-packages/botocore/__pycache__/utils.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
117
+ py311/lib/python3.11/site-packages/fontTools/pens/momentsPen.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
118
+ py311/lib/python3.11/site-packages/fontTools/feaLib/lexer.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
119
+ py311/lib/python3.11/site-packages/botocore/__pycache__/credentials.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
ccevolve/dataclaw_export.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53728b02ef81ee69bc72ff737e505f5bb6d13d327254cdc1f551a9548f35f3cb
3
+ size 10932298
docs/code_evolution_history_panes.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a90a7215e02977c250db579166772ca26bb481731c0e012a298e5fe598d78329
3
+ size 502256
docs/conceptual.png ADDED

Git LFS Details

  • SHA256: eac2c3bf9ec79108b87e650a4fb2adf7844e0c32d160ec4d2b6547df222b0f1e
  • Pointer size: 131 Bytes
  • Size of remote file: 717 kB
docs/webui.png ADDED

Git LFS Details

  • SHA256: 721c8b96c2bb1a84ebe59596783d407b0bbab324cd3758c954deccd575ef5ffc
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
my/ablation_study_summary.png ADDED

Git LFS Details

  • SHA256: e71ddc209a71804af23cee6ba1a2ea32dd0b665a7efb02e9dd6406253b94b578
  • Pointer size: 131 Bytes
  • Size of remote file: 991 kB
my/aux_7vs4_comparison.png ADDED

Git LFS Details

  • SHA256: a0b523cde167360ab0ed98961ccaf11e8d8f14928ce16bbd7168ee5cf090bfbf
  • Pointer size: 131 Bytes
  • Size of remote file: 164 kB
my/auxiliary_ablation_plots.png ADDED

Git LFS Details

  • SHA256: c6e5cbb58134035e68261f375baae5b99f81c58fd969e7ff6df20d74ef62c7ca
  • Pointer size: 131 Bytes
  • Size of remote file: 833 kB
my/auxiliary_metric_correlations.png ADDED

Git LFS Details

  • SHA256: 5f36ad8907366bde65a36dc210e23754270b26eb8427dd811f84122879d558e0
  • Pointer size: 131 Bytes
  • Size of remote file: 420 kB
my/correlation_evolution_over_time.png ADDED

Git LFS Details

  • SHA256: b45c32295a9d53dd50933cff5f6bba862d7dcf7449dd14ba63bbdabaac513d86
  • Pointer size: 131 Bytes
  • Size of remote file: 653 kB
my/correlation_explanation.png ADDED

Git LFS Details

  • SHA256: d1257097a9b9bc0d40556e98851bc4b7454c893c315a9a30aed319999cee01ed
  • Pointer size: 131 Bytes
  • Size of remote file: 544 kB
my/refined_aux_comparison.png ADDED

Git LFS Details

  • SHA256: eea9757ddd89b4106c9c53033846cd5536472d3d3bd467bbdd4703b2a97419e9
  • Pointer size: 131 Bytes
  • Size of remote file: 249 kB
py311/bin/python ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55e8f2734d0e882df2013e591fc2cd91fc31a4fc4d82dd0e78326af8fd3abdc1
3
+ size 23756024
py311/bin/python3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55e8f2734d0e882df2013e591fc2cd91fc31a4fc4d82dd0e78326af8fd3abdc1
3
+ size 23756024
py311/bin/python3.11 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55e8f2734d0e882df2013e591fc2cd91fc31a4fc4d82dd0e78326af8fd3abdc1
3
+ size 23756024
py311/bin/ty ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c50510e61c0d7bcaf2bb30cd9dca61be9b1b164321b97f0580138909d8bef954
3
+ size 22765536
py311/lib/python3.11/site-packages/30fcd23745efe32ce681__mypyc.cpython-311-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:816d8ee44739af6e291f26efa53b5a267dd666a092580cf3a4701e4ebffaea6c
3
+ size 4314072
py311/lib/python3.11/site-packages/Levenshtein/levenshtein_cpp.cpython-311-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:94f6dd7f87f4f8ee2b6754571ef3ec48634bba145496f446e0d074a072b77e5c
3
+ size 447280
py311/lib/python3.11/site-packages/PIL/_imaging.cpython-311-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a088c23d0d939dea916cba106792e4a00587a3bef93cb4030f872e704af15818
3
+ size 3361609
py311/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64506a7eeb0f21196c226d962b13d558db0700d5f0b94c875404e1d8e7a5b0f5
3
+ size 141369
py311/lib/python3.11/site-packages/PIL/_imagingft.cpython-311-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f1660966e539c84b02033fc88aa7186782dbe3400125d6bf7e50d3cf30941b93
3
+ size 306489
py311/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:210be7038a9a937f8ed3c43056f8163fa09bdd47e1b27606e68cb446200ee076
3
+ size 161744
py311/lib/python3.11/site-packages/__editable__.shinka-0.0.1.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d856a7c7cc50f7367291c07c65ff7fcc8438539cd5a4adc4204502ec56b3e1f4
3
+ size 83
py311/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:afd7c53f001f4d3cec0e922645b62a56888e3cec2fc480e1a1492af8bea3a6af
3
+ size 179469
py311/lib/python3.11/site-packages/_pytest/_code/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Python inspection/code generation API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .code import Code
6
+ from .code import ExceptionInfo
7
+ from .code import filter_traceback
8
+ from .code import Frame
9
+ from .code import getfslineno
10
+ from .code import Traceback
11
+ from .code import TracebackEntry
12
+ from .source import getrawcode
13
+ from .source import Source
14
+
15
+
16
+ __all__ = [
17
+ "Code",
18
+ "ExceptionInfo",
19
+ "Frame",
20
+ "Source",
21
+ "Traceback",
22
+ "TracebackEntry",
23
+ "filter_traceback",
24
+ "getfslineno",
25
+ "getrawcode",
26
+ ]
py311/lib/python3.11/site-packages/_pytest/_code/code.py ADDED
@@ -0,0 +1,1565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 inspect import CO_VARARGS
12
+ from inspect import CO_VARKEYWORDS
13
+ from io import StringIO
14
+ import os
15
+ from pathlib import Path
16
+ import re
17
+ import sys
18
+ from traceback import extract_tb
19
+ from traceback import format_exception
20
+ from traceback import format_exception_only
21
+ from traceback import FrameSummary
22
+ from types import CodeType
23
+ from types import FrameType
24
+ from types import TracebackType
25
+ from typing import Any
26
+ from typing import ClassVar
27
+ from typing import Final
28
+ from typing import final
29
+ from typing import Generic
30
+ from typing import Literal
31
+ from typing import overload
32
+ from typing import SupportsIndex
33
+ from typing import TypeAlias
34
+ from typing import TypeVar
35
+
36
+ import pluggy
37
+
38
+ import _pytest
39
+ from _pytest._code.source import findsource
40
+ from _pytest._code.source import getrawcode
41
+ from _pytest._code.source import getstatementrange_ast
42
+ from _pytest._code.source import Source
43
+ from _pytest._io import TerminalWriter
44
+ from _pytest._io.saferepr import safeformat
45
+ from _pytest._io.saferepr import saferepr
46
+ from _pytest.compat import get_real_func
47
+ from _pytest.deprecated import check_ispytest
48
+ from _pytest.pathlib import absolutepath
49
+ from _pytest.pathlib import bestrelpath
50
+
51
+
52
+ if sys.version_info < (3, 11):
53
+ from exceptiongroup import BaseExceptionGroup
54
+
55
+ TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
56
+
57
+ EXCEPTION_OR_MORE = type[BaseException] | tuple[type[BaseException], ...]
58
+
59
+
60
+ class Code:
61
+ """Wrapper around Python code objects."""
62
+
63
+ __slots__ = ("raw",)
64
+
65
+ def __init__(self, obj: CodeType) -> None:
66
+ self.raw = obj
67
+
68
+ @classmethod
69
+ def from_function(cls, obj: object) -> Code:
70
+ return cls(getrawcode(obj))
71
+
72
+ def __eq__(self, other):
73
+ return self.raw == other.raw
74
+
75
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
76
+ __hash__ = None # type: ignore
77
+
78
+ @property
79
+ def firstlineno(self) -> int:
80
+ return self.raw.co_firstlineno - 1
81
+
82
+ @property
83
+ def name(self) -> str:
84
+ return self.raw.co_name
85
+
86
+ @property
87
+ def path(self) -> Path | str:
88
+ """Return a path object pointing to source code, or an ``str`` in
89
+ case of ``OSError`` / non-existing file."""
90
+ if not self.raw.co_filename:
91
+ return ""
92
+ try:
93
+ p = absolutepath(self.raw.co_filename)
94
+ # maybe don't try this checking
95
+ if not p.exists():
96
+ raise OSError("path check failed.")
97
+ return p
98
+ except OSError:
99
+ # XXX maybe try harder like the weird logic
100
+ # in the standard lib [linecache.updatecache] does?
101
+ return self.raw.co_filename
102
+
103
+ @property
104
+ def fullsource(self) -> Source | None:
105
+ """Return a _pytest._code.Source object for the full source file of the code."""
106
+ full, _ = findsource(self.raw)
107
+ return full
108
+
109
+ def source(self) -> Source:
110
+ """Return a _pytest._code.Source object for the code object's source only."""
111
+ # return source only for that part of code
112
+ return Source(self.raw)
113
+
114
+ def getargs(self, var: bool = False) -> tuple[str, ...]:
115
+ """Return a tuple with the argument names for the code object.
116
+
117
+ If 'var' is set True also return the names of the variable and
118
+ keyword arguments when present.
119
+ """
120
+ # Handy shortcut for getting args.
121
+ raw = self.raw
122
+ argcount = raw.co_argcount
123
+ if var:
124
+ argcount += raw.co_flags & CO_VARARGS
125
+ argcount += raw.co_flags & CO_VARKEYWORDS
126
+ return raw.co_varnames[:argcount]
127
+
128
+
129
+ class Frame:
130
+ """Wrapper around a Python frame holding f_locals and f_globals
131
+ in which expressions can be evaluated."""
132
+
133
+ __slots__ = ("raw",)
134
+
135
+ def __init__(self, frame: FrameType) -> None:
136
+ self.raw = frame
137
+
138
+ @property
139
+ def lineno(self) -> int:
140
+ return self.raw.f_lineno - 1
141
+
142
+ @property
143
+ def f_globals(self) -> dict[str, Any]:
144
+ return self.raw.f_globals
145
+
146
+ @property
147
+ def f_locals(self) -> dict[str, Any]:
148
+ return self.raw.f_locals
149
+
150
+ @property
151
+ def code(self) -> Code:
152
+ return Code(self.raw.f_code)
153
+
154
+ @property
155
+ def statement(self) -> Source:
156
+ """Statement this frame is at."""
157
+ if self.code.fullsource is None:
158
+ return Source("")
159
+ return self.code.fullsource.getstatement(self.lineno)
160
+
161
+ def eval(self, code, **vars):
162
+ """Evaluate 'code' in the frame.
163
+
164
+ 'vars' are optional additional local variables.
165
+
166
+ Returns the result of the evaluation.
167
+ """
168
+ f_locals = self.f_locals.copy()
169
+ f_locals.update(vars)
170
+ return eval(code, self.f_globals, f_locals)
171
+
172
+ def repr(self, object: object) -> str:
173
+ """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
174
+ return saferepr(object)
175
+
176
+ def getargs(self, var: bool = False):
177
+ """Return a list of tuples (name, value) for all arguments.
178
+
179
+ If 'var' is set True, also include the variable and keyword arguments
180
+ when present.
181
+ """
182
+ retval = []
183
+ for arg in self.code.getargs(var):
184
+ try:
185
+ retval.append((arg, self.f_locals[arg]))
186
+ except KeyError:
187
+ pass # this can occur when using Psyco
188
+ return retval
189
+
190
+
191
+ class TracebackEntry:
192
+ """A single entry in a Traceback."""
193
+
194
+ __slots__ = ("_rawentry", "_repr_style")
195
+
196
+ def __init__(
197
+ self,
198
+ rawentry: TracebackType,
199
+ repr_style: Literal["short", "long"] | None = None,
200
+ ) -> None:
201
+ self._rawentry: Final = rawentry
202
+ self._repr_style: Final = repr_style
203
+
204
+ def with_repr_style(
205
+ self, repr_style: Literal["short", "long"] | None
206
+ ) -> TracebackEntry:
207
+ return TracebackEntry(self._rawentry, repr_style)
208
+
209
+ @property
210
+ def lineno(self) -> int:
211
+ return self._rawentry.tb_lineno - 1
212
+
213
+ def get_python_framesummary(self) -> FrameSummary:
214
+ # Python's built-in traceback module implements all the nitty gritty
215
+ # details to get column numbers of out frames.
216
+ stack_summary = extract_tb(self._rawentry, limit=1)
217
+ return stack_summary[0]
218
+
219
+ # Column and end line numbers introduced in python 3.11
220
+ if sys.version_info < (3, 11):
221
+
222
+ @property
223
+ def end_lineno_relative(self) -> int | None:
224
+ return None
225
+
226
+ @property
227
+ def colno(self) -> int | None:
228
+ return None
229
+
230
+ @property
231
+ def end_colno(self) -> int | None:
232
+ return None
233
+ else:
234
+
235
+ @property
236
+ def end_lineno_relative(self) -> int | None:
237
+ frame_summary = self.get_python_framesummary()
238
+ if frame_summary.end_lineno is None: # pragma: no cover
239
+ return None
240
+ return frame_summary.end_lineno - 1 - self.frame.code.firstlineno
241
+
242
+ @property
243
+ def colno(self) -> int | None:
244
+ """Starting byte offset of the expression in the traceback entry."""
245
+ return self.get_python_framesummary().colno
246
+
247
+ @property
248
+ def end_colno(self) -> int | None:
249
+ """Ending byte offset of the expression in the traceback entry."""
250
+ return self.get_python_framesummary().end_colno
251
+
252
+ @property
253
+ def frame(self) -> Frame:
254
+ return Frame(self._rawentry.tb_frame)
255
+
256
+ @property
257
+ def relline(self) -> int:
258
+ return self.lineno - self.frame.code.firstlineno
259
+
260
+ def __repr__(self) -> str:
261
+ return f"<TracebackEntry {self.frame.code.path}:{self.lineno + 1}>"
262
+
263
+ @property
264
+ def statement(self) -> Source:
265
+ """_pytest._code.Source object for the current statement."""
266
+ source = self.frame.code.fullsource
267
+ assert source is not None
268
+ return source.getstatement(self.lineno)
269
+
270
+ @property
271
+ def path(self) -> Path | str:
272
+ """Path to the source code."""
273
+ return self.frame.code.path
274
+
275
+ @property
276
+ def locals(self) -> dict[str, Any]:
277
+ """Locals of underlying frame."""
278
+ return self.frame.f_locals
279
+
280
+ def getfirstlinesource(self) -> int:
281
+ return self.frame.code.firstlineno
282
+
283
+ def getsource(
284
+ self, astcache: dict[str | Path, ast.AST] | None = None
285
+ ) -> Source | None:
286
+ """Return failing source code."""
287
+ # we use the passed in astcache to not reparse asttrees
288
+ # within exception info printing
289
+ source = self.frame.code.fullsource
290
+ if source is None:
291
+ return None
292
+ key = astnode = None
293
+ if astcache is not None:
294
+ key = self.frame.code.path
295
+ if key is not None:
296
+ astnode = astcache.get(key, None)
297
+ start = self.getfirstlinesource()
298
+ try:
299
+ astnode, _, end = getstatementrange_ast(
300
+ self.lineno, source, astnode=astnode
301
+ )
302
+ except SyntaxError:
303
+ end = self.lineno + 1
304
+ else:
305
+ if key is not None and astcache is not None:
306
+ astcache[key] = astnode
307
+ return source[start:end]
308
+
309
+ source = property(getsource)
310
+
311
+ def ishidden(self, excinfo: ExceptionInfo[BaseException] | None) -> bool:
312
+ """Return True if the current frame has a var __tracebackhide__
313
+ resolving to True.
314
+
315
+ If __tracebackhide__ is a callable, it gets called with the
316
+ ExceptionInfo instance and can decide whether to hide the traceback.
317
+
318
+ Mostly for internal use.
319
+ """
320
+ tbh: bool | Callable[[ExceptionInfo[BaseException] | None], bool] = False
321
+ for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
322
+ # in normal cases, f_locals and f_globals are dictionaries
323
+ # however via `exec(...)` / `eval(...)` they can be other types
324
+ # (even incorrect types!).
325
+ # as such, we suppress all exceptions while accessing __tracebackhide__
326
+ try:
327
+ tbh = maybe_ns_dct["__tracebackhide__"]
328
+ except Exception:
329
+ pass
330
+ else:
331
+ break
332
+ if tbh and callable(tbh):
333
+ return tbh(excinfo)
334
+ return tbh
335
+
336
+ def __str__(self) -> str:
337
+ name = self.frame.code.name
338
+ try:
339
+ line = str(self.statement).lstrip()
340
+ except KeyboardInterrupt:
341
+ raise
342
+ except BaseException:
343
+ line = "???"
344
+ # This output does not quite match Python's repr for traceback entries,
345
+ # but changing it to do so would break certain plugins. See
346
+ # https://github.com/pytest-dev/pytest/pull/7535/ for details.
347
+ return f" File '{self.path}':{self.lineno + 1} in {name}\n {line}\n"
348
+
349
+ @property
350
+ def name(self) -> str:
351
+ """co_name of underlying code."""
352
+ return self.frame.code.raw.co_name
353
+
354
+
355
+ class Traceback(list[TracebackEntry]):
356
+ """Traceback objects encapsulate and offer higher level access to Traceback entries."""
357
+
358
+ def __init__(
359
+ self,
360
+ tb: TracebackType | Iterable[TracebackEntry],
361
+ ) -> None:
362
+ """Initialize from given python traceback object and ExceptionInfo."""
363
+ if isinstance(tb, TracebackType):
364
+
365
+ def f(cur: TracebackType) -> Iterable[TracebackEntry]:
366
+ cur_: TracebackType | None = cur
367
+ while cur_ is not None:
368
+ yield TracebackEntry(cur_)
369
+ cur_ = cur_.tb_next
370
+
371
+ super().__init__(f(tb))
372
+ else:
373
+ super().__init__(tb)
374
+
375
+ def cut(
376
+ self,
377
+ path: os.PathLike[str] | str | None = None,
378
+ lineno: int | None = None,
379
+ firstlineno: int | None = None,
380
+ excludepath: os.PathLike[str] | None = None,
381
+ ) -> Traceback:
382
+ """Return a Traceback instance wrapping part of this Traceback.
383
+
384
+ By providing any combination of path, lineno and firstlineno, the
385
+ first frame to start the to-be-returned traceback is determined.
386
+
387
+ This allows cutting the first part of a Traceback instance e.g.
388
+ for formatting reasons (removing some uninteresting bits that deal
389
+ with handling of the exception/traceback).
390
+ """
391
+ path_ = None if path is None else os.fspath(path)
392
+ excludepath_ = None if excludepath is None else os.fspath(excludepath)
393
+ for x in self:
394
+ code = x.frame.code
395
+ codepath = code.path
396
+ if path is not None and str(codepath) != path_:
397
+ continue
398
+ if (
399
+ excludepath is not None
400
+ and isinstance(codepath, Path)
401
+ and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
402
+ ):
403
+ continue
404
+ if lineno is not None and x.lineno != lineno:
405
+ continue
406
+ if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
407
+ continue
408
+ return Traceback(x._rawentry)
409
+ return self
410
+
411
+ @overload
412
+ def __getitem__(self, key: SupportsIndex) -> TracebackEntry: ...
413
+
414
+ @overload
415
+ def __getitem__(self, key: slice) -> Traceback: ...
416
+
417
+ def __getitem__(self, key: SupportsIndex | slice) -> TracebackEntry | Traceback:
418
+ if isinstance(key, slice):
419
+ return self.__class__(super().__getitem__(key))
420
+ else:
421
+ return super().__getitem__(key)
422
+
423
+ def filter(
424
+ self,
425
+ excinfo_or_fn: ExceptionInfo[BaseException] | Callable[[TracebackEntry], bool],
426
+ /,
427
+ ) -> Traceback:
428
+ """Return a Traceback instance with certain items removed.
429
+
430
+ If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
431
+ which are hidden (see ishidden() above).
432
+
433
+ Otherwise, the filter is a function that gets a single argument, a
434
+ ``TracebackEntry`` instance, and should return True when the item should
435
+ be added to the ``Traceback``, False when not.
436
+ """
437
+ if isinstance(excinfo_or_fn, ExceptionInfo):
438
+ fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731
439
+ else:
440
+ fn = excinfo_or_fn
441
+ return Traceback(filter(fn, self))
442
+
443
+ def recursionindex(self) -> int | None:
444
+ """Return the index of the frame/TracebackEntry where recursion originates if
445
+ appropriate, None if no recursion occurred."""
446
+ cache: dict[tuple[Any, int, int], list[dict[str, Any]]] = {}
447
+ for i, entry in enumerate(self):
448
+ # id for the code.raw is needed to work around
449
+ # the strange metaprogramming in the decorator lib from pypi
450
+ # which generates code objects that have hash/value equality
451
+ # XXX needs a test
452
+ key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
453
+ values = cache.setdefault(key, [])
454
+ # Since Python 3.13 f_locals is a proxy, freeze it.
455
+ loc = dict(entry.frame.f_locals)
456
+ if values:
457
+ for otherloc in values:
458
+ if otherloc == loc:
459
+ return i
460
+ values.append(loc)
461
+ return None
462
+
463
+
464
+ def stringify_exception(
465
+ exc: BaseException, include_subexception_msg: bool = True
466
+ ) -> str:
467
+ try:
468
+ notes = getattr(exc, "__notes__", [])
469
+ except KeyError:
470
+ # Workaround for https://github.com/python/cpython/issues/98778 on
471
+ # some 3.10 and 3.11 patch versions.
472
+ HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
473
+ if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
474
+ notes = []
475
+ else: # pragma: no cover
476
+ # exception not related to above bug, reraise
477
+ raise
478
+ if not include_subexception_msg and isinstance(exc, BaseExceptionGroup):
479
+ message = exc.message
480
+ else:
481
+ message = str(exc)
482
+
483
+ return "\n".join(
484
+ [
485
+ message,
486
+ *notes,
487
+ ]
488
+ )
489
+
490
+
491
+ E = TypeVar("E", bound=BaseException, covariant=True)
492
+
493
+
494
+ @final
495
+ @dataclasses.dataclass
496
+ class ExceptionInfo(Generic[E]):
497
+ """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
498
+
499
+ _assert_start_repr: ClassVar = "AssertionError('assert "
500
+
501
+ _excinfo: tuple[type[E], E, TracebackType] | None
502
+ _striptext: str
503
+ _traceback: Traceback | None
504
+
505
+ def __init__(
506
+ self,
507
+ excinfo: tuple[type[E], E, TracebackType] | None,
508
+ striptext: str = "",
509
+ traceback: Traceback | None = None,
510
+ *,
511
+ _ispytest: bool = False,
512
+ ) -> None:
513
+ check_ispytest(_ispytest)
514
+ self._excinfo = excinfo
515
+ self._striptext = striptext
516
+ self._traceback = traceback
517
+
518
+ @classmethod
519
+ def from_exception(
520
+ cls,
521
+ # Ignoring error: "Cannot use a covariant type variable as a parameter".
522
+ # This is OK to ignore because this class is (conceptually) readonly.
523
+ # See https://github.com/python/mypy/issues/7049.
524
+ exception: E, # type: ignore[misc]
525
+ exprinfo: str | None = None,
526
+ ) -> ExceptionInfo[E]:
527
+ """Return an ExceptionInfo for an existing exception.
528
+
529
+ The exception must have a non-``None`` ``__traceback__`` attribute,
530
+ otherwise this function fails with an assertion error. This means that
531
+ the exception must have been raised, or added a traceback with the
532
+ :py:meth:`~BaseException.with_traceback()` method.
533
+
534
+ :param exprinfo:
535
+ A text string helping to determine if we should strip
536
+ ``AssertionError`` from the output. Defaults to the exception
537
+ message/``__str__()``.
538
+
539
+ .. versionadded:: 7.4
540
+ """
541
+ assert exception.__traceback__, (
542
+ "Exceptions passed to ExcInfo.from_exception(...)"
543
+ " must have a non-None __traceback__."
544
+ )
545
+ exc_info = (type(exception), exception, exception.__traceback__)
546
+ return cls.from_exc_info(exc_info, exprinfo)
547
+
548
+ @classmethod
549
+ def from_exc_info(
550
+ cls,
551
+ exc_info: tuple[type[E], E, TracebackType],
552
+ exprinfo: str | None = None,
553
+ ) -> ExceptionInfo[E]:
554
+ """Like :func:`from_exception`, but using old-style exc_info tuple."""
555
+ _striptext = ""
556
+ if exprinfo is None and isinstance(exc_info[1], AssertionError):
557
+ exprinfo = getattr(exc_info[1], "msg", None)
558
+ if exprinfo is None:
559
+ exprinfo = saferepr(exc_info[1])
560
+ if exprinfo and exprinfo.startswith(cls._assert_start_repr):
561
+ _striptext = "AssertionError: "
562
+
563
+ return cls(exc_info, _striptext, _ispytest=True)
564
+
565
+ @classmethod
566
+ def from_current(cls, exprinfo: str | None = None) -> ExceptionInfo[BaseException]:
567
+ """Return an ExceptionInfo matching the current traceback.
568
+
569
+ .. warning::
570
+
571
+ Experimental API
572
+
573
+ :param exprinfo:
574
+ A text string helping to determine if we should strip
575
+ ``AssertionError`` from the output. Defaults to the exception
576
+ message/``__str__()``.
577
+ """
578
+ tup = sys.exc_info()
579
+ assert tup[0] is not None, "no current exception"
580
+ assert tup[1] is not None, "no current exception"
581
+ assert tup[2] is not None, "no current exception"
582
+ exc_info = (tup[0], tup[1], tup[2])
583
+ return ExceptionInfo.from_exc_info(exc_info, exprinfo)
584
+
585
+ @classmethod
586
+ def for_later(cls) -> ExceptionInfo[E]:
587
+ """Return an unfilled ExceptionInfo."""
588
+ return cls(None, _ispytest=True)
589
+
590
+ def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None:
591
+ """Fill an unfilled ExceptionInfo created with ``for_later()``."""
592
+ assert self._excinfo is None, "ExceptionInfo was already filled"
593
+ self._excinfo = exc_info
594
+
595
+ @property
596
+ def type(self) -> type[E]:
597
+ """The exception class."""
598
+ assert self._excinfo is not None, (
599
+ ".type can only be used after the context manager exits"
600
+ )
601
+ return self._excinfo[0]
602
+
603
+ @property
604
+ def value(self) -> E:
605
+ """The exception value."""
606
+ assert self._excinfo is not None, (
607
+ ".value can only be used after the context manager exits"
608
+ )
609
+ return self._excinfo[1]
610
+
611
+ @property
612
+ def tb(self) -> TracebackType:
613
+ """The exception raw traceback."""
614
+ assert self._excinfo is not None, (
615
+ ".tb can only be used after the context manager exits"
616
+ )
617
+ return self._excinfo[2]
618
+
619
+ @property
620
+ def typename(self) -> str:
621
+ """The type name of the exception."""
622
+ assert self._excinfo is not None, (
623
+ ".typename can only be used after the context manager exits"
624
+ )
625
+ return self.type.__name__
626
+
627
+ @property
628
+ def traceback(self) -> Traceback:
629
+ """The traceback."""
630
+ if self._traceback is None:
631
+ self._traceback = Traceback(self.tb)
632
+ return self._traceback
633
+
634
+ @traceback.setter
635
+ def traceback(self, value: Traceback) -> None:
636
+ self._traceback = value
637
+
638
+ def __repr__(self) -> str:
639
+ if self._excinfo is None:
640
+ return "<ExceptionInfo for raises contextmanager>"
641
+ return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
642
+
643
+ def exconly(self, tryshort: bool = False) -> str:
644
+ """Return the exception as a string.
645
+
646
+ When 'tryshort' resolves to True, and the exception is an
647
+ AssertionError, only the actual exception part of the exception
648
+ representation is returned (so 'AssertionError: ' is removed from
649
+ the beginning).
650
+ """
651
+
652
+ def _get_single_subexc(
653
+ eg: BaseExceptionGroup[BaseException],
654
+ ) -> BaseException | None:
655
+ if len(eg.exceptions) != 1:
656
+ return None
657
+ if isinstance(e := eg.exceptions[0], BaseExceptionGroup):
658
+ return _get_single_subexc(e)
659
+ return e
660
+
661
+ if (
662
+ tryshort
663
+ and isinstance(self.value, BaseExceptionGroup)
664
+ and (subexc := _get_single_subexc(self.value)) is not None
665
+ ):
666
+ return f"{subexc!r} [single exception in {type(self.value).__name__}]"
667
+
668
+ lines = format_exception_only(self.type, self.value)
669
+ text = "".join(lines)
670
+ text = text.rstrip()
671
+ if tryshort:
672
+ if text.startswith(self._striptext):
673
+ text = text[len(self._striptext) :]
674
+ return text
675
+
676
+ def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool:
677
+ """Return True if the exception is an instance of exc.
678
+
679
+ Consider using ``isinstance(excinfo.value, exc)`` instead.
680
+ """
681
+ return isinstance(self.value, exc)
682
+
683
+ def _getreprcrash(self) -> ReprFileLocation | None:
684
+ # Find last non-hidden traceback entry that led to the exception of the
685
+ # traceback, or None if all hidden.
686
+ for i in range(-1, -len(self.traceback) - 1, -1):
687
+ entry = self.traceback[i]
688
+ if not entry.ishidden(self):
689
+ path, lineno = entry.frame.code.raw.co_filename, entry.lineno
690
+ exconly = self.exconly(tryshort=True)
691
+ return ReprFileLocation(path, lineno + 1, exconly)
692
+ return None
693
+
694
+ def getrepr(
695
+ self,
696
+ showlocals: bool = False,
697
+ style: TracebackStyle = "long",
698
+ abspath: bool = False,
699
+ tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True,
700
+ funcargs: bool = False,
701
+ truncate_locals: bool = True,
702
+ truncate_args: bool = True,
703
+ chain: bool = True,
704
+ ) -> ReprExceptionInfo | ExceptionChainRepr:
705
+ """Return str()able representation of this exception info.
706
+
707
+ :param bool showlocals:
708
+ Show locals per traceback entry.
709
+ Ignored if ``style=="native"``.
710
+
711
+ :param str style:
712
+ long|short|line|no|native|value traceback style.
713
+
714
+ :param bool abspath:
715
+ If paths should be changed to absolute or left unchanged.
716
+
717
+ :param tbfilter:
718
+ A filter for traceback entries.
719
+
720
+ * If false, don't hide any entries.
721
+ * If true, hide internal entries and entries that contain a local
722
+ variable ``__tracebackhide__ = True``.
723
+ * If a callable, delegates the filtering to the callable.
724
+
725
+ Ignored if ``style`` is ``"native"``.
726
+
727
+ :param bool funcargs:
728
+ Show fixtures ("funcargs" for legacy purposes) per traceback entry.
729
+
730
+ :param bool truncate_locals:
731
+ With ``showlocals==True``, make sure locals can be safely represented as strings.
732
+
733
+ :param bool truncate_args:
734
+ With ``showargs==True``, make sure args can be safely represented as strings.
735
+
736
+ :param bool chain:
737
+ If chained exceptions in Python 3 should be shown.
738
+
739
+ .. versionchanged:: 3.9
740
+
741
+ Added the ``chain`` parameter.
742
+ """
743
+ if style == "native":
744
+ return ReprExceptionInfo(
745
+ reprtraceback=ReprTracebackNative(
746
+ format_exception(
747
+ self.type,
748
+ self.value,
749
+ self.traceback[0]._rawentry if self.traceback else None,
750
+ )
751
+ ),
752
+ reprcrash=self._getreprcrash(),
753
+ )
754
+
755
+ fmt = FormattedExcinfo(
756
+ showlocals=showlocals,
757
+ style=style,
758
+ abspath=abspath,
759
+ tbfilter=tbfilter,
760
+ funcargs=funcargs,
761
+ truncate_locals=truncate_locals,
762
+ truncate_args=truncate_args,
763
+ chain=chain,
764
+ )
765
+ return fmt.repr_excinfo(self)
766
+
767
+ def match(self, regexp: str | re.Pattern[str]) -> Literal[True]:
768
+ """Check whether the regular expression `regexp` matches the string
769
+ representation of the exception using :func:`python:re.search`.
770
+
771
+ If it matches `True` is returned, otherwise an `AssertionError` is raised.
772
+ """
773
+ __tracebackhide__ = True
774
+ value = stringify_exception(self.value)
775
+ msg = (
776
+ f"Regex pattern did not match.\n"
777
+ f" Expected regex: {regexp!r}\n"
778
+ f" Actual message: {value!r}"
779
+ )
780
+ if regexp == value:
781
+ msg += "\n Did you mean to `re.escape()` the regex?"
782
+ assert re.search(regexp, value), msg
783
+ # Return True to allow for "assert excinfo.match()".
784
+ return True
785
+
786
+ def _group_contains(
787
+ self,
788
+ exc_group: BaseExceptionGroup[BaseException],
789
+ expected_exception: EXCEPTION_OR_MORE,
790
+ match: str | re.Pattern[str] | None,
791
+ target_depth: int | None = None,
792
+ current_depth: int = 1,
793
+ ) -> bool:
794
+ """Return `True` if a `BaseExceptionGroup` contains a matching exception."""
795
+ if (target_depth is not None) and (current_depth > target_depth):
796
+ # already descended past the target depth
797
+ return False
798
+ for exc in exc_group.exceptions:
799
+ if isinstance(exc, BaseExceptionGroup):
800
+ if self._group_contains(
801
+ exc, expected_exception, match, target_depth, current_depth + 1
802
+ ):
803
+ return True
804
+ if (target_depth is not None) and (current_depth != target_depth):
805
+ # not at the target depth, no match
806
+ continue
807
+ if not isinstance(exc, expected_exception):
808
+ continue
809
+ if match is not None:
810
+ value = stringify_exception(exc)
811
+ if not re.search(match, value):
812
+ continue
813
+ return True
814
+ return False
815
+
816
+ def group_contains(
817
+ self,
818
+ expected_exception: EXCEPTION_OR_MORE,
819
+ *,
820
+ match: str | re.Pattern[str] | None = None,
821
+ depth: int | None = None,
822
+ ) -> bool:
823
+ """Check whether a captured exception group contains a matching exception.
824
+
825
+ :param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
826
+ The expected exception type, or a tuple if one of multiple possible
827
+ exception types are expected.
828
+
829
+ :param str | re.Pattern[str] | None match:
830
+ If specified, a string containing a regular expression,
831
+ or a regular expression object, that is tested against the string
832
+ representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
833
+ using :func:`re.search`.
834
+
835
+ To match a literal string that may contain :ref:`special characters
836
+ <re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
837
+
838
+ :param Optional[int] depth:
839
+ If `None`, will search for a matching exception at any nesting depth.
840
+ If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
841
+ the exceptions contained within the topmost exception group).
842
+
843
+ .. versionadded:: 8.0
844
+
845
+ .. warning::
846
+ This helper makes it easy to check for the presence of specific exceptions,
847
+ but it is very bad for checking that the group does *not* contain
848
+ *any other exceptions*.
849
+ You should instead consider using :class:`pytest.RaisesGroup`
850
+
851
+ """
852
+ msg = "Captured exception is not an instance of `BaseExceptionGroup`"
853
+ assert isinstance(self.value, BaseExceptionGroup), msg
854
+ msg = "`depth` must be >= 1 if specified"
855
+ assert (depth is None) or (depth >= 1), msg
856
+ return self._group_contains(self.value, expected_exception, match, depth)
857
+
858
+
859
+ # Type alias for the `tbfilter` setting:
860
+ # bool: If True, it should be filtered using Traceback.filter()
861
+ # callable: A callable that takes an ExceptionInfo and returns the filtered traceback.
862
+ TracebackFilter: TypeAlias = bool | Callable[[ExceptionInfo[BaseException]], Traceback]
863
+
864
+
865
+ @dataclasses.dataclass
866
+ class FormattedExcinfo:
867
+ """Presenting information about failing Functions and Generators."""
868
+
869
+ # for traceback entries
870
+ flow_marker: ClassVar = ">"
871
+ fail_marker: ClassVar = "E"
872
+
873
+ showlocals: bool = False
874
+ style: TracebackStyle = "long"
875
+ abspath: bool = True
876
+ tbfilter: TracebackFilter = True
877
+ funcargs: bool = False
878
+ truncate_locals: bool = True
879
+ truncate_args: bool = True
880
+ chain: bool = True
881
+ astcache: dict[str | Path, ast.AST] = dataclasses.field(
882
+ default_factory=dict, init=False, repr=False
883
+ )
884
+
885
+ def _getindent(self, source: Source) -> int:
886
+ # Figure out indent for the given source.
887
+ try:
888
+ s = str(source.getstatement(len(source) - 1))
889
+ except KeyboardInterrupt:
890
+ raise
891
+ except BaseException:
892
+ try:
893
+ s = str(source[-1])
894
+ except KeyboardInterrupt:
895
+ raise
896
+ except BaseException:
897
+ return 0
898
+ return 4 + (len(s) - len(s.lstrip()))
899
+
900
+ def _getentrysource(self, entry: TracebackEntry) -> Source | None:
901
+ source = entry.getsource(self.astcache)
902
+ if source is not None:
903
+ source = source.deindent()
904
+ return source
905
+
906
+ def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None:
907
+ if self.funcargs:
908
+ args = []
909
+ for argname, argvalue in entry.frame.getargs(var=True):
910
+ if self.truncate_args:
911
+ str_repr = saferepr(argvalue)
912
+ else:
913
+ str_repr = saferepr(argvalue, maxsize=None)
914
+ args.append((argname, str_repr))
915
+ return ReprFuncArgs(args)
916
+ return None
917
+
918
+ def get_source(
919
+ self,
920
+ source: Source | None,
921
+ line_index: int = -1,
922
+ excinfo: ExceptionInfo[BaseException] | None = None,
923
+ short: bool = False,
924
+ end_line_index: int | None = None,
925
+ colno: int | None = None,
926
+ end_colno: int | None = None,
927
+ ) -> list[str]:
928
+ """Return formatted and marked up source lines."""
929
+ lines = []
930
+ if source is not None and line_index < 0:
931
+ line_index += len(source)
932
+ if source is None or line_index >= len(source.lines) or line_index < 0:
933
+ # `line_index` could still be outside `range(len(source.lines))` if
934
+ # we're processing AST with pathological position attributes.
935
+ source = Source("???")
936
+ line_index = 0
937
+ space_prefix = " "
938
+ if short:
939
+ lines.append(space_prefix + source.lines[line_index].strip())
940
+ lines.extend(
941
+ self.get_highlight_arrows_for_line(
942
+ raw_line=source.raw_lines[line_index],
943
+ line=source.lines[line_index].strip(),
944
+ lineno=line_index,
945
+ end_lineno=end_line_index,
946
+ colno=colno,
947
+ end_colno=end_colno,
948
+ )
949
+ )
950
+ else:
951
+ for line in source.lines[:line_index]:
952
+ lines.append(space_prefix + line)
953
+ lines.append(self.flow_marker + " " + source.lines[line_index])
954
+ lines.extend(
955
+ self.get_highlight_arrows_for_line(
956
+ raw_line=source.raw_lines[line_index],
957
+ line=source.lines[line_index],
958
+ lineno=line_index,
959
+ end_lineno=end_line_index,
960
+ colno=colno,
961
+ end_colno=end_colno,
962
+ )
963
+ )
964
+ for line in source.lines[line_index + 1 :]:
965
+ lines.append(space_prefix + line)
966
+ if excinfo is not None:
967
+ indent = 4 if short else self._getindent(source)
968
+ lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
969
+ return lines
970
+
971
+ def get_highlight_arrows_for_line(
972
+ self,
973
+ line: str,
974
+ raw_line: str,
975
+ lineno: int | None,
976
+ end_lineno: int | None,
977
+ colno: int | None,
978
+ end_colno: int | None,
979
+ ) -> list[str]:
980
+ """Return characters highlighting a source line.
981
+
982
+ Example with colno and end_colno pointing to the bar expression:
983
+ "foo() + bar()"
984
+ returns " ^^^^^"
985
+ """
986
+ if lineno != end_lineno:
987
+ # Don't handle expressions that span multiple lines.
988
+ return []
989
+ if colno is None or end_colno is None:
990
+ # Can't do anything without column information.
991
+ return []
992
+
993
+ num_stripped_chars = len(raw_line) - len(line)
994
+
995
+ start_char_offset = _byte_offset_to_character_offset(raw_line, colno)
996
+ end_char_offset = _byte_offset_to_character_offset(raw_line, end_colno)
997
+ num_carets = end_char_offset - start_char_offset
998
+ # If the highlight would span the whole line, it is redundant, don't
999
+ # show it.
1000
+ if num_carets >= len(line.strip()):
1001
+ return []
1002
+
1003
+ highlights = " "
1004
+ highlights += " " * (start_char_offset - num_stripped_chars + 1)
1005
+ highlights += "^" * num_carets
1006
+ return [highlights]
1007
+
1008
+ def get_exconly(
1009
+ self,
1010
+ excinfo: ExceptionInfo[BaseException],
1011
+ indent: int = 4,
1012
+ markall: bool = False,
1013
+ ) -> list[str]:
1014
+ lines = []
1015
+ indentstr = " " * indent
1016
+ # Get the real exception information out.
1017
+ exlines = excinfo.exconly(tryshort=True).split("\n")
1018
+ failindent = self.fail_marker + indentstr[1:]
1019
+ for line in exlines:
1020
+ lines.append(failindent + line)
1021
+ if not markall:
1022
+ failindent = indentstr
1023
+ return lines
1024
+
1025
+ def repr_locals(self, locals: Mapping[str, object]) -> ReprLocals | None:
1026
+ if self.showlocals:
1027
+ lines = []
1028
+ keys = [loc for loc in locals if loc[0] != "@"]
1029
+ keys.sort()
1030
+ for name in keys:
1031
+ value = locals[name]
1032
+ if name == "__builtins__":
1033
+ lines.append("__builtins__ = <builtins>")
1034
+ else:
1035
+ # This formatting could all be handled by the
1036
+ # _repr() function, which is only reprlib.Repr in
1037
+ # disguise, so is very configurable.
1038
+ if self.truncate_locals:
1039
+ str_repr = saferepr(value)
1040
+ else:
1041
+ str_repr = safeformat(value)
1042
+ # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
1043
+ lines.append(f"{name:<10} = {str_repr}")
1044
+ # else:
1045
+ # self._line("%-10s =\\" % (name,))
1046
+ # # XXX
1047
+ # pprint.pprint(value, stream=self.excinfowriter)
1048
+ return ReprLocals(lines)
1049
+ return None
1050
+
1051
+ def repr_traceback_entry(
1052
+ self,
1053
+ entry: TracebackEntry | None,
1054
+ excinfo: ExceptionInfo[BaseException] | None = None,
1055
+ ) -> ReprEntry:
1056
+ lines: list[str] = []
1057
+ style = (
1058
+ entry._repr_style
1059
+ if entry is not None and entry._repr_style is not None
1060
+ else self.style
1061
+ )
1062
+ if style in ("short", "long") and entry is not None:
1063
+ source = self._getentrysource(entry)
1064
+ if source is None:
1065
+ source = Source("???")
1066
+ line_index = 0
1067
+ end_line_index, colno, end_colno = None, None, None
1068
+ else:
1069
+ line_index = entry.relline
1070
+ end_line_index = entry.end_lineno_relative
1071
+ colno = entry.colno
1072
+ end_colno = entry.end_colno
1073
+ short = style == "short"
1074
+ reprargs = self.repr_args(entry) if not short else None
1075
+ s = self.get_source(
1076
+ source=source,
1077
+ line_index=line_index,
1078
+ excinfo=excinfo,
1079
+ short=short,
1080
+ end_line_index=end_line_index,
1081
+ colno=colno,
1082
+ end_colno=end_colno,
1083
+ )
1084
+ lines.extend(s)
1085
+ if short:
1086
+ message = f"in {entry.name}"
1087
+ else:
1088
+ message = (excinfo and excinfo.typename) or ""
1089
+ entry_path = entry.path
1090
+ path = self._makepath(entry_path)
1091
+ reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
1092
+ localsrepr = self.repr_locals(entry.locals)
1093
+ return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
1094
+ elif style == "value":
1095
+ if excinfo:
1096
+ lines.extend(str(excinfo.value).split("\n"))
1097
+ return ReprEntry(lines, None, None, None, style)
1098
+ else:
1099
+ if excinfo:
1100
+ lines.extend(self.get_exconly(excinfo, indent=4))
1101
+ return ReprEntry(lines, None, None, None, style)
1102
+
1103
+ def _makepath(self, path: Path | str) -> str:
1104
+ if not self.abspath and isinstance(path, Path):
1105
+ try:
1106
+ np = bestrelpath(Path.cwd(), path)
1107
+ except OSError:
1108
+ return str(path)
1109
+ if len(np) < len(str(path)):
1110
+ return np
1111
+ return str(path)
1112
+
1113
+ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
1114
+ traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
1115
+
1116
+ if isinstance(excinfo.value, RecursionError):
1117
+ traceback, extraline = self._truncate_recursive_traceback(traceback)
1118
+ else:
1119
+ extraline = None
1120
+
1121
+ if not traceback:
1122
+ if extraline is None:
1123
+ extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
1124
+ entries = [self.repr_traceback_entry(None, excinfo)]
1125
+ return ReprTraceback(entries, extraline, style=self.style)
1126
+
1127
+ last = traceback[-1]
1128
+ if self.style == "value":
1129
+ entries = [self.repr_traceback_entry(last, excinfo)]
1130
+ return ReprTraceback(entries, None, style=self.style)
1131
+
1132
+ entries = [
1133
+ self.repr_traceback_entry(entry, excinfo if last == entry else None)
1134
+ for entry in traceback
1135
+ ]
1136
+ return ReprTraceback(entries, extraline, style=self.style)
1137
+
1138
+ def _truncate_recursive_traceback(
1139
+ self, traceback: Traceback
1140
+ ) -> tuple[Traceback, str | None]:
1141
+ """Truncate the given recursive traceback trying to find the starting
1142
+ point of the recursion.
1143
+
1144
+ The detection is done by going through each traceback entry and
1145
+ finding the point in which the locals of the frame are equal to the
1146
+ locals of a previous frame (see ``recursionindex()``).
1147
+
1148
+ Handle the situation where the recursion process might raise an
1149
+ exception (for example comparing numpy arrays using equality raises a
1150
+ TypeError), in which case we do our best to warn the user of the
1151
+ error and show a limited traceback.
1152
+ """
1153
+ try:
1154
+ recursionindex = traceback.recursionindex()
1155
+ except Exception as e:
1156
+ max_frames = 10
1157
+ extraline: str | None = (
1158
+ "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
1159
+ " The following exception happened when comparing locals in the stack frame:\n"
1160
+ f" {type(e).__name__}: {e!s}\n"
1161
+ f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
1162
+ )
1163
+ # Type ignored because adding two instances of a List subtype
1164
+ # currently incorrectly has type List instead of the subtype.
1165
+ traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
1166
+ else:
1167
+ if recursionindex is not None:
1168
+ extraline = "!!! Recursion detected (same locals & position)"
1169
+ traceback = traceback[: recursionindex + 1]
1170
+ else:
1171
+ extraline = None
1172
+
1173
+ return traceback, extraline
1174
+
1175
+ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainRepr:
1176
+ repr_chain: list[tuple[ReprTraceback, ReprFileLocation | None, str | None]] = []
1177
+ e: BaseException | None = excinfo.value
1178
+ excinfo_: ExceptionInfo[BaseException] | None = excinfo
1179
+ descr = None
1180
+ seen: set[int] = set()
1181
+ while e is not None and id(e) not in seen:
1182
+ seen.add(id(e))
1183
+
1184
+ if excinfo_:
1185
+ # Fall back to native traceback as a temporary workaround until
1186
+ # full support for exception groups added to ExceptionInfo.
1187
+ # See https://github.com/pytest-dev/pytest/issues/9159
1188
+ reprtraceback: ReprTraceback | ReprTracebackNative
1189
+ if isinstance(e, BaseExceptionGroup):
1190
+ # don't filter any sub-exceptions since they shouldn't have any internal frames
1191
+ traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
1192
+ reprtraceback = ReprTracebackNative(
1193
+ format_exception(
1194
+ type(excinfo.value),
1195
+ excinfo.value,
1196
+ traceback[0]._rawentry,
1197
+ )
1198
+ )
1199
+ else:
1200
+ reprtraceback = self.repr_traceback(excinfo_)
1201
+ reprcrash = excinfo_._getreprcrash()
1202
+ else:
1203
+ # Fallback to native repr if the exception doesn't have a traceback:
1204
+ # ExceptionInfo objects require a full traceback to work.
1205
+ reprtraceback = ReprTracebackNative(format_exception(type(e), e, None))
1206
+ reprcrash = None
1207
+ repr_chain += [(reprtraceback, reprcrash, descr)]
1208
+
1209
+ if e.__cause__ is not None and self.chain:
1210
+ e = e.__cause__
1211
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
1212
+ descr = "The above exception was the direct cause of the following exception:"
1213
+ elif (
1214
+ e.__context__ is not None and not e.__suppress_context__ and self.chain
1215
+ ):
1216
+ e = e.__context__
1217
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
1218
+ descr = "During handling of the above exception, another exception occurred:"
1219
+ else:
1220
+ e = None
1221
+ repr_chain.reverse()
1222
+ return ExceptionChainRepr(repr_chain)
1223
+
1224
+
1225
+ @dataclasses.dataclass(eq=False)
1226
+ class TerminalRepr:
1227
+ def __str__(self) -> str:
1228
+ # FYI this is called from pytest-xdist's serialization of exception
1229
+ # information.
1230
+ io = StringIO()
1231
+ tw = TerminalWriter(file=io)
1232
+ self.toterminal(tw)
1233
+ return io.getvalue().strip()
1234
+
1235
+ def __repr__(self) -> str:
1236
+ return f"<{self.__class__} instance at {id(self):0x}>"
1237
+
1238
+ def toterminal(self, tw: TerminalWriter) -> None:
1239
+ raise NotImplementedError()
1240
+
1241
+
1242
+ # This class is abstract -- only subclasses are instantiated.
1243
+ @dataclasses.dataclass(eq=False)
1244
+ class ExceptionRepr(TerminalRepr):
1245
+ # Provided by subclasses.
1246
+ reprtraceback: ReprTraceback
1247
+ reprcrash: ReprFileLocation | None
1248
+ sections: list[tuple[str, str, str]] = dataclasses.field(
1249
+ init=False, default_factory=list
1250
+ )
1251
+
1252
+ def addsection(self, name: str, content: str, sep: str = "-") -> None:
1253
+ self.sections.append((name, content, sep))
1254
+
1255
+ def toterminal(self, tw: TerminalWriter) -> None:
1256
+ for name, content, sep in self.sections:
1257
+ tw.sep(sep, name)
1258
+ tw.line(content)
1259
+
1260
+
1261
+ @dataclasses.dataclass(eq=False)
1262
+ class ExceptionChainRepr(ExceptionRepr):
1263
+ chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]]
1264
+
1265
+ def __init__(
1266
+ self,
1267
+ chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]],
1268
+ ) -> None:
1269
+ # reprcrash and reprtraceback of the outermost (the newest) exception
1270
+ # in the chain.
1271
+ super().__init__(
1272
+ reprtraceback=chain[-1][0],
1273
+ reprcrash=chain[-1][1],
1274
+ )
1275
+ self.chain = chain
1276
+
1277
+ def toterminal(self, tw: TerminalWriter) -> None:
1278
+ for element in self.chain:
1279
+ element[0].toterminal(tw)
1280
+ if element[2] is not None:
1281
+ tw.line("")
1282
+ tw.line(element[2], yellow=True)
1283
+ super().toterminal(tw)
1284
+
1285
+
1286
+ @dataclasses.dataclass(eq=False)
1287
+ class ReprExceptionInfo(ExceptionRepr):
1288
+ reprtraceback: ReprTraceback
1289
+ reprcrash: ReprFileLocation | None
1290
+
1291
+ def toterminal(self, tw: TerminalWriter) -> None:
1292
+ self.reprtraceback.toterminal(tw)
1293
+ super().toterminal(tw)
1294
+
1295
+
1296
+ @dataclasses.dataclass(eq=False)
1297
+ class ReprTraceback(TerminalRepr):
1298
+ reprentries: Sequence[ReprEntry | ReprEntryNative]
1299
+ extraline: str | None
1300
+ style: TracebackStyle
1301
+
1302
+ entrysep: ClassVar = "_ "
1303
+
1304
+ def toterminal(self, tw: TerminalWriter) -> None:
1305
+ # The entries might have different styles.
1306
+ for i, entry in enumerate(self.reprentries):
1307
+ if entry.style == "long":
1308
+ tw.line("")
1309
+ entry.toterminal(tw)
1310
+ if i < len(self.reprentries) - 1:
1311
+ next_entry = self.reprentries[i + 1]
1312
+ if entry.style == "long" or (
1313
+ entry.style == "short" and next_entry.style == "long"
1314
+ ):
1315
+ tw.sep(self.entrysep)
1316
+
1317
+ if self.extraline:
1318
+ tw.line(self.extraline)
1319
+
1320
+
1321
+ class ReprTracebackNative(ReprTraceback):
1322
+ def __init__(self, tblines: Sequence[str]) -> None:
1323
+ self.reprentries = [ReprEntryNative(tblines)]
1324
+ self.extraline = None
1325
+ self.style = "native"
1326
+
1327
+
1328
+ @dataclasses.dataclass(eq=False)
1329
+ class ReprEntryNative(TerminalRepr):
1330
+ lines: Sequence[str]
1331
+
1332
+ style: ClassVar[TracebackStyle] = "native"
1333
+
1334
+ def toterminal(self, tw: TerminalWriter) -> None:
1335
+ tw.write("".join(self.lines))
1336
+
1337
+
1338
+ @dataclasses.dataclass(eq=False)
1339
+ class ReprEntry(TerminalRepr):
1340
+ lines: Sequence[str]
1341
+ reprfuncargs: ReprFuncArgs | None
1342
+ reprlocals: ReprLocals | None
1343
+ reprfileloc: ReprFileLocation | None
1344
+ style: TracebackStyle
1345
+
1346
+ def _write_entry_lines(self, tw: TerminalWriter) -> None:
1347
+ """Write the source code portions of a list of traceback entries with syntax highlighting.
1348
+
1349
+ Usually entries are lines like these:
1350
+
1351
+ " x = 1"
1352
+ "> assert x == 2"
1353
+ "E assert 1 == 2"
1354
+
1355
+ This function takes care of rendering the "source" portions of it (the lines without
1356
+ the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
1357
+ character, as doing so might break line continuations.
1358
+ """
1359
+ if not self.lines:
1360
+ return
1361
+
1362
+ if self.style == "value":
1363
+ # Using tw.write instead of tw.line for testing purposes due to TWMock implementation;
1364
+ # lines written with TWMock.line and TWMock._write_source cannot be distinguished
1365
+ # from each other, whereas lines written with TWMock.write are marked with TWMock.WRITE
1366
+ for line in self.lines:
1367
+ tw.write(line)
1368
+ tw.write("\n")
1369
+ return
1370
+
1371
+ # separate indents and source lines that are not failures: we want to
1372
+ # highlight the code but not the indentation, which may contain markers
1373
+ # such as "> assert 0"
1374
+ fail_marker = f"{FormattedExcinfo.fail_marker} "
1375
+ indent_size = len(fail_marker)
1376
+ indents: list[str] = []
1377
+ source_lines: list[str] = []
1378
+ failure_lines: list[str] = []
1379
+ for index, line in enumerate(self.lines):
1380
+ is_failure_line = line.startswith(fail_marker)
1381
+ if is_failure_line:
1382
+ # from this point on all lines are considered part of the failure
1383
+ failure_lines.extend(self.lines[index:])
1384
+ break
1385
+ else:
1386
+ indents.append(line[:indent_size])
1387
+ source_lines.append(line[indent_size:])
1388
+
1389
+ tw._write_source(source_lines, indents)
1390
+
1391
+ # failure lines are always completely red and bold
1392
+ for line in failure_lines:
1393
+ tw.line(line, bold=True, red=True)
1394
+
1395
+ def toterminal(self, tw: TerminalWriter) -> None:
1396
+ if self.style == "short":
1397
+ if self.reprfileloc:
1398
+ self.reprfileloc.toterminal(tw)
1399
+ self._write_entry_lines(tw)
1400
+ if self.reprlocals:
1401
+ self.reprlocals.toterminal(tw, indent=" " * 8)
1402
+ return
1403
+
1404
+ if self.reprfuncargs:
1405
+ self.reprfuncargs.toterminal(tw)
1406
+
1407
+ self._write_entry_lines(tw)
1408
+
1409
+ if self.reprlocals:
1410
+ tw.line("")
1411
+ self.reprlocals.toterminal(tw)
1412
+ if self.reprfileloc:
1413
+ if self.lines:
1414
+ tw.line("")
1415
+ self.reprfileloc.toterminal(tw)
1416
+
1417
+ def __str__(self) -> str:
1418
+ return "{}\n{}\n{}".format(
1419
+ "\n".join(self.lines), self.reprlocals, self.reprfileloc
1420
+ )
1421
+
1422
+
1423
+ @dataclasses.dataclass(eq=False)
1424
+ class ReprFileLocation(TerminalRepr):
1425
+ path: str
1426
+ lineno: int
1427
+ message: str
1428
+
1429
+ def __post_init__(self) -> None:
1430
+ self.path = str(self.path)
1431
+
1432
+ def toterminal(self, tw: TerminalWriter) -> None:
1433
+ # Filename and lineno output for each entry, using an output format
1434
+ # that most editors understand.
1435
+ msg = self.message
1436
+ i = msg.find("\n")
1437
+ if i != -1:
1438
+ msg = msg[:i]
1439
+ tw.write(self.path, bold=True, red=True)
1440
+ tw.line(f":{self.lineno}: {msg}")
1441
+
1442
+
1443
+ @dataclasses.dataclass(eq=False)
1444
+ class ReprLocals(TerminalRepr):
1445
+ lines: Sequence[str]
1446
+
1447
+ def toterminal(self, tw: TerminalWriter, indent="") -> None:
1448
+ for line in self.lines:
1449
+ tw.line(indent + line)
1450
+
1451
+
1452
+ @dataclasses.dataclass(eq=False)
1453
+ class ReprFuncArgs(TerminalRepr):
1454
+ args: Sequence[tuple[str, object]]
1455
+
1456
+ def toterminal(self, tw: TerminalWriter) -> None:
1457
+ if self.args:
1458
+ linesofar = ""
1459
+ for name, value in self.args:
1460
+ ns = f"{name} = {value}"
1461
+ if len(ns) + len(linesofar) + 2 > tw.fullwidth:
1462
+ if linesofar:
1463
+ tw.line(linesofar)
1464
+ linesofar = ns
1465
+ else:
1466
+ if linesofar:
1467
+ linesofar += ", " + ns
1468
+ else:
1469
+ linesofar = ns
1470
+ if linesofar:
1471
+ tw.line(linesofar)
1472
+ tw.line("")
1473
+
1474
+
1475
+ def getfslineno(obj: object) -> tuple[str | Path, int]:
1476
+ """Return source location (path, lineno) for the given object.
1477
+
1478
+ If the source cannot be determined return ("", -1).
1479
+
1480
+ The line number is 0-based.
1481
+ """
1482
+ # xxx let decorators etc specify a sane ordering
1483
+ # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
1484
+ # in 6ec13a2b9. It ("place_as") appears to be something very custom.
1485
+ obj = get_real_func(obj)
1486
+ if hasattr(obj, "place_as"):
1487
+ obj = obj.place_as
1488
+
1489
+ try:
1490
+ code = Code.from_function(obj)
1491
+ except TypeError:
1492
+ try:
1493
+ fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
1494
+ except TypeError:
1495
+ return "", -1
1496
+
1497
+ fspath = (fn and absolutepath(fn)) or ""
1498
+ lineno = -1
1499
+ if fspath:
1500
+ try:
1501
+ _, lineno = findsource(obj)
1502
+ except OSError:
1503
+ pass
1504
+ return fspath, lineno
1505
+
1506
+ return code.path, code.firstlineno
1507
+
1508
+
1509
+ def _byte_offset_to_character_offset(str, offset):
1510
+ """Converts a byte based offset in a string to a code-point."""
1511
+ as_utf8 = str.encode("utf-8")
1512
+ return len(as_utf8[:offset].decode("utf-8", errors="replace"))
1513
+
1514
+
1515
+ # Relative paths that we use to filter traceback entries from appearing to the user;
1516
+ # see filter_traceback.
1517
+ # note: if we need to add more paths than what we have now we should probably use a list
1518
+ # for better maintenance.
1519
+
1520
+ _PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
1521
+ # pluggy is either a package or a single module depending on the version
1522
+ if _PLUGGY_DIR.name == "__init__.py":
1523
+ _PLUGGY_DIR = _PLUGGY_DIR.parent
1524
+ _PYTEST_DIR = Path(_pytest.__file__).parent
1525
+
1526
+
1527
+ def filter_traceback(entry: TracebackEntry) -> bool:
1528
+ """Return True if a TracebackEntry instance should be included in tracebacks.
1529
+
1530
+ We hide traceback entries of:
1531
+
1532
+ * dynamically generated code (no code to show up for it);
1533
+ * internal traceback from pytest or its internal libraries, py and pluggy.
1534
+ """
1535
+ # entry.path might sometimes return a str object when the entry
1536
+ # points to dynamically generated code.
1537
+ # See https://bitbucket.org/pytest-dev/py/issues/71.
1538
+ raw_filename = entry.frame.code.raw.co_filename
1539
+ is_generated = "<" in raw_filename and ">" in raw_filename
1540
+ if is_generated:
1541
+ return False
1542
+
1543
+ # entry.path might point to a non-existing file, in which case it will
1544
+ # also return a str object. See #1133.
1545
+ p = Path(entry.path)
1546
+
1547
+ parents = p.parents
1548
+ if _PLUGGY_DIR in parents:
1549
+ return False
1550
+ if _PYTEST_DIR in parents:
1551
+ return False
1552
+
1553
+ return True
1554
+
1555
+
1556
+ def filter_excinfo_traceback(
1557
+ tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException]
1558
+ ) -> Traceback:
1559
+ """Filter the exception traceback in ``excinfo`` according to ``tbfilter``."""
1560
+ if callable(tbfilter):
1561
+ return tbfilter(excinfo)
1562
+ elif tbfilter:
1563
+ return excinfo.traceback.filter(excinfo)
1564
+ else:
1565
+ return excinfo.traceback
py311/lib/python3.11/site-packages/_pytest/_code/source.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ start = values[insert_index - 1]
172
+ if insert_index >= len(values):
173
+ end = None
174
+ else:
175
+ end = values[insert_index]
176
+ return start, end
177
+
178
+
179
+ def getstatementrange_ast(
180
+ lineno: int,
181
+ source: Source,
182
+ assertion: bool = False,
183
+ astnode: ast.AST | None = None,
184
+ ) -> tuple[ast.AST, int, int]:
185
+ if astnode is None:
186
+ content = str(source)
187
+ # See #4260:
188
+ # Don't produce duplicate warnings when compiling source to find AST.
189
+ with warnings.catch_warnings():
190
+ warnings.simplefilter("ignore")
191
+ astnode = ast.parse(content, "source", "exec")
192
+
193
+ start, end = get_statement_startend2(lineno, astnode)
194
+ # We need to correct the end:
195
+ # - ast-parsing strips comments
196
+ # - there might be empty lines
197
+ # - we might have lesser indented code blocks at the end
198
+ if end is None:
199
+ end = len(source.lines)
200
+
201
+ if end > start + 1:
202
+ # Make sure we don't span differently indented code blocks
203
+ # by using the BlockFinder helper used which inspect.getsource() uses itself.
204
+ block_finder = inspect.BlockFinder()
205
+ # If we start with an indented line, put blockfinder to "started" mode.
206
+ block_finder.started = (
207
+ bool(source.lines[start]) and source.lines[start][0].isspace()
208
+ )
209
+ it = ((x + "\n") for x in source.lines[start:end])
210
+ try:
211
+ for tok in tokenize.generate_tokens(lambda: next(it)):
212
+ block_finder.tokeneater(*tok)
213
+ except (inspect.EndOfBlock, IndentationError):
214
+ end = block_finder.last + start
215
+ except Exception:
216
+ pass
217
+
218
+ # The end might still point to a comment or empty line, correct it.
219
+ while end:
220
+ line = source.lines[end - 1].lstrip()
221
+ if line.startswith("#") or not line:
222
+ end -= 1
223
+ else:
224
+ break
225
+ return astnode, start, end
py311/lib/python3.11/site-packages/_pytest/_io/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from .terminalwriter import get_terminal_width
4
+ from .terminalwriter import TerminalWriter
5
+
6
+
7
+ __all__ = [
8
+ "TerminalWriter",
9
+ "get_terminal_width",
10
+ ]
py311/lib/python3.11/site-packages/_pytest/_io/pprint.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ # This module was imported from the cpython standard library
3
+ # (https://github.com/python/cpython/) at commit
4
+ # c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
5
+ #
6
+ #
7
+ # Original Author: Fred L. Drake, Jr.
8
+ # fdrake@acm.org
9
+ #
10
+ # This is a simple little module I wrote to make life easier. I didn't
11
+ # see anything quite like it in the library, though I may have overlooked
12
+ # something. I wrote this when I was trying to read some heavily nested
13
+ # tuples with fairly non-descriptive content. This is modeled very much
14
+ # after Lisp/Scheme - style pretty-printing of lists. If you find it
15
+ # useful, thank small children who sleep at night.
16
+ from __future__ import annotations
17
+
18
+ import collections as _collections
19
+ 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 = sorted(object.items(), key=_safe_tuple)
166
+ self._format_dict_items(items, stream, indent, allowance, context, level)
167
+ write("}")
168
+
169
+ _dispatch[dict.__repr__] = _pprint_dict
170
+
171
+ def _pprint_ordered_dict(
172
+ self,
173
+ object: Any,
174
+ stream: IO[str],
175
+ indent: int,
176
+ allowance: int,
177
+ context: set[int],
178
+ level: int,
179
+ ) -> None:
180
+ if not len(object):
181
+ stream.write(repr(object))
182
+ return
183
+ cls = object.__class__
184
+ stream.write(cls.__name__ + "(")
185
+ self._pprint_dict(object, stream, indent, allowance, context, level)
186
+ stream.write(")")
187
+
188
+ _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
189
+
190
+ def _pprint_list(
191
+ self,
192
+ object: Any,
193
+ stream: IO[str],
194
+ indent: int,
195
+ allowance: int,
196
+ context: set[int],
197
+ level: int,
198
+ ) -> None:
199
+ stream.write("[")
200
+ self._format_items(object, stream, indent, allowance, context, level)
201
+ stream.write("]")
202
+
203
+ _dispatch[list.__repr__] = _pprint_list
204
+
205
+ def _pprint_tuple(
206
+ self,
207
+ object: Any,
208
+ stream: IO[str],
209
+ indent: int,
210
+ allowance: int,
211
+ context: set[int],
212
+ level: int,
213
+ ) -> None:
214
+ stream.write("(")
215
+ self._format_items(object, stream, indent, allowance, context, level)
216
+ stream.write(")")
217
+
218
+ _dispatch[tuple.__repr__] = _pprint_tuple
219
+
220
+ def _pprint_set(
221
+ self,
222
+ object: Any,
223
+ stream: IO[str],
224
+ indent: int,
225
+ allowance: int,
226
+ context: set[int],
227
+ level: int,
228
+ ) -> None:
229
+ if not len(object):
230
+ stream.write(repr(object))
231
+ return
232
+ typ = object.__class__
233
+ if typ is set:
234
+ stream.write("{")
235
+ endchar = "}"
236
+ else:
237
+ stream.write(typ.__name__ + "({")
238
+ endchar = "})"
239
+ object = sorted(object, key=_safe_key)
240
+ self._format_items(object, stream, indent, allowance, context, level)
241
+ stream.write(endchar)
242
+
243
+ _dispatch[set.__repr__] = _pprint_set
244
+ _dispatch[frozenset.__repr__] = _pprint_set
245
+
246
+ def _pprint_str(
247
+ self,
248
+ object: Any,
249
+ stream: IO[str],
250
+ indent: int,
251
+ allowance: int,
252
+ context: set[int],
253
+ level: int,
254
+ ) -> None:
255
+ write = stream.write
256
+ if not len(object):
257
+ write(repr(object))
258
+ return
259
+ chunks = []
260
+ lines = object.splitlines(True)
261
+ if level == 1:
262
+ indent += 1
263
+ allowance += 1
264
+ max_width1 = max_width = self._width - indent
265
+ for i, line in enumerate(lines):
266
+ rep = repr(line)
267
+ if i == len(lines) - 1:
268
+ max_width1 -= allowance
269
+ if len(rep) <= max_width1:
270
+ chunks.append(rep)
271
+ else:
272
+ # A list of alternating (non-space, space) strings
273
+ parts = re.findall(r"\S*\s*", line)
274
+ assert parts
275
+ assert not parts[-1]
276
+ parts.pop() # drop empty last part
277
+ max_width2 = max_width
278
+ current = ""
279
+ for j, part in enumerate(parts):
280
+ candidate = current + part
281
+ if j == len(parts) - 1 and i == len(lines) - 1:
282
+ max_width2 -= allowance
283
+ if len(repr(candidate)) > max_width2:
284
+ if current:
285
+ chunks.append(repr(current))
286
+ current = part
287
+ else:
288
+ current = candidate
289
+ if current:
290
+ chunks.append(repr(current))
291
+ if len(chunks) == 1:
292
+ write(rep)
293
+ return
294
+ if level == 1:
295
+ write("(")
296
+ for i, rep in enumerate(chunks):
297
+ if i > 0:
298
+ write("\n" + " " * indent)
299
+ write(rep)
300
+ if level == 1:
301
+ write(")")
302
+
303
+ _dispatch[str.__repr__] = _pprint_str
304
+
305
+ def _pprint_bytes(
306
+ self,
307
+ object: Any,
308
+ stream: IO[str],
309
+ indent: int,
310
+ allowance: int,
311
+ context: set[int],
312
+ level: int,
313
+ ) -> None:
314
+ write = stream.write
315
+ if len(object) <= 4:
316
+ write(repr(object))
317
+ return
318
+ parens = level == 1
319
+ if parens:
320
+ indent += 1
321
+ allowance += 1
322
+ write("(")
323
+ delim = ""
324
+ for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
325
+ write(delim)
326
+ write(rep)
327
+ if not delim:
328
+ delim = "\n" + " " * indent
329
+ if parens:
330
+ write(")")
331
+
332
+ _dispatch[bytes.__repr__] = _pprint_bytes
333
+
334
+ def _pprint_bytearray(
335
+ self,
336
+ object: Any,
337
+ stream: IO[str],
338
+ indent: int,
339
+ allowance: int,
340
+ context: set[int],
341
+ level: int,
342
+ ) -> None:
343
+ write = stream.write
344
+ write("bytearray(")
345
+ self._pprint_bytes(
346
+ bytes(object), stream, indent + 10, allowance + 1, context, level + 1
347
+ )
348
+ write(")")
349
+
350
+ _dispatch[bytearray.__repr__] = _pprint_bytearray
351
+
352
+ def _pprint_mappingproxy(
353
+ self,
354
+ object: Any,
355
+ stream: IO[str],
356
+ indent: int,
357
+ allowance: int,
358
+ context: set[int],
359
+ level: int,
360
+ ) -> None:
361
+ stream.write("mappingproxy(")
362
+ self._format(object.copy(), stream, indent, allowance, context, level)
363
+ stream.write(")")
364
+
365
+ _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
366
+
367
+ def _pprint_simplenamespace(
368
+ self,
369
+ object: Any,
370
+ stream: IO[str],
371
+ indent: int,
372
+ allowance: int,
373
+ context: set[int],
374
+ level: int,
375
+ ) -> None:
376
+ if type(object) is _types.SimpleNamespace:
377
+ # The SimpleNamespace repr is "namespace" instead of the class
378
+ # name, so we do the same here. For subclasses; use the class name.
379
+ cls_name = "namespace"
380
+ else:
381
+ cls_name = object.__class__.__name__
382
+ items = object.__dict__.items()
383
+ stream.write(cls_name + "(")
384
+ self._format_namespace_items(items, stream, indent, allowance, context, level)
385
+ stream.write(")")
386
+
387
+ _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
388
+
389
+ def _format_dict_items(
390
+ self,
391
+ items: list[tuple[Any, Any]],
392
+ stream: IO[str],
393
+ indent: int,
394
+ allowance: int,
395
+ context: set[int],
396
+ level: int,
397
+ ) -> None:
398
+ if not items:
399
+ return
400
+
401
+ write = stream.write
402
+ item_indent = indent + self._indent_per_level
403
+ delimnl = "\n" + " " * item_indent
404
+ for key, ent in items:
405
+ write(delimnl)
406
+ write(self._repr(key, context, level))
407
+ write(": ")
408
+ self._format(ent, stream, item_indent, 1, context, level)
409
+ write(",")
410
+
411
+ write("\n" + " " * indent)
412
+
413
+ def _format_namespace_items(
414
+ self,
415
+ items: list[tuple[Any, Any]],
416
+ stream: IO[str],
417
+ indent: int,
418
+ allowance: int,
419
+ context: set[int],
420
+ level: int,
421
+ ) -> None:
422
+ if not items:
423
+ return
424
+
425
+ write = stream.write
426
+ item_indent = indent + self._indent_per_level
427
+ delimnl = "\n" + " " * item_indent
428
+ for key, ent in items:
429
+ write(delimnl)
430
+ write(key)
431
+ write("=")
432
+ if id(ent) in context:
433
+ # Special-case representation of recursion to match standard
434
+ # recursive dataclass repr.
435
+ write("...")
436
+ else:
437
+ self._format(
438
+ ent,
439
+ stream,
440
+ item_indent + len(key) + 1,
441
+ 1,
442
+ context,
443
+ level,
444
+ )
445
+
446
+ write(",")
447
+
448
+ write("\n" + " " * indent)
449
+
450
+ def _format_items(
451
+ self,
452
+ items: list[Any],
453
+ stream: IO[str],
454
+ indent: int,
455
+ allowance: int,
456
+ context: set[int],
457
+ level: int,
458
+ ) -> None:
459
+ if not items:
460
+ return
461
+
462
+ write = stream.write
463
+ item_indent = indent + self._indent_per_level
464
+ delimnl = "\n" + " " * item_indent
465
+
466
+ for item in items:
467
+ write(delimnl)
468
+ self._format(item, stream, item_indent, 1, context, level)
469
+ write(",")
470
+
471
+ write("\n" + " " * indent)
472
+
473
+ def _repr(self, object: Any, context: set[int], level: int) -> str:
474
+ return self._safe_repr(object, context.copy(), self._depth, level)
475
+
476
+ def _pprint_default_dict(
477
+ self,
478
+ object: Any,
479
+ stream: IO[str],
480
+ indent: int,
481
+ allowance: int,
482
+ context: set[int],
483
+ level: int,
484
+ ) -> None:
485
+ rdf = self._repr(object.default_factory, context, level)
486
+ stream.write(f"{object.__class__.__name__}({rdf}, ")
487
+ self._pprint_dict(object, stream, indent, allowance, context, level)
488
+ stream.write(")")
489
+
490
+ _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
491
+
492
+ def _pprint_counter(
493
+ self,
494
+ object: Any,
495
+ stream: IO[str],
496
+ indent: int,
497
+ allowance: int,
498
+ context: set[int],
499
+ level: int,
500
+ ) -> None:
501
+ stream.write(object.__class__.__name__ + "(")
502
+
503
+ if object:
504
+ stream.write("{")
505
+ items = object.most_common()
506
+ self._format_dict_items(items, stream, indent, allowance, context, level)
507
+ stream.write("}")
508
+
509
+ stream.write(")")
510
+
511
+ _dispatch[_collections.Counter.__repr__] = _pprint_counter
512
+
513
+ def _pprint_chain_map(
514
+ self,
515
+ object: Any,
516
+ stream: IO[str],
517
+ indent: int,
518
+ allowance: int,
519
+ context: set[int],
520
+ level: int,
521
+ ) -> None:
522
+ if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
523
+ stream.write(repr(object))
524
+ return
525
+
526
+ stream.write(object.__class__.__name__ + "(")
527
+ self._format_items(object.maps, stream, indent, allowance, context, level)
528
+ stream.write(")")
529
+
530
+ _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
531
+
532
+ def _pprint_deque(
533
+ self,
534
+ object: Any,
535
+ stream: IO[str],
536
+ indent: int,
537
+ allowance: int,
538
+ context: set[int],
539
+ level: int,
540
+ ) -> None:
541
+ stream.write(object.__class__.__name__ + "(")
542
+ if object.maxlen is not None:
543
+ stream.write(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 sorted(object.items(), key=_safe_tuple):
612
+ krepr = self._safe_repr(k, context, maxlevels, level)
613
+ vrepr = self._safe_repr(v, context, maxlevels, level)
614
+ append(f"{krepr}: {vrepr}")
615
+ context.remove(objid)
616
+ return "{{{}}}".format(", ".join(components))
617
+
618
+ if (issubclass(typ, list) and r is list.__repr__) or (
619
+ issubclass(typ, tuple) and r is tuple.__repr__
620
+ ):
621
+ if issubclass(typ, list):
622
+ if not object:
623
+ return "[]"
624
+ format = "[%s]"
625
+ elif len(object) == 1:
626
+ format = "(%s,)"
627
+ else:
628
+ if not object:
629
+ return "()"
630
+ format = "(%s)"
631
+ objid = id(object)
632
+ if maxlevels and level >= maxlevels:
633
+ return format % "..."
634
+ if objid in context:
635
+ return _recursion(object)
636
+ context.add(objid)
637
+ components = []
638
+ append = components.append
639
+ level += 1
640
+ for o in object:
641
+ orepr = self._safe_repr(o, context, maxlevels, level)
642
+ append(orepr)
643
+ context.remove(objid)
644
+ return format % ", ".join(components)
645
+
646
+ return repr(object)
647
+
648
+
649
+ _builtin_scalars = frozenset(
650
+ {str, bytes, bytearray, float, complex, bool, type(None), int}
651
+ )
652
+
653
+
654
+ def _recursion(object: Any) -> str:
655
+ return f"<Recursion on {type(object).__name__} with id={id(object)}>"
656
+
657
+
658
+ def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]:
659
+ current = b""
660
+ last = len(object) // 4 * 4
661
+ for i in range(0, len(object), 4):
662
+ part = object[i : i + 4]
663
+ candidate = current + part
664
+ if i == last:
665
+ width -= allowance
666
+ if len(repr(candidate)) > width:
667
+ if current:
668
+ yield repr(current)
669
+ current = part
670
+ else:
671
+ current = candidate
672
+ if current:
673
+ yield repr(current)
py311/lib/python3.11/site-packages/_pytest/_io/saferepr.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import pprint
4
+ import reprlib
5
+
6
+
7
+ def _try_repr_or_str(obj: object) -> str:
8
+ try:
9
+ return repr(obj)
10
+ except (KeyboardInterrupt, SystemExit):
11
+ raise
12
+ except BaseException:
13
+ return f'{type(obj).__name__}("{obj}")'
14
+
15
+
16
+ def _format_repr_exception(exc: BaseException, obj: object) -> str:
17
+ try:
18
+ exc_info = _try_repr_or_str(exc)
19
+ except (KeyboardInterrupt, SystemExit):
20
+ raise
21
+ except BaseException as inner_exc:
22
+ exc_info = f"unpresentable exception ({_try_repr_or_str(inner_exc)})"
23
+ return (
24
+ f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
25
+ )
26
+
27
+
28
+ def _ellipsize(s: str, maxsize: int) -> str:
29
+ if len(s) > maxsize:
30
+ i = max(0, (maxsize - 3) // 2)
31
+ j = max(0, maxsize - 3 - i)
32
+ return s[:i] + "..." + s[len(s) - j :]
33
+ return s
34
+
35
+
36
+ class SafeRepr(reprlib.Repr):
37
+ """
38
+ repr.Repr that limits the resulting size of repr() and includes
39
+ information on exceptions raised during the call.
40
+ """
41
+
42
+ def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None:
43
+ """
44
+ :param maxsize:
45
+ If not None, will truncate the resulting repr to that specific size, using ellipsis
46
+ somewhere in the middle to hide the extra text.
47
+ If None, will not impose any size limits on the returning repr.
48
+ """
49
+ super().__init__()
50
+ # ``maxstring`` is used by the superclass, and needs to be an int; using a
51
+ # very large number in case maxsize is None, meaning we want to disable
52
+ # truncation.
53
+ self.maxstring = maxsize if maxsize is not None else 1_000_000_000
54
+ self.maxsize = maxsize
55
+ self.use_ascii = use_ascii
56
+
57
+ def repr(self, x: object) -> str:
58
+ try:
59
+ if self.use_ascii:
60
+ s = ascii(x)
61
+ else:
62
+ s = super().repr(x)
63
+ except (KeyboardInterrupt, SystemExit):
64
+ raise
65
+ except BaseException as exc:
66
+ s = _format_repr_exception(exc, x)
67
+ if self.maxsize is not None:
68
+ s = _ellipsize(s, self.maxsize)
69
+ return s
70
+
71
+ def repr_instance(self, x: object, level: int) -> str:
72
+ try:
73
+ s = repr(x)
74
+ except (KeyboardInterrupt, SystemExit):
75
+ raise
76
+ except BaseException as exc:
77
+ s = _format_repr_exception(exc, x)
78
+ if self.maxsize is not None:
79
+ s = _ellipsize(s, self.maxsize)
80
+ return s
81
+
82
+
83
+ def safeformat(obj: object) -> str:
84
+ """Return a pretty printed string for the given object.
85
+
86
+ Failing __repr__ functions of user instances will be represented
87
+ with a short exception info.
88
+ """
89
+ try:
90
+ return pprint.pformat(obj)
91
+ except Exception as exc:
92
+ return _format_repr_exception(exc, obj)
93
+
94
+
95
+ # Maximum size of overall repr of objects to display during assertion errors.
96
+ DEFAULT_REPR_MAX_SIZE = 240
97
+
98
+
99
+ def saferepr(
100
+ obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
101
+ ) -> str:
102
+ """Return a size-limited safe repr-string for the given object.
103
+
104
+ Failing __repr__ functions of user instances will be represented
105
+ with a short exception info and 'saferepr' generally takes
106
+ care to never raise exceptions itself.
107
+
108
+ This function is a wrapper around the Repr/reprlib functionality of the
109
+ stdlib.
110
+ """
111
+ return SafeRepr(maxsize, use_ascii).repr(obj)
112
+
113
+
114
+ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
115
+ """Return an unlimited-size safe repr-string for the given object.
116
+
117
+ As with saferepr, failing __repr__ functions of user instances
118
+ will be represented with a short exception info.
119
+
120
+ This function is a wrapper around simple repr.
121
+
122
+ Note: a cleaner solution would be to alter ``saferepr``this way
123
+ when maxsize=None, but that might affect some other code.
124
+ """
125
+ try:
126
+ if use_ascii:
127
+ return ascii(obj)
128
+ return repr(obj)
129
+ except Exception as exc:
130
+ return _format_repr_exception(exc, obj)
py311/lib/python3.11/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
py311/lib/python3.11/site-packages/_pytest/_io/wcwidth.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ import unicodedata
5
+
6
+
7
+ @lru_cache(100)
8
+ def wcwidth(c: str) -> int:
9
+ """Determine how many columns are needed to display a character in a terminal.
10
+
11
+ Returns -1 if the character is not printable.
12
+ Returns 0, 1 or 2 for other characters.
13
+ """
14
+ o = ord(c)
15
+
16
+ # ASCII fast path.
17
+ if 0x20 <= o < 0x07F:
18
+ return 1
19
+
20
+ # Some Cf/Zp/Zl characters which should be zero-width.
21
+ if (
22
+ o == 0x0000
23
+ or 0x200B <= o <= 0x200F
24
+ or 0x2028 <= o <= 0x202E
25
+ or 0x2060 <= o <= 0x2063
26
+ ):
27
+ return 0
28
+
29
+ category = unicodedata.category(c)
30
+
31
+ # Control characters.
32
+ if category == "Cc":
33
+ return -1
34
+
35
+ # Combining characters with zero width.
36
+ if category in ("Me", "Mn"):
37
+ return 0
38
+
39
+ # Full/Wide east asian characters.
40
+ if unicodedata.east_asian_width(c) in ("F", "W"):
41
+ return 2
42
+
43
+ return 1
44
+
45
+
46
+ def wcswidth(s: str) -> int:
47
+ """Determine how many columns are needed to display a string in a terminal.
48
+
49
+ Returns -1 if the string contains non-printable characters.
50
+ """
51
+ width = 0
52
+ for c in unicodedata.normalize("NFC", s):
53
+ wc = wcwidth(c)
54
+ if wc < 0:
55
+ return -1
56
+ width += wc
57
+ return width
py311/lib/python3.11/site-packages/_pytest/_py/__init__.py ADDED
File without changes
py311/lib/python3.11/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]
py311/lib/python3.11/site-packages/_pytest/_py/path.py ADDED
@@ -0,0 +1,1475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """local path implementation."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import atexit
7
+ from 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):
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
py311/lib/python3.11/site-packages/_pytest/assertion/__init__.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
61
+ Config._add_verbosity_ini(
62
+ parser,
63
+ Config.VERBOSITY_ASSERTIONS,
64
+ help=(
65
+ "Specify a verbosity level for assertions, overriding the main level. "
66
+ "Higher levels will provide more detailed explanation when an assertion fails."
67
+ ),
68
+ )
69
+
70
+
71
+ def register_assert_rewrite(*names: str) -> None:
72
+ """Register one or more module names to be rewritten on import.
73
+
74
+ This function will make sure that this module or all modules inside
75
+ the package will get their assert statements rewritten.
76
+ Thus you should make sure to call this before the module is
77
+ actually imported, usually in your __init__.py if you are a plugin
78
+ using a package.
79
+
80
+ :param names: The module names to register.
81
+ """
82
+ for name in names:
83
+ if not isinstance(name, str):
84
+ msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
85
+ raise TypeError(msg.format(repr(names)))
86
+ rewrite_hook: RewriteHook
87
+ for hook in sys.meta_path:
88
+ if isinstance(hook, rewrite.AssertionRewritingHook):
89
+ rewrite_hook = hook
90
+ break
91
+ else:
92
+ rewrite_hook = DummyRewriteHook()
93
+ rewrite_hook.mark_rewrite(*names)
94
+
95
+
96
+ class RewriteHook(Protocol):
97
+ def mark_rewrite(self, *names: str) -> None: ...
98
+
99
+
100
+ class DummyRewriteHook:
101
+ """A no-op import hook for when rewriting is disabled."""
102
+
103
+ def mark_rewrite(self, *names: str) -> None:
104
+ pass
105
+
106
+
107
+ class AssertionState:
108
+ """State for the assertion plugin."""
109
+
110
+ def __init__(self, config: Config, mode) -> None:
111
+ self.mode = mode
112
+ self.trace = config.trace.root.get("assertion")
113
+ self.hook: rewrite.AssertionRewritingHook | None = None
114
+
115
+
116
+ def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
117
+ """Try to install the rewrite hook, raise SystemError if it fails."""
118
+ config.stash[assertstate_key] = AssertionState(config, "rewrite")
119
+ config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
120
+ sys.meta_path.insert(0, hook)
121
+ config.stash[assertstate_key].trace("installed rewrite import hook")
122
+
123
+ def undo() -> None:
124
+ hook = config.stash[assertstate_key].hook
125
+ if hook is not None and hook in sys.meta_path:
126
+ sys.meta_path.remove(hook)
127
+
128
+ config.add_cleanup(undo)
129
+ return hook
130
+
131
+
132
+ def pytest_collection(session: Session) -> None:
133
+ # This hook is only called when test modules are collected
134
+ # so for example not in the managing process of pytest-xdist
135
+ # (which does not collect test modules).
136
+ assertstate = session.config.stash.get(assertstate_key, None)
137
+ if assertstate:
138
+ if assertstate.hook is not None:
139
+ assertstate.hook.set_session(session)
140
+
141
+
142
+ @hookimpl(wrapper=True, tryfirst=True)
143
+ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
144
+ """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
145
+
146
+ The rewrite module will use util._reprcompare if it exists to use custom
147
+ reporting via the pytest_assertrepr_compare hook. This sets up this custom
148
+ comparison for the test.
149
+ """
150
+ ihook = item.ihook
151
+
152
+ def callbinrepr(op, left: object, right: object) -> str | None:
153
+ """Call the pytest_assertrepr_compare hook and prepare the result.
154
+
155
+ This uses the first result from the hook and then ensures the
156
+ following:
157
+ * Overly verbose explanations are truncated unless configured otherwise
158
+ (eg. if running in verbose mode).
159
+ * Embedded newlines are escaped to help util.format_explanation()
160
+ later.
161
+ * If the rewrite mode is used embedded %-characters are replaced
162
+ to protect later % formatting.
163
+
164
+ The result can be formatted by util.format_explanation() for
165
+ pretty printing.
166
+ """
167
+ hook_result = ihook.pytest_assertrepr_compare(
168
+ config=item.config, op=op, left=left, right=right
169
+ )
170
+ for new_expl in hook_result:
171
+ if new_expl:
172
+ new_expl = truncate.truncate_if_required(new_expl, item)
173
+ new_expl = [line.replace("\n", "\\n") for line in new_expl]
174
+ res = "\n~".join(new_expl)
175
+ if item.config.getvalue("assertmode") == "rewrite":
176
+ res = res.replace("%", "%%")
177
+ return res
178
+ return None
179
+
180
+ saved_assert_hooks = util._reprcompare, util._assertion_pass
181
+ util._reprcompare = callbinrepr
182
+ util._config = item.config
183
+
184
+ if ihook.pytest_assertion_pass.get_hookimpls():
185
+
186
+ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
187
+ ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl)
188
+
189
+ util._assertion_pass = call_assertion_pass_hook
190
+
191
+ try:
192
+ return (yield)
193
+ finally:
194
+ util._reprcompare, util._assertion_pass = saved_assert_hooks
195
+ util._config = None
196
+
197
+
198
+ def pytest_sessionfinish(session: Session) -> None:
199
+ assertstate = session.config.stash.get(assertstate_key, None)
200
+ if assertstate:
201
+ if assertstate.hook is not None:
202
+ assertstate.hook.set_session(None)
203
+
204
+
205
+ def pytest_assertrepr_compare(
206
+ config: Config, op: str, left: Any, right: Any
207
+ ) -> list[str] | None:
208
+ return util.assertrepr_compare(config=config, op=op, left=left, right=right)
py311/lib/python3.11/site-packages/_pytest/assertion/rewrite.py ADDED
@@ -0,0 +1,1202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 spec is None and path is not None:
119
+ # With --import-mode=importlib, PathFinder cannot find spec without modifying `sys.path`,
120
+ # causing inability to assert rewriting (#12659).
121
+ # At this point, try using the file path to find the module spec.
122
+ for _path_str in path:
123
+ spec = importlib.util.spec_from_file_location(name, _path_str)
124
+ if spec is not None:
125
+ break
126
+
127
+ if (
128
+ # the import machinery could not find a file to import
129
+ spec is None
130
+ # this is a namespace package (without `__init__.py`)
131
+ # there's nothing to rewrite there
132
+ or spec.origin is None
133
+ # we can only rewrite source files
134
+ or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
135
+ # if the file doesn't exist, we can't rewrite it
136
+ or not os.path.exists(spec.origin)
137
+ ):
138
+ return None
139
+ else:
140
+ fn = spec.origin
141
+
142
+ if not self._should_rewrite(name, fn, state):
143
+ return None
144
+
145
+ return importlib.util.spec_from_file_location(
146
+ name,
147
+ fn,
148
+ loader=self,
149
+ submodule_search_locations=spec.submodule_search_locations,
150
+ )
151
+
152
+ def create_module(
153
+ self, spec: importlib.machinery.ModuleSpec
154
+ ) -> types.ModuleType | None:
155
+ return None # default behaviour is fine
156
+
157
+ def exec_module(self, module: types.ModuleType) -> None:
158
+ assert module.__spec__ is not None
159
+ assert module.__spec__.origin is not None
160
+ fn = Path(module.__spec__.origin)
161
+ state = self.config.stash[assertstate_key]
162
+
163
+ self._rewritten_names[module.__name__] = fn
164
+
165
+ # The requested module looks like a test file, so rewrite it. This is
166
+ # the most magical part of the process: load the source, rewrite the
167
+ # asserts, and load the rewritten source. We also cache the rewritten
168
+ # module code in a special pyc. We must be aware of the possibility of
169
+ # concurrent pytest processes rewriting and loading pycs. To avoid
170
+ # tricky race conditions, we maintain the following invariant: The
171
+ # cached pyc is always a complete, valid pyc. Operations on it must be
172
+ # atomic. POSIX's atomic rename comes in handy.
173
+ write = not sys.dont_write_bytecode
174
+ cache_dir = get_cache_dir(fn)
175
+ if write:
176
+ ok = try_makedirs(cache_dir)
177
+ if not ok:
178
+ write = False
179
+ state.trace(f"read only directory: {cache_dir}")
180
+
181
+ cache_name = fn.name[:-3] + PYC_TAIL
182
+ pyc = cache_dir / cache_name
183
+ # Notice that even if we're in a read-only directory, I'm going
184
+ # to check for a cached pyc. This may not be optimal...
185
+ co = _read_pyc(fn, pyc, state.trace)
186
+ if co is None:
187
+ state.trace(f"rewriting {fn!r}")
188
+ source_stat, co = _rewrite_test(fn, self.config)
189
+ if write:
190
+ self._writing_pyc = True
191
+ try:
192
+ _write_pyc(state, co, source_stat, pyc)
193
+ finally:
194
+ self._writing_pyc = False
195
+ else:
196
+ state.trace(f"found cached rewritten pyc for {fn}")
197
+ exec(co, module.__dict__)
198
+
199
+ def _early_rewrite_bailout(self, name: str, state: AssertionState) -> bool:
200
+ """A fast way to get out of rewriting modules.
201
+
202
+ Profiling has shown that the call to PathFinder.find_spec (inside of
203
+ the find_spec from this class) is a major slowdown, so, this method
204
+ tries to filter what we're sure won't be rewritten before getting to
205
+ it.
206
+ """
207
+ if self.session is not None and not self._session_paths_checked:
208
+ self._session_paths_checked = True
209
+ for initial_path in self.session._initialpaths:
210
+ # Make something as c:/projects/my_project/path.py ->
211
+ # ['c:', 'projects', 'my_project', 'path.py']
212
+ parts = str(initial_path).split(os.sep)
213
+ # add 'path' to basenames to be checked.
214
+ self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
215
+
216
+ # Note: conftest already by default in _basenames_to_check_rewrite.
217
+ parts = name.split(".")
218
+ if parts[-1] in self._basenames_to_check_rewrite:
219
+ return False
220
+
221
+ # For matching the name it must be as if it was a filename.
222
+ path = PurePath(*parts).with_suffix(".py")
223
+
224
+ for pat in self.fnpats:
225
+ # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
226
+ # on the name alone because we need to match against the full path
227
+ if os.path.dirname(pat):
228
+ return False
229
+ if fnmatch_ex(pat, path):
230
+ return False
231
+
232
+ if self._is_marked_for_rewrite(name, state):
233
+ return False
234
+
235
+ state.trace(f"early skip of rewriting module: {name}")
236
+ return True
237
+
238
+ def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool:
239
+ # always rewrite conftest files
240
+ if os.path.basename(fn) == "conftest.py":
241
+ state.trace(f"rewriting conftest file: {fn!r}")
242
+ return True
243
+
244
+ if self.session is not None:
245
+ if self.session.isinitpath(absolutepath(fn)):
246
+ state.trace(f"matched test file (was specified on cmdline): {fn!r}")
247
+ return True
248
+
249
+ # modules not passed explicitly on the command line are only
250
+ # rewritten if they match the naming convention for test files
251
+ fn_path = PurePath(fn)
252
+ for pat in self.fnpats:
253
+ if fnmatch_ex(pat, fn_path):
254
+ state.trace(f"matched test file {fn!r}")
255
+ return True
256
+
257
+ return self._is_marked_for_rewrite(name, state)
258
+
259
+ def _is_marked_for_rewrite(self, name: str, state: AssertionState) -> bool:
260
+ try:
261
+ return self._marked_for_rewrite_cache[name]
262
+ except KeyError:
263
+ for marked in self._must_rewrite:
264
+ if name == marked or name.startswith(marked + "."):
265
+ state.trace(f"matched marked file {name!r} (from {marked!r})")
266
+ self._marked_for_rewrite_cache[name] = True
267
+ return True
268
+
269
+ self._marked_for_rewrite_cache[name] = False
270
+ return False
271
+
272
+ def mark_rewrite(self, *names: str) -> None:
273
+ """Mark import names as needing to be rewritten.
274
+
275
+ The named module or package as well as any nested modules will
276
+ be rewritten on import.
277
+ """
278
+ already_imported = (
279
+ set(names).intersection(sys.modules).difference(self._rewritten_names)
280
+ )
281
+ for name in already_imported:
282
+ mod = sys.modules[name]
283
+ if not AssertionRewriter.is_rewrite_disabled(
284
+ mod.__doc__ or ""
285
+ ) and not isinstance(mod.__loader__, type(self)):
286
+ self._warn_already_imported(name)
287
+ self._must_rewrite.update(names)
288
+ self._marked_for_rewrite_cache.clear()
289
+
290
+ def _warn_already_imported(self, name: str) -> None:
291
+ from _pytest.warning_types import PytestAssertRewriteWarning
292
+
293
+ self.config.issue_config_time_warning(
294
+ PytestAssertRewriteWarning(
295
+ f"Module already imported so cannot be rewritten; {name}"
296
+ ),
297
+ stacklevel=5,
298
+ )
299
+
300
+ def get_data(self, pathname: str | bytes) -> bytes:
301
+ """Optional PEP302 get_data API."""
302
+ with open(pathname, "rb") as f:
303
+ return f.read()
304
+
305
+ def get_resource_reader(self, name: str) -> TraversableResources:
306
+ return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) # type: ignore[arg-type]
307
+
308
+
309
+ def _write_pyc_fp(
310
+ fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType
311
+ ) -> None:
312
+ # Technically, we don't have to have the same pyc format as
313
+ # (C)Python, since these "pycs" should never be seen by builtin
314
+ # import. However, there's little reason to deviate.
315
+ fp.write(importlib.util.MAGIC_NUMBER)
316
+ # https://www.python.org/dev/peps/pep-0552/
317
+ flags = b"\x00\x00\x00\x00"
318
+ fp.write(flags)
319
+ # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
320
+ mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
321
+ size = source_stat.st_size & 0xFFFFFFFF
322
+ # "<LL" stands for 2 unsigned longs, little-endian.
323
+ fp.write(struct.pack("<LL", mtime, size))
324
+ fp.write(marshal.dumps(co))
325
+
326
+
327
+ def _write_pyc(
328
+ state: AssertionState,
329
+ co: types.CodeType,
330
+ source_stat: os.stat_result,
331
+ pyc: Path,
332
+ ) -> bool:
333
+ proc_pyc = f"{pyc}.{os.getpid()}"
334
+ try:
335
+ with open(proc_pyc, "wb") as fp:
336
+ _write_pyc_fp(fp, source_stat, co)
337
+ except OSError as e:
338
+ state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
339
+ return False
340
+
341
+ try:
342
+ os.replace(proc_pyc, pyc)
343
+ except OSError as e:
344
+ state.trace(f"error writing pyc file at {pyc}: {e}")
345
+ # we ignore any failure to write the cache file
346
+ # there are many reasons, permission-denied, pycache dir being a
347
+ # file etc.
348
+ return False
349
+ return True
350
+
351
+
352
+ def _rewrite_test(fn: Path, config: Config) -> tuple[os.stat_result, types.CodeType]:
353
+ """Read and rewrite *fn* and return the code object."""
354
+ stat = os.stat(fn)
355
+ source = fn.read_bytes()
356
+ strfn = str(fn)
357
+ tree = ast.parse(source, filename=strfn)
358
+ rewrite_asserts(tree, source, strfn, config)
359
+ co = compile(tree, strfn, "exec", dont_inherit=True)
360
+ return stat, co
361
+
362
+
363
+ def _read_pyc(
364
+ source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None
365
+ ) -> types.CodeType | None:
366
+ """Possibly read a pytest pyc containing rewritten code.
367
+
368
+ Return rewritten code if successful or None if not.
369
+ """
370
+ try:
371
+ fp = open(pyc, "rb")
372
+ except OSError:
373
+ return None
374
+ with fp:
375
+ try:
376
+ stat_result = os.stat(source)
377
+ mtime = int(stat_result.st_mtime)
378
+ size = stat_result.st_size
379
+ data = fp.read(16)
380
+ except OSError as e:
381
+ trace(f"_read_pyc({source}): OSError {e}")
382
+ return None
383
+ # Check for invalid or out of date pyc file.
384
+ if len(data) != (16):
385
+ trace(f"_read_pyc({source}): invalid pyc (too short)")
386
+ return None
387
+ if data[:4] != importlib.util.MAGIC_NUMBER:
388
+ trace(f"_read_pyc({source}): invalid pyc (bad magic number)")
389
+ return None
390
+ if data[4:8] != b"\x00\x00\x00\x00":
391
+ trace(f"_read_pyc({source}): invalid pyc (unsupported flags)")
392
+ return None
393
+ mtime_data = data[8:12]
394
+ if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
395
+ trace(f"_read_pyc({source}): out of date")
396
+ return None
397
+ size_data = data[12:16]
398
+ if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
399
+ trace(f"_read_pyc({source}): invalid pyc (incorrect size)")
400
+ return None
401
+ try:
402
+ co = marshal.load(fp)
403
+ except Exception as e:
404
+ trace(f"_read_pyc({source}): marshal.load error {e}")
405
+ return None
406
+ if not isinstance(co, types.CodeType):
407
+ trace(f"_read_pyc({source}): not a code object")
408
+ return None
409
+ return co
410
+
411
+
412
+ def rewrite_asserts(
413
+ mod: ast.Module,
414
+ source: bytes,
415
+ module_path: str | None = None,
416
+ config: Config | None = None,
417
+ ) -> None:
418
+ """Rewrite the assert statements in mod."""
419
+ AssertionRewriter(module_path, config, source).run(mod)
420
+
421
+
422
+ def _saferepr(obj: object) -> str:
423
+ r"""Get a safe repr of an object for assertion error messages.
424
+
425
+ The assertion formatting (util.format_explanation()) requires
426
+ newlines to be escaped since they are a special character for it.
427
+ Normally assertion.util.format_explanation() does this but for a
428
+ custom repr it is possible to contain one of the special escape
429
+ sequences, especially '\n{' and '\n}' are likely to be present in
430
+ JSON reprs.
431
+ """
432
+ if isinstance(obj, types.MethodType):
433
+ # for bound methods, skip redundant <bound method ...> information
434
+ return obj.__name__
435
+
436
+ maxsize = _get_maxsize_for_saferepr(util._config)
437
+ if not maxsize:
438
+ return saferepr_unlimited(obj).replace("\n", "\\n")
439
+ return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
440
+
441
+
442
+ def _get_maxsize_for_saferepr(config: Config | None) -> int | None:
443
+ """Get `maxsize` configuration for saferepr based on the given config object."""
444
+ if config is None:
445
+ verbosity = 0
446
+ else:
447
+ verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
448
+ if verbosity >= 2:
449
+ return None
450
+ if verbosity >= 1:
451
+ return DEFAULT_REPR_MAX_SIZE * 10
452
+ return DEFAULT_REPR_MAX_SIZE
453
+
454
+
455
+ def _format_assertmsg(obj: object) -> str:
456
+ r"""Format the custom assertion message given.
457
+
458
+ For strings this simply replaces newlines with '\n~' so that
459
+ util.format_explanation() will preserve them instead of escaping
460
+ newlines. For other objects saferepr() is used first.
461
+ """
462
+ # reprlib appears to have a bug which means that if a string
463
+ # contains a newline it gets escaped, however if an object has a
464
+ # .__repr__() which contains newlines it does not get escaped.
465
+ # However in either case we want to preserve the newline.
466
+ replaces = [("\n", "\n~"), ("%", "%%")]
467
+ if not isinstance(obj, str):
468
+ obj = saferepr(obj, _get_maxsize_for_saferepr(util._config))
469
+ replaces.append(("\\n", "\n~"))
470
+
471
+ for r1, r2 in replaces:
472
+ obj = obj.replace(r1, r2)
473
+
474
+ return obj
475
+
476
+
477
+ def _should_repr_global_name(obj: object) -> bool:
478
+ if callable(obj):
479
+ # For pytest fixtures the __repr__ method provides more information than the function name.
480
+ return isinstance(obj, FixtureFunctionDefinition)
481
+
482
+ try:
483
+ return not hasattr(obj, "__name__")
484
+ except Exception:
485
+ return True
486
+
487
+
488
+ def _format_boolop(explanations: Iterable[str], is_or: bool) -> str:
489
+ explanation = "(" + ((is_or and " or ") or " and ").join(explanations) + ")"
490
+ return explanation.replace("%", "%%")
491
+
492
+
493
+ def _call_reprcompare(
494
+ ops: Sequence[str],
495
+ results: Sequence[bool],
496
+ expls: Sequence[str],
497
+ each_obj: Sequence[object],
498
+ ) -> str:
499
+ for i, res, expl in zip(range(len(ops)), results, expls, strict=True):
500
+ try:
501
+ done = not res
502
+ except Exception:
503
+ done = True
504
+ if done:
505
+ break
506
+ if util._reprcompare is not None:
507
+ custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
508
+ if custom is not None:
509
+ return custom
510
+ return expl
511
+
512
+
513
+ def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
514
+ if util._assertion_pass is not None:
515
+ util._assertion_pass(lineno, orig, expl)
516
+
517
+
518
+ def _check_if_assertion_pass_impl() -> bool:
519
+ """Check if any plugins implement the pytest_assertion_pass hook
520
+ in order not to generate explanation unnecessarily (might be expensive)."""
521
+ return True if util._assertion_pass else False
522
+
523
+
524
+ UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
525
+
526
+ BINOP_MAP = {
527
+ ast.BitOr: "|",
528
+ ast.BitXor: "^",
529
+ ast.BitAnd: "&",
530
+ ast.LShift: "<<",
531
+ ast.RShift: ">>",
532
+ ast.Add: "+",
533
+ ast.Sub: "-",
534
+ ast.Mult: "*",
535
+ ast.Div: "/",
536
+ ast.FloorDiv: "//",
537
+ ast.Mod: "%%", # escaped for string formatting
538
+ ast.Eq: "==",
539
+ ast.NotEq: "!=",
540
+ ast.Lt: "<",
541
+ ast.LtE: "<=",
542
+ ast.Gt: ">",
543
+ ast.GtE: ">=",
544
+ ast.Pow: "**",
545
+ ast.Is: "is",
546
+ ast.IsNot: "is not",
547
+ ast.In: "in",
548
+ ast.NotIn: "not in",
549
+ ast.MatMult: "@",
550
+ }
551
+
552
+
553
+ def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
554
+ """Recursively yield node and all its children in depth-first order."""
555
+ yield node
556
+ for child in ast.iter_child_nodes(node):
557
+ yield from traverse_node(child)
558
+
559
+
560
+ @functools.lru_cache(maxsize=1)
561
+ def _get_assertion_exprs(src: bytes) -> dict[int, str]:
562
+ """Return a mapping from {lineno: "assertion test expression"}."""
563
+ ret: dict[int, str] = {}
564
+
565
+ depth = 0
566
+ lines: list[str] = []
567
+ assert_lineno: int | None = None
568
+ seen_lines: set[int] = set()
569
+
570
+ def _write_and_reset() -> None:
571
+ nonlocal depth, lines, assert_lineno, seen_lines
572
+ assert assert_lineno is not None
573
+ ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\")
574
+ depth = 0
575
+ lines = []
576
+ assert_lineno = None
577
+ seen_lines = set()
578
+
579
+ tokens = tokenize.tokenize(io.BytesIO(src).readline)
580
+ for tp, source, (lineno, offset), _, line in tokens:
581
+ if tp == tokenize.NAME and source == "assert":
582
+ assert_lineno = lineno
583
+ elif assert_lineno is not None:
584
+ # keep track of depth for the assert-message `,` lookup
585
+ if tp == tokenize.OP and source in "([{":
586
+ depth += 1
587
+ elif tp == tokenize.OP and source in ")]}":
588
+ depth -= 1
589
+
590
+ if not lines:
591
+ lines.append(line[offset:])
592
+ seen_lines.add(lineno)
593
+ # a non-nested comma separates the expression from the message
594
+ elif depth == 0 and tp == tokenize.OP and source == ",":
595
+ # one line assert with message
596
+ if lineno in seen_lines and len(lines) == 1:
597
+ offset_in_trimmed = offset + len(lines[-1]) - len(line)
598
+ lines[-1] = lines[-1][:offset_in_trimmed]
599
+ # multi-line assert with message
600
+ elif lineno in seen_lines:
601
+ lines[-1] = lines[-1][:offset]
602
+ # multi line assert with escaped newline before message
603
+ else:
604
+ lines.append(line[:offset])
605
+ _write_and_reset()
606
+ elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}:
607
+ _write_and_reset()
608
+ elif lines and lineno not in seen_lines:
609
+ lines.append(line)
610
+ seen_lines.add(lineno)
611
+
612
+ return ret
613
+
614
+
615
+ class AssertionRewriter(ast.NodeVisitor):
616
+ """Assertion rewriting implementation.
617
+
618
+ The main entrypoint is to call .run() with an ast.Module instance,
619
+ this will then find all the assert statements and rewrite them to
620
+ provide intermediate values and a detailed assertion error. See
621
+ http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
622
+ for an overview of how this works.
623
+
624
+ The entry point here is .run() which will iterate over all the
625
+ statements in an ast.Module and for each ast.Assert statement it
626
+ finds call .visit() with it. Then .visit_Assert() takes over and
627
+ is responsible for creating new ast statements to replace the
628
+ original assert statement: it rewrites the test of an assertion
629
+ to provide intermediate values and replace it with an if statement
630
+ which raises an assertion error with a detailed explanation in
631
+ case the expression is false and calls pytest_assertion_pass hook
632
+ if expression is true.
633
+
634
+ For this .visit_Assert() uses the visitor pattern to visit all the
635
+ AST nodes of the ast.Assert.test field, each visit call returning
636
+ an AST node and the corresponding explanation string. During this
637
+ state is kept in several instance attributes:
638
+
639
+ :statements: All the AST statements which will replace the assert
640
+ statement.
641
+
642
+ :variables: This is populated by .variable() with each variable
643
+ used by the statements so that they can all be set to None at
644
+ the end of the statements.
645
+
646
+ :variable_counter: Counter to create new unique variables needed
647
+ by statements. Variables are created using .variable() and
648
+ have the form of "@py_assert0".
649
+
650
+ :expl_stmts: The AST statements which will be executed to get
651
+ data from the assertion. This is the code which will construct
652
+ the detailed assertion message that is used in the AssertionError
653
+ or for the pytest_assertion_pass hook.
654
+
655
+ :explanation_specifiers: A dict filled by .explanation_param()
656
+ with %-formatting placeholders and their corresponding
657
+ expressions to use in the building of an assertion message.
658
+ This is used by .pop_format_context() to build a message.
659
+
660
+ :stack: A stack of the explanation_specifiers dicts maintained by
661
+ .push_format_context() and .pop_format_context() which allows
662
+ to build another %-formatted string while already building one.
663
+
664
+ :scope: A tuple containing the current scope used for variables_overwrite.
665
+
666
+ :variables_overwrite: A dict filled with references to variables
667
+ that change value within an assert. This happens when a variable is
668
+ reassigned with the walrus operator
669
+
670
+ This state, except the variables_overwrite, is reset on every new assert
671
+ statement visited and used by the other visitors.
672
+ """
673
+
674
+ def __init__(
675
+ self, module_path: str | None, config: Config | None, source: bytes
676
+ ) -> None:
677
+ super().__init__()
678
+ self.module_path = module_path
679
+ self.config = config
680
+ if config is not None:
681
+ self.enable_assertion_pass_hook = config.getini(
682
+ "enable_assertion_pass_hook"
683
+ )
684
+ else:
685
+ self.enable_assertion_pass_hook = False
686
+ self.source = source
687
+ self.scope: tuple[ast.AST, ...] = ()
688
+ self.variables_overwrite: defaultdict[tuple[ast.AST, ...], dict[str, str]] = (
689
+ defaultdict(dict)
690
+ )
691
+
692
+ def run(self, mod: ast.Module) -> None:
693
+ """Find all assert statements in *mod* and rewrite them."""
694
+ if not mod.body:
695
+ # Nothing to do.
696
+ return
697
+
698
+ # We'll insert some special imports at the top of the module, but after any
699
+ # docstrings and __future__ imports, so first figure out where that is.
700
+ doc = getattr(mod, "docstring", None)
701
+ expect_docstring = doc is None
702
+ if doc is not None and self.is_rewrite_disabled(doc):
703
+ return
704
+ pos = 0
705
+ for item in mod.body:
706
+ match item:
707
+ case ast.Expr(value=ast.Constant(value=str() as doc)) if (
708
+ expect_docstring
709
+ ):
710
+ if self.is_rewrite_disabled(doc):
711
+ return
712
+ expect_docstring = False
713
+ case ast.ImportFrom(level=0, module="__future__"):
714
+ pass
715
+ case _:
716
+ break
717
+ pos += 1
718
+ # Special case: for a decorated function, set the lineno to that of the
719
+ # first decorator, not the `def`. Issue #4984.
720
+ if isinstance(item, ast.FunctionDef) and item.decorator_list:
721
+ lineno = item.decorator_list[0].lineno
722
+ else:
723
+ lineno = item.lineno
724
+ # Now actually insert the special imports.
725
+ aliases = [
726
+ ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0),
727
+ ast.alias(
728
+ "_pytest.assertion.rewrite",
729
+ "@pytest_ar",
730
+ lineno=lineno,
731
+ col_offset=0,
732
+ ),
733
+ ]
734
+ imports = [
735
+ ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases
736
+ ]
737
+ mod.body[pos:pos] = imports
738
+
739
+ # Collect asserts.
740
+ self.scope = (mod,)
741
+ nodes: list[ast.AST | Sentinel] = [mod]
742
+ while nodes:
743
+ node = nodes.pop()
744
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
745
+ self.scope = tuple((*self.scope, node))
746
+ nodes.append(_SCOPE_END_MARKER)
747
+ if node == _SCOPE_END_MARKER:
748
+ self.scope = self.scope[:-1]
749
+ continue
750
+ assert isinstance(node, ast.AST)
751
+ for name, field in ast.iter_fields(node):
752
+ if isinstance(field, list):
753
+ new: list[ast.AST] = []
754
+ for i, child in enumerate(field):
755
+ if isinstance(child, ast.Assert):
756
+ # Transform assert.
757
+ new.extend(self.visit(child))
758
+ else:
759
+ new.append(child)
760
+ if isinstance(child, ast.AST):
761
+ nodes.append(child)
762
+ setattr(node, name, new)
763
+ elif (
764
+ isinstance(field, ast.AST)
765
+ # Don't recurse into expressions as they can't contain
766
+ # asserts.
767
+ and not isinstance(field, ast.expr)
768
+ ):
769
+ nodes.append(field)
770
+
771
+ @staticmethod
772
+ def is_rewrite_disabled(docstring: str) -> bool:
773
+ return "PYTEST_DONT_REWRITE" in docstring
774
+
775
+ def variable(self) -> str:
776
+ """Get a new variable."""
777
+ # Use a character invalid in python identifiers to avoid clashing.
778
+ name = "@py_assert" + str(next(self.variable_counter))
779
+ self.variables.append(name)
780
+ return name
781
+
782
+ def assign(self, expr: ast.expr) -> ast.Name:
783
+ """Give *expr* a name."""
784
+ name = self.variable()
785
+ self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
786
+ return ast.copy_location(ast.Name(name, ast.Load()), expr)
787
+
788
+ def display(self, expr: ast.expr) -> ast.expr:
789
+ """Call saferepr on the expression."""
790
+ return self.helper("_saferepr", expr)
791
+
792
+ def helper(self, name: str, *args: ast.expr) -> ast.expr:
793
+ """Call a helper in this module."""
794
+ py_name = ast.Name("@pytest_ar", ast.Load())
795
+ attr = ast.Attribute(py_name, name, ast.Load())
796
+ return ast.Call(attr, list(args), [])
797
+
798
+ def builtin(self, name: str) -> ast.Attribute:
799
+ """Return the builtin called *name*."""
800
+ builtin_name = ast.Name("@py_builtins", ast.Load())
801
+ return ast.Attribute(builtin_name, name, ast.Load())
802
+
803
+ def explanation_param(self, expr: ast.expr) -> str:
804
+ """Return a new named %-formatting placeholder for expr.
805
+
806
+ This creates a %-formatting placeholder for expr in the
807
+ current formatting context, e.g. ``%(py0)s``. The placeholder
808
+ and expr are placed in the current format context so that it
809
+ can be used on the next call to .pop_format_context().
810
+ """
811
+ specifier = "py" + str(next(self.variable_counter))
812
+ self.explanation_specifiers[specifier] = expr
813
+ return "%(" + specifier + ")s"
814
+
815
+ def push_format_context(self) -> None:
816
+ """Create a new formatting context.
817
+
818
+ The format context is used for when an explanation wants to
819
+ have a variable value formatted in the assertion message. In
820
+ this case the value required can be added using
821
+ .explanation_param(). Finally .pop_format_context() is used
822
+ to format a string of %-formatted values as added by
823
+ .explanation_param().
824
+ """
825
+ self.explanation_specifiers: dict[str, ast.expr] = {}
826
+ self.stack.append(self.explanation_specifiers)
827
+
828
+ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
829
+ """Format the %-formatted string with current format context.
830
+
831
+ The expl_expr should be an str ast.expr instance constructed from
832
+ the %-placeholders created by .explanation_param(). This will
833
+ add the required code to format said string to .expl_stmts and
834
+ return the ast.Name instance of the formatted string.
835
+ """
836
+ current = self.stack.pop()
837
+ if self.stack:
838
+ self.explanation_specifiers = self.stack[-1]
839
+ keys: list[ast.expr | None] = [ast.Constant(key) for key in current.keys()]
840
+ format_dict = ast.Dict(keys, list(current.values()))
841
+ form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
842
+ name = "@py_format" + str(next(self.variable_counter))
843
+ if self.enable_assertion_pass_hook:
844
+ self.format_variables.append(name)
845
+ self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form))
846
+ return ast.Name(name, ast.Load())
847
+
848
+ def generic_visit(self, node: ast.AST) -> tuple[ast.Name, str]:
849
+ """Handle expressions we don't have custom code for."""
850
+ assert isinstance(node, ast.expr)
851
+ res = self.assign(node)
852
+ return res, self.explanation_param(self.display(res))
853
+
854
+ def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]:
855
+ """Return the AST statements to replace the ast.Assert instance.
856
+
857
+ This rewrites the test of an assertion to provide
858
+ intermediate values and replace it with an if statement which
859
+ raises an assertion error with a detailed explanation in case
860
+ the expression is false.
861
+ """
862
+ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
863
+ import warnings
864
+
865
+ from _pytest.warning_types import PytestAssertRewriteWarning
866
+
867
+ # TODO: This assert should not be needed.
868
+ assert self.module_path is not None
869
+ warnings.warn_explicit(
870
+ PytestAssertRewriteWarning(
871
+ "assertion is always true, perhaps remove parentheses?"
872
+ ),
873
+ category=None,
874
+ filename=self.module_path,
875
+ lineno=assert_.lineno,
876
+ )
877
+
878
+ self.statements: list[ast.stmt] = []
879
+ self.variables: list[str] = []
880
+ self.variable_counter = itertools.count()
881
+
882
+ if self.enable_assertion_pass_hook:
883
+ self.format_variables: list[str] = []
884
+
885
+ self.stack: list[dict[str, ast.expr]] = []
886
+ self.expl_stmts: list[ast.stmt] = []
887
+ self.push_format_context()
888
+ # Rewrite assert into a bunch of statements.
889
+ top_condition, explanation = self.visit(assert_.test)
890
+
891
+ negation = ast.UnaryOp(ast.Not(), top_condition)
892
+
893
+ if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
894
+ msg = self.pop_format_context(ast.Constant(explanation))
895
+
896
+ # Failed
897
+ if assert_.msg:
898
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
899
+ gluestr = "\n>assert "
900
+ else:
901
+ assertmsg = ast.Constant("")
902
+ gluestr = "assert "
903
+ err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
904
+ err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
905
+ err_name = ast.Name("AssertionError", ast.Load())
906
+ fmt = self.helper("_format_explanation", err_msg)
907
+ exc = ast.Call(err_name, [fmt], [])
908
+ raise_ = ast.Raise(exc, None)
909
+ statements_fail = []
910
+ statements_fail.extend(self.expl_stmts)
911
+ statements_fail.append(raise_)
912
+
913
+ # Passed
914
+ fmt_pass = self.helper("_format_explanation", msg)
915
+ orig = _get_assertion_exprs(self.source)[assert_.lineno]
916
+ hook_call_pass = ast.Expr(
917
+ self.helper(
918
+ "_call_assertion_pass",
919
+ ast.Constant(assert_.lineno),
920
+ ast.Constant(orig),
921
+ fmt_pass,
922
+ )
923
+ )
924
+ # If any hooks implement assert_pass hook
925
+ hook_impl_test = ast.If(
926
+ self.helper("_check_if_assertion_pass_impl"),
927
+ [*self.expl_stmts, hook_call_pass],
928
+ [],
929
+ )
930
+ statements_pass: list[ast.stmt] = [hook_impl_test]
931
+
932
+ # Test for assertion condition
933
+ main_test = ast.If(negation, statements_fail, statements_pass)
934
+ self.statements.append(main_test)
935
+ if self.format_variables:
936
+ variables: list[ast.expr] = [
937
+ ast.Name(name, ast.Store()) for name in self.format_variables
938
+ ]
939
+ clear_format = ast.Assign(variables, ast.Constant(None))
940
+ self.statements.append(clear_format)
941
+
942
+ else: # Original assertion rewriting
943
+ # Create failure message.
944
+ body = self.expl_stmts
945
+ self.statements.append(ast.If(negation, body, []))
946
+ if assert_.msg:
947
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
948
+ explanation = "\n>assert " + explanation
949
+ else:
950
+ assertmsg = ast.Constant("")
951
+ explanation = "assert " + explanation
952
+ template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
953
+ msg = self.pop_format_context(template)
954
+ fmt = self.helper("_format_explanation", msg)
955
+ err_name = ast.Name("AssertionError", ast.Load())
956
+ exc = ast.Call(err_name, [fmt], [])
957
+ raise_ = ast.Raise(exc, None)
958
+
959
+ body.append(raise_)
960
+
961
+ # Clear temporary variables by setting them to None.
962
+ if self.variables:
963
+ variables = [ast.Name(name, ast.Store()) for name in self.variables]
964
+ clear = ast.Assign(variables, ast.Constant(None))
965
+ self.statements.append(clear)
966
+ # Fix locations (line numbers/column offsets).
967
+ for stmt in self.statements:
968
+ for node in traverse_node(stmt):
969
+ if getattr(node, "lineno", None) is None:
970
+ # apply the assertion location to all generated ast nodes without source location
971
+ # and preserve the location of existing nodes or generated nodes with an correct location.
972
+ ast.copy_location(node, assert_)
973
+ return self.statements
974
+
975
+ def visit_NamedExpr(self, name: ast.NamedExpr) -> tuple[ast.NamedExpr, str]:
976
+ # This method handles the 'walrus operator' repr of the target
977
+ # name if it's a local variable or _should_repr_global_name()
978
+ # thinks it's acceptable.
979
+ locs = ast.Call(self.builtin("locals"), [], [])
980
+ target_id = name.target.id
981
+ inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
982
+ dorepr = self.helper("_should_repr_global_name", name)
983
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
984
+ expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
985
+ return name, self.explanation_param(expr)
986
+
987
+ def visit_Name(self, name: ast.Name) -> tuple[ast.Name, str]:
988
+ # Display the repr of the name if it's a local variable or
989
+ # _should_repr_global_name() thinks it's acceptable.
990
+ locs = ast.Call(self.builtin("locals"), [], [])
991
+ inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs])
992
+ dorepr = self.helper("_should_repr_global_name", name)
993
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
994
+ expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
995
+ return name, self.explanation_param(expr)
996
+
997
+ def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]:
998
+ res_var = self.variable()
999
+ expl_list = self.assign(ast.List([], ast.Load()))
1000
+ app = ast.Attribute(expl_list, "append", ast.Load())
1001
+ is_or = int(isinstance(boolop.op, ast.Or))
1002
+ body = save = self.statements
1003
+ fail_save = self.expl_stmts
1004
+ levels = len(boolop.values) - 1
1005
+ self.push_format_context()
1006
+ # Process each operand, short-circuiting if needed.
1007
+ for i, v in enumerate(boolop.values):
1008
+ if i:
1009
+ fail_inner: list[ast.stmt] = []
1010
+ # cond is set in a prior loop iteration below
1011
+ self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
1012
+ self.expl_stmts = fail_inner
1013
+ match v:
1014
+ # Check if the left operand is an ast.NamedExpr and the value has already been visited
1015
+ case ast.Compare(
1016
+ left=ast.NamedExpr(target=ast.Name(id=target_id))
1017
+ ) if target_id in [
1018
+ e.id for e in boolop.values[:i] if hasattr(e, "id")
1019
+ ]:
1020
+ pytest_temp = self.variable()
1021
+ self.variables_overwrite[self.scope][target_id] = v.left # type:ignore[assignment]
1022
+ # mypy's false positive, we're checking that the 'target' attribute exists.
1023
+ v.left.target.id = pytest_temp # type:ignore[attr-defined]
1024
+ self.push_format_context()
1025
+ res, expl = self.visit(v)
1026
+ body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
1027
+ expl_format = self.pop_format_context(ast.Constant(expl))
1028
+ call = ast.Call(app, [expl_format], [])
1029
+ self.expl_stmts.append(ast.Expr(call))
1030
+ if i < levels:
1031
+ cond: ast.expr = res
1032
+ if is_or:
1033
+ cond = ast.UnaryOp(ast.Not(), cond)
1034
+ inner: list[ast.stmt] = []
1035
+ self.statements.append(ast.If(cond, inner, []))
1036
+ self.statements = body = inner
1037
+ self.statements = save
1038
+ self.expl_stmts = fail_save
1039
+ expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
1040
+ expl = self.pop_format_context(expl_template)
1041
+ return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
1042
+
1043
+ def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]:
1044
+ pattern = UNARY_MAP[unary.op.__class__]
1045
+ operand_res, operand_expl = self.visit(unary.operand)
1046
+ res = self.assign(ast.copy_location(ast.UnaryOp(unary.op, operand_res), unary))
1047
+ return res, pattern % (operand_expl,)
1048
+
1049
+ def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]:
1050
+ symbol = BINOP_MAP[binop.op.__class__]
1051
+ left_expr, left_expl = self.visit(binop.left)
1052
+ right_expr, right_expl = self.visit(binop.right)
1053
+ explanation = f"({left_expl} {symbol} {right_expl})"
1054
+ res = self.assign(
1055
+ ast.copy_location(ast.BinOp(left_expr, binop.op, right_expr), binop)
1056
+ )
1057
+ return res, explanation
1058
+
1059
+ def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]:
1060
+ new_func, func_expl = self.visit(call.func)
1061
+ arg_expls = []
1062
+ new_args = []
1063
+ new_kwargs = []
1064
+ for arg in call.args:
1065
+ if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
1066
+ self.scope, {}
1067
+ ):
1068
+ arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
1069
+ res, expl = self.visit(arg)
1070
+ arg_expls.append(expl)
1071
+ new_args.append(res)
1072
+ for keyword in call.keywords:
1073
+ match keyword.value:
1074
+ case ast.Name(id=id) if id in self.variables_overwrite.get(
1075
+ self.scope, {}
1076
+ ):
1077
+ keyword.value = self.variables_overwrite[self.scope][id] # type:ignore[assignment]
1078
+ res, expl = self.visit(keyword.value)
1079
+ new_kwargs.append(ast.keyword(keyword.arg, res))
1080
+ if keyword.arg:
1081
+ arg_expls.append(keyword.arg + "=" + expl)
1082
+ else: # **args have `arg` keywords with an .arg of None
1083
+ arg_expls.append("**" + expl)
1084
+
1085
+ expl = "{}({})".format(func_expl, ", ".join(arg_expls))
1086
+ new_call = ast.copy_location(ast.Call(new_func, new_args, new_kwargs), call)
1087
+ res = self.assign(new_call)
1088
+ res_expl = self.explanation_param(self.display(res))
1089
+ outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
1090
+ return res, outer_expl
1091
+
1092
+ def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]:
1093
+ # A Starred node can appear in a function call.
1094
+ res, expl = self.visit(starred.value)
1095
+ new_starred = ast.Starred(res, starred.ctx)
1096
+ return new_starred, "*" + expl
1097
+
1098
+ def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]:
1099
+ if not isinstance(attr.ctx, ast.Load):
1100
+ return self.generic_visit(attr)
1101
+ value, value_expl = self.visit(attr.value)
1102
+ res = self.assign(
1103
+ ast.copy_location(ast.Attribute(value, attr.attr, ast.Load()), attr)
1104
+ )
1105
+ res_expl = self.explanation_param(self.display(res))
1106
+ pat = "%s\n{%s = %s.%s\n}"
1107
+ expl = pat % (res_expl, res_expl, value_expl, attr.attr)
1108
+ return res, expl
1109
+
1110
+ def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]:
1111
+ self.push_format_context()
1112
+ # We first check if we have overwritten a variable in the previous assert
1113
+ match comp.left:
1114
+ case ast.Name(id=name_id) if name_id in self.variables_overwrite.get(
1115
+ self.scope, {}
1116
+ ):
1117
+ comp.left = self.variables_overwrite[self.scope][name_id] # type: ignore[assignment]
1118
+ case ast.NamedExpr(target=ast.Name(id=target_id)):
1119
+ self.variables_overwrite[self.scope][target_id] = comp.left # type: ignore[assignment]
1120
+ left_res, left_expl = self.visit(comp.left)
1121
+ if isinstance(comp.left, ast.Compare | ast.BoolOp):
1122
+ left_expl = f"({left_expl})"
1123
+ res_variables = [self.variable() for i in range(len(comp.ops))]
1124
+ load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables]
1125
+ store_names = [ast.Name(v, ast.Store()) for v in res_variables]
1126
+ it = zip(range(len(comp.ops)), comp.ops, comp.comparators, strict=True)
1127
+ expls: list[ast.expr] = []
1128
+ syms: list[ast.expr] = []
1129
+ results = [left_res]
1130
+ for i, op, next_operand in it:
1131
+ match (next_operand, left_res):
1132
+ case (
1133
+ ast.NamedExpr(target=ast.Name(id=target_id)),
1134
+ ast.Name(id=name_id),
1135
+ ) if target_id == name_id:
1136
+ next_operand.target.id = self.variable()
1137
+ self.variables_overwrite[self.scope][name_id] = next_operand # type: ignore[assignment]
1138
+
1139
+ next_res, next_expl = self.visit(next_operand)
1140
+ if isinstance(next_operand, ast.Compare | ast.BoolOp):
1141
+ next_expl = f"({next_expl})"
1142
+ results.append(next_res)
1143
+ sym = BINOP_MAP[op.__class__]
1144
+ syms.append(ast.Constant(sym))
1145
+ expl = f"{left_expl} {sym} {next_expl}"
1146
+ expls.append(ast.Constant(expl))
1147
+ res_expr = ast.copy_location(ast.Compare(left_res, [op], [next_res]), comp)
1148
+ self.statements.append(ast.Assign([store_names[i]], res_expr))
1149
+ left_res, left_expl = next_res, next_expl
1150
+ # Use pytest.assertion.util._reprcompare if that's available.
1151
+ expl_call = self.helper(
1152
+ "_call_reprcompare",
1153
+ ast.Tuple(syms, ast.Load()),
1154
+ ast.Tuple(load_names, ast.Load()),
1155
+ ast.Tuple(expls, ast.Load()),
1156
+ ast.Tuple(results, ast.Load()),
1157
+ )
1158
+ if len(comp.ops) > 1:
1159
+ res: ast.expr = ast.BoolOp(ast.And(), load_names)
1160
+ else:
1161
+ res = load_names[0]
1162
+
1163
+ return res, self.explanation_param(self.pop_format_context(expl_call))
1164
+
1165
+
1166
+ def try_makedirs(cache_dir: Path) -> bool:
1167
+ """Attempt to create the given directory and sub-directories exist.
1168
+
1169
+ Returns True if successful or if it already exists.
1170
+ """
1171
+ try:
1172
+ os.makedirs(cache_dir, exist_ok=True)
1173
+ except (FileNotFoundError, NotADirectoryError, FileExistsError):
1174
+ # One of the path components was not a directory:
1175
+ # - we're in a zip file
1176
+ # - it is a file
1177
+ return False
1178
+ except PermissionError:
1179
+ return False
1180
+ except OSError as e:
1181
+ # as of now, EROFS doesn't have an equivalent OSError-subclass
1182
+ #
1183
+ # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not
1184
+ # implemented" for a read-only error
1185
+ if e.errno in {errno.EROFS, errno.ENOSYS}:
1186
+ return False
1187
+ raise
1188
+ return True
1189
+
1190
+
1191
+ def get_cache_dir(file_path: Path) -> Path:
1192
+ """Return the cache directory to write .pyc files for the given .py file path."""
1193
+ if sys.pycache_prefix:
1194
+ # given:
1195
+ # prefix = '/tmp/pycs'
1196
+ # path = '/home/user/proj/test_app.py'
1197
+ # we want:
1198
+ # '/tmp/pycs/home/user/proj'
1199
+ return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1])
1200
+ else:
1201
+ # classic pycache directory
1202
+ return file_path.parent / "__pycache__"
py311/lib/python3.11/site-packages/_pytest/assertion/truncate.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Check if truncation required
64
+ input_char_count = len("".join(input_lines))
65
+ # The length of the truncation explanation depends on the number of lines
66
+ # removed but is at least 68 characters:
67
+ # The real value is
68
+ # 64 (for the base message:
69
+ # '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
70
+ # )
71
+ # + 1 (for plural)
72
+ # + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
73
+ # + 3 for the '...' added to the truncated line
74
+ # But if there's more than 100 lines it's very likely that we're going to
75
+ # truncate, so we don't need the exact value using log10.
76
+ tolerable_max_chars = (
77
+ max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
78
+ )
79
+ # The truncation explanation add two lines to the output
80
+ tolerable_max_lines = max_lines + 2
81
+ if (
82
+ len(input_lines) <= tolerable_max_lines
83
+ and input_char_count <= tolerable_max_chars
84
+ ):
85
+ return input_lines
86
+ # Truncate first to max_lines, and then truncate to max_chars if necessary
87
+ if max_lines > 0:
88
+ truncated_explanation = input_lines[:max_lines]
89
+ else:
90
+ truncated_explanation = input_lines
91
+ truncated_char = True
92
+ # We reevaluate the need to truncate chars following removal of some lines
93
+ if len("".join(truncated_explanation)) > tolerable_max_chars and max_chars > 0:
94
+ truncated_explanation = _truncate_by_char_count(
95
+ truncated_explanation, max_chars
96
+ )
97
+ else:
98
+ truncated_char = False
99
+
100
+ if truncated_explanation == input_lines:
101
+ # No truncation happened, so we do not need to add any explanations
102
+ return truncated_explanation
103
+
104
+ truncated_line_count = len(input_lines) - len(truncated_explanation)
105
+ if truncated_explanation[-1]:
106
+ # Add ellipsis and take into account part-truncated final line
107
+ truncated_explanation[-1] = truncated_explanation[-1] + "..."
108
+ if truncated_char:
109
+ # It's possible that we did not remove any char from this line
110
+ truncated_line_count += 1
111
+ else:
112
+ # Add proper ellipsis when we were able to fit a full line exactly
113
+ truncated_explanation[-1] = "..."
114
+ return [
115
+ *truncated_explanation,
116
+ "",
117
+ f"...Full output truncated ({truncated_line_count} line"
118
+ f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
119
+ ]
120
+
121
+
122
+ def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]:
123
+ # Find point at which input length exceeds total allowed length
124
+ iterated_char_count = 0
125
+ for iterated_index, input_line in enumerate(input_lines):
126
+ if iterated_char_count + len(input_line) > max_chars:
127
+ break
128
+ iterated_char_count += len(input_line)
129
+
130
+ # Create truncated explanation with modified final line
131
+ truncated_result = input_lines[:iterated_index]
132
+ final_line = input_lines[iterated_index]
133
+ if final_line:
134
+ final_line_truncate_point = max_chars - iterated_char_count
135
+ final_line = final_line[:final_line_truncate_point]
136
+ truncated_result.append(final_line)
137
+ return truncated_result
py311/lib/python3.11/site-packages/_pytest/assertion/util.py ADDED
@@ -0,0 +1,615 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """Utilities for assertion debugging."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import collections.abc
7
+ from collections.abc import Callable
8
+ from collections.abc import Iterable
9
+ from collections.abc import Mapping
10
+ from collections.abc import Sequence
11
+ from collections.abc import Set as AbstractSet
12
+ import pprint
13
+ from typing import Any
14
+ from typing import Literal
15
+ from typing import Protocol
16
+ from unicodedata import normalize
17
+
18
+ from _pytest import outcomes
19
+ import _pytest._code
20
+ from _pytest._io.pprint import PrettyPrinter
21
+ from _pytest._io.saferepr import saferepr
22
+ from _pytest._io.saferepr import saferepr_unlimited
23
+ from _pytest.compat import running_on_ci
24
+ from _pytest.config import Config
25
+
26
+
27
+ # The _reprcompare attribute on the util module is used by the new assertion
28
+ # interpretation code and assertion rewriter to detect this plugin was
29
+ # loaded and in turn call the hooks defined here as part of the
30
+ # DebugInterpreter.
31
+ _reprcompare: Callable[[str, object, object], str | None] | None = None
32
+
33
+ # Works similarly as _reprcompare attribute. Is populated with the hook call
34
+ # when pytest_runtest_setup is called.
35
+ _assertion_pass: Callable[[int, str, str], None] | None = None
36
+
37
+ # Config object which is assigned during pytest_runtest_protocol.
38
+ _config: Config | None = None
39
+
40
+
41
+ class _HighlightFunc(Protocol):
42
+ def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
43
+ """Apply highlighting to the given source."""
44
+
45
+
46
+ def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python") -> str:
47
+ """Dummy highlighter that returns the text unprocessed.
48
+
49
+ Needed for _notin_text, as the diff gets post-processed to only show the "+" part.
50
+ """
51
+ return source
52
+
53
+
54
+ def format_explanation(explanation: str) -> str:
55
+ r"""Format an explanation.
56
+
57
+ Normally all embedded newlines are escaped, however there are
58
+ three exceptions: \n{, \n} and \n~. The first two are intended
59
+ cover nested explanations, see function and attribute explanations
60
+ for examples (.visit_Call(), visit_Attribute()). The last one is
61
+ for when one explanation needs to span multiple lines, e.g. when
62
+ displaying diffs.
63
+ """
64
+ lines = _split_explanation(explanation)
65
+ result = _format_lines(lines)
66
+ return "\n".join(result)
67
+
68
+
69
+ def _split_explanation(explanation: str) -> list[str]:
70
+ r"""Return a list of individual lines in the explanation.
71
+
72
+ This will return a list of lines split on '\n{', '\n}' and '\n~'.
73
+ Any other newlines will be escaped and appear in the line as the
74
+ literal '\n' characters.
75
+ """
76
+ raw_lines = (explanation or "").split("\n")
77
+ lines = [raw_lines[0]]
78
+ for values in raw_lines[1:]:
79
+ if values and values[0] in ["{", "}", "~", ">"]:
80
+ lines.append(values)
81
+ else:
82
+ lines[-1] += "\\n" + values
83
+ return lines
84
+
85
+
86
+ def _format_lines(lines: Sequence[str]) -> list[str]:
87
+ """Format the individual lines.
88
+
89
+ This will replace the '{', '}' and '~' characters of our mini formatting
90
+ language with the proper 'where ...', 'and ...' and ' + ...' text, taking
91
+ care of indentation along the way.
92
+
93
+ Return a list of formatted lines.
94
+ """
95
+ result = list(lines[:1])
96
+ stack = [0]
97
+ stackcnt = [0]
98
+ for line in lines[1:]:
99
+ if line.startswith("{"):
100
+ if stackcnt[-1]:
101
+ s = "and "
102
+ else:
103
+ s = "where "
104
+ stack.append(len(result))
105
+ stackcnt[-1] += 1
106
+ stackcnt.append(0)
107
+ result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
108
+ elif line.startswith("}"):
109
+ stack.pop()
110
+ stackcnt.pop()
111
+ result[stack[-1]] += line[1:]
112
+ else:
113
+ assert line[0] in ["~", ">"]
114
+ stack[-1] += 1
115
+ indent = len(stack) if line.startswith("~") else len(stack) - 1
116
+ result.append(" " * indent + line[1:])
117
+ assert len(stack) == 1
118
+ return result
119
+
120
+
121
+ def issequence(x: Any) -> bool:
122
+ return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
123
+
124
+
125
+ def istext(x: Any) -> bool:
126
+ return isinstance(x, str)
127
+
128
+
129
+ def isdict(x: Any) -> bool:
130
+ return isinstance(x, dict)
131
+
132
+
133
+ def isset(x: Any) -> bool:
134
+ return isinstance(x, set | frozenset)
135
+
136
+
137
+ def isnamedtuple(obj: Any) -> bool:
138
+ return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
139
+
140
+
141
+ def isdatacls(obj: Any) -> bool:
142
+ return getattr(obj, "__dataclass_fields__", None) is not None
143
+
144
+
145
+ def isattrs(obj: Any) -> bool:
146
+ return getattr(obj, "__attrs_attrs__", None) is not None
147
+
148
+
149
+ def isiterable(obj: Any) -> bool:
150
+ try:
151
+ iter(obj)
152
+ return not istext(obj)
153
+ except Exception:
154
+ return False
155
+
156
+
157
+ def has_default_eq(
158
+ obj: object,
159
+ ) -> bool:
160
+ """Check if an instance of an object contains the default eq
161
+
162
+ First, we check if the object's __eq__ attribute has __code__,
163
+ if so, we check the equally of the method code filename (__code__.co_filename)
164
+ to the default one generated by the dataclass and attr module
165
+ for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
166
+ """
167
+ # inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
168
+ if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
169
+ code_filename = obj.__eq__.__code__.co_filename
170
+
171
+ if isattrs(obj):
172
+ return "attrs generated " in code_filename
173
+
174
+ return code_filename == "<string>" # data class
175
+ return True
176
+
177
+
178
+ def assertrepr_compare(
179
+ config, op: str, left: Any, right: Any, use_ascii: bool = False
180
+ ) -> list[str] | None:
181
+ """Return specialised explanations for some operators/operands."""
182
+ verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
183
+
184
+ # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
185
+ # See issue #3246.
186
+ use_ascii = (
187
+ isinstance(left, str)
188
+ and isinstance(right, str)
189
+ and normalize("NFD", left) == normalize("NFD", right)
190
+ )
191
+
192
+ if verbose > 1:
193
+ left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
194
+ right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
195
+ else:
196
+ # XXX: "15 chars indentation" is wrong
197
+ # ("E AssertionError: assert "); should use term width.
198
+ maxsize = (
199
+ 80 - 15 - len(op) - 2
200
+ ) // 2 # 15 chars indentation, 1 space around op
201
+
202
+ left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
203
+ right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
204
+
205
+ summary = f"{left_repr} {op} {right_repr}"
206
+ highlighter = config.get_terminal_writer()._highlight
207
+
208
+ explanation = None
209
+ try:
210
+ if op == "==":
211
+ explanation = _compare_eq_any(left, right, highlighter, verbose)
212
+ elif op == "not in":
213
+ if istext(left) and istext(right):
214
+ explanation = _notin_text(left, right, verbose)
215
+ elif op == "!=":
216
+ if isset(left) and isset(right):
217
+ explanation = ["Both sets are equal"]
218
+ elif op == ">=":
219
+ if isset(left) and isset(right):
220
+ explanation = _compare_gte_set(left, right, highlighter, verbose)
221
+ elif op == "<=":
222
+ if isset(left) and isset(right):
223
+ explanation = _compare_lte_set(left, right, highlighter, verbose)
224
+ elif op == ">":
225
+ if isset(left) and isset(right):
226
+ explanation = _compare_gt_set(left, right, highlighter, verbose)
227
+ elif op == "<":
228
+ if isset(left) and isset(right):
229
+ explanation = _compare_lt_set(left, right, highlighter, verbose)
230
+
231
+ except outcomes.Exit:
232
+ raise
233
+ except Exception:
234
+ repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
235
+ explanation = [
236
+ f"(pytest_assertion plugin: representation of details failed: {repr_crash}.",
237
+ " Probably an object has a faulty __repr__.)",
238
+ ]
239
+
240
+ if not explanation:
241
+ return None
242
+
243
+ if explanation[0] != "":
244
+ explanation = ["", *explanation]
245
+ return [summary, *explanation]
246
+
247
+
248
+ def _compare_eq_any(
249
+ left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0
250
+ ) -> list[str]:
251
+ explanation = []
252
+ if istext(left) and istext(right):
253
+ explanation = _diff_text(left, right, highlighter, verbose)
254
+ else:
255
+ from _pytest.python_api import ApproxBase
256
+
257
+ if isinstance(left, ApproxBase) or isinstance(right, ApproxBase):
258
+ # Although the common order should be obtained == expected, this ensures both ways
259
+ approx_side = left if isinstance(left, ApproxBase) else right
260
+ other_side = right if isinstance(left, ApproxBase) else left
261
+
262
+ explanation = approx_side._repr_compare(other_side)
263
+ elif type(left) is type(right) and (
264
+ isdatacls(left) or isattrs(left) or isnamedtuple(left)
265
+ ):
266
+ # Note: unlike dataclasses/attrs, namedtuples compare only the
267
+ # field values, not the type or field names. But this branch
268
+ # intentionally only handles the same-type case, which was often
269
+ # used in older code bases before dataclasses/attrs were available.
270
+ explanation = _compare_eq_cls(left, right, highlighter, verbose)
271
+ elif issequence(left) and issequence(right):
272
+ explanation = _compare_eq_sequence(left, right, highlighter, verbose)
273
+ elif isset(left) and isset(right):
274
+ explanation = _compare_eq_set(left, right, highlighter, verbose)
275
+ elif isdict(left) and isdict(right):
276
+ explanation = _compare_eq_dict(left, right, highlighter, verbose)
277
+
278
+ if isiterable(left) and isiterable(right):
279
+ expl = _compare_eq_iterable(left, right, highlighter, verbose)
280
+ explanation.extend(expl)
281
+
282
+ return explanation
283
+
284
+
285
+ def _diff_text(
286
+ left: str, right: str, highlighter: _HighlightFunc, verbose: int = 0
287
+ ) -> list[str]:
288
+ """Return the explanation for the diff between text.
289
+
290
+ Unless --verbose is used this will skip leading and trailing
291
+ characters which are identical to keep the diff minimal.
292
+ """
293
+ from difflib import ndiff
294
+
295
+ explanation: list[str] = []
296
+
297
+ if verbose < 1:
298
+ i = 0 # just in case left or right has zero length
299
+ for i in range(min(len(left), len(right))):
300
+ if left[i] != right[i]:
301
+ break
302
+ if i > 42:
303
+ i -= 10 # Provide some context
304
+ explanation = [
305
+ f"Skipping {i} identical leading characters in diff, use -v to show"
306
+ ]
307
+ left = left[i:]
308
+ right = right[i:]
309
+ if len(left) == len(right):
310
+ for i in range(len(left)):
311
+ if left[-i] != right[-i]:
312
+ break
313
+ if i > 42:
314
+ i -= 10 # Provide some context
315
+ explanation += [
316
+ f"Skipping {i} identical trailing "
317
+ "characters in diff, use -v to show"
318
+ ]
319
+ left = left[:-i]
320
+ right = right[:-i]
321
+ keepends = True
322
+ if left.isspace() or right.isspace():
323
+ left = repr(str(left))
324
+ right = repr(str(right))
325
+ explanation += ["Strings contain only whitespace, escaping them using repr()"]
326
+ # "right" is the expected base against which we compare "left",
327
+ # see https://github.com/pytest-dev/pytest/issues/3333
328
+ explanation.extend(
329
+ highlighter(
330
+ "\n".join(
331
+ line.strip("\n")
332
+ for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
333
+ ),
334
+ lexer="diff",
335
+ ).splitlines()
336
+ )
337
+ return explanation
338
+
339
+
340
+ def _compare_eq_iterable(
341
+ left: Iterable[Any],
342
+ right: Iterable[Any],
343
+ highlighter: _HighlightFunc,
344
+ verbose: int = 0,
345
+ ) -> list[str]:
346
+ if verbose <= 0 and not running_on_ci():
347
+ return ["Use -v to get more diff"]
348
+ # dynamic import to speedup pytest
349
+ import difflib
350
+
351
+ left_formatting = PrettyPrinter().pformat(left).splitlines()
352
+ right_formatting = PrettyPrinter().pformat(right).splitlines()
353
+
354
+ explanation = ["", "Full diff:"]
355
+ # "right" is the expected base against which we compare "left",
356
+ # see https://github.com/pytest-dev/pytest/issues/3333
357
+ explanation.extend(
358
+ highlighter(
359
+ "\n".join(
360
+ line.rstrip()
361
+ for line in difflib.ndiff(right_formatting, left_formatting)
362
+ ),
363
+ lexer="diff",
364
+ ).splitlines()
365
+ )
366
+ return explanation
367
+
368
+
369
+ def _compare_eq_sequence(
370
+ left: Sequence[Any],
371
+ right: Sequence[Any],
372
+ highlighter: _HighlightFunc,
373
+ verbose: int = 0,
374
+ ) -> list[str]:
375
+ comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
376
+ explanation: list[str] = []
377
+ len_left = len(left)
378
+ len_right = len(right)
379
+ for i in range(min(len_left, len_right)):
380
+ if left[i] != right[i]:
381
+ if comparing_bytes:
382
+ # when comparing bytes, we want to see their ascii representation
383
+ # instead of their numeric values (#5260)
384
+ # using a slice gives us the ascii representation:
385
+ # >>> s = b'foo'
386
+ # >>> s[0]
387
+ # 102
388
+ # >>> s[0:1]
389
+ # b'f'
390
+ left_value = left[i : i + 1]
391
+ right_value = right[i : i + 1]
392
+ else:
393
+ left_value = left[i]
394
+ right_value = right[i]
395
+
396
+ explanation.append(
397
+ f"At index {i} diff:"
398
+ f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
399
+ )
400
+ break
401
+
402
+ if comparing_bytes:
403
+ # when comparing bytes, it doesn't help to show the "sides contain one or more
404
+ # items" longer explanation, so skip it
405
+
406
+ return explanation
407
+
408
+ len_diff = len_left - len_right
409
+ if len_diff:
410
+ if len_diff > 0:
411
+ dir_with_more = "Left"
412
+ extra = saferepr(left[len_right])
413
+ else:
414
+ len_diff = 0 - len_diff
415
+ dir_with_more = "Right"
416
+ extra = saferepr(right[len_left])
417
+
418
+ if len_diff == 1:
419
+ explanation += [
420
+ f"{dir_with_more} contains one more item: {highlighter(extra)}"
421
+ ]
422
+ else:
423
+ explanation += [
424
+ f"{dir_with_more} contains {len_diff} more items, first extra item: {highlighter(extra)}"
425
+ ]
426
+ return explanation
427
+
428
+
429
+ def _compare_eq_set(
430
+ left: AbstractSet[Any],
431
+ right: AbstractSet[Any],
432
+ highlighter: _HighlightFunc,
433
+ verbose: int = 0,
434
+ ) -> list[str]:
435
+ explanation = []
436
+ explanation.extend(_set_one_sided_diff("left", left, right, highlighter))
437
+ explanation.extend(_set_one_sided_diff("right", right, left, highlighter))
438
+ return explanation
439
+
440
+
441
+ def _compare_gt_set(
442
+ left: AbstractSet[Any],
443
+ right: AbstractSet[Any],
444
+ highlighter: _HighlightFunc,
445
+ verbose: int = 0,
446
+ ) -> list[str]:
447
+ explanation = _compare_gte_set(left, right, highlighter)
448
+ if not explanation:
449
+ return ["Both sets are equal"]
450
+ return explanation
451
+
452
+
453
+ def _compare_lt_set(
454
+ left: AbstractSet[Any],
455
+ right: AbstractSet[Any],
456
+ highlighter: _HighlightFunc,
457
+ verbose: int = 0,
458
+ ) -> list[str]:
459
+ explanation = _compare_lte_set(left, right, highlighter)
460
+ if not explanation:
461
+ return ["Both sets are equal"]
462
+ return explanation
463
+
464
+
465
+ def _compare_gte_set(
466
+ left: AbstractSet[Any],
467
+ right: AbstractSet[Any],
468
+ highlighter: _HighlightFunc,
469
+ verbose: int = 0,
470
+ ) -> list[str]:
471
+ return _set_one_sided_diff("right", right, left, highlighter)
472
+
473
+
474
+ def _compare_lte_set(
475
+ left: AbstractSet[Any],
476
+ right: AbstractSet[Any],
477
+ highlighter: _HighlightFunc,
478
+ verbose: int = 0,
479
+ ) -> list[str]:
480
+ return _set_one_sided_diff("left", left, right, highlighter)
481
+
482
+
483
+ def _set_one_sided_diff(
484
+ posn: str,
485
+ set1: AbstractSet[Any],
486
+ set2: AbstractSet[Any],
487
+ highlighter: _HighlightFunc,
488
+ ) -> list[str]:
489
+ explanation = []
490
+ diff = set1 - set2
491
+ if diff:
492
+ explanation.append(f"Extra items in the {posn} set:")
493
+ for item in diff:
494
+ explanation.append(highlighter(saferepr(item)))
495
+ return explanation
496
+
497
+
498
+ def _compare_eq_dict(
499
+ left: Mapping[Any, Any],
500
+ right: Mapping[Any, Any],
501
+ highlighter: _HighlightFunc,
502
+ verbose: int = 0,
503
+ ) -> list[str]:
504
+ explanation: list[str] = []
505
+ set_left = set(left)
506
+ set_right = set(right)
507
+ common = set_left.intersection(set_right)
508
+ same = {k: left[k] for k in common if left[k] == right[k]}
509
+ if same and verbose < 2:
510
+ explanation += [f"Omitting {len(same)} identical items, use -vv to show"]
511
+ elif same:
512
+ explanation += ["Common items:"]
513
+ explanation += highlighter(pprint.pformat(same)).splitlines()
514
+ diff = {k for k in common if left[k] != right[k]}
515
+ if diff:
516
+ explanation += ["Differing items:"]
517
+ for k in diff:
518
+ explanation += [
519
+ highlighter(saferepr({k: left[k]}))
520
+ + " != "
521
+ + highlighter(saferepr({k: right[k]}))
522
+ ]
523
+ extra_left = set_left - set_right
524
+ len_extra_left = len(extra_left)
525
+ if len_extra_left:
526
+ explanation.append(
527
+ f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:"
528
+ )
529
+ explanation.extend(
530
+ highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines()
531
+ )
532
+ extra_right = set_right - set_left
533
+ len_extra_right = len(extra_right)
534
+ if len_extra_right:
535
+ explanation.append(
536
+ f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:"
537
+ )
538
+ explanation.extend(
539
+ highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines()
540
+ )
541
+ return explanation
542
+
543
+
544
+ def _compare_eq_cls(
545
+ left: Any, right: Any, highlighter: _HighlightFunc, verbose: int
546
+ ) -> list[str]:
547
+ if not has_default_eq(left):
548
+ return []
549
+ if isdatacls(left):
550
+ import dataclasses
551
+
552
+ all_fields = dataclasses.fields(left)
553
+ fields_to_check = [info.name for info in all_fields if info.compare]
554
+ elif isattrs(left):
555
+ all_fields = left.__attrs_attrs__
556
+ fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
557
+ elif isnamedtuple(left):
558
+ fields_to_check = left._fields
559
+ else:
560
+ assert False
561
+
562
+ indent = " "
563
+ same = []
564
+ diff = []
565
+ for field in fields_to_check:
566
+ if getattr(left, field) == getattr(right, field):
567
+ same.append(field)
568
+ else:
569
+ diff.append(field)
570
+
571
+ explanation = []
572
+ if same or diff:
573
+ explanation += [""]
574
+ if same and verbose < 2:
575
+ explanation.append(f"Omitting {len(same)} identical items, use -vv to show")
576
+ elif same:
577
+ explanation += ["Matching attributes:"]
578
+ explanation += highlighter(pprint.pformat(same)).splitlines()
579
+ if diff:
580
+ explanation += ["Differing attributes:"]
581
+ explanation += highlighter(pprint.pformat(diff)).splitlines()
582
+ for field in diff:
583
+ field_left = getattr(left, field)
584
+ field_right = getattr(right, field)
585
+ explanation += [
586
+ "",
587
+ f"Drill down into differing attribute {field}:",
588
+ f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}",
589
+ ]
590
+ explanation += [
591
+ indent + line
592
+ for line in _compare_eq_any(
593
+ field_left, field_right, highlighter, verbose
594
+ )
595
+ ]
596
+ return explanation
597
+
598
+
599
+ def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]:
600
+ index = text.find(term)
601
+ head = text[:index]
602
+ tail = text[index + len(term) :]
603
+ correct_text = head + tail
604
+ diff = _diff_text(text, correct_text, dummy_highlighter, verbose)
605
+ newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"]
606
+ for line in diff:
607
+ if line.startswith("Skipping"):
608
+ continue
609
+ if line.startswith("- "):
610
+ continue
611
+ if line.startswith("+ "):
612
+ newdiff.append(" " + line[2:])
613
+ else:
614
+ newdiff.append(line)
615
+ return newdiff
py311/lib/python3.11/site-packages/_pytest/config/__init__.py ADDED
@@ -0,0 +1,2197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.metadata
23
+ import inspect
24
+ import os
25
+ import pathlib
26
+ import re
27
+ import shlex
28
+ import sys
29
+ from textwrap import dedent
30
+ import types
31
+ from types import FunctionType
32
+ from typing import Any
33
+ from typing import cast
34
+ from typing import Final
35
+ from typing import final
36
+ from typing import IO
37
+ from typing import TextIO
38
+ from typing import TYPE_CHECKING
39
+ import warnings
40
+
41
+ import pluggy
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 .compat import PathAwareHookProxy
49
+ from .exceptions import PrintHelp as PrintHelp
50
+ from .exceptions import UsageError as UsageError
51
+ from .findpaths import ConfigValue
52
+ from .findpaths import determine_setup
53
+ from _pytest import __version__
54
+ import _pytest._code
55
+ from _pytest._code import ExceptionInfo
56
+ from _pytest._code import filter_traceback
57
+ from _pytest._code.code import TracebackStyle
58
+ from _pytest._io import TerminalWriter
59
+ from _pytest.compat import assert_never
60
+ from _pytest.config.argparsing import Argument
61
+ from _pytest.config.argparsing import FILE_OR_DIR
62
+ from _pytest.config.argparsing import Parser
63
+ import _pytest.deprecated
64
+ import _pytest.hookspec
65
+ from _pytest.outcomes import fail
66
+ from _pytest.outcomes import Skipped
67
+ from _pytest.pathlib import absolutepath
68
+ from _pytest.pathlib import bestrelpath
69
+ from _pytest.pathlib import import_path
70
+ from _pytest.pathlib import ImportMode
71
+ from _pytest.pathlib import resolve_package_path
72
+ from _pytest.pathlib import safe_exists
73
+ from _pytest.stash import Stash
74
+ from _pytest.warning_types import PytestConfigWarning
75
+ from _pytest.warning_types import warn_explicit_for
76
+
77
+
78
+ if TYPE_CHECKING:
79
+ from _pytest.assertion.rewrite import AssertionRewritingHook
80
+ from _pytest.cacheprovider import Cache
81
+ from _pytest.terminal import TerminalReporter
82
+
83
+ _PluggyPlugin = object
84
+ """A type to represent plugin objects.
85
+
86
+ Plugins can be any namespace, so we can't narrow it down much, but we use an
87
+ alias to make the intent clear.
88
+
89
+ Ideally this type would be provided by pluggy itself.
90
+ """
91
+
92
+
93
+ hookimpl = HookimplMarker("pytest")
94
+ hookspec = HookspecMarker("pytest")
95
+
96
+
97
+ @final
98
+ class ExitCode(enum.IntEnum):
99
+ """Encodes the valid exit codes by pytest.
100
+
101
+ Currently users and plugins may supply other exit codes as well.
102
+
103
+ .. versionadded:: 5.0
104
+ """
105
+
106
+ #: Tests passed.
107
+ OK = 0
108
+ #: Tests failed.
109
+ TESTS_FAILED = 1
110
+ #: pytest was interrupted.
111
+ INTERRUPTED = 2
112
+ #: An internal error got in the way.
113
+ INTERNAL_ERROR = 3
114
+ #: pytest was misused.
115
+ USAGE_ERROR = 4
116
+ #: pytest couldn't find tests.
117
+ NO_TESTS_COLLECTED = 5
118
+
119
+ __module__ = "pytest"
120
+
121
+
122
+ class ConftestImportFailure(Exception):
123
+ def __init__(
124
+ self,
125
+ path: pathlib.Path,
126
+ *,
127
+ cause: Exception,
128
+ ) -> None:
129
+ self.path = path
130
+ self.cause = cause
131
+
132
+ def __str__(self) -> str:
133
+ return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
134
+
135
+
136
+ def filter_traceback_for_conftest_import_failure(
137
+ entry: _pytest._code.TracebackEntry,
138
+ ) -> bool:
139
+ """Filter tracebacks entries which point to pytest internals or importlib.
140
+
141
+ Make a special case for importlib because we use it to import test modules and conftest files
142
+ in _pytest.pathlib.import_path.
143
+ """
144
+ return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
145
+
146
+
147
+ def print_conftest_import_error(e: ConftestImportFailure, file: TextIO) -> None:
148
+ exc_info = ExceptionInfo.from_exception(e.cause)
149
+ tw = TerminalWriter(file)
150
+ tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
151
+ exc_info.traceback = exc_info.traceback.filter(
152
+ filter_traceback_for_conftest_import_failure
153
+ )
154
+ exc_repr = (
155
+ exc_info.getrepr(style="short", chain=False)
156
+ if exc_info.traceback
157
+ else exc_info.exconly()
158
+ )
159
+ formatted_tb = str(exc_repr)
160
+ for line in formatted_tb.splitlines():
161
+ tw.line(line.rstrip(), red=True)
162
+
163
+
164
+ def print_usage_error(e: UsageError, file: TextIO) -> None:
165
+ tw = TerminalWriter(file)
166
+ for msg in e.args:
167
+ tw.line(f"ERROR: {msg}\n", red=True)
168
+
169
+
170
+ def main(
171
+ args: list[str] | os.PathLike[str] | None = None,
172
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
173
+ ) -> int | ExitCode:
174
+ """Perform an in-process test run.
175
+
176
+ :param args:
177
+ List of command line arguments. If `None` or not given, defaults to reading
178
+ arguments directly from the process command line (:data:`sys.argv`).
179
+ :param plugins: List of plugin objects to be auto-registered during initialization.
180
+
181
+ :returns: An exit code.
182
+ """
183
+ # Handle a single `--version` argument early to avoid starting up the entire pytest infrastructure.
184
+ new_args = sys.argv[1:] if args is None else args
185
+ if isinstance(new_args, Sequence) and new_args.count("--version") == 1:
186
+ sys.stdout.write(f"pytest {__version__}\n")
187
+ return ExitCode.OK
188
+
189
+ old_pytest_version = os.environ.get("PYTEST_VERSION")
190
+ try:
191
+ os.environ["PYTEST_VERSION"] = __version__
192
+ try:
193
+ config = _prepareconfig(new_args, plugins)
194
+ except ConftestImportFailure as e:
195
+ print_conftest_import_error(e, file=sys.stderr)
196
+ return ExitCode.USAGE_ERROR
197
+
198
+ try:
199
+ ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
200
+ try:
201
+ return ExitCode(ret)
202
+ except ValueError:
203
+ return ret
204
+ finally:
205
+ config._ensure_unconfigure()
206
+ except UsageError as e:
207
+ print_usage_error(e, file=sys.stderr)
208
+ return ExitCode.USAGE_ERROR
209
+ finally:
210
+ if old_pytest_version is None:
211
+ os.environ.pop("PYTEST_VERSION", None)
212
+ else:
213
+ os.environ["PYTEST_VERSION"] = old_pytest_version
214
+
215
+
216
+ def console_main() -> int:
217
+ """The CLI entry point of pytest.
218
+
219
+ This function is not meant for programmable use; use `main()` instead.
220
+ """
221
+ # https://docs.python.org/3/library/signal.html#note-on-sigpipe
222
+ try:
223
+ code = main()
224
+ sys.stdout.flush()
225
+ return code
226
+ except BrokenPipeError:
227
+ # Python flushes standard streams on exit; redirect remaining output
228
+ # to devnull to avoid another BrokenPipeError at shutdown
229
+ devnull = os.open(os.devnull, os.O_WRONLY)
230
+ os.dup2(devnull, sys.stdout.fileno())
231
+ return 1 # Python exits with error code 1 on EPIPE
232
+
233
+
234
+ class cmdline: # compatibility namespace
235
+ main = staticmethod(main)
236
+
237
+
238
+ def filename_arg(path: str, optname: str) -> str:
239
+ """Argparse type validator for filename arguments.
240
+
241
+ :path: Path of filename.
242
+ :optname: Name of the option.
243
+ """
244
+ if os.path.isdir(path):
245
+ raise UsageError(f"{optname} must be a filename, given: {path}")
246
+ return path
247
+
248
+
249
+ def directory_arg(path: str, optname: str) -> str:
250
+ """Argparse type validator for directory arguments.
251
+
252
+ :path: Path of directory.
253
+ :optname: Name of the option.
254
+ """
255
+ if not os.path.isdir(path):
256
+ raise UsageError(f"{optname} must be a directory, given: {path}")
257
+ return path
258
+
259
+
260
+ # Plugins that cannot be disabled via "-p no:X" currently.
261
+ essential_plugins = (
262
+ "mark",
263
+ "main",
264
+ "runner",
265
+ "fixtures",
266
+ "helpconfig", # Provides -p.
267
+ )
268
+
269
+ default_plugins = (
270
+ *essential_plugins,
271
+ "python",
272
+ "terminal",
273
+ "debugging",
274
+ "unittest",
275
+ "capture",
276
+ "skipping",
277
+ "legacypath",
278
+ "tmpdir",
279
+ "monkeypatch",
280
+ "recwarn",
281
+ "pastebin",
282
+ "assertion",
283
+ "junitxml",
284
+ "doctest",
285
+ "cacheprovider",
286
+ "setuponly",
287
+ "setupplan",
288
+ "stepwise",
289
+ "unraisableexception",
290
+ "threadexception",
291
+ "warnings",
292
+ "logging",
293
+ "reports",
294
+ "faulthandler",
295
+ "subtests",
296
+ )
297
+
298
+ builtin_plugins = {
299
+ *default_plugins,
300
+ "pytester",
301
+ "pytester_assertions",
302
+ "terminalprogress",
303
+ }
304
+
305
+
306
+ def get_config(
307
+ args: Iterable[str] | None = None,
308
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
309
+ ) -> Config:
310
+ # Subsequent calls to main will create a fresh instance.
311
+ pluginmanager = PytestPluginManager()
312
+ invocation_params = Config.InvocationParams(
313
+ args=args or (),
314
+ plugins=plugins,
315
+ dir=pathlib.Path.cwd(),
316
+ )
317
+ config = Config(pluginmanager, invocation_params=invocation_params)
318
+
319
+ if invocation_params.args:
320
+ # Handle any "-p no:plugin" args.
321
+ pluginmanager.consider_preparse(invocation_params.args, exclude_only=True)
322
+
323
+ for spec in default_plugins:
324
+ pluginmanager.import_plugin(spec)
325
+
326
+ return config
327
+
328
+
329
+ def get_plugin_manager() -> PytestPluginManager:
330
+ """Obtain a new instance of the
331
+ :py:class:`pytest.PytestPluginManager`, with default plugins
332
+ already loaded.
333
+
334
+ This function can be used by integration with other tools, like hooking
335
+ into pytest to run tests into an IDE.
336
+ """
337
+ return get_config().pluginmanager
338
+
339
+
340
+ def _prepareconfig(
341
+ args: list[str] | os.PathLike[str],
342
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
343
+ ) -> Config:
344
+ if isinstance(args, os.PathLike):
345
+ args = [os.fspath(args)]
346
+ elif not isinstance(args, list):
347
+ msg = ( # type:ignore[unreachable]
348
+ "`args` parameter expected to be a list of strings, got: {!r} (type: {})"
349
+ )
350
+ raise TypeError(msg.format(args, type(args)))
351
+
352
+ initial_config = get_config(args, plugins)
353
+ pluginmanager = initial_config.pluginmanager
354
+ try:
355
+ if plugins:
356
+ for plugin in plugins:
357
+ if isinstance(plugin, str):
358
+ pluginmanager.consider_pluginarg(plugin)
359
+ else:
360
+ pluginmanager.register(plugin)
361
+ config: Config = pluginmanager.hook.pytest_cmdline_parse(
362
+ pluginmanager=pluginmanager, args=args
363
+ )
364
+ return config
365
+ except BaseException:
366
+ initial_config._ensure_unconfigure()
367
+ raise
368
+
369
+
370
+ def _get_directory(path: pathlib.Path) -> pathlib.Path:
371
+ """Get the directory of a path - itself if already a directory."""
372
+ if path.is_file():
373
+ return path.parent
374
+ else:
375
+ return path
376
+
377
+
378
+ def _get_legacy_hook_marks(
379
+ method: Any,
380
+ hook_type: str,
381
+ opt_names: tuple[str, ...],
382
+ ) -> dict[str, bool]:
383
+ if TYPE_CHECKING:
384
+ # abuse typeguard from importlib to avoid massive method type union that's lacking an alias
385
+ assert inspect.isroutine(method)
386
+ known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
387
+ must_warn: list[str] = []
388
+ opts: dict[str, bool] = {}
389
+ for opt_name in opt_names:
390
+ opt_attr = getattr(method, opt_name, AttributeError)
391
+ if opt_attr is not AttributeError:
392
+ must_warn.append(f"{opt_name}={opt_attr}")
393
+ opts[opt_name] = True
394
+ elif opt_name in known_marks:
395
+ must_warn.append(f"{opt_name}=True")
396
+ opts[opt_name] = True
397
+ else:
398
+ opts[opt_name] = False
399
+ if must_warn:
400
+ hook_opts = ", ".join(must_warn)
401
+ message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
402
+ type=hook_type,
403
+ fullname=method.__qualname__,
404
+ hook_opts=hook_opts,
405
+ )
406
+ warn_explicit_for(cast(FunctionType, method), message)
407
+ return opts
408
+
409
+
410
+ @final
411
+ class PytestPluginManager(PluginManager):
412
+ """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
413
+ additional pytest-specific functionality:
414
+
415
+ * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
416
+ ``pytest_plugins`` global variables found in plugins being loaded.
417
+ * ``conftest.py`` loading during start-up.
418
+ """
419
+
420
+ def __init__(self) -> None:
421
+ from _pytest.assertion import DummyRewriteHook
422
+ from _pytest.assertion import RewriteHook
423
+
424
+ super().__init__("pytest")
425
+
426
+ # -- State related to local conftest plugins.
427
+ # All loaded conftest modules.
428
+ self._conftest_plugins: set[types.ModuleType] = set()
429
+ # All conftest modules applicable for a directory.
430
+ # This includes the directory's own conftest modules as well
431
+ # as those of its parent directories.
432
+ self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {}
433
+ # Cutoff directory above which conftests are no longer discovered.
434
+ self._confcutdir: pathlib.Path | None = None
435
+ # If set, conftest loading is skipped.
436
+ self._noconftest = False
437
+
438
+ # _getconftestmodules()'s call to _get_directory() causes a stat
439
+ # storm when it's called potentially thousands of times in a test
440
+ # session (#9478), often with the same path, so cache it.
441
+ self._get_directory = lru_cache(256)(_get_directory)
442
+
443
+ # plugins that were explicitly skipped with pytest.skip
444
+ # list of (module name, skip reason)
445
+ # previously we would issue a warning when a plugin was skipped, but
446
+ # since we refactored warnings as first citizens of Config, they are
447
+ # just stored here to be used later.
448
+ self.skipped_plugins: list[tuple[str, str]] = []
449
+
450
+ self.add_hookspecs(_pytest.hookspec)
451
+ self.register(self)
452
+ if os.environ.get("PYTEST_DEBUG"):
453
+ err: IO[str] = sys.stderr
454
+ encoding: str = getattr(err, "encoding", "utf8")
455
+ try:
456
+ err = open(
457
+ os.dup(err.fileno()),
458
+ mode=err.mode,
459
+ buffering=1,
460
+ encoding=encoding,
461
+ )
462
+ except Exception:
463
+ pass
464
+ self.trace.root.setwriter(err.write)
465
+ self.enable_tracing()
466
+
467
+ # Config._consider_importhook will set a real object if required.
468
+ self.rewrite_hook: RewriteHook = DummyRewriteHook()
469
+ # Used to know when we are importing conftests after the pytest_configure stage.
470
+ self._configured = False
471
+
472
+ def parse_hookimpl_opts(
473
+ self, plugin: _PluggyPlugin, name: str
474
+ ) -> HookimplOpts | None:
475
+ """:meta private:"""
476
+ # pytest hooks are always prefixed with "pytest_",
477
+ # so we avoid accessing possibly non-readable attributes
478
+ # (see issue #1073).
479
+ if not name.startswith("pytest_"):
480
+ return None
481
+ # Ignore names which cannot be hooks.
482
+ if name == "pytest_plugins":
483
+ return None
484
+
485
+ opts = super().parse_hookimpl_opts(plugin, name)
486
+ if opts is not None:
487
+ return opts
488
+
489
+ method = getattr(plugin, name)
490
+ # Consider only actual functions for hooks (#3775).
491
+ if not inspect.isroutine(method):
492
+ return None
493
+ # Collect unmarked hooks as long as they have the `pytest_' prefix.
494
+ legacy = _get_legacy_hook_marks(
495
+ method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
496
+ )
497
+ return cast(HookimplOpts, legacy)
498
+
499
+ def parse_hookspec_opts(self, module_or_class, name: str) -> HookspecOpts | None:
500
+ """:meta private:"""
501
+ opts = super().parse_hookspec_opts(module_or_class, name)
502
+ if opts is None:
503
+ method = getattr(module_or_class, name)
504
+ if name.startswith("pytest_"):
505
+ legacy = _get_legacy_hook_marks(
506
+ method, "spec", ("firstresult", "historic")
507
+ )
508
+ opts = cast(HookspecOpts, legacy)
509
+ return opts
510
+
511
+ def register(self, plugin: _PluggyPlugin, name: str | None = None) -> str | None:
512
+ if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
513
+ warnings.warn(
514
+ PytestConfigWarning(
515
+ "{} plugin has been merged into the core, "
516
+ "please remove it from your requirements.".format(
517
+ name.replace("_", "-")
518
+ )
519
+ )
520
+ )
521
+ return None
522
+ plugin_name = super().register(plugin, name)
523
+ if plugin_name is not None:
524
+ self.hook.pytest_plugin_registered.call_historic(
525
+ kwargs=dict(
526
+ plugin=plugin,
527
+ plugin_name=plugin_name,
528
+ manager=self,
529
+ )
530
+ )
531
+
532
+ if isinstance(plugin, types.ModuleType):
533
+ self.consider_module(plugin)
534
+ return plugin_name
535
+
536
+ def getplugin(self, name: str):
537
+ # Support deprecated naming because plugins (xdist e.g.) use it.
538
+ plugin: _PluggyPlugin | None = self.get_plugin(name)
539
+ return plugin
540
+
541
+ def hasplugin(self, name: str) -> bool:
542
+ """Return whether a plugin with the given name is registered."""
543
+ return bool(self.get_plugin(name))
544
+
545
+ def pytest_configure(self, config: Config) -> None:
546
+ """:meta private:"""
547
+ # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
548
+ # we should remove tryfirst/trylast as markers.
549
+ config.addinivalue_line(
550
+ "markers",
551
+ "tryfirst: mark a hook implementation function such that the "
552
+ "plugin machinery will try to call it first/as early as possible. "
553
+ "DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.",
554
+ )
555
+ config.addinivalue_line(
556
+ "markers",
557
+ "trylast: mark a hook implementation function such that the "
558
+ "plugin machinery will try to call it last/as late as possible. "
559
+ "DEPRECATED, use @pytest.hookimpl(trylast=True) instead.",
560
+ )
561
+ self._configured = True
562
+
563
+ #
564
+ # Internal API for local conftest plugin handling.
565
+ #
566
+ def _set_initial_conftests(
567
+ self,
568
+ args: Sequence[str | pathlib.Path],
569
+ pyargs: bool,
570
+ noconftest: bool,
571
+ rootpath: pathlib.Path,
572
+ confcutdir: pathlib.Path | None,
573
+ invocation_dir: pathlib.Path,
574
+ importmode: ImportMode | str,
575
+ *,
576
+ consider_namespace_packages: bool,
577
+ ) -> None:
578
+ """Load initial conftest files given a preparsed "namespace".
579
+
580
+ As conftest files may add their own command line options which have
581
+ arguments ('--my-opt somepath') we might get some false positives.
582
+ All builtin and 3rd party plugins will have been loaded, however, so
583
+ common options will not confuse our logic here.
584
+ """
585
+ self._confcutdir = (
586
+ absolutepath(invocation_dir / confcutdir) if confcutdir else None
587
+ )
588
+ self._noconftest = noconftest
589
+ self._using_pyargs = pyargs
590
+ foundanchor = False
591
+ for initial_path in args:
592
+ path = str(initial_path)
593
+ # remove node-id syntax
594
+ i = path.find("::")
595
+ if i != -1:
596
+ path = path[:i]
597
+ anchor = absolutepath(invocation_dir / path)
598
+
599
+ # Ensure we do not break if what appears to be an anchor
600
+ # is in fact a very long option (#10169, #11394).
601
+ if safe_exists(anchor):
602
+ self._try_load_conftest(
603
+ anchor,
604
+ importmode,
605
+ rootpath,
606
+ consider_namespace_packages=consider_namespace_packages,
607
+ )
608
+ foundanchor = True
609
+ if not foundanchor:
610
+ self._try_load_conftest(
611
+ invocation_dir,
612
+ importmode,
613
+ rootpath,
614
+ consider_namespace_packages=consider_namespace_packages,
615
+ )
616
+
617
+ def _is_in_confcutdir(self, path: pathlib.Path) -> bool:
618
+ """Whether to consider the given path to load conftests from."""
619
+ if self._confcutdir is None:
620
+ return True
621
+ # The semantics here are literally:
622
+ # Do not load a conftest if it is found upwards from confcut dir.
623
+ # But this is *not* the same as:
624
+ # Load only conftests from confcutdir or below.
625
+ # At first glance they might seem the same thing, however we do support use cases where
626
+ # we want to load conftests that are not found in confcutdir or below, but are found
627
+ # in completely different directory hierarchies like packages installed
628
+ # in out-of-source trees.
629
+ # (see #9767 for a regression where the logic was inverted).
630
+ return path not in self._confcutdir.parents
631
+
632
+ def _try_load_conftest(
633
+ self,
634
+ anchor: pathlib.Path,
635
+ importmode: str | ImportMode,
636
+ rootpath: pathlib.Path,
637
+ *,
638
+ consider_namespace_packages: bool,
639
+ ) -> None:
640
+ self._loadconftestmodules(
641
+ anchor,
642
+ importmode,
643
+ rootpath,
644
+ consider_namespace_packages=consider_namespace_packages,
645
+ )
646
+ # let's also consider test* subdirs
647
+ if anchor.is_dir():
648
+ for x in anchor.glob("test*"):
649
+ if x.is_dir():
650
+ self._loadconftestmodules(
651
+ x,
652
+ importmode,
653
+ rootpath,
654
+ consider_namespace_packages=consider_namespace_packages,
655
+ )
656
+
657
+ def _loadconftestmodules(
658
+ self,
659
+ path: pathlib.Path,
660
+ importmode: str | ImportMode,
661
+ rootpath: pathlib.Path,
662
+ *,
663
+ consider_namespace_packages: bool,
664
+ ) -> None:
665
+ if self._noconftest:
666
+ return
667
+
668
+ directory = self._get_directory(path)
669
+
670
+ # Optimization: avoid repeated searches in the same directory.
671
+ # Assumes always called with same importmode and rootpath.
672
+ if directory in self._dirpath2confmods:
673
+ return
674
+
675
+ clist = []
676
+ for parent in reversed((directory, *directory.parents)):
677
+ if self._is_in_confcutdir(parent):
678
+ conftestpath = parent / "conftest.py"
679
+ if conftestpath.is_file():
680
+ mod = self._importconftest(
681
+ conftestpath,
682
+ importmode,
683
+ rootpath,
684
+ consider_namespace_packages=consider_namespace_packages,
685
+ )
686
+ clist.append(mod)
687
+ self._dirpath2confmods[directory] = clist
688
+
689
+ def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]:
690
+ directory = self._get_directory(path)
691
+ return self._dirpath2confmods.get(directory, ())
692
+
693
+ def _rget_with_confmod(
694
+ self,
695
+ name: str,
696
+ path: pathlib.Path,
697
+ ) -> tuple[types.ModuleType, Any]:
698
+ modules = self._getconftestmodules(path)
699
+ for mod in reversed(modules):
700
+ try:
701
+ return mod, getattr(mod, name)
702
+ except AttributeError:
703
+ continue
704
+ raise KeyError(name)
705
+
706
+ def _importconftest(
707
+ self,
708
+ conftestpath: pathlib.Path,
709
+ importmode: str | ImportMode,
710
+ rootpath: pathlib.Path,
711
+ *,
712
+ consider_namespace_packages: bool,
713
+ ) -> types.ModuleType:
714
+ conftestpath_plugin_name = str(conftestpath)
715
+ existing = self.get_plugin(conftestpath_plugin_name)
716
+ if existing is not None:
717
+ return cast(types.ModuleType, existing)
718
+
719
+ # conftest.py files there are not in a Python package all have module
720
+ # name "conftest", and thus conflict with each other. Clear the existing
721
+ # before loading the new one, otherwise the existing one will be
722
+ # returned from the module cache.
723
+ pkgpath = resolve_package_path(conftestpath)
724
+ if pkgpath is None:
725
+ try:
726
+ del sys.modules[conftestpath.stem]
727
+ except KeyError:
728
+ pass
729
+
730
+ try:
731
+ mod = import_path(
732
+ conftestpath,
733
+ mode=importmode,
734
+ root=rootpath,
735
+ consider_namespace_packages=consider_namespace_packages,
736
+ )
737
+ except Exception as e:
738
+ assert e.__traceback__ is not None
739
+ raise ConftestImportFailure(conftestpath, cause=e) from e
740
+
741
+ self._check_non_top_pytest_plugins(mod, conftestpath)
742
+
743
+ self._conftest_plugins.add(mod)
744
+ dirpath = conftestpath.parent
745
+ if dirpath in self._dirpath2confmods:
746
+ for path, mods in self._dirpath2confmods.items():
747
+ if dirpath in path.parents or path == dirpath:
748
+ if mod in mods:
749
+ raise AssertionError(
750
+ f"While trying to load conftest path {conftestpath!s}, "
751
+ f"found that the module {mod} is already loaded with path {mod.__file__}. "
752
+ "This is not supposed to happen. Please report this issue to pytest."
753
+ )
754
+ mods.append(mod)
755
+ self.trace(f"loading conftestmodule {mod!r}")
756
+ self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
757
+ return mod
758
+
759
+ def _check_non_top_pytest_plugins(
760
+ self,
761
+ mod: types.ModuleType,
762
+ conftestpath: pathlib.Path,
763
+ ) -> None:
764
+ if (
765
+ hasattr(mod, "pytest_plugins")
766
+ and self._configured
767
+ and not self._using_pyargs
768
+ ):
769
+ msg = (
770
+ "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n"
771
+ "It affects the entire test suite instead of just below the conftest as expected.\n"
772
+ " {}\n"
773
+ "Please move it to a top level conftest file at the rootdir:\n"
774
+ " {}\n"
775
+ "For more information, visit:\n"
776
+ " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files"
777
+ )
778
+ fail(msg.format(conftestpath, self._confcutdir), pytrace=False)
779
+
780
+ #
781
+ # API for bootstrapping plugin loading
782
+ #
783
+ #
784
+
785
+ def consider_preparse(
786
+ self, args: Sequence[str], *, exclude_only: bool = False
787
+ ) -> None:
788
+ """:meta private:"""
789
+ i = 0
790
+ n = len(args)
791
+ while i < n:
792
+ opt = args[i]
793
+ i += 1
794
+ if isinstance(opt, str):
795
+ if opt == "-p":
796
+ try:
797
+ parg = args[i]
798
+ except IndexError:
799
+ return
800
+ i += 1
801
+ elif opt.startswith("-p"):
802
+ parg = opt[2:]
803
+ else:
804
+ continue
805
+ parg = parg.strip()
806
+ if exclude_only and not parg.startswith("no:"):
807
+ continue
808
+ self.consider_pluginarg(parg)
809
+
810
+ def consider_pluginarg(self, arg: str) -> None:
811
+ """:meta private:"""
812
+ if arg.startswith("no:"):
813
+ name = arg[3:]
814
+ if name in essential_plugins:
815
+ raise UsageError(f"plugin {name} cannot be disabled")
816
+
817
+ # PR #4304: remove stepwise if cacheprovider is blocked.
818
+ if name == "cacheprovider":
819
+ self.set_blocked("stepwise")
820
+ self.set_blocked("pytest_stepwise")
821
+
822
+ self.set_blocked(name)
823
+ if not name.startswith("pytest_"):
824
+ self.set_blocked("pytest_" + name)
825
+ else:
826
+ name = arg
827
+ # Unblock the plugin.
828
+ self.unblock(name)
829
+ if not name.startswith("pytest_"):
830
+ self.unblock("pytest_" + name)
831
+ self.import_plugin(arg, consider_entry_points=True)
832
+
833
+ def consider_conftest(
834
+ self, conftestmodule: types.ModuleType, registration_name: str
835
+ ) -> None:
836
+ """:meta private:"""
837
+ self.register(conftestmodule, name=registration_name)
838
+
839
+ def consider_env(self) -> None:
840
+ """:meta private:"""
841
+ self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
842
+
843
+ def consider_module(self, mod: types.ModuleType) -> None:
844
+ """:meta private:"""
845
+ self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
846
+
847
+ def _import_plugin_specs(
848
+ self, spec: None | types.ModuleType | str | Sequence[str]
849
+ ) -> None:
850
+ plugins = _get_plugin_specs_as_list(spec)
851
+ for import_spec in plugins:
852
+ self.import_plugin(import_spec)
853
+
854
+ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
855
+ """Import a plugin with ``modname``.
856
+
857
+ If ``consider_entry_points`` is True, entry point names are also
858
+ considered to find a plugin.
859
+ """
860
+ # Most often modname refers to builtin modules, e.g. "pytester",
861
+ # "terminal" or "capture". Those plugins are registered under their
862
+ # basename for historic purposes but must be imported with the
863
+ # _pytest prefix.
864
+ assert isinstance(modname, str), (
865
+ f"module name as text required, got {modname!r}"
866
+ )
867
+ if self.is_blocked(modname) or self.get_plugin(modname) is not None:
868
+ return
869
+
870
+ importspec = "_pytest." + modname if modname in builtin_plugins else modname
871
+ self.rewrite_hook.mark_rewrite(importspec)
872
+
873
+ if consider_entry_points:
874
+ loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
875
+ if loaded:
876
+ return
877
+
878
+ try:
879
+ __import__(importspec)
880
+ except ImportError as e:
881
+ raise ImportError(
882
+ f'Error importing plugin "{modname}": {e.args[0]}'
883
+ ).with_traceback(e.__traceback__) from e
884
+
885
+ except Skipped as e:
886
+ self.skipped_plugins.append((modname, e.msg or ""))
887
+ else:
888
+ mod = sys.modules[importspec]
889
+ self.register(mod, modname)
890
+
891
+
892
+ def _get_plugin_specs_as_list(
893
+ specs: None | types.ModuleType | str | Sequence[str],
894
+ ) -> list[str]:
895
+ """Parse a plugins specification into a list of plugin names."""
896
+ # None means empty.
897
+ if specs is None:
898
+ return []
899
+ # Workaround for #3899 - a submodule which happens to be called "pytest_plugins".
900
+ if isinstance(specs, types.ModuleType):
901
+ return []
902
+ # Comma-separated list.
903
+ if isinstance(specs, str):
904
+ return specs.split(",") if specs else []
905
+ # Direct specification.
906
+ if isinstance(specs, collections.abc.Sequence):
907
+ return list(specs)
908
+ raise UsageError(
909
+ f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}"
910
+ )
911
+
912
+
913
+ class Notset:
914
+ def __repr__(self):
915
+ return "<NOTSET>"
916
+
917
+
918
+ notset = Notset()
919
+
920
+
921
+ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
922
+ """Given an iterable of file names in a source distribution, return the "names" that should
923
+ be marked for assertion rewrite.
924
+
925
+ For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
926
+ the assertion rewrite mechanism.
927
+
928
+ This function has to deal with dist-info based distributions and egg based distributions
929
+ (which are still very much in use for "editable" installs).
930
+
931
+ Here are the file names as seen in a dist-info based distribution:
932
+
933
+ pytest_mock/__init__.py
934
+ pytest_mock/_version.py
935
+ pytest_mock/plugin.py
936
+ pytest_mock.egg-info/PKG-INFO
937
+
938
+ Here are the file names as seen in an egg based distribution:
939
+
940
+ src/pytest_mock/__init__.py
941
+ src/pytest_mock/_version.py
942
+ src/pytest_mock/plugin.py
943
+ src/pytest_mock.egg-info/PKG-INFO
944
+ LICENSE
945
+ setup.py
946
+
947
+ We have to take in account those two distribution flavors in order to determine which
948
+ names should be considered for assertion rewriting.
949
+
950
+ More information:
951
+ https://github.com/pytest-dev/pytest-mock/issues/167
952
+ """
953
+ package_files = list(package_files)
954
+ seen_some = False
955
+ for fn in package_files:
956
+ is_simple_module = "/" not in fn and fn.endswith(".py")
957
+ is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
958
+ if is_simple_module:
959
+ module_name, _ = os.path.splitext(fn)
960
+ # we ignore "setup.py" at the root of the distribution
961
+ # as well as editable installation finder modules made by setuptools
962
+ if module_name != "setup" and not module_name.startswith("__editable__"):
963
+ seen_some = True
964
+ yield module_name
965
+ elif is_package:
966
+ package_name = os.path.dirname(fn)
967
+ seen_some = True
968
+ yield package_name
969
+
970
+ if not seen_some:
971
+ # At this point we did not find any packages or modules suitable for assertion
972
+ # rewriting, so we try again by stripping the first path component (to account for
973
+ # "src" based source trees for example).
974
+ # This approach lets us have the common case continue to be fast, as egg-distributions
975
+ # are rarer.
976
+ new_package_files = []
977
+ for fn in package_files:
978
+ parts = fn.split("/")
979
+ new_fn = "/".join(parts[1:])
980
+ if new_fn:
981
+ new_package_files.append(new_fn)
982
+ if new_package_files:
983
+ yield from _iter_rewritable_modules(new_package_files)
984
+
985
+
986
+ class _DeprecatedInicfgProxy(MutableMapping[str, Any]):
987
+ """Compatibility proxy for the deprecated Config.inicfg."""
988
+
989
+ __slots__ = ("_config",)
990
+
991
+ def __init__(self, config: Config) -> None:
992
+ self._config = config
993
+
994
+ def __getitem__(self, key: str) -> Any:
995
+ return self._config._inicfg[key].value
996
+
997
+ def __setitem__(self, key: str, value: Any) -> None:
998
+ self._config._inicfg[key] = ConfigValue(value, origin="override", mode="toml")
999
+
1000
+ def __delitem__(self, key: str) -> None:
1001
+ del self._config._inicfg[key]
1002
+
1003
+ def __iter__(self) -> Iterator[str]:
1004
+ return iter(self._config._inicfg)
1005
+
1006
+ def __len__(self) -> int:
1007
+ return len(self._config._inicfg)
1008
+
1009
+
1010
+ @final
1011
+ class Config:
1012
+ """Access to configuration values, pluginmanager and plugin hooks.
1013
+
1014
+ :param PytestPluginManager pluginmanager:
1015
+ A pytest PluginManager.
1016
+
1017
+ :param InvocationParams invocation_params:
1018
+ Object containing parameters regarding the :func:`pytest.main`
1019
+ invocation.
1020
+ """
1021
+
1022
+ @final
1023
+ @dataclasses.dataclass(frozen=True)
1024
+ class InvocationParams:
1025
+ """Holds parameters passed during :func:`pytest.main`.
1026
+
1027
+ The object attributes are read-only.
1028
+
1029
+ .. versionadded:: 5.1
1030
+
1031
+ .. note::
1032
+
1033
+ Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``
1034
+ configuration option are handled by pytest, not being included in the ``args`` attribute.
1035
+
1036
+ Plugins accessing ``InvocationParams`` must be aware of that.
1037
+ """
1038
+
1039
+ args: tuple[str, ...]
1040
+ """The command-line arguments as passed to :func:`pytest.main`."""
1041
+ plugins: Sequence[str | _PluggyPlugin] | None
1042
+ """Extra plugins, might be `None`."""
1043
+ dir: pathlib.Path
1044
+ """The directory from which :func:`pytest.main` was invoked."""
1045
+
1046
+ def __init__(
1047
+ self,
1048
+ *,
1049
+ args: Iterable[str],
1050
+ plugins: Sequence[str | _PluggyPlugin] | None,
1051
+ dir: pathlib.Path,
1052
+ ) -> None:
1053
+ object.__setattr__(self, "args", tuple(args))
1054
+ object.__setattr__(self, "plugins", plugins)
1055
+ object.__setattr__(self, "dir", dir)
1056
+
1057
+ class ArgsSource(enum.Enum):
1058
+ """Indicates the source of the test arguments.
1059
+
1060
+ .. versionadded:: 7.2
1061
+ """
1062
+
1063
+ #: Command line arguments.
1064
+ ARGS = enum.auto()
1065
+ #: Invocation directory.
1066
+ INVOCATION_DIR = enum.auto()
1067
+ INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
1068
+ #: 'testpaths' configuration value.
1069
+ TESTPATHS = enum.auto()
1070
+
1071
+ # Set by cacheprovider plugin.
1072
+ cache: Cache
1073
+
1074
+ def __init__(
1075
+ self,
1076
+ pluginmanager: PytestPluginManager,
1077
+ *,
1078
+ invocation_params: InvocationParams | None = None,
1079
+ ) -> None:
1080
+ if invocation_params is None:
1081
+ invocation_params = self.InvocationParams(
1082
+ args=(), plugins=None, dir=pathlib.Path.cwd()
1083
+ )
1084
+
1085
+ self.option = argparse.Namespace()
1086
+ """Access to command line option as attributes.
1087
+
1088
+ :type: argparse.Namespace
1089
+ """
1090
+
1091
+ self.invocation_params = invocation_params
1092
+ """The parameters with which pytest was invoked.
1093
+
1094
+ :type: InvocationParams
1095
+ """
1096
+
1097
+ self._parser = Parser(
1098
+ usage=f"%(prog)s [options] [{FILE_OR_DIR}] [{FILE_OR_DIR}] [...]",
1099
+ processopt=self._processopt,
1100
+ _ispytest=True,
1101
+ )
1102
+ self.pluginmanager = pluginmanager
1103
+ """The plugin manager handles plugin registration and hook invocation.
1104
+
1105
+ :type: PytestPluginManager
1106
+ """
1107
+
1108
+ self.stash = Stash()
1109
+ """A place where plugins can store information on the config for their
1110
+ own use.
1111
+
1112
+ :type: Stash
1113
+ """
1114
+ # Deprecated alias. Was never public. Can be removed in a few releases.
1115
+ self._store = self.stash
1116
+
1117
+ self.trace = self.pluginmanager.trace.root.get("config")
1118
+ self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
1119
+ self._inicache: dict[str, Any] = {}
1120
+ self._opt2dest: dict[str, str] = {}
1121
+ self._cleanup_stack = contextlib.ExitStack()
1122
+ self.pluginmanager.register(self, "pytestconfig")
1123
+ self._configured = False
1124
+ self.hook.pytest_addoption.call_historic(
1125
+ kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
1126
+ )
1127
+ self.args_source = Config.ArgsSource.ARGS
1128
+ self.args: list[str] = []
1129
+
1130
+ @property
1131
+ def inicfg(self) -> _DeprecatedInicfgProxy:
1132
+ return _DeprecatedInicfgProxy(self)
1133
+
1134
+ @property
1135
+ def rootpath(self) -> pathlib.Path:
1136
+ """The path to the :ref:`rootdir <rootdir>`.
1137
+
1138
+ .. versionadded:: 6.1
1139
+ """
1140
+ return self._rootpath
1141
+
1142
+ @property
1143
+ def inipath(self) -> pathlib.Path | None:
1144
+ """The path to the :ref:`configfile <configfiles>`.
1145
+
1146
+ .. versionadded:: 6.1
1147
+ """
1148
+ return self._inipath
1149
+
1150
+ def add_cleanup(self, func: Callable[[], None]) -> None:
1151
+ """Add a function to be called when the config object gets out of
1152
+ use (usually coinciding with pytest_unconfigure).
1153
+ """
1154
+ self._cleanup_stack.callback(func)
1155
+
1156
+ def _do_configure(self) -> None:
1157
+ assert not self._configured
1158
+ self._configured = True
1159
+ self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
1160
+
1161
+ def _ensure_unconfigure(self) -> None:
1162
+ try:
1163
+ if self._configured:
1164
+ self._configured = False
1165
+ try:
1166
+ self.hook.pytest_unconfigure(config=self)
1167
+ finally:
1168
+ self.hook.pytest_configure._call_history = []
1169
+ finally:
1170
+ try:
1171
+ self._cleanup_stack.close()
1172
+ finally:
1173
+ self._cleanup_stack = contextlib.ExitStack()
1174
+
1175
+ def get_terminal_writer(self) -> TerminalWriter:
1176
+ terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin(
1177
+ "terminalreporter"
1178
+ )
1179
+ assert terminalreporter is not None
1180
+ return terminalreporter._tw
1181
+
1182
+ def pytest_cmdline_parse(
1183
+ self, pluginmanager: PytestPluginManager, args: list[str]
1184
+ ) -> Config:
1185
+ try:
1186
+ self.parse(args)
1187
+ except UsageError:
1188
+ # Handle `--version --version` and `--help` here in a minimal fashion.
1189
+ # This gets done via helpconfig normally, but its
1190
+ # pytest_cmdline_main is not called in case of errors.
1191
+ if getattr(self.option, "version", False) or "--version" in args:
1192
+ from _pytest.helpconfig import show_version_verbose
1193
+
1194
+ # Note that `--version` (single argument) is handled early by `Config.main()`, so the only
1195
+ # way we are reaching this point is via `--version --version`.
1196
+ show_version_verbose(self)
1197
+ elif (
1198
+ getattr(self.option, "help", False) or "--help" in args or "-h" in args
1199
+ ):
1200
+ self._parser.optparser.print_help()
1201
+ sys.stdout.write(
1202
+ "\nNOTE: displaying only minimal help due to UsageError.\n\n"
1203
+ )
1204
+
1205
+ raise
1206
+
1207
+ return self
1208
+
1209
+ def notify_exception(
1210
+ self,
1211
+ excinfo: ExceptionInfo[BaseException],
1212
+ option: argparse.Namespace | None = None,
1213
+ ) -> None:
1214
+ if option and getattr(option, "fulltrace", False):
1215
+ style: TracebackStyle = "long"
1216
+ else:
1217
+ style = "native"
1218
+ excrepr = excinfo.getrepr(
1219
+ funcargs=True, showlocals=getattr(option, "showlocals", False), style=style
1220
+ )
1221
+ res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
1222
+ if not any(res):
1223
+ for line in str(excrepr).split("\n"):
1224
+ sys.stderr.write(f"INTERNALERROR> {line}\n")
1225
+ sys.stderr.flush()
1226
+
1227
+ def cwd_relative_nodeid(self, nodeid: str) -> str:
1228
+ # nodeid's are relative to the rootpath, compute relative to cwd.
1229
+ if self.invocation_params.dir != self.rootpath:
1230
+ base_path_part, *nodeid_part = nodeid.split("::")
1231
+ # Only process path part
1232
+ fullpath = self.rootpath / base_path_part
1233
+ relative_path = bestrelpath(self.invocation_params.dir, fullpath)
1234
+
1235
+ nodeid = "::".join([relative_path, *nodeid_part])
1236
+ return nodeid
1237
+
1238
+ @classmethod
1239
+ def fromdictargs(cls, option_dict: Mapping[str, Any], args: list[str]) -> Config:
1240
+ """Constructor usable for subprocesses."""
1241
+ config = get_config(args)
1242
+ config.option.__dict__.update(option_dict)
1243
+ config.parse(args, addopts=False)
1244
+ for x in config.option.plugins:
1245
+ config.pluginmanager.consider_pluginarg(x)
1246
+ return config
1247
+
1248
+ def _processopt(self, opt: Argument) -> None:
1249
+ for name in opt._short_opts + opt._long_opts:
1250
+ self._opt2dest[name] = opt.dest
1251
+
1252
+ if hasattr(opt, "default"):
1253
+ if not hasattr(self.option, opt.dest):
1254
+ setattr(self.option, opt.dest, opt.default)
1255
+
1256
+ @hookimpl(trylast=True)
1257
+ def pytest_load_initial_conftests(self, early_config: Config) -> None:
1258
+ # We haven't fully parsed the command line arguments yet, so
1259
+ # early_config.args it not set yet. But we need it for
1260
+ # discovering the initial conftests. So "pre-run" the logic here.
1261
+ # It will be done for real in `parse()`.
1262
+ args, _args_source = early_config._decide_args(
1263
+ args=early_config.known_args_namespace.file_or_dir,
1264
+ pyargs=early_config.known_args_namespace.pyargs,
1265
+ testpaths=early_config.getini("testpaths"),
1266
+ invocation_dir=early_config.invocation_params.dir,
1267
+ rootpath=early_config.rootpath,
1268
+ warn=False,
1269
+ )
1270
+ self.pluginmanager._set_initial_conftests(
1271
+ args=args,
1272
+ pyargs=early_config.known_args_namespace.pyargs,
1273
+ noconftest=early_config.known_args_namespace.noconftest,
1274
+ rootpath=early_config.rootpath,
1275
+ confcutdir=early_config.known_args_namespace.confcutdir,
1276
+ invocation_dir=early_config.invocation_params.dir,
1277
+ importmode=early_config.known_args_namespace.importmode,
1278
+ consider_namespace_packages=early_config.getini(
1279
+ "consider_namespace_packages"
1280
+ ),
1281
+ )
1282
+
1283
+ def _consider_importhook(self) -> None:
1284
+ """Install the PEP 302 import hook if using assertion rewriting.
1285
+
1286
+ Needs to parse the --assert=<mode> option from the commandline
1287
+ and find all the installed plugins to mark them for rewriting
1288
+ by the importhook.
1289
+ """
1290
+ mode = getattr(self.known_args_namespace, "assertmode", "plain")
1291
+
1292
+ disable_autoload = getattr(
1293
+ self.known_args_namespace, "disable_plugin_autoload", False
1294
+ ) or bool(os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"))
1295
+ if mode == "rewrite":
1296
+ import _pytest.assertion
1297
+
1298
+ try:
1299
+ hook = _pytest.assertion.install_importhook(self)
1300
+ except SystemError:
1301
+ mode = "plain"
1302
+ else:
1303
+ self._mark_plugins_for_rewrite(hook, disable_autoload)
1304
+ self._warn_about_missing_assertion(mode)
1305
+
1306
+ def _mark_plugins_for_rewrite(
1307
+ self, hook: AssertionRewritingHook, disable_autoload: bool
1308
+ ) -> None:
1309
+ """Given an importhook, mark for rewrite any top-level
1310
+ modules or packages in the distribution package for
1311
+ all pytest plugins."""
1312
+ self.pluginmanager.rewrite_hook = hook
1313
+
1314
+ if disable_autoload:
1315
+ # We don't autoload from distribution package entry points,
1316
+ # no need to continue.
1317
+ return
1318
+
1319
+ package_files = (
1320
+ str(file)
1321
+ for dist in importlib.metadata.distributions()
1322
+ if any(ep.group == "pytest11" for ep in dist.entry_points)
1323
+ for file in dist.files or []
1324
+ )
1325
+
1326
+ for name in _iter_rewritable_modules(package_files):
1327
+ hook.mark_rewrite(name)
1328
+
1329
+ def _configure_python_path(self) -> None:
1330
+ # `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`
1331
+ for path in reversed(self.getini("pythonpath")):
1332
+ sys.path.insert(0, str(path))
1333
+ self.add_cleanup(self._unconfigure_python_path)
1334
+
1335
+ def _unconfigure_python_path(self) -> None:
1336
+ for path in self.getini("pythonpath"):
1337
+ path_str = str(path)
1338
+ if path_str in sys.path:
1339
+ sys.path.remove(path_str)
1340
+
1341
+ def _validate_args(self, args: list[str], via: str) -> list[str]:
1342
+ """Validate known args."""
1343
+ self._parser.extra_info["config source"] = via
1344
+ try:
1345
+ self._parser.parse_known_and_unknown_args(
1346
+ args, namespace=copy.copy(self.option)
1347
+ )
1348
+ finally:
1349
+ self._parser.extra_info.pop("config source", None)
1350
+
1351
+ return args
1352
+
1353
+ def _decide_args(
1354
+ self,
1355
+ *,
1356
+ args: list[str],
1357
+ pyargs: bool,
1358
+ testpaths: list[str],
1359
+ invocation_dir: pathlib.Path,
1360
+ rootpath: pathlib.Path,
1361
+ warn: bool,
1362
+ ) -> tuple[list[str], ArgsSource]:
1363
+ """Decide the args (initial paths/nodeids) to use given the relevant inputs.
1364
+
1365
+ :param warn: Whether can issue warnings.
1366
+
1367
+ :returns: The args and the args source. Guaranteed to be non-empty.
1368
+ """
1369
+ if args:
1370
+ source = Config.ArgsSource.ARGS
1371
+ result = args
1372
+ else:
1373
+ if invocation_dir == rootpath:
1374
+ source = Config.ArgsSource.TESTPATHS
1375
+ if pyargs:
1376
+ result = testpaths
1377
+ else:
1378
+ result = []
1379
+ for path in testpaths:
1380
+ result.extend(sorted(glob.iglob(path, recursive=True)))
1381
+ if testpaths and not result:
1382
+ if warn:
1383
+ warning_text = (
1384
+ "No files were found in testpaths; "
1385
+ "consider removing or adjusting your testpaths configuration. "
1386
+ "Searching recursively from the current directory instead."
1387
+ )
1388
+ self.issue_config_time_warning(
1389
+ PytestConfigWarning(warning_text), stacklevel=3
1390
+ )
1391
+ else:
1392
+ result = []
1393
+ if not result:
1394
+ source = Config.ArgsSource.INVOCATION_DIR
1395
+ result = [str(invocation_dir)]
1396
+ return result, source
1397
+
1398
+ @hookimpl(wrapper=True)
1399
+ def pytest_collection(self) -> Generator[None, object, object]:
1400
+ # Validate invalid configuration keys after collection is done so we
1401
+ # take in account options added by late-loading conftest files.
1402
+ try:
1403
+ return (yield)
1404
+ finally:
1405
+ self._validate_config_options()
1406
+
1407
+ def _checkversion(self) -> None:
1408
+ import pytest
1409
+
1410
+ minver_ini_value = self._inicfg.get("minversion", None)
1411
+ minver = minver_ini_value.value if minver_ini_value is not None else None
1412
+ if minver:
1413
+ # Imported lazily to improve start-up time.
1414
+ from packaging.version import Version
1415
+
1416
+ if not isinstance(minver, str):
1417
+ raise pytest.UsageError(
1418
+ f"{self.inipath}: 'minversion' must be a single value"
1419
+ )
1420
+
1421
+ if Version(minver) > Version(pytest.__version__):
1422
+ raise pytest.UsageError(
1423
+ f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
1424
+ )
1425
+
1426
+ def _validate_config_options(self) -> None:
1427
+ for key in sorted(self._get_unknown_ini_keys()):
1428
+ self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")
1429
+
1430
+ def _validate_plugins(self) -> None:
1431
+ required_plugins = sorted(self.getini("required_plugins"))
1432
+ if not required_plugins:
1433
+ return
1434
+
1435
+ # Imported lazily to improve start-up time.
1436
+ from packaging.requirements import InvalidRequirement
1437
+ from packaging.requirements import Requirement
1438
+ from packaging.version import Version
1439
+
1440
+ plugin_info = self.pluginmanager.list_plugin_distinfo()
1441
+ plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
1442
+
1443
+ missing_plugins = []
1444
+ for required_plugin in required_plugins:
1445
+ try:
1446
+ req = Requirement(required_plugin)
1447
+ except InvalidRequirement:
1448
+ missing_plugins.append(required_plugin)
1449
+ continue
1450
+
1451
+ if req.name not in plugin_dist_info:
1452
+ missing_plugins.append(required_plugin)
1453
+ elif not req.specifier.contains(
1454
+ Version(plugin_dist_info[req.name]), prereleases=True
1455
+ ):
1456
+ missing_plugins.append(required_plugin)
1457
+
1458
+ if missing_plugins:
1459
+ raise UsageError(
1460
+ "Missing required plugins: {}".format(", ".join(missing_plugins)),
1461
+ )
1462
+
1463
+ def _warn_or_fail_if_strict(self, message: str) -> None:
1464
+ strict_config = self.getini("strict_config")
1465
+ if strict_config is None:
1466
+ strict_config = self.getini("strict")
1467
+ if strict_config:
1468
+ raise UsageError(message)
1469
+
1470
+ self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
1471
+
1472
+ def _get_unknown_ini_keys(self) -> set[str]:
1473
+ known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys()
1474
+ return self._inicfg.keys() - known_keys
1475
+
1476
+ def parse(self, args: list[str], addopts: bool = True) -> None:
1477
+ # Parse given cmdline arguments into this config object.
1478
+ assert self.args == [], (
1479
+ "can only parse cmdline args at most once per Config object"
1480
+ )
1481
+
1482
+ self.hook.pytest_addhooks.call_historic(
1483
+ kwargs=dict(pluginmanager=self.pluginmanager)
1484
+ )
1485
+
1486
+ if addopts:
1487
+ env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
1488
+ if len(env_addopts):
1489
+ args[:] = (
1490
+ self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
1491
+ + args
1492
+ )
1493
+
1494
+ ns = self._parser.parse_known_args(args, namespace=copy.copy(self.option))
1495
+ rootpath, inipath, inicfg, ignored_config_files = determine_setup(
1496
+ inifile=ns.inifilename,
1497
+ override_ini=ns.override_ini,
1498
+ args=ns.file_or_dir,
1499
+ rootdir_cmd_arg=ns.rootdir or None,
1500
+ invocation_dir=self.invocation_params.dir,
1501
+ )
1502
+ self._rootpath = rootpath
1503
+ self._inipath = inipath
1504
+ self._ignored_config_files = ignored_config_files
1505
+ self._inicfg = inicfg
1506
+ self._parser.extra_info["rootdir"] = str(self.rootpath)
1507
+ self._parser.extra_info["inifile"] = str(self.inipath)
1508
+
1509
+ self._parser.addini("addopts", "Extra command line options", "args")
1510
+ self._parser.addini("minversion", "Minimally required pytest version")
1511
+ self._parser.addini(
1512
+ "pythonpath", type="paths", help="Add paths to sys.path", default=[]
1513
+ )
1514
+ self._parser.addini(
1515
+ "required_plugins",
1516
+ "Plugins that must be present for pytest to run",
1517
+ type="args",
1518
+ default=[],
1519
+ )
1520
+
1521
+ if addopts:
1522
+ args[:] = (
1523
+ self._validate_args(self.getini("addopts"), "via addopts config") + args
1524
+ )
1525
+
1526
+ self.known_args_namespace = self._parser.parse_known_args(
1527
+ args, namespace=copy.copy(self.option)
1528
+ )
1529
+ self._checkversion()
1530
+ self._consider_importhook()
1531
+ self._configure_python_path()
1532
+ self.pluginmanager.consider_preparse(args, exclude_only=False)
1533
+ if (
1534
+ not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
1535
+ and not self.known_args_namespace.disable_plugin_autoload
1536
+ ):
1537
+ # Autoloading from distribution package entry point has
1538
+ # not been disabled.
1539
+ self.pluginmanager.load_setuptools_entrypoints("pytest11")
1540
+ # Otherwise only plugins explicitly specified in PYTEST_PLUGINS
1541
+ # are going to be loaded.
1542
+ self.pluginmanager.consider_env()
1543
+
1544
+ self._parser.parse_known_args(args, namespace=self.known_args_namespace)
1545
+
1546
+ self._validate_plugins()
1547
+ self._warn_about_skipped_plugins()
1548
+
1549
+ if self.known_args_namespace.confcutdir is None:
1550
+ if self.inipath is not None:
1551
+ confcutdir = str(self.inipath.parent)
1552
+ else:
1553
+ confcutdir = str(self.rootpath)
1554
+ self.known_args_namespace.confcutdir = confcutdir
1555
+ try:
1556
+ self.hook.pytest_load_initial_conftests(
1557
+ early_config=self, args=args, parser=self._parser
1558
+ )
1559
+ except ConftestImportFailure as e:
1560
+ if self.known_args_namespace.help or self.known_args_namespace.version:
1561
+ # we don't want to prevent --help/--version to work
1562
+ # so just let it pass and print a warning at the end
1563
+ self.issue_config_time_warning(
1564
+ PytestConfigWarning(f"could not load initial conftests: {e.path}"),
1565
+ stacklevel=2,
1566
+ )
1567
+ else:
1568
+ raise
1569
+
1570
+ try:
1571
+ self._parser.parse(args, namespace=self.option)
1572
+ except PrintHelp:
1573
+ return
1574
+
1575
+ self.args, self.args_source = self._decide_args(
1576
+ args=getattr(self.option, FILE_OR_DIR),
1577
+ pyargs=self.option.pyargs,
1578
+ testpaths=self.getini("testpaths"),
1579
+ invocation_dir=self.invocation_params.dir,
1580
+ rootpath=self.rootpath,
1581
+ warn=True,
1582
+ )
1583
+
1584
+ def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
1585
+ """Issue and handle a warning during the "configure" stage.
1586
+
1587
+ During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
1588
+ function because it is not possible to have hook wrappers around ``pytest_configure``.
1589
+
1590
+ This function is mainly intended for plugins that need to issue warnings during
1591
+ ``pytest_configure`` (or similar stages).
1592
+
1593
+ :param warning: The warning instance.
1594
+ :param stacklevel: stacklevel forwarded to warnings.warn.
1595
+ """
1596
+ if self.pluginmanager.is_blocked("warnings"):
1597
+ return
1598
+
1599
+ cmdline_filters = self.known_args_namespace.pythonwarnings or []
1600
+ config_filters = self.getini("filterwarnings")
1601
+
1602
+ with warnings.catch_warnings(record=True) as records:
1603
+ warnings.simplefilter("always", type(warning))
1604
+ apply_warning_filters(config_filters, cmdline_filters)
1605
+ warnings.warn(warning, stacklevel=stacklevel)
1606
+
1607
+ if records:
1608
+ frame = sys._getframe(stacklevel - 1)
1609
+ location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
1610
+ self.hook.pytest_warning_recorded.call_historic(
1611
+ kwargs=dict(
1612
+ warning_message=records[0],
1613
+ when="config",
1614
+ nodeid="",
1615
+ location=location,
1616
+ )
1617
+ )
1618
+
1619
+ def addinivalue_line(self, name: str, line: str) -> None:
1620
+ """Add a line to a configuration option. The option must have been
1621
+ declared but might not yet be set in which case the line becomes
1622
+ the first line in its value."""
1623
+ x = self.getini(name)
1624
+ assert isinstance(x, list)
1625
+ x.append(line) # modifies the cached list inline
1626
+
1627
+ def getini(self, name: str) -> Any:
1628
+ """Return configuration value the an :ref:`configuration file <configfiles>`.
1629
+
1630
+ If a configuration value is not defined in a
1631
+ :ref:`configuration file <configfiles>`, then the ``default`` value
1632
+ provided while registering the configuration through
1633
+ :func:`parser.addini <pytest.Parser.addini>` will be returned.
1634
+ Please note that you can even provide ``None`` as a valid
1635
+ default value.
1636
+
1637
+ If ``default`` is not provided while registering using
1638
+ :func:`parser.addini <pytest.Parser.addini>`, then a default value
1639
+ based on the ``type`` parameter passed to
1640
+ :func:`parser.addini <pytest.Parser.addini>` will be returned.
1641
+ The default values based on ``type`` are:
1642
+ ``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
1643
+ ``bool`` : ``False``
1644
+ ``string`` : empty string ``""``
1645
+ ``int`` : ``0``
1646
+ ``float`` : ``0.0``
1647
+
1648
+ If neither the ``default`` nor the ``type`` parameter is passed
1649
+ while registering the configuration through
1650
+ :func:`parser.addini <pytest.Parser.addini>`, then the configuration
1651
+ is treated as a string and a default empty string '' is returned.
1652
+
1653
+ If the specified name hasn't been registered through a prior
1654
+ :func:`parser.addini <pytest.Parser.addini>` call (usually from a
1655
+ plugin), a ValueError is raised.
1656
+ """
1657
+ canonical_name = self._parser._ini_aliases.get(name, name)
1658
+ try:
1659
+ return self._inicache[canonical_name]
1660
+ except KeyError:
1661
+ pass
1662
+ self._inicache[canonical_name] = val = self._getini(canonical_name)
1663
+ return val
1664
+
1665
+ # Meant for easy monkeypatching by legacypath plugin.
1666
+ # Can be inlined back (with no cover removed) once legacypath is gone.
1667
+ def _getini_unknown_type(self, name: str, type: str, value: object):
1668
+ msg = (
1669
+ f"Option {name} has unknown configuration type {type} with value {value!r}"
1670
+ )
1671
+ raise ValueError(msg) # pragma: no cover
1672
+
1673
+ def _getini(self, name: str):
1674
+ # If this is an alias, resolve to canonical name.
1675
+ canonical_name = self._parser._ini_aliases.get(name, name)
1676
+
1677
+ try:
1678
+ _description, type, default = self._parser._inidict[canonical_name]
1679
+ except KeyError as e:
1680
+ raise ValueError(f"unknown configuration value: {name!r}") from e
1681
+
1682
+ # Collect all possible values (canonical name + aliases) from _inicfg.
1683
+ # Each candidate is (ConfigValue, is_canonical).
1684
+ candidates = []
1685
+ if canonical_name in self._inicfg:
1686
+ candidates.append((self._inicfg[canonical_name], True))
1687
+ for alias, target in self._parser._ini_aliases.items():
1688
+ if target == canonical_name and alias in self._inicfg:
1689
+ candidates.append((self._inicfg[alias], False))
1690
+
1691
+ if not candidates:
1692
+ return default
1693
+
1694
+ # Pick the best candidate based on precedence:
1695
+ # 1. CLI override takes precedence over file, then
1696
+ # 2. Canonical name takes precedence over alias.
1697
+ selected = max(candidates, key=lambda x: (x[0].origin == "override", x[1]))[0]
1698
+ value = selected.value
1699
+ mode = selected.mode
1700
+
1701
+ if mode == "ini":
1702
+ # In ini mode, values are always str | list[str].
1703
+ assert isinstance(value, (str, list))
1704
+ return self._getini_ini(name, canonical_name, type, value, default)
1705
+ elif mode == "toml":
1706
+ return self._getini_toml(name, canonical_name, type, value, default)
1707
+ else:
1708
+ assert_never(mode)
1709
+
1710
+ def _getini_ini(
1711
+ self,
1712
+ name: str,
1713
+ canonical_name: str,
1714
+ type: str,
1715
+ value: str | list[str],
1716
+ default: Any,
1717
+ ):
1718
+ """Handle config values read in INI mode.
1719
+
1720
+ In INI mode, values are stored as str or list[str] only, and coerced
1721
+ from string based on the registered type.
1722
+ """
1723
+ # Note: some coercions are only required if we are reading from .ini
1724
+ # files, because the file format doesn't contain type information, but
1725
+ # when reading from toml (in ini mode) we will get either str or list of
1726
+ # str values (see load_config_dict_from_file). For example:
1727
+ #
1728
+ # ini:
1729
+ # a_line_list = "tests acceptance"
1730
+ #
1731
+ # in this case, we need to split the string to obtain a list of strings.
1732
+ #
1733
+ # toml (ini mode):
1734
+ # a_line_list = ["tests", "acceptance"]
1735
+ #
1736
+ # in this case, we already have a list ready to use.
1737
+ if type == "paths":
1738
+ dp = (
1739
+ self.inipath.parent
1740
+ if self.inipath is not None
1741
+ else self.invocation_params.dir
1742
+ )
1743
+ input_values = shlex.split(value) if isinstance(value, str) else value
1744
+ return [dp / x for x in input_values]
1745
+ elif type == "args":
1746
+ return shlex.split(value) if isinstance(value, str) else value
1747
+ elif type == "linelist":
1748
+ if isinstance(value, str):
1749
+ return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
1750
+ else:
1751
+ return value
1752
+ elif type == "bool":
1753
+ return _strtobool(str(value).strip())
1754
+ elif type == "string":
1755
+ return value
1756
+ elif type == "int":
1757
+ if not isinstance(value, str):
1758
+ raise TypeError(
1759
+ f"Expected an int string for option {name} of type integer, but got: {value!r}"
1760
+ ) from None
1761
+ return int(value)
1762
+ elif type == "float":
1763
+ if not isinstance(value, str):
1764
+ raise TypeError(
1765
+ f"Expected a float string for option {name} of type float, but got: {value!r}"
1766
+ ) from None
1767
+ return float(value)
1768
+ else:
1769
+ return self._getini_unknown_type(name, type, value)
1770
+
1771
+ def _getini_toml(
1772
+ self,
1773
+ name: str,
1774
+ canonical_name: str,
1775
+ type: str,
1776
+ value: object,
1777
+ default: Any,
1778
+ ):
1779
+ """Handle TOML config values with strict type validation and no coercion.
1780
+
1781
+ In TOML mode, values already have native types from TOML parsing.
1782
+ We validate types match expectations exactly, including list items.
1783
+ """
1784
+ value_type = builtins.type(value).__name__
1785
+ if type == "paths":
1786
+ # Expect a list of strings.
1787
+ if not isinstance(value, list):
1788
+ raise TypeError(
1789
+ f"{self.inipath}: config option '{name}' expects a list for type 'paths', "
1790
+ f"got {value_type}: {value!r}"
1791
+ )
1792
+ for i, item in enumerate(value):
1793
+ if not isinstance(item, str):
1794
+ item_type = builtins.type(item).__name__
1795
+ raise TypeError(
1796
+ f"{self.inipath}: config option '{name}' expects a list of strings, "
1797
+ f"but item at index {i} is {item_type}: {item!r}"
1798
+ )
1799
+ dp = (
1800
+ self.inipath.parent
1801
+ if self.inipath is not None
1802
+ else self.invocation_params.dir
1803
+ )
1804
+ return [dp / x for x in value]
1805
+ elif type in {"args", "linelist"}:
1806
+ # Expect a list of strings.
1807
+ if not isinstance(value, list):
1808
+ raise TypeError(
1809
+ f"{self.inipath}: config option '{name}' expects a list for type '{type}', "
1810
+ f"got {value_type}: {value!r}"
1811
+ )
1812
+ for i, item in enumerate(value):
1813
+ if not isinstance(item, str):
1814
+ item_type = builtins.type(item).__name__
1815
+ raise TypeError(
1816
+ f"{self.inipath}: config option '{name}' expects a list of strings, "
1817
+ f"but item at index {i} is {item_type}: {item!r}"
1818
+ )
1819
+ return list(value)
1820
+ elif type == "bool":
1821
+ # Expect a boolean.
1822
+ if not isinstance(value, bool):
1823
+ raise TypeError(
1824
+ f"{self.inipath}: config option '{name}' expects a bool, "
1825
+ f"got {value_type}: {value!r}"
1826
+ )
1827
+ return value
1828
+ elif type == "int":
1829
+ # Expect an integer (but not bool, which is a subclass of int).
1830
+ if not isinstance(value, int) or isinstance(value, bool):
1831
+ raise TypeError(
1832
+ f"{self.inipath}: config option '{name}' expects an int, "
1833
+ f"got {value_type}: {value!r}"
1834
+ )
1835
+ return value
1836
+ elif type == "float":
1837
+ # Expect a float or integer only.
1838
+ if not isinstance(value, (float, int)) or isinstance(value, bool):
1839
+ raise TypeError(
1840
+ f"{self.inipath}: config option '{name}' expects a float, "
1841
+ f"got {value_type}: {value!r}"
1842
+ )
1843
+ return value
1844
+ elif type == "string":
1845
+ # Expect a string.
1846
+ if not isinstance(value, str):
1847
+ raise TypeError(
1848
+ f"{self.inipath}: config option '{name}' expects a string, "
1849
+ f"got {value_type}: {value!r}"
1850
+ )
1851
+ return value
1852
+ else:
1853
+ return self._getini_unknown_type(name, type, value)
1854
+
1855
+ def _getconftest_pathlist(
1856
+ self, name: str, path: pathlib.Path
1857
+ ) -> list[pathlib.Path] | None:
1858
+ try:
1859
+ mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
1860
+ except KeyError:
1861
+ return None
1862
+ assert mod.__file__ is not None
1863
+ modpath = pathlib.Path(mod.__file__).parent
1864
+ values: list[pathlib.Path] = []
1865
+ for relroot in relroots:
1866
+ if isinstance(relroot, os.PathLike):
1867
+ relroot = pathlib.Path(relroot)
1868
+ else:
1869
+ relroot = relroot.replace("/", os.sep)
1870
+ relroot = absolutepath(modpath / relroot)
1871
+ values.append(relroot)
1872
+ return values
1873
+
1874
+ def getoption(self, name: str, default: Any = notset, skip: bool = False):
1875
+ """Return command line option value.
1876
+
1877
+ :param name: Name of the option. You may also specify
1878
+ the literal ``--OPT`` option instead of the "dest" option name.
1879
+ :param default: Fallback value if no option of that name is **declared** via :hook:`pytest_addoption`.
1880
+ Note this parameter will be ignored when the option is **declared** even if the option's value is ``None``.
1881
+ :param skip: If ``True``, raise :func:`pytest.skip` if option is undeclared or has a ``None`` value.
1882
+ Note that even if ``True``, if a default was specified it will be returned instead of a skip.
1883
+ """
1884
+ name = self._opt2dest.get(name, name)
1885
+ try:
1886
+ val = getattr(self.option, name)
1887
+ if val is None and skip:
1888
+ raise AttributeError(name)
1889
+ return val
1890
+ except AttributeError as e:
1891
+ if default is not notset:
1892
+ return default
1893
+ if skip:
1894
+ import pytest
1895
+
1896
+ pytest.skip(f"no {name!r} option found")
1897
+ raise ValueError(f"no option named {name!r}") from e
1898
+
1899
+ def getvalue(self, name: str, path=None):
1900
+ """Deprecated, use getoption() instead."""
1901
+ return self.getoption(name)
1902
+
1903
+ def getvalueorskip(self, name: str, path=None):
1904
+ """Deprecated, use getoption(skip=True) instead."""
1905
+ return self.getoption(name, skip=True)
1906
+
1907
+ #: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
1908
+ VERBOSITY_ASSERTIONS: Final = "assertions"
1909
+ #: Verbosity type for test case execution (see :confval:`verbosity_test_cases`).
1910
+ VERBOSITY_TEST_CASES: Final = "test_cases"
1911
+ #: Verbosity type for failed subtests (see :confval:`verbosity_subtests`).
1912
+ VERBOSITY_SUBTESTS: Final = "subtests"
1913
+
1914
+ _VERBOSITY_INI_DEFAULT: Final = "auto"
1915
+
1916
+ def get_verbosity(self, verbosity_type: str | None = None) -> int:
1917
+ r"""Retrieve the verbosity level for a fine-grained verbosity type.
1918
+
1919
+ :param verbosity_type: Verbosity type to get level for. If a level is
1920
+ configured for the given type, that value will be returned. If the
1921
+ given type is not a known verbosity type, the global verbosity
1922
+ level will be returned. If the given type is None (default), the
1923
+ global verbosity level will be returned.
1924
+
1925
+ To configure a level for a fine-grained verbosity type, the
1926
+ configuration file should have a setting for the configuration name
1927
+ and a numeric value for the verbosity level. A special value of "auto"
1928
+ can be used to explicitly use the global verbosity level.
1929
+
1930
+ Example:
1931
+
1932
+ .. tab:: toml
1933
+
1934
+ .. code-block:: toml
1935
+
1936
+ [tool.pytest]
1937
+ verbosity_assertions = 2
1938
+
1939
+ .. tab:: ini
1940
+
1941
+ .. code-block:: ini
1942
+
1943
+ [pytest]
1944
+ verbosity_assertions = 2
1945
+
1946
+ .. code-block:: console
1947
+
1948
+ pytest -v
1949
+
1950
+ .. code-block:: python
1951
+
1952
+ print(config.get_verbosity()) # 1
1953
+ print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
1954
+ """
1955
+ global_level = self.getoption("verbose", default=0)
1956
+ assert isinstance(global_level, int)
1957
+ if verbosity_type is None:
1958
+ return global_level
1959
+
1960
+ ini_name = Config._verbosity_ini_name(verbosity_type)
1961
+ if ini_name not in self._parser._inidict:
1962
+ return global_level
1963
+
1964
+ level = self.getini(ini_name)
1965
+ if level == Config._VERBOSITY_INI_DEFAULT:
1966
+ return global_level
1967
+
1968
+ return int(level)
1969
+
1970
+ @staticmethod
1971
+ def _verbosity_ini_name(verbosity_type: str) -> str:
1972
+ return f"verbosity_{verbosity_type}"
1973
+
1974
+ @staticmethod
1975
+ def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None:
1976
+ """Add a output verbosity configuration option for the given output type.
1977
+
1978
+ :param parser: Parser for command line arguments and config-file values.
1979
+ :param verbosity_type: Fine-grained verbosity category.
1980
+ :param help: Description of the output this type controls.
1981
+
1982
+ The value should be retrieved via a call to
1983
+ :py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
1984
+ """
1985
+ parser.addini(
1986
+ Config._verbosity_ini_name(verbosity_type),
1987
+ help=help,
1988
+ type="string",
1989
+ default=Config._VERBOSITY_INI_DEFAULT,
1990
+ )
1991
+
1992
+ def _warn_about_missing_assertion(self, mode: str) -> None:
1993
+ if not _assertion_supported():
1994
+ if mode == "plain":
1995
+ warning_text = (
1996
+ "ASSERTIONS ARE NOT EXECUTED"
1997
+ " and FAILING TESTS WILL PASS. Are you"
1998
+ " using python -O?"
1999
+ )
2000
+ else:
2001
+ warning_text = (
2002
+ "assertions not in test modules or"
2003
+ " plugins will be ignored"
2004
+ " because assert statements are not executed "
2005
+ "by the underlying Python interpreter "
2006
+ "(are you using python -O?)\n"
2007
+ )
2008
+ self.issue_config_time_warning(
2009
+ PytestConfigWarning(warning_text),
2010
+ stacklevel=3,
2011
+ )
2012
+
2013
+ def _warn_about_skipped_plugins(self) -> None:
2014
+ for module_name, msg in self.pluginmanager.skipped_plugins:
2015
+ self.issue_config_time_warning(
2016
+ PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),
2017
+ stacklevel=2,
2018
+ )
2019
+
2020
+
2021
+ def _assertion_supported() -> bool:
2022
+ try:
2023
+ assert False
2024
+ except AssertionError:
2025
+ return True
2026
+ else:
2027
+ return False # type: ignore[unreachable]
2028
+
2029
+
2030
+ def create_terminal_writer(
2031
+ config: Config, file: TextIO | None = None
2032
+ ) -> TerminalWriter:
2033
+ """Create a TerminalWriter instance configured according to the options
2034
+ in the config object.
2035
+
2036
+ Every code which requires a TerminalWriter object and has access to a
2037
+ config object should use this function.
2038
+ """
2039
+ tw = TerminalWriter(file=file)
2040
+
2041
+ if config.option.color == "yes":
2042
+ tw.hasmarkup = True
2043
+ elif config.option.color == "no":
2044
+ tw.hasmarkup = False
2045
+
2046
+ if config.option.code_highlight == "yes":
2047
+ tw.code_highlight = True
2048
+ elif config.option.code_highlight == "no":
2049
+ tw.code_highlight = False
2050
+
2051
+ return tw
2052
+
2053
+
2054
+ def _strtobool(val: str) -> bool:
2055
+ """Convert a string representation of truth to True or False.
2056
+
2057
+ True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
2058
+ are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
2059
+ 'val' is anything else.
2060
+
2061
+ .. note:: Copied from distutils.util.
2062
+ """
2063
+ val = val.lower()
2064
+ if val in ("y", "yes", "t", "true", "on", "1"):
2065
+ return True
2066
+ elif val in ("n", "no", "f", "false", "off", "0"):
2067
+ return False
2068
+ else:
2069
+ raise ValueError(f"invalid truth value {val!r}")
2070
+
2071
+
2072
+ @lru_cache(maxsize=50)
2073
+ def parse_warning_filter(
2074
+ arg: str, *, escape: bool
2075
+ ) -> tuple[warnings._ActionKind, str, type[Warning], str, int]:
2076
+ """Parse a warnings filter string.
2077
+
2078
+ This is copied from warnings._setoption with the following changes:
2079
+
2080
+ * Does not apply the filter.
2081
+ * Escaping is optional.
2082
+ * Raises UsageError so we get nice error messages on failure.
2083
+ """
2084
+ __tracebackhide__ = True
2085
+ error_template = dedent(
2086
+ f"""\
2087
+ while parsing the following warning configuration:
2088
+
2089
+ {arg}
2090
+
2091
+ This error occurred:
2092
+
2093
+ {{error}}
2094
+ """
2095
+ )
2096
+
2097
+ parts = arg.split(":")
2098
+ if len(parts) > 5:
2099
+ doc_url = (
2100
+ "https://docs.python.org/3/library/warnings.html#describing-warning-filters"
2101
+ )
2102
+ error = dedent(
2103
+ f"""\
2104
+ Too many fields ({len(parts)}), expected at most 5 separated by colons:
2105
+
2106
+ action:message:category:module:line
2107
+
2108
+ For more information please consult: {doc_url}
2109
+ """
2110
+ )
2111
+ raise UsageError(error_template.format(error=error))
2112
+
2113
+ while len(parts) < 5:
2114
+ parts.append("")
2115
+ action_, message, category_, module, lineno_ = (s.strip() for s in parts)
2116
+ try:
2117
+ action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined]
2118
+ except warnings._OptionError as e:
2119
+ raise UsageError(error_template.format(error=str(e))) from None
2120
+ try:
2121
+ category: type[Warning] = _resolve_warning_category(category_)
2122
+ except ImportError:
2123
+ raise
2124
+ except Exception:
2125
+ exc_info = ExceptionInfo.from_current()
2126
+ exception_text = exc_info.getrepr(style="native")
2127
+ raise UsageError(error_template.format(error=exception_text)) from None
2128
+ if message and escape:
2129
+ message = re.escape(message)
2130
+ if module and escape:
2131
+ module = re.escape(module) + r"\Z"
2132
+ if lineno_:
2133
+ try:
2134
+ lineno = int(lineno_)
2135
+ if lineno < 0:
2136
+ raise ValueError("number is negative")
2137
+ except ValueError as e:
2138
+ raise UsageError(
2139
+ error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
2140
+ ) from None
2141
+ else:
2142
+ lineno = 0
2143
+ try:
2144
+ re.compile(message)
2145
+ re.compile(module)
2146
+ except re.error as e:
2147
+ raise UsageError(
2148
+ error_template.format(error=f"Invalid regex {e.pattern!r}: {e}")
2149
+ ) from None
2150
+ return action, message, category, module, lineno
2151
+
2152
+
2153
+ def _resolve_warning_category(category: str) -> type[Warning]:
2154
+ """
2155
+ Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
2156
+ propagate so we can get access to their tracebacks (#9218).
2157
+ """
2158
+ __tracebackhide__ = True
2159
+ if not category:
2160
+ return Warning
2161
+
2162
+ if "." not in category:
2163
+ import builtins as m
2164
+
2165
+ klass = category
2166
+ else:
2167
+ module, _, klass = category.rpartition(".")
2168
+ m = __import__(module, None, None, [klass])
2169
+ cat = getattr(m, klass)
2170
+ if not issubclass(cat, Warning):
2171
+ raise UsageError(f"{cat} is not a Warning subclass")
2172
+ return cast(type[Warning], cat)
2173
+
2174
+
2175
+ def apply_warning_filters(
2176
+ config_filters: Iterable[str], cmdline_filters: Iterable[str]
2177
+ ) -> None:
2178
+ """Applies pytest-configured filters to the warnings module"""
2179
+ # Filters should have this precedence: cmdline options, config.
2180
+ # Filters should be applied in the inverse order of precedence.
2181
+ for arg in config_filters:
2182
+ try:
2183
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
2184
+ except ImportError as e:
2185
+ warnings.warn(
2186
+ f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning
2187
+ )
2188
+ continue
2189
+
2190
+ for arg in cmdline_filters:
2191
+ try:
2192
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
2193
+ except ImportError as e:
2194
+ warnings.warn(
2195
+ f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning
2196
+ )
2197
+ continue
py311/lib/python3.11/site-packages/_pytest/config/argparsing.py ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Mapping
7
+ from collections.abc import Sequence
8
+ import os
9
+ import sys
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.deprecated import check_ispytest
18
+
19
+
20
+ FILE_OR_DIR = "file_or_dir"
21
+
22
+
23
+ class NotSet:
24
+ def __repr__(self) -> str:
25
+ return "<notset>"
26
+
27
+
28
+ NOT_SET = NotSet()
29
+
30
+
31
+ @final
32
+ class Parser:
33
+ """Parser for command line arguments and config-file values.
34
+
35
+ :ivar extra_info: Dict of generic param -> value to display in case
36
+ there's an error processing the command line arguments.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ usage: str | None = None,
42
+ processopt: Callable[[Argument], None] | None = None,
43
+ *,
44
+ _ispytest: bool = False,
45
+ ) -> None:
46
+ check_ispytest(_ispytest)
47
+
48
+ from _pytest._argcomplete import filescompleter
49
+
50
+ self._processopt = processopt
51
+ self.extra_info: dict[str, Any] = {}
52
+ self.optparser = PytestArgumentParser(self, usage, self.extra_info)
53
+ anonymous_arggroup = self.optparser.add_argument_group("Custom options")
54
+ self._anonymous = OptionGroup(
55
+ anonymous_arggroup, "_anonymous", self, _ispytest=True
56
+ )
57
+ self._groups = [self._anonymous]
58
+ file_or_dir_arg = self.optparser.add_argument(FILE_OR_DIR, nargs="*")
59
+ file_or_dir_arg.completer = filescompleter # type: ignore
60
+
61
+ self._inidict: dict[str, tuple[str, str, Any]] = {}
62
+ # Maps alias -> canonical name.
63
+ self._ini_aliases: dict[str, str] = {}
64
+
65
+ @property
66
+ def prog(self) -> str:
67
+ return self.optparser.prog
68
+
69
+ @prog.setter
70
+ def prog(self, value: str) -> None:
71
+ self.optparser.prog = value
72
+
73
+ def processoption(self, option: Argument) -> None:
74
+ if self._processopt:
75
+ if option.dest:
76
+ self._processopt(option)
77
+
78
+ def getgroup(
79
+ self, name: str, description: str = "", after: str | None = None
80
+ ) -> OptionGroup:
81
+ """Get (or create) a named option Group.
82
+
83
+ :param name: Name of the option group.
84
+ :param description: Long description for --help output.
85
+ :param after: Name of another group, used for ordering --help output.
86
+ :returns: The option group.
87
+
88
+ The returned group object has an ``addoption`` method with the same
89
+ signature as :func:`parser.addoption <pytest.Parser.addoption>` but
90
+ will be shown in the respective group in the output of
91
+ ``pytest --help``.
92
+ """
93
+ for group in self._groups:
94
+ if group.name == name:
95
+ return group
96
+
97
+ arggroup = self.optparser.add_argument_group(description or name)
98
+ group = OptionGroup(arggroup, name, self, _ispytest=True)
99
+ i = 0
100
+ for i, grp in enumerate(self._groups):
101
+ if grp.name == after:
102
+ break
103
+ self._groups.insert(i + 1, group)
104
+ # argparse doesn't provide a way to control `--help` order, so must
105
+ # access its internals ☹.
106
+ self.optparser._action_groups.insert(i + 1, self.optparser._action_groups.pop())
107
+ return group
108
+
109
+ def addoption(self, *opts: str, **attrs: Any) -> None:
110
+ """Register a command line option.
111
+
112
+ :param opts:
113
+ Option names, can be short or long options.
114
+ :param attrs:
115
+ Same attributes as the argparse library's :meth:`add_argument()
116
+ <argparse.ArgumentParser.add_argument>` function accepts.
117
+
118
+ After command line parsing, options are available on the pytest config
119
+ object via ``config.option.NAME`` where ``NAME`` is usually set
120
+ by passing a ``dest`` attribute, for example
121
+ ``addoption("--long", dest="NAME", ...)``.
122
+ """
123
+ self._anonymous.addoption(*opts, **attrs)
124
+
125
+ def parse(
126
+ self,
127
+ args: Sequence[str | os.PathLike[str]],
128
+ namespace: argparse.Namespace | None = None,
129
+ ) -> argparse.Namespace:
130
+ """Parse the arguments.
131
+
132
+ Unlike ``parse_known_args`` and ``parse_known_and_unknown_args``,
133
+ raises PrintHelp on `--help` and UsageError on unknown flags
134
+
135
+ :meta private:
136
+ """
137
+ from _pytest._argcomplete import try_argcomplete
138
+
139
+ try_argcomplete(self.optparser)
140
+ strargs = [os.fspath(x) for x in args]
141
+ if namespace is None:
142
+ namespace = argparse.Namespace()
143
+ try:
144
+ namespace._raise_print_help = True
145
+ return self.optparser.parse_intermixed_args(strargs, namespace=namespace)
146
+ finally:
147
+ del namespace._raise_print_help
148
+
149
+ def parse_known_args(
150
+ self,
151
+ args: Sequence[str | os.PathLike[str]],
152
+ namespace: argparse.Namespace | None = None,
153
+ ) -> argparse.Namespace:
154
+ """Parse the known arguments at this point.
155
+
156
+ :returns: An argparse namespace object.
157
+ """
158
+ return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
159
+
160
+ def parse_known_and_unknown_args(
161
+ self,
162
+ args: Sequence[str | os.PathLike[str]],
163
+ namespace: argparse.Namespace | None = None,
164
+ ) -> tuple[argparse.Namespace, list[str]]:
165
+ """Parse the known arguments at this point, and also return the
166
+ remaining unknown flag arguments.
167
+
168
+ :returns:
169
+ A tuple containing an argparse namespace object for the known
170
+ arguments, and a list of unknown flag arguments.
171
+ """
172
+ strargs = [os.fspath(x) for x in args]
173
+ if sys.version_info < (3, 12, 8) or (3, 13) <= sys.version_info < (3, 13, 1):
174
+ # Older argparse have a bugged parse_known_intermixed_args.
175
+ namespace, unknown = self.optparser.parse_known_args(strargs, namespace)
176
+ assert namespace is not None
177
+ file_or_dir = getattr(namespace, FILE_OR_DIR)
178
+ unknown_flags: list[str] = []
179
+ for arg in unknown:
180
+ (unknown_flags if arg.startswith("-") else file_or_dir).append(arg)
181
+ return namespace, unknown_flags
182
+ else:
183
+ return self.optparser.parse_known_intermixed_args(strargs, namespace)
184
+
185
+ def addini(
186
+ self,
187
+ name: str,
188
+ help: str,
189
+ type: Literal[
190
+ "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
191
+ ]
192
+ | None = None,
193
+ default: Any = NOT_SET,
194
+ *,
195
+ aliases: Sequence[str] = (),
196
+ ) -> None:
197
+ """Register a configuration file option.
198
+
199
+ :param name:
200
+ Name of the configuration.
201
+ :param type:
202
+ Type of the configuration. Can be:
203
+
204
+ * ``string``: a string
205
+ * ``bool``: a boolean
206
+ * ``args``: a list of strings, separated as in a shell
207
+ * ``linelist``: a list of strings, separated by line breaks
208
+ * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
209
+ * ``pathlist``: a list of ``py.path``, separated as in a shell
210
+ * ``int``: an integer
211
+ * ``float``: a floating-point number
212
+
213
+ .. versionadded:: 8.4
214
+
215
+ The ``float`` and ``int`` types.
216
+
217
+ For ``paths`` and ``pathlist`` types, they are considered relative to the config-file.
218
+ In case the execution is happening without a config-file defined,
219
+ they will be considered relative to the current working directory (for example with ``--override-ini``).
220
+
221
+ .. versionadded:: 7.0
222
+ The ``paths`` variable type.
223
+
224
+ .. versionadded:: 8.1
225
+ Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of a config-file.
226
+
227
+ Defaults to ``string`` if ``None`` or not passed.
228
+ :param default:
229
+ Default value if no config-file option exists but is queried.
230
+ :param aliases:
231
+ Additional names by which this option can be referenced.
232
+ Aliases resolve to the canonical name.
233
+
234
+ .. versionadded:: 9.0
235
+ The ``aliases`` parameter.
236
+
237
+ The value of configuration keys can be retrieved via a call to
238
+ :py:func:`config.getini(name) <pytest.Config.getini>`.
239
+ """
240
+ assert type in (
241
+ None,
242
+ "string",
243
+ "paths",
244
+ "pathlist",
245
+ "args",
246
+ "linelist",
247
+ "bool",
248
+ "int",
249
+ "float",
250
+ )
251
+ if type is None:
252
+ type = "string"
253
+ if default is NOT_SET:
254
+ default = get_ini_default_for_type(type)
255
+
256
+ self._inidict[name] = (help, type, default)
257
+
258
+ for alias in aliases:
259
+ if alias in self._inidict:
260
+ raise ValueError(
261
+ f"alias {alias!r} conflicts with existing configuration option"
262
+ )
263
+ if (already := self._ini_aliases.get(alias)) is not None:
264
+ raise ValueError(f"{alias!r} is already an alias of {already!r}")
265
+ self._ini_aliases[alias] = name
266
+
267
+
268
+ def get_ini_default_for_type(
269
+ type: Literal[
270
+ "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
271
+ ],
272
+ ) -> Any:
273
+ """
274
+ Used by addini to get the default value for a given config option type, when
275
+ default is not supplied.
276
+ """
277
+ if type in ("paths", "pathlist", "args", "linelist"):
278
+ return []
279
+ elif type == "bool":
280
+ return False
281
+ elif type == "int":
282
+ return 0
283
+ elif type == "float":
284
+ return 0.0
285
+ else:
286
+ return ""
287
+
288
+
289
+ class ArgumentError(Exception):
290
+ """Raised if an Argument instance is created with invalid or
291
+ inconsistent arguments."""
292
+
293
+ def __init__(self, msg: str, option: Argument | str) -> None:
294
+ self.msg = msg
295
+ self.option_id = str(option)
296
+
297
+ def __str__(self) -> str:
298
+ if self.option_id:
299
+ return f"option {self.option_id}: {self.msg}"
300
+ else:
301
+ return self.msg
302
+
303
+
304
+ class Argument:
305
+ """Class that mimics the necessary behaviour of optparse.Option.
306
+
307
+ It's currently a least effort implementation and ignoring choices
308
+ and integer prefixes.
309
+
310
+ https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
311
+ """
312
+
313
+ def __init__(self, *names: str, **attrs: Any) -> None:
314
+ """Store params in private vars for use in add_argument."""
315
+ self._attrs = attrs
316
+ self._short_opts: list[str] = []
317
+ self._long_opts: list[str] = []
318
+ try:
319
+ self.type = attrs["type"]
320
+ except KeyError:
321
+ pass
322
+ try:
323
+ # Attribute existence is tested in Config._processopt.
324
+ self.default = attrs["default"]
325
+ except KeyError:
326
+ pass
327
+ self._set_opt_strings(names)
328
+ dest: str | None = attrs.get("dest")
329
+ if dest:
330
+ self.dest = dest
331
+ elif self._long_opts:
332
+ self.dest = self._long_opts[0][2:].replace("-", "_")
333
+ else:
334
+ try:
335
+ self.dest = self._short_opts[0][1:]
336
+ except IndexError as e:
337
+ self.dest = "???" # Needed for the error repr.
338
+ raise ArgumentError("need a long or short option", self) from e
339
+
340
+ def names(self) -> list[str]:
341
+ return self._short_opts + self._long_opts
342
+
343
+ def attrs(self) -> Mapping[str, Any]:
344
+ # Update any attributes set by processopt.
345
+ for attr in ("default", "dest", "help", self.dest):
346
+ try:
347
+ self._attrs[attr] = getattr(self, attr)
348
+ except AttributeError:
349
+ pass
350
+ return self._attrs
351
+
352
+ def _set_opt_strings(self, opts: Sequence[str]) -> None:
353
+ """Directly from optparse.
354
+
355
+ Might not be necessary as this is passed to argparse later on.
356
+ """
357
+ for opt in opts:
358
+ if len(opt) < 2:
359
+ raise ArgumentError(
360
+ f"invalid option string {opt!r}: "
361
+ "must be at least two characters long",
362
+ self,
363
+ )
364
+ elif len(opt) == 2:
365
+ if not (opt[0] == "-" and opt[1] != "-"):
366
+ raise ArgumentError(
367
+ f"invalid short option string {opt!r}: "
368
+ "must be of the form -x, (x any non-dash char)",
369
+ self,
370
+ )
371
+ self._short_opts.append(opt)
372
+ else:
373
+ if not (opt[0:2] == "--" and opt[2] != "-"):
374
+ raise ArgumentError(
375
+ f"invalid long option string {opt!r}: "
376
+ "must start with --, followed by non-dash",
377
+ self,
378
+ )
379
+ self._long_opts.append(opt)
380
+
381
+ def __repr__(self) -> str:
382
+ args: list[str] = []
383
+ if self._short_opts:
384
+ args += ["_short_opts: " + repr(self._short_opts)]
385
+ if self._long_opts:
386
+ args += ["_long_opts: " + repr(self._long_opts)]
387
+ args += ["dest: " + repr(self.dest)]
388
+ if hasattr(self, "type"):
389
+ args += ["type: " + repr(self.type)]
390
+ if hasattr(self, "default"):
391
+ args += ["default: " + repr(self.default)]
392
+ return "Argument({})".format(", ".join(args))
393
+
394
+
395
+ class OptionGroup:
396
+ """A group of options shown in its own section."""
397
+
398
+ def __init__(
399
+ self,
400
+ arggroup: argparse._ArgumentGroup,
401
+ name: str,
402
+ parser: Parser | None,
403
+ _ispytest: bool = False,
404
+ ) -> None:
405
+ check_ispytest(_ispytest)
406
+ self._arggroup = arggroup
407
+ self.name = name
408
+ self.options: list[Argument] = []
409
+ self.parser = parser
410
+
411
+ def addoption(self, *opts: str, **attrs: Any) -> None:
412
+ """Add an option to this group.
413
+
414
+ If a shortened version of a long option is specified, it will
415
+ be suppressed in the help. ``addoption('--twowords', '--two-words')``
416
+ results in help showing ``--two-words`` only, but ``--twowords`` gets
417
+ accepted **and** the automatic destination is in ``args.twowords``.
418
+
419
+ :param opts:
420
+ Option names, can be short or long options.
421
+ :param attrs:
422
+ Same attributes as the argparse library's :meth:`add_argument()
423
+ <argparse.ArgumentParser.add_argument>` function accepts.
424
+ """
425
+ conflict = set(opts).intersection(
426
+ name for opt in self.options for name in opt.names()
427
+ )
428
+ if conflict:
429
+ raise ValueError(f"option names {conflict} already added")
430
+ option = Argument(*opts, **attrs)
431
+ self._addoption_instance(option, shortupper=False)
432
+
433
+ def _addoption(self, *opts: str, **attrs: Any) -> None:
434
+ option = Argument(*opts, **attrs)
435
+ self._addoption_instance(option, shortupper=True)
436
+
437
+ def _addoption_instance(self, option: Argument, shortupper: bool = False) -> None:
438
+ if not shortupper:
439
+ for opt in option._short_opts:
440
+ if opt[0] == "-" and opt[1].islower():
441
+ raise ValueError("lowercase shortoptions reserved")
442
+
443
+ if self.parser:
444
+ self.parser.processoption(option)
445
+
446
+ self._arggroup.add_argument(*option.names(), **option.attrs())
447
+ self.options.append(option)
448
+
449
+
450
+ class PytestArgumentParser(argparse.ArgumentParser):
451
+ def __init__(
452
+ self,
453
+ parser: Parser,
454
+ usage: str | None,
455
+ extra_info: dict[str, str],
456
+ ) -> None:
457
+ self._parser = parser
458
+ super().__init__(
459
+ usage=usage,
460
+ add_help=False,
461
+ formatter_class=DropShorterLongHelpFormatter,
462
+ allow_abbrev=False,
463
+ fromfile_prefix_chars="@",
464
+ )
465
+ # extra_info is a dict of (param -> value) to display if there's
466
+ # an usage error to provide more contextual information to the user.
467
+ self.extra_info = extra_info
468
+
469
+ def error(self, message: str) -> NoReturn:
470
+ """Transform argparse error message into UsageError."""
471
+ msg = f"{self.prog}: error: {message}"
472
+ if self.extra_info:
473
+ msg += "\n" + "\n".join(
474
+ f" {k}: {v}" for k, v in sorted(self.extra_info.items())
475
+ )
476
+ raise UsageError(self.format_usage() + msg)
477
+
478
+
479
+ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
480
+ """Shorten help for long options that differ only in extra hyphens.
481
+
482
+ - Collapse **long** options that are the same except for extra hyphens.
483
+ - Shortcut if there are only two options and one of them is a short one.
484
+ - Cache result on the action object as this is called at least 2 times.
485
+ """
486
+
487
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
488
+ # Use more accurate terminal width.
489
+ if "width" not in kwargs:
490
+ kwargs["width"] = _pytest._io.get_terminal_width()
491
+ super().__init__(*args, **kwargs)
492
+
493
+ def _format_action_invocation(self, action: argparse.Action) -> str:
494
+ orgstr = super()._format_action_invocation(action)
495
+ if orgstr and orgstr[0] != "-": # only optional arguments
496
+ return orgstr
497
+ res: str | None = getattr(action, "_formatted_action_invocation", None)
498
+ if res:
499
+ return res
500
+ options = orgstr.split(", ")
501
+ if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
502
+ # a shortcut for '-h, --help' or '--abc', '-a'
503
+ action._formatted_action_invocation = orgstr # type: ignore
504
+ return orgstr
505
+ return_list = []
506
+ short_long: dict[str, str] = {}
507
+ for option in options:
508
+ if len(option) == 2 or option[2] == " ":
509
+ continue
510
+ if not option.startswith("--"):
511
+ raise ArgumentError(
512
+ f'long optional argument without "--": [{option}]', option
513
+ )
514
+ xxoption = option[2:]
515
+ shortened = xxoption.replace("-", "")
516
+ if shortened not in short_long or len(short_long[shortened]) < len(
517
+ xxoption
518
+ ):
519
+ short_long[shortened] = xxoption
520
+ # now short_long has been filled out to the longest with dashes
521
+ # **and** we keep the right option ordering from add_argument
522
+ for option in options:
523
+ if len(option) == 2 or option[2] == " ":
524
+ return_list.append(option)
525
+ if option[2:] == short_long.get(option.replace("-", "")):
526
+ return_list.append(option.replace(" ", "=", 1))
527
+ formatted_action_invocation = ", ".join(return_list)
528
+ action._formatted_action_invocation = formatted_action_invocation # type: ignore
529
+ return formatted_action_invocation
530
+
531
+ def _split_lines(self, text, width):
532
+ """Wrap lines after splitting on original newlines.
533
+
534
+ This allows to have explicit line breaks in the help text.
535
+ """
536
+ import textwrap
537
+
538
+ lines = []
539
+ for line in text.splitlines():
540
+ lines.extend(textwrap.wrap(line.strip(), width))
541
+ return lines
542
+
543
+
544
+ class OverrideIniAction(argparse.Action):
545
+ """Custom argparse action that makes a CLI flag equivalent to overriding an
546
+ option, in addition to behaving like `store_true`.
547
+
548
+ This can simplify things since code only needs to inspect the config option
549
+ and not consider the CLI flag.
550
+ """
551
+
552
+ def __init__(
553
+ self,
554
+ option_strings: Sequence[str],
555
+ dest: str,
556
+ nargs: int | str | None = None,
557
+ *args,
558
+ ini_option: str,
559
+ ini_value: str,
560
+ **kwargs,
561
+ ) -> None:
562
+ super().__init__(option_strings, dest, 0, *args, **kwargs)
563
+ self.ini_option = ini_option
564
+ self.ini_value = ini_value
565
+
566
+ def __call__(
567
+ self,
568
+ parser: argparse.ArgumentParser,
569
+ namespace: argparse.Namespace,
570
+ *args,
571
+ **kwargs,
572
+ ) -> None:
573
+ setattr(namespace, self.dest, True)
574
+ current_overrides = getattr(namespace, "override_ini", None)
575
+ if current_overrides is None:
576
+ current_overrides = []
577
+ current_overrides.append(f"{self.ini_option}={self.ini_value}")
578
+ setattr(namespace, "override_ini", current_overrides)
py311/lib/python3.11/site-packages/_pytest/config/compat.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ import functools
5
+ from pathlib import Path
6
+ from typing import Any
7
+ import warnings
8
+
9
+ import pluggy
10
+
11
+ from ..compat import LEGACY_PATH
12
+ from ..compat import legacy_path
13
+ from ..deprecated import HOOK_LEGACY_PATH_ARG
14
+
15
+
16
+ # hookname: (Path, LEGACY_PATH)
17
+ imply_paths_hooks: Mapping[str, tuple[str, str]] = {
18
+ "pytest_ignore_collect": ("collection_path", "path"),
19
+ "pytest_collect_file": ("file_path", "path"),
20
+ "pytest_pycollect_makemodule": ("module_path", "path"),
21
+ "pytest_report_header": ("start_path", "startdir"),
22
+ "pytest_report_collectionfinish": ("start_path", "startdir"),
23
+ }
24
+
25
+
26
+ def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
27
+ if Path(fspath) != path:
28
+ raise ValueError(
29
+ f"Path({fspath!r}) != {path!r}\n"
30
+ "if both path and fspath are given they need to be equal"
31
+ )
32
+
33
+
34
+ class PathAwareHookProxy:
35
+ """
36
+ this helper wraps around hook callers
37
+ until pluggy supports fixingcalls, this one will do
38
+
39
+ it currently doesn't return full hook caller proxies for fixed hooks,
40
+ this may have to be changed later depending on bugs
41
+ """
42
+
43
+ def __init__(self, hook_relay: pluggy.HookRelay) -> None:
44
+ self._hook_relay = hook_relay
45
+
46
+ def __dir__(self) -> list[str]:
47
+ return dir(self._hook_relay)
48
+
49
+ def __getattr__(self, key: str) -> pluggy.HookCaller:
50
+ hook: pluggy.HookCaller = getattr(self._hook_relay, key)
51
+ if key not in imply_paths_hooks:
52
+ self.__dict__[key] = hook
53
+ return hook
54
+ else:
55
+ path_var, fspath_var = imply_paths_hooks[key]
56
+
57
+ @functools.wraps(hook)
58
+ def fixed_hook(**kw: Any) -> Any:
59
+ path_value: Path | None = kw.pop(path_var, None)
60
+ fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
61
+ if fspath_value is not None:
62
+ warnings.warn(
63
+ HOOK_LEGACY_PATH_ARG.format(
64
+ pylib_path_arg=fspath_var, pathlib_path_arg=path_var
65
+ ),
66
+ stacklevel=2,
67
+ )
68
+ if path_value is not None:
69
+ if fspath_value is not None:
70
+ _check_path(path_value, fspath_value)
71
+ else:
72
+ fspath_value = legacy_path(path_value)
73
+ else:
74
+ assert fspath_value is not None
75
+ path_value = Path(fspath_value)
76
+
77
+ kw[path_var] = path_value
78
+ kw[fspath_var] = fspath_value
79
+ return hook(**kw)
80
+
81
+ fixed_hook.name = hook.name # type: ignore[attr-defined]
82
+ fixed_hook.spec = hook.spec # type: ignore[attr-defined]
83
+ fixed_hook.__name__ = key
84
+ self.__dict__[key] = fixed_hook
85
+ return fixed_hook # type: ignore[return-value]
py311/lib/python3.11/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."""
py311/lib/python3.11/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("::")[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
py311/lib/python3.11/site-packages/_pytest/mark/__init__.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generic mechanism for marking and selecting python functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import collections
6
+ from collections.abc import Collection
7
+ from collections.abc import Iterable
8
+ from collections.abc import Set as AbstractSet
9
+ import dataclasses
10
+ from typing import TYPE_CHECKING
11
+
12
+ from .expression import Expression
13
+ from .structures import _HiddenParam
14
+ from .structures import EMPTY_PARAMETERSET_OPTION
15
+ from .structures import get_empty_parameterset_mark
16
+ from .structures import HIDDEN_PARAM
17
+ from .structures import Mark
18
+ from .structures import MARK_GEN
19
+ from .structures import MarkDecorator
20
+ from .structures import MarkGenerator
21
+ from .structures import ParameterSet
22
+ from _pytest.config import Config
23
+ from _pytest.config import ExitCode
24
+ from _pytest.config import hookimpl
25
+ from _pytest.config import UsageError
26
+ from _pytest.config.argparsing import NOT_SET
27
+ from _pytest.config.argparsing import Parser
28
+ from _pytest.stash import StashKey
29
+
30
+
31
+ if TYPE_CHECKING:
32
+ from _pytest.nodes import Item
33
+
34
+
35
+ __all__ = [
36
+ "HIDDEN_PARAM",
37
+ "MARK_GEN",
38
+ "Mark",
39
+ "MarkDecorator",
40
+ "MarkGenerator",
41
+ "ParameterSet",
42
+ "get_empty_parameterset_mark",
43
+ ]
44
+
45
+
46
+ old_mark_config_key = StashKey[Config | None]()
47
+
48
+
49
+ def param(
50
+ *values: object,
51
+ marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
52
+ id: str | _HiddenParam | None = None,
53
+ ) -> ParameterSet:
54
+ """Specify a parameter in `pytest.mark.parametrize`_ calls or
55
+ :ref:`parametrized fixtures <fixture-parametrize-marks>`.
56
+
57
+ .. code-block:: python
58
+
59
+ @pytest.mark.parametrize(
60
+ "test_input,expected",
61
+ [
62
+ ("3+5", 8),
63
+ pytest.param("6*9", 42, marks=pytest.mark.xfail),
64
+ ],
65
+ )
66
+ def test_eval(test_input, expected):
67
+ assert eval(test_input) == expected
68
+
69
+ :param values: Variable args of the values of the parameter set, in order.
70
+
71
+ :param marks:
72
+ A single mark or a list of marks to be applied to this parameter set.
73
+
74
+ :ref:`pytest.mark.usefixtures <pytest.mark.usefixtures ref>` cannot be added via this parameter.
75
+
76
+ :type id: str | Literal[pytest.HIDDEN_PARAM] | None
77
+ :param id:
78
+ The id to attribute to this parameter set.
79
+
80
+ .. versionadded:: 8.4
81
+ :ref:`hidden-param` means to hide the parameter set
82
+ from the test name. Can only be used at most 1 time, as
83
+ test names need to be unique.
84
+ """
85
+ return ParameterSet.param(*values, marks=marks, id=id)
86
+
87
+
88
+ def pytest_addoption(parser: Parser) -> None:
89
+ group = parser.getgroup("general")
90
+ group._addoption( # private to use reserved lower-case short option
91
+ "-k",
92
+ action="store",
93
+ dest="keyword",
94
+ default="",
95
+ metavar="EXPRESSION",
96
+ help="Only run tests which match the given substring expression. "
97
+ "An expression is a Python evaluable expression "
98
+ "where all names are substring-matched against test names "
99
+ "and their parent classes. Example: -k 'test_method or test_"
100
+ "other' matches all test functions and classes whose name "
101
+ "contains 'test_method' or 'test_other', while -k 'not test_method' "
102
+ "matches those that don't contain 'test_method' in their names. "
103
+ "-k 'not test_method and not test_other' will eliminate the matches. "
104
+ "Additionally keywords are matched to classes and functions "
105
+ "containing extra names in their 'extra_keyword_matches' set, "
106
+ "as well as functions which have names assigned directly to them. "
107
+ "The matching is case-insensitive.",
108
+ )
109
+
110
+ group._addoption( # private to use reserved lower-case short option
111
+ "-m",
112
+ action="store",
113
+ dest="markexpr",
114
+ default="",
115
+ metavar="MARKEXPR",
116
+ help="Only run tests matching given mark expression. "
117
+ "For example: -m 'mark1 and not mark2'.",
118
+ )
119
+
120
+ group.addoption(
121
+ "--markers",
122
+ action="store_true",
123
+ help="show markers (builtin, plugin and per-project ones).",
124
+ )
125
+
126
+ parser.addini("markers", "Register new markers for test functions", "linelist")
127
+ parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")
128
+
129
+
130
+ @hookimpl(tryfirst=True)
131
+ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
132
+ import _pytest.config
133
+
134
+ if config.option.markers:
135
+ config._do_configure()
136
+ tw = _pytest.config.create_terminal_writer(config)
137
+ for line in config.getini("markers"):
138
+ parts = line.split(":", 1)
139
+ name = parts[0]
140
+ rest = parts[1] if len(parts) == 2 else ""
141
+ tw.write(f"@pytest.mark.{name}:", bold=True)
142
+ tw.line(rest)
143
+ tw.line()
144
+ config._ensure_unconfigure()
145
+ return 0
146
+
147
+ return None
148
+
149
+
150
+ @dataclasses.dataclass
151
+ class KeywordMatcher:
152
+ """A matcher for keywords.
153
+
154
+ Given a list of names, matches any substring of one of these names. The
155
+ string inclusion check is case-insensitive.
156
+
157
+ Will match on the name of colitem, including the names of its parents.
158
+ Only matches names of items which are either a :class:`Class` or a
159
+ :class:`Function`.
160
+
161
+ Additionally, matches on names in the 'extra_keyword_matches' set of
162
+ any item, as well as names directly assigned to test functions.
163
+ """
164
+
165
+ __slots__ = ("_names",)
166
+
167
+ _names: AbstractSet[str]
168
+
169
+ @classmethod
170
+ def from_item(cls, item: Item) -> KeywordMatcher:
171
+ mapped_names = set()
172
+
173
+ # Add the names of the current item and any parent items,
174
+ # except the Session and root Directory's which are not
175
+ # interesting for matching.
176
+ import pytest
177
+
178
+ for node in item.listchain():
179
+ if isinstance(node, pytest.Session):
180
+ continue
181
+ if isinstance(node, pytest.Directory) and isinstance(
182
+ node.parent, pytest.Session
183
+ ):
184
+ continue
185
+ mapped_names.add(node.name)
186
+
187
+ # Add the names added as extra keywords to current or parent items.
188
+ mapped_names.update(item.listextrakeywords())
189
+
190
+ # Add the names attached to the current function through direct assignment.
191
+ function_obj = getattr(item, "function", None)
192
+ if function_obj:
193
+ mapped_names.update(function_obj.__dict__)
194
+
195
+ # Add the markers to the keywords as we no longer handle them correctly.
196
+ mapped_names.update(mark.name for mark in item.iter_markers())
197
+
198
+ return cls(mapped_names)
199
+
200
+ def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool:
201
+ if kwargs:
202
+ raise UsageError("Keyword expressions do not support call parameters.")
203
+ subname = subname.lower()
204
+ return any(subname in name.lower() for name in self._names)
205
+
206
+
207
+ def deselect_by_keyword(items: list[Item], config: Config) -> None:
208
+ keywordexpr = config.option.keyword.lstrip()
209
+ if not keywordexpr:
210
+ return
211
+
212
+ expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'")
213
+
214
+ remaining = []
215
+ deselected = []
216
+ for colitem in items:
217
+ if not expr.evaluate(KeywordMatcher.from_item(colitem)):
218
+ deselected.append(colitem)
219
+ else:
220
+ remaining.append(colitem)
221
+
222
+ if deselected:
223
+ config.hook.pytest_deselected(items=deselected)
224
+ items[:] = remaining
225
+
226
+
227
+ @dataclasses.dataclass
228
+ class MarkMatcher:
229
+ """A matcher for markers which are present.
230
+
231
+ Tries to match on any marker names, attached to the given colitem.
232
+ """
233
+
234
+ __slots__ = ("own_mark_name_mapping",)
235
+
236
+ own_mark_name_mapping: dict[str, list[Mark]]
237
+
238
+ @classmethod
239
+ def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher:
240
+ mark_name_mapping = collections.defaultdict(list)
241
+ for mark in markers:
242
+ mark_name_mapping[mark.name].append(mark)
243
+ return cls(mark_name_mapping)
244
+
245
+ def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool:
246
+ if not (matches := self.own_mark_name_mapping.get(name, [])):
247
+ return False
248
+
249
+ for mark in matches: # pylint: disable=consider-using-any-or-all
250
+ if all(mark.kwargs.get(k, NOT_SET) == v for k, v in kwargs.items()):
251
+ return True
252
+ return False
253
+
254
+
255
+ def deselect_by_mark(items: list[Item], config: Config) -> None:
256
+ matchexpr = config.option.markexpr
257
+ if not matchexpr:
258
+ return
259
+
260
+ expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'")
261
+ remaining: list[Item] = []
262
+ deselected: list[Item] = []
263
+ for item in items:
264
+ if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())):
265
+ remaining.append(item)
266
+ else:
267
+ deselected.append(item)
268
+ if deselected:
269
+ config.hook.pytest_deselected(items=deselected)
270
+ items[:] = remaining
271
+
272
+
273
+ def _parse_expression(expr: str, exc_message: str) -> Expression:
274
+ try:
275
+ return Expression.compile(expr)
276
+ except SyntaxError as e:
277
+ raise UsageError(
278
+ f"{exc_message}: {e.text}: at column {e.offset}: {e.msg}"
279
+ ) from None
280
+
281
+
282
+ def pytest_collection_modifyitems(items: list[Item], config: Config) -> None:
283
+ deselect_by_keyword(items, config)
284
+ deselect_by_mark(items, config)
285
+
286
+
287
+ def pytest_configure(config: Config) -> None:
288
+ config.stash[old_mark_config_key] = MARK_GEN._config
289
+ MARK_GEN._config = config
290
+
291
+ empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
292
+
293
+ if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
294
+ raise UsageError(
295
+ f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect"
296
+ f" but it is {empty_parameterset!r}"
297
+ )
298
+
299
+
300
+ def pytest_unconfigure(config: Config) -> None:
301
+ MARK_GEN._config = config.stash.get(old_mark_config_key, None)
py311/lib/python3.11/site-packages/_pytest/mark/expression.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r"""Evaluate match expressions, as used by `-k` and `-m`.
2
+
3
+ The grammar is:
4
+
5
+ expression: expr? EOF
6
+ expr: and_expr ('or' and_expr)*
7
+ and_expr: not_expr ('and' not_expr)*
8
+ not_expr: 'not' not_expr | '(' expr ')' | ident kwargs?
9
+
10
+ ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
11
+ kwargs: ('(' name '=' value ( ', ' name '=' value )* ')')
12
+ name: a valid ident, but not a reserved keyword
13
+ value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None'
14
+
15
+ The semantics are:
16
+
17
+ - Empty expression evaluates to False.
18
+ - ident evaluates to True or False according to a provided matcher function.
19
+ - ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function.
20
+ - or/and/not evaluate according to the usual boolean semantics.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import ast
26
+ from collections.abc import Iterator
27
+ from collections.abc import Mapping
28
+ from collections.abc import Sequence
29
+ import dataclasses
30
+ import enum
31
+ import keyword
32
+ import re
33
+ import types
34
+ from typing import Final
35
+ from typing import final
36
+ from typing import Literal
37
+ from typing import NoReturn
38
+ from typing import overload
39
+ from typing import Protocol
40
+
41
+
42
+ __all__ = [
43
+ "Expression",
44
+ "ExpressionMatcher",
45
+ ]
46
+
47
+
48
+ FILE_NAME: Final = "<pytest match expression>"
49
+
50
+
51
+ class TokenType(enum.Enum):
52
+ LPAREN = "left parenthesis"
53
+ RPAREN = "right parenthesis"
54
+ OR = "or"
55
+ AND = "and"
56
+ NOT = "not"
57
+ IDENT = "identifier"
58
+ EOF = "end of input"
59
+ EQUAL = "="
60
+ STRING = "string literal"
61
+ COMMA = ","
62
+
63
+
64
+ @dataclasses.dataclass(frozen=True)
65
+ class Token:
66
+ __slots__ = ("pos", "type", "value")
67
+ type: TokenType
68
+ value: str
69
+ pos: int
70
+
71
+
72
+ class Scanner:
73
+ __slots__ = ("current", "input", "tokens")
74
+
75
+ def __init__(self, input: str) -> None:
76
+ self.input = input
77
+ self.tokens = self.lex(input)
78
+ self.current = next(self.tokens)
79
+
80
+ def lex(self, input: str) -> Iterator[Token]:
81
+ pos = 0
82
+ while pos < len(input):
83
+ if input[pos] in (" ", "\t"):
84
+ pos += 1
85
+ elif input[pos] == "(":
86
+ yield Token(TokenType.LPAREN, "(", pos)
87
+ pos += 1
88
+ elif input[pos] == ")":
89
+ yield Token(TokenType.RPAREN, ")", pos)
90
+ pos += 1
91
+ elif input[pos] == "=":
92
+ yield Token(TokenType.EQUAL, "=", pos)
93
+ pos += 1
94
+ elif input[pos] == ",":
95
+ yield Token(TokenType.COMMA, ",", pos)
96
+ pos += 1
97
+ elif (quote_char := input[pos]) in ("'", '"'):
98
+ end_quote_pos = input.find(quote_char, pos + 1)
99
+ if end_quote_pos == -1:
100
+ raise SyntaxError(
101
+ f'closing quote "{quote_char}" is missing',
102
+ (FILE_NAME, 1, pos + 1, input),
103
+ )
104
+ value = input[pos : end_quote_pos + 1]
105
+ if (backslash_pos := input.find("\\")) != -1:
106
+ raise SyntaxError(
107
+ r'escaping with "\" not supported in marker expression',
108
+ (FILE_NAME, 1, backslash_pos + 1, input),
109
+ )
110
+ yield Token(TokenType.STRING, value, pos)
111
+ pos += len(value)
112
+ else:
113
+ match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
114
+ if match:
115
+ value = match.group(0)
116
+ if value == "or":
117
+ yield Token(TokenType.OR, value, pos)
118
+ elif value == "and":
119
+ yield Token(TokenType.AND, value, pos)
120
+ elif value == "not":
121
+ yield Token(TokenType.NOT, value, pos)
122
+ else:
123
+ yield Token(TokenType.IDENT, value, pos)
124
+ pos += len(value)
125
+ else:
126
+ raise SyntaxError(
127
+ f'unexpected character "{input[pos]}"',
128
+ (FILE_NAME, 1, pos + 1, input),
129
+ )
130
+ yield Token(TokenType.EOF, "", pos)
131
+
132
+ @overload
133
+ def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ...
134
+
135
+ @overload
136
+ def accept(
137
+ self, type: TokenType, *, reject: Literal[False] = False
138
+ ) -> Token | None: ...
139
+
140
+ def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
141
+ if self.current.type is type:
142
+ token = self.current
143
+ if token.type is not TokenType.EOF:
144
+ self.current = next(self.tokens)
145
+ return token
146
+ if reject:
147
+ self.reject((type,))
148
+ return None
149
+
150
+ def reject(self, expected: Sequence[TokenType]) -> NoReturn:
151
+ raise SyntaxError(
152
+ "expected {}; got {}".format(
153
+ " OR ".join(type.value for type in expected),
154
+ self.current.type.value,
155
+ ),
156
+ (FILE_NAME, 1, self.current.pos + 1, self.input),
157
+ )
158
+
159
+
160
+ # True, False and None are legal match expression identifiers,
161
+ # but illegal as Python identifiers. To fix this, this prefix
162
+ # is added to identifiers in the conversion to Python AST.
163
+ IDENT_PREFIX = "$"
164
+
165
+
166
+ def expression(s: Scanner) -> ast.Expression:
167
+ if s.accept(TokenType.EOF):
168
+ ret: ast.expr = ast.Constant(False)
169
+ else:
170
+ ret = expr(s)
171
+ s.accept(TokenType.EOF, reject=True)
172
+ return ast.fix_missing_locations(ast.Expression(ret))
173
+
174
+
175
+ def expr(s: Scanner) -> ast.expr:
176
+ ret = and_expr(s)
177
+ while s.accept(TokenType.OR):
178
+ rhs = and_expr(s)
179
+ ret = ast.BoolOp(ast.Or(), [ret, rhs])
180
+ return ret
181
+
182
+
183
+ def and_expr(s: Scanner) -> ast.expr:
184
+ ret = not_expr(s)
185
+ while s.accept(TokenType.AND):
186
+ rhs = not_expr(s)
187
+ ret = ast.BoolOp(ast.And(), [ret, rhs])
188
+ return ret
189
+
190
+
191
+ def not_expr(s: Scanner) -> ast.expr:
192
+ if s.accept(TokenType.NOT):
193
+ return ast.UnaryOp(ast.Not(), not_expr(s))
194
+ if s.accept(TokenType.LPAREN):
195
+ ret = expr(s)
196
+ s.accept(TokenType.RPAREN, reject=True)
197
+ return ret
198
+ ident = s.accept(TokenType.IDENT)
199
+ if ident:
200
+ name = ast.Name(IDENT_PREFIX + ident.value, ast.Load())
201
+ if s.accept(TokenType.LPAREN):
202
+ ret = ast.Call(func=name, args=[], keywords=all_kwargs(s))
203
+ s.accept(TokenType.RPAREN, reject=True)
204
+ else:
205
+ ret = name
206
+ return ret
207
+
208
+ s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
209
+
210
+
211
+ BUILTIN_MATCHERS = {"True": True, "False": False, "None": None}
212
+
213
+
214
+ def single_kwarg(s: Scanner) -> ast.keyword:
215
+ keyword_name = s.accept(TokenType.IDENT, reject=True)
216
+ if not keyword_name.value.isidentifier():
217
+ raise SyntaxError(
218
+ f"not a valid python identifier {keyword_name.value}",
219
+ (FILE_NAME, 1, keyword_name.pos + 1, s.input),
220
+ )
221
+ if keyword.iskeyword(keyword_name.value):
222
+ raise SyntaxError(
223
+ f"unexpected reserved python keyword `{keyword_name.value}`",
224
+ (FILE_NAME, 1, keyword_name.pos + 1, s.input),
225
+ )
226
+ s.accept(TokenType.EQUAL, reject=True)
227
+
228
+ if value_token := s.accept(TokenType.STRING):
229
+ value: str | int | bool | None = value_token.value[1:-1] # strip quotes
230
+ else:
231
+ value_token = s.accept(TokenType.IDENT, reject=True)
232
+ if (number := value_token.value).isdigit() or (
233
+ number.startswith("-") and number[1:].isdigit()
234
+ ):
235
+ value = int(number)
236
+ elif value_token.value in BUILTIN_MATCHERS:
237
+ value = BUILTIN_MATCHERS[value_token.value]
238
+ else:
239
+ raise SyntaxError(
240
+ f'unexpected character/s "{value_token.value}"',
241
+ (FILE_NAME, 1, value_token.pos + 1, s.input),
242
+ )
243
+
244
+ ret = ast.keyword(keyword_name.value, ast.Constant(value))
245
+ return ret
246
+
247
+
248
+ def all_kwargs(s: Scanner) -> list[ast.keyword]:
249
+ ret = [single_kwarg(s)]
250
+ while s.accept(TokenType.COMMA):
251
+ ret.append(single_kwarg(s))
252
+ return ret
253
+
254
+
255
+ class ExpressionMatcher(Protocol):
256
+ """A callable which, given an identifier and optional kwargs, should return
257
+ whether it matches in an :class:`Expression` evaluation.
258
+
259
+ Should be prepared to handle arbitrary strings as input.
260
+
261
+ If no kwargs are provided, the expression of the form `foo`.
262
+ If kwargs are provided, the expression is of the form `foo(1, b=True, "s")`.
263
+
264
+ If the expression is not supported (e.g. don't want to accept the kwargs
265
+ syntax variant), should raise :class:`~pytest.UsageError`.
266
+
267
+ Example::
268
+
269
+ def matcher(name: str, /, **kwargs: str | int | bool | None) -> bool:
270
+ # Match `cat`.
271
+ if name == "cat" and not kwargs:
272
+ return True
273
+ # Match `dog(barks=True)`.
274
+ if name == "dog" and kwargs == {"barks": False}:
275
+ return True
276
+ return False
277
+ """
278
+
279
+ def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: ...
280
+
281
+
282
+ @dataclasses.dataclass
283
+ class MatcherNameAdapter:
284
+ matcher: ExpressionMatcher
285
+ name: str
286
+
287
+ def __bool__(self) -> bool:
288
+ return self.matcher(self.name)
289
+
290
+ def __call__(self, **kwargs: str | int | bool | None) -> bool:
291
+ return self.matcher(self.name, **kwargs)
292
+
293
+
294
+ class MatcherAdapter(Mapping[str, MatcherNameAdapter]):
295
+ """Adapts a matcher function to a locals mapping as required by eval()."""
296
+
297
+ def __init__(self, matcher: ExpressionMatcher) -> None:
298
+ self.matcher = matcher
299
+
300
+ def __getitem__(self, key: str) -> MatcherNameAdapter:
301
+ return MatcherNameAdapter(matcher=self.matcher, name=key[len(IDENT_PREFIX) :])
302
+
303
+ def __iter__(self) -> Iterator[str]:
304
+ raise NotImplementedError()
305
+
306
+ def __len__(self) -> int:
307
+ raise NotImplementedError()
308
+
309
+
310
+ @final
311
+ class Expression:
312
+ """A compiled match expression as used by -k and -m.
313
+
314
+ The expression can be evaluated against different matchers.
315
+ """
316
+
317
+ __slots__ = ("_code", "input")
318
+
319
+ def __init__(self, input: str, code: types.CodeType) -> None:
320
+ #: The original input line, as a string.
321
+ self.input: Final = input
322
+ self._code: Final = code
323
+
324
+ @classmethod
325
+ def compile(cls, input: str) -> Expression:
326
+ """Compile a match expression.
327
+
328
+ :param input: The input expression - one line.
329
+
330
+ :raises SyntaxError: If the expression is malformed.
331
+ """
332
+ astexpr = expression(Scanner(input))
333
+ code = compile(
334
+ astexpr,
335
+ filename="<pytest match expression>",
336
+ mode="eval",
337
+ )
338
+ return Expression(input, code)
339
+
340
+ def evaluate(self, matcher: ExpressionMatcher) -> bool:
341
+ """Evaluate the match expression.
342
+
343
+ :param matcher:
344
+ A callback which determines whether an identifier matches or not.
345
+ See the :class:`ExpressionMatcher` protocol for details and example.
346
+
347
+ :returns: Whether the expression matches or not.
348
+
349
+ :raises UsageError:
350
+ If the matcher doesn't support the expression. Cannot happen if the
351
+ matcher supports all expressions.
352
+ """
353
+ return bool(eval(self._code, {"__builtins__": {}}, MatcherAdapter(matcher)))
py311/lib/python3.11/site-packages/_pytest/mark/structures.py ADDED
@@ -0,0 +1,664 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import collections.abc
5
+ from collections.abc import Callable
6
+ from collections.abc import Collection
7
+ from collections.abc import Iterable
8
+ from collections.abc import Iterator
9
+ from collections.abc import Mapping
10
+ from collections.abc import MutableMapping
11
+ from collections.abc import Sequence
12
+ import dataclasses
13
+ import enum
14
+ import inspect
15
+ from typing import Any
16
+ from typing import final
17
+ from typing import NamedTuple
18
+ from typing import overload
19
+ from typing import TYPE_CHECKING
20
+ from typing import TypeVar
21
+ import warnings
22
+
23
+ from .._code import getfslineno
24
+ from ..compat import NOTSET
25
+ from ..compat import NotSetType
26
+ from _pytest.config import Config
27
+ from _pytest.deprecated import check_ispytest
28
+ from _pytest.deprecated import MARKED_FIXTURE
29
+ from _pytest.outcomes import fail
30
+ from _pytest.raises import AbstractRaises
31
+ from _pytest.scope import _ScopeName
32
+ from _pytest.warning_types import PytestUnknownMarkWarning
33
+
34
+
35
+ if TYPE_CHECKING:
36
+ from ..nodes import Node
37
+
38
+
39
+ EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
40
+
41
+
42
+ # Singleton type for HIDDEN_PARAM, as described in:
43
+ # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
44
+ class _HiddenParam(enum.Enum):
45
+ token = 0
46
+
47
+
48
+ #: Can be used as a parameter set id to hide it from the test name.
49
+ HIDDEN_PARAM = _HiddenParam.token
50
+
51
+
52
+ def istestfunc(func) -> bool:
53
+ return callable(func) and getattr(func, "__name__", "<lambda>") != "<lambda>"
54
+
55
+
56
+ def get_empty_parameterset_mark(
57
+ config: Config, argnames: Sequence[str], func
58
+ ) -> MarkDecorator:
59
+ from ..nodes import Collector
60
+
61
+ argslisting = ", ".join(argnames)
62
+
63
+ _fs, lineno = getfslineno(func)
64
+ reason = f"got empty parameter set for ({argslisting})"
65
+ requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
66
+ if requested_mark in ("", None, "skip"):
67
+ mark = MARK_GEN.skip(reason=reason)
68
+ elif requested_mark == "xfail":
69
+ mark = MARK_GEN.xfail(reason=reason, run=False)
70
+ elif requested_mark == "fail_at_collect":
71
+ raise Collector.CollectError(
72
+ f"Empty parameter set in '{func.__name__}' at line {lineno + 1}"
73
+ )
74
+ else:
75
+ raise LookupError(requested_mark)
76
+ return mark
77
+
78
+
79
+ class ParameterSet(NamedTuple):
80
+ """A set of values for a set of parameters along with associated marks and
81
+ an optional ID for the set.
82
+
83
+ Examples::
84
+
85
+ pytest.param(1, 2, 3)
86
+ # ParameterSet(values=(1, 2, 3), marks=(), id=None)
87
+
88
+ pytest.param("hello", id="greeting")
89
+ # ParameterSet(values=("hello",), marks=(), id="greeting")
90
+
91
+ # Parameter set with marks
92
+ pytest.param(42, marks=pytest.mark.xfail)
93
+ # ParameterSet(values=(42,), marks=(MarkDecorator(...),), id=None)
94
+
95
+ # From parametrize mark (parameter names + list of parameter sets)
96
+ pytest.mark.parametrize(
97
+ ("a", "b", "expected"),
98
+ [
99
+ (1, 2, 3),
100
+ pytest.param(40, 2, 42, id="everything"),
101
+ ],
102
+ )
103
+ # ParameterSet(values=(1, 2, 3), marks=(), id=None)
104
+ # ParameterSet(values=(40, 2, 42), marks=(), id="everything")
105
+ """
106
+
107
+ values: Sequence[object | NotSetType]
108
+ marks: Collection[MarkDecorator | Mark]
109
+ id: str | _HiddenParam | None
110
+
111
+ @classmethod
112
+ def param(
113
+ cls,
114
+ *values: object,
115
+ marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
116
+ id: str | _HiddenParam | None = None,
117
+ ) -> ParameterSet:
118
+ if isinstance(marks, MarkDecorator):
119
+ marks = (marks,)
120
+ else:
121
+ assert isinstance(marks, collections.abc.Collection)
122
+ if any(i.name == "usefixtures" for i in marks):
123
+ raise ValueError(
124
+ "pytest.param cannot add pytest.mark.usefixtures; see "
125
+ "https://docs.pytest.org/en/stable/reference/reference.html#pytest-param"
126
+ )
127
+
128
+ if id is not None:
129
+ if not isinstance(id, str) and id is not HIDDEN_PARAM:
130
+ raise TypeError(
131
+ "Expected id to be a string or a `pytest.HIDDEN_PARAM` sentinel, "
132
+ f"got {type(id)}: {id!r}",
133
+ )
134
+ return cls(values, marks, id)
135
+
136
+ @classmethod
137
+ def extract_from(
138
+ cls,
139
+ parameterset: ParameterSet | Sequence[object] | object,
140
+ force_tuple: bool = False,
141
+ ) -> ParameterSet:
142
+ """Extract from an object or objects.
143
+
144
+ :param parameterset:
145
+ A legacy style parameterset that may or may not be a tuple,
146
+ and may or may not be wrapped into a mess of mark objects.
147
+
148
+ :param force_tuple:
149
+ Enforce tuple wrapping so single argument tuple values
150
+ don't get decomposed and break tests.
151
+ """
152
+ if isinstance(parameterset, cls):
153
+ return parameterset
154
+ if force_tuple:
155
+ return cls.param(parameterset)
156
+ else:
157
+ # TODO: Refactor to fix this type-ignore. Currently the following
158
+ # passes type-checking but crashes:
159
+ #
160
+ # @pytest.mark.parametrize(('x', 'y'), [1, 2])
161
+ # def test_foo(x, y): pass
162
+ return cls(parameterset, marks=[], id=None) # type: ignore[arg-type]
163
+
164
+ @staticmethod
165
+ def _parse_parametrize_args(
166
+ argnames: str | Sequence[str],
167
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
168
+ *args,
169
+ **kwargs,
170
+ ) -> tuple[Sequence[str], bool]:
171
+ if isinstance(argnames, str):
172
+ argnames = [x.strip() for x in argnames.split(",") if x.strip()]
173
+ force_tuple = len(argnames) == 1
174
+ else:
175
+ force_tuple = False
176
+ return argnames, force_tuple
177
+
178
+ @staticmethod
179
+ def _parse_parametrize_parameters(
180
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
181
+ force_tuple: bool,
182
+ ) -> list[ParameterSet]:
183
+ return [
184
+ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
185
+ ]
186
+
187
+ @classmethod
188
+ def _for_parametrize(
189
+ cls,
190
+ argnames: str | Sequence[str],
191
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
192
+ func,
193
+ config: Config,
194
+ nodeid: str,
195
+ ) -> tuple[Sequence[str], list[ParameterSet]]:
196
+ argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
197
+ parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
198
+ del argvalues
199
+
200
+ if parameters:
201
+ # Check all parameter sets have the correct number of values.
202
+ for param in parameters:
203
+ if len(param.values) != len(argnames):
204
+ msg = (
205
+ '{nodeid}: in "parametrize" the number of names ({names_len}):\n'
206
+ " {names}\n"
207
+ "must be equal to the number of values ({values_len}):\n"
208
+ " {values}"
209
+ )
210
+ fail(
211
+ msg.format(
212
+ nodeid=nodeid,
213
+ values=param.values,
214
+ names=argnames,
215
+ names_len=len(argnames),
216
+ values_len=len(param.values),
217
+ ),
218
+ pytrace=False,
219
+ )
220
+ else:
221
+ # Empty parameter set (likely computed at runtime): create a single
222
+ # parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
223
+ mark = get_empty_parameterset_mark(config, argnames, func)
224
+ parameters.append(
225
+ ParameterSet(
226
+ values=(NOTSET,) * len(argnames), marks=[mark], id="NOTSET"
227
+ )
228
+ )
229
+ return argnames, parameters
230
+
231
+
232
+ @final
233
+ @dataclasses.dataclass(frozen=True)
234
+ class Mark:
235
+ """A pytest mark."""
236
+
237
+ #: Name of the mark.
238
+ name: str
239
+ #: Positional arguments of the mark decorator.
240
+ args: tuple[Any, ...]
241
+ #: Keyword arguments of the mark decorator.
242
+ kwargs: Mapping[str, Any]
243
+
244
+ #: Source Mark for ids with parametrize Marks.
245
+ _param_ids_from: Mark | None = dataclasses.field(default=None, repr=False)
246
+ #: Resolved/generated ids with parametrize Marks.
247
+ _param_ids_generated: Sequence[str] | None = dataclasses.field(
248
+ default=None, repr=False
249
+ )
250
+
251
+ def __init__(
252
+ self,
253
+ name: str,
254
+ args: tuple[Any, ...],
255
+ kwargs: Mapping[str, Any],
256
+ param_ids_from: Mark | None = None,
257
+ param_ids_generated: Sequence[str] | None = None,
258
+ *,
259
+ _ispytest: bool = False,
260
+ ) -> None:
261
+ """:meta private:"""
262
+ check_ispytest(_ispytest)
263
+ # Weirdness to bypass frozen=True.
264
+ object.__setattr__(self, "name", name)
265
+ object.__setattr__(self, "args", args)
266
+ object.__setattr__(self, "kwargs", kwargs)
267
+ object.__setattr__(self, "_param_ids_from", param_ids_from)
268
+ object.__setattr__(self, "_param_ids_generated", param_ids_generated)
269
+
270
+ def _has_param_ids(self) -> bool:
271
+ return "ids" in self.kwargs or len(self.args) >= 4
272
+
273
+ def combined_with(self, other: Mark) -> Mark:
274
+ """Return a new Mark which is a combination of this
275
+ Mark and another Mark.
276
+
277
+ Combines by appending args and merging kwargs.
278
+
279
+ :param Mark other: The mark to combine with.
280
+ :rtype: Mark
281
+ """
282
+ assert self.name == other.name
283
+
284
+ # Remember source of ids with parametrize Marks.
285
+ param_ids_from: Mark | None = None
286
+ if self.name == "parametrize":
287
+ if other._has_param_ids():
288
+ param_ids_from = other
289
+ elif self._has_param_ids():
290
+ param_ids_from = self
291
+
292
+ return Mark(
293
+ self.name,
294
+ self.args + other.args,
295
+ dict(self.kwargs, **other.kwargs),
296
+ param_ids_from=param_ids_from,
297
+ _ispytest=True,
298
+ )
299
+
300
+
301
+ # A generic parameter designating an object to which a Mark may
302
+ # be applied -- a test function (callable) or class.
303
+ # Note: a lambda is not allowed, but this can't be represented.
304
+ Markable = TypeVar("Markable", bound=Callable[..., object] | type)
305
+
306
+
307
+ @dataclasses.dataclass
308
+ class MarkDecorator:
309
+ """A decorator for applying a mark on test functions and classes.
310
+
311
+ ``MarkDecorators`` are created with ``pytest.mark``::
312
+
313
+ mark1 = pytest.mark.NAME # Simple MarkDecorator
314
+ mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
315
+
316
+ and can then be applied as decorators to test functions::
317
+
318
+ @mark2
319
+ def test_function():
320
+ pass
321
+
322
+ When a ``MarkDecorator`` is called, it does the following:
323
+
324
+ 1. If called with a single class as its only positional argument and no
325
+ additional keyword arguments, it attaches the mark to the class so it
326
+ gets applied automatically to all test cases found in that class.
327
+
328
+ 2. If called with a single function as its only positional argument and
329
+ no additional keyword arguments, it attaches the mark to the function,
330
+ containing all the arguments already stored internally in the
331
+ ``MarkDecorator``.
332
+
333
+ 3. When called in any other case, it returns a new ``MarkDecorator``
334
+ instance with the original ``MarkDecorator``'s content updated with
335
+ the arguments passed to this call.
336
+
337
+ Note: The rules above prevent a ``MarkDecorator`` from storing only a
338
+ single function or class reference as its positional argument with no
339
+ additional keyword or positional arguments. You can work around this by
340
+ using `with_args()`.
341
+ """
342
+
343
+ mark: Mark
344
+
345
+ def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:
346
+ """:meta private:"""
347
+ check_ispytest(_ispytest)
348
+ self.mark = mark
349
+
350
+ @property
351
+ def name(self) -> str:
352
+ """Alias for mark.name."""
353
+ return self.mark.name
354
+
355
+ @property
356
+ def args(self) -> tuple[Any, ...]:
357
+ """Alias for mark.args."""
358
+ return self.mark.args
359
+
360
+ @property
361
+ def kwargs(self) -> Mapping[str, Any]:
362
+ """Alias for mark.kwargs."""
363
+ return self.mark.kwargs
364
+
365
+ @property
366
+ def markname(self) -> str:
367
+ """:meta private:"""
368
+ return self.name # for backward-compat (2.4.1 had this attr)
369
+
370
+ def with_args(self, *args: object, **kwargs: object) -> MarkDecorator:
371
+ """Return a MarkDecorator with extra arguments added.
372
+
373
+ Unlike calling the MarkDecorator, with_args() can be used even
374
+ if the sole argument is a callable/class.
375
+ """
376
+ mark = Mark(self.name, args, kwargs, _ispytest=True)
377
+ return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)
378
+
379
+ # Type ignored because the overloads overlap with an incompatible
380
+ # return type. Not much we can do about that. Thankfully mypy picks
381
+ # the first match so it works out even if we break the rules.
382
+ @overload
383
+ def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap]
384
+ pass
385
+
386
+ @overload
387
+ def __call__(self, *args: object, **kwargs: object) -> MarkDecorator:
388
+ pass
389
+
390
+ def __call__(self, *args: object, **kwargs: object):
391
+ """Call the MarkDecorator."""
392
+ if args and not kwargs:
393
+ func = args[0]
394
+ is_class = inspect.isclass(func)
395
+ # For staticmethods/classmethods, the marks are eventually fetched from the
396
+ # function object, not the descriptor, so unwrap.
397
+ unwrapped_func = func
398
+ if isinstance(func, staticmethod | classmethod):
399
+ unwrapped_func = func.__func__
400
+ if len(args) == 1 and (istestfunc(unwrapped_func) or is_class):
401
+ store_mark(unwrapped_func, self.mark, stacklevel=3)
402
+ return func
403
+ return self.with_args(*args, **kwargs)
404
+
405
+
406
+ def get_unpacked_marks(
407
+ obj: object | type,
408
+ *,
409
+ consider_mro: bool = True,
410
+ ) -> list[Mark]:
411
+ """Obtain the unpacked marks that are stored on an object.
412
+
413
+ If obj is a class and consider_mro is true, return marks applied to
414
+ this class and all of its super-classes in MRO order. If consider_mro
415
+ is false, only return marks applied directly to this class.
416
+ """
417
+ if isinstance(obj, type):
418
+ if not consider_mro:
419
+ mark_lists = [obj.__dict__.get("pytestmark", [])]
420
+ else:
421
+ mark_lists = [
422
+ x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
423
+ ]
424
+ mark_list = []
425
+ for item in mark_lists:
426
+ if isinstance(item, list):
427
+ mark_list.extend(item)
428
+ else:
429
+ mark_list.append(item)
430
+ else:
431
+ mark_attribute = getattr(obj, "pytestmark", [])
432
+ if isinstance(mark_attribute, list):
433
+ mark_list = mark_attribute
434
+ else:
435
+ mark_list = [mark_attribute]
436
+ return list(normalize_mark_list(mark_list))
437
+
438
+
439
+ def normalize_mark_list(
440
+ mark_list: Iterable[Mark | MarkDecorator],
441
+ ) -> Iterable[Mark]:
442
+ """
443
+ Normalize an iterable of Mark or MarkDecorator objects into a list of marks
444
+ by retrieving the `mark` attribute on MarkDecorator instances.
445
+
446
+ :param mark_list: marks to normalize
447
+ :returns: A new list of the extracted Mark objects
448
+ """
449
+ for mark in mark_list:
450
+ mark_obj = getattr(mark, "mark", mark)
451
+ if not isinstance(mark_obj, Mark):
452
+ raise TypeError(f"got {mark_obj!r} instead of Mark")
453
+ yield mark_obj
454
+
455
+
456
+ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
457
+ """Store a Mark on an object.
458
+
459
+ This is used to implement the Mark declarations/decorators correctly.
460
+ """
461
+ assert isinstance(mark, Mark), mark
462
+
463
+ from ..fixtures import getfixturemarker
464
+
465
+ if getfixturemarker(obj) is not None:
466
+ warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel)
467
+
468
+ # Always reassign name to avoid updating pytestmark in a reference that
469
+ # was only borrowed.
470
+ obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
471
+
472
+
473
+ # Typing for builtin pytest marks. This is cheating; it gives builtin marks
474
+ # special privilege, and breaks modularity. But practicality beats purity...
475
+ if TYPE_CHECKING:
476
+
477
+ class _SkipMarkDecorator(MarkDecorator):
478
+ @overload # type: ignore[override,no-overload-impl]
479
+ def __call__(self, arg: Markable) -> Markable: ...
480
+
481
+ @overload
482
+ def __call__(self, reason: str = ...) -> MarkDecorator: ...
483
+
484
+ class _SkipifMarkDecorator(MarkDecorator):
485
+ def __call__( # type: ignore[override]
486
+ self,
487
+ condition: str | bool = ...,
488
+ *conditions: str | bool,
489
+ reason: str = ...,
490
+ ) -> MarkDecorator: ...
491
+
492
+ class _XfailMarkDecorator(MarkDecorator):
493
+ @overload # type: ignore[override,no-overload-impl]
494
+ def __call__(self, arg: Markable) -> Markable: ...
495
+
496
+ @overload
497
+ def __call__(
498
+ self,
499
+ condition: str | bool = False,
500
+ *conditions: str | bool,
501
+ reason: str = ...,
502
+ run: bool = ...,
503
+ raises: None
504
+ | type[BaseException]
505
+ | tuple[type[BaseException], ...]
506
+ | AbstractRaises[BaseException] = ...,
507
+ strict: bool = ...,
508
+ ) -> MarkDecorator: ...
509
+
510
+ class _ParametrizeMarkDecorator(MarkDecorator):
511
+ def __call__( # type: ignore[override]
512
+ self,
513
+ argnames: str | Sequence[str],
514
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
515
+ *,
516
+ indirect: bool | Sequence[str] = ...,
517
+ ids: Iterable[None | str | float | int | bool]
518
+ | Callable[[Any], object | None]
519
+ | None = ...,
520
+ scope: _ScopeName | None = ...,
521
+ ) -> MarkDecorator: ...
522
+
523
+ class _UsefixturesMarkDecorator(MarkDecorator):
524
+ def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
525
+ ...
526
+
527
+ class _FilterwarningsMarkDecorator(MarkDecorator):
528
+ def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override]
529
+ ...
530
+
531
+
532
+ @final
533
+ class MarkGenerator:
534
+ """Factory for :class:`MarkDecorator` objects - exposed as
535
+ a ``pytest.mark`` singleton instance.
536
+
537
+ Example::
538
+
539
+ import pytest
540
+
541
+
542
+ @pytest.mark.slowtest
543
+ def test_function():
544
+ pass
545
+
546
+ applies a 'slowtest' :class:`Mark` on ``test_function``.
547
+ """
548
+
549
+ # See TYPE_CHECKING above.
550
+ if TYPE_CHECKING:
551
+ skip: _SkipMarkDecorator
552
+ skipif: _SkipifMarkDecorator
553
+ xfail: _XfailMarkDecorator
554
+ parametrize: _ParametrizeMarkDecorator
555
+ usefixtures: _UsefixturesMarkDecorator
556
+ filterwarnings: _FilterwarningsMarkDecorator
557
+
558
+ def __init__(self, *, _ispytest: bool = False) -> None:
559
+ check_ispytest(_ispytest)
560
+ self._config: Config | None = None
561
+ self._markers: set[str] = set()
562
+
563
+ def __getattr__(self, name: str) -> MarkDecorator:
564
+ """Generate a new :class:`MarkDecorator` with the given name."""
565
+ if name[0] == "_":
566
+ raise AttributeError("Marker name must NOT start with underscore")
567
+
568
+ if self._config is not None:
569
+ # We store a set of markers as a performance optimisation - if a mark
570
+ # name is in the set we definitely know it, but a mark may be known and
571
+ # not in the set. We therefore start by updating the set!
572
+ if name not in self._markers:
573
+ for line in self._config.getini("markers"):
574
+ # example lines: "skipif(condition): skip the given test if..."
575
+ # or "hypothesis: tests which use Hypothesis", so to get the
576
+ # marker name we split on both `:` and `(`.
577
+ marker = line.split(":")[0].split("(")[0].strip()
578
+ self._markers.add(marker)
579
+
580
+ # If the name is not in the set of known marks after updating,
581
+ # then it really is time to issue a warning or an error.
582
+ if name not in self._markers:
583
+ # Raise a specific error for common misspellings of "parametrize".
584
+ if name in ["parameterize", "parametrise", "parameterise"]:
585
+ __tracebackhide__ = True
586
+ fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
587
+
588
+ strict_markers = self._config.getini("strict_markers")
589
+ if strict_markers is None:
590
+ strict_markers = self._config.getini("strict")
591
+ if strict_markers:
592
+ fail(
593
+ f"{name!r} not found in `markers` configuration option",
594
+ pytrace=False,
595
+ )
596
+
597
+ warnings.warn(
598
+ f"Unknown pytest.mark.{name} - is this a typo? You can register "
599
+ "custom marks to avoid this warning - for details, see "
600
+ "https://docs.pytest.org/en/stable/how-to/mark.html",
601
+ PytestUnknownMarkWarning,
602
+ 2,
603
+ )
604
+
605
+ return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
606
+
607
+
608
+ MARK_GEN = MarkGenerator(_ispytest=True)
609
+
610
+
611
+ @final
612
+ class NodeKeywords(MutableMapping[str, Any]):
613
+ __slots__ = ("_markers", "node", "parent")
614
+
615
+ def __init__(self, node: Node) -> None:
616
+ self.node = node
617
+ self.parent = node.parent
618
+ self._markers = {node.name: True}
619
+
620
+ def __getitem__(self, key: str) -> Any:
621
+ try:
622
+ return self._markers[key]
623
+ except KeyError:
624
+ if self.parent is None:
625
+ raise
626
+ return self.parent.keywords[key]
627
+
628
+ def __setitem__(self, key: str, value: Any) -> None:
629
+ self._markers[key] = value
630
+
631
+ # Note: we could've avoided explicitly implementing some of the methods
632
+ # below and use the collections.abc fallback, but that would be slow.
633
+
634
+ def __contains__(self, key: object) -> bool:
635
+ return key in self._markers or (
636
+ self.parent is not None and key in self.parent.keywords
637
+ )
638
+
639
+ def update( # type: ignore[override]
640
+ self,
641
+ other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (),
642
+ **kwds: Any,
643
+ ) -> None:
644
+ self._markers.update(other)
645
+ self._markers.update(kwds)
646
+
647
+ def __delitem__(self, key: str) -> None:
648
+ raise ValueError("cannot delete key in keywords dict")
649
+
650
+ def __iter__(self) -> Iterator[str]:
651
+ # Doesn't need to be fast.
652
+ yield from self._markers
653
+ if self.parent is not None:
654
+ for keyword in self.parent.keywords:
655
+ # self._marks and self.parent.keywords can have duplicates.
656
+ if keyword not in self._markers:
657
+ yield keyword
658
+
659
+ def __len__(self) -> int:
660
+ # Doesn't need to be fast.
661
+ return sum(1 for keyword in self)
662
+
663
+ def __repr__(self) -> str:
664
+ return f"<NodeKeywords for node {self.node}>"
py311/lib/python3.11/site-packages/_virtualenv.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:69ac3d8f27e679c81b94ab30b3b56e9cd138219b1ba94a1fa3606d5a76a1433d
3
+ size 18
py311/lib/python3.11/site-packages/aiohttp-3.13.3.dist-info/licenses/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright aio-libs contributors.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
py311/lib/python3.11/site-packages/aiohttp-3.13.3.dist-info/licenses/vendor/llhttp/LICENSE ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This software is licensed under the MIT License.
2
+
3
+ Copyright Fedor Indutny, 2018.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a
6
+ copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to permit
10
+ persons to whom the Software is furnished to do so, subject to the
11
+ following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included
14
+ in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
19
+ NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
20
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
21
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
22
+ USE OR OTHER DEALINGS IN THE SOFTWARE.