Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +84 -0
- ccevolve/dataclaw_export.jsonl +3 -0
- docs/code_evolution_history_panes.mp4 +3 -0
- docs/conceptual.png +3 -0
- docs/webui.png +3 -0
- my/ablation_study_summary.png +3 -0
- my/aux_7vs4_comparison.png +3 -0
- my/auxiliary_ablation_plots.png +3 -0
- my/auxiliary_metric_correlations.png +3 -0
- my/correlation_evolution_over_time.png +3 -0
- my/correlation_explanation.png +3 -0
- my/refined_aux_comparison.png +3 -0
- py311/bin/python +3 -0
- py311/bin/python3 +3 -0
- py311/bin/python3.11 +3 -0
- py311/bin/ty +3 -0
- py311/lib/python3.11/site-packages/30fcd23745efe32ce681__mypyc.cpython-311-x86_64-linux-gnu.so +3 -0
- py311/lib/python3.11/site-packages/Levenshtein/levenshtein_cpp.cpython-311-x86_64-linux-gnu.so +3 -0
- py311/lib/python3.11/site-packages/PIL/_imaging.cpython-311-x86_64-linux-gnu.so +3 -0
- py311/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so +3 -0
- py311/lib/python3.11/site-packages/PIL/_imagingft.cpython-311-x86_64-linux-gnu.so +3 -0
- py311/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so +3 -0
- py311/lib/python3.11/site-packages/__editable__.shinka-0.0.1.pth +3 -0
- py311/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc +3 -0
- py311/lib/python3.11/site-packages/_pytest/_code/__init__.py +26 -0
- py311/lib/python3.11/site-packages/_pytest/_code/code.py +1565 -0
- py311/lib/python3.11/site-packages/_pytest/_code/source.py +225 -0
- py311/lib/python3.11/site-packages/_pytest/_io/__init__.py +10 -0
- py311/lib/python3.11/site-packages/_pytest/_io/pprint.py +673 -0
- py311/lib/python3.11/site-packages/_pytest/_io/saferepr.py +130 -0
- py311/lib/python3.11/site-packages/_pytest/_io/terminalwriter.py +258 -0
- py311/lib/python3.11/site-packages/_pytest/_io/wcwidth.py +57 -0
- py311/lib/python3.11/site-packages/_pytest/_py/__init__.py +0 -0
- py311/lib/python3.11/site-packages/_pytest/_py/error.py +119 -0
- py311/lib/python3.11/site-packages/_pytest/_py/path.py +1475 -0
- py311/lib/python3.11/site-packages/_pytest/assertion/__init__.py +208 -0
- py311/lib/python3.11/site-packages/_pytest/assertion/rewrite.py +1202 -0
- py311/lib/python3.11/site-packages/_pytest/assertion/truncate.py +137 -0
- py311/lib/python3.11/site-packages/_pytest/assertion/util.py +615 -0
- py311/lib/python3.11/site-packages/_pytest/config/__init__.py +2197 -0
- py311/lib/python3.11/site-packages/_pytest/config/argparsing.py +578 -0
- py311/lib/python3.11/site-packages/_pytest/config/compat.py +85 -0
- py311/lib/python3.11/site-packages/_pytest/config/exceptions.py +15 -0
- py311/lib/python3.11/site-packages/_pytest/config/findpaths.py +350 -0
- py311/lib/python3.11/site-packages/_pytest/mark/__init__.py +301 -0
- py311/lib/python3.11/site-packages/_pytest/mark/expression.py +353 -0
- py311/lib/python3.11/site-packages/_pytest/mark/structures.py +664 -0
- py311/lib/python3.11/site-packages/_virtualenv.pth +3 -0
- py311/lib/python3.11/site-packages/aiohttp-3.13.3.dist-info/licenses/LICENSE.txt +13 -0
- 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
|
docs/webui.png
ADDED
|
Git LFS Details
|
my/ablation_study_summary.png
ADDED
|
Git LFS Details
|
my/aux_7vs4_comparison.png
ADDED
|
Git LFS Details
|
my/auxiliary_ablation_plots.png
ADDED
|
Git LFS Details
|
my/auxiliary_metric_correlations.png
ADDED
|
Git LFS Details
|
my/correlation_evolution_over_time.png
ADDED
|
Git LFS Details
|
my/correlation_explanation.png
ADDED
|
Git LFS Details
|
my/refined_aux_comparison.png
ADDED
|
Git LFS Details
|
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.
|