From f20080305d10d27e489993bcf87df7f41a749b8e Mon Sep 17 00:00:00 2001 From: Viktor Antropov Date: Wed, 9 Sep 2026 13:12:55 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B6/=D0=B4=20=D1=81=D0=BB=D0=BE?= =?UTF-8?q?=D0=B9=20+=20cant=5Fswim=20=D0=B2=20=D1=81=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=20+=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=20=D0=B2=D0=BD=D0=B5=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Определение специфических рекомендаций (матрица профилей §8): 1. Ж/д слой (закрыт мёртвый railway ×2.5 у РАС): - /api/v1/water/{case_id} отдаёт railway=rail как LineString (без service/industrial/military веток), кэш общий v2; - railway_warning «перекрыть/проверить немедленно» по профилям; - SearchMap: Polyline слой ж/д (тёмно-красный), счётчики 💧/🚂. 2. cant_swim → профиль не_умеет_плавать (water ×3.0, без изменения радиуса, critical_warning «обследовать водоёмы НЕМЕДЛЕННО»): - раньше чекбокс влиял только на текст, в скоринге был пробел; - derive в analyze._derive_profiles — работает и для closed_cases. 3. unmodeled_profiles: ДЦП/слабое зрение/слух — честная пометка «вне поведенческой модели» с пояснением (vector_tasks B12: профили без аналога не выдавать за учтённые); блок на фронте в карточке здоровья. Площадь воды: сферический эксцесс, проверен на квадрате 53° (744017 м² vs 743272 точного). Тесты: 202 passed (новый test_cant_swim_profile). --- .venv-test/bin/Activate.ps1 | 247 + .venv-test/bin/activate | 71 + .venv-test/bin/activate.csh | 27 + .venv-test/bin/activate.fish | 69 + .venv-test/bin/cffi-gen-src | 8 + .venv-test/bin/fastapi | 8 + .venv-test/bin/httpx | 8 + .venv-test/bin/idna | 8 + .venv-test/bin/pip | 8 + .venv-test/bin/pip3 | 8 + .venv-test/bin/pip3.12 | 8 + .venv-test/bin/py.test | 8 + .venv-test/bin/pygmentize | 8 + .venv-test/bin/pyrsa-decrypt | 8 + .venv-test/bin/pyrsa-encrypt | 8 + .venv-test/bin/pyrsa-keygen | 8 + .venv-test/bin/pyrsa-priv2pub | 8 + .venv-test/bin/pyrsa-sign | 8 + .venv-test/bin/pyrsa-verify | 8 + .venv-test/bin/pytest | 8 + .venv-test/bin/python | 1 + .venv-test/bin/python3 | 1 + .venv-test/bin/python3.12 | 1 + .../_cffi_backend.cpython-312-darwin.so | Bin 0 -> 229552 bytes .../site-packages/_pytest/__init__.py | 13 + .../site-packages/_pytest/_argcomplete.py | 117 + .../site-packages/_pytest/_code/__init__.py | 26 + .../site-packages/_pytest/_code/code.py | 1632 ++ .../site-packages/_pytest/_code/source.py | 228 + .../site-packages/_pytest/_io/__init__.py | 10 + .../site-packages/_pytest/_io/pprint.py | 673 + .../site-packages/_pytest/_io/saferepr.py | 155 + .../_pytest/_io/terminalwriter.py | 258 + .../site-packages/_pytest/_io/wcwidth.py | 57 + .../site-packages/_pytest/_py/__init__.py | 0 .../site-packages/_pytest/_py/error.py | 119 + .../site-packages/_pytest/_py/path.py | 1475 ++ .../site-packages/_pytest/_version.py | 24 + .../_pytest/assertion/__init__.py | 236 + .../_pytest/assertion/_compare_any.py | 131 + .../_pytest/assertion/_compare_mapping.py | 48 + .../_pytest/assertion/_compare_sequence.py | 90 + .../_pytest/assertion/_compare_set.py | 101 + .../_pytest/assertion/_guards.py | 60 + .../_pytest/assertion/_typing.py | 12 + .../_pytest/assertion/compare_text.py | 104 + .../_pytest/assertion/highlight.py | 11 + .../_pytest/assertion/rewrite.py | 1193 ++ .../_pytest/assertion/truncate.py | 128 + .../site-packages/_pytest/assertion/util.py | 215 + .../site-packages/_pytest/cacheprovider.py | 640 + .../site-packages/_pytest/capture.py | 1151 ++ .../site-packages/_pytest/compat.py | 329 + .../site-packages/_pytest/config/__init__.py | 2246 ++ .../_pytest/config/argparsing.py | 503 + .../_pytest/config/exceptions.py | 15 + .../site-packages/_pytest/config/findpaths.py | 350 + .../site-packages/_pytest/debugging.py | 404 + .../site-packages/_pytest/deprecated.py | 139 + .../site-packages/_pytest/doctest.py | 735 + .../site-packages/_pytest/faulthandler.py | 119 + .../site-packages/_pytest/fixtures.py | 2393 +++ .../site-packages/_pytest/freeze_support.py | 45 + .../site-packages/_pytest/helpconfig.py | 293 + .../site-packages/_pytest/hookspec.py | 1289 ++ .../site-packages/_pytest/junitxml.py | 703 + .../site-packages/_pytest/legacypath.py | 468 + .../site-packages/_pytest/logging.py | 975 + .../python3.12/site-packages/_pytest/main.py | 1215 ++ .../site-packages/_pytest/mark/__init__.py | 301 + .../site-packages/_pytest/mark/expression.py | 353 + .../site-packages/_pytest/mark/structures.py | 695 + .../site-packages/_pytest/monkeypatch.py | 430 + .../python3.12/site-packages/_pytest/nodes.py | 764 + .../site-packages/_pytest/outcomes.py | 286 + .../site-packages/_pytest/pastebin.py | 121 + .../site-packages/_pytest/pathlib.py | 1054 + .../python3.12/site-packages/_pytest/py.typed | 0 .../site-packages/_pytest/pytester.py | 1803 ++ .../_pytest/pytester_assertions.py | 74 + .../site-packages/_pytest/python.py | 1759 ++ .../site-packages/_pytest/python_api.py | 922 + .../site-packages/_pytest/raises.py | 1505 ++ .../site-packages/_pytest/recwarn.py | 379 + .../site-packages/_pytest/reports.py | 694 + .../site-packages/_pytest/runner.py | 593 + .../python3.12/site-packages/_pytest/scope.py | 91 + .../site-packages/_pytest/setuponly.py | 98 + .../site-packages/_pytest/setupplan.py | 39 + .../site-packages/_pytest/skipping.py | 321 + .../python3.12/site-packages/_pytest/stash.py | 116 + .../site-packages/_pytest/stepwise.py | 209 + .../site-packages/_pytest/subtests.py | 418 + .../site-packages/_pytest/terminal.py | 1786 ++ .../site-packages/_pytest/terminalprogress.py | 30 + .../site-packages/_pytest/threadexception.py | 152 + .../site-packages/_pytest/timing.py | 97 + .../site-packages/_pytest/tmpdir.py | 351 + .../site-packages/_pytest/tracemalloc.py | 24 + .../site-packages/_pytest/unittest.py | 653 + .../_pytest/unraisableexception.py | 188 + .../site-packages/_pytest/warning_types.py | 166 + .../site-packages/_pytest/warnings.py | 152 + .../aiosqlite-0.22.1.dist-info/INSTALLER | 1 + .../aiosqlite-0.22.1.dist-info/METADATA | 123 + .../aiosqlite-0.22.1.dist-info/RECORD | 27 + .../aiosqlite-0.22.1.dist-info/REQUESTED | 0 .../aiosqlite-0.22.1.dist-info/WHEEL | 4 + .../licenses/LICENSE | 21 + .../site-packages/aiosqlite/__init__.py | 44 + .../site-packages/aiosqlite/__version__.py | 7 + .../site-packages/aiosqlite/context.py | 56 + .../site-packages/aiosqlite/core.py | 468 + .../site-packages/aiosqlite/cursor.py | 110 + .../site-packages/aiosqlite/py.typed | 0 .../site-packages/aiosqlite/tests/__init__.py | 4 + .../site-packages/aiosqlite/tests/__main__.py | 7 + .../site-packages/aiosqlite/tests/helpers.py | 29 + .../site-packages/aiosqlite/tests/perf.py | 221 + .../site-packages/aiosqlite/tests/smoke.py | 537 + .../annotated_doc-0.0.5.dist-info/INSTALLER | 1 + .../annotated_doc-0.0.5.dist-info/METADATA | 144 + .../annotated_doc-0.0.5.dist-info/RECORD | 11 + .../annotated_doc-0.0.5.dist-info/WHEEL | 4 + .../entry_points.txt | 4 + .../licenses/LICENSE | 21 + .../site-packages/annotated_doc/__init__.py | 3 + .../site-packages/annotated_doc/main.py | 36 + .../site-packages/annotated_doc/py.typed | 0 .../annotated_types-0.8.0.dist-info/INSTALLER | 1 + .../annotated_types-0.8.0.dist-info/METADATA | 295 + .../annotated_types-0.8.0.dist-info/RECORD | 10 + .../annotated_types-0.8.0.dist-info/WHEEL | 4 + .../licenses/LICENSE | 21 + .../site-packages/annotated_types/__init__.py | 416 + .../site-packages/annotated_types/py.typed | 0 .../annotated_types/test_cases.py | 146 + .../anyio-4.15.1.dist-info/INSTALLER | 1 + .../anyio-4.15.1.dist-info/METADATA | 108 + .../anyio-4.15.1.dist-info/RECORD | 100 + .../anyio-4.15.1.dist-info/WHEEL | 5 + .../anyio-4.15.1.dist-info/entry_points.txt | 2 + .../anyio-4.15.1.dist-info/licenses/LICENSE | 20 + .../anyio-4.15.1.dist-info/top_level.txt | 1 + .../site-packages/anyio/__init__.py | 154 + .../site-packages/anyio/_backends/__init__.py | 0 .../site-packages/anyio/_backends/_asyncio.py | 3201 +++ .../site-packages/anyio/_backends/_trio.py | 1473 ++ .../site-packages/anyio/_core/__init__.py | 0 .../anyio/_core/_asyncio_selector_thread.py | 167 + .../anyio/_core/_concurrency_utils.py | 156 + .../anyio/_core/_contextmanagers.py | 200 + .../site-packages/anyio/_core/_eventloop.py | 240 + .../site-packages/anyio/_core/_exceptions.py | 205 + .../site-packages/anyio/_core/_fileio.py | 1045 + .../site-packages/anyio/_core/_futures.py | 191 + .../site-packages/anyio/_core/_resources.py | 18 + .../site-packages/anyio/_core/_signals.py | 29 + .../site-packages/anyio/_core/_sockets.py | 1011 + .../site-packages/anyio/_core/_streams.py | 52 + .../anyio/_core/_subprocesses.py | 196 + .../anyio/_core/_synchronization.py | 778 + .../site-packages/anyio/_core/_tasks.py | 464 + .../site-packages/anyio/_core/_tempfile.py | 615 + .../site-packages/anyio/_core/_testing.py | 82 + .../site-packages/anyio/_core/_typedattr.py | 85 + .../site-packages/anyio/_lazyimport.py | 176 + .../site-packages/anyio/abc/__init__.py | 63 + .../site-packages/anyio/abc/_eventloop.py | 428 + .../site-packages/anyio/abc/_resources.py | 39 + .../site-packages/anyio/abc/_sockets.py | 401 + .../site-packages/anyio/abc/_streams.py | 237 + .../site-packages/anyio/abc/_subprocesses.py | 82 + .../site-packages/anyio/abc/_tasks.py | 223 + .../site-packages/anyio/abc/_testing.py | 79 + .../site-packages/anyio/from_thread.py | 588 + .../site-packages/anyio/functools.py | 400 + .../site-packages/anyio/itertools.py | 629 + .../site-packages/anyio/lowlevel.py | 228 + .../python3.12/site-packages/anyio/py.typed | 0 .../site-packages/anyio/pytest_plugin.py | 404 + .../site-packages/anyio/streams/__init__.py | 0 .../site-packages/anyio/streams/buffered.py | 201 + .../site-packages/anyio/streams/file.py | 157 + .../site-packages/anyio/streams/memory.py | 326 + .../site-packages/anyio/streams/stapled.py | 160 + .../site-packages/anyio/streams/text.py | 176 + .../site-packages/anyio/streams/tls.py | 436 + .../site-packages/anyio/to_interpreter.py | 246 + .../site-packages/anyio/to_process.py | 269 + .../site-packages/anyio/to_thread.py | 80 + .../bcrypt-5.0.0.dist-info/INSTALLER | 1 + .../bcrypt-5.0.0.dist-info/LICENSE | 201 + .../bcrypt-5.0.0.dist-info/METADATA | 342 + .../bcrypt-5.0.0.dist-info/RECORD | 12 + .../bcrypt-5.0.0.dist-info/REQUESTED | 0 .../bcrypt-5.0.0.dist-info/WHEEL | 5 + .../bcrypt-5.0.0.dist-info/top_level.txt | 1 + .../site-packages/bcrypt/__init__.py | 43 + .../site-packages/bcrypt/__init__.pyi | 10 + .../site-packages/bcrypt/_bcrypt.abi3.so | Bin 0 -> 1172304 bytes .../python3.12/site-packages/bcrypt/py.typed | 0 .../certifi-2026.7.22.dist-info/INSTALLER | 1 + .../certifi-2026.7.22.dist-info/METADATA | 78 + .../certifi-2026.7.22.dist-info/RECORD | 18 + .../certifi-2026.7.22.dist-info/WHEEL | 5 + .../licenses/LICENSE | 20 + .../certifi-2026.7.22.dist-info/top_level.txt | 1 + .../site-packages/certifi/__init__.py | 4 + .../site-packages/certifi/__main__.py | 12 + .../site-packages/certifi/cacert.pem | 3959 ++++ .../python3.12/site-packages/certifi/core.py | 83 + .../python3.12/site-packages/certifi/py.typed | 0 .../site-packages/certifi/tests/__init__.py | 0 .../certifi/tests/test_certify.py | 18 + .../cffi-2.1.1.dist-info/INSTALLER | 1 + .../cffi-2.1.1.dist-info/METADATA | 67 + .../site-packages/cffi-2.1.1.dist-info/RECORD | 53 + .../site-packages/cffi-2.1.1.dist-info/WHEEL | 6 + .../cffi-2.1.1.dist-info/entry_points.txt | 5 + .../cffi-2.1.1.dist-info/licenses/LICENSE | 23 + .../cffi-2.1.1.dist-info/top_level.txt | 2 + .../python3.12/site-packages/cffi/__init__.py | 14 + .../site-packages/cffi/_cffi_errors.h | 135 + .../site-packages/cffi/_cffi_gen_src.py | 216 + .../site-packages/cffi/_cffi_include.h | 397 + .../site-packages/cffi/_embedding.h | 512 + .../site-packages/cffi/_imp_emulation.py | 83 + .../site-packages/cffi/_shimmed_dist_utils.py | 45 + .../lib/python3.12/site-packages/cffi/api.py | 960 + .../site-packages/cffi/backend_ctypes.py | 1120 + .../site-packages/cffi/cffi_opcode.py | 187 + .../site-packages/cffi/commontypes.py | 82 + .../python3.12/site-packages/cffi/cparser.py | 1014 + .../python3.12/site-packages/cffi/error.py | 31 + .../site-packages/cffi/ffiplatform.py | 113 + .../python3.12/site-packages/cffi/gen_src.py | 5 + .../lib/python3.12/site-packages/cffi/lock.py | 30 + .../python3.12/site-packages/cffi/model.py | 617 + .../site-packages/cffi/parse_c_type.h | 181 + .../site-packages/cffi/pkgconfig.py | 121 + .../site-packages/cffi/recompiler.py | 1586 ++ .../site-packages/cffi/setuptools_ext.py | 231 + .../site-packages/cffi/vengine_cpy.py | 1058 + .../site-packages/cffi/vengine_gen.py | 679 + .../python3.12/site-packages/cffi/verifier.py | 306 + .../cryptography-50.0.1.dist-info/INSTALLER | 1 + .../cryptography-50.0.1.dist-info/METADATA | 110 + .../cryptography-50.0.1.dist-info/RECORD | 199 + .../cryptography-50.0.1.dist-info/WHEEL | 4 + .../licenses/LICENSE | 3 + .../licenses/LICENSE.APACHE | 202 + .../licenses/LICENSE.BSD | 27 + .../sboms/cryptography-rust.cyclonedx.json | 1365 ++ .../sboms/sbom.json | 43 + .../site-packages/cryptography/__about__.py | 17 + .../site-packages/cryptography/__init__.py | 13 + .../site-packages/cryptography/cobblestone.py | 21 + .../site-packages/cryptography/exceptions.py | 52 + .../site-packages/cryptography/fernet.py | 240 + .../cryptography/hazmat/__init__.py | 13 + .../site-packages/cryptography/hazmat/_oid.py | 373 + .../cryptography/hazmat/asn1/__init__.py | 47 + .../cryptography/hazmat/asn1/asn1.py | 533 + .../cryptography/hazmat/backends/__init__.py | 13 + .../hazmat/backends/openssl/__init__.py | 9 + .../hazmat/backends/openssl/backend.py | 309 + .../cryptography/hazmat/bindings/__init__.py | 5 + .../hazmat/bindings/_rust.abi3.so | Bin 0 -> 11839072 bytes .../hazmat/bindings/_rust/__init__.pyi | 67 + .../hazmat/bindings/_rust/_openssl.pyi | 8 + .../hazmat/bindings/_rust/asn1.pyi | 7 + .../hazmat/bindings/_rust/cobblestone.pyi | 33 + .../bindings/_rust/declarative_asn1.pyi | 133 + .../hazmat/bindings/_rust/exceptions.pyi | 17 + .../hazmat/bindings/_rust/ocsp.pyi | 117 + .../bindings/_rust/openssl/__init__.pyi | 82 + .../hazmat/bindings/_rust/openssl/aead.pyi | 189 + .../hazmat/bindings/_rust/openssl/ciphers.pyi | 38 + .../hazmat/bindings/_rust/openssl/cmac.pyi | 18 + .../hazmat/bindings/_rust/openssl/dh.pyi | 51 + .../hazmat/bindings/_rust/openssl/dsa.pyi | 41 + .../hazmat/bindings/_rust/openssl/ec.pyi | 52 + .../hazmat/bindings/_rust/openssl/ed25519.pyi | 13 + .../hazmat/bindings/_rust/openssl/ed448.pyi | 13 + .../hazmat/bindings/_rust/openssl/hashes.pyi | 30 + .../hazmat/bindings/_rust/openssl/hmac.pyi | 22 + .../hazmat/bindings/_rust/openssl/hpke.pyi | 109 + .../hazmat/bindings/_rust/openssl/kdf.pyi | 205 + .../hazmat/bindings/_rust/openssl/keys.pyi | 34 + .../hazmat/bindings/_rust/openssl/keywrap.pyi | 28 + .../hazmat/bindings/_rust/openssl/mldsa.pyi | 35 + .../hazmat/bindings/_rust/openssl/mlkem.pyi | 18 + .../bindings/_rust/openssl/poly1305.pyi | 15 + .../hazmat/bindings/_rust/openssl/rsa.pyi | 55 + .../hazmat/bindings/_rust/openssl/x25519.pyi | 13 + .../hazmat/bindings/_rust/openssl/x448.pyi | 13 + .../hazmat/bindings/_rust/pkcs12.pyi | 52 + .../hazmat/bindings/_rust/pkcs7.pyi | 50 + .../hazmat/bindings/_rust/test_support.pyi | 23 + .../hazmat/bindings/_rust/x509.pyi | 313 + .../hazmat/bindings/openssl/__init__.py | 5 + .../hazmat/bindings/openssl/_conditional.py | 199 + .../hazmat/bindings/openssl/binding.py | 107 + .../cryptography/hazmat/decrepit/__init__.py | 5 + .../hazmat/decrepit/ciphers/__init__.py | 5 + .../hazmat/decrepit/ciphers/algorithms.py | 145 + .../hazmat/decrepit/ciphers/modes.py | 53 + .../hazmat/primitives/__init__.py | 5 + .../hazmat/primitives/_asymmetric.py | 19 + .../hazmat/primitives/_cipheralgorithm.py | 60 + .../cryptography/hazmat/primitives/_modes.py | 105 + .../hazmat/primitives/_serialization.py | 136 + .../hazmat/primitives/asymmetric/__init__.py | 5 + .../hazmat/primitives/asymmetric/dh.py | 253 + .../hazmat/primitives/asymmetric/dsa.py | 179 + .../hazmat/primitives/asymmetric/ec.py | 369 + .../hazmat/primitives/asymmetric/ed25519.py | 116 + .../hazmat/primitives/asymmetric/ed448.py | 143 + .../hazmat/primitives/asymmetric/mldsa.py | 505 + .../hazmat/primitives/asymmetric/mlkem.py | 278 + .../hazmat/primitives/asymmetric/padding.py | 141 + .../hazmat/primitives/asymmetric/rsa.py | 295 + .../hazmat/primitives/asymmetric/types.py | 94 + .../hazmat/primitives/asymmetric/utils.py | 28 + .../hazmat/primitives/asymmetric/x25519.py | 134 + .../hazmat/primitives/asymmetric/x448.py | 137 + .../hazmat/primitives/ciphers/__init__.py | 27 + .../hazmat/primitives/ciphers/aead.py | 23 + .../hazmat/primitives/ciphers/algorithms.py | 138 + .../hazmat/primitives/ciphers/base.py | 146 + .../hazmat/primitives/ciphers/modes.py | 192 + .../cryptography/hazmat/primitives/cmac.py | 10 + .../hazmat/primitives/constant_time.py | 14 + .../cryptography/hazmat/primitives/hashes.py | 357 + .../cryptography/hazmat/primitives/hmac.py | 13 + .../cryptography/hazmat/primitives/hpke.py | 27 + .../hazmat/primitives/kdf/__init__.py | 32 + .../hazmat/primitives/kdf/argon2.py | 17 + .../hazmat/primitives/kdf/concatkdf.py | 16 + .../hazmat/primitives/kdf/hkdf.py | 16 + .../hazmat/primitives/kdf/kbkdf.py | 26 + .../hazmat/primitives/kdf/pbkdf2.py | 13 + .../hazmat/primitives/kdf/scrypt.py | 19 + .../hazmat/primitives/kdf/x963kdf.py | 13 + .../cryptography/hazmat/primitives/keywrap.py | 25 + .../cryptography/hazmat/primitives/padding.py | 69 + .../hazmat/primitives/poly1305.py | 11 + .../primitives/serialization/__init__.py | 85 + .../hazmat/primitives/serialization/base.py | 16 + .../hazmat/primitives/serialization/pkcs12.py | 176 + .../hazmat/primitives/serialization/pkcs7.py | 412 + .../hazmat/primitives/serialization/ssh.py | 1615 ++ .../hazmat/primitives/twofactor/__init__.py | 9 + .../hazmat/primitives/twofactor/hotp.py | 101 + .../hazmat/primitives/twofactor/totp.py | 56 + .../site-packages/cryptography/py.typed | 0 .../site-packages/cryptography/utils.py | 136 + .../cryptography/x509/__init__.py | 270 + .../site-packages/cryptography/x509/base.py | 834 + .../x509/certificate_transparency.py | 35 + .../cryptography/x509/extensions.py | 2533 +++ .../cryptography/x509/general_name.py | 281 + .../site-packages/cryptography/x509/name.py | 489 + .../site-packages/cryptography/x509/ocsp.py | 379 + .../site-packages/cryptography/x509/oid.py | 37 + .../cryptography/x509/verification.py | 34 + .../ecdsa-0.19.2.dist-info/INSTALLER | 1 + .../ecdsa-0.19.2.dist-info/LICENSE | 24 + .../ecdsa-0.19.2.dist-info/METADATA | 671 + .../ecdsa-0.19.2.dist-info/RECORD | 66 + .../ecdsa-0.19.2.dist-info/WHEEL | 6 + .../ecdsa-0.19.2.dist-info/top_level.txt | 1 + .../site-packages/ecdsa/__init__.py | 104 + .../python3.12/site-packages/ecdsa/_compat.py | 138 + .../python3.12/site-packages/ecdsa/_rwlock.py | 86 + .../python3.12/site-packages/ecdsa/_sha3.py | 181 + .../site-packages/ecdsa/_version.py | 21 + .../python3.12/site-packages/ecdsa/curves.py | 590 + .../lib/python3.12/site-packages/ecdsa/der.py | 484 + .../python3.12/site-packages/ecdsa/ecdh.py | 336 + .../python3.12/site-packages/ecdsa/ecdsa.py | 1094 + .../python3.12/site-packages/ecdsa/eddsa.py | 252 + .../site-packages/ecdsa/ellipticcurve.py | 1609 ++ .../python3.12/site-packages/ecdsa/errors.py | 4 + .../python3.12/site-packages/ecdsa/keys.py | 1631 ++ .../site-packages/ecdsa/numbertheory.py | 835 + .../python3.12/site-packages/ecdsa/rfc6979.py | 113 + .../lib/python3.12/site-packages/ecdsa/ssh.py | 83 + .../site-packages/ecdsa/test_curves.py | 361 + .../site-packages/ecdsa/test_der.py | 622 + .../site-packages/ecdsa/test_ecdh.py | 449 + .../site-packages/ecdsa/test_ecdsa.py | 694 + .../site-packages/ecdsa/test_eddsa.py | 1124 + .../site-packages/ecdsa/test_ellipticcurve.py | 294 + .../site-packages/ecdsa/test_jacobi.py | 934 + .../site-packages/ecdsa/test_keys.py | 1138 ++ .../ecdsa/test_malformed_sigs.py | 378 + .../site-packages/ecdsa/test_numbertheory.py | 483 + .../site-packages/ecdsa/test_pyecdsa.py | 2564 +++ .../site-packages/ecdsa/test_rw_lock.py | 180 + .../site-packages/ecdsa/test_sha3.py | 111 + .../python3.12/site-packages/ecdsa/util.py | 533 + .../fastapi-0.141.1.dist-info/INSTALLER | 1 + .../fastapi-0.141.1.dist-info/METADATA | 617 + .../fastapi-0.141.1.dist-info/RECORD | 112 + .../fastapi-0.141.1.dist-info/REQUESTED | 0 .../fastapi-0.141.1.dist-info/WHEEL | 4 + .../entry_points.txt | 5 + .../licenses/LICENSE | 21 + .../fastapi/.agents/skills/fastapi/SKILL.md | 321 + .../skills/fastapi/references/dependencies.md | 142 + .../skills/fastapi/references/other-tools.md | 76 + .../fastapi/references/path-operations.md | 93 + .../skills/fastapi/references/pydantic.md | 93 + .../skills/fastapi/references/responses.md | 79 + .../skills/fastapi/references/streaming.md | 105 + .../site-packages/fastapi/__init__.py | 25 + .../site-packages/fastapi/__main__.py | 3 + .../site-packages/fastapi/_compat/__init__.py | 40 + .../site-packages/fastapi/_compat/shared.py | 222 + .../site-packages/fastapi/_compat/v2.py | 493 + .../site-packages/fastapi/applications.py | 4774 +++++ .../site-packages/fastapi/background.py | 61 + .../python3.12/site-packages/fastapi/cli.py | 13 + .../site-packages/fastapi/concurrency.py | 41 + .../site-packages/fastapi/datastructures.py | 186 + .../fastapi/dependencies/__init__.py | 0 .../fastapi/dependencies/models.py | 234 + .../fastapi/dependencies/utils.py | 1053 + .../site-packages/fastapi/encoders.py | 366 + .../fastapi/exception_handlers.py | 34 + .../site-packages/fastapi/exceptions.py | 256 + .../site-packages/fastapi/logger.py | 3 + .../fastapi/middleware/__init__.py | 1 + .../fastapi/middleware/asyncexitstack.py | 18 + .../site-packages/fastapi/middleware/cors.py | 1 + .../site-packages/fastapi/middleware/gzip.py | 1 + .../fastapi/middleware/httpsredirect.py | 3 + .../fastapi/middleware/trustedhost.py | 3 + .../site-packages/fastapi/middleware/wsgi.py | 3 + .../site-packages/fastapi/openapi/__init__.py | 0 .../fastapi/openapi/constants.py | 3 + .../site-packages/fastapi/openapi/docs.py | 389 + .../site-packages/fastapi/openapi/models.py | 435 + .../site-packages/fastapi/openapi/utils.py | 679 + .../site-packages/fastapi/param_functions.py | 2460 +++ .../site-packages/fastapi/params.py | 754 + .../python3.12/site-packages/fastapi/py.typed | 0 .../site-packages/fastapi/requests.py | 2 + .../site-packages/fastapi/responses.py | 98 + .../site-packages/fastapi/routing.py | 6447 ++++++ .../fastapi/security/__init__.py | 15 + .../site-packages/fastapi/security/api_key.py | 320 + .../site-packages/fastapi/security/base.py | 6 + .../site-packages/fastapi/security/http.py | 417 + .../site-packages/fastapi/security/oauth2.py | 693 + .../fastapi/security/open_id_connect_url.py | 94 + .../site-packages/fastapi/security/utils.py | 7 + .../python3.12/site-packages/fastapi/sse.py | 241 + .../site-packages/fastapi/staticfiles.py | 1 + .../site-packages/fastapi/templating.py | 1 + .../site-packages/fastapi/testclient.py | 1 + .../python3.12/site-packages/fastapi/types.py | 12 + .../python3.12/site-packages/fastapi/utils.py | 136 + .../site-packages/fastapi/websockets.py | 3 + .../h11-0.16.0.dist-info/INSTALLER | 1 + .../h11-0.16.0.dist-info/METADATA | 202 + .../site-packages/h11-0.16.0.dist-info/RECORD | 29 + .../site-packages/h11-0.16.0.dist-info/WHEEL | 5 + .../h11-0.16.0.dist-info/licenses/LICENSE.txt | 22 + .../h11-0.16.0.dist-info/top_level.txt | 1 + .../python3.12/site-packages/h11/__init__.py | 62 + .../lib/python3.12/site-packages/h11/_abnf.py | 132 + .../site-packages/h11/_connection.py | 659 + .../python3.12/site-packages/h11/_events.py | 369 + .../python3.12/site-packages/h11/_headers.py | 282 + .../python3.12/site-packages/h11/_readers.py | 250 + .../site-packages/h11/_receivebuffer.py | 153 + .../python3.12/site-packages/h11/_state.py | 365 + .../lib/python3.12/site-packages/h11/_util.py | 135 + .../python3.12/site-packages/h11/_version.py | 16 + .../python3.12/site-packages/h11/_writers.py | 145 + .../lib/python3.12/site-packages/h11/py.typed | 1 + .../httpcore-1.0.9.dist-info/INSTALLER | 1 + .../httpcore-1.0.9.dist-info/METADATA | 625 + .../httpcore-1.0.9.dist-info/RECORD | 68 + .../httpcore-1.0.9.dist-info/WHEEL | 4 + .../licenses/LICENSE.md | 27 + .../site-packages/httpcore/__init__.py | 141 + .../python3.12/site-packages/httpcore/_api.py | 94 + .../site-packages/httpcore/_async/__init__.py | 39 + .../httpcore/_async/connection.py | 222 + .../httpcore/_async/connection_pool.py | 420 + .../site-packages/httpcore/_async/http11.py | 379 + .../site-packages/httpcore/_async/http2.py | 592 + .../httpcore/_async/http_proxy.py | 367 + .../httpcore/_async/interfaces.py | 137 + .../httpcore/_async/socks_proxy.py | 341 + .../httpcore/_backends/__init__.py | 0 .../site-packages/httpcore/_backends/anyio.py | 146 + .../site-packages/httpcore/_backends/auto.py | 52 + .../site-packages/httpcore/_backends/base.py | 101 + .../site-packages/httpcore/_backends/mock.py | 143 + .../site-packages/httpcore/_backends/sync.py | 241 + .../site-packages/httpcore/_backends/trio.py | 159 + .../site-packages/httpcore/_exceptions.py | 81 + .../site-packages/httpcore/_models.py | 516 + .../python3.12/site-packages/httpcore/_ssl.py | 9 + .../site-packages/httpcore/_sync/__init__.py | 39 + .../httpcore/_sync/connection.py | 222 + .../httpcore/_sync/connection_pool.py | 420 + .../site-packages/httpcore/_sync/http11.py | 379 + .../site-packages/httpcore/_sync/http2.py | 592 + .../httpcore/_sync/http_proxy.py | 367 + .../httpcore/_sync/interfaces.py | 137 + .../httpcore/_sync/socks_proxy.py | 341 + .../httpcore/_synchronization.py | 318 + .../site-packages/httpcore/_trace.py | 107 + .../site-packages/httpcore/_utils.py | 37 + .../site-packages/httpcore/py.typed | 0 .../httpx-0.28.1.dist-info/INSTALLER | 1 + .../httpx-0.28.1.dist-info/METADATA | 203 + .../httpx-0.28.1.dist-info/RECORD | 55 + .../httpx-0.28.1.dist-info/REQUESTED | 0 .../httpx-0.28.1.dist-info/WHEEL | 4 + .../httpx-0.28.1.dist-info/entry_points.txt | 2 + .../licenses/LICENSE.md | 12 + .../site-packages/httpx/__init__.py | 105 + .../site-packages/httpx/__version__.py | 3 + .../python3.12/site-packages/httpx/_api.py | 438 + .../python3.12/site-packages/httpx/_auth.py | 348 + .../python3.12/site-packages/httpx/_client.py | 2019 ++ .../python3.12/site-packages/httpx/_config.py | 248 + .../site-packages/httpx/_content.py | 240 + .../site-packages/httpx/_decoders.py | 393 + .../site-packages/httpx/_exceptions.py | 379 + .../python3.12/site-packages/httpx/_main.py | 506 + .../python3.12/site-packages/httpx/_models.py | 1277 ++ .../site-packages/httpx/_multipart.py | 300 + .../site-packages/httpx/_status_codes.py | 162 + .../httpx/_transports/__init__.py | 15 + .../site-packages/httpx/_transports/asgi.py | 187 + .../site-packages/httpx/_transports/base.py | 86 + .../httpx/_transports/default.py | 406 + .../site-packages/httpx/_transports/mock.py | 43 + .../site-packages/httpx/_transports/wsgi.py | 149 + .../python3.12/site-packages/httpx/_types.py | 114 + .../site-packages/httpx/_urlparse.py | 527 + .../python3.12/site-packages/httpx/_urls.py | 641 + .../python3.12/site-packages/httpx/_utils.py | 242 + .../python3.12/site-packages/httpx/py.typed | 0 .../idna-3.19.dist-info/INSTALLER | 1 + .../idna-3.19.dist-info/METADATA | 209 + .../site-packages/idna-3.19.dist-info/RECORD | 28 + .../site-packages/idna-3.19.dist-info/WHEEL | 4 + .../idna-3.19.dist-info/entry_points.txt | 3 + .../idna-3.19.dist-info/licenses/LICENSE.md | 31 + .../python3.12/site-packages/idna/__init__.py | 47 + .../python3.12/site-packages/idna/__main__.py | 6 + .../lib/python3.12/site-packages/idna/cli.py | 132 + .../python3.12/site-packages/idna/codec.py | 163 + .../python3.12/site-packages/idna/compat.py | 43 + .../lib/python3.12/site-packages/idna/core.py | 862 + .../python3.12/site-packages/idna/idnadata.py | 1897 ++ .../site-packages/idna/intranges.py | 55 + .../site-packages/idna/package_data.py | 1 + .../python3.12/site-packages/idna/py.typed | 0 .../site-packages/idna/uts46data.py | 16897 ++++++++++++++++ .../iniconfig-2.3.0.dist-info/INSTALLER | 1 + .../iniconfig-2.3.0.dist-info/METADATA | 79 + .../iniconfig-2.3.0.dist-info/RECORD | 15 + .../iniconfig-2.3.0.dist-info/WHEEL | 5 + .../licenses/LICENSE | 21 + .../iniconfig-2.3.0.dist-info/top_level.txt | 1 + .../site-packages/iniconfig/__init__.py | 249 + .../site-packages/iniconfig/_parse.py | 163 + .../site-packages/iniconfig/_version.py | 34 + .../site-packages/iniconfig/exceptions.py | 16 + .../site-packages/iniconfig/py.typed | 0 .../python3.12/site-packages/jose/__init__.py | 10 + .../site-packages/jose/backends/__init__.py | 26 + .../site-packages/jose/backends/_asn1.py | 84 + .../site-packages/jose/backends/base.py | 89 + .../jose/backends/cryptography_backend.py | 586 + .../jose/backends/ecdsa_backend.py | 150 + .../site-packages/jose/backends/native.py | 69 + .../jose/backends/rsa_backend.py | 283 + .../site-packages/jose/constants.py | 100 + .../site-packages/jose/exceptions.py | 59 + .../lib/python3.12/site-packages/jose/jwe.py | 619 + .../lib/python3.12/site-packages/jose/jwk.py | 79 + .../lib/python3.12/site-packages/jose/jws.py | 268 + .../lib/python3.12/site-packages/jose/jwt.py | 512 + .../python3.12/site-packages/jose/utils.py | 165 + .../python3.12/site-packages/jwt/__init__.py | 78 + .../site-packages/jwt/algorithms.py | 1025 + .../python3.12/site-packages/jwt/api_jwk.py | 188 + .../python3.12/site-packages/jwt/api_jws.py | 456 + .../python3.12/site-packages/jwt/api_jwt.py | 593 + .../site-packages/jwt/exceptions.py | 113 + .../lib/python3.12/site-packages/jwt/help.py | 66 + .../site-packages/jwt/jwk_set_cache.py | 31 + .../site-packages/jwt/jwks_client.py | 246 + .../lib/python3.12/site-packages/jwt/py.typed | 0 .../lib/python3.12/site-packages/jwt/types.py | 69 + .../lib/python3.12/site-packages/jwt/utils.py | 142 + .../python3.12/site-packages/jwt/warnings.py | 11 + .../site-packages/multipart/__init__.py | 38 + .../site-packages/multipart/decoders.py | 1 + .../site-packages/multipart/exceptions.py | 1 + .../site-packages/multipart/multipart.py | 1 + .../packaging-26.3.dist-info/INSTALLER | 1 + .../packaging-26.3.dist-info/METADATA | 112 + .../packaging-26.3.dist-info/RECORD | 52 + .../packaging-26.3.dist-info/WHEEL | 4 + .../packaging-26.3.dist-info/licenses/LICENSE | 3 + .../licenses/LICENSE.APACHE | 177 + .../licenses/LICENSE.BSD | 23 + .../site-packages/packaging/__init__.py | 15 + .../site-packages/packaging/_elffile.py | 108 + .../site-packages/packaging/_manylinux.py | 278 + .../site-packages/packaging/_musllinux.py | 88 + .../site-packages/packaging/_parser.py | 418 + .../site-packages/packaging/_ranges.py | 836 + .../site-packages/packaging/_structures.py | 33 + .../site-packages/packaging/_tokenizer.py | 200 + .../packaging/dependency_groups.py | 335 + .../site-packages/packaging/direct_url.py | 349 + .../site-packages/packaging/errors.py | 94 + .../packaging/licenses/__init__.py | 198 + .../site-packages/packaging/licenses/_spdx.py | 799 + .../site-packages/packaging/markers.py | 576 + .../site-packages/packaging/metadata.py | 1054 + .../site-packages/packaging/py.typed | 0 .../site-packages/packaging/pylock.py | 943 + .../site-packages/packaging/ranges.py | 2067 ++ .../site-packages/packaging/requirements.py | 192 + .../site-packages/packaging/specifiers.py | 1457 ++ .../site-packages/packaging/tags.py | 1053 + .../site-packages/packaging/utils.py | 370 + .../site-packages/packaging/version.py | 1252 ++ .../pip-24.3.1.dist-info/AUTHORS.txt | 799 + .../pip-24.3.1.dist-info/INSTALLER | 1 + .../pip-24.3.1.dist-info/LICENSE.txt | 20 + .../pip-24.3.1.dist-info/METADATA | 90 + .../site-packages/pip-24.3.1.dist-info/RECORD | 853 + .../pip-24.3.1.dist-info/REQUESTED | 0 .../site-packages/pip-24.3.1.dist-info/WHEEL | 5 + .../pip-24.3.1.dist-info/entry_points.txt | 3 + .../pip-24.3.1.dist-info/top_level.txt | 1 + .../python3.12/site-packages/pip/__init__.py | 13 + .../python3.12/site-packages/pip/__main__.py | 24 + .../site-packages/pip/__pip-runner__.py | 50 + .../site-packages/pip/_internal/__init__.py | 18 + .../site-packages/pip/_internal/build_env.py | 319 + .../site-packages/pip/_internal/cache.py | 290 + .../pip/_internal/cli/__init__.py | 4 + .../pip/_internal/cli/autocompletion.py | 176 + .../pip/_internal/cli/base_command.py | 231 + .../pip/_internal/cli/cmdoptions.py | 1075 + .../pip/_internal/cli/command_context.py | 27 + .../pip/_internal/cli/index_command.py | 170 + .../site-packages/pip/_internal/cli/main.py | 80 + .../pip/_internal/cli/main_parser.py | 134 + .../site-packages/pip/_internal/cli/parser.py | 294 + .../pip/_internal/cli/progress_bars.py | 94 + .../pip/_internal/cli/req_command.py | 329 + .../pip/_internal/cli/spinners.py | 159 + .../pip/_internal/cli/status_codes.py | 6 + .../pip/_internal/commands/__init__.py | 132 + .../pip/_internal/commands/cache.py | 225 + .../pip/_internal/commands/check.py | 67 + .../pip/_internal/commands/completion.py | 130 + .../pip/_internal/commands/configuration.py | 280 + .../pip/_internal/commands/debug.py | 201 + .../pip/_internal/commands/download.py | 146 + .../pip/_internal/commands/freeze.py | 109 + .../pip/_internal/commands/hash.py | 59 + .../pip/_internal/commands/help.py | 41 + .../pip/_internal/commands/index.py | 139 + .../pip/_internal/commands/inspect.py | 92 + .../pip/_internal/commands/install.py | 783 + .../pip/_internal/commands/list.py | 375 + .../pip/_internal/commands/search.py | 172 + .../pip/_internal/commands/show.py | 217 + .../pip/_internal/commands/uninstall.py | 114 + .../pip/_internal/commands/wheel.py | 182 + .../pip/_internal/configuration.py | 383 + .../pip/_internal/distributions/__init__.py | 21 + .../pip/_internal/distributions/base.py | 53 + .../pip/_internal/distributions/installed.py | 29 + .../pip/_internal/distributions/sdist.py | 158 + .../pip/_internal/distributions/wheel.py | 42 + .../site-packages/pip/_internal/exceptions.py | 809 + .../pip/_internal/index/__init__.py | 2 + .../pip/_internal/index/collector.py | 494 + .../pip/_internal/index/package_finder.py | 1020 + .../pip/_internal/index/sources.py | 284 + .../pip/_internal/locations/__init__.py | 456 + .../pip/_internal/locations/_distutils.py | 172 + .../pip/_internal/locations/_sysconfig.py | 214 + .../pip/_internal/locations/base.py | 81 + .../site-packages/pip/_internal/main.py | 12 + .../pip/_internal/metadata/__init__.py | 128 + .../pip/_internal/metadata/_json.py | 84 + .../pip/_internal/metadata/base.py | 688 + .../_internal/metadata/importlib/__init__.py | 6 + .../_internal/metadata/importlib/_compat.py | 85 + .../_internal/metadata/importlib/_dists.py | 221 + .../pip/_internal/metadata/importlib/_envs.py | 189 + .../pip/_internal/metadata/pkg_resources.py | 301 + .../pip/_internal/models/__init__.py | 2 + .../pip/_internal/models/candidate.py | 25 + .../pip/_internal/models/direct_url.py | 224 + .../pip/_internal/models/format_control.py | 78 + .../pip/_internal/models/index.py | 28 + .../_internal/models/installation_report.py | 56 + .../pip/_internal/models/link.py | 590 + .../pip/_internal/models/scheme.py | 25 + .../pip/_internal/models/search_scope.py | 127 + .../pip/_internal/models/selection_prefs.py | 53 + .../pip/_internal/models/target_python.py | 121 + .../pip/_internal/models/wheel.py | 118 + .../pip/_internal/network/__init__.py | 2 + .../pip/_internal/network/auth.py | 566 + .../pip/_internal/network/cache.py | 106 + .../pip/_internal/network/download.py | 187 + .../pip/_internal/network/lazy_wheel.py | 210 + .../pip/_internal/network/session.py | 522 + .../pip/_internal/network/utils.py | 98 + .../pip/_internal/network/xmlrpc.py | 62 + .../pip/_internal/operations/__init__.py | 0 .../_internal/operations/build/__init__.py | 0 .../operations/build/build_tracker.py | 138 + .../_internal/operations/build/metadata.py | 39 + .../operations/build/metadata_editable.py | 41 + .../operations/build/metadata_legacy.py | 74 + .../pip/_internal/operations/build/wheel.py | 37 + .../operations/build/wheel_editable.py | 46 + .../operations/build/wheel_legacy.py | 102 + .../pip/_internal/operations/check.py | 181 + .../pip/_internal/operations/freeze.py | 258 + .../_internal/operations/install/__init__.py | 2 + .../operations/install/editable_legacy.py | 47 + .../pip/_internal/operations/install/wheel.py | 741 + .../pip/_internal/operations/prepare.py | 732 + .../site-packages/pip/_internal/pyproject.py | 185 + .../pip/_internal/req/__init__.py | 90 + .../pip/_internal/req/constructors.py | 560 + .../pip/_internal/req/req_file.py | 574 + .../pip/_internal/req/req_install.py | 934 + .../pip/_internal/req/req_set.py | 82 + .../pip/_internal/req/req_uninstall.py | 633 + .../pip/_internal/resolution/__init__.py | 0 .../pip/_internal/resolution/base.py | 20 + .../_internal/resolution/legacy/__init__.py | 0 .../_internal/resolution/legacy/resolver.py | 597 + .../resolution/resolvelib/__init__.py | 0 .../_internal/resolution/resolvelib/base.py | 139 + .../resolution/resolvelib/candidates.py | 574 + .../resolution/resolvelib/factory.py | 823 + .../resolution/resolvelib/found_candidates.py | 174 + .../resolution/resolvelib/provider.py | 258 + .../resolution/resolvelib/reporter.py | 81 + .../resolution/resolvelib/requirements.py | 245 + .../resolution/resolvelib/resolver.py | 317 + .../pip/_internal/self_outdated_check.py | 244 + .../pip/_internal/utils/__init__.py | 0 .../pip/_internal/utils/_jaraco_text.py | 109 + .../site-packages/pip/_internal/utils/_log.py | 38 + .../pip/_internal/utils/appdirs.py | 52 + .../pip/_internal/utils/compat.py | 79 + .../pip/_internal/utils/compatibility_tags.py | 188 + .../pip/_internal/utils/datetime.py | 11 + .../pip/_internal/utils/deprecation.py | 124 + .../pip/_internal/utils/direct_url_helpers.py | 87 + .../pip/_internal/utils/egg_link.py | 80 + .../pip/_internal/utils/encoding.py | 36 + .../pip/_internal/utils/entrypoints.py | 84 + .../pip/_internal/utils/filesystem.py | 149 + .../pip/_internal/utils/filetypes.py | 27 + .../pip/_internal/utils/glibc.py | 101 + .../pip/_internal/utils/hashes.py | 147 + .../pip/_internal/utils/logging.py | 347 + .../site-packages/pip/_internal/utils/misc.py | 772 + .../pip/_internal/utils/packaging.py | 57 + .../pip/_internal/utils/retry.py | 42 + .../pip/_internal/utils/setuptools_build.py | 146 + .../pip/_internal/utils/subprocess.py | 245 + .../pip/_internal/utils/temp_dir.py | 296 + .../pip/_internal/utils/unpacking.py | 337 + .../site-packages/pip/_internal/utils/urls.py | 55 + .../pip/_internal/utils/virtualenv.py | 104 + .../pip/_internal/utils/wheel.py | 134 + .../pip/_internal/vcs/__init__.py | 15 + .../site-packages/pip/_internal/vcs/bazaar.py | 112 + .../site-packages/pip/_internal/vcs/git.py | 527 + .../pip/_internal/vcs/mercurial.py | 163 + .../pip/_internal/vcs/subversion.py | 324 + .../pip/_internal/vcs/versioncontrol.py | 688 + .../pip/_internal/wheel_builder.py | 354 + .../site-packages/pip/_vendor/__init__.py | 116 + .../pip/_vendor/cachecontrol/__init__.py | 28 + .../pip/_vendor/cachecontrol/_cmd.py | 70 + .../pip/_vendor/cachecontrol/adapter.py | 161 + .../pip/_vendor/cachecontrol/cache.py | 74 + .../_vendor/cachecontrol/caches/__init__.py | 8 + .../_vendor/cachecontrol/caches/file_cache.py | 182 + .../cachecontrol/caches/redis_cache.py | 48 + .../pip/_vendor/cachecontrol/controller.py | 499 + .../pip/_vendor/cachecontrol/filewrapper.py | 119 + .../pip/_vendor/cachecontrol/heuristics.py | 154 + .../pip/_vendor/cachecontrol/py.typed | 0 .../pip/_vendor/cachecontrol/serialize.py | 146 + .../pip/_vendor/cachecontrol/wrapper.py | 43 + .../pip/_vendor/certifi/__init__.py | 4 + .../pip/_vendor/certifi/__main__.py | 12 + .../pip/_vendor/certifi/cacert.pem | 4929 +++++ .../site-packages/pip/_vendor/certifi/core.py | 114 + .../pip/_vendor/certifi/py.typed | 0 .../pip/_vendor/distlib/__init__.py | 33 + .../pip/_vendor/distlib/compat.py | 1137 ++ .../pip/_vendor/distlib/database.py | 1329 ++ .../pip/_vendor/distlib/index.py | 508 + .../pip/_vendor/distlib/locators.py | 1295 ++ .../pip/_vendor/distlib/manifest.py | 384 + .../pip/_vendor/distlib/markers.py | 162 + .../pip/_vendor/distlib/metadata.py | 1031 + .../pip/_vendor/distlib/resources.py | 358 + .../pip/_vendor/distlib/scripts.py | 447 + .../site-packages/pip/_vendor/distlib/t32.exe | Bin 0 -> 97792 bytes .../pip/_vendor/distlib/t64-arm.exe | Bin 0 -> 182784 bytes .../site-packages/pip/_vendor/distlib/t64.exe | Bin 0 -> 108032 bytes .../site-packages/pip/_vendor/distlib/util.py | 1984 ++ .../pip/_vendor/distlib/version.py | 750 + .../site-packages/pip/_vendor/distlib/w32.exe | Bin 0 -> 91648 bytes .../pip/_vendor/distlib/w64-arm.exe | Bin 0 -> 168448 bytes .../site-packages/pip/_vendor/distlib/w64.exe | Bin 0 -> 101888 bytes .../pip/_vendor/distlib/wheel.py | 1100 + .../pip/_vendor/distro/__init__.py | 54 + .../pip/_vendor/distro/__main__.py | 4 + .../pip/_vendor/distro/distro.py | 1403 ++ .../site-packages/pip/_vendor/distro/py.typed | 0 .../pip/_vendor/idna/__init__.py | 44 + .../site-packages/pip/_vendor/idna/codec.py | 118 + .../site-packages/pip/_vendor/idna/compat.py | 13 + .../site-packages/pip/_vendor/idna/core.py | 395 + .../pip/_vendor/idna/idnadata.py | 4245 ++++ .../pip/_vendor/idna/intranges.py | 54 + .../pip/_vendor/idna/package_data.py | 2 + .../site-packages/pip/_vendor/idna/py.typed | 0 .../pip/_vendor/idna/uts46data.py | 8598 ++++++++ .../pip/_vendor/msgpack/__init__.py | 55 + .../pip/_vendor/msgpack/exceptions.py | 48 + .../site-packages/pip/_vendor/msgpack/ext.py | 168 + .../pip/_vendor/msgpack/fallback.py | 951 + .../pip/_vendor/packaging/__init__.py | 15 + .../pip/_vendor/packaging/_elffile.py | 110 + .../pip/_vendor/packaging/_manylinux.py | 262 + .../pip/_vendor/packaging/_musllinux.py | 85 + .../pip/_vendor/packaging/_parser.py | 354 + .../pip/_vendor/packaging/_structures.py | 61 + .../pip/_vendor/packaging/_tokenizer.py | 194 + .../pip/_vendor/packaging/markers.py | 325 + .../pip/_vendor/packaging/metadata.py | 804 + .../pip/_vendor/packaging/py.typed | 0 .../pip/_vendor/packaging/requirements.py | 91 + .../pip/_vendor/packaging/specifiers.py | 1009 + .../pip/_vendor/packaging/tags.py | 627 + .../pip/_vendor/packaging/utils.py | 174 + .../pip/_vendor/packaging/version.py | 563 + .../pip/_vendor/pkg_resources/__init__.py | 3676 ++++ .../pip/_vendor/platformdirs/__init__.py | 627 + .../pip/_vendor/platformdirs/__main__.py | 55 + .../pip/_vendor/platformdirs/android.py | 249 + .../pip/_vendor/platformdirs/api.py | 292 + .../pip/_vendor/platformdirs/macos.py | 130 + .../pip/_vendor/platformdirs/py.typed | 0 .../pip/_vendor/platformdirs/unix.py | 275 + .../pip/_vendor/platformdirs/version.py | 16 + .../pip/_vendor/platformdirs/windows.py | 272 + .../pip/_vendor/pygments/__init__.py | 82 + .../pip/_vendor/pygments/__main__.py | 17 + .../pip/_vendor/pygments/cmdline.py | 668 + .../pip/_vendor/pygments/console.py | 70 + .../pip/_vendor/pygments/filter.py | 70 + .../pip/_vendor/pygments/filters/__init__.py | 940 + .../pip/_vendor/pygments/formatter.py | 129 + .../_vendor/pygments/formatters/__init__.py | 157 + .../_vendor/pygments/formatters/_mapping.py | 23 + .../pip/_vendor/pygments/formatters/bbcode.py | 108 + .../pip/_vendor/pygments/formatters/groff.py | 170 + .../pip/_vendor/pygments/formatters/html.py | 987 + .../pip/_vendor/pygments/formatters/img.py | 685 + .../pip/_vendor/pygments/formatters/irc.py | 154 + .../pip/_vendor/pygments/formatters/latex.py | 518 + .../pip/_vendor/pygments/formatters/other.py | 160 + .../pygments/formatters/pangomarkup.py | 83 + .../pip/_vendor/pygments/formatters/rtf.py | 349 + .../pip/_vendor/pygments/formatters/svg.py | 185 + .../_vendor/pygments/formatters/terminal.py | 127 + .../pygments/formatters/terminal256.py | 338 + .../pip/_vendor/pygments/lexer.py | 963 + .../pip/_vendor/pygments/lexers/__init__.py | 362 + .../pip/_vendor/pygments/lexers/_mapping.py | 589 + .../pip/_vendor/pygments/lexers/python.py | 1198 ++ .../pip/_vendor/pygments/modeline.py | 43 + .../pip/_vendor/pygments/plugin.py | 72 + .../pip/_vendor/pygments/regexopt.py | 91 + .../pip/_vendor/pygments/scanner.py | 104 + .../pip/_vendor/pygments/sphinxext.py | 247 + .../pip/_vendor/pygments/style.py | 203 + .../pip/_vendor/pygments/styles/__init__.py | 61 + .../pip/_vendor/pygments/styles/_mapping.py | 54 + .../pip/_vendor/pygments/token.py | 214 + .../pip/_vendor/pygments/unistring.py | 153 + .../pip/_vendor/pygments/util.py | 324 + .../pip/_vendor/pyproject_hooks/__init__.py | 23 + .../pip/_vendor/pyproject_hooks/_compat.py | 8 + .../pip/_vendor/pyproject_hooks/_impl.py | 330 + .../pyproject_hooks/_in_process/__init__.py | 18 + .../_in_process/_in_process.py | 353 + .../pip/_vendor/requests/__init__.py | 179 + .../pip/_vendor/requests/__version__.py | 14 + .../pip/_vendor/requests/_internal_utils.py | 50 + .../pip/_vendor/requests/adapters.py | 719 + .../site-packages/pip/_vendor/requests/api.py | 157 + .../pip/_vendor/requests/auth.py | 314 + .../pip/_vendor/requests/certs.py | 24 + .../pip/_vendor/requests/compat.py | 78 + .../pip/_vendor/requests/cookies.py | 561 + .../pip/_vendor/requests/exceptions.py | 151 + .../pip/_vendor/requests/help.py | 127 + .../pip/_vendor/requests/hooks.py | 33 + .../pip/_vendor/requests/models.py | 1037 + .../pip/_vendor/requests/packages.py | 25 + .../pip/_vendor/requests/sessions.py | 831 + .../pip/_vendor/requests/status_codes.py | 128 + .../pip/_vendor/requests/structures.py | 99 + .../pip/_vendor/requests/utils.py | 1096 + .../pip/_vendor/resolvelib/__init__.py | 26 + .../pip/_vendor/resolvelib/compat/__init__.py | 0 .../resolvelib/compat/collections_abc.py | 6 + .../pip/_vendor/resolvelib/providers.py | 133 + .../pip/_vendor/resolvelib/py.typed | 0 .../pip/_vendor/resolvelib/reporters.py | 43 + .../pip/_vendor/resolvelib/resolvers.py | 547 + .../pip/_vendor/resolvelib/structs.py | 170 + .../pip/_vendor/rich/__init__.py | 177 + .../pip/_vendor/rich/__main__.py | 273 + .../pip/_vendor/rich/_cell_widths.py | 454 + .../pip/_vendor/rich/_emoji_codes.py | 3610 ++++ .../pip/_vendor/rich/_emoji_replace.py | 32 + .../pip/_vendor/rich/_export_format.py | 76 + .../pip/_vendor/rich/_extension.py | 10 + .../site-packages/pip/_vendor/rich/_fileno.py | 24 + .../pip/_vendor/rich/_inspect.py | 270 + .../pip/_vendor/rich/_log_render.py | 94 + .../site-packages/pip/_vendor/rich/_loop.py | 43 + .../pip/_vendor/rich/_null_file.py | 69 + .../pip/_vendor/rich/_palettes.py | 309 + .../site-packages/pip/_vendor/rich/_pick.py | 17 + .../site-packages/pip/_vendor/rich/_ratio.py | 159 + .../pip/_vendor/rich/_spinners.py | 482 + .../site-packages/pip/_vendor/rich/_stack.py | 16 + .../site-packages/pip/_vendor/rich/_timer.py | 19 + .../pip/_vendor/rich/_win32_console.py | 662 + .../pip/_vendor/rich/_windows.py | 71 + .../pip/_vendor/rich/_windows_renderer.py | 56 + .../site-packages/pip/_vendor/rich/_wrap.py | 93 + .../site-packages/pip/_vendor/rich/abc.py | 33 + .../site-packages/pip/_vendor/rich/align.py | 311 + .../site-packages/pip/_vendor/rich/ansi.py | 240 + .../site-packages/pip/_vendor/rich/bar.py | 93 + .../site-packages/pip/_vendor/rich/box.py | 480 + .../site-packages/pip/_vendor/rich/cells.py | 167 + .../site-packages/pip/_vendor/rich/color.py | 621 + .../pip/_vendor/rich/color_triplet.py | 38 + .../site-packages/pip/_vendor/rich/columns.py | 187 + .../site-packages/pip/_vendor/rich/console.py | 2633 +++ .../pip/_vendor/rich/constrain.py | 37 + .../pip/_vendor/rich/containers.py | 167 + .../site-packages/pip/_vendor/rich/control.py | 225 + .../pip/_vendor/rich/default_styles.py | 190 + .../pip/_vendor/rich/diagnose.py | 37 + .../site-packages/pip/_vendor/rich/emoji.py | 96 + .../site-packages/pip/_vendor/rich/errors.py | 34 + .../pip/_vendor/rich/file_proxy.py | 57 + .../pip/_vendor/rich/filesize.py | 89 + .../pip/_vendor/rich/highlighter.py | 232 + .../site-packages/pip/_vendor/rich/json.py | 139 + .../site-packages/pip/_vendor/rich/jupyter.py | 101 + .../site-packages/pip/_vendor/rich/layout.py | 442 + .../site-packages/pip/_vendor/rich/live.py | 375 + .../pip/_vendor/rich/live_render.py | 112 + .../site-packages/pip/_vendor/rich/logging.py | 289 + .../site-packages/pip/_vendor/rich/markup.py | 251 + .../site-packages/pip/_vendor/rich/measure.py | 151 + .../site-packages/pip/_vendor/rich/padding.py | 141 + .../site-packages/pip/_vendor/rich/pager.py | 34 + .../site-packages/pip/_vendor/rich/palette.py | 100 + .../site-packages/pip/_vendor/rich/panel.py | 312 + .../site-packages/pip/_vendor/rich/pretty.py | 995 + .../pip/_vendor/rich/progress.py | 1699 ++ .../pip/_vendor/rich/progress_bar.py | 223 + .../site-packages/pip/_vendor/rich/prompt.py | 375 + .../pip/_vendor/rich/protocol.py | 42 + .../site-packages/pip/_vendor/rich/py.typed | 0 .../site-packages/pip/_vendor/rich/region.py | 10 + .../site-packages/pip/_vendor/rich/repr.py | 149 + .../site-packages/pip/_vendor/rich/rule.py | 130 + .../site-packages/pip/_vendor/rich/scope.py | 86 + .../site-packages/pip/_vendor/rich/screen.py | 54 + .../site-packages/pip/_vendor/rich/segment.py | 738 + .../site-packages/pip/_vendor/rich/spinner.py | 137 + .../site-packages/pip/_vendor/rich/status.py | 131 + .../site-packages/pip/_vendor/rich/style.py | 796 + .../site-packages/pip/_vendor/rich/styled.py | 42 + .../site-packages/pip/_vendor/rich/syntax.py | 958 + .../site-packages/pip/_vendor/rich/table.py | 1000 + .../pip/_vendor/rich/terminal_theme.py | 153 + .../site-packages/pip/_vendor/rich/text.py | 1357 ++ .../site-packages/pip/_vendor/rich/theme.py | 115 + .../site-packages/pip/_vendor/rich/themes.py | 5 + .../pip/_vendor/rich/traceback.py | 753 + .../site-packages/pip/_vendor/rich/tree.py | 249 + .../pip/_vendor/tomli/__init__.py | 11 + .../pip/_vendor/tomli/_parser.py | 691 + .../site-packages/pip/_vendor/tomli/_re.py | 107 + .../site-packages/pip/_vendor/tomli/_types.py | 10 + .../site-packages/pip/_vendor/tomli/py.typed | 1 + .../pip/_vendor/truststore/__init__.py | 36 + .../pip/_vendor/truststore/_api.py | 316 + .../pip/_vendor/truststore/_macos.py | 571 + .../pip/_vendor/truststore/_openssl.py | 66 + .../pip/_vendor/truststore/_ssl_constants.py | 31 + .../pip/_vendor/truststore/_windows.py | 567 + .../pip/_vendor/truststore/py.typed | 0 .../pip/_vendor/typing_extensions.py | 3641 ++++ .../pip/_vendor/urllib3/__init__.py | 102 + .../pip/_vendor/urllib3/_collections.py | 355 + .../pip/_vendor/urllib3/_version.py | 2 + .../pip/_vendor/urllib3/connection.py | 572 + .../pip/_vendor/urllib3/connectionpool.py | 1140 ++ .../pip/_vendor/urllib3/contrib/__init__.py | 0 .../urllib3/contrib/_appengine_environ.py | 36 + .../contrib/_securetransport/__init__.py | 0 .../contrib/_securetransport/bindings.py | 519 + .../contrib/_securetransport/low_level.py | 397 + .../pip/_vendor/urllib3/contrib/appengine.py | 314 + .../pip/_vendor/urllib3/contrib/ntlmpool.py | 130 + .../pip/_vendor/urllib3/contrib/pyopenssl.py | 518 + .../urllib3/contrib/securetransport.py | 920 + .../pip/_vendor/urllib3/contrib/socks.py | 216 + .../pip/_vendor/urllib3/exceptions.py | 323 + .../pip/_vendor/urllib3/fields.py | 274 + .../pip/_vendor/urllib3/filepost.py | 98 + .../pip/_vendor/urllib3/packages/__init__.py | 0 .../urllib3/packages/backports/__init__.py | 0 .../urllib3/packages/backports/makefile.py | 51 + .../packages/backports/weakref_finalize.py | 155 + .../pip/_vendor/urllib3/packages/six.py | 1076 + .../pip/_vendor/urllib3/poolmanager.py | 540 + .../pip/_vendor/urllib3/request.py | 191 + .../pip/_vendor/urllib3/response.py | 879 + .../pip/_vendor/urllib3/util/__init__.py | 49 + .../pip/_vendor/urllib3/util/connection.py | 149 + .../pip/_vendor/urllib3/util/proxy.py | 57 + .../pip/_vendor/urllib3/util/queue.py | 22 + .../pip/_vendor/urllib3/util/request.py | 137 + .../pip/_vendor/urllib3/util/response.py | 107 + .../pip/_vendor/urllib3/util/retry.py | 622 + .../pip/_vendor/urllib3/util/ssl_.py | 504 + .../urllib3/util/ssl_match_hostname.py | 159 + .../pip/_vendor/urllib3/util/ssltransport.py | 221 + .../pip/_vendor/urllib3/util/timeout.py | 271 + .../pip/_vendor/urllib3/util/url.py | 435 + .../pip/_vendor/urllib3/util/wait.py | 152 + .../site-packages/pip/_vendor/vendor.txt | 18 + .../lib/python3.12/site-packages/pip/py.typed | 4 + .../pluggy-1.6.0.dist-info/INSTALLER | 1 + .../pluggy-1.6.0.dist-info/METADATA | 152 + .../pluggy-1.6.0.dist-info/RECORD | 23 + .../pluggy-1.6.0.dist-info/WHEEL | 5 + .../pluggy-1.6.0.dist-info/licenses/LICENSE | 21 + .../pluggy-1.6.0.dist-info/top_level.txt | 1 + .../site-packages/pluggy/__init__.py | 30 + .../site-packages/pluggy/_callers.py | 169 + .../python3.12/site-packages/pluggy/_hooks.py | 714 + .../site-packages/pluggy/_manager.py | 523 + .../site-packages/pluggy/_result.py | 107 + .../site-packages/pluggy/_tracing.py | 72 + .../site-packages/pluggy/_version.py | 21 + .../site-packages/pluggy/_warnings.py | 27 + .../python3.12/site-packages/pluggy/py.typed | 0 .../psycopg2/.dylibs/libcom_err.3.0.dylib | Bin 0 -> 73488 bytes .../psycopg2/.dylibs/libcrypto.3.dylib | Bin 0 -> 5002400 bytes .../psycopg2/.dylibs/libgssapi_krb5.2.2.dylib | Bin 0 -> 398944 bytes .../psycopg2/.dylibs/libk5crypto.3.1.dylib | Bin 0 -> 233008 bytes .../psycopg2/.dylibs/libkrb5.3.3.dylib | Bin 0 -> 987984 bytes .../psycopg2/.dylibs/libkrb5support.1.1.dylib | Bin 0 -> 110528 bytes .../psycopg2/.dylibs/liblber.2.dylib | Bin 0 -> 137472 bytes .../psycopg2/.dylibs/libldap.2.dylib | Bin 0 -> 479248 bytes .../psycopg2/.dylibs/libpq.5.dylib | Bin 0 -> 354496 bytes .../psycopg2/.dylibs/libssl.3.dylib | Bin 0 -> 1037088 bytes .../site-packages/psycopg2/__init__.py | 126 + .../site-packages/psycopg2/_ipaddress.py | 90 + .../site-packages/psycopg2/_json.py | 199 + .../psycopg2/_psycopg.cpython-312-darwin.so | Bin 0 -> 330544 bytes .../site-packages/psycopg2/_range.py | 554 + .../site-packages/psycopg2/errorcodes.py | 455 + .../site-packages/psycopg2/errors.py | 38 + .../site-packages/psycopg2/extensions.py | 213 + .../site-packages/psycopg2/extras.py | 1340 ++ .../python3.12/site-packages/psycopg2/pool.py | 187 + .../python3.12/site-packages/psycopg2/sql.py | 455 + .../python3.12/site-packages/psycopg2/tz.py | 158 + .../INSTALLER | 1 + .../psycopg2_binary-2.9.12.dist-info/METADATA | 131 + .../psycopg2_binary-2.9.12.dist-info/RECORD | 40 + .../REQUESTED | 0 .../psycopg2_binary-2.9.12.dist-info/WHEEL | 6 + .../licenses/LICENSE | 49 + .../top_level.txt | 1 + .venv-test/lib/python3.12/site-packages/py.py | 15 + .../pyasn1-0.6.4.dist-info/INSTALLER | 1 + .../pyasn1-0.6.4.dist-info/METADATA | 228 + .../pyasn1-0.6.4.dist-info/RECORD | 71 + .../pyasn1-0.6.4.dist-info/WHEEL | 5 + .../licenses/LICENSE.rst | 24 + .../pyasn1-0.6.4.dist-info/top_level.txt | 1 + .../pyasn1-0.6.4.dist-info/zip-safe | 1 + .../site-packages/pyasn1/__init__.py | 2 + .../site-packages/pyasn1/codec/__init__.py | 1 + .../pyasn1/codec/ber/__init__.py | 1 + .../site-packages/pyasn1/codec/ber/decoder.py | 2236 ++ .../site-packages/pyasn1/codec/ber/encoder.py | 954 + .../site-packages/pyasn1/codec/ber/eoo.py | 28 + .../pyasn1/codec/cer/__init__.py | 1 + .../site-packages/pyasn1/codec/cer/decoder.py | 149 + .../site-packages/pyasn1/codec/cer/encoder.py | 331 + .../pyasn1/codec/der/__init__.py | 1 + .../site-packages/pyasn1/codec/der/decoder.py | 120 + .../site-packages/pyasn1/codec/der/encoder.py | 126 + .../pyasn1/codec/native/__init__.py | 1 + .../pyasn1/codec/native/decoder.py | 244 + .../pyasn1/codec/native/encoder.py | 285 + .../site-packages/pyasn1/codec/streaming.py | 234 + .../site-packages/pyasn1/compat/__init__.py | 4 + .../site-packages/pyasn1/compat/integer.py | 13 + .../python3.12/site-packages/pyasn1/debug.py | 146 + .../python3.12/site-packages/pyasn1/error.py | 116 + .../site-packages/pyasn1/type/__init__.py | 1 + .../site-packages/pyasn1/type/base.py | 699 + .../site-packages/pyasn1/type/char.py | 288 + .../site-packages/pyasn1/type/constraint.py | 751 + .../site-packages/pyasn1/type/error.py | 11 + .../site-packages/pyasn1/type/namedtype.py | 550 + .../site-packages/pyasn1/type/namedval.py | 192 + .../site-packages/pyasn1/type/opentype.py | 104 + .../site-packages/pyasn1/type/tag.py | 347 + .../site-packages/pyasn1/type/tagmap.py | 96 + .../site-packages/pyasn1/type/univ.py | 3338 +++ .../site-packages/pyasn1/type/useful.py | 190 + .../pycparser-3.0.dist-info/INSTALLER | 1 + .../pycparser-3.0.dist-info/METADATA | 244 + .../pycparser-3.0.dist-info/RECORD | 21 + .../pycparser-3.0.dist-info/WHEEL | 5 + .../pycparser-3.0.dist-info/licenses/LICENSE | 27 + .../pycparser-3.0.dist-info/top_level.txt | 1 + .../site-packages/pycparser/__init__.py | 99 + .../site-packages/pycparser/_ast_gen.py | 355 + .../site-packages/pycparser/_c_ast.cfg | 195 + .../site-packages/pycparser/ast_transforms.py | 174 + .../site-packages/pycparser/c_ast.py | 1341 ++ .../site-packages/pycparser/c_generator.py | 573 + .../site-packages/pycparser/c_lexer.py | 706 + .../site-packages/pycparser/c_parser.py | 2376 +++ .../pydantic-2.13.5.dist-info/INSTALLER | 1 + .../pydantic-2.13.5.dist-info/METADATA | 1307 ++ .../pydantic-2.13.5.dist-info/RECORD | 218 + .../pydantic-2.13.5.dist-info/REQUESTED | 0 .../pydantic-2.13.5.dist-info/WHEEL | 4 + .../licenses/LICENSE | 21 + .../site-packages/pydantic/__init__.py | 456 + .../pydantic/_internal/__init__.py | 0 .../pydantic/_internal/_config.py | 386 + .../pydantic/_internal/_core_metadata.py | 97 + .../pydantic/_internal/_core_utils.py | 174 + .../pydantic/_internal/_dataclasses.py | 315 + .../pydantic/_internal/_decorators.py | 873 + .../pydantic/_internal/_decorators_v1.py | 174 + .../_internal/_discriminated_union.py | 494 + .../pydantic/_internal/_docs_extraction.py | 113 + .../pydantic/_internal/_fields.py | 729 + .../pydantic/_internal/_forward_ref.py | 23 + .../pydantic/_internal/_generate_schema.py | 2934 +++ .../pydantic/_internal/_generics.py | 530 + .../site-packages/pydantic/_internal/_git.py | 27 + .../pydantic/_internal/_import_utils.py | 20 + .../pydantic/_internal/_internal_dataclass.py | 7 + .../_internal/_known_annotated_metadata.py | 403 + .../pydantic/_internal/_mock_val_ser.py | 228 + .../pydantic/_internal/_model_construction.py | 868 + .../pydantic/_internal/_namespace_utils.py | 293 + .../site-packages/pydantic/_internal/_repr.py | 124 + .../pydantic/_internal/_schema_gather.py | 212 + .../_internal/_schema_generation_shared.py | 125 + .../pydantic/_internal/_serializers.py | 53 + .../pydantic/_internal/_signature.py | 189 + .../pydantic/_internal/_typing_extra.py | 785 + .../pydantic/_internal/_utils.py | 446 + .../pydantic/_internal/_validate_call.py | 141 + .../pydantic/_internal/_validators.py | 534 + .../site-packages/pydantic/_migration.py | 316 + .../pydantic/alias_generators.py | 62 + .../site-packages/pydantic/aliases.py | 135 + .../pydantic/annotated_handlers.py | 122 + .../pydantic/class_validators.py | 5 + .../site-packages/pydantic/color.py | 604 + .../site-packages/pydantic/config.py | 1296 ++ .../site-packages/pydantic/dataclasses.py | 413 + .../site-packages/pydantic/datetime_parse.py | 5 + .../site-packages/pydantic/decorator.py | 5 + .../pydantic/deprecated/__init__.py | 0 .../pydantic/deprecated/class_validators.py | 256 + .../pydantic/deprecated/config.py | 72 + .../pydantic/deprecated/copy_internals.py | 224 + .../pydantic/deprecated/decorator.py | 284 + .../site-packages/pydantic/deprecated/json.py | 141 + .../pydantic/deprecated/parse.py | 80 + .../pydantic/deprecated/tools.py | 103 + .../site-packages/pydantic/env_settings.py | 5 + .../site-packages/pydantic/error_wrappers.py | 5 + .../site-packages/pydantic/errors.py | 189 + .../pydantic/experimental/__init__.py | 1 + .../pydantic/experimental/arguments_schema.py | 44 + .../pydantic/experimental/missing_sentinel.py | 5 + .../pydantic/experimental/pipeline.py | 663 + .../site-packages/pydantic/fields.py | 1892 ++ .../pydantic/functional_serializers.py | 470 + .../pydantic/functional_validators.py | 889 + .../site-packages/pydantic/generics.py | 5 + .../python3.12/site-packages/pydantic/json.py | 5 + .../site-packages/pydantic/json_schema.py | 2911 +++ .../python3.12/site-packages/pydantic/main.py | 1838 ++ .../python3.12/site-packages/pydantic/mypy.py | 1414 ++ .../site-packages/pydantic/networks.py | 1332 ++ .../site-packages/pydantic/parse.py | 5 + .../site-packages/pydantic/plugin/__init__.py | 193 + .../site-packages/pydantic/plugin/_loader.py | 58 + .../pydantic/plugin/_schema_validator.py | 152 + .../site-packages/pydantic/py.typed | 0 .../site-packages/pydantic/root_model.py | 157 + .../site-packages/pydantic/schema.py | 5 + .../site-packages/pydantic/tools.py | 5 + .../site-packages/pydantic/type_adapter.py | 801 + .../site-packages/pydantic/types.py | 3310 +++ .../site-packages/pydantic/typing.py | 5 + .../site-packages/pydantic/utils.py | 5 + .../site-packages/pydantic/v1/__init__.py | 131 + .../pydantic/v1/_hypothesis_plugin.py | 391 + .../pydantic/v1/annotated_types.py | 72 + .../pydantic/v1/class_validators.py | 361 + .../site-packages/pydantic/v1/color.py | 494 + .../site-packages/pydantic/v1/config.py | 191 + .../site-packages/pydantic/v1/dataclasses.py | 500 + .../pydantic/v1/datetime_parse.py | 248 + .../site-packages/pydantic/v1/decorator.py | 264 + .../site-packages/pydantic/v1/env_settings.py | 350 + .../pydantic/v1/error_wrappers.py | 161 + .../site-packages/pydantic/v1/errors.py | 646 + .../site-packages/pydantic/v1/fields.py | 1253 ++ .../site-packages/pydantic/v1/generics.py | 400 + .../site-packages/pydantic/v1/json.py | 112 + .../site-packages/pydantic/v1/main.py | 1130 ++ .../site-packages/pydantic/v1/mypy.py | 949 + .../site-packages/pydantic/v1/networks.py | 747 + .../site-packages/pydantic/v1/parse.py | 66 + .../site-packages/pydantic/v1/py.typed | 0 .../site-packages/pydantic/v1/schema.py | 1163 ++ .../site-packages/pydantic/v1/tools.py | 92 + .../site-packages/pydantic/v1/types.py | 1205 ++ .../site-packages/pydantic/v1/typing.py | 627 + .../site-packages/pydantic/v1/utils.py | 807 + .../site-packages/pydantic/v1/validators.py | 768 + .../site-packages/pydantic/v1/version.py | 38 + .../pydantic/validate_call_decorator.py | 116 + .../site-packages/pydantic/validators.py | 5 + .../site-packages/pydantic/version.py | 113 + .../site-packages/pydantic/warnings.py | 122 + .../pydantic_core-2.46.5.dist-info/INSTALLER | 1 + .../pydantic_core-2.46.5.dist-info/METADATA | 173 + .../pydantic_core-2.46.5.dist-info/RECORD | 13 + .../pydantic_core-2.46.5.dist-info/WHEEL | 4 + .../licenses/LICENSE | 21 + .../sboms/pydantic-core.cyclonedx.json | 3961 ++++ .../site-packages/pydantic_core/__init__.py | 171 + .../_pydantic_core.cpython-312-darwin.so | Bin 0 -> 4119520 bytes .../pydantic_core/_pydantic_core.pyi | 1056 + .../pydantic_core/core_schema.py | 4461 ++++ .../site-packages/pydantic_core/py.typed | 0 .../pygments-2.21.0.dist-info/INSTALLER | 1 + .../pygments-2.21.0.dist-info/METADATA | 58 + .../pygments-2.21.0.dist-info/RECORD | 694 + .../pygments-2.21.0.dist-info/WHEEL | 4 + .../entry_points.txt | 2 + .../licenses/AUTHORS | 296 + .../licenses/LICENSE | 25 + .../site-packages/pygments/__init__.py | 82 + .../site-packages/pygments/__main__.py | 17 + .../site-packages/pygments/cmdline.py | 668 + .../site-packages/pygments/console.py | 70 + .../site-packages/pygments/filter.py | 70 + .../pygments/filters/__init__.py | 942 + .../site-packages/pygments/formatter.py | 129 + .../pygments/formatters/__init__.py | 157 + .../pygments/formatters/_mapping.py | 23 + .../pygments/formatters/bbcode.py | 108 + .../pygments/formatters/groff.py | 170 + .../site-packages/pygments/formatters/html.py | 993 + .../site-packages/pygments/formatters/img.py | 686 + .../site-packages/pygments/formatters/irc.py | 153 + .../pygments/formatters/latex.py | 526 + .../pygments/formatters/other.py | 160 + .../pygments/formatters/pangomarkup.py | 83 + .../site-packages/pygments/formatters/rtf.py | 349 + .../site-packages/pygments/formatters/svg.py | 176 + .../pygments/formatters/terminal.py | 127 + .../pygments/formatters/terminal256.py | 338 + .../site-packages/pygments/lexer.py | 963 + .../site-packages/pygments/lexers/__init__.py | 362 + .../pygments/lexers/_ada_builtins.py | 103 + .../pygments/lexers/_asy_builtins.py | 1644 ++ .../pygments/lexers/_cl_builtins.py | 231 + .../pygments/lexers/_cocoa_builtins.py | 75 + .../pygments/lexers/_csound_builtins.py | 1780 ++ .../pygments/lexers/_css_builtins.py | 558 + .../pygments/lexers/_googlesql_builtins.py | 918 + .../pygments/lexers/_julia_builtins.py | 411 + .../pygments/lexers/_lasso_builtins.py | 5326 +++++ .../pygments/lexers/_lilypond_builtins.py | 5184 +++++ .../pygments/lexers/_lua_builtins.py | 285 + .../pygments/lexers/_luau_builtins.py | 62 + .../site-packages/pygments/lexers/_mapping.py | 607 + .../pygments/lexers/_mql_builtins.py | 1171 ++ .../pygments/lexers/_mysql_builtins.py | 1384 ++ .../pygments/lexers/_openedge_builtins.py | 2600 +++ .../pygments/lexers/_php_builtins.py | 3328 +++ .../pygments/lexers/_postgres_builtins.py | 739 + .../pygments/lexers/_qlik_builtins.py | 666 + .../pygments/lexers/_scheme_builtins.py | 1609 ++ .../pygments/lexers/_scilab_builtins.py | 3093 +++ .../pygments/lexers/_sourcemod_builtins.py | 1151 ++ .../pygments/lexers/_sql_builtins.py | 106 + .../pygments/lexers/_stan_builtins.py | 761 + .../pygments/lexers/_stata_builtins.py | 457 + .../pygments/lexers/_tsql_builtins.py | 1003 + .../pygments/lexers/_usd_builtins.py | 112 + .../pygments/lexers/_vbscript_builtins.py | 279 + .../pygments/lexers/_vim_builtins.py | 3852 ++++ .../pygments/lexers/actionscript.py | 243 + .../site-packages/pygments/lexers/ada.py | 143 + .../site-packages/pygments/lexers/agile.py | 25 + .../site-packages/pygments/lexers/algebra.py | 303 + .../site-packages/pygments/lexers/ambient.py | 75 + .../site-packages/pygments/lexers/amdgpu.py | 54 + .../site-packages/pygments/lexers/ampl.py | 87 + .../site-packages/pygments/lexers/apdlexer.py | 593 + .../site-packages/pygments/lexers/apl.py | 103 + .../pygments/lexers/archetype.py | 315 + .../site-packages/pygments/lexers/arrow.py | 116 + .../site-packages/pygments/lexers/arturo.py | 249 + .../site-packages/pygments/lexers/asc.py | 55 + .../site-packages/pygments/lexers/asm.py | 1058 + .../site-packages/pygments/lexers/asn1.py | 178 + .../pygments/lexers/automation.py | 379 + .../site-packages/pygments/lexers/bare.py | 101 + .../site-packages/pygments/lexers/basic.py | 655 + .../site-packages/pygments/lexers/bdd.py | 57 + .../site-packages/pygments/lexers/berry.py | 99 + .../site-packages/pygments/lexers/bibtex.py | 159 + .../site-packages/pygments/lexers/bitbake.py | 166 + .../pygments/lexers/blueprint.py | 173 + .../site-packages/pygments/lexers/boa.py | 97 + .../site-packages/pygments/lexers/bqn.py | 112 + .../site-packages/pygments/lexers/business.py | 625 + .../site-packages/pygments/lexers/c_cpp.py | 481 + .../site-packages/pygments/lexers/c_like.py | 743 + .../pygments/lexers/capnproto.py | 74 + .../site-packages/pygments/lexers/carbon.py | 95 + .../site-packages/pygments/lexers/cddl.py | 172 + .../site-packages/pygments/lexers/cel.py | 142 + .../site-packages/pygments/lexers/chapel.py | 139 + .../site-packages/pygments/lexers/clean.py | 180 + .../site-packages/pygments/lexers/codeql.py | 80 + .../site-packages/pygments/lexers/comal.py | 81 + .../site-packages/pygments/lexers/compiled.py | 35 + .../site-packages/pygments/lexers/configs.py | 1499 ++ .../site-packages/pygments/lexers/console.py | 125 + .../site-packages/pygments/lexers/cplint.py | 43 + .../site-packages/pygments/lexers/crystal.py | 364 + .../site-packages/pygments/lexers/csound.py | 466 + .../site-packages/pygments/lexers/css.py | 633 + .../site-packages/pygments/lexers/d.py | 259 + .../site-packages/pygments/lexers/dalvik.py | 126 + .../site-packages/pygments/lexers/data.py | 763 + .../site-packages/pygments/lexers/dax.py | 135 + .../pygments/lexers/devicetree.py | 120 + .../site-packages/pygments/lexers/diff.py | 169 + .../site-packages/pygments/lexers/dns.py | 109 + .../site-packages/pygments/lexers/dotnet.py | 886 + .../site-packages/pygments/lexers/dsls.py | 970 + .../site-packages/pygments/lexers/dylan.py | 279 + .../site-packages/pygments/lexers/ecl.py | 144 + .../site-packages/pygments/lexers/eiffel.py | 67 + .../site-packages/pygments/lexers/elm.py | 123 + .../site-packages/pygments/lexers/elpi.py | 201 + .../site-packages/pygments/lexers/email.py | 132 + .../site-packages/pygments/lexers/erlang.py | 526 + .../site-packages/pygments/lexers/esoteric.py | 300 + .../site-packages/pygments/lexers/ezhil.py | 76 + .../site-packages/pygments/lexers/factor.py | 361 + .../site-packages/pygments/lexers/fantom.py | 250 + .../site-packages/pygments/lexers/felix.py | 275 + .../site-packages/pygments/lexers/fift.py | 68 + .../pygments/lexers/floscript.py | 87 + .../site-packages/pygments/lexers/forth.py | 178 + .../site-packages/pygments/lexers/fortran.py | 212 + .../site-packages/pygments/lexers/foxpro.py | 427 + .../site-packages/pygments/lexers/freefem.py | 893 + .../site-packages/pygments/lexers/func.py | 110 + .../pygments/lexers/functional.py | 21 + .../site-packages/pygments/lexers/futhark.py | 104 + .../pygments/lexers/gcodelexer.py | 35 + .../site-packages/pygments/lexers/gdscript.py | 189 + .../site-packages/pygments/lexers/gleam.py | 74 + .../site-packages/pygments/lexers/go.py | 91 + .../pygments/lexers/grammar_notation.py | 262 + .../site-packages/pygments/lexers/graph.py | 108 + .../site-packages/pygments/lexers/graphics.py | 794 + .../site-packages/pygments/lexers/graphql.py | 176 + .../site-packages/pygments/lexers/graphviz.py | 58 + .../site-packages/pygments/lexers/gsql.py | 103 + .../site-packages/pygments/lexers/hare.py | 73 + .../site-packages/pygments/lexers/haskell.py | 867 + .../site-packages/pygments/lexers/haxe.py | 941 + .../site-packages/pygments/lexers/hdl.py | 466 + .../site-packages/pygments/lexers/hexdump.py | 102 + .../site-packages/pygments/lexers/html.py | 670 + .../site-packages/pygments/lexers/idl.py | 284 + .../site-packages/pygments/lexers/igor.py | 435 + .../site-packages/pygments/lexers/inferno.py | 95 + .../pygments/lexers/installers.py | 352 + .../pygments/lexers/int_fiction.py | 1370 ++ .../site-packages/pygments/lexers/iolang.py | 61 + .../site-packages/pygments/lexers/j.py | 151 + .../pygments/lexers/javascript.py | 1592 ++ .../site-packages/pygments/lexers/jmespath.py | 69 + .../site-packages/pygments/lexers/jslt.py | 94 + .../site-packages/pygments/lexers/json5.py | 83 + .../site-packages/pygments/lexers/jsonnet.py | 167 + .../site-packages/pygments/lexers/jsx.py | 102 + .../site-packages/pygments/lexers/julia.py | 293 + .../site-packages/pygments/lexers/jvm.py | 1812 ++ .../site-packages/pygments/lexers/kuin.py | 332 + .../site-packages/pygments/lexers/kusto.py | 93 + .../site-packages/pygments/lexers/ldap.py | 155 + .../site-packages/pygments/lexers/lean.py | 241 + .../site-packages/pygments/lexers/lilypond.py | 225 + .../site-packages/pygments/lexers/lisp.py | 3152 +++ .../pygments/lexers/macaulay2.py | 1914 ++ .../site-packages/pygments/lexers/make.py | 212 + .../site-packages/pygments/lexers/maple.py | 291 + .../site-packages/pygments/lexers/markup.py | 1689 ++ .../site-packages/pygments/lexers/math.py | 21 + .../site-packages/pygments/lexers/matlab.py | 3313 +++ .../site-packages/pygments/lexers/maxima.py | 84 + .../site-packages/pygments/lexers/meson.py | 139 + .../site-packages/pygments/lexers/mime.py | 210 + .../pygments/lexers/minecraft.py | 392 + .../site-packages/pygments/lexers/mips.py | 130 + .../site-packages/pygments/lexers/ml.py | 958 + .../site-packages/pygments/lexers/modeling.py | 387 + .../site-packages/pygments/lexers/modula2.py | 1579 ++ .../site-packages/pygments/lexers/mojo.py | 707 + .../site-packages/pygments/lexers/monte.py | 203 + .../site-packages/pygments/lexers/mosel.py | 447 + .../site-packages/pygments/lexers/ncl.py | 894 + .../site-packages/pygments/lexers/nimrod.py | 199 + .../site-packages/pygments/lexers/nit.py | 63 + .../site-packages/pygments/lexers/nix.py | 144 + .../site-packages/pygments/lexers/numbair.py | 63 + .../site-packages/pygments/lexers/oberon.py | 120 + .../pygments/lexers/objective.py | 519 + .../site-packages/pygments/lexers/ooc.py | 84 + .../site-packages/pygments/lexers/openscad.py | 96 + .../site-packages/pygments/lexers/other.py | 41 + .../site-packages/pygments/lexers/parasail.py | 82 + .../site-packages/pygments/lexers/parsers.py | 798 + .../site-packages/pygments/lexers/pascal.py | 644 + .../site-packages/pygments/lexers/pawn.py | 202 + .../site-packages/pygments/lexers/pddl.py | 82 + .../site-packages/pygments/lexers/perl.py | 733 + .../site-packages/pygments/lexers/phix.py | 363 + .../site-packages/pygments/lexers/php.py | 335 + .../pygments/lexers/pointless.py | 70 + .../site-packages/pygments/lexers/pony.py | 93 + .../site-packages/pygments/lexers/praat.py | 303 + .../site-packages/pygments/lexers/procfile.py | 41 + .../site-packages/pygments/lexers/prolog.py | 318 + .../site-packages/pygments/lexers/promql.py | 176 + .../site-packages/pygments/lexers/prql.py | 251 + .../site-packages/pygments/lexers/ptx.py | 119 + .../pygments/lexers/purescript.py | 154 + .../site-packages/pygments/lexers/python.py | 1211 ++ .../site-packages/pygments/lexers/q.py | 187 + .../site-packages/pygments/lexers/qlik.py | 117 + .../site-packages/pygments/lexers/qvt.py | 153 + .../site-packages/pygments/lexers/r.py | 196 + .../site-packages/pygments/lexers/rdf.py | 468 + .../site-packages/pygments/lexers/rebol.py | 419 + .../site-packages/pygments/lexers/rego.py | 57 + .../site-packages/pygments/lexers/rell.py | 68 + .../site-packages/pygments/lexers/resource.py | 83 + .../site-packages/pygments/lexers/ride.py | 138 + .../site-packages/pygments/lexers/rita.py | 42 + .../site-packages/pygments/lexers/rnc.py | 66 + .../site-packages/pygments/lexers/roboconf.py | 81 + .../pygments/lexers/robotframework.py | 551 + .../site-packages/pygments/lexers/ruby.py | 522 + .../site-packages/pygments/lexers/rust.py | 222 + .../site-packages/pygments/lexers/sas.py | 227 + .../site-packages/pygments/lexers/savi.py | 171 + .../site-packages/pygments/lexers/scdoc.py | 85 + .../pygments/lexers/scripting.py | 1646 ++ .../site-packages/pygments/lexers/sgf.py | 59 + .../site-packages/pygments/lexers/shell.py | 902 + .../site-packages/pygments/lexers/sieve.py | 78 + .../site-packages/pygments/lexers/slash.py | 183 + .../pygments/lexers/smalltalk.py | 194 + .../site-packages/pygments/lexers/smithy.py | 83 + .../site-packages/pygments/lexers/smv.py | 78 + .../site-packages/pygments/lexers/snobol.py | 82 + .../site-packages/pygments/lexers/solidity.py | 87 + .../site-packages/pygments/lexers/soong.py | 78 + .../site-packages/pygments/lexers/sophia.py | 102 + .../site-packages/pygments/lexers/special.py | 122 + .../site-packages/pygments/lexers/spice.py | 70 + .../site-packages/pygments/lexers/sql.py | 1114 + .../site-packages/pygments/lexers/srcinfo.py | 62 + .../site-packages/pygments/lexers/stata.py | 170 + .../pygments/lexers/supercollider.py | 94 + .../site-packages/pygments/lexers/tablegen.py | 177 + .../site-packages/pygments/lexers/tact.py | 303 + .../site-packages/pygments/lexers/tal.py | 77 + .../site-packages/pygments/lexers/tcl.py | 150 + .../site-packages/pygments/lexers/teal.py | 88 + .../pygments/lexers/templates.py | 2355 +++ .../site-packages/pygments/lexers/teraterm.py | 335 + .../site-packages/pygments/lexers/testing.py | 209 + .../site-packages/pygments/lexers/text.py | 27 + .../site-packages/pygments/lexers/textedit.py | 209 + .../site-packages/pygments/lexers/textfmts.py | 436 + .../site-packages/pygments/lexers/theorem.py | 410 + .../site-packages/pygments/lexers/thingsdb.py | 143 + .../site-packages/pygments/lexers/tlb.py | 59 + .../site-packages/pygments/lexers/tls.py | 54 + .../site-packages/pygments/lexers/tnt.py | 270 + .../pygments/lexers/trafficscript.py | 51 + .../pygments/lexers/typoscript.py | 216 + .../site-packages/pygments/lexers/typst.py | 160 + .../site-packages/pygments/lexers/ul4.py | 309 + .../site-packages/pygments/lexers/unicon.py | 411 + .../site-packages/pygments/lexers/urbi.py | 145 + .../site-packages/pygments/lexers/usd.py | 85 + .../site-packages/pygments/lexers/varnish.py | 189 + .../pygments/lexers/verification.py | 116 + .../site-packages/pygments/lexers/verifpal.py | 65 + .../site-packages/pygments/lexers/vip.py | 150 + .../site-packages/pygments/lexers/vyper.py | 140 + .../site-packages/pygments/lexers/web.py | 24 + .../pygments/lexers/webassembly.py | 122 + .../site-packages/pygments/lexers/webidl.py | 298 + .../site-packages/pygments/lexers/webmisc.py | 1006 + .../site-packages/pygments/lexers/wgsl.py | 406 + .../site-packages/pygments/lexers/whiley.py | 115 + .../site-packages/pygments/lexers/wowtoc.py | 120 + .../site-packages/pygments/lexers/wren.py | 98 + .../site-packages/pygments/lexers/x10.py | 66 + .../site-packages/pygments/lexers/xorg.py | 38 + .../site-packages/pygments/lexers/yang.py | 103 + .../site-packages/pygments/lexers/yara.py | 69 + .../site-packages/pygments/lexers/zig.py | 125 + .../site-packages/pygments/modeline.py | 43 + .../site-packages/pygments/plugin.py | 75 + .../site-packages/pygments/regexopt.py | 102 + .../site-packages/pygments/scanner.py | 104 + .../site-packages/pygments/sphinxext.py | 247 + .../site-packages/pygments/style.py | 205 + .../site-packages/pygments/styles/__init__.py | 61 + .../site-packages/pygments/styles/_mapping.py | 55 + .../site-packages/pygments/styles/abap.py | 32 + .../site-packages/pygments/styles/algol.py | 65 + .../site-packages/pygments/styles/algol_nu.py | 65 + .../site-packages/pygments/styles/arduino.py | 100 + .../site-packages/pygments/styles/autumn.py | 67 + .../site-packages/pygments/styles/borland.py | 53 + .../site-packages/pygments/styles/bw.py | 52 + .../site-packages/pygments/styles/coffee.py | 80 + .../site-packages/pygments/styles/colorful.py | 83 + .../site-packages/pygments/styles/default.py | 76 + .../site-packages/pygments/styles/dracula.py | 90 + .../site-packages/pygments/styles/emacs.py | 75 + .../site-packages/pygments/styles/friendly.py | 76 + .../pygments/styles/friendly_grayscale.py | 80 + .../site-packages/pygments/styles/fruity.py | 47 + .../site-packages/pygments/styles/gh_dark.py | 113 + .../site-packages/pygments/styles/gruvbox.py | 118 + .../site-packages/pygments/styles/igor.py | 32 + .../site-packages/pygments/styles/inkpot.py | 72 + .../pygments/styles/lightbulb.py | 110 + .../site-packages/pygments/styles/lilypond.py | 62 + .../site-packages/pygments/styles/lovelace.py | 100 + .../site-packages/pygments/styles/manni.py | 79 + .../site-packages/pygments/styles/material.py | 124 + .../site-packages/pygments/styles/monokai.py | 112 + .../site-packages/pygments/styles/murphy.py | 82 + .../site-packages/pygments/styles/native.py | 70 + .../pygments/styles/night_owl.py | 143 + .../site-packages/pygments/styles/nord.py | 156 + .../site-packages/pygments/styles/onedark.py | 73 + .../pygments/styles/paraiso_dark.py | 124 + .../pygments/styles/paraiso_light.py | 124 + .../site-packages/pygments/styles/pastie.py | 78 + .../site-packages/pygments/styles/perldoc.py | 73 + .../pygments/styles/rainbow_dash.py | 95 + .../site-packages/pygments/styles/rrt.py | 54 + .../site-packages/pygments/styles/sas.py | 46 + .../pygments/styles/solarized.py | 144 + .../pygments/styles/staroffice.py | 31 + .../pygments/styles/stata_dark.py | 42 + .../pygments/styles/stata_light.py | 42 + .../site-packages/pygments/styles/tango.py | 143 + .../site-packages/pygments/styles/trac.py | 66 + .../site-packages/pygments/styles/vim.py | 67 + .../site-packages/pygments/styles/vs.py | 41 + .../site-packages/pygments/styles/xcode.py | 53 + .../site-packages/pygments/styles/zenburn.py | 83 + .../site-packages/pygments/token.py | 214 + .../site-packages/pygments/unistring.py | 153 + .../python3.12/site-packages/pygments/util.py | 344 + .../pyjwt-2.13.0.dist-info/INSTALLER | 1 + .../pyjwt-2.13.0.dist-info/METADATA | 93 + .../pyjwt-2.13.0.dist-info/RECORD | 33 + .../pyjwt-2.13.0.dist-info/REQUESTED | 0 .../pyjwt-2.13.0.dist-info/WHEEL | 5 + .../licenses/AUTHORS.rst | 7 + .../pyjwt-2.13.0.dist-info/licenses/LICENSE | 21 + .../pyjwt-2.13.0.dist-info/top_level.txt | 1 + .../pytest-9.1.1.dist-info/INSTALLER | 1 + .../pytest-9.1.1.dist-info/METADATA | 211 + .../pytest-9.1.1.dist-info/RECORD | 174 + .../pytest-9.1.1.dist-info/REQUESTED | 0 .../pytest-9.1.1.dist-info/WHEEL | 5 + .../pytest-9.1.1.dist-info/entry_points.txt | 3 + .../pytest-9.1.1.dist-info/licenses/LICENSE | 21 + .../pytest-9.1.1.dist-info/top_level.txt | 3 + .../site-packages/pytest/__init__.py | 188 + .../site-packages/pytest/__main__.py | 9 + .../python3.12/site-packages/pytest/py.typed | 0 .../pytest_asyncio-1.4.0.dist-info/INSTALLER | 1 + .../pytest_asyncio-1.4.0.dist-info/METADATA | 92 + .../pytest_asyncio-1.4.0.dist-info/RECORD | 13 + .../pytest_asyncio-1.4.0.dist-info/REQUESTED | 0 .../pytest_asyncio-1.4.0.dist-info/WHEEL | 5 + .../entry_points.txt | 2 + .../licenses/LICENSE | 201 + .../top_level.txt | 1 + .../site-packages/pytest_asyncio/__init__.py | 11 + .../site-packages/pytest_asyncio/plugin.py | 1139 ++ .../site-packages/pytest_asyncio/py.typed | 0 .../python_jose-3.5.0.dist-info/INSTALLER | 1 + .../python_jose-3.5.0.dist-info/METADATA | 149 + .../python_jose-3.5.0.dist-info/RECORD | 37 + .../python_jose-3.5.0.dist-info/REQUESTED | 0 .../python_jose-3.5.0.dist-info/WHEEL | 6 + .../licenses/LICENSE | 22 + .../python_jose-3.5.0.dist-info/top_level.txt | 1 + .../INSTALLER | 1 + .../METADATA | 43 + .../python_multipart-0.0.32.dist-info/RECORD | 23 + .../REQUESTED | 0 .../python_multipart-0.0.32.dist-info/WHEEL | 4 + .../licenses/LICENSE.txt | 202 + .../python_multipart/__init__.py | 35 + .../python_multipart/decoders.py | 185 + .../python_multipart/exceptions.py | 34 + .../python_multipart/multipart.py | 1925 ++ .../site-packages/python_multipart/py.typed | 0 .../rsa-4.9.1.dist-info/INSTALLER | 1 + .../site-packages/rsa-4.9.1.dist-info/LICENSE | 13 + .../rsa-4.9.1.dist-info/METADATA | 140 + .../site-packages/rsa-4.9.1.dist-info/RECORD | 41 + .../site-packages/rsa-4.9.1.dist-info/WHEEL | 4 + .../rsa-4.9.1.dist-info/entry_points.txt | 8 + .../python3.12/site-packages/rsa/__init__.py | 60 + .../lib/python3.12/site-packages/rsa/asn1.py | 52 + .../lib/python3.12/site-packages/rsa/cli.py | 321 + .../python3.12/site-packages/rsa/common.py | 184 + .../lib/python3.12/site-packages/rsa/core.py | 53 + .../lib/python3.12/site-packages/rsa/key.py | 858 + .../python3.12/site-packages/rsa/parallel.py | 96 + .../lib/python3.12/site-packages/rsa/pem.py | 134 + .../lib/python3.12/site-packages/rsa/pkcs1.py | 485 + .../python3.12/site-packages/rsa/pkcs1_v2.py | 100 + .../lib/python3.12/site-packages/rsa/prime.py | 198 + .../lib/python3.12/site-packages/rsa/py.typed | 1 + .../python3.12/site-packages/rsa/randnum.py | 95 + .../python3.12/site-packages/rsa/transform.py | 72 + .../lib/python3.12/site-packages/rsa/util.py | 97 + .../six-1.17.0.dist-info/INSTALLER | 1 + .../six-1.17.0.dist-info/LICENSE | 18 + .../six-1.17.0.dist-info/METADATA | 43 + .../site-packages/six-1.17.0.dist-info/RECORD | 8 + .../site-packages/six-1.17.0.dist-info/WHEEL | 6 + .../six-1.17.0.dist-info/top_level.txt | 1 + .../lib/python3.12/site-packages/six.py | 1003 + .../sqlalchemy-2.0.52.dist-info/INSTALLER | 1 + .../sqlalchemy-2.0.52.dist-info/METADATA | 245 + .../sqlalchemy-2.0.52.dist-info/RECORD | 532 + .../sqlalchemy-2.0.52.dist-info/REQUESTED | 0 .../sqlalchemy-2.0.52.dist-info/WHEEL | 6 + .../licenses/LICENSE | 19 + .../sqlalchemy-2.0.52.dist-info/top_level.txt | 1 + .../site-packages/sqlalchemy/__init__.py | 283 + .../sqlalchemy/connectors/__init__.py | 18 + .../sqlalchemy/connectors/aioodbc.py | 193 + .../sqlalchemy/connectors/asyncio.py | 429 + .../sqlalchemy/connectors/pyodbc.py | 259 + .../sqlalchemy/cyextension/__init__.py | 6 + .../collections.cpython-312-darwin.so | Bin 0 -> 296368 bytes .../sqlalchemy/cyextension/collections.pyx | 409 + .../immutabledict.cpython-312-darwin.so | Bin 0 -> 133168 bytes .../sqlalchemy/cyextension/immutabledict.pxd | 8 + .../sqlalchemy/cyextension/immutabledict.pyx | 133 + .../processors.cpython-312-darwin.so | Bin 0 -> 111536 bytes .../sqlalchemy/cyextension/processors.pyx | 68 + .../resultproxy.cpython-312-darwin.so | Bin 0 -> 116656 bytes .../sqlalchemy/cyextension/resultproxy.pyx | 102 + .../cyextension/util.cpython-312-darwin.so | Bin 0 -> 137848 bytes .../sqlalchemy/cyextension/util.pyx | 90 + .../sqlalchemy/dialects/__init__.py | 62 + .../sqlalchemy/dialects/_typing.py | 29 + .../sqlalchemy/dialects/mssql/__init__.py | 87 + .../sqlalchemy/dialects/mssql/aioodbc.py | 63 + .../sqlalchemy/dialects/mssql/base.py | 4104 ++++ .../dialects/mssql/information_schema.py | 284 + .../sqlalchemy/dialects/mssql/json.py | 129 + .../sqlalchemy/dialects/mssql/provision.py | 185 + .../sqlalchemy/dialects/mssql/pymssql.py | 127 + .../sqlalchemy/dialects/mssql/pyodbc.py | 759 + .../sqlalchemy/dialects/mysql/__init__.py | 104 + .../sqlalchemy/dialects/mysql/aiomysql.py | 274 + .../sqlalchemy/dialects/mysql/asyncmy.py | 253 + .../sqlalchemy/dialects/mysql/base.py | 3951 ++++ .../sqlalchemy/dialects/mysql/cymysql.py | 107 + .../sqlalchemy/dialects/mysql/dml.py | 224 + .../sqlalchemy/dialects/mysql/enumerated.py | 282 + .../sqlalchemy/dialects/mysql/expression.py | 146 + .../sqlalchemy/dialects/mysql/json.py | 91 + .../sqlalchemy/dialects/mysql/mariadb.py | 72 + .../dialects/mysql/mariadbconnector.py | 323 + .../dialects/mysql/mysqlconnector.py | 303 + .../sqlalchemy/dialects/mysql/mysqldb.py | 315 + .../sqlalchemy/dialects/mysql/provision.py | 153 + .../sqlalchemy/dialects/mysql/pymysql.py | 179 + .../sqlalchemy/dialects/mysql/pyodbc.py | 158 + .../sqlalchemy/dialects/mysql/reflection.py | 727 + .../dialects/mysql/reserved_words.py | 570 + .../sqlalchemy/dialects/mysql/types.py | 835 + .../sqlalchemy/dialects/oracle/__init__.py | 81 + .../sqlalchemy/dialects/oracle/base.py | 3806 ++++ .../sqlalchemy/dialects/oracle/cx_oracle.py | 1556 ++ .../sqlalchemy/dialects/oracle/dictionary.py | 507 + .../sqlalchemy/dialects/oracle/oracledb.py | 942 + .../sqlalchemy/dialects/oracle/provision.py | 297 + .../sqlalchemy/dialects/oracle/types.py | 316 + .../sqlalchemy/dialects/oracle/vector.py | 363 + .../dialects/postgresql/__init__.py | 166 + .../dialects/postgresql/_psycopg_common.py | 225 + .../sqlalchemy/dialects/postgresql/array.py | 519 + .../sqlalchemy/dialects/postgresql/asyncpg.py | 1287 ++ .../sqlalchemy/dialects/postgresql/base.py | 5488 +++++ .../sqlalchemy/dialects/postgresql/dml.py | 338 + .../sqlalchemy/dialects/postgresql/ext.py | 546 + .../sqlalchemy/dialects/postgresql/hstore.py | 405 + .../sqlalchemy/dialects/postgresql/json.py | 407 + .../dialects/postgresql/named_types.py | 524 + .../dialects/postgresql/operators.py | 128 + .../sqlalchemy/dialects/postgresql/pg8000.py | 667 + .../dialects/postgresql/pg_catalog.py | 326 + .../dialects/postgresql/provision.py | 184 + .../sqlalchemy/dialects/postgresql/psycopg.py | 846 + .../dialects/postgresql/psycopg2.py | 866 + .../dialects/postgresql/psycopg2cffi.py | 62 + .../sqlalchemy/dialects/postgresql/ranges.py | 1031 + .../sqlalchemy/dialects/postgresql/types.py | 313 + .../sqlalchemy/dialects/sqlite/__init__.py | 57 + .../sqlalchemy/dialects/sqlite/aiosqlite.py | 513 + .../sqlalchemy/dialects/sqlite/base.py | 3035 +++ .../sqlalchemy/dialects/sqlite/dml.py | 263 + .../sqlalchemy/dialects/sqlite/json.py | 92 + .../sqlalchemy/dialects/sqlite/provision.py | 229 + .../sqlalchemy/dialects/sqlite/pysqlcipher.py | 160 + .../sqlalchemy/dialects/sqlite/pysqlite.py | 834 + .../dialects/type_migration_guidelines.txt | 145 + .../sqlalchemy/engine/__init__.py | 62 + .../sqlalchemy/engine/_py_processors.py | 135 + .../sqlalchemy/engine/_py_row.py | 128 + .../sqlalchemy/engine/_py_util.py | 74 + .../site-packages/sqlalchemy/engine/base.py | 3392 ++++ .../sqlalchemy/engine/characteristics.py | 155 + .../site-packages/sqlalchemy/engine/create.py | 899 + .../site-packages/sqlalchemy/engine/cursor.py | 2307 +++ .../sqlalchemy/engine/default.py | 2394 +++ .../site-packages/sqlalchemy/engine/events.py | 965 + .../sqlalchemy/engine/interfaces.py | 3471 ++++ .../site-packages/sqlalchemy/engine/mock.py | 133 + .../sqlalchemy/engine/processors.py | 62 + .../sqlalchemy/engine/reflection.py | 2103 ++ .../site-packages/sqlalchemy/engine/result.py | 2449 +++ .../site-packages/sqlalchemy/engine/row.py | 400 + .../sqlalchemy/engine/strategies.py | 16 + .../site-packages/sqlalchemy/engine/url.py | 924 + .../site-packages/sqlalchemy/engine/util.py | 167 + .../sqlalchemy/event/__init__.py | 26 + .../site-packages/sqlalchemy/event/api.py | 220 + .../site-packages/sqlalchemy/event/attr.py | 677 + .../site-packages/sqlalchemy/event/base.py | 473 + .../site-packages/sqlalchemy/event/legacy.py | 259 + .../sqlalchemy/event/registry.py | 391 + .../site-packages/sqlalchemy/events.py | 17 + .../site-packages/sqlalchemy/exc.py | 833 + .../site-packages/sqlalchemy/ext/__init__.py | 10 + .../sqlalchemy/ext/associationproxy.py | 2028 ++ .../sqlalchemy/ext/asyncio/__init__.py | 25 + .../sqlalchemy/ext/asyncio/base.py | 281 + .../sqlalchemy/ext/asyncio/engine.py | 1471 ++ .../sqlalchemy/ext/asyncio/exc.py | 21 + .../sqlalchemy/ext/asyncio/result.py | 965 + .../sqlalchemy/ext/asyncio/scoping.py | 1599 ++ .../sqlalchemy/ext/asyncio/session.py | 1946 ++ .../site-packages/sqlalchemy/ext/automap.py | 1702 ++ .../site-packages/sqlalchemy/ext/baked.py | 569 + .../site-packages/sqlalchemy/ext/compiler.py | 601 + .../sqlalchemy/ext/declarative/__init__.py | 65 + .../sqlalchemy/ext/declarative/extensions.py | 565 + .../sqlalchemy/ext/horizontal_shard.py | 479 + .../site-packages/sqlalchemy/ext/hybrid.py | 1535 ++ .../site-packages/sqlalchemy/ext/indexable.py | 364 + .../sqlalchemy/ext/instrumentation.py | 450 + .../site-packages/sqlalchemy/ext/mutable.py | 1085 + .../sqlalchemy/ext/mypy/__init__.py | 6 + .../sqlalchemy/ext/mypy/apply.py | 324 + .../sqlalchemy/ext/mypy/decl_class.py | 515 + .../sqlalchemy/ext/mypy/infer.py | 590 + .../sqlalchemy/ext/mypy/names.py | 335 + .../sqlalchemy/ext/mypy/plugin.py | 304 + .../site-packages/sqlalchemy/ext/mypy/util.py | 357 + .../sqlalchemy/ext/orderinglist.py | 440 + .../sqlalchemy/ext/serializer.py | 184 + .../sqlalchemy/future/__init__.py | 17 + .../site-packages/sqlalchemy/future/engine.py | 15 + .../site-packages/sqlalchemy/inspection.py | 175 + .../site-packages/sqlalchemy/log.py | 288 + .../site-packages/sqlalchemy/orm/__init__.py | 171 + .../sqlalchemy/orm/_orm_constructors.py | 2661 +++ .../site-packages/sqlalchemy/orm/_typing.py | 179 + .../sqlalchemy/orm/attributes.py | 2845 +++ .../site-packages/sqlalchemy/orm/base.py | 971 + .../sqlalchemy/orm/bulk_persistence.py | 2151 ++ .../sqlalchemy/orm/clsregistry.py | 571 + .../sqlalchemy/orm/collections.py | 1628 ++ .../site-packages/sqlalchemy/orm/context.py | 3334 +++ .../site-packages/sqlalchemy/orm/decl_api.py | 2007 ++ .../site-packages/sqlalchemy/orm/decl_base.py | 2206 ++ .../sqlalchemy/orm/dependency.py | 1302 ++ .../sqlalchemy/orm/descriptor_props.py | 1092 + .../site-packages/sqlalchemy/orm/dynamic.py | 306 + .../site-packages/sqlalchemy/orm/evaluator.py | 378 + .../site-packages/sqlalchemy/orm/events.py | 3253 +++ .../site-packages/sqlalchemy/orm/exc.py | 237 + .../site-packages/sqlalchemy/orm/identity.py | 302 + .../sqlalchemy/orm/instrumentation.py | 753 + .../sqlalchemy/orm/interfaces.py | 1496 ++ .../site-packages/sqlalchemy/orm/loading.py | 1686 ++ .../sqlalchemy/orm/mapped_collection.py | 557 + .../site-packages/sqlalchemy/orm/mapper.py | 4445 ++++ .../sqlalchemy/orm/path_registry.py | 866 + .../sqlalchemy/orm/persistence.py | 1789 ++ .../sqlalchemy/orm/properties.py | 934 + .../site-packages/sqlalchemy/orm/query.py | 3466 ++++ .../sqlalchemy/orm/relationships.py | 3515 ++++ .../site-packages/sqlalchemy/orm/scoping.py | 2147 ++ .../site-packages/sqlalchemy/orm/session.py | 5304 +++++ .../site-packages/sqlalchemy/orm/state.py | 1168 ++ .../sqlalchemy/orm/state_changes.py | 196 + .../sqlalchemy/orm/strategies.py | 3482 ++++ .../sqlalchemy/orm/strategy_options.py | 2591 +++ .../site-packages/sqlalchemy/orm/sync.py | 164 + .../sqlalchemy/orm/unitofwork.py | 795 + .../site-packages/sqlalchemy/orm/util.py | 2410 +++ .../site-packages/sqlalchemy/orm/writeonly.py | 674 + .../site-packages/sqlalchemy/pool/__init__.py | 44 + .../site-packages/sqlalchemy/pool/base.py | 1524 ++ .../site-packages/sqlalchemy/pool/events.py | 375 + .../site-packages/sqlalchemy/pool/impl.py | 589 + .../site-packages/sqlalchemy/py.typed | 0 .../site-packages/sqlalchemy/schema.py | 69 + .../site-packages/sqlalchemy/sql/__init__.py | 145 + .../sqlalchemy/sql/_dml_constructors.py | 132 + .../sqlalchemy/sql/_elements_constructors.py | 1884 ++ .../sqlalchemy/sql/_orm_types.py | 19 + .../site-packages/sqlalchemy/sql/_py_util.py | 75 + .../sql/_selectable_constructors.py | 763 + .../site-packages/sqlalchemy/sql/_typing.py | 482 + .../sqlalchemy/sql/annotation.py | 587 + .../site-packages/sqlalchemy/sql/base.py | 2292 +++ .../site-packages/sqlalchemy/sql/cache_key.py | 1057 + .../site-packages/sqlalchemy/sql/coercions.py | 1404 ++ .../site-packages/sqlalchemy/sql/compiler.py | 8095 ++++++++ .../site-packages/sqlalchemy/sql/crud.py | 1753 ++ .../site-packages/sqlalchemy/sql/ddl.py | 1445 ++ .../sqlalchemy/sql/default_comparator.py | 551 + .../site-packages/sqlalchemy/sql/dml.py | 1854 ++ .../site-packages/sqlalchemy/sql/elements.py | 5592 +++++ .../site-packages/sqlalchemy/sql/events.py | 458 + .../sqlalchemy/sql/expression.py | 158 + .../site-packages/sqlalchemy/sql/functions.py | 2199 ++ .../site-packages/sqlalchemy/sql/lambdas.py | 1441 ++ .../site-packages/sqlalchemy/sql/naming.py | 209 + .../site-packages/sqlalchemy/sql/operators.py | 2623 +++ .../site-packages/sqlalchemy/sql/roles.py | 323 + .../site-packages/sqlalchemy/sql/schema.py | 6223 ++++++ .../sqlalchemy/sql/selectable.py | 7250 +++++++ .../site-packages/sqlalchemy/sql/sqltypes.py | 3934 ++++ .../sqlalchemy/sql/traversals.py | 1023 + .../site-packages/sqlalchemy/sql/type_api.py | 2368 +++ .../site-packages/sqlalchemy/sql/util.py | 1486 ++ .../site-packages/sqlalchemy/sql/visitors.py | 1164 ++ .../sqlalchemy/testing/__init__.py | 98 + .../sqlalchemy/testing/assertions.py | 994 + .../sqlalchemy/testing/assertsql.py | 520 + .../sqlalchemy/testing/asyncio.py | 135 + .../sqlalchemy/testing/config.py | 441 + .../sqlalchemy/testing/engines.py | 482 + .../sqlalchemy/testing/entities.py | 117 + .../sqlalchemy/testing/exclusions.py | 476 + .../sqlalchemy/testing/fixtures/__init__.py | 28 + .../sqlalchemy/testing/fixtures/base.py | 389 + .../sqlalchemy/testing/fixtures/mypy.py | 333 + .../sqlalchemy/testing/fixtures/orm.py | 227 + .../sqlalchemy/testing/fixtures/sql.py | 482 + .../sqlalchemy/testing/pickleable.py | 155 + .../sqlalchemy/testing/plugin/__init__.py | 6 + .../sqlalchemy/testing/plugin/bootstrap.py | 50 + .../sqlalchemy/testing/plugin/plugin_base.py | 832 + .../sqlalchemy/testing/plugin/pytestplugin.py | 919 + .../sqlalchemy/testing/profiling.py | 328 + .../sqlalchemy/testing/provision.py | 602 + .../sqlalchemy/testing/requirements.py | 1945 ++ .../sqlalchemy/testing/schema.py | 198 + .../sqlalchemy/testing/suite/__init__.py | 19 + .../sqlalchemy/testing/suite/test_cte.py | 237 + .../sqlalchemy/testing/suite/test_ddl.py | 389 + .../testing/suite/test_deprecations.py | 153 + .../sqlalchemy/testing/suite/test_dialect.py | 776 + .../sqlalchemy/testing/suite/test_insert.py | 630 + .../testing/suite/test_reflection.py | 3644 ++++ .../sqlalchemy/testing/suite/test_results.py | 504 + .../sqlalchemy/testing/suite/test_rowcount.py | 258 + .../sqlalchemy/testing/suite/test_select.py | 2010 ++ .../sqlalchemy/testing/suite/test_sequence.py | 317 + .../sqlalchemy/testing/suite/test_types.py | 2147 ++ .../testing/suite/test_unicode_ddl.py | 189 + .../testing/suite/test_update_delete.py | 139 + .../site-packages/sqlalchemy/testing/util.py | 575 + .../sqlalchemy/testing/warnings.py | 52 + .../site-packages/sqlalchemy/types.py | 73 + .../site-packages/sqlalchemy/util/__init__.py | 164 + .../sqlalchemy/util/_collections.py | 712 + .../sqlalchemy/util/_concurrency_py3k.py | 288 + .../site-packages/sqlalchemy/util/_has_cy.py | 40 + .../sqlalchemy/util/_py_collections.py | 541 + .../site-packages/sqlalchemy/util/compat.py | 420 + .../sqlalchemy/util/concurrency.py | 110 + .../sqlalchemy/util/deprecations.py | 401 + .../sqlalchemy/util/langhelpers.py | 2299 +++ .../sqlalchemy/util/preloaded.py | 151 + .../site-packages/sqlalchemy/util/queue.py | 322 + .../sqlalchemy/util/tool_support.py | 202 + .../sqlalchemy/util/topological.py | 120 + .../site-packages/sqlalchemy/util/typing.py | 736 + .../starlette-1.6.0.dist-info/INSTALLER | 1 + .../starlette-1.6.0.dist-info/METADATA | 178 + .../starlette-1.6.0.dist-info/RECORD | 76 + .../starlette-1.6.0.dist-info/WHEEL | 4 + .../licenses/LICENSE.md | 27 + .../site-packages/starlette/__init__.py | 1 + .../starlette/_exception_handler.py | 65 + .../site-packages/starlette/_utils.py | 111 + .../site-packages/starlette/applications.py | 124 + .../site-packages/starlette/authentication.py | 149 + .../site-packages/starlette/background.py | 36 + .../site-packages/starlette/concurrency.py | 59 + .../site-packages/starlette/config.py | 140 + .../site-packages/starlette/convertors.py | 89 + .../site-packages/starlette/datastructures.py | 704 + .../site-packages/starlette/endpoints.py | 127 + .../site-packages/starlette/exceptions.py | 43 + .../site-packages/starlette/formparsers.py | 297 + .../starlette/middleware/__init__.py | 37 + .../starlette/middleware/authentication.py | 52 + .../starlette/middleware/base.py | 244 + .../starlette/middleware/body_limit.py | 132 + .../starlette/middleware/cors.py | 179 + .../starlette/middleware/errors.py | 259 + .../starlette/middleware/exceptions.py | 73 + .../starlette/middleware/gzip.py | 222 + .../starlette/middleware/httpsredirect.py | 19 + .../starlette/middleware/sessions.py | 125 + .../starlette/middleware/trustedhost.py | 60 + .../starlette/middleware/wsgi.py | 156 + .../site-packages/starlette/py.typed | 0 .../site-packages/starlette/requests.py | 348 + .../site-packages/starlette/responses.py | 573 + .../site-packages/starlette/routing.py | 757 + .../site-packages/starlette/schemas.py | 148 + .../site-packages/starlette/staticfiles.py | 223 + .../site-packages/starlette/status.py | 211 + .../site-packages/starlette/templating.py | 156 + .../site-packages/starlette/testclient.py | 749 + .../site-packages/starlette/types.py | 26 + .../site-packages/starlette/websockets.py | 196 + .../INSTALLER | 1 + .../METADATA | 73 + .../typing_extensions-4.16.0.dist-info/RECORD | 7 + .../typing_extensions-4.16.0.dist-info/WHEEL | 4 + .../licenses/LICENSE | 279 + .../site-packages/typing_extensions.py | 4422 ++++ .../INSTALLER | 1 + .../METADATA | 49 + .../typing_inspection-0.4.4.dist-info/RECORD | 13 + .../typing_inspection-0.4.4.dist-info/WHEEL | 4 + .../licenses/LICENSE | 21 + .../typing_inspection/__init__.py | 0 .../typing_inspection/introspection.py | 587 + .../site-packages/typing_inspection/py.typed | 0 .../typing_inspection/typing_objects.py | 620 + .../typing_inspection/typing_objects.pyi | 421 + .venv-test/pyvenv.cfg | 5 + backend/routers/analyze.py | 23 + backend/routers/water.py | 207 +- backend/tests/test_scoring_service.py | 13 + frontend/src/components/SearchMap.jsx | 46 +- .../src/pages/analysis/AnalysisResult.jsx | 24 +- services/scoring_service.py | 4 + 2069 files changed, 803865 insertions(+), 97 deletions(-) create mode 100644 .venv-test/bin/Activate.ps1 create mode 100644 .venv-test/bin/activate create mode 100644 .venv-test/bin/activate.csh create mode 100644 .venv-test/bin/activate.fish create mode 100755 .venv-test/bin/cffi-gen-src create mode 100755 .venv-test/bin/fastapi create mode 100755 .venv-test/bin/httpx create mode 100755 .venv-test/bin/idna create mode 100755 .venv-test/bin/pip create mode 100755 .venv-test/bin/pip3 create mode 100755 .venv-test/bin/pip3.12 create mode 100755 .venv-test/bin/py.test create mode 100755 .venv-test/bin/pygmentize create mode 100755 .venv-test/bin/pyrsa-decrypt create mode 100755 .venv-test/bin/pyrsa-encrypt create mode 100755 .venv-test/bin/pyrsa-keygen create mode 100755 .venv-test/bin/pyrsa-priv2pub create mode 100755 .venv-test/bin/pyrsa-sign create mode 100755 .venv-test/bin/pyrsa-verify create mode 100755 .venv-test/bin/pytest create mode 120000 .venv-test/bin/python create mode 120000 .venv-test/bin/python3 create mode 120000 .venv-test/bin/python3.12 create mode 100755 .venv-test/lib/python3.12/site-packages/_cffi_backend.cpython-312-darwin.so create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_argcomplete.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_code/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_code/code.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_code/source.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_io/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_io/pprint.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_io/saferepr.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_io/wcwidth.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_py/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_py/error.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_py/path.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/_version.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_any.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_mapping.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_sequence.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_set.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/_guards.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/_typing.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/compare_text.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/highlight.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/rewrite.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/truncate.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/assertion/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/cacheprovider.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/capture.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/config/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/config/argparsing.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/config/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/config/findpaths.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/debugging.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/deprecated.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/doctest.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/faulthandler.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/fixtures.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/freeze_support.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/helpconfig.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/hookspec.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/junitxml.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/legacypath.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/logging.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/main.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/mark/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/mark/expression.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/mark/structures.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/monkeypatch.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/nodes.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/outcomes.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/pastebin.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/pathlib.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/pytester.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/pytester_assertions.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/python.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/python_api.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/raises.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/recwarn.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/reports.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/runner.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/scope.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/setuponly.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/setupplan.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/skipping.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/stash.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/stepwise.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/subtests.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/terminal.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/terminalprogress.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/threadexception.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/timing.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/tmpdir.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/tracemalloc.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/unittest.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/unraisableexception.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/warning_types.py create mode 100644 .venv-test/lib/python3.12/site-packages/_pytest/warnings.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/__version__.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/context.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/core.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/cursor.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/tests/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/tests/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/tests/helpers.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/tests/perf.py create mode 100644 .venv-test/lib/python3.12/site-packages/aiosqlite/tests/smoke.py create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_doc/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_doc/main.py create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_doc/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_types/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_types/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/annotated_types/test_cases.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_backends/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_backends/_asyncio.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_backends/_trio.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_concurrency_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_contextmanagers.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_eventloop.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_fileio.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_futures.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_resources.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_signals.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_sockets.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_streams.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_subprocesses.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_synchronization.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_tasks.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_tempfile.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_testing.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_core/_typedattr.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/_lazyimport.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/abc/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/abc/_eventloop.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/abc/_resources.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/abc/_sockets.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/abc/_streams.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/abc/_subprocesses.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/abc/_tasks.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/abc/_testing.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/from_thread.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/functools.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/itertools.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/lowlevel.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/pytest_plugin.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/streams/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/streams/buffered.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/streams/file.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/streams/memory.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/streams/stapled.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/streams/text.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/streams/tls.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/to_interpreter.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/to_process.py create mode 100644 .venv-test/lib/python3.12/site-packages/anyio/to_thread.py create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt/__init__.pyi create mode 100755 .venv-test/lib/python3.12/site-packages/bcrypt/_bcrypt.abi3.so create mode 100644 .venv-test/lib/python3.12/site-packages/bcrypt/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/certifi-2026.7.22.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/certifi-2026.7.22.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/certifi-2026.7.22.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/certifi-2026.7.22.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/certifi-2026.7.22.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/certifi-2026.7.22.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/certifi/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/certifi/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/certifi/cacert.pem create mode 100644 .venv-test/lib/python3.12/site-packages/certifi/core.py create mode 100644 .venv-test/lib/python3.12/site-packages/certifi/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/certifi/tests/__init__.py create mode 100755 .venv-test/lib/python3.12/site-packages/certifi/tests/test_certify.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi-2.1.1.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/cffi-2.1.1.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/cffi-2.1.1.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/cffi-2.1.1.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/cffi-2.1.1.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/cffi-2.1.1.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/cffi-2.1.1.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/_cffi_errors.h create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/_cffi_gen_src.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/_cffi_include.h create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/_embedding.h create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/_imp_emulation.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/_shimmed_dist_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/api.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/backend_ctypes.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/cffi_opcode.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/commontypes.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/cparser.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/error.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/ffiplatform.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/gen_src.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/lock.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/model.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/parse_c_type.h create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/pkgconfig.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/recompiler.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/setuptools_ext.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/vengine_cpy.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/vengine_gen.py create mode 100644 .venv-test/lib/python3.12/site-packages/cffi/verifier.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography-50.0.1.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography-50.0.1.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography-50.0.1.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography-50.0.1.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography-50.0.1.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography-50.0.1.dist-info/licenses/LICENSE.APACHE create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography-50.0.1.dist-info/licenses/LICENSE.BSD create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography-50.0.1.dist-info/sboms/cryptography-rust.cyclonedx.json create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography-50.0.1.dist-info/sboms/sbom.json create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/__about__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/cobblestone.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/fernet.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/_oid.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/asn1/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/asn1/asn1.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/backends/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/backends/openssl/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/backends/openssl/backend.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/__init__.py create mode 100755 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust.abi3.so create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/__init__.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/_openssl.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/asn1.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/cobblestone.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/exceptions.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/ocsp.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/aead.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/cmac.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/dh.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/dsa.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/ec.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed448.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/hmac.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/keys.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/keywrap.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/mldsa.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/mlkem.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/rsa.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/openssl/x448.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/pkcs12.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/pkcs7.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/test_support.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/_rust/x509.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/openssl/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/openssl/_conditional.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/bindings/openssl/binding.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/decrepit/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/decrepit/ciphers/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/decrepit/ciphers/algorithms.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/decrepit/ciphers/modes.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/_asymmetric.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/_cipheralgorithm.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/_modes.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/_serialization.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/dh.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/dsa.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/ec.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/ed25519.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/ed448.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/mldsa.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/mlkem.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/padding.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/rsa.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/x25519.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/asymmetric/x448.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/ciphers/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/ciphers/aead.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/ciphers/algorithms.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/ciphers/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/ciphers/modes.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/cmac.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/constant_time.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/hashes.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/hmac.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/hpke.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/argon2.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/concatkdf.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/hkdf.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/kbkdf.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/pbkdf2.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/scrypt.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/kdf/x963kdf.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/keywrap.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/padding.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/poly1305.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/serialization/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/serialization/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/serialization/pkcs12.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/serialization/pkcs7.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/serialization/ssh.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/twofactor/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/twofactor/hotp.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/hazmat/primitives/twofactor/totp.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/x509/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/x509/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/x509/certificate_transparency.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/x509/extensions.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/x509/general_name.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/x509/name.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/x509/ocsp.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/x509/oid.py create mode 100644 .venv-test/lib/python3.12/site-packages/cryptography/x509/verification.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa-0.19.2.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa-0.19.2.dist-info/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa-0.19.2.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa-0.19.2.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa-0.19.2.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa-0.19.2.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/_compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/_rwlock.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/_sha3.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/_version.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/curves.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/der.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/ecdh.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/ecdsa.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/eddsa.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/ellipticcurve.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/errors.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/keys.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/numbertheory.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/rfc6979.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/ssh.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_curves.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_der.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_ecdh.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_ecdsa.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_eddsa.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_ellipticcurve.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_jacobi.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_keys.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_malformed_sigs.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_numbertheory.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_pyecdsa.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_rw_lock.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/test_sha3.py create mode 100644 .venv-test/lib/python3.12/site-packages/ecdsa/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi-0.141.1.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi-0.141.1.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi-0.141.1.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi-0.141.1.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi-0.141.1.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi-0.141.1.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi-0.141.1.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/SKILL.md create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/references/dependencies.md create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/references/other-tools.md create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/references/path-operations.md create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/references/pydantic.md create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/references/responses.md create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/.agents/skills/fastapi/references/streaming.md create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/_compat/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/_compat/shared.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/_compat/v2.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/applications.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/background.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/cli.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/concurrency.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/datastructures.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/dependencies/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/dependencies/models.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/dependencies/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/encoders.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/exception_handlers.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/logger.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/middleware/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/middleware/cors.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/middleware/gzip.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/middleware/httpsredirect.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/middleware/trustedhost.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/middleware/wsgi.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/openapi/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/openapi/constants.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/openapi/docs.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/openapi/models.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/openapi/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/param_functions.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/params.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/requests.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/responses.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/routing.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/security/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/security/api_key.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/security/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/security/http.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/security/oauth2.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/security/open_id_connect_url.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/security/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/sse.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/staticfiles.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/templating.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/testclient.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/fastapi/websockets.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11-0.16.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/h11-0.16.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/h11-0.16.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/h11-0.16.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/h11-0.16.0.dist-info/licenses/LICENSE.txt create mode 100644 .venv-test/lib/python3.12/site-packages/h11-0.16.0.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/h11/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_abnf.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_connection.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_events.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_headers.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_readers.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_receivebuffer.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_state.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_util.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_version.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/_writers.py create mode 100644 .venv-test/lib/python3.12/site-packages/h11/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/licenses/LICENSE.md create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_api.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_async/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_async/connection.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_async/connection_pool.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_async/http11.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_async/http2.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_async/http_proxy.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_async/interfaces.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_async/socks_proxy.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_backends/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_backends/anyio.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_backends/auto.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_backends/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_backends/mock.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_backends/sync.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_backends/trio.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_models.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_ssl.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_sync/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_sync/connection.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_sync/http11.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_sync/http2.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_sync/http_proxy.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_sync/interfaces.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_sync/socks_proxy.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_synchronization.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_trace.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpcore/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/httpx-0.28.1.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/httpx-0.28.1.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/httpx-0.28.1.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/httpx-0.28.1.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/httpx-0.28.1.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/httpx-0.28.1.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/httpx-0.28.1.dist-info/licenses/LICENSE.md create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/__version__.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_api.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_auth.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_client.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_config.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_content.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_decoders.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_main.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_models.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_multipart.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_status_codes.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_transports/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_transports/asgi.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_transports/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_transports/default.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_transports/mock.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_transports/wsgi.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_types.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_urlparse.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_urls.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/httpx/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/idna-3.19.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/idna-3.19.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/idna-3.19.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/idna-3.19.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/idna-3.19.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/idna-3.19.dist-info/licenses/LICENSE.md create mode 100644 .venv-test/lib/python3.12/site-packages/idna/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/idna/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/idna/cli.py create mode 100644 .venv-test/lib/python3.12/site-packages/idna/codec.py create mode 100644 .venv-test/lib/python3.12/site-packages/idna/compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/idna/core.py create mode 100644 .venv-test/lib/python3.12/site-packages/idna/idnadata.py create mode 100644 .venv-test/lib/python3.12/site-packages/idna/intranges.py create mode 100644 .venv-test/lib/python3.12/site-packages/idna/package_data.py create mode 100644 .venv-test/lib/python3.12/site-packages/idna/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/idna/uts46data.py create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig/_parse.py create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig/_version.py create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/iniconfig/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/jose/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/backends/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/backends/_asn1.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/backends/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/backends/cryptography_backend.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/backends/ecdsa_backend.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/backends/native.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/backends/rsa_backend.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/constants.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/jwe.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/jwk.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/jws.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/jwt.py create mode 100644 .venv-test/lib/python3.12/site-packages/jose/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/algorithms.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/api_jwk.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/api_jws.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/api_jwt.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/help.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/jwk_set_cache.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/jwks_client.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/jwt/warnings.py create mode 100644 .venv-test/lib/python3.12/site-packages/multipart/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/multipart/decoders.py create mode 100644 .venv-test/lib/python3.12/site-packages/multipart/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/multipart/multipart.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging-26.3.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/packaging-26.3.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/packaging-26.3.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/packaging-26.3.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/packaging-26.3.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/packaging-26.3.dist-info/licenses/LICENSE.APACHE create mode 100644 .venv-test/lib/python3.12/site-packages/packaging-26.3.dist-info/licenses/LICENSE.BSD create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/_elffile.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/_manylinux.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/_musllinux.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/_parser.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/_ranges.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/_structures.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/_tokenizer.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/dependency_groups.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/direct_url.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/errors.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/licenses/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/licenses/_spdx.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/markers.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/metadata.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/pylock.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/ranges.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/requirements.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/specifiers.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/tags.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/packaging/version.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip-24.3.1.dist-info/AUTHORS.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pip-24.3.1.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pip-24.3.1.dist-info/LICENSE.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pip-24.3.1.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pip-24.3.1.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pip-24.3.1.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/pip-24.3.1.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pip-24.3.1.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pip-24.3.1.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pip/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/__pip-runner__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/build_env.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cache.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/autocompletion.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/base_command.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/cmdoptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/command_context.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/index_command.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/main.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/main_parser.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/parser.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/progress_bars.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/req_command.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/spinners.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/cli/status_codes.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/cache.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/check.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/completion.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/configuration.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/debug.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/download.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/freeze.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/hash.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/help.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/index.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/inspect.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/install.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/list.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/search.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/show.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/uninstall.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/commands/wheel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/configuration.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/distributions/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/distributions/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/distributions/installed.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/distributions/sdist.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/distributions/wheel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/index/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/index/collector.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/index/package_finder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/index/sources.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/locations/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/locations/_distutils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/locations/_sysconfig.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/locations/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/main.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/metadata/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/metadata/_json.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/metadata/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/metadata/importlib/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/metadata/importlib/_compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/metadata/importlib/_dists.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/metadata/importlib/_envs.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/metadata/pkg_resources.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/candidate.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/direct_url.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/format_control.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/index.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/installation_report.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/link.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/scheme.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/search_scope.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/selection_prefs.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/target_python.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/models/wheel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/network/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/network/auth.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/network/cache.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/network/download.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/network/lazy_wheel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/network/session.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/network/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/network/xmlrpc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/build/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/build/build_tracker.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/build/metadata.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/build/metadata_editable.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/build/metadata_legacy.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/build/wheel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/build/wheel_editable.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/build/wheel_legacy.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/check.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/freeze.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/install/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/install/editable_legacy.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/install/wheel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/operations/prepare.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/pyproject.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/req/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/req/constructors.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/req/req_file.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/req/req_install.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/req/req_set.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/req/req_uninstall.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/legacy/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/legacy/resolver.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/candidates.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/factory.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/provider.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/reporter.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/requirements.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/resolution/resolvelib/resolver.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/self_outdated_check.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/_jaraco_text.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/_log.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/appdirs.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/compatibility_tags.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/datetime.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/deprecation.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/direct_url_helpers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/egg_link.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/encoding.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/entrypoints.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/filesystem.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/filetypes.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/glibc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/hashes.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/logging.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/misc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/packaging.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/retry.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/setuptools_build.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/subprocess.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/temp_dir.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/unpacking.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/urls.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/virtualenv.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/utils/wheel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/vcs/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/vcs/bazaar.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/vcs/git.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/vcs/mercurial.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/vcs/subversion.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/vcs/versioncontrol.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_internal/wheel_builder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/_cmd.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/adapter.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/cache.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/controller.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/filewrapper.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/heuristics.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/serialize.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/cachecontrol/wrapper.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/certifi/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/certifi/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/certifi/cacert.pem create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/certifi/core.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/certifi/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/database.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/index.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/locators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/manifest.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/markers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/metadata.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/resources.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/scripts.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/t32.exe create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/t64-arm.exe create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/t64.exe create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/version.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/w32.exe create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/w64-arm.exe create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/w64.exe create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distlib/wheel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distro/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distro/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distro/distro.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/distro/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/idna/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/idna/codec.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/idna/compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/idna/core.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/idna/idnadata.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/idna/intranges.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/idna/package_data.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/idna/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/idna/uts46data.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/msgpack/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/msgpack/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/msgpack/ext.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/msgpack/fallback.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/_elffile.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/_manylinux.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/_musllinux.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/_parser.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/_structures.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/_tokenizer.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/markers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/metadata.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/requirements.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/specifiers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/tags.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/packaging/version.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pkg_resources/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/platformdirs/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/platformdirs/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/platformdirs/android.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/platformdirs/api.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/platformdirs/macos.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/platformdirs/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/platformdirs/unix.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/platformdirs/version.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/platformdirs/windows.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/cmdline.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/console.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/filter.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/filters/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatter.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/_mapping.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/bbcode.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/groff.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/html.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/img.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/irc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/latex.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/other.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/pangomarkup.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/rtf.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/svg.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/terminal.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/formatters/terminal256.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/lexer.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/lexers/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/lexers/_mapping.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/lexers/python.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/modeline.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/plugin.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/regexopt.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/scanner.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/sphinxext.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/style.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/styles/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/styles/_mapping.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/token.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/unistring.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pygments/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_impl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/__version__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/_internal_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/adapters.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/api.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/auth.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/certs.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/cookies.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/help.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/hooks.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/models.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/packages.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/sessions.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/status_codes.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/structures.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/requests/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/resolvelib/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/resolvelib/compat/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/resolvelib/compat/collections_abc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/resolvelib/providers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/resolvelib/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/resolvelib/reporters.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/resolvelib/resolvers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/resolvelib/structs.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_cell_widths.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_emoji_codes.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_emoji_replace.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_export_format.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_extension.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_fileno.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_inspect.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_log_render.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_loop.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_null_file.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_palettes.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_pick.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_ratio.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_spinners.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_stack.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_timer.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_win32_console.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_windows.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_windows_renderer.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/_wrap.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/abc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/align.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/ansi.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/bar.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/box.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/cells.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/color.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/color_triplet.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/columns.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/console.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/constrain.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/containers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/control.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/default_styles.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/diagnose.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/emoji.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/errors.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/file_proxy.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/filesize.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/highlighter.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/json.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/jupyter.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/layout.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/live.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/live_render.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/logging.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/markup.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/measure.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/padding.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/pager.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/palette.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/panel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/pretty.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/progress.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/progress_bar.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/prompt.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/protocol.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/region.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/repr.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/rule.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/scope.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/screen.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/segment.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/spinner.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/status.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/style.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/styled.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/syntax.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/table.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/terminal_theme.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/text.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/theme.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/themes.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/traceback.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/rich/tree.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/tomli/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/tomli/_parser.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/tomli/_re.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/tomli/_types.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/tomli/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/truststore/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/truststore/_api.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/truststore/_macos.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/truststore/_openssl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/truststore/_ssl_constants.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/truststore/_windows.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/truststore/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/typing_extensions.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/_collections.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/_version.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/connection.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/connectionpool.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/bindings.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/_securetransport/low_level.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/appengine.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/ntlmpool.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/pyopenssl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/securetransport.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/contrib/socks.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/fields.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/filepost.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/backports/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/backports/makefile.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/backports/weakref_finalize.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/packages/six.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/poolmanager.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/request.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/response.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/connection.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/proxy.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/queue.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/request.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/response.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/retry.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/ssl_.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/ssl_match_hostname.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/ssltransport.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/timeout.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/url.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/urllib3/util/wait.py create mode 100644 .venv-test/lib/python3.12/site-packages/pip/_vendor/vendor.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pip/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy/_callers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy/_hooks.py create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy/_manager.py create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy/_result.py create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy/_tracing.py create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy/_version.py create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy/_warnings.py create mode 100644 .venv-test/lib/python3.12/site-packages/pluggy/py.typed create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/libcom_err.3.0.dylib create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/libcrypto.3.dylib create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/libgssapi_krb5.2.2.dylib create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/libk5crypto.3.1.dylib create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/libkrb5.3.3.dylib create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/libkrb5support.1.1.dylib create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/liblber.2.dylib create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/libldap.2.dylib create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/libpq.5.dylib create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/.dylibs/libssl.3.dylib create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/_ipaddress.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/_json.py create mode 100755 .venv-test/lib/python3.12/site-packages/psycopg2/_psycopg.cpython-312-darwin.so create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/_range.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/errorcodes.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/errors.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/extensions.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/extras.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/pool.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/sql.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2/tz.py create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2_binary-2.9.12.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2_binary-2.9.12.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2_binary-2.9.12.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2_binary-2.9.12.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2_binary-2.9.12.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2_binary-2.9.12.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/psycopg2_binary-2.9.12.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/py.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1-0.6.4.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1-0.6.4.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1-0.6.4.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1-0.6.4.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1-0.6.4.dist-info/licenses/LICENSE.rst create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1-0.6.4.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1-0.6.4.dist-info/zip-safe create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/ber/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/ber/decoder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/ber/encoder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/ber/eoo.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/cer/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/cer/decoder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/cer/encoder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/der/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/der/decoder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/der/encoder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/native/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/native/decoder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/native/encoder.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/codec/streaming.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/compat/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/compat/integer.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/debug.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/error.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/char.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/constraint.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/error.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/namedtype.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/namedval.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/opentype.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/tag.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/tagmap.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/univ.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyasn1/type/useful.py create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser-3.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser-3.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser-3.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser-3.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser-3.0.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser-3.0.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser/_ast_gen.py create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser/_c_ast.cfg create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser/ast_transforms.py create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser/c_ast.py create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser/c_generator.py create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser/c_lexer.py create mode 100644 .venv-test/lib/python3.12/site-packages/pycparser/c_parser.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic-2.13.5.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic-2.13.5.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic-2.13.5.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic-2.13.5.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic-2.13.5.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic-2.13.5.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_config.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_core_metadata.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_core_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_dataclasses.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_decorators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_decorators_v1.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_discriminated_union.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_docs_extraction.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_fields.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_forward_ref.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_generics.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_git.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_import_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_internal_dataclass.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_known_annotated_metadata.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_mock_val_ser.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_model_construction.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_namespace_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_repr.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_schema_gather.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_schema_generation_shared.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_serializers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_signature.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_typing_extra.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_validate_call.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_internal/_validators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/_migration.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/alias_generators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/aliases.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/annotated_handlers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/class_validators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/color.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/config.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/dataclasses.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/datetime_parse.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/decorator.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/deprecated/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/deprecated/class_validators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/deprecated/config.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/deprecated/copy_internals.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/deprecated/decorator.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/deprecated/json.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/deprecated/parse.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/deprecated/tools.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/env_settings.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/error_wrappers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/errors.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/experimental/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/experimental/arguments_schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/experimental/missing_sentinel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/experimental/pipeline.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/fields.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/functional_serializers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/functional_validators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/generics.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/json.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/json_schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/main.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/mypy.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/networks.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/parse.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/plugin/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/plugin/_loader.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/plugin/_schema_validator.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/root_model.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/tools.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/type_adapter.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/typing.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/_hypothesis_plugin.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/annotated_types.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/class_validators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/color.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/config.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/dataclasses.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/datetime_parse.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/decorator.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/env_settings.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/error_wrappers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/errors.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/fields.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/generics.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/json.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/main.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/mypy.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/networks.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/parse.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/tools.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/typing.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/validators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/v1/version.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/validate_call_decorator.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/validators.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/version.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic/warnings.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core-2.46.5.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core-2.46.5.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core-2.46.5.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core-2.46.5.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core-2.46.5.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core-2.46.5.dist-info/sboms/pydantic-core.cyclonedx.json create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core/__init__.py create mode 100755 .venv-test/lib/python3.12/site-packages/pydantic_core/_pydantic_core.cpython-312-darwin.so create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core/_pydantic_core.pyi create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core/core_schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/pydantic_core/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pygments-2.21.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pygments-2.21.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pygments-2.21.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pygments-2.21.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pygments-2.21.0.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pygments-2.21.0.dist-info/licenses/AUTHORS create mode 100644 .venv-test/lib/python3.12/site-packages/pygments-2.21.0.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/cmdline.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/console.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/filter.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/filters/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatter.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/_mapping.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/bbcode.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/groff.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/html.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/img.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/irc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/latex.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/other.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/pangomarkup.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/rtf.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/svg.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/terminal.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/formatters/terminal256.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexer.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_ada_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_asy_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_cl_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_cocoa_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_csound_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_css_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_googlesql_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_julia_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_lasso_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_lilypond_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_lua_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_luau_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_mapping.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_mql_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_mysql_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_openedge_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_php_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_postgres_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_qlik_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_scheme_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_scilab_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_sourcemod_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_sql_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_stan_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_stata_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_tsql_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_usd_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_vbscript_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/_vim_builtins.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/actionscript.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ada.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/agile.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/algebra.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ambient.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/amdgpu.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ampl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/apdlexer.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/apl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/archetype.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/arrow.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/arturo.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/asc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/asm.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/asn1.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/automation.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/bare.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/basic.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/bdd.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/berry.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/bibtex.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/bitbake.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/blueprint.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/boa.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/bqn.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/business.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/c_cpp.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/c_like.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/capnproto.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/carbon.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/cddl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/cel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/chapel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/clean.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/codeql.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/comal.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/compiled.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/configs.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/console.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/cplint.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/crystal.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/csound.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/css.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/d.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/dalvik.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/data.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/dax.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/devicetree.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/diff.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/dns.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/dotnet.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/dsls.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/dylan.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ecl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/eiffel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/elm.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/elpi.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/email.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/erlang.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/esoteric.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ezhil.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/factor.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/fantom.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/felix.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/fift.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/floscript.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/forth.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/fortran.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/foxpro.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/freefem.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/func.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/functional.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/futhark.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/gcodelexer.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/gdscript.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/gleam.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/go.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/grammar_notation.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/graph.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/graphics.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/graphql.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/graphviz.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/gsql.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/hare.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/haskell.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/haxe.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/hdl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/hexdump.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/html.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/idl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/igor.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/inferno.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/installers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/int_fiction.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/iolang.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/j.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/javascript.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/jmespath.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/jslt.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/json5.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/jsonnet.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/jsx.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/julia.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/jvm.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/kuin.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/kusto.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ldap.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/lean.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/lilypond.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/lisp.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/macaulay2.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/make.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/maple.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/markup.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/math.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/matlab.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/maxima.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/meson.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/mime.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/minecraft.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/mips.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ml.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/modeling.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/modula2.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/mojo.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/monte.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/mosel.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ncl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/nimrod.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/nit.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/nix.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/numbair.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/oberon.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/objective.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ooc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/openscad.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/other.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/parasail.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/parsers.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/pascal.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/pawn.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/pddl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/perl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/phix.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/php.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/pointless.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/pony.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/praat.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/procfile.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/prolog.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/promql.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/prql.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ptx.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/purescript.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/python.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/q.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/qlik.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/qvt.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/r.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/rdf.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/rebol.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/rego.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/rell.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/resource.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ride.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/rita.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/rnc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/roboconf.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/robotframework.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ruby.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/rust.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/sas.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/savi.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/scdoc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/scripting.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/sgf.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/shell.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/sieve.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/slash.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/smalltalk.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/smithy.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/smv.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/snobol.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/solidity.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/soong.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/sophia.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/special.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/spice.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/sql.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/srcinfo.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/stata.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/supercollider.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/tablegen.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/tact.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/tal.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/tcl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/teal.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/templates.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/teraterm.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/testing.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/text.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/textedit.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/textfmts.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/theorem.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/thingsdb.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/tlb.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/tls.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/tnt.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/trafficscript.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/typoscript.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/typst.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/ul4.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/unicon.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/urbi.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/usd.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/varnish.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/verification.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/verifpal.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/vip.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/vyper.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/web.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/webassembly.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/webidl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/webmisc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/wgsl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/whiley.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/wowtoc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/wren.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/x10.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/xorg.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/yang.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/yara.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/lexers/zig.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/modeline.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/plugin.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/regexopt.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/scanner.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/sphinxext.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/style.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/_mapping.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/abap.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/algol.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/algol_nu.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/arduino.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/autumn.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/borland.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/bw.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/coffee.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/colorful.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/default.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/dracula.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/emacs.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/friendly.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/friendly_grayscale.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/fruity.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/gh_dark.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/gruvbox.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/igor.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/inkpot.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/lightbulb.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/lilypond.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/lovelace.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/manni.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/material.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/monokai.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/murphy.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/native.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/night_owl.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/nord.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/onedark.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/paraiso_dark.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/paraiso_light.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/pastie.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/perldoc.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/rainbow_dash.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/rrt.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/sas.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/solarized.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/staroffice.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/stata_dark.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/stata_light.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/tango.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/trac.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/vim.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/vs.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/xcode.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/styles/zenburn.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/token.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/unistring.py create mode 100644 .venv-test/lib/python3.12/site-packages/pygments/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/licenses/AUTHORS.rst create mode 100644 .venv-test/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/pyjwt-2.13.0.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pytest-9.1.1.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pytest-9.1.1.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pytest-9.1.1.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pytest-9.1.1.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/pytest-9.1.1.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pytest-9.1.1.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pytest-9.1.1.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/pytest-9.1.1.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pytest/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pytest/__main__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pytest/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio-1.4.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio-1.4.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio-1.4.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio-1.4.0.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio-1.4.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio-1.4.0.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio-1.4.0.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio-1.4.0.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio/plugin.py create mode 100644 .venv-test/lib/python3.12/site-packages/pytest_asyncio/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/python_jose-3.5.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/python_jose-3.5.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/python_jose-3.5.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/python_jose-3.5.0.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/python_jose-3.5.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/python_jose-3.5.0.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/python_jose-3.5.0.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart-0.0.32.dist-info/licenses/LICENSE.txt create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart/decoders.py create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart/multipart.py create mode 100644 .venv-test/lib/python3.12/site-packages/python_multipart/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/rsa-4.9.1.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/rsa-4.9.1.dist-info/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/rsa-4.9.1.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/rsa-4.9.1.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/rsa-4.9.1.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/rsa-4.9.1.dist-info/entry_points.txt create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/asn1.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/cli.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/common.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/core.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/key.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/parallel.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/pem.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/pkcs1.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/pkcs1_v2.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/prime.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/randnum.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/transform.py create mode 100644 .venv-test/lib/python3.12/site-packages/rsa/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/six-1.17.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/six-1.17.0.dist-info/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/six-1.17.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/six-1.17.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/six-1.17.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/six-1.17.0.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/six.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy-2.0.52.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy-2.0.52.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy-2.0.52.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy-2.0.52.dist-info/REQUESTED create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy-2.0.52.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy-2.0.52.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy-2.0.52.dist-info/top_level.txt create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/connectors/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/connectors/aioodbc.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/connectors/asyncio.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/connectors/pyodbc.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/__init__.py create mode 100755 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/collections.cpython-312-darwin.so create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/collections.pyx create mode 100755 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/immutabledict.cpython-312-darwin.so create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/immutabledict.pxd create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/immutabledict.pyx create mode 100755 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/processors.cpython-312-darwin.so create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/processors.pyx create mode 100755 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/resultproxy.cpython-312-darwin.so create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/resultproxy.pyx create mode 100755 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/util.cpython-312-darwin.so create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/cyextension/util.pyx create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/_typing.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mssql/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mssql/aioodbc.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mssql/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mssql/information_schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mssql/json.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mssql/provision.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mssql/pymssql.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mssql/pyodbc.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/aiomysql.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/asyncmy.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/cymysql.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/dml.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/enumerated.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/expression.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/json.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/mariadb.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/mariadbconnector.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/mysqlconnector.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/mysqldb.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/provision.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/pymysql.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/pyodbc.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/reflection.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/reserved_words.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/mysql/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/oracle/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/oracle/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/oracle/cx_oracle.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/oracle/dictionary.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/oracle/oracledb.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/oracle/provision.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/oracle/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/oracle/vector.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/_psycopg_common.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/array.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/dml.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/ext.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/hstore.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/json.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/named_types.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/operators.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/pg8000.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/pg_catalog.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/provision.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/psycopg.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/psycopg2.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/psycopg2cffi.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/ranges.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/postgresql/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/sqlite/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/sqlite/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/sqlite/dml.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/sqlite/json.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/sqlite/provision.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/sqlite/pysqlcipher.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/dialects/type_migration_guidelines.txt create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/_py_processors.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/_py_row.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/_py_util.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/characteristics.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/create.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/cursor.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/default.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/events.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/interfaces.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/mock.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/processors.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/reflection.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/result.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/row.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/strategies.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/url.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/engine/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/event/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/event/api.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/event/attr.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/event/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/event/legacy.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/event/registry.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/events.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/exc.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/associationproxy.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/engine.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/exc.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/result.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/scoping.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/session.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/automap.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/baked.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/compiler.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/declarative/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/declarative/extensions.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/horizontal_shard.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/hybrid.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/indexable.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/instrumentation.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/mutable.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/mypy/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/mypy/apply.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/mypy/decl_class.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/mypy/infer.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/mypy/names.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/mypy/plugin.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/mypy/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/orderinglist.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/ext/serializer.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/future/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/future/engine.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/inspection.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/log.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/_orm_constructors.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/_typing.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/attributes.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/bulk_persistence.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/clsregistry.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/collections.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/context.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/decl_api.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/decl_base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/dependency.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/descriptor_props.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/dynamic.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/evaluator.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/events.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/exc.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/identity.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/instrumentation.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/interfaces.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/loading.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/mapped_collection.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/mapper.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/path_registry.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/persistence.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/properties.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/query.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/relationships.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/scoping.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/session.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/state.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/state_changes.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/strategies.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/strategy_options.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/sync.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/unitofwork.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/orm/writeonly.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/pool/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/pool/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/pool/events.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/pool/impl.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/_dml_constructors.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/_elements_constructors.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/_orm_types.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/_py_util.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/_selectable_constructors.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/_typing.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/annotation.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/cache_key.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/coercions.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/compiler.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/crud.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/default_comparator.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/dml.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/elements.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/events.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/expression.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/functions.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/lambdas.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/naming.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/operators.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/roles.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/selectable.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/sqltypes.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/traversals.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/type_api.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/sql/visitors.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/assertions.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/assertsql.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/asyncio.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/config.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/engines.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/entities.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/exclusions.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/fixtures/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/fixtures/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/fixtures/mypy.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/fixtures/orm.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/fixtures/sql.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/pickleable.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/plugin/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/plugin/bootstrap.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/plugin/plugin_base.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/plugin/pytestplugin.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/profiling.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/provision.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/requirements.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/schema.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_cte.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_ddl.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_deprecations.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_dialect.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_insert.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_reflection.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_results.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_rowcount.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_select.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_sequence.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_types.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_unicode_ddl.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/suite/test_update_delete.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/util.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/testing/warnings.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/_collections.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/_concurrency_py3k.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/_has_cy.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/_py_collections.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/compat.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/concurrency.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/deprecations.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/langhelpers.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/preloaded.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/queue.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/tool_support.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/topological.py create mode 100644 .venv-test/lib/python3.12/site-packages/sqlalchemy/util/typing.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette-1.6.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/starlette-1.6.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/starlette-1.6.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/starlette-1.6.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/starlette-1.6.0.dist-info/licenses/LICENSE.md create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/_exception_handler.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/_utils.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/applications.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/authentication.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/background.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/concurrency.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/config.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/convertors.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/datastructures.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/endpoints.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/formparsers.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/authentication.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/base.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/body_limit.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/cors.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/errors.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/exceptions.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/gzip.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/httpsredirect.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/sessions.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/trustedhost.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/middleware/wsgi.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/requests.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/responses.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/routing.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/schemas.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/staticfiles.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/status.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/templating.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/testclient.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/types.py create mode 100644 .venv-test/lib/python3.12/site-packages/starlette/websockets.py create mode 100644 .venv-test/lib/python3.12/site-packages/typing_extensions-4.16.0.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/typing_extensions-4.16.0.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/typing_extensions-4.16.0.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/typing_extensions-4.16.0.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/typing_extensions-4.16.0.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/typing_extensions.py create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection-0.4.4.dist-info/INSTALLER create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection-0.4.4.dist-info/METADATA create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection-0.4.4.dist-info/RECORD create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection-0.4.4.dist-info/WHEEL create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection-0.4.4.dist-info/licenses/LICENSE create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection/__init__.py create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection/introspection.py create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection/py.typed create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection/typing_objects.py create mode 100644 .venv-test/lib/python3.12/site-packages/typing_inspection/typing_objects.pyi create mode 100644 .venv-test/pyvenv.cfg diff --git a/.venv-test/bin/Activate.ps1 b/.venv-test/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/.venv-test/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/.venv-test/bin/activate b/.venv-test/bin/activate new file mode 100644 index 0000000..a30a9b4 --- /dev/null +++ b/.venv-test/bin/activate @@ -0,0 +1,71 @@ +# This file must be used with "source bin/activate" *from bash* +# You cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # Call hash to forget past locations. Without forgetting + # past locations the $PATH changes we made may not be respected. + # See "man bash" for more details. hash is usually a builtin of your shell + hash -r 2> /dev/null + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +# on Windows, a path can contain colons and backslashes and has to be converted: +if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then + # transform D:\path\to\venv to /d/path/to/venv on MSYS + # and to /cygdrive/d/path/to/venv on Cygwin + export VIRTUAL_ENV=$(cygpath /private/tmp/vector-ro/.venv-test) +else + # use the path as-is + export VIRTUAL_ENV=/private/tmp/vector-ro/.venv-test +fi + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/"bin":$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1='(.venv-test) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(.venv-test) ' + export VIRTUAL_ENV_PROMPT +fi + +# Call hash to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +hash -r 2> /dev/null diff --git a/.venv-test/bin/activate.csh b/.venv-test/bin/activate.csh new file mode 100644 index 0000000..2bf21f8 --- /dev/null +++ b/.venv-test/bin/activate.csh @@ -0,0 +1,27 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. + +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV /private/tmp/vector-ro/.venv-test + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(.venv-test) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(.venv-test) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/.venv-test/bin/activate.fish b/.venv-test/bin/activate.fish new file mode 100644 index 0000000..9b3e07c --- /dev/null +++ b/.venv-test/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/). You cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV /private/tmp/vector-ro/.venv-test + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/"bin $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) '(.venv-test) ' (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT '(.venv-test) ' +end diff --git a/.venv-test/bin/cffi-gen-src b/.venv-test/bin/cffi-gen-src new file mode 100755 index 0000000..941577b --- /dev/null +++ b/.venv-test/bin/cffi-gen-src @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from cffi._cffi_gen_src import run +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(run()) diff --git a/.venv-test/bin/fastapi b/.venv-test/bin/fastapi new file mode 100755 index 0000000..cdcd356 --- /dev/null +++ b/.venv-test/bin/fastapi @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from fastapi.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-test/bin/httpx b/.venv-test/bin/httpx new file mode 100755 index 0000000..b8c7c18 --- /dev/null +++ b/.venv-test/bin/httpx @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from httpx import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-test/bin/idna b/.venv-test/bin/idna new file mode 100755 index 0000000..ecfbffb --- /dev/null +++ b/.venv-test/bin/idna @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from idna.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-test/bin/pip b/.venv-test/bin/pip new file mode 100755 index 0000000..1792efd --- /dev/null +++ b/.venv-test/bin/pip @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-test/bin/pip3 b/.venv-test/bin/pip3 new file mode 100755 index 0000000..1792efd --- /dev/null +++ b/.venv-test/bin/pip3 @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-test/bin/pip3.12 b/.venv-test/bin/pip3.12 new file mode 100755 index 0000000..1792efd --- /dev/null +++ b/.venv-test/bin/pip3.12 @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-test/bin/py.test b/.venv-test/bin/py.test new file mode 100755 index 0000000..6d87de4 --- /dev/null +++ b/.venv-test/bin/py.test @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from _pytest.config import _console_main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(_console_main()) diff --git a/.venv-test/bin/pygmentize b/.venv-test/bin/pygmentize new file mode 100755 index 0000000..221a85b --- /dev/null +++ b/.venv-test/bin/pygmentize @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pygments.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/.venv-test/bin/pyrsa-decrypt b/.venv-test/bin/pyrsa-decrypt new file mode 100755 index 0000000..277f732 --- /dev/null +++ b/.venv-test/bin/pyrsa-decrypt @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import decrypt +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(decrypt()) diff --git a/.venv-test/bin/pyrsa-encrypt b/.venv-test/bin/pyrsa-encrypt new file mode 100755 index 0000000..21958a3 --- /dev/null +++ b/.venv-test/bin/pyrsa-encrypt @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import encrypt +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(encrypt()) diff --git a/.venv-test/bin/pyrsa-keygen b/.venv-test/bin/pyrsa-keygen new file mode 100755 index 0000000..3f4ef2e --- /dev/null +++ b/.venv-test/bin/pyrsa-keygen @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import keygen +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(keygen()) diff --git a/.venv-test/bin/pyrsa-priv2pub b/.venv-test/bin/pyrsa-priv2pub new file mode 100755 index 0000000..695551d --- /dev/null +++ b/.venv-test/bin/pyrsa-priv2pub @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.util import private_to_public +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(private_to_public()) diff --git a/.venv-test/bin/pyrsa-sign b/.venv-test/bin/pyrsa-sign new file mode 100755 index 0000000..53e28bd --- /dev/null +++ b/.venv-test/bin/pyrsa-sign @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import sign +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(sign()) diff --git a/.venv-test/bin/pyrsa-verify b/.venv-test/bin/pyrsa-verify new file mode 100755 index 0000000..442233d --- /dev/null +++ b/.venv-test/bin/pyrsa-verify @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import verify +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(verify()) diff --git a/.venv-test/bin/pytest b/.venv-test/bin/pytest new file mode 100755 index 0000000..6d87de4 --- /dev/null +++ b/.venv-test/bin/pytest @@ -0,0 +1,8 @@ +#!/private/tmp/vector-ro/.venv-test/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from _pytest.config import _console_main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(_console_main()) diff --git a/.venv-test/bin/python b/.venv-test/bin/python new file mode 120000 index 0000000..11b9d88 --- /dev/null +++ b/.venv-test/bin/python @@ -0,0 +1 @@ +python3.12 \ No newline at end of file diff --git a/.venv-test/bin/python3 b/.venv-test/bin/python3 new file mode 120000 index 0000000..11b9d88 --- /dev/null +++ b/.venv-test/bin/python3 @@ -0,0 +1 @@ +python3.12 \ No newline at end of file diff --git a/.venv-test/bin/python3.12 b/.venv-test/bin/python3.12 new file mode 120000 index 0000000..a7a5fcc --- /dev/null +++ b/.venv-test/bin/python3.12 @@ -0,0 +1 @@ +/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12 \ No newline at end of file diff --git a/.venv-test/lib/python3.12/site-packages/_cffi_backend.cpython-312-darwin.so b/.venv-test/lib/python3.12/site-packages/_cffi_backend.cpython-312-darwin.so new file mode 100755 index 0000000000000000000000000000000000000000..5db157e2e2bb2f7cdc5053aeda9aef5337ef9ed6 GIT binary patch literal 229552 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
eLQ+QD`Kve1pPz@Z%=Z|0b|Ld{DzWKW#+AZ<64E z|AnFS)mQBBsh_S+z}kNF%yAMt^cbZxMw5dpKW~hr>;}<&gZReb!&tG%I@UIex!=U& z!!Ma0ec8?ayyyDzQvYzXMElKI13P|S{CC_pKFT^LYu-uy&RSXCGgv=~ueUFKk6XVh z_Ht#-DtA-~eEgEJ_t2lvnmv3o9G3faxVx(!|6Diw+r)r9_bhZ;jGse%e8XAS%ect8 zGufBq_|9b=YTI4zutJB4?k&e=3gRFAgf;S#bIV(teYN=ys;%4$v$g7UL(gICsKtio zInF&+t|#tL>xFCj*5U``97|pw|MvSQopuiq+obDQtVQr6i$70vV+h=fSr__)?RDKc z7RPKz#rtmNj1Ri=t4C+#6-KdN(63?oUj{EpjD+;xV+`JZ$xDSVvMtrtxm2-tk+452 zTpfX8*8$>)-w9N>JN2E%^kt_>{|&?5tC>mPoA9&iboz)r3;k_>hGXkgW5cDU)sjYR zkd>OwI#uH1d&QS2cJX=ak(k7UDdI{}%L?4HwH@8Jn!YjL>HGZ^)>CIGD`&9I_M0-|`n5-+YogK%rZ1cs%10cP(*2 zI}&(+1K&B}VdP7^iz43{^L#z#x#2nF#mE!8yPS84!4MctCh{K(U9he`! z6xhwsJdzeIxe%V-H!D)AUFLezw||SzFu^T5+;P(0qZGeA=f&QT zv$k@cRPd@uoTF@pce`jq=yx4`5%^M0$`GerLLBF=4&FNw@ns#9uhHf6G)=~kA4$rO z^yQBUUje*q@@tdwYy0xE!_&zBz3oTui#@9+Uu+b35}ULC4B8pL6T^6&LwzZC99lK8 zMpWGC%orT-ai_DFJmJg1e&Ll$(CZNS?~m-ym%cMsMYisgb+3ZY zH14kqwRT@=`wgelw)lPinQ!{}Mk+m=b9vI{+46)2r|vO`&l&tTpH8{cp+#Ca3QbNh zH)Dyp>K>(HY$jM+Z{r*jzOQP%u9Q0v$Au?=yU3CPV_d`iL(r3M&c1Aij~O?KXOI{) z8FQ0-8E+>)M1Hfx+%dQ7U$31}*kbedNpz~o`NRshD%Eh2kpfrlZaSB?#&N#rUnnE7 zO#HC06kIiYnq-k&7m4kq%-Bq~2KCsH2`< z`(rDfB6jY$@F{uYh(&$iRk`PIT#px>rN^j$KLuaqQ0pdpOh$i?{)*o~_ZOJbU%ht6 z2XWglHH}HVtAlonGgWEFFCJM~>{fNU{o!`|vVSh;JU*a51@!Y)cx{hfOG-Zr!g~|_ z9BULb2)+^nSAcI|4|r8@c5p0osZU?q50`tLrCc3%X1&!SQ&j#6{TY1$z9PHbbmwPO)B>b9b4*w(*oXlOKMv56^rXPs$K$*vovW{J}p2 zqx+z18T8!Fow4=s(mHTPZ`-s;;CzBQLW>q)`{7XN92*lhjKz+DbtiFbK+BC z$jKL*$j^Ogi^-REYrs|THaS1oMtj9F1_}O>J1K={qz}n(^*iGtZS>J5F{Ur;*wCEt z!?bIvG4tUo9kYECuvfdrG>Bf1{q38`XRa=kF*c3dhFaQ@a|Sw1&k53ipE_UVH?U6K z!&-n?La|ZKb{59A7ge|fUHbON555^Wq^#UD_HE()W|#S#v?n^lV~lB7%)3e3LK`_h zAno?kihRCnGgR!p#NF{-%>N>AzaAU0dW4F7Zw`H-f8dTR9(doTbGPY@%qzWr&Hdw3 zXp{2@2fI15FxT*`_fO!wLhpkcTMB3w`Ido?N7i)ap7mP-;PYoEjU*UThr`BkES7f4{->mm( z2k-mm(M8TpeqNk!v-mcjFm2yvjcHZkx15$$zr{~nA;0~xyc)WAiHF!g|4ki#Xf6qn z#@OzZ-&A^(^rzVykUKtf8g}(+(u%#ZHsH)1GHsvxqJSr3MgSTgn73}U#TkGDOSu=U z%*b25*jT^iFtFxbNh|zPxl}n zRc|qH5-}C*eVTgBD?VPa!8-I^nqvDI;g=%u&3YZcxxM1bU>kfIC?>{8Y(p>O_@~)g zSN1Mt{Vz6KCOj~VKZ$#L{b_qWEVjS+#4ccr_8XZEJGLup1M_nm_tr{`;SO+np}f8B zn?s@{KOvssZ>-&3`0TgFgD_$5nDgP{yjo^WBkIvpTYmv$mxjaVCbNzU(DTS zx_^HeZk zWnARxKPMQw{K0WseR`e8-JsA}{Ch6)E@!_~=IBarZ8{lmU4@*Me)k}&r?|&8teRx| z|9a2^(yr{6)N)>FDlzz>5$1vq@K7OP43+;+YvJGi6BqJx3G$McB#|O9J$)a z+@Cozvtb42qu`I;J>Qk^`;z*VPQPXg{g1_4GoaNb?s3`X%GUUZ4@LSYGEL~*iL4P@$|M#o0<8-lD2*0R zp3`C~=srr-H6tTAyIz-Xs@P-5BMx?vgN&3p7UIleg}eV;1ol_k{dX8GD`%;??|ezs-M3MdHYdf& zG!rM&32%<2zSx|9n`OrscO}j;$zAU6k*3dVkY=hg??mTf_D!|Wz&U>+Ith7Qna9}} zf%!-3H$y*(KNEXV&mZx&NQ%iiyD(3!+`Q*n>@pvq?XsEydZS|0F+z?gPI@6CZ$s zFMaMiXJRVVCN!u|thM-#iH&@mx-wrJn5%(V%{d*JzrU1s+8vB`5$aXr3qs!OSkLCf znoQu9i*As7_JYoq{|@=JU()wkmsDfxO^|Qw6l1%sM;#x4{9cDw6~2}1$2F0C5$N6| z`sahG)}z!-_K&90(!P_>zC%SJ3&f6)Siow2*8&*RlG_rw)CArk`sI{S+|?wyIJI1I zOq9eJ)B=|~YPib=UmbKt$M?!yK`v@NBC=--xXRd@=o!XqS0QT~DZdW-Ipx8r(r&Lm zK1k{|P&YMwu(&^&V={gZGJe8Go$!r`KhT&wJGPp4*3q1Y9INe_c6@q!zBen14bOTd zC;XcPPYpKaOKHEI`ErrqkvJ14eUW{pD(W@MT;gmM_GoW2bIFH3lQpfZ=Wo;f9ut;3 zXNFK#+FZ@Lz@8`lemf_wuohU(I=>cJHNsQ0H4ixb^XtrdCN>LtZKwaKi!6w+b6(p; zVrw)m<@aQt=edFVD~R)N+`)YoFOdJ+gBMvJt>(@K-bWq32sUX>?2*6B z!rq2okwrNkbW~aT_~l-aB{D|vjE6bOxL>@NG!JuBe51mH&VCs@(r6M#rQi|xXs56K zeCyqE%?IdLd(TYn6q4V+IcuF$=Ucx?siV-1L1LswgI6bQs75`{wY+elqfBrSJY>y2 znBPL$lzW-0!CUyvtmRw+K7^ydx_2XU!I9PEGk(Y(ZGQ~x%Uxfvehgmy<2lgB;;cXF z?{Mxy&k0}k*ZAhpXZrU=-}nV}g}=Sn*2m#p@lPoDS@z6-z}W#T=vauk+=U)Iw1&Ov zMasH`I-SJ4%2^`kj%Yb|7UBN?AandH@Mb6T*!J7myxmNlX85r;@Pj^F!Vu)YcoGN+v} zSI7?+ws$!=7c+)VU4d0$$1M(|^(_}zR}TWiNBvxI2-$L6kN+a(n$r&B*4Fp__wekv zz`8W04fcEG%vPiSF@DFB=e(zc?($nt+yRi#PqS%Hcum?$=KZtwWyXcp*pzmJ9tQL| zMEZZHFUj;WjTwOmxDA+R-@VXMz!Kdku!T)a}2<-PEH zF6$=Ivy9`WqxkCSt8K@uoNiT;RxkZc*l6|Wy)MSa9^;&F1NkCLw48%~rc+O}{5%GVS!fn9Q9{Or~ANj>%MvvEaT3zF$ngTCi~<;4gNL_c@>V>!Mt*<<=u8RJHphE09= z&i1cwa$EmJUIsj+VP8K8>;o?ARm$|=i}K|&#=L9V-@hwT=#-NszlCC8j~U@-856G& zeW^!u7CbAqZgTz)$;Zd~@@Dc*@f(akK#$6uWyQIS34Vb>HNJ;f@ycfILX86Z#WiE1 z+;I(0-V#-mxkl=9ZNnWK}A(!Oa?$>1>$2YeZMFS3;UEEPxwB?I_Y?MT zSJwK>;Ko(*dz;7u@qKNeZ!a@8M+-PF3p|m_3b~*<1Kjf?{9N{yz{*3PF9miUdR*?o zHLzvnURjrsjZfO9&3in*PnjD2sLQ<=tmC1<1;E|T??T8~b@rc^1HZD`PC9OWMA|Pct@-GVPpxo=EYkcw3eBd6o|FONwdI3HX9savq#Tj08 zkXRDyxBP80GJBffm7lDxiXI^*brg31JcDleolzM53AA+5UX`@fq^0+#y{F5mnE3Ua za&Bn#yL8gTrhJQci;7^*E5r-wN+ zsMAyMy4^`<&6$kbEGIn$zal3+1xJ^Yo`UBgaMJCj;9A5!ilobLK*(>fW}t)b!nfDJ zp2Jf7MH`Hqh7H?o|M}gCdxFH~c#M04UVv7=mAm&@bG!hIC@{8h&(C3K!krLSCVrx& z{;Oi_zi(W}9hk%T-he(L-eTttbkPW&za_S1Bx~A_h-(?e_bBcs5Iz0V`=`jAMApT` zNR{Ew58~TVbxWdI=(RG}u%2^9Uvu)D(F^#ymcQ#xifxgFy!UeEw9r(`1xIfY_UbUz zd-2|eE!BSvo?K1ZdB6^0cU+82^s>%T?5jS_dH{Xa_8$0$&{5BRQEeiAMz4MCd*Q~1 zS4GjQ$ji>R;;rJ_n*to+XW?b}9g222Uz4;BlY5uYduKChc0@jjE_eq!HpeY?QGWQ}$d57yH*p7&%+r72 zTkMQo%=ssN9cM2`fiWTe9X7VKihB4i7#Gc zNiH;(yT@ylx1^PR$-DloE@VJ6YrkgVSGCQDU847}NuNc(TIqXhhDX~|;?IAQcDb9h zRpw1IV9%oMXU7U8<-f zXBx1pmhM5tOhZ{j(06 zJ%jl$Z;ySJw~2An-^>?@Su&ubz@5UK0!x%C*@RuI+aacG-w-q4Vb0~y=jqs$uR#lm z{mSzp?}vmI-Ux2gZv2OBf5WM6XV*~Lpb0_cH;x(qOFLulC$e-EgT{CrE>a^=R8>qt_ zao0<%?Q@CxvLE0#&R_3kZy}-vhw~J027}*7I(*QWh)-#h_>`55olaYOp7QV+ji(B_ZiCEtIW_Kg-;$fo6l@B|OkSpY1bkKZd`XI4j_Xj;ZsR zyCL*^7I^l2&I$kF3V0CR^AdXrUGSvD&*=SN=5_{ge(U*tQw8k?Yk&Fi#Qa^5sqgv} zo)DY(QJIq`<0Z)9zBw##o&1L+KNjbAdR6#s=JE;Vawj};09<4)?-!ha^R~=Y=0}7) zv7=Mx^6R=>-k`EYXwxC`KiLo1nPI&{9p?Y&!TKY0bnN0;`uov`a?axO<8hpEmNk&c z{F8N}+=(;={aJuq`7-wg5qsS;miJQb4Vsg6>&blH^Ji!9e#=Q>k{VBNZ_pg>5W?rY z^DXr9oUEHpRwEaXEequ?G-UL$!(+A;G(eN3+kzSAMsV|-+ z@R5`Fd3(xvpGdu0WR{a(gk4h4w^K&uyxf%(($D;!pni_&QTmzJBfMXqfY*))zAE%PUH>#=$)7SC zj+8bW8%A&v8%Ag>b8|Xy#J;Gd40n6QSc-ij_@1xJMC2c*k*$++FkJ8`@N&O>RR1aSlV@cHkf|9xjz zu@6|A+I>GT%xX>a-PyMSj@Gr>XCiZ{=~C-Mks)!;!6)W}=q zGFW_ZCibHIX50B9o6#jr$lF4(6^ZjM=8UtmK5+IoJlyZPnEPD?@6D0P? zeJsZoWvm6aJ+$eitXf$!Lj1&DqkX2-lly^HTK?DMUGdw%PWvH|1H>w?<|($|C(z$c z^V?~!`K=>PTICcw?fEHI4{51#N&A!W-}u#9)=b4qM1E=>2 zm2(%EGD3C9@+oo|@|C-uM4!n1Xy8BaiOJp+Jddn9;71N=`N1)ABXyv8P z9Us3$OrV zt*_4m<;oJHG2mx@%UCODEMs5J6FTd$bL52&{75_Pd{1!jO~P%4;5J+KxefMAT{cnBS~ir?+- z1W$PS0IT~x+u>pO&Q!A3dghu?##rtklJV2Dy2d&G;|kA&KBD7TTm4kzTRk?A$i%t) z#?ZPnZ6EaCagj{hf1hso;qT+fRuA$%;0wk!vxl|`JG)b40_*y}0=peOUCTY_&HS#A zdHtBJ`>+$7wf#Qqggmd&A~7sy!@H>zo^>gGUtAvgW*_jSt+!9bU)skVg&yt&*eCNE z-2xx?*>8IMzg1$bmHMCK;|QQ*lW}eXXVz=2XT!&_ouu7lempA@|ro|GXH_n-0gv99pFChh>Ks0-N>At|#E9JmkqcMY3KYHsE*V z|C_jTkB_Q4_x_$Fz)S*2ZUn+5GvFl&-cTUWVlx3R0hE@AT5Wqsz_ydb(uS%P5gEYC zK)}ORTCMgRgH}5;N=sX)rtLWf=_QC-z+11!1Z+(}TR_l+s(HV^wPz;p?4+xWkYr|L!CH#ExX4`(|~-pkX+!#2+% z9xi3XG@aQ9X#7vln$b2Sp&yHYRd`^WJCT<_VX(6izCc!|Xq+M4FP|7s6LZi!V3)nS zh%%Mrxsr~M4$H5@c2sOT^Q4IZrA#oj^mWO({CFCNzvN?seX*z3lR^=)`kG9sVr2d@NU z7Jkls8v0p#dGGQYS=ioo?Z`)}lXNrt>+Gws*UB9{1y^$yH}Our<(PJ`p)N!3 zT*|yRo7~B>y5m#VXk5&Rz2`&a&DXeIg9mqf3i^O|+2@Y{hhWz{#Mn-mhkU1-hdz&g z2wEq6)tp&&eATRlHfb%SbIP)5-vb{F;>F}S?43*Osni`p-|ST<^kdKb3H)ZgdogE> zeaL(f=VVu<;OAi60rd%P@lQMcfAoVFJX>dFKaxCmxNWHK)x!H-GwqWledi>%oWb#e ze&(_}K4b2LkpEdQ)UsdXj_17}IC1=@{h6iqZ%@;{XflwUqWmA?B|qz;bZ{a%i5}9W zEsS*#@3ek4@1Ri|Yi|RuEm^=PfgUuxKEsho15eYZ&AEaf0#geyb#2IGa-G$*)2Gg> zpR#ko?-td8E^YLkT&ES?-KWl#pJp#v_Ib`>tvCC8pFY$6UO#y0ScF}epmT%tJ?oEZ z{37kC{%*Tc^PZ0wOy+EdhVYztR%cva<-DZtO!Ce$AH<-|o6tt<)a3r(Cs(P1i(#)lCuiA@(jDe?0LvOXkk2>$D7F0r+HfT?hnyVmFUnW&ht4oN00x*jnCB{ z-8$m8cUyjwo0s`xVQd0(ENh-B{;x!D2Q3R<%0tttpv#@)R|ir)k)J!}8rQ z*T{b^e_=CoO#@|rmQE}icV1}UMDz4C>WY5udQq|bvXP?gp9!yC@p zcEGZJul?`3xnsZ|v}sm&C}ZTD z*Hqv#acT)T5@Xjo^N?V~kE(KJ%pG0R6uT^Z%08^7O^v1Z*i}BvH$&e!jtrhnTLZy% z9cBBYx!X>E-&{U-$Fz%aR3>6_zgZ&Rpu^+IGL+4H7)rz!d=ves|4r;VI11IE7tu3L zUyeLoFpWK+?wIrJI2$JqHoz~H%$MCVH`R&V=>82~C$A^v-zX$*FzOkhd8nJ;3zO}a zw}#m-0>9fg^^Lt^d}i`(l;O`}E|QN!ek3d5*Yn`VQ@<*|L2%&XOMZ7R@51sgIR3i5 z0sM7c&_VWpJF+aFgywtkz4P12t7`sd{pI>ATwELYtGadSk-#0?r`-*A;tT@gb?$8( zyDZ4w1~O*!!)$OYnfIc9^mN--et7;p2-1` z6W^~s(Y4Z7?!FXocJ2gM5$LHkQVTp_a`DZ!>}iy5U0W7Az!^bo+#^f2kFeiDP6weQ zh%XvY4sW5YEva|L4zSiQp^aCNr%;lv3!zUMz>&LFNIt{%73%hWKhS#UJC2)>jz~!I*r6t32t4&AY^&ll-TLk*!0(S#5=u+4gq~etZ18y>TYHcsDXzl<=Wd0;llv`46!vp>GHr`M@bYwUT6CTHza~`qD;z zh#iKF4?eqX7w~zC9i{pAn^SsxI-L30?&@bZPQ5pLA5dN}C`Q7|o>^#6(4j4BfpX;6 z=aF6CbG~m2^ix05V;Z~W)V(}`B_{!2AD%$Z8=lxpU;no}A>B0)PZ%16ava}%Q;vNL zWwnQ!d{>^!x8j+0#^*~5mek;{I}AV7!cU8cr8WPOvr`gPxUSNvQm)FjeL2r!m)VBOaPD=uGDk>-V|#NqtMw0TJmpqXV4vfxCa}@^ub$>x#{4KI2iopGDi@TjX!D*5$KjUURB=NMy<}W5z*GEjX(O;a$)0t0T=~TW#7xp*JxOPSbgW5`#8__D8Ikb>6;{SZn zL~@L*>_#8zj0kJtgdOXRqf0j{uEx7X{-zu=PSK>~Vn;4qJ1hg8t9{9jRuF5*+-PD4 z;UOP=6=4^wLmu@kLTBk7*2DM;+F0L%%Olp_JX^9ayVi}B0A~*0JT%h2sn<6*CPI4h zQ1W~DN#HE`CH?mu8|-5V{$hC-CK#ldbX5MaE9-re*K7j@AM>SI&;T zYVeU8{|(RRY4CX)@I|}vncIy|t=*FC`u!jEJ)3^Yd1{m6x#z5OgWyqdXP8NRW;=c2!bmtX}!I4YHZr?yT@wkP&NOpDSN)z=x@Nkiz zcjSEa5$pcgYbLkK{@y$(J$F}vpKj$D!rpHN?%pu9oM!$NTmj%3NZ-mo^nb$xW<1oP z&+U>?$y>55Pzfxqe7XGMF}&9!cm1EybM`gGH#@bj5e{~KNn|W#JP03x(@l6Vy64dpbf@kVbK8-8syzO}X~4F>zw>A(E>_HsMf>Ky=8B6F zeXpYZ7Wysgr=RDste*L}~o9mnPk-CmGYKwQCU)&kGrM-1{@HJrPVi6=ydZ7TXM z_6yIs8{rL)Rd+S#@zmGZ>JwPbG|z0sfjRa8=N)uj{n3=%!)rJ@)G|phBa6VQ_S@cg zG~c`B(Hq!XASOASzbc(r6L4`cc+x&Ewh1wER-NJ*^{vno$yZ)_!4#z2sw^k(UVCa` zanZ_xtGR32l+is~UVgvAQ}q?Qu~+{SJJI%oNAq8D-_yT*OI>RR;iu2WkDtT(BA4~W zAmYGu&z~o5#?Agi5poq0DqfG3c7xb=TosY|iv_ZwY4^?$>>vZof4jo-^$@HfRR& zeRF{OX83k9zhys6(R=3HdDx$|v@?`48z{4RWyT{aw{URBGcj++Gu4jmvRAeXcPYwt z*;{M&8s?qkUKC>kw*yZNHn8UINl7+vk#q_D9Kv=vquQ}*SG?HYCZp$5tf|Mr`7QV= zko&z>(>PoDzmi;1XEIJh)9a;2=(us2Zdx1UvgWKQy!1FXroTYv1FX+MTv1g8%; z1>;c8`Xu*rQ2j7R<$lf+%VuA{TNb4A@beYqSe??7B?(#48PsGR>p6pZ4g9!}eWk1M zQw%bGjd!V|m>%g}541Jhlfi{S4A*NA-kW(TgUZiQ! zFX(-rtGi|v-nDn`=*?84mnX@efxeD@ykq^NJ) z{_=y9-x>N@_S+Y_c16z}q;CFy#?4szzL&221N96JsvQ0`V_G=IzPNi#ee3m(7n2Vp z;Ro95o-u9*H${oK8uP!=6?0DV|28+S#wp{()igjq#UeaLzYW~wCwm~^4OjdS_?n0# z742Mm#w=IZxdP-gM^~>wtyQ%@$|;B)h%q zR$Rz=6V_U0{K)Q8r|HA>V;!96*aZtjQ`Q!h^m&kcOYZk_-na2vdomuz+&-}^=KUzT zzP7k5rgKe2KUN-=9vx5#zh6kdyFBAsA}AQRdqi~0_2fQvBlyzYmBl>8AJTi(=slhLlHL`e*{m{Re_b&Z+g1-}AD2siI@wKk)u_ymZ-s%SKf^GizXX~F!$vYhT$%EZIacE4B zUfjZXjb6-l^x`^rOFq>t7r1({e^~p_3BEZ_oo41&)wlJ(=J#Iu@i6b7qs(9771{PF zL(A}IYaK`q5AeS7f`RXH{1W4q?+2T}S*Q2%8>RxgWM!c5|5Sa}G);;6$?J#)@DO?| zs2o01EB|G7?AF&Wjoo~{;@hwVsP`?N@}p+)|E=c#k?}M5{}%IqZhR>JA2_|C<}d*X5v=ee0ve*PVb2e?dQm=;KiPP?(yld z=ekN_U0q&cCoHXTH+h${H<+>#`RgoH=kUzSD|y#j15^R-Snh2M;=ybGE%eHjgMu?^%ZA3U^zjS~jn5qx zoKfT9&Ir!8tNdKhDo3%V@OFK^e)LCW4|qFQ z-S72`uFre-j#ngOGu8iWXa5%i8JjIHekM39$RKVHov-|uou&9DMvkr@6R_+*&$8@i z7}E#$1v_=_eZ_3c{vJB$2A;&^9jzp;NBw%q+iZNAtHU$qGKNWa2WQMJ%ZN`}$1}il zBTr98e0{3bS!9#rBDJ7n)k>@G#?kX`S4`h1{DkG=mB~}=$l8Jq#g_>-ty^vo%&AVy z*-`LX$@eWh8;CtrKH@aK|Bdq}-x+UpwgG!&v6CYrG}*z0VjlNE%Ytm{=<_^%jFGkK zem~=ZXZIKK)NeNz>u~<}y?5vR!**KsKgbiZ?bmn48tz^d8-e^x=1)GLxt0ah`S`xj6TccVOSv)zl|$U%4@%U;Q4>(0V-!$UzW>cK- z_f}c9k9-A1&}|OI`Z)dL0{~&u!{;#}N=dZ`67QNxO8W(X#(>!9; zmUyjwtHzQO1lll9?Vsfh-alcywL|xHrSkjxx#{(ny|eQEzorh_b9R$uX9Dlr_lIY^ z{k_mDo!<%0csn>c-uW<3KhJ;XX^oD5K+MR!;ew6_!{a;dveN7&erw-vEDw8ssfU~L zt%Yw`i#p!M4tblgblzjyDDGTu+9>Y4&$LneHt{<5T3H=Kz|p-{W=H82_~DI!Ns|M$_l0jyG7#eX{Y5Wqf1) zhvOSVU-=zx;FtYm;~SqC-?;z9_{P!a*p4^YYxrd2vpVm!3e5O+9wb*G@^BWi`c`V{ zp1GdXJ(Axwf9cx33%LypO{))Z&(mB_O1*Lx+`TD0;|b*U9-}`7$L}%vgQw9SgX7X2 zUq*M_Nc&5uHwzu%|5Mk(Gw%v^+>Osfy62v|cm@X--xJ~K=edEWHMsZ*e#Gt#%;I1ihh z_;3ECnQteN&!-68KUeE8&YgefeEM2SU$gOT zV_#bIwT(WSenw0!@7R|xXK@y5zN8Jki>&d+qOSj%_fdRyKgI40CA|yrPWIBbU-rem z&;HIc=VG62@7nPro{#c7d7sYvkds$Y`-&E8T(d_)IsGr2NcZ8%X0A+0-CCKFvbAz} z_>#~p_7c}bW>&6QH**#7fcU;US5;Wc-90+3yMf2-ajPykJ_<}G#ss+bCzpGf@%DbF zb1xl>24N!(##S7H&Fkf?R*H$KiDC!!+{rr@T6*Huw1JQQ=u6l^H?uy9VC%QbpR}{s ziG#wXGQV~91iyjv&AEHndEb4ui*sJi-NQ~9#|H)PEsW7`x1R{V$~v_ln~Y7I5{t-Y zBBrGc`?$@qKeq*rOt!BAkBXTvF!!waw*{KcwyT6=a1mYm#SYWy|7z=*idQ-H zED1O<_Ho-Ta(QQ9V;#iD@AmckLKD~cCGb$mTC9cjZz=!#6Y+*^1=vf#thGrR{Wf7w zxA0ECQ{a;a(s?)5p?w6pH7C}c5oo5lPkT5w(q72LYuRK6ufv=;jX%7}-C-lmn$_&9 zEGF-Y*RmRSqmRPaYAxJxpf$L1_sR#MeSresm}RT6$@}1@@*KO7dIiJAE(??Ap_I7W zdC0kP&)vGlvUMJ8=RLs=+4KheRz1AgLd+a?+xNc{)om6Fe@3qQzH$h7)R zfevb;T6DZ@oL$3n?lt2W$Czbh>=TLaXVLaI|IFF-_c$*PO|6~JUt*02U*by%SNJ}M z{K3mL#1<;oV><7Ot%hYqp7r_qruXZ)AEC&Tagly&4<&Aum%PAT)rzB8q;&$gEb`=- z@9+uu_n19_y~I8@{lm$xlsN0QN$2xE7LTA8A}P7tpV4zJuNwKzPMpgNL9e!Vd(MHb zez)fw=m*pn?Y;^=ja&fZK;u_AjsIee|M02f_cB&5V_HZ3QpPTP6ft(MXI;MgUUx%y z8N2&C#@Kf=_H`2+zmvx9W$gNXJZbDUV{dueiHp4j-^I7_K|VPtkp}=7OOB0J{8z5U zJ&}wn7yi;-lJu5*ZNKK7mv3$0M(dILY2!7{qqT^R;PNTHhgp|JlJsPRvN;Q#^NpAE zC#ORT?UCT_tF+Z{ple4D|NqbQ$ZBvPzVs3|TFyQ3QR%YD8Ur*$X3@9uqized722Pn z&koIvlE?pY=nK4^RsW^4SKM#Mc#iNrlo-!JWJJ359p1IkMsDDWPWh51Q?A8r_tgu{ ze(yQ7E#0HGy?^d$dkb^C4-J3py)9mi-l~8dyd|Cc*c3STERzq`dn383@zH23Km8qN+;|H92>U3$@?fLd z%AjxVW=W^q)E4eFbNw&cLnxh;=;Iovk95wKXzvu-YzjPk7C7|AUoUp&tyA{~ddGok zpH^@f9Z!r+?3~ZUbl*k$+=3nJ;_2am@RWLL%$;I` zL@({tADS(^82;K8_*I_auNvA3<5O#8kI#%B8RO!U&6V;oa@KG7sJ<~}&KP=P*kp(G zx9=prYNOwF_*phmyK6`6&9|?lUt+UO{`4Gdl7-m71*0uvKfF_f%}n3f_=$`TqHGX< z-)f#3e;e&d?+D&(-nDRNjL~O*&Sy-yabLI0^gYAZpndR7$QwEwdvWps&D-es{`Pd7 zarSMacU+^3 z-Z+hw`n|sk_sO{iTwTzWjbEm~YHa_eqXQ%xr|5vn@s2zMM|8`>L2%asKPZ>IWTFrm zaQWiJ!4A*2^6fxDu<@&m!^N9>b}kub?z>jt5Y4QqSD1R&_fs!3juuiU#bP`>V@*(vA)wp3&G{vLB0y&08ef8W{Y!J$$y9wW-(eSx?`l zzXi^`Z1^{4qWuQ*P+0o**@=p!#lD|tKL$;dW9|Lx(paYu_rGE=zIo+na$<9&;}7jh z=)A|ReU4r)vTK4lM&8%Z&a6M5ZDMh1X{&+zZ;xXKnEvfx=TC{f&>k%B7v92L-XdBB z^Xz3R3;oftee9V~l6`}2BL8krEOeCLW}h%&J7#|_RdKWNUxGu2e|zp{U|eRs;n1b; z`%2aVZ3$k`x%DRGqm|zQ;L89$=>@^B`(-R>u?oL1vfU@{dA`Pykp1MmT5kUa7s+cU z>l{0)o92DtQhddSUr%9QA(j1wG}e%Vpbd0c!8(#LxiTm_coJ*2_Sw*933B)&D|QGw z$lfJu(ZRtPZR`)7!2d1%c_(G0 zXT$hNq^Gc_>#d`2PR~m8;f1%8`%v7PV$0wgQaw2Rd<^c~_m$X4B)mVW`AdDrfWI_G zzBuHihO?U2=MK7>eT(8Y;_geo=Be}I!$!VuKS`Szv?tz}$Fq>R$*mhiZ{wG(P`%K{ zL#DBpQBj)8*$=(TeDq<#Jf`eZ_1`l0PUgku0K3k)SEX9@YVR|J=w{Bw$)+EP?`g5Q z3sGl2^5XfF4e{H@9g->Bt=E*|t8XO-cuwN|EByZlk2N))Jx+Jt)H#u%e7~M|TO>oQ z4OsK;UkV<)q;(GexuJ0)iz=QaOc4UH2$#^&mpAF}I-7yW~eFMMME^^s@UIM;t3{yAx z9lX=1v-F0y9~RD7(~)b#PtFT>%+xbu7TshUeBO*XS7#u%1-2KO9I|`QK`(G;vidxa z{@t>QD{0}kc>GMBE{uPmd>9?4ST5b=QYn7}@>hlI2JXtTi|Ipc1j#%9{&mj%E(fVE z{~WT_JyxC5{k{4o?p-u@{ToqWrmcnQ_gvbNf8c#^(oC5scL&OE)l3=Xj1u2Wq|9d% zeDB(i$>)J&Z~d8m!2gaPX*}d~amPOam}gm;=8P?u{cYwfx4}bH^WjFb_KO9moWiXGC;l42sk|-7t7$u}$ZlioMsL9n;Rf`j zjgF|8JH*UQ+mqLZqVW>wC;oEaICcFO6+I^>*7UT++S8%a^dftXc*DScYMrjNnzPc- z2ZNZi1|$DNkbiuhD~LauonnihO}p6RUTm7S0;@Ccrf}n^8^ho8_|w_J&dS>59bSw4cQ)r- z@ptEozSu1v(03?xbUksZjg061@-w{k7WSER{AIu>{vf8r$e(y4!2I-2>S(O9)CM_e z6t{i+4d>iXCA9i0^`feml*e3tM&V9;GsV z=75h@5T~)$xo;*)8E=)t=jfqLfwW@#Y3hp4lFO;h%lmoXcdgd#C)qX$+t;1zlI*fg zvp!cWx!t=YnAJVs{Tt(N(mFZGE{mi(a;te{F|f^Qxy1hECtWY^>ffH1Tp{q=j8B6& z`{T$_5%_GsfOXtY9a^>T(%mLWw2GYPz#A^=hF3D!gkNI^w(j-_YaR*=t$Fr}!gIP(CESPx$P`pB8QPXG=S@>1#6^T3(+- z%U92{XLQr7Z+VyZ7Xh2&6Y8hTDR&Whl%woO&WtN$^B%E+>2{gwGOkF{91&rT;IO;N zH^-Q|=MKiyn4O4Aw}H!Cc>f^9_}t9d(*pJ&>cJ^z753l49>g@hZ($GOO?MAsqGi7-o?|?1 z=zQX5>t9doLA;sRgK+TV_zx6!rZa?-&US2Q*Kgpy*Bqn1`l~aZI)%=8*&}XD^=@$L z##EdC6EW5Nzt`Q*a>_XSSz)7>o?}nVvisnJ_4v=hhr=t*8AIVP{Yc+D#R)h}AKF*z z3x|_yU=Q%s;sZ$I>}WE4T4$JbK6@p_oHe;=^6tV}e^MgeJ%C7Vh^K)w1|DbF$WAftbI>rB7cmZ@C)WyU+S; zn)TuZW}diO@_YvQcJLqTegxU`?btAd;6B}FWo3AVWi0~NmEhW9ZjZ3n)8@IYBsBD) z63*0R6%FP7?+MOc&Q9!M`Rrx4?>|$%rXG6@m>s_X-?hK^+;7RbL;h*$&j{-Zokcad z1*sRrUh!IvoKGO1tCu^-b=k8VkcJX}GW^U1x|bkc#t@caVFDL8;uEvd#v+Z6av z^vj9A32Y(hRBRgAr_!kb&ibgI3Bse9u0|c}kBqaK=dcmaF0K*L)q%oPbWi zwm%nJEOdL-ZOuMwN`PEJ&!u<{uf{G?K7%>H==C`DEw?`Nt_NHM85cpKIR@^8o{q47 zZgb(j@k0A2z&sFKg8LhcQ)7xA>)9(TJJ#d3e#hXU$G@+2jZ4c)#?1U=?44@|pm#30 z*ECM-j{UjFW9at9&o(p9%`)RmG2;|%bZ$Xo3Nxk&{M}E#{qYa@x?lUP#BbatT(OtT zJ`HEcn!6tCue`pM0;hbsYFj+EVt}>>%F*>0^_C~{cDj7oWBc^zZZ}6(B|b1ilk-@g zCUQ4o8*U4nOKc*2={w6z9 z=isZ68|l{;emgYo3rmrBmi9;bI1hJGxbZb;{nOiT*1cLK(A}9!;bG0Ch4l9ZFtkmZ z-&nBa46_C><3J`XWTHxA7@J{_>*lGx{UH~bZPSLc{*Z6R$rXAK+4;sx=v&(5`vQFr zk~34X-wZrDmmr%s3S8zcH^$QqoB5wumfNS8{T|87+w_kuy1h3osM8sFuZNEV@AdHU zr@HyLFU)w?3aOyU(#=3O^G|TCDMma@uR0h54-q$F4*P!#}5R1xw=^^N{u+)=t zC^(XMAof_{iJEfu#qu&Ay+Qi|1?Z|t*eSYuuQN5T_y*?x8+M&*zs7S1_jG6tbs4me zLEr9JmY(y4n*viN+YJ-DV^&VJ?_Zp3Z|9rCf2YQ#oc!vgtZC2}vz;}~d)(tB--qb# z@|AS$eT=zV*Ur4i=-RtzQ}irPtoNjATk*+Do_|Yut@A?Qh_lg0<^Pc_*1)|M8tYE( zB{TLN_`HyIbpM6)xA-ye8+YCx6ZZ~fKL4^pO*P}%806kEaxwAkCzKPc z{QuD%{I9!$so(fC|LaUJ?>l4suXtMh|0DiaK0(SXf1LkyCYbljh5!A||Be5~|G(q^ z?@?ZB*xSJeIv+heCLV%!s*&rWM7{fbPYPBnsr^#(a+c=%R|qW@vYGFP)YZ9oiPz3=ZvWm`&u?#K>=so`Cs*Nz8G$dq+1KJR|X&8yFDCQ z`{BDS*dB#ePJJ%1lUfTZ7vROv`q7$;r#AzOg|W1(4{wFGTd{K*&>80e=T`J$dr3;{F!6PVE;jA%Reaqov?qNe zpN=1!Hif77Q2vMGpS=>=&V$Bt@wv?D_FMeydCTaXTE?O=zCXt5^nNE?(Kefy$!&L4 zye3%juT`?|?e+711#w@y&=+qEik;{h9BWzSjkPTC*rNtfj_2aR$m9y;n@g*+u=fgl zX?6IxIJ0RUS`~FsYBzE)j9O}GEeSnE%^XH(uo@? zJHCx<{3uu*8|q726U8hvX}A=X37x z$gqnj`yOSPn==bbhBrF*7i#`iKCQg?$<)+%cu&`ZYKJllRHlXeuEJ9@I!N(!n!np_ zxSjbs*X;SZFv?C6j3;jRREOXS3y<)H)&PR{M1Z;cm~+=);X95lpnT`f!>-RcH$D%V zsLu7k*!w)zd%z}Mb7iwF#m_wIiI>xl;0ts1KVtHWj$Kv>zQWi-TI+e4gS{(r9|_Ty zPrrxl=A2-zxns%T=&EiUk#l)4$NH=sbih=#RnA>0ipO)>&0kijb&>K0B6s3voxgg7 zc}Dotp4!*cFSeTGEY5gakQFcYl7+}=YWhmp?#RWFz=$+kve4)K&_T9x-@QQna3}Z@ z9h2*Y)6CtMecm^v*e_DYnV+3?r+b!6>#hP1`LWrz6P)jm_u?m_$H!ee`n0n>)z*Ax z-e2P2Ml@9UcHV3Lv9yMjtpZNnHMHB;e`jMU*QDSYs! zi^yMwpE(12M*D-@DO~Yq>}{QY!k07o)4LwY|K@MLo4+b$Ozk0f^5)d+jL4k}Ge)oU zWEZYl@xa5iA?(Qf1aFUuUqqWh&fk^S-Zq`sovDweTa({mo%P3ypcix7?a-?0eox(b z{mD*vqJ2vOw|}Ppa{P+2KlUVSt=G>Fc79=rWiMZGTL za0jp6J=?|I#*~>(JC5x-SFj zoi)N)!-tdJOD}j?*X#T3yx)l)N%^Rpxc-sxYvdbX|6D$L3)$3q{cBn17w8Ne+m|tZ z^%Km)uf{vcx#B%#3uz3=ZOx)B(J{>aqRPET`6kUp!O8Zy;G(i0Ut`o=7bSF;i3Poq z^N+wNc%#0KlAZMP9y%bouXE^2z6-%E9V1@2F&;dbmU;EbGHlcX^ewm#Ks!I}1n~1p z2mTJaYJFPC-k)Qqj*Kfl?|t4c?=_y`^x;qJ)3hum2PE}x+ST=-U@JV{qd(P86?*Am z>I>%N{(efk7JKQxr;ccy3f{kU$I1tCYroZTjjv~Zc^rF2xI^!mHXL5fi?2cE(tEwr zntf2OcPZFa@ablQfBZIu@V{(-`LrDUpB=x=FWZ%Talk@;mG1ms;J14(We@ZgUQ|vp zZtYY)=F_h=qSB9-IeMx+)$Eh*4P1DheHpr{Ibn-5W3QYb#;uCqf#b#YXZW_8|NYo0 zZNIZ(@Qc>GdjoqX*_FU4xe{KbpI_yDB(YAGyqo@d&8x+<*Bkc>!M*Td;d4?>-;bW} zda#+XyLa|9qdRqHkAtU?ajhe+Nc6pn_M*(Csy~kRDx*0&(!Ji+d_5}u?cH4u3O3Ev z`qsc&TfDD+3eXkE;X-5g(M|^Kc%i$Q`@pZ-Mn0Icd;IL`Gq<*+V+;KL9Psk3Gjgy!>d(Bggi-{#izaNHoB}Zf6 ztDh4qJQQDD%Rw0p^8&6gd0cQRGW{KxdLh!avU-vn{vZ+Ofvy^z8UE==~Ot zwc}OFhk0s$Ky|b~u!ps$d_c|cnrvt2?eOE~^Xy+z-kH;T_E>tus{Miwzy5RJ_kjPt z@gv$F(ON+F2grtsN@hmGi`duEwfPZbOaLz-_(t%D8IN>(D|6j{{W@{i1#9on5GxaZbyiKPFJrFH%AC7$?_~BsA)d-2mFSKjY?V%q;6bD_>h=XH3jul##|{cWW`Kl>F?&JSDo zf8IZ%pu^~>nHSjaLL1KY+iS4vv?nedt9j(ucqi^dV}m}O#5tD?1wYzL((gjzUxp=q zkL5WDxb@8~@5Y-zPw}789bciZN7>VtEIj}&qr=D6=LW2OT62Aq_4q3Gz>463hI(8qh0hzhUMoJ5K3jW(&N*#L-XAZcp7i~5vpwv8_xRIwPsz!zmLZG3G5O@L zGvn1>NDAw^p1ZqtGN%<6z&vjyS8?&fY{ba+Zyp9b)|Q zM;W|6E8l;1T==?*x<-#uZY1Rd9C?puuc}+#$HBiT@ml&U z*gNFtI_P{Wbb0-rEc<-U??*Tj>AT9Z&qWS>L7`bn29|mLB-qkk9_=t_k!1%HPO)Cd@D4Z}wZ(XK&;C za=za>W!lnjO(|LW*4v)X4tmS;*=NVzxU`YKpYit$e}fBtvh-tZFTwS8>2m27kLI9ZQ$O~C&$kPrg{I*a8xxcWvlq;Mo%g*3_co`eKYvVt&a`A zYU`QE+Mv76oPRcdSMYZue-|QagQlIebnvvZmkybB&eF7LXD&U0tYzMH;rxsEyPCgm z@V5|Iv!_j7djGVGmOe1;;-#CWUAXi(vX+ai{doM%OMk-OPx;%%pMU%lOUuSTxpeXP zA1tjLl(scI_WSd#@k^F2_6*uuKyEtho`o4!`kr`7di^^?!WF}aubrP8oc_CyJPY=o zWi2QmcjmUq8}hqWdL|FQ=)U~((JAEcII38)V&*>9DmM1V4C!_cbrsV)4t?C55w3{V zOt}qxzRlQ8UwF>3@t;nymyUDhNaZg?#~n$YcXp!lo?ELN!l(L4{sUdEJ@@3;Ta&}i zj~{Rnb9ucLJJ98c$#+`BT&J8lEfX!>g|t~u_Rlvf4lKm{**3}X2?iOv@d?tW@|YbW zJ|y82+*XwE2{M0>7sr|Z+&x#;m5r)bMm^bgUh*GOuil5hH-$OMH!e86c#vnoBJ%1V zq>tjeJqzZtme=nk`py6I!0oDBtL|~|xM($d`+5?KS4o?TR(k3J=$mTB=JQ*PUaw{1 znbbCRne7{Db*5KWPoK9uKdY90R?+Tk=5y6&Ki|}EqP@B8)`CSv*5t+fpAKwuR|3i=_Uffs_`{7p8$IicZEes`>qwN6i+_qoXK-=df z@RMjez+Oxwxeog)O~hhoyk+k=eeC`_;p{Ac+752R@O<#zrT^iL}1j_1(4NX4N9EnGZS{RMtZ;#>9IUK`opzIE#{-+i<>^dF}0 zz@OoPq;aoH;H@ZuH{o|4_j{~r^_aXob9irb2e_UGZC0IUO^$+(x#*F(mvDCv*S;b zFYsFvf{k5gr`3gNx9i%px*$*1KNZCN8=n?uVKP;Ai(B?HY3!w03%fp*R@a~>zsvAN z+&tKtO1-QT@_o#r9etahR#(Lu=^DRt?drlZV)z&^wG$p6FP3JHtq%A0z%)S`Bt??HHn)8cyt*JmJYLu5^ zz;?bn)m9w-fZrEPv9%X9;P)S2Yzcr33dI<^g0;YfQyVPHiF2 zuGWC|dG_3oyGBf^4o#oy%cu`}3h?P>$7h505H!-f7X)Vo!_w*k)W1#mNhKfjwPmr9 z_#iw}gN<5y$qqP(P4Mte`H>Z?g)DzQ`UrlC#;&<(b?xkTaK?LXD9f5A7`)`BDe_=5 zFfQT<%)QbD%#X59y2hu~RqC0~llejGz^NzLCm=??l{rE1jRC&GmemzH1G4aV*VHM{ zS?_as&pv6U+S)-JuJWaOug|;MJ7{R;rIek8UFiHCks0K7A$a%<&u+Yc8~M_^e+R=A ztND!|sbXr=>WV4;dn#5YfAd!+F0iBxa#=TBGf57TVru5i}$+>6GT8i>hr z;Sf)~Ho+U8cDgp|rrX!jM%U%YE_8X0*a8nv;jMkry8Z3#PaWG`xck_;SewyzP0S7M zI70aGBPm|(-*Puii7g*jzu*2^Vtaq!i47zRF8qpB_2c{39zYd7Nck0Rgcg;YHFC#P z1f8Y^G$wrN(CSW?R^V(4a8D!F-G!l-?$}iHr5W}ZgD>L!sCR<)tGJsX#8`cq;GO*0 zi)ll7Gjf*)Itq!a3z4@_{UpaAE57a76f4Kj3R$$MGY6ilMqkxD8H}->-vcjgo12+p z`(mCLv06qBdbjZd+Hhe_!sVu-|Cnw^pBxxp$#tR*{x=n^n`TE&Q%ATT!q~g@2e~ZV zcKYCH*fe_|^UBGu{sMn#-#g(;(>ULPo_F?~d;B#z*Xa7EPdxwa{2BPCOH*@eUjRSR zl+hX9mE{Tl^c@fUzDE0x@=rT=C1#Cv{nOtx{%P6Sic69IF4;$=`%X`O_1DJlRXWN# zy6b%HpLyzit&66YVzZa&_t8bu3-)>HTF>ME5yTe$nl;ogPhE)LMOy2$o@cLT?{3oa z&~2`ME%jqFo~yQkjXrO1c^tdtg^jXd(`yTVb5DieQ+|8PB;_r{uHk#_(2W(7uyJMA z1o^hQ*jgCmeqv*HpNS0d+q89I@Y&?HBE#>ih|K@tGPT!QNS)L~ozD}$bvCx%LgKe3 z@}!Mc@*KKtq{}|KPc~+HZRFgI70oX`ZQ9sLol3V2>Xh-@t#eAhKipVRIP^!RPT(y3 zip&KDR>r;M%>Kt6x;!y%;2CJ#J15Y;J8tqLmhs#458jje_p+|ln9Bl;85uTxPr=4l z4x&a@HLC*Z=*gW9zqK-`@0h^iKNW$nble`UP6=CF;}P&hb6{X|1w; z>5~RW<-nx=c8+7c@Ae13v|>*SzG~<@=SPmsUP&3fE293o;Xk0huhuh`bMS6GaI_jf zm>CzeH~pA31-vozUrj%&S*JC*I0Y^bzfF1YaA@g2Vhnl7L zRTN+KBSW9)&;)&0=QbquH(Pt;h3rSY#2!9%3;tr&Z2+c%eXPsDQNbT9WAl{4J3(k) zIvxJiIa|TzC8yN+8}}o(#}3uMbS!?Z?Edt_A!Mido+pI-AyBzj1%;+^3IK-}mydqTl@O80(gkmEdroGS!zmWg37bxl9}FL}@2P zJI(i~O!>(Gb9*=egNqwO!}A>)HYaG6hN z)yS{j2Z^0aelJ=BhuP~3CwSI-W-yk%E!c6mcEkQXFFt*2(z|<(?O|=H^CoTQZ7_HW zz4Ows=&-*Z1CJ-$DA&eX$je$N%34U{l6`L_bb)DaM6fZrz4~{Y_M-D?Z|l>?+G<}s z*0%Ls)1LRfjTKS!li&*CqtkpP9%@RAKkPmeykaHrXC>e-_%%6{h%YMG4WF}==Rmhc}?uW0_K~Y_;HN>;%WMTCfz)^ryu=A zJZ(#&J;AmUzs`RLTj6gU*sAd5_}#kje=EKlQx~4?x6i-V?{7}xJ<+WU+r7cnQ?#)Y z+r3X4NwO264biIbrk9UJhQDoSC7(`q*MgAPg-zR`0gq#Qt`E)8>-ur}g3z0+B z4dC1HC+ZGlqqyhWWP@MF-$U$Gje6W~V#5?WQL^j7{MU((dNU=jmcz^o+fqgEOO&-@h-|f5c`hyK(-0|%Q?@UJ+EZDWgwVk zSIry1uF41BpUk%U)FZuZt6u^J9GUZd*zC|kj-}sf7klM6*pt^Zq7=v z^ZcwYE7I!TMIIHO5yHl<#>Otj#+E*Q9(!;dIfP>qd~w~4IA?ibhYwsXk`luY#8HT~t!1?FQD^tYZ&gx2er< zQjWO)xbbt)?u+tJNNYdGtRBBz}y%E~8V+94M9{eztS zehhya-*4dkFIfMn&Fd(4GwZpC?&Z2Z&G>)xeu065d!+fcgZ1-HVAh)NMf^a@FJb|^ z@`*I@`wZYKy@v7V+gbQf_!hcnmhp+qGi#QN!)usxv@Y_oW+`ILB0OsSTe>K%?(?aS z=7)$oC|!_N*Gk=L_Sjt?w((yleBf&tt9;-Z^P(r59MXdIY+zM=ALC;0vLb_hiB(%H z1M42vTKd0=I&+CD=wyAdin@Y()sL+@{6q`!`PUWYuFF5hH_Frv;@u0J2M|xpWgYiI ziq%;IZ)hxLG?)yVYhiQ}Xif(6t_hM+# z`&%n=t^W3RI=%&Ra2j8l`VTPYyvY0Mv^i3AV@>H-KYnLTS!>pm8GGgovpTD_)@EHf zCzJh%J1SopdCR(bwVOlyjQ5V)9#HNBcZ`w&d;&S@OLE!@I{W5oZ#6cp;t_`Ojd`fCU>)-_V>&Uo<7Ypj-S@w4#XjT=sMh)U z_;Ihl7&wTn+5;T;6)NN(`aJtiu73UQuJ7h={>}IDA59ru8>gL5r{-jYR(&C3EU{G) zVrTPAY?b0l6lXRvzJz|IU)yS&*ec?3Cm+UU_2!MO_YqrFy3(pUlh`WR)+e*kaqN|O zIp@~0#Ij9(PT~QB_&!f$c09v)>%N~KJXwYv4gNPtpZ@c`U@RH$@8jzeo=d@VZ`{ha zCftq&xAGNB7iiqVy%${6qH|WkUk&i{?da2+ei)3^dWP=FK%WYiDcFjR|D%UChM%CN z?9tbUk1_n_cWEp=#e6JYkWTj^6XJ!I2WCI}b^4P(mt1b$I?{ThYhmNNT~oT!)9SP) z>CXo-M{D%EOP%}kfLH!b*>uTe3T%SykfylHnqseJ|I z%$S4+e2>XxRjxd#-E!KUnP@l2_>;@J?N-ZY`xlQrnR3;@EBS3_8NEB&=) z#xTc|_8vLF1s_2 z98^mZvdx_6#!cq4=bS9tD}MVu6E_)WEqF0;lc77W?w0NAddc=pw0Vkb4@S1(#cPr6 z=2@&$pTKX#?;Iz7y5F2ce*tb!>ge7}J}-AXbj}Xz8qj^|NEG#F}<6e%*fr zE>D}A=mpx^j*s-WlnZlSTCsAk;fK}u+j7}w=j@Wk?Zmw~djM6uZ#FW}le1|H^6$ph z?x0QSlSopT2r}GLCMW)MqD&M1rXI{*Se~tRY zacw>#K2AM-{yYj3eOO>6Xi6#GMY@g?w*@)IdX$a~}@tpu;utLgT+)M*AM z5#@y--b8vmVAdz%uaWU{_%1r;FqY)8M&L)q!m_X8(C9{=?dw%0+PjREZtn(ehfb%@ z11FfsP1Frj6*@@pqympx%@TJBUrVjWgPO>MT%3Zuuo2oC~RXObg-OGDNrgDrdwIy=ScrBAZ0T^@Q zf20pRAK-3awd0Jpr|sVPxeEQk&H}}QptlRj86EPJmypL|N;K6f**(EI;~a#h*bUuh zjg+JLnO}2$G$EslIC~$aelm>=AGW32f*BZyuQ@dr2k$C+zth+htKLBDa3%QHIG#b) znPYxjN&D!Y&en;Rk!_uMxaz0uJNDDg7wEI{!>%1(zroyT+uSrUJRb`T~67Ox-xb!V|MIJ;K;;AZc5|; zYkeHs3m?LdRE`*S(Xm0gh1@%;TS>gKVxwH$wip~sx9Oc?f!fg7-VZ&oN68x~eZ}S^ zvEpCq&|TXe+Ix%dtg|-FI-)pO?Zalr8)&zITvUb**ATyrEo$g6Gn15e*d*fXq&!7@ zgJ-hiS5eQ@DNCy3A7+mNk44lI4C;e5i^D)@&C=x@@B@7ESLSqIOtrYvqzN%EMC?JhR!V|=p^B&^mWab zUgjrajhypj@ZJO<3zT;%)eyRY9L48NhC;NPkGMwj%h zr+$B#C>Mnfy{$8B-Bl+4ZUc1L!Mh!cXV?wUEdYF91coT*L1O4}#lXn!OW#)(^Kk~f z82fdO&+5cK8GDr&f7Q9(;5jEgi)SRCd3pKRtVljFd+<$=cZZNc^=qAXMdy5etG|Ye zuIL;N&LkU>kr}*~TnJy_kb9YZ_P_D2iEomD0%A?FKbN8y!%YEl_&PBNx$#RGR}>iB z9C6~CE%1$_E6M*!8%Ea>kI(%Eo!_Fov+vgzPoj+n-SRkfEOfb%Ti}$xu3hb%czW(PU6Db~EZwh?8h;;N zlC36sg8j`W`lq&W*Ru_Sgql11w4%d)N=Ip38!^N$-El@Aiq8#dK~r zk{Wrql;^tCbq^n6ZGg||==#+44e%fCY7iHR}y9_tJE_H{pfgf?AU zAVQm~!NJMYvEV&F{yK0yNZUbZ&U|OLGPfPrW!Y;_(_iQ>oT*+rJ9Ys3*uZjpq|Mr4 zKX*#)CvNS*)7ip5r@xWNO*1^<&Na=*9CENwJo6L2hmyX*&xvpNhxqoP(>OySZ1r7BH}H zQqOnYnbUh-z?Rz^KK}}%7e`V?d48h6pgVO6(S<&A;i@}5G4WkF@8OwFmCsRs%&+bo z^2;o8ip@^Nt{C#QlV9~RJ|E*uXS~S&3UXjDM)z!X1pe}OClgQl!-*SgY zu=7vwCi9KavyWj5Vq2Pg83mrvhnY(j7H~(SWuAG5o7gv2ZsM?c<{l36{8J|L$a&n! zOb&sz;r?l&mv;_%1NcA6*geCXGeyhkhZtbf&m8W-Y2Z#tos+`8Ipv(x&gsq_=pU`P z5?alJW^dzp+Q^9sFK2r&pEL6T1z5RM*?r<*?E*(2OUe96Uw09 zx(Vb9%uKm@5qlq%6NvT2@4kg+`-H-hb=V9=#G$i3HTR?c9DLs(-t^=jHhe1H%)I`j z;nUf=_sj66jT#J3#5ma%|M=cCYTo0dE@-Q*-bzJz&#KjA7RoxMEhV`!Iz zkK_1>3_il6ZEUQZ9CEcZAP>6R2s?4J72upDuqMyb*`ITAX?ha4NTzQXnM$Uw<^E34 zH;nIy@7t6=($L%a%{s)Pa~R)dGHxdD-NnsUfNwS9E^G)ccXL?<8NcN27p$3NONGaf zpL@3E$K3cCoSOi5lNGlOFHQ~3MQ+H2r!}@jYhd50^_bL3&{Fr4wkm9VB1bb-Yh?1&VLo$rS?#3#UP_+=_% z4G+~C!zy0OyCU|Od}U=b+PN#swR<$42)GijV4FL73SRJf@P|P6cIMbJ>8hF2z#sW0 z`@uK1fWh~o1iq~i;9GnKZhnfrFMKz^t6rU1#fK`K>f6`ALE+df^IOgs7;H{it{^%|4TjGd&YV?oj9*K+!wcwy9cCaWDgGTZMot_2l&=lOirbC z?wu;|3|{6Z$CL8D;o~@}nB@Y`uw^;cx`$mpJO+#|A4+!G_^mmqHy<{@hd+f6u_Zef zLkF|R|104>H{Rd8qL|;)%_)jcX@d{MH}ZF6{>b6wcd=0{X#EOLzwq}yd1U&Tw>X!mu_AyIK#n9tlwAN8BBU#1%G+{*gecO zrOY*w7v`Y4R-T3AtM!gG{mjrBr45;Mb-ICfqH<$-%={;vJ~1&q(Zq}G=i19&V3E9Q ze;|W0I=d|RfHMnz$Z3Wr6a5&TEQBZZTRe$wT;_!*eeh(VC-Sh@^45c6!;=GiGdwxK zw?@meWW3+9KJtU3D0d941DDC?=HNm2#qo2vK8_>MKiOUtzJ26QTgUx&rhc^}+d**o z0PTIVM){HCQ&esc{EyTJcl^$YN703rH~5j)UpZd6id?%@eE2lwz(-y0_dfcIqNBac zQ|aUn)*fHFXPC*mmcBaMd3y5A^Gw}SB)gDt5aZf8Z$Ypz!!xR+cyh4w5bORqbATON zs1Vz4PFb+?4DkYdILWiAgx|!iJA3AwjjC%!cF@lz$KI=w2Y6d~`&4fo>rnY!EY@RY zZ7d$dHuHKy_ZL{%hpo}Ulfor%Yzl<1YZ#k9VUKOa?-NO3?(A^6+jGt^dJVs4kMFflxzFX>rHOLtGeTaOgNy+g*k;{aWYZS+ zkrmk?`qa6|-nodyvt8$KhdBIN2+xEpi*tn7?ch@OiRsTKZh;&Ze%A8Ix?lPpU5@T{ z?qN0mC-PSFzp*>gGO_JVnMB@daBkYX+sIvK`vdHsWmqQnaksuN9A}RK-X`Vcgr|-& zKIV~5?1~*NtkrLD#*4qMC*Od3=CV!gq3haRTPrxm_RXq?oQ~mH}KXN`^Uqbxvamw$VIW))P={<5hh-#J=M&SdjsDX zXY2^#OwC-_Uwxgs8HsG*JJDYLChN+nHnx&-+;%?1(~;qk>iz6@ zY`~r!_8?9jkX26V-0WQck@P1%%7W2!>x-Bbm;p1@pjQ^$`3ouH-kT5 z@73nEz_VlR9O_*NtnwRAVhpR8yFJ*5KGB}>98XP;YfQzwt3^&~tGI&%n9Lr*51jkG z4BtKLw}qcEv{jFt?ABj6Y|q20KMB8~!&f8hJZR~M@1cM9+6wwNhTw->-J^y-IC~U( zJkX>D_#EBb7e{)37k%n^!)eA}4ctXvK6P)yz`VJ@mc8}S3MX&BdB?pOQ_}q6! zzdhW_`DlfcvtRnUzg$30E+*Z3Xqo$%%eh}KH0V^l;_l_T^#2Tax)uAa+2mKuUuH3v zH)Rcea`ky#?5Xr+>(mVk_6nPhSW78J?`Jxh>$Tq&0HD1%lUC&5Y8D9^)nR|!#5$_%4j}(0L?Zdi* zjWKTbqR&l#S2;Q`06c-sU^^zC3YS3`u0exQ)^4_(7rgappWdGYUo@| ze$8Y%?F;`YKaupysX5}4@i7pd)Xq2Q)3y1^X{(9-;RrJ1J>l%L;&L9ebRUR&!X zeJf1lxzl@{y;t9}822OKN_w=vvs+HgK}tMiId%wlslZDKn!ckvh~D^E+Hm!#>{Z9! zkdMgNB-@f~66_G?Zg|RUa_`bGifHG|#a1HhPUa8e3_j0lkKgc6(bA&$KzZty|ZH_zf+(e?tp%hh&a>hvXMi zW1{aQ!32Fm&`q*St|=40K>g00%%cu35g$r9eOFnHv#pbyZSb69-+JQ*Ogr*f_m3MF zCjn&TkCb&~KzP9iwYl`7J}jm(PY*8^t`xn&%MfnnM%0CkI@{xN|wSw&z}H;+FR6?xe@z;pDrM z<^xj$I;s1PWpFy9{r|A{E%0$yRsQ!kN!w|tP-uaQNT;PG37du#N-3d0+O(zB(v~(n zgu-N!nIuCeGsDcJO|U?q#SOx0qE)M+hKFogi&m|Wx+oD*>vOBiYTd0G6*WS2tFmiQ zu>bEl_uSv_{w9-B_Vsr^pDDEW{OIfswx z)97o%UC&&^d%=u<;iF4Pmt1#I-xTCU-+VUD=t&qO#*-g^sr2U(gy`pf(`U119z@@2 zq>tPd=3#Q)IWT;`d>{6Ja5n6BI6FaWi`Mme+vaM-JgHuK+n@X9OS8vn+P85wRr+`` zhu!<9NPldcO)r46S25-j#o5!ytLM|R6F=eM46@IQKLY&H9KL{XJe>U;X*929+?0=e z(gD7sM0&SZANvX>fM?F{-d}Ng@czkDyY~lAnwB0{Me!W)4j*0v*2ary(_YBbVG|)7anT~qYOUL%%oy6M+zb`@_ z)7_fyFID(WRQ$dT=nwh=%ftIXzZE~ccocVLPW}EpNXOfX-LTIN;OzO#d$HGrvl25u zqJK+qC=ed~D(Vj54eu4GW0%$Tne*%RzZc(iN0040hIUWF`$5!p3Gz{$&=VVmCN3R) z0_AZ&oX@tfE(=|AE6UOP1RlMezWXGS3D7Mmp5}ve((TAMj5C!3koRr!+Z~{TmmREg zlDCIY_b-6cZO8{4A5>}hUhw_UoptXezuFxXk2QB6@(n{jt0TG_xuoxYe2+)3WaHCs z4bWSc_zfzYk1_ga;rZx6-+J(SN7QGdUplzUNAjEcZR9ldJHqSx^xeDBK9xIg)bqbZ zfIq6^5u|k=NB1Oug=?>a{wvW(jYoaoj@Iqq2y|bZVg3Wz16uclUa8k^2>N#$aD_Lp zUY-WoLjE%23kVUNzEs0+!^ZDy@{Kegj@EBK`D?5- zP3wI+)@>)oGaYMsMAkIXNEUIlrVsF%K7Dr`>L4G;bog8Jn%4IQ3(e8`f6(iiXrAVj z`}QqbbJKR0p&l)R3(wU6`e@c(saL+2)*hXC&41%k+p*#s0j?GBNxZQUZ^mN03(VP0 z^DccqXssJ^!sJo-J*j?-+m36*9TzRX{qMpR@=uYU^K`89>AcRT?>-CTBc9j@otoZ^ zc@c9-=b1k6`#b)rA8YMjY5c$;>EbHBkk2R`3!L%dEUanj6XA%?*b;3MuKJm_N#2Y- zBS!1M&i|Lm$dBJLZL?^)&82DjZ8YNDx=f+^Z23y#%GYb^NTvYC`SIO|H}ZULnSQ65 z>?`7v)IS<4mqFj?4OqetrGHn_P1!S0HvjiPN%te$`e%@%Kfg%zHR7Hg?9+Wz2j&dt zSZn&8Ks`fhP}w!dvQ=p2581E7KYaWwKcG@j{q8Htxu|B0Uy zzo&9-;In_U#?bGM!C%gn|1^e?bMf0DW-SD-?8{M~L7&5u>R$fZAnDegE}&(L_f5Ki z{oXCr*GBx|jo=Gd`?S9`0vz23e3@w_2|eYA0;9<{|eKSy| z8~Zu*8@P-+=<#&k2kTVxTG$W{+HA%ef}ddbkTd$ki$TyY^exUGA>Kk~Qek5}x47>Q zNYnF#-@6i8!Fqg8KYn+26#L+`zD#`nS$D2a=8n#VGaU~t?0X3KQ0V6{Xn7{iT+iHE zvTJ68+CMWrVUXw@4$2*Oy~nw<0(}V1I=c`4%J;-)tCuI&^{kJt$8WO(hhP8gw|`&x zcSn97e`dlc(Hq9Xe3x)N2s#@9y%ol>|6KelXlp#3bPPR@UfDN<{B;vSXQ-cXR5O#? zeGA3|o=kdI@2riJIBq%O8bSMK!aq&A2!&?``r-GUw)-lik^Jd_?sGc!#tm*r*VvCX zFkfSELvrj5NDBfV2WFS<`T=lJf$<)=)w%R1@N*5ppGUrNdK2p;!Y}*|&)qV}0R zDPLJA4lH$XAa03g?blhX-eE$8YAMk0J6kl-#xJ zVfYpvJ>~6{AFsc8@Ke~&r{8!mza6Lh>V1=6$Re+-n&93_6ke5cTOw>o`)B0eGGoedq0euMf=kYP4|F!SQhM_zl!ZR1hUHrg1H z@w^ki>wVn1zpNlj$oGZ!!@+|qpnDpANq*06$Uq+7+cdw!x;;4SfYbNnB*X9gd}8E- zICp^iIp^bC=v$p&*=pys)9%7u_-_1uYy`afZ`zhz`U&i5V?X8AC8&G1Q?YOsbcjOv zp}RDYeh|d@j>$M*aX$9J;6s?s{Sp3K(XD;+;4w#MRn6_&JWjtVqGn%$-fwVrU$lqz zC8q5je)-rX6)SO;1NEPT{!Dfzmwg>~lA({BZ*)bvR|MUBI@1o$jD*1VE72~|5Vt!x zx9^5=+O_3=C2-3$i8Bc#%Lc#$gV^V$^9KwsIg{Z}Abu14$aCk>}udk?CwJ;d&1JRxfP1^Cxs)G8w-q9G?)pqzZWBwy_?l zZSa?|^EwddTjGwx(>Es1#ak8lecj6G&@bMAzq3FKcTE1U(NS>kdwM7Yo%z_YC)!9B z<;@M#OiiHglmd0rZy#4;OqJA6{H9(A-*vAZdqSZNQ-3S!C%W7PyqMp%X4pF;fOzeX zzmIet;8)HF^wPORf{BjJ9vS!1qvyS|@Y~iUZr}7iSqOa!!rmbtmGPI5{zo$K)wh>| zH)_7~bp1Jl16lAKtarYbh4s&Ukb7A?_QfTAub}=Rto!~+;B)o23_jAVzWZ(Z?P2Jh zq~{ESmg>mX?J4P-g7r%}j?r&4Kk&ZkcUAI#ONC|5BlP=~&}%`j=mXABEbKcUH1dMn z7ZaYhch-X?eTUGWVf1H&?wU>p{%K#}4QFQ*=;E`u&qaGZl_l5%M4!yL0*qzvtP%82 z^fUW+Qs}o)2tRgyUK>*?`u59G#J2 z-u>D*?9OSjw@|3v@!po9^+&kjeu<7XuqycoH4#!9IY@T@#^dj(olHId0Kctg<4rklQr*RIxN#{5d+r}gC-vsd+3BShgxTJwE^BW1jE>V4p zoZvTSBD^$AJ|OcOHE-Pg0@^vNe}J%)5a!2ZhSPzHQi!I*GRP?TQhMpXC|obqtR1 zn{z8rE(AS#A85FT&bZe-rninS-quwC%d$u`$?o?pr2#*aHcDLmbneIV%92b-2Kz0>%I5;(H_lNzRf^m^{wMCV;w_pAYFmi@wo-Y z`%-CN8Tv6Svi0FF-!k}~Kaza)*5;VC3OX}>-yY(PSd){nCW+4KptF1d{WE-y)=$1a zgJe|*d`~4V?>k6ik+Wl1TfMVxxSZ*c`~$o2yOTYz^No+Y2X?-qv%c6T=ki0B_YF~b z=#@mfTz-#N{)19JD86ti{}AN0w#}#SenIf4u-DrNx>LPl`J<7S#94CD1P|jzK06M*Ie57;X?6^wSDg^6n}6* z-@6ON@2%~-uTXs5qP}f~;=3>IyQ5Hi@1=cP3dQ$d(sx^4JoA<}YhCo#yfmK6y_fYx z3&js#+ILH#_;ri>Zpe%0HbjT{W2uo{9?j(I|4gsqYha#yhihN#owayE->aw}`UlhR z0h}vs!x@Ukyfst;-S&8T`Y!kpDzAa=2fBaDmlpS(kA20FSIZX;AU=qAtb^XlYcB7j z{l@&dm%4jwo)6->t@1yjaf>t?deoHF!;+@1;l7F7OlW|IJP4!x<}R`;$iJG&cgO@49dmwG;QH`e>Cp{l+&gC{7w{r_(?Kj3L({{YzQT=MwP#K7Z2OLg0?z?abd z?w-X%_uRMmCpc5^lQNuv4Y8fxb-=V$DG25r>gJ#Dht$uLZ{)@2T`f*Q#bPO)@idW`yDARw+=q01@tHGzW?}ylD3YOq6a6;$Tg6HC{E#5;a z>xz&58oI;oW5+%R8k&y2ygY()fsmQMLtkFL8~1dm-89Aw8m2dh;Y&Vq_i04Cm@|S; z0SvypJ9H6zRA>XX*zZblW_cKQg!Y%5HL4ItdyiE1YV>Oezw!36=`+T;4{?9ScMtW? zIrDzp4dFYJbYIwv>l4UBcU8#;coymzD7kTz_B|;7S;$|3vSxp#0{x0WMp5~t=-Z9B zyW0r=%1m|oi`|GPAJK17UpLOotVH~MWE*_xeBKX*KGhH`K_B3&|Kn`c*ZrqspR1!a zIVH&bgpZW>zesm@40TVQ?$_slhrY?{R9_`@y)&K3-#ivjm`8dW3uiizpXBKX-b(!X zz4(octM_EEF89woRsU8E>2NgPPtp77?9bP~O-6G3NyGuKw+(-G$A{?d1oIQd9C;gl3A}y`_zzu#IUgs_kC2D@c_rwjP+y1ez97|8q0~tVzvV#s zPiQD~KZiKqM4=j*eNL#N@f;ggbxr(j?@xRa^`M{or(Ld2Jpgzb_OD3pzqIxlelwfW zf)AZ?4d~*i>R*I2DEhZzf?KC<2s)vY)aAN;`i-;^jGz3lbpMI?8hFV1ufOnt^FGTin|nD_`L)dM_f0+p?7ry5 zM&RZ*c%Od=ebIJ=-{191qzxkt`)H>XN+;g_0jw!?>=^G+wb9zc-okXO=Yxn}N#FA) z_mTZSi*)*RSF${K7WKA)N3WDU^fvJ5pW?klYGV@9cj5L5jb|3dy2Fe~mHw_7W5PTB zgnK7r$Mo*wj0u8{_96@7u+W%(03R*cCNul>9Bn_&cnuSkB=SCO~)C}M_=c7Xpa8I%u!t(&(YfBjA`Wwj0q3%*mUR4(S>GA-yJ)q z^Nus7`0E@~(#+A2o})J%XFStSU_4(LXFPvFJl0Wfa8}uvb@Y4Os~vA0UHm%7Lv!?u zj4Aj^<3F`w-TxBxkH;H+g0$D-4ZY)x>FZ`pSC1Xj7mhO~b(}GkJ#p+z8bcpKpUn+f z2L97`K`}YRS*)z&Z=WPz-Z2u7Sf?>!+WBX&RvfS%||1cN`E$pGQcetZ8 z-~a}%)^8k4+YNi3_d)I^9HFjLzDaHSc`NA-`EmLF5#rZ)YxA@L{LapO|I)^1q;|m% zt*}@452%}XL>Yt>(OY<=aeX{Ys@k#&u^nZEg;6w0< zbR#eA+rAZT*!hEc&*q@Yf_$ui8M?%;89FZA&7|=Jp&Jg69R(ZUEx$ST0kXTE#P>PqZwP;( z>%WM7`Gz@1@Fo!aadf7N?8Ox{9-NgJ9fgQy4G-?U`cz%9Fr$q2WACg{8>wRH(# zvYoC+NcP+a;@Sw8;xicl?&A2a{AsAKvTMtz+>h*?weJmmm!dBZ;cOYr%|yWGpdPYY zOdkTk^REMHzv&0@HsElKZ9sf)2IhafG5-MRkI;E1^pVauk=+vpo~gf2p}$#-net4- zJQ%+%{Z6xe7sBl2ypi{6&D|!?*V_N{nVk=vq`q5+HfYV1zocxgvuh>Uqj+P7-qg7gW%QcYHpj2AhU$Q4 zg}3KmFZIsaF}3eOx{|6;*`r562b7<rhmEckmG#wF^#^Pjn-@90%e?^;LnHe2&#z%%GKOceeQOlPj>gJ622xJF<) zZ-2GG^wv*IV0uIK9RmLf@P`F{9`I)c{wd&p6!^!0pBMOF0mD|{whjS?j?M5t1HMpT zdOu~k!2byNCV}a#pW6hc_Yr#q{wiR+8wLIZ;GDn%fbSFd=SAA#e-e9}3(I7<+U)u13HC7%DWzn*pCC zFx~&D5%_w*ZxOfwaI?VI0KQk?HGn@M@LK^tEAUFdhXq~^cvRq}fT!Vy5UAZN0beBW zV!+o6d@10JzzYH2EAT~tKO}Gs;Li&@5AZ(=d_Lgc3S16&GA^o9TXO)<6nGY3oUP;d za{ym2F#Rs;R)J>#{;o;F&>P|3tu73S0vC7J=b| zKC?^USMm1Ndj$Ryy~8E&9|3<+;FkdZi@?7F{Ck0a3pfbFlg9WW;JE^i0A3;RF962` z{u$ug1^x-(odO>Q{3(Hd2>5FPe;@GA1^y?%u(i3ZgMiNw_}hT%1^x!$h`?V1+%534 zfIlqomjLe<_=|vX*M#f*2f!}~JP7zN0)Gzhw3Bq6&j79v_)~zF35?$kKl5gRcLB}_ z{1{-|SL6C00sN@I{eZtL@J9d-3;ZF#0UW9$e0~7%Oo4X*t{1oua9H4X0lr7z`vE^J za1Y=?f$s(UoWS=0{*AzQ0iN*&-PTsXiv;che51gd0jC7c0>&M6?pqr0lLEH`J}B_5 zfR6~=3OI1G&L0JQp1}C6f-~y{z6J300^bBUBQV}rKJ%RdUkms{0DNIrBb&D*%5;;JJYJ2|OF{w*{UF_-6v24fxLjzY#FrVq!d>0k}foX@D;lcq-s! z0>1(927$`}Hwiom@NEK@0=`>d2XL>ze|h!TnRqvi+xj!$uL(Q~_~!!uJ7Byo$a#Jb zIB=?l{|)e|0v`b!68P7EFA(^bfUgkv=YX#kco^`_0{;jwe&^Kv1J=TSj>#XeZUvUA zz}^(4K|CvnO1QU`f;ds?jZ>$Uojvo63*IohM17q8+}XA5RCPt$sdH{vfaLIs?e9`* zgHoF)vr--E??0@<+qT`bVcWLr6LJuvJJc+;8;O~Fme_g2U zZ>7e6R$pN##Q)x`=5sg=|GpQ0Kc=Rr>e{o{N9#6Mq|car%hUyBD{kGk`i491YiWsB zH+8hM#4;7l(MT?GQ&p&`D;EoA?_BsBko%L3E<4tUZ#9r#li6>dcmSuC2DLEQCMEdQG);*l)MW|1Qi3pKYzGLhz7%*%;pE)P{#SNn@{REV-C zTVsr%<`giN%&FD!cAz^Nirv-`-yBIGim*udEkCb3luE}kksL;d4%TBZ^Q%KNnUQET z6U$~(EftFy!=ZBYxuZEZFO`|skp!m7uc*Xy(X3KubsnC>IKE}NGP)te=)#0ZE?iw! zc46UmuBZyN#AAslwG&T9W1SeN*AR7DdPZoClyP33T$)+nIo6JWWaCkd0!<^j@s3Q) z%~*x5H+N*RAR$aCM&2CBBDE!yjAaQ`Zi%d0B#S;@G^WpqcvB{l=|Zn(CnN2#+2eMV zdQLf_WrozK=GH{2DUt|nj%4DIrbMh-$vV-wfm|d;67kk#JIxv`CYmN%S?E{`&tW9f z+Ciz+Wvg-+2Gcc&hY5^Y)RbHw%5|kkw>2o_tlS`v!Jno~>%SRG4pxkz)H ztU*ZtQr30l+EU4o+kYXVdQf>ZR@NFz5+Na93kJs273&1qCPTAz!P(_rQR#aY2w|*L zm(@oS3F;~ptoJf*i6;^im&CX?r#9(X&FcszW^I}lFCj~?k*y7t6S~VoAU=&vG$qY? z^;d=BEoIR-@hw7dOD5IMx$|1M47?*6DvxC{sZ6QYfhAFo=7-BCJzHHgCzP8F6vgHan1-V%YrS&vD|=%G!sIY zvJusmhbp$T#nE^>Rv>ZJOlmV0AQ%qVW6WcnAo#M)@rd2`O0>ADJ)O#c4s*4PHMen* z3x<`0?{=WUuCh%)9W9E@F~%2oWg`&dee);5DhQhP<6bT8B!R&)FlBPvs($NICcd~-8xTFPZ4c!2_ z6AG$Ak!DCy;3VZT9B#_68W5Yoh^e2~S__CTRFO_)v+<@xmmXZCEE{Vj9uuKe7HWxf zByyn7)u~977lXzc&1)AioMyMjpn_yh6Ags6UXVg#gxqK(6D_NEa|`oIpjyIH7mH`2 z>@`?ySmbL`P#Rdo7#2i}2`T}wm8JE7=``3WC-i{uTPiBCV8Qv5sazQt%qC>u8Ba$t zxp*@RV;O%{=vLAi5^->u`cND66wD6MXOd@Ea+PfX8%Y8)iBxKn%jzJYL7btAjwE#^ zS{cfAwKt^_=pQLI=ylxXKJg5MRt62}39^>C5ws-nZ@Rhs3O!b)4Vp);h;$)Ck(%B# zt18p0r3!qAS0arh+mWV~#pOiTA{{Ez5&_E(0jma`Of{fT;3(7{!%C#qu#h`QIl_d~ z2%0+Li5w)dU>ov<2p7VfnSVuBssnTf;e%}85HbW{9#WZgszkgw4)v)LIlw0gfv8w_ z8cPG33P_vg8gmHs<+cv=0!vJbIZ^|8;AlRZB1|%0ft+cN3hjXTp4S z*=mUP43QUD7t_m_^5}GHB-0dWjm>LDAD}JAqE%((QD{`8IHoccsYEo)2ng#Yu{61B zAT{Dyu_zXK0|u5d(!88Bp7Ib8x|W;33Z~o@pzud|0}O#C8|67OU}GHkKfrSIV&#u(+8@ zTp=ro`rZ;M&&1LRtnn~OCRTu$Q$W|rX;hY>PnihD*vTtBl7XYZ>}v5~2{MKq5wR9nH{c$%dK| zDd>a{Igr@YnOG-m8j{du&~yF6h>`?l;;`Zi=%Y*{kOW{Y=}0S2I}Htjp^a#i@e9i! zTCJ{veM^nHcmwk%=|T$-?3~0qOcG2Fv>YO~8i|<4mJmXw zD_ZwpczG3TYoEzUOGEU`jEs7j5R(_gh_wxTLGdor9F!zgTLj7@I!wKTu+R<&5=(PAbM zNnchLA>GQTm*AXWJ54b{In7JKdYjW-P+~zX=ta5KBWQlQL_nprvca-ko3Ka{x$Scz z!PubzoKO`Zazc^$;;%^*DJ+%-43aHwpoRQ6RZw8du|SC7Zy~J)s!|rKJxc47**!*Z zZIXwm6_oZE4CW|ii3tKr(5PfoMZ(`MDrru1M6t@ss4g+!iTX(u&c(DMGjHBJB2!>g z>j%*k%_xXgbUmnWNl0I?gw~E!2UrBVLrrR;MWe2bCmfqv z7Y6C`>BMseLHQ+&xhA$>K?huIsId|1Lq+)wP{k@68wnfSI~s221|GAwRzzq}ALF?y zy^29{riz=AH;=7@u|=E#)s?gEV8%gBXg44Rrk#z<*>c8+-IB^|!rZ2^&{j3Qx?>Q@ zhv}{dwZ2VM-bf;=xs2PRn?WW?8W*~3hDIZEdlfPB#B4GIms~4s$Ry~8h)z}(2+cM# zwO+@Qg*;lZ!vM9}L(vd!>`n6IGIg|;3@O+_WU~?7pw`=CnG`g6*Mfzs7Bq$a!S%^% zI{2+g1T#kh6f|vYXzN0vSVP7TTePN{U`v*kzIL;e>2Y9C*K4KG(x0@cW7L*VE0J-E z41<4(^;=m+goB~znQ;9+us`M{VsNd7%DG-~ znb9PAK0xxPnq(kL8vvHJM7~bo);RQN^4X7W7{i)tSFeUPRCYD1)yv5lN5sua4UtN@ zZmvMp@N3gz0If;;W&U=S0#%0Wg^h`@F)9XT*>Y>udma|uORs72Tre#s!KEP*A>&OQ z)LCQoU}R0ysKE!ZU8#h87j7-yLe8?)0~C_JpKVKZkZF~Gln6)25fm1@qm3*nYAINV zupFf;v=TJMl)0;i{4-u5=s$2O>)jSiLxS{BNMmXMv1BQb8ifz|G9cF21tum}M7yI_Yg4u(Zr3k)eJ=BgSBo z`*Y`2Ck7z^w z)L8tNS5Fx)l`U{WGB2X~GP+xO53`_+!joMc4#zrSw}xSKVhyfwJ!$YE)CsyE37$GT|BjB%$BH{5^(!#7+^Mp@R)*lGV?_MG< zg8A1HhnWx2S2RY}K8#lOf~qbpQhhV9i5~9jM;*vTHi;8te5d>Cgv!~!2_!ep}44cplHj; z0=wy9t9aC4nV!6#?Ae#O6hxkO`Dniytr1=kKq~(CxpHcmISVglqJ-(hgj$J15?W~~ zM61*%Ask<~@+qZUSH?@+w!%%>>}5bL^@?~OO}~#@9Um#s8o;_Sx8!QKaYOjTT$Fi@ zIRm(suE|M6&>)LCpvCsRl*}0>3QO=!=auL@ z6MfT}CJN0TI^P5ZA`9=L(vmF|#oj__3l<(ZiR19z^YDZg=Qh)SDok=COqv#0WVI@v zo7HR?P6%*V+Q30E*7`^z@n=`za62~=4lA+k5JmYALG`L|eR#>bRqDd(`PK80B>h7w zw9fFdiX6C_y+2i?Y$6n@d`zIkYS^#E7E?$#@ns< z@RIl0OuMvU6jy6!Wo!KfO%22vjT^%iL`)TO+z2}*FoGc%(7H0EF3_8Z9Lbd2lInm+ z2l?2Ytqsl1Dm#L;4WqI*(N;5eG0Oe(55M4HguH8ZGHaYcQOgqTQ)O=ET4=_AYU3hRL%XCQT4C!5epaaS(4y zC1)u)^^?AQZWPl&yZ^kv$)46p<*Hm&WY@-%m1bEfE*erKGeU|;4xTjZRnkhAvp`VC zaw&3wLc2#z@g!^;94N9@xa;BNgC54bhAT?eZZ`Dx5bZEloKK66oQsteCJA=Y9p&Yy zY@OD8u}MRlvv75jDMiwN*A<^CffOKr73%1Mbq*|=ty~%*vXfUP3-N^^u3cNHB297H z?!-1lXf8YJP5xL^r5H(?jga(LS)3+9jSP3d+VH+$MrLxrlf>yycn`cN`XG0O7sFqC z{$gxAdp1-HmP8Ko+yQKG;gG<>tQ9I<6hK4AT9eQk38| z=YE+auQ5t8qvujyQLrf;&S1H(p~4g07*wDk&7f>ekcy0Vx+kj%tAN%MnV{|&o@^IRB6MPd5DWsUw$LihAkv1^W#v2q;ySd-t>JM0 znKzQD$GDgqy-cef`VZZAgM90mNm3y&;JVU%S1Kh#Q2cs%%mO{S5f&Pc)R$o|C z10k(lN+jChv7&=QWdG?ciWp38^81nKCv37*0!JX#Kv${knlJ@gSb!uLA-(WXQJg&4= zRBrGJY(P_nqY3KSTzE(bkyL<=D&dTAHqr_oVOC|=&Z7Tn;bjez#0nFEDZOh5Zx=k% zbV`M{F{w1$X*A!;U5~JQ-L)$W9klK;_a<__aHXCRCAGqCp)+T!?1nI*z))6c=`T8r z%GYI7oR0}W=Vs@cF)iqz4HFMehPh`fO<8KZx&oWYFnU}MJ4Nax0^%aMsK;{|@8W{1 z;FEVKct)r#&>|!#a6m_WXh#*+S#z#3uS47kwsxpX=-zl8stx^*^~fD}N>3Lf->5~@ zLBC=|nh1_O;z+Z%7o!h|GMlL&&1>%M(MrYc3({Bglwl)*_Lh}ZjIDAu(x_5qhs5`M z^;n*WN?OZws_XcbnoOYR6Rg(XLEF&P3MBa|`8baFd_x;MYgm0mIN$9Q$tFGz?=$6@ zlWS~}jY4y5q*-kU%d=BiG6`MIki80DHt^&gEfD(wRvcL~qUYo6Sy=Lf$*nX_G`87r z7}8Y6OA*5XgOVAB8b>Sxf>abWbkHnDt4bDd`R#a-x)uIrY2uq@no|}PV%J-eL};YN z>@-6D5a-rR>3EfpHXtX5nJt(}*KC0Vk+xw;qFFVLn++7J_=F$M0O8QwyqeAx_~Q}Z z@%lmv$$z3ROy;f-f~7=^O;!m3(Kw85gj#grToO%k!CW45Vy9wFvD()D8gXpZZFQ5_Uu=CcPMX9Gf6??wItMyku5bYt$9_@`w4)bS!51)pI*p{aIcST8RuE>7H9cNM@DB1mH#28C zcMOdK=VrRv>BNl2nszi~-M*BQZ#EW%Ud&E9?4M&7j?VqtibNs;vLtm*D=K^dmX@%_ zqFa})LKk;eHWrZHO%s&63sG#9f~6z+D2MPMW-hN)jfHX|D9;kY^kDtFy@j9RTGHsX zGR9IlKd;uieEZquaK~YXBZ_T)@?g+mBz7@@$A&8ST!A~Ave60hVmC}IBd7KM#)`on zX;^OyLe0|^0=@dS3!JUIVOtJ@pcDR@;wqI2Rj@`ZXWx0DYrB_ej);(%t>77JPLBE* z5@9DdqFGA95N#I{kOU7j$drApw=bO78ugLOt*N#YHI`2eiUBdoZ%0ynZIqJjyZ zlsV>Sd3XkcxAGR4G)&c8azk&R`#+kv{?E-CO_||h!jSC~C}0*^z_|N)bcEQ?f#%}M z*W4`q2OXZQDK@--Rn08u+7G*_#UdkgSA&K+X{P{lM!T$Aq z?rBj$o&x|H0Qo>u`cwgR(0D=pwScN2uHre4)o7t*M7zFH7CGEAfO%bTS1zcXbt-Rb znJd+8Dwm_VeMg;cSrn|D8-<|P%#QBC#pE0pt2U%j8d}I}->!yPZ*7g@(C}t>(3_ia zFpL}?GS|9KZci|0nHWtpUm`(Ii5qZxI8eShPVApiBlygvauEEi%VWa5;i8Xb#qyOk z=pm$wa8Y0wSBS{JNV*6bB#l>3kFE}aSl{NfS2f0FO{tefJN)yMm8MiHI_hS^=%VnW zyB-fc<+M%0)1;utggK74PW&4nwWQ`)sTJe^!CnXHlGn6E1{frwKPN zqpF!Vuj=_AkK)zq8T)$QpKXOyixSn}wgZS2&;c(^?r{C+Z9rm4%pf#bkxrF$cZOVX zqXiIN_~NXJxTcgae$s_hT~@raw6&f=z0hyD&eK z`e;<-Ov$g(Yr#w}`TsKT|lF!gM|lMk=J06cNSRlpzRA%$d- z_LRYJmSj@hYYd&h0(8$v(m4$?C)yk9RWzo1ENWcj=l51S9C5`W5$dF4_kL_>7aSMX zC-$;H21wm~i~tklk-gsUCi>@0gbg`Wh?UvFXJNc7*%4=f>(v&F5Y{43& z&^MNfg$=`{**$Dw#Vj#b!Sp7BD#4hTAU4Ii=oq@ojI)aMB0a+}r!XYfEbzwS%i}5w z<777FxiWdJL{t{nu;SY`ghXEN?4}cX()XIQtC;t7a6Kr|k!^$Y;&T~%%!CXReE>zw z7G6ZaA<-BgxD}n1F18t|1c}2Yj+YGc-V3H~Ml7xfZzI*HtB}33+vrLTRpU zql;HyMG}*SwSiYX!1(P3^38gf4|ISHOqJ$D+o{khaJ-acnuhqGsglIk;Dl_Yn$u>5 z&&~4I37wmTpu(OW?>v}OvoyaHN!v~!bh?v+YrBy>JdL2k% zbB4B3t)PuVS}}aons^IU-r5{eI2@FnyB?-317qa77VvAgBq85HiJBN)F)y;1gqo)b zjFGyG3QsIU0jTG_rhMVvsqw&C0UkB5W|FRt@b(}UGJDXW_W8~;h24H$Rj+4RmuPJc zN9piJs#)fqE5~q9+g0`929i?T4Qv0LSCUqC7-od-UZtj=f&~g^EZC>3>em(SY*^ENDwCJ2JFKMN zT7cT8HcbldD42(F3S?qYuCtKQYhKI|0p#DVz~O(KfaZsg;I)!0Tmid$Ls~=7@!lqY z>x-aOqgfoA=$h4QZk2x%Pxdn*&1HDR5NMl7oIczX(HxTpYaFvK$MGo-8PFcReOlRX z67Rcg=Nwl)OGQRNGp|*wiopSqtkucKA0tV4Ym9myo=zjMCCv<93s&vg$Gs9_+_ds_ z?fP&%+z&J-P_3K6r+x2j(Zc3^b(uAo7_QdKJH=RKP-*E62`?MgH}eYbtpjtk$~=Su zOF`dz*9YnIs1BBaa^JjCY+b`%gC7Ye{mH!GwMcvnKo;kj5`3r*A-%w&Xg$c?0}bmn ztXiAN{?Mzd%#BCYlq;dV$fbqzFuGppAtx z+_!M`VJ@s^yy%m0K~Iku_QOX!$#+igYq_zmN+9pGRe1%m3iF=RlAASNjf8LAhTinC zO$PEw(R({gz@kF(85@;ftEtZ%l7U<#^-WXjm^-!PG}OK}5@Y671QY-n(C|PuIEEM| zC8!oWhz=JSy%Vd<|2`OT8tqVYH?7Tpp1C&pH9sm1*^b>4IB&8&&-9`0@fZ*p&46;RWxv$@U)|s z{xzIPj~`_38d$o2q*syq5n(nLC4q`#!&=dTPDSfaJGxL(fXyj9?uQcqitg2Au;t9x zgrE-V8!=SW|9#K%1_5!wyPc z6J0?eN3qaqmbnMJJNT$*Rmis=ISbx$cPBG_D^@lzRY9$C7#Dr6-1?~S=ujL??$H(TW>5!x4}N%#4rR`=k=xL{$&58%_@n?{etf@2(=V#_XxU$?7$Pcr7=zc522cUGh%I7V40BD?m6R z=|4uK!aYJFK>8w*S?~ND@35z*$-p+jVF$A86f*~ABMKg9x#& zq_}y7D~NYPX~T)la;+E@dft(-&Zh))3$n~H3M1O|zL#O%&<_oINYew8=7%}7B=8-j z89y5vfaNj_>@d+RiAGGLarHN?T9e9Q`!M*@u^efUcy`K*r$lbBh%OjTcfqhDp_2B$ zgs9ds`i#kIVHz8JNUAj{c#kYe9b_Ysp5uY_2o{Vek1<13E6;T51o_MhQS13#Juc8#s=SS-|*%cZlm7hFJg zEpER?aJ(o+Pf=FKQy0XN7tk}eIM&3+WUJe9?TK?uUtqdp!_|Cgq*}b%)htPJ+la;| zQ-|JwfP#UB3m-5sbrS`KF`zUqxTomB74pZ^X*)P0GQ8D;vjwoAV&v%N(bMFb!+az* z@hJ@%g$9{{GVltzw27TEEN<_h5o_Xd9636y|*YnyJJcSR1$TrTexlvFO&Dcs=C z=tm%Mz)|0+;ql3i4?9}GA?n5VLdZ3E3}KUyECi^~c(IfK!42M-z_~>CO;RYbA$J`q zCR%7o7!DDvIp3=plQwd#6fc_gKD{qralI7iyYdVjRW z3LgTMgPdz;DWV)2v6+ABIA5IFx;C!wY)Oh*Ow0ZXEabd+^VBAtKY;~KA_YXj5(Omf zzFDAcG|hed*n!_!>L6XWQpIxlO$dtV3fb6>3a1eRU5>AYiER+8B*qi39b<)x!OiYF znmw}+LEA?01`T+ZfJODT>S2Cgh1aIB=4eT11IGAB3<2@>rgR_H5G@h7b6qEgsJ=Kr z2P9Ouwi51d6Y4M(cBCx{{GQD5Q#4Dlo_7>~5!ujsM#$aiUa*s%lH-<#YO>HS9zRGS zvQ{-=`NFW*=`0DYZ&=iLt5=`$a@meStN7}w$}A>kbqZsbW>(XLUWum6 zvoIt>iOA*67CjoyY}`YI96lGiVmYe{`R#xs4`T{;34ETQ9gq%GVR9~6D@Vtm70?Y7 zZSz?REE~=ovbJ)JmW%3=wc?y|^Lg4W7y_%b)@pU{TTA0$Fop>1{^&a0{xDndmLF|7 zkKG1`%9~6n{#Q=xxjVH=?jExOYpxb6@zaI1M+5s7+C&Jm)qqd3auJYWLInZJ-KDJB znoVZUrE^~TzgluQ%7{op^U;K`UM+Z5#MlyLmd(02fGcJjuvvVwCPJ zCw)bVqx37IUIbH=_epp6yf2wSnlkpX@>J?%ZCcW=Sg@xfoXY5l=6q%tUQIOl?pInI zvGmXn`D!CuL^R-om?hH{oxajENJm{Ygb}2T#A$8D^id}29f*lQbgDg-G%=)}Gs_JV zMDm_U4jh*^s^*7l#AJyif z1yo-1lr1Da3s5@dV2)`5yWsFBp|J9VG2(&%ec&7?)L>ebq+V;yzSgqma50c>xuYyS zp4g=Q(PEY_nn#C4p}i{&8UH6o&0pxI3g}C|=t4JHKwmQ46MTb>?x=z{u|tR~60}Em zDA)q6L#J%dMkT*y;5wH)d#FORNxg4$Wu+QZ(!C2yoE)l>9MO>1f$8iNR|~BQo~v=2 z$+mUmqS)I4HD^0wXeZCX0=&wu2c`oeLN})O7Wf4CdOnmv6H!xLrq(yCUKU=xHJxTf`(Au=y#P4rmZlB9&-uk){Z25q>Fs29r0^Wj7RzE=9g; zJ_ktenw*Wl^czfz@t6KkNbj5=jh;3sEx}*BV?uCXCPMtARp}`Xz7g4oNcuZ+AJwE` z2IL=<#cv;dMTJVJMpcSRN*YR(vl9tNO4P)o_#L>OGUe>U@3I{^Sp{cIQ_j|DfTyXz zOJ^u&@tJDEQ)jD*jc-;Z-#SN4sJTd~rxvI{)j~Dl>{_KBxJ;GwT%jgbELA0m6>4JQ zDm7uwDmCfZwW{>_wJNY^gDQF8TBSy>Q_kM&RUrK~s=(1Uq~EG09!jW^!|iIqj@#74hOC;fXN#IR+NlCFZdcB-+f`uC?Ml_W zU6s^rRi%fwDyR1jRr1oE$_d_$dhb?&joW~a?P|hv_oHupYQpsQDK+#y75LWsRq1o@ zSI(XfDs}k7%ISPil{9`-IYS>+s_J8E!oG*m=SNf^^l?>Ex=X1=yOa~%h3`GeS-A(} zM|j|qs`St&m3sD5=>J}&Hhx+;rJun(eoi@uKd02PCzMn1qzb(9B*rqRO4EZ%4G$`( zVZRCte?gUwd_g%2zNkuX_#(#qvlymUMD$qErCY=3q^#7OmG5KF%jK9YCUr?(51r->1QK^O_ zDzNM~%6aNH7~8+8!1mv(lC3YH&QZ+SE9lFsYQmzR6X-wLQ5B~+r8i7>N**}NnOOBE zr?lo=XJXGBXTq}ijvBqt2~4@zQHw8jO156&Oqg?-qq;A1CaqiMsKKk8!1T8`CF-qC zpzdnNX}j7fnX$o9Q?7GL7hLD4;p-e{>h(^^qK!^q-$tio$IXbl#c`^_j;d>PoFk2n zdNksc3^qA|2bvwHGv=s6El7`}zFVD%+c!BXop79ByAyb&-Eq>lITK##K;4@i)wtOy z?b+;19zodI>8R)(PHFlM$JucQ#&f3=c>YeunR1s?I`uBcS&Z<(yPUx4Zl|Q?9@KM> zqa0dCQ+c^zc@+N?F;K)n5d%dG6fsc5KoJ8)3=}a?#6S@PMGO=%P{cqH14RrJF;K)n z5d%dG6fsc5KoJ8)3=}a?#6S@PMGO=%P{cqH14RrJF;K)n5d%dG6fsc5KoJ8)3=}a? z#6S@PMGO=%P{cqH14RrJF;K)n5d%dG6fsc5KoJ9f{TP_~Am1BSdq1MXwjLd>`6dH*Wt(mI!yn)4yW$Y;ojXkT(?Ju`Iva}%lba^7bJW}!tY3USi+x4 z_&W*zUBbY#y8I*wPnU3}gcnM6e5-^TB)n0=CJAqqFe~BPCEPCI0}{SZ!jDQg zAmL{u+%MsmB>awqhb4SL!j~nS^c6jxGbEfN;YAW&DdB1f-zH(JggFWCk?`FTeni5@ zB-|_ES0((Bguj%~%m?*`{?es%`eWvUW|RKv-YRA0g)g#O@n(ML-aGv@`OQ4#$D8># z?V9|C9!$KUCPNpdKPKMLhv|=rH*{j!HSvaC^5YHtn0hFk{up{P`Axi`FE3tW&fuHk zIYvh-ga4_B=Q%_#H{AS&l{+Rq`9hrEO->fTxHgT|Pm|xQPZMwQoAqk?Y4V%(YvN6Qvz|>qO@6b!O}xo();oREUz6Xge-m%= z8-8H=Y4RKXVB$@F!!JxfO@6~aOuWf&_@0S3`3)a5@g~3FKPKMfe@ys|i8uKTA2snN zzu~JU-sCrY*2J6qKvls%*FCLsY?AQr1G=UkNjO2jqoI~bSg?{}WDx^J3=}a?#6S@P zMGO=%P{cqH14RrJF;K)n5d%dG6fsc5KoJ8)3=}a?#6S@PMGO=%P{cqH14RrJF;K)n z5d%dG6fsc5KoJ8)3=}a?#6S@PMGO=%P{cqH14RrJF;K)n5d%dG6fsc5KoJ8)3=}a? z#6S@PMGTw}2DZLO-|OpJ_Oi0)%;dTjoC45lA0SWg>I3(e>Bs?VH^AaAF@D&MzJ9K}iOBj-{O2S1F zE|YMbgc~JnlQ1V?w}cN!xKqMMCEP3FQxYDK@Hq*GB|IYGsD!2O)BT((;n@;aNVq`4 zItf=xc!Pvd3DXj8m9R&`9TN6SxJSZ437?hlpoE7d9Fg!P3Dx^${w16t;T#ETBwQ@v zN(mbzY?Lq|VW))KCG3^(K?w&W+$Z6Xgx`|zkc7`mcvQkyBn*B)=3l~)gjEtQl5m-X z>m=MLVVi_G3A-hHK*F68J}Ti}37?YifP~LUI4t222}dO?-6``g;n@;aNVq`4Itf=x zc!Pvd3DXj8m9R&`9TN6SxJSZ437?hlpoE7d9Fg!P3DpN>{w16t;T#ETBwQ@vN(mbz zY?Lq|VW))KCG3^(K?w&W+$Z6Xgx`|zkc7`mcvQkyBn*B?=3l~)gjEtQl5m-X>m=ML zVVi_G3A-hHK*F68J}Ti}37?YifP~LUI4t2234b-D`?v2@&DQmo$KwB8((9%bPA~bA zF0pcY;q=oa{f5(ZI^B1FJ%6VDtFFIpo4=HaH?gNnzUT7ty-DItzS;79I4@s$UcM{k zd%<>7Qim(@^4%ogy?ObX^73`b_e=Ns>$@{A-}~iz(L4P4K9ZO3GxGglUcP_G%l94m zF8y17ec#K=_Y?VU%gZ;Cm+wF1doVAbI-E~A{`SWE3QB!7_vjq2mp;a8^-AeaOv_OnDeo55?&*EPrpCI;fMX-|G&Q)KdyT)Bw^KK z`rGhP4@-JJ{pnKU|7-ePx?krp{T{ELg}>A( z`TXaVFOJ{FUt>z$(|?Vhj4esI{Io_I)?FZs0&M}Ml*^_NoLcvwS0Nl*XGpPqO`r%#pi zjX(FNPkB_QpDF1_B;E9H!^d^{xsty3d0pO&Z}2gleu1PP{Dn?8<-hdzI{iXPZ~T=% zy?&QYzf984e!-u9&2F8(+?4-~KYj8ZoxVZRr~g)`yZq~Moqmf+Kk6_4kfgUrdhmDt z^ubT*^0!KQe_r}GKdaL_C4J=gy1c+}yxdewjU```R|o&K<-Z-3ce|35yZ(|1Yw)<65p-}nWcu4Bjlcf4HD zf7dDbe0a{%@k)%}Mlt0-kAca5(D?59qYnR8LQ|tjpY$jHyv&mC^YY@q^E-dOjHGXo z@a=i|P5tlAi+@4*^nXZrOu~t;>iSQS@Ei%}N?0SI>A%rm_BHEr?)o3l3v$ZIDJUck4Soc|Gy*YkG2&q|6@rXj2BM-jif(t zYvJ^X$8>znro!nnB>jP<8YeoJhoAE#ea|@Mjr=-0QMi61|3)t@oNnZ6r<6DR)%3vd z$3yM9e15v&f1y<2^uLk*4@kPsWopMemy%wUF5JK6l3sUP;q>wNvuR;G{%g|LNc~eb z>iYBP;|57@ko5fVH%a>TOyT~GC*Mu`Mn6br3zr{HzMAq$X+K{-FzMsTKa-x3@`t4T z{PEu*>A{@tU;g;tE$OdFy3S>4SL%b3zGq9}@jW8x>CVFSdo>k?u4-7S)~{Wv{_$73 z{69-*a+~-gfA;sx#2bH@$-iIt@1ctd_s8((f0BGZ%;VQ{MIKC9T)4bxchAd(`+J4- zbD4yz^7_3~zGvL1%Nu>r^xw#XQ=}i`^;4HNrN$3$5kINPaZLWyyf0v)PAKfWOSibP zTZd=gt-~V{4&9@_d$;K@x?P8D5*}{U&7|dfhlCaP>U^bpb^0O+pO^R};-8(fPNzpD zeabs@xv3H^mheFdhacDZpTACrtN&K#uenc$%OuoaI*#laZ`T5nq=#)mO(c~Mw>%^LvarcSkJKwb;_9a(_?ZRKin(%M)Um2GvpoljqlXu8}Ikm z^W1y%_XF?u$D6NS>6iI7VPdC-LJ~3kL6f4x8fn)b``7<9-uztueBt>y_~eO=Y2#qw zd#WVqZhkkyjrc1A#+#q}exXwq<@NXQ3F2ef7xD`JS3m1?1!jH@N&Qp)i+=t8 ztLx{@ztr<1XmPPy@n6J;ThZ$T9O;OepB?{A{rGE{p97yOjF9ox&$Ti?`TQ;YpWN5! zd6(9g4oO(JH)CUun*1_9Gal4E9+rMg9nxhQB+S=mbn1WqzYCuE@2|5^jpNPFH)Y>t zAg@sAxAG$Xw?3Eo8`}A8|MzQst$TN#KAr#l8WmK0$Dd*QA^-RP)Yo|P^I7rp*5x(4 z;O6m4_%jZlU_O5-rKaAZ>wh+q*G^tX7Ky((zx;ULM|b4S&v+U0;%DT2|8IZhVZm)a zB7Wt3{ft_}4_A8;tk)Q0+oB@L*3!|VDly$Cgo1U4*LkN?s^3U63~PU;5! zEB{_aHR8Xes1twb--Z=vtfKKv)aww2kv@Qa1G-`TG7O6bkq*%S)-MGV#D9`L6}?=U z-m5dO+i=wwuyg&&bhnePK(U(j%hSPztJ9r;IVyB_~HWHOO19nEp|D>{_vrHP)3Eb9JmemC27>lggpWM65lQ%*CR4CDv}pkJuc`v?Nkn z@}k#wBy;ih{H|{3%I0G2d07b|c~RG9W0~tCnPfcK>W%ZdNTMS@0QF8mAI?w!^>g`)jc?|e`iVm zp~=3qSTdH0H(wRY(cGx+{x8UBM5li=*x!E+l^C4H|4y{aZHOgWR^?(Sap+5?1gCFE z#G9pGw~aU))zE>du)0dvvOcd(#+y@7J0`p$o``8gFN?KAIug0%Nn{aJ5*4~M7i&-F zy24qImBup?!z&_*Y-~d=)6twG)Kk=&RB}v=O(Oo>4Vex%_o;Q^mX>%pM}3Z_I+_wO z)qUVqYshXYNGQS$x-scg9Q4O@?#5*iHGd)Mn2N6XVlTQ-bqDh!7cNlUGx8%BsqP`^ z!7)OyTTO>lUY8#^rf$kPrf!NHx9-iUcvN+d{?;A(X+W(^50*-7b*ec6& zPFR(#Phq{ccBDG8lsLmqTpvrsB3TShWkNb01enEQOSOY?Kzd7((GBt2IYR|!sJ}Xv zYfDALYhqidRE>^|BoYx&P`JJ=*1U=07fF0NORE{HaGfC;m8tWkNkuDlWQw?3XLw1r zzFvb=**aeubtRZ-Hgm6X!5 zi=s#DXjGFxFMZ{XdKuO>H+N(*F)Sgh5-)mvESpPZm;}26jCdLwkVk|7Fv&FE<4Yp8 zNY(WEVztb+^zs73wbG$y!B{ zA}^U1D`n}?Sy%;Fl1|5xjI>@WR+iFED+)8uvY0C9mw0eQ@C?Hk2gbyjl)b)`4OviQ zn0qqlOSm?fjkhLaQ7UGN4jnhg)sZaoy#u~n*Fm(bgZ6=*9<&l@R!y~s3dEbV;k-1f zzax1`c8{sKqsK`%)sN=ov3lEG%5#TxtQ9YsD=+G5u=`XqHPb7RT4`GnjbgSTZXgqr zjMkm`>E06T_a)V1#pGfaQk?@jVNFMS6Xe>GtTpPrz9ch_K^-ef;gT#VS<7PC=1e>d z#z9pM`Et~w`xJX%OsvuFQPq-6EAz&K{!Aobu8&1Ft&g>E@*!U`E2*e?*cU5mD&j}- z;~O$i9;h!z^HZ)(x@n{SG{0htvfUGSvSj>M>2$0TR(z4(lb6m39?$E|PhQFz1@&rY zeiEb-BedU_uriWmnK9stU6qV84ea&DW>+zJG&A!Y^rd-pHROw5A4_wq2mG<|<~C9i zBANvr950b9I~vR(Uk1UChlXK)vR<_#I#TQYOQN@SWOJ*MQ5XU|`A6;4Ym;f2%TbB$ z%97&aQGEAAo+pbbdlI^5Y#Ou_@Kow~@7P3CY?$cIf~gkUr7HUEOyNlLZ5{DUER0pm z)HmQyrPWRH#U$_bC6n&zCJp+MGFqKs#?Id*BC^ZN2;$41z&`uEEf5?toCpaFq zBaMbSB9Ua$$TC=BIMq=rZB=$dM-v%7gqcw*VLik)D~sKec&dyAwbNB)FmF*5yDM{@Dxd`jNwPbLYl!d0vK$0oiNMgVm0WszKjiVuv<;Zg41@e(cN zebAo=bFn&>Y|Zhq9rC9!Wbx6_{mVnFcHUnt=^ZMh8i)1<^+iM4sh zwMppj&6{FTOeS@EQ1XPAcEl4=_Q-hM9Wsf_Vy^H$VB%JQ$`U|QDnsL>x(=EoV(pgc zb|^^3aX6;ETo82nyB*q32BtC<9nn$QG|b0b3mL`~dsJdwW4R>~XRbU-2~D}w7H+qD zGJV|+&k+^sp)VNUM%m!cL`vJMJLxN$Xg1&d`rGZ=fc{QFtE9U2>d$N!n|*`))d2$? z$^pSGZaoM1GfL&=EL*bv`c-Sf%Wr7du!gc8oGe}w>J&vBawE_U=;C3wv%~o*Z1SVX zh)IA>oDMg~ndpz^M}{|Rav1d|NGpPn?lS7RMl|)Nhrh|SfRA4KfWaksck*Yfy}3;b zaP!&DI2bUW+E(5xpY5s5T-Ts{XEmh_(PuixR!+DzmdmDN7)P9E@PM7RDb^KkX3dVV zXD6X5yVxO0LtIk_<71dVy^)RZcP<05L^T~XpSt&>=2Q2+`y{F>mx=26?UB!fCWK!3 zOg8JnJLNl<>fkBur|&JzS)H9-Fiu$Un`|X&`^lRdT`>hhV4@}F0Oi@q$kiDSiN#jFq-a}xU&1CN1Y>q=T7gP_CU$e zz|l!N12?=fp?}hz(w(K_+06cz@Rf!k{EJ^OP&S74AUy26i~X8#X>4SQc%Iu3NV(x~F7&;H8rXCk>YjOxoi- z5C|R&^pxx^eb5;xX-xD?I^^sq87g_SbV1c2=lPO~T)(s38F8i_LCy!90jJ*?cD9!^ zbUS#4cfr|-wq-Txnq|W!dnN=|&lzwII}Zj1N)D8sy`!YpIXu1J=|Abgz)@!)(0Rn^ zb)H2}drO|0@a)8W*t~ej+36ezB#t_rM+1i@j+XQUADuK<^3qA$ONJ-xbK1UD*5m9g z+cyR6Pu=G{>KvSUV8Wv%PdTH`j?$X#&Qs|9#)GGBFL}P?aOn|esI+2u!odkc&dMVW z9_Xpqy; zmV-y7xoC_?&^mTz;Ae4 z=H1b!Q|F5(&fs&%gx__%ExXs{#D=Hu&B0Gqv zs6YdPK!fZmV^Q5*O_x+xHC@%vAhO9IA}a11j_BZy3WB>L3ZmnRiu*FCIPQulF5|-g z-i&vvq65G4{m=RG@TmO7i<^;aWMpQ<>xLLd&h43Ie5mnP#@&s7IK<^2V%+0U#}kbo zJKXVn<8dt=UuXQn5sp_IFYf60J>!nYJKkda*a?nz+}o{R+Y=ohYTUG!OXE^=- zs8qPW7mRh>-uUOgIqqk?>@3F#Ks37JYuTjH;k7~cl?pD#^wKLtnv2^aUF1fYrK3@V~v|{Z>%`1CmAd5?qFlZ*ZrHZ z;^?M~6)*Q(W5vb2*jVvzZ!lJz+k1=^&-O`U#jSnWSn+8;GFBYgAB`2?b(j6z_70lo z_D>UIjdyQutnuybj5W^vIAe`-A7HHU?x!1TJbTJmHo$$C$5U}O z_cm6%&6dWByV=25<0g9;Yy9G=#u{HZ+F0WY%ZwFYv&LBQG-ntqe&!{{>o>aXyTSP9 z4;|lTtaz9!jTQfLwXxz|zGAHSmK%)~uj*4{#jo6Itaz0T4tD!%oW-Zy$5`(|~`@g|QoR(#1*jTKMw3}eNQtTa}<$gHvAL(VW(Jje@-760){W5s*CQT}ai{q8hw zy%SCXOivhZY~c89;|;qx{?2&J9*%cy>h?$XzdAm^_$%Y1j7Kzf{_e($n>Ze5+<9SCr|2kvE)B4bOaX07x!?;g($4$@< z<@Hfy@#?x6D}G&XW5w4x)3`?ur%xHLHm))HAx65zib)PiWINz6yHJkvEnyQGgiFDON2&^?rB_YJjnQ5<8tHq#&yP58((9* z#CWOka^tUzR~hd+V~1#cpEvGk{I>B3<1dUA5APRa#l>sX!tF1`$2-7Saq?OlD_&lA zW5vzuZ>;!vV~iC?uhLj?^rjmtp5A4~imP{*vEu2iGFJS&SB(`%?=xe?(<{Pz1J;L% ztGBnY;_0Kii#9xW16wzgMML?xT$rkNZSp#or!kta#gH#zhuCd$O_O zY0ooO{Ol`@pI_nDcd4=BW3M(=JnUDE761BUW5v7v-dORicfxxeZlB^=?_;d^)rT9` zJ>kmlW~_MdeaVdw=e5_4(f&*r-)=nLxPP(pFEMW2-|@rJ8^27(yEnxneAigd}-X#c*i5%_VzbE$atJ_XX9$)e#Q%pPdC2Z zIAgroc$%@|e_kT}wQl`xGQPt20pn|o*BLjy!{vX+c=O$kKPT^w=Og1^$$KDf$UV-# zUu*E5xc-e>8#gl^VBFexv~fpco~3>WjS29VM`xz{?}~tzfGdi2D4PCS_1VlpeS7}b zkc9Pswe*;?xP*#u$wU*mB=A$;!1UZI>cz}T{m`B8A0{&OzMp!%pWgW>rx#1#CsG&m zzMp!%&*uAWmM@lmM4--lB#mcGF*ZUJPjzwf7B@5}lAoO&_ybNk0Sd4BtS zKlOUQ&iC!qi>2=o=zTx+dLPgC^VEx_ZyV@+KlOTl&-eM%i=|({8BF{s3H;RSeLvs- zQ!i$IZhs@rVB!z;zMp!1AK>=_>cz}Yy*>Wr^}e5aeNW)`1?t7p55h7yky*a)r(WM5 z_&tJpG4r$h{xD8t>U}@;`d-2B7u1WHpZc~9@E;~J^}e5aec#~s4(i3sPklLz6PbG7 zPrbf}@cRh$V&w6KuA5kxsegp3DoX91CpL%^?;`b)%#mvuDTevI!!$hXu_fxO$QT#qdy_or_53gU} zPrbfh@p~5aV(BfMeZG9(Prbf(@%tC`V(B*r=a28FzG*M4Bqn|@qh2ii0i40apOU~& zy}qaM`x^CP=I8d0fpH>J@B67=&^vM$@p~NgV&wQ1<`d-KHchrleUmR?| z@26hh_xQb!da?9vgYtbp_4*#j?}OBfr9U9h`+n;6{gB@isTWHhZ8-J=xg_vMdi&nU z?~jq*So()JgDJ1~{M76FB)?ZuFJ^x3|HZ-m*Y{Jub`+OhRK)L_)QhEW7I_Qh`+n*- znV#Q6sTWH>A~=4&pL%^S<@ZzS#d&?O|9wC8`o7BVt<;O959=S_Prbg!^7}0HV(A|W zw%_+tukW|~o=d%0`qiAl#GjJDPkr?`dW(wq{g-+%^Yi$n=)^>(-uF}g-Z*-Tiuk>l zdNK1;zZ%AgOug@?eo;Jf7xDWt^ve!r$(%=~|*_x;rC`!>IKQ!kc&DGSF$X8FFKdVLS)_i^gQ%+K-#ysr&BLxe(J;atM8}2Z4#L<@%uaVV(CBQ3?}}teBV#KzR&Y}J@sPdXZc?R*N^X~ zUf=WieV=-<^qV<@i9amg_fxO$|LhMyy_i{9{!q?f;t%z{pL+EdVE+N?#mrCrHNpD( ze(Ke~fc*`q7fauOGnn|p@_j$`>W{$w3Dk?3pXEOk)L*`zdi7sme+KHs(y!(WCjPK| z-%q{zJFx!)^E{G`<)@$ej!sV|3dB2(}CsaJm-_RpbS%>2}c=b!JVUj292pND#} z^kMnFpL+H8VgDcM#nKPOI&dO${e3_6>L0}ZLez_ypX(p$eLwZ;PsIL3)QhF>gJp0c zvwYuAz4{;RvY*q7r4P5?_fxO_O6kD^}8{M6^{{T!Kk-%q{zPq9B0^YUe(KfViv6#s7fZj9Gnn|p z@_j$`>Yv5_TGWe~pXJ{k+`oK3_3F>X{$13Ir4QRrzMp#a|6+eI>c!IU!5K{a;rjc2 z>eXM2{l};mGe6fqtp9vJ_3B^7{$|vRr4QR5zMp#aM`Qmq>c!H};|wPLaQ%Hh^-CAi zTU5mUY}AXHpX=W-XutS=>fgJW>Y}1`rWZ@U$hq_7`+n-{{^>HXzZ}aKOCPo$eLwZ; zPxl|wi=`hIl<)hgUww_fxO_fb1Vgy;%AyID?5lC4rxM^&ez^Lh8lL&*Q(EGnn{8z3-=9{SDdwka{um zQ{OT;e|$gn4VEDjCiYjPUMziA=gyb!`>9udM)q%{UMzjse)Ij*ms|Pl4@tdP`f&gI ze(KdCaPyJ^w zPGstRKlSQQ%KoL)iU}@;>VL}qsML#@pZc$0oXFJse(KdylQZHtHmVY~p6PbG7PrdrjvOg{LV&K{Ri=icZ6{w zQ}6q!SAT8x-=<#7{M1*&IFYIM{nT%KB61h8zc=+_=Kr(ueLwY$S35oXhf^<>KHPra zPyGnfvp+fYV(B+>1`~g{{=T1j^*3k#bLz#+&-Gs#-2Z()^&8hB6DIaokG9`f`bN&3 zFJJlTr(XTp*}t87vGjuiz3-3K|7n+j{o$kaHRf|GIj9>eXMK{pYC{Ge5Oq z{pGRyb< z)N4Eh$45{vW`35xcxU{FiA=rkr(WYHIG%!fG4oR&o_zUXA(r?BxIFVVt z@27tKMtX~iI9`K#G4r$h)||n_AL@NS^*?_Yxr;cygL*M$BYmB7=k>mydX4|!co6Ev z(ueIo-%q{9i*Wo1^8 z*Z38VXQ5uq{M`QwIfIEm)cbzw$9)>Pi#Yy;dNK1;|5#A}`+n**K8E9Es25A$CwPAL z{ZaWgo`&OVqVkQUU+Gpazy2D3LqGKzf5Y)O)QhDr3fABEM|wNKIesV78%zH~Q2xH2 zpL&h&;dmeF#nN9B=zTx+8V|(rLDY+-@6Q=b{3!|i)NA|@#}iR6W_}*Ou>I!ysn>WT zjz6MaEWK@RzI@+Ly~ZbTyb|?d>BH;S_fxO&OdQ`ty;%Bc&S2tCN#Lhmp#cyQ?Kz`9M46)So*O4hVQ3dtApZjlPaQ*sz>NWn1c!HB&o928 zdW~=6csJ_B(hmyupYNw$)*g0Ajs7Fe(E*; zkK+NU7c)Qg;qmwV)N8yT#}863mVODA!HLZBeLwXYU&!%>)Qg#)VJlDB2(}Csn>W)j<2L%%&gS!8*IPtr(WYPIUbXGvGg6W3{GU0@B69OcukJq zq+ZPYEPoD+6PbG7Prb%>a=a(?V&FuT$OTRuie|NQ@L<7ZjESo-k!!S_>NWaDc&-j;f? z^gV;}eLwXYkIV77)QhEWjYV=Imjr(5HGY@ld8rpOKes=;fBJsvHQtxwf2kKszdl%h z-%q{92XnkI^rdZLy~byAyf*b>=~o8rSKm*)#&dIgH}zua zbDY7%pOU~&y~clYJUI1Y=I8OB2IEAg-uF|l@!~~?IlWl=jTB>&-uF|l@#P$E&ho|5 z&vEj+-uF|l@#q|%PQ6(AX9K-(u!`~b!ipk6F} zHI~7N%<_Fd^@=yZ_yg37nV;o{?FZjaz2XxvUIF!D>Bn&f6MtB~@26hz3>e>ldNK2} z{P6vU@26hz4;T-Dda?BLunbOQmhbziSG;~%l`_-NqXN;z2ZIeXy)`{=_@03 zLGSyiSA2+prWZ>e-amXl^@=BPy6MHzhwtBfKlO?~!FUur{$lBu1??B#Prc$*oMU>i z^lOXUL@x32lc`sHi+Rm#|JBjs_80g6f(8-h_rLN>|JyW||Bhz1{>DtDeh(+l>y=-6 z#m~6k^kV571$y7l^;f)&CrvN@pY*<;dd25>(ez^J!}a(5)GMCH+ol&w-?+$4t$Ed2o#W0KzYQ(t#3Jw-*oH*|Gr z49gcw-wgYZ6S*YtQ?K|mj8{Xw7_-s-56k!c)GxK|XM7v##nSi0ayXG$zVE00E7LD; z?(|~j=kae1<3y(3_fxNUIjfpGy_or_Zwli?rr!5culPEQx5M=pV>Z%r8Jx(}`+n*b zkLT^?PA_JDYD2y6r(W@U7|$mGuhCuK8saJfU zMlD?VV(G*3eLwYzCv<@6#nOl6`+n*bf2g(T#nOl6`+n*buc*7}#nOl6`+n-n=P)Ps zqv^%cHwgBh@26hzkj9u^EPYsi`F`pZKdI96V(G*4$M;jOcuS1G#Pdfi{T0Fb`+n*b zpNa9Bs25BBSfKa))GMA7<2z9=mOi|GeLwYz|HODu)QhDL_rLF_Uh$$BKZ<&>^kMtK z_fxO9v_D#o*-UM&44&S2tCN#Lhm@va#E zih42g^W51N#)(Y5@26hzv3i(ZEPc5Dd_VPyr^WbMEMF{rT~vCZeEAuFs}&BeAAgJN z$KN9R@wdo+{4KH{e~awL--`TR{4KH{e~awL-y-|*x5$3{EwUefi|ohWBKz^T$bS4S zvLAno?8o0C`|-EPe*7)6AAgJN$KN9R@wdo+{4KH{e~awL-y-|*x5$3{tyZYa{Pe3?MfT%wk^T5v` z&+;!cX8bMs{rFqt&E{wPEwUefi@fk!w>~$S-;cjVzaM{#toU1ux78|$x77-bXFuLn zt03MM*^jqH_Tz1l|EG9cTuX+kRXi)Uv!l?p4Wr#zgx$$H?+-^sMaKr*C*aWmGfcg& zpBeCF0pA_)69K;z@Wz0D40vZ0?w8N7x;}RaxL3fe>;3#^1)L7}{D7|rcyYiCD~Nyj z{kuBg^#T7U;O_%2!U086etC?W;`0#!cM15EfJX;BG2mGNFAR7|z$*e?8}RD^e;n|C z18%%i{p~wE;Nt=w6!2LArvsi9@S=e43V3zE8v_0`;GYBD9rp_VcpVaOw}ATxJT~C7 z13ow4MFB4h_?dv;2>6?TcSAoLfBg>(_=teJ1za5Puz(W*PY(F9fR_inCg2SLe;)Af z0q=)?K>qf13HX$NhX))Fcxu2G2fR4odjnn*@T&oT9q?}fAB1||-=59^_X>Dez=?pT z27GzIO9NgT@LK_Y5%4boH$vOUU;je_ZXfW80S^zjD&Uy`UlZ`H0Y4t_^8vpb@K*uv zuxtJ8*+1a+0S^keEZ}njz9isV1AZ{ze+B$Tz?%Zz5^xdPHvaKx67Z1$9}{rzfEhQ_ zUtUGP(*wRD;ClmpI^d52{wd&n&^GhSZyWFl0S^p#T);H}FLaE%oST*e{$~UJFyP+; z-XCp8e|?V#xH#Zgz!L+W8}O9@-xu(+0lyybe**q*z`LXE>aYL70Ur}^U&s0VH!9#* zz!L(V8t{UEZw&a(fFB9?$$*~?czwWc2D~xg&jbE0;GYBjJ>Z@H67A3Y{yE4ocCnjU z27GM50|Fi$a5CT-0WS=AalrQlye8lc0e=$k)_`~UYqY)j?b|=#)&X}9_!P(a{WUb; zQ30P7a7DoBfF}h!BjED`UKH@MfS(Td#em-k_=AA|6Yw_y{}}Ke0XJ-1|NLqk@BtyC zJ#rB2U|3Vwp|EDK=CBs9!(lC9N5ERaj)b*_9R+IxI~vv&)(+Ml)&bTL)(O@b#`YQe zZXW~d20Iqk9d;b-c-RTB9`d5L z*xz6=*f<#5d8M#2SUD^KtALG%Rl<_6vtbipDOeRO4a>kL!fIeySPoVTn*^H-tAkB} zvE6tMY#Qub*mT$o*i6_g*lgH&usN{5!{)-~!7hN!hb@3z1Y>*i64*l6rLfCjm&2}r zT?xAib~Wr8*tM|hV2fZk!2SVS47&+-Gwh$RC9qpyx593N-4440b|-8p>~7dH*mBqk z*h<*_um@ld!XAP>40{CjDC{xVD%caSwXmmQ>tN5oo`<~vdlB{$>}A+`*ekGCVXwh9 zz+Q*F0echn7VK@;u@RuzO)^V9&z-1^XR#AnXv>VX$IYA6Q>lKiDa-{;&bCQ(*&P zgJ6SUC9ol|p|IhwvtZS*3t`v8ZiL+hy9ah3>~Yv?*pslQUk+QQnw+QT})I>I`^I>Wlay26fub%PxX>kc~(c0B9^ zSP$5Vu#;dX!+OGc!Ft0^gAId?fQ^KWf{liaft?OJ19m2CEbMQv7;GFY4l9L~!OCF? zSOsi6tP+-loei4+OTnsOX;=m}5mp1s!g8=$*d*9wSRHH%Y%1&=*fiL=u<5WFu$iz~ zu-UNlU~^!9hs}k}gIxfd4_g4c2zD{-64*l6rLfCjm&2}rT?xAib~Wr8*tM|hV2fZk z!2SVS47&+-Gwh$RC9qpyx593N-4440b|-8p>~7dH*mBqk*h<*_um@ld!XAP>40{Cj zDC{xVD%caSwXmmQ>tN5oo`<~vdlB{$>}A+`*ekGCVXwh9z+Q*F0echn7VK@l)+zB7W7G^2ILY|c}CHH#-o$D%IHs<=QHB($VYoATBNO-&DZ7fxrQfleG(P% z+El%ydLy^vZBui^^2`oAj9d~S0Y(Q0YrY2WEDP5kZtCxbKT94gMs+Uxf$zf5c zL{%c4OO)4h4H;e^CnahsQklv15{K2|8mp?mYQv{waR$`O#%rct!pLl*hPx^0u4!)3 zsCcTjex@_|P4f@Z zb7$w1aEXklOlI9ak8;&>d-a9e<+kSsXUcJHGwT5E_QF*^0Q<29f5smFyE0LL%eH+w zuQ2)sC9~M1>T3SEbfH~@k&9 zC}+kXbLbiz&qfuREq+ucSw1uuorzJ3Upd!HsJB?=3fF>ZeqqtZbM->Uc1!Zs0Vg%` zC!(Fh`N61&Gi6+qJ4Ufy<>fdixEo}1@pQBc3XZ~+Y-zrt7d#u11d8s`q7E_gkgyaKV4gE z*-j~rq2BkFii8n0ad#6cU0zF?(gLvkR1>mA$OWvp2ESjgkgzlf04tIWtF^d#@$t|Ooddo7Q8}+ zCsGx`TG>`^w@pZ(FuL$?4xL?_&EZnVRTv!#m#lNc(a3bQ?R8XvyzRTy;ZnTnrEmlE z6LADlWrSI=4Al{SW+Z#nOhFWjmrblq)+A!M0HQ0_iDJ!5fpneODLv#KdHFtkLb4os%Y z(E{uf&&5MQ;bidzF4b!^%89)_l- ziJC-3{c}!<(4Ye0Ca|5HaJOe`$MsEPuS7Rt)FDyJ4tMm#piEhuH#bO7CB=ruYqE*x z*kRkyrkI?mDbL1uP)Gbp9?#J`-@;B*Rp+L}vS`WhhL7dqejW>2MuW4?72QLID@xp4E54B;m*HCD^lu>L%7ZR1nzs^Rc(IzdMkSNCijPk~M*Ctct zQA@>J3GBM)3UgWd#Z%b?4rwj#?O1-SA5NfD)PCUxxHMLU`G*2#Vhht3)o+*io)I;* zg`Ban$_d%rgjjjJW^$4{ES7Ms#8|8fckLP+OeSWlvEy?U%pGA4mnR`s5l^CeM239I z_}X|4S|Lm-$Fiz1n@dfKm1Z(I9KJZXG?$r-%_yCU=2=k;XG(OtElXwiSXG|N&)Axj zr!r{R(372l3Z%Rii>Ra1l(Xw+JTGTLcsQMNC373`=xJ7BkV#z*A4Oc~yz3vP!Hof2**= z`OD`-{8nWqVXt8(%agh~SBVx+d2D*+R|7ls$cH*B$^4a zvS`nxaO_IG@R2u`icgKIT2w&MT|8EqNa0M$qA^)ki9N-gPhV{sTZ}y_U0HNN;KGi%MPrjK zXQsww_7_)Q6+;&8#8^6mw;`phB@3Hl%jK5FqxUVm@UV@AMFiy()@ghyQ;KJ0ocdn= zbi4}k(rDKu(zR9I@^v7+r2JmBG@R+&nu)b|9*WkZj6daUtw&4Dq;cGE>c%Sg4kDJQ z!^NM*I&8O&I?5F|I{D*_s(ehJzQx1(_CwWZr-9q(df9R{Q@CWb(sQwLwAjjW73pd= zINgDdZh`K(&OKYWGmz&Gc12=5oPiOv(t?mtR}vZz**NGpc(vka#dN)1vvb>p*Q znA)W?Qi1A*+b0~VxQvc-V4IOXC?DVhi zE2L|!CRHSHwxc%mjtP%=;p9Ut6Sc&o@Uo)L$whipr=`(t1kV+AUqOWwoz?E-gFC_~ zV+Gzm+PM^qm!oj50(Mm4yvo-+UYe_cyab#Ls5N*K^%Be6%RVm!r;Fv_c1=Jn>0y~$ zy(l}~sJC#>&0~JsxoY%BtBN`xpCcYUg66R%fmfJ3=n^DSsbqCFSx-`$td}8rm*i*Q zBL&|tG0h#G!v5fCT&S3%`V;j6>t;7!89g()c{Vkaj+Kw&p_L7gXc4#*)nh!@R`MK- z9t>R;>|l08Zs> zL7Cs}ZdGeb-J1a|IG)Y=xwGz7k1Z`aihRYy8$?t?xH6F*WyAr)mC7a$&sA%zMC~VB zUE^!H-&vhTZ#mgGOyfFBr+7=o*;X$>d&Vwo-n=IB1)W#7@%h1CZ?Jk`lMkm5Z+!9S zt+uwvoukp#T16laUo++pZlaEF0HW;~UsjDJp`MK%73-6`=KF5{#9&%m&H5*mC3@$Q zrQo>K+x;$id|7Z7=99Nu0r4B3O>2f|hm&R~toF3Z{ z)t`={rfMe5=Xx#!)d=e(m6hVFU{_@(Q*%$j}C^INngrD&vcCGi}>mk}}7R*m*} z5q3-++u9TOU}5g4ZOr#+g;p=+ygOpYS|Z;EQ@G8!Paos?!Yyi22bt_auq>XAqj8$Q*6?8A zu7v1SNUSvaL|Vb-e)J(Gy2s{{(We+!PuokDURn3*71d29f6?$9h_;#y>gas7ldIm9 zgu{yWlKJbzUPW3?sBl?DkGb4$lkq82#5e8HPT>U-I(VSC)1zCUT?+-o)#?MT9cASC znSWC8A4j+?`MpGNRXXReEO#vstYuQfhefZtB}KPME)%!^YJSfvEU?r)m?BpV4hIiB zH!6IVu~hel!p>Ni=09k;6uvL0mqqva=sb6MqDR7fCU@)J{xMK%6Fh81i@>qrIm+q` zcLLS6*uZ?fQ>bg)oN5&8!1!dZx{h|EwP)R;^EI4RweGIOf}*3x#YMGdMBY{G67gw2 z=V~0}Cs0mt;f+G4SjWQiUX^{}i}F&~5oebcf>+4-`Oe;a7jM3+H-C&bkB9iY_E>NJ zIB&jFy~6y`I{C$Q^2_Vw7ud-!k?#%iMRxMb?1VQ;1&_Z5o&7cF?5{y*e+@co4Y<>z zJ9V@;qZk|L_VNbxNG!?X+`AlNZGB8e>*=`zPVxnrFAq4$_hr7nz)}5Drsi?IT>1BV zPRV+}u8dTCN~RVkvUAuX^uQe5Hn|a#nc^OonP%C7R6c3ribi8v?;E}KjJcXJ+HltA zF?WAd!I3svUYE{#Fb_+T$L&1#N-tYkn=41t)KzxwVnOk^FZfu=IXsd2G{B;u&V4weRwdA?D@SD; z9Vqug4TlZ)zxrwUe8v5ucQqrT8<4#jz_Gzsb_k05@XY`o7SQZMikDCoudenU)(eZH zV#wq)Mfp6@Vg~Su$Q2VkWZ<5R=3CSRcbw1eP&XECX?_a``Lf(sqGH!pa!aP)JGa~8 z<$3g>%pEkHzd>^TB;GFFo(jD5{NsR^rsj^9g!RzlkeABlMt<%4^&8L|EmCwh8LVUG z*V^s>tRA<-!HJsj_#PI$mc>zu)#LWZc4Q44y~D>@HVF+hoDJMDeA!U0CRLcy5+9ga za!>FZ<$xi*(LXObT4+gPY1nhywUpblc@=Y~Z~pextv%XF+pW+sMIG4LDBA(w!qD>V zfMYVD1J}U&FI!XAp{#?gD%TWeaCk3nyPMH z+u37IyDptNcW@=ReW3zyYb-Q9a`?8v>AxTBvAU?J;X=%648*=j!+3^<(~5S);LRO5 zVmJz~I0nC*x)<#i_ir4zoX#tN!YEtJ}_fPHdme6g5m=P`m#f=biTc zh137Nd#2sWIgQ`jtNkf2c71$(!*hp~-?v|zKCL!X-G5&HD;Ljev18lvxgFm=;iAJ^ zzu5EPU3XZr&!T%s0w zFsV!1(|+3eOpA~1EBkWq)W{Wc9((9{6_Iv2Xr3CwbcLqdz&h_Q;!y z&mZ3G#@E-JvV3aUed``CuDWdIUyr@)#k~%FYT=Dnov^Omx)}%e{pq!2#XkBqZnd-=r`4IkT9dd;9VvySZA=dAUavlplD`g+Wi_AlM?iZrc`Lz57)UT=)I{5AO8jkqfu3?^@G&>s#%1x_;br#HRy%nhR_^dGxy*B*PsnpCtKao>V|-#oMK{I>VpF!B6x zTN~}teNF2o-5b90>WtK?zs~ym*_;13^Xs2I{*70rUa+F* z*#XV(xvarWr*D~g-k9B=@3eaP>CLMTylv6#du~4e-+TY(hwr~k?wz>lf)ywC+voY( zsvrL{cI@SiCd_>5#@zF5`d{?g4+ph-|Cod3o_qg{cQ5Vs+~lkNvggXncy z4t#TD&%MXYJGMIW%?+)GobT&6Ypw1 z@1%c?tg0>lV6W%S|Lm03FJ1EMn%sd$k6m?V>Y$507&7ksH)a%nv&(LiA6zxB{o1c? zSaI0Mbtkv{b@T`EYrk#y+H0MTy7ALHJN-7b-Si{B{PBbHp1%6{V|IA_jjd}gZS>&k zwtZ7uIvp~4`M0N@S2cM4iAzVGws!4%uk=1=O{cR?J^ITgmwj;RrHz_izv8-qGiDs$ z=ZaayzjT=R^0XTtJhEcqF?(;i;jJ;#FFbBZ^G6PPzG=%1BhEkhrAg*qruEy_^Rd~pdNjQ8$BAcry8M?bOD7$C^om~_o|nF_!OCx*o6uz8s-Nan zWd`o++; zm#nF(==Q^duRb+>&x_A~ewW94Ki$2}g@anhn~YlAwbh`rW)GX&W!d?!@6kTHsb=79 z2lx8+pqX8E*t+bt2Ro1aF!jYxOOEM%&Eqr2{N?+*_r7&z>EuhgTr_p|^-Ipo40-&N zb%*`9=c|AFtWAqIyPf#dP0inY<-^^sx_{B4@nsJ!I(gGYS2VuuozcfXd-{Pre|X`A z3#VPzW#UPjk2>My{f|Dab7jqIJFh!zuQN)kJGI%f!WeDLXalZUPRwfO> zx9|3Q{a<(W{OX1mUc9Z#uQxuLEuFgQ{Kf-zd3B!_zsx-AsJmw@y>Cor-RBjn8@&0- zD-(7cGG^y((^gKK`uBGpzp(!WZEyR(`g3)S*1Wp&&}~;``*!`Q>5hA}9CpiPw~gI) z;q-xX539In+L-_A^8YvekdpsSU(j@?!P~YCe{|j>Yg%1ca{a|GuUuRE%&yN4ysu5` zPcwagxcPxZ!)Cpf9KLqUkvptD{n|}a`)+GEt+q#-{xAGdb;!z#yR^M!?zNBXe{tKk zou__s@Yc!Wp1Nh}ty})ly5H|J7H!<~e=EOu;&)FDtXuVd`KE0vM-ABLp!xgU-MV7s zHBXOkGr#JWeVVqK^5lJIeRa*ZFYNipTmNbH)JLCO({Rs5^QLb8*ZOu-&K`Wrgt2d2 z{`WUu%`~YVRyF;UpWnb_zo#4A`1U2YtZVc6Ay@x$+N@_@JFaHOHBa69-P_mx zvBQ&{CVaD~#q6_R-T3Lpryspf>8ep@u9-dckK9G?pL**T<;`CkaOjsC&ba-715z`$ zRc-lY+0}J(=DxGrjZL21*l^-ohn{l(zh@T@7&&KT!y{g6v~1IXGmq^Xn{(H+jc=?! zb-^u1eRJv=({7$|=66re$_&}|`-nO3f6}q(EB8$9_4%^WEtlNfc?%Gm(%{6bexTGZY(1DZZc6|4odB3fD zsboP_hkmCnZnx!}#b+(NeE)x)@#8VEy_O&S#5ad6TRCw+uhgm*eOo>H$C$@1oH*sD PmPh<`)%X{#Ui<$5Z2J~P literal 0 HcmV?d00001 diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/__init__.py b/.venv-test/lib/python3.12/site-packages/_pytest/__init__.py new file mode 100644 index 0000000..8eb8ec9 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations + + +__all__ = ["__version__", "version_tuple"] + +try: + from ._version import version as __version__ + from ._version import version_tuple +except ImportError: # pragma: no cover + # broken installation, we don't even try + # unknown only works because we do poor mans version compare + __version__ = "unknown" + version_tuple = (0, 0, "unknown") diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_argcomplete.py b/.venv-test/lib/python3.12/site-packages/_pytest/_argcomplete.py new file mode 100644 index 0000000..59426ef --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_argcomplete.py @@ -0,0 +1,117 @@ +"""Allow bash-completion for argparse with argcomplete if installed. + +Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail +to find the magic string, so _ARGCOMPLETE env. var is never set, and +this does not need special code). + +Function try_argcomplete(parser) should be called directly before +the call to ArgumentParser.parse_args(). + +The filescompleter is what you normally would use on the positional +arguments specification, in order to get "dirname/" after "dirn" +instead of the default "dirname ": + + optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter + +Other, application specific, completers should go in the file +doing the add_argument calls as they need to be specified as .completer +attributes as well. (If argcomplete is not installed, the function the +attribute points to will not be used). + +SPEEDUP +======= + +The generic argcomplete script for bash-completion +(/etc/bash_completion.d/python-argcomplete.sh) +uses a python program to determine startup script generated by pip. +You can speed up completion somewhat by changing this script to include + # PYTHON_ARGCOMPLETE_OK +so the python-argcomplete-check-easy-install-script does not +need to be called to find the entry point of the code and see if that is +marked with PYTHON_ARGCOMPLETE_OK. + +INSTALL/DEBUGGING +================= + +To include this support in another application that has setup.py generated +scripts: + +- Add the line: + # PYTHON_ARGCOMPLETE_OK + near the top of the main python entry point. + +- Include in the file calling parse_args(): + from _argcomplete import try_argcomplete, filescompleter + Call try_argcomplete just before parse_args(), and optionally add + filescompleter to the positional arguments' add_argument(). + +If things do not work right away: + +- Switch on argcomplete debugging with (also helpful when doing custom + completers): + export _ARC_DEBUG=1 + +- Run: + python-argcomplete-check-easy-install-script $(which appname) + echo $? + will echo 0 if the magic line has been found, 1 if not. + +- Sometimes it helps to find early on errors using: + _ARGCOMPLETE=1 _ARC_DEBUG=1 appname + which should throw a KeyError: 'COMPLINE' (which is properly set by the + global argcomplete script). +""" + +from __future__ import annotations + +import argparse +from glob import glob +import os +import sys +from typing import Any + + +class FastFilesCompleter: + """Fast file completer class.""" + + def __init__(self, directories: bool = True) -> None: + self.directories = directories + + def __call__(self, prefix: str, **kwargs: Any) -> list[str]: + # Only called on non option completions. + if os.sep in prefix[1:]: + prefix_dir = len(os.path.dirname(prefix) + os.sep) + else: + prefix_dir = 0 + completion = [] + globbed = [] + if "*" not in prefix and "?" not in prefix: + # We are on unix, otherwise no bash. + if not prefix or prefix[-1] == os.sep: + globbed.extend(glob(prefix + ".*")) + prefix += "*" + globbed.extend(glob(prefix)) + for x in sorted(globbed): + if os.path.isdir(x): + x += "/" + # Append stripping the prefix (like bash, not like compgen). + completion.append(x[prefix_dir:]) + return completion + + +if os.environ.get("_ARGCOMPLETE"): + try: + import argcomplete.completers + except ImportError: + sys.exit(-1) + filescompleter: FastFilesCompleter | None = FastFilesCompleter() + + def try_argcomplete(parser: argparse.ArgumentParser) -> None: + argcomplete.autocomplete(parser, always_complete_options=False) + +else: + + def try_argcomplete(parser: argparse.ArgumentParser) -> None: + pass + + filescompleter = None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_code/__init__.py b/.venv-test/lib/python3.12/site-packages/_pytest/_code/__init__.py new file mode 100644 index 0000000..7f67a2e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_code/__init__.py @@ -0,0 +1,26 @@ +"""Python inspection/code generation API.""" + +from __future__ import annotations + +from .code import Code +from .code import ExceptionInfo +from .code import filter_traceback +from .code import Frame +from .code import getfslineno +from .code import Traceback +from .code import TracebackEntry +from .source import getrawcode +from .source import Source + + +__all__ = [ + "Code", + "ExceptionInfo", + "Frame", + "Source", + "Traceback", + "TracebackEntry", + "filter_traceback", + "getfslineno", + "getrawcode", +] diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_code/code.py b/.venv-test/lib/python3.12/site-packages/_pytest/_code/code.py new file mode 100644 index 0000000..3c453b1 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_code/code.py @@ -0,0 +1,1632 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import ast +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +import inspect +from io import StringIO +import os +from pathlib import Path +import re +import sys +from traceback import extract_tb +from traceback import format_exception +from traceback import format_exception_only +from traceback import FrameSummary +from types import CodeType +from types import FrameType +from types import TracebackType +from typing import Any +from typing import ClassVar +from typing import Final +from typing import final +from typing import Generic +from typing import Literal +from typing import overload +from typing import SupportsIndex +from typing import TypeAlias +from typing import TypeVar + +import pluggy + +import _pytest +from _pytest._code.source import findsource +from _pytest._code.source import getrawcode +from _pytest._code.source import getstatementrange_ast +from _pytest._code.source import Source +from _pytest._io import TerminalWriter +from _pytest._io.saferepr import safeformat +from _pytest._io.saferepr import saferepr +from _pytest.compat import get_real_func +from _pytest.deprecated import check_ispytest +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath + + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + +TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] + +EXCEPTION_OR_MORE = type[BaseException] | tuple[type[BaseException], ...] + + +class Code: + """Wrapper around Python code objects.""" + + __slots__ = ("raw",) + + def __init__(self, obj: CodeType) -> None: + self.raw = obj + + @classmethod + def from_function(cls, obj: object) -> Code: + return cls(getrawcode(obj)) + + def __eq__(self, other): + return self.raw == other.raw + + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore + + @property + def firstlineno(self) -> int: + return self.raw.co_firstlineno - 1 + + @property + def name(self) -> str: + return self.raw.co_name + + @property + def path(self) -> Path | str: + """Return a path object pointing to source code, or an ``str`` in + case of ``OSError`` / non-existing file.""" + filename = inspect.getfile(self.raw) + if not filename: + return "" + try: + p = absolutepath(filename) + # maybe don't try this checking + if not p.exists(): + raise OSError("path check failed.") + return p + except OSError: + # XXX maybe try harder like the weird logic + # in the standard lib [linecache.updatecache] does? + return filename + + @property + def fullsource(self) -> Source | None: + """Return a _pytest._code.Source object for the full source file of the code.""" + full, _ = findsource(self.raw) + return full + + def source(self) -> Source: + """Return a _pytest._code.Source object for the code object's source only.""" + # return source only for that part of code + return Source(self.raw) + + def getargs(self, var: bool = False) -> tuple[str, ...]: + """Return a tuple with the argument names for the code object. + + If 'var' is set True also return the names of the variable and + keyword arguments when present. + """ + # inspect.getargs merges positional and kwonly into a single list; + # co_argcount is needed to exclude kwonly when var=False. + args, varargs, varkw = inspect.getargs(self.raw) + if not var: + return tuple(args[: self.raw.co_argcount]) + result = list(args) + if varargs is not None: + result.append(varargs) + if varkw is not None: + result.append(varkw) + return tuple(result) + + +class Frame: + """Wrapper around a Python frame holding f_locals and f_globals + in which expressions can be evaluated.""" + + __slots__ = ("raw",) + + def __init__(self, frame: FrameType) -> None: + self.raw = frame + + @property + def lineno(self) -> int: + return self.raw.f_lineno - 1 + + @property + def f_globals(self) -> dict[str, Any]: + return self.raw.f_globals + + @property + def f_locals(self) -> dict[str, Any]: + return self.raw.f_locals + + @property + def code(self) -> Code: + return Code(self.raw.f_code) + + @property + def statement(self) -> Source: + """Statement this frame is at.""" + if self.code.fullsource is None: + return Source("") + return self.code.fullsource.getstatement(self.lineno) + + def eval(self, code, **vars): + """Evaluate 'code' in the frame. + + 'vars' are optional additional local variables. + + Returns the result of the evaluation. + """ + f_locals = self.f_locals.copy() + f_locals.update(vars) + return eval(code, self.f_globals, f_locals) + + def repr(self, object: object) -> str: + """Return a 'safe' (non-recursive, one-line) string repr for 'object'.""" + return saferepr(object) + + def getargs(self, var: bool = False): + """Return a list of tuples (name, value) for all arguments. + + If 'var' is set True, also include the variable and keyword arguments + when present. + """ + retval = [] + for arg in self.code.getargs(var): + try: + retval.append((arg, self.f_locals[arg])) + except KeyError: + pass # this can occur when using Psyco + return retval + + +class TracebackEntry: + """A single entry in a Traceback.""" + + __slots__ = ("_rawentry", "_repr_style") + + def __init__( + self, + rawentry: TracebackType, + repr_style: Literal["short", "long"] | None = None, + ) -> None: + self._rawentry: Final = rawentry + self._repr_style: Final = repr_style + + def with_repr_style( + self, repr_style: Literal["short", "long"] | None + ) -> TracebackEntry: + return TracebackEntry(self._rawentry, repr_style) + + @property + def lineno(self) -> int: + return self._rawentry.tb_lineno - 1 + + def get_python_framesummary(self) -> FrameSummary: + # Python's built-in traceback module implements all the nitty gritty + # details to get column numbers of out frames. + stack_summary = extract_tb(self._rawentry, limit=1) + return stack_summary[0] + + # Column and end line numbers introduced in python 3.11 + if sys.version_info < (3, 11): + + @property + def end_lineno_relative(self) -> int | None: + return None + + @property + def colno(self) -> int | None: + return None + + @property + def end_colno(self) -> int | None: + return None + else: + + @property + def end_lineno_relative(self) -> int | None: + frame_summary = self.get_python_framesummary() + if frame_summary.end_lineno is None: # pragma: no cover + return None + return frame_summary.end_lineno - 1 - self.frame.code.firstlineno + + @property + def colno(self) -> int | None: + """Starting byte offset of the expression in the traceback entry.""" + return self.get_python_framesummary().colno + + @property + def end_colno(self) -> int | None: + """Ending byte offset of the expression in the traceback entry.""" + return self.get_python_framesummary().end_colno + + @property + def frame(self) -> Frame: + return Frame(self._rawentry.tb_frame) + + @property + def relline(self) -> int: + return self.lineno - self.frame.code.firstlineno + + def __repr__(self) -> str: + return f"" + + @property + def statement(self) -> Source: + """_pytest._code.Source object for the current statement.""" + source = self.frame.code.fullsource + assert source is not None + return source.getstatement(self.lineno) + + @property + def path(self) -> Path | str: + """Path to the source code.""" + return self.frame.code.path + + @property + def locals(self) -> dict[str, Any]: + """Locals of underlying frame.""" + return self.frame.f_locals + + def getfirstlinesource(self) -> int: + return self.frame.code.firstlineno + + def getsource( + self, astcache: dict[str | Path, ast.AST] | None = None + ) -> Source | None: + """Return failing source code.""" + # we use the passed in astcache to not reparse asttrees + # within exception info printing + source = self.frame.code.fullsource + if source is None: + return None + key = astnode = None + if astcache is not None: + key = self.frame.code.path + if key is not None: + astnode = astcache.get(key, None) + start = self.getfirstlinesource() + try: + astnode, _, end = getstatementrange_ast( + self.lineno, source, astnode=astnode + ) + except SyntaxError: + end = self.lineno + 1 + else: + if key is not None and astcache is not None: + astcache[key] = astnode + return source[start:end] + + source = property(getsource) + + def ishidden(self, excinfo: ExceptionInfo[BaseException] | None) -> bool: + """Return True if the current frame has a var __tracebackhide__ + resolving to True. + + If __tracebackhide__ is a callable, it gets called with the + ExceptionInfo instance and can decide whether to hide the traceback. + + Mostly for internal use. + """ + tbh: bool | Callable[[ExceptionInfo[BaseException] | None], bool] = False + for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals): + # in normal cases, f_locals and f_globals are dictionaries + # however via `exec(...)` / `eval(...)` they can be other types + # (even incorrect types!). + # as such, we suppress all exceptions while accessing __tracebackhide__ + try: + tbh = maybe_ns_dct["__tracebackhide__"] + except Exception: + pass + else: + break + if tbh and callable(tbh): + return tbh(excinfo) + return tbh + + def __str__(self) -> str: + name = self.frame.code.name + try: + line = str(self.statement).lstrip() + except KeyboardInterrupt: + raise + except BaseException: + line = "???" + # This output does not quite match Python's repr for traceback entries, + # but changing it to do so would break certain plugins. See + # https://github.com/pytest-dev/pytest/pull/7535/ for details. + return f" File '{self.path}':{self.lineno + 1} in {name}\n {line}\n" + + @property + def name(self) -> str: + """co_name of underlying code.""" + return self.frame.code.raw.co_name + + +class Traceback(list[TracebackEntry]): + """Traceback objects encapsulate and offer higher level access to Traceback entries.""" + + def __init__( + self, + tb: TracebackType | Iterable[TracebackEntry], + ) -> None: + """Initialize from given python traceback object and ExceptionInfo.""" + if isinstance(tb, TracebackType): + + def f(cur: TracebackType) -> Iterable[TracebackEntry]: + cur_: TracebackType | None = cur + while cur_ is not None: + yield TracebackEntry(cur_) + cur_ = cur_.tb_next + + super().__init__(f(tb)) + else: + super().__init__(tb) + + def cut( + self, + path: os.PathLike[str] | str | None = None, + lineno: int | None = None, + firstlineno: int | None = None, + excludepath: os.PathLike[str] | None = None, + ) -> Traceback: + """Return a Traceback instance wrapping part of this Traceback. + + By providing any combination of path, lineno and firstlineno, the + first frame to start the to-be-returned traceback is determined. + + This allows cutting the first part of a Traceback instance e.g. + for formatting reasons (removing some uninteresting bits that deal + with handling of the exception/traceback). + """ + path_ = None if path is None else os.fspath(path) + excludepath_ = None if excludepath is None else os.fspath(excludepath) + for x in self: + code = x.frame.code + codepath = code.path + if path is not None and str(codepath) != path_: + continue + if ( + excludepath is not None + and isinstance(codepath, Path) + and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator] + ): + continue + if lineno is not None and x.lineno != lineno: + continue + if firstlineno is not None and x.frame.code.firstlineno != firstlineno: + continue + return Traceback(x._rawentry) + return self + + @overload + def __getitem__(self, key: SupportsIndex) -> TracebackEntry: ... + + @overload + def __getitem__(self, key: slice) -> Traceback: ... + + def __getitem__(self, key: SupportsIndex | slice) -> TracebackEntry | Traceback: + if isinstance(key, slice): + return self.__class__(super().__getitem__(key)) + else: + return super().__getitem__(key) + + def filter( + self, + excinfo_or_fn: ExceptionInfo[BaseException] | Callable[[TracebackEntry], bool], + /, + ) -> Traceback: + """Return a Traceback instance with certain items removed. + + If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s + which are hidden (see ishidden() above). + + Otherwise, the filter is a function that gets a single argument, a + ``TracebackEntry`` instance, and should return True when the item should + be added to the ``Traceback``, False when not. + """ + if isinstance(excinfo_or_fn, ExceptionInfo): + fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731 + else: + fn = excinfo_or_fn + return Traceback(filter(fn, self)) + + def recursionindex(self) -> int | None: + """Return the index of the frame/TracebackEntry where recursion originates if + appropriate, None if no recursion occurred.""" + cache: dict[tuple[Any, int, int], list[dict[str, Any]]] = {} + for i, entry in enumerate(self): + # id for the code.raw is needed to work around + # the strange metaprogramming in the decorator lib from pypi + # which generates code objects that have hash/value equality + # XXX needs a test + key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno + values = cache.setdefault(key, []) + # Since Python 3.13 f_locals is a proxy, freeze it. + loc = dict(entry.frame.f_locals) + if values: + for otherloc in values: + if otherloc == loc: + return i + values.append(loc) + return None + + +def stringify_exception( + exc: BaseException, include_subexception_msg: bool = True +) -> str: + try: + notes = getattr(exc, "__notes__", []) + except KeyError: + # Workaround for https://github.com/python/cpython/issues/98778 on + # some 3.10 and 3.11 patch versions. + HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ()) + if sys.version_info < (3, 12) and isinstance(exc, HTTPError): + notes = [] + else: # pragma: no cover + # exception not related to above bug, reraise + raise + if not include_subexception_msg and isinstance(exc, BaseExceptionGroup): + message = exc.message + else: + message = str(exc) + + return "\n".join( + [ + message, + *notes, + ] + ) + + +E = TypeVar("E", bound=BaseException, covariant=True) + + +@final +@dataclasses.dataclass +class ExceptionInfo(Generic[E]): + """Wraps sys.exc_info() objects and offers help for navigating the traceback.""" + + _assert_start_repr: ClassVar = "AssertionError('assert " + + _excinfo: tuple[type[E], E, TracebackType] | None + _striptext: str + _traceback: Traceback | None + + def __init__( + self, + excinfo: tuple[type[E], E, TracebackType] | None, + striptext: str = "", + traceback: Traceback | None = None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._excinfo = excinfo + self._striptext = striptext + self._traceback = traceback + + @classmethod + def from_exception( + cls, + # Ignoring error: "Cannot use a covariant type variable as a parameter". + # This is OK to ignore because this class is (conceptually) readonly. + # See https://github.com/python/mypy/issues/7049. + exception: E, # type: ignore[misc] + exprinfo: str | None = None, + ) -> ExceptionInfo[E]: + """Return an ExceptionInfo for an existing exception. + + The exception must have a non-``None`` ``__traceback__`` attribute, + otherwise this function fails with an assertion error. This means that + the exception must have been raised, or added a traceback with the + :py:meth:`~BaseException.with_traceback()` method. + + :param exprinfo: + A text string helping to determine if we should strip + ``AssertionError`` from the output. Defaults to the exception + message/``__str__()``. + + .. versionadded:: 7.4 + """ + assert exception.__traceback__, ( + "Exceptions passed to ExcInfo.from_exception(...)" + " must have a non-None __traceback__." + ) + exc_info = (type(exception), exception, exception.__traceback__) + return cls.from_exc_info(exc_info, exprinfo) + + @classmethod + def from_exc_info( + cls, + exc_info: tuple[type[E], E, TracebackType], + exprinfo: str | None = None, + ) -> ExceptionInfo[E]: + """Like :func:`from_exception`, but using old-style exc_info tuple.""" + _striptext = "" + if exprinfo is None and isinstance(exc_info[1], AssertionError): + exprinfo = getattr(exc_info[1], "msg", None) + if exprinfo is None: + exprinfo = saferepr(exc_info[1]) + if exprinfo and exprinfo.startswith(cls._assert_start_repr): + _striptext = "AssertionError: " + + return cls(exc_info, _striptext, _ispytest=True) + + @classmethod + def from_current(cls, exprinfo: str | None = None) -> ExceptionInfo[BaseException]: + """Return an ExceptionInfo matching the current traceback. + + .. warning:: + + Experimental API + + :param exprinfo: + A text string helping to determine if we should strip + ``AssertionError`` from the output. Defaults to the exception + message/``__str__()``. + """ + tup = sys.exc_info() + assert tup[0] is not None, "no current exception" + assert tup[1] is not None, "no current exception" + assert tup[2] is not None, "no current exception" + exc_info = (tup[0], tup[1], tup[2]) + return ExceptionInfo.from_exc_info(exc_info, exprinfo) + + @classmethod + def for_later(cls) -> ExceptionInfo[E]: + """Return an unfilled ExceptionInfo.""" + return cls(None, _ispytest=True) + + def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None: + """Fill an unfilled ExceptionInfo created with ``for_later()``.""" + assert self._excinfo is None, "ExceptionInfo was already filled" + self._excinfo = exc_info + + @property + def type(self) -> type[E]: + """The exception class.""" + assert self._excinfo is not None, ( + ".type can only be used after the context manager exits" + ) + return self._excinfo[0] + + @property + def value(self) -> E: + """The exception value.""" + assert self._excinfo is not None, ( + ".value can only be used after the context manager exits" + ) + return self._excinfo[1] + + @property + def tb(self) -> TracebackType: + """The exception raw traceback.""" + assert self._excinfo is not None, ( + ".tb can only be used after the context manager exits" + ) + return self._excinfo[2] + + @property + def typename(self) -> str: + """The type name of the exception.""" + assert self._excinfo is not None, ( + ".typename can only be used after the context manager exits" + ) + return self.type.__name__ + + @property + def traceback(self) -> Traceback: + """The traceback.""" + if self._traceback is None: + self._traceback = Traceback(self.tb) + return self._traceback + + @traceback.setter + def traceback(self, value: Traceback) -> None: + self._traceback = value + + def __repr__(self) -> str: + if self._excinfo is None: + return "" + return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>" + + def exconly(self, tryshort: bool = False) -> str: + """Return the exception as a string. + + This is usually a single line ": ", but + may also include additional lines for the exception notes, and detailed + information for SyntaxError's. + + :param tryshort: + If true, and the exception is an AssertionError, strip + 'AssertionError: ' from the beginning. + """ + + def _get_single_subexc( + eg: BaseExceptionGroup[BaseException], + ) -> BaseException | None: + if len(eg.exceptions) != 1: + return None + if isinstance(e := eg.exceptions[0], BaseExceptionGroup): + return _get_single_subexc(e) + return e + + if ( + tryshort + and isinstance(self.value, BaseExceptionGroup) + and (subexc := _get_single_subexc(self.value)) is not None + ): + return f"{subexc!r} [single exception in {type(self.value).__name__}]" + + lines = format_exception_only(self.value) + # The lines already include line separators. + text = "".join(lines) + text = text.rstrip() + if tryshort: + if text.startswith(self._striptext): + text = text[len(self._striptext) :] + return text + + def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool: + """Return True if the exception is an instance of exc. + + Consider using ``isinstance(excinfo.value, exc)`` instead. + """ + return isinstance(self.value, exc) + + def _getreprcrash(self) -> ReprFileLocation | None: + # Find last non-hidden traceback entry that led to the exception of the + # traceback, or None if all hidden. + for i in range(-1, -len(self.traceback) - 1, -1): + entry = self.traceback[i] + if not entry.ishidden(self): + path, lineno = entry.frame.code.raw.co_filename, entry.lineno + exconly = self.exconly(tryshort=True) + return ReprFileLocation(path, lineno + 1, exconly) + return None + + def getrepr( + self, + showlocals: bool = False, + style: TracebackStyle = "long", + abspath: bool = False, + tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True, + funcargs: bool = False, + truncate_locals: bool = True, + truncate_args: bool = True, + chain: bool = True, + ) -> ReprExceptionInfo | ExceptionChainRepr: + """Return str()able representation of this exception info. + + The formatting parameters are ineffective if ``style="native"``, + since in this case the native formatting is used. + + :param bool showlocals: + Show locals per traceback entry. + + :param str style: + long|short|line|no|native|value traceback style. + + :param bool abspath: + If paths should be changed to absolute or left unchanged. + + :param tbfilter: + A filter for traceback entries. + + * If false, don't hide any entries. + * If true, hide internal entries and entries that contain a local + variable ``__tracebackhide__ = True``. + * If a callable, delegates the filtering to the callable. + + :param bool funcargs: + Show function arguments per traceback entry. + + :param bool truncate_locals: + Whether to show a size-limited `repr()` of locals, or a full + pretty-printing. + + :param bool truncate_args: + Whether to show a size-limited truncated `repr()` of function + arguments, or a full pretty-printing. + + :param bool chain: + If chained exceptions should be shown. + + .. versionchanged:: 3.9 + + Added the ``chain`` parameter. + """ + if style == "native": + return ReprExceptionInfo( + reprtraceback=ReprTracebackNative( + format_exception( + self.type, + self.value, + self.traceback[0]._rawentry if self.traceback else None, + ) + ), + reprcrash=self._getreprcrash(), + ) + + fmt = ExceptionInfoFormatter( + showlocals=showlocals, + style=style, + abspath=abspath, + tbfilter=tbfilter, + funcargs=funcargs, + truncate_locals=truncate_locals, + truncate_args=truncate_args, + chain=chain, + ) + return fmt.repr_excinfo(self) + + def match(self, regexp: str | re.Pattern[str]) -> Literal[True]: + """Check whether the regular expression `regexp` matches the string + representation of the exception using :func:`python:re.search`. + + If it matches `True` is returned, otherwise an `AssertionError` is raised. + """ + __tracebackhide__ = True + value = stringify_exception(self.value) + msg = ( + f"Regex pattern did not match.\n" + f" Expected regex: {regexp!r}\n" + f" Actual message: {value!r}" + ) + if regexp == value: + msg += "\n Did you mean to `re.escape()` the regex?" + assert re.search(regexp, value), msg + # Return True to allow for "assert excinfo.match()". + return True + + def _group_contains( + self, + exc_group: BaseExceptionGroup[BaseException], + expected_exception: EXCEPTION_OR_MORE, + match: str | re.Pattern[str] | None, + target_depth: int | None = None, + current_depth: int = 1, + ) -> bool: + """Return `True` if a `BaseExceptionGroup` contains a matching exception.""" + if (target_depth is not None) and (current_depth > target_depth): + # already descended past the target depth + return False + for exc in exc_group.exceptions: + if isinstance(exc, BaseExceptionGroup): + if self._group_contains( + exc, expected_exception, match, target_depth, current_depth + 1 + ): + return True + if (target_depth is not None) and (current_depth != target_depth): + # not at the target depth, no match + continue + if not isinstance(exc, expected_exception): + continue + if match is not None: + value = stringify_exception(exc) + if not re.search(match, value): + continue + return True + return False + + def group_contains( + self, + expected_exception: EXCEPTION_OR_MORE, + *, + match: str | re.Pattern[str] | None = None, + depth: int | None = None, + ) -> bool: + """Check whether a captured exception group contains a matching exception. + + :param Type[BaseException] | Tuple[Type[BaseException]] expected_exception: + The expected exception type, or a tuple if one of multiple possible + exception types are expected. + + :param str | re.Pattern[str] | None match: + If specified, a string containing a regular expression, + or a regular expression object, that is tested against the string + representation of the exception and its `PEP-678 ` `__notes__` + using :func:`re.search`. + + To match a literal string that may contain :ref:`special characters + `, the pattern can first be escaped with :func:`re.escape`. + + :param Optional[int] depth: + If `None`, will search for a matching exception at any nesting depth. + If >= 1, will only match an exception if it's at the specified depth (depth = 1 being + the exceptions contained within the topmost exception group). + + .. versionadded:: 8.0 + + .. warning:: + This helper makes it easy to check for the presence of specific exceptions, + but it is very bad for checking that the group does *not* contain + *any other exceptions*. + You should instead consider using :class:`pytest.RaisesGroup` + + """ + msg = "Captured exception is not an instance of `BaseExceptionGroup`" + assert isinstance(self.value, BaseExceptionGroup), msg + msg = "`depth` must be >= 1 if specified" + assert (depth is None) or (depth >= 1), msg + return self._group_contains(self.value, expected_exception, match, depth) + + +# Type alias for the `tbfilter` setting: +# bool: If True, it should be filtered using Traceback.filter() +# callable: A callable that takes an ExceptionInfo and returns the filtered traceback. +TracebackFilter: TypeAlias = bool | Callable[[ExceptionInfo[BaseException]], Traceback] + + +@dataclasses.dataclass +class ExceptionInfoFormatter: + """Helper object to format ExceptionInfo's and individual exception parts + into TerminalRepr's. + + See :func:`ExceptionInfo.getrepr` for parameters. + """ + + # for traceback entries + flow_marker: ClassVar = ">" + fail_marker: ClassVar = "E" + + showlocals: bool = False + # Note: "native" is handled outside of ExceptionInfoFormatter. + style: TracebackStyle = "long" + abspath: bool = True + tbfilter: TracebackFilter = True + funcargs: bool = False + truncate_locals: bool = True + truncate_args: bool = True + chain: bool = True + + astcache: dict[str | Path, ast.AST] = dataclasses.field( + default_factory=dict, init=False, repr=False + ) + + def _getindent(self, source: Source) -> int: + # Figure out indent for the given source. + try: + s = str(source.getstatement(len(source) - 1)) + except KeyboardInterrupt: + raise + except BaseException: + try: + s = str(source[-1]) + except KeyboardInterrupt: + raise + except BaseException: + return 0 + return 4 + (len(s) - len(s.lstrip())) + + def _getentrysource(self, entry: TracebackEntry) -> Source | None: + source = entry.getsource(self.astcache) + if source is not None: + source = source.deindent() + return source + + def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None: + if self.funcargs: + args = [] + for argname, argvalue in entry.frame.getargs(var=True): + if self.truncate_args: + str_repr = saferepr(argvalue) + else: + str_repr = saferepr(argvalue, maxsize=None) + args.append((argname, str_repr)) + return ReprFuncArgs(args) + return None + + def get_source( + self, + source: Source | None, + line_index: int = -1, + excinfo: ExceptionInfo[BaseException] | None = None, + short: bool = False, + end_line_index: int | None = None, + colno: int | None = None, + end_colno: int | None = None, + ) -> list[str]: + """Return formatted and marked up source lines.""" + lines = [] + if source is not None and line_index < 0: + line_index += len(source) + if source is None or line_index >= len(source.lines) or line_index < 0: + # `line_index` could still be outside `range(len(source.lines))` if + # we're processing AST with pathological position attributes. + source = Source("???") + line_index = 0 + space_prefix = " " + if short: + lines.append(space_prefix + source.lines[line_index].strip()) + lines.extend( + self.get_highlight_arrows_for_line( + raw_line=source.raw_lines[line_index], + line=source.lines[line_index].strip(), + lineno=line_index, + end_lineno=end_line_index, + colno=colno, + end_colno=end_colno, + ) + ) + else: + for line in source.lines[:line_index]: + lines.append(space_prefix + line) + lines.append(self.flow_marker + " " + source.lines[line_index]) + lines.extend( + self.get_highlight_arrows_for_line( + raw_line=source.raw_lines[line_index], + line=source.lines[line_index], + lineno=line_index, + end_lineno=end_line_index, + colno=colno, + end_colno=end_colno, + ) + ) + for line in source.lines[line_index + 1 :]: + lines.append(space_prefix + line) + if excinfo is not None: + indent = 4 if short else self._getindent(source) + lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) + return lines + + def get_highlight_arrows_for_line( + self, + line: str, + raw_line: str, + lineno: int | None, + end_lineno: int | None, + colno: int | None, + end_colno: int | None, + ) -> list[str]: + """Return characters highlighting a source line. + + Example with colno and end_colno pointing to the bar expression: + "foo() + bar()" + returns " ^^^^^" + """ + if lineno != end_lineno: + # Don't handle expressions that span multiple lines. + return [] + if colno is None or end_colno is None: + # Can't do anything without column information. + return [] + + num_stripped_chars = len(raw_line) - len(line) + + start_char_offset = _byte_offset_to_character_offset(raw_line, colno) + end_char_offset = _byte_offset_to_character_offset(raw_line, end_colno) + num_carets = end_char_offset - start_char_offset + # If the highlight would span the whole line, it is redundant, don't + # show it. + if num_carets >= len(line.strip()): + return [] + + highlights = " " + highlights += " " * (start_char_offset - num_stripped_chars + 1) + highlights += "^" * num_carets + return [highlights] + + def get_exconly( + self, + excinfo: ExceptionInfo[BaseException], + indent: int = 4, + markall: bool = False, + ) -> list[str]: + lines = [] + indentstr = " " * indent + # Get the real exception information out. + exlines = excinfo.exconly(tryshort=True).split("\n") + failindent = self.fail_marker + indentstr[1:] + for line in exlines: + lines.append(failindent + line) + if not markall: + failindent = indentstr + return lines + + def repr_locals(self, locals: Mapping[str, object]) -> ReprLocals | None: + if self.showlocals: + lines = [] + # Variables starting with `@` are helpers injected by assertion + # rewriting, not user variables, so hide them. + keys = [loc for loc in locals if loc[0] != "@"] + keys.sort() + for name in keys: + value = locals[name] + if name == "__builtins__": + lines.append("__builtins__ = ") + else: + # This formatting could all be handled by the + # _repr() function, which is only reprlib.Repr in + # disguise, so is very configurable. + if self.truncate_locals: + str_repr = saferepr(value) + else: + str_repr = safeformat(value) + # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)): + lines.append(f"{name:<10} = {str_repr}") + # else: + # self._line("%-10s =\\" % (name,)) + # # XXX + # pprint.pprint(value, stream=self.excinfowriter) + return ReprLocals(lines) + return None + + def repr_traceback_entry( + self, + entry: TracebackEntry | None, + excinfo: ExceptionInfo[BaseException] | None = None, + ) -> ReprEntry: + lines: list[str] = [] + style = ( + entry._repr_style + if entry is not None and entry._repr_style is not None + else self.style + ) + if style in ("short", "long") and entry is not None: + source = self._getentrysource(entry) + if source is None: + source = Source("???") + line_index = 0 + end_line_index, colno, end_colno = None, None, None + else: + line_index = entry.relline + end_line_index = entry.end_lineno_relative + colno = entry.colno + end_colno = entry.end_colno + short = style == "short" + reprargs = self.repr_args(entry) if not short else None + s = self.get_source( + source=source, + line_index=line_index, + excinfo=excinfo, + short=short, + end_line_index=end_line_index, + colno=colno, + end_colno=end_colno, + ) + lines.extend(s) + if short: + message = f"in {entry.name}" + else: + message = (excinfo and excinfo.typename) or "" + entry_path = entry.path + path = self._makepath(entry_path) + reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) + localsrepr = self.repr_locals(entry.locals) + return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style) + elif style == "value": + if excinfo: + lines.extend(str(excinfo.value).split("\n")) + return ReprEntry(lines, None, None, None, style) + else: + if excinfo: + lines.extend(self.get_exconly(excinfo, indent=4)) + return ReprEntry(lines, None, None, None, style) + + def _makepath(self, path: Path | str) -> str: + if not self.abspath and isinstance(path, Path): + try: + np = bestrelpath(Path.cwd(), path) + except OSError: + return str(path) + if len(np) < len(str(path)): + return np + return str(path) + + def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback: + traceback = filter_excinfo_traceback(self.tbfilter, excinfo) + + if isinstance(excinfo.value, RecursionError): + traceback, extraline = self._truncate_recursive_traceback(traceback) + else: + extraline = None + + if not traceback: + if extraline is None: + extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames." + entries = [self.repr_traceback_entry(None, excinfo)] + return ReprTraceback(entries, extraline, style=self.style) + + last = traceback[-1] + if self.style == "value": + entries = [self.repr_traceback_entry(last, excinfo)] + return ReprTraceback(entries, None, style=self.style) + + entries = [ + self.repr_traceback_entry(entry, excinfo if last == entry else None) + for entry in traceback + ] + return ReprTraceback(entries, extraline, style=self.style) + + def _truncate_recursive_traceback( + self, traceback: Traceback + ) -> tuple[Traceback, str | None]: + """Truncate the given recursive traceback trying to find the starting + point of the recursion. + + The detection is done by going through each traceback entry and + finding the point in which the locals of the frame are equal to the + locals of a previous frame (see ``recursionindex()``). + + Handle the situation where the recursion process might raise an + exception (for example comparing numpy arrays using equality raises a + TypeError), in which case we do our best to warn the user of the + error and show a limited traceback. + """ + try: + recursionindex = traceback.recursionindex() + except Exception as e: + max_frames = 10 + extraline: str | None = ( + "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n" + " The following exception happened when comparing locals in the stack frame:\n" + f" {type(e).__name__}: {e!s}\n" + f" Displaying first and last {max_frames} stack frames out of {len(traceback)}." + ) + # Type ignored because adding two instances of a List subtype + # currently incorrectly has type List instead of the subtype. + traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore + else: + if recursionindex is not None: + extraline = "!!! Recursion detected (same locals & position)" + traceback = traceback[: recursionindex + 1] + else: + extraline = None + + return traceback, extraline + + def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainRepr: + repr_chain: list[tuple[ReprTraceback, ReprFileLocation | None, str | None]] = [] + e: BaseException | None = excinfo.value + excinfo_: ExceptionInfo[BaseException] | None = excinfo + description = None + seen: set[int] = set() + while e is not None and id(e) not in seen: + seen.add(id(e)) + + if excinfo_: + # Fall back to native traceback as a temporary workaround until + # full support for exception groups added to ExceptionInfo. + # See https://github.com/pytest-dev/pytest/issues/9159 + reprtraceback: ReprTraceback | ReprTracebackNative + if isinstance(e, BaseExceptionGroup): + # don't filter any sub-exceptions since they shouldn't have any internal frames + traceback = filter_excinfo_traceback(self.tbfilter, excinfo) + extraline = ( + "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames." + if not traceback + else None + ) + reprtraceback = ReprTracebackNative( + format_exception( + type(excinfo.value), + excinfo.value, + traceback[0]._rawentry if traceback else None, + ), + extraline=extraline, + ) + + else: + reprtraceback = self.repr_traceback(excinfo_) + reprcrash = excinfo_._getreprcrash() + else: + # Fallback to native repr if the exception doesn't have a traceback: + # ExceptionInfo objects require a full traceback to work. + reprtraceback = ReprTracebackNative(format_exception(type(e), e, None)) + reprcrash = None + repr_chain.append((reprtraceback, reprcrash, description)) + + if e.__cause__ is not None and self.chain: + e = e.__cause__ + excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None + description = "The above exception was the direct cause of the following exception:" + elif ( + e.__context__ is not None and not e.__suppress_context__ and self.chain + ): + e = e.__context__ + excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None + description = "During handling of the above exception, another exception occurred:" + else: + e = None + repr_chain.reverse() + return ExceptionChainRepr(repr_chain) + + +@dataclasses.dataclass(eq=False) +class TerminalRepr: + """Base class for terminal representations -- pieces of data that display + themselves to a terminal.""" + + def __str__(self) -> str: + # FYI this is called from pytest-xdist's serialization of exception + # information. + io = StringIO() + tw = TerminalWriter(file=io) + self.toterminal(tw) + return io.getvalue().strip() + + def __repr__(self) -> str: + return f"<{self.__class__} instance at {id(self):0x}>" + + def toterminal(self, tw: TerminalWriter) -> None: + raise NotImplementedError() + + +@dataclasses.dataclass(eq=False) +class ExceptionRepr(TerminalRepr): + """Base class for exception terminal representations. + + The representation generally contains: + - The exception traceback (`reprtraceback`) + - The exception message and location (`reprcrash`) + - Separated, titled sections with additional data (pytest core doesn't use + this currently). + """ + + reprtraceback: ReprTraceback + reprcrash: ReprFileLocation | None + sections: list[tuple[str, str, str]] = dataclasses.field( + init=False, default_factory=list + ) + + def addsection(self, name: str, content: str, sep: str = "-") -> None: + self.sections.append((name, content, sep)) + + def toterminal(self, tw: TerminalWriter) -> None: + for name, content, sep in self.sections: + tw.sep(sep, name) + tw.line(content) + + +@dataclasses.dataclass(eq=False) +class ExceptionChainRepr(ExceptionRepr): + """A chain of exceptions, separated by descriptions (e.g. "The above + exception was the direct cause of the following exception").""" + + chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]] + + def __init__( + self, + chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]], + ) -> None: + # reprcrash and reprtraceback of the outermost (the newest) exception + # in the chain. + super().__init__( + reprtraceback=chain[-1][0], + reprcrash=chain[-1][1], + ) + self.chain = chain + + def toterminal(self, tw: TerminalWriter) -> None: + for reprtraceback, reprcrash, description in self.chain: + reprtraceback.toterminal(tw) + if description is not None: + tw.line("") + tw.line(description, yellow=True) + super().toterminal(tw) + + +@dataclasses.dataclass(eq=False) +class ReprExceptionInfo(ExceptionRepr): + """A single exception with optional extra details (function arguments, + function locals, file location) and possible extra line and sections emitted + at the end.""" + + def toterminal(self, tw: TerminalWriter) -> None: + self.reprtraceback.toterminal(tw) + super().toterminal(tw) + + +@dataclasses.dataclass(eq=False) +class ReprTraceback(TerminalRepr): + """A traceback with optional extra details (function arguments, function + locals, file location) and possible extra line emitted at the end.""" + + reprentries: Sequence[ReprEntry | ReprEntryNative] + extraline: str | None + style: TracebackStyle + + entrysep: ClassVar = "_ " + + def toterminal(self, tw: TerminalWriter) -> None: + # The entries might have different styles. + for i, entry in enumerate(self.reprentries): + if entry.style == "long": + tw.line("") + entry.toterminal(tw) + if i < len(self.reprentries) - 1: + next_entry = self.reprentries[i + 1] + if entry.style == "long" or ( + entry.style == "short" and next_entry.style == "long" + ): + tw.sep(self.entrysep) + + if self.extraline: + tw.line(self.extraline) + + +class ReprTracebackNative(ReprTraceback): + """A traceback in native style. + + The lines are emitted as is; uses a single entry for the entire native + traceback. + """ + + def __init__(self, tblines: Sequence[str], *, extraline: str | None = None) -> None: + self.reprentries = [ReprEntryNative(tblines)] + self.extraline = extraline + self.style = "native" + + +@dataclasses.dataclass(eq=False) +class ReprEntryNative(TerminalRepr): + """An entry in a traceback in native style. + + Emits the lines as is. The lines are assumed to include the line separators. + + [Note that we currently use a single "entry" for the entire native + traceback, so this is a bit misleading, but there's no point trying to parse + or split a native traceback.] + """ + + lines: Sequence[str] + + style: ClassVar[TracebackStyle] = "native" + + def toterminal(self, tw: TerminalWriter) -> None: + tw.write("".join(self.lines)) + + +@dataclasses.dataclass(eq=False) +class ReprEntry(TerminalRepr): + """An entry in a traceback with possible extra details (function arguments, + function locals, source snippet, function's file and line).""" + + lines: Sequence[str] + reprfuncargs: ReprFuncArgs | None + reprlocals: ReprLocals | None + reprfileloc: ReprFileLocation | None + style: TracebackStyle + + def _write_entry_lines(self, tw: TerminalWriter) -> None: + """Write the source code portions of a list of traceback entries with syntax highlighting. + + Usually entries are lines like these: + + " x = 1" + "> assert x == 2" + "E assert 1 == 2" + + This function takes care of rendering the "source" portions of it (the lines without + the "E" prefix) using syntax highlighting, taking care to not highlighting the ">" + character, as doing so might break line continuations. + """ + if not self.lines: + return + + if self.style == "value": + # Using tw.write instead of tw.line for testing purposes due to TWMock implementation; + # lines written with TWMock.line and TWMock._write_source cannot be distinguished + # from each other, whereas lines written with TWMock.write are marked with TWMock.WRITE + for line in self.lines: + tw.write(line) + tw.write("\n") + return + + # separate indents and source lines that are not failures: we want to + # highlight the code but not the indentation, which may contain markers + # such as "> assert 0" + fail_marker = f"{ExceptionInfoFormatter.fail_marker} " + indent_size = len(fail_marker) + indents: list[str] = [] + source_lines: list[str] = [] + failure_lines: list[str] = [] + for index, line in enumerate(self.lines): + is_failure_line = line.startswith(fail_marker) + if is_failure_line: + # from this point on all lines are considered part of the failure + failure_lines.extend(self.lines[index:]) + break + else: + indents.append(line[:indent_size]) + source_lines.append(line[indent_size:]) + + tw._write_source(source_lines, indents) + + # failure lines are always completely red and bold + for line in failure_lines: + tw.line(line, bold=True, red=True) + + def toterminal(self, tw: TerminalWriter) -> None: + if self.style == "short": + if self.reprfileloc: + self.reprfileloc.toterminal(tw) + self._write_entry_lines(tw) + if self.reprlocals: + self.reprlocals.toterminal(tw, indent=" " * 8) + return + + if self.reprfuncargs: + self.reprfuncargs.toterminal(tw) + + self._write_entry_lines(tw) + + if self.reprlocals: + tw.line("") + self.reprlocals.toterminal(tw) + if self.reprfileloc: + if self.lines: + tw.line("") + self.reprfileloc.toterminal(tw) + + def __str__(self) -> str: + return "{}\n{}\n{}".format( + "\n".join(self.lines), self.reprlocals, self.reprfileloc + ) + + +@dataclasses.dataclass(eq=False) +class ReprFileLocation(TerminalRepr): + """A message at a file location, using the `:: ` + format that most editors understand. + + Only the first line of the message is emitted. + """ + + path: str + lineno: int + message: str + + def __post_init__(self) -> None: + self.path = str(self.path) + + def toterminal(self, tw: TerminalWriter) -> None: + msg = self.message + i = msg.find("\n") + if i != -1: + msg = msg[:i] + tw.write(self.path, bold=True, red=True) + tw.line(f":{self.lineno}: {msg}") + + +@dataclasses.dataclass(eq=False) +class ReprLocals(TerminalRepr): + """Function local variables (pre-formatted).""" + + lines: Sequence[str] + + def toterminal(self, tw: TerminalWriter, indent: str = "") -> None: + for line in self.lines: + tw.line(indent + line) + + +@dataclasses.dataclass(eq=False) +class ReprFuncArgs(TerminalRepr): + """Function arguments - name = value, comma separated.""" + + args: Sequence[tuple[str, object]] + + def toterminal(self, tw: TerminalWriter) -> None: + if self.args: + linesofar = "" + for name, value in self.args: + ns = f"{name} = {value}" + if len(ns) + len(linesofar) + 2 > tw.fullwidth: + if linesofar: + tw.line(linesofar) + linesofar = ns + else: + if linesofar: + linesofar += ", " + ns + else: + linesofar = ns + if linesofar: + tw.line(linesofar) + tw.line("") + + +def getfslineno(obj: object) -> tuple[str | Path, int]: + """Return source location (path, lineno) for the given object. + + If the source cannot be determined return ("", -1). + + The line number is 0-based. + """ + # xxx let decorators etc specify a sane ordering + # NOTE: this used to be done in _pytest.compat.getfslineno, initially added + # in 6ec13a2b9. It ("place_as") appears to be something very custom. + obj = get_real_func(obj) + if hasattr(obj, "place_as"): + obj = obj.place_as + + try: + code = Code.from_function(obj) + except TypeError: + try: + fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type] + except TypeError: + return "", -1 + + fspath = (fn and absolutepath(fn)) or "" + lineno = -1 + if fspath: + try: + _, lineno = findsource(obj) + except OSError: + pass + return fspath, lineno + + return code.path, code.firstlineno + + +def _byte_offset_to_character_offset(str, offset): + """Converts a byte based offset in a string to a code-point.""" + as_utf8 = str.encode("utf-8") + return len(as_utf8[:offset].decode("utf-8", errors="replace")) + + +# Relative paths that we use to filter traceback entries from appearing to the user; +# see filter_traceback. +# note: if we need to add more paths than what we have now we should probably use a list +# for better maintenance. + +_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc")) +# pluggy is either a package or a single module depending on the version +if _PLUGGY_DIR.name == "__init__.py": + _PLUGGY_DIR = _PLUGGY_DIR.parent +_PYTEST_DIR = Path(_pytest.__file__).parent + + +def filter_traceback(entry: TracebackEntry) -> bool: + """Return True if a TracebackEntry instance should be included in tracebacks. + + We hide traceback entries of: + + * dynamically generated code (no code to show up for it); + * internal traceback from pytest or its internal libraries, py and pluggy. + """ + # entry.path might sometimes return a str object when the entry + # points to dynamically generated code. + # See https://bitbucket.org/pytest-dev/py/issues/71. + raw_filename = entry.frame.code.raw.co_filename + is_generated = "<" in raw_filename and ">" in raw_filename + if is_generated: + return False + + # entry.path might point to a non-existing file, in which case it will + # also return a str object. See #1133. + p = Path(entry.path) + + parents = p.parents + if _PLUGGY_DIR in parents: + return False + if _PYTEST_DIR in parents: + return False + + return True + + +def filter_excinfo_traceback( + tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException] +) -> Traceback: + """Filter the exception traceback in ``excinfo`` according to ``tbfilter``.""" + if callable(tbfilter): + return tbfilter(excinfo) + elif tbfilter: + return excinfo.traceback.filter(excinfo) + else: + return excinfo.traceback diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_code/source.py b/.venv-test/lib/python3.12/site-packages/_pytest/_code/source.py new file mode 100644 index 0000000..cbadf66 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_code/source.py @@ -0,0 +1,228 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import ast +from bisect import bisect_right +from collections.abc import Iterable +from collections.abc import Iterator +import inspect +import textwrap +import tokenize +import types +from typing import overload +import warnings + + +class Source: + """An immutable object holding a source code fragment. + + When using Source(...), the source lines are deindented. + """ + + def __init__(self, obj: object = None) -> None: + if not obj: + self.lines: list[str] = [] + self.raw_lines: list[str] = [] + elif isinstance(obj, Source): + self.lines = obj.lines + self.raw_lines = obj.raw_lines + elif isinstance(obj, tuple | list): + self.lines = deindent(x.rstrip("\n") for x in obj) + self.raw_lines = list(x.rstrip("\n") for x in obj) + elif isinstance(obj, str): + self.lines = deindent(obj.split("\n")) + self.raw_lines = obj.split("\n") + else: + try: + rawcode = getrawcode(obj) + src = inspect.getsource(rawcode) + except TypeError: + src = inspect.getsource(obj) # type: ignore[arg-type] + self.lines = deindent(src.split("\n")) + self.raw_lines = src.split("\n") + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Source): + return NotImplemented + return self.lines == other.lines + + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore + + @overload + def __getitem__(self, key: int) -> str: ... + + @overload + def __getitem__(self, key: slice) -> Source: ... + + def __getitem__(self, key: int | slice) -> str | Source: + if isinstance(key, int): + return self.lines[key] + else: + if key.step not in (None, 1): + raise IndexError("cannot slice a Source with a step") + newsource = Source() + newsource.lines = self.lines[key.start : key.stop] + newsource.raw_lines = self.raw_lines[key.start : key.stop] + return newsource + + def __iter__(self) -> Iterator[str]: + return iter(self.lines) + + def __len__(self) -> int: + return len(self.lines) + + def strip(self) -> Source: + """Return new Source object with trailing and leading blank lines removed.""" + start, end = 0, len(self) + while start < end and not self.lines[start].strip(): + start += 1 + while end > start and not self.lines[end - 1].strip(): + end -= 1 + source = Source() + source.raw_lines = self.raw_lines + source.lines[:] = self.lines[start:end] + return source + + def indent(self, indent: str = " " * 4) -> Source: + """Return a copy of the source object with all lines indented by the + given indent-string.""" + newsource = Source() + newsource.raw_lines = self.raw_lines + newsource.lines = [(indent + line) for line in self.lines] + return newsource + + def getstatement(self, lineno: int) -> Source: + """Return Source statement which contains the given linenumber + (counted from 0).""" + start, end = self.getstatementrange(lineno) + return self[start:end] + + def getstatementrange(self, lineno: int) -> tuple[int, int]: + """Return (start, end) tuple which spans the minimal statement region + which containing the given lineno.""" + if not (0 <= lineno < len(self)): + raise IndexError("lineno out of range") + _ast, start, end = getstatementrange_ast(lineno, self) + return start, end + + def deindent(self) -> Source: + """Return a new Source object deindented.""" + newsource = Source() + newsource.lines[:] = deindent(self.lines) + newsource.raw_lines = self.raw_lines + return newsource + + def __str__(self) -> str: + return "\n".join(self.lines) + + +# +# helper functions +# + + +def findsource(obj) -> tuple[Source | None, int]: + try: + sourcelines, lineno = inspect.findsource(obj) + except Exception: + return None, -1 + source = Source() + source.lines = [line.rstrip() for line in sourcelines] + source.raw_lines = sourcelines + return source, lineno + + +def getrawcode(obj: object, trycall: bool = True) -> types.CodeType: + """Return code object for given function.""" + try: + return obj.__code__ # type: ignore[attr-defined,no-any-return] + except AttributeError: + pass + if trycall: + call = getattr(obj, "__call__", None) + if call and not isinstance(obj, type): + return getrawcode(call, trycall=False) + raise TypeError(f"could not get code object for {obj!r}") + + +def deindent(lines: Iterable[str]) -> list[str]: + return textwrap.dedent("\n".join(lines)).splitlines() + + +def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None]: + # Flatten all statements and except handlers into one lineno-list. + # AST's line numbers start indexing at 1. + values: list[int] = [] + for x in ast.walk(node): + if isinstance(x, ast.stmt | ast.ExceptHandler): + # The lineno points to the class/def, so need to include the decorators. + if isinstance(x, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef): + for d in x.decorator_list: + values.append(d.lineno - 1) + values.append(x.lineno - 1) + for name in ("finalbody", "orelse"): + val: list[ast.stmt] | None = getattr(x, name, None) + if val: + # Treat the finally/orelse part as its own statement. + values.append(val[0].lineno - 1 - 1) + values.sort() + insert_index = bisect_right(values, lineno) + if insert_index == 0: + return 0, None + start = values[insert_index - 1] + if insert_index >= len(values): + end = None + else: + end = values[insert_index] + return start, end + + +def getstatementrange_ast( + lineno: int, + source: Source, + assertion: bool = False, + astnode: ast.AST | None = None, +) -> tuple[ast.AST, int, int]: + if astnode is None: + content = str(source) + # See #4260: + # Don't produce duplicate warnings when compiling source to find AST. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + astnode = ast.parse(content, "source", "exec") + + start, end = get_statement_startend2(lineno, astnode) + # We need to correct the end: + # - ast-parsing strips comments + # - there might be empty lines + # - we might have lesser indented code blocks at the end + if end is None: + end = len(source.lines) + + if end > start + 1: + # Make sure we don't span differently indented code blocks + # by using the BlockFinder helper used which inspect.getsource() uses itself. + block_finder = inspect.BlockFinder() + # If we start with an indented line, put blockfinder to "started" mode. + block_finder.started = ( + bool(source.lines[start]) and source.lines[start][0].isspace() + ) + it = ((x + "\n") for x in source.lines[start:end]) + try: + for tok in tokenize.generate_tokens(lambda: next(it)): + block_finder.tokeneater(*tok) + except (inspect.EndOfBlock, IndentationError): + end = block_finder.last + start + except Exception: + pass + + # The end might still point to a comment or empty line, correct it. + end = min(end, len(source.lines)) + while end: + line = source.lines[end - 1].lstrip() + if line.startswith("#") or not line: + end -= 1 + else: + break + return astnode, start, end diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_io/__init__.py b/.venv-test/lib/python3.12/site-packages/_pytest/_io/__init__.py new file mode 100644 index 0000000..b0155b1 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_io/__init__.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from .terminalwriter import get_terminal_width +from .terminalwriter import TerminalWriter + + +__all__ = [ + "TerminalWriter", + "get_terminal_width", +] diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_io/pprint.py b/.venv-test/lib/python3.12/site-packages/_pytest/_io/pprint.py new file mode 100644 index 0000000..ec41b44 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_io/pprint.py @@ -0,0 +1,673 @@ +# mypy: allow-untyped-defs +# This module was imported from the cpython standard library +# (https://github.com/python/cpython/) at commit +# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12). +# +# +# Original Author: Fred L. Drake, Jr. +# fdrake@acm.org +# +# This is a simple little module I wrote to make life easier. I didn't +# see anything quite like it in the library, though I may have overlooked +# something. I wrote this when I was trying to read some heavily nested +# tuples with fairly non-descriptive content. This is modeled very much +# after Lisp/Scheme - style pretty-printing of lists. If you find it +# useful, thank small children who sleep at night. +from __future__ import annotations + +import collections as _collections +from collections.abc import Callable +from collections.abc import Iterator +import dataclasses as _dataclasses +from io import StringIO as _StringIO +import re +import types as _types +from typing import Any +from typing import IO + + +class _safe_key: + """Helper function for key functions when sorting unorderable objects. + + The wrapped-object will fallback to a Py2.x style comparison for + unorderable types (sorting first comparing the type name and then by + the obj ids). Does not work recursively, so dict.items() must have + _safe_key applied to both the key and the value. + + """ + + __slots__ = ["obj"] + + def __init__(self, obj): + self.obj = obj + + def __lt__(self, other): + try: + return self.obj < other.obj + except TypeError: + return (str(type(self.obj)), id(self.obj)) < ( + str(type(other.obj)), + id(other.obj), + ) + + +def _safe_tuple(t): + """Helper function for comparing 2-tuples""" + return _safe_key(t[0]), _safe_key(t[1]) + + +class PrettyPrinter: + def __init__( + self, + indent: int = 4, + width: int = 80, + depth: int | None = None, + ) -> None: + """Handle pretty printing operations onto a stream using a set of + configured parameters. + + indent + Number of spaces to indent for each level of nesting. + + width + Attempted maximum number of columns in the output. + + depth + The maximum depth to print out nested structures. + + """ + if indent < 0: + raise ValueError("indent must be >= 0") + if depth is not None and depth <= 0: + raise ValueError("depth must be > 0") + if not width: + raise ValueError("width must be != 0") + self._depth = depth + self._indent_per_level = indent + self._width = width + + def pformat(self, object: Any) -> str: + sio = _StringIO() + self._format(object, sio, 0, 0, set(), 0) + return sio.getvalue() + + def _format( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + objid = id(object) + if objid in context: + stream.write(_recursion(object)) + return + + p = self._dispatch.get(type(object).__repr__, None) + if p is not None: + context.add(objid) + p(self, object, stream, indent, allowance, context, level + 1) + context.remove(objid) + elif ( + _dataclasses.is_dataclass(object) + and not isinstance(object, type) + and object.__dataclass_params__.repr # type:ignore[attr-defined] + and + # Check dataclass has generated repr method. + hasattr(object.__repr__, "__wrapped__") + and "__create_fn__" in object.__repr__.__wrapped__.__qualname__ + ): + context.add(objid) + self._pprint_dataclass( + object, stream, indent, allowance, context, level + 1 + ) + context.remove(objid) + else: + stream.write(self._repr(object, context, level)) + + def _pprint_dataclass( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + cls_name = object.__class__.__name__ + items = [ + (f.name, getattr(object, f.name)) + for f in _dataclasses.fields(object) + if f.repr + ] + stream.write(cls_name + "(") + self._format_namespace_items(items, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch: dict[ + Callable[..., str], + Callable[[PrettyPrinter, Any, IO[str], int, int, set[int], int], None], + ] = {} + + def _pprint_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + write = stream.write + write("{") + items = object.items() + self._format_dict_items(items, stream, indent, allowance, context, level) + write("}") + + _dispatch[dict.__repr__] = _pprint_dict + + def _pprint_ordered_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not len(object): + stream.write(repr(object)) + return + cls = object.__class__ + stream.write(cls.__name__ + "(") + self._pprint_dict(object, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict + + def _pprint_list( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write("[") + self._format_items(object, stream, indent, allowance, context, level) + stream.write("]") + + _dispatch[list.__repr__] = _pprint_list + + def _pprint_tuple( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write("(") + self._format_items(object, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[tuple.__repr__] = _pprint_tuple + + def _pprint_set( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not len(object): + stream.write(repr(object)) + return + typ = object.__class__ + if typ is set: + stream.write("{") + endchar = "}" + else: + stream.write(typ.__name__ + "({") + endchar = "})" + object = sorted(object, key=_safe_key) + self._format_items(object, stream, indent, allowance, context, level) + stream.write(endchar) + + _dispatch[set.__repr__] = _pprint_set + _dispatch[frozenset.__repr__] = _pprint_set + + def _pprint_str( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + write = stream.write + if not len(object): + write(repr(object)) + return + chunks = [] + lines = object.splitlines(True) + if level == 1: + indent += 1 + allowance += 1 + max_width1 = max_width = self._width - indent + for i, line in enumerate(lines): + rep = repr(line) + if i == len(lines) - 1: + max_width1 -= allowance + if len(rep) <= max_width1: + chunks.append(rep) + else: + # A list of alternating (non-space, space) strings + parts = re.findall(r"\S*\s*", line) + assert parts + assert not parts[-1] + parts.pop() # drop empty last part + max_width2 = max_width + current = "" + for j, part in enumerate(parts): + candidate = current + part + if j == len(parts) - 1 and i == len(lines) - 1: + max_width2 -= allowance + if len(repr(candidate)) > max_width2: + if current: + chunks.append(repr(current)) + current = part + else: + current = candidate + if current: + chunks.append(repr(current)) + if len(chunks) == 1: + write(rep) + return + if level == 1: + write("(") + for i, rep in enumerate(chunks): + if i > 0: + write("\n" + " " * indent) + write(rep) + if level == 1: + write(")") + + _dispatch[str.__repr__] = _pprint_str + + def _pprint_bytes( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + write = stream.write + if len(object) <= 4: + write(repr(object)) + return + parens = level == 1 + if parens: + indent += 1 + allowance += 1 + write("(") + delim = "" + for rep in _wrap_bytes_repr(object, self._width - indent, allowance): + write(delim) + write(rep) + if not delim: + delim = "\n" + " " * indent + if parens: + write(")") + + _dispatch[bytes.__repr__] = _pprint_bytes + + def _pprint_bytearray( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + write = stream.write + write("bytearray(") + self._pprint_bytes( + bytes(object), stream, indent + 10, allowance + 1, context, level + 1 + ) + write(")") + + _dispatch[bytearray.__repr__] = _pprint_bytearray + + def _pprint_mappingproxy( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write("mappingproxy(") + self._format(object.copy(), stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy + + def _pprint_simplenamespace( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if type(object) is _types.SimpleNamespace: + # The SimpleNamespace repr is "namespace" instead of the class + # name, so we do the same here. For subclasses; use the class name. + cls_name = "namespace" + else: + cls_name = object.__class__.__name__ + items = object.__dict__.items() + stream.write(cls_name + "(") + self._format_namespace_items(items, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace + + def _format_dict_items( + self, + items: list[tuple[Any, Any]], + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not items: + return + + write = stream.write + item_indent = indent + self._indent_per_level + delimnl = "\n" + " " * item_indent + for key, ent in items: + write(delimnl) + write(self._repr(key, context, level)) + write(": ") + self._format(ent, stream, item_indent, 1, context, level) + write(",") + + write("\n" + " " * indent) + + def _format_namespace_items( + self, + items: list[tuple[Any, Any]], + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not items: + return + + write = stream.write + item_indent = indent + self._indent_per_level + delimnl = "\n" + " " * item_indent + for key, ent in items: + write(delimnl) + write(key) + write("=") + if id(ent) in context: + # Special-case representation of recursion to match standard + # recursive dataclass repr. + write("...") + else: + self._format( + ent, + stream, + item_indent + len(key) + 1, + 1, + context, + level, + ) + + write(",") + + write("\n" + " " * indent) + + def _format_items( + self, + items: list[Any], + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not items: + return + + write = stream.write + item_indent = indent + self._indent_per_level + delimnl = "\n" + " " * item_indent + + for item in items: + write(delimnl) + self._format(item, stream, item_indent, 1, context, level) + write(",") + + write("\n" + " " * indent) + + def _repr(self, object: Any, context: set[int], level: int) -> str: + return self._safe_repr(object, context.copy(), self._depth, level) + + def _pprint_default_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + rdf = self._repr(object.default_factory, context, level) + stream.write(f"{object.__class__.__name__}({rdf}, ") + self._pprint_dict(object, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict + + def _pprint_counter( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write(object.__class__.__name__ + "(") + + if object: + stream.write("{") + items = object.most_common() + self._format_dict_items(items, stream, indent, allowance, context, level) + stream.write("}") + + stream.write(")") + + _dispatch[_collections.Counter.__repr__] = _pprint_counter + + def _pprint_chain_map( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])): + stream.write(repr(object)) + return + + stream.write(object.__class__.__name__ + "(") + self._format_items(object.maps, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map + + def _pprint_deque( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + stream.write(object.__class__.__name__ + "(") + if object.maxlen is not None: + stream.write(f"maxlen={object.maxlen}, ") + stream.write("[") + + self._format_items(object, stream, indent, allowance + 1, context, level) + stream.write("])") + + _dispatch[_collections.deque.__repr__] = _pprint_deque + + def _pprint_user_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict + + def _pprint_user_list( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserList.__repr__] = _pprint_user_list + + def _pprint_user_string( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: set[int], + level: int, + ) -> None: + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserString.__repr__] = _pprint_user_string + + def _safe_repr( + self, object: Any, context: set[int], maxlevels: int | None, level: int + ) -> str: + typ = type(object) + if typ in _builtin_scalars: + return repr(object) + + r = getattr(typ, "__repr__", None) + + if issubclass(typ, dict) and r is dict.__repr__: + if not object: + return "{}" + objid = id(object) + if maxlevels and level >= maxlevels: + return "{...}" + if objid in context: + return _recursion(object) + context.add(objid) + components: list[str] = [] + append = components.append + level += 1 + for k, v in object.items(): + krepr = self._safe_repr(k, context, maxlevels, level) + vrepr = self._safe_repr(v, context, maxlevels, level) + append(f"{krepr}: {vrepr}") + context.remove(objid) + return "{{{}}}".format(", ".join(components)) + + if (issubclass(typ, list) and r is list.__repr__) or ( + issubclass(typ, tuple) and r is tuple.__repr__ + ): + if issubclass(typ, list): + if not object: + return "[]" + format = "[%s]" + elif len(object) == 1: + format = "(%s,)" + else: + if not object: + return "()" + format = "(%s)" + objid = id(object) + if maxlevels and level >= maxlevels: + return format % "..." + if objid in context: + return _recursion(object) + context.add(objid) + components = [] + append = components.append + level += 1 + for o in object: + orepr = self._safe_repr(o, context, maxlevels, level) + append(orepr) + context.remove(objid) + return format % ", ".join(components) + + return repr(object) + + +_builtin_scalars = frozenset( + {str, bytes, bytearray, float, complex, bool, type(None), int} +) + + +def _recursion(object: Any) -> str: + return f"" + + +def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]: + current = b"" + last = len(object) // 4 * 4 + for i in range(0, len(object), 4): + part = object[i : i + 4] + candidate = current + part + if i == last: + width -= allowance + if len(repr(candidate)) > width: + if current: + yield repr(current) + current = part + else: + current = candidate + if current: + yield repr(current) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_io/saferepr.py b/.venv-test/lib/python3.12/site-packages/_pytest/_io/saferepr.py new file mode 100644 index 0000000..3f5c956 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_io/saferepr.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +from itertools import islice +import pprint +import reprlib + + +def _try_repr_or_str(obj: object) -> str: + try: + return repr(obj) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException: + return f'{type(obj).__name__}("{obj}")' + + +def _format_repr_exception(exc: BaseException, obj: object) -> str: + try: + exc_info = _try_repr_or_str(exc) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as inner_exc: + exc_info = f"unpresentable exception ({_try_repr_or_str(inner_exc)})" + return ( + f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>" + ) + + +def _ellipsize(s: str, maxsize: int) -> str: + if len(s) > maxsize: + i = max(0, (maxsize - 3) // 2) + j = max(0, maxsize - 3 - i) + return s[:i] + "..." + s[len(s) - j :] + return s + + +class SafeRepr(reprlib.Repr): + """ + repr.Repr that limits the resulting size of repr() and includes + information on exceptions raised during the call. + """ + + def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None: + """ + :param maxsize: + If not None, will truncate the resulting repr to that specific size, using ellipsis + somewhere in the middle to hide the extra text. + If None, will not impose any size limits on the returning repr. + """ + super().__init__() + # ``maxstring`` is used by the superclass, and needs to be an int; using a + # very large number in case maxsize is None, meaning we want to disable + # truncation. + self.maxstring = maxsize if maxsize is not None else 1_000_000_000 + self.maxsize = maxsize + self.use_ascii = use_ascii + + def repr(self, x: object) -> str: + try: + if self.use_ascii: + s = ascii(x) + else: + s = super().repr(x) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: + s = _format_repr_exception(exc, x) + if self.maxsize is not None: + s = _ellipsize(s, self.maxsize) + return s + + def repr_instance(self, x: object, level: int) -> str: + try: + s = repr(x) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: + s = _format_repr_exception(exc, x) + if self.maxsize is not None: + s = _ellipsize(s, self.maxsize) + + return s + + def repr_dict(self, x: dict[object, object], level: int) -> str: + """Represent a dict while preserving its insertion order. + + Differs from ``reprlib.Repr.repr_dict`` by iterating directly over ``x`` + rather than using the stdlib's sorting helper. + """ + fillvalue = "..." + n = len(x) + if n == 0: + return "{}" + if level <= 0: + return "{" + fillvalue + "}" + newlevel = level - 1 + repr1 = self.repr1 + pieces = [] + for key in islice(x, self.maxdict): + keyrepr = repr1(key, newlevel) + valrepr = repr1(x[key], newlevel) + pieces.append(f"{keyrepr}: {valrepr}") + if n > self.maxdict: + pieces.append(fillvalue) + return "{" + ", ".join(pieces) + "}" + + +def safeformat(obj: object) -> str: + """Return a pretty printed string for the given object. + + Failing __repr__ functions of user instances will be represented + with a short exception info. + """ + try: + return pprint.pformat(obj) + except Exception as exc: + return _format_repr_exception(exc, obj) + + +# Maximum size of overall repr of objects to display during assertion errors. +DEFAULT_REPR_MAX_SIZE = 240 + + +def saferepr( + obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False +) -> str: + """Return a size-limited safe repr-string for the given object. + + Failing __repr__ functions of user instances will be represented + with a short exception info and 'saferepr' generally takes + care to never raise exceptions itself. + + This function is a wrapper around the Repr/reprlib functionality of the + stdlib. + """ + return SafeRepr(maxsize, use_ascii).repr(obj) + + +def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str: + """Return an unlimited-size safe repr-string for the given object. + + As with saferepr, failing __repr__ functions of user instances + will be represented with a short exception info. + + This function is a wrapper around simple repr. + + Note: a cleaner solution would be to alter ``saferepr``this way + when maxsize=None, but that might affect some other code. + """ + try: + if use_ascii: + return ascii(obj) + return repr(obj) + except Exception as exc: + return _format_repr_exception(exc, obj) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py b/.venv-test/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py new file mode 100644 index 0000000..9191b4e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py @@ -0,0 +1,258 @@ +"""Helper functions for writing to terminals and files.""" + +from __future__ import annotations + +from collections.abc import Sequence +import os +import shutil +import sys +from typing import final +from typing import Literal +from typing import TextIO + +import pygments +from pygments.formatters.terminal import TerminalFormatter +from pygments.lexer import Lexer +from pygments.lexers.diff import DiffLexer +from pygments.lexers.python import PythonLexer + +from ..compat import assert_never +from .wcwidth import wcswidth + + +# This code was initially copied from py 1.8.1, file _io/terminalwriter.py. + + +def get_terminal_width() -> int: + width, _ = shutil.get_terminal_size(fallback=(80, 24)) + + # The Windows get_terminal_size may be bogus, let's sanify a bit. + if width < 40: + width = 80 + + return width + + +def should_do_markup(file: TextIO) -> bool: + if os.environ.get("PY_COLORS") == "1": + return True + if os.environ.get("PY_COLORS") == "0": + return False + if os.environ.get("NO_COLOR"): + return False + if os.environ.get("FORCE_COLOR"): + return True + return ( + hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb" + ) + + +@final +class TerminalWriter: + _esctable = dict( + black=30, + red=31, + green=32, + yellow=33, + blue=34, + purple=35, + cyan=36, + white=37, + Black=40, + Red=41, + Green=42, + Yellow=43, + Blue=44, + Purple=45, + Cyan=46, + White=47, + bold=1, + light=2, + blink=5, + invert=7, + ) + + def __init__(self, file: TextIO | None = None) -> None: + if file is None: + file = sys.stdout + if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32": + try: + import colorama + except ImportError: + pass + else: + file = colorama.AnsiToWin32(file).stream + assert file is not None + self._file = file + self.hasmarkup = should_do_markup(file) + self._current_line = "" + self._terminal_width: int | None = None + self.code_highlight = True + + @property + def fullwidth(self) -> int: + if self._terminal_width is not None: + return self._terminal_width + return get_terminal_width() + + @fullwidth.setter + def fullwidth(self, value: int) -> None: + self._terminal_width = value + + @property + def width_of_current_line(self) -> int: + """Return an estimate of the width so far in the current line.""" + return wcswidth(self._current_line) + + def markup(self, text: str, **markup: bool) -> str: + for name in markup: + if name not in self._esctable: + raise ValueError(f"unknown markup: {name!r}") + if self.hasmarkup: + esc = [self._esctable[name] for name, on in markup.items() if on] + if esc: + text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m" + return text + + def sep( + self, + sepchar: str, + title: str | None = None, + fullwidth: int | None = None, + **markup: bool, + ) -> None: + if fullwidth is None: + fullwidth = self.fullwidth + # The goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth. + if sys.platform == "win32": + # If we print in the last column on windows we are on a + # new line but there is no way to verify/neutralize this + # (we may not know the exact line width). + # So let's be defensive to avoid empty lines in the output. + fullwidth -= 1 + if title is not None: + # we want 2 + 2*len(fill) + len(title) <= fullwidth + # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth + # 2*len(sepchar)*N <= fullwidth - len(title) - 2 + # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) + N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1) + fill = sepchar * N + line = f"{fill} {title} {fill}" + else: + # we want len(sepchar)*N <= fullwidth + # i.e. N <= fullwidth // len(sepchar) + line = sepchar * (fullwidth // len(sepchar)) + # In some situations there is room for an extra sepchar at the right, + # in particular if we consider that with a sepchar like "_ " the + # trailing space is not important at the end of the line. + if len(line) + len(sepchar.rstrip()) <= fullwidth: + line += sepchar.rstrip() + + self.line(line, **markup) + + def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None: + if msg: + current_line = msg.rsplit("\n", 1)[-1] + if "\n" in msg: + self._current_line = current_line + else: + self._current_line += current_line + + msg = self.markup(msg, **markup) + + self.write_raw(msg, flush=flush) + + def write_raw(self, msg: str, *, flush: bool = False) -> None: + try: + self._file.write(msg) + except UnicodeEncodeError: + # Some environments don't support printing general Unicode + # strings, due to misconfiguration or otherwise; in that case, + # print the string escaped to ASCII. + # When the Unicode situation improves we should consider + # letting the error propagate instead of masking it (see #7475 + # for one brief attempt). + msg = msg.encode("unicode-escape").decode("ascii") + self._file.write(msg) + + if flush: + self.flush() + + def line(self, s: str = "", **markup: bool) -> None: + self.write(s, **markup) + self.write("\n") + + def flush(self) -> None: + self._file.flush() + + def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None: + """Write lines of source code possibly highlighted. + + Keeping this private for now because the API is clunky. We should discuss how + to evolve the terminal writer so we can have more precise color support, for example + being able to write part of a line in one color and the rest in another, and so on. + """ + if indents and len(indents) != len(lines): + raise ValueError( + f"indents size ({len(indents)}) should have same size as lines ({len(lines)})" + ) + if not indents: + indents = [""] * len(lines) + source = "\n".join(lines) + new_lines = self._highlight(source).splitlines() + # Would be better to strict=True but that fails some CI jobs. + for indent, new_line in zip(indents, new_lines, strict=False): + self.line(indent + new_line) + + def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer: + if lexer == "python": + return PythonLexer() + elif lexer == "diff": + return DiffLexer() + else: + assert_never(lexer) + + def _get_pygments_formatter(self) -> TerminalFormatter: + from _pytest.config.exceptions import UsageError + + theme = os.getenv("PYTEST_THEME") + theme_mode = os.getenv("PYTEST_THEME_MODE", "dark") + + try: + return TerminalFormatter(bg=theme_mode, style=theme) + except pygments.util.ClassNotFound as e: + raise UsageError( + f"PYTEST_THEME environment variable has an invalid value: '{theme}'. " + "Hint: See available pygments styles with `pygmentize -L styles`." + ) from e + except pygments.util.OptionError as e: + raise UsageError( + f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. " + "The allowed values are 'dark' (default) and 'light'." + ) from e + + def _highlight( + self, source: str, lexer: Literal["diff", "python"] = "python" + ) -> str: + """Highlight the given source if we have markup support.""" + if not source or not self.hasmarkup or not self.code_highlight: + return source + + pygments_lexer = self._get_pygments_lexer(lexer) + pygments_formatter = self._get_pygments_formatter() + + highlighted: str = pygments.highlight( + source, pygments_lexer, pygments_formatter + ) + # pygments terminal formatter may add a newline when there wasn't one. + # We don't want this, remove. + if highlighted[-1] == "\n" and source[-1] != "\n": + highlighted = highlighted[:-1] + + # Some lexers will not set the initial color explicitly + # which may lead to the previous color being propagated to the + # start of the expression, so reset first. + highlighted = "\x1b[0m" + highlighted + + return highlighted diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_io/wcwidth.py b/.venv-test/lib/python3.12/site-packages/_pytest/_io/wcwidth.py new file mode 100644 index 0000000..23886ff --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_io/wcwidth.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from functools import lru_cache +import unicodedata + + +@lru_cache(100) +def wcwidth(c: str) -> int: + """Determine how many columns are needed to display a character in a terminal. + + Returns -1 if the character is not printable. + Returns 0, 1 or 2 for other characters. + """ + o = ord(c) + + # ASCII fast path. + if 0x20 <= o < 0x07F: + return 1 + + # Some Cf/Zp/Zl characters which should be zero-width. + if ( + o == 0x0000 + or 0x200B <= o <= 0x200F + or 0x2028 <= o <= 0x202E + or 0x2060 <= o <= 0x2063 + ): + return 0 + + category = unicodedata.category(c) + + # Control characters. + if category == "Cc": + return -1 + + # Combining characters with zero width. + if category in ("Me", "Mn"): + return 0 + + # Full/Wide east asian characters. + if unicodedata.east_asian_width(c) in ("F", "W"): + return 2 + + return 1 + + +def wcswidth(s: str) -> int: + """Determine how many columns are needed to display a string in a terminal. + + Returns -1 if the string contains non-printable characters. + """ + width = 0 + for c in unicodedata.normalize("NFC", s): + wc = wcwidth(c) + if wc < 0: + return -1 + width += wc + return width diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_py/__init__.py b/.venv-test/lib/python3.12/site-packages/_pytest/_py/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_py/error.py b/.venv-test/lib/python3.12/site-packages/_pytest/_py/error.py new file mode 100644 index 0000000..dace237 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_py/error.py @@ -0,0 +1,119 @@ +"""create errno-specific classes for IO or os calls.""" + +from __future__ import annotations + +from collections.abc import Callable +import errno +import os +import sys +from typing import TYPE_CHECKING +from typing import TypeVar + + +if TYPE_CHECKING: + from typing_extensions import ParamSpec + + P = ParamSpec("P") + +R = TypeVar("R") + + +class Error(EnvironmentError): + def __repr__(self) -> str: + return "{}.{} {!r}: {} ".format( + self.__class__.__module__, + self.__class__.__name__, + self.__class__.__doc__, + " ".join(map(str, self.args)), + # repr(self.args) + ) + + def __str__(self) -> str: + s = "[{}]: {}".format( + self.__class__.__doc__, + " ".join(map(str, self.args)), + ) + return s + + +_winerrnomap = { + 2: errno.ENOENT, + 3: errno.ENOENT, + 17: errno.EEXIST, + 18: errno.EXDEV, + 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable + 22: errno.ENOTDIR, + 20: errno.ENOTDIR, + 267: errno.ENOTDIR, + 5: errno.EACCES, # anything better? +} + + +class ErrorMaker: + """lazily provides Exception classes for each possible POSIX errno + (as defined per the 'errno' module). All such instances + subclass EnvironmentError. + """ + + _errno2class: dict[int, type[Error]] = {} + + def __getattr__(self, name: str) -> type[Error]: + if name[0] == "_": + raise AttributeError(name) + eno = getattr(errno, name) + cls = self._geterrnoclass(eno) + setattr(self, name, cls) + return cls + + def _geterrnoclass(self, eno: int) -> type[Error]: + try: + return self._errno2class[eno] + except KeyError: + clsname = errno.errorcode.get(eno, f"UnknownErrno{eno}") + errorcls = type( + clsname, + (Error,), + {"__module__": "py.error", "__doc__": os.strerror(eno)}, + ) + self._errno2class[eno] = errorcls + return errorcls + + def checked_call( + self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs + ) -> R: + """Call a function and raise an errno-exception if applicable.""" + __tracebackhide__ = True + try: + return func(*args, **kwargs) + except Error: + raise + except OSError as value: + if not hasattr(value, "errno"): + raise + if sys.platform == "win32": + try: + # error: Invalid index type "Optional[int]" for "dict[int, int]"; expected type "int" [index] + # OK to ignore because we catch the KeyError below. + cls = self._geterrnoclass(_winerrnomap[value.errno]) # type:ignore[index] + except KeyError: + raise value + else: + # we are not on Windows, or we got a proper OSError + if value.errno is None: + cls = type( + "UnknownErrnoNone", + (Error,), + {"__module__": "py.error", "__doc__": None}, + ) + else: + cls = self._geterrnoclass(value.errno) + + raise cls(f"{func.__name__}{args!r}") + + +_error_maker = ErrorMaker() +checked_call = _error_maker.checked_call + + +def __getattr__(attr: str) -> type[Error]: + return getattr(_error_maker, attr) # type: ignore[no-any-return] diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_py/path.py b/.venv-test/lib/python3.12/site-packages/_pytest/_py/path.py new file mode 100644 index 0000000..998a781 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_py/path.py @@ -0,0 +1,1475 @@ +# mypy: allow-untyped-defs +"""local path implementation.""" + +from __future__ import annotations + +import atexit +from collections.abc import Callable +from contextlib import contextmanager +import fnmatch +import importlib.util +import io +import os +from os.path import abspath +from os.path import dirname +from os.path import exists +from os.path import isabs +from os.path import isdir +from os.path import isfile +from os.path import islink +from os.path import normpath +import posixpath +from stat import S_ISDIR +from stat import S_ISLNK +from stat import S_ISREG +import sys +from typing import Any +from typing import cast +from typing import Literal +from typing import overload +from typing import TYPE_CHECKING +import uuid +import warnings + +from . import error + + +# Moved from local.py. +iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt") + + +class Checkers: + _depend_on_existence = "exists", "link", "dir", "file" + + def __init__(self, path): + self.path = path + + def dotfile(self): + return self.path.basename.startswith(".") + + def ext(self, arg): + if not arg.startswith("."): + arg = "." + arg + return self.path.ext == arg + + def basename(self, arg): + return self.path.basename == arg + + def basestarts(self, arg): + return self.path.basename.startswith(arg) + + def relto(self, arg): + return self.path.relto(arg) + + def fnmatch(self, arg): + return self.path.fnmatch(arg) + + def endswith(self, arg): + return str(self.path).endswith(arg) + + def _evaluate(self, kw): + from .._code.source import getrawcode + + for name, value in kw.items(): + invert = False + meth = None + try: + meth = getattr(self, name) + except AttributeError: + if name[:3] == "not": + invert = True + try: + meth = getattr(self, name[3:]) + except AttributeError: + pass + if meth is None: + raise TypeError(f"no {name!r} checker available for {self.path!r}") + try: + if getrawcode(meth).co_argcount > 1: + if (not meth(value)) ^ invert: + return False + else: + if bool(value) ^ bool(meth()) ^ invert: + return False + except (error.ENOENT, error.ENOTDIR, error.EBUSY): + # EBUSY feels not entirely correct, + # but its kind of necessary since ENOMEDIUM + # is not accessible in python + for name in self._depend_on_existence: + if name in kw: + if kw.get(name): + return False + name = "not" + name + if name in kw: + if not kw.get(name): + return False + return True + + _statcache: Stat + + def _stat(self) -> Stat: + try: + return self._statcache + except AttributeError: + try: + self._statcache = self.path.stat() + except error.ELOOP: + self._statcache = self.path.lstat() + return self._statcache + + def dir(self): + return S_ISDIR(self._stat().mode) + + def file(self): + return S_ISREG(self._stat().mode) + + def exists(self): + return self._stat() + + def link(self): + st = self.path.lstat() + return S_ISLNK(st.mode) + + +class NeverRaised(Exception): + pass + + +class Visitor: + def __init__(self, fil, rec, ignore, bf, sort): + if isinstance(fil, (str, bytes)): + fil = FNMatcher(fil) + if isinstance(rec, str): + self.rec: Callable[[LocalPath], bool] = FNMatcher(rec) + elif not hasattr(rec, "__call__") and rec: + self.rec = lambda path: True + else: + self.rec = rec + self.fil = fil + self.ignore = ignore + self.breadthfirst = bf + self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x) + + def gen(self, path): + try: + entries = path.listdir() + except self.ignore: + return + rec = self.rec + dirs = self.optsort( + [p for p in entries if p.check(dir=1) and (rec is None or rec(p))] + ) + if not self.breadthfirst: + for subdir in dirs: + yield from self.gen(subdir) + for p in self.optsort(entries): + if self.fil is None or self.fil(p): + yield p + if self.breadthfirst: + for subdir in dirs: + yield from self.gen(subdir) + + +class FNMatcher: + def __init__(self, pattern): + self.pattern = pattern + + def __call__(self, path): + pattern = self.pattern + + if ( + pattern.find(path.sep) == -1 + and iswin32 + and pattern.find(posixpath.sep) != -1 + ): + # Running on Windows, the pattern has no Windows path separators, + # and the pattern has one or more Posix path separators. Replace + # the Posix path separators with the Windows path separator. + pattern = pattern.replace(posixpath.sep, path.sep) + + if pattern.find(path.sep) == -1: + name = path.basename + else: + name = str(path) # path.strpath # XXX svn? + if not os.path.isabs(pattern): + pattern = "*" + path.sep + pattern + return fnmatch.fnmatch(name, pattern) + + +def map_as_list(func, iter): + return list(map(func, iter)) + + +class Stat: + if TYPE_CHECKING: + + @property + def size(self) -> int: ... + + @property + def mtime(self) -> float: ... + + def __getattr__(self, name: str) -> Any: + return getattr(self._osstatresult, "st_" + name) + + def __init__(self, path, osstatresult): + self.path = path + self._osstatresult = osstatresult + + @property + def owner(self): + if iswin32: + raise NotImplementedError("XXX win32") + import pwd + + entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore] + return entry[0] + + @property + def group(self): + """Return group name of file.""" + if iswin32: + raise NotImplementedError("XXX win32") + import grp + + entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore] + return entry[0] + + def isdir(self): + return S_ISDIR(self._osstatresult.st_mode) + + def isfile(self): + return S_ISREG(self._osstatresult.st_mode) + + def islink(self): + self.path.lstat() + return S_ISLNK(self._osstatresult.st_mode) + + +def getuserid(user): + import pwd + + if not isinstance(user, int): + user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore] + return user + + +def getgroupid(group): + import grp + + if not isinstance(group, int): + group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore] + return group + + +class LocalPath: + """Object oriented interface to os.path and other local filesystem + related information. + """ + + class ImportMismatchError(ImportError): + """raised on pyimport() if there is a mismatch of __file__'s""" + + sep = os.sep + + def __init__(self, path=None, expanduser=False): + """Initialize and return a local Path instance. + + Path can be relative to the current directory. + If path is None it defaults to the current working directory. + If expanduser is True, tilde-expansion is performed. + Note that Path instances always carry an absolute path. + Note also that passing in a local path object will simply return + the exact same path object. Use new() to get a new copy. + """ + if path is None: + self.strpath = error.checked_call(os.getcwd) + else: + try: + path = os.fspath(path) + except TypeError: + raise ValueError( + "can only pass None, Path instances " + "or non-empty strings to LocalPath" + ) + if expanduser: + path = os.path.expanduser(path) + self.strpath = abspath(path) + + if sys.platform != "win32": + + def chown(self, user, group, rec=0): + """Change ownership to the given user and group. + user and group may be specified by a number or + by a name. if rec is True change ownership + recursively. + """ + uid = getuserid(user) + gid = getgroupid(group) + if rec: + for x in self.visit(rec=lambda x: x.check(link=0)): + if x.check(link=0): + error.checked_call(os.chown, str(x), uid, gid) + error.checked_call(os.chown, str(self), uid, gid) + + def readlink(self) -> str: + """Return value of a symbolic link.""" + # https://github.com/python/mypy/issues/12278 + return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore] + + def mklinkto(self, oldname): + """Posix style hard link to another name.""" + error.checked_call(os.link, str(oldname), str(self)) + + def mksymlinkto(self, value, absolute=1): + """Create a symbolic link with the given value (pointing to another name).""" + if absolute: + error.checked_call(os.symlink, str(value), self.strpath) + else: + base = self.common(value) + # with posix local paths '/' is always a common base + relsource = self.__class__(value).relto(base) + reldest = self.relto(base) + n = reldest.count(self.sep) + target = self.sep.join(("..",) * n + (relsource,)) + error.checked_call(os.symlink, target, self.strpath) + + def __div__(self, other): + return self.join(os.fspath(other)) + + __truediv__ = __div__ # py3k + + @property + def basename(self): + """Basename part of path.""" + return self._getbyspec("basename")[0] + + @property + def dirname(self): + """Dirname part of path.""" + return self._getbyspec("dirname")[0] + + @property + def purebasename(self): + """Pure base name of the path.""" + return self._getbyspec("purebasename")[0] + + @property + def ext(self): + """Extension of the path (including the '.').""" + return self._getbyspec("ext")[0] + + def read_binary(self): + """Read and return a bytestring from reading the path.""" + with self.open("rb") as f: + return f.read() + + def read_text(self, encoding): + """Read and return a Unicode string from reading the path.""" + with self.open("r", encoding=encoding) as f: + return f.read() + + def read(self, mode="r"): + """Read and return a bytestring from reading the path.""" + with self.open(mode) as f: + return f.read() + + def readlines(self, cr=1): + """Read and return a list of lines from the path. if cr is False, the + newline will be removed from the end of each line.""" + mode = "r" + + if not cr: + content = self.read(mode) + return content.split("\n") + else: + f = self.open(mode) + try: + return f.readlines() + finally: + f.close() + + def load(self): + """(deprecated) return object unpickled from self.read()""" + f = self.open("rb") + try: + import pickle + + return error.checked_call(pickle.load, f) + finally: + f.close() + + def move(self, target): + """Move this path to target.""" + if target.relto(self): + raise error.EINVAL(target, "cannot move path into a subdirectory of itself") + try: + self.rename(target) + except error.EXDEV: # invalid cross-device link + self.copy(target) + self.remove() + + def fnmatch(self, pattern): + """Return true if the basename/fullname matches the glob-'pattern'. + + valid pattern characters:: + + * matches everything + ? matches any single character + [seq] matches any character in seq + [!seq] matches any char not in seq + + If the pattern contains a path-separator then the full path + is used for pattern matching and a '*' is prepended to the + pattern. + + if the pattern doesn't contain a path-separator the pattern + is only matched against the basename. + """ + return FNMatcher(pattern)(self) + + def relto(self, relpath): + """Return a string which is the relative part of the path + to the given 'relpath'. + """ + if not isinstance(relpath, str | LocalPath): + raise TypeError(f"{relpath!r}: not a string or path object") + strrelpath = str(relpath) + if strrelpath and strrelpath[-1] != self.sep: + strrelpath += self.sep + # assert strrelpath[-1] == self.sep + # assert strrelpath[-2] != self.sep + strself = self.strpath + if sys.platform == "win32" or getattr(os, "_name", None) == "nt": + if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)): + return strself[len(strrelpath) :] + elif strself.startswith(strrelpath): + return strself[len(strrelpath) :] + return "" + + def ensure_dir(self, *args): + """Ensure the path joined with args is a directory.""" + return self.ensure(*args, dir=True) + + def bestrelpath(self, dest): + """Return a string which is a relative path from self + (assumed to be a directory) to dest such that + self.join(bestrelpath) == dest and if not such + path can be determined return dest. + """ + try: + if self == dest: + return os.curdir + base = self.common(dest) + if not base: # can be the case on windows + return str(dest) + self2base = self.relto(base) + reldest = dest.relto(base) + if self2base: + n = self2base.count(self.sep) + 1 + else: + n = 0 + lst = [os.pardir] * n + if reldest: + lst.append(reldest) + target = dest.sep.join(lst) + return target + except AttributeError: + return str(dest) + + def exists(self): + return self.check() + + def isdir(self): + return self.check(dir=1) + + def isfile(self): + return self.check(file=1) + + def parts(self, reverse=False): + """Return a root-first list of all ancestor directories + plus the path itself. + """ + current = self + lst = [self] + while 1: + last = current + current = current.dirpath() + if last == current: + break + lst.append(current) + if not reverse: + lst.reverse() + return lst + + def common(self, other): + """Return the common part shared with the other path + or None if there is no common part. + """ + last = None + for x, y in zip(self.parts(), other.parts()): + if x != y: + return last + last = x + return last + + def __add__(self, other): + """Return new path object with 'other' added to the basename""" + return self.new(basename=self.basename + str(other)) + + def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): + """Yields all paths below the current one + + fil is a filter (glob pattern or callable), if not matching the + path will not be yielded, defaulting to None (everything is + returned) + + rec is a filter (glob pattern or callable) that controls whether + a node is descended, defaulting to None + + ignore is an Exception class that is ignoredwhen calling dirlist() + on any of the paths (by default, all exceptions are reported) + + bf if True will cause a breadthfirst search instead of the + default depthfirst. Default: False + + sort if True will sort entries within each directory level. + """ + yield from Visitor(fil, rec, ignore, bf, sort).gen(self) + + def _sortlist(self, res, sort): + if sort: + if hasattr(sort, "__call__"): + warnings.warn( + DeprecationWarning( + "listdir(sort=callable) is deprecated and breaks on python3" + ), + stacklevel=3, + ) + res.sort(sort) + else: + res.sort() + + def __fspath__(self): + return self.strpath + + def __hash__(self): + s = self.strpath + if iswin32: + s = s.lower() + return hash(s) + + def __eq__(self, other): + s1 = os.fspath(self) + try: + s2 = os.fspath(other) + except TypeError: + return False + if iswin32: + s1 = s1.lower() + try: + s2 = s2.lower() + except AttributeError: + return False + return s1 == s2 + + def __ne__(self, other): + return not (self == other) + + def __lt__(self, other): + return os.fspath(self) < os.fspath(other) + + def __gt__(self, other): + return os.fspath(self) > os.fspath(other) + + def samefile(self, other): + """Return True if 'other' references the same file as 'self'.""" + other = os.fspath(other) + if not isabs(other): + other = abspath(other) + if self == other: + return True + if not hasattr(os.path, "samefile"): + return False + return error.checked_call(os.path.samefile, self.strpath, other) + + def remove(self, rec=1, ignore_errors=False): + """Remove a file or directory (or a directory tree if rec=1). + if ignore_errors is True, errors while removing directories will + be ignored. + """ + if self.check(dir=1, link=0): + if rec: + # force remove of readonly files on windows + if iswin32: + self.chmod(0o700, rec=1) + import shutil + + error.checked_call( + shutil.rmtree, self.strpath, ignore_errors=ignore_errors + ) + else: + error.checked_call(os.rmdir, self.strpath) + else: + if iswin32: + self.chmod(0o700) + error.checked_call(os.remove, self.strpath) + + def computehash(self, hashtype="md5", chunksize=524288): + """Return hexdigest of hashvalue for this file.""" + try: + try: + import hashlib as mod + except ImportError: + if hashtype == "sha1": + hashtype = "sha" + mod = __import__(hashtype) + hash = getattr(mod, hashtype)() + except (AttributeError, ImportError): + raise ValueError(f"Don't know how to compute {hashtype!r} hash") + f = self.open("rb") + try: + while 1: + buf = f.read(chunksize) + if not buf: + return hash.hexdigest() + hash.update(buf) + finally: + f.close() + + def new(self, **kw): + """Create a modified version of this path. + the following keyword arguments modify various path parts:: + + a:/some/path/to/a/file.ext + xx drive + xxxxxxxxxxxxxxxxx dirname + xxxxxxxx basename + xxxx purebasename + xxx ext + """ + obj = object.__new__(self.__class__) + if not kw: + obj.strpath = self.strpath + return obj + drive, dirname, _basename, purebasename, ext = self._getbyspec( + "drive,dirname,basename,purebasename,ext" + ) + if "basename" in kw: + if "purebasename" in kw or "ext" in kw: + raise ValueError(f"invalid specification {kw!r}") + else: + pb = kw.setdefault("purebasename", purebasename) + try: + ext = kw["ext"] + except KeyError: + pass + else: + if ext and not ext.startswith("."): + ext = "." + ext + kw["basename"] = pb + ext + + if "dirname" in kw and not kw["dirname"]: + kw["dirname"] = drive + else: + kw.setdefault("dirname", dirname) + kw.setdefault("sep", self.sep) + obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw)) + return obj + + def _getbyspec(self, spec: str) -> list[str]: + """See new for what 'spec' can be.""" + res = [] + parts = self.strpath.split(self.sep) + + args = filter(None, spec.split(",")) + for name in args: + if name == "drive": + res.append(parts[0]) + elif name == "dirname": + res.append(self.sep.join(parts[:-1])) + else: + basename = parts[-1] + if name == "basename": + res.append(basename) + else: + i = basename.rfind(".") + if i == -1: + purebasename, ext = basename, "" + else: + purebasename, ext = basename[:i], basename[i:] + if name == "purebasename": + res.append(purebasename) + elif name == "ext": + res.append(ext) + else: + raise ValueError(f"invalid part specification {name!r}") + return res + + def dirpath(self, *args, **kwargs): + """Return the directory path joined with any given path arguments.""" + if not kwargs: + path = object.__new__(self.__class__) + path.strpath = dirname(self.strpath) + if args: + path = path.join(*args) + return path + return self.new(basename="").join(*args, **kwargs) + + def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath: + """Return a new path by appending all 'args' as path + components. if abs=1 is used restart from root if any + of the args is an absolute path. + """ + sep = self.sep + strargs = [os.fspath(arg) for arg in args] + strpath = self.strpath + if abs: + newargs: list[str] = [] + for arg in reversed(strargs): + if isabs(arg): + strpath = arg + strargs = newargs + break + newargs.insert(0, arg) + # special case for when we have e.g. strpath == "/" + actual_sep = "" if strpath.endswith(sep) else sep + for arg in strargs: + arg = arg.strip(sep) + if iswin32: + # allow unix style paths even on windows. + arg = arg.strip("/") + arg = arg.replace("/", sep) + strpath = strpath + actual_sep + arg + actual_sep = sep + obj = object.__new__(self.__class__) + obj.strpath = normpath(strpath) + return obj + + def open(self, mode="r", ensure=False, encoding=None): + """Return an opened file with the given mode. + + If ensure is True, create parent directories if needed. + """ + if ensure: + self.dirpath().ensure(dir=1) + if encoding: + return error.checked_call( + io.open, + self.strpath, + mode, + encoding=encoding, + ) + return error.checked_call(open, self.strpath, mode) + + def _fastjoin(self, name): + child = object.__new__(self.__class__) + child.strpath = self.strpath + self.sep + name + return child + + def islink(self): + return islink(self.strpath) + + def check(self, **kw): + """Check a path for existence and properties. + + Without arguments, return True if the path exists, otherwise False. + + valid checkers:: + + file = 1 # is a file + file = 0 # is not a file (may not even exist) + dir = 1 # is a dir + link = 1 # is a link + exists = 1 # exists + + You can specify multiple checker definitions, for example:: + + path.check(file=1, link=1) # a link pointing to a file + """ + if not kw: + return exists(self.strpath) + if len(kw) == 1: + if "dir" in kw: + return not kw["dir"] ^ isdir(self.strpath) + if "file" in kw: + return not kw["file"] ^ isfile(self.strpath) + if not kw: + kw = {"exists": 1} + return Checkers(self)._evaluate(kw) + + _patternchars = set("*?[" + os.sep) + + def listdir(self, fil=None, sort=None): + """List directory contents, possibly filter by the given fil func + and possibly sorted. + """ + if fil is None and sort is None: + names = error.checked_call(os.listdir, self.strpath) + return map_as_list(self._fastjoin, names) + if isinstance(fil, str): + if not self._patternchars.intersection(fil): + child = self._fastjoin(fil) + if exists(child.strpath): + return [child] + return [] + fil = FNMatcher(fil) + names = error.checked_call(os.listdir, self.strpath) + res = [] + for name in names: + child = self._fastjoin(name) + if fil is None or fil(child): + res.append(child) + self._sortlist(res, sort) + return res + + def size(self) -> int: + """Return size of the underlying file object""" + return self.stat().size + + def mtime(self) -> float: + """Return last modification time of the path.""" + return self.stat().mtime + + def copy(self, target, mode=False, stat=False): + """Copy path to target. + + If mode is True, will copy permission from path to target. + If stat is True, copy permission, last modification + time, last access time, and flags from path to target. + """ + if self.check(file=1): + if target.check(dir=1): + target = target.join(self.basename) + assert self != target + copychunked(self, target) + if mode: + copymode(self.strpath, target.strpath) + if stat: + copystat(self, target) + else: + + def rec(p): + return p.check(link=0) + + for x in self.visit(rec=rec): + relpath = x.relto(self) + newx = target.join(relpath) + newx.dirpath().ensure(dir=1) + if x.check(link=1): + newx.mksymlinkto(x.readlink()) + continue + elif x.check(file=1): + copychunked(x, newx) + elif x.check(dir=1): + newx.ensure(dir=1) + if mode: + copymode(x.strpath, newx.strpath) + if stat: + copystat(x, newx) + + def rename(self, target): + """Rename this path to target.""" + target = os.fspath(target) + return error.checked_call(os.rename, self.strpath, target) + + def dump(self, obj, bin=1): + """Pickle object into path location""" + f = self.open("wb") + import pickle + + try: + error.checked_call(pickle.dump, obj, f, bin) + finally: + f.close() + + def mkdir(self, *args): + """Create & return the directory joined with args.""" + p = self.join(*args) + error.checked_call(os.mkdir, os.fspath(p)) + return p + + def write_binary(self, data, ensure=False): + """Write binary data into path. If ensure is True create + missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + with self.open("wb") as f: + f.write(data) + + def write_text(self, data, encoding, ensure=False): + """Write text data into path using the specified encoding. + If ensure is True create missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + with self.open("w", encoding=encoding) as f: + f.write(data) + + def write(self, data, mode="w", ensure=False): + """Write data into path. If ensure is True create + missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + if "b" in mode: + if not isinstance(data, bytes): + raise ValueError("can only process bytes") + else: + if not isinstance(data, str): + if not isinstance(data, bytes): + data = str(data) + else: + data = data.decode(sys.getdefaultencoding()) + f = self.open(mode) + try: + f.write(data) + finally: + f.close() + + def _ensuredirs(self): + parent = self.dirpath() + if parent == self: + return self + if parent.check(dir=0): + parent._ensuredirs() + if self.check(dir=0): + try: + self.mkdir() + except error.EEXIST: + # race condition: file/dir created by another thread/process. + # complain if it is not a dir + if self.check(dir=0): + raise + return self + + def ensure(self, *args, **kwargs): + """Ensure that an args-joined path exists (by default as + a file). if you specify a keyword argument 'dir=True' + then the path is forced to be a directory path. + """ + p = self.join(*args) + if kwargs.get("dir", 0): + return p._ensuredirs() + else: + p.dirpath()._ensuredirs() + if not p.check(file=1): + p.open("wb").close() + return p + + @overload + def stat(self, raising: Literal[True] = ...) -> Stat: ... + + @overload + def stat(self, raising: Literal[False]) -> Stat | None: ... + + def stat(self, raising: bool = True) -> Stat | None: + """Return an os.stat() tuple.""" + if raising: + return Stat(self, error.checked_call(os.stat, self.strpath)) + try: + return Stat(self, os.stat(self.strpath)) + except KeyboardInterrupt: + raise + except Exception: + return None + + def lstat(self) -> Stat: + """Return an os.lstat() tuple.""" + return Stat(self, error.checked_call(os.lstat, self.strpath)) + + def setmtime(self, mtime=None): + """Set modification time for the given path. if 'mtime' is None + (the default) then the file's mtime is set to current time. + + Note that the resolution for 'mtime' is platform dependent. + """ + if mtime is None: + return error.checked_call(os.utime, self.strpath, mtime) + try: + return error.checked_call(os.utime, self.strpath, (-1, mtime)) + except error.EINVAL: + return error.checked_call(os.utime, self.strpath, (self.atime(), mtime)) + + def chdir(self): + """Change directory to self and return old current directory""" + try: + old = self.__class__() + except error.ENOENT: + old = None + error.checked_call(os.chdir, self.strpath) + return old + + @contextmanager + def as_cwd(self): + """ + Return a context manager, which changes to the path's dir during the + managed "with" context. + On __enter__ it returns the old dir, which might be ``None``. + """ + old = self.chdir() + try: + yield old + finally: + if old is not None: + old.chdir() + + def realpath(self): + """Return a new path which contains no symbolic links.""" + return self.__class__(os.path.realpath(self.strpath)) + + def atime(self): + """Return last access time of the path.""" + return self.stat().atime + + def __repr__(self): + return f"local({self.strpath!r})" + + def __str__(self): + """Return string representation of the Path.""" + return self.strpath + + def chmod(self, mode, rec=0): + """Change permissions to the given mode. If mode is an + integer it directly encodes the os-specific modes. + if rec is True perform recursively. + """ + if not isinstance(mode, int): + raise TypeError(f"mode {mode!r} must be an integer") + if rec: + for x in self.visit(rec=rec): + error.checked_call(os.chmod, str(x), mode) + error.checked_call(os.chmod, self.strpath, mode) + + def pypkgpath(self): + """Return the Python package path by looking for the last + directory upwards which still contains an __init__.py. + Return None if a pkgpath cannot be determined. + """ + pkgpath = None + for parent in self.parts(reverse=True): + if parent.isdir(): + if not parent.join("__init__.py").exists(): + break + if not isimportable(parent.basename): + break + pkgpath = parent + return pkgpath + + def _ensuresyspath(self, ensuremode, path): + if ensuremode: + s = str(path) + if ensuremode == "append": + if s not in sys.path: + sys.path.append(s) + else: + if s != sys.path[0]: + sys.path.insert(0, s) + + def pyimport(self, modname=None, ensuresyspath=True): + """Return path as an imported python module. + + If modname is None, look for the containing package + and construct an according module name. + The module will be put/looked up in sys.modules. + if ensuresyspath is True then the root dir for importing + the file (taking __init__.py files into account) will + be prepended to sys.path if it isn't there already. + If ensuresyspath=="append" the root dir will be appended + if it isn't already contained in sys.path. + if ensuresyspath is False no modification of syspath happens. + + Special value of ensuresyspath=="importlib" is intended + purely for using in pytest, it is capable only of importing + separate .py files outside packages, e.g. for test suite + without any __init__.py file. It effectively allows having + same-named test modules in different places and offers + mild opt-in via this option. Note that it works only in + recent versions of python. + """ + if not self.check(): + raise error.ENOENT(self) + + if ensuresyspath == "importlib": + if modname is None: + modname = self.purebasename + spec = importlib.util.spec_from_file_location(modname, str(self)) + if spec is None or spec.loader is None: + raise ImportError(f"Can't find module {modname} at location {self!s}") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + pkgpath = None + if modname is None: + pkgpath = self.pypkgpath() + if pkgpath is not None: + pkgroot = pkgpath.dirpath() + names = self.new(ext="").relto(pkgroot).split(self.sep) + if names[-1] == "__init__": + names.pop() + modname = ".".join(names) + else: + pkgroot = self.dirpath() + modname = self.purebasename + + self._ensuresyspath(ensuresyspath, pkgroot) + __import__(modname) + mod = sys.modules[modname] + if self.basename == "__init__.py": + return mod # we don't check anything as we might + # be in a namespace package ... too icky to check + modfile = mod.__file__ + assert modfile is not None + if modfile[-4:] in (".pyc", ".pyo"): + modfile = modfile[:-1] + elif modfile.endswith("$py.class"): + modfile = modfile[:-9] + ".py" + if modfile.endswith(os.sep + "__init__.py"): + if self.basename != "__init__.py": + modfile = modfile[:-12] + try: + issame = self.samefile(modfile) + except error.ENOENT: + issame = False + if not issame: + ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH") + if ignore != "1": + raise self.ImportMismatchError(modname, modfile, self) + return mod + else: + try: + return sys.modules[modname] + except KeyError: + # we have a custom modname, do a pseudo-import + import types + + mod = types.ModuleType(modname) + mod.__file__ = str(self) + sys.modules[modname] = mod + try: + with open(str(self), "rb") as f: + exec(f.read(), mod.__dict__) + except BaseException: + del sys.modules[modname] + raise + return mod + + def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str: + """Return stdout text from executing a system child process, + where the 'self' path points to executable. + The process is directly invoked and not through a system shell. + """ + from subprocess import PIPE + from subprocess import Popen + + popen_opts.pop("stdout", None) + popen_opts.pop("stderr", None) + proc = Popen( + [str(self)] + [str(arg) for arg in argv], + **popen_opts, + stdout=PIPE, + stderr=PIPE, + ) + stdout: str | bytes + stdout, stderr = proc.communicate() + ret = proc.wait() + if isinstance(stdout, bytes): + stdout = stdout.decode(sys.getdefaultencoding()) + if ret != 0: + if isinstance(stderr, bytes): + stderr = stderr.decode(sys.getdefaultencoding()) + raise RuntimeError( + ret, + ret, + str(self), + stdout, + stderr, + ) + return stdout + + @classmethod + def sysfind(cls, name, checker=None, paths=None): + """Return a path object found by looking at the systems + underlying PATH specification. If the checker is not None + it will be invoked to filter matching paths. If a binary + cannot be found, None is returned + Note: This is probably not working on plain win32 systems + but may work on cygwin. + """ + if isabs(name): + p = local(name) + if p.check(file=1): + return p + else: + if paths is None: + if iswin32: + paths = os.environ["Path"].split(";") + if "" not in paths and "." not in paths: + paths.append(".") + try: + systemroot = os.environ["SYSTEMROOT"] + except KeyError: + pass + else: + paths = [ + path.replace("%SystemRoot%", systemroot) for path in paths + ] + else: + paths = os.environ["PATH"].split(":") + tryadd = [] + if iswin32: + tryadd += os.environ["PATHEXT"].split(os.pathsep) + tryadd.append("") + + for x in paths: + for addext in tryadd: + p = local(x).join(name, abs=True) + addext + try: + if p.check(file=1): + if checker: + if not checker(p): + continue + return p + except error.EACCES: + pass + return None + + @classmethod + def _gethomedir(cls): + try: + x = os.environ["HOME"] + except KeyError: + try: + x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"] + except KeyError: + return None + return cls(x) + + # """ + # special class constructors for local filesystem paths + # """ + @classmethod + def get_temproot(cls): + """Return the system's temporary directory + (where tempfiles are usually created in) + """ + import tempfile + + return local(tempfile.gettempdir()) + + @classmethod + def mkdtemp(cls, rootdir=None): + """Return a Path object pointing to a fresh new temporary directory + (which we created ourselves). + """ + import tempfile + + if rootdir is None: + rootdir = cls.get_temproot() + path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir)) + return cls(path) + + @classmethod + def make_numbered_dir( + cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800 + ): # two days + """Return unique directory with a number greater than the current + maximum one. The number is assumed to start directly after prefix. + if keep is true directories with a number less than (maxnum-keep) + will be removed. If .lock files are used (lock_timeout non-zero), + algorithm is multi-process safe. + """ + if rootdir is None: + rootdir = cls.get_temproot() + + nprefix = prefix.lower() + + def parse_num(path): + """Parse the number out of a path (if it matches the prefix)""" + nbasename = path.basename.lower() + if nbasename.startswith(nprefix): + try: + return int(nbasename[len(nprefix) :]) + except ValueError: + pass + + def create_lockfile(path): + """Exclusively create lockfile. Throws when failed""" + mypid = os.getpid() + lockfile = path.join(".lock") + if hasattr(lockfile, "mksymlinkto"): + lockfile.mksymlinkto(str(mypid)) + else: + fd = error.checked_call( + os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644 + ) + with os.fdopen(fd, "w") as f: + f.write(str(mypid)) + return lockfile + + def atexit_remove_lockfile(lockfile): + """Ensure lockfile is removed at process exit""" + mypid = os.getpid() + + def try_remove_lockfile(): + # in a fork() situation, only the last process should + # remove the .lock, otherwise the other processes run the + # risk of seeing their temporary dir disappear. For now + # we remove the .lock in the parent only (i.e. we assume + # that the children finish before the parent). + if os.getpid() != mypid: + return + try: + lockfile.remove() + except error.Error: + pass + + atexit.register(try_remove_lockfile) + + # compute the maximum number currently in use with the prefix + lastmax = None + while True: + maxnum = -1 + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None: + maxnum = max(maxnum, num) + + # make the new directory + try: + udir = rootdir.mkdir(prefix + str(maxnum + 1)) + if lock_timeout: + lockfile = create_lockfile(udir) + atexit_remove_lockfile(lockfile) + except (error.EEXIST, error.ENOENT, error.EBUSY): + # race condition (1): another thread/process created the dir + # in the meantime - try again + # race condition (2): another thread/process spuriously acquired + # lock treating empty directory as candidate + # for removal - try again + # race condition (3): another thread/process tried to create the lock at + # the same time (happened in Python 3.3 on Windows) + # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa + if lastmax == maxnum: + raise + lastmax = maxnum + continue + break + + def get_mtime(path): + """Read file modification time""" + try: + return path.lstat().mtime + except error.Error: + pass + + garbage_prefix = prefix + "garbage-" + + def is_garbage(path): + """Check if path denotes directory scheduled for removal""" + bn = path.basename + return bn.startswith(garbage_prefix) + + # prune old directories + udir_time = get_mtime(udir) + if keep and udir_time: + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None and num <= (maxnum - keep): + try: + # try acquiring lock to remove directory as exclusive user + if lock_timeout: + create_lockfile(path) + except (error.EEXIST, error.ENOENT, error.EBUSY): + path_time = get_mtime(path) + if not path_time: + # assume directory doesn't exist now + continue + if abs(udir_time - path_time) < lock_timeout: + # assume directory with lockfile exists + # and lock timeout hasn't expired yet + continue + + # path dir locked for exclusive use + # and scheduled for removal to avoid another thread/process + # treating it as a new directory or removal candidate + garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) + try: + path.rename(garbage_path) + garbage_path.remove(rec=1) + except KeyboardInterrupt: + raise + except Exception: # this might be error.Error, WindowsError ... + pass + if is_garbage(path): + try: + path.remove(rec=1) + except KeyboardInterrupt: + raise + except Exception: # this might be error.Error, WindowsError ... + pass + + # make link... + try: + username = os.environ["USER"] # linux, et al + except KeyError: + try: + username = os.environ["USERNAME"] # windows + except KeyError: + username = "current" + + src = str(udir) + dest = src[: src.rfind("-")] + "-" + username + try: + os.unlink(dest) + except OSError: + pass + try: + os.symlink(src, dest) + except (OSError, AttributeError, NotImplementedError): + pass + + return udir + + +def copymode(src, dest): + """Copy permission from src to dst.""" + import shutil + + shutil.copymode(src, dest) + + +def copystat(src, dest): + """Copy permission, last modification time, + last access time, and flags from src to dst.""" + import shutil + + shutil.copystat(str(src), str(dest)) + + +def copychunked(src, dest): + chunksize = 524288 # half a meg of bytes + fsrc = src.open("rb") + try: + fdest = dest.open("wb") + try: + while 1: + buf = fsrc.read(chunksize) + if not buf: + break + fdest.write(buf) + finally: + fdest.close() + finally: + fsrc.close() + + +def isimportable(name): + if name and (name[0].isalpha() or name[0] == "_"): + name = name.replace("_", "") + return not name or name.isalnum() + + +local = LocalPath diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/_version.py b/.venv-test/lib/python3.12/site-packages/_pytest/_version.py new file mode 100644 index 0000000..faf5c55 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/_version.py @@ -0,0 +1,24 @@ +# file generated by vcs-versioning +# don't change, don't track in version control +from __future__ import annotations + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +version: str +__version__: str +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None + +__version__ = version = '9.1.1' +__version_tuple__ = version_tuple = (9, 1, 1) + +__commit_id__ = commit_id = None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/__init__.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/__init__.py new file mode 100644 index 0000000..e33f8b2 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/__init__.py @@ -0,0 +1,236 @@ +# mypy: allow-untyped-defs +"""Support for presenting detailed information in failing assertions.""" + +from __future__ import annotations + +from collections.abc import Generator +import sys +from typing import Any +from typing import Protocol +from typing import TYPE_CHECKING + +from _pytest.assertion import rewrite +from _pytest.assertion import truncate +from _pytest.assertion import util +from _pytest.assertion.rewrite import assertstate_key +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.nodes import Item + + +if TYPE_CHECKING: + from _pytest.main import Session + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("debugconfig") + group.addoption( + "--assert", + action="store", + dest="assertmode", + choices=("rewrite", "plain"), + default="rewrite", + metavar="MODE", + help=( + "Control assertion debugging tools.\n" + "'plain' performs no assertion debugging.\n" + "'rewrite' (the default) rewrites assert statements in test modules" + " on import to provide assert expression information." + ), + ) + parser.addini( + "enable_assertion_pass_hook", + type="bool", + default=False, + help="Enables the pytest_assertion_pass hook. " + "Make sure to delete any previously generated pyc cache files.", + ) + + parser.addini( + "truncation_limit_lines", + default=None, + help="Set threshold of LINES after which truncation will take effect", + ) + parser.addini( + "truncation_limit_chars", + default=None, + help=("Set threshold of CHARS after which truncation will take effect"), + ) + parser.addini( + "assertion_text_diff_style", + default=util.ASSERTION_TEXT_DIFF_STYLE_NDIFF, + help=( + "Choose how pytest renders diffs for string equality assertions: " + f"{util.ASSERTION_TEXT_DIFF_STYLE_NDIFF} or " + f"{util.ASSERTION_TEXT_DIFF_STYLE_BLOCK}" + ), + ) + + Config._add_verbosity_ini( + parser, + Config.VERBOSITY_ASSERTIONS, + help=( + "Specify a verbosity level for assertions, overriding the main level. " + "Higher levels will provide more detailed explanation when an assertion fails." + ), + ) + + +def pytest_configure(config: Config) -> None: + util.validate_assertion_text_diff_style(config) + + +def register_assert_rewrite(*names: str) -> None: + """Register one or more module names to be rewritten on import. + + This function will make sure that this module or all modules inside + the package will get their assert statements rewritten. + Thus you should make sure to call this before the module is + actually imported, usually in your __init__.py if you are a plugin + using a package. + + :param names: The module names to register. + """ + for name in names: + if not isinstance(name, str): + msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable] + raise TypeError(msg.format(repr(names))) + rewrite_hook: RewriteHook + for hook in sys.meta_path: + if isinstance(hook, rewrite.AssertionRewritingHook): + rewrite_hook = hook + break + else: + rewrite_hook = DummyRewriteHook() + rewrite_hook.mark_rewrite(*names) + + +class RewriteHook(Protocol): + def mark_rewrite(self, *names: str) -> None: ... + + +class DummyRewriteHook: + """A no-op import hook for when rewriting is disabled.""" + + def mark_rewrite(self, *names: str) -> None: + pass + + +class AssertionState: + """State for the assertion plugin.""" + + def __init__(self, config: Config, mode) -> None: + self.mode = mode + self.trace = config.trace.root.get("assertion") + self.hook: rewrite.AssertionRewritingHook | None = None + + +def install_importhook(config: Config) -> rewrite.AssertionRewritingHook: + """Try to install the rewrite hook, raise SystemError if it fails.""" + config.stash[assertstate_key] = AssertionState(config, "rewrite") + config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config) + sys.meta_path.insert(0, hook) + config.stash[assertstate_key].trace("installed rewrite import hook") + + def undo() -> None: + hook = config.stash[assertstate_key].hook + if hook is not None and hook in sys.meta_path: + sys.meta_path.remove(hook) + + config.add_cleanup(undo) + return hook + + +def pytest_collection(session: Session) -> None: + # This hook is only called when test modules are collected + # so for example not in the managing process of pytest-xdist + # (which does not collect test modules). + assertstate = session.config.stash.get(assertstate_key, None) + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(session) + + +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: + """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. + + The rewrite module will use util._reprcompare if it exists to use custom + reporting via the pytest_assertrepr_compare hook. This sets up this custom + comparison for the test. + """ + ihook = item.ihook + + def callbinrepr(op, left: object, right: object) -> str | None: + """Call the pytest_assertrepr_compare hook and prepare the result. + + This uses the first result from the hook and then ensures the + following: + * Overly verbose explanations are truncated unless configured otherwise + (eg. if running in verbose mode). + * Embedded newlines are escaped to help util.format_explanation() + later. + * If the rewrite mode is used embedded %-characters are replaced + to protect later % formatting. + + The result can be formatted by util.format_explanation() for + pretty printing. + """ + hook_result = ihook.pytest_assertrepr_compare( + config=item.config, op=op, left=left, right=right + ) + for new_expl in hook_result: + if new_expl: + new_expl = truncate.truncate_if_required(new_expl, item) + new_expl = [line.replace("\n", "\\n") for line in new_expl] + res = "\n~".join(new_expl) + if item.config.getvalue("assertmode") == "rewrite": + res = res.replace("%", "%%") + return res + return None + + saved_assert_hooks = util._reprcompare, util._assertion_pass + util._reprcompare = callbinrepr + util._config = item.config + + if ihook.pytest_assertion_pass.get_hookimpls(): + + def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None: + ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl) + + util._assertion_pass = call_assertion_pass_hook + + try: + return (yield) + finally: + util._reprcompare, util._assertion_pass = saved_assert_hooks + util._config = None + + +def pytest_sessionfinish(session: Session) -> None: + assertstate = session.config.stash.get(assertstate_key, None) + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(None) + + +def pytest_assertrepr_compare( + config: Config, op: str, left: Any, right: Any +) -> list[str] | None: + if config.pluginmanager.has_plugin("terminalreporter"): + highlighter = config.get_terminal_writer()._highlight + else: + # Keep it plaintext when not using terminalrepoterer (#14377). + highlighter = util.dummy_highlighter + explanation = list( + util.assertrepr_compare( + op=op, + left=left, + right=right, + verbose=config.get_verbosity(Config.VERBOSITY_ASSERTIONS), + highlighter=highlighter, + assertion_text_diff_style=util.get_assertion_text_diff_style(config), + ) + ) + return explanation or None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_any.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_any.py new file mode 100644 index 0000000..9e57768 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_any.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from collections.abc import Iterator +import dataclasses +import pprint + +from _pytest.assertion._compare_mapping import _compare_eq_mapping +from _pytest.assertion._compare_sequence import _compare_eq_iterable +from _pytest.assertion._compare_sequence import _compare_eq_sequence +from _pytest.assertion._compare_set import _compare_eq_set +from _pytest.assertion._guards import has_default_eq +from _pytest.assertion._guards import isattrs +from _pytest.assertion._guards import isdatacls +from _pytest.assertion._guards import isiterable +from _pytest.assertion._guards import ismapping +from _pytest.assertion._guards import isnamedtuple +from _pytest.assertion._guards import issequence +from _pytest.assertion._guards import isset +from _pytest.assertion._guards import istext +from _pytest.assertion._typing import _AssertionTextDiffStyle +from _pytest.assertion._typing import _HighlightFunc +from _pytest.assertion.compare_text import _compare_eq_text + + +def _compare_eq_any( + left: object, + right: object, + highlighter: _HighlightFunc, + verbose: int, + assertion_text_diff_style: _AssertionTextDiffStyle, +) -> Iterator[str]: + """Yield the per-line explanation for ``left == right`` (without summary). + + Yields nothing when no specialised explanation applies, so consumers + can stream the output and bail out early (e.g. for truncation) without + materialising the entire diff first. + """ + if istext(left) and istext(right): + yield from _compare_eq_text( + left, + right, + highlighter, + verbose, + assertion_text_diff_style, + ) + else: + from _pytest.python_api import ApproxBase + + # Although the common order should be obtained == approx(...), allow both ways. + if isinstance(right, ApproxBase): + yield from right._repr_compare(left) + elif isinstance(left, ApproxBase): + yield from left._repr_compare(right) + elif type(left) is type(right) and ( + isdatacls(left) or isattrs(left) or isnamedtuple(left) + ): + # Note: unlike dataclasses/attrs, namedtuples compare only the + # field values, not the type or field names. But this branch + # intentionally only handles the same-type case, which was often + # used in older code bases before dataclasses/attrs were available. + yield from _compare_eq_cls( + left, + right, + highlighter, + verbose, + assertion_text_diff_style, + ) + elif issequence(left) and issequence(right): + yield from _compare_eq_sequence(left, right, highlighter, verbose) + elif isset(left) and isset(right): + yield from _compare_eq_set(left, right, highlighter, verbose) + elif ismapping(left) and ismapping(right): + yield from _compare_eq_mapping(left, right, highlighter, verbose) + + if isiterable(left) and isiterable(right): + yield from _compare_eq_iterable(left, right, highlighter, verbose) + + +def _compare_eq_cls( + left: object, + right: object, + highlighter: _HighlightFunc, + verbose: int, + assertion_text_diff_style: _AssertionTextDiffStyle, +) -> Iterator[str]: + if not has_default_eq(left): + return + if isdatacls(left): + all_fields = dataclasses.fields(left) + fields_to_check = [info.name for info in all_fields if info.compare] + elif isattrs(left): + all_fields = left.__attrs_attrs__ # type: ignore[attr-defined] + fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] + elif isnamedtuple(left): + fields_to_check = left._fields # type: ignore[attr-defined] + else: + assert False + + indent = " " + same = [] + diff = [] + for field in fields_to_check: + if getattr(left, field) == getattr(right, field): + same.append(field) + else: + diff.append(field) + + if same or diff: + yield "" + if same and verbose < 2: + yield f"Omitting {len(same)} identical items, use -vv to show" + elif same: + yield "Matching attributes:" + yield from highlighter(pprint.pformat(same)).splitlines() + if diff: + yield "Differing attributes:" + yield from highlighter(pprint.pformat(diff)).splitlines() + for field in diff: + field_left = getattr(left, field) + field_right = getattr(right, field) + yield "" + yield f"Drill down into differing attribute {field}:" + yield f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}" + for line in _compare_eq_any( + field_left, + field_right, + highlighter, + verbose, + assertion_text_diff_style, + ): + yield indent + line diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_mapping.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_mapping.py new file mode 100644 index 0000000..4edb470 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_mapping.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from collections.abc import Iterator +from collections.abc import Mapping +import pprint + +from _pytest._io.saferepr import saferepr +from _pytest.assertion._typing import _HighlightFunc + + +def _compare_eq_mapping( + left: Mapping[object, object], + right: Mapping[object, object], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> Iterator[str]: + set_left = set(left) + set_right = set(right) + common = set_left.intersection(set_right) + same = {k: left[k] for k in common if left[k] == right[k]} + if same and verbose < 2: + yield f"Omitting {len(same)} identical items, use -vv to show" + elif same: + yield "Common items:" + yield from highlighter(pprint.pformat(same)).splitlines() + diff = {k for k in common if left[k] != right[k]} + if diff: + yield "Differing items:" + for k in diff: + yield ( + highlighter(saferepr({k: left[k]})) + + " != " + + highlighter(saferepr({k: right[k]})) + ) + extra_left = set_left - set_right + len_extra_left = len(extra_left) + if len_extra_left: + yield f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:" + yield from highlighter( + pprint.pformat({k: left[k] for k in extra_left}) + ).splitlines() + extra_right = set_right - set_left + len_extra_right = len(extra_right) + if len_extra_right: + yield f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:" + yield from highlighter( + pprint.pformat({k: right[k] for k in extra_right}) + ).splitlines() diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_sequence.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_sequence.py new file mode 100644 index 0000000..cd0043b --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_sequence.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Sequence + +from _pytest._io.pprint import PrettyPrinter +from _pytest._io.saferepr import saferepr +from _pytest.assertion._typing import _HighlightFunc +from _pytest.compat import running_on_ci + + +def _compare_eq_iterable( + left: Iterable[object], + right: Iterable[object], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> Iterator[str]: + if verbose <= 0 and not running_on_ci(): + yield "Use -v to get more diff" + return + # dynamic import to speedup pytest + import difflib + + left_formatting = PrettyPrinter().pformat(left).splitlines() + right_formatting = PrettyPrinter().pformat(right).splitlines() + + yield "" + yield "Full diff:" + # "right" is the expected base against which we compare "left", + # see https://github.com/pytest-dev/pytest/issues/3333 + yield from highlighter( + "\n".join( + line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting) + ), + lexer="diff", + ).splitlines() + + +def _compare_eq_sequence( + left: Sequence[object], + right: Sequence[object], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> Iterator[str]: + comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) + len_left = len(left) + len_right = len(right) + for i in range(min(len_left, len_right)): + if left[i] != right[i]: + if comparing_bytes: + # when comparing bytes, we want to see their ascii representation + # instead of their numeric values (#5260) + # using a slice gives us the ascii representation: + # >>> s = b'foo' + # >>> s[0] + # 102 + # >>> s[0:1] + # b'f' + left_value: object = left[i : i + 1] + right_value: object = right[i : i + 1] + else: + left_value = left[i] + right_value = right[i] + + yield ( + f"At index {i} diff:" + f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}" + ) + break + + if comparing_bytes: + # when comparing bytes, it doesn't help to show the "sides contain one or more + # items" longer explanation, so skip it + return + + len_diff = len_left - len_right + if len_diff: + if len_diff > 0: + dir_with_more = "Left" + extra = saferepr(left[len_right]) + else: + len_diff = 0 - len_diff + dir_with_more = "Right" + extra = saferepr(right[len_left]) + + if len_diff == 1: + yield f"{dir_with_more} contains one more item: {highlighter(extra)}" + else: + yield f"{dir_with_more} contains {len_diff} more items, first extra item: {highlighter(extra)}" diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_set.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_set.py new file mode 100644 index 0000000..2817133 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_compare_set.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from collections.abc import Callable +from collections.abc import Iterator +from collections.abc import Set as AbstractSet +from typing import TypeAlias + +from _pytest._io.saferepr import saferepr +from _pytest.assertion._typing import _HighlightFunc + + +def _set_one_sided_diff( + posn: str, + set1: AbstractSet[object], + set2: AbstractSet[object], + highlighter: _HighlightFunc, +) -> Iterator[str]: + diff = set1 - set2 + if diff: + yield f"Extra items in the {posn} set:" + for item in diff: + yield highlighter(saferepr(item)) + + +def _compare_eq_set( + left: AbstractSet[object], + right: AbstractSet[object], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> Iterator[str]: + yield from _set_one_sided_diff("left", left, right, highlighter) + yield from _set_one_sided_diff("right", right, left, highlighter) + + +def _compare_gte_set( + left: AbstractSet[object], + right: AbstractSet[object], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> Iterator[str]: + yield from _set_one_sided_diff("right", right, left, highlighter) + + +def _compare_lte_set( + left: AbstractSet[object], + right: AbstractSet[object], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> Iterator[str]: + yield from _set_one_sided_diff("left", left, right, highlighter) + + +def _compare_gt_set( + left: AbstractSet[object], + right: AbstractSet[object], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> Iterator[str]: + if left == right: + yield "Both sets are equal" + else: + yield from _set_one_sided_diff("right", right, left, highlighter) + + +def _compare_lt_set( + left: AbstractSet[object], + right: AbstractSet[object], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> Iterator[str]: + if left == right: + yield "Both sets are equal" + else: + yield from _set_one_sided_diff("left", left, right, highlighter) + + +SetComparisonFunction: TypeAlias = Callable[ + [AbstractSet[object], AbstractSet[object], _HighlightFunc, int], + Iterator[str], +] + + +def _both_sets_are_equal( + left: AbstractSet[object], + right: AbstractSet[object], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> Iterator[str]: + yield "Both sets are equal" + + +SET_COMPARISON_FUNCTIONS: dict[str, SetComparisonFunction] = { + # == can't be done here without a prior refactor because there's an additional + # explanation for iterable in _compare_eq_any + # "==": _compare_eq_set, + "!=": _both_sets_are_equal, + ">=": _compare_gte_set, + "<=": _compare_lte_set, + ">": _compare_gt_set, + "<": _compare_lt_set, +} diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_guards.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_guards.py new file mode 100644 index 0000000..bb9fedd --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_guards.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import collections.abc +from collections.abc import Mapping +import dataclasses +from typing import TypeGuard + + +def issequence(x: object) -> TypeGuard[collections.abc.Sequence[object]]: + return isinstance(x, collections.abc.Sequence) and not isinstance(x, str) + + +def istext(x: object) -> TypeGuard[str]: + return isinstance(x, str) + + +def ismapping(x: object) -> TypeGuard[Mapping[object, object]]: + return isinstance(x, Mapping) + + +def isset(x: object) -> TypeGuard[set[object] | frozenset[object]]: + return isinstance(x, set | frozenset) + + +def isnamedtuple(obj: object) -> bool: + return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None + + +isdatacls = dataclasses.is_dataclass + + +def isattrs(obj: object) -> bool: + return getattr(obj, "__attrs_attrs__", None) is not None + + +def isiterable(obj: object) -> TypeGuard[collections.abc.Iterable[object]]: + try: + iter(obj) # type: ignore[call-overload] + return not istext(obj) + except Exception: + return False + + +def has_default_eq(obj: object) -> bool: + """Check if an instance of an object contains the default eq + + First, we check if the object's __eq__ attribute has __code__, + if so, we check the equally of the method code filename (__code__.co_filename) + to the default one generated by the dataclass and attr module + for dataclasses the default co_filename is , for attrs class, the __eq__ should contain "attrs eq generated" + """ + # inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68 + if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"): + code_filename = obj.__eq__.__code__.co_filename + + if isattrs(obj): + return "attrs generated " in code_filename + + return code_filename == "" # data class + return True diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_typing.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_typing.py new file mode 100644 index 0000000..f032f34 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/_typing.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from typing import Literal +from typing import Protocol + + +_AssertionTextDiffStyle = Literal["ndiff", "block"] + + +class _HighlightFunc(Protocol): # noqa: PYI046 + def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str: + """Apply highlighting to the given source.""" diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/compare_text.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/compare_text.py new file mode 100644 index 0000000..3109644 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/compare_text.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from collections.abc import Iterator + +from _pytest._io.saferepr import saferepr +from _pytest.assertion._typing import _AssertionTextDiffStyle +from _pytest.assertion._typing import _HighlightFunc +from _pytest.assertion.highlight import dummy_highlighter +from _pytest.compat import assert_never + + +def _compare_eq_text( + left: str, + right: str, + highlighter: _HighlightFunc, + verbose: int, + assertion_text_diff_style: _AssertionTextDiffStyle, +) -> Iterator[str]: + match assertion_text_diff_style: + case "block": + yield from _diff_text_block(left, right) + case "ndiff": + yield from _diff_text(left, right, highlighter, verbose) + case unreachable: + assert_never(unreachable) + + +def _diff_text_block(left: str, right: str) -> Iterator[str]: + yield "Left:" + yield from _format_text_block_lines(left) + yield "" + yield "Right:" + yield from _format_text_block_lines(right) + + +def _format_text_block_lines(text: str) -> Iterator[str]: + for line in text.split("\n"): + yield f" {line}" + + +def _diff_text( + left: str, right: str, highlighter: _HighlightFunc, verbose: int = 0 +) -> Iterator[str]: + """Yield the explanation for the diff between text. + + Unless --verbose is used this will skip leading and trailing + characters which are identical to keep the diff minimal. + """ + from difflib import ndiff + + if verbose < 1: + i = 0 # just in case left or right has zero length + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + break + if i > 42: + i -= 10 # Provide some context + yield f"Skipping {i} identical leading characters in diff, use -v to show" + left = left[i:] + right = right[i:] + if len(left) == len(right): + for i in range(len(left)): + if left[-i] != right[-i]: + break + if i > 42: + i -= 10 # Provide some context + yield ( + f"Skipping {i} identical trailing " + "characters in diff, use -v to show" + ) + left = left[:-i] + right = right[:-i] + keepends = True + if left.isspace() or right.isspace(): + left = repr(str(left)) + right = repr(str(right)) + yield "Strings contain only whitespace, escaping them using repr()" + # "right" is the expected base against which we compare "left", + # see https://github.com/pytest-dev/pytest/issues/3333 + yield from highlighter( + "\n".join( + line.strip("\n") + for line in ndiff(right.splitlines(keepends), left.splitlines(keepends)) + ), + lexer="diff", + ).splitlines() + + +def _notin_text(term: str, text: str, verbose: int = 0) -> Iterator[str]: + index = text.find(term) + head = text[:index] + tail = text[index + len(term) :] + correct_text = head + tail + diff = _diff_text(text, correct_text, dummy_highlighter, verbose) + yield f"{saferepr(term, maxsize=42)} is contained here:" + for line in diff: + if line.startswith("Skipping"): + continue + if line.startswith("- "): + continue + if line.startswith("+ "): + yield " " + line[2:] + else: + yield line diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/highlight.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/highlight.py new file mode 100644 index 0000000..9ae833f --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/highlight.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from typing import Literal + + +def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python") -> str: + """Dummy highlighter that returns the text unprocessed. + + Needed for _notin_text, as the diff gets post-processed to only show the "+" part. + """ + return source diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/rewrite.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/rewrite.py new file mode 100644 index 0000000..99815b7 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/rewrite.py @@ -0,0 +1,1193 @@ +"""Rewrite assertion AST to produce nice error messages.""" + +from __future__ import annotations + +import ast +from collections import defaultdict +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Sequence +import errno +import functools +import importlib.abc +import importlib.machinery +import importlib.util +import io +import itertools +import marshal +import os +from pathlib import Path +from pathlib import PurePath +import struct +import sys +import tokenize +import types +from typing import IO +from typing import TYPE_CHECKING + + +if sys.version_info >= (3, 12): + from importlib.resources.abc import TraversableResources +else: + from importlib.abc import TraversableResources +if sys.version_info < (3, 11): + from importlib.readers import FileReader +else: + from importlib.resources.readers import FileReader + + +from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE +from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited +from _pytest._version import version +from _pytest.assertion import util +from _pytest.config import Config +from _pytest.fixtures import FixtureFunctionDefinition +from _pytest.main import Session +from _pytest.pathlib import absolutepath +from _pytest.pathlib import fnmatch_ex +from _pytest.stash import StashKey + + +# fmt: off +from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip +# fmt:on + +if TYPE_CHECKING: + from _pytest.assertion import AssertionState + + +class Sentinel: + pass + + +assertstate_key = StashKey["AssertionState"]() + +# pytest caches rewritten pycs in pycache dirs +PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}" +PYC_EXT = ".py" + ((__debug__ and "c") or "o") +PYC_TAIL = "." + PYTEST_TAG + PYC_EXT + +# Special marker that denotes we have just left a scope definition +_SCOPE_END_MARKER = Sentinel() + + +class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader): + """PEP302/PEP451 import hook which rewrites asserts.""" + + def __init__(self, config: Config) -> None: + self.config = config + try: + self.fnpats = config.getini("python_files") + except ValueError: + self.fnpats = ["test_*.py", "*_test.py"] + self.session: Session | None = None + self._rewritten_names: dict[str, Path] = {} + self._must_rewrite: set[str] = set() + # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, + # which might result in infinite recursion (#3506) + self._writing_pyc = False + self._basenames_to_check_rewrite = {"conftest"} + self._marked_for_rewrite_cache: dict[str, bool] = {} + self._session_paths_checked = False + + def set_session(self, session: Session | None) -> None: + self.session = session + self._session_paths_checked = False + + # Indirection so we can mock calls to find_spec originated from the hook during testing + _find_spec = importlib.machinery.PathFinder.find_spec + + def find_spec( + self, + name: str, + path: Sequence[str | bytes] | None = None, + target: types.ModuleType | None = None, + ) -> importlib.machinery.ModuleSpec | None: + if self._writing_pyc: + return None + state = self.config.stash[assertstate_key] + if self._early_rewrite_bailout(name, state): + return None + state.trace(f"find_module called for: {name}") + + # Type ignored because mypy is confused about the `self` binding here. + spec = self._find_spec(name, path) # type: ignore + + if ( + # the import machinery could not find a file to import + spec is None + # this is a namespace package (without `__init__.py`) + # there's nothing to rewrite there + or spec.origin is None + # we can only rewrite source files + or not isinstance(spec.loader, importlib.machinery.SourceFileLoader) + # if the file doesn't exist, we can't rewrite it + or not os.path.exists(spec.origin) + ): + return None + else: + fn = spec.origin + + if not self._should_rewrite(name, fn, state): + return None + + return importlib.util.spec_from_file_location( + name, + fn, + loader=self, + submodule_search_locations=spec.submodule_search_locations, + ) + + def create_module( + self, spec: importlib.machinery.ModuleSpec + ) -> types.ModuleType | None: + return None # default behaviour is fine + + def exec_module(self, module: types.ModuleType) -> None: + assert module.__spec__ is not None + assert module.__spec__.origin is not None + fn = Path(module.__spec__.origin) + state = self.config.stash[assertstate_key] + + self._rewritten_names[module.__name__] = fn + + # The requested module looks like a test file, so rewrite it. This is + # the most magical part of the process: load the source, rewrite the + # asserts, and load the rewritten source. We also cache the rewritten + # module code in a special pyc. We must be aware of the possibility of + # concurrent pytest processes rewriting and loading pycs. To avoid + # tricky race conditions, we maintain the following invariant: The + # cached pyc is always a complete, valid pyc. Operations on it must be + # atomic. POSIX's atomic rename comes in handy. + write = not sys.dont_write_bytecode + cache_dir = get_cache_dir(fn) + if write: + ok = try_makedirs(cache_dir) + if not ok: + write = False + state.trace(f"read only directory: {cache_dir}") + + cache_name = fn.name[:-3] + PYC_TAIL + pyc = cache_dir / cache_name + # Notice that even if we're in a read-only directory, I'm going + # to check for a cached pyc. This may not be optimal... + co = _read_pyc(fn, pyc, state.trace) + if co is None: + state.trace(f"rewriting {fn!r}") + source_stat, co = _rewrite_test(fn, self.config) + if write: + self._writing_pyc = True + try: + _write_pyc(state, co, source_stat, pyc) + finally: + self._writing_pyc = False + else: + state.trace(f"found cached rewritten pyc for {fn}") + exec(co, module.__dict__) + + def _early_rewrite_bailout(self, name: str, state: AssertionState) -> bool: + """A fast way to get out of rewriting modules. + + Profiling has shown that the call to PathFinder.find_spec (inside of + the find_spec from this class) is a major slowdown, so, this method + tries to filter what we're sure won't be rewritten before getting to + it. + """ + if self.session is not None and not self._session_paths_checked: + self._session_paths_checked = True + for initial_path in self.session._initialpaths: + # Make something as c:/projects/my_project/path.py -> + # ['c:', 'projects', 'my_project', 'path.py'] + parts = str(initial_path).split(os.sep) + # add 'path' to basenames to be checked. + self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0]) + + # Note: conftest already by default in _basenames_to_check_rewrite. + parts = name.split(".") + if parts[-1] in self._basenames_to_check_rewrite: + return False + + # For matching the name it must be as if it was a filename. + path = PurePath(*parts).with_suffix(".py") + + for pat in self.fnpats: + # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based + # on the name alone because we need to match against the full path + if os.path.dirname(pat): + return False + if fnmatch_ex(pat, path): + return False + + if self._is_marked_for_rewrite(name, state): + return False + + state.trace(f"early skip of rewriting module: {name}") + return True + + def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool: + # always rewrite conftest files + if os.path.basename(fn) == "conftest.py": + state.trace(f"rewriting conftest file: {fn!r}") + return True + + if self.session is not None: + if self.session.isinitpath(absolutepath(fn)): + state.trace(f"matched test file (was specified on cmdline): {fn!r}") + return True + + # modules not passed explicitly on the command line are only + # rewritten if they match the naming convention for test files + fn_path = PurePath(fn) + for pat in self.fnpats: + if fnmatch_ex(pat, fn_path): + state.trace(f"matched test file {fn!r}") + return True + + return self._is_marked_for_rewrite(name, state) + + def _is_marked_for_rewrite(self, name: str, state: AssertionState) -> bool: + try: + return self._marked_for_rewrite_cache[name] + except KeyError: + for marked in self._must_rewrite: + if name == marked or name.startswith(marked + "."): + state.trace(f"matched marked file {name!r} (from {marked!r})") + self._marked_for_rewrite_cache[name] = True + return True + + self._marked_for_rewrite_cache[name] = False + return False + + def mark_rewrite(self, *names: str) -> None: + """Mark import names as needing to be rewritten. + + The named module or package as well as any nested modules will + be rewritten on import. + """ + already_imported = ( + set(names).intersection(sys.modules).difference(self._rewritten_names) + ) + for name in already_imported: + mod = sys.modules[name] + if not AssertionRewriter.is_rewrite_disabled( + mod.__doc__ or "" + ) and not isinstance(mod.__loader__, type(self)): + self._warn_already_imported(name) + self._must_rewrite.update(names) + self._marked_for_rewrite_cache.clear() + + def _warn_already_imported(self, name: str) -> None: + from _pytest.warning_types import PytestAssertRewriteWarning + + self.config.issue_config_time_warning( + PytestAssertRewriteWarning( + f"Module already imported so cannot be rewritten; {name}" + ), + stacklevel=5, + ) + + def get_data(self, pathname: str | bytes) -> bytes: + """Optional PEP302 get_data API.""" + with open(pathname, "rb") as f: + return f.read() + + def get_resource_reader(self, name: str) -> TraversableResources: + return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) # type: ignore[arg-type] + + +def _write_pyc_fp( + fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType +) -> None: + # Technically, we don't have to have the same pyc format as + # (C)Python, since these "pycs" should never be seen by builtin + # import. However, there's little reason to deviate. + fp.write(importlib.util.MAGIC_NUMBER) + # https://www.python.org/dev/peps/pep-0552/ + flags = b"\x00\x00\x00\x00" + fp.write(flags) + # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) + mtime = int(source_stat.st_mtime) & 0xFFFFFFFF + size = source_stat.st_size & 0xFFFFFFFF + # " bool: + proc_pyc = f"{pyc}.{os.getpid()}" + try: + with open(proc_pyc, "wb") as fp: + _write_pyc_fp(fp, source_stat, co) + except OSError as e: + state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}") + return False + + try: + os.replace(proc_pyc, pyc) + except OSError as e: + state.trace(f"error writing pyc file at {pyc}: {e}") + # we ignore any failure to write the cache file + # there are many reasons, permission-denied, pycache dir being a + # file etc. + return False + return True + + +def _rewrite_test(fn: Path, config: Config) -> tuple[os.stat_result, types.CodeType]: + """Read and rewrite *fn* and return the code object.""" + stat = os.stat(fn) + source = fn.read_bytes() + strfn = str(fn) + tree = ast.parse(source, filename=strfn) + rewrite_asserts(tree, source, strfn, config) + co = compile(tree, strfn, "exec", dont_inherit=True) + return stat, co + + +def _read_pyc( + source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None +) -> types.CodeType | None: + """Possibly read a pytest pyc containing rewritten code. + + Return rewritten code if successful or None if not. + """ + try: + fp = open(pyc, "rb") + except OSError: + return None + with fp: + try: + stat_result = os.stat(source) + mtime = int(stat_result.st_mtime) + size = stat_result.st_size + data = fp.read(16) + except OSError as e: + trace(f"_read_pyc({source}): OSError {e}") + return None + # Check for invalid or out of date pyc file. + if len(data) != (16): + trace(f"_read_pyc({source}): invalid pyc (too short)") + return None + if data[:4] != importlib.util.MAGIC_NUMBER: + trace(f"_read_pyc({source}): invalid pyc (bad magic number)") + return None + if data[4:8] != b"\x00\x00\x00\x00": + trace(f"_read_pyc({source}): invalid pyc (unsupported flags)") + return None + mtime_data = data[8:12] + if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF: + trace(f"_read_pyc({source}): out of date") + return None + size_data = data[12:16] + if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF: + trace(f"_read_pyc({source}): invalid pyc (incorrect size)") + return None + try: + co = marshal.load(fp) + except Exception as e: + trace(f"_read_pyc({source}): marshal.load error {e}") + return None + if not isinstance(co, types.CodeType): + trace(f"_read_pyc({source}): not a code object") + return None + return co + + +def rewrite_asserts( + mod: ast.Module, + source: bytes, + module_path: str | None = None, + config: Config | None = None, +) -> None: + """Rewrite the assert statements in mod.""" + AssertionRewriter(module_path, config, source).run(mod) + + +def _saferepr(obj: object) -> str: + r"""Get a safe repr of an object for assertion error messages. + + The assertion formatting (util.format_explanation()) requires + newlines to be escaped since they are a special character for it. + Normally assertion.util.format_explanation() does this but for a + custom repr it is possible to contain one of the special escape + sequences, especially '\n{' and '\n}' are likely to be present in + JSON reprs. + """ + if isinstance(obj, types.MethodType): + # for bound methods, skip redundant information + return obj.__name__ + + maxsize = _get_maxsize_for_saferepr(util._config) + if not maxsize: + return saferepr_unlimited(obj).replace("\n", "\\n") + return saferepr(obj, maxsize=maxsize).replace("\n", "\\n") + + +def _get_maxsize_for_saferepr(config: Config | None) -> int | None: + """Get `maxsize` configuration for saferepr based on the given config object.""" + if config is None: + verbosity = 0 + else: + verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS) + if verbosity >= 2: + return None + if verbosity >= 1: + return DEFAULT_REPR_MAX_SIZE * 10 + return DEFAULT_REPR_MAX_SIZE + + +def _format_assertmsg(obj: object) -> str: + r"""Format the custom assertion message given. + + For strings this simply replaces newlines with '\n~' so that + util.format_explanation() will preserve them instead of escaping + newlines. For other objects saferepr() is used first. + """ + # reprlib appears to have a bug which means that if a string + # contains a newline it gets escaped, however if an object has a + # .__repr__() which contains newlines it does not get escaped. + # However in either case we want to preserve the newline. + replaces = [("\n", "\n~"), ("%", "%%")] + if not isinstance(obj, str): + obj = saferepr(obj, _get_maxsize_for_saferepr(util._config)) + replaces.append(("\\n", "\n~")) + + for r1, r2 in replaces: + obj = obj.replace(r1, r2) + + return obj + + +def _should_repr_global_name(obj: object) -> bool: + if callable(obj): + # For pytest fixtures the __repr__ method provides more information than the function name. + return isinstance(obj, FixtureFunctionDefinition) + + try: + return not hasattr(obj, "__name__") + except Exception: + return True + + +def _format_boolop(explanations: Iterable[str], is_or: bool) -> str: + explanation = "(" + ((is_or and " or ") or " and ").join(explanations) + ")" + return explanation.replace("%", "%%") + + +def _call_reprcompare( + ops: Sequence[str], + results: Sequence[bool], + expls: Sequence[str], + each_obj: Sequence[object], +) -> str: + for i, res, expl in zip(range(len(ops)), results, expls, strict=True): + try: + done = not res + except Exception: + done = True + if done: + break + if util._reprcompare is not None: + custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + if custom is not None: + return custom + return expl + + +def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None: + if util._assertion_pass is not None: + util._assertion_pass(lineno, orig, expl) + + +def _check_if_assertion_pass_impl() -> bool: + """Check if any plugins implement the pytest_assertion_pass hook + in order not to generate explanation unnecessarily (might be expensive).""" + return True if util._assertion_pass else False + + +UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"} + +BINOP_MAP = { + ast.BitOr: "|", + ast.BitXor: "^", + ast.BitAnd: "&", + ast.LShift: "<<", + ast.RShift: ">>", + ast.Add: "+", + ast.Sub: "-", + ast.Mult: "*", + ast.Div: "/", + ast.FloorDiv: "//", + ast.Mod: "%%", # escaped for string formatting + ast.Eq: "==", + ast.NotEq: "!=", + ast.Lt: "<", + ast.LtE: "<=", + ast.Gt: ">", + ast.GtE: ">=", + ast.Pow: "**", + ast.Is: "is", + ast.IsNot: "is not", + ast.In: "in", + ast.NotIn: "not in", + ast.MatMult: "@", +} + + +def traverse_node(node: ast.AST) -> Iterator[ast.AST]: + """Recursively yield node and all its children in depth-first order.""" + yield node + for child in ast.iter_child_nodes(node): + yield from traverse_node(child) + + +@functools.lru_cache(maxsize=1) +def _get_assertion_exprs(src: bytes) -> dict[int, str]: + """Return a mapping from {lineno: "assertion test expression"}.""" + ret: dict[int, str] = {} + + depth = 0 + lines: list[str] = [] + assert_lineno: int | None = None + seen_lines: set[int] = set() + + def _write_and_reset() -> None: + nonlocal depth, lines, assert_lineno, seen_lines + assert assert_lineno is not None + ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\") + depth = 0 + lines = [] + assert_lineno = None + seen_lines = set() + + tokens = tokenize.tokenize(io.BytesIO(src).readline) + for tp, source, (lineno, offset), _, line in tokens: + if tp == tokenize.NAME and source == "assert": + assert_lineno = lineno + elif assert_lineno is not None: + # keep track of depth for the assert-message `,` lookup + if tp == tokenize.OP and source in "([{": + depth += 1 + elif tp == tokenize.OP and source in ")]}": + depth -= 1 + + if not lines: + lines.append(line[offset:]) + seen_lines.add(lineno) + # a non-nested comma separates the expression from the message + elif depth == 0 and tp == tokenize.OP and source == ",": + # one line assert with message + if lineno in seen_lines and len(lines) == 1: + offset_in_trimmed = offset + len(lines[-1]) - len(line) + lines[-1] = lines[-1][:offset_in_trimmed] + # multi-line assert with message + elif lineno in seen_lines: + lines[-1] = lines[-1][:offset] + # multi line assert with escaped newline before message + else: + lines.append(line[:offset]) + _write_and_reset() + elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}: + _write_and_reset() + elif lines and lineno not in seen_lines: + lines.append(line) + seen_lines.add(lineno) + + return ret + + +class AssertionRewriter(ast.NodeVisitor): + """Assertion rewriting implementation. + + The main entrypoint is to call .run() with an ast.Module instance, + this will then find all the assert statements and rewrite them to + provide intermediate values and a detailed assertion error. See + http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html + for an overview of how this works. + + The entry point here is .run() which will iterate over all the + statements in an ast.Module and for each ast.Assert statement it + finds call .visit() with it. Then .visit_Assert() takes over and + is responsible for creating new ast statements to replace the + original assert statement: it rewrites the test of an assertion + to provide intermediate values and replace it with an if statement + which raises an assertion error with a detailed explanation in + case the expression is false and calls pytest_assertion_pass hook + if expression is true. + + For this .visit_Assert() uses the visitor pattern to visit all the + AST nodes of the ast.Assert.test field, each visit call returning + an AST node and the corresponding explanation string. During this + state is kept in several instance attributes: + + :statements: All the AST statements which will replace the assert + statement. + + :variables: This is populated by .variable() with each variable + used by the statements so that they can all be set to None at + the end of the statements. + + :variable_counter: Counter to create new unique variables needed + by statements. Variables are created using .variable() and + have the form of "@py_assert0". + + :expl_stmts: The AST statements which will be executed to get + data from the assertion. This is the code which will construct + the detailed assertion message that is used in the AssertionError + or for the pytest_assertion_pass hook. + + :explanation_specifiers: A dict filled by .explanation_param() + with %-formatting placeholders and their corresponding + expressions to use in the building of an assertion message. + This is used by .pop_format_context() to build a message. + + :stack: A stack of the explanation_specifiers dicts maintained by + .push_format_context() and .pop_format_context() which allows + to build another %-formatted string while already building one. + + :scope: A tuple containing the current scope used for variables_overwrite. + + :variables_overwrite: A dict filled with references to variables + that change value within an assert. This happens when a variable is + reassigned with the walrus operator + + This state, except the variables_overwrite, is reset on every new assert + statement visited and used by the other visitors. + """ + + def __init__( + self, module_path: str | None, config: Config | None, source: bytes + ) -> None: + super().__init__() + self.module_path = module_path + self.config = config + if config is not None: + self.enable_assertion_pass_hook = config.getini( + "enable_assertion_pass_hook" + ) + else: + self.enable_assertion_pass_hook = False + self.source = source + self.scope: tuple[ast.AST, ...] = () + self.variables_overwrite: defaultdict[tuple[ast.AST, ...], dict[str, str]] = ( + defaultdict(dict) + ) + + def run(self, mod: ast.Module) -> None: + """Find all assert statements in *mod* and rewrite them.""" + if not mod.body: + # Nothing to do. + return + + # We'll insert some special imports at the top of the module, but after any + # docstrings and __future__ imports, so first figure out where that is. + doc = getattr(mod, "docstring", None) + expect_docstring = doc is None + if doc is not None and self.is_rewrite_disabled(doc): + return + pos = 0 + for item in mod.body: + match item: + case ast.Expr(value=ast.Constant(value=str() as doc)) if ( + expect_docstring + ): + if self.is_rewrite_disabled(doc): + return + expect_docstring = False + case ast.ImportFrom(level=0, module="__future__"): + pass + case _: + break + pos += 1 + # Special case: for a decorated function, set the lineno to that of the + # first decorator, not the `def`. Issue #4984. + if isinstance(item, ast.FunctionDef) and item.decorator_list: + lineno = item.decorator_list[0].lineno + else: + lineno = item.lineno + # Now actually insert the special imports. + aliases = [ + ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0), + ast.alias( + "_pytest.assertion.rewrite", + "@pytest_ar", + lineno=lineno, + col_offset=0, + ), + ] + imports = [ + ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases + ] + mod.body[pos:pos] = imports + + # Collect asserts. + self.scope = (mod,) + nodes: list[ast.AST | Sentinel] = [mod] + while nodes: + node = nodes.pop() + if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef): + self.scope = tuple((*self.scope, node)) + nodes.append(_SCOPE_END_MARKER) + if node == _SCOPE_END_MARKER: + self.scope = self.scope[:-1] + continue + assert isinstance(node, ast.AST) + for name, field in ast.iter_fields(node): + if isinstance(field, list): + new: list[ast.AST] = [] + for i, child in enumerate(field): + if isinstance(child, ast.Assert): + # Transform assert. + new.extend(self.visit(child)) + else: + new.append(child) + if isinstance(child, ast.AST): + nodes.append(child) + setattr(node, name, new) + elif ( + isinstance(field, ast.AST) + # Don't recurse into expressions as they can't contain + # asserts. + and not isinstance(field, ast.expr) + ): + nodes.append(field) + + @staticmethod + def is_rewrite_disabled(docstring: str) -> bool: + return "PYTEST_DONT_REWRITE" in docstring + + def variable(self) -> str: + """Get a new variable.""" + # Use a character invalid in python identifiers to avoid clashing. + name = "@py_assert" + str(next(self.variable_counter)) + self.variables.append(name) + return name + + def assign(self, expr: ast.expr) -> ast.Name: + """Give *expr* a name.""" + name = self.variable() + self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) + return ast.copy_location(ast.Name(name, ast.Load()), expr) + + def display(self, expr: ast.expr) -> ast.expr: + """Call saferepr on the expression.""" + return self.helper("_saferepr", expr) + + def helper(self, name: str, *args: ast.expr) -> ast.expr: + """Call a helper in this module.""" + py_name = ast.Name("@pytest_ar", ast.Load()) + attr = ast.Attribute(py_name, name, ast.Load()) + return ast.Call(attr, list(args), []) + + def builtin(self, name: str) -> ast.Attribute: + """Return the builtin called *name*.""" + builtin_name = ast.Name("@py_builtins", ast.Load()) + return ast.Attribute(builtin_name, name, ast.Load()) + + def explanation_param(self, expr: ast.expr) -> str: + """Return a new named %-formatting placeholder for expr. + + This creates a %-formatting placeholder for expr in the + current formatting context, e.g. ``%(py0)s``. The placeholder + and expr are placed in the current format context so that it + can be used on the next call to .pop_format_context(). + """ + specifier = "py" + str(next(self.variable_counter)) + self.explanation_specifiers[specifier] = expr + return "%(" + specifier + ")s" + + def push_format_context(self) -> None: + """Create a new formatting context. + + The format context is used for when an explanation wants to + have a variable value formatted in the assertion message. In + this case the value required can be added using + .explanation_param(). Finally .pop_format_context() is used + to format a string of %-formatted values as added by + .explanation_param(). + """ + self.explanation_specifiers: dict[str, ast.expr] = {} + self.stack.append(self.explanation_specifiers) + + def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: + """Format the %-formatted string with current format context. + + The expl_expr should be an str ast.expr instance constructed from + the %-placeholders created by .explanation_param(). This will + add the required code to format said string to .expl_stmts and + return the ast.Name instance of the formatted string. + """ + current = self.stack.pop() + if self.stack: + self.explanation_specifiers = self.stack[-1] + keys: list[ast.expr | None] = [ast.Constant(key) for key in current.keys()] + format_dict = ast.Dict(keys, list(current.values())) + form = ast.BinOp(expl_expr, ast.Mod(), format_dict) + name = "@py_format" + str(next(self.variable_counter)) + if self.enable_assertion_pass_hook: + self.format_variables.append(name) + self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form)) + return ast.Name(name, ast.Load()) + + def generic_visit(self, node: ast.AST) -> tuple[ast.Name, str]: + """Handle expressions we don't have custom code for.""" + assert isinstance(node, ast.expr) + res = self.assign(node) + return res, self.explanation_param(self.display(res)) + + def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]: + """Return the AST statements to replace the ast.Assert instance. + + This rewrites the test of an assertion to provide + intermediate values and replace it with an if statement which + raises an assertion error with a detailed explanation in case + the expression is false. + """ + if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: + import warnings + + from _pytest.warning_types import PytestAssertRewriteWarning + + # TODO: This assert should not be needed. + assert self.module_path is not None + warnings.warn_explicit( + PytestAssertRewriteWarning( + "assertion is always true, perhaps remove parentheses?" + ), + category=None, + filename=self.module_path, + lineno=assert_.lineno, + ) + + self.statements: list[ast.stmt] = [] + self.variables: list[str] = [] + self.variable_counter = itertools.count() + + if self.enable_assertion_pass_hook: + self.format_variables: list[str] = [] + + self.stack: list[dict[str, ast.expr]] = [] + self.expl_stmts: list[ast.stmt] = [] + self.push_format_context() + # Rewrite assert into a bunch of statements. + top_condition, explanation = self.visit(assert_.test) + + negation = ast.UnaryOp(ast.Not(), top_condition) + + if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook + msg = self.pop_format_context(ast.Constant(explanation)) + + # Failed + if assert_.msg: + assertmsg = self.helper("_format_assertmsg", assert_.msg) + gluestr = "\n>assert " + else: + assertmsg = ast.Constant("") + gluestr = "assert " + err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg) + err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) + err_name = ast.Name("AssertionError", ast.Load()) + fmt = self.helper("_format_explanation", err_msg) + exc = ast.Call(err_name, [fmt], []) + raise_ = ast.Raise(exc, None) + statements_fail = [] + statements_fail.extend(self.expl_stmts) + statements_fail.append(raise_) + + # Passed + fmt_pass = self.helper("_format_explanation", msg) + orig = _get_assertion_exprs(self.source)[assert_.lineno] + hook_call_pass = ast.Expr( + self.helper( + "_call_assertion_pass", + ast.Constant(assert_.lineno), + ast.Constant(orig), + fmt_pass, + ) + ) + # If any hooks implement assert_pass hook + hook_impl_test = ast.If( + self.helper("_check_if_assertion_pass_impl"), + [*self.expl_stmts, hook_call_pass], + [], + ) + statements_pass: list[ast.stmt] = [hook_impl_test] + + # Test for assertion condition + main_test = ast.If(negation, statements_fail, statements_pass) + self.statements.append(main_test) + if self.format_variables: + variables: list[ast.expr] = [ + ast.Name(name, ast.Store()) for name in self.format_variables + ] + clear_format = ast.Assign(variables, ast.Constant(None)) + self.statements.append(clear_format) + + else: # Original assertion rewriting + # Create failure message. + body = self.expl_stmts + self.statements.append(ast.If(negation, body, [])) + if assert_.msg: + assertmsg = self.helper("_format_assertmsg", assert_.msg) + explanation = "\n>assert " + explanation + else: + assertmsg = ast.Constant("") + explanation = "assert " + explanation + template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation)) + msg = self.pop_format_context(template) + fmt = self.helper("_format_explanation", msg) + err_name = ast.Name("AssertionError", ast.Load()) + exc = ast.Call(err_name, [fmt], []) + raise_ = ast.Raise(exc, None) + + body.append(raise_) + + # Clear temporary variables by setting them to None. + if self.variables: + variables = [ast.Name(name, ast.Store()) for name in self.variables] + clear = ast.Assign(variables, ast.Constant(None)) + self.statements.append(clear) + # Fix locations (line numbers/column offsets). + for stmt in self.statements: + for node in traverse_node(stmt): + if getattr(node, "lineno", None) is None: + # apply the assertion location to all generated ast nodes without source location + # and preserve the location of existing nodes or generated nodes with an correct location. + ast.copy_location(node, assert_) + return self.statements + + def visit_NamedExpr(self, name: ast.NamedExpr) -> tuple[ast.NamedExpr, str]: + # This method handles the 'walrus operator' repr of the target + # name if it's a local variable or _should_repr_global_name() + # thinks it's acceptable. + locs = ast.Call(self.builtin("locals"), [], []) + target_id = name.target.id + inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs]) + dorepr = self.helper("_should_repr_global_name", name) + test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) + expr = ast.IfExp(test, self.display(name), ast.Constant(target_id)) + return name, self.explanation_param(expr) + + def visit_Name(self, name: ast.Name) -> tuple[ast.Name, str]: + # Display the repr of the name if it's a local variable or + # _should_repr_global_name() thinks it's acceptable. + locs = ast.Call(self.builtin("locals"), [], []) + inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs]) + dorepr = self.helper("_should_repr_global_name", name) + test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) + expr = ast.IfExp(test, self.display(name), ast.Constant(name.id)) + return name, self.explanation_param(expr) + + def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]: + res_var = self.variable() + expl_list = self.assign(ast.List([], ast.Load())) + app = ast.Attribute(expl_list, "append", ast.Load()) + is_or = int(isinstance(boolop.op, ast.Or)) + body = save = self.statements + fail_save = self.expl_stmts + levels = len(boolop.values) - 1 + self.push_format_context() + # Process each operand, short-circuiting if needed. + for i, v in enumerate(boolop.values): + if i: + fail_inner: list[ast.stmt] = [] + # cond is set in a prior loop iteration below + self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821 + self.expl_stmts = fail_inner + match v: + # Check if the left operand is an ast.NamedExpr and the value has already been visited + case ast.Compare( + left=ast.NamedExpr(target=ast.Name(id=target_id)) + ) if target_id in [ + e.id for e in boolop.values[:i] if hasattr(e, "id") + ]: + pytest_temp = self.variable() + self.variables_overwrite[self.scope][target_id] = v.left # type:ignore[assignment] + # mypy's false positive, we're checking that the 'target' attribute exists. + v.left.target.id = pytest_temp # type:ignore[attr-defined] + self.push_format_context() + res, expl = self.visit(v) + body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) + expl_format = self.pop_format_context(ast.Constant(expl)) + call = ast.Call(app, [expl_format], []) + self.expl_stmts.append(ast.Expr(call)) + if i < levels: + cond: ast.expr = res + if is_or: + cond = ast.UnaryOp(ast.Not(), cond) + inner: list[ast.stmt] = [] + self.statements.append(ast.If(cond, inner, [])) + self.statements = body = inner + self.statements = save + self.expl_stmts = fail_save + expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or)) + expl = self.pop_format_context(expl_template) + return ast.Name(res_var, ast.Load()), self.explanation_param(expl) + + def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]: + pattern = UNARY_MAP[unary.op.__class__] + operand_res, operand_expl = self.visit(unary.operand) + res = self.assign(ast.copy_location(ast.UnaryOp(unary.op, operand_res), unary)) + return res, pattern % (operand_expl,) + + def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]: + symbol = BINOP_MAP[binop.op.__class__] + left_expr, left_expl = self.visit(binop.left) + right_expr, right_expl = self.visit(binop.right) + explanation = f"({left_expl} {symbol} {right_expl})" + res = self.assign( + ast.copy_location(ast.BinOp(left_expr, binop.op, right_expr), binop) + ) + return res, explanation + + def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]: + new_func, func_expl = self.visit(call.func) + arg_expls = [] + new_args = [] + new_kwargs = [] + for arg in call.args: + if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get( + self.scope, {} + ): + arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment] + res, expl = self.visit(arg) + arg_expls.append(expl) + new_args.append(res) + for keyword in call.keywords: + match keyword.value: + case ast.Name(id=id) if id in self.variables_overwrite.get( + self.scope, {} + ): + keyword.value = self.variables_overwrite[self.scope][id] # type:ignore[assignment] + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) + if keyword.arg: + arg_expls.append(keyword.arg + "=" + expl) + else: # **args have `arg` keywords with an .arg of None + arg_expls.append("**" + expl) + + expl = "{}({})".format(func_expl, ", ".join(arg_expls)) + new_call = ast.copy_location(ast.Call(new_func, new_args, new_kwargs), call) + res = self.assign(new_call) + res_expl = self.explanation_param(self.display(res)) + outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}" + return res, outer_expl + + def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]: + # A Starred node can appear in a function call. + res, expl = self.visit(starred.value) + new_starred = ast.Starred(res, starred.ctx) + return new_starred, "*" + expl + + def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]: + if not isinstance(attr.ctx, ast.Load): + return self.generic_visit(attr) + value, value_expl = self.visit(attr.value) + res = self.assign( + ast.copy_location(ast.Attribute(value, attr.attr, ast.Load()), attr) + ) + res_expl = self.explanation_param(self.display(res)) + pat = "%s\n{%s = %s.%s\n}" + expl = pat % (res_expl, res_expl, value_expl, attr.attr) + return res, expl + + def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]: + self.push_format_context() + # We first check if we have overwritten a variable in the previous assert + match comp.left: + case ast.Name(id=name_id) if name_id in self.variables_overwrite.get( + self.scope, {} + ): + comp.left = self.variables_overwrite[self.scope][name_id] # type: ignore[assignment] + case ast.NamedExpr(target=ast.Name(id=target_id)): + self.variables_overwrite[self.scope][target_id] = comp.left # type: ignore[assignment] + left_res, left_expl = self.visit(comp.left) + if isinstance(comp.left, ast.Compare | ast.BoolOp): + left_expl = f"({left_expl})" + res_variables = [self.variable() for i in range(len(comp.ops))] + load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables] + store_names = [ast.Name(v, ast.Store()) for v in res_variables] + it = zip(range(len(comp.ops)), comp.ops, comp.comparators, strict=True) + expls: list[ast.expr] = [] + syms: list[ast.expr] = [] + results = [left_res] + for i, op, next_operand in it: + match (next_operand, left_res): + case ( + ast.NamedExpr(target=ast.Name(id=target_id)), + ast.Name(id=name_id), + ) if target_id == name_id: + next_operand.target.id = self.variable() + self.variables_overwrite[self.scope][name_id] = next_operand # type: ignore[assignment] + + next_res, next_expl = self.visit(next_operand) + if isinstance(next_operand, ast.Compare | ast.BoolOp): + next_expl = f"({next_expl})" + results.append(next_res) + sym = BINOP_MAP[op.__class__] + syms.append(ast.Constant(sym)) + expl = f"{left_expl} {sym} {next_expl}" + expls.append(ast.Constant(expl)) + res_expr = ast.copy_location(ast.Compare(left_res, [op], [next_res]), comp) + self.statements.append(ast.Assign([store_names[i]], res_expr)) + left_res, left_expl = next_res, next_expl + # Use pytest.assertion.util._reprcompare if that's available. + expl_call = self.helper( + "_call_reprcompare", + ast.Tuple(syms, ast.Load()), + ast.Tuple(load_names, ast.Load()), + ast.Tuple(expls, ast.Load()), + ast.Tuple(results, ast.Load()), + ) + if len(comp.ops) > 1: + res: ast.expr = ast.BoolOp(ast.And(), load_names) + else: + res = load_names[0] + + return res, self.explanation_param(self.pop_format_context(expl_call)) + + +def try_makedirs(cache_dir: Path) -> bool: + """Attempt to create the given directory and sub-directories exist. + + Returns True if successful or if it already exists. + """ + try: + os.makedirs(cache_dir, exist_ok=True) + except (FileNotFoundError, NotADirectoryError, FileExistsError): + # One of the path components was not a directory: + # - we're in a zip file + # - it is a file + return False + except PermissionError: + return False + except OSError as e: + # as of now, EROFS doesn't have an equivalent OSError-subclass + # + # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not + # implemented" for a read-only error + if e.errno in {errno.EROFS, errno.ENOSYS}: + return False + raise + return True + + +def get_cache_dir(file_path: Path) -> Path: + """Return the cache directory to write .pyc files for the given .py file path.""" + if sys.pycache_prefix: + # given: + # prefix = '/tmp/pycs' + # path = '/home/user/proj/test_app.py' + # we want: + # '/tmp/pycs/home/user/proj' + return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) + else: + # classic pycache directory + return file_path.parent / "__pycache__" diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/truncate.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/truncate.py new file mode 100644 index 0000000..d62ca33 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/truncate.py @@ -0,0 +1,128 @@ +"""Utilities for truncating assertion output. + +Current default behaviour is to truncate assertion explanations at +terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI. +""" + +from __future__ import annotations + +from _pytest.compat import running_on_ci +from _pytest.config import Config +from _pytest.nodes import Item + + +DEFAULT_MAX_LINES = 8 +DEFAULT_MAX_CHARS = DEFAULT_MAX_LINES * 80 +USAGE_MSG = "use '-vv' to show" + + +def truncate_if_required(explanation: list[str], item: Item) -> list[str]: + """Truncate this assertion explanation if the given test item is eligible.""" + should_truncate, max_lines, max_chars = _get_truncation_parameters(item) + if should_truncate: + return _truncate_explanation( + explanation, + max_lines=max_lines, + max_chars=max_chars, + ) + return explanation + + +def _get_truncation_parameters(item: Item) -> tuple[bool, int, int]: + """Return the truncation parameters related to the given item, as (should truncate, max lines, max chars).""" + # We do not need to truncate if one of conditions is met: + # 1. Verbosity level is 2 or more; + # 2. Test is being run in CI environment; + # 3. Both truncation_limit_lines and truncation_limit_chars + # .ini parameters are set to 0 explicitly. + max_lines = item.config.getini("truncation_limit_lines") + max_lines = int(max_lines if max_lines is not None else DEFAULT_MAX_LINES) + + max_chars = item.config.getini("truncation_limit_chars") + max_chars = int(max_chars if max_chars is not None else DEFAULT_MAX_CHARS) + + verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS) + + should_truncate = verbose < 2 and not running_on_ci() + should_truncate = should_truncate and (max_lines > 0 or max_chars > 0) + + return should_truncate, max_lines, max_chars + + +def _truncate_explanation( + input_lines: list[str], + max_lines: int, + max_chars: int, +) -> list[str]: + """Truncate given list of strings that makes up the assertion explanation. + + Truncates to either max_lines, or max_chars - whichever the input reaches + first, taking the truncation explanation into account. The remaining lines + will be replaced by a usage message. + + If max_chars=0, no truncation by character count is performed. + If max_lines=0, no truncation by line count is performed. + + When this function is launched we know max_lines > 0 or max_chars > 0 + because _get_truncation_parameters was called first. + """ + # The length of the truncation explanation depends on the number of lines + # removed but is at least 68 characters: + # The real value is + # 64 (for the base message: + # '...\n...Full output truncated (1 line hidden), use '-vv' to show")' + # ) + # + 1 (for plural) + # + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1) + # + 3 for the '...' added to the truncated line + # But if there's more than 100 lines it's very likely that we're going to + # truncate, so we don't need the exact value using log10. + tolerable_max_chars = ( + max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...' + ) + # The truncation explanation add two lines to the output + if max_lines == 0 or len(input_lines) <= max_lines + 2: + if max_chars == 0 or sum(len(s) for s in input_lines) <= tolerable_max_chars: + return input_lines + truncated_explanation = input_lines + else: + # Truncate first to max_lines, and then truncate to max_chars if necessary + truncated_explanation = input_lines[:max_lines] + # We reevaluate the need to truncate chars following removal of some lines + need_to_truncate_char = ( + max_chars > 0 + and sum(len(e) for e in truncated_explanation) > tolerable_max_chars + ) + if need_to_truncate_char: + truncated_explanation = _truncate_by_char_count( + truncated_explanation, max_chars + ) + # Something was truncated, adding '...' at the end to show that + truncated_explanation[-1] += "..." + truncated_line_count = ( + len(input_lines) - len(truncated_explanation) + int(need_to_truncate_char) + ) + return [ + *truncated_explanation, + "", + f"...Full output truncated ({truncated_line_count} line" + f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}", + ] + + +def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]: + # Find point at which input length exceeds total allowed length + iterated_char_count = 0 + for iterated_index, input_line in enumerate(input_lines): + if iterated_char_count + len(input_line) > max_chars: + break + iterated_char_count += len(input_line) + + # Create truncated explanation with modified final line + truncated_result = input_lines[:iterated_index] + final_line = input_lines[iterated_index] + if final_line: + final_line_truncate_point = max_chars - iterated_char_count + final_line = final_line[:final_line_truncate_point] + truncated_result.append(final_line) + return truncated_result diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/assertion/util.py b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/util.py new file mode 100644 index 0000000..5e5ef54 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/assertion/util.py @@ -0,0 +1,215 @@ +# mypy: allow-untyped-defs +"""Utilities for assertion debugging.""" + +from __future__ import annotations + +from collections.abc import Callable +from collections.abc import Iterator +from collections.abc import Sequence +from typing import Literal +from unicodedata import normalize + +from _pytest import outcomes +import _pytest._code +from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited +from _pytest.assertion._compare_any import _compare_eq_any +from _pytest.assertion._compare_set import SET_COMPARISON_FUNCTIONS +from _pytest.assertion._guards import isset +from _pytest.assertion._guards import istext +from _pytest.assertion._typing import _AssertionTextDiffStyle +from _pytest.assertion._typing import _HighlightFunc +from _pytest.assertion.compare_text import _notin_text +from _pytest.assertion.highlight import dummy_highlighter as dummy_highlighter +from _pytest.config import Config +from _pytest.config import UsageError + + +# The _reprcompare attribute on the util module is used by the new assertion +# interpretation code and assertion rewriter to detect this plugin was +# loaded and in turn call the hooks defined here as part of the +# DebugInterpreter. +_reprcompare: Callable[[str, object, object], str | None] | None = None + +# Works similarly as _reprcompare attribute. Is populated with the hook call +# when pytest_runtest_setup is called. +_assertion_pass: Callable[[int, str, str], None] | None = None + +# Config object which is assigned during pytest_runtest_protocol. +_config: Config | None = None + +ASSERTION_TEXT_DIFF_STYLE_INI = "assertion_text_diff_style" +ASSERTION_TEXT_DIFF_STYLE_NDIFF: Literal["ndiff"] = "ndiff" +ASSERTION_TEXT_DIFF_STYLE_BLOCK: Literal["block"] = "block" +ASSERTION_TEXT_DIFF_STYLE_CHOICES = ( + ASSERTION_TEXT_DIFF_STYLE_NDIFF, + ASSERTION_TEXT_DIFF_STYLE_BLOCK, +) + + +def get_assertion_text_diff_style(config: Config) -> _AssertionTextDiffStyle: + style = str(config.getini(ASSERTION_TEXT_DIFF_STYLE_INI)) + match style: + case "ndiff" | "block": + return style + case _: + choices = ", ".join( + repr(choice) for choice in ASSERTION_TEXT_DIFF_STYLE_CHOICES + ) + raise UsageError( + f"{ASSERTION_TEXT_DIFF_STYLE_INI} must be one of {choices}; got {style!r}" + ) + + +def validate_assertion_text_diff_style(config: Config) -> None: + get_assertion_text_diff_style(config) + + +def format_explanation(explanation: str) -> str: + r"""Format an explanation. + + Normally all embedded newlines are escaped, however there are + three exceptions: \n{, \n} and \n~. The first two are intended + cover nested explanations, see function and attribute explanations + for examples (.visit_Call(), visit_Attribute()). The last one is + for when one explanation needs to span multiple lines, e.g. when + displaying diffs. + """ + lines = _split_explanation(explanation) + result = _format_lines(lines) + return "\n".join(result) + + +def _split_explanation(explanation: str) -> list[str]: + r"""Return a list of individual lines in the explanation. + + This will return a list of lines split on '\n{', '\n}' and '\n~'. + Any other newlines will be escaped and appear in the line as the + literal '\n' characters. + """ + raw_lines = (explanation or "").split("\n") + lines = [raw_lines[0]] + for values in raw_lines[1:]: + if values and values[0] in ["{", "}", "~", ">"]: + lines.append(values) + else: + lines[-1] += "\\n" + values + return lines + + +def _format_lines(lines: Sequence[str]) -> list[str]: + """Format the individual lines. + + This will replace the '{', '}' and '~' characters of our mini formatting + language with the proper 'where ...', 'and ...' and ' + ...' text, taking + care of indentation along the way. + + Return a list of formatted lines. + """ + result = list(lines[:1]) + stack = [0] + stackcnt = [0] + for line in lines[1:]: + if line.startswith("{"): + if stackcnt[-1]: + s = "and " + else: + s = "where " + stack.append(len(result)) + stackcnt[-1] += 1 + stackcnt.append(0) + result.append(" +" + " " * (len(stack) - 1) + s + line[1:]) + elif line.startswith("}"): + stack.pop() + stackcnt.pop() + result[stack[-1]] += line[1:] + else: + assert line[0] in ["~", ">"] + stack[-1] += 1 + indent = len(stack) if line.startswith("~") else len(stack) - 1 + result.append(" " * indent + line[1:]) + assert len(stack) == 1 + return result + + +def assertrepr_compare( + op: str, + left: object, + right: object, + *, + verbose: int, + highlighter: _HighlightFunc, + assertion_text_diff_style: _AssertionTextDiffStyle, +) -> Iterator[str]: + """Yield specialised explanations for some operators/operands. + + The first line yielded is always the summary (``left op right``); + subsequent lines are the detailed explanation. Yields nothing when no + specialised explanation applies, which lets consumers map an empty + iterator to "no explanation" without materialising anything. + + The iterator is lazy on purpose: a streaming consumer can stop pulling + lines as soon as it has enough to show, so an enormous diff doesn't + have to be built in full just to be thrown away. + """ + # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier. + # See issue #3246. + use_ascii = ( + isinstance(left, str) + and isinstance(right, str) + and normalize("NFD", left) == normalize("NFD", right) + ) + + if verbose > 1: + left_repr = saferepr_unlimited(left, use_ascii=use_ascii) + right_repr = saferepr_unlimited(right, use_ascii=use_ascii) + else: + # XXX: "15 chars indentation" is wrong + # ("E AssertionError: assert "); should use term width. + maxsize = ( + 80 - 15 - len(op) - 2 + ) // 2 # 15 chars indentation, 1 space around op + + left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii) + right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii) + + summary = f"{left_repr} {op} {right_repr}" + + try: + if op == "==": + source = _compare_eq_any( + left, + right, + highlighter, + verbose, + assertion_text_diff_style, + ) + elif op == "not in" and istext(left) and istext(right): + source = _notin_text(left, right, verbose) + elif op in {"!=", ">=", "<=", ">", "<"} and isset(left) and isset(right): + source = SET_COMPARISON_FUNCTIONS[op](left, right, highlighter, verbose) + else: + source = iter(()) + + # Only yield the summary if there is a detailed explanation. + # Make sure there's a separating empty line after the summary. + summary_yielded = False + for line in source: + if not summary_yielded: + yield summary + if line != "": + yield "" + summary_yielded = True + yield line + except outcomes.Exit: + raise + except Exception: + repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash() + if not summary_yielded: + yield summary + yield "" + summary_yielded = True + yield ( + f"(pytest_assertion plugin: representation of details failed: {repr_crash}." + ) + yield " Probably an object has a faulty __repr__.)" diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/cacheprovider.py b/.venv-test/lib/python3.12/site-packages/_pytest/cacheprovider.py new file mode 100644 index 0000000..6bcac1a --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/cacheprovider.py @@ -0,0 +1,640 @@ +# mypy: allow-untyped-defs +"""Implementation of the cache provider.""" + +# This plugin was not named "cache" to avoid conflicts with the external +# pytest-cache version. +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Iterable +import dataclasses +import errno +import json +import os +from pathlib import Path +import shutil +import tempfile +from typing import final + +from .pathlib import resolve_from_str +from .pathlib import rm_rf +from .reports import CollectReport +from _pytest import nodes +from _pytest._io import TerminalWriter +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.main import Session +from _pytest.nodes import Directory +from _pytest.nodes import File +from _pytest.reports import TestReport + + +CACHEDIR_FILES: dict[str, bytes] = { + "README.md": b"""\ +# pytest cache directory # + +This directory contains data from the pytest's cache plugin, +which provides the `--lf` and `--ff` options, as well as the `cache` fixture. + +**Do not** commit this to version control. + +See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information. +""", + ".gitignore": b"# Created by pytest automatically.\n*\n", + "CACHEDIR.TAG": b"""\ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by pytest. +# For information about cache directory tags, see: +# https://bford.info/cachedir/spec.html +""", +} + + +def _make_cachedir(target: Path) -> None: + """Create the pytest cache directory atomically with supporting files. + + Creates a temporary directory with README.md, .gitignore, and CACHEDIR.TAG, + then atomically renames it to the target location. If another process wins + the race, the temporary directory is cleaned up. + """ + target.parent.mkdir(parents=True, exist_ok=True) + path = Path(tempfile.mkdtemp(prefix="pytest-cache-files-", dir=target.parent)) + try: + # Reset permissions to the default, see #12308. + # Note: there's no way to get the current umask atomically, eek. + umask = os.umask(0o022) + os.umask(umask) + path.chmod(0o777 - umask) + + for name, content in CACHEDIR_FILES.items(): + path.joinpath(name).write_bytes(content) + + path.rename(target) + except OSError as e: + # If 2 concurrent pytests both race to the rename, the loser + # gets "Directory not empty" from the rename. In this case, + # everything is handled so just continue after cleanup. + # On Windows, the error is a FileExistsError which translates to EEXIST. + if e.errno not in (errno.ENOTEMPTY, errno.EEXIST): + raise + finally: + shutil.rmtree(path, ignore_errors=True) + + +@final +@dataclasses.dataclass +class Cache: + """Instance of the `cache` fixture.""" + + _cachedir: Path = dataclasses.field(repr=False) + _config: Config = dataclasses.field(repr=False) + + # Sub-directory under cache-dir for directories created by `mkdir()`. + _CACHE_PREFIX_DIRS = "d" + + # Sub-directory under cache-dir for values created by `set()`. + _CACHE_PREFIX_VALUES = "v" + + def __init__( + self, cachedir: Path, config: Config, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + self._cachedir = cachedir + self._config = config + + @classmethod + def for_config(cls, config: Config, *, _ispytest: bool = False) -> Cache: + """Create the Cache instance for a Config. + + :meta private: + """ + check_ispytest(_ispytest) + cachedir = cls.cache_dir_from_config(config, _ispytest=True) + if config.getoption("cacheclear") and cachedir.is_dir(): + cls.clear_cache(cachedir, _ispytest=True) + return cls(cachedir, config, _ispytest=True) + + @classmethod + def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None: + """Clear the sub-directories used to hold cached directories and values. + + :meta private: + """ + check_ispytest(_ispytest) + for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES): + d = cachedir / prefix + if d.is_dir(): + rm_rf(d) + + @staticmethod + def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path: + """Get the path to the cache directory for a Config. + + :meta private: + """ + check_ispytest(_ispytest) + return resolve_from_str(config.getini("cache_dir"), config.rootpath) + + def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None: + """Issue a cache warning. + + :meta private: + """ + check_ispytest(_ispytest) + import warnings + + from _pytest.warning_types import PytestCacheWarning + + warnings.warn( + PytestCacheWarning(fmt.format(**args) if args else fmt), + self._config.hook, + stacklevel=3, + ) + + def _mkdir(self, path: Path) -> None: + self._ensure_cache_dir_and_supporting_files() + path.mkdir(exist_ok=True, parents=True) + + def mkdir(self, name: str) -> Path: + """Return a directory path object with the given name. + + If the directory does not yet exist, it will be created. You can use + it to manage files to e.g. store/retrieve database dumps across test + sessions. + + .. versionadded:: 7.0 + + :param name: + Must be a string not containing a ``/`` separator. + Make sure the name contains your plugin or application + identifiers to prevent clashes with other cache users. + """ + path = Path(name) + if len(path.parts) > 1: + raise ValueError("name is not allowed to contain path separators") + res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path) + self._mkdir(res) + return res + + def _getvaluepath(self, key: str) -> Path: + return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) + + def get(self, key: str, default): + """Return the cached value for the given key. + + If no value was yet cached or the value cannot be read, the specified + default is returned. + + :param key: + Must be a ``/`` separated value. Usually the first + name is the name of your plugin or your application. + :param default: + The value to return in case of a cache-miss or invalid cache value. + """ + path = self._getvaluepath(key) + try: + with path.open("r", encoding="UTF-8") as f: + return json.load(f) + except (ValueError, OSError): + return default + + def set(self, key: str, value: object) -> None: + """Save value for the given key. + + :param key: + Must be a ``/`` separated value. Usually the first + name is the name of your plugin or your application. + :param value: + Must be of any combination of basic python types, + including nested types like lists of dictionaries. + """ + path = self._getvaluepath(key) + try: + self._mkdir(path.parent) + except OSError as exc: + self.warn( + f"could not create cache path {path}: {exc}", + _ispytest=True, + ) + return + data = json.dumps(value, ensure_ascii=False, indent=2) + try: + f = path.open("w", encoding="UTF-8") + except OSError as exc: + self.warn( + f"cache could not write path {path}: {exc}", + _ispytest=True, + ) + else: + with f: + f.write(data) + + def _ensure_cache_dir_and_supporting_files(self) -> None: + """Create the cache dir and its supporting files.""" + if not self._cachedir.is_dir(): + _make_cachedir(self._cachedir) + + +class LFPluginCollWrapper: + def __init__(self, lfplugin: LFPlugin) -> None: + self.lfplugin = lfplugin + self._collected_at_least_one_failure = False + + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: nodes.Collector + ) -> Generator[None, CollectReport, CollectReport]: + res = yield + if isinstance(collector, Session | Directory): + # Sort any lf-paths to the beginning. + lf_paths = self.lfplugin._last_failed_paths + + # Use stable sort to prioritize last failed. + def sort_key(node: nodes.Item | nodes.Collector) -> bool: + return node.path in lf_paths + + res.result = sorted( + res.result, + key=sort_key, + reverse=True, + ) + + elif isinstance(collector, File): + if collector.path in self.lfplugin._last_failed_paths: + result = res.result + lastfailed = self.lfplugin.lastfailed + + # Only filter with known failures. + if not self._collected_at_least_one_failure: + if not any(x.nodeid in lastfailed for x in result): + return res + self.lfplugin.config.pluginmanager.register( + LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" + ) + self._collected_at_least_one_failure = True + + session = collector.session + result[:] = [ + x + for x in result + if x.nodeid in lastfailed + # Include any passed arguments (not trivial to filter). + or session.isinitpath(x.path) + # Keep all sub-collectors. + or isinstance(x, nodes.Collector) + ] + + return res + + +class LFPluginCollSkipfiles: + def __init__(self, lfplugin: LFPlugin) -> None: + self.lfplugin = lfplugin + + @hookimpl + def pytest_make_collect_report( + self, collector: nodes.Collector + ) -> CollectReport | None: + if isinstance(collector, File): + if collector.path not in self.lfplugin._last_failed_paths: + self.lfplugin._skipped_files += 1 + + return CollectReport( + collector.nodeid, "passed", longrepr=None, result=[] + ) + return None + + +class LFPlugin: + """Plugin which implements the --lf (run last-failing) option.""" + + def __init__(self, config: Config) -> None: + self.config = config + active_keys = "lf", "failedfirst" + self.active = any(config.getoption(key) for key in active_keys) + assert config.cache + self.lastfailed: dict[str, bool] = config.cache.get("cache/lastfailed", {}) + self._previously_failed_count: int | None = None + self._report_status: str | None = None + self._skipped_files = 0 # count skipped files during collection due to --lf + + if config.getoption("lf"): + self._last_failed_paths = self.get_last_failed_paths() + config.pluginmanager.register( + LFPluginCollWrapper(self), "lfplugin-collwrapper" + ) + + def get_last_failed_paths(self) -> set[Path]: + """Return a set with all Paths of the previously failed nodeids and + their parents.""" + rootpath = self.config.rootpath + result = set() + for nodeid in self.lastfailed: + path = rootpath / nodeid.split("::")[0] + result.add(path) + result.update(path.parents) + return {x for x in result if x.exists()} + + def pytest_report_collectionfinish(self) -> str | None: + if self.active and self.config.get_verbosity() >= 0: + return f"run-last-failure: {self._report_status}" + return None + + def pytest_runtest_logreport(self, report: TestReport) -> None: + if (report.when == "call" and report.passed) or report.skipped: + self.lastfailed.pop(report.nodeid, None) + elif report.failed: + self.lastfailed[report.nodeid] = True + + def pytest_collectreport(self, report: CollectReport) -> None: + passed = report.outcome in ("passed", "skipped") + if passed: + if report.nodeid in self.lastfailed: + self.lastfailed.pop(report.nodeid) + self.lastfailed.update((item.nodeid, True) for item in report.result) + else: + self.lastfailed[report.nodeid] = True + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_collection_modifyitems( + self, config: Config, items: list[nodes.Item] + ) -> Generator[None]: + res = yield + + if not self.active: + return res + + if self.lastfailed: + previously_failed = [] + previously_passed = [] + for item in items: + if item.nodeid in self.lastfailed: + previously_failed.append(item) + else: + previously_passed.append(item) + self._previously_failed_count = len(previously_failed) + + if not previously_failed: + # Running a subset of all tests with recorded failures + # only outside of it. + self._report_status = ( + f"{len(self.lastfailed)} known failures not in selected tests" + ) + else: + if self.config.getoption("lf"): + items[:] = previously_failed + config.hook.pytest_deselected(items=previously_passed) + else: # --failedfirst + items[:] = previously_failed + previously_passed + + noun = "failure" if self._previously_failed_count == 1 else "failures" + suffix = " first" if self.config.getoption("failedfirst") else "" + self._report_status = ( + f"rerun previous {self._previously_failed_count} {noun}{suffix}" + ) + + if self._skipped_files > 0: + files_noun = "file" if self._skipped_files == 1 else "files" + self._report_status += f" (skipped {self._skipped_files} {files_noun})" + else: + self._report_status = "no previously failed tests, " + if self.config.getoption("last_failed_no_failures") == "none": + self._report_status += "deselecting all items." + config.hook.pytest_deselected(items=items[:]) + items[:] = [] + else: + self._report_status += "not deselecting items." + + return res + + def pytest_sessionfinish(self, session: Session) -> None: + config = self.config + if config.getoption("cacheshow") or hasattr(config, "workerinput"): + return + + assert config.cache is not None + saved_lastfailed = config.cache.get("cache/lastfailed", {}) + if saved_lastfailed != self.lastfailed: + config.cache.set("cache/lastfailed", self.lastfailed) + + +class NFPlugin: + """Plugin which implements the --nf (run new-first) option.""" + + def __init__(self, config: Config) -> None: + self.config = config + self.active = config.option.newfirst + assert config.cache is not None + self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> Generator[None]: + res = yield + + if self.active: + new_items: dict[str, nodes.Item] = {} + other_items: dict[str, nodes.Item] = {} + for item in items: + if item.nodeid not in self.cached_nodeids: + new_items[item.nodeid] = item + else: + other_items[item.nodeid] = item + + items[:] = self._get_increasing_order( + new_items.values() + ) + self._get_increasing_order(other_items.values()) + self.cached_nodeids.update(new_items) + else: + self.cached_nodeids.update(item.nodeid for item in items) + + return res + + def _get_increasing_order(self, items: Iterable[nodes.Item]) -> list[nodes.Item]: + return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) + + def pytest_sessionfinish(self) -> None: + config = self.config + if config.getoption("cacheshow") or hasattr(config, "workerinput"): + return + + if config.getoption("collectonly"): + return + + assert config.cache is not None + config.cache.set("cache/nodeids", sorted(self.cached_nodeids)) + + +def pytest_addoption(parser: Parser) -> None: + """Add command-line options for cache functionality. + + :param parser: Parser object to add command-line options to. + """ + group = parser.getgroup("general") + group.addoption( + "--lf", + "--last-failed", + action="store_true", + dest="lf", + help="Rerun only the tests that failed at the last run (or all if none failed)", + ) + group.addoption( + "--ff", + "--failed-first", + action="store_true", + dest="failedfirst", + help="Run all tests, but run the last failures first. " + "This may re-order tests and thus lead to " + "repeated fixture setup/teardown.", + ) + group.addoption( + "--nf", + "--new-first", + action="store_true", + dest="newfirst", + help="Run tests from new files first, then the rest of the tests " + "sorted by file mtime", + ) + group.addoption( + "--cache-show", + action="append", + nargs="?", + dest="cacheshow", + help=( + "Show cache contents, don't perform collection or tests. " + "Optional argument: glob (default: '*')." + ), + ) + group.addoption( + "--cache-clear", + action="store_true", + dest="cacheclear", + help="Remove all cache contents at start of test run", + ) + cache_dir_default = ".pytest_cache" + if "TOX_ENV_DIR" in os.environ: + cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default) + parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path") + group.addoption( + "--lfnf", + "--last-failed-no-failures", + action="store", + dest="last_failed_no_failures", + choices=("all", "none"), + default="all", + help="With ``--lf``, determines whether to execute tests when there " + "are no previously (known) failures or when no " + "cached ``lastfailed`` data was found. " + "``all`` (the default) runs the full test suite again. " + "``none`` just emits a message about no known failures and exits successfully.", + ) + + +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + if config.option.cacheshow and not config.option.help: + from _pytest.main import wrap_session + + return wrap_session(config, cacheshow) + return None + + +@hookimpl(tryfirst=True) +def pytest_configure(config: Config) -> None: + """Configure cache system and register related plugins. + + Creates the Cache instance and registers the last-failed (LFPlugin) + and new-first (NFPlugin) plugins with the plugin manager. + + :param config: pytest configuration object. + """ + config.cache = Cache.for_config(config, _ispytest=True) + config.pluginmanager.register(LFPlugin(config), "lfplugin") + config.pluginmanager.register(NFPlugin(config), "nfplugin") + + +@fixture +def cache(request: FixtureRequest) -> Cache: + """Return a cache object that can persist state between testing sessions. + + cache.get(key, default) + cache.set(key, value) + + Keys must be ``/`` separated strings, where the first part is usually the + name of your plugin or application to avoid clashes with other cache users. + + Values can be any object handled by the json stdlib module. + """ + assert request.config.cache is not None + return request.config.cache + + +def pytest_report_header(config: Config) -> str | None: + """Display cachedir with --cache-show and if non-default.""" + if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache": + assert config.cache is not None + cachedir = config.cache._cachedir + # TODO: evaluate generating upward relative paths + # starting with .., ../.. if sensible + + try: + displaypath = cachedir.relative_to(config.rootpath) + except ValueError: + displaypath = cachedir + return f"cachedir: {displaypath}" + return None + + +def cacheshow(config: Config, session: Session) -> int: + """Display cache contents when --cache-show is used. + + Shows cached values and directories matching the specified glob pattern + (default: '*'). Displays cache location, cached test results, and + any cached directories created by plugins. + + :param config: pytest configuration object. + :param session: pytest session object. + :returns: Exit code (0 for success). + """ + from pprint import pformat + + assert config.cache is not None + + tw = TerminalWriter() + tw.line("cachedir: " + str(config.cache._cachedir)) + if not config.cache._cachedir.is_dir(): + tw.line("cache is empty") + return 0 + + glob = config.option.cacheshow[0] + if glob is None: + glob = "*" + + dummy = object() + basedir = config.cache._cachedir + vdir = basedir / Cache._CACHE_PREFIX_VALUES + tw.sep("-", f"cache values for {glob!r}") + for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()): + key = str(valpath.relative_to(vdir)) + val = config.cache.get(key, dummy) + if val is dummy: + tw.line(f"{key} contains unreadable content, will be ignored") + else: + tw.line(f"{key} contains:") + for line in pformat(val).splitlines(): + tw.line(" " + line) + + ddir = basedir / Cache._CACHE_PREFIX_DIRS + if ddir.is_dir(): + contents = sorted(ddir.rglob(glob)) + tw.sep("-", f"cache directories for {glob!r}") + for p in contents: + # if p.is_dir(): + # print("%s/" % p.relative_to(basedir)) + if p.is_file(): + key = str(p.relative_to(basedir)) + tw.line(f"{key} is a file of length {p.stat().st_size}") + return 0 diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/capture.py b/.venv-test/lib/python3.12/site-packages/_pytest/capture.py new file mode 100644 index 0000000..f9f8f9e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/capture.py @@ -0,0 +1,1151 @@ +# mypy: allow-untyped-defs +"""Per-test stdout/stderr capturing mechanism.""" + +from __future__ import annotations + +import abc +import collections +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +import contextlib +import io +from io import UnsupportedOperation +import os +import sys +from tempfile import TemporaryFile +from types import TracebackType +from typing import Any +from typing import AnyStr +from typing import BinaryIO +from typing import cast +from typing import Final +from typing import final +from typing import Generic +from typing import Literal +from typing import NamedTuple +from typing import TextIO +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from typing_extensions import Self + +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import SubRequest +from _pytest.nodes import Collector +from _pytest.nodes import File +from _pytest.nodes import Item +from _pytest.reports import CollectReport + + +_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group.addoption( + "--capture", + action="store", + default="fd", + metavar="method", + choices=["fd", "sys", "no", "tee-sys"], + help="Per-test capturing method: one of fd|sys|no|tee-sys", + ) + group._addoption( # private to use reserved lower-case short option + "-s", + action="store_const", + const="no", + dest="capture", + help="Shortcut for --capture=no", + ) + + +def _colorama_workaround() -> None: + """Ensure colorama is imported so that it attaches to the correct stdio + handles on Windows. + + colorama uses the terminal on import time. So if something does the + first import of colorama while I/O capture is active, colorama will + fail in various ways. + """ + if sys.platform.startswith("win32"): + try: + import colorama # noqa: F401 + except ImportError: + pass + + +def _readline_workaround() -> None: + """Ensure readline is imported early so it attaches to the correct stdio handles. + + This isn't a problem with the default GNU readline implementation, but in + some configurations, Python uses libedit instead (on macOS, and for prebuilt + binaries such as used by uv). + + In theory this is only needed if readline.backend == "libedit", but the + workaround consists of importing readline here, so we already worked around + the issue by the time we could check if we need to. + """ + try: + import readline # noqa: F401 + except ImportError: + pass + + +def _windowsconsoleio_workaround(stream: TextIO) -> None: + """Workaround for Windows Unicode console handling. + + Python 3.6 implemented Unicode console handling for Windows. This works + by reading/writing to the raw console handle using + ``{Read,Write}ConsoleW``. + + The problem is that we are going to ``dup2`` over the stdio file + descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the + handles used by Python to write to the console. Though there is still some + weirdness and the console handle seems to only be closed randomly and not + on the first call to ``CloseHandle``, or maybe it gets reopened with the + same handle value when we suspend capturing. + + The workaround in this case will reopen stdio with a different fd which + also means a different handle by replicating the logic in + "Py_lifecycle.c:initstdio/create_stdio". + + :param stream: + In practice ``sys.stdout`` or ``sys.stderr``, but given + here as parameter for unittesting purposes. + + See https://github.com/pytest-dev/py/issues/103. + """ + if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"): + return + + # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666). + if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore] + return + + raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer + + if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore] + return + + def _reopen_stdio(f, mode): + if not hasattr(stream.buffer, "raw") and mode[0] == "w": + buffering = 0 + else: + buffering = -1 + + return io.TextIOWrapper( + open(os.dup(f.fileno()), mode, buffering), + f.encoding, + f.errors, + f.newlines, + f.line_buffering, + ) + + sys.stdin = _reopen_stdio(sys.stdin, "rb") + sys.stdout = _reopen_stdio(sys.stdout, "wb") + sys.stderr = _reopen_stdio(sys.stderr, "wb") + + +@hookimpl(wrapper=True) +def pytest_load_initial_conftests(early_config: Config) -> Generator[None]: + ns = early_config.known_args_namespace + if ns.capture == "fd": + _windowsconsoleio_workaround(sys.stdout) + _colorama_workaround() + _readline_workaround() + pluginmanager = early_config.pluginmanager + capman = CaptureManager(ns.capture) + pluginmanager.register(capman, "capturemanager") + + # Make sure that capturemanager is properly reset at final shutdown. + early_config.add_cleanup(capman.stop_global_capturing) + + # Finally trigger conftest loading but while capturing (issue #93). + capman.start_global_capturing() + try: + try: + yield + finally: + capman.suspend_global_capture() + except BaseException: + out, err = capman.read_global_capture() + sys.stdout.write(out) + sys.stderr.write(err) + raise + + +# IO Helpers. + + +class EncodedFile(io.TextIOWrapper): + __slots__ = () + + @property + def name(self) -> str: + # Ensure that file.name is a string. Workaround for a Python bug + # fixed in >=3.7.4: https://bugs.python.org/issue36015 + return repr(self.buffer) + + @property + def mode(self) -> str: + # TextIOWrapper doesn't expose a mode, but at least some of our + # tests check it. + assert hasattr(self.buffer, "mode") + return cast(str, self.buffer.mode.replace("b", "")) + + +class CaptureIO(io.TextIOWrapper): + def __init__(self) -> None: + super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) + + def getvalue(self) -> str: + assert isinstance(self.buffer, io.BytesIO) + return self.buffer.getvalue().decode("UTF-8") + + +class TeeCaptureIO(CaptureIO): + def __init__(self, other: TextIO) -> None: + self._other = other + super().__init__() + + def write(self, s: str) -> int: + super().write(s) + return self._other.write(s) + + +class DontReadFromInput(TextIO): + @property + def encoding(self) -> str: + assert sys.__stdin__ is not None + return sys.__stdin__.encoding + + def read(self, size: int = -1) -> str: + raise OSError( + "pytest: reading from stdin while output is captured! Consider using `-s`." + ) + + readline = read + + def __next__(self) -> str: + return self.readline() + + def readlines(self, hint: int | None = -1) -> list[str]: + raise OSError( + "pytest: reading from stdin while output is captured! Consider using `-s`." + ) + + def __iter__(self) -> Iterator[str]: + return self + + def fileno(self) -> int: + raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") + + def flush(self) -> None: + raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()") + + def isatty(self) -> bool: + return False + + def close(self) -> None: + pass + + def readable(self) -> bool: + return False + + def seek(self, offset: int, whence: int = 0) -> int: + raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)") + + def seekable(self) -> bool: + return False + + def tell(self) -> int: + raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()") + + def truncate(self, size: int | None = None) -> int: + raise UnsupportedOperation("cannot truncate stdin") + + def write(self, data: str) -> int: + raise UnsupportedOperation("cannot write to stdin") + + def writelines(self, lines: Iterable[str]) -> None: + raise UnsupportedOperation("Cannot write to stdin") + + def writable(self) -> bool: + return False + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + type: type[BaseException] | None, + value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + pass + + @property + def buffer(self) -> BinaryIO: + # The str/bytes doesn't actually matter in this type, so OK to fake. + return self # type: ignore[return-value] + + +# Capture classes. + + +class CaptureBase(abc.ABC, Generic[AnyStr]): + EMPTY_BUFFER: AnyStr + + @abc.abstractmethod + def __init__(self, fd: int) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def start(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def done(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def suspend(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def resume(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def writeorg(self, data: AnyStr) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def snap(self) -> AnyStr: + raise NotImplementedError() + + +patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} + + +class NoCapture(CaptureBase[str]): + EMPTY_BUFFER = "" + + def __init__(self, fd: int) -> None: + pass + + def start(self) -> None: + pass + + def done(self) -> None: + pass + + def suspend(self) -> None: + pass + + def resume(self) -> None: + pass + + def snap(self) -> str: + return "" + + def writeorg(self, data: str) -> None: + pass + + +class SysCaptureBase(CaptureBase[AnyStr]): + def __init__( + self, fd: int, tmpfile: TextIO | None = None, *, tee: bool = False + ) -> None: + name = patchsysdict[fd] + self._old: TextIO = getattr(sys, name) + self.name = name + if tmpfile is None: + if name == "stdin": + tmpfile = DontReadFromInput() + else: + tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old) + self.tmpfile = tmpfile + self._state = "initialized" + + def repr(self, class_name: str) -> str: + return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( + class_name, + self.name, + (hasattr(self, "_old") and repr(self._old)) or "", + self._state, + self.tmpfile, + ) + + def __repr__(self) -> str: + return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( + self.__class__.__name__, + self.name, + (hasattr(self, "_old") and repr(self._old)) or "", + self._state, + self.tmpfile, + ) + + def _assert_state(self, op: str, states: tuple[str, ...]) -> None: + assert self._state in states, ( + "cannot {} in state {!r}: expected one of {}".format( + op, self._state, ", ".join(states) + ) + ) + + def start(self) -> None: + self._assert_state("start", ("initialized",)) + setattr(sys, self.name, self.tmpfile) + self._state = "started" + + def done(self) -> None: + self._assert_state("done", ("initialized", "started", "suspended", "done")) + if self._state == "done": + return + setattr(sys, self.name, self._old) + del self._old + self.tmpfile.close() + self._state = "done" + + def suspend(self) -> None: + self._assert_state("suspend", ("started", "suspended")) + setattr(sys, self.name, self._old) + self._state = "suspended" + + def resume(self) -> None: + self._assert_state("resume", ("started", "suspended")) + if self._state == "started": + return + setattr(sys, self.name, self.tmpfile) + self._state = "started" + + +class SysCaptureBinary(SysCaptureBase[bytes]): + EMPTY_BUFFER = b"" + + def snap(self) -> bytes: + self._assert_state("snap", ("started", "suspended")) + self.tmpfile.seek(0) + res = self.tmpfile.buffer.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def writeorg(self, data: bytes) -> None: + self._assert_state("writeorg", ("started", "suspended")) + self._old.flush() + self._old.buffer.write(data) + self._old.buffer.flush() + + +class SysCapture(SysCaptureBase[str]): + EMPTY_BUFFER = "" + + def snap(self) -> str: + self._assert_state("snap", ("started", "suspended")) + assert isinstance(self.tmpfile, CaptureIO) + res = self.tmpfile.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def writeorg(self, data: str) -> None: + self._assert_state("writeorg", ("started", "suspended")) + self._old.write(data) + self._old.flush() + + +class FDCaptureBase(CaptureBase[AnyStr]): + def __init__(self, targetfd: int) -> None: + self.targetfd = targetfd + + try: + os.fstat(targetfd) + except OSError: + # FD capturing is conceptually simple -- create a temporary file, + # redirect the FD to it, redirect back when done. But when the + # target FD is invalid it throws a wrench into this lovely scheme. + # + # Tests themselves shouldn't care if the FD is valid, FD capturing + # should work regardless of external circumstances. So falling back + # to just sys capturing is not a good option. + # + # Further complications are the need to support suspend() and the + # possibility of FD reuse (e.g. the tmpfile getting the very same + # target FD). The following approach is robust, I believe. + self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR) + os.dup2(self.targetfd_invalid, targetfd) + else: + self.targetfd_invalid = None + self.targetfd_save = os.dup(targetfd) + + if targetfd == 0: + self.tmpfile = open(os.devnull, encoding="utf-8") + self.syscapture: CaptureBase[str] = SysCapture(targetfd) + else: + self.tmpfile = EncodedFile( + TemporaryFile(buffering=0), + encoding="utf-8", + errors="replace", + newline="", + write_through=True, + ) + if targetfd in patchsysdict: + self.syscapture = SysCapture(targetfd, self.tmpfile) + else: + self.syscapture = NoCapture(targetfd) + + self._state = "initialized" + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} " + f"_state={self._state!r} tmpfile={self.tmpfile!r}>" + ) + + def _assert_state(self, op: str, states: tuple[str, ...]) -> None: + assert self._state in states, ( + "cannot {} in state {!r}: expected one of {}".format( + op, self._state, ", ".join(states) + ) + ) + + def start(self) -> None: + """Start capturing on targetfd using memorized tmpfile.""" + self._assert_state("start", ("initialized",)) + os.dup2(self.tmpfile.fileno(), self.targetfd) + self.syscapture.start() + self._state = "started" + + def done(self) -> None: + """Stop capturing, restore streams, return original capture file, + seeked to position zero.""" + self._assert_state("done", ("initialized", "started", "suspended", "done")) + if self._state == "done": + return + os.dup2(self.targetfd_save, self.targetfd) + os.close(self.targetfd_save) + if self.targetfd_invalid is not None: + if self.targetfd_invalid != self.targetfd: + os.close(self.targetfd) + os.close(self.targetfd_invalid) + self.syscapture.done() + self.tmpfile.close() + self._state = "done" + + def suspend(self) -> None: + self._assert_state("suspend", ("started", "suspended")) + if self._state == "suspended": + return + self.syscapture.suspend() + os.dup2(self.targetfd_save, self.targetfd) + self._state = "suspended" + + def resume(self) -> None: + self._assert_state("resume", ("started", "suspended")) + if self._state == "started": + return + self.syscapture.resume() + os.dup2(self.tmpfile.fileno(), self.targetfd) + self._state = "started" + + +class FDCaptureBinary(FDCaptureBase[bytes]): + """Capture IO to/from a given OS-level file descriptor. + + snap() produces `bytes`. + """ + + EMPTY_BUFFER = b"" + + def snap(self) -> bytes: + self._assert_state("snap", ("started", "suspended")) + self.tmpfile.seek(0) + res = self.tmpfile.buffer.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res # type: ignore[return-value] + + def writeorg(self, data: bytes) -> None: + """Write to original file descriptor.""" + self._assert_state("writeorg", ("started", "suspended")) + os.write(self.targetfd_save, data) + + +class FDCapture(FDCaptureBase[str]): + """Capture IO to/from a given OS-level file descriptor. + + snap() produces text. + """ + + EMPTY_BUFFER = "" + + def snap(self) -> str: + self._assert_state("snap", ("started", "suspended")) + self.tmpfile.seek(0) + res = self.tmpfile.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def writeorg(self, data: str) -> None: + """Write to original file descriptor.""" + self._assert_state("writeorg", ("started", "suspended")) + # XXX use encoding of original stream + os.write(self.targetfd_save, data.encode("utf-8")) + + +# MultiCapture + + +# Generic NamedTuple only supported since Python 3.11. +if sys.version_info >= (3, 11) or TYPE_CHECKING: + + @final + class CaptureResult(NamedTuple, Generic[AnyStr]): + """The result of :method:`caplog.readouterr() `.""" + + out: AnyStr + err: AnyStr + +else: + + class CaptureResult( + collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024 + Generic[AnyStr], + ): + """The result of :method:`caplog.readouterr() `.""" + + __slots__ = () + + +class MultiCapture(Generic[AnyStr]): + _state = None + _in_suspended = False + + def __init__( + self, + in_: CaptureBase[AnyStr] | None, + out: CaptureBase[AnyStr] | None, + err: CaptureBase[AnyStr] | None, + ) -> None: + self.in_: CaptureBase[AnyStr] | None = in_ + self.out: CaptureBase[AnyStr] | None = out + self.err: CaptureBase[AnyStr] | None = err + + def __repr__(self) -> str: + return ( + f"" + ) + + def start_capturing(self) -> None: + self._state = "started" + if self.in_: + self.in_.start() + if self.out: + self.out.start() + if self.err: + self.err.start() + + def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]: + """Pop current snapshot out/err capture and flush to orig streams.""" + out, err = self.readouterr() + if out: + assert self.out is not None + self.out.writeorg(out) + if err: + assert self.err is not None + self.err.writeorg(err) + return out, err + + def suspend_capturing(self, in_: bool = False) -> None: + self._state = "suspended" + if self.out: + self.out.suspend() + if self.err: + self.err.suspend() + if in_ and self.in_: + self.in_.suspend() + self._in_suspended = True + + def resume_capturing(self) -> None: + self._state = "started" + if self.out: + self.out.resume() + if self.err: + self.err.resume() + if self._in_suspended: + assert self.in_ is not None + self.in_.resume() + self._in_suspended = False + + def stop_capturing(self) -> None: + """Stop capturing and reset capturing streams.""" + if self._state == "stopped": + raise ValueError("was already stopped") + self._state = "stopped" + if self.out: + self.out.done() + if self.err: + self.err.done() + if self.in_: + self.in_.done() + + def is_started(self) -> bool: + """Whether actively capturing -- not suspended or stopped.""" + return self._state == "started" + + def readouterr(self) -> CaptureResult[AnyStr]: + out = self.out.snap() if self.out else "" + err = self.err.snap() if self.err else "" + # TODO: This type error is real, need to fix. + return CaptureResult(out, err) # type: ignore[arg-type] + + +def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]: + if method == "fd": + return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) + elif method == "sys": + return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2)) + elif method == "no": + return MultiCapture(in_=None, out=None, err=None) + elif method == "tee-sys": + return MultiCapture( + in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) + ) + raise ValueError(f"unknown capturing method: {method!r}") + + +# CaptureManager and CaptureFixture + + +class CaptureManager: + """The capture plugin. + + Manages that the appropriate capture method is enabled/disabled during + collection and each test phase (setup, call, teardown). After each of + those points, the captured output is obtained and attached to the + collection/runtest report. + + There are two levels of capture: + + * global: enabled by default and can be suppressed by the ``-s`` + option. This is always enabled/disabled during collection and each test + phase. + + * fixture: when a test function or one of its fixture depend on the + ``capsys`` or ``capfd`` fixtures. In this case special handling is + needed to ensure the fixtures take precedence over the global capture. + """ + + def __init__(self, method: _CaptureMethod) -> None: + self._method: Final = method + self._global_capturing: MultiCapture[str] | None = None + self._capture_fixture: CaptureFixture[Any] | None = None + + def __repr__(self) -> str: + return ( + f"" + ) + + def is_capturing(self) -> str | bool: + if self.is_globally_capturing(): + return "global" + if self._capture_fixture: + return f"fixture {self._capture_fixture.request.fixturename}" + return False + + # Global capturing control + + def is_globally_capturing(self) -> bool: + return self._method != "no" + + def start_global_capturing(self) -> None: + assert self._global_capturing is None + self._global_capturing = _get_multicapture(self._method) + self._global_capturing.start_capturing() + + def stop_global_capturing(self) -> None: + if self._global_capturing is not None: + self._global_capturing.pop_outerr_to_orig() + self._global_capturing.stop_capturing() + self._global_capturing = None + + def resume_global_capture(self) -> None: + # During teardown of the python process, and on rare occasions, capture + # attributes can be `None` while trying to resume global capture. + if self._global_capturing is not None: + self._global_capturing.resume_capturing() + + def suspend_global_capture(self, in_: bool = False) -> None: + if self._global_capturing is not None: + self._global_capturing.suspend_capturing(in_=in_) + + def suspend(self, in_: bool = False) -> None: + # Need to undo local capsys-et-al if it exists before disabling global capture. + self.suspend_fixture() + self.suspend_global_capture(in_) + + def resume(self) -> None: + self.resume_global_capture() + self.resume_fixture() + + def read_global_capture(self) -> CaptureResult[str]: + assert self._global_capturing is not None + return self._global_capturing.readouterr() + + # Fixture Control + + def set_fixture(self, capture_fixture: CaptureFixture[Any]) -> None: + if self._capture_fixture: + current_fixture = self._capture_fixture.request.fixturename + requested_fixture = capture_fixture.request.fixturename + capture_fixture.request.raiseerror( + f"cannot use {requested_fixture} and {current_fixture} at the same time" + ) + self._capture_fixture = capture_fixture + + def unset_fixture(self) -> None: + self._capture_fixture = None + + def activate_fixture(self) -> None: + """If the current item is using ``capsys`` or ``capfd``, activate + them so they take precedence over the global capture.""" + if self._capture_fixture: + self._capture_fixture._start() + + def deactivate_fixture(self) -> None: + """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any.""" + if self._capture_fixture: + self._capture_fixture.close() + + def suspend_fixture(self) -> None: + if self._capture_fixture: + self._capture_fixture._suspend() + + def resume_fixture(self) -> None: + if self._capture_fixture: + self._capture_fixture._resume() + + # Helper context managers + + @contextlib.contextmanager + def global_and_fixture_disabled(self) -> Generator[None]: + """Context manager to temporarily disable global and current fixture capturing.""" + do_fixture = self._capture_fixture and self._capture_fixture._is_started() + if do_fixture: + self.suspend_fixture() + do_global = self._global_capturing and self._global_capturing.is_started() + if do_global: + self.suspend_global_capture() + try: + yield + finally: + if do_global: + self.resume_global_capture() + if do_fixture: + self.resume_fixture() + + @contextlib.contextmanager + def item_capture(self, when: str, item: Item) -> Generator[None]: + self.resume_global_capture() + self.activate_fixture() + try: + yield + finally: + self.deactivate_fixture() + self.suspend_global_capture(in_=False) + + out, err = self.read_global_capture() + item.add_report_section(when, "stdout", out) + item.add_report_section(when, "stderr", err) + + # Hooks + + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: Collector + ) -> Generator[None, CollectReport, CollectReport]: + if isinstance(collector, File): + self.resume_global_capture() + try: + rep = yield + finally: + self.suspend_global_capture() + out, err = self.read_global_capture() + if out: + rep.sections.append(("Captured stdout", out)) + if err: + rep.sections.append(("Captured stderr", err)) + else: + rep = yield + return rep + + @hookimpl(wrapper=True) + def pytest_runtest_setup(self, item: Item) -> Generator[None]: + with self.item_capture("setup", item): + return (yield) + + @hookimpl(wrapper=True) + def pytest_runtest_call(self, item: Item) -> Generator[None]: + with self.item_capture("call", item): + return (yield) + + @hookimpl(wrapper=True) + def pytest_runtest_teardown(self, item: Item) -> Generator[None]: + with self.item_capture("teardown", item): + return (yield) + + @hookimpl(tryfirst=True) + def pytest_keyboard_interrupt(self) -> None: + self.stop_global_capturing() + + @hookimpl(tryfirst=True) + def pytest_internalerror(self) -> None: + self.stop_global_capturing() + + +class CaptureFixture(Generic[AnyStr]): + """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`, + :fixture:`capfd` and :fixture:`capfdbinary` fixtures.""" + + def __init__( + self, + captureclass: type[CaptureBase[AnyStr]], + request: SubRequest, + *, + config: dict[str, Any] | None = None, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self.captureclass: type[CaptureBase[AnyStr]] = captureclass + self.request = request + self._config = config if config else {} + self._capture: MultiCapture[AnyStr] | None = None + self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER + self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER + + def _start(self) -> None: + if self._capture is None: + self._capture = MultiCapture( + in_=None, + out=self.captureclass(1, **self._config), + err=self.captureclass(2, **self._config), + ) + self._capture.start_capturing() + + def close(self) -> None: + if self._capture is not None: + if self._config.get("tee"): + # When tee is enabled, output was already written to the + # original stream in real-time by TeeCaptureIO. Using + # pop_outerr_to_orig() would write it a second time via + # writeorg(), causing doubled output (see #13784). + out, err = self._capture.readouterr() + else: + out, err = self._capture.pop_outerr_to_orig() + self._captured_out += out + self._captured_err += err + self._capture.stop_capturing() + self._capture = None + + def readouterr(self) -> CaptureResult[AnyStr]: + """Read and return the captured output so far, resetting the internal + buffer. + + :returns: + The captured content as a namedtuple with ``out`` and ``err`` + string attributes. + """ + captured_out, captured_err = self._captured_out, self._captured_err + if self._capture is not None: + out, err = self._capture.readouterr() + captured_out += out + captured_err += err + self._captured_out = self.captureclass.EMPTY_BUFFER + self._captured_err = self.captureclass.EMPTY_BUFFER + return CaptureResult(captured_out, captured_err) + + def _suspend(self) -> None: + """Suspend this fixture's own capturing temporarily.""" + if self._capture is not None: + self._capture.suspend_capturing() + + def _resume(self) -> None: + """Resume this fixture's own capturing temporarily.""" + if self._capture is not None: + self._capture.resume_capturing() + + def _is_started(self) -> bool: + """Whether actively capturing -- not disabled or closed.""" + if self._capture is not None: + return self._capture.is_started() + return False + + @contextlib.contextmanager + def disabled(self) -> Generator[None]: + """Temporarily disable capturing while inside the ``with`` block.""" + capmanager: CaptureManager = self.request.config.pluginmanager.getplugin( + "capturemanager" + ) + with capmanager.global_and_fixture_disabled(): + yield + + +# The fixtures. + + +@fixture +def capsys(request: SubRequest) -> Generator[CaptureFixture[str]]: + r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. + + The captured output is made available via ``capsys.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. + + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_output(capsys): + print("hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() + + +@fixture +def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]: + r"""Enable simultaneous text capturing and pass-through of writes + to ``sys.stdout`` and ``sys.stderr`` as defined by ``--capture=``. + + + The captured output is made available via ``capteesys.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. + + The output is also passed-through, allowing it to be "live-printed", + reported, or both as defined by ``--capture=``. + + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_output(capteesys): + print("hello") + captured = capteesys.readouterr() + assert captured.out == "hello\n" + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture( + SysCapture, request, config=dict(tee=True), _ispytest=True + ) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() + + +@fixture +def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]: + r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. + + The captured output is made available via ``capsysbinary.readouterr()`` + method calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``bytes`` objects. + + Returns an instance of :class:`CaptureFixture[bytes] `. + + Example: + + .. code-block:: python + + def test_output(capsysbinary): + print("hello") + captured = capsysbinary.readouterr() + assert captured.out == b"hello\n" + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() + + +@fixture +def capfd(request: SubRequest) -> Generator[CaptureFixture[str]]: + r"""Enable text capturing of writes to file descriptors ``1`` and ``2``. + + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. + + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_system_echo(capfd): + os.system('echo "hello"') + captured = capfd.readouterr() + assert captured.out == "hello\n" + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() + + +@fixture +def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]: + r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``. + + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``byte`` objects. + + Returns an instance of :class:`CaptureFixture[bytes] `. + + Example: + + .. code-block:: python + + def test_system_echo(capfdbinary): + os.system('echo "hello"') + captured = capfdbinary.readouterr() + assert captured.out == b"hello\n" + + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/compat.py b/.venv-test/lib/python3.12/site-packages/_pytest/compat.py new file mode 100644 index 0000000..d3b2a46 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/compat.py @@ -0,0 +1,329 @@ +# mypy: allow-untyped-defs +"""Python version compatibility code and random general utilities.""" + +from __future__ import annotations + +from collections.abc import Callable +import enum +import functools +import inspect +from inspect import Parameter +from inspect import Signature +import os +from pathlib import Path +import sys +from typing import Any +from typing import Final +from typing import NoReturn +from typing import TYPE_CHECKING + +import py + + +if sys.version_info >= (3, 14): + from annotationlib import Format + + +#: constant to prepare valuing pylib path replacements/lazy proxies later on +# intended for removal in pytest 8.0 or 9.0 + +# fmt: off +# intentional space to create a fake difference for the verification +LEGACY_PATH = py.path. local +# fmt: on + + +def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH: + """Internal wrapper to prepare lazy proxies for legacy_path instances""" + return LEGACY_PATH(path) + + +# fmt: off +# Singleton type for NOTSET, as described in: +# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions +class NotSetType(enum.Enum): + token = 0 +NOTSET: Final = NotSetType.token +# fmt: on + + +def iscoroutinefunction(func: object) -> bool: + """Return True if func is a coroutine function (a function defined with async + def syntax, and doesn't contain yield), or a function decorated with + @asyncio.coroutine. + + Note: copied and modified from Python 3.5's builtin coroutines.py to avoid + importing asyncio directly, which in turns also initializes the "logging" + module as a side-effect (see issue #8). + """ + return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) + + +def is_async_function(func: object) -> bool: + """Return True if the given function seems to be an async function or + an async generator.""" + return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) + + +def signature(obj: Callable[..., Any]) -> Signature: + """Return signature without evaluating annotations.""" + if sys.version_info >= (3, 14): + return inspect.signature(obj, annotation_format=Format.STRING) + return inspect.signature(obj) + + +def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str: + function = get_real_func(function) + fn = Path(inspect.getfile(function)) + lineno = function.__code__.co_firstlineno + if curdir is not None: + try: + relfn = fn.relative_to(curdir) + except ValueError: + pass + else: + return f"{relfn}:{lineno + 1}" + return f"{fn}:{lineno + 1}" + + +def num_mock_patch_args(function) -> int: + """Return number of arguments used up by mock arguments (if any).""" + patchings = getattr(function, "patchings", None) + if not patchings: + return 0 + + mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object()) + ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object()) + + return len( + [ + p + for p in patchings + if not p.attribute_name + and (p.new is mock_sentinel or p.new is ut_mock_sentinel) + ] + ) + + +def getfuncargnames( + function: Callable[..., object], + *, + name: str = "", + cls: type | None = None, +) -> tuple[str, ...]: + """Return the names of a function's mandatory arguments. + + Should return the names of all function arguments that: + * Aren't bound to an instance or type as in instance or class methods. + * Don't have default values. + * Aren't bound with functools.partial. + * Aren't replaced with mocks. + + The cls arguments indicate that the function should be treated as a bound + method even though it's not unless the function is a static method. + + The name parameter should be the original name in which the function was collected. + """ + # TODO(RonnyPfannschmidt): This function should be refactored when we + # revisit fixtures. The fixture mechanism should ask the node for + # the fixture names, and not try to obtain directly from the + # function object well after collection has occurred. + + # The parameters attribute of a Signature object contains an + # ordered mapping of parameter names to Parameter instances. This + # creates a tuple of the names of the parameters that don't have + # defaults. + try: + parameters = signature(function).parameters.values() + except (ValueError, TypeError) as e: + from _pytest.outcomes import fail + + fail( + f"Could not determine arguments of {function!r}: {e}", + pytrace=False, + ) + + arg_names = tuple( + p.name + for p in parameters + if ( + p.kind is Parameter.POSITIONAL_OR_KEYWORD + or p.kind is Parameter.KEYWORD_ONLY + ) + and p.default is Parameter.empty + ) + if not name: + name = function.__name__ + + # If this function should be treated as a bound method even though + # it's passed as an unbound method or function, and its first parameter + # wasn't defined as positional only, remove the first parameter name. + if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and ( + # Not using `getattr` because we don't want to resolve the staticmethod. + # Not using `cls.__dict__` because we want to check the entire MRO. + cls + and not isinstance( + inspect.getattr_static(cls, name, default=None), staticmethod + ) + ): + arg_names = arg_names[1:] + # Remove any names that will be replaced with mocks. + if hasattr(function, "__wrapped__"): + arg_names = arg_names[num_mock_patch_args(function) :] + return arg_names + + +def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]: + # Note: this code intentionally mirrors the code at the beginning of + # getfuncargnames, to get the arguments which were excluded from its result + # because they had default values. + return tuple( + p.name + for p in signature(function).parameters.values() + if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) + and p.default is not Parameter.empty + ) + + +_non_printable_ascii_translate_table = { + i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127) +} +_non_printable_ascii_translate_table.update( + {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"} +) + + +def ascii_escaped(val: bytes | str) -> str: + r"""If val is pure ASCII, return it as an str, otherwise, escape + bytes objects into a sequence of escaped bytes: + + b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6' + + and escapes strings into a sequence of escaped unicode ids, e.g.: + + r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944' + + Note: + The obvious "v.decode('unicode-escape')" will return + valid UTF-8 unicode if it finds them in bytes, but we + want to return escaped bytes for any byte, even if they match + a UTF-8 string. + """ + if isinstance(val, bytes): + ret = val.decode("ascii", "backslashreplace") + else: + ret = val.encode("unicode_escape").decode("ascii") + return ret.translate(_non_printable_ascii_translate_table) + + +def get_real_func(obj): + """Get the real function object of the (possibly) wrapped object by + :func:`functools.wraps`, or :func:`functools.partial`.""" + obj = inspect.unwrap(obj) + + if isinstance(obj, functools.partial): + obj = obj.func + return obj + + +def getimfunc(func): + try: + return func.__func__ + except AttributeError: + return func + + +def safe_getattr(object: Any, name: str, default: Any) -> Any: + """Like getattr but return default upon any Exception or any OutcomeException. + + Attribute access can potentially fail for 'evil' Python objects. + See issue #214. + It catches OutcomeException because of #2490 (issue #580), new outcomes + are derived from BaseException instead of Exception (for more details + check #2707). + """ + from _pytest.outcomes import TEST_OUTCOME + + try: + return getattr(object, name, default) + except TEST_OUTCOME: + return default + + +def safe_isclass(obj: object) -> bool: + """Ignore any exception via isinstance on Python 3.""" + try: + return inspect.isclass(obj) + except Exception: + return False + + +def get_user_id() -> int | None: + """Return the current process's real user id or None if it could not be + determined. + + :return: The user id or None if it could not be determined. + """ + # mypy follows the version and platform checking expectation of PEP 484: + # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks + # Containment checks are too complex for mypy v1.5.0 and cause failure. + if sys.platform == "win32" or sys.platform == "emscripten": + # win32 does not have a getuid() function. + # Emscripten has a return 0 stub. + return None + else: + # On other platforms, a return value of -1 is assumed to indicate that + # the current process's real user id could not be determined. + ERROR = -1 + uid = os.getuid() + return uid if uid != ERROR else None + + +if sys.version_info >= (3, 11): + from typing import assert_never +else: + + def assert_never(value: NoReturn) -> NoReturn: + assert False, f"Unhandled value: {value} ({type(value).__name__})" + + +class CallableBool: + """ + A bool-like object that can also be called, returning its true/false value. + + Used for backwards compatibility in cases where something was supposed to be a method + but was implemented as a simple attribute by mistake (see `TerminalReporter.isatty`). + + Do not use in new code. + """ + + def __init__(self, value: bool) -> None: + self._value = value + + def __bool__(self) -> bool: + return self._value + + def __call__(self) -> bool: + return self._value + + +def running_on_ci() -> bool: + """Check if we're currently running on a CI system.""" + # Only enable CI mode if one of these env variables is defined and non-empty. + # Note: review `regendoc` tox env in case this list is changed. + env_vars = ["CI", "BUILD_NUMBER"] + return any(os.environ.get(var) for var in env_vars) + + +if sys.version_info >= (3, 13): + from warnings import deprecated as deprecated +else: + if TYPE_CHECKING: + from typing_extensions import deprecated as deprecated + else: + + def deprecated(msg, /, *, category=None, stacklevel=1): + def decorator(func): + return func + + return decorator diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/config/__init__.py b/.venv-test/lib/python3.12/site-packages/_pytest/config/__init__.py new file mode 100644 index 0000000..96b3dd6 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/config/__init__.py @@ -0,0 +1,2246 @@ +# mypy: allow-untyped-defs +"""Command line options, config-file and conftest.py processing.""" + +from __future__ import annotations + +import argparse +import builtins +import collections.abc +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import MutableMapping +from collections.abc import Sequence +import contextlib +import copy +import dataclasses +import enum +from functools import lru_cache +import glob +import importlib +import importlib.metadata +import inspect +import os +import pathlib +import re +import shlex +import sys +from textwrap import dedent +import types +from types import FunctionType +from typing import Any +from typing import cast +from typing import Final +from typing import final +from typing import IO +from typing import TextIO +from typing import TYPE_CHECKING +import warnings + +from pluggy import HookimplMarker +from pluggy import HookimplOpts +from pluggy import HookspecMarker +from pluggy import HookspecOpts +from pluggy import PluginManager + +from .exceptions import PrintHelp as PrintHelp +from .exceptions import UsageError as UsageError +from .findpaths import ConfigDict +from .findpaths import ConfigValue +from .findpaths import determine_setup +from .findpaths import parse_override_ini +from _pytest import __version__ +import _pytest._code +from _pytest._code import ExceptionInfo +from _pytest._code import filter_traceback +from _pytest._code.code import TracebackStyle +from _pytest._io import TerminalWriter +from _pytest.compat import assert_never +from _pytest.compat import deprecated +from _pytest.compat import NOTSET +from _pytest.config.argparsing import Argument +from _pytest.config.argparsing import FILE_OR_DIR +from _pytest.config.argparsing import Parser +import _pytest.deprecated +import _pytest.hookspec +from _pytest.outcomes import fail +from _pytest.outcomes import Skipped +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import import_path +from _pytest.pathlib import ImportMode +from _pytest.pathlib import resolve_package_path +from _pytest.pathlib import safe_exists +from _pytest.stash import Stash +from _pytest.warning_types import PytestConfigWarning +from _pytest.warning_types import warn_explicit_for + + +if TYPE_CHECKING: + from _pytest.assertion.rewrite import AssertionRewritingHook + from _pytest.cacheprovider import Cache + from _pytest.terminal import TerminalReporter + +_PluggyPlugin = object +"""A type to represent plugin objects. + +Plugins can be any namespace, so we can't narrow it down much, but we use an +alias to make the intent clear. + +Ideally this type would be provided by pluggy itself. +""" + + +hookimpl = HookimplMarker("pytest") +hookspec = HookspecMarker("pytest") + + +@final +class ExitCode(enum.IntEnum): + """Encodes the valid exit codes by pytest. + + Currently users and plugins may supply other exit codes as well. + + .. versionadded:: 5.0 + """ + + #: Tests passed. + OK = 0 + #: Tests failed. + TESTS_FAILED = 1 + #: pytest was interrupted. + INTERRUPTED = 2 + #: An internal error got in the way. + INTERNAL_ERROR = 3 + #: pytest was misused. + USAGE_ERROR = 4 + #: pytest couldn't find tests. + NO_TESTS_COLLECTED = 5 + #: All tests pass, but maximum number of warnings exceeded. + MAX_WARNINGS_ERROR = 6 + + __module__ = "pytest" + + +class ConftestImportFailure(Exception): + def __init__( + self, + path: pathlib.Path, + *, + cause: Exception, + ) -> None: + self.path = path + self.cause = cause + + def __str__(self) -> str: + return f"{type(self.cause).__name__}: {self.cause} (from {self.path})" + + +def filter_traceback_for_conftest_import_failure( + entry: _pytest._code.TracebackEntry, +) -> bool: + """Filter tracebacks entries which point to pytest internals or importlib. + + Make a special case for importlib because we use it to import test modules and conftest files + in _pytest.pathlib.import_path. + """ + return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep) + + +def print_conftest_import_error(e: ConftestImportFailure, file: TextIO) -> None: + exc_info = ExceptionInfo.from_exception(e.cause) + tw = TerminalWriter(file) + tw.line(f"ImportError while loading conftest '{e.path}'.", red=True) + exc_info.traceback = exc_info.traceback.filter( + filter_traceback_for_conftest_import_failure + ) + exc_repr = ( + exc_info.getrepr(style="short", chain=False) + if exc_info.traceback + else exc_info.exconly() + ) + formatted_tb = str(exc_repr) + for line in formatted_tb.splitlines(): + tw.line(line.rstrip(), red=True) + + +def print_usage_error(e: UsageError, file: TextIO) -> None: + tw = TerminalWriter(file) + for msg in e.args: + tw.line(f"ERROR: {msg}\n", red=True) + + +def _get_prog_name(argv: Sequence[str]) -> str: + """Determine the CLI program name from the argument vector. + + :param argv: The argument vector (typically ``sys.argv``). + :returns: ``"python -m pytest"`` when invoked via ``python -m``, + ``"pytest"`` otherwise. + """ + argv0 = argv[0] if argv else "" + if os.path.basename(argv0) == "__main__.py": + return "python -m pytest" + return "pytest" + + +def main( + args: list[str] | os.PathLike[str] | None = None, + plugins: Sequence[str | _PluggyPlugin] | None = None, +) -> int | ExitCode: + """Perform an in-process test run. + + :param args: + List of command line arguments. If `None` or not given, defaults to reading + arguments directly from the process command line (:data:`sys.argv`). + :param plugins: List of plugin objects to be auto-registered during initialization. + + :returns: An exit code. + """ + return _main(args=args, plugins=plugins, prog="pytest.main()") + + +def _main( + *, + args: list[str] | os.PathLike[str] | None = None, + plugins: Sequence[str | _PluggyPlugin] | None = None, + prog: str, +) -> int | ExitCode: + # Handle a single `--version`/`-V` argument early to avoid starting up the entire pytest infrastructure. + new_args = sys.argv[1:] if args is None else args + if ( + isinstance(new_args, Sequence) + and (new_args.count("--version") + new_args.count("-V")) == 1 + ): + sys.stdout.write(f"pytest {__version__}\n") + return ExitCode.OK + + old_pytest_version = os.environ.get("PYTEST_VERSION") + try: + os.environ["PYTEST_VERSION"] = __version__ + try: + config = _prepareconfig(new_args, plugins, prog=prog) + except ConftestImportFailure as e: + print_conftest_import_error(e, file=sys.stderr) + return ExitCode.USAGE_ERROR + + try: + ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config) + try: + return ExitCode(ret) + except ValueError: + return ret + finally: + config._ensure_unconfigure() + except UsageError as e: + print_usage_error(e, file=sys.stderr) + return ExitCode.USAGE_ERROR + finally: + if old_pytest_version is None: + os.environ.pop("PYTEST_VERSION", None) + else: + os.environ["PYTEST_VERSION"] = old_pytest_version + + +def _console_main() -> int: + """The CLI entry point of pytest (internal). + + This is the real implementation used by entry points and ``__main__.py``. + """ + # https://docs.python.org/3/library/signal.html#note-on-sigpipe + try: + code = _main(prog=_get_prog_name(sys.argv)) + sys.stdout.flush() + return code + except BrokenPipeError: + # Python flushes standard streams on exit; redirect remaining output + # to devnull to avoid another BrokenPipeError at shutdown + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, sys.stdout.fileno()) + return 1 # Python exits with error code 1 on EPIPE + + +def console_main() -> int: + """The CLI entry point of pytest. + + .. deprecated:: 9.1 + This function is slated for removal in pytest 10. + It is not meant for programmable use; use :func:`pytest.main` instead. + """ + import warnings + + from _pytest.deprecated import CONSOLE_MAIN + + warnings.warn(CONSOLE_MAIN, stacklevel=2) + return _console_main() + + +class cmdline: # compatibility namespace + main = staticmethod(main) + + +def filename_arg(path: str, optname: str) -> str: + """Argparse type validator for filename arguments. + + :path: Path of filename. + :optname: Name of the option. + """ + if os.path.isdir(path): + raise UsageError(f"{optname} must be a filename, given: {path}") + return path + + +def directory_arg(path: str, optname: str) -> str: + """Argparse type validator for directory arguments. + + :path: Path of directory. + :optname: Name of the option. + """ + if not os.path.isdir(path): + raise UsageError(f"{optname} must be a directory, given: {path}") + return path + + +# Plugins that cannot be disabled via "-p no:X" currently. +essential_plugins = ( + "mark", + "main", + "runner", + "fixtures", + "helpconfig", # Provides -p. +) + +default_plugins = ( + *essential_plugins, + "python", + "terminal", + "debugging", + "unittest", + "capture", + "skipping", + "legacypath", + "tmpdir", + "monkeypatch", + "recwarn", + "pastebin", + "assertion", + "junitxml", + "doctest", + "cacheprovider", + "setuponly", + "setupplan", + "stepwise", + "unraisableexception", + "threadexception", + "warnings", + "logging", + "reports", + "faulthandler", + "subtests", +) + +builtin_plugins = { + *default_plugins, + "pytester", + "pytester_assertions", + "terminalprogress", +} + + +def get_config( + args: Iterable[str] | None = None, + plugins: Sequence[str | _PluggyPlugin] | None = None, + *, + prog: str | None = None, +) -> Config: + # Subsequent calls to main will create a fresh instance. + pluginmanager = PytestPluginManager() + invocation_params = Config.InvocationParams( + args=args or (), + plugins=plugins, + dir=pathlib.Path.cwd(), + ) + config = Config(pluginmanager, invocation_params=invocation_params, prog=prog) + + if invocation_params.args: + # Handle any "-p no:plugin" args. + pluginmanager.consider_preparse(invocation_params.args, exclude_only=True) + + for spec in default_plugins: + pluginmanager.import_plugin(spec) + + return config + + +def get_plugin_manager() -> PytestPluginManager: + """Obtain a new instance of the + :py:class:`pytest.PytestPluginManager`, with default plugins + already loaded. + + This function can be used by integration with other tools, like hooking + into pytest to run tests into an IDE. + """ + return get_config().pluginmanager + + +def _prepareconfig( + args: list[str] | os.PathLike[str], + plugins: Sequence[str | _PluggyPlugin] | None = None, + *, + prog: str | None = None, +) -> Config: + if isinstance(args, os.PathLike): + args = [os.fspath(args)] + elif not isinstance(args, list): + msg = ( # type:ignore[unreachable] + "`args` parameter expected to be a list of strings, got: {!r} (type: {})" + ) + raise TypeError(msg.format(args, type(args))) + + initial_config = get_config(args, plugins, prog=prog) + pluginmanager = initial_config.pluginmanager + try: + if plugins: + for plugin in plugins: + if isinstance(plugin, str): + pluginmanager.consider_pluginarg(plugin) + else: + pluginmanager.register(plugin) + config: Config = pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args + ) + return config + except BaseException: + initial_config._ensure_unconfigure() + raise + + +def _get_directory(path: pathlib.Path) -> pathlib.Path: + """Get the directory of a path - itself if already a directory.""" + if path.is_file(): + return path.parent + else: + return path + + +def _get_legacy_hook_marks( + method: Any, + hook_type: str, + opt_names: tuple[str, ...], +) -> dict[str, bool]: + if TYPE_CHECKING: + # abuse typeguard from importlib to avoid massive method type union that's lacking an alias + assert inspect.isroutine(method) + known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])} + must_warn: list[str] = [] + opts: dict[str, bool] = {} + for opt_name in opt_names: + opt_attr = getattr(method, opt_name, AttributeError) + if opt_attr is not AttributeError: + must_warn.append(f"{opt_name}={opt_attr}") + opts[opt_name] = True + elif opt_name in known_marks: + must_warn.append(f"{opt_name}=True") + opts[opt_name] = True + else: + opts[opt_name] = False + if must_warn: + hook_opts = ", ".join(must_warn) + message = _pytest.deprecated.HOOK_LEGACY_MARKING.format( + type=hook_type, + fullname=method.__qualname__, + hook_opts=hook_opts, + ) + warn_explicit_for(cast(FunctionType, method), message) + return opts + + +@final +class PytestPluginManager(PluginManager): + """A :py:class:`pluggy.PluginManager ` with + additional pytest-specific functionality: + + * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and + ``pytest_plugins`` global variables found in plugins being loaded. + * ``conftest.py`` loading during start-up. + """ + + def __init__(self) -> None: + from _pytest.assertion import DummyRewriteHook + from _pytest.assertion import RewriteHook + + super().__init__("pytest") + + # -- State related to local conftest plugins. + # All loaded conftest modules. + self._conftest_plugins: set[types.ModuleType] = set() + # All conftest modules applicable for a directory. + # This includes the directory's own conftest modules as well + # as those of its parent directories. + self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {} + # Cutoff directory above which conftests are no longer discovered. + self._confcutdir: pathlib.Path | None = None + # If set, conftest loading is skipped. + self._noconftest = False + + # _getconftestmodules()'s call to _get_directory() causes a stat + # storm when it's called potentially thousands of times in a test + # session (#9478), often with the same path, so cache it. + self._get_directory = lru_cache(256)(_get_directory) + + # plugins that were explicitly skipped with pytest.skip + # list of (module name, skip reason) + # previously we would issue a warning when a plugin was skipped, but + # since we refactored warnings as first citizens of Config, they are + # just stored here to be used later. + self.skipped_plugins: list[tuple[str, str]] = [] + + self.add_hookspecs(_pytest.hookspec) + self.register(self) + if os.environ.get("PYTEST_DEBUG"): + err: IO[str] = sys.stderr + encoding: str = getattr(err, "encoding", "utf8") + try: + err = open( + os.dup(err.fileno()), + mode=err.mode, + buffering=1, + encoding=encoding, + ) + except Exception: + pass + self.trace.root.setwriter(err.write) + self.enable_tracing() + + # Config._consider_importhook will set a real object if required. + self.rewrite_hook: RewriteHook = DummyRewriteHook() + # Used to know when we are importing conftests after the pytest_configure stage. + self._configured = False + + def parse_hookimpl_opts( + self, plugin: _PluggyPlugin, name: str + ) -> HookimplOpts | None: + """:meta private:""" + # pytest hooks are always prefixed with "pytest_", + # so we avoid accessing possibly non-readable attributes + # (see issue #1073). + if not name.startswith("pytest_"): + return None + # Ignore names which cannot be hooks. + if name == "pytest_plugins": + return None + + opts = super().parse_hookimpl_opts(plugin, name) + if opts is not None: + return opts + + method = getattr(plugin, name) + # Consider only actual functions for hooks (#3775). + if not inspect.isroutine(method): + return None + # Collect unmarked hooks as long as they have the `pytest_' prefix. + legacy = _get_legacy_hook_marks( + method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper") + ) + return cast(HookimplOpts, legacy) + + def parse_hookspec_opts(self, module_or_class, name: str) -> HookspecOpts | None: + """:meta private:""" + opts = super().parse_hookspec_opts(module_or_class, name) + if opts is None: + method = getattr(module_or_class, name) + if name.startswith("pytest_"): + legacy = _get_legacy_hook_marks( + method, "spec", ("firstresult", "historic") + ) + opts = cast(HookspecOpts, legacy) + return opts + + def register(self, plugin: _PluggyPlugin, name: str | None = None) -> str | None: + if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: + warnings.warn( + PytestConfigWarning( + "{} plugin has been merged into the core, " + "please remove it from your requirements.".format( + name.replace("_", "-") + ) + ) + ) + return None + plugin_name = super().register(plugin, name) + if plugin_name is not None: + self.hook.pytest_plugin_registered.call_historic( + kwargs=dict( + plugin=plugin, + plugin_name=plugin_name, + manager=self, + ) + ) + + if isinstance(plugin, types.ModuleType): + self.consider_module(plugin) + return plugin_name + + def getplugin(self, name: str): + # Support deprecated naming because plugins (xdist e.g.) use it. + plugin: _PluggyPlugin | None = self.get_plugin(name) + return plugin + + def hasplugin(self, name: str) -> bool: + """Return whether a plugin with the given name is registered.""" + return bool(self.get_plugin(name)) + + def pytest_configure(self, config: Config) -> None: + """:meta private:""" + # XXX now that the pluginmanager exposes hookimpl(tryfirst...) + # we should remove tryfirst/trylast as markers. + config.addinivalue_line( + "markers", + "tryfirst: mark a hook implementation function such that the " + "plugin machinery will try to call it first/as early as possible. " + "DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.", + ) + config.addinivalue_line( + "markers", + "trylast: mark a hook implementation function such that the " + "plugin machinery will try to call it last/as late as possible. " + "DEPRECATED, use @pytest.hookimpl(trylast=True) instead.", + ) + self._configured = True + + # + # Internal API for local conftest plugin handling. + # + def _set_initial_conftests( + self, + args: Sequence[str | pathlib.Path], + pyargs: bool, + noconftest: bool, + rootpath: pathlib.Path, + confcutdir: pathlib.Path | None, + invocation_dir: pathlib.Path, + importmode: ImportMode | str, + *, + consider_namespace_packages: bool, + ) -> None: + """Load initial conftest files given a preparsed "namespace". + + As conftest files may add their own command line options which have + arguments ('--my-opt somepath') we might get some false positives. + All builtin and 3rd party plugins will have been loaded, however, so + common options will not confuse our logic here. + """ + self._confcutdir = ( + absolutepath(invocation_dir / confcutdir) if confcutdir else None + ) + self._noconftest = noconftest + self._using_pyargs = pyargs + + anchors = [] + for initial_path in args: + path = str(initial_path) + # remove node-id syntax + i = path.find("::") + if i != -1: + path = path[:i] + anchor = absolutepath(invocation_dir / path) + # Ensure we do not break if what appears to be an anchor + # is in fact a very long option (#10169, #11394). + if not safe_exists(anchor): + continue + + anchors.append(anchor) + # Let's also consider test* subdirs. + if anchor.is_dir(): + anchors.extend(x for x in anchor.glob("test*") if x.is_dir()) + if not anchors: + anchors.append(invocation_dir) + anchors.extend(x for x in invocation_dir.glob("test*") if x.is_dir()) + + for anchor in anchors: + self._loadconftestmodules( + anchor, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + + def _is_in_confcutdir(self, path: pathlib.Path) -> bool: + """Whether to consider the given path to load conftests from.""" + if self._confcutdir is None: + return True + # The semantics here are literally: + # Do not load a conftest if it is found upwards from confcut dir. + # But this is *not* the same as: + # Load only conftests from confcutdir or below. + # At first glance they might seem the same thing, however we do support use cases where + # we want to load conftests that are not found in confcutdir or below, but are found + # in completely different directory hierarchies like packages installed + # in out-of-source trees. + # (see #9767 for a regression where the logic was inverted). + return path not in self._confcutdir.parents + + def _loadconftestmodules( + self, + path: pathlib.Path, + importmode: str | ImportMode, + rootpath: pathlib.Path, + *, + consider_namespace_packages: bool, + ) -> None: + if self._noconftest: + return + + directory = self._get_directory(path) + + # Optimization: avoid repeated searches in the same directory. + # Assumes always called with same importmode and rootpath. + if directory in self._dirpath2confmods: + return + + clist = [] + for parent in reversed((directory, *directory.parents)): + if self._is_in_confcutdir(parent): + conftestpath = parent / "conftest.py" + if conftestpath.is_file(): + mod = self._importconftest( + conftestpath, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + clist.append(mod) + self._dirpath2confmods[directory] = clist + + def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]: + directory = self._get_directory(path) + return self._dirpath2confmods.get(directory, ()) + + def _rget_with_confmod( + self, + name: str, + path: pathlib.Path, + ) -> tuple[types.ModuleType, Any]: + modules = self._getconftestmodules(path) + for mod in reversed(modules): + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + + def _importconftest( + self, + conftestpath: pathlib.Path, + importmode: str | ImportMode, + rootpath: pathlib.Path, + *, + consider_namespace_packages: bool, + ) -> types.ModuleType: + conftestpath_plugin_name = str(conftestpath) + existing = self.get_plugin(conftestpath_plugin_name) + if existing is not None: + return cast(types.ModuleType, existing) + + # conftest.py files there are not in a Python package all have module + # name "conftest", and thus conflict with each other. Clear the existing + # before loading the new one, otherwise the existing one will be + # returned from the module cache. + pkgpath = resolve_package_path(conftestpath) + if pkgpath is None: + try: + del sys.modules[conftestpath.stem] + except KeyError: + pass + + try: + mod = import_path( + conftestpath, + mode=importmode, + root=rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + except Exception as e: + assert e.__traceback__ is not None + raise ConftestImportFailure(conftestpath, cause=e) from e + + self._check_non_top_pytest_plugins(mod, conftestpath) + + self._conftest_plugins.add(mod) + dirpath = conftestpath.parent + if dirpath in self._dirpath2confmods: + for path, mods in self._dirpath2confmods.items(): + if dirpath in path.parents or path == dirpath: + if mod in mods: + raise AssertionError( + f"While trying to load conftest path {conftestpath!s}, " + f"found that the module {mod} is already loaded with path {mod.__file__}. " + "This is not supposed to happen. Please report this issue to pytest." + ) + mods.append(mod) + self.trace(f"loading conftestmodule {mod!r}") + self.consider_conftest(mod, registration_name=conftestpath_plugin_name) + return mod + + def _check_non_top_pytest_plugins( + self, + mod: types.ModuleType, + conftestpath: pathlib.Path, + ) -> None: + if ( + hasattr(mod, "pytest_plugins") + and self._configured + and not self._using_pyargs + ): + msg = ( + "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n" + "It affects the entire test suite instead of just below the conftest as expected.\n" + " {}\n" + "Please move it to a top level conftest file at the rootdir:\n" + " {}\n" + "For more information, visit:\n" + " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" + ) + fail(msg.format(conftestpath, self._confcutdir), pytrace=False) + + # + # API for bootstrapping plugin loading + # + # + + def consider_preparse( + self, args: Sequence[str], *, exclude_only: bool = False + ) -> None: + """:meta private:""" + i = 0 + n = len(args) + while i < n: + opt = args[i] + i += 1 + if isinstance(opt, str): + if opt == "-p": + try: + parg = args[i] + except IndexError: + return + i += 1 + elif opt.startswith("-p"): + parg = opt[2:] + else: + continue + parg = parg.strip() + if exclude_only and not parg.startswith("no:"): + continue + self.consider_pluginarg(parg) + + def consider_pluginarg(self, arg: str) -> None: + """:meta private:""" + if arg.startswith("no:"): + name = arg[3:] + if name in essential_plugins: + raise UsageError(f"plugin {name} cannot be disabled") + + if name.endswith("conftest.py"): + raise UsageError( + f"Blocking conftest files using -p is not supported: -p no:{name}\n" + "conftest.py files are not plugins and cannot be disabled via -p.\n" + ) + + # PR #4304: remove stepwise if cacheprovider is blocked. + if name == "cacheprovider": + self.set_blocked("stepwise") + self.set_blocked("pytest_stepwise") + + self.set_blocked(name) + if not name.startswith("pytest_"): + self.set_blocked("pytest_" + name) + else: + name = arg + # Unblock the plugin. + self.unblock(name) + if not name.startswith("pytest_"): + self.unblock("pytest_" + name) + self.import_plugin(arg, consider_entry_points=True) + + def consider_conftest( + self, conftestmodule: types.ModuleType, registration_name: str + ) -> None: + """:meta private:""" + self.register(conftestmodule, name=registration_name) + + def consider_env(self) -> None: + """:meta private:""" + self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) + + def consider_module(self, mod: types.ModuleType) -> None: + """:meta private:""" + self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) + + def _import_plugin_specs( + self, spec: None | types.ModuleType | str | Sequence[str] + ) -> None: + plugins = _get_plugin_specs_as_list(spec) + for import_spec in plugins: + self.import_plugin(import_spec) + + def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None: + """Import a plugin with ``modname``. + + If ``consider_entry_points`` is True, entry point names are also + considered to find a plugin. + """ + # Most often modname refers to builtin modules, e.g. "pytester", + # "terminal" or "capture". Those plugins are registered under their + # basename for historic purposes but must be imported with the + # _pytest prefix. + assert isinstance(modname, str), ( + f"module name as text required, got {modname!r}" + ) + if self.is_blocked(modname) or self.get_plugin(modname) is not None: + return + + importspec = "_pytest." + modname if modname in builtin_plugins else modname + self.rewrite_hook.mark_rewrite(importspec) + + if consider_entry_points: + loaded = self.load_setuptools_entrypoints("pytest11", name=modname) + if loaded: + return + + try: + if sys.version_info >= (3, 11): + mod = importlib.import_module(importspec) + else: + # On Python 3.10, import_module breaks + # testing/test_config.py::test_disable_plugin_autoload. + __import__(importspec) + mod = sys.modules[importspec] + except ImportError as e: + raise ImportError( + f'Error importing plugin "{modname}": {e.args[0]}' + ).with_traceback(e.__traceback__) from e + + except Skipped as e: + self.skipped_plugins.append((modname, e.msg or "")) + else: + self.register(mod, modname) + + +def _get_plugin_specs_as_list( + specs: None | types.ModuleType | str | Sequence[str], +) -> list[str]: + """Parse a plugins specification into a list of plugin names.""" + # None means empty. + if specs is None: + return [] + # Workaround for #3899 - a submodule which happens to be called "pytest_plugins". + if isinstance(specs, types.ModuleType): + return [] + # Comma-separated list. + if isinstance(specs, str): + return specs.split(",") if specs else [] + # Direct specification. + if isinstance(specs, collections.abc.Sequence): + return list(specs) + raise UsageError( + f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}" + ) + + +def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: + """Given an iterable of file names in a source distribution, return the "names" that should + be marked for assertion rewrite. + + For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in + the assertion rewrite mechanism. + + This function has to deal with dist-info based distributions and egg based distributions + (which are still very much in use for "editable" installs). + + Here are the file names as seen in a dist-info based distribution: + + pytest_mock/__init__.py + pytest_mock/_version.py + pytest_mock/plugin.py + pytest_mock.egg-info/PKG-INFO + + Here are the file names as seen in an egg based distribution: + + src/pytest_mock/__init__.py + src/pytest_mock/_version.py + src/pytest_mock/plugin.py + src/pytest_mock.egg-info/PKG-INFO + LICENSE + setup.py + + We have to take in account those two distribution flavors in order to determine which + names should be considered for assertion rewriting. + + More information: + https://github.com/pytest-dev/pytest-mock/issues/167 + """ + package_files = list(package_files) + seen_some = False + for fn in package_files: + is_simple_module = "/" not in fn and fn.endswith(".py") + is_package = fn.count("/") == 1 and fn.endswith("__init__.py") + if is_simple_module: + module_name, _ = os.path.splitext(fn) + # we ignore "setup.py" at the root of the distribution + # as well as editable installation finder modules made by setuptools + if module_name != "setup" and not module_name.startswith("__editable__"): + seen_some = True + yield module_name + elif is_package: + package_name = os.path.dirname(fn) + seen_some = True + yield package_name + + if not seen_some: + # At this point we did not find any packages or modules suitable for assertion + # rewriting, so we try again by stripping the first path component (to account for + # "src" based source trees for example). + # This approach lets us have the common case continue to be fast, as egg-distributions + # are rarer. + new_package_files = [] + for fn in package_files: + parts = fn.split("/") + new_fn = "/".join(parts[1:]) + if new_fn: + new_package_files.append(new_fn) + if new_package_files: + yield from _iter_rewritable_modules(new_package_files) + + +class _DeprecatedInicfgProxy(MutableMapping[str, Any]): + """Compatibility proxy for the deprecated Config.inicfg.""" + + __slots__ = ("_config",) + + def __init__(self, config: Config) -> None: + self._config = config + + def __getitem__(self, key: str) -> Any: + return self._config._inicfg[key].value + + def __setitem__(self, key: str, value: Any) -> None: + self._config._inicfg[key] = ConfigValue(value, origin="override", mode="toml") + + def __delitem__(self, key: str) -> None: + del self._config._inicfg[key] + + def __iter__(self) -> Iterator[str]: + return iter(self._config._inicfg) + + def __len__(self) -> int: + return len(self._config._inicfg) + + +@final +class Config: + """Access to configuration values, pluginmanager and plugin hooks. + + :param PytestPluginManager pluginmanager: + A pytest PluginManager. + + :param InvocationParams invocation_params: + Object containing parameters regarding the :func:`pytest.main` + invocation. + """ + + @final + @dataclasses.dataclass(frozen=True) + class InvocationParams: + """Holds parameters passed during :func:`pytest.main`. + + The object attributes are read-only. + + .. versionadded:: 5.1 + + .. note:: + + Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts`` + configuration option are handled by pytest, not being included in the ``args`` attribute. + + Plugins accessing ``InvocationParams`` must be aware of that. + """ + + args: tuple[str, ...] + """The command-line arguments as passed to :func:`pytest.main`.""" + plugins: Sequence[str | _PluggyPlugin] | None + """Extra plugins, might be `None`.""" + dir: pathlib.Path + """The directory from which :func:`pytest.main` was invoked.""" + + def __init__( + self, + *, + args: Iterable[str], + plugins: Sequence[str | _PluggyPlugin] | None, + dir: pathlib.Path, + ) -> None: + object.__setattr__(self, "args", tuple(args)) + object.__setattr__(self, "plugins", plugins) + object.__setattr__(self, "dir", dir) + + class ArgsSource(enum.Enum): + """Indicates the source of the test arguments. + + .. versionadded:: 7.2 + """ + + #: Command line arguments. + ARGS = enum.auto() + #: Invocation directory. + INVOCATION_DIR = enum.auto() + INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias + #: 'testpaths' configuration value. + TESTPATHS = enum.auto() + + # Set by cacheprovider plugin. + cache: Cache + + def __init__( + self, + pluginmanager: PytestPluginManager, + *, + invocation_params: InvocationParams | None = None, + prog: str | None = None, + ) -> None: + if invocation_params is None: + invocation_params = self.InvocationParams( + args=(), plugins=None, dir=pathlib.Path.cwd() + ) + + self.option = argparse.Namespace() + """Access to command line option as attributes. + + :type: argparse.Namespace + """ + + self.invocation_params = invocation_params + """The parameters with which pytest was invoked. + + :type: InvocationParams + """ + + self._parser = Parser( + usage=f"%(prog)s [options] [{FILE_OR_DIR}] [{FILE_OR_DIR}] [...]", + processopt=self._processopt, + prog=prog, + _ispytest=True, + ) + self.pluginmanager = pluginmanager + """The plugin manager handles plugin registration and hook invocation. + + :type: PytestPluginManager + """ + + self.stash = Stash() + """A place where plugins can store information on the config for their + own use. + + :type: Stash + """ + # Deprecated alias. Was never public. Can be removed in a few releases. + self._store = self.stash + + self.trace = self.pluginmanager.trace.root.get("config") + self.hook = self.pluginmanager.hook + self._inicache: dict[str, Any] = {} + self._inicfg: ConfigDict = {} + self._cleanup_stack = contextlib.ExitStack() + self.pluginmanager.register(self, "pytestconfig") + self._configured = False + self.hook.pytest_addoption.call_historic( + kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) + ) + self.args_source = Config.ArgsSource.ARGS + self.args: list[str] = [] + + if TYPE_CHECKING: + + @deprecated( + "config.inicfg is deprecated, use config.getini() to access configuration values instead.", + ) + @property + def inicfg(self) -> _DeprecatedInicfgProxy: + raise NotImplementedError() + else: + + @property + def inicfg(self) -> _DeprecatedInicfgProxy: + warnings.warn( + _pytest.deprecated.CONFIG_INICFG, + stacklevel=2, + ) + return _DeprecatedInicfgProxy(self) + + @property + def rootpath(self) -> pathlib.Path: + """The path to the :ref:`rootdir `. + + .. versionadded:: 6.1 + """ + return self._rootpath + + @property + def inipath(self) -> pathlib.Path | None: + """The path to the :ref:`configfile `. + + .. versionadded:: 6.1 + """ + return self._inipath + + def add_cleanup(self, func: Callable[[], None]) -> None: + """Add a function to be called when the config object gets out of + use (usually coinciding with pytest_unconfigure). + """ + self._cleanup_stack.callback(func) + + def _do_configure(self) -> None: + assert not self._configured + self._configured = True + self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) + + def _ensure_unconfigure(self) -> None: + try: + if self._configured: + self._configured = False + try: + self.hook.pytest_unconfigure(config=self) + finally: + self.hook.pytest_configure._call_history = [] + finally: + try: + self._cleanup_stack.close() + finally: + self._cleanup_stack = contextlib.ExitStack() + + def get_terminal_writer(self) -> TerminalWriter: + terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin( + "terminalreporter" + ) + assert terminalreporter is not None + return terminalreporter._tw + + def pytest_cmdline_parse( + self, pluginmanager: PytestPluginManager, args: list[str] + ) -> Config: + try: + self.parse(args) + except UsageError: + # Handle `--version --version` and `--help` here in a minimal fashion. + # This gets done via helpconfig normally, but its + # pytest_cmdline_main is not called in case of errors. + if getattr(self.option, "version", False) or "--version" in args: + from _pytest.helpconfig import show_version_verbose + + # Note that `--version` (single argument) is handled early by `Config.main()`, so the only + # way we are reaching this point is via `--version --version`. + show_version_verbose(self) + elif ( + getattr(self.option, "help", False) or "--help" in args or "-h" in args + ): + self._parser.optparser.print_help() + sys.stdout.write( + "\nNOTE: displaying only minimal help due to UsageError.\n\n" + ) + + raise + + return self + + def notify_exception( + self, + excinfo: ExceptionInfo[BaseException], + option: argparse.Namespace | None = None, + ) -> None: + if option and getattr(option, "fulltrace", False): + style: TracebackStyle = "long" + else: + style = "native" + excrepr = excinfo.getrepr( + funcargs=True, showlocals=getattr(option, "showlocals", False), style=style + ) + res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo) + if not any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write(f"INTERNALERROR> {line}\n") + sys.stderr.flush() + + def cwd_relative_nodeid(self, nodeid: str) -> str: + # nodeid's are relative to the rootpath, compute relative to cwd. + if self.invocation_params.dir != self.rootpath: + base_path_part, *nodeid_part = nodeid.split("::") + # Only process path part + fullpath = self.rootpath / base_path_part + relative_path = bestrelpath(self.invocation_params.dir, fullpath) + + nodeid = "::".join([relative_path, *nodeid_part]) + return nodeid + + @classmethod + def fromdictargs(cls, option_dict: Mapping[str, Any], args: list[str]) -> Config: + """Constructor usable for subprocesses.""" + config = get_config(args) + config.option.__dict__.update(option_dict) + config.parse(args, addopts=False) + for x in config.option.plugins: + config.pluginmanager.consider_pluginarg(x) + return config + + def _processopt(self, opt: Argument) -> None: + if not hasattr(self.option, opt.dest): + setattr(self.option, opt.dest, opt.default) + + @hookimpl(trylast=True) + def pytest_load_initial_conftests(self, early_config: Config) -> None: + # We haven't fully parsed the command line arguments yet, so + # early_config.args it not set yet. But we need it for + # discovering the initial conftests. So "pre-run" the logic here. + # It will be done for real in `parse()`. + args, _args_source = early_config._decide_args( + args=early_config.known_args_namespace.file_or_dir, + pyargs=early_config.known_args_namespace.pyargs, + testpaths=early_config.getini("testpaths"), + invocation_dir=early_config.invocation_params.dir, + rootpath=early_config.rootpath, + warn=False, + ) + self.pluginmanager._set_initial_conftests( + args=args, + pyargs=early_config.known_args_namespace.pyargs, + noconftest=early_config.known_args_namespace.noconftest, + rootpath=early_config.rootpath, + confcutdir=early_config.known_args_namespace.confcutdir, + invocation_dir=early_config.invocation_params.dir, + importmode=early_config.known_args_namespace.importmode, + consider_namespace_packages=early_config.getini( + "consider_namespace_packages" + ), + ) + + def _consider_importhook(self) -> None: + """Install the PEP 302 import hook if using assertion rewriting. + + Needs to parse the --assert= option from the commandline + and find all the installed plugins to mark them for rewriting + by the importhook. + """ + mode = getattr(self.known_args_namespace, "assertmode", "plain") + + disable_autoload = getattr( + self.known_args_namespace, "disable_plugin_autoload", False + ) or bool(os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")) + if mode == "rewrite": + import _pytest.assertion + + try: + hook = _pytest.assertion.install_importhook(self) + except SystemError: + mode = "plain" + else: + self._mark_plugins_for_rewrite(hook, disable_autoload) + self._warn_about_missing_assertion(mode) + + def _mark_plugins_for_rewrite( + self, hook: AssertionRewritingHook, disable_autoload: bool + ) -> None: + """Given an importhook, mark for rewrite any top-level + modules or packages in the distribution package for + all pytest plugins.""" + self.pluginmanager.rewrite_hook = hook + + if disable_autoload: + # We don't autoload from distribution package entry points, + # no need to continue. + return + + package_files = ( + str(file) + for dist in importlib.metadata.distributions() + if any(ep.group == "pytest11" for ep in dist.entry_points) + for file in dist.files or [] + ) + + for name in _iter_rewritable_modules(package_files): + hook.mark_rewrite(name) + + def _configure_python_path(self) -> None: + # `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]` + for path in reversed(self.getini("pythonpath")): + sys.path.insert(0, str(path)) + self.add_cleanup(self._unconfigure_python_path) + + def _unconfigure_python_path(self) -> None: + for path in self.getini("pythonpath"): + path_str = str(path) + if path_str in sys.path: + sys.path.remove(path_str) + + def _validate_args(self, args: list[str], via: str) -> list[str]: + """Validate known args.""" + self._parser.extra_info["config source"] = via + try: + self._parser.parse_known_and_unknown_args( + args, namespace=copy.copy(self.option) + ) + finally: + self._parser.extra_info.pop("config source", None) + + return args + + def _decide_args( + self, + *, + args: list[str], + pyargs: bool, + testpaths: list[str], + invocation_dir: pathlib.Path, + rootpath: pathlib.Path, + warn: bool, + ) -> tuple[list[str], ArgsSource]: + """Decide the args (initial paths/nodeids) to use given the relevant inputs. + + :param warn: Whether can issue warnings. + + :returns: The args and the args source. Guaranteed to be non-empty. + """ + if args: + source = Config.ArgsSource.ARGS + result = args + else: + if invocation_dir == rootpath: + source = Config.ArgsSource.TESTPATHS + if pyargs: + result = testpaths + else: + result = [] + for path in testpaths: + result.extend(sorted(glob.iglob(path, recursive=True))) + if testpaths and not result: + if warn: + warning_text = ( + "No files were found in testpaths; " + "consider removing or adjusting your testpaths configuration. " + "Searching recursively from the current directory instead." + ) + self.issue_config_time_warning( + PytestConfigWarning(warning_text), stacklevel=3 + ) + else: + result = [] + if not result: + source = Config.ArgsSource.INVOCATION_DIR + result = [str(invocation_dir)] + return result, source + + @hookimpl(wrapper=True) + def pytest_collection(self) -> Generator[None, object, object]: + # Validate invalid configuration keys after collection is done so we + # take in account options added by late-loading conftest files. + try: + return (yield) + finally: + self._validate_config_options() + + def _checkversion(self) -> None: + import pytest + + minver = self.getini("minversion") + if minver: + # Imported lazily to improve start-up time. + from packaging.version import Version + + if Version(minver) > Version(pytest.__version__): + raise pytest.UsageError( + f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'" + ) + + def _validate_config_options(self) -> None: + for key in sorted(self._get_unknown_ini_keys()): + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + + def _validate_plugins(self) -> None: + required_plugins = sorted(self.getini("required_plugins")) + if not required_plugins: + return + + # Imported lazily to improve start-up time. + from packaging.requirements import InvalidRequirement + from packaging.requirements import Requirement + from packaging.version import Version + + plugin_info = self.pluginmanager.list_plugin_distinfo() + plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} + + missing_plugins = [] + for required_plugin in required_plugins: + try: + req = Requirement(required_plugin) + except InvalidRequirement: + missing_plugins.append(required_plugin) + continue + + if req.name not in plugin_dist_info: + missing_plugins.append(required_plugin) + elif not req.specifier.contains( + Version(plugin_dist_info[req.name]), prereleases=True + ): + missing_plugins.append(required_plugin) + + if missing_plugins: + raise UsageError( + "Missing required plugins: {}".format(", ".join(missing_plugins)), + ) + + def _warn_or_fail_if_strict(self, message: str) -> None: + strict_config = self.getini("strict_config") + if strict_config is None: + strict_config = self.getini("strict") + if strict_config: + raise UsageError(message) + + self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3) + + def _get_unknown_ini_keys(self) -> set[str]: + known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys() + return self._inicfg.keys() - known_keys + + def parse(self, args: list[str], addopts: bool = True) -> None: + # Parse given cmdline arguments into this config object. + assert self.args == [], ( + "can only parse cmdline args at most once per Config object" + ) + + self.hook.pytest_addhooks.call_historic( + kwargs=dict(pluginmanager=self.pluginmanager) + ) + + if addopts: + env_addopts = os.environ.get("PYTEST_ADDOPTS", "") + if len(env_addopts): + args[:] = ( + self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS") + + args + ) + + # At this point, self.option contains only defaults from the _processopt + # callback. + ns = self._parser.parse_known_args(args, namespace=copy.copy(self.option)) + rootpath, inipath, inicfg, ignored_config_files = determine_setup( + inifile=ns.inifilename, + override_ini=ns.override_ini, + args=ns.file_or_dir, + rootdir_cmd_arg=ns.rootdir or None, + invocation_dir=self.invocation_params.dir, + ) + self._rootpath = rootpath + self._inipath = inipath + self._ignored_config_files = ignored_config_files + self._inicfg = inicfg + self._parser.extra_info["rootdir"] = str(self.rootpath) + self._parser.extra_info["inifile"] = str(self.inipath) + + self._parser.addini("addopts", "Extra command line options", "args") + self._parser.addini("minversion", "Minimally required pytest version") + self._parser.addini( + "pythonpath", type="paths", help="Add paths to sys.path", default=[] + ) + self._parser.addini( + "required_plugins", + "Plugins that must be present for pytest to run", + type="args", + default=[], + ) + + if addopts: + args[:] = ( + self._validate_args(self.getini("addopts"), "via addopts config") + args + ) + + self.known_args_namespace = self._parser.parse_known_args( + args, namespace=copy.copy(self.option) + ) + if addopts: + # addopts may have added overrides (especially via OverrideIniAction). + # The thing can be endlessly circular but we only do one level (#14442). + if overrides := parse_override_ini(self.known_args_namespace.override_ini): + self._inicfg.update(overrides) + self._inicache.clear() + self._checkversion() + self._consider_importhook() + self._configure_python_path() + self.pluginmanager.consider_preparse(args, exclude_only=False) + if ( + not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + and not self.known_args_namespace.disable_plugin_autoload + ): + # Autoloading from distribution package entry point has + # not been disabled. + self.pluginmanager.load_setuptools_entrypoints("pytest11") + # Otherwise only plugins explicitly specified in PYTEST_PLUGINS + # are going to be loaded. + self.pluginmanager.consider_env() + + # Parse again, now including options added in pytest_addoption + # by third-party plugins loaded above. This way they're available + # on early_config in the pytest_load_initial_conftests hook call below. + self.known_args_namespace = self._parser.parse_known_args( + args, namespace=copy.copy(self.option) + ) + + self._validate_plugins() + self._warn_about_skipped_plugins() + + if self.known_args_namespace.confcutdir is None: + if self.inipath is not None: + confcutdir = str(self.inipath.parent) + else: + confcutdir = str(self.rootpath) + self.known_args_namespace.confcutdir = confcutdir + try: + self.hook.pytest_load_initial_conftests( + early_config=self, args=args, parser=self._parser + ) + except ConftestImportFailure as e: + if self.known_args_namespace.help or self.known_args_namespace.version: + # we don't want to prevent --help/--version to work + # so just let it pass and print a warning at the end + self.issue_config_time_warning( + PytestConfigWarning(f"could not load initial conftests: {e.path}"), + stacklevel=2, + ) + else: + raise + + try: + self._parser.parse(args, namespace=self.option) + except PrintHelp: + return + + self.args, self.args_source = self._decide_args( + args=getattr(self.option, FILE_OR_DIR), + pyargs=self.option.pyargs, + testpaths=self.getini("testpaths"), + invocation_dir=self.invocation_params.dir, + rootpath=self.rootpath, + warn=True, + ) + + def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None: + """Issue and handle a warning during the "configure" stage. + + During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item`` + function because it is not possible to have hook wrappers around ``pytest_configure``. + + This function is mainly intended for plugins that need to issue warnings during + ``pytest_configure`` (or similar stages). + + :param warning: The warning instance. + :param stacklevel: stacklevel forwarded to warnings.warn. + """ + if self.pluginmanager.is_blocked("warnings"): + return + + cmdline_filters = self.known_args_namespace.pythonwarnings or [] + config_filters = self.getini("filterwarnings") + + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always", type(warning)) + apply_warning_filters(config_filters, cmdline_filters) + warnings.warn(warning, stacklevel=stacklevel) + + if records: + frame = sys._getframe(stacklevel - 1) + location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + self.hook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=records[0], + when="config", + nodeid="", + location=location, + ) + ) + + def addinivalue_line(self, name: str, line: str) -> None: + """Add a line to a configuration option. The option must have been + declared but might not yet be set in which case the line becomes + the first line in its value.""" + x = self.getini(name) + assert isinstance(x, list) + x.append(line) # modifies the cached list inline + + def getini(self, name: str) -> Any: + """Return configuration value the an :ref:`configuration file `. + + If a configuration value is not defined in a + :ref:`configuration file `, then the ``default`` value + provided while registering the configuration through + :func:`parser.addini ` will be returned. + Please note that you can even provide ``None`` as a valid + default value. + + If ``default`` is not provided while registering using + :func:`parser.addini `, then a default value + based on the ``type`` parameter passed to + :func:`parser.addini ` will be returned. + The default values based on ``type`` are: + ``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]`` + ``bool`` : ``False`` + ``string`` : empty string ``""`` + ``int`` : ``0`` + ``float`` : ``0.0`` + + If neither the ``default`` nor the ``type`` parameter is passed + while registering the configuration through + :func:`parser.addini `, then the configuration + is treated as a string and a default empty string '' is returned. + + If the specified name hasn't been registered through a prior + :func:`parser.addini ` call (usually from a + plugin), a ValueError is raised. + """ + canonical_name = self._parser._ini_aliases.get(name, name) + try: + return self._inicache[canonical_name] + except KeyError: + pass + self._inicache[canonical_name] = val = self._getini(canonical_name) + return val + + # Meant for easy monkeypatching by legacypath plugin. + # Can be inlined back (with no cover removed) once legacypath is gone. + def _getini_unknown_type(self, name: str, type: str, value: object): + msg = ( + f"Option {name} has unknown configuration type {type} with value {value!r}" + ) + raise ValueError(msg) # pragma: no cover + + def _getini(self, name: str): + # If this is an alias, resolve to canonical name. + canonical_name = self._parser._ini_aliases.get(name, name) + + try: + _description, type, default = self._parser._inidict[canonical_name] + except KeyError as e: + raise ValueError(f"unknown configuration value: {name!r}") from e + + # Collect all possible values (canonical name + aliases) from _inicfg. + # Each candidate is (ConfigValue, is_canonical). + candidates = [] + if canonical_name in self._inicfg: + candidates.append((self._inicfg[canonical_name], True)) + for alias, target in self._parser._ini_aliases.items(): + if target == canonical_name and alias in self._inicfg: + candidates.append((self._inicfg[alias], False)) + + if not candidates: + return default + + # Pick the best candidate based on precedence: + # 1. CLI override takes precedence over file, then + # 2. Canonical name takes precedence over alias. + selected = max(candidates, key=lambda x: (x[0].origin == "override", x[1]))[0] + value = selected.value + mode = selected.mode + + if mode == "ini": + # In ini mode, values are always str | list[str]. + assert isinstance(value, (str, list)) + return self._getini_ini(name, canonical_name, type, value, default) + elif mode == "toml": + return self._getini_toml(name, canonical_name, type, value, default) + else: + assert_never(mode) + + def _getini_ini( + self, + name: str, + canonical_name: str, + type: str, + value: str | list[str], + default: Any, + ): + """Handle config values read in INI mode. + + In INI mode, values are stored as str or list[str] only, and coerced + from string based on the registered type. + """ + # Note: some coercions are only required if we are reading from .ini + # files, because the file format doesn't contain type information, but + # when reading from toml (in ini mode) we will get either str or list of + # str values (see load_config_dict_from_file). For example: + # + # ini: + # a_line_list = "tests acceptance" + # + # in this case, we need to split the string to obtain a list of strings. + # + # toml (ini mode): + # a_line_list = ["tests", "acceptance"] + # + # in this case, we already have a list ready to use. + if type == "paths": + dp = ( + self.inipath.parent + if self.inipath is not None + else self.invocation_params.dir + ) + input_values = shlex.split(value) if isinstance(value, str) else value + return [dp / x for x in input_values] + elif type == "args": + return shlex.split(value) if isinstance(value, str) else value + elif type == "linelist": + if isinstance(value, str): + return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] + else: + return value + elif type == "bool": + return _strtobool(str(value).strip()) + elif type == "string": + return value + elif type == "int": + if not isinstance(value, str): + raise TypeError( + f"Expected an int string for option {name} of type integer, but got: {value!r}" + ) from None + return int(value) + elif type == "float": + if not isinstance(value, str): + raise TypeError( + f"Expected a float string for option {name} of type float, but got: {value!r}" + ) from None + return float(value) + else: + return self._getini_unknown_type(name, type, value) + + def _getini_toml( + self, + name: str, + canonical_name: str, + type: str, + value: object, + default: Any, + ): + """Handle TOML config values with strict type validation and no coercion. + + In TOML mode, values already have native types from TOML parsing. + We validate types match expectations exactly, including list items. + """ + value_type = builtins.type(value).__name__ + if type == "paths": + # Expect a list of strings. + if not isinstance(value, list): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list for type 'paths', " + f"got {value_type}: {value!r}" + ) + for i, item in enumerate(value): + if not isinstance(item, str): + item_type = builtins.type(item).__name__ + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list of strings, " + f"but item at index {i} is {item_type}: {item!r}" + ) + dp = ( + self.inipath.parent + if self.inipath is not None + else self.invocation_params.dir + ) + return [dp / x for x in value] + elif type in {"args", "linelist"}: + # Expect a list of strings. + if not isinstance(value, list): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list for type '{type}', " + f"got {value_type}: {value!r}" + ) + for i, item in enumerate(value): + if not isinstance(item, str): + item_type = builtins.type(item).__name__ + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list of strings, " + f"but item at index {i} is {item_type}: {item!r}" + ) + return list(value) + elif type == "bool": + # Expect a boolean. + if not isinstance(value, bool): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a bool, " + f"got {value_type}: {value!r}" + ) + return value + elif type == "int": + # Expect an integer (but not bool, which is a subclass of int). + if not isinstance(value, int) or isinstance(value, bool): + raise TypeError( + f"{self.inipath}: config option '{name}' expects an int, " + f"got {value_type}: {value!r}" + ) + return value + elif type == "float": + # Expect a float or integer only. + if not isinstance(value, (float, int)) or isinstance(value, bool): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a float, " + f"got {value_type}: {value!r}" + ) + return value + elif type == "string": + # Expect a string. + if not isinstance(value, str): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a string, " + f"got {value_type}: {value!r}" + ) + return value + else: + return self._getini_unknown_type(name, type, value) + + def _getconftest_pathlist( + self, name: str, path: pathlib.Path + ) -> list[pathlib.Path] | None: + try: + mod, relroots = self.pluginmanager._rget_with_confmod(name, path) + except KeyError: + return None + assert mod.__file__ is not None + modpath = pathlib.Path(mod.__file__).parent + values: list[pathlib.Path] = [] + for relroot in relroots: + if isinstance(relroot, os.PathLike): + relroot = pathlib.Path(relroot) + else: + relroot = relroot.replace("/", os.sep) + relroot = absolutepath(modpath / relroot) + values.append(relroot) + return values + + def getoption(self, name: str, default: Any = NOTSET, skip: bool = False): + """Return command line option value. + + :param name: Name of the option. You may also specify + the literal ``--OPT`` option instead of the "dest" option name. + :param default: Fallback value if no option of that name is **declared** via :hook:`pytest_addoption`. + Note this parameter will be ignored when the option is **declared** even if the option's value is ``None``. + :param skip: If ``True``, raise :func:`pytest.skip` if option is undeclared or has a ``None`` value. + Note that even if ``True``, if a default was specified it will be returned instead of a skip. + """ + name = self._parser._opt2dest.get(name, name) + try: + val = getattr(self.option, name) + if val is None and skip: + raise AttributeError(name) + return val + except AttributeError as e: + if default is not NOTSET: + return default + if skip: + import pytest + + pytest.skip(f"no {name!r} option found") + raise ValueError(f"no option named {name!r}") from e + + def getvalue(self, name: str, path=None): + """Deprecated, use getoption() instead.""" + return self.getoption(name) + + def getvalueorskip(self, name: str, path=None): + """Deprecated, use getoption(skip=True) instead.""" + return self.getoption(name, skip=True) + + #: Verbosity type for failed assertions (see :confval:`verbosity_assertions`). + VERBOSITY_ASSERTIONS: Final = "assertions" + #: Verbosity type for test case execution (see :confval:`verbosity_test_cases`). + VERBOSITY_TEST_CASES: Final = "test_cases" + #: Verbosity type for failed subtests (see :confval:`verbosity_subtests`). + VERBOSITY_SUBTESTS: Final = "subtests" + + _VERBOSITY_INI_DEFAULT: Final = "auto" + + def get_verbosity(self, verbosity_type: str | None = None) -> int: + r"""Retrieve the verbosity level for a fine-grained verbosity type. + + :param verbosity_type: Verbosity type to get level for. If a level is + configured for the given type, that value will be returned. If the + given type is not a known verbosity type, the global verbosity + level will be returned. If the given type is None (default), the + global verbosity level will be returned. + + To configure a level for a fine-grained verbosity type, the + configuration file should have a setting for the configuration name + and a numeric value for the verbosity level. A special value of "auto" + can be used to explicitly use the global verbosity level. + + Example: + + .. tab:: toml + + .. code-block:: toml + + [tool.pytest] + verbosity_assertions = 2 + + .. tab:: ini + + .. code-block:: ini + + [pytest] + verbosity_assertions = 2 + + .. code-block:: console + + pytest -v + + .. code-block:: python + + print(config.get_verbosity()) # 1 + print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2 + """ + global_level = self.getoption("verbose", default=0) + assert isinstance(global_level, int) + if verbosity_type is None: + return global_level + + ini_name = Config._verbosity_ini_name(verbosity_type) + if ini_name not in self._parser._inidict: + return global_level + + level = self.getini(ini_name) + if level == Config._VERBOSITY_INI_DEFAULT: + return global_level + + return int(level) + + @staticmethod + def _verbosity_ini_name(verbosity_type: str) -> str: + return f"verbosity_{verbosity_type}" + + @staticmethod + def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None: + """Add a output verbosity configuration option for the given output type. + + :param parser: Parser for command line arguments and config-file values. + :param verbosity_type: Fine-grained verbosity category. + :param help: Description of the output this type controls. + + The value should be retrieved via a call to + :py:func:`config.get_verbosity(type) `. + """ + parser.addini( + Config._verbosity_ini_name(verbosity_type), + help=help, + type="string", + default=Config._VERBOSITY_INI_DEFAULT, + ) + + def _warn_about_missing_assertion(self, mode: str) -> None: + if not _assertion_supported(): + if mode == "plain": + warning_text = ( + "ASSERTIONS ARE NOT EXECUTED" + " and FAILING TESTS WILL PASS. Are you" + " using python -O?" + ) + else: + warning_text = ( + "assertions not in test modules or" + " plugins will be ignored" + " because assert statements are not executed " + "by the underlying Python interpreter " + "(are you using python -O?)\n" + ) + self.issue_config_time_warning( + PytestConfigWarning(warning_text), + stacklevel=3, + ) + + def _warn_about_skipped_plugins(self) -> None: + for module_name, msg in self.pluginmanager.skipped_plugins: + self.issue_config_time_warning( + PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"), + stacklevel=2, + ) + + +def _assertion_supported() -> bool: + try: + assert False + except AssertionError: + return True + else: + return False # type: ignore[unreachable] + + +def create_terminal_writer( + config: Config, file: TextIO | None = None +) -> TerminalWriter: + """Create a TerminalWriter instance configured according to the options + in the config object. + + Every code which requires a TerminalWriter object and has access to a + config object should use this function. + """ + tw = TerminalWriter(file=file) + + if config.option.color == "yes": + tw.hasmarkup = True + elif config.option.color == "no": + tw.hasmarkup = False + + if config.option.code_highlight == "yes": + tw.code_highlight = True + elif config.option.code_highlight == "no": + tw.code_highlight = False + + return tw + + +def _strtobool(val: str) -> bool: + """Convert a string representation of truth to True or False. + + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + + .. note:: Copied from distutils.util. + """ + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + elif val in ("n", "no", "f", "false", "off", "0"): + return False + else: + raise ValueError(f"invalid truth value {val!r}") + + +@lru_cache(maxsize=50) +def parse_warning_filter( + arg: str, *, escape: bool +) -> tuple[warnings._ActionKind, str, type[Warning], str, int]: + """Parse a warnings filter string. + + This is copied from warnings._setoption with the following changes: + + * Does not apply the filter. + * Escaping is optional. + * Raises UsageError so we get nice error messages on failure. + """ + __tracebackhide__ = True + error_template = dedent( + f"""\ + while parsing the following warning configuration: + + {arg} + + This error occurred: + + {{error}} + """ + ) + + parts = arg.split(":") + if len(parts) > 5: + doc_url = ( + "https://docs.python.org/3/library/warnings.html#describing-warning-filters" + ) + error = dedent( + f"""\ + Too many fields ({len(parts)}), expected at most 5 separated by colons: + + action:message:category:module:line + + For more information please consult: {doc_url} + """ + ) + raise UsageError(error_template.format(error=error)) + + while len(parts) < 5: + parts.append("") + action_, message, category_, module, lineno_ = (s.strip() for s in parts) + try: + action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined] + except warnings._OptionError as e: + raise UsageError(error_template.format(error=str(e))) from None + try: + category: type[Warning] = _resolve_warning_category(category_) + except ImportError: + raise + except Exception: + exc_info = ExceptionInfo.from_current() + exception_text = exc_info.getrepr(style="native") + raise UsageError(error_template.format(error=exception_text)) from None + if message and escape: + message = re.escape(message) + if module and escape: + module = re.escape(module) + r"\Z" + if lineno_: + try: + lineno = int(lineno_) + if lineno < 0: + raise ValueError("number is negative") + except ValueError as e: + raise UsageError( + error_template.format(error=f"invalid lineno {lineno_!r}: {e}") + ) from None + else: + lineno = 0 + try: + re.compile(message) + re.compile(module) + except re.error as e: + raise UsageError( + error_template.format(error=f"Invalid regex {e.pattern!r}: {e}") + ) from None + return action, message, category, module, lineno + + +def _resolve_warning_category(category: str) -> type[Warning]: + """ + Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors) + propagate so we can get access to their tracebacks (#9218). + """ + __tracebackhide__ = True + if not category: + return Warning + + if "." not in category: + import builtins as m + + klass = category + else: + module, _, klass = category.rpartition(".") + m = importlib.import_module(module) + cat = getattr(m, klass) + if not issubclass(cat, Warning): + raise UsageError(f"{cat} is not a Warning subclass") + return cast(type[Warning], cat) + + +def apply_warning_filters( + config_filters: Iterable[str], cmdline_filters: Iterable[str] +) -> None: + """Applies pytest-configured filters to the warnings module""" + # Filters should have this precedence: cmdline options, config. + # Filters should be applied in the inverse order of precedence. + for arg in config_filters: + try: + warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) + except ImportError as e: + warnings.warn( + f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning + ) + continue + + for arg in cmdline_filters: + try: + warnings.filterwarnings(*parse_warning_filter(arg, escape=True)) + except ImportError as e: + warnings.warn( + f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning + ) + continue diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/config/argparsing.py b/.venv-test/lib/python3.12/site-packages/_pytest/config/argparsing.py new file mode 100644 index 0000000..f70e276 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/config/argparsing.py @@ -0,0 +1,503 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import argparse +from collections.abc import Callable +from collections.abc import Sequence +import os +import sys +import textwrap +from typing import Any +from typing import final +from typing import Literal +from typing import NoReturn + +from .exceptions import UsageError +import _pytest._io +from _pytest.compat import NOTSET +from _pytest.deprecated import check_ispytest + + +FILE_OR_DIR = "file_or_dir" + + +@final +class Parser: + """Parser for command line arguments and config-file values. + + :ivar extra_info: Dict of generic param -> value to display in case + there's an error processing the command line arguments. + """ + + def __init__( + self, + usage: str | None = None, + processopt: Callable[[Argument], None] | None = None, + *, + prog: str | None = None, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + + from _pytest._argcomplete import filescompleter + + self._processopt = processopt + self.extra_info: dict[str, Any] = {} + self.optparser = PytestArgumentParser(usage, self.extra_info, prog=prog) + anonymous_arggroup = self.optparser.add_argument_group("Custom options") + self._anonymous = OptionGroup( + anonymous_arggroup, "_anonymous", self, _ispytest=True + ) + self._groups = [self._anonymous] + # Maps option strings -> dest, e.g. "-V" and "--version" to "version". + self._opt2dest: dict[str, str] = {} + file_or_dir_arg = self.optparser.add_argument(FILE_OR_DIR, nargs="*") + file_or_dir_arg.completer = filescompleter # type: ignore + + self._inidict: dict[str, tuple[str, str, Any]] = {} + # Maps alias -> canonical name. + self._ini_aliases: dict[str, str] = {} + + @property + def prog(self) -> str: + return self.optparser.prog + + @prog.setter + def prog(self, value: str) -> None: + self.optparser.prog = value + + def processoption(self, option: Argument) -> None: + if self._processopt: + if option.dest: + self._processopt(option) + + def getgroup( + self, name: str, description: str = "", after: str | None = None + ) -> OptionGroup: + """Get (or create) a named option Group. + + :param name: Name of the option group. + :param description: Long description for --help output. + :param after: Name of another group, used for ordering --help output. + :returns: The option group. + + The returned group object has an ``addoption`` method with the same + signature as :func:`parser.addoption ` but + will be shown in the respective group in the output of + ``pytest --help``. + """ + for group in self._groups: + if group.name == name: + return group + + arggroup = self.optparser.add_argument_group(description or name) + group = OptionGroup(arggroup, name, self, _ispytest=True) + i = 0 + for i, grp in enumerate(self._groups): + if grp.name == after: + break + self._groups.insert(i + 1, group) + # argparse doesn't provide a way to control `--help` order, so must + # access its internals ☹. + self.optparser._action_groups.insert(i + 1, self.optparser._action_groups.pop()) + return group + + def addoption(self, *opts: str, **attrs: Any) -> None: + """Register a command line option. + + :param opts: + Option names, can be short or long options. + :param attrs: + Same attributes as the argparse library's :meth:`add_argument() + ` function accepts. + + After command line parsing, options are available on the pytest config + object via ``config.option.NAME`` where ``NAME`` is usually set + by passing a ``dest`` attribute, for example + ``addoption("--long", dest="NAME", ...)``. + """ + self._anonymous.addoption(*opts, **attrs) + + def parse( + self, + args: Sequence[str | os.PathLike[str]], + namespace: argparse.Namespace | None = None, + ) -> argparse.Namespace: + """Parse the arguments. + + Unlike ``parse_known_args`` and ``parse_known_and_unknown_args``, + raises PrintHelp on `--help` and UsageError on unknown flags + + :meta private: + """ + from _pytest._argcomplete import try_argcomplete + + try_argcomplete(self.optparser) + strargs = [os.fspath(x) for x in args] + if namespace is None: + namespace = argparse.Namespace() + try: + namespace._raise_print_help = True + return self.optparser.parse_intermixed_args(strargs, namespace=namespace) + finally: + del namespace._raise_print_help + + def parse_known_args( + self, + args: Sequence[str | os.PathLike[str]], + namespace: argparse.Namespace | None = None, + ) -> argparse.Namespace: + """Parse the known arguments at this point. + + :returns: An argparse namespace object. + """ + return self.parse_known_and_unknown_args(args, namespace=namespace)[0] + + def parse_known_and_unknown_args( + self, + args: Sequence[str | os.PathLike[str]], + namespace: argparse.Namespace | None = None, + ) -> tuple[argparse.Namespace, list[str]]: + """Parse the known arguments at this point, and also return the + remaining unknown flag arguments. + + :returns: + A tuple containing an argparse namespace object for the known + arguments, and a list of unknown flag arguments. + """ + strargs = [os.fspath(x) for x in args] + if sys.version_info < (3, 12, 8) or (3, 13) <= sys.version_info < (3, 13, 1): + # Older argparse have a bugged parse_known_intermixed_args. + namespace, unknown = self.optparser.parse_known_args(strargs, namespace) + assert namespace is not None + file_or_dir = getattr(namespace, FILE_OR_DIR) + unknown_flags: list[str] = [] + for arg in unknown: + (unknown_flags if arg.startswith("-") else file_or_dir).append(arg) + return namespace, unknown_flags + else: + return self.optparser.parse_known_intermixed_args(strargs, namespace) + + def addini( + self, + name: str, + help: str, + type: Literal[ + "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float" + ] + | None = None, + default: Any = NOTSET, + *, + aliases: Sequence[str] = (), + ) -> None: + """Register a configuration file option. + + :param name: + Name of the configuration. + :param type: + Type of the configuration. Can be: + + * ``string``: a string + * ``bool``: a boolean + * ``args``: a list of strings, separated as in a shell + * ``linelist``: a list of strings, separated by line breaks + * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell + * ``pathlist``: a list of ``py.path``, separated as in a shell + * ``int``: an integer + * ``float``: a floating-point number + + .. versionadded:: 8.4 + + The ``float`` and ``int`` types. + + For ``paths`` and ``pathlist`` types, they are considered relative to the config-file. + In case the execution is happening without a config-file defined, + they will be considered relative to the current working directory (for example with ``--override-ini``). + + .. versionadded:: 7.0 + The ``paths`` variable type. + + .. versionadded:: 8.1 + Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of a config-file. + + Defaults to ``string`` if ``None`` or not passed. + :param default: + Default value if no config-file option exists but is queried. + :param aliases: + Additional names by which this option can be referenced. + Aliases resolve to the canonical name. + + .. versionadded:: 9.0 + The ``aliases`` parameter. + + The value of configuration keys can be retrieved via a call to + :py:func:`config.getini(name) `. + """ + assert type in ( + None, + "string", + "paths", + "pathlist", + "args", + "linelist", + "bool", + "int", + "float", + ) + if type is None: + type = "string" + if default is NOTSET: + default = get_ini_default_for_type(type) + + self._inidict[name] = (help, type, default) + + for alias in aliases: + if alias in self._inidict: + raise ValueError( + f"alias {alias!r} conflicts with existing configuration option" + ) + if (already := self._ini_aliases.get(alias)) is not None: + raise ValueError(f"{alias!r} is already an alias of {already!r}") + self._ini_aliases[alias] = name + + +def get_ini_default_for_type( + type: Literal[ + "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float" + ], +) -> Any: + """ + Used by addini to get the default value for a given config option type, when + default is not supplied. + """ + if type in ("paths", "pathlist", "args", "linelist"): + return [] + elif type == "bool": + return False + elif type == "int": + return 0 + elif type == "float": + return 0.0 + else: + return "" + + +class Argument: + """An option defined in an OptionGroup.""" + + def __init__(self, action: argparse.Action) -> None: + self._action = action + + def attrs(self) -> dict[str, Any]: + return self._action.__dict__ + + def names(self) -> Sequence[str]: + return self._action.option_strings + + @property + def dest(self) -> str: + return self._action.dest + + @property + def default(self) -> Any: + return self._action.default + + @property + def type(self) -> Any | None: + return self._action.type + + def __repr__(self) -> str: + action = getattr(self, "_action", None) + if action is None: + return "Argument()" + args: list[str] = [] + args += ["opts: " + repr(self.names())] + args += ["dest: " + repr(self.dest)] + if action.type: + args += ["type: " + repr(self.type)] + args += ["default: " + repr(self.default)] + return "Argument({})".format(", ".join(args)) + + +class OptionGroup: + """A group of options shown in its own section.""" + + def __init__( + self, + arggroup: argparse._ArgumentGroup, + name: str, + parser: Parser | None, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._arggroup = arggroup + self.name = name + self.options: list[Argument] = [] + self.parser = parser + + def addoption(self, *opts: str, **attrs: Any) -> None: + """Add an option to this group. + + If a shortened version of a long option is specified, it will + be suppressed in the help. ``addoption('--twowords', '--two-words')`` + results in help showing ``--two-words`` only, but ``--twowords`` gets + accepted **and** the automatic destination is in ``args.twowords``. + + :param opts: + Option names, can be short or long options. + Note that lower-case short options (e.g. `-x`) are reserved. + :param attrs: + Same attributes as the argparse library's :meth:`add_argument() + ` function accepts. + """ + conflict = set(opts).intersection( + name for opt in self.options for name in opt.names() + ) + if conflict: + raise ValueError(f"option names {conflict} already added") + self._addoption_inner(opts, attrs, allow_reserved=False) + + def _addoption(self, *opts: str, **attrs: Any) -> None: + """Like addoption(), but also allows registering short lower case options (e.g. -x), + which are reserved for pytest core.""" + self._addoption_inner(opts, attrs, allow_reserved=True) + + def _addoption_inner( + self, opts: tuple[str, ...], attrs: dict[str, Any], allow_reserved: bool + ) -> None: + if not allow_reserved: + for opt in opts: + if len(opt) >= 2 and opt[0] == "-" and opt[1].islower(): + raise ValueError("lowercase short options are reserved") + + action = self._arggroup.add_argument(*opts, **attrs) + option = Argument(action) + self.options.append(option) + if self.parser: + for name in option.names(): + self.parser._opt2dest[name] = option.dest + self.parser.processoption(option) + + +class PytestArgumentParser(argparse.ArgumentParser): + def __init__( + self, + usage: str | None, + extra_info: dict[str, str], + *, + prog: str | None = None, + ) -> None: + super().__init__( + usage=usage, + prog=prog, + add_help=False, + formatter_class=DropShorterLongHelpFormatter, + allow_abbrev=False, + fromfile_prefix_chars="@", + ) + # extra_info is a dict of (param -> value) to display if there's + # an usage error to provide more contextual information to the user. + self.extra_info = extra_info + + def error(self, message: str) -> NoReturn: + """Transform argparse error message into UsageError.""" + # TODO(py313): Replace with `exit_on_error=False`. Note that while it + # was added in Python 3.9, it was broken until 3.13 (cpython#121018). + msg = f"{self.prog}: error: {message}" + if self.extra_info: + msg += "\n" + "\n".join( + f" {k}: {v}" for k, v in sorted(self.extra_info.items()) + ) + raise UsageError(self.format_usage() + msg) + + +class DropShorterLongHelpFormatter(argparse.HelpFormatter): + """Shorten help for long options that differ only in extra hyphens. + + - Collapse **long** options that are the same except for extra hyphens. + - Shortcut if there are only two options and one of them is a short one. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + # Use more accurate terminal width. + if "width" not in kwargs: + kwargs["width"] = _pytest._io.get_terminal_width() + super().__init__(*args, **kwargs) + + def _format_action_invocation(self, action: argparse.Action) -> str: + orgstr = super()._format_action_invocation(action) + if orgstr and orgstr[0] != "-": # only optional arguments + return orgstr + options = orgstr.split(", ") + if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2): + # a shortcut for '-h, --help' or '--abc', '-a' + return orgstr + return_list = [] + short_long: dict[str, str] = {} + for option in options: + if len(option) == 2 or option[2] == " ": + continue + assert option.startswith("--"), ( + f'long optional argument without "--": [{option}]' + ) + xxoption = option[2:] + shortened = xxoption.replace("-", "") + if shortened not in short_long or len(short_long[shortened]) < len( + xxoption + ): + short_long[shortened] = xxoption + # now short_long has been filled out to the longest with dashes + # **and** we keep the right option ordering from add_argument + for option in options: + if len(option) == 2 or option[2] == " ": + return_list.append(option) + if option[2:] == short_long.get(option.replace("-", "")): + return_list.append(option.replace(" ", "=", 1)) + return ", ".join(return_list) + + def _split_lines(self, text: str, width: int) -> list[str]: + """Wrap lines after splitting on original newlines. + + This allows to have explicit line breaks in the help text. + """ + lines = [] + for line in text.splitlines(): + lines.extend(textwrap.wrap(line.strip(), width)) + return lines + + +class OverrideIniAction(argparse.Action): + """Custom argparse action that makes a CLI flag equivalent to overriding an + option, in addition to behaving like `store_true`. + + This can simplify things since code only needs to inspect the config option + and not consider the CLI flag. + """ + + def __init__( + self, + option_strings: Sequence[str], + dest: str, + nargs: int | str | None = None, + *args, + ini_option: str, + ini_value: str, + **kwargs, + ) -> None: + super().__init__(option_strings, dest, 0, *args, **kwargs) + self.ini_option = ini_option + self.ini_value = ini_value + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + *args, + **kwargs, + ) -> None: + setattr(namespace, self.dest, True) + current_overrides = getattr(namespace, "override_ini", None) + if current_overrides is None: + current_overrides = [] + current_overrides.append(f"{self.ini_option}={self.ini_value}") + setattr(namespace, "override_ini", current_overrides) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/config/exceptions.py b/.venv-test/lib/python3.12/site-packages/_pytest/config/exceptions.py new file mode 100644 index 0000000..d84a9ea --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/config/exceptions.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import final + + +@final +class UsageError(Exception): + """Error in pytest usage or invocation.""" + + __module__ = "pytest" + + +class PrintHelp(Exception): + """Raised when pytest should print its help to skip the rest of the + argument parsing and validation.""" diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/config/findpaths.py b/.venv-test/lib/python3.12/site-packages/_pytest/config/findpaths.py new file mode 100644 index 0000000..e74546c --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/config/findpaths.py @@ -0,0 +1,350 @@ +from __future__ import annotations + +from collections.abc import Iterable +from collections.abc import Sequence +from dataclasses import dataclass +from dataclasses import KW_ONLY +import os +from pathlib import Path +import sys +from typing import Literal +from typing import TypeAlias + +import iniconfig + +from .exceptions import UsageError +from _pytest.outcomes import fail +from _pytest.pathlib import absolutepath +from _pytest.pathlib import commonpath +from _pytest.pathlib import safe_exists + + +@dataclass(frozen=True) +class ConfigValue: + """Represents a configuration value with its origin and parsing mode. + + This allows tracking whether a value came from a configuration file + or from a CLI override (--override-ini), which is important for + determining precedence when dealing with ini option aliases. + + The mode tracks the parsing mode/data model used for the value: + - "ini": from INI files or [tool.pytest.ini_options], where the only + supported value types are `str` or `list[str]`. + - "toml": from TOML files (not in INI mode), where native TOML types + are preserved. + """ + + value: object + _: KW_ONLY + origin: Literal["file", "override"] + mode: Literal["ini", "toml"] + + +ConfigDict: TypeAlias = dict[str, ConfigValue] + + +def _parse_ini_config(path: Path) -> iniconfig.IniConfig: + """Parse the given generic '.ini' file using legacy IniConfig parser, returning + the parsed object. + + Raise UsageError if the file cannot be parsed. + """ + try: + return iniconfig.IniConfig(str(path)) + except iniconfig.ParseError as exc: + raise UsageError(str(exc)) from exc + + +def load_config_dict_from_file( + filepath: Path, +) -> ConfigDict | None: + """Load pytest configuration from the given file path, if supported. + + Return None if the file does not contain valid pytest configuration. + """ + # Configuration from ini files are obtained from the [pytest] section, if present. + if filepath.suffix == ".ini": + iniconfig = _parse_ini_config(filepath) + + if "pytest" in iniconfig: + return { + k: ConfigValue(v, origin="file", mode="ini") + for k, v in iniconfig["pytest"].items() + } + else: + # "pytest.ini" files are always the source of configuration, even if empty. + if filepath.name in {"pytest.ini", ".pytest.ini"}: + return {} + + # '.cfg' files are considered if they contain a "[tool:pytest]" section. + elif filepath.suffix == ".cfg": + iniconfig = _parse_ini_config(filepath) + + if "tool:pytest" in iniconfig.sections: + return { + k: ConfigValue(v, origin="file", mode="ini") + for k, v in iniconfig["tool:pytest"].items() + } + elif "pytest" in iniconfig.sections: + # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that + # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). + fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) + + # '.toml' files are considered if they contain a [tool.pytest] table (toml mode) + # or [tool.pytest.ini_options] table (ini mode) for pyproject.toml, + # or [pytest] table (toml mode) for pytest.toml/.pytest.toml. + elif filepath.suffix == ".toml": + if sys.version_info >= (3, 11): + import tomllib + else: + import tomli as tomllib + + toml_text = filepath.read_text(encoding="utf-8") + try: + config = tomllib.loads(toml_text) + except tomllib.TOMLDecodeError as exc: + raise UsageError(f"{filepath}: {exc}") from exc + + # pytest.toml and .pytest.toml use [pytest] table directly. + if filepath.name in ("pytest.toml", ".pytest.toml"): + pytest_config = config.get("pytest", {}) + if pytest_config: + # TOML mode - preserve native TOML types. + return { + k: ConfigValue(v, origin="file", mode="toml") + for k, v in pytest_config.items() + } + # "pytest.toml" files are always the source of configuration, even if empty. + return {} + + # pyproject.toml uses [tool.pytest] or [tool.pytest.ini_options]. + else: + tool_pytest = config.get("tool", {}).get("pytest", {}) + + # Check for toml mode config: [tool.pytest] with content outside of ini_options. + toml_config = {k: v for k, v in tool_pytest.items() if k != "ini_options"} + # Check for ini mode config: [tool.pytest.ini_options]. + ini_config = tool_pytest.get("ini_options", None) + + if toml_config and ini_config: + raise UsageError( + f"{filepath}: Cannot use both [tool.pytest] (native TOML types) and " + "[tool.pytest.ini_options] (string-based INI format) simultaneously. " + "Please use [tool.pytest] with native TOML types (recommended) " + "or [tool.pytest.ini_options] for backwards compatibility." + ) + + if toml_config: + # TOML mode - preserve native TOML types. + return { + k: ConfigValue(v, origin="file", mode="toml") + for k, v in toml_config.items() + } + + elif ini_config is not None: + # INI mode - TOML supports richer data types than INI files, but we need to + # convert all scalar values to str for compatibility with the INI system. + def make_scalar(v: object) -> str | list[str]: + return v if isinstance(v, list) else str(v) + + return { + k: ConfigValue(make_scalar(v), origin="file", mode="ini") + for k, v in ini_config.items() + } + + return None + + +def locate_config( + invocation_dir: Path, + args: Iterable[Path], +) -> tuple[Path | None, Path | None, ConfigDict, Sequence[str]]: + """Search in the list of arguments for a valid ini-file for pytest, + and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where + ignored-config-files is a list of config basenames found that contain + pytest configuration but were ignored.""" + config_names = [ + "pytest.toml", + ".pytest.toml", + "pytest.ini", + ".pytest.ini", + "pyproject.toml", + "tox.ini", + "setup.cfg", + ] + args = [x for x in args if not str(x).startswith("-")] + if not args: + args = [invocation_dir] + found_pyproject_toml: Path | None = None + ignored_config_files: list[str] = [] + + for arg in args: + argpath = absolutepath(arg) + for base in (argpath, *argpath.parents): + for config_name in config_names: + p = base / config_name + if p.is_file(): + if p.name == "pyproject.toml" and found_pyproject_toml is None: + found_pyproject_toml = p + ini_config = load_config_dict_from_file(p) + if ini_config is not None: + index = config_names.index(config_name) + for remainder in config_names[index + 1 :]: + p2 = base / remainder + if ( + p2.is_file() + and load_config_dict_from_file(p2) is not None + ): + ignored_config_files.append(remainder) + return base, p, ini_config, ignored_config_files + if found_pyproject_toml is not None: + return found_pyproject_toml.parent, found_pyproject_toml, {}, [] + return None, None, {}, [] + + +def get_common_ancestor( + invocation_dir: Path, + paths: Iterable[Path], +) -> Path: + common_ancestor: Path | None = None + for path in paths: + if not path.exists(): + continue + if common_ancestor is None: + common_ancestor = path + else: + if common_ancestor in path.parents or path == common_ancestor: + continue + elif path in common_ancestor.parents: + common_ancestor = path + else: + shared = commonpath(path, common_ancestor) + if shared is not None: + common_ancestor = shared + if common_ancestor is None: + common_ancestor = invocation_dir + elif common_ancestor.is_file(): + common_ancestor = common_ancestor.parent + return common_ancestor + + +def get_dirs_from_args(args: Iterable[str]) -> list[Path]: + def is_option(x: str) -> bool: + return x.startswith("-") + + def get_file_part_from_node_id(x: str) -> str: + return x.split("::", maxsplit=1)[0] + + def get_dir_from_path(path: Path) -> Path: + if path.is_dir(): + return path + return path.parent + + # These look like paths but may not exist + possible_paths = ( + absolutepath(get_file_part_from_node_id(arg)) + for arg in args + if not is_option(arg) + ) + + return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)] + + +def parse_override_ini(override_ini: Sequence[str] | None) -> ConfigDict: + """Parse the -o/--override-ini command line arguments and return the overrides. + + :raises UsageError: + If one of the values is malformed. + """ + overrides = {} + # override_ini is a list of "ini=value" options. + # Always use the last item if multiple values are set for same ini-name, + # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2. + for ini_config in override_ini or (): + try: + key, user_ini_value = ini_config.split("=", 1) + except ValueError as e: + raise UsageError( + f"-o/--override-ini expects option=value style (got: {ini_config!r})." + ) from e + else: + overrides[key] = ConfigValue(user_ini_value, origin="override", mode="ini") + return overrides + + +CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." + + +def determine_setup( + *, + inifile: str | None, + override_ini: Sequence[str] | None, + args: Sequence[str], + rootdir_cmd_arg: str | None, + invocation_dir: Path, +) -> tuple[Path, Path | None, ConfigDict, Sequence[str]]: + """Determine the rootdir, inifile and ini configuration values from the + command line arguments. + + :param inifile: + The `--inifile` command line argument, if given. + :param override_ini: + The -o/--override-ini command line arguments, if given. + :param args: + The free command line arguments. + :param rootdir_cmd_arg: + The `--rootdir` command line argument, if given. + :param invocation_dir: + The working directory when pytest was invoked. + + :raises UsageError: + """ + rootdir = None + dirs = get_dirs_from_args(args) + ignored_config_files: Sequence[str] = [] + + if inifile: + inipath_ = absolutepath(inifile) + inipath: Path | None = inipath_ + inicfg = load_config_dict_from_file(inipath_) or {} + if rootdir_cmd_arg is None: + rootdir = inipath_.parent + else: + ancestor = get_common_ancestor(invocation_dir, dirs) + rootdir, inipath, inicfg, ignored_config_files = locate_config( + invocation_dir, [ancestor] + ) + if rootdir is None and rootdir_cmd_arg is None: + for possible_rootdir in (ancestor, *ancestor.parents): + if (possible_rootdir / "setup.py").is_file(): + rootdir = possible_rootdir + break + else: + if dirs != [ancestor]: + rootdir, inipath, inicfg, _ = locate_config(invocation_dir, dirs) + if rootdir is None: + rootdir = get_common_ancestor( + invocation_dir, [invocation_dir, ancestor] + ) + if is_fs_root(rootdir): + rootdir = ancestor + if rootdir_cmd_arg: + rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) + if not rootdir.is_dir(): + raise UsageError( + f"Directory '{rootdir}' not found. Check your '--rootdir' option." + ) + + ini_overrides = parse_override_ini(override_ini) + inicfg.update(ini_overrides) + + assert rootdir is not None + return rootdir, inipath, inicfg, ignored_config_files + + +def is_fs_root(p: Path) -> bool: + r""" + Return True if the given path is pointing to the root of the + file system ("/" on Unix and "C:\\" on Windows for example). + """ + return os.path.splitdrive(str(p))[1] == os.sep diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/debugging.py b/.venv-test/lib/python3.12/site-packages/_pytest/debugging.py new file mode 100644 index 0000000..b256f83 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/debugging.py @@ -0,0 +1,404 @@ +# mypy: allow-untyped-defs +# ruff: noqa: T100 +"""Interactive debugging with PDB, the Python Debugger.""" + +from __future__ import annotations + +import argparse +from collections.abc import Callable +from collections.abc import Generator +import functools +import importlib +import sys +import types +from typing import Any + +from _pytest import outcomes +from _pytest._code import ExceptionInfo +from _pytest.capture import CaptureManager +from _pytest.config import Config +from _pytest.config import ConftestImportFailure +from _pytest.config import hookimpl +from _pytest.config import PytestPluginManager +from _pytest.config.argparsing import Parser +from _pytest.config.exceptions import UsageError +from _pytest.nodes import Node +from _pytest.reports import BaseReport +from _pytest.runner import CallInfo + + +def _validate_usepdb_cls(value: str) -> tuple[str, str]: + """Validate syntax of --pdbcls option.""" + try: + modname, classname = value.split(":") + except ValueError as e: + raise argparse.ArgumentTypeError( + f"{value!r} is not in the format 'modname:classname'" + ) from e + return (modname, classname) + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group.addoption( + "--pdb", + dest="usepdb", + action="store_true", + help="Start the interactive Python debugger on errors or KeyboardInterrupt", + ) + group.addoption( + "--pdbcls", + dest="usepdb_cls", + metavar="modulename:classname", + type=_validate_usepdb_cls, + help="Specify a custom interactive Python debugger for use with --pdb." + "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", + ) + group.addoption( + "--trace", + dest="trace", + action="store_true", + help="Immediately break when running each test", + ) + + +def pytest_configure(config: Config) -> None: + import pdb + + if config.getvalue("trace"): + config.pluginmanager.register(PdbTrace(), "pdbtrace") + if config.getvalue("usepdb"): + config.pluginmanager.register(PdbInvoke(), "pdbinvoke") + + pytestPDB._saved.append( + (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config) + ) + pdb.set_trace = pytestPDB.set_trace + pytestPDB._pluginmanager = config.pluginmanager + pytestPDB._config = config + + # NOTE: not using pytest_unconfigure, since it might get called although + # pytest_configure was not (if another plugin raises UsageError). + def fin() -> None: + ( + pdb.set_trace, + pytestPDB._pluginmanager, + pytestPDB._config, + ) = pytestPDB._saved.pop() + + config.add_cleanup(fin) + + +class pytestPDB: + """Pseudo PDB that defers to the real pdb.""" + + _pluginmanager: PytestPluginManager | None = None + _config: Config | None = None + _saved: list[ + tuple[Callable[..., None], PytestPluginManager | None, Config | None] + ] = [] + _recursive_debug = 0 + _wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None + + @classmethod + def _is_capturing(cls, capman: CaptureManager | None) -> str | bool: + if capman: + return capman.is_capturing() + return False + + @classmethod + def _import_pdb_cls(cls, capman: CaptureManager | None): + if not cls._config: + import pdb + + # Happens when using pytest.set_trace outside of a test. + return pdb.Pdb + + usepdb_cls = cls._config.getvalue("usepdb_cls") + + if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls: + return cls._wrapped_pdb_cls[1] + + if usepdb_cls: + modname, classname = usepdb_cls + + try: + mod = importlib.import_module(modname) + + # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). + parts = classname.split(".") + pdb_cls = getattr(mod, parts[0]) + for part in parts[1:]: + pdb_cls = getattr(pdb_cls, part) + except Exception as exc: + value = ":".join((modname, classname)) + raise UsageError( + f"--pdbcls: could not import {value!r}: {exc}" + ) from exc + else: + import pdb + + pdb_cls = pdb.Pdb + + wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman) + cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls) + return wrapped_cls + + @classmethod + def _get_pdb_wrapper_class(cls, pdb_cls, capman: CaptureManager | None): + import _pytest.config + + class PytestPdbWrapper(pdb_cls): + _pytest_capman = capman + _continued = False + + def do_debug(self, arg): + cls._recursive_debug += 1 + ret = super().do_debug(arg) + cls._recursive_debug -= 1 + return ret + + if hasattr(pdb_cls, "do_debug"): + do_debug.__doc__ = pdb_cls.do_debug.__doc__ + + def do_continue(self, arg): + ret = super().do_continue(arg) + if cls._recursive_debug == 0: + assert cls._config is not None + tw = _pytest.config.create_terminal_writer(cls._config) + tw.line() + + capman = self._pytest_capman + capturing = pytestPDB._is_capturing(capman) + if capturing: + if capturing == "global": + tw.sep(">", "PDB continue (IO-capturing resumed)") + else: + tw.sep( + ">", + f"PDB continue (IO-capturing resumed for {capturing})", + ) + assert capman is not None + capman.resume() + else: + tw.sep(">", "PDB continue") + assert cls._pluginmanager is not None + cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self) + self._continued = True + return ret + + if hasattr(pdb_cls, "do_continue"): + do_continue.__doc__ = pdb_cls.do_continue.__doc__ + + do_c = do_cont = do_continue + + def do_quit(self, arg): + # Raise Exit outcome when quit command is used in pdb. + # + # This is a bit of a hack - it would be better if BdbQuit + # could be handled, but this would require to wrap the + # whole pytest run, and adjust the report etc. + ret = super().do_quit(arg) + + if cls._recursive_debug == 0: + outcomes.exit("Quitting debugger") + + return ret + + if hasattr(pdb_cls, "do_quit"): + do_quit.__doc__ = pdb_cls.do_quit.__doc__ + + do_q = do_quit + do_exit = do_quit + + def setup(self, f, tb): + """Suspend on setup(). + + Needed after do_continue resumed, and entering another + breakpoint again. + """ + ret = super().setup(f, tb) + if not ret and self._continued: + # pdb.setup() returns True if the command wants to exit + # from the interaction: do not suspend capturing then. + if self._pytest_capman: + self._pytest_capman.suspend_global_capture(in_=True) + return ret + + def get_stack(self, f, t): + stack, i = super().get_stack(f, t) + if f is None: + # Find last non-hidden frame. + i = max(0, len(stack) - 1) + while i and stack[i][0].f_locals.get("__tracebackhide__", False): + i -= 1 + return stack, i + + return PytestPdbWrapper + + @classmethod + def _init_pdb(cls, method, *args, **kwargs): + """Initialize PDB debugging, dropping any IO capturing.""" + import _pytest.config + + if cls._pluginmanager is None: + capman: CaptureManager | None = None + else: + capman = cls._pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend(in_=True) + + if cls._config: + tw = _pytest.config.create_terminal_writer(cls._config) + tw.line() + + if cls._recursive_debug == 0: + # Handle header similar to pdb.set_trace in py37+. + header = kwargs.pop("header", None) + if header is not None: + tw.sep(">", header) + else: + capturing = cls._is_capturing(capman) + if capturing == "global": + tw.sep(">", f"PDB {method} (IO-capturing turned off)") + elif capturing: + tw.sep( + ">", + f"PDB {method} (IO-capturing turned off for {capturing})", + ) + else: + tw.sep(">", f"PDB {method}") + + _pdb = cls._import_pdb_cls(capman)(**kwargs) + + if cls._pluginmanager: + cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb) + return _pdb + + @classmethod + def set_trace(cls, *args, **kwargs) -> None: + """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing.""" + frame = sys._getframe().f_back + _pdb = cls._init_pdb("set_trace", *args, **kwargs) + _pdb.set_trace(frame) + + +class PdbInvoke: + def pytest_exception_interact( + self, node: Node, call: CallInfo[Any], report: BaseReport + ) -> None: + capman = node.config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture(in_=True) + out, err = capman.read_global_capture() + sys.stdout.write(out) + sys.stdout.write(err) + assert call.excinfo is not None + _enter_pdb(node, call.excinfo, report) + + def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None: + exc_or_tb = _postmortem_exc_or_tb(excinfo) + post_mortem(exc_or_tb) + + +class PdbTrace: + @hookimpl(wrapper=True) + def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]: + wrap_pytest_function_for_tracing(pyfuncitem) + return (yield) + + +def wrap_pytest_function_for_tracing(pyfuncitem) -> None: + """Change the Python function object of the given Function item by a + wrapper which actually enters pdb before calling the python function + itself, effectively leaving the user in the pdb prompt in the first + statement of the function.""" + _pdb = pytestPDB._init_pdb("runcall") + testfunction = pyfuncitem.obj + + # we can't just return `partial(pdb.runcall, testfunction)` because (on + # python < 3.7.4) runcall's first param is `func`, which means we'd get + # an exception if one of the kwargs to testfunction was called `func`. + @functools.wraps(testfunction) + def wrapper(*args, **kwargs) -> None: + func = functools.partial(testfunction, *args, **kwargs) + _pdb.runcall(func) + + pyfuncitem.obj = wrapper + + +def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None: + """Wrap the given pytestfunct item for tracing support if --trace was given in + the command line.""" + if pyfuncitem.config.getvalue("trace"): + wrap_pytest_function_for_tracing(pyfuncitem) + + +def _enter_pdb( + node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport +) -> BaseReport: + # XXX we reuse the TerminalReporter's terminalwriter + # because this seems to avoid some encoding related troubles + # for not completely clear reasons. + tw = node.config.pluginmanager.getplugin("terminalreporter")._tw + tw.line() + + showcapture = node.config.option.showcapture + + for sectionname, content in ( + ("stdout", rep.capstdout), + ("stderr", rep.capstderr), + ("log", rep.caplog), + ): + if showcapture in (sectionname, "all") and content: + tw.sep(">", "captured " + sectionname) + if content[-1:] == "\n": + content = content[:-1] + tw.line(content) + + tw.sep(">", "traceback") + rep.toterminal(tw) + tw.sep(">", "entering PDB") + tb_or_exc = _postmortem_exc_or_tb(excinfo) + rep._pdbshown = True # type: ignore[attr-defined] + post_mortem(tb_or_exc) + return rep + + +def _postmortem_exc_or_tb( + excinfo: ExceptionInfo[BaseException], +) -> types.TracebackType | BaseException: + from doctest import UnexpectedException + + get_exc = sys.version_info >= (3, 13) + if isinstance(excinfo.value, UnexpectedException): + # A doctest.UnexpectedException is not useful for post_mortem. + # Use the underlying exception instead: + underlying_exc = excinfo.value + if get_exc: + return underlying_exc.exc_info[1] + + return underlying_exc.exc_info[2] + elif isinstance(excinfo.value, ConftestImportFailure): + # A config.ConftestImportFailure is not useful for post_mortem. + # Use the underlying exception instead: + cause = excinfo.value.cause + if get_exc: + return cause + + assert cause.__traceback__ is not None + return cause.__traceback__ + else: + assert excinfo._excinfo is not None + if get_exc: + return excinfo._excinfo[1] + + return excinfo._excinfo[2] + + +def post_mortem(tb_or_exc: types.TracebackType | BaseException) -> None: + p = pytestPDB._init_pdb("post_mortem") + p.reset() + p.interaction(None, tb_or_exc) + if p.quitting: + outcomes.exit("Quitting debugger") diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/deprecated.py b/.venv-test/lib/python3.12/site-packages/_pytest/deprecated.py new file mode 100644 index 0000000..95e75e6 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/deprecated.py @@ -0,0 +1,139 @@ +"""Deprecation messages and bits of code used elsewhere in the codebase that +is planned to be removed in the next pytest release. + +Keeping it in a central location makes it easy to track what is deprecated and should +be removed when the time comes. + +All constants defined in this module should be either instances of +:class:`PytestWarning`, or :class:`UnformattedWarning` +in case of warnings which need to format their messages. +""" + +from __future__ import annotations + +from warnings import warn + +from _pytest.warning_types import PytestDeprecationWarning +from _pytest.warning_types import PytestRemovedIn10Warning +from _pytest.warning_types import UnformattedWarning + + +# set of plugins which have been integrated into the core; we use this list to ignore +# them during registration to avoid conflicts +DEPRECATED_EXTERNAL_PLUGINS = { + "pytest_catchlog", + "pytest_capturelog", + "pytest_faulthandler", + "pytest_subtests", +} + + +# This could have been removed pytest 8, but it's harmless and common, so no rush to remove. +YIELD_FIXTURE = PytestDeprecationWarning( + "@pytest.yield_fixture is deprecated.\n" + "Use @pytest.fixture instead; they are the same." +) + +CLASS_FIXTURE_INSTANCE_METHOD = PytestRemovedIn10Warning( + "Class-scoped fixture defined as instance method is deprecated.\n" + "Instance attributes set in this fixture will NOT be visible to test methods,\n" + "as each test gets a new instance while the fixture runs only once per class.\n" + "Use @classmethod decorator and set attributes on cls instead.\n" + "See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method" +) + +# This deprecation is never really meant to be removed. +PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") + + +HOOK_LEGACY_MARKING = UnformattedWarning( + PytestRemovedIn10Warning, + "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n" + "Please use the pytest.hook{type}({hook_opts}) decorator instead\n" + " to configure the hooks.\n" + " See https://docs.pytest.org/en/latest/deprecations.html" + "#configuring-hook-specs-impls-using-markers", +) + +MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES = PytestRemovedIn10Warning( + "monkeypatch.syspath_prepend() called with pkg_resources legacy namespace packages detected.\n" + "Legacy namespace packages (using pkg_resources.declare_namespace) are deprecated.\n" + "Please use native namespace packages (PEP 420) instead.\n" + "See https://docs.pytest.org/en/stable/deprecations.html#monkeypatch-fixup-namespace-packages" +) + +PARAMETRIZE_NON_COLLECTION_ITERABLE = UnformattedWarning( + PytestRemovedIn10Warning, + "Passing a non-Collection iterable to parametrize is deprecated.\n" + "Test: {nodeid}, argvalues type: {type_name}\n" + "Please convert to a list or tuple.\n" + "See https://docs.pytest.org/en/stable/deprecations.html#parametrize-iterators", +) + +CONSOLE_MAIN = PytestRemovedIn10Warning( + "pytest.console_main() is deprecated and will be removed in pytest 10.\n" + "It was never intended for programmatic use; use pytest.main() instead.\n" + "See https://docs.pytest.org/en/stable/deprecations.html#console-main" +) + +CONFIG_INICFG = PytestRemovedIn10Warning( + "config.inicfg is deprecated, use config.getini() to access configuration values instead.\n" + "See https://docs.pytest.org/en/stable/deprecations.html#config-inicfg" +) + +FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN = UnformattedWarning( + PytestRemovedIn10Warning, + 'Calling request.getfixturevalue("{argname}") during teardown is deprecated.\n' + "Please request the fixture before teardown begins, either by declaring it in the fixture signature " + "or by calling request.getfixturevalue() before the fixture yields.\n" + "See https://docs.pytest.org/en/stable/deprecations.html#dynamic-fixture-request-during-teardown", +) + +PASTEBIN = PytestRemovedIn10Warning( + "The --pastebin option is deprecated. " + "The functionality is now available in an external plugin package, pytest-pastebin.\n" + "See https://docs.pytest.org/en/stable/deprecations.html#the-pastebin-option" +) + +# You want to make some `__init__` or function "private". +# +# def my_private_function(some, args): +# ... +# +# Do this: +# +# def my_private_function(some, args, *, _ispytest: bool = False): +# check_ispytest(_ispytest) +# ... +# +# Change all internal/allowed calls to +# +# my_private_function(some, args, _ispytest=True) +# +# All other calls will get the default _ispytest=False and trigger +# the warning (possibly error in the future). + + +FIXTURE_BASEID_DEPRECATED = PytestRemovedIn10Warning( + "Passing baseid to FixtureDef is deprecated. Pass node instead for fixture scoping." +) + +FIXTURE_NODEID_DEPRECATED = PytestRemovedIn10Warning( + "Passing nodeid to _register_fixture is deprecated. " + "Pass node instead for fixture scoping." +) + +FIXTUREDEF_HAS_LOCATION_DEPRECATED = PytestRemovedIn10Warning( + "FixtureDef.has_location is deprecated and will be removed in pytest 10. " + "See https://docs.pytest.org/en/stable/deprecations.html#fixturedef-has-location-deprecated" +) + +PARSEFACTORIES_NODEID_DEPRECATED = PytestRemovedIn10Warning( + "Passing nodeid string to parsefactories is deprecated. " + "Use parsefactories(holder=obj, node=node) instead." +) + + +def check_ispytest(ispytest: bool) -> None: + if not ispytest: + warn(PRIVATE, stacklevel=3) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/doctest.py b/.venv-test/lib/python3.12/site-packages/_pytest/doctest.py new file mode 100644 index 0000000..b1f3651 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/doctest.py @@ -0,0 +1,735 @@ +# mypy: allow-untyped-defs +"""Discover and run doctests in modules and test files.""" + +from __future__ import annotations + +import bdb +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Sequence +from contextlib import contextmanager +import functools +import inspect +import os +from pathlib import Path +import platform +import re +import sys +import traceback +import types +from typing import Any +from typing import TYPE_CHECKING +import warnings + +from _pytest import outcomes +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ReprFileLocation +from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter +from _pytest.compat import safe_getattr +from _pytest.config import Config +from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture +from _pytest.fixtures import TopRequest +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.outcomes import OutcomeException +from _pytest.outcomes import skip +from _pytest.pathlib import fnmatch_ex +from _pytest.python import Module +from _pytest.python_api import approx +from _pytest.warning_types import PytestWarning + + +if TYPE_CHECKING: + import doctest + + from typing_extensions import Self + +DOCTEST_REPORT_CHOICE_NONE = "none" +DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" +DOCTEST_REPORT_CHOICE_NDIFF = "ndiff" +DOCTEST_REPORT_CHOICE_UDIFF = "udiff" +DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure" + +DOCTEST_REPORT_CHOICES = ( + DOCTEST_REPORT_CHOICE_NONE, + DOCTEST_REPORT_CHOICE_CDIFF, + DOCTEST_REPORT_CHOICE_NDIFF, + DOCTEST_REPORT_CHOICE_UDIFF, + DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE, +) + +# Lazy definition of runner class +RUNNER_CLASS = None +# Lazy definition of output checker class +CHECKER_CLASS: type[doctest.OutputChecker] | None = None + + +def pytest_addoption(parser: Parser) -> None: + parser.addini( + "doctest_optionflags", + "Option flags for doctests", + type="args", + default=["ELLIPSIS"], + ) + parser.addini( + "doctest_encoding", "Encoding used for doctest files", default="utf-8" + ) + group = parser.getgroup("collect") + group.addoption( + "--doctest-modules", + action="store_true", + default=False, + help="Run doctests in all .py modules", + dest="doctestmodules", + ) + group.addoption( + "--doctest-report", + type=str.lower, + default="udiff", + help="Choose another output format for diffs on doctest failure", + choices=DOCTEST_REPORT_CHOICES, + dest="doctestreport", + ) + group.addoption( + "--doctest-glob", + action="append", + default=[], + metavar="pat", + help="Doctests file matching pattern, default: test*.txt", + dest="doctestglob", + ) + group.addoption( + "--doctest-ignore-import-errors", + action="store_true", + default=False, + help="Ignore doctest collection errors", + dest="doctest_ignore_import_errors", + ) + group.addoption( + "--doctest-continue-on-failure", + action="store_true", + default=False, + help="For a given doctest, continue to run after the first failure", + dest="doctest_continue_on_failure", + ) + + +def pytest_unconfigure() -> None: + global RUNNER_CLASS + + RUNNER_CLASS = None + + +def pytest_collect_file( + file_path: Path, + parent: Collector, +) -> DoctestModule | DoctestTextfile | None: + config = parent.config + if file_path.suffix == ".py": + if config.option.doctestmodules and not any( + (_is_setup_py(file_path), _is_main_py(file_path)) + ): + return DoctestModule.from_parent(parent, path=file_path) + elif _is_doctest(config, file_path, parent): + return DoctestTextfile.from_parent(parent, path=file_path) + return None + + +def _is_setup_py(path: Path) -> bool: + if path.name != "setup.py": + return False + contents = path.read_bytes() + return b"setuptools" in contents or b"distutils" in contents + + +def _is_doctest(config: Config, path: Path, parent: Collector) -> bool: + if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): + return True + globs = config.getoption("doctestglob") or ["test*.txt"] + return any(fnmatch_ex(glob, path) for glob in globs) + + +def _is_main_py(path: Path) -> bool: + return path.name == "__main__.py" + + +class ReprFailDoctest(TerminalRepr): + def __init__( + self, reprlocation_lines: Sequence[tuple[ReprFileLocation, Sequence[str]]] + ) -> None: + self.reprlocation_lines = reprlocation_lines + + def toterminal(self, tw: TerminalWriter) -> None: + for reprlocation, lines in self.reprlocation_lines: + for line in lines: + tw.line(line) + reprlocation.toterminal(tw) + + +class MultipleDoctestFailures(Exception): + def __init__(self, failures: Sequence[doctest.DocTestFailure]) -> None: + super().__init__() + self.failures = failures + + +def _init_runner_class() -> type[doctest.DocTestRunner]: + import doctest + + class PytestDoctestRunner(doctest.DebugRunner): + """Runner to collect failures. + + Note that the out variable in this case is a list instead of a + stdout-like object. + """ + + def __init__( + self, + checker: doctest.OutputChecker | None = None, + verbose: bool | None = None, + optionflags: int = 0, + continue_on_failure: bool = True, + ) -> None: + super().__init__(checker=checker, verbose=verbose, optionflags=optionflags) + self.continue_on_failure = continue_on_failure + + def report_failure( + self, + out, + test: doctest.DocTest, + example: doctest.Example, + got: str, + ) -> None: + failure = doctest.DocTestFailure(test, example, got) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + + def report_unexpected_exception( + self, + out, + test: doctest.DocTest, + example: doctest.Example, + exc_info: tuple[type[BaseException], BaseException, types.TracebackType], + ) -> None: + if isinstance(exc_info[1], OutcomeException): + raise exc_info[1] + if isinstance(exc_info[1], bdb.BdbQuit): + outcomes.exit("Quitting debugger") + failure = doctest.UnexpectedException(test, example, exc_info) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + + return PytestDoctestRunner + + +def _get_runner( + checker: doctest.OutputChecker | None = None, + verbose: bool | None = None, + optionflags: int = 0, + continue_on_failure: bool = True, +) -> doctest.DocTestRunner: + # We need this in order to do a lazy import on doctest + global RUNNER_CLASS + if RUNNER_CLASS is None: + RUNNER_CLASS = _init_runner_class() + # Type ignored because the continue_on_failure argument is only defined on + # PytestDoctestRunner, which is lazily defined so can't be used as a type. + return RUNNER_CLASS( # type: ignore + checker=checker, + verbose=verbose, + optionflags=optionflags, + continue_on_failure=continue_on_failure, + ) + + +class DoctestItem(Item): + def __init__( + self, + name: str, + parent: DoctestTextfile | DoctestModule, + runner: doctest.DocTestRunner, + dtest: doctest.DocTest, + ) -> None: + super().__init__(name, parent) + self.runner = runner + self.dtest = dtest + + # Stuff needed for fixture support. + self.obj = None + fm = self.session._fixturemanager + fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None) + self._fixtureinfo = fixtureinfo + self.fixturenames = fixtureinfo.names_closure + self._initrequest() + + @classmethod + def from_parent( # type: ignore[override] + cls, + parent: DoctestTextfile | DoctestModule, + *, + name: str, + runner: doctest.DocTestRunner, + dtest: doctest.DocTest, + ) -> Self: + # incompatible signature due to imposed limits on subclass + """The public named constructor.""" + return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) + + def _initrequest(self) -> None: + self.funcargs: dict[str, object] = {} + self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type] + + def setup(self) -> None: + self._request._fillfixtures() + globs = dict(getfixture=self._request.getfixturevalue) + for name, value in self._request.getfixturevalue("doctest_namespace").items(): + globs[name] = value + self.dtest.globs.update(globs) + + def runtest(self) -> None: + _check_all_skipped(self.dtest) + self._disable_output_capturing_for_darwin() + failures: list[doctest.DocTestFailure] = [] + # Type ignored because we change the type of `out` from what + # doctest expects. + self.runner.run(self.dtest, out=failures) # type: ignore[arg-type] + if failures: + raise MultipleDoctestFailures(failures) + + def _disable_output_capturing_for_darwin(self) -> None: + """Disable output capturing. Otherwise, stdout is lost to doctest (#985).""" + if platform.system() != "Darwin": + return + capman = self.config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture(in_=True) + out, err = capman.read_global_capture() + sys.stdout.write(out) + sys.stderr.write(err) + + # TODO: Type ignored -- breaks Liskov Substitution. + def repr_failure( # type: ignore[override] + self, + excinfo: ExceptionInfo[BaseException], + ) -> str | TerminalRepr: + import doctest + + failures: ( + Sequence[doctest.DocTestFailure | doctest.UnexpectedException] | None + ) = None + if isinstance( + excinfo.value, doctest.DocTestFailure | doctest.UnexpectedException + ): + failures = [excinfo.value] + elif isinstance(excinfo.value, MultipleDoctestFailures): + failures = excinfo.value.failures + + if failures is None: + return super().repr_failure(excinfo) + + reprlocation_lines = [] + for failure in failures: + example = failure.example + test = failure.test + filename = test.filename + if test.lineno is None: + lineno = None + else: + lineno = test.lineno + example.lineno + 1 + message = type(failure).__name__ + # TODO: ReprFileLocation doesn't expect a None lineno. + reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] + checker = _get_checker() + report_choice = _get_report_choice(self.config.getoption("doctestreport")) + if lineno is not None: + assert failure.test.docstring is not None + lines = failure.test.docstring.splitlines(False) + # add line numbers to the left of the error message + assert test.lineno is not None + lines = [ + f"{i + test.lineno + 1:03d} {x}" for (i, x) in enumerate(lines) + ] + # trim docstring error lines to 10 + lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] + else: + lines = [ + "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" + ] + indent = ">>>" + for line in example.source.splitlines(): + lines.append(f"??? {indent} {line}") + indent = "..." + if isinstance(failure, doctest.DocTestFailure): + lines += checker.output_difference( + example, failure.got, report_choice + ).split("\n") + else: + inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info) + lines += [f"UNEXPECTED EXCEPTION: {inner_excinfo.value!r}"] + lines += [ + x.strip("\n") for x in traceback.format_exception(*failure.exc_info) + ] + reprlocation_lines.append((reprlocation, lines)) + return ReprFailDoctest(reprlocation_lines) + + def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: + return self.path, self.dtest.lineno, f"[doctest] {self.name}" + + +def _get_flag_lookup() -> dict[str, int]: + import doctest + + return dict( + DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1, + DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE, + NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE, + ELLIPSIS=doctest.ELLIPSIS, + IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, + COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, + ALLOW_UNICODE=_get_allow_unicode_flag(), + ALLOW_BYTES=_get_allow_bytes_flag(), + NUMBER=_get_number_flag(), + ) + + +def get_optionflags(config: Config) -> int: + optionflags_str = config.getini("doctest_optionflags") + flag_lookup_table = _get_flag_lookup() + flag_acc = 0 + for flag in optionflags_str: + flag_acc |= flag_lookup_table[flag] + return flag_acc + + +def _get_continue_on_failure(config: Config) -> bool: + continue_on_failure: bool = config.getvalue("doctest_continue_on_failure") + if continue_on_failure: + # We need to turn off this if we use pdb since we should stop at + # the first failure. + if config.getvalue("usepdb"): + continue_on_failure = False + return continue_on_failure + + +class DoctestTextfile(Module): + obj = None + + def collect(self) -> Iterable[DoctestItem]: + import doctest + + # Inspired by doctest.testfile; ideally we would use it directly, + # but it doesn't support passing a custom checker. + encoding = self.config.getini("doctest_encoding") + text = self.path.read_text(encoding) + filename = str(self.path) + name = self.path.name + globs = {"__name__": "__main__"} + + optionflags = get_optionflags(self.config) + + runner = _get_runner( + verbose=False, + optionflags=optionflags, + checker=_get_checker(), + continue_on_failure=_get_continue_on_failure(self.config), + ) + + parser = doctest.DocTestParser() + test = parser.get_doctest(text, globs, name, filename, 0) + if test.examples: + yield DoctestItem.from_parent( + self, name=test.name, runner=runner, dtest=test + ) + + +def _check_all_skipped(test: doctest.DocTest) -> None: + """Raise pytest.skip() if all examples in the given DocTest have the SKIP + option set.""" + import doctest + + all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples) + if all_skipped: + skip("all tests skipped by +SKIP option") + + +def _is_mocked(obj: object) -> bool: + """Return if an object is possibly a mock object by checking the + existence of a highly improbable attribute.""" + return ( + safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None) + is not None + ) + + +@contextmanager +def _patch_unwrap_mock_aware() -> Generator[None]: + """Context manager which replaces ``inspect.unwrap`` with a version + that's aware of mock objects and doesn't recurse into them.""" + real_unwrap = inspect.unwrap + + def _mock_aware_unwrap( + func: Callable[..., Any], *, stop: Callable[[Any], Any] | None = None + ) -> Any: + try: + if stop is None or stop is _is_mocked: + return real_unwrap(func, stop=_is_mocked) + _stop = stop + return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func)) + except Exception as e: + warnings.warn( + f"Got {e!r} when unwrapping {func!r}. This is usually caused " + "by a violation of Python's object protocol; see e.g. " + "https://github.com/pytest-dev/pytest/issues/5080", + PytestWarning, + ) + raise + + inspect.unwrap = _mock_aware_unwrap + try: + yield + finally: + inspect.unwrap = real_unwrap + + +class DoctestModule(Module): + def collect(self) -> Iterable[DoctestItem]: + import doctest + + class MockAwareDocTestFinder(doctest.DocTestFinder): + py_ver_info_minor = sys.version_info[:2] + is_find_lineno_broken = ( + py_ver_info_minor < (3, 11) + or (py_ver_info_minor == (3, 11) and sys.version_info.micro < 9) + or (py_ver_info_minor == (3, 12) and sys.version_info.micro < 3) + ) + if is_find_lineno_broken: + + def _find_lineno(self, obj, source_lines): + """On older Pythons, doctest code does not take into account + `@property`. https://github.com/python/cpython/issues/61648 + + Moreover, wrapped Doctests need to be unwrapped so the correct + line number is returned. #8796 + """ + if isinstance(obj, property): + obj = getattr(obj, "fget", obj) + + if hasattr(obj, "__wrapped__"): + # Get the main obj in case of it being wrapped + obj = inspect.unwrap(obj) + + # Type ignored because this is a private function. + return super()._find_lineno( # type:ignore[misc] + obj, + source_lines, + ) + + if sys.version_info < (3, 13): + + def _from_module(self, module, object): + """`cached_property` objects are never considered a part + of the 'current module'. As such they are skipped by doctest. + Here we override `_from_module` to check the underlying + function instead. https://github.com/python/cpython/issues/107995 + """ + if isinstance(object, functools.cached_property): + object = object.func + + # Type ignored because this is a private function. + return super()._from_module(module, object) # type: ignore[misc] + + try: + module = self.obj + except Collector.CollectError: + if self.config.getvalue("doctest_ignore_import_errors"): + skip(f"unable to import module {self.path!r}") + else: + raise + + # doctests supports fixtures via `getfixture` and autouse. + self.session._fixturemanager.parsefactories(self) + + # Uses internal doctest module parsing mechanism. + finder = MockAwareDocTestFinder() + optionflags = get_optionflags(self.config) + runner = _get_runner( + verbose=False, + optionflags=optionflags, + checker=_get_checker(), + continue_on_failure=_get_continue_on_failure(self.config), + ) + + for test in finder.find(module, module.__name__): + if test.examples: # skip empty doctests + yield DoctestItem.from_parent( + self, name=test.name, runner=runner, dtest=test + ) + + +def _init_checker_class() -> type[doctest.OutputChecker]: + import doctest + + class LiteralsOutputChecker(doctest.OutputChecker): + # Based on doctest_nose_plugin.py from the nltk project + # (https://github.com/nltk/nltk) and on the "numtest" doctest extension + # by Sebastien Boisgerault (https://github.com/boisgera/numtest). + + _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) + _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) + _number_re = re.compile( + r""" + (?P + (?P + (?P [+-]?\d*)\.(?P\d+) + | + (?P [+-]?\d+)\. + ) + (?: + [Ee] + (?P [+-]?\d+) + )? + | + (?P [+-]?\d+) + (?: + [Ee] + (?P [+-]?\d+) + ) + ) + """, + re.VERBOSE, + ) + + def check_output(self, want: str, got: str, optionflags: int) -> bool: + if super().check_output(want, got, optionflags): + return True + + allow_unicode = optionflags & _get_allow_unicode_flag() + allow_bytes = optionflags & _get_allow_bytes_flag() + allow_number = optionflags & _get_number_flag() + + if not allow_unicode and not allow_bytes and not allow_number: + return False + + def remove_prefixes(regex: re.Pattern[str], txt: str) -> str: + return re.sub(regex, r"\1\2", txt) + + if allow_unicode: + want = remove_prefixes(self._unicode_literal_re, want) + got = remove_prefixes(self._unicode_literal_re, got) + + if allow_bytes: + want = remove_prefixes(self._bytes_literal_re, want) + got = remove_prefixes(self._bytes_literal_re, got) + + if allow_number: + got = self._remove_unwanted_precision(want, got) + + return super().check_output(want, got, optionflags) + + def _remove_unwanted_precision(self, want: str, got: str) -> str: + wants = list(self._number_re.finditer(want)) + gots = list(self._number_re.finditer(got)) + if len(wants) != len(gots): + return got + offset = 0 + for w, g in zip(wants, gots, strict=True): + fraction: str | None = w.group("fraction") + exponent: str | None = w.group("exponent1") + if exponent is None: + exponent = w.group("exponent2") + precision = 0 if fraction is None else len(fraction) + if exponent is not None: + precision -= int(exponent) + if float(w.group()) == approx(float(g.group()), abs=10**-precision): + # They're close enough. Replace the text we actually + # got with the text we want, so that it will match when we + # check the string literally. + got = ( + got[: g.start() + offset] + w.group() + got[g.end() + offset :] + ) + offset += w.end() - w.start() - (g.end() - g.start()) + return got + + return LiteralsOutputChecker + + +def _get_checker() -> doctest.OutputChecker: + """Return a doctest.OutputChecker subclass that supports some + additional options: + + * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b'' + prefixes (respectively) in string literals. Useful when the same + doctest should run in Python 2 and Python 3. + + * NUMBER to ignore floating-point differences smaller than the + precision of the literal number in the doctest. + + An inner class is used to avoid importing "doctest" at the module + level. + """ + global CHECKER_CLASS + if CHECKER_CLASS is None: + CHECKER_CLASS = _init_checker_class() + return CHECKER_CLASS() + + +def _get_allow_unicode_flag() -> int: + """Register and return the ALLOW_UNICODE flag.""" + import doctest + + return doctest.register_optionflag("ALLOW_UNICODE") + + +def _get_allow_bytes_flag() -> int: + """Register and return the ALLOW_BYTES flag.""" + import doctest + + return doctest.register_optionflag("ALLOW_BYTES") + + +def _get_number_flag() -> int: + """Register and return the NUMBER flag.""" + import doctest + + return doctest.register_optionflag("NUMBER") + + +def _get_report_choice(key: str) -> int: + """Return the actual `doctest` module flag value. + + We want to do it as late as possible to avoid importing `doctest` and all + its dependencies when parsing options, as it adds overhead and breaks tests. + """ + import doctest + + return { + DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF, + DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF, + DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF, + DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE, + DOCTEST_REPORT_CHOICE_NONE: 0, + }[key] + + +@fixture(scope="session") +def doctest_namespace() -> dict[str, Any]: + """Fixture that returns a :py:class:`dict` that will be injected into the + namespace of doctests. + + Usually this fixture is used in conjunction with another ``autouse`` fixture: + + .. code-block:: python + + @pytest.fixture(autouse=True) + def add_np(doctest_namespace): + doctest_namespace["np"] = numpy + + For more details: :ref:`doctest_namespace`. + """ + return dict() diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/faulthandler.py b/.venv-test/lib/python3.12/site-packages/_pytest/faulthandler.py new file mode 100644 index 0000000..080cf58 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/faulthandler.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from collections.abc import Generator +import os +import sys + +from _pytest.config import Config +from _pytest.config.argparsing import Parser +from _pytest.nodes import Item +from _pytest.stash import StashKey +import pytest + + +fault_handler_original_stderr_fd_key = StashKey[int]() +fault_handler_stderr_fd_key = StashKey[int]() + + +def pytest_addoption(parser: Parser) -> None: + help_timeout = ( + "Dump the traceback of all threads if a test takes " + "more than TIMEOUT seconds to finish" + ) + help_exit_on_timeout = ( + "Exit the test process if a test takes more than " + "faulthandler_timeout seconds to finish" + ) + parser.addini("faulthandler_timeout", help_timeout, default=0.0) + parser.addini( + "faulthandler_exit_on_timeout", help_exit_on_timeout, type="bool", default=False + ) + + +def pytest_configure(config: Config) -> None: + import faulthandler + + # at teardown we want to restore the original faulthandler fileno + # but faulthandler has no api to return the original fileno + # so here we stash the stderr fileno to be used at teardown + # sys.stderr and sys.__stderr__ may be closed or patched during the session + # so we can't rely on their values being good at that point (#11572). + stderr_fileno = get_stderr_fileno() + if faulthandler.is_enabled(): + config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno + config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno) + faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key]) + + +def pytest_unconfigure(config: Config) -> None: + import faulthandler + + faulthandler.disable() + # Close the dup file installed during pytest_configure. + if fault_handler_stderr_fd_key in config.stash: + os.close(config.stash[fault_handler_stderr_fd_key]) + del config.stash[fault_handler_stderr_fd_key] + # Re-enable the faulthandler if it was originally enabled. + if fault_handler_original_stderr_fd_key in config.stash: + faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key]) + del config.stash[fault_handler_original_stderr_fd_key] + + +def get_stderr_fileno() -> int: + try: + fileno = sys.stderr.fileno() + # The Twisted Logger will return an invalid file descriptor since it is not backed + # by an FD. So, let's also forward this to the same code path as with pytest-xdist. + if fileno == -1: + raise AttributeError() + return fileno + except (AttributeError, ValueError): + # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. + # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors + # This is potentially dangerous, but the best we can do. + assert sys.__stderr__ is not None + return sys.__stderr__.fileno() + + +def get_timeout_config_value(config: Config) -> float: + return float(config.getini("faulthandler_timeout") or 0.0) + + +def get_exit_on_timeout_config_value(config: Config) -> bool: + exit_on_timeout = config.getini("faulthandler_exit_on_timeout") + assert isinstance(exit_on_timeout, bool) + return exit_on_timeout + + +@pytest.hookimpl(wrapper=True, trylast=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: + timeout = get_timeout_config_value(item.config) + exit_on_timeout = get_exit_on_timeout_config_value(item.config) + if timeout > 0: + import faulthandler + + stderr = item.config.stash[fault_handler_stderr_fd_key] + faulthandler.dump_traceback_later(timeout, file=stderr, exit=exit_on_timeout) + try: + return (yield) + finally: + faulthandler.cancel_dump_traceback_later() + else: + return (yield) + + +@pytest.hookimpl(tryfirst=True) +def pytest_enter_pdb() -> None: + """Cancel any traceback dumping due to timeout before entering pdb.""" + import faulthandler + + faulthandler.cancel_dump_traceback_later() + + +@pytest.hookimpl(tryfirst=True) +def pytest_exception_interact() -> None: + """Cancel any traceback dumping due to an interactive exception being + raised.""" + import faulthandler + + faulthandler.cancel_dump_traceback_later() diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/fixtures.py b/.venv-test/lib/python3.12/site-packages/_pytest/fixtures.py new file mode 100644 index 0000000..f4ca2ea --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/fixtures.py @@ -0,0 +1,2393 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import abc +from collections import defaultdict +from collections import deque +from collections import OrderedDict +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import MutableMapping +from collections.abc import Sequence +from collections.abc import Set as AbstractSet +import dataclasses +import functools +import inspect +import os +from pathlib import Path +import sys +import types +from typing import Any +from typing import cast +from typing import Final +from typing import final +from typing import Generic +from typing import Literal +from typing import NoReturn +from typing import overload +from typing import TYPE_CHECKING +from typing import TypeVar +import warnings + +import _pytest +from _pytest import nodes +from _pytest._code import getfslineno +from _pytest._code import Source +from _pytest._code.code import ExceptionInfoFormatter +from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter +from _pytest.compat import assert_never +from _pytest.compat import deprecated +from _pytest.compat import get_real_func +from _pytest.compat import getfuncargnames +from _pytest.compat import getimfunc +from _pytest.compat import getlocation +from _pytest.compat import NOTSET +from _pytest.compat import NotSetType +from _pytest.compat import safe_getattr +from _pytest.compat import safe_isclass +from _pytest.compat import signature +from _pytest.config import _PluggyPlugin +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.deprecated import CLASS_FIXTURE_INSTANCE_METHOD +from _pytest.deprecated import FIXTURE_BASEID_DEPRECATED +from _pytest.deprecated import FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN +from _pytest.deprecated import FIXTURE_NODEID_DEPRECATED +from _pytest.deprecated import FIXTUREDEF_HAS_LOCATION_DEPRECATED +from _pytest.deprecated import PARSEFACTORIES_NODEID_DEPRECATED +from _pytest.deprecated import YIELD_FIXTURE +from _pytest.main import Session +from _pytest.mark import Mark +from _pytest.mark import ParameterSet +from _pytest.mark.structures import MarkDecorator +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.outcomes import TEST_OUTCOME +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath +from _pytest.scope import HIGH_SCOPES +from _pytest.scope import Scope +from _pytest.scope import ScopeName +from _pytest.warning_types import PytestWarning + + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + + +if TYPE_CHECKING: + from _pytest.python import CallSpec2 + from _pytest.python import Function + from _pytest.python import Metafunc + from _pytest.reports import CollectReport + + +# The value of the fixture -- return/yield of the fixture function (type variable). +FixtureValue = TypeVar("FixtureValue", covariant=True) +# The type of the fixture function (type alias). +FixtureFunction = Callable[..., object] +# The type of a fixture function (type alias generic in fixture value). +_FixtureFunc = Callable[..., FixtureValue] | Callable[..., Generator[FixtureValue]] +# The type of FixtureDef.cached_result (type alias generic in fixture value). +_FixtureCachedResult = ( + tuple[ + # The result. + FixtureValue, + # Cache key. + object, + None, + ] + | tuple[ + None, + # Cache key. + object, + # The exception and the original traceback. + tuple[BaseException, types.TracebackType | None], + ] +) + + +def pytest_sessionstart(session: Session) -> None: + session._fixturemanager = FixtureManager(session) + + +def get_scope_package( + node: nodes.Item, + fixturedef: FixtureDef[object], +) -> nodes.Node | None: + from _pytest.python import Package + + for parent in node.iter_parents(): + if isinstance(parent, Package): + if fixturedef.node is not None: + if parent == fixturedef.node: + return parent + else: + if parent.nodeid == fixturedef.baseid: + return parent + return node.session + + +def is_visibility_more_specific( + candidate: FixtureDef[Any], other: FixtureDef[Any] +) -> bool: + """Return whether the visibility of ``candidate`` is strictly more specific + than that of ``other``, i.e. ``candidate`` is defined on a strict descendant + in the collection tree of where ``other`` is defined.""" + if candidate.node is None or other.node is None: + # Fallback for fixtures registered with a string nodeid (deprecated). + # In this case compare baseids, which are nodeid prefixes. + # This branch can be removed once baseid deprecation is done (pytest 10). + if candidate.baseid == other.baseid: + return False + if other.baseid == "": + return True + # `candidate.baseid` must continue with a node separator for it to be a + # true descendant. + return candidate.baseid.startswith(other.baseid) and candidate.baseid[ + len(other.baseid) + ] in ("/", ":") + + return ( + candidate.node is not other.node and other.node in candidate.node.iter_parents() + ) + + +def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: + """Get the closest parent node (including self) which matches the given + scope. + + If there is no parent node for the scope (e.g. asking for class scope on a + Module, or on a Function when not defined in a class), returns None. + """ + import _pytest.python + + if scope is Scope.Function: + # Type ignored because this is actually safe, see: + # https://github.com/python/mypy/issues/4717 + return node.getparent(nodes.Item) # type: ignore[type-abstract] + elif scope is Scope.Class: + return node.getparent(_pytest.python.Class) + elif scope is Scope.Module: + return node.getparent(_pytest.python.Module) + elif scope is Scope.Package: + return node.getparent(_pytest.python.Package) + elif scope is Scope.Session: + return node.getparent(_pytest.main.Session) + else: + assert_never(scope) + + +# TODO: Try to use FixtureFunctionDefinition instead of the marker +def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: + """Return fixturemarker or None if it doesn't exist""" + if isinstance(obj, FixtureFunctionDefinition): + return obj._fixture_function_marker + return None + + +# Algorithm for sorting on a per-parametrized resource setup basis. +# It is called for Session scope first and performs sorting +# down to the lower scopes such as to minimize number of "high scope" +# setups and teardowns. + + +@dataclasses.dataclass(frozen=True) +class ParamArgKey: + """A key for a high-scoped parameter used by an item. + + For use as a hashable key in `reorder_items`. The combination of fields + is meant to uniquely identify a particular "instance" of a param, + potentially shared by multiple items in a scope. + """ + + #: The param name. + argname: str + param_index: int + #: For scopes Package, Module, Class, the path to the file (directory in + #: Package's case) of the package/module/class where the item is defined. + scoped_item_path: Path | None + #: For Class scope, the class where the item is defined. + item_cls: type | None + + +_V = TypeVar("_V") +OrderedSet = dict[_V, None] + + +def get_param_argkeys(item: nodes.Item, scope: Scope) -> Iterator[ParamArgKey]: + """Return all ParamArgKeys for item matching the specified high scope.""" + assert scope is not Scope.Function + + try: + callspec: CallSpec2 = item.callspec # type: ignore[attr-defined] + except AttributeError: + return + + item_cls = None + if scope is Scope.Session: + scoped_item_path = None + elif scope is Scope.Package: + # Package key = module's directory. + scoped_item_path = item.path.parent + elif scope is Scope.Module: + scoped_item_path = item.path + elif scope is Scope.Class: + scoped_item_path = item.path + item_cls = item.cls # type: ignore[attr-defined] + else: + assert_never(scope) + + for argname in callspec.indices: + if callspec._arg2scope[argname] != scope: + continue + param_index = callspec.indices[argname] + yield ParamArgKey(argname, param_index, scoped_item_path, item_cls) + + +def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]: + argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[ParamArgKey]]] = {} + items_by_argkey: dict[Scope, dict[ParamArgKey, OrderedDict[nodes.Item, None]]] = {} + for scope in HIGH_SCOPES: + scoped_argkeys_by_item = argkeys_by_item[scope] = {} + scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict) + for item in items: + argkeys = dict.fromkeys(get_param_argkeys(item, scope)) + if argkeys: + scoped_argkeys_by_item[item] = argkeys + for argkey in argkeys: + scoped_items_by_argkey[argkey][item] = None + + items_set = dict.fromkeys(items) + return list( + reorder_items_atscope( + items_set, argkeys_by_item, items_by_argkey, Scope.Session + ) + ) + + +def reorder_items_atscope( + items: OrderedSet[nodes.Item], + argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[ParamArgKey]]], + items_by_argkey: Mapping[ + Scope, Mapping[ParamArgKey, OrderedDict[nodes.Item, None]] + ], + scope: Scope, +) -> OrderedSet[nodes.Item]: + if scope is Scope.Function or len(items) < 3: + return items + + scoped_items_by_argkey = items_by_argkey[scope] + scoped_argkeys_by_item = argkeys_by_item[scope] + + ignore: set[ParamArgKey] = set() + items_deque = deque(items) + items_done: OrderedSet[nodes.Item] = {} + while items_deque: + no_argkey_items: OrderedSet[nodes.Item] = {} + slicing_argkey = None + while items_deque: + item = items_deque.popleft() + if item in items_done or item in no_argkey_items: + continue + argkeys = dict.fromkeys( + k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore + ) + if not argkeys: + no_argkey_items[item] = None + else: + slicing_argkey, _ = argkeys.popitem() + # We don't have to remove relevant items from later in the + # deque because they'll just be ignored. + matching_items = [ + i for i in scoped_items_by_argkey[slicing_argkey] if i in items + ] + for i in reversed(matching_items): + items_deque.appendleft(i) + # Fix items_by_argkey order. + for other_scope in HIGH_SCOPES: + other_scoped_items_by_argkey = items_by_argkey[other_scope] + for argkey in argkeys_by_item[other_scope].get(i, ()): + argkey_dict = other_scoped_items_by_argkey[argkey] + if not hasattr(sys, "pypy_version_info"): + argkey_dict[i] = None + argkey_dict.move_to_end(i, last=False) + else: + # Work around a bug in PyPy: + # https://github.com/pypy/pypy/issues/5257 + # https://github.com/pytest-dev/pytest/issues/13312 + bkp = argkey_dict.copy() + argkey_dict.clear() + argkey_dict[i] = None + argkey_dict.update(bkp) + break + if no_argkey_items: + reordered_no_argkey_items = reorder_items_atscope( + no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower() + ) + items_done.update(reordered_no_argkey_items) + if slicing_argkey is not None: + ignore.add(slicing_argkey) + return items_done + + +def traverse_fixture_closure( + initialnames: Iterable[str], + *, + getfixturedefs: Callable[[str], Sequence[FixtureDef[Any]] | None], +) -> Iterator[str]: + """Statically traverse the fixture dependency closure in DFS order starting + from initialnames, yielding all requested fixture names (argnames). + + Each argname is only yielded once. + """ + # Track the index for each fixture name in the simulated stack. + # Needed for handling override chains correctly, similar to + # FixtureRequest._get_active_fixturedef. + # Using negative indices: -1 is the most specific (last), -2 is second to + # last, etc. + current_indices: dict[str, int] = {} + + def process_argname(argname: str) -> Iterator[str]: + index = current_indices.get(argname) + + # Optimization: already processed this argname. + if index == -1: + return + + # Only yield each argname once. + if index is None: + yield argname + current_indices[argname] = -1 + + fixturedefs = getfixturedefs(argname) + if not fixturedefs: + return + + index = current_indices.get(argname, -1) + if -index > len(fixturedefs): + # Exhausted the override chain (will error during runtest). + return + fixturedef = fixturedefs[index] + + current_indices[argname] = index - 1 + for dep in fixturedef.argnames: + yield from process_argname(dep) + current_indices[argname] = index + + for argname in initialnames: + yield from process_argname(argname) + + +@dataclasses.dataclass(frozen=True) +class FuncFixtureInfo: + """Fixture-related information for a fixture-requesting item (e.g. test + function). + + This is used to examine the fixtures which an item requests statically + (known during collection). This includes autouse fixtures, fixtures + requested by the `usefixtures` marker, fixtures requested in the function + parameters, and the transitive closure of these. + + An item may also request fixtures dynamically (using `request.getfixturevalue`); + these are not reflected here. + """ + + __slots__ = ("argnames", "initialnames", "name2fixturedefs", "names_closure") + + # Fixture names that the item requests directly by function parameters. + argnames: tuple[str, ...] + # Fixture names that the item immediately requires. These include + # argnames + fixture names specified via usefixtures and via autouse=True in + # fixture definitions. + initialnames: tuple[str, ...] + # The transitive closure of the fixture names that the item requires. + # Note: can't include dynamic dependencies (`request.getfixturevalue` calls). + names_closure: list[str] + # A map from a fixture name in the transitive closure to the FixtureDefs + # matching the name which are applicable to this function. + # There may be multiple overriding fixtures with the same name. The + # sequence is ordered from furthest to closes to the function. + name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] + + def prune_dependency_tree(self) -> None: + """Recompute names_closure from initialnames and name2fixturedefs. + + Can only reduce names_closure, which means that the new closure will + always be a subset of the old one. The order is preserved. + + This method is needed because direct parametrization may shadow some + of the fixtures that were included in the originally built dependency + tree. In this way the dependency tree can get pruned, and the closure + of argnames may get reduced. + """ + closure = set( + traverse_fixture_closure( + self.initialnames, + getfixturedefs=self.name2fixturedefs.get, + ) + ) + self.names_closure[:] = (name for name in self.names_closure if name in closure) + + +class FixtureRequest(abc.ABC): + """The type of the ``request`` fixture. + + A request object gives access to the requesting test context and has a + ``param`` attribute in case the fixture is parametrized. + """ + + def __init__( + self, + pyfuncitem: Function, + fixturename: str | None, + arg2fixturedefs: Mapping[str, Sequence[FixtureDef[Any]]], + fixture_defs: dict[str, FixtureDef[Any]], + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + #: Fixture for which this request is being performed. + self.fixturename: Final = fixturename + self._pyfuncitem: Final = pyfuncitem + # The FixtureDefs for each fixture name statically requested by this + # item (computed during collection). Dynamically requested fixtures + # (using `request.getfixturevalue("foo")`) are not included here. + self._arg2fixturedefs: Final = arg2fixturedefs + # The evaluated argnames so far, mapping to the FixtureDef they resolved + # to. + self._fixture_defs: Final = fixture_defs + # Notes on the type of `param`: + # -`request.param` is only defined in parametrized fixtures, and will raise + # AttributeError otherwise. Python typing has no notion of "undefined", so + # this cannot be reflected in the type. + # - Technically `param` is only (possibly) defined on SubRequest, not + # FixtureRequest, but the typing of that is still in flux so this cheats. + # - In the future we might consider using a generic for the param type, but + # for now just using Any. + self.param: Any + + @property + def _fixturemanager(self) -> FixtureManager: + return self._pyfuncitem.session._fixturemanager + + @property + @abc.abstractmethod + def _scope(self) -> Scope: + raise NotImplementedError() + + @property + def scope(self) -> ScopeName: + """Scope string, one of "function", "class", "module", "package", "session".""" + return self._scope.value + + @abc.abstractmethod + def _check_scope( + self, + requested_fixturedef: FixtureDef[object], + requested_scope: Scope, + ) -> None: + raise NotImplementedError() + + @property + def fixturenames(self) -> list[str]: + """Names of all active fixtures in this request.""" + result = list(self._pyfuncitem.fixturenames) + result.extend(set(self._fixture_defs).difference(result)) + return result + + @property + @abc.abstractmethod + def node(self): + """Underlying collection node (depends on current request scope).""" + raise NotImplementedError() + + @property + def config(self) -> Config: + """The pytest config object associated with this request.""" + return self._pyfuncitem.config + + @property + def function(self): + """Test function object if the request has a per-function scope.""" + if self.scope != "function": + raise AttributeError( + f"function not available in {self.scope}-scoped context" + ) + return self._pyfuncitem.obj + + @property + def cls(self): + """Class (can be None) where the test function was collected.""" + if self.scope not in ("class", "function"): + raise AttributeError(f"cls not available in {self.scope}-scoped context") + clscol = self._pyfuncitem.getparent(_pytest.python.Class) + if clscol: + return clscol.obj + + @property + def instance(self): + """Instance (can be None) on which test function was collected.""" + if self.scope != "function": + return None + return getattr(self._pyfuncitem, "instance", None) + + @property + def module(self): + """Python module object where the test function was collected.""" + if self.scope not in ("function", "class", "module"): + raise AttributeError(f"module not available in {self.scope}-scoped context") + mod = self._pyfuncitem.getparent(_pytest.python.Module) + assert mod is not None + return mod.obj + + @property + def path(self) -> Path: + """Path where the test function was collected.""" + if self.scope not in ("function", "class", "module", "package"): + raise AttributeError(f"path not available in {self.scope}-scoped context") + return self._pyfuncitem.path + + @property + def keywords(self) -> MutableMapping[str, Any]: + """Keywords/markers dictionary for the underlying node.""" + node: nodes.Node = self.node + return node.keywords + + @property + def session(self) -> Session: + """Pytest session object.""" + return self._pyfuncitem.session + + @abc.abstractmethod + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + """Add finalizer/teardown function to be called without arguments after + the last test within the requesting test context finished execution.""" + raise NotImplementedError() + + def applymarker(self, marker: str | MarkDecorator) -> None: + """Apply a marker to a single test function invocation. + + This method is useful if you don't want to have a keyword/marker + on all function invocations. + + :param marker: + An object created by a call to ``pytest.mark.NAME(...)``. + """ + self.node.add_marker(marker) + + def raiseerror(self, msg: str | None) -> NoReturn: + """Raise a FixtureLookupError exception. + + :param msg: + An optional custom error message. + """ + raise FixtureLookupError(None, self, msg) + + def _raise_teardown_lookup_error(self, argname: str) -> NoReturn: + msg = ( + f'The fixture value for "{argname}" is not available during teardown ' + "because it was not previously requested.\n" + "Only fixtures that were already active can be retrieved during teardown.\n" + "Request the fixture before teardown begins by declaring it in the fixture " + "signature or by calling request.getfixturevalue() before the fixture yields." + ) + raise FixtureLookupError(argname, self, msg) + + def getfixturevalue(self, argname: str) -> Any: + """Dynamically run a named fixture function. + + Declaring fixtures via function argument is recommended where possible. + But if you can only decide whether to use another fixture at test + setup time, you may use this function to retrieve it inside a fixture + or test function body. + + This method can be used during the test setup phase or the test run + phase. Avoid using it during the teardown phase. + + .. versionchanged:: 9.1 + Calling ``request.getfixturevalue()`` during teardown to request a + fixture that was not already requested + :ref:`is deprecated `. + + :param argname: + The fixture name. + :raises pytest.FixtureLookupError: + If the given fixture could not be found. + """ + # Note that in addition to the use case described in the docstring, + # getfixturevalue() is also called by pytest itself during item and fixture + # setup to evaluate the fixtures that are requested statically + # (using function parameters, autouse, etc). + + fixturedef = self._get_active_fixturedef(argname) + assert fixturedef.cached_result is not None, ( + f'The fixture value for "{argname}" is not available. ' + "This can happen when the fixture has already been torn down." + ) + return fixturedef.cached_result[0] + + def _iter_chain(self) -> Iterator[SubRequest]: + """Yield all SubRequests in the chain, from self up. + + Note: does *not* yield the TopRequest. + """ + current = self + while isinstance(current, SubRequest): + yield current + current = current._parent_request + + def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]: + if argname == "request": + return RequestFixtureDef(self) + + # If we already finished computing a fixture by this name in this item, + # return it. + fixturedef = self._fixture_defs.get(argname) + if fixturedef is not None: + self._check_scope(fixturedef, fixturedef._scope) + return fixturedef + + # Find the appropriate fixturedef. + fixturedefs = self._arg2fixturedefs.get(argname, None) + if fixturedefs is None: + # We arrive here because of a dynamic call to + # getfixturevalue(argname) which was naturally + # not known at parsing/collection time. + fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem) + # No fixtures defined with this name. + if fixturedefs is None: + raise FixtureLookupError(argname, self) + # The are no fixtures with this name applicable for the function. + if not fixturedefs: + raise FixtureLookupError(argname, self) + + # A fixture may override another fixture with the same name, e.g. a + # fixture in a module can override a fixture in a conftest, a fixture in + # a class can override a fixture in the module, and so on. + # An overriding fixture can request its own name (possibly indirectly); + # in this case it gets the value of the fixture it overrides, one level + # up. + # Check how many `argname`s deep we are, and take the next one. + # `fixturedefs` is sorted from furthest to closest, so use negative + # indexing to go in reverse. + index = -1 + for request in self._iter_chain(): + if request.fixturename == argname: + index -= 1 + # If already consumed all of the available levels, fail. + if -index > len(fixturedefs): + raise FixtureLookupError(argname, self) + fixturedef = fixturedefs[index] + + # Prepare a SubRequest object for calling the fixture. + try: + callspec = self._pyfuncitem.callspec + except AttributeError: + callspec = None + if callspec is not None and argname in callspec.params: + param = callspec.params[argname] + param_index = callspec.indices[argname] + # The parametrize invocation scope overrides the fixture's scope. + scope = callspec._arg2scope[argname] + else: + param = NOTSET + param_index = 0 + scope = fixturedef._scope + self._check_fixturedef_without_param(fixturedef) + # The parametrize invocation scope only controls caching behavior while + # allowing wider-scoped fixtures to keep depending on the parametrized + # fixture. Scope control is enforced for parametrized fixtures + # by recreating the whole fixture tree on parameter change. + # Hence `fixturedef._scope`, not `scope`. + self._check_scope(fixturedef, fixturedef._scope) + subrequest = SubRequest( + self, scope, param, param_index, fixturedef, _ispytest=True + ) + + if not self.session._setupstate.is_node_active(self.node): + # TODO(pytest10.1): Remove the `warn` and `if` and call + # _raise_teardown_lookup_error unconditionally. + warnings.warn( + FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN.format(argname=argname), + stacklevel=3, + ) + if subrequest.node not in self.session._setupstate.stack: + self._raise_teardown_lookup_error(argname) + + # Make sure the fixture value is cached, running it if it isn't + fixturedef.execute(request=subrequest) + + self._fixture_defs[argname] = fixturedef + return fixturedef + + def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None: + """Check that this request is allowed to execute this fixturedef without + a param.""" + funcitem = self._pyfuncitem + has_params = fixturedef.params is not None + fixtures_not_supported = getattr(funcitem, "nofuncargs", False) + if has_params and fixtures_not_supported: + msg = ( + f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n" + f"Node id: {funcitem.nodeid}\n" + f"Function type: {type(funcitem).__name__}" + ) + fail(msg, pytrace=False) + if has_params: + frame = inspect.stack()[3] + frameinfo = inspect.getframeinfo(frame[0]) + source_path = absolutepath(frameinfo.filename) + source_lineno = frameinfo.lineno + try: + source_path_str = str(source_path.relative_to(funcitem.config.rootpath)) + except ValueError: + source_path_str = str(source_path) + location = getlocation(fixturedef.func, funcitem.config.rootpath) + msg = ( + "The requested fixture has no parameter defined for test:\n" + f" {funcitem.nodeid}\n\n" + f"Requested fixture '{fixturedef.argname}' defined in:\n" + f"{location}\n\n" + f"Requested here:\n" + f"{source_path_str}:{source_lineno}" + ) + fail(msg, pytrace=False) + + def _get_fixturestack(self) -> list[FixtureDef[Any]]: + values = [request._fixturedef for request in self._iter_chain()] + values.reverse() + return values + + +@final +class TopRequest(FixtureRequest): + """The type of the ``request`` fixture in a test function.""" + + def __init__(self, pyfuncitem: Function, *, _ispytest: bool = False) -> None: + super().__init__( + fixturename=None, + pyfuncitem=pyfuncitem, + arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs, + fixture_defs={}, + _ispytest=_ispytest, + ) + + @property + def _scope(self) -> Scope: + return Scope.Function + + def _check_scope( + self, + requested_fixturedef: FixtureDef[object], + requested_scope: Scope, + ) -> None: + # TopRequest always has function scope so always valid. + pass + + @property + def node(self): + return self._pyfuncitem + + def __repr__(self) -> str: + return f"" + + def _fillfixtures(self) -> None: + item = self._pyfuncitem + for argname in item.fixturenames: + if argname not in item.funcargs: + item.funcargs[argname] = self.getfixturevalue(argname) + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + self.node.addfinalizer(finalizer) + + +@final +class SubRequest(FixtureRequest): + """The type of the ``request`` fixture in a fixture function requested + (transitively) by a test function.""" + + def __init__( + self, + request: FixtureRequest, + scope: Scope, + param: Any, + param_index: int, + fixturedef: FixtureDef[object], + *, + _ispytest: bool = False, + ) -> None: + super().__init__( + pyfuncitem=request._pyfuncitem, + fixturename=fixturedef.argname, + fixture_defs=request._fixture_defs, + arg2fixturedefs=request._arg2fixturedefs, + _ispytest=_ispytest, + ) + self._parent_request: Final[FixtureRequest] = request + self._fixturedef: Final[FixtureDef[object]] = fixturedef + if param is not NOTSET: + self.param = param + self.param_index: Final = param_index + self._scope_field: Final = scope + if scope is Scope.Function: + # This might also be a non-function Item despite its attribute name. + node: nodes.Node | None = self._pyfuncitem + elif scope is Scope.Package: + node = get_scope_package(self._pyfuncitem, self._fixturedef) + else: + node = get_scope_node(self._pyfuncitem, scope) + if node is None and scope is Scope.Class: + # Fallback to function item itself. + node = self._pyfuncitem + assert node, ( + f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}' + ) + self._node: Final = node + + def __repr__(self) -> str: + return f"" + + @property + def _scope(self) -> Scope: + return self._scope_field + + @property + def node(self): + return self._node + + def _check_scope( + self, + requested_fixturedef: FixtureDef[object], + requested_scope: Scope, + ) -> None: + if self._scope > requested_scope: + # Try to report something helpful. + argname = requested_fixturedef.argname + fixture_stack = "\n".join( + self._format_fixturedef_line(fixturedef) + for fixturedef in self._get_fixturestack() + ) + requested_fixture = self._format_fixturedef_line(requested_fixturedef) + fail( + f"ScopeMismatch: You tried to access the {requested_scope.value} scoped " + f"fixture {argname} with a {self._scope.value} scoped request object. " + f"Requesting fixture stack:\n{fixture_stack}\n" + f"Requested fixture:\n{requested_fixture}", + pytrace=False, + ) + + def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str: + factory = fixturedef.func + path, lineno = getfslineno(factory) + if isinstance(path, Path): + path = bestrelpath(self._pyfuncitem.session.path, path) + sig = signature(factory) + return f"{path}:{lineno + 1}: def {factory.__name__}{sig}" + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + self._fixturedef.addfinalizer(finalizer) + + +@final +class FixtureLookupError(LookupError): + """Could not return a requested fixture (missing or invalid).""" + + def __init__( + self, argname: str | None, request: FixtureRequest, msg: str | None = None + ) -> None: + self.argname = argname + self.request = request + self.fixturestack = request._get_fixturestack() + self.msg = msg + + def formatrepr(self) -> FixtureLookupErrorRepr: + tblines: list[str] = [] + addline = tblines.append + stack = [self.request._pyfuncitem.obj] + stack.extend(map(lambda x: x.func, self.fixturestack)) + msg = self.msg + if msg is not None and len(stack) > 1: + # The last fixture raise an error, let's present + # it at the requesting side. + stack = stack[:-1] + for function in stack: + fspath, lineno = getfslineno(function) + try: + lines, _ = inspect.getsourcelines(get_real_func(function)) + except (OSError, IndexError, TypeError): + error_msg = "file %s, line %s: source code not available" + addline(error_msg % (fspath, lineno + 1)) + else: + addline(f"file {fspath}, line {lineno + 1}") + for i, line in enumerate(lines): + line = line.rstrip() + addline(" " + line) + if line.lstrip().startswith("def"): + break + + if msg is None: + fm = self.request._fixturemanager + available = set() + parent = self.request._pyfuncitem.parent + assert parent is not None + for name, fixturedefs in fm._arg2fixturedefs.items(): + faclist = list(fm._matchfactories(fixturedefs, parent)) + if faclist: + available.add(name) + if self.argname in available: + msg = ( + f" recursive dependency involving fixture '{self.argname}' detected" + ) + else: + msg = f"fixture '{self.argname}' not found" + msg += "\n available fixtures: {}".format(", ".join(sorted(available))) + msg += "\n use 'pytest --fixtures [testpath]' for help on them." + + return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname) + + +class FixtureLookupErrorRepr(TerminalRepr): + def __init__( + self, + filename: str | os.PathLike[str], + firstlineno: int, + tblines: Sequence[str], + errorstring: str, + argname: str | None, + ) -> None: + self.tblines = tblines + self.errorstring = errorstring + self.filename = filename + self.firstlineno = firstlineno + self.argname = argname + + def toterminal(self, tw: TerminalWriter) -> None: + # tw.line("FixtureLookupError: %s" %(self.argname), red=True) + for tbline in self.tblines: + tw.line(tbline.rstrip()) + lines = self.errorstring.split("\n") + if lines: + tw.line( + f"{ExceptionInfoFormatter.fail_marker} {lines[0].strip()}", + red=True, + ) + for line in lines[1:]: + tw.line( + f"{ExceptionInfoFormatter.flow_marker} {line.strip()}", + red=True, + ) + tw.line() + tw.line(f"{os.fspath(self.filename)}:{self.firstlineno + 1}") + + +def call_fixture_func( + fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs +) -> FixtureValue: + if inspect.isgeneratorfunction(fixturefunc): + fixturefunc = cast(Callable[..., Generator[FixtureValue]], fixturefunc) + generator = fixturefunc(**kwargs) + try: + fixture_result = next(generator) + except StopIteration: + raise ValueError(f"{request.fixturename} did not yield a value") from None + finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) + request.addfinalizer(finalizer) + else: + fixturefunc = cast(Callable[..., FixtureValue], fixturefunc) + fixture_result = fixturefunc(**kwargs) + return fixture_result + + +def _teardown_yield_fixture(fixturefunc, it) -> None: + """Execute the teardown of a fixture function by advancing the iterator + after the yield and ensure the iteration ends (if not it means there is + more than one yield in the function).""" + try: + next(it) + except StopIteration: + pass + else: + fs, lineno = getfslineno(fixturefunc) + fail( + f"fixture function has more than one 'yield':\n\n" + f"{Source(fixturefunc).indent()}\n" + f"{fs}:{lineno + 1}", + pytrace=False, + ) + + +def _eval_scope_callable( + scope_callable: Callable[[str, Config], ScopeName], + fixture_name: str, + config: Config, +) -> ScopeName: + try: + # Type ignored because there is no typing mechanism to specify + # keyword arguments, currently. + result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg] + except Exception as e: + raise TypeError( + f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n" + "Expected a function with the signature (*, fixture_name, config)" + ) from e + if not isinstance(result, str): + fail( + f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n" + f"{result!r}", + pytrace=False, + ) + return result + + +class FixtureDef(Generic[FixtureValue]): + """A container for a fixture definition. + + Note: At this time, only explicitly documented fields and methods are + considered public stable API. + """ + + def __init__( + self, + config: Config, + baseid: str | None | NotSetType, + argname: str, + func: _FixtureFunc[FixtureValue], + scope: Scope | ScopeName | Callable[[str, Config], ScopeName] | None, + params: Sequence[object] | None, + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, + *, + node: nodes.Node | NotSetType = NOTSET, + # only used in a deprecationwarning msg, can be removed in pytest9 + _autouse: bool = False, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + # Emit deprecation warning if deprecated baseid string is used. + if node is NOTSET: + warnings.warn(FIXTURE_BASEID_DEPRECATED, stacklevel=2) + if baseid is NOTSET: + baseid = None + # The node where this fixture was defined, if available. + # Used for node-based matching which is more robust than string matching. + self.node: Final = node if node is not NOTSET else None + # The "base" node ID for the fixture. + # + # This is a node ID prefix. A fixture is only available to a node (e.g. + # a `Function` item) if the fixture's baseid is a nodeid of a parent of + # node. + # + # For a fixture found in a Collector's object (e.g. a `Module`s module, + # a `Class`'s class), the baseid is the Collector's nodeid. + # + # For a fixture found in a conftest plugin, the baseid is the conftest's + # directory path relative to the rootdir. + # + # For other plugins, the baseid is the empty string (always matches). + # When node is available, baseid is derived from node.nodeid. + # + # Deprecated: replaced by ``node``. + self.baseid: Final = node.nodeid if node is not NOTSET else (baseid or "") + # Whether the fixture was found from a node or a conftest in the + # collection tree. Will be false for fixtures defined in non-conftest + # plugins. + # + # Deprecated: kept only to back the deprecated ``has_location`` property. + self._has_location: Final = node is not NOTSET or baseid is not None + # The fixture factory function. + self.func: Final = func + # The name by which the fixture may be requested. + self.argname: Final = argname + if scope is None: + scope = Scope.Function + elif callable(scope): + scope = _eval_scope_callable(scope, argname, config) + if isinstance(scope, str): + scope = Scope.from_user( + scope, descr=f"Fixture '{func.__name__}'", where=self.baseid + ) + self._scope: Final = scope + # If the fixture is directly parametrized, the parameter values. + self.params: Final = params + # If the fixture is directly parametrized, a tuple of explicit IDs to + # assign to the parameter values, or a callable to generate an ID given + # a parameter value. + self.ids: Final = ids + # The names requested by the fixtures. + self.argnames: Final = getfuncargnames(func, name=argname) + # If the fixture was executed, the current value of the fixture. + # Can change if the fixture is executed with different parameters. + self.cached_result: _FixtureCachedResult[FixtureValue] | None = None + self._finalizers: Final[list[Callable[[], object]]] = [] + + # only used to emit a deprecationwarning, can be removed in pytest9 + self._autouse = _autouse + + @property + def scope(self) -> ScopeName: + """Scope string, one of "function", "class", "module", "package", "session".""" + return self._scope.value + + @property + def has_location(self) -> bool: + warnings.warn(FIXTUREDEF_HAS_LOCATION_DEPRECATED, stacklevel=2) + return self._has_location + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + self._finalizers.append(finalizer) + + def finish(self, request: SubRequest) -> None: + if self.cached_result is None: + # Already finished. It is assumed that finalizers cannot be added in + # this state. + return + + exceptions: list[BaseException] = [] + while self._finalizers: + fin = self._finalizers.pop() + try: + fin() + except BaseException as e: + exceptions.append(e) + node = request.node + # Even if finalization fails, we invalidate the cached fixture + # value and remove all finalizers because they may be bound methods + # which will keep instances alive. + self.cached_result = None + self._finalizers.clear() + if len(exceptions) == 1: + raise exceptions[0] + elif len(exceptions) > 1: + msg = f'errors while tearing down fixture "{self.argname}" of {node}' + raise BaseExceptionGroup(msg, exceptions[::-1]) + + def execute(self, request: SubRequest) -> FixtureValue: + """Return the value of this fixture, executing it if not cached.""" + # Ensure that the dependent fixtures requested by this fixture are loaded. + # This needs to be done before checking if we have a cached value, since + # if a dependent fixture has their cache invalidated, e.g. due to + # parametrization, they finalize themselves and fixtures depending on it + # (which will likely include this fixture) setting `self.cached_result = None`. + # See #4871 + requested_fixtures_that_should_finalize_us = [] + for argname in self.argnames: + fixturedef = request._get_active_fixturedef(argname) + # Saves requested fixtures in a list so we later can add our finalizer + # to them, ensuring that if a requested fixture gets torn down we get torn + # down first. This is generally handled by SetupState, but still currently + # needed when this fixture is not parametrized but depends on a parametrized + # fixture. + requested_fixtures_that_should_finalize_us.append(fixturedef) + + # Check for (and return) cached value/exception. + if self.cached_result is not None: + request_cache_key = self.cache_key(request) + cache_key = self.cached_result[1] + try: + # Attempt to make a normal == check: this might fail for objects + # which do not implement the standard comparison (like numpy arrays -- #6497). + cache_hit = bool(request_cache_key == cache_key) + except (ValueError, RuntimeError): + # If the comparison raises, use 'is' as fallback. + cache_hit = request_cache_key is cache_key + + if cache_hit: + if self.cached_result[2] is not None: + exc, exc_tb = self.cached_result[2] + raise exc.with_traceback(exc_tb) + else: + return self.cached_result[0] + # We have a previous but differently parametrized fixture instance + # so we need to tear it down before creating a new one. + self.finish(request) + assert self.cached_result is None + + # Add finalizer to requested fixtures we saved previously. + # We make sure to do this after checking for cached value to avoid + # adding our finalizer multiple times. (#12135) + finalizer = functools.partial(self.finish, request=request) + for parent_fixture in requested_fixtures_that_should_finalize_us: + parent_fixture.addfinalizer(finalizer) + + # Register the pytest_fixture_post_finalizer as the first finalizer, + # which is executed last. + assert not self._finalizers + self.addfinalizer( + lambda: request.node.ihook.pytest_fixture_post_finalizer( + fixturedef=self, request=request + ) + ) + + ihook = request.node.ihook + try: + # Setup the fixture, run the code in it, and cache the value + # in self.cached_result. + result: FixtureValue = ihook.pytest_fixture_setup( + fixturedef=self, request=request + ) + finally: + # Schedule our finalizer, even if the setup failed. + request.node.addfinalizer(finalizer) + + return result + + def cache_key(self, request: SubRequest) -> object: + return getattr(request, "param", None) + + def __repr__(self) -> str: + return f"" + + +class RequestFixtureDef(FixtureDef[FixtureRequest]): + """A custom FixtureDef for the special "request" fixture. + + A new one is generated on-demand whenever "request" is requested. + """ + + def __init__(self, request: FixtureRequest) -> None: + super().__init__( + config=request.config, + baseid=NOTSET, + argname="request", + func=lambda: request, + scope=Scope.Function, + params=None, + node=request.node, + _ispytest=True, + ) + self.cached_result = (request, [0], None) + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + pass + + +def resolve_fixture_function( + fixturedef: FixtureDef[FixtureValue], request: FixtureRequest +) -> _FixtureFunc[FixtureValue]: + """Get the actual callable that can be called to obtain the fixture + value.""" + fixturefunc = fixturedef.func + # The fixture function needs to be bound to the actual + # request.instance so that code working with "fixturedef" behaves + # as expected. + instance = request.instance + + if fixturedef._scope is Scope.Class: + # Check if fixture is an instance method (bound to instance, not class) + if hasattr(fixturefunc, "__self__"): + bound_to = fixturefunc.__self__ + # classmethod: bound_to is the class itself (a type) + # instance method: bound_to is an instance (not a type) + if not isinstance(bound_to, type): + warnings.warn(CLASS_FIXTURE_INSTANCE_METHOD, stacklevel=2) + + if instance is not None: + # Handle the case where fixture is defined not in a test class, but some other class + # (for example a plugin class with a fixture), see #2270. + if hasattr(fixturefunc, "__self__") and not isinstance( + instance, + fixturefunc.__self__.__class__, + ): + return fixturefunc + fixturefunc = getimfunc(fixturedef.func) + if fixturefunc != fixturedef.func: + fixturefunc = fixturefunc.__get__(instance) + return fixturefunc + + +def pytest_fixture_setup( + fixturedef: FixtureDef[FixtureValue], request: SubRequest +) -> FixtureValue: + """Execution of fixture setup.""" + kwargs = {} + for argname in fixturedef.argnames: + kwargs[argname] = request.getfixturevalue(argname) + + fixturefunc = resolve_fixture_function(fixturedef, request) + my_cache_key = fixturedef.cache_key(request) + + if inspect.isasyncgenfunction(fixturefunc) or inspect.iscoroutinefunction( + fixturefunc + ): + auto_str = " with autouse=True" if fixturedef._autouse else "" + fail( + f"{request.node.name!r} requested an async fixture {request.fixturename!r}{auto_str}, " + "with no plugin or hook that handled it. This is an error, as pytest does not natively support it.\n" + "See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture", + pytrace=False, + ) + + try: + result = call_fixture_func(fixturefunc, request, kwargs) + except TEST_OUTCOME as e: + if isinstance(e, skip.Exception): + # The test requested a fixture which caused a skip. + # Don't show the fixture as the skip location, as then the user + # wouldn't know which test skipped. + e._use_item_location = True + fixturedef.cached_result = (None, my_cache_key, (e, e.__traceback__)) + raise + fixturedef.cached_result = (result, my_cache_key, None) + return result + + +@final +@dataclasses.dataclass(frozen=True) +class FixtureFunctionMarker: + scope: ScopeName | Callable[[str, Config], ScopeName] + params: tuple[object, ...] | None + autouse: bool = False + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None + name: str | None = None + + _ispytest: dataclasses.InitVar[bool] = False + + def __post_init__(self, _ispytest: bool) -> None: + check_ispytest(_ispytest) + + def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: + if inspect.isclass(function): + raise ValueError("class fixtures not supported (maybe in the future)") + + if isinstance(function, FixtureFunctionDefinition): + raise ValueError( + f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}" + ) + + if hasattr(function, "pytestmark"): + fail( + "Marks cannot be applied to fixtures.\n" + "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" + ) + + fixture_definition = FixtureFunctionDefinition( + function=function, fixture_function_marker=self, _ispytest=True + ) + + name = self.name or function.__name__ + if name == "request": + location = getlocation(function) + fail( + f"'request' is a reserved word for fixtures, use another name:\n {location}", + pytrace=False, + ) + + return fixture_definition + + +# TODO: paramspec/return type annotation tracking and storing +class FixtureFunctionDefinition: + def __init__( + self, + *, + function: Callable[..., Any], + fixture_function_marker: FixtureFunctionMarker, + instance: object | None = None, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self.name = fixture_function_marker.name or function.__name__ + # In order to show the function that this fixture contains in messages. + # Set the __name__ to be same as the function __name__ or the given fixture name. + self.__name__ = self.name + self._fixture_function_marker = fixture_function_marker + if instance is not None: + self._fixture_function = cast( + Callable[..., Any], function.__get__(instance) + ) + else: + self._fixture_function = function + functools.update_wrapper(self, function) + + def __repr__(self) -> str: + return f"" + + def __get__(self, instance, owner=None): + """Behave like a method if the function it was applied to was a method.""" + return FixtureFunctionDefinition( + function=self._fixture_function, + fixture_function_marker=self._fixture_function_marker, + instance=instance, + _ispytest=True, + ) + + def __call__(self, *args: Any, **kwds: Any) -> Any: + message = ( + f'Fixture "{self.name}" called directly. Fixtures are not meant to be called directly,\n' + "but are created automatically when test functions request them as parameters.\n" + "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n" + "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly" + ) + fail(message, pytrace=False) + + def _get_wrapped_function(self) -> Callable[..., Any]: + return self._fixture_function + + +@overload +def fixture( + fixture_function: Callable[..., object], + *, + scope: ScopeName | Callable[[str, Config], ScopeName] = ..., + params: Iterable[object] | None = ..., + autouse: bool = ..., + ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + name: str | None = ..., +) -> FixtureFunctionDefinition: ... + + +@overload +def fixture( + fixture_function: None = ..., + *, + scope: ScopeName | Callable[[str, Config], ScopeName] = ..., + params: Iterable[object] | None = ..., + autouse: bool = ..., + ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + name: str | None = None, +) -> FixtureFunctionMarker: ... + + +def fixture( + fixture_function: FixtureFunction | None = None, + *, + scope: ScopeName | Callable[[str, Config], ScopeName] = "function", + params: Iterable[object] | None = None, + autouse: bool = False, + ids: Sequence[object | None] | Callable[[Any], object | None] | None = None, + name: str | None = None, +) -> FixtureFunctionMarker | FixtureFunctionDefinition: + """Decorator to mark a fixture factory function. + + This decorator can be used, with or without parameters, to define a + fixture function. + + The name of the fixture function can later be referenced to cause its + invocation ahead of running tests: test modules or classes can use the + ``pytest.mark.usefixtures(fixturename)`` marker. + + Test functions can directly use fixture names as input arguments in which + case the fixture instance returned from the fixture function will be + injected. + + Fixtures can provide their values to test functions using ``return`` or + ``yield`` statements. When using ``yield`` the code block after the + ``yield`` statement is executed as teardown code regardless of the test + outcome, and must yield exactly once. + + :param scope: + The scope for which this fixture is shared; one of ``"function"`` + (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``. + + This parameter may also be a callable which receives ``(fixture_name, config)`` + as parameters, and must return a ``str`` with one of the values mentioned above. + + See :ref:`dynamic scope` in the docs for more information. + + :param params: + An optional list of parameters which will cause multiple invocations + of the fixture function and all of the tests using it. The current + parameter is available in ``request.param``. + + :param autouse: + If True, the fixture func is activated for all tests that can see it. + If False (the default), an explicit reference is needed to activate + the fixture. + + :param ids: + Sequence of ids each corresponding to the params so that they are + part of the test id. If no ids are provided they will be generated + automatically from the params. + + :param name: + The name of the fixture. This defaults to the name of the decorated + function. If a fixture is used in the same module in which it is + defined, the function name of the fixture will be shadowed by the + function arg that requests the fixture; one way to resolve this is to + name the decorated function ``fixture_`` and then use + ``@pytest.fixture(name='')``. + """ + fixture_marker = FixtureFunctionMarker( + scope=scope, + params=tuple(params) if params is not None else None, + autouse=autouse, + ids=None if ids is None else ids if callable(ids) else tuple(ids), + name=name, + _ispytest=True, + ) + + # Direct decoration. + if fixture_function: + return fixture_marker(fixture_function) + + return fixture_marker + + +@deprecated( + "@pytest.yield_fixture is deprecated. Use @pytest.fixture instead; they are the same.", + category=None, # We have our own runtime warning logic +) +def yield_fixture( + fixture_function=None, + *args, + scope="function", + params=None, + autouse=False, + ids=None, + name=None, +): + """(Return a) decorator to mark a yield-fixture factory function. + + .. deprecated:: 3.0 + Use :py:func:`pytest.fixture` directly instead. + """ + warnings.warn(YIELD_FIXTURE, stacklevel=2) + return fixture( + fixture_function, + *args, + scope=scope, + params=params, + autouse=autouse, + ids=ids, + name=name, + ) + + +@fixture(scope="session") +def pytestconfig(request: FixtureRequest) -> Config: + """Session-scoped fixture that returns the session's :class:`pytest.Config` + object. + + Example:: + + def test_foo(pytestconfig): + if pytestconfig.get_verbosity() > 0: + ... + + """ + return request.config + + +def pytest_addoption(parser: Parser) -> None: + parser.addini( + "usefixtures", + type="args", + default=[], + help="List of default fixtures to be used with this project", + ) + group = parser.getgroup("general") + group.addoption( + "--fixtures", + "--funcargs", + action="store_true", + dest="showfixtures", + default=False, + help="Show available fixtures, sorted by plugin appearance " + "(fixtures with leading '_' are only shown with '-v')", + ) + group.addoption( + "--fixtures-per-test", + action="store_true", + dest="show_fixtures_per_test", + default=False, + help="Show fixtures per test", + ) + + +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + if config.option.showfixtures: + showfixtures(config) + return 0 + if config.option.show_fixtures_per_test: + show_fixtures_per_test(config) + return 0 + return None + + +def _resolve_args_directness( + argnames: Sequence[str], + indirect: bool | Sequence[str], + nodeid: str, +) -> dict[str, Literal["indirect", "direct"]]: + """Resolve if each parametrized argument must be considered an indirect + parameter to a fixture of the same name, or a direct parameter to the + parametrized function, based on the ``indirect`` parameter of the + parametrize() call. + + :param argnames: + List of argument names passed to ``parametrize()``. + :param indirect: + Same as the ``indirect`` parameter of ``parametrize()``. + :param nodeid: + Node ID to which the parametrization is applied. + :returns: + A dict mapping each arg name to either "indirect" or "direct". + """ + arg_directness: dict[str, Literal["indirect", "direct"]] + if isinstance(indirect, bool): + arg_directness = dict.fromkeys(argnames, "indirect" if indirect else "direct") + elif isinstance(indirect, Sequence): + arg_directness = dict.fromkeys(argnames, "direct") + for arg in indirect: + if arg not in argnames: + fail( + f"In {nodeid}: indirect fixture '{arg}' doesn't exist", + pytrace=False, + ) + arg_directness[arg] = "indirect" + else: + fail( + f"In {nodeid}: expected Sequence or boolean for indirect, got {type(indirect).__name__}", + pytrace=False, + ) + return arg_directness + + +def _get_direct_parametrize_args(node: nodes.Node) -> set[str]: + """Return all direct parametrization arguments of a node, so we don't + mistake them for fixtures. + + Check https://github.com/pytest-dev/pytest/issues/5036. + + These things are done later as well when dealing with parametrization + so this could be improved. + """ + parametrize_argnames: set[str] = set() + for marker in node.iter_markers(name="parametrize"): + indirect = marker.kwargs.get("indirect", False) + p_argnames, _ = ParameterSet._parse_parametrize_args( + *marker.args, **marker.kwargs + ) + p_directness = _resolve_args_directness(p_argnames, indirect, node.nodeid) + parametrize_argnames.update( + argname + for argname, directness in p_directness.items() + if directness == "direct" + ) + return parametrize_argnames + + +def deduplicate_names(*seqs: Iterable[str]) -> tuple[str, ...]: + """De-duplicate the sequence of names while keeping the original order.""" + # Ideally we would use a set, but it does not preserve insertion order. + return tuple(dict.fromkeys(name for seq in seqs for name in seq)) + + +class FixtureManager: + """pytest fixture definitions and information is stored and managed + from this class. + + During collection fm.parsefactories() is called multiple times to parse + fixture function definitions into FixtureDef objects and internal + data structures. + + During collection of test functions, metafunc-mechanics instantiate + a FuncFixtureInfo object which is cached per node/func-name. + This FuncFixtureInfo object is later retrieved by Function nodes + which themselves offer a fixturenames attribute. + + The FuncFixtureInfo object holds information about fixtures and FixtureDefs + relevant for a particular function. An initial list of fixtures is + assembled like this: + + - config-defined usefixtures + - autouse-marked fixtures along the collection chain up from the function + - usefixtures markers at module/class/function level + - test function funcargs + + Subsequently the funcfixtureinfo.fixturenames attribute is computed + as the closure of the fixtures needed to setup the initial fixtures, + i.e. fixtures needed by fixture functions themselves are appended + to the fixturenames list. + + Upon the test-setup phases all fixturenames are instantiated, retrieved + by a lookup of their FuncFixtureInfo. + """ + + def __init__(self, session: Session) -> None: + self.session = session + self.config: Config = session.config + # Maps a fixture name (argname) to all of the FixtureDefs in the test + # suite/plugins defined with this name. Populated by parsefactories(). + # TODO: The order of the FixtureDefs list of each arg is significant, + # explain. + self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {} + # A mapping from a node to a list of autouse fixture names it defines. + # The Session entry holds global usefixtures from config. + self._node_autousenames: Final[dict[nodes.Node, list[str]]] = { + session: list(self.config.getini("usefixtures")), + } + # Legacy fallback: nodeid string -> autouse names, for plugins still + # using the deprecated nodeid-based API without a node reference. + self._nodeid_autousenames: Final[dict[str, list[str]]] = {} + # Pending conftest modules waiting to be parsed when their Directory is collected. + # Maps directory path -> conftest plugin module. + self._pending_conftests: Final[dict[Path, object]] = {} + session.config.pluginmanager.register(self, "funcmanage") + # Flush initial conftests from directories above rootpath immediately. + # These will never get a Directory collector, so they need Session scope. + # This must happen here (not in pytest_make_collect_report) because + # collection may fail before Session collection starts (e.g. bad args). + self._flush_pending_conftests_to_session(session) + + def getfixtureinfo( + self, + node: nodes.Item, + func: Callable[..., object] | None, + cls: type | None, + ) -> FuncFixtureInfo: + """Calculate the :class:`FuncFixtureInfo` for an item. + + If ``func`` is None, or if the item sets an attribute + ``nofuncargs = True``, then ``func`` is not examined at all. + + :param node: + The item requesting the fixtures. + :param func: + The item's function. + :param cls: + If the function is a method, the method's class. + """ + if func is not None and not getattr(node, "nofuncargs", False): + argnames = getfuncargnames(func, name=node.name, cls=cls) + else: + argnames = () + usefixturesnames = self._getusefixturesnames(node) + autousenames = self._getautousenames(node) + initialnames = deduplicate_names(autousenames, usefixturesnames, argnames) + + direct_parametrize_args = _get_direct_parametrize_args(node) + + names_closure, arg2fixturedefs = self.getfixtureclosure( + parentnode=node, + initialnames=initialnames, + ignore_args=direct_parametrize_args, + ) + + return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) + + def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None: + # Fixtures defined in conftest plugins are only visible to within the + # conftest's directory. This is unlike fixtures in non-conftest plugins + # which have global visibility. Conftest fixtures are deferred until + # their Directory is collected, so we can use the Directory's nodeid. + if plugin_name and plugin_name.endswith("conftest.py"): + # Note: we explicitly do *not* use `plugin.__file__` here -- The + # difference is that plugin_name has the correct capitalization on + # case-insensitive systems (Windows) and other normalization issues + # (issue #11816). + conftestpath = absolutepath(plugin_name) + conftest_dir = conftestpath.parent + # Store conftest for deferred parsing when its Directory is collected. + self._pending_conftests[conftest_dir] = plugin + else: + # Non-conftest plugins have global visibility. + self.parsefactories(holder=plugin, node=self.session) + + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: nodes.Collector + ) -> Generator[None, CollectReport, CollectReport]: + result = yield + if isinstance(collector, nodes.Directory): + plugin = self._pending_conftests.pop(collector.path, None) + if plugin is not None: + self.parsefactories(holder=plugin, node=collector) + return result + + def _flush_pending_conftests_to_session(self, session: Session) -> None: + """Assign Session scope to initial conftests whose directories won't + be collected as Directory nodes (e.g. ancestors above rootdir).""" + rootpath = session.config.rootpath + orphaned: list[tuple[Path, object]] = [] + for conftest_dir, plugin in list(self._pending_conftests.items()): + # If the conftest dir is not under rootpath, it will never get + # a Directory collector — assign it to Session now. + try: + conftest_dir.relative_to(rootpath) + except ValueError: + orphaned.append((conftest_dir, plugin)) + for conftest_dir, plugin in orphaned: + del self._pending_conftests[conftest_dir] + self.parsefactories(holder=plugin, node=session) + + def pytest_collection_finish(self) -> None: + """Clean up any conftests that were never collected by a Directory. + + After __init__ flushes above-rootdir conftests and collection pops + under-rootdir ones, remaining entries mean collection was interrupted + (e.g. UsageError for a bad path). These conftests' fixtures aren't + needed since their directories' tests weren't collected either. + """ + self._pending_conftests.clear() + + def _getautousenames(self, node: nodes.Node) -> Iterator[str]: + """Return the names of autouse fixtures applicable to node.""" + for parentnode in node.listchain(): + basenames = self._node_autousenames.get(parentnode) + if basenames: + yield from basenames + # Legacy fallback: check string-based nodeid autouse names. + nodeid_basenames = self._nodeid_autousenames.get(parentnode.nodeid) + if nodeid_basenames: + yield from nodeid_basenames + + def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]: + """Return the names of usefixtures fixtures applicable to node.""" + for marker_node, mark in node.iter_markers_with_node(name="usefixtures"): + if not mark.args: + marker_node.warn( + PytestWarning( + f"usefixtures() in {node.nodeid} without arguments has no effect" + ) + ) + yield from mark.args + + def getfixtureclosure( + self, + parentnode: nodes.Node, + initialnames: tuple[str, ...], + ignore_args: AbstractSet[str], + ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]: + # Collect the closure of all fixtures, starting with the given + # fixturenames as the initial set. As we have to visit all + # factory definitions anyway, we also return an arg2fixturedefs + # mapping so that the caller can reuse it and does not have + # to re-discover fixturedefs again for each fixturename + # (discovering matching fixtures for a given name/node is expensive). + + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {} + + def getfixturedefs(argname: str) -> Sequence[FixtureDef[Any]] | None: + if argname in ignore_args: + return None + + fixturedefs = arg2fixturedefs.get(argname) + if not fixturedefs: + fixturedefs = self.getfixturedefs(argname, parentnode) + if not fixturedefs: + # Fixture not defined or not visible (will error during runtest). + return None + arg2fixturedefs[argname] = fixturedefs + return fixturedefs + + def sort_by_scope(arg_name: str) -> Scope: + try: + fixturedefs = arg2fixturedefs[arg_name] + except KeyError: + return Scope.Function + else: + return fixturedefs[-1]._scope + + fixturenames_closure = sorted( + traverse_fixture_closure( + initialnames, + getfixturedefs=getfixturedefs, + ), + key=sort_by_scope, + reverse=True, + ) + + return fixturenames_closure, arg2fixturedefs + + def pytest_generate_tests(self, metafunc: Metafunc) -> None: + """Generate new tests based on parametrized fixtures used by the given metafunc""" + + def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: + args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs) + return args + + for argname in metafunc.fixturenames: + # Get the FixtureDefs for the argname. + fixture_defs = metafunc._arg2fixturedefs.get(argname, ()) + + # If the test itself parametrizes using this argname, give it + # precedence. + if any( + argname in get_parametrize_mark_argnames(mark) + for mark in metafunc.definition.iter_markers("parametrize") + ): + continue + + # In the common case we only look at the fixture def with the + # closest scope (last in the list). But if the fixture overrides + # another fixture, while requesting the super fixture, keep going + # in case the super fixture is parametrized (#1953). + for fixturedef in reversed(fixture_defs): + # Fixture is parametrized, apply it and stop. + if fixturedef.params is not None: + metafunc.parametrize( + argname, + fixturedef.params, + indirect=True, + scope=fixturedef.scope, + ids=fixturedef.ids, + ) + break + + # Not requesting the overridden super fixture, stop. + # + # TODO: Handle the case where the super-fixture is transitively + # requested (see #7737 and the xfail'd test + # test_override_parametrized_fixture_via_transitive_fixture). + if argname not in fixturedef.argnames: + break + + # Try next super fixture, if any. + + def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> None: + # Separate parametrized setups. + items[:] = reorder_items(items) + + def _register_fixture( + self, + *, + name: str, + func: _FixtureFunc[object], + nodeid: str | None | NotSetType = NOTSET, + scope: Scope | ScopeName | Callable[[str, Config], ScopeName] = "function", + params: Sequence[object] | None = None, + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, + autouse: bool = False, + node: nodes.Node | NotSetType = NOTSET, + ) -> None: + """Register a fixture + + :param name: + The fixture's name. + :param func: + The fixture's implementation function. + :param nodeid: + The visibility of the fixture (deprecated, use node instead). + The fixture will be available to the node with this nodeid and + its children in the collection tree. None means global visibility. + :param node: + The node where the fixture is defined (preferred over nodeid). + When provided, enables node-based matching which is more robust. + :param scope: + The fixture's scope. + :param params: + The fixture's parametrization params. + :param ids: + The fixture's IDs. + :param autouse: + Whether this is an autouse fixture. + """ + # Emit deprecation warning if nodeid string. + if nodeid is not NOTSET or node is NOTSET: + warnings.warn(FIXTURE_NODEID_DEPRECATED, stacklevel=2) + fixture_def = FixtureDef( + config=self.config, + baseid=nodeid, + argname=name, + func=func, + scope=scope, + params=params, + ids=ids, + _ispytest=True, + _autouse=autouse, + node=node, + ) + + faclist = self._arg2fixturedefs.setdefault(name, []) + # Insert the fixturedef into the list while maintaining a partial order + # based on visibility: a fixturedef whose visibility is more specific + # sorts after a more general one, so that it takes precedence in the + # override chain (the last applicable fixturedef in the list is used + # first, see getfixturedefs). + # fixturedefs with the same visibility keep registration order, i.e. the + # last registered wins. + # The order between non-comparable fixturedefs doesn't matter since they + # cannot be visible together. + # The idea is that a fixture that is defined closer to the item should + # take precedence. + for i, existing in enumerate(faclist): + if is_visibility_more_specific(existing, fixture_def): + faclist.insert(i, fixture_def) + break + else: + faclist.append(fixture_def) + if autouse: + if node is not NOTSET: + self._node_autousenames.setdefault(node, []).append(name) + elif nodeid is not NOTSET and nodeid is not None: + # Legacy: plugin passed nodeid string without node reference. + self._nodeid_autousenames.setdefault(nodeid, []).append(name) + else: + # Global plugin autouse fixtures go under Session. + self._node_autousenames.setdefault(self.session, []).append(name) + + @overload + def parsefactories( + self, + node_or_obj: nodes.Node, + ) -> None: + raise NotImplementedError() + + @overload + @deprecated( + "parsefactories(obj, nodeid) is deprecated, use parsefactories(holder=obj, node=node) instead" + ) + def parsefactories( + self, + node_or_obj: object, + nodeid: str | None, + ) -> None: + raise NotImplementedError() + + @overload + def parsefactories( + self, + node_or_obj: NotSetType = ..., + nodeid: NotSetType = ..., + *, + holder: object, + node: nodes.Node, + ) -> None: + raise NotImplementedError() + + def parsefactories( + self, + node_or_obj: nodes.Node | object | NotSetType = NOTSET, + nodeid: str | None | NotSetType = NOTSET, + *, + holder: object | NotSetType = NOTSET, + node: nodes.Node | NotSetType = NOTSET, + ) -> None: + """Collect fixtures from a collection node or object. + + Found fixtures are parsed into `FixtureDef`s and saved. + + The preferred API uses keyword-only arguments: + - ``holder``: The object to scan for fixtures. + - ``node``: The node determining fixture visibility. + + Legacy positional API (translated internally): + - ``parsefactories(node)``: Uses node.obj as holder, node for scope. + - ``parsefactories(obj, nodeid)``: Uses obj as holder, nodeid string for scope. + """ + # Translate legacy API to holder/node sources of truth + # Either effective_node or effective_nodeid will be set, not both + effective_node: nodes.Node | NotSetType = NOTSET + effective_nodeid: str | None | NotSetType = NOTSET + + if holder is not NOTSET: + # New API: holder and node explicitly provided + holderobj = holder + effective_node = node + elif node_or_obj is NOTSET: + raise TypeError("parsefactories() requires holder or node_or_obj") + elif nodeid is not NOTSET: + # Legacy: parsefactories(obj, nodeid) - string-based scoping only. + warnings.warn(PARSEFACTORIES_NODEID_DEPRECATED, stacklevel=2) + holderobj = node_or_obj + effective_nodeid = nodeid + else: + # parsefactories(node) - node has .obj attribute + assert isinstance(node_or_obj, nodes.Node) + holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined] + effective_node = node_or_obj + + # Avoid accessing `@property` (and other descriptors) when iterating fixtures. + if not safe_isclass(holderobj) and not isinstance(holderobj, types.ModuleType): + holderobj_tp: object = type(holderobj) + else: + holderobj_tp = holderobj + + for name in dir(holderobj): + # The attribute can be an arbitrary descriptor, so the attribute + # access below can raise. safe_getattr() ignores such exceptions. + obj_ub = safe_getattr(holderobj_tp, name, None) + if type(obj_ub) is FixtureFunctionDefinition: + marker = obj_ub._fixture_function_marker + if marker.name: + fixture_name = marker.name + else: + fixture_name = name + + # OK we know it is a fixture -- now safe to look up on the _instance_. + try: + obj = getattr(holderobj, name) + # if the fixture is named in the decorator we cannot find it in the module + except AttributeError: + obj = obj_ub + + func = obj._get_wrapped_function() + + self._register_fixture( + name=fixture_name, + func=func, + scope=marker.scope, + params=marker.params, + ids=marker.ids, + autouse=marker.autouse, + node=effective_node, + nodeid=effective_nodeid, + ) + + def getfixturedefs( + self, argname: str, node: nodes.Node + ) -> Sequence[FixtureDef[Any]] | None: + """Get FixtureDefs for a fixture name which are applicable + to a given node. + + Returns None if there are no fixtures at all defined with the given + name. (This is different from the case in which there are fixtures + with the given name, but none applicable to the node. In this case, + an empty result is returned). + + :param argname: Name of the fixture to search for. + :param node: The requesting Node. + """ + try: + fixturedefs = self._arg2fixturedefs[argname] + except KeyError: + return None + return tuple(self._matchfactories(fixturedefs, node)) + + def _matchfactories( + self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node + ) -> Iterator[FixtureDef[Any]]: + # Collect parent nodes and their IDs for matching + parent_nodes = set(node.iter_parents()) + parentnodeids = {n.nodeid for n in parent_nodes} + + for fixturedef in fixturedefs: + if fixturedef.node is not None: + # Node-based matching: check if fixture's node is a parent + if fixturedef.node in parent_nodes: + yield fixturedef + elif fixturedef.baseid in parentnodeids: + # Fallback to string-based matching for legacy/plugins + yield fixturedef + + +def show_fixtures_per_test(config: Config) -> int | ExitCode: + from _pytest.main import wrap_session + + return wrap_session(config, _show_fixtures_per_test) + + +_PYTEST_DIR = Path(_pytest.__file__).parent + + +def _pretty_fixture_path(invocation_dir: Path, func) -> str: + loc = Path(getlocation(func, invocation_dir)) + prefix = Path("...", "_pytest") + try: + return str(prefix / loc.relative_to(_PYTEST_DIR)) + except ValueError: + return bestrelpath(invocation_dir, loc) + + +def _get_fixtures_per_test(test: nodes.Item) -> Iterator[FixtureDef[object]]: + """Returns all fixtures used by the test item except for those created by + direct parametrization and those requested dynamically with + ``request.getfixturevalue``. + + The justification for excluding fixtures created by direct parametrization + is that for users, they are internal implementation detail. + + Dynamically requested fixtures are excluded because they are not known + statically. + """ + from _pytest.python import DirectParamFixtureDef + + # Custom Items may not have _fixtureinfo attribute. + fixture_info: FuncFixtureInfo | None = getattr(test, "_fixtureinfo", None) + if fixture_info is None: + return # pragma: no cover + + # dict key not used in loop but needed for sorting. + for argname, fixturedefs in sorted(fixture_info.name2fixturedefs.items()): + if not fixturedefs: + # Not supposed to be empty, but for safety. + continue # pragma: no cover + # Last item is expected to be the one directly used by the test item. + fixturedef = fixturedefs[-1] + if isinstance(fixturedef, DirectParamFixtureDef): + continue + yield fixturedef + + +def _show_fixtures_per_test(config: Config, session: Session) -> None: + import _pytest.config + + session.perform_collect() + invocation_dir = config.invocation_params.dir + tw = _pytest.config.create_terminal_writer(config) + verbose = config.get_verbosity() + + def get_best_relpath(func) -> str: + loc = getlocation(func, invocation_dir) + return bestrelpath(invocation_dir, Path(loc)) + + def write_fixture(fixture_def: FixtureDef[object]) -> None: + argname = fixture_def.argname + if verbose <= 0 and argname.startswith("_"): + return + prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func) + tw.write(f"{argname}", green=True) + tw.write(f" -- {prettypath}", yellow=True) + tw.write("\n") + fixture_doc = inspect.getdoc(fixture_def.func) + if fixture_doc: + write_docstring( + tw, + fixture_doc.split("\n\n", maxsplit=1)[0] + if verbose <= 0 + else fixture_doc, + ) + else: + tw.line(" no docstring available", red=True) + + def write_item(item: nodes.Item) -> None: + fixturedefs = list(_get_fixtures_per_test(item)) + if not fixturedefs: + # This test item does not use any fixtures. + return + + tw.line() + tw.sep("-", f"fixtures used by {item.name}") + # TODO: Fix this type ignore. + tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined] + + for fixturedef in fixturedefs: + write_fixture(fixturedef) + + for session_item in session.items: + write_item(session_item) + + +def showfixtures(config: Config) -> int | ExitCode: + from _pytest.main import wrap_session + + return wrap_session(config, _showfixtures_main) + + +def _showfixtures_main(config: Config, session: Session) -> None: + import _pytest.config + + session.perform_collect() + invocation_dir = config.invocation_params.dir + tw = _pytest.config.create_terminal_writer(config) + verbose = config.get_verbosity() + + fm = session._fixturemanager + + available = [] + seen: set[tuple[str, str]] = set() + + for argname, fixturedefs in fm._arg2fixturedefs.items(): + assert fixturedefs is not None + if not fixturedefs: + continue + for fixturedef in fixturedefs: + loc = getlocation(fixturedef.func, invocation_dir) + if (fixturedef.argname, loc) in seen: + continue + seen.add((fixturedef.argname, loc)) + available.append( + ( + len(fixturedef.baseid), + fixturedef.func.__module__, + _pretty_fixture_path(invocation_dir, fixturedef.func), + fixturedef.argname, + fixturedef, + ) + ) + + available.sort() + currentmodule = None + for baseid, module, prettypath, argname, fixturedef in available: + if currentmodule != module: + if not module.startswith("_pytest."): + tw.line() + tw.sep("-", f"fixtures defined from {module}") + currentmodule = module + if verbose <= 0 and argname.startswith("_"): + continue + tw.write(f"{argname}", green=True) + if fixturedef.scope != "function": + tw.write(f" [{fixturedef.scope} scope]", cyan=True) + tw.write(f" -- {prettypath}", yellow=True) + tw.write("\n") + doc = inspect.getdoc(fixturedef.func) + if doc: + write_docstring( + tw, doc.split("\n\n", maxsplit=1)[0] if verbose <= 0 else doc + ) + else: + tw.line(" no docstring available", red=True) + tw.line() + + +def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: + for line in doc.split("\n"): + tw.line(indent + line) + + +def register_fixture( + *, + name: str, + func: _FixtureFunc[object], + node: nodes.Node, + scope: ScopeName | Callable[[str, Config], ScopeName] = "function", + params: Sequence[object] | None = None, + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, + autouse: bool = False, +) -> None: + """Register a fixture imperatively. + + This is an advanced function intended for use by plugins. + + Normally, fixtures should be registered declaratively using the + :func:`@pytest.fixture ` decorator. Pytest looks for these + fixture definitions during the collection phase and registers them + automatically. For some plugin usecases the declarative interface can be + cumbersome or nonviable, in which case the imperative interface can be used. + + Fixture registration is expected to happen during the collection phase, and + this is the only sanctioned use. However, to allow for more creative uses, + this is not enforced. But do so at your own risk! + + .. versionadded: 9.1 + + :param name: + The fixture's name. + :param func: + The fixture's implementation function. + :param node: + The visibility of the fixture. + + Only items that are descendents of this node in the collection tree will + be able to request this fixture. You can think of this as the place + where you would put the `@pytest.fixture`. + + For global visibility, pass the :class:`session ` node, + which is the root of the collection tree. + :param scope: + The fixture's scope. + :param params: + The fixture's parametrization params. + :param ids: + The fixture's IDs. + :param autouse: + Whether this is an autouse fixture. + """ + node.session._fixturemanager._register_fixture( + name=name, + func=func, + node=node, + scope=scope, + params=params, + ids=ids, + autouse=autouse, + ) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/freeze_support.py b/.venv-test/lib/python3.12/site-packages/_pytest/freeze_support.py new file mode 100644 index 0000000..959ff07 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/freeze_support.py @@ -0,0 +1,45 @@ +"""Provides a function to report all internal modules for using freezing +tools.""" + +from __future__ import annotations + +from collections.abc import Iterator +import types + + +def freeze_includes() -> list[str]: + """Return a list of module names used by pytest that should be + included by cx_freeze.""" + import _pytest + + result = list(_iter_all_modules(_pytest)) + return result + + +def _iter_all_modules( + package: str | types.ModuleType, + prefix: str = "", +) -> Iterator[str]: + """Iterate over the names of all modules that can be found in the given + package, recursively. + + >>> import _pytest + >>> list(_iter_all_modules(_pytest)) + ['_pytest._argcomplete', '_pytest._code.code', ...] + """ + import os + import pkgutil + + if isinstance(package, str): + path = package + else: + # Type ignored because typeshed doesn't define ModuleType.__path__ + # (only defined on packages). + package_path = package.__path__ + path, prefix = package_path[0], package.__name__ + "." + for _, name, is_package in pkgutil.iter_modules([path]): + if is_package: + for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."): + yield prefix + m + else: + yield prefix + name diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/helpconfig.py b/.venv-test/lib/python3.12/site-packages/_pytest/helpconfig.py new file mode 100644 index 0000000..fdba02b --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/helpconfig.py @@ -0,0 +1,293 @@ +# mypy: allow-untyped-defs +"""Version info, help messages, tracing configuration.""" + +from __future__ import annotations + +import argparse +from collections.abc import Generator +from collections.abc import Sequence +import os +import sys +from typing import Any + +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import PrintHelp +from _pytest.config.argparsing import Parser +from _pytest.terminal import TerminalReporter +import pytest + + +class HelpAction(argparse.Action): + """An argparse Action that will raise a PrintHelp exception in order to skip + the rest of the argument parsing when --help is passed. + + This prevents argparse from raising UsageError when `--help` is used along + with missing required arguments when any are defined, for example by + ``pytest_addoption``. This is similar to the way that the builtin argparse + --help option is implemented by raising SystemExit. + + To opt in to this behavior, the parse caller must set + `namespace._raise_print_help = True`. Otherwise it just sets the option. + """ + + def __init__( + self, option_strings: Sequence[str], dest: str, *, help: str | None = None + ) -> None: + super().__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + const=True, + default=False, + help=help, + ) + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: str | Sequence[Any] | None, + option_string: str | None = None, + ) -> None: + setattr(namespace, self.dest, self.const) + + if getattr(namespace, "_raise_print_help", False): + raise PrintHelp + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("debugconfig") + group.addoption( + "-V", + "--version", + action="count", + default=0, + dest="version", + help="Display pytest version and information about plugins. " + "When given twice, also display information about plugins.", + ) + group._addoption( # private to use reserved lower-case short option + "-h", + "--help", + action=HelpAction, + dest="help", + help="Show help message and configuration info", + ) + group._addoption( # private to use reserved lower-case short option + "-p", + action="append", + dest="plugins", + default=[], + metavar="name", + help="Early-load given plugin module name or entry point (multi-allowed). " + "To avoid loading of plugins, use the `no:` prefix, e.g. " + "`no:doctest`. See also --disable-plugin-autoload.", + ) + group.addoption( + "--disable-plugin-autoload", + action="store_true", + default=False, + help="Disable plugin auto-loading through entry point packaging metadata. " + "Only plugins explicitly specified in -p or env var PYTEST_PLUGINS will be loaded.", + ) + group.addoption( + "--traceconfig", + "--trace-config", + action="store_true", + default=False, + help="Trace considerations of conftest.py files", + ) + group.addoption( + "--debug", + action="store", + nargs="?", + const="pytestdebug.log", + dest="debug", + metavar="DEBUG_FILE_NAME", + help="Store internal tracing debug information in this log file. " + "This file is opened with 'w' and truncated as a result, care advised. " + "Default: pytestdebug.log.", + ) + group._addoption( # private to use reserved lower-case short option + "-o", + "--override-ini", + dest="override_ini", + action="append", + help='Override configuration option with "option=value" style, ' + "e.g. `-o strict_xfail=True -o cache_dir=cache`.", + ) + + +@pytest.hookimpl(wrapper=True) +def pytest_cmdline_parse() -> Generator[None, Config, Config]: + config = yield + + if config.option.debug: + # --debug | --debug was provided. + path = config.option.debug + debugfile = open(path, "w", encoding="utf-8") + debugfile.write( + "versions pytest-{}, " + "python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format( + pytest.__version__, + ".".join(map(str, sys.version_info)), + config.invocation_params.dir, + os.getcwd(), + config.invocation_params.args, + ) + ) + config.trace.root.setwriter(debugfile.write) + undo_tracing = config.pluginmanager.enable_tracing() + sys.stderr.write(f"writing pytest debug information to {path}\n") + + def unset_tracing() -> None: + debugfile.close() + sys.stderr.write(f"wrote pytest debug information to {debugfile.name}\n") + config.trace.root.setwriter(None) + undo_tracing() + + config.add_cleanup(unset_tracing) + + return config + + +def show_version_verbose(config: Config) -> None: + """Show verbose pytest version installation, including plugins.""" + sys.stdout.write( + f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n" + ) + plugininfo = getpluginversioninfo(config) + if plugininfo: + for line in plugininfo: + sys.stdout.write(line + "\n") + + +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + # Note: a single `--version` argument is handled directly by `Config.main()` to avoid starting up the entire + # pytest infrastructure just to display the version (#13574). + if config.option.version > 1: + show_version_verbose(config) + return ExitCode.OK + elif config.option.help: + config._do_configure() + showhelp(config) + config._ensure_unconfigure() + return ExitCode.OK + return None + + +def showhelp(config: Config) -> None: + import textwrap + + reporter: TerminalReporter | None = config.pluginmanager.get_plugin( + "terminalreporter" + ) + assert reporter is not None + tw = reporter._tw + tw.write(config._parser.optparser.format_help()) + tw.line() + tw.line( + "[pytest] configuration options in the first " + "pytest.toml|pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:" + ) + tw.line() + + columns = tw.fullwidth # costly call + indent_len = 24 # based on argparse's max_help_position=24 + indent = " " * indent_len + for name in config._parser._inidict: + help, type, _default = config._parser._inidict[name] + if help is None: + raise TypeError(f"help argument cannot be None for {name}") + spec = f"{name} ({type}):" + tw.write(f" {spec}") + spec_len = len(spec) + if spec_len > (indent_len - 3): + # Display help starting at a new line. + tw.line() + helplines = textwrap.wrap( + help, + columns, + initial_indent=indent, + subsequent_indent=indent, + break_on_hyphens=False, + ) + + for line in helplines: + tw.line(line) + else: + # Display help starting after the spec, following lines indented. + tw.write(" " * (indent_len - spec_len - 2)) + wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False) + + if wrapped: + tw.line(wrapped[0]) + for line in wrapped[1:]: + tw.line(indent + line) + + tw.line() + tw.line("Environment variables:") + vars = [ + ( + "CI", + "When set to a non-empty value, pytest knows it is running in a " + "CI process and does not truncate summary info", + ), + ("BUILD_NUMBER", "Equivalent to CI"), + ("PYTEST_ADDOPTS", "Extra command line options"), + ("PYTEST_PLUGINS", "Comma-separated plugins to load during startup"), + ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "Set to disable plugin auto-loading"), + ("PYTEST_DEBUG", "Set to enable debug tracing of pytest's internals"), + ("PYTEST_DEBUG_TEMPROOT", "Override the system temporary directory"), + ("PYTEST_THEME", "The Pygments style to use for code output"), + ("PYTEST_THEME_MODE", "Set the PYTEST_THEME to be either 'dark' or 'light'"), + ] + for name, help in vars: + tw.line(f" {name:<24} {help}") + tw.line() + tw.line() + + tw.line("to see available markers type: pytest --markers") + tw.line("to see available fixtures type: pytest --fixtures") + tw.line( + "(shown according to specified file_or_dir or current dir " + "if not specified; fixtures with leading '_' are only shown " + "with the '-v' option" + ) + + for warningreport in reporter.stats.get("warnings", []): + tw.line("warning : " + warningreport.message, red=True) + + +def getpluginversioninfo(config: Config) -> list[str]: + lines = [] + plugininfo = config.pluginmanager.list_plugin_distinfo() + if plugininfo: + lines.append("registered third-party plugins:") + for plugin, dist in plugininfo: + loc = getattr(plugin, "__file__", repr(plugin)) + content = f"{dist.project_name}-{dist.version} at {loc}" + lines.append(" " + content) + return lines + + +def pytest_report_header(config: Config) -> list[str]: + lines = [] + if config.option.debug or config.option.traceconfig: + lines.append(f"using: pytest-{pytest.__version__}") + + verinfo = getpluginversioninfo(config) + if verinfo: + lines.extend(verinfo) + + if config.option.traceconfig: + lines.append("active plugins:") + items = config.pluginmanager.list_name_plugin() + for name, plugin in items: + if hasattr(plugin, "__file__"): + r = plugin.__file__ + else: + r = repr(plugin) + lines.append(f" {name:<20}: {r}") + return lines diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/hookspec.py b/.venv-test/lib/python3.12/site-packages/_pytest/hookspec.py new file mode 100644 index 0000000..6c5dd4b --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/hookspec.py @@ -0,0 +1,1289 @@ +# mypy: allow-untyped-defs +# ruff: noqa: T100 +"""Hook specifications for pytest plugins which are invoked by pytest itself +and by builtin plugins.""" + +from __future__ import annotations + +from collections.abc import Mapping +from collections.abc import Sequence +from pathlib import Path +from typing import Any +from typing import TYPE_CHECKING + +from pluggy import HookspecMarker + + +if TYPE_CHECKING: + import pdb + from typing import Literal + import warnings + + from _pytest._code.code import ExceptionInfo + from _pytest._code.code import ExceptionRepr + from _pytest.config import _PluggyPlugin + from _pytest.config import Config + from _pytest.config import ExitCode + from _pytest.config import PytestPluginManager + from _pytest.config.argparsing import Parser + from _pytest.fixtures import FixtureDef + from _pytest.fixtures import SubRequest + from _pytest.main import Session + from _pytest.nodes import Collector + from _pytest.nodes import Item + from _pytest.outcomes import Exit + from _pytest.python import Class + from _pytest.python import Function + from _pytest.python import Metafunc + from _pytest.python import Module + from _pytest.reports import CollectReport + from _pytest.reports import TestReport + from _pytest.runner import CallInfo + from _pytest.terminal import TerminalReporter + from _pytest.terminal import TestShortLogReport + + +hookspec = HookspecMarker("pytest") + +# ------------------------------------------------------------------------- +# Initialization hooks called for every plugin +# ------------------------------------------------------------------------- + + +@hookspec(historic=True) +def pytest_addhooks(pluginmanager: PytestPluginManager) -> None: + """Called at plugin registration time to allow adding new hooks via a call to + :func:`pluginmanager.add_hookspecs(module_or_class, prefix) `. + + :param pluginmanager: The pytest plugin manager. + + .. note:: + This hook is incompatible with hook wrappers. + + Use in conftest plugins + ======================= + + If a conftest plugin implements this hook, it will be called immediately + when the conftest is registered. + """ + + +@hookspec(historic=True) +def pytest_plugin_registered( + plugin: _PluggyPlugin, + plugin_name: str, + manager: PytestPluginManager, +) -> None: + """A new pytest plugin got registered. + + :param plugin: The plugin module or instance. + :param plugin_name: The name by which the plugin is registered. + :param manager: The pytest plugin manager. + + .. note:: + This hook is incompatible with hook wrappers. + + Use in conftest plugins + ======================= + + If a conftest plugin implements this hook, it will be called immediately + when the conftest is registered, once for each plugin registered thus far + (including itself!), and for all plugins thereafter when they are + registered. + """ + + +@hookspec(historic=True) +def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None: + """Register argparse-style options and config-style config values, + called once at the beginning of a test run. + + :param parser: + To add command line options, call + :py:func:`parser.addoption(...) `. + To add config-file values call :py:func:`parser.addini(...) + `. + + :param pluginmanager: + The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s + or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks + to change how command line options are added. + + Options can later be accessed through the + :py:class:`config ` object, respectively: + + - :py:func:`config.getoption(name) ` to + retrieve the value of a command line option. + + - :py:func:`config.getini(name) ` to retrieve + a value read from a configuration file. + + The config object is passed around on many internal objects via the ``.config`` + attribute or can be retrieved as the ``pytestconfig`` fixture. + + .. note:: + This hook is incompatible with hook wrappers. + + Use in conftest plugins + ======================= + + If a conftest plugin implements this hook, it will be called immediately + when the conftest is registered. + + This hook is only called for :ref:`initial conftests `. + """ + + +@hookspec(historic=True) +def pytest_configure(config: Config) -> None: + """Allow plugins and conftest files to perform initial configuration. + + .. note:: + This hook is incompatible with hook wrappers. + + :param config: The pytest config object. + + Use in conftest plugins + ======================= + + This hook is called for every :ref:`initial conftest ` file + after command line options have been parsed. After that, the hook is called + for other conftest files as they are registered. + """ + + +# ------------------------------------------------------------------------- +# Bootstrapping hooks called for plugins registered early enough: +# internal and 3rd party plugins. +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_cmdline_parse( + pluginmanager: PytestPluginManager, args: list[str] +) -> Config | None: + """Return an initialized :class:`~pytest.Config`, parsing the specified args. + + Stops at first non-None result, see :ref:`firstresult`. + + .. note:: + This hook is only called for plugin classes passed to the + ``plugins`` arg when using `pytest.main`_ to perform an in-process + test run. + + :param pluginmanager: The pytest plugin manager. + :param args: List of arguments passed on the command line. + :returns: A pytest config object. + + Use in conftest plugins + ======================= + + This hook is not called for conftest files. + """ + + +def pytest_load_initial_conftests( + early_config: Config, parser: Parser, args: list[str] +) -> None: + """Called to implement the loading of :ref:`initial conftest files + ` ahead of command line option parsing. + + :param early_config: The pytest config object. + :param args: Arguments passed on the command line. + :param parser: To add command line options. + + Use in conftest plugins + ======================= + + This hook is not called for conftest files. + """ + + +@hookspec(firstresult=True) +def pytest_cmdline_main(config: Config) -> ExitCode | int | None: + """Called for performing the main command line action. + + The default implementation will invoke the configure hooks and + :hook:`pytest_runtestloop`. + + Stops at first non-None result, see :ref:`firstresult`. + + :param config: The pytest config object. + :returns: The exit code. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests `. + """ + + +# ------------------------------------------------------------------------- +# collection hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_collection(session: Session) -> object | None: + """Perform the collection phase for the given session. + + Stops at first non-None result, see :ref:`firstresult`. + The return value is not used, but only stops further processing. + + The default collection phase is this (see individual hooks for full details): + + 1. Starting from ``session`` as the initial collector: + + 1. ``pytest_collectstart(collector)`` + 2. ``report = pytest_make_collect_report(collector)`` + 3. ``pytest_exception_interact(collector, call, report)`` if an interactive exception occurred + 4. For each collected node: + + 1. If an item, ``pytest_itemcollected(item)`` + 2. If a collector, recurse into it. + + 5. ``pytest_collectreport(report)`` + + 2. ``pytest_collection_modifyitems(session, config, items)`` + + 1. ``pytest_deselected(items)`` for any deselected items (may be called multiple times) + + 3. Set ``session.items`` to the list of collected items + 4. ``pytest_collection_finish(session)`` + 5. Set ``session.testscollected`` to the number of collected items + + You can implement this hook to only perform some action before collection, + for example the terminal plugin uses it to start displaying the collection + counter (and returns `None`). + + :param session: The pytest session object. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests `. + """ + + +def pytest_collection_modifyitems( + session: Session, config: Config, items: list[Item] +) -> None: + """Called after collection has been performed. May filter or re-order + the items in-place. + + When items are deselected (filtered out from ``items``), + the hook :hook:`pytest_deselected` must be called explicitly + with the deselected items to properly notify other plugins, + e.g. with ``config.hook.pytest_deselected(items=deselected_items)``. + + :param session: The pytest session object. + :param config: The pytest config object. + :param items: List of item objects. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_collection_finish(session: Session) -> None: + """Called after collection has been performed and modified. + + :param session: The pytest session object. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +@hookspec(firstresult=True) +def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: + """Return ``True`` to ignore this path for collection. + + Return ``None`` to let other plugins ignore the path for collection. + + Returning ``False`` will forcefully *not* ignore this path for collection, + without giving a chance for other plugins to ignore this path. + + This hook is consulted for all files and directories prior to calling + more specific hooks. + + Stops at first non-None result, see :ref:`firstresult`. + + :param collection_path: The path to analyze. + :type collection_path: pathlib.Path + :param config: The pytest config object. + + .. versionchanged:: 7.0.0 + The ``collection_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. The ``path`` parameter + has been deprecated and removed in pytest 9.0.0. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collection path, only + conftest files in parent directories of the collection path are consulted + (if the path is a directory, its own conftest file is *not* consulted - a + directory cannot ignore itself!). + """ + + +@hookspec(firstresult=True) +def pytest_collect_directory(path: Path, parent: Collector) -> Collector | None: + """Create a :class:`~pytest.Collector` for the given directory, or None if + not relevant. + + .. versionadded:: 8.0 + + For best results, the returned collector should be a subclass of + :class:`~pytest.Directory`, but this is not required. + + The new node needs to have the specified ``parent`` as a parent. + + Stops at first non-None result, see :ref:`firstresult`. + + :param path: The path to analyze. + :type path: pathlib.Path + + See :ref:`custom directory collectors` for a simple example of use of this + hook. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collection path, only + conftest files in parent directories of the collection path are consulted + (if the path is a directory, its own conftest file is *not* consulted - a + directory cannot collect itself!). + """ + + +def pytest_collect_file(file_path: Path, parent: Collector) -> Collector | None: + """Create a :class:`~pytest.Collector` for the given path, or None if not relevant. + + For best results, the returned collector should be a subclass of + :class:`~pytest.File`, but this is not required. + + The new node needs to have the specified ``parent`` as a parent. + + :param file_path: The path to analyze. + :type file_path: pathlib.Path + + .. versionchanged:: 7.0.0 + The ``file_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. The ``path`` parameter + has been deprecated and removed in pytest 9.0.0. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given file path, only + conftest files in parent directories of the file path are consulted. + """ + + +# logging hooks for collection + + +def pytest_collectstart(collector: Collector) -> None: + """Collector starts collecting. + + :param collector: + The collector. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories are + consulted. + """ + + +def pytest_itemcollected(item: Item) -> None: + """We just collected a test item. + + :param item: + The item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_collectreport(report: CollectReport) -> None: + """Collector finished collecting. + + :param report: + The collect report. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories are + consulted. + """ + + +def pytest_deselected(items: Sequence[Item]) -> None: + """Called for deselected test items, e.g. by keyword. + + Note that this hook has two integration aspects for plugins: + + - it can be *implemented* to be notified of deselected items + - it must be *called* from :hook:`pytest_collection_modifyitems` + implementations when items are deselected (to properly notify other plugins). + + May be called multiple times. + + :param items: + The items. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +@hookspec(firstresult=True) +def pytest_make_collect_report(collector: Collector) -> CollectReport | None: + """Perform :func:`collector.collect() ` and return + a :class:`~pytest.CollectReport`. + + Stops at first non-None result, see :ref:`firstresult`. + + :param collector: + The collector. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories are + consulted. + """ + + +# ------------------------------------------------------------------------- +# Python test function related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_pycollect_makemodule(module_path: Path, parent) -> Module | None: + """Return a :class:`pytest.Module` collector or None for the given path. + + This hook will be called for each matching test module path. + The :hook:`pytest_collect_file` hook needs to be used if you want to + create test modules for files that do not match as a test module. + + Stops at first non-None result, see :ref:`firstresult`. + + :param module_path: The path of the module to collect. + :type module_path: pathlib.Path + + .. versionchanged:: 7.0.0 + The ``module_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. The ``path`` parameter has been + deprecated in favor of ``module_path`` and removed in pytest 9.0.0. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given parent collector, + only conftest files in the collector's directory and its parent directories + are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_pycollect_makeitem( + collector: Module | Class, name: str, obj: object +) -> None | Item | Collector | list[Item | Collector]: + """Return a custom item/collector for a Python object in a module, or None. + + Stops at first non-None result, see :ref:`firstresult`. + + :param collector: + The module/class collector. + :param name: + The name of the object in the module/class. + :param obj: + The object. + :returns: + The created items/collectors. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories + are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: + """Call underlying test function. + + Stops at first non-None result, see :ref:`firstresult`. + + :param pyfuncitem: + The function item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only + conftest files in the item's directory and its parent directories + are consulted. + """ + + +def pytest_generate_tests(metafunc: Metafunc) -> None: + """Generate (multiple) parametrized calls to a test function. + + :param metafunc: + The :class:`~pytest.Metafunc` helper for the test function. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given function definition, + only conftest files in the functions's directory and its parent directories + are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_make_parametrize_id(config: Config, val: object, argname: str) -> str | None: + """Return a user-friendly string representation of the given ``val`` + that will be used by @pytest.mark.parametrize calls, or None if the hook + doesn't know about ``val``. + + The parameter name is available as ``argname``, if required. + + Stops at first non-None result, see :ref:`firstresult`. + + :param config: The pytest config object. + :param val: The parametrized value. + :param argname: The automatic parameter name produced by pytest. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +# ------------------------------------------------------------------------- +# runtest related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_runtestloop(session: Session) -> object | None: + """Perform the main runtest loop (after collection finished). + + The default hook implementation performs the runtest protocol for all items + collected in the session (``session.items``), unless the collection failed + or the ``collectonly`` pytest option is set. + + If at any point :py:func:`pytest.exit` is called, the loop is + terminated immediately. + + If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the + loop is terminated after the runtest protocol for the current item is finished. + + :param session: The pytest session object. + + Stops at first non-None result, see :ref:`firstresult`. + The return value is not used, but only stops further processing. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +@hookspec(firstresult=True) +def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> object | None: + """Perform the runtest protocol for a single test item. + + The default runtest protocol is this (see individual hooks for full details): + + - ``pytest_runtest_logstart(nodeid, location)`` + + - Setup phase: + - ``call = pytest_runtest_setup(item)`` (wrapped in ``CallInfo(when="setup")``) + - ``report = pytest_runtest_makereport(item, call)`` + - ``pytest_runtest_logreport(report)`` + - ``pytest_exception_interact(call, report)`` if an interactive exception occurred + + - Call phase, if the setup passed and the ``setuponly`` pytest option is not set: + - ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``) + - ``report = pytest_runtest_makereport(item, call)`` + - ``pytest_runtest_logreport(report)`` + - ``pytest_exception_interact(call, report)`` if an interactive exception occurred + + - Teardown phase: + - ``call = pytest_runtest_teardown(item, nextitem)`` (wrapped in ``CallInfo(when="teardown")``) + - ``report = pytest_runtest_makereport(item, call)`` + - ``pytest_runtest_logreport(report)`` + - ``pytest_exception_interact(call, report)`` if an interactive exception occurred + + - ``pytest_runtest_logfinish(nodeid, location)`` + + :param item: Test item for which the runtest protocol is performed. + :param nextitem: The scheduled-to-be-next test item (or None if this is the end my friend). + + Stops at first non-None result, see :ref:`firstresult`. + The return value is not used, but only stops further processing. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +def pytest_runtest_logstart(nodeid: str, location: tuple[str, int | None, str]) -> None: + """Called at the start of running the runtest protocol for a single item. + + See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + + :param nodeid: Full node ID of the item. + :param location: A tuple of ``(filename, lineno, testname)`` + where ``filename`` is a file path relative to ``config.rootpath`` + and ``lineno`` is 0-based. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_logfinish( + nodeid: str, location: tuple[str, int | None, str] +) -> None: + """Called at the end of running the runtest protocol for a single item. + + See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + + :param nodeid: Full node ID of the item. + :param location: A tuple of ``(filename, lineno, testname)`` + where ``filename`` is a file path relative to ``config.rootpath`` + and ``lineno`` is 0-based. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_setup(item: Item) -> None: + """Called to perform the setup phase for a test item. + + The default implementation runs ``setup()`` on ``item`` and all of its + parents (which haven't been setup yet). This includes obtaining the + values of fixtures required by the item (which haven't been obtained + yet). + + :param item: + The item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_call(item: Item) -> None: + """Called to run the test for test item (the call phase). + + The default implementation calls ``item.runtest()``. + + :param item: + The item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None: + """Called to perform the teardown phase for a test item. + + The default implementation runs the finalizers and calls ``teardown()`` + on ``item`` and all of its parents (which need to be torn down). This + includes running the teardown phase of fixtures required by the item (if + they go out of scope). + + :param item: + The item. + :param nextitem: + The scheduled-to-be-next test item (None if no further test item is + scheduled). This argument is used to perform exact teardowns, i.e. + calling just enough finalizers so that nextitem only needs to call + setup functions. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport | None: + """Called to create a :class:`~pytest.TestReport` for each of + the setup, call and teardown runtest phases of a test item. + + See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + + :param item: The item. + :param call: The :class:`~pytest.CallInfo` for the phase. + + Stops at first non-None result, see :ref:`firstresult`. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_runtest_logreport(report: TestReport) -> None: + """Process the :class:`~pytest.TestReport` produced for each + of the setup, call and teardown runtest phases of an item. + + See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +@hookspec(firstresult=True) +def pytest_report_to_serializable( + config: Config, + report: CollectReport | TestReport, +) -> dict[str, Any] | None: + """Serialize the given report object into a data structure suitable for + sending over the wire, e.g. converted to JSON. + + :param config: The pytest config object. + :param report: The report. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. The exact details may depend + on the plugin which calls the hook. + """ + + +@hookspec(firstresult=True) +def pytest_report_from_serializable( + config: Config, + data: dict[str, Any], +) -> CollectReport | TestReport | None: + """Restore a report object previously serialized with + :hook:`pytest_report_to_serializable`. + + :param config: The pytest config object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. The exact details may depend + on the plugin which calls the hook. + """ + + +# ------------------------------------------------------------------------- +# Fixture related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_fixture_setup( + fixturedef: FixtureDef[Any], request: SubRequest +) -> object | None: + """Perform fixture setup execution. + + :param fixturedef: + The fixture definition object. + :param request: + The fixture request object. + :returns: + The return value of the call to the fixture function. + + Stops at first non-None result, see :ref:`firstresult`. + + .. note:: + If the fixture function returns None, other implementations of + this hook function will continue to be called, according to the + behavior of the :ref:`firstresult` option. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given fixture, only + conftest files in the fixture scope's directory and its parent directories + are consulted. + """ + + +def pytest_fixture_post_finalizer( + fixturedef: FixtureDef[Any], request: SubRequest +) -> None: + """Called after fixture teardown, but before the cache is cleared, so + the fixture result ``fixturedef.cached_result`` is still available (not + ``None``). + + :param fixturedef: + The fixture definition object. + :param request: + The fixture request object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given fixture, only + conftest files in the fixture scope's directory and its parent directories + are consulted. + """ + + +# ------------------------------------------------------------------------- +# test session related hooks +# ------------------------------------------------------------------------- + + +def pytest_sessionstart(session: Session) -> None: + """Called after the ``Session`` object has been created and before performing collection + and entering the run test loop. + + :param session: The pytest session object. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests `. + """ + + +def pytest_sessionfinish( + session: Session, + exitstatus: int | ExitCode, +) -> None: + """Called after whole test run finished, right before returning the exit status to the system. + + :param session: The pytest session object. + :param exitstatus: The status which pytest will return to the system. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +def pytest_unconfigure(config: Config) -> None: + """Called before test process is exited. + + :param config: The pytest config object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. + """ + + +# ------------------------------------------------------------------------- +# hooks for customizing the assert methods +# ------------------------------------------------------------------------- + + +def pytest_assertrepr_compare( + config: Config, op: str, left: object, right: object +) -> list[str] | None: + """Return explanation for comparisons in failing assert expressions. + + Return None for no custom explanation, otherwise return a list + of strings. The strings will be joined by newlines but any newlines + *in* a string will be escaped. Note that all but the first line will + be indented slightly, the intention is for the first line to be a summary. + + :param config: The pytest config object. + :param op: The operator, e.g. `"=="`, `"!="`, `"not in"`. + :param left: The left operand. + :param right: The right operand. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +def pytest_assertion_pass(item: Item, lineno: int, orig: str, expl: str) -> None: + """Called whenever an assertion passes. + + .. versionadded:: 5.0 + + Use this hook to do some processing after a passing assertion. + The original assertion information is available in the `orig` string + and the pytest introspected assertion information is available in the + `expl` string. + + This hook must be explicitly enabled by the :confval:`enable_assertion_pass_hook` + configuration option: + + .. tab:: toml + + .. code-block:: toml + + [pytest] + enable_assertion_pass_hook = true + + .. tab:: ini + + .. code-block:: ini + + [pytest] + enable_assertion_pass_hook = true + + You need to **clean the .pyc** files in your project directory and interpreter libraries + when enabling this option, as assertions will require to be re-written. + + :param item: pytest item object of current test. + :param lineno: Line number of the assert statement. + :param orig: String with the original assertion. + :param expl: String with the assert explanation. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ + + +# ------------------------------------------------------------------------- +# Hooks for influencing reporting (invoked from _pytest_terminal). +# ------------------------------------------------------------------------- + + +def pytest_report_header(config: Config, start_path: Path) -> str | list[str]: # type: ignore[empty-body] + """Return a string or list of strings to be displayed as header info for terminal reporting. + + :param config: The pytest config object. + :param start_path: The starting dir. + :type start_path: pathlib.Path + + .. note:: + + Lines returned by a plugin are displayed before those of plugins which + ran before it. + If you want to have your line(s) displayed first, use + :ref:`trylast=True `. + + .. versionchanged:: 7.0.0 + The ``start_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``startdir`` parameter. The ``startdir`` parameter + has been deprecated and removed in pytest 9.0.0. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests `. + """ + + +def pytest_report_collectionfinish( # type: ignore[empty-body] + config: Config, + start_path: Path, + items: Sequence[Item], +) -> str | list[str]: + """Return a string or list of strings to be displayed after collection + has finished successfully. + + These strings will be displayed after the standard "collected X items" message. + + .. versionadded:: 3.2 + + :param config: The pytest config object. + :param start_path: The starting dir. + :type start_path: pathlib.Path + :param items: List of pytest items that are going to be executed; this list should not be modified. + + .. note:: + + Lines returned by a plugin are displayed before those of plugins which + ran before it. + If you want to have your line(s) displayed first, use + :ref:`trylast=True `. + + .. versionchanged:: 7.0.0 + The ``start_path`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``startdir`` parameter. The ``startdir`` parameter + has been deprecated and removed in pytest 9.0.0. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +@hookspec(firstresult=True) +def pytest_report_teststatus( # type:ignore[empty-body] + report: CollectReport | TestReport, config: Config +) -> TestShortLogReport | tuple[str, str, str | tuple[str, Mapping[str, bool]]]: + """Return result-category, shortletter and verbose word for status + reporting. + + The result-category is a category in which to count the result, for + example "passed", "skipped", "error" or the empty string. + + The shortletter is shown as testing progresses, for example ".", "s", + "E" or the empty string. + + The verbose word is shown as testing progresses in verbose mode, for + example "PASSED", "SKIPPED", "ERROR" or the empty string. + + pytest may style these implicitly according to the report outcome. + To provide explicit styling, return a tuple for the verbose word, + for example ``"rerun", "R", ("RERUN", {"yellow": True})``. + + :param report: The report object whose status is to be returned. + :param config: The pytest config object. + :returns: The test status. + + Stops at first non-None result, see :ref:`firstresult`. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_terminal_summary( + terminalreporter: TerminalReporter, + exitstatus: ExitCode, + config: Config, +) -> None: + """Add a section to terminal summary reporting. + + :param terminalreporter: The internal terminal reporter object. + :param exitstatus: The exit status that will be reported back to the OS. + :param config: The pytest config object. + + .. versionadded:: 4.2 + The ``config`` parameter. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +@hookspec(historic=True) +def pytest_warning_recorded( + warning_message: warnings.WarningMessage, + when: Literal["config", "collect", "runtest"], + nodeid: str, + location: tuple[str, int, str] | None, +) -> None: + """Process a warning captured by the internal pytest warnings plugin. + + :param warning_message: + The captured warning. This is the same object produced by :class:`warnings.catch_warnings`, + and contains the same attributes as the parameters of :py:func:`warnings.showwarning`. + + :param when: + Indicates when the warning was captured. Possible values: + + * ``"config"``: during pytest configuration/initialization stage. + * ``"collect"``: during test collection. + * ``"runtest"``: during test execution. + + :param nodeid: + Full id of the item. Empty string for warnings that are not specific to + a particular node. + + :param location: + When available, holds information about the execution context of the captured + warning (filename, linenumber, function). ``function`` evaluates to + when the execution context is at the module level. + + .. versionadded:: 6.0 + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. If the warning is specific to a + particular node, only conftest files in parent directories of the node are + consulted. + """ + + +# ------------------------------------------------------------------------- +# Hooks for influencing skipping +# ------------------------------------------------------------------------- + + +def pytest_markeval_namespace( # type:ignore[empty-body] + config: Config, +) -> dict[str, Any]: + """Called when constructing the globals dictionary used for + evaluating string conditions in xfail/skipif markers. + + This is useful when the condition for a marker requires + objects that are expensive or impossible to obtain during + collection time, which is required by normal boolean + conditions. + + .. versionadded:: 6.2 + + :param config: The pytest config object. + :returns: A dictionary of additional globals to add. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in parent directories of the item are consulted. + """ + + +# ------------------------------------------------------------------------- +# error handling and internal debugging hooks +# ------------------------------------------------------------------------- + + +def pytest_internalerror( + excrepr: ExceptionRepr, + excinfo: ExceptionInfo[BaseException], +) -> bool | None: + """Called for internal errors. + + Return True to suppress the fallback handling of printing an + INTERNALERROR message directly to sys.stderr. + + :param excrepr: The exception repr object. + :param excinfo: The exception info. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_keyboard_interrupt( + excinfo: ExceptionInfo[KeyboardInterrupt | Exit], +) -> None: + """Called for keyboard interrupt. + + :param excinfo: The exception info. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_exception_interact( + node: Item | Collector, + call: CallInfo[Any], + report: CollectReport | TestReport, +) -> None: + """Called when an exception was raised which can potentially be + interactively handled. + + May be called during collection (see :hook:`pytest_make_collect_report`), + in which case ``report`` is a :class:`~pytest.CollectReport`. + + May be called during runtest of an item (see :hook:`pytest_runtest_protocol`), + in which case ``report`` is a :class:`~pytest.TestReport`. + + This hook is not called if the exception that was raised is an internal + exception like ``skip.Exception``. + + :param node: + The item or collector. + :param call: + The call information. Contains the exception. + :param report: + The collection or test report. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given node, only conftest + files in parent directories of the node are consulted. + """ + + +def pytest_enter_pdb(config: Config, pdb: pdb.Pdb) -> None: + """Called upon pdb.set_trace(). + + Can be used by plugins to take special action just before the python + debugger enters interactive mode. + + :param config: The pytest config object. + :param pdb: The Pdb instance. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ + + +def pytest_leave_pdb(config: Config, pdb: pdb.Pdb) -> None: + """Called when leaving pdb (e.g. with continue after pdb.set_trace()). + + Can be used by plugins to take special action just after the python + debugger leaves interactive mode. + + :param config: The pytest config object. + :param pdb: The Pdb instance. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/junitxml.py b/.venv-test/lib/python3.12/site-packages/_pytest/junitxml.py new file mode 100644 index 0000000..ac78f50 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/junitxml.py @@ -0,0 +1,703 @@ +# mypy: allow-untyped-defs +"""Report test results in JUnit-XML format, for use with Jenkins and build +integration servers. + +Based on initial code from Ross Lawley. + +Output conforms to +https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd +""" + +from __future__ import annotations + +from collections.abc import Callable +import functools +import os +import platform +import re +import xml.etree.ElementTree as ET + +from _pytest import nodes +from _pytest import timing +from _pytest._code.code import ExceptionRepr +from _pytest._code.code import ReprFileLocation +from _pytest.config import Config +from _pytest.config import filename_arg +from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureRequest +from _pytest.reports import TestReport +from _pytest.stash import StashKey +from _pytest.terminal import TerminalReporter +import pytest + + +xml_key = StashKey["LogXML"]() + + +def bin_xml_escape(arg: object) -> str: + r"""Visually escape invalid XML characters. + + For example, transforms + 'hello\aworld\b' + into + 'hello#x07world#x08' + Note that the #xABs are *not* XML escapes - missing the ampersand «. + The idea is to escape visually for the user rather than for XML itself. + """ + + def repl(matchobj: re.Match[str]) -> str: + i = ord(matchobj.group()) + if i <= 0xFF: + return f"#x{i:02X}" + else: + return f"#x{i:04X}" + + # The spec range of valid chars is: + # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + # For an unknown(?) reason, we disallow #x7F (DEL) as well. + illegal_xml_re = "[^\u0009\u000a\u000d\u0020-\u007e\u0080-\ud7ff\ue000-\ufffd\U00010000-\U0010ffff]" + return re.sub(illegal_xml_re, repl, str(arg)) + + +def merge_family(left, right) -> None: + result = {} + for kl, vl in left.items(): + for kr, vr in right.items(): + if not isinstance(vl, list): + raise TypeError(type(vl)) + result[kl] = vl + vr + left.update(result) + + +families = { # pylint: disable=dict-init-mutate + "_base": {"testcase": ["classname", "name"]}, + "_base_legacy": {"testcase": ["file", "line", "url"]}, +} +# xUnit 1.x inherits legacy attributes. +families["xunit1"] = families["_base"].copy() +merge_family(families["xunit1"], families["_base_legacy"]) + +# xUnit 2.x uses strict base attributes. +families["xunit2"] = families["_base"] + + +class _NodeReporter: + def __init__(self, nodeid: str | TestReport, xml: LogXML) -> None: + self.id = nodeid + self.xml = xml + self.add_stats = self.xml.add_stats + self.family = self.xml.family + self.duration = 0.0 + self.properties: list[tuple[str, str]] = [] + self.nodes: list[ET.Element] = [] + self.attrs: dict[str, str] = {} + + def append(self, node: ET.Element) -> None: + self.xml.add_stats(node.tag) + self.nodes.append(node) + + def add_property(self, name: str, value: object) -> None: + self.properties.append((str(name), bin_xml_escape(value))) + + def add_attribute(self, name: str, value: object) -> None: + self.attrs[str(name)] = bin_xml_escape(value) + + def make_properties_node(self) -> ET.Element | None: + """Return a Junit node containing custom properties, if any.""" + if self.properties: + properties = ET.Element("properties") + for name, value in self.properties: + properties.append(ET.Element("property", name=name, value=value)) + return properties + return None + + def record_testreport(self, testreport: TestReport) -> None: + names = mangle_test_address(testreport.nodeid) + existing_attrs = self.attrs + classnames = names[:-1] + if self.xml.prefix: + classnames.insert(0, self.xml.prefix) + attrs: dict[str, str] = { + "classname": ".".join(classnames), + "name": bin_xml_escape(names[-1]), + "file": testreport.location[0], + } + if testreport.location[1] is not None: + attrs["line"] = str(testreport.location[1]) + if hasattr(testreport, "url"): + attrs["url"] = testreport.url + self.attrs = attrs + self.attrs.update(existing_attrs) # Restore any user-defined attributes. + + # Preserve legacy testcase behavior. + if self.family == "xunit1": + return + + # Filter out attributes not permitted by this test family. + # Including custom attributes because they are not valid here. + temp_attrs = {} + for key in self.attrs: + if key in families[self.family]["testcase"]: + temp_attrs[key] = self.attrs[key] + self.attrs = temp_attrs + + def to_xml(self) -> ET.Element: + testcase = ET.Element("testcase", self.attrs, time=f"{self.duration:.3f}") + properties = self.make_properties_node() + if properties is not None: + testcase.append(properties) + testcase.extend(self.nodes) + return testcase + + def _add_simple(self, tag: str, message: str, data: str | None = None) -> None: + node = ET.Element(tag, message=message) + node.text = bin_xml_escape(data) + self.append(node) + + def write_captured_output(self, report: TestReport) -> None: + if not self.xml.log_passing_tests and report.passed: + return + + content_out = report.capstdout + content_log = report.caplog + content_err = report.capstderr + if self.xml.logging == "no": + return + content_all = "" + if self.xml.logging in ["log", "all"]: + content_all = self._prepare_content(content_log, " Captured Log ") + if self.xml.logging in ["system-out", "out-err", "all"]: + content_all += self._prepare_content(content_out, " Captured Out ") + self._write_content(report, content_all, "system-out") + content_all = "" + if self.xml.logging in ["system-err", "out-err", "all"]: + content_all += self._prepare_content(content_err, " Captured Err ") + self._write_content(report, content_all, "system-err") + content_all = "" + if content_all: + self._write_content(report, content_all, "system-out") + + def _prepare_content(self, content: str, header: str) -> str: + return "\n".join([header.center(80, "-"), content, ""]) + + def _write_content(self, report: TestReport, content: str, jheader: str) -> None: + tag = ET.Element(jheader) + tag.text = bin_xml_escape(content) + self.append(tag) + + def append_pass(self, report: TestReport) -> None: + self.add_stats("passed") + + def append_failure(self, report: TestReport) -> None: + # msg = str(report.longrepr.reprtraceback.extraline) + if hasattr(report, "wasxfail"): + self._add_simple("skipped", "xfail-marked test passes unexpectedly") + else: + assert report.longrepr is not None + reprcrash: ReprFileLocation | None = getattr( + report.longrepr, "reprcrash", None + ) + if reprcrash is not None: + message = reprcrash.message + else: + message = str(report.longrepr) + message = bin_xml_escape(message) + self._add_simple("failure", message, str(report.longrepr)) + + def append_collect_error(self, report: TestReport) -> None: + # msg = str(report.longrepr.reprtraceback.extraline) + assert report.longrepr is not None + self._add_simple("error", "collection failure", str(report.longrepr)) + + def append_collect_skipped(self, report: TestReport) -> None: + self._add_simple("skipped", "collection skipped", str(report.longrepr)) + + def append_error(self, report: TestReport) -> None: + assert report.longrepr is not None + reprcrash: ReprFileLocation | None = getattr(report.longrepr, "reprcrash", None) + if reprcrash is not None: + reason = reprcrash.message + else: + reason = str(report.longrepr) + + if report.when == "teardown": + msg = f'failed on teardown with "{reason}"' + else: + msg = f'failed on setup with "{reason}"' + self._add_simple("error", bin_xml_escape(msg), str(report.longrepr)) + + def append_skipped(self, report: TestReport) -> None: + if hasattr(report, "wasxfail"): + xfailreason = report.wasxfail + if xfailreason.startswith("reason: "): + xfailreason = xfailreason[8:] + xfailreason = bin_xml_escape(xfailreason) + skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason) + self.append(skipped) + else: + assert isinstance(report.longrepr, tuple) + filename, lineno, skipreason = report.longrepr + if skipreason.startswith("Skipped: "): + skipreason = skipreason[9:] + details = f"{filename}:{lineno}: {skipreason}" + + skipped = ET.Element( + "skipped", type="pytest.skip", message=bin_xml_escape(skipreason) + ) + skipped.text = bin_xml_escape(details) + self.append(skipped) + self.write_captured_output(report) + + def finalize(self) -> None: + data = self.to_xml() + self.__dict__.clear() + # Type ignored because mypy doesn't like overriding a method. + # Also the return value doesn't match... + self.to_xml = lambda: data # type: ignore[method-assign] + + +def _warn_incompatibility_with_xunit2( + request: FixtureRequest, fixture_name: str +) -> None: + """Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions.""" + from _pytest.warning_types import PytestWarning + + xml = request.config.stash.get(xml_key, None) + if xml is not None and xml.family not in ("xunit1", "legacy"): + request.node.warn( + PytestWarning( + f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')" + ) + ) + + +@pytest.fixture +def record_property(request: FixtureRequest) -> Callable[[str, object], None]: + """Add extra properties to the calling test. + + User properties become part of the test report and are available to the + configured reporters, like JUnit XML. + + The fixture is callable with ``name, value``. The value is automatically + XML-encoded. + + Example:: + + def test_function(record_property): + record_property("example_key", 1) + """ + _warn_incompatibility_with_xunit2(request, "record_property") + + def append_property(name: str, value: object) -> None: + request.node.user_properties.append((name, value)) + + return append_property + + +@pytest.fixture +def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], None]: + """Add extra xml attributes to the tag for the calling test. + + The fixture is callable with ``name, value``. The value is + automatically XML-encoded. + """ + from _pytest.warning_types import PytestExperimentalApiWarning + + request.node.warn( + PytestExperimentalApiWarning("record_xml_attribute is an experimental feature") + ) + + _warn_incompatibility_with_xunit2(request, "record_xml_attribute") + + # Declare noop + def add_attr_noop(name: str, value: object) -> None: + pass + + attr_func = add_attr_noop + + xml = request.config.stash.get(xml_key, None) + if xml is not None: + node_reporter = xml.node_reporter(request.node.nodeid) + attr_func = node_reporter.add_attribute + + return attr_func + + +def _check_record_param_type(param: str, v: str) -> None: + """Used by record_testsuite_property to check that the given parameter name is of the proper + type.""" + __tracebackhide__ = True + if not isinstance(v, str): + msg = "{param} parameter needs to be a string, but {g} given" # type: ignore[unreachable] + raise TypeError(msg.format(param=param, g=type(v).__name__)) + + +@pytest.fixture(scope="session") +def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]: + """Record a new ```` tag as child of the root ````. + + This is suitable to writing global information regarding the entire test + suite, and is compatible with ``xunit2`` JUnit family. + + This is a ``session``-scoped fixture which is called with ``(name, value)``. Example: + + .. code-block:: python + + def test_foo(record_testsuite_property): + record_testsuite_property("ARCH", "PPC") + record_testsuite_property("STORAGE_TYPE", "CEPH") + + :param name: + The property name. + :param value: + The property value. Will be converted to a string. + + .. warning:: + + Currently this fixture **does not work** with the + `pytest-xdist `__ plugin. See + :issue:`7767` for details. + """ + __tracebackhide__ = True + + def record_func(name: str, value: object) -> None: + """No-op function in case --junit-xml was not passed in the command-line.""" + __tracebackhide__ = True + _check_record_param_type("name", name) + + xml = request.config.stash.get(xml_key, None) + if xml is not None: + record_func = xml.add_global_property + return record_func + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("terminal reporting") + group.addoption( + "--junitxml", + "--junit-xml", + action="store", + dest="xmlpath", + metavar="path", + type=functools.partial(filename_arg, optname="--junitxml"), + default=None, + help="Create junit-xml style report file at given path", + ) + group.addoption( + "--junitprefix", + "--junit-prefix", + action="store", + metavar="str", + default=None, + help="Prepend prefix to classnames in junit-xml output", + ) + parser.addini( + "junit_suite_name", "Test suite name for JUnit report", default="pytest" + ) + parser.addini( + "junit_logging", + "Write captured log messages to JUnit report: " + "one of no|log|system-out|system-err|out-err|all", + default="no", + ) + parser.addini( + "junit_log_passing_tests", + "Capture log information for passing tests to JUnit report: ", + type="bool", + default=True, + ) + parser.addini( + "junit_duration_report", + "Duration time to report: one of total|call", + default="total", + ) # choices=['total', 'call']) + parser.addini( + "junit_family", + "Emit XML for schema: one of legacy|xunit1|xunit2", + default="xunit2", + ) + + +def pytest_configure(config: Config) -> None: + xmlpath = config.option.xmlpath + # Prevent opening xmllog on worker nodes (xdist). + if xmlpath and not hasattr(config, "workerinput"): + junit_family = config.getini("junit_family") + config.stash[xml_key] = LogXML( + xmlpath, + config.option.junitprefix, + config.getini("junit_suite_name"), + config.getini("junit_logging"), + config.getini("junit_duration_report"), + junit_family, + config.getini("junit_log_passing_tests"), + ) + config.pluginmanager.register(config.stash[xml_key]) + + +def pytest_unconfigure(config: Config) -> None: + xml = config.stash.get(xml_key, None) + if xml: + del config.stash[xml_key] + config.pluginmanager.unregister(xml) + + +def mangle_test_address(address: str) -> list[str]: + path, possible_open_bracket, params = address.partition("[") + names = path.split("::") + # Convert file path to dotted path. + names[0] = names[0].replace(nodes.SEP, ".") + names[0] = re.sub(r"\.py$", "", names[0]) + # Put any params back. + names[-1] += possible_open_bracket + params + return names + + +class LogXML: + def __init__( + self, + logfile, + prefix: str | None, + suite_name: str = "pytest", + logging: str = "no", + report_duration: str = "total", + family="xunit1", + log_passing_tests: bool = True, + ) -> None: + logfile = os.path.expanduser(os.path.expandvars(logfile)) + self.logfile = os.path.normpath(os.path.abspath(logfile)) + self.prefix = prefix + self.suite_name = suite_name + self.logging = logging + self.log_passing_tests = log_passing_tests + self.report_duration = report_duration + self.family = family + self.stats: dict[str, int] = dict.fromkeys( + ["error", "passed", "failure", "skipped"], 0 + ) + self.node_reporters: dict[tuple[str | TestReport, object], _NodeReporter] = {} + self.node_reporters_ordered: list[_NodeReporter] = [] + self.global_properties: list[tuple[str, str]] = [] + + # List of reports that failed on call but teardown is pending. + self.open_reports: list[TestReport] = [] + self.cnt_double_fail_tests = 0 + + # Replaces convenience family with real family. + if self.family == "legacy": + self.family = "xunit1" + + def finalize(self, report: TestReport) -> None: + nodeid = getattr(report, "nodeid", report) + # Local hack to handle xdist report order. + workernode = getattr(report, "node", None) + reporter = self.node_reporters.pop((nodeid, workernode)) + + for propname, propvalue in report.user_properties: + reporter.add_property(propname, str(propvalue)) + + if reporter is not None: + reporter.finalize() + + def node_reporter(self, report: TestReport | str) -> _NodeReporter: + nodeid: str | TestReport = getattr(report, "nodeid", report) + # Local hack to handle xdist report order. + workernode = getattr(report, "node", None) + + key = nodeid, workernode + + if key in self.node_reporters: + # TODO: breaks for --dist=each + return self.node_reporters[key] + + reporter = _NodeReporter(nodeid, self) + + self.node_reporters[key] = reporter + self.node_reporters_ordered.append(reporter) + + return reporter + + def add_stats(self, key: str) -> None: + if key in self.stats: + self.stats[key] += 1 + + def _opentestcase(self, report: TestReport) -> _NodeReporter: + reporter = self.node_reporter(report) + reporter.record_testreport(report) + return reporter + + def pytest_runtest_logreport(self, report: TestReport) -> None: + """Handle a setup/call/teardown report, generating the appropriate + XML tags as necessary. + + Note: due to plugins like xdist, this hook may be called in interlaced + order with reports from other nodes. For example: + + Usual call order: + -> setup node1 + -> call node1 + -> teardown node1 + -> setup node2 + -> call node2 + -> teardown node2 + + Possible call order in xdist: + -> setup node1 + -> call node1 + -> setup node2 + -> call node2 + -> teardown node2 + -> teardown node1 + """ + close_report = None + if report.passed: + if report.when == "call": # ignore setup/teardown + reporter = self._opentestcase(report) + reporter.append_pass(report) + elif report.failed: + if report.when == "teardown": + # The following vars are needed when xdist plugin is used. + report_wid = getattr(report, "worker_id", None) + report_ii = getattr(report, "item_index", None) + close_report = next( + ( + rep + for rep in self.open_reports + if ( + rep.nodeid == report.nodeid + and getattr(rep, "item_index", None) == report_ii + and getattr(rep, "worker_id", None) == report_wid + ) + ), + None, + ) + if close_report: + # We need to open new testcase in case we have failure in + # call and error in teardown in order to follow junit + # schema. + self.finalize(close_report) + else: + # A passing call with a teardown error creates separate + # terminal reports, but JUnit XML keeps one testcase + # element for that item (#3850). + self.cnt_double_fail_tests += int( + ( + report.nodeid, + getattr(report, "node", None), + ) + in self.node_reporters + ) + reporter = self._opentestcase(report) + if report.when == "call": + reporter.append_failure(report) + self.open_reports.append(report) + if not self.log_passing_tests: + reporter.write_captured_output(report) + else: + reporter.append_error(report) + elif report.skipped: + reporter = self._opentestcase(report) + reporter.append_skipped(report) + self.update_testcase_duration(report) + if report.when == "teardown": + reporter = self._opentestcase(report) + reporter.write_captured_output(report) + + self.finalize(report) + report_wid = getattr(report, "worker_id", None) + report_ii = getattr(report, "item_index", None) + close_report = next( + ( + rep + for rep in self.open_reports + if ( + rep.nodeid == report.nodeid + and getattr(rep, "item_index", None) == report_ii + and getattr(rep, "worker_id", None) == report_wid + ) + ), + None, + ) + if close_report: + self.open_reports.remove(close_report) + + def update_testcase_duration(self, report: TestReport) -> None: + """Accumulate total duration for nodeid from given report and update + the Junit.testcase with the new total if already created.""" + if self.report_duration in {"total", report.when}: + reporter = self.node_reporter(report) + reporter.duration += getattr(report, "duration", 0.0) + + def pytest_collectreport(self, report: TestReport) -> None: + if not report.passed: + reporter = self._opentestcase(report) + if report.failed: + reporter.append_collect_error(report) + else: + reporter.append_collect_skipped(report) + + def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: + reporter = self.node_reporter("internal") + reporter.attrs.update(classname="pytest", name="internal") + reporter._add_simple("error", "internal error", str(excrepr)) + + def pytest_sessionstart(self) -> None: + self.suite_start = timing.Instant() + + def pytest_sessionfinish(self) -> None: + dirname = os.path.dirname(os.path.abspath(self.logfile)) + # exist_ok avoids filesystem race conditions between checking path existence and requesting creation + os.makedirs(dirname, exist_ok=True) + + with open(self.logfile, "w", encoding="utf-8") as logfile: + duration = self.suite_start.elapsed() + + numtests = ( + self.stats["passed"] + + self.stats["failure"] + + self.stats["skipped"] + + self.stats["error"] + - self.cnt_double_fail_tests + ) + logfile.write('') + + suite_node = ET.Element( + "testsuite", + name=self.suite_name, + errors=str(self.stats["error"]), + failures=str(self.stats["failure"]), + skipped=str(self.stats["skipped"]), + tests=str(numtests), + time=f"{duration.seconds:.3f}", + timestamp=self.suite_start.as_utc().astimezone().isoformat(), + hostname=platform.node(), + ) + global_properties = self._get_global_properties_node() + if global_properties is not None: + suite_node.append(global_properties) + for node_reporter in self.node_reporters_ordered: + suite_node.append(node_reporter.to_xml()) + testsuites = ET.Element("testsuites") + testsuites.set("name", "pytest tests") + testsuites.append(suite_node) + logfile.write(ET.tostring(testsuites, encoding="unicode")) + + def pytest_terminal_summary( + self, terminalreporter: TerminalReporter, config: pytest.Config + ) -> None: + if config.get_verbosity() >= 0: + terminalreporter.write_sep("-", f"generated xml file: {self.logfile}") + + def add_global_property(self, name: str, value: object) -> None: + __tracebackhide__ = True + _check_record_param_type("name", name) + self.global_properties.append((name, bin_xml_escape(value))) + + def _get_global_properties_node(self) -> ET.Element | None: + """Return a Junit node containing custom properties, if any.""" + if self.global_properties: + properties = ET.Element("properties") + for name, value in self.global_properties: + properties.append(ET.Element("property", name=name, value=value)) + return properties + return None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/legacypath.py b/.venv-test/lib/python3.12/site-packages/_pytest/legacypath.py new file mode 100644 index 0000000..59e8ef6 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/legacypath.py @@ -0,0 +1,468 @@ +# mypy: allow-untyped-defs +"""Add backward compatibility support for the legacy py path type.""" + +from __future__ import annotations + +import dataclasses +from pathlib import Path +import shlex +import subprocess +from typing import Final +from typing import final +from typing import TYPE_CHECKING + +from iniconfig import SectionWrapper + +from _pytest.cacheprovider import Cache +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config import PytestPluginManager +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.main import Session +from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.nodes import Node +from _pytest.pytester import HookRecorder +from _pytest.pytester import Pytester +from _pytest.pytester import RunResult +from _pytest.terminal import TerminalReporter +from _pytest.tmpdir import TempPathFactory + + +if TYPE_CHECKING: + import pexpect + + +@final +class Testdir: + """ + Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead. + + All methods just forward to an internal :class:`Pytester` instance, converting results + to `legacy_path` objects as necessary. + """ + + __test__ = False + + CLOSE_STDIN: Final = Pytester.CLOSE_STDIN + TimeoutExpired: Final = Pytester.TimeoutExpired + + def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + self._pytester = pytester + + @property + def tmpdir(self) -> LEGACY_PATH: + """Temporary directory where tests are executed.""" + return legacy_path(self._pytester.path) + + @property + def test_tmproot(self) -> LEGACY_PATH: + return legacy_path(self._pytester._test_tmproot) + + @property + def request(self): + return self._pytester._request + + @property + def plugins(self): + return self._pytester.plugins + + @plugins.setter + def plugins(self, plugins): + self._pytester.plugins = plugins + + @property + def monkeypatch(self) -> MonkeyPatch: + return self._pytester._monkeypatch + + def make_hook_recorder(self, pluginmanager) -> HookRecorder: + """See :meth:`Pytester.make_hook_recorder`.""" + return self._pytester.make_hook_recorder(pluginmanager) + + def chdir(self) -> None: + """See :meth:`Pytester.chdir`.""" + return self._pytester.chdir() + + def finalize(self) -> None: + return self._pytester._finalize() + + def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH: + """See :meth:`Pytester.makefile`.""" + if ext and not ext.startswith("."): + # pytester.makefile is going to throw a ValueError in a way that + # testdir.makefile did not, because + # pathlib.Path is stricter suffixes than py.path + # This ext arguments is likely user error, but since testdir has + # allowed this, we will prepend "." as a workaround to avoid breaking + # testdir usage that worked before + ext = "." + ext + return legacy_path(self._pytester.makefile(ext, *args, **kwargs)) + + def makeconftest(self, source) -> LEGACY_PATH: + """See :meth:`Pytester.makeconftest`.""" + return legacy_path(self._pytester.makeconftest(source)) + + def makeini(self, source) -> LEGACY_PATH: + """See :meth:`Pytester.makeini`.""" + return legacy_path(self._pytester.makeini(source)) + + def getinicfg(self, source: str) -> SectionWrapper: + """See :meth:`Pytester.getinicfg`.""" + return self._pytester.getinicfg(source) + + def makepyprojecttoml(self, source) -> LEGACY_PATH: + """See :meth:`Pytester.makepyprojecttoml`.""" + return legacy_path(self._pytester.makepyprojecttoml(source)) + + def makepyfile(self, *args, **kwargs) -> LEGACY_PATH: + """See :meth:`Pytester.makepyfile`.""" + return legacy_path(self._pytester.makepyfile(*args, **kwargs)) + + def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH: + """See :meth:`Pytester.maketxtfile`.""" + return legacy_path(self._pytester.maketxtfile(*args, **kwargs)) + + def syspathinsert(self, path=None) -> None: + """See :meth:`Pytester.syspathinsert`.""" + return self._pytester.syspathinsert(path) + + def mkdir(self, name) -> LEGACY_PATH: + """See :meth:`Pytester.mkdir`.""" + return legacy_path(self._pytester.mkdir(name)) + + def mkpydir(self, name) -> LEGACY_PATH: + """See :meth:`Pytester.mkpydir`.""" + return legacy_path(self._pytester.mkpydir(name)) + + def copy_example(self, name=None) -> LEGACY_PATH: + """See :meth:`Pytester.copy_example`.""" + return legacy_path(self._pytester.copy_example(name)) + + def getnode(self, config: Config, arg) -> Item | Collector | None: + """See :meth:`Pytester.getnode`.""" + return self._pytester.getnode(config, arg) + + def getpathnode(self, path): + """See :meth:`Pytester.getpathnode`.""" + return self._pytester.getpathnode(path) + + def genitems(self, colitems: list[Item | Collector]) -> list[Item]: + """See :meth:`Pytester.genitems`.""" + return self._pytester.genitems(colitems) + + def runitem(self, source): + """See :meth:`Pytester.runitem`.""" + return self._pytester.runitem(source) + + def inline_runsource(self, source, *cmdlineargs): + """See :meth:`Pytester.inline_runsource`.""" + return self._pytester.inline_runsource(source, *cmdlineargs) + + def inline_genitems(self, *args): + """See :meth:`Pytester.inline_genitems`.""" + return self._pytester.inline_genitems(*args) + + def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): + """See :meth:`Pytester.inline_run`.""" + return self._pytester.inline_run( + *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc + ) + + def runpytest_inprocess(self, *args, **kwargs) -> RunResult: + """See :meth:`Pytester.runpytest_inprocess`.""" + return self._pytester.runpytest_inprocess(*args, **kwargs) + + def runpytest(self, *args, **kwargs) -> RunResult: + """See :meth:`Pytester.runpytest`.""" + return self._pytester.runpytest(*args, **kwargs) + + def parseconfig(self, *args) -> Config: + """See :meth:`Pytester.parseconfig`.""" + return self._pytester.parseconfig(*args) + + def parseconfigure(self, *args) -> Config: + """See :meth:`Pytester.parseconfigure`.""" + return self._pytester.parseconfigure(*args) + + def getitem(self, source, funcname="test_func"): + """See :meth:`Pytester.getitem`.""" + return self._pytester.getitem(source, funcname) + + def getitems(self, source): + """See :meth:`Pytester.getitems`.""" + return self._pytester.getitems(source) + + def getmodulecol(self, source, configargs=(), withinit=False): + """See :meth:`Pytester.getmodulecol`.""" + return self._pytester.getmodulecol( + source, configargs=configargs, withinit=withinit + ) + + def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None: + """See :meth:`Pytester.collect_by_name`.""" + return self._pytester.collect_by_name(modcol, name) + + def popen( + self, + cmdargs, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=CLOSE_STDIN, + **kw, + ): + """See :meth:`Pytester.popen`.""" + return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw) + + def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: + """See :meth:`Pytester.run`.""" + return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin) + + def runpython(self, script) -> RunResult: + """See :meth:`Pytester.runpython`.""" + return self._pytester.runpython(script) + + def runpython_c(self, command): + """See :meth:`Pytester.runpython_c`.""" + return self._pytester.runpython_c(command) + + def runpytest_subprocess(self, *args, timeout=None) -> RunResult: + """See :meth:`Pytester.runpytest_subprocess`.""" + return self._pytester.runpytest_subprocess(*args, timeout=timeout) + + def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn: + """See :meth:`Pytester.spawn_pytest`.""" + return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) + + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn: + """See :meth:`Pytester.spawn`.""" + return self._pytester.spawn(cmd, expect_timeout=expect_timeout) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return str(self.tmpdir) + + +class LegacyTestdirPlugin: + @staticmethod + @fixture + def testdir(pytester: Pytester) -> Testdir: + """ + Identical to :fixture:`pytester`, and provides an instance whose methods return + legacy ``LEGACY_PATH`` objects instead when applicable. + + New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. + """ + return Testdir(pytester, _ispytest=True) + + +@final +@dataclasses.dataclass +class TempdirFactory: + """Backward compatibility wrapper that implements ``py.path.local`` + for :class:`TempPathFactory`. + + .. note:: + These days, it is preferred to use ``tmp_path_factory``. + + :ref:`About the tmpdir and tmpdir_factory fixtures`. + + """ + + _tmppath_factory: TempPathFactory + + def __init__( + self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + self._tmppath_factory = tmppath_factory + + def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH: + """Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object.""" + return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve()) + + def getbasetemp(self) -> LEGACY_PATH: + """Same as :meth:`TempPathFactory.getbasetemp`, but returns a ``py.path.local`` object.""" + return legacy_path(self._tmppath_factory.getbasetemp().resolve()) + + +class LegacyTmpdirPlugin: + @staticmethod + @fixture(scope="session") + def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: + """Return a :class:`pytest.TempdirFactory` instance for the test session.""" + # Set dynamically by pytest_configure(). + return request.config._tmpdirhandler # type: ignore + + @staticmethod + @fixture + def tmpdir(tmp_path: Path) -> LEGACY_PATH: + """Return a temporary directory (as `legacy_path`_ object) + which is unique to each test function invocation. + The temporary directory is created as a subdirectory + of the base temporary directory, with configurable retention, + as discussed in :ref:`temporary directory location and retention`. + + .. note:: + These days, it is preferred to use ``tmp_path``. + + :ref:`About the tmpdir and tmpdir_factory fixtures`. + + .. _legacy_path: https://py.readthedocs.io/en/latest/path.html + """ + return legacy_path(tmp_path) + + +def Cache_makedir(self: Cache, name: str) -> LEGACY_PATH: + """Return a directory path object with the given name. + + Same as :func:`mkdir`, but returns a legacy py path instance. + """ + return legacy_path(self.mkdir(name)) + + +def FixtureRequest_fspath(self: FixtureRequest) -> LEGACY_PATH: + """(deprecated) The file system path of the test module which collected this test.""" + return legacy_path(self.path) + + +def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH: + """The directory from which pytest was invoked. + + Prefer to use ``startpath`` which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(self.startpath) + + +def Config_invocation_dir(self: Config) -> LEGACY_PATH: + """The directory from which pytest was invoked. + + Prefer to use :attr:`invocation_params.dir `, + which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(str(self.invocation_params.dir)) + + +def Config_rootdir(self: Config) -> LEGACY_PATH: + """The path to the :ref:`rootdir `. + + Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(str(self.rootpath)) + + +def Config_inifile(self: Config) -> LEGACY_PATH | None: + """The path to the :ref:`configfile `. + + Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. + + :type: Optional[LEGACY_PATH] + """ + return legacy_path(str(self.inipath)) if self.inipath else None + + +def Session_startdir(self: Session) -> LEGACY_PATH: + """The path from which pytest was invoked. + + Prefer to use ``startpath`` which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(self.startpath) + + +def Config__getini_unknown_type(self, name: str, type: str, value: str | list[str]): + if type == "pathlist": + # TODO: This assert is probably not valid in all cases. + assert self.inipath is not None + dp = self.inipath.parent + input_values = shlex.split(value) if isinstance(value, str) else value + return [legacy_path(str(dp / x)) for x in input_values] + else: + raise ValueError(f"unknown configuration type: {type}", value) + + +def Node_fspath(self: Node) -> LEGACY_PATH: + """(deprecated) returns a legacy_path copy of self.path""" + return legacy_path(self.path) + + +def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None: + self.path = Path(value) + + +@hookimpl(tryfirst=True) +def pytest_load_initial_conftests(early_config: Config) -> None: + """Monkeypatch legacy path attributes in several classes, as early as possible.""" + mp = MonkeyPatch() + early_config.add_cleanup(mp.undo) + + # Add Cache.makedir(). + mp.setattr(Cache, "makedir", Cache_makedir, raising=False) + + # Add FixtureRequest.fspath property. + mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False) + + # Add TerminalReporter.startdir property. + mp.setattr( + TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False + ) + + # Add Config.{invocation_dir,rootdir,inifile} properties. + mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False) + mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False) + mp.setattr(Config, "inifile", property(Config_inifile), raising=False) + + # Add Session.startdir property. + mp.setattr(Session, "startdir", property(Session_startdir), raising=False) + + # Add pathlist configuration type. + mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type) + + # Add Node.fspath property. + mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False) + + +@hookimpl +def pytest_configure(config: Config) -> None: + """Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed.""" + if config.pluginmanager.has_plugin("tmpdir"): + mp = MonkeyPatch() + config.add_cleanup(mp.undo) + # Create TmpdirFactory and attach it to the config object. + # + # This is to comply with existing plugins which expect the handler to be + # available at pytest_configure time, but ideally should be moved entirely + # to the tmpdir_factory session fixture. + try: + tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined] + except AttributeError: + # tmpdir plugin is blocked. + pass + else: + _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) + mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) + + config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir") + + +@hookimpl +def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None: + # pytester is not loaded by default and is commonly loaded from a conftest, + # so checking for it in `pytest_configure` is not enough. + is_pytester = plugin is manager.get_plugin("pytester") + if is_pytester and not manager.is_registered(LegacyTestdirPlugin): + manager.register(LegacyTestdirPlugin, "legacypath-pytester") diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/logging.py b/.venv-test/lib/python3.12/site-packages/_pytest/logging.py new file mode 100644 index 0000000..3204d43 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/logging.py @@ -0,0 +1,975 @@ +# mypy: allow-untyped-defs +"""Access and control log capturing.""" + +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Mapping +from collections.abc import Set as AbstractSet +from contextlib import contextmanager +from contextlib import nullcontext +from datetime import datetime +from datetime import timedelta +from datetime import timezone +import io +from io import StringIO +import logging +from logging import LogRecord +import os +from pathlib import Path +import re +from types import TracebackType +from typing import final +from typing import Generic +from typing import Literal +from typing import TYPE_CHECKING +from typing import TypeVar + +from _pytest import nodes +from _pytest._io import TerminalWriter +from _pytest.capture import CaptureManager +from _pytest.config import _strtobool +from _pytest.config import Config +from _pytest.config import create_terminal_writer +from _pytest.config import hookimpl +from _pytest.config import UsageError +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.main import Session +from _pytest.stash import StashKey +from _pytest.terminal import TerminalReporter + + +if TYPE_CHECKING: + logging_StreamHandler = logging.StreamHandler[StringIO] +else: + logging_StreamHandler = logging.StreamHandler + +DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" +DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" +_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m") +caplog_handler_key = StashKey["LogCaptureHandler"]() +caplog_records_key = StashKey[dict[str, list[logging.LogRecord]]]() + + +def _remove_ansi_escape_sequences(text: str) -> str: + return _ANSI_ESCAPE_SEQ.sub("", text) + + +class DatetimeFormatter(logging.Formatter): + """A logging formatter which formats record with + :func:`datetime.datetime.strftime` formatter instead of + :func:`time.strftime` in case of microseconds in format string. + """ + + def formatTime(self, record: LogRecord, datefmt: str | None = None) -> str: + if datefmt and "%f" in datefmt: + ct = self.converter(record.created) + tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone) + # Construct `datetime.datetime` object from `struct_time` + # and msecs information from `record` + # Using int() instead of round() to avoid it exceeding 1_000_000 and causing a ValueError (#11861). + dt = datetime(*ct[0:6], microsecond=int(record.msecs * 1000), tzinfo=tz) + return dt.strftime(datefmt) + # Use `logging.Formatter` for non-microsecond formats + return super().formatTime(record, datefmt) + + +class ColoredLevelFormatter(DatetimeFormatter): + """A logging formatter which colorizes the %(levelname)..s part of the + log format passed to __init__.""" + + LOGLEVEL_COLOROPTS: Mapping[int, AbstractSet[str]] = { + logging.CRITICAL: {"red"}, + logging.ERROR: {"red", "bold"}, + logging.WARNING: {"yellow"}, + logging.WARN: {"yellow"}, + logging.INFO: {"green"}, + logging.DEBUG: {"purple"}, + logging.NOTSET: set(), + } + LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*(?:\.\d+)?s)") + + def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._terminalwriter = terminalwriter + self._original_fmt = self._style._fmt + self._level_to_fmt_mapping: dict[int, str] = {} + + for level, color_opts in self.LOGLEVEL_COLOROPTS.items(): + self.add_color_level(level, *color_opts) + + def add_color_level(self, level: int, *color_opts: str) -> None: + """Add or update color opts for a log level. + + :param level: + Log level to apply a style to, e.g. ``logging.INFO``. + :param color_opts: + ANSI escape sequence color options. Capitalized colors indicates + background color, i.e. ``'green', 'Yellow', 'bold'`` will give bold + green text on yellow background. + + .. warning:: + This is an experimental API. + """ + assert self._fmt is not None + levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) + if not levelname_fmt_match: + return + levelname_fmt = levelname_fmt_match.group() + + formatted_levelname = levelname_fmt % {"levelname": logging.getLevelName(level)} + + # add ANSI escape sequences around the formatted levelname + color_kwargs = {name: True for name in color_opts} + colorized_formatted_levelname = self._terminalwriter.markup( + formatted_levelname, **color_kwargs + ) + self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub( + colorized_formatted_levelname, self._fmt + ) + + def format(self, record: logging.LogRecord) -> str: + fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt) + self._style._fmt = fmt + return super().format(record) + + +class PercentStyleMultiline(logging.PercentStyle): + """A logging style with special support for multiline messages. + + If the message of a record consists of multiple lines, this style + formats the message as if each line were logged separately. + """ + + def __init__(self, fmt: str, auto_indent: int | str | bool | None) -> None: + super().__init__(fmt) + self._auto_indent = self._get_auto_indent(auto_indent) + + @staticmethod + def _get_auto_indent(auto_indent_option: int | str | bool | None) -> int: + """Determine the current auto indentation setting. + + Specify auto indent behavior (on/off/fixed) by passing in + extra={"auto_indent": [value]} to the call to logging.log() or + using a --log-auto-indent [value] command line or the + log_auto_indent [value] config option. + + Default behavior is auto-indent off. + + Using the string "True" or "on" or the boolean True as the value + turns auto indent on, using the string "False" or "off" or the + boolean False or the int 0 turns it off, and specifying a + positive integer fixes the indentation position to the value + specified. + + Any other values for the option are invalid, and will silently be + converted to the default. + + :param None|bool|int|str auto_indent_option: + User specified option for indentation from command line, config + or extra kwarg. Accepts int, bool or str. str option accepts the + same range of values as boolean config options, as well as + positive integers represented in str form. + + :returns: + Indentation value, which can be + -1 (automatically determine indentation) or + 0 (auto-indent turned off) or + >0 (explicitly set indentation position). + """ + match auto_indent_option: + case None | False: + return 0 + case True: + return -1 + case int(): + return auto_indent_option + case str(): + try: + return int(auto_indent_option) + except ValueError: + pass + try: + if _strtobool(auto_indent_option): + return -1 + except ValueError: + return 0 + return 0 + + def format(self, record: logging.LogRecord) -> str: + if "\n" in record.message: + if hasattr(record, "auto_indent"): + # Passed in from the "extra={}" kwarg on the call to logging.log(). + auto_indent = self._get_auto_indent(record.auto_indent) + else: + auto_indent = self._auto_indent + + if auto_indent: + lines = record.message.splitlines() + formatted = self._fmt % {**record.__dict__, "message": lines[0]} + + if auto_indent < 0: + indentation = _remove_ansi_escape_sequences(formatted).find( + lines[0] + ) + else: + # Optimizes logging by allowing a fixed indentation. + indentation = auto_indent + lines[0] = formatted + return ("\n" + " " * indentation).join(lines) + return self._fmt % record.__dict__ + + +def get_option_ini(config: Config, *names: str): + for name in names: + ret = config.getoption(name) # 'default' arg won't work as expected + if ret is None: + ret = config.getini(name) + if ret: + return ret + + +def pytest_addoption(parser: Parser) -> None: + """Add options to control log capturing.""" + group = parser.getgroup("logging") + + def add_option_ini(option, dest, default=None, type=None, **kwargs): + parser.addini( + dest, default=default, type=type, help="Default value for " + option + ) + group.addoption(option, dest=dest, **kwargs) + + add_option_ini( + "--log-level", + dest="log_level", + default=None, + metavar="LEVEL", + help=( + "Level of messages to catch/display." + " Not set by default, so it depends on the root/parent log handler's" + ' effective level, where it is "WARNING" by default.' + ), + ) + add_option_ini( + "--log-format", + dest="log_format", + default=DEFAULT_LOG_FORMAT, + help="Log format used by the logging module", + ) + add_option_ini( + "--log-date-format", + dest="log_date_format", + default=DEFAULT_LOG_DATE_FORMAT, + help="Log date format used by the logging module", + ) + parser.addini( + "log_cli", + default=False, + type="bool", + help='Enable log display during test run (also known as "live logging")', + ) + add_option_ini( + "--log-cli-level", dest="log_cli_level", default=None, help="CLI logging level" + ) + add_option_ini( + "--log-cli-format", + dest="log_cli_format", + default=None, + help="Log format used by the logging module", + ) + add_option_ini( + "--log-cli-date-format", + dest="log_cli_date_format", + default=None, + help="Log date format used by the logging module", + ) + add_option_ini( + "--log-file", + dest="log_file", + default=None, + help="Path to a file when logging will be written to", + ) + add_option_ini( + "--log-file-mode", + dest="log_file_mode", + default="w", + choices=["w", "a"], + help="Log file open mode", + ) + add_option_ini( + "--log-file-level", + dest="log_file_level", + default=None, + help="Log file logging level", + ) + add_option_ini( + "--log-file-format", + dest="log_file_format", + default=None, + help="Log format used by the logging module", + ) + add_option_ini( + "--log-file-date-format", + dest="log_file_date_format", + default=None, + help="Log date format used by the logging module", + ) + add_option_ini( + "--log-auto-indent", + dest="log_auto_indent", + default=None, + help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.", + ) + group.addoption( + "--log-disable", + action="append", + default=[], + dest="logger_disable", + help="Disable a logger by name. Can be passed multiple times.", + ) + + +_HandlerType = TypeVar("_HandlerType", bound=logging.Handler) + + +# Not using @contextmanager for performance reasons. +class catching_logs(Generic[_HandlerType]): + """Context manager that prepares the whole logging machinery properly.""" + + __slots__ = ("attached_loggers", "handler", "level", "orig_level") + + def __init__(self, handler: _HandlerType, level: int | None = None) -> None: + self.handler = handler + self.level = level + self.attached_loggers: list[logging.Logger] = [] + + def __enter__(self) -> _HandlerType: + root_logger = logging.getLogger() + if self.level is not None: + self.handler.setLevel(self.level) + # Attach to root logger. + root_logger.addHandler(self.handler) + self.attached_loggers.append(root_logger) + # Attach to all non-propagating loggers (won't reach root). + # Note that will miss loggers that *become* non-propagating + # after the `__enter__`. Not worth the trouble for now. + for logger in root_logger.manager.loggerDict.values(): + if ( + isinstance(logger, logging.Logger) + and not logger.propagate + and logger is not root_logger + ): + logger.addHandler(self.handler) + self.attached_loggers.append(logger) + if self.level is not None: + # Non-propagating loggers still inherit the level (unless a logger + # explicitly set level), so only do this on the root logger. + self.orig_level = root_logger.level + root_logger.setLevel(min(self.orig_level, self.level)) + return self.handler + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + root_logger = logging.getLogger() + if self.level is not None: + root_logger.setLevel(self.orig_level) + for logger in self.attached_loggers: + logger.removeHandler(self.handler) + self.attached_loggers.clear() + + +class LogCaptureHandler(logging_StreamHandler): + """A logging handler that stores log records and the log text.""" + + def __init__(self) -> None: + """Create a new log handler.""" + super().__init__(StringIO()) + self.records: list[logging.LogRecord] = [] + + def emit(self, record: logging.LogRecord) -> None: + """Keep the log records in a list in addition to the log text.""" + self.records.append(record) + super().emit(record) + + def reset(self) -> None: + self.records = [] + self.stream = StringIO() + + def clear(self) -> None: + self.records.clear() + self.stream = StringIO() + + def handleError(self, record: logging.LogRecord) -> None: + if logging.raiseExceptions: + # Fail the test if the log message is bad (emit failed). + # The default behavior of logging is to print "Logging error" + # to stderr with the call stack and some extra details. + # pytest wants to make such mistakes visible during testing. + raise # noqa: PLE0704 + + +@final +class LogCaptureFixture: + """Provides access and control of log capturing.""" + + def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + self._item = item + self._initial_handler_level: int | None = None + # Dict of log name -> log level. + self._initial_logger_levels: dict[str | None, int] = {} + self._initial_disabled_logging_level: int | None = None + + def _finalize(self) -> None: + """Finalize the fixture. + + This restores the log levels and the disabled logging levels changed by :meth:`set_level`. + """ + # Restore log levels. + if self._initial_handler_level is not None: + self.handler.setLevel(self._initial_handler_level) + for logger_name, level in self._initial_logger_levels.items(): + logger = logging.getLogger(logger_name) + logger.setLevel(level) + # Disable logging at the original disabled logging level. + if self._initial_disabled_logging_level is not None: + logging.disable(self._initial_disabled_logging_level) + self._initial_disabled_logging_level = None + + @property + def handler(self) -> LogCaptureHandler: + """Get the logging handler used by the fixture.""" + return self._item.stash[caplog_handler_key] + + def get_records( + self, when: Literal["setup", "call", "teardown"] + ) -> list[logging.LogRecord]: + """Get the logging records for one of the possible test phases. + + :param when: + Which test phase to obtain the records from. + Valid values are: "setup", "call" and "teardown". + + :returns: The list of captured records at the given stage. + + .. versionadded:: 3.4 + """ + return self._item.stash[caplog_records_key].get(when, []) + + @property + def text(self) -> str: + """The formatted log text.""" + return _remove_ansi_escape_sequences(self.handler.stream.getvalue()) + + @property + def records(self) -> list[logging.LogRecord]: + """The list of log records.""" + return self.handler.records + + @property + def record_tuples(self) -> list[tuple[str, int, str]]: + """A list of a stripped down version of log records intended + for use in assertion comparison. + + The format of the tuple is: + + (logger_name, log_level, message) + """ + return [(r.name, r.levelno, r.getMessage()) for r in self.records] + + @property + def messages(self) -> list[str]: + """A list of format-interpolated log messages. + + Unlike 'records', which contains the format string and parameters for + interpolation, log messages in this list are all interpolated. + + Unlike 'text', which contains the output from the handler, log + messages in this list are unadorned with levels, timestamps, etc, + making exact comparisons more reliable. + + Note that traceback or stack info (from :func:`logging.exception` or + the `exc_info` or `stack_info` arguments to the logging functions) is + not included, as this is added by the formatter in the handler. + + .. versionadded:: 3.7 + """ + return [r.getMessage() for r in self.records] + + def clear(self) -> None: + """Reset the list of log records and the captured log text.""" + self.handler.clear() + + def _force_enable_logging( + self, level: int | str, logger_obj: logging.Logger + ) -> int: + """Enable the desired logging level if the global level was disabled via ``logging.disabled``. + + Only enables logging levels greater than or equal to the requested ``level``. + + Does nothing if the desired ``level`` wasn't disabled. + + :param level: + The logger level caplog should capture. + All logging is enabled if a non-standard logging level string is supplied. + Valid level strings are in :data:`logging._nameToLevel`. + :param logger_obj: The logger object to check. + + :return: The original disabled logging level. + """ + original_disable_level: int = logger_obj.manager.disable + + if isinstance(level, str): + # Try to translate the level string to an int for `logging.disable()` + level = logging.getLevelName(level) # type: ignore[deprecated] + + if not isinstance(level, int): + # The level provided was not valid, so just un-disable all logging. + logging.disable(logging.NOTSET) + elif not logger_obj.isEnabledFor(level): + # Each level is `10` away from other levels. + # https://docs.python.org/3/library/logging.html#logging-levels + disable_level = max(level - 10, logging.NOTSET) + logging.disable(disable_level) + + return original_disable_level + + def set_level(self, level: int | str, logger: str | None = None) -> None: + """Set the threshold level of a logger for the duration of a test. + + Logging messages which are less severe than this level will not be captured. + + .. versionchanged:: 3.4 + The levels of the loggers changed by this function will be + restored to their initial values at the end of the test. + + Will enable the requested logging level if it was disabled via :func:`logging.disable`. + + :param level: The level. + :param logger: The logger to update. If not given, the root logger. + """ + logger_obj = logging.getLogger(logger) + # Save the original log-level to restore it during teardown. + self._initial_logger_levels.setdefault(logger, logger_obj.level) + logger_obj.setLevel(level) + if self._initial_handler_level is None: + self._initial_handler_level = self.handler.level + self.handler.setLevel(level) + initial_disabled_logging_level = self._force_enable_logging(level, logger_obj) + if self._initial_disabled_logging_level is None: + self._initial_disabled_logging_level = initial_disabled_logging_level + + @contextmanager + def at_level(self, level: int | str, logger: str | None = None) -> Generator[None]: + """Context manager that sets the level for capturing of logs. After + the end of the 'with' statement the level is restored to its original + value. + + Will enable the requested logging level if it was disabled via :func:`logging.disable`. + + :param level: The level. + :param logger: The logger to update. If not given, the root logger. + """ + logger_obj = logging.getLogger(logger) + orig_level = logger_obj.level + logger_obj.setLevel(level) + handler_orig_level = self.handler.level + self.handler.setLevel(level) + original_disable_level = self._force_enable_logging(level, logger_obj) + try: + yield + finally: + logger_obj.setLevel(orig_level) + self.handler.setLevel(handler_orig_level) + logging.disable(original_disable_level) + + @contextmanager + def filtering(self, filter_: logging.Filter) -> Generator[None]: + """Context manager that temporarily adds the given filter to the caplog's + :meth:`handler` for the 'with' statement block, and removes that filter at the + end of the block. + + :param filter_: A custom :class:`logging.Filter` object. + + .. versionadded:: 7.5 + """ + self.handler.addFilter(filter_) + try: + yield + finally: + self.handler.removeFilter(filter_) + + +@fixture +def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture]: + """Access and control log capturing. + + Captured logs are available through the following properties/methods:: + + * caplog.messages -> list of format-interpolated log messages + * caplog.text -> string containing formatted log output + * caplog.records -> list of logging.LogRecord instances + * caplog.record_tuples -> list of (logger_name, level, message) tuples + * caplog.clear() -> clear captured records and formatted log output string + """ + result = LogCaptureFixture(request.node, _ispytest=True) + yield result + result._finalize() + + +def get_log_level_for_setting(config: Config, *setting_names: str) -> int | None: + for setting_name in setting_names: + log_level = config.getoption(setting_name) + if log_level is None: + log_level = config.getini(setting_name) + if log_level: + break + else: + return None + + if isinstance(log_level, str): + log_level = log_level.upper() + try: + return int(getattr(logging, log_level, log_level)) + except ValueError as e: + # Python logging does not recognise this as a logging level + raise UsageError( + f"'{log_level}' is not recognized as a logging level name for " + f"'{setting_name}'. Please consider passing the " + "logging level num instead." + ) from e + + +# run after terminalreporter/capturemanager are configured +@hookimpl(trylast=True) +def pytest_configure(config: Config) -> None: + config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") + + +class LoggingPlugin: + """Attaches to the logging module and captures log messages for each test.""" + + def __init__(self, config: Config) -> None: + """Create a new plugin to capture log messages. + + The formatter can be safely shared across all handlers so + create a single one for the entire test session here. + """ + self._config = config + + # Report logging. + self.formatter = self._create_formatter( + get_option_ini(config, "log_format"), + get_option_ini(config, "log_date_format"), + get_option_ini(config, "log_auto_indent"), + ) + self.log_level = get_log_level_for_setting(config, "log_level") + self.caplog_handler = LogCaptureHandler() + self.caplog_handler.setFormatter(self.formatter) + self.report_handler = LogCaptureHandler() + self.report_handler.setFormatter(self.formatter) + + # File logging. + self.log_file_level = get_log_level_for_setting( + config, "log_file_level", "log_level" + ) + log_file = get_option_ini(config, "log_file") or os.devnull + if log_file != os.devnull: + directory = os.path.dirname(os.path.abspath(log_file)) + if not os.path.isdir(directory): + os.makedirs(directory) + + self.log_file_mode = get_option_ini(config, "log_file_mode") or "w" + self.log_file_handler = _FileHandler( + log_file, mode=self.log_file_mode, encoding="UTF-8" + ) + log_file_format = get_option_ini(config, "log_file_format", "log_format") + log_file_date_format = get_option_ini( + config, "log_file_date_format", "log_date_format" + ) + + log_file_formatter = DatetimeFormatter( + log_file_format, datefmt=log_file_date_format + ) + self.log_file_handler.setFormatter(log_file_formatter) + + # CLI/live logging. + self.log_cli_level = get_log_level_for_setting( + config, "log_cli_level", "log_level" + ) + if self._log_cli_enabled(): + terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") + # Guaranteed by `_log_cli_enabled()`. + assert terminal_reporter is not None + capture_manager = config.pluginmanager.get_plugin("capturemanager") + # if capturemanager plugin is disabled, live logging still works. + self.log_cli_handler: ( + _LiveLoggingStreamHandler | _LiveLoggingNullHandler + ) = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) + else: + self.log_cli_handler = _LiveLoggingNullHandler() + log_cli_formatter = self._create_formatter( + get_option_ini(config, "log_cli_format", "log_format"), + get_option_ini(config, "log_cli_date_format", "log_date_format"), + get_option_ini(config, "log_auto_indent"), + ) + self.log_cli_handler.setFormatter(log_cli_formatter) + self._disable_loggers(loggers_to_disable=config.option.logger_disable) + + def _disable_loggers(self, loggers_to_disable: list[str]) -> None: + if not loggers_to_disable: + return + + for name in loggers_to_disable: + logger = logging.getLogger(name) + logger.disabled = True + + def _create_formatter(self, log_format, log_date_format, auto_indent): + # Color option doesn't exist if terminal plugin is disabled. + color = getattr(self._config.option, "color", "no") + if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search( + log_format + ): + formatter: logging.Formatter = ColoredLevelFormatter( + create_terminal_writer(self._config), log_format, log_date_format + ) + else: + formatter = DatetimeFormatter(log_format, log_date_format) + + formatter._style = PercentStyleMultiline( + formatter._style._fmt, auto_indent=auto_indent + ) + + return formatter + + def set_log_path(self, fname: str) -> None: + """Set the filename parameter for Logging.FileHandler(). + + Creates parent directory if it does not exist. + + .. warning:: + This is an experimental API. + """ + fpath = Path(fname) + + if not fpath.is_absolute(): + fpath = self._config.rootpath / fpath + + if not fpath.parent.exists(): + fpath.parent.mkdir(exist_ok=True, parents=True) + + # https://github.com/python/mypy/issues/11193 + stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment] + old_stream = self.log_file_handler.setStream(stream) + if old_stream: + old_stream.close() + + def _log_cli_enabled(self) -> bool: + """Return whether live logging is enabled.""" + enabled = self._config.getoption( + "--log-cli-level" + ) is not None or self._config.getini("log_cli") + if not enabled: + return False + + terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") + if terminal_reporter is None: + # terminal reporter is disabled e.g. by pytest-xdist. + return False + + return True + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_sessionstart(self) -> Generator[None]: + self.log_cli_handler.set_when("sessionstart") + + with catching_logs(self.log_cli_handler, level=self.log_cli_level): + with catching_logs(self.log_file_handler, level=self.log_file_level): + return (yield) + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_collection(self) -> Generator[None]: + self.log_cli_handler.set_when("collection") + + with catching_logs(self.log_cli_handler, level=self.log_cli_level): + with catching_logs(self.log_file_handler, level=self.log_file_level): + return (yield) + + @hookimpl(wrapper=True) + def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]: + if session.config.option.collectonly: + return (yield) + + if self._log_cli_enabled() and self._config.get_verbosity() < 1: + # The verbose flag is needed to avoid messy test progress output. + self._config.option.verbose = 1 + + with catching_logs(self.log_cli_handler, level=self.log_cli_level): + with catching_logs(self.log_file_handler, level=self.log_file_level): + return (yield) # Run all the tests. + + @hookimpl + def pytest_runtest_logstart(self) -> None: + self.log_cli_handler.reset() + self.log_cli_handler.set_when("start") + + @hookimpl + def pytest_runtest_logreport(self) -> None: + self.log_cli_handler.set_when("logreport") + + @contextmanager + def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None]: + """Implement the internals of the pytest_runtest_xxx() hooks.""" + with ( + catching_logs( + self.caplog_handler, + level=self.log_level, + ) as caplog_handler, + catching_logs( + self.report_handler, + level=self.log_level, + ) as report_handler, + ): + caplog_handler.reset() + report_handler.reset() + item.stash[caplog_records_key][when] = caplog_handler.records + item.stash[caplog_handler_key] = caplog_handler + + try: + yield + finally: + log = report_handler.stream.getvalue().strip() + item.add_report_section(when, "log", log) + + @hookimpl(wrapper=True) + def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None]: + self.log_cli_handler.set_when("setup") + + empty: dict[str, list[logging.LogRecord]] = {} + item.stash[caplog_records_key] = empty + with self._runtest_for(item, "setup"): + yield + + @hookimpl(wrapper=True) + def pytest_runtest_call(self, item: nodes.Item) -> Generator[None]: + self.log_cli_handler.set_when("call") + + with self._runtest_for(item, "call"): + yield + + @hookimpl(wrapper=True) + def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None]: + self.log_cli_handler.set_when("teardown") + + try: + with self._runtest_for(item, "teardown"): + yield + finally: + del item.stash[caplog_records_key] + del item.stash[caplog_handler_key] + + @hookimpl + def pytest_runtest_logfinish(self) -> None: + self.log_cli_handler.set_when("finish") + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_sessionfinish(self) -> Generator[None]: + self.log_cli_handler.set_when("sessionfinish") + + with catching_logs(self.log_cli_handler, level=self.log_cli_level): + with catching_logs(self.log_file_handler, level=self.log_file_level): + return (yield) + + @hookimpl + def pytest_unconfigure(self) -> None: + # Close the FileHandler explicitly. + # (logging.shutdown might have lost the weakref?!) + self.log_file_handler.close() + + +class _FileHandler(logging.FileHandler): + """A logging FileHandler with pytest tweaks.""" + + def handleError(self, record: logging.LogRecord) -> None: + # Handled by LogCaptureHandler. + pass + + +class _LiveLoggingStreamHandler(logging_StreamHandler): + """A logging StreamHandler used by the live logging feature: it will + write a newline before the first log message in each test. + + During live logging we must also explicitly disable stdout/stderr + capturing otherwise it will get captured and won't appear in the + terminal. + """ + + # Officially stream needs to be a IO[str], but TerminalReporter + # isn't. So force it. + stream: TerminalReporter = None # type: ignore + + def __init__( + self, + terminal_reporter: TerminalReporter, + capture_manager: CaptureManager | None, + ) -> None: + super().__init__(stream=terminal_reporter) # type: ignore[arg-type] + self.capture_manager = capture_manager + self.reset() + self.set_when(None) + self._test_outcome_written = False + + def reset(self) -> None: + """Reset the handler; should be called before the start of each test.""" + self._first_record_emitted = False + + def set_when(self, when: str | None) -> None: + """Prepare for the given test phase (setup/call/teardown).""" + self._when = when + self._section_name_shown = False + if when == "start": + self._test_outcome_written = False + + def emit(self, record: logging.LogRecord) -> None: + ctx_manager = ( + self.capture_manager.global_and_fixture_disabled() + if self.capture_manager + else nullcontext() + ) + with ctx_manager: + if not self._first_record_emitted: + self.stream.write("\n") + self._first_record_emitted = True + elif self._when in ("teardown", "finish"): + if not self._test_outcome_written: + self._test_outcome_written = True + self.stream.write("\n") + if not self._section_name_shown and self._when: + self.stream.section("live log " + self._when, sep="-", bold=True) + self._section_name_shown = True + super().emit(record) + + def handleError(self, record: logging.LogRecord) -> None: + # Handled by LogCaptureHandler. + pass + + +class _LiveLoggingNullHandler(logging.NullHandler): + """A logging handler used when live logging is disabled.""" + + def reset(self) -> None: + pass + + def set_when(self, when: str) -> None: + pass + + def handleError(self, record: logging.LogRecord) -> None: + # Handled by LogCaptureHandler. + pass diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/main.py b/.venv-test/lib/python3.12/site-packages/_pytest/main.py new file mode 100644 index 0000000..c4df4e4 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/main.py @@ -0,0 +1,1215 @@ +"""Core implementation of the testing process: init, session, runtest loop.""" + +from __future__ import annotations + +import argparse +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Sequence +from collections.abc import Set as AbstractSet +import dataclasses +import fnmatch +import functools +import importlib +import importlib.util +import os +from pathlib import Path +import sys +from typing import final +from typing import Literal +from typing import overload +from typing import TYPE_CHECKING +import warnings + +import pluggy + +from _pytest import nodes +import _pytest._code +from _pytest.config import Config +from _pytest.config import directory_arg +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config import PytestPluginManager +from _pytest.config import UsageError +from _pytest.config.argparsing import OverrideIniAction +from _pytest.config.argparsing import Parser +from _pytest.outcomes import exit +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import safe_exists +from _pytest.pathlib import samefile_nofollow +from _pytest.pathlib import scandir +from _pytest.reports import CollectReport +from _pytest.reports import TestReport +from _pytest.runner import collect_one_node +from _pytest.runner import SetupState +from _pytest.warning_types import PytestWarning + + +if TYPE_CHECKING: + from typing_extensions import Self + + from _pytest.fixtures import FixtureManager + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group._addoption( # private to use reserved lower-case short option + "-x", + "--exitfirst", + action="store_const", + dest="maxfail", + const=1, + help="Exit instantly on first error or failed test", + ) + group.addoption( + "--maxfail", + metavar="num", + action="store", + type=int, + dest="maxfail", + default=0, + help="Exit after first num failures or errors", + ) + group.addoption( + "--strict-config", + action=OverrideIniAction, + ini_option="strict_config", + ini_value="true", + help="Enables the strict_config option", + ) + group.addoption( + "--strict-markers", + action=OverrideIniAction, + ini_option="strict_markers", + ini_value="true", + help="Enables the strict_markers option", + ) + group.addoption( + "--strict", + action=OverrideIniAction, + ini_option="strict", + ini_value="true", + help="Enables the strict option", + ) + parser.addini( + "strict_config", + "Any warnings encountered while parsing the `pytest` section of the " + "configuration file raise errors", + type="bool", + # None => fallback to `strict`. + default=None, + ) + parser.addini( + "strict_markers", + "Markers not registered in the `markers` section of the configuration " + "file raise errors", + type="bool", + # None => fallback to `strict`. + default=None, + ) + parser.addini( + "strict", + "Enables all strictness options, currently: " + "strict_config, strict_markers, strict_xfail, strict_parametrization_ids", + type="bool", + default=False, + ) + + group = parser.getgroup("pytest-warnings") + group.addoption( + "-W", + "--pythonwarnings", + action="append", + help="Set which warnings to report, see -W option of Python itself", + ) + group.addoption( + "--max-warnings", + action="store", + type=int, + default=None, + metavar="num", + dest="max_warnings", + help="Exit with error if all tests pass but the number of warnings exceeds this threshold", + ) + parser.addini( + "filterwarnings", + type="linelist", + help="Each line specifies a pattern for " + "warnings.filterwarnings. " + "Processed after -W/--pythonwarnings.", + ) + parser.addini( + "max_warnings", + help="Exit with error if all tests pass but the number of warnings exceeds this threshold", + ) + + group = parser.getgroup("collect", "collection") + group.addoption( + "--collectonly", + "--collect-only", + "--co", + action="store_true", + help="Only collect tests, don't execute them", + ) + group.addoption( + "--pyargs", + action="store_true", + help="Try to interpret all arguments as Python packages", + ) + group.addoption( + "--ignore", + action="append", + metavar="path", + help="Ignore path during collection (multi-allowed)", + ) + group.addoption( + "--ignore-glob", + action="append", + metavar="path", + help="Ignore path pattern during collection (multi-allowed)", + ) + group.addoption( + "--deselect", + action="append", + metavar="nodeid_prefix", + help="Deselect item (via node id prefix) during collection (multi-allowed)", + ) + group.addoption( + "--confcutdir", + dest="confcutdir", + default=None, + metavar="dir", + type=functools.partial(directory_arg, optname="--confcutdir"), + help="Only load conftest.py's relative to specified dir", + ) + group.addoption( + "--noconftest", + action="store_true", + dest="noconftest", + default=False, + help="Don't load any conftest.py files", + ) + group.addoption( + "--keepduplicates", + "--keep-duplicates", + action="store_true", + dest="keepduplicates", + default=False, + help="Keep duplicate tests", + ) + group.addoption( + "--collect-in-virtualenv", + action="store_true", + dest="collect_in_virtualenv", + default=False, + help="Don't ignore tests in a local virtualenv directory", + ) + group.addoption( + "--continue-on-collection-errors", + action="store_true", + default=False, + dest="continue_on_collection_errors", + help="Force test execution even if collection errors occur", + ) + group.addoption( + "--import-mode", + default="prepend", + choices=["prepend", "append", "importlib"], + dest="importmode", + help="Prepend/append to sys.path when importing test modules and conftest " + "files. Default: prepend.", + ) + parser.addini( + "norecursedirs", + "Directory patterns to avoid for recursion", + type="args", + default=[ + "*.egg", + ".*", + "_darcs", + "build", + "CVS", + "dist", + "node_modules", + "venv", + "{arch}", + ], + ) + parser.addini( + "testpaths", + "Directories to search for tests when no files or directories are given on the " + "command line", + type="args", + default=[], + ) + parser.addini( + "collect_imported_tests", + "Whether to collect tests in imported modules outside `testpaths`", + type="bool", + default=True, + ) + parser.addini( + "consider_namespace_packages", + type="bool", + default=False, + help="Consider namespace packages when resolving module names during import", + ) + + group = parser.getgroup("debugconfig", "test session debugging and configuration") + group._addoption( # private to use reserved lower-case short option + "-c", + "--config-file", + metavar="FILE", + type=str, + dest="inifilename", + help="Load configuration from `FILE` instead of trying to locate one of the " + "implicit configuration files.", + ) + group.addoption( + "--rootdir", + action="store", + dest="rootdir", + help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', " + "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: " + "'$HOME/root_dir'.", + ) + group.addoption( + "--basetemp", + dest="basetemp", + default=None, + type=validate_basetemp, + metavar="dir", + help=( + "Base temporary directory for this test run. " + "(Warning: this directory is removed if it exists.)" + ), + ) + + +def validate_basetemp(path: str) -> str: + # GH 7119 + msg = "basetemp must not be empty, the current working directory or any parent directory of it" + + # empty path + if not path: + raise argparse.ArgumentTypeError(msg) + + def is_ancestor(base: Path, query: Path) -> bool: + """Return whether query is an ancestor of base.""" + if base == query: + return True + return query in base.parents + + # check if path is an ancestor of cwd + if is_ancestor(Path.cwd(), Path(path).absolute()): + raise argparse.ArgumentTypeError(msg) + + # check symlinks for ancestors + if is_ancestor(Path.cwd().resolve(), Path(path).resolve()): + raise argparse.ArgumentTypeError(msg) + + return path + + +def wrap_session( + config: Config, doit: Callable[[Config, Session], int | ExitCode | None] +) -> int | ExitCode: + """Skeleton command line program.""" + session = Session.from_config(config) + session.exitstatus = ExitCode.OK + initstate = 0 + try: + try: + config._do_configure() + initstate = 1 + config.hook.pytest_sessionstart(session=session) + initstate = 2 + session.exitstatus = doit(config, session) or 0 + except UsageError: + session.exitstatus = ExitCode.USAGE_ERROR + raise + except Failed: + session.exitstatus = ExitCode.TESTS_FAILED + except (KeyboardInterrupt, exit.Exception): + excinfo = _pytest._code.ExceptionInfo.from_current() + exitstatus: int | ExitCode = ExitCode.INTERRUPTED + if isinstance(excinfo.value, exit.Exception): + if excinfo.value.returncode is not None: + exitstatus = excinfo.value.returncode + if initstate < 2: + sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n") + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = exitstatus + except BaseException: + session.exitstatus = ExitCode.INTERNAL_ERROR + excinfo = _pytest._code.ExceptionInfo.from_current() + try: + config.notify_exception(excinfo, config.option) + except exit.Exception as exc: + if exc.returncode is not None: + session.exitstatus = exc.returncode + sys.stderr.write(f"{type(exc).__name__}: {exc}\n") + else: + if isinstance(excinfo.value, SystemExit): + sys.stderr.write("mainloop: caught unexpected SystemExit!\n") + + finally: + # Explicitly break reference cycle. + excinfo = None # type: ignore + os.chdir(session.startpath) + if initstate >= 2: + try: + config.hook.pytest_sessionfinish( + session=session, exitstatus=session.exitstatus + ) + except exit.Exception as exc: + if exc.returncode is not None: + session.exitstatus = exc.returncode + sys.stderr.write(f"{type(exc).__name__}: {exc}\n") + config._ensure_unconfigure() + return session.exitstatus + + +def pytest_cmdline_main(config: Config) -> int | ExitCode: + return wrap_session(config, _main) + + +def _main(config: Config, session: Session) -> int | ExitCode | None: + """Default command line protocol for initialization, session, + running tests and reporting.""" + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + + if session.testsfailed: + return ExitCode.TESTS_FAILED + elif session.testscollected == 0: + return ExitCode.NO_TESTS_COLLECTED + return None + + +def pytest_collection(session: Session) -> None: + session.perform_collect() + + +def pytest_runtestloop(session: Session) -> bool: + if session.testsfailed and not session.config.option.continue_on_collection_errors: + raise session.Interrupted( + f"{session.testsfailed} error{'s' if session.testsfailed != 1 else ''} during collection" + ) + + if session.config.option.collectonly: + return True + + for i, item in enumerate(session.items): + nextitem = session.items[i + 1] if i + 1 < len(session.items) else None + item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) + if session.shouldfail: + raise session.Failed(session.shouldfail) + if session.shouldstop: + raise session.Interrupted(session.shouldstop) + return True + + +def _in_venv(path: Path) -> bool: + """Attempt to detect if ``path`` is the root of a Virtual Environment by + checking for the existence of the pyvenv.cfg file. + + [https://peps.python.org/pep-0405/] + + For regression protection we also check for conda environments that do not include pyenv.cfg yet -- + https://github.com/conda/conda/issues/13337 is the conda issue tracking adding pyenv.cfg. + + Checking for the `conda-meta/history` file per https://github.com/pytest-dev/pytest/issues/12652#issuecomment-2246336902. + + """ + try: + return ( + path.joinpath("pyvenv.cfg").is_file() + or path.joinpath("conda-meta", "history").is_file() + ) + except OSError: + return False + + +def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: + if collection_path.name == "__pycache__": + return True + + ignore_paths = config._getconftest_pathlist( + "collect_ignore", path=collection_path.parent + ) + ignore_paths = ignore_paths or [] + excludeopt = config.getoption("ignore") + if excludeopt: + ignore_paths.extend(absolutepath(x) for x in excludeopt) + + if collection_path in ignore_paths: + return True + + ignore_globs = config._getconftest_pathlist( + "collect_ignore_glob", path=collection_path.parent + ) + ignore_globs = ignore_globs or [] + excludeglobopt = config.getoption("ignore_glob") + if excludeglobopt: + ignore_globs.extend(absolutepath(x) for x in excludeglobopt) + + if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs): + return True + + allow_in_venv = config.getoption("collect_in_virtualenv") + if not allow_in_venv and _in_venv(collection_path): + return True + + if collection_path.is_dir(): + norecursepatterns = config.getini("norecursedirs") + if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns): + return True + + return None + + +def pytest_collect_directory( + path: Path, parent: nodes.Collector +) -> nodes.Collector | None: + return Dir.from_parent(parent, path=path) + + +def pytest_collection_modifyitems(items: list[nodes.Item], config: Config) -> None: + deselect_prefixes = tuple(config.getoption("deselect") or []) + if not deselect_prefixes: + return + + remaining = [] + deselected = [] + for colitem in items: + if colitem.nodeid.startswith(deselect_prefixes): + deselected.append(colitem) + else: + remaining.append(colitem) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + +class FSHookProxy: + def __init__( + self, + pm: PytestPluginManager, + remove_mods: AbstractSet[object], + ) -> None: + self.pm = pm + self.remove_mods = remove_mods + + def __getattr__(self, name: str) -> pluggy.HookCaller: + x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) + self.__dict__[name] = x + return x + + +class Interrupted(KeyboardInterrupt): + """Signals that the test run was interrupted.""" + + __module__ = "builtins" # For py3. + + +class Failed(Exception): + """Signals a stop as failed test run.""" + + +@dataclasses.dataclass +class _bestrelpath_cache(dict[Path, str]): + __slots__ = ("path",) + + path: Path + + def __missing__(self, path: Path) -> str: + r = bestrelpath(self.path, path) + self[path] = r + return r + + +@final +class Dir(nodes.Directory): + """Collector of files in a file system directory. + + .. versionadded:: 8.0 + + .. note:: + + Python directories with an `__init__.py` file are instead collected by + :class:`~pytest.Package` by default. Both are :class:`~pytest.Directory` + collectors. + """ + + @classmethod + def from_parent( # type: ignore[override] + cls, + parent: nodes.Collector, + *, + path: Path, + ) -> Self: + """The public constructor. + + :param parent: The parent collector of this Dir. + :param path: The directory's path. + :type path: pathlib.Path + """ + return super().from_parent(parent=parent, path=path) + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + config = self.config + col: nodes.Collector | None + cols: Sequence[nodes.Collector] + ihook = self.ihook + for direntry in scandir(self.path): + if direntry.is_dir(): + path = Path(direntry.path) + if not self.session.isinitpath(path, with_parents=True): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + col = ihook.pytest_collect_directory(path=path, parent=self) + if col is not None: + yield col + + elif direntry.is_file(): + path = Path(direntry.path) + if not self.session.isinitpath(path): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + cols = ihook.pytest_collect_file(file_path=path, parent=self) + yield from cols + + +@final +class Session(nodes.Collector): + """The root of the collection tree. + + ``Session`` collects the initial paths given as arguments to pytest. + """ + + Interrupted = Interrupted + Failed = Failed + # Set on the session by runner.pytest_sessionstart. + _setupstate: SetupState + # Set on the session by fixtures.pytest_sessionstart. + _fixturemanager: FixtureManager + exitstatus: int | ExitCode + + def __init__(self, config: Config) -> None: + super().__init__( + name="", + path=config.rootpath, + fspath=None, + parent=None, + config=config, + session=self, + nodeid="", + ) + self.testsfailed = 0 + self.testscollected = 0 + self._shouldstop: bool | str = False + self._shouldfail: bool | str = False + self.trace = config.trace.root.get("collection") + self._initialpaths: frozenset[Path] = frozenset() + self._initialpaths_with_parents: frozenset[Path] = frozenset() + self._notfound: list[tuple[str, Sequence[nodes.Collector]]] = [] + self._initial_parts: list[CollectionArgument] = [] + self._collection_cache: dict[nodes.Collector, CollectReport] = {} + self.items: list[nodes.Item] = [] + + self._bestrelpathcache: dict[Path, str] = _bestrelpath_cache(config.rootpath) + + self.config.pluginmanager.register(self, name="session") + + @classmethod + def from_config(cls, config: Config) -> Session: + session: Session = cls._create(config=config) + return session + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self.name} " + f"exitstatus=%r " + f"testsfailed={self.testsfailed} " + f"testscollected={self.testscollected}>" + ) % getattr(self, "exitstatus", "") + + @property + def shouldstop(self) -> bool | str: + return self._shouldstop + + @shouldstop.setter + def shouldstop(self, value: bool | str) -> None: + # The runner checks shouldfail and assumes that if it is set we are + # definitely stopping, so prevent unsetting it. + if value is False and self._shouldstop: + warnings.warn( + PytestWarning( + "session.shouldstop cannot be unset after it has been set; ignoring." + ), + stacklevel=2, + ) + return + self._shouldstop = value + + @property + def shouldfail(self) -> bool | str: + return self._shouldfail + + @shouldfail.setter + def shouldfail(self, value: bool | str) -> None: + # The runner checks shouldfail and assumes that if it is set we are + # definitely stopping, so prevent unsetting it. + if value is False and self._shouldfail: + warnings.warn( + PytestWarning( + "session.shouldfail cannot be unset after it has been set; ignoring." + ), + stacklevel=2, + ) + return + self._shouldfail = value + + @property + def startpath(self) -> Path: + """The path from which pytest was invoked. + + .. versionadded:: 7.0.0 + """ + return self.config.invocation_params.dir + + def _node_location_to_relpath(self, node_path: Path) -> str: + # bestrelpath is a quite slow function. + return self._bestrelpathcache[node_path] + + @hookimpl(tryfirst=True) + def pytest_collectstart(self) -> None: + if self.shouldfail: + raise self.Failed(self.shouldfail) + if self.shouldstop: + raise self.Interrupted(self.shouldstop) + + @hookimpl(tryfirst=True) + def pytest_runtest_logreport(self, report: TestReport | CollectReport) -> None: + if report.failed and not hasattr(report, "wasxfail"): + self.testsfailed += 1 + maxfail = self.config.getvalue("maxfail") + if maxfail and self.testsfailed >= maxfail: + self.shouldfail = f"stopping after {self.testsfailed} failures" + + pytest_collectreport = pytest_runtest_logreport + + def isinitpath( + self, + path: str | os.PathLike[str], + *, + with_parents: bool = False, + ) -> bool: + """Is path an initial path? + + An initial path is a path explicitly given to pytest on the command + line. + + :param with_parents: + If set, also return True if the path is a parent of an initial path. + + .. versionchanged:: 8.0 + Added the ``with_parents`` parameter. + """ + # Optimization: Path(Path(...)) is much slower than isinstance. + path_ = path if isinstance(path, Path) else Path(path) + if with_parents: + return path_ in self._initialpaths_with_parents + else: + return path_ in self._initialpaths + + def gethookproxy(self, fspath: os.PathLike[str]) -> pluggy.HookRelay: + # Optimization: Path(Path(...)) is much slower than isinstance. + path = fspath if isinstance(fspath, Path) else Path(fspath) + pm = self.config.pluginmanager + # Check if we have the common case of running + # hooks with all conftest.py files. + my_conftestmodules = pm._getconftestmodules(path) + remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + proxy: pluggy.HookRelay + if remove_mods: + # One or more conftests are not in use at this path. + proxy = FSHookProxy(pm, remove_mods) # type: ignore[assignment] + else: + # All plugins are active for this fspath. + proxy = self.config.hook + return proxy + + def _collect_path( + self, + path: Path, + path_cache: dict[Path, Sequence[nodes.Collector]], + ) -> Sequence[nodes.Collector]: + """Create a Collector for the given path. + + `path_cache` makes it so the same Collectors are returned for the same + path. + """ + if path in path_cache: + return path_cache[path] + + if path.is_dir(): + ihook = self.gethookproxy(path.parent) + col: nodes.Collector | None = ihook.pytest_collect_directory( + path=path, parent=self + ) + cols: Sequence[nodes.Collector] = (col,) if col is not None else () + + elif path.is_file(): + ihook = self.gethookproxy(path) + cols = ihook.pytest_collect_file(file_path=path, parent=self) + + else: + # Broken symlink or invalid/missing file. + cols = () + + path_cache[path] = cols + return cols + + @overload + def perform_collect( + self, args: Sequence[str] | None = ..., genitems: Literal[True] = ... + ) -> Sequence[nodes.Item]: ... + + @overload + def perform_collect( + self, args: Sequence[str] | None = ..., genitems: bool = ... + ) -> Sequence[nodes.Item | nodes.Collector]: ... + + def perform_collect( + self, args: Sequence[str] | None = None, genitems: bool = True + ) -> Sequence[nodes.Item | nodes.Collector]: + """Perform the collection phase for this session. + + This is called by the default :hook:`pytest_collection` hook + implementation; see the documentation of this hook for more details. + For testing purposes, it may also be called directly on a fresh + ``Session``. + + This function normally recursively expands any collectors collected + from the session to their items, and only items are returned. For + testing purposes, this may be suppressed by passing ``genitems=False``, + in which case the return value contains these collectors unexpanded, + and ``session.items`` is empty. + """ + if args is None: + args = self.config.args + + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + + hook = self.config.hook + + self._notfound = [] + self._initial_parts = [] + self._collection_cache = {} + self.items = [] + items: Sequence[nodes.Item | nodes.Collector] = self.items + consider_namespace_packages: bool = self.config.getini( + "consider_namespace_packages" + ) + try: + initialpaths: list[Path] = [] + initialpaths_with_parents: list[Path] = [] + + collection_args = [ + resolve_collection_argument( + self.config.invocation_params.dir, + arg, + i, + as_pypath=self.config.option.pyargs, + consider_namespace_packages=consider_namespace_packages, + ) + for i, arg in enumerate(args) + ] + + if not self.config.getoption("keepduplicates"): + # Normalize the collection arguments -- remove duplicates and overlaps. + self._initial_parts = normalize_collection_arguments(collection_args) + else: + self._initial_parts = collection_args + + for collection_argument in self._initial_parts: + initialpaths.append(collection_argument.path) + initialpaths_with_parents.append(collection_argument.path) + initialpaths_with_parents.extend(collection_argument.path.parents) + self._initialpaths = frozenset(initialpaths) + self._initialpaths_with_parents = frozenset(initialpaths_with_parents) + + rep = collect_one_node(self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + errors = [] + for arg, collectors in self._notfound: + if collectors: + errors.append( + f"not found: {arg}\n(no match in any of {collectors!r})" + ) + else: + errors.append(f"found no collectors for {arg}") + + raise UsageError(*errors) + + if not genitems: + items = rep.result + else: + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + + self.config.pluginmanager.check_pending() + hook.pytest_collection_modifyitems( + session=self, config=self.config, items=items + ) + finally: + self._notfound = [] + self._initial_parts = [] + self._collection_cache = {} + hook.pytest_collection_finish(session=self) + + if genitems: + self.testscollected = len(items) + + return items + + def _collect_one_node( + self, + node: nodes.Collector, + handle_dupes: bool = True, + ) -> tuple[CollectReport, bool]: + if node in self._collection_cache and handle_dupes: + rep = self._collection_cache[node] + return rep, True + else: + rep = collect_one_node(node) + self._collection_cache[node] = rep + return rep, False + + def collect(self) -> Iterator[nodes.Item | nodes.Collector]: + # This is a cache for the root directories of the initial paths. + # We can't use collection_cache for Session because of its special + # role as the bootstrapping collector. + path_cache: dict[Path, Sequence[nodes.Collector]] = {} + + pm = self.config.pluginmanager + + for collection_argument in self._initial_parts: + self.trace("processing argument", collection_argument) + self.trace.root.indent += 1 + + argpath = collection_argument.path + names = collection_argument.parts + parametrization = collection_argument.parametrization + module_name = collection_argument.module_name + + # resolve_collection_argument() ensures this. + if argpath.is_dir(): + assert not names, f"invalid arg {(argpath, names)!r}" + + paths = [argpath] + # Add relevant parents of the path, from the root, e.g. + # /a/b/c.py -> [/, /a, /a/b, /a/b/c.py] + if module_name is None: + # Paths outside of the confcutdir should not be considered. + for path in argpath.parents: + if not pm._is_in_confcutdir(path): + break + paths.insert(0, path) + else: + # For --pyargs arguments, only consider paths matching the module + # name. Paths beyond the package hierarchy are not included. + module_name_parts = module_name.split(".") + for i, path in enumerate(argpath.parents, 2): + if i > len(module_name_parts) or path.stem != module_name_parts[-i]: + break + paths.insert(0, path) + + # Start going over the parts from the root, collecting each level + # and discarding all nodes which don't match the level's part. + any_matched_in_initial_part = False + notfound_collectors = [] + work: list[tuple[nodes.Collector | nodes.Item, list[Path | str]]] = [ + (self, [*paths, *names]) + ] + while work: + matchnode, matchparts = work.pop() + + # Pop'd all of the parts, this is a match. + if not matchparts: + yield matchnode + any_matched_in_initial_part = True + continue + + # Should have been matched by now, discard. + if not isinstance(matchnode, nodes.Collector): + continue + + # Collect this level of matching. + # Collecting Session (self) is done directly to avoid endless + # recursion to this function. + subnodes: Sequence[nodes.Collector | nodes.Item] + if isinstance(matchnode, Session): + assert isinstance(matchparts[0], Path) + subnodes = matchnode._collect_path(matchparts[0], path_cache) + else: + # For backward compat, files given directly multiple + # times on the command line should not be deduplicated. + handle_dupes = not ( + len(matchparts) == 1 + and isinstance(matchparts[0], Path) + and matchparts[0].is_file() + ) + rep, duplicate = self._collect_one_node(matchnode, handle_dupes) + if not duplicate and not rep.passed: + # Report collection failures here to avoid failing to + # run some test specified in the command line because + # the module could not be imported (#134). + matchnode.ihook.pytest_collectreport(report=rep) + if not rep.passed: + continue + subnodes = rep.result + + # Prune this level. + any_matched_in_collector = False + for node in reversed(subnodes): + # Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`. + if isinstance(matchparts[0], Path): + is_match = node.path == matchparts[0] + if sys.platform == "win32" and not is_match: + # In case the file paths do not match, fallback to samefile() to + # account for short-paths on Windows (#11895). But use a version + # which doesn't resolve symlinks, otherwise we might match the + # same file more than once (#12039). + is_match = samefile_nofollow(node.path, matchparts[0]) + + # Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`. + else: + if len(matchparts) == 1: + # This the last part, one parametrization goes. + if parametrization is not None: + # A parametrized arg must match exactly. + is_match = node.name == matchparts[0] + parametrization + else: + # A non-parameterized arg matches all parametrizations (if any). + # TODO: Remove the hacky split once the collection structure + # contains parametrization. + is_match = node.name.split("[")[0] == matchparts[0] + else: + is_match = node.name == matchparts[0] + if is_match: + work.append((node, matchparts[1:])) + any_matched_in_collector = True + + if not any_matched_in_collector: + notfound_collectors.append(matchnode) + + if not any_matched_in_initial_part: + report_arg = "::".join((str(argpath), *names)) + self._notfound.append((report_arg, notfound_collectors)) + + self.trace.root.indent -= 1 + + def genitems(self, node: nodes.Item | nodes.Collector) -> Iterator[nodes.Item]: + self.trace("genitems", node) + if isinstance(node, nodes.Item): + node.ihook.pytest_itemcollected(item=node) + yield node + else: + assert isinstance(node, nodes.Collector) + # For backward compat, dedup only applies to files. + handle_dupes = not isinstance(node, nodes.File) + rep, duplicate = self._collect_one_node(node, handle_dupes) + if rep.passed: + for subnode in rep.result: + yield from self.genitems(subnode) + if not duplicate: + node.ihook.pytest_collectreport(report=rep) + + +def search_pypath( + module_name: str, *, consider_namespace_packages: bool = False +) -> str | None: + """Search sys.path for the given a dotted module name, and return its file + system path if found.""" + try: + spec = importlib.util.find_spec(module_name) + # AttributeError: looks like package module, but actually filename + # ImportError: module does not exist + # ValueError: not a module name + except (AttributeError, ImportError, ValueError): + return None + + if spec is None: + return None + + if ( + spec.submodule_search_locations is None + or len(spec.submodule_search_locations) == 0 + ): + # Must be a simple module. + return spec.origin + + if consider_namespace_packages: + # If submodule_search_locations is set, it's a package (regular or namespace). + # Typically there is a single entry, but documentation claims it can be empty too + # (e.g. if the package has no physical location). + return spec.submodule_search_locations[0] + + if spec.origin is None: + # This is only the case for namespace packages + return None + + return os.path.dirname(spec.origin) + + +@dataclasses.dataclass(frozen=True) +class CollectionArgument: + """A resolved collection argument.""" + + path: Path + parts: Sequence[str] + parametrization: str | None + module_name: str | None + original_index: int + + +def resolve_collection_argument( + invocation_path: Path, + arg: str, + arg_index: int, + *, + as_pypath: bool = False, + consider_namespace_packages: bool = False, +) -> CollectionArgument: + """Parse path arguments optionally containing selection parts and return (fspath, names). + + Command-line arguments can point to files and/or directories, and optionally contain + parts for specific tests selection, for example: + + "pkg/tests/test_foo.py::TestClass::test_foo" + + This function ensures the path exists, and returns a resolved `CollectionArgument`: + + CollectionArgument( + path=Path("/full/path/to/pkg/tests/test_foo.py"), + parts=["TestClass", "test_foo"], + module_name=None, + ) + + When as_pypath is True, expects that the command-line argument actually contains + module paths instead of file-system paths: + + "pkg.tests.test_foo::TestClass::test_foo[a,b]" + + In which case we search sys.path for a matching module, and then return the *path* to the + found module, which may look like this: + + CollectionArgument( + path=Path("/home/u/myvenv/lib/site-packages/pkg/tests/test_foo.py"), + parts=["TestClass", "test_foo"], + parametrization="[a,b]", + module_name="pkg.tests.test_foo", + ) + + If the path doesn't exist, raise UsageError. + If the path is a directory and selection parts are present, raise UsageError. + """ + base, squacket, rest = arg.partition("[") + strpath, *parts = base.split("::") + if squacket and not parts: + raise UsageError(f"path cannot contain [] parametrization: {arg}") + parametrization = f"{squacket}{rest}" if squacket else None + module_name = None + if as_pypath: + pyarg_strpath = search_pypath( + strpath, consider_namespace_packages=consider_namespace_packages + ) + if pyarg_strpath is not None: + module_name = strpath + strpath = pyarg_strpath + fspath = invocation_path / strpath + fspath = absolutepath(fspath) + if not safe_exists(fspath): + msg = ( + "module or package not found: {arg} (missing __init__.py?)" + if as_pypath + else "file or directory not found: {arg}" + ) + raise UsageError(msg.format(arg=arg)) + if parts and fspath.is_dir(): + msg = ( + "package argument cannot contain :: selection parts: {arg}" + if as_pypath + else "directory argument cannot contain :: selection parts: {arg}" + ) + raise UsageError(msg.format(arg=arg)) + return CollectionArgument( + path=fspath, + parts=parts, + parametrization=parametrization, + module_name=module_name, + original_index=arg_index, + ) + + +def is_collection_argument_subsumed_by( + arg: CollectionArgument, by: CollectionArgument +) -> bool: + """Check if `arg` is subsumed (contained) by `by`.""" + # First check path subsumption. + if by.path != arg.path: + # `by` subsumes `arg` if `by` is a parent directory of `arg` and has no + # parts (collects everything in that directory). + if not by.parts: + return arg.path.is_relative_to(by.path) + return False + # Paths are equal, check parts. + # For example: ("TestClass",) is a prefix of ("TestClass", "test_method"). + if len(by.parts) > len(arg.parts) or arg.parts[: len(by.parts)] != by.parts: + return False + # Paths and parts are equal, check parametrization. + # A `by` without parametrization (None) matches everything, e.g. + # `pytest x.py::test_it` matches `x.py::test_it[0]`. Otherwise must be + # exactly equal. + if by.parametrization is not None and by.parametrization != arg.parametrization: + return False + return True + + +def normalize_collection_arguments( + collection_args: Sequence[CollectionArgument], +) -> list[CollectionArgument]: + """Normalize collection arguments to eliminate overlapping paths and parts. + + Detects when collection arguments overlap in either paths or parts and only + keeps the shorter prefix, or the earliest argument if duplicate, preserving + order. The result is prefix-free. + """ + # A quadratic algorithm is not acceptable since large inputs are possible. + # So this uses an O(n*log(n)) algorithm which takes advantage of the + # property that after sorting, a collection argument will immediately + # precede collection arguments it subsumes. An O(n) algorithm is not worth + # it. + collection_args_sorted = sorted( + collection_args, + key=lambda arg: (arg.path, arg.parts, arg.parametrization or ""), + ) + normalized: list[CollectionArgument] = [] + last_kept = None + for arg in collection_args_sorted: + if last_kept is None or not is_collection_argument_subsumed_by(arg, last_kept): + normalized.append(arg) + last_kept = arg + normalized.sort(key=lambda arg: arg.original_index) + return normalized diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/mark/__init__.py b/.venv-test/lib/python3.12/site-packages/_pytest/mark/__init__.py new file mode 100644 index 0000000..56c407a --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/mark/__init__.py @@ -0,0 +1,301 @@ +"""Generic mechanism for marking and selecting python functions.""" + +from __future__ import annotations + +import collections +from collections.abc import Collection +from collections.abc import Iterable +from collections.abc import Set as AbstractSet +import dataclasses +from typing import TYPE_CHECKING + +from .expression import Expression +from .structures import _HiddenParam +from .structures import EMPTY_PARAMETERSET_OPTION +from .structures import get_empty_parameterset_mark +from .structures import HIDDEN_PARAM +from .structures import Mark +from .structures import MARK_GEN +from .structures import MarkDecorator +from .structures import MarkGenerator +from .structures import ParameterSet +from _pytest.compat import NOTSET +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config import UsageError +from _pytest.config.argparsing import Parser +from _pytest.stash import StashKey + + +if TYPE_CHECKING: + from _pytest.nodes import Item + + +__all__ = [ + "HIDDEN_PARAM", + "MARK_GEN", + "Mark", + "MarkDecorator", + "MarkGenerator", + "ParameterSet", + "get_empty_parameterset_mark", +] + + +old_mark_config_key = StashKey[Config | None]() + + +def param( + *values: object, + marks: MarkDecorator | Collection[MarkDecorator | Mark] = (), + id: str | _HiddenParam | None = None, +) -> ParameterSet: + """Specify a parameter in `pytest.mark.parametrize`_ calls or + :ref:`parametrized fixtures `. + + .. code-block:: python + + @pytest.mark.parametrize( + "test_input,expected", + [ + ("3+5", 8), + pytest.param("6*9", 42, marks=pytest.mark.xfail), + ], + ) + def test_eval(test_input, expected): + assert eval(test_input) == expected + + :param values: Variable args of the values of the parameter set, in order. + + :param marks: + A single mark or a list of marks to be applied to this parameter set. + + :ref:`pytest.mark.usefixtures ` cannot be added via this parameter. + + :type id: str | Literal[pytest.HIDDEN_PARAM] | None + :param id: + The id to attribute to this parameter set. + + .. versionadded:: 8.4 + :ref:`hidden-param` means to hide the parameter set + from the test name. Can only be used at most 1 time, as + test names need to be unique. + """ + return ParameterSet.param(*values, marks=marks, id=id) + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group._addoption( # private to use reserved lower-case short option + "-k", + action="store", + dest="keyword", + default="", + metavar="EXPRESSION", + help="Only run tests which match the given substring expression. " + "An expression is a Python evaluable expression " + "where all names are substring-matched against test names " + "and their parent classes. Example: -k 'test_method or test_" + "other' matches all test functions and classes whose name " + "contains 'test_method' or 'test_other', while -k 'not test_method' " + "matches those that don't contain 'test_method' in their names. " + "-k 'not test_method and not test_other' will eliminate the matches. " + "Additionally keywords are matched to classes and functions " + "containing extra names in their 'extra_keyword_matches' set, " + "as well as functions which have names assigned directly to them. " + "The matching is case-insensitive.", + ) + + group._addoption( # private to use reserved lower-case short option + "-m", + action="store", + dest="markexpr", + default="", + metavar="MARKEXPR", + help="Only run tests matching given mark expression. " + "For example: -m 'mark1 and not mark2'.", + ) + + group.addoption( + "--markers", + action="store_true", + help="show markers (builtin, plugin and per-project ones).", + ) + + parser.addini("markers", "Register new markers for test functions", "linelist") + parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets") + + +@hookimpl(tryfirst=True) +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + import _pytest.config + + if config.option.markers: + config._do_configure() + tw = _pytest.config.create_terminal_writer(config) + for line in config.getini("markers"): + parts = line.split(":", 1) + name = parts[0] + rest = parts[1] if len(parts) == 2 else "" + tw.write(f"@pytest.mark.{name}:", bold=True) + tw.line(rest) + tw.line() + config._ensure_unconfigure() + return 0 + + return None + + +@dataclasses.dataclass +class KeywordMatcher: + """A matcher for keywords. + + Given a list of names, matches any substring of one of these names. The + string inclusion check is case-insensitive. + + Will match on the name of colitem, including the names of its parents. + Only matches names of items which are either a :class:`Class` or a + :class:`Function`. + + Additionally, matches on names in the 'extra_keyword_matches' set of + any item, as well as names directly assigned to test functions. + """ + + __slots__ = ("_names",) + + _names: AbstractSet[str] + + @classmethod + def from_item(cls, item: Item) -> KeywordMatcher: + mapped_names = set() + + # Add the names of the current item and any parent items, + # except the Session and root Directory's which are not + # interesting for matching. + import pytest + + for node in item.listchain(): + if isinstance(node, pytest.Session): + continue + if isinstance(node, pytest.Directory) and isinstance( + node.parent, pytest.Session + ): + continue + mapped_names.add(node.name) + + # Add the names added as extra keywords to current or parent items. + mapped_names.update(item.listextrakeywords()) + + # Add the names attached to the current function through direct assignment. + function_obj = getattr(item, "function", None) + if function_obj: + mapped_names.update(function_obj.__dict__) + + # Add the markers to the keywords as we no longer handle them correctly. + mapped_names.update(mark.name for mark in item.iter_markers()) + + return cls(mapped_names) + + def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool: + if kwargs: + raise UsageError("Keyword expressions do not support call parameters.") + subname = subname.lower() + return any(subname in name.lower() for name in self._names) + + +def deselect_by_keyword(items: list[Item], config: Config) -> None: + keywordexpr = config.option.keyword.lstrip() + if not keywordexpr: + return + + expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'") + + remaining = [] + deselected = [] + for colitem in items: + if not expr.evaluate(KeywordMatcher.from_item(colitem)): + deselected.append(colitem) + else: + remaining.append(colitem) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + +@dataclasses.dataclass +class MarkMatcher: + """A matcher for markers which are present. + + Tries to match on any marker names, attached to the given colitem. + """ + + __slots__ = ("own_mark_name_mapping",) + + own_mark_name_mapping: dict[str, list[Mark]] + + @classmethod + def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher: + mark_name_mapping = collections.defaultdict(list) + for mark in markers: + mark_name_mapping[mark.name].append(mark) + return cls(mark_name_mapping) + + def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: + if not (matches := self.own_mark_name_mapping.get(name, [])): + return False + + for mark in matches: # pylint: disable=consider-using-any-or-all + if all(mark.kwargs.get(k, NOTSET) == v for k, v in kwargs.items()): + return True + return False + + +def deselect_by_mark(items: list[Item], config: Config) -> None: + matchexpr = config.option.markexpr + if not matchexpr: + return + + expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'") + remaining: list[Item] = [] + deselected: list[Item] = [] + for item in items: + if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())): + remaining.append(item) + else: + deselected.append(item) + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + +def _parse_expression(expr: str, exc_message: str) -> Expression: + try: + return Expression.compile(expr) + except SyntaxError as e: + raise UsageError( + f"{exc_message}: {e.text}: at column {e.offset}: {e.msg}" + ) from None + + +def pytest_collection_modifyitems(items: list[Item], config: Config) -> None: + deselect_by_keyword(items, config) + deselect_by_mark(items, config) + + +def pytest_configure(config: Config) -> None: + config.stash[old_mark_config_key] = MARK_GEN._config + MARK_GEN._config = config + + empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) + + if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""): + raise UsageError( + f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect" + f" but it is {empty_parameterset!r}" + ) + + +def pytest_unconfigure(config: Config) -> None: + MARK_GEN._config = config.stash.get(old_mark_config_key, None) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/mark/expression.py b/.venv-test/lib/python3.12/site-packages/_pytest/mark/expression.py new file mode 100644 index 0000000..4b4a68d --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/mark/expression.py @@ -0,0 +1,353 @@ +r"""Evaluate match expressions, as used by `-k` and `-m`. + +The grammar is: + +expression: expr? EOF +expr: and_expr ('or' and_expr)* +and_expr: not_expr ('and' not_expr)* +not_expr: 'not' not_expr | '(' expr ')' | ident kwargs? + +ident: (\w|:|\+|-|\.|\[|\]|\\|/)+ +kwargs: ('(' name '=' value ( ', ' name '=' value )* ')') +name: a valid ident, but not a reserved keyword +value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None' + +The semantics are: + +- Empty expression evaluates to False. +- ident evaluates to True or False according to a provided matcher function. +- ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function. +- or/and/not evaluate according to the usual boolean semantics. +""" + +from __future__ import annotations + +import ast +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +import enum +import keyword +import re +import types +from typing import Final +from typing import final +from typing import Literal +from typing import NoReturn +from typing import overload +from typing import Protocol + + +__all__ = [ + "Expression", + "ExpressionMatcher", +] + + +FILE_NAME: Final = "" + + +class TokenType(enum.Enum): + LPAREN = "left parenthesis" + RPAREN = "right parenthesis" + OR = "or" + AND = "and" + NOT = "not" + IDENT = "identifier" + EOF = "end of input" + EQUAL = "=" + STRING = "string literal" + COMMA = "," + + +@dataclasses.dataclass(frozen=True) +class Token: + __slots__ = ("pos", "type", "value") + type: TokenType + value: str + pos: int + + +class Scanner: + __slots__ = ("current", "input", "tokens") + + def __init__(self, input: str) -> None: + self.input = input + self.tokens = self.lex(input) + self.current = next(self.tokens) + + def lex(self, input: str) -> Iterator[Token]: + pos = 0 + while pos < len(input): + if input[pos] in (" ", "\t"): + pos += 1 + elif input[pos] == "(": + yield Token(TokenType.LPAREN, "(", pos) + pos += 1 + elif input[pos] == ")": + yield Token(TokenType.RPAREN, ")", pos) + pos += 1 + elif input[pos] == "=": + yield Token(TokenType.EQUAL, "=", pos) + pos += 1 + elif input[pos] == ",": + yield Token(TokenType.COMMA, ",", pos) + pos += 1 + elif (quote_char := input[pos]) in ("'", '"'): + end_quote_pos = input.find(quote_char, pos + 1) + if end_quote_pos == -1: + raise SyntaxError( + f'closing quote "{quote_char}" is missing', + (FILE_NAME, 1, pos + 1, input), + ) + value = input[pos : end_quote_pos + 1] + if (backslash_pos := value.find("\\")) != -1: + raise SyntaxError( + r'escaping with "\" not supported in marker expression', + (FILE_NAME, 1, pos + backslash_pos + 1, input), + ) + yield Token(TokenType.STRING, value, pos) + pos += len(value) + else: + match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:]) + if match: + value = match.group(0) + if value == "or": + yield Token(TokenType.OR, value, pos) + elif value == "and": + yield Token(TokenType.AND, value, pos) + elif value == "not": + yield Token(TokenType.NOT, value, pos) + else: + yield Token(TokenType.IDENT, value, pos) + pos += len(value) + else: + raise SyntaxError( + f'unexpected character "{input[pos]}"', + (FILE_NAME, 1, pos + 1, input), + ) + yield Token(TokenType.EOF, "", pos) + + @overload + def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ... + + @overload + def accept( + self, type: TokenType, *, reject: Literal[False] = False + ) -> Token | None: ... + + def accept(self, type: TokenType, *, reject: bool = False) -> Token | None: + if self.current.type is type: + token = self.current + if token.type is not TokenType.EOF: + self.current = next(self.tokens) + return token + if reject: + self.reject((type,)) + return None + + def reject(self, expected: Sequence[TokenType]) -> NoReturn: + raise SyntaxError( + "expected {}; got {}".format( + " OR ".join(type.value for type in expected), + self.current.type.value, + ), + (FILE_NAME, 1, self.current.pos + 1, self.input), + ) + + +# True, False and None are legal match expression identifiers, +# but illegal as Python identifiers. To fix this, this prefix +# is added to identifiers in the conversion to Python AST. +IDENT_PREFIX = "$" + + +def expression(s: Scanner) -> ast.Expression: + if s.accept(TokenType.EOF): + ret: ast.expr = ast.Constant(False) + else: + ret = expr(s) + s.accept(TokenType.EOF, reject=True) + return ast.fix_missing_locations(ast.Expression(ret)) + + +def expr(s: Scanner) -> ast.expr: + ret = and_expr(s) + while s.accept(TokenType.OR): + rhs = and_expr(s) + ret = ast.BoolOp(ast.Or(), [ret, rhs]) + return ret + + +def and_expr(s: Scanner) -> ast.expr: + ret = not_expr(s) + while s.accept(TokenType.AND): + rhs = not_expr(s) + ret = ast.BoolOp(ast.And(), [ret, rhs]) + return ret + + +def not_expr(s: Scanner) -> ast.expr: + if s.accept(TokenType.NOT): + return ast.UnaryOp(ast.Not(), not_expr(s)) + if s.accept(TokenType.LPAREN): + ret = expr(s) + s.accept(TokenType.RPAREN, reject=True) + return ret + ident = s.accept(TokenType.IDENT) + if ident: + name = ast.Name(IDENT_PREFIX + ident.value, ast.Load()) + if s.accept(TokenType.LPAREN): + ret = ast.Call(func=name, args=[], keywords=all_kwargs(s)) + s.accept(TokenType.RPAREN, reject=True) + else: + ret = name + return ret + + s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) + + +BUILTIN_MATCHERS = {"True": True, "False": False, "None": None} + + +def single_kwarg(s: Scanner) -> ast.keyword: + keyword_name = s.accept(TokenType.IDENT, reject=True) + if not keyword_name.value.isidentifier(): + raise SyntaxError( + f"not a valid python identifier {keyword_name.value}", + (FILE_NAME, 1, keyword_name.pos + 1, s.input), + ) + if keyword.iskeyword(keyword_name.value): + raise SyntaxError( + f"unexpected reserved python keyword `{keyword_name.value}`", + (FILE_NAME, 1, keyword_name.pos + 1, s.input), + ) + s.accept(TokenType.EQUAL, reject=True) + + if value_token := s.accept(TokenType.STRING): + value: str | int | bool | None = value_token.value[1:-1] # strip quotes + else: + value_token = s.accept(TokenType.IDENT, reject=True) + if (number := value_token.value).isdigit() or ( + number.startswith("-") and number[1:].isdigit() + ): + value = int(number) + elif value_token.value in BUILTIN_MATCHERS: + value = BUILTIN_MATCHERS[value_token.value] + else: + raise SyntaxError( + f'unexpected character/s "{value_token.value}"', + (FILE_NAME, 1, value_token.pos + 1, s.input), + ) + + ret = ast.keyword(keyword_name.value, ast.Constant(value)) + return ret + + +def all_kwargs(s: Scanner) -> list[ast.keyword]: + ret = [single_kwarg(s)] + while s.accept(TokenType.COMMA): + ret.append(single_kwarg(s)) + return ret + + +class ExpressionMatcher(Protocol): + """A callable which, given an identifier and optional kwargs, should return + whether it matches in an :class:`Expression` evaluation. + + Should be prepared to handle arbitrary strings as input. + + If no kwargs are provided, the expression of the form `foo`. + If kwargs are provided, the expression is of the form `foo(1, b=True, "s")`. + + If the expression is not supported (e.g. don't want to accept the kwargs + syntax variant), should raise :class:`~pytest.UsageError`. + + Example:: + + def matcher(name: str, /, **kwargs: str | int | bool | None) -> bool: + # Match `cat`. + if name == "cat" and not kwargs: + return True + # Match `dog(barks=True)`. + if name == "dog" and kwargs == {"barks": False}: + return True + return False + """ + + def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: ... + + +@dataclasses.dataclass +class MatcherNameAdapter: + matcher: ExpressionMatcher + name: str + + def __bool__(self) -> bool: + return self.matcher(self.name) + + def __call__(self, **kwargs: str | int | bool | None) -> bool: + return self.matcher(self.name, **kwargs) + + +class MatcherAdapter(Mapping[str, MatcherNameAdapter]): + """Adapts a matcher function to a locals mapping as required by eval().""" + + def __init__(self, matcher: ExpressionMatcher) -> None: + self.matcher = matcher + + def __getitem__(self, key: str) -> MatcherNameAdapter: + return MatcherNameAdapter(matcher=self.matcher, name=key[len(IDENT_PREFIX) :]) + + def __iter__(self) -> Iterator[str]: + raise NotImplementedError() + + def __len__(self) -> int: + raise NotImplementedError() + + +@final +class Expression: + """A compiled match expression as used by -k and -m. + + The expression can be evaluated against different matchers. + """ + + __slots__ = ("_code", "input") + + def __init__(self, input: str, code: types.CodeType) -> None: + #: The original input line, as a string. + self.input: Final = input + self._code: Final = code + + @classmethod + def compile(cls, input: str) -> Expression: + """Compile a match expression. + + :param input: The input expression - one line. + + :raises SyntaxError: If the expression is malformed. + """ + astexpr = expression(Scanner(input)) + code = compile( + astexpr, + filename="", + mode="eval", + ) + return Expression(input, code) + + def evaluate(self, matcher: ExpressionMatcher) -> bool: + """Evaluate the match expression. + + :param matcher: + A callback which determines whether an identifier matches or not. + See the :class:`ExpressionMatcher` protocol for details and example. + + :returns: Whether the expression matches or not. + + :raises UsageError: + If the matcher doesn't support the expression. Cannot happen if the + matcher supports all expressions. + """ + return bool(eval(self._code, {"__builtins__": {}}, MatcherAdapter(matcher))) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/mark/structures.py b/.venv-test/lib/python3.12/site-packages/_pytest/mark/structures.py new file mode 100644 index 0000000..5449b17 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/mark/structures.py @@ -0,0 +1,695 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import collections.abc +from collections.abc import Callable +from collections.abc import Collection +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import MutableMapping +from collections.abc import Sequence +import dataclasses +import enum +import inspect +from typing import Any +from typing import final +from typing import NamedTuple +from typing import overload +from typing import TYPE_CHECKING +from typing import TypeVar +import warnings + +from .._code import getfslineno +from ..compat import NOTSET +from ..compat import NotSetType +from _pytest.config import Config +from _pytest.deprecated import check_ispytest +from _pytest.deprecated import PARAMETRIZE_NON_COLLECTION_ITERABLE +from _pytest.outcomes import fail +from _pytest.raises import AbstractRaises +from _pytest.scope import ScopeName +from _pytest.warning_types import PytestCollectionWarning +from _pytest.warning_types import PytestUnknownMarkWarning + + +if TYPE_CHECKING: + from ..nodes import Node + + +EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" + + +# Singleton type for HIDDEN_PARAM, as described in: +# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions +class _HiddenParam(enum.Enum): + token = 0 + + +#: Can be used as a parameter set id to hide it from the test name. +HIDDEN_PARAM = _HiddenParam.token + + +def istestfunc(func) -> bool: + return callable(func) and getattr(func, "__name__", "") != "" + + +def get_empty_parameterset_mark( + config: Config, argnames: Sequence[str], func +) -> MarkDecorator: + from ..nodes import Collector + + argslisting = ", ".join(argnames) + + _fs, lineno = getfslineno(func) + reason = f"got empty parameter set for ({argslisting})" + requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) + if requested_mark in ("", None, "skip"): + mark = MARK_GEN.skip(reason=reason) + elif requested_mark == "xfail": + mark = MARK_GEN.xfail(reason=reason, run=False) + elif requested_mark == "fail_at_collect": + raise Collector.CollectError( + f"Empty parameter set in '{func.__name__}' at line {lineno + 1}" + ) + else: + raise LookupError(requested_mark) + return mark + + +class ParameterSet(NamedTuple): + """A set of values for a set of parameters along with associated marks and + an optional ID for the set. + + Examples:: + + pytest.param(1, 2, 3) + # ParameterSet(values=(1, 2, 3), marks=(), id=None) + + pytest.param("hello", id="greeting") + # ParameterSet(values=("hello",), marks=(), id="greeting") + + # Parameter set with marks + pytest.param(42, marks=pytest.mark.xfail) + # ParameterSet(values=(42,), marks=(MarkDecorator(...),), id=None) + + # From parametrize mark (parameter names + list of parameter sets) + pytest.mark.parametrize( + ("a", "b", "expected"), + [ + (1, 2, 3), + pytest.param(40, 2, 42, id="everything"), + ], + ) + # ParameterSet(values=(1, 2, 3), marks=(), id=None) + # ParameterSet(values=(40, 2, 42), marks=(), id="everything") + """ + + values: Sequence[object | NotSetType] + marks: Collection[MarkDecorator | Mark] + id: str | _HiddenParam | None + + @classmethod + def param( + cls, + *values: object, + marks: MarkDecorator | Collection[MarkDecorator | Mark] = (), + id: str | _HiddenParam | None = None, + ) -> ParameterSet: + if isinstance(marks, MarkDecorator): + marks = (marks,) + else: + assert isinstance(marks, collections.abc.Collection) + if any(i.name == "usefixtures" for i in marks): + raise ValueError( + "pytest.param cannot add pytest.mark.usefixtures; see " + "https://docs.pytest.org/en/stable/reference/reference.html#pytest-param" + ) + + if id is not None: + if not isinstance(id, str) and id is not HIDDEN_PARAM: + raise TypeError( + "Expected id to be a string or a `pytest.HIDDEN_PARAM` sentinel, " + f"got {type(id)}: {id!r}", + ) + return cls(values, marks, id) + + @classmethod + def extract_from( + cls, + parameterset: ParameterSet | Sequence[object] | object, + force_tuple: bool = False, + ) -> ParameterSet: + """Extract from an object or objects. + + :param parameterset: + A legacy style parameterset that may or may not be a tuple, + and may or may not be wrapped into a mess of mark objects. + + :param force_tuple: + Enforce tuple wrapping so single argument tuple values + don't get decomposed and break tests. + """ + if isinstance(parameterset, cls): + return parameterset + if force_tuple: + return cls.param(parameterset) + else: + # TODO: Refactor to fix this type-ignore. Currently the following + # passes type-checking but crashes: + # + # @pytest.mark.parametrize(('x', 'y'), [1, 2]) + # def test_foo(x, y): pass + return cls(parameterset, marks=[], id=None) # type: ignore[arg-type] + + @staticmethod + def _parse_parametrize_args( + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], + *args, + **kwargs, + ) -> tuple[Sequence[str], bool]: + if isinstance(argnames, str): + # A trailing comma indicates tuple-style: "arg," is equivalent to ("arg",) + # In this case, argvalues should be a list of tuples, not wrapped values. + # See https://github.com/pytest-dev/pytest/issues/719 + has_trailing_comma = argnames.rstrip().endswith(",") + argnames = [x.strip() for x in argnames.split(",") if x.strip()] + force_tuple = len(argnames) == 1 and not has_trailing_comma + else: + force_tuple = False + return argnames, force_tuple + + @staticmethod + def _parse_parametrize_parameters( + argvalues: Iterable[ParameterSet | Sequence[object] | object], + force_tuple: bool, + ) -> list[ParameterSet]: + return [ + ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues + ] + + @classmethod + def _for_parametrize( + cls, + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], + func, + config: Config, + nodeid: str, + ) -> tuple[Sequence[str], list[ParameterSet]]: + if not isinstance(argvalues, Collection): + warnings.warn( + PARAMETRIZE_NON_COLLECTION_ITERABLE.format( + nodeid=nodeid, + type_name=type(argvalues).__name__, + ), + stacklevel=3, + ) + + argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) + parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) + del argvalues + + if parameters: + # Check all parameter sets have the correct number of values. + for param in parameters: + if len(param.values) != len(argnames): + msg = ( + '{nodeid}: in "parametrize" the number of names ({names_len}):\n' + " {names}\n" + "must be equal to the number of values ({values_len}):\n" + " {values}" + ) + fail( + msg.format( + nodeid=nodeid, + values=param.values, + names=argnames, + names_len=len(argnames), + values_len=len(param.values), + ), + pytrace=False, + ) + else: + # Empty parameter set (likely computed at runtime): create a single + # parameter set with NOTSET values, with the "empty parameter set" mark applied to it. + mark = get_empty_parameterset_mark(config, argnames, func) + parameters.append( + ParameterSet( + values=(NOTSET,) * len(argnames), marks=[mark], id="NOTSET" + ) + ) + return argnames, parameters + + +@final +@dataclasses.dataclass(frozen=True) +class Mark: + """A pytest mark.""" + + #: Name of the mark. + name: str + #: Positional arguments of the mark decorator. + args: tuple[Any, ...] + #: Keyword arguments of the mark decorator. + kwargs: Mapping[str, Any] + + #: Source Mark for ids with parametrize Marks. + _param_ids_from: Mark | None = dataclasses.field(default=None, repr=False) + #: Resolved/generated ids with parametrize Marks. + _param_ids_generated: Sequence[str] | None = dataclasses.field( + default=None, repr=False + ) + + def __init__( + self, + name: str, + args: tuple[Any, ...], + kwargs: Mapping[str, Any], + param_ids_from: Mark | None = None, + param_ids_generated: Sequence[str] | None = None, + *, + _ispytest: bool = False, + ) -> None: + """:meta private:""" + check_ispytest(_ispytest) + # Weirdness to bypass frozen=True. + object.__setattr__(self, "name", name) + object.__setattr__(self, "args", args) + object.__setattr__(self, "kwargs", kwargs) + object.__setattr__(self, "_param_ids_from", param_ids_from) + object.__setattr__(self, "_param_ids_generated", param_ids_generated) + + def _has_param_ids(self) -> bool: + return "ids" in self.kwargs or len(self.args) >= 4 + + def combined_with(self, other: Mark) -> Mark: + """Return a new Mark which is a combination of this + Mark and another Mark. + + Combines by appending args and merging kwargs. + + :param Mark other: The mark to combine with. + :rtype: Mark + """ + assert self.name == other.name + + # Remember source of ids with parametrize Marks. + param_ids_from: Mark | None = None + if self.name == "parametrize": + if other._has_param_ids(): + param_ids_from = other + elif self._has_param_ids(): + param_ids_from = self + + return Mark( + self.name, + self.args + other.args, + dict(self.kwargs, **other.kwargs), + param_ids_from=param_ids_from, + _ispytest=True, + ) + + +# A generic parameter designating an object to which a Mark may +# be applied -- a test function (callable) or class. +# Note: a lambda is not allowed, but this can't be represented. +Markable = TypeVar("Markable", bound=Callable[..., object] | type) + + +@dataclasses.dataclass +class MarkDecorator: + """A decorator for applying a mark on test functions and classes. + + ``MarkDecorators`` are created with ``pytest.mark``:: + + mark1 = pytest.mark.NAME # Simple MarkDecorator + mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator + + and can then be applied as decorators to test functions:: + + @mark2 + def test_function(): + pass + + When a ``MarkDecorator`` is called, it does the following: + + 1. If called with a single class as its only positional argument and no + additional keyword arguments, it attaches the mark to the class so it + gets applied automatically to all test cases found in that class. + + 2. If called with a single function as its only positional argument and + no additional keyword arguments, it attaches the mark to the function, + containing all the arguments already stored internally in the + ``MarkDecorator``. + + 3. When called in any other case, it returns a new ``MarkDecorator`` + instance with the original ``MarkDecorator``'s content updated with + the arguments passed to this call. + + Note: The rules above prevent a ``MarkDecorator`` from storing only a + single function or class reference as its positional argument with no + additional keyword or positional arguments. You can work around this by + using `with_args()`. + """ + + mark: Mark + + def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None: + """:meta private:""" + check_ispytest(_ispytest) + self.mark = mark + + @property + def name(self) -> str: + """Alias for mark.name.""" + return self.mark.name + + @property + def args(self) -> tuple[Any, ...]: + """Alias for mark.args.""" + return self.mark.args + + @property + def kwargs(self) -> Mapping[str, Any]: + """Alias for mark.kwargs.""" + return self.mark.kwargs + + @property + def markname(self) -> str: + """:meta private:""" + return self.name # for backward-compat (2.4.1 had this attr) + + def with_args(self, *args: object, **kwargs: object) -> MarkDecorator: + """Return a MarkDecorator with extra arguments added. + + Unlike calling the MarkDecorator, with_args() can be used even + if the sole argument is a callable/class. + """ + mark = Mark(self.name, args, kwargs, _ispytest=True) + return MarkDecorator(self.mark.combined_with(mark), _ispytest=True) + + # Type ignored because the overloads overlap with an incompatible + # return type. Not much we can do about that. Thankfully mypy picks + # the first match so it works out even if we break the rules. + @overload + def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap] + pass + + @overload + def __call__(self, *args: object, **kwargs: object) -> MarkDecorator: + pass + + def __call__(self, *args: object, **kwargs: object): + """Call the MarkDecorator.""" + if args and not kwargs: + func = args[0] + is_class = inspect.isclass(func) + # For staticmethods/classmethods, the marks are eventually fetched from the + # function object, not the descriptor, so unwrap. + unwrapped_func = func + if isinstance(func, staticmethod | classmethod): + unwrapped_func = func.__func__ + if len(args) == 1 and (istestfunc(unwrapped_func) or is_class): + store_mark(unwrapped_func, self.mark) + return func + return self.with_args(*args, **kwargs) + + +def get_unpacked_marks( + obj: object | type, + *, + consider_mro: bool = True, +) -> list[Mark]: + """Obtain the unpacked marks that are stored on an object. + + If obj is a class and consider_mro is true, return marks applied to + this class and all of its super-classes in MRO order. If consider_mro + is false, only return marks applied directly to this class. + """ + if isinstance(obj, type): + if not consider_mro: + mark_lists = [obj.__dict__.get("pytestmark", [])] + else: + mark_lists = [ + x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__) + ] + mark_list = [] + for item in mark_lists: + if isinstance(item, list): + mark_list.extend(item) + else: + mark_list.append(item) + else: + mark_attribute = getattr(obj, "pytestmark", []) + if mark_attribute is None: + warnings.warn( + "Module defines a `__getattr__` which returns None for " + "'pytestmark' instead of raising AttributeError. " + "Make sure `__getattr__` raises AttributeError for " + "attributes it does not provide. " + "See https://github.com/pytest-dev/pytest/issues/8265", + PytestCollectionWarning, + stacklevel=2, + ) + mark_list = [] + elif isinstance(mark_attribute, list): + mark_list = mark_attribute + else: + mark_list = [mark_attribute] + return list(normalize_mark_list(mark_list)) + + +def normalize_mark_list( + mark_list: Iterable[Mark | MarkDecorator], +) -> Iterable[Mark]: + """ + Normalize an iterable of Mark or MarkDecorator objects into a list of marks + by retrieving the `mark` attribute on MarkDecorator instances. + + :param mark_list: marks to normalize + :returns: A new list of the extracted Mark objects + """ + for mark in mark_list: + mark_obj = getattr(mark, "mark", mark) + if not isinstance(mark_obj, Mark): + raise TypeError(f"got {mark_obj!r} instead of Mark") + yield mark_obj + + +def store_mark(obj, mark: Mark) -> None: + """Store a Mark on an object. + + This is used to implement the Mark declarations/decorators correctly. + """ + assert isinstance(mark, Mark), mark + + from ..fixtures import getfixturemarker + + if getfixturemarker(obj) is not None: + fail( + "Marks cannot be applied to fixtures.\n" + "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" + ) + + # Always reassign name to avoid updating pytestmark in a reference that + # was only borrowed. + obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark] + + +# Typing for builtin pytest marks. This is cheating; it gives builtin marks +# special privilege, and breaks modularity. But practicality beats purity... +if TYPE_CHECKING: + + class _SkipMarkDecorator(MarkDecorator): + @overload # type: ignore[override,no-overload-impl] + def __call__(self, arg: Markable) -> Markable: ... + + @overload + def __call__(self, reason: str = ...) -> MarkDecorator: ... + + class _SkipifMarkDecorator(MarkDecorator): + def __call__( # type: ignore[override] + self, + condition: str | bool = ..., + *conditions: str | bool, + reason: str = ..., + ) -> MarkDecorator: ... + + class _XfailMarkDecorator(MarkDecorator): + @overload # type: ignore[override,no-overload-impl] + def __call__(self, arg: Markable) -> Markable: ... + + @overload + def __call__( + self, + condition: str | bool = True, + *conditions: str | bool, + reason: str = ..., + run: bool = ..., + raises: None + | type[BaseException] + | tuple[type[BaseException], ...] + | AbstractRaises[BaseException] = ..., + strict: bool = ..., + ) -> MarkDecorator: ... + + class _ParametrizeMarkDecorator(MarkDecorator): + def __call__( # type: ignore[override] + self, + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], + # TODO(pytest10): Change to below after PARAMETRIZE_NON_COLLECTION_ITERABLE deprecation. + # Overload doesn't work, see #14606. + # argvalues: Collection[ParameterSet | Sequence[object] | object], + *, + indirect: bool | Sequence[str] = ..., + ids: Iterable[None | str | float | int | bool | _HiddenParam] + | Callable[[Any], object | None] + | None = ..., + scope: ScopeName | None = ..., + ) -> MarkDecorator: ... + + class _UsefixturesMarkDecorator(MarkDecorator): + def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override] + ... + + class _FilterwarningsMarkDecorator(MarkDecorator): + def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override] + ... + + +@final +class MarkGenerator: + """Factory for :class:`MarkDecorator` objects - exposed as + a ``pytest.mark`` singleton instance. + + Example:: + + import pytest + + + @pytest.mark.slowtest + def test_function(): + pass + + applies a 'slowtest' :class:`Mark` on ``test_function``. + """ + + # See TYPE_CHECKING above. + if TYPE_CHECKING: + skip: _SkipMarkDecorator + skipif: _SkipifMarkDecorator + xfail: _XfailMarkDecorator + parametrize: _ParametrizeMarkDecorator + usefixtures: _UsefixturesMarkDecorator + filterwarnings: _FilterwarningsMarkDecorator + + def __init__(self, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + self._config: Config | None = None + self._markers: set[str] = set() + + def __getattr__(self, name: str) -> MarkDecorator: + """Generate a new :class:`MarkDecorator` with the given name.""" + if name[0] == "_": + raise AttributeError("Marker name must NOT start with underscore") + + if self._config is not None: + # We store a set of markers as a performance optimisation - if a mark + # name is in the set we definitely know it, but a mark may be known and + # not in the set. We therefore start by updating the set! + if name not in self._markers: + for line in self._config.getini("markers"): + # example lines: "skipif(condition): skip the given test if..." + # or "hypothesis: tests which use Hypothesis", so to get the + # marker name we split on both `:` and `(`. + marker = line.split(":")[0].split("(")[0].strip() + self._markers.add(marker) + + # If the name is not in the set of known marks after updating, + # then it really is time to issue a warning or an error. + if name not in self._markers: + # Raise a specific error for common misspellings of "parametrize". + if name in ["parameterize", "parametrise", "parameterise"]: + __tracebackhide__ = True + fail(f"Unknown '{name}' mark, did you mean 'parametrize'?") + + strict_markers = self._config.getini("strict_markers") + if strict_markers is None: + strict_markers = self._config.getini("strict") + if strict_markers: + fail( + f"{name!r} not found in `markers` configuration option", + pytrace=False, + ) + + warnings.warn( + f"Unknown pytest.mark.{name} - is this a typo? You can register " + "custom marks to avoid this warning - for details, see " + "https://docs.pytest.org/en/stable/how-to/mark.html", + PytestUnknownMarkWarning, + 2, + ) + + return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True) + + +MARK_GEN = MarkGenerator(_ispytest=True) + + +@final +class NodeKeywords(MutableMapping[str, Any]): + __slots__ = ("_markers", "node", "parent") + + def __init__(self, node: Node) -> None: + self.node = node + self.parent = node.parent + self._markers = {node.name: True} + + def __getitem__(self, key: str) -> Any: + try: + return self._markers[key] + except KeyError: + if self.parent is None: + raise + return self.parent.keywords[key] + + def __setitem__(self, key: str, value: Any) -> None: + self._markers[key] = value + + # Note: we could've avoided explicitly implementing some of the methods + # below and use the collections.abc fallback, but that would be slow. + + def __contains__(self, key: object) -> bool: + return key in self._markers or ( + self.parent is not None and key in self.parent.keywords + ) + + def update( # type: ignore[override] + self, + other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (), + **kwds: Any, + ) -> None: + self._markers.update(other) + self._markers.update(kwds) + + def __delitem__(self, key: str) -> None: + raise ValueError("cannot delete key in keywords dict") + + def __iter__(self) -> Iterator[str]: + # Doesn't need to be fast. + yield from self._markers + if self.parent is not None: + for keyword in self.parent.keywords: + # self._marks and self.parent.keywords can have duplicates. + if keyword not in self._markers: + yield keyword + + def __len__(self) -> int: + # Doesn't need to be fast. + return sum(1 for keyword in self) + + def __repr__(self) -> str: + return f"" diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/monkeypatch.py b/.venv-test/lib/python3.12/site-packages/_pytest/monkeypatch.py new file mode 100644 index 0000000..d6db724 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/monkeypatch.py @@ -0,0 +1,430 @@ +# mypy: allow-untyped-defs +"""Monkeypatching and mocking functionality.""" + +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Mapping +from collections.abc import MutableMapping +from contextlib import contextmanager +import importlib +import os +from pathlib import Path +import re +import sys +from typing import Any +from typing import final +from typing import overload +from typing import TypeVar +import warnings + +from _pytest.compat import NOTSET +from _pytest.compat import NotSetType +from _pytest.deprecated import MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES +from _pytest.fixtures import fixture +from _pytest.warning_types import PytestWarning + + +RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") + + +K = TypeVar("K") +V = TypeVar("V") + + +@fixture +def monkeypatch() -> Generator[MonkeyPatch]: + """A convenient fixture for monkey-patching. + + The fixture provides these methods to modify objects, dictionaries, or + :data:`os.environ`: + + * :meth:`monkeypatch.setattr(obj, name, value, raising=True) ` + * :meth:`monkeypatch.delattr(obj, name, raising=True) ` + * :meth:`monkeypatch.setitem(mapping, name, value) ` + * :meth:`monkeypatch.delitem(obj, name, raising=True) ` + * :meth:`monkeypatch.setenv(name, value, prepend=None) ` + * :meth:`monkeypatch.delenv(name, raising=True) ` + * :meth:`monkeypatch.syspath_prepend(path) ` + * :meth:`monkeypatch.chdir(path) ` + * :meth:`monkeypatch.context() ` + + All modifications will be undone after the requesting test function or + fixture has finished. The ``raising`` parameter determines if a :class:`KeyError` + or :class:`AttributeError` will be raised if the set/deletion operation does not have the + specified target. + + To undo modifications done by the fixture in a contained scope, + use :meth:`context() `. + """ + mpatch = MonkeyPatch() + yield mpatch + mpatch.undo() + + +def resolve(name: str) -> object: + # Simplified from zope.dottedname. + parts = name.split(".") + + used = parts.pop(0) + found: object = importlib.import_module(used) + for part in parts: + used += "." + part + try: + found = getattr(found, part) + except AttributeError: + pass + else: + continue + # We use explicit un-nesting of the handling block in order + # to avoid nested exceptions. + try: + importlib.import_module(used) + except ImportError as ex: + expected = str(ex).split()[-1] + if expected == used: + raise + else: + raise ImportError(f"import error in {used}: {ex}") from ex + found = annotated_getattr(found, part, used) + return found + + +def annotated_getattr(obj: object, name: str, ann: str) -> object: + try: + obj = getattr(obj, name) + except AttributeError as e: + raise AttributeError( + f"{type(obj).__name__!r} object at {ann} has no attribute {name!r}" + ) from e + return obj + + +def derive_importpath(import_path: str, raising: bool) -> tuple[str, object]: + if not isinstance(import_path, str) or "." not in import_path: + raise TypeError(f"must be absolute import path string, not {import_path!r}") + module, attr = import_path.rsplit(".", 1) + target = resolve(module) + if raising: + annotated_getattr(target, attr, ann=module) + return attr, target + + +@final +class MonkeyPatch: + """Helper to conveniently monkeypatch attributes/items/environment + variables/syspath. + + Returned by the :fixture:`monkeypatch` fixture. + + .. versionchanged:: 6.2 + Can now also be used directly as `pytest.MonkeyPatch()`, for when + the fixture is not available. In this case, use + :meth:`with MonkeyPatch.context() as mp: ` or remember to call + :meth:`undo` explicitly. + """ + + def __init__(self) -> None: + self._setattr: list[tuple[object, str, object]] = [] + self._setitem: list[tuple[Mapping[Any, Any], object, object]] = [] + self._cwd: str | None = None + self._savesyspath: list[str] | None = None + + @classmethod + @contextmanager + def context(cls) -> Generator[MonkeyPatch]: + """Context manager that returns a new :class:`MonkeyPatch` object + which undoes any patching done inside the ``with`` block upon exit. + + Example: + + .. code-block:: python + + import functools + + + def test_partial(monkeypatch): + with monkeypatch.context() as m: + m.setattr(functools, "partial", 3) + + Useful in situations where it is desired to undo some patches before the test ends, + such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples + of this see :issue:`3290`). + """ + m = cls() + try: + yield m + finally: + m.undo() + + @overload + def setattr( + self, + target: str, + name: object, + value: NotSetType = ..., + raising: bool = ..., + ) -> None: ... + + @overload + def setattr( + self, + target: object, + name: str, + value: object, + raising: bool = ..., + ) -> None: ... + + def setattr( + self, + target: str | object, + name: object | str, + value: object = NOTSET, + raising: bool = True, + ) -> None: + """ + Set attribute value on target, memorizing the old value. + + For example: + + .. code-block:: python + + import os + + monkeypatch.setattr(os, "getcwd", lambda: "/") + + The code above replaces the :func:`os.getcwd` function by a ``lambda`` which + always returns ``"/"``. + + For convenience, you can specify a string as ``target`` which + will be interpreted as a dotted import path, with the last part + being the attribute name: + + .. code-block:: python + + monkeypatch.setattr("os.getcwd", lambda: "/") + + Raises :class:`AttributeError` if the attribute does not exist, unless + ``raising`` is set to False. + + **Where to patch** + + ``monkeypatch.setattr`` works by (temporarily) changing the object that a name points to with another one. + There can be many names pointing to any individual object, so for patching to work you must ensure + that you patch the name used by the system under test. + + See the section :ref:`Where to patch ` in the :mod:`unittest.mock` + docs for a complete explanation, which is meant for :func:`unittest.mock.patch` but + applies to ``monkeypatch.setattr`` as well. + """ + __tracebackhide__ = True + import inspect + + if value is NOTSET: + if not isinstance(target, str): + raise TypeError( + "use setattr(target, name, value) or " + "setattr(target, value) with target being a dotted " + "import string" + ) + value = name + name, target = derive_importpath(target, raising) + else: + if not isinstance(name, str): + raise TypeError( + "use setattr(target, name, value) with name being a string or " + "setattr(target, value) with target being a dotted " + "import string" + ) + + oldval = getattr(target, name, NOTSET) + if raising and oldval is NOTSET: + raise AttributeError(f"{target!r} has no attribute {name!r}") + + # avoid class descriptors like staticmethod/classmethod + if inspect.isclass(target): + oldval = target.__dict__.get(name, NOTSET) + setattr(target, name, value) + self._setattr.append((target, name, oldval)) + + def delattr( + self, + target: object | str, + name: str | NotSetType = NOTSET, + raising: bool = True, + ) -> None: + """Delete attribute ``name`` from ``target``. + + If no ``name`` is specified and ``target`` is a string + it will be interpreted as a dotted import path with the + last part being the attribute name. + + Raises AttributeError it the attribute does not exist, unless + ``raising`` is set to False. + """ + __tracebackhide__ = True + import inspect + + if name is NOTSET: + if not isinstance(target, str): + raise TypeError( + "use delattr(target, name) or " + "delattr(target) with target being a dotted " + "import string" + ) + name, target = derive_importpath(target, raising) + + if not hasattr(target, name): + if raising: + raise AttributeError(name) + else: + oldval = getattr(target, name, NOTSET) + # Avoid class descriptors like staticmethod/classmethod. + if inspect.isclass(target): + oldval = target.__dict__.get(name, NOTSET) + self._setattr.append((target, name, oldval)) + delattr(target, name) + + def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None: + """Set dictionary entry ``name`` to value.""" + self._setitem.append((dic, name, dic.get(name, NOTSET))) + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + dic[name] = value # type: ignore[index] + + def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None: + """Delete ``name`` from dict. + + Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to + False. + """ + if name not in dic: + if raising: + raise KeyError(name) + else: + self._setitem.append((dic, name, dic.get(name, NOTSET))) + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + del dic[name] # type: ignore[attr-defined] + + def setenv(self, name: str, value: str, prepend: str | None = None) -> None: + """Set environment variable ``name`` to ``value``. + + If ``prepend`` is a character, read the current environment variable + value and prepend the ``value`` adjoined with the ``prepend`` + character. + """ + if not isinstance(value, str): + warnings.warn( # type: ignore[unreachable] + PytestWarning( + f"Value of environment variable {name} type should be str, but got " + f"{value!r} (type: {type(value).__name__}); converted to str implicitly" + ), + stacklevel=2, + ) + value = str(value) + if prepend and name in os.environ: + value = value + prepend + os.environ[name] + self.setitem(os.environ, name, value) + + def delenv(self, name: str, raising: bool = True) -> None: + """Delete ``name`` from the environment. + + Raises ``KeyError`` if it does not exist, unless ``raising`` is set to + False. + """ + environ: MutableMapping[str, str] = os.environ + self.delitem(environ, name, raising=raising) + + def syspath_prepend(self, path) -> None: + """Prepend ``path`` to ``sys.path`` list of import locations.""" + if self._savesyspath is None: + self._savesyspath = sys.path[:] + sys.path.insert(0, str(path)) + + # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171 + # this is only needed when pkg_resources was already loaded by the namespace package + if "pkg_resources" in sys.modules: + import pkg_resources + from pkg_resources import fixup_namespace_packages + + # Only issue deprecation warning if this call would actually have an + # effect for this specific path. + if ( + hasattr(pkg_resources, "_namespace_packages") + and pkg_resources._namespace_packages + ): + path_obj = Path(str(path)) + for ns_pkg in pkg_resources._namespace_packages: + if ns_pkg is None: + continue + ns_pkg_path = path_obj / ns_pkg.replace(".", os.sep) + if ns_pkg_path.is_dir(): + warnings.warn( + MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES, stacklevel=2 + ) + break + + fixup_namespace_packages(str(path)) + + # A call to syspathinsert() usually means that the caller wants to + # import some dynamically created files, thus with python3 we + # invalidate its import caches. + # This is especially important when any namespace package is in use, + # since then the mtime based FileFinder cache (that gets created in + # this case already) gets not invalidated when writing the new files + # quickly afterwards. + from importlib import invalidate_caches + + invalidate_caches() + + def chdir(self, path: str | os.PathLike[str]) -> None: + """Change the current working directory to the specified path. + + :param path: + The path to change into. + """ + if self._cwd is None: + self._cwd = os.getcwd() + os.chdir(path) + + def undo(self) -> None: + """Undo previous changes. + + This call consumes the undo stack. Calling it a second time has no + effect unless you do more monkeypatching after the undo call. + + There is generally no need to call `undo()`, since it is + called automatically during tear-down. + + .. note:: + The same `monkeypatch` fixture is used across a + single test function invocation. If `monkeypatch` is used both by + the test function itself and one of the test fixtures, + calling `undo()` will undo all of the changes made in + both functions. + + Prefer to use :meth:`context() ` instead. + """ + for obj, name, value in reversed(self._setattr): + if value is not NOTSET: + setattr(obj, name, value) + else: + delattr(obj, name) + self._setattr[:] = [] + for dictionary, key, value in reversed(self._setitem): + if value is NOTSET: + try: + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + del dictionary[key] # type: ignore[attr-defined] + except KeyError: + pass # Was already deleted, so we have the desired state. + else: + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + dictionary[key] = value # type: ignore[index] + self._setitem[:] = [] + if self._savesyspath is not None: + sys.path[:] = self._savesyspath + self._savesyspath = None + + if self._cwd is not None: + os.chdir(self._cwd) + self._cwd = None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/nodes.py b/.venv-test/lib/python3.12/site-packages/_pytest/nodes.py new file mode 100644 index 0000000..f0629c2 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/nodes.py @@ -0,0 +1,764 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import abc +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import MutableMapping +from functools import cached_property +from functools import lru_cache +import os +import pathlib +from pathlib import Path +from typing import Any +from typing import cast +from typing import NoReturn +from typing import overload +from typing import TYPE_CHECKING +from typing import TypeVar +import warnings + +import pluggy + +import _pytest._code +from _pytest._code import getfslineno +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import TerminalRepr +from _pytest._code.code import Traceback +from _pytest._code.code import TracebackStyle +from _pytest.compat import LEGACY_PATH +from _pytest.compat import signature +from _pytest.config import Config +from _pytest.config import ConftestImportFailure +from _pytest.mark.structures import Mark +from _pytest.mark.structures import MarkDecorator +from _pytest.mark.structures import NodeKeywords +from _pytest.outcomes import fail +from _pytest.pathlib import absolutepath +from _pytest.stash import Stash +from _pytest.warning_types import PytestWarning + + +if TYPE_CHECKING: + from typing_extensions import Self + + # Imported here due to circular import. + from _pytest.main import Session + + +SEP = "/" + + +def norm_sep(path: str | os.PathLike[str]) -> str: + """Normalize path separators to forward slashes for nodeid compatibility. + + Replaces backslashes with forward slashes. This handles both Windows native + paths and cross-platform data (e.g., Windows paths in serialized test reports + when running on Linux). + + :param path: A path string or PathLike object. + :returns: String with all backslashes replaced by forward slashes. + """ + return os.fspath(path).replace("\\", SEP) + + +tracebackcutdir = Path(_pytest.__file__).parent + + +_T = TypeVar("_T") + + +_NodeType = TypeVar("_NodeType", bound="Node") + + +class NodeMeta(abc.ABCMeta): + """Metaclass used by :class:`Node` to enforce that direct construction raises + :class:`Failed`. + + This behaviour supports the indirection introduced with :meth:`Node.from_parent`, + the named constructor to be used instead of direct construction. The design + decision to enforce indirection with :class:`NodeMeta` was made as a + temporary aid for refactoring the collection tree, which was diagnosed to + have :class:`Node` objects whose creational patterns were overly entangled. + Once the refactoring is complete, this metaclass can be removed. + + See https://github.com/pytest-dev/pytest/projects/3 for an overview of the + progress on detangling the :class:`Node` classes. + """ + + def __call__(cls, *k, **kw) -> NoReturn: + msg = ( + "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" + "See " + "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent" + " for more details." + ).format(name=f"{cls.__module__}.{cls.__name__}") + fail(msg, pytrace=False) + + def _create(cls: type[_T], *k, **kw) -> _T: + try: + return super().__call__(*k, **kw) # type: ignore[no-any-return,misc] + except TypeError: + sig = signature(getattr(cls, "__init__")) + known_kw = {k: v for k, v in kw.items() if k in sig.parameters} + from .warning_types import PytestDeprecationWarning + + warnings.warn( + PytestDeprecationWarning( + f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n" + "See https://docs.pytest.org/en/stable/deprecations.html" + "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs " + "for more details." + ) + ) + + return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc] + + +class Node(abc.ABC, metaclass=NodeMeta): + r"""Base class of :class:`Collector` and :class:`Item`, the components of + the test collection tree. + + ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the + leaf nodes. + """ + + # Implemented in the legacypath plugin. + #: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage + #: for methods not migrated to ``pathlib.Path`` yet, such as + #: :meth:`Item.reportinfo `. Will be deprecated in + #: a future release, prefer using :attr:`path` instead. + fspath: LEGACY_PATH + + # Use __slots__ to make attribute access faster. + # Note that __dict__ is still available. + __slots__ = ( + "__dict__", + "_nodeid", + "_store", + "config", + "name", + "parent", + "path", + "session", + ) + + def __init__( + self, + name: str, + parent: Node | None = None, + config: Config | None = None, + session: Session | None = None, + fspath: None = None, + path: Path | None = None, + nodeid: str | None = None, + ) -> None: + #: A unique name within the scope of the parent node. + self.name: str = name + + #: The parent collector node. + self.parent = parent + + if config: + #: The pytest config object. + self.config: Config = config + else: + if not parent: + raise TypeError("config or parent must be provided") + self.config = parent.config + + if session: + #: The pytest session this node is part of. + self.session: Session = session + else: + if not parent: + raise TypeError("session or parent must be provided") + self.session = parent.session + + if path is None: + assert parent is not None + path = parent.path + #: Filesystem path where this node was collected from. + self.path: pathlib.Path = path + + # The explicit annotation is to avoid publicly exposing NodeKeywords. + #: Keywords/markers collected from all scopes. + self.keywords: MutableMapping[str, Any] = NodeKeywords(self) + + #: The marker objects belonging to this node. + self.own_markers: list[Mark] = [] + + #: Allow adding of extra keywords to use for matching. + self.extra_keyword_matches: set[str] = set() + + if nodeid is not None: + assert "::()" not in nodeid + self._nodeid = nodeid + else: + if not self.parent: + raise TypeError("nodeid or parent must be provided") + self._nodeid = self.parent.nodeid + "::" + self.name + + #: A place where plugins can store information on the node for their + #: own use. + self.stash: Stash = Stash() + # Deprecated alias. Was never public. Can be removed in a few releases. + self._store = self.stash + + @classmethod + def from_parent(cls, parent: Node, **kw) -> Self: + """Public constructor for Nodes. + + This indirection got introduced in order to enable removing + the fragile logic from the node constructors. + + Subclasses can use ``super().from_parent(...)`` when overriding the + construction. + + :param parent: The parent node of this Node. + """ + if "config" in kw: + raise TypeError("config is not a valid argument for from_parent") + if "session" in kw: + raise TypeError("session is not a valid argument for from_parent") + return cls._create(parent=parent, **kw) + + @property + def ihook(self) -> pluggy.HookRelay: + """Path-sensitive hook proxy used to call pytest hooks.""" + return self.session.gethookproxy(self.path) + + def __repr__(self) -> str: + return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) + + def warn(self, warning: Warning) -> None: + """Issue a warning for this Node. + + Warnings will be displayed after the test session, unless explicitly suppressed. + + :param Warning warning: + The warning instance to issue. + + :raises ValueError: If ``warning`` instance is not a subclass of Warning. + + Example usage: + + .. code-block:: python + + node.warn(PytestWarning("some message")) + node.warn(UserWarning("some message")) + + .. versionchanged:: 6.2 + Any subclass of :class:`Warning` is now accepted, rather than only + :class:`PytestWarning ` subclasses. + """ + # enforce type checks here to avoid getting a generic type error later otherwise. + if not isinstance(warning, Warning): + raise ValueError( + f"warning must be an instance of Warning or subclass, got {warning!r}" + ) + path, lineno = get_fslocation_from_item(self) + assert lineno is not None + warnings.warn_explicit( + warning, + category=None, + filename=str(path), + lineno=lineno + 1, + ) + + # Methods for ordering nodes. + + @property + def nodeid(self) -> str: + """A ::-separated string denoting its collection tree address.""" + return self._nodeid + + def __hash__(self) -> int: + return hash(self._nodeid) + + def setup(self) -> None: + pass + + def teardown(self) -> None: + pass + + def iter_parents(self) -> Iterator[Node]: + """Iterate over all parent collectors starting from and including self + up to the root of the collection tree. + + .. versionadded:: 8.1 + """ + parent: Node | None = self + while parent is not None: + yield parent + parent = parent.parent + + def listchain(self) -> list[Node]: + """Return a list of all parent collectors starting from the root of the + collection tree down to and including self.""" + chain = [] + item: Node | None = self + while item is not None: + chain.append(item) + item = item.parent + chain.reverse() + return chain + + def add_marker(self, marker: str | MarkDecorator, append: bool = True) -> None: + """Dynamically add a marker object to the node. + + :param marker: + The marker. + :param append: + Whether to append the marker, or prepend it. + """ + from _pytest.mark import MARK_GEN + + if isinstance(marker, MarkDecorator): + marker_ = marker + elif isinstance(marker, str): + marker_ = getattr(MARK_GEN, marker) + else: + raise ValueError("is not a string or pytest.mark.* Marker") + self.keywords[marker_.name] = marker_ + if append: + self.own_markers.append(marker_.mark) + else: + self.own_markers.insert(0, marker_.mark) + + def iter_markers(self, name: str | None = None) -> Iterator[Mark]: + """Iterate over all markers of the node. + + :param name: If given, filter the results by the name attribute. + :returns: An iterator of the markers of the node. + """ + return (x[1] for x in self.iter_markers_with_node(name=name)) + + def iter_markers_with_node( + self, name: str | None = None + ) -> Iterator[tuple[Node, Mark]]: + """Iterate over all markers of the node. + + :param name: If given, filter the results by the name attribute. + :returns: An iterator of (node, mark) tuples. + """ + for node in self.iter_parents(): + for mark in node.own_markers: + if name is None or getattr(mark, "name", None) == name: + yield node, mark + + @overload + def get_closest_marker(self, name: str) -> Mark | None: ... + + @overload + def get_closest_marker(self, name: str, default: Mark) -> Mark: ... + + def get_closest_marker(self, name: str, default: Mark | None = None) -> Mark | None: + """Return the first marker matching the name, from closest (for + example function) to farther level (for example module level). + + :param default: Fallback return value if no marker was found. + :param name: Name to filter by. + """ + return next(self.iter_markers(name=name), default) + + def listextrakeywords(self) -> set[str]: + """Return a set of all extra keywords in self and any parents.""" + extra_keywords: set[str] = set() + for item in self.listchain(): + extra_keywords.update(item.extra_keyword_matches) + return extra_keywords + + def listnames(self) -> list[str]: + return [x.name for x in self.listchain()] + + def addfinalizer(self, fin: Callable[[], object]) -> None: + """Register a function to be called without arguments when this node is + finalized. + + This method can only be called when this node is active + in a setup chain, for example during self.setup(). + """ + self.session._setupstate.addfinalizer(fin, self) + + def getparent(self, cls: type[_NodeType]) -> _NodeType | None: + """Get the closest parent node (including self) which is an instance of + the given class. + + :param cls: The node class to search for. + :returns: The node, if found. + """ + for node in self.iter_parents(): + if isinstance(node, cls): + return node + return None + + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: + return excinfo.traceback + + def _repr_failure_py( + self, + excinfo: ExceptionInfo[BaseException], + style: TracebackStyle | None = None, + ) -> TerminalRepr: + from _pytest.fixtures import FixtureLookupError + + if isinstance(excinfo.value, ConftestImportFailure): + excinfo = ExceptionInfo.from_exception(excinfo.value.cause) + if isinstance(excinfo.value, fail.Exception): + if not excinfo.value.pytrace: + style = "value" + if isinstance(excinfo.value, FixtureLookupError): + return excinfo.value.formatrepr() + + tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] + if self.config.getoption("fulltrace", False): + style = "long" + tbfilter = False + else: + tbfilter = self._traceback_filter + if style == "auto": + style = "long" + # XXX should excinfo.getrepr record all data and toterminal() process it? + if style is None: + if self.config.getoption("tbstyle", "auto") == "short": + style = "short" + else: + style = "long" + + if self.config.get_verbosity() > 1: + truncate_locals = False + else: + truncate_locals = True + + truncate_args = False if self.config.get_verbosity() > 2 else True + + # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. + # It is possible for a fixture/test to change the CWD while this code runs, which + # would then result in the user seeing confusing paths in the failure message. + # To fix this, if the CWD changed, always display the full absolute path. + # It will be better to just always display paths relative to invocation_dir, but + # this requires a lot of plumbing (#6428). + try: + abspath = Path(os.getcwd()) != self.config.invocation_params.dir + except OSError: + abspath = True + + return excinfo.getrepr( + funcargs=True, + abspath=abspath, + showlocals=self.config.getoption("showlocals", False), + style=style, + tbfilter=tbfilter, + truncate_locals=truncate_locals, + truncate_args=truncate_args, + ) + + def repr_failure( + self, + excinfo: ExceptionInfo[BaseException], + style: TracebackStyle | None = None, + ) -> str | TerminalRepr: + """Return a representation of a collection or test failure. + + .. seealso:: :ref:`non-python tests` + + :param excinfo: Exception information for the failure. + """ + return self._repr_failure_py(excinfo, style) + + +def get_fslocation_from_item(node: Node) -> tuple[str | Path, int | None]: + """Try to extract the actual location from a node, depending on available attributes: + + * "location": a pair (path, lineno) + * "obj": a Python object that the node wraps. + * "path": just a path + + :rtype: A tuple of (str|Path, int) with filename and 0-based line number. + """ + # See Item.location. + location: tuple[str, int | None, str] | None = getattr(node, "location", None) + if location is not None: + return location[:2] + obj = getattr(node, "obj", None) + if obj is not None: + return getfslineno(obj) + return getattr(node, "path", "unknown location"), -1 + + +class Collector(Node, abc.ABC): + """Base class of all collectors. + + Collector create children through `collect()` and thus iteratively build + the collection tree. + """ + + class CollectError(Exception): + """An error during collection, contains a custom message.""" + + @abc.abstractmethod + def collect(self) -> Iterable[Item | Collector]: + """Collect children (items and collectors) for this collector.""" + raise NotImplementedError("abstract") + + # TODO: This omits the style= parameter which breaks Liskov Substitution. + def repr_failure( # type: ignore[override] + self, excinfo: ExceptionInfo[BaseException] + ) -> str | TerminalRepr: + """Return a representation of a collection failure. + + :param excinfo: Exception information for the failure. + """ + if isinstance(excinfo.value, self.CollectError) and not self.config.getoption( + "fulltrace", False + ): + exc = excinfo.value + return str(exc.args[0]) + + # Respect explicit tbstyle option, but default to "short" + # (_repr_failure_py uses "long" with "fulltrace" option always). + tbstyle = self.config.getoption("tbstyle", "auto") + if tbstyle == "auto": + tbstyle = "short" + + return self._repr_failure_py(excinfo, style=tbstyle) + + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: + if hasattr(self, "path"): + traceback = excinfo.traceback + ntraceback = traceback.cut(path=self.path) + if ntraceback == traceback: + ntraceback = ntraceback.cut(excludepath=tracebackcutdir) + return ntraceback.filter(excinfo) + return excinfo.traceback + + +@lru_cache(maxsize=1000) +def _check_initialpaths_for_relpath( + initial_paths: frozenset[Path], path: Path +) -> str | None: + if path in initial_paths: + return "" + + for parent in path.parents: + if parent in initial_paths: + return str(path.relative_to(parent)) + + return None + + +class FSCollector(Collector, abc.ABC): + """Base class for filesystem collectors.""" + + def __init__( + self, + fspath: None = None, + path_or_parent: Path | Node | None = None, + path: Path | None = None, + name: str | None = None, + parent: Node | None = None, + config: Config | None = None, + session: Session | None = None, + nodeid: str | None = None, + ) -> None: + if path_or_parent: + if isinstance(path_or_parent, Node): + assert parent is None + parent = cast(FSCollector, path_or_parent) + elif isinstance(path_or_parent, Path): + assert path is None + path = path_or_parent + assert path is not None + + if name is None: + name = path.name + if parent is not None and parent.path != path: + try: + rel = path.relative_to(parent.path) + except ValueError: + pass + else: + name = str(rel) + name = norm_sep(name) + self.path = path + + if session is None: + assert parent is not None + session = parent.session + + if nodeid is None: + try: + nodeid = str(self.path.relative_to(session.config.rootpath)) + except ValueError: + nodeid = _check_initialpaths_for_relpath(session._initialpaths, path) + + if nodeid: + nodeid = norm_sep(nodeid) + + super().__init__( + name=name, + parent=parent, + config=config, + session=session, + nodeid=nodeid, + path=path, + ) + + @classmethod + def from_parent( + cls, + parent, + *, + fspath: None = None, + path: Path | None = None, + **kw, + ) -> Self: + """The public constructor.""" + return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) + + +class File(FSCollector, abc.ABC): + """Base class for collecting tests from a file. + + :ref:`non-python tests`. + """ + + +class Directory(FSCollector, abc.ABC): + """Base class for collecting files from a directory. + + A basic directory collector does the following: goes over the files and + sub-directories in the directory and creates collectors for them by calling + the hooks :hook:`pytest_collect_directory` and :hook:`pytest_collect_file`, + after checking that they are not ignored using + :hook:`pytest_ignore_collect`. + + The default directory collectors are :class:`~pytest.Dir` and + :class:`~pytest.Package`. + + .. versionadded:: 8.0 + + :ref:`custom directory collectors`. + """ + + +class Item(Node, abc.ABC): + """Base class of all test invocation items. + + Note that for a single function there might be multiple test invocation items. + """ + + nextitem = None + + def __init__( + self, + name, + parent=None, + config: Config | None = None, + session: Session | None = None, + nodeid: str | None = None, + **kw, + ) -> None: + # The first two arguments are intentionally passed positionally, + # to keep plugins who define a node type which inherits from + # (pytest.Item, pytest.File) working (see issue #8435). + # They can be made kwargs when the deprecation above is done. + super().__init__( + name, + parent, + config=config, + session=session, + nodeid=nodeid, + **kw, + ) + self._report_sections: list[tuple[str, str, str]] = [] + + #: A list of tuples (name, value) that holds user defined properties + #: for this test. + self.user_properties: list[tuple[str, object]] = [] + + self._check_item_and_collector_diamond_inheritance() + + def _check_item_and_collector_diamond_inheritance(self) -> None: + """ + Check if the current type inherits from both File and Collector + at the same time, emitting a warning accordingly (#8447). + """ + cls = type(self) + + # We inject an attribute in the type to avoid issuing this warning + # for the same class more than once, which is not helpful. + # It is a hack, but was deemed acceptable in order to avoid + # flooding the user in the common case. + attr_name = "_pytest_diamond_inheritance_warning_shown" + if getattr(cls, attr_name, False): + return + setattr(cls, attr_name, True) + + problems = ", ".join( + base.__name__ for base in cls.__bases__ if issubclass(base, Collector) + ) + if problems: + warnings.warn( + f"{cls.__name__} is an Item subclass and should not be a collector, " + f"however its bases {problems} are collectors.\n" + "Please split the Collectors and the Item into separate node types.\n" + "Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n" + "example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/", + PytestWarning, + ) + + @abc.abstractmethod + def runtest(self) -> None: + """Run the test case for this item. + + Must be implemented by subclasses. + + .. seealso:: :ref:`non-python tests` + """ + raise NotImplementedError("runtest must be implemented by Item subclass") + + def add_report_section(self, when: str, key: str, content: str) -> None: + """Add a new report section, similar to what's done internally to add + stdout and stderr captured output:: + + item.add_report_section("call", "stdout", "report section contents") + + :param str when: + One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``. + :param str key: + Name of the section, can be customized at will. Pytest uses ``"stdout"`` and + ``"stderr"`` internally. + :param str content: + The full contents as a string. + """ + if content: + self._report_sections.append((when, key, content)) + + def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: + """Get location information for this item for test reports. + + Returns a tuple with three elements: + + - The path of the test (default ``self.path``) + - The 0-based line number of the test (default ``None``) + - A name of the test to be shown (default ``""``) + + .. seealso:: :ref:`non-python tests` + """ + return self.path, None, "" + + @cached_property + def location(self) -> tuple[str, int | None, str]: + """ + Returns a tuple of ``(relfspath, lineno, testname)`` for this item + where ``relfspath`` is file path relative to ``config.rootpath`` + and lineno is a 0-based line number. + """ + location = self.reportinfo() + path = absolutepath(location[0]) + relfspath = self.session._node_location_to_relpath(path) + assert type(location[2]) is str + return (relfspath, location[1], location[2]) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/outcomes.py b/.venv-test/lib/python3.12/site-packages/_pytest/outcomes.py new file mode 100644 index 0000000..257b95c --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/outcomes.py @@ -0,0 +1,286 @@ +"""Exception classes and constants handling test outcomes as well as +functions creating them.""" + +from __future__ import annotations + +import importlib +import sys +from typing import Any +from typing import ClassVar +from typing import NoReturn + + +class OutcomeException(BaseException): + """OutcomeException and its subclass instances indicate and contain info + about test and collection outcomes.""" + + def __init__(self, msg: str | None = None, pytrace: bool = True) -> None: + if msg is not None and not isinstance(msg, str): + error_msg = ( # type: ignore[unreachable] + "{} expected string as 'msg' parameter, got '{}' instead.\n" + "Perhaps you meant to use a mark?" + ) + raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__)) + super().__init__(msg) + self.msg = msg + self.pytrace = pytrace + + def __repr__(self) -> str: + if self.msg is not None: + return self.msg + return f"<{self.__class__.__name__} instance>" + + __str__ = __repr__ + + +TEST_OUTCOME = (OutcomeException, Exception) + + +class Skipped(OutcomeException): + # XXX hackish: on 3k we fake to live in the builtins + # in order to have Skipped exception printing shorter/nicer + __module__ = "builtins" + + def __init__( + self, + msg: str | None = None, + pytrace: bool = True, + allow_module_level: bool = False, + *, + _use_item_location: bool = False, + ) -> None: + super().__init__(msg=msg, pytrace=pytrace) + self.allow_module_level = allow_module_level + # If true, the skip location is reported as the item's location, + # instead of the place that raises the exception/calls skip(). + self._use_item_location = _use_item_location + + +class Failed(OutcomeException): + """Raised from an explicit call to pytest.fail().""" + + __module__ = "builtins" + + +class Exit(Exception): + """Raised for immediate program exits (no tracebacks/summaries).""" + + def __init__( + self, msg: str = "unknown reason", returncode: int | None = None + ) -> None: + self.msg = msg + self.returncode = returncode + super().__init__(msg) + + +class XFailed(Failed): + """Raised from an explicit call to pytest.xfail().""" + + +class _Exit: + """Exit testing process. + + :param reason: + The message to show as the reason for exiting pytest. reason has a default value + only because `msg` is deprecated. + + :param returncode: + Return code to be used when exiting pytest. None means the same as ``0`` (no error), + same as :func:`sys.exit`. + + :raises pytest.exit.Exception: + The exception that is raised. + """ + + Exception: ClassVar[type[Exit]] = Exit + + def __call__(self, reason: str = "", returncode: int | None = None) -> NoReturn: + __tracebackhide__ = True + raise Exit(msg=reason, returncode=returncode) + + +exit: _Exit = _Exit() + + +class _Skip: + """Skip an executing test with the given message. + + This function should be called only during testing (setup, call or teardown) or + during collection by using the ``allow_module_level`` flag. This function can + be called in doctests as well. + + :param reason: + The message to show the user as reason for the skip. + + :param allow_module_level: + Allows this function to be called at module level. + Raising the skip exception at module level will stop + the execution of the module and prevent the collection of all tests in the module, + even those defined before the `skip` call. + + Defaults to False. + + :raises pytest.skip.Exception: + The exception that is raised. + + .. note:: + It is better to use the :ref:`pytest.mark.skipif ref` marker when + possible to declare a test to be skipped under certain conditions + like mismatching platforms or dependencies. + Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`) + to skip a doctest statically. + """ + + Exception: ClassVar[type[Skipped]] = Skipped + + def __call__(self, reason: str = "", allow_module_level: bool = False) -> NoReturn: + __tracebackhide__ = True + raise Skipped(msg=reason, allow_module_level=allow_module_level) + + +skip: _Skip = _Skip() + + +class _Fail: + """Explicitly fail an executing test with the given message. + + :param reason: + The message to show the user as reason for the failure. + + :param pytrace: + If False, msg represents the full failure information and no + python traceback will be reported. + + :raises pytest.fail.Exception: + The exception that is raised. + """ + + Exception: ClassVar[type[Failed]] = Failed + + def __call__(self, reason: str = "", pytrace: bool = True) -> NoReturn: + __tracebackhide__ = True + raise Failed(msg=reason, pytrace=pytrace) + + +fail: _Fail = _Fail() + + +class _XFail: + """Imperatively xfail an executing test or setup function with the given reason. + + This function should be called only during testing (setup, call or teardown). + + No other code is executed after using ``xfail()`` (it is implemented + internally by raising an exception). + + :param reason: + The message to show the user as reason for the xfail. + + .. note:: + It is better to use the :ref:`pytest.mark.xfail ref` marker when + possible to declare a test to be xfailed under certain conditions + like known bugs or missing features. + + :raises pytest.xfail.Exception: + The exception that is raised. + """ + + Exception: ClassVar[type[XFailed]] = XFailed + + def __call__(self, reason: str = "") -> NoReturn: + __tracebackhide__ = True + raise XFailed(msg=reason) + + +xfail: _XFail = _XFail() + + +def importorskip( + modname: str, + minversion: str | None = None, + reason: str | None = None, + *, + exc_type: type[ImportError] | None = None, +) -> Any: + """Import and return the requested module ``modname``, or skip the + current test if the module cannot be imported. + + :param modname: + The name of the module to import. + :param minversion: + If given, the imported module's ``__version__`` attribute must be at + least this minimal version, otherwise the test is still skipped. + :param reason: + If given, this reason is shown as the message when the module cannot + be imported. + :param exc_type: + The exception that should be captured in order to skip modules. + Must be :py:class:`ImportError` or a subclass. + + Defaults to :class:`ModuleNotFoundError` when not given, which means + the module must be missing for the test to be skipped. + Pass ``exc_type=ImportError`` to also skip modules that raise + :class:`ImportError` during import. + + See :ref:`import-or-skip-import-error` for details. + + + :returns: + The imported module. This should be assigned to its canonical name. + + :raises pytest.skip.Exception: + If the module cannot be imported. + + Example:: + + docutils = pytest.importorskip("docutils") + + .. versionadded:: 8.2 + + The ``exc_type`` parameter. + + .. versionchanged:: 9.1 + + The default for ``exc_type`` is now :class:`ModuleNotFoundError`. + """ + import warnings + + __tracebackhide__ = True + compile(modname, "", "eval") # to catch syntaxerrors + + # Keep the public signature compatible while using the pytest 9.1 default behavior. + if exc_type is None: + exc_type = ModuleNotFoundError + + skipped: Skipped | None = None + + with warnings.catch_warnings(): + # Make sure to ignore ImportWarnings that might happen because + # of existing directories with the same name we're trying to + # import but without a __init__.py file. + warnings.simplefilter("ignore") + + try: + importlib.import_module(modname) + except exc_type as exc: + # Do not raise or issue warnings inside the catch_warnings() block. + if reason is None: + reason = f"could not import {modname!r}: {exc}" + skipped = Skipped(reason, allow_module_level=True) + if skipped: + raise skipped + + mod = sys.modules[modname] + if minversion is None: + return mod + verattr = getattr(mod, "__version__", None) + if minversion is not None: + # Imported lazily to improve start-up time. + from packaging.version import Version + + if verattr is None or Version(verattr) < Version(minversion): + raise Skipped( + f"module {modname!r} has __version__ {verattr!r}, required is: {minversion!r}", + allow_module_level=True, + ) + return mod diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/pastebin.py b/.venv-test/lib/python3.12/site-packages/_pytest/pastebin.py new file mode 100644 index 0000000..e6a1430 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/pastebin.py @@ -0,0 +1,121 @@ +# mypy: allow-untyped-defs +"""Submit failure or test session information to a pastebin service.""" + +from __future__ import annotations + +from io import StringIO +import tempfile +from typing import IO + +from _pytest.config import Config +from _pytest.config import create_terminal_writer +from _pytest.config.argparsing import Parser +from _pytest.deprecated import PASTEBIN +from _pytest.stash import StashKey +from _pytest.terminal import TerminalReporter +import pytest + + +pastebinfile_key = StashKey[IO[bytes]]() + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("terminal reporting") + group.addoption( + "--pastebin", + metavar="mode", + action="store", + dest="pastebin", + default=None, + choices=["failed", "all"], + help="Send failed|all info to bpaste.net pastebin service", + ) + + +@pytest.hookimpl(trylast=True) +def pytest_configure(config: Config) -> None: + if config.option.pastebin: + config.issue_config_time_warning(PASTEBIN, 2) + + if config.option.pastebin == "all": + tr = config.pluginmanager.getplugin("terminalreporter") + # If no terminal reporter plugin is present, nothing we can do here; + # this can happen when this function executes in a worker node + # when using pytest-xdist, for example. + if tr is not None: + # pastebin file will be UTF-8 encoded binary file. + config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b") + oldwrite = tr._tw.write + + def tee_write(s, **kwargs): + oldwrite(s, **kwargs) + if isinstance(s, str): + s = s.encode("utf-8") + config.stash[pastebinfile_key].write(s) + + tr._tw.write = tee_write + + +def pytest_unconfigure(config: Config) -> None: + if pastebinfile_key in config.stash: + pastebinfile = config.stash[pastebinfile_key] + # Get terminal contents and delete file. + pastebinfile.seek(0) + sessionlog = pastebinfile.read() + pastebinfile.close() + del config.stash[pastebinfile_key] + # Undo our patching in the terminal reporter. + tr = config.pluginmanager.getplugin("terminalreporter") + del tr._tw.__dict__["write"] + # Write summary. + tr.write_sep("=", "Sending information to Paste Service") + pastebinurl = create_new_paste(sessionlog) + tr.write_line(f"pastebin session-log: {pastebinurl}\n") + + +def create_new_paste(contents: str | bytes) -> str: + """Create a new paste using the bpaste.net service. + + :contents: Paste contents string. + :returns: URL to the pasted contents, or an error message. + """ + import re + from urllib.error import HTTPError + from urllib.parse import urlencode + from urllib.request import urlopen + + params = {"code": contents, "lexer": "text", "expiry": "1week"} + url = "https://bpa.st" + try: + response: str = ( + urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8") + ) + except HTTPError as e: + with e: # HTTPErrors are also http responses that must be closed! + return f"bad response: {e}" + except OSError as e: # eg urllib.error.URLError + return f"bad response: {e}" + m = re.search(r'href="/raw/(\w+)"', response) + if m: + return f"{url}/show/{m.group(1)}" + else: + return "bad response: invalid format ('" + response + "')" + + +def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None: + if terminalreporter.config.option.pastebin != "failed": + return + if "failed" in terminalreporter.stats: + terminalreporter.write_sep("=", "Sending information to Paste Service") + for rep in terminalreporter.stats["failed"]: + try: + msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc + except AttributeError: + msg = terminalreporter._getfailureheadline(rep) + file = StringIO() + tw = create_terminal_writer(terminalreporter.config, file) + rep.toterminal(tw) + s = file.getvalue() + assert len(s) + pastebinurl = create_new_paste(s) + terminalreporter.write_line(f"{msg} --> {pastebinurl}") diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/pathlib.py b/.venv-test/lib/python3.12/site-packages/_pytest/pathlib.py new file mode 100644 index 0000000..291bdf4 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/pathlib.py @@ -0,0 +1,1054 @@ +from __future__ import annotations + +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +import contextlib +from enum import Enum +from errno import EBADF +from errno import ELOOP +from errno import ENOENT +from errno import ENOTDIR +import fnmatch +from functools import partial +from importlib.machinery import ModuleSpec +from importlib.machinery import PathFinder +import importlib.util +import itertools +import os +from os.path import expanduser +from os.path import expandvars +from os.path import isabs +from os.path import sep +from pathlib import Path +from pathlib import PurePath +from posixpath import sep as posix_sep +import shutil +import sys +import types +from types import ModuleType +from typing import Any +from typing import TypeVar +import uuid +import warnings + +from _pytest.compat import assert_never +from _pytest.outcomes import skip +from _pytest.warning_types import PytestWarning + + +if sys.version_info < (3, 11): + from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader +else: + from importlib.machinery import NamespaceLoader + +LOCK_TIMEOUT = 60 * 60 * 24 * 3 + +_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath) + +# The following function, variables and comments were +# copied from cpython 3.9 Lib/pathlib.py file. + +# EBADF - guard against macOS `stat` throwing EBADF +_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP) + +_IGNORED_WINERRORS = ( + 21, # ERROR_NOT_READY - drive exists but is not accessible + 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself +) + + +def _ignore_error(exception: Exception) -> bool: + return ( + getattr(exception, "errno", None) in _IGNORED_ERRORS + or getattr(exception, "winerror", None) in _IGNORED_WINERRORS + ) + + +def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: + return path.joinpath(".lock") + + +def on_rm_rf_error( + func: Callable[..., Any] | None, + path: str, + excinfo: BaseException + | tuple[type[BaseException], BaseException, types.TracebackType | None], + *, + start_path: Path, +) -> bool: + """Handle known read-only errors during rmtree. + + The returned value is used only by our own tests. + """ + if isinstance(excinfo, BaseException): + exc = excinfo + else: + exc = excinfo[1] + + # Another process removed the file in the middle of the "rm_rf" (xdist for example). + # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018 + if isinstance(exc, FileNotFoundError): + return False + + if not isinstance(exc, PermissionError): + warnings.warn( + PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}") + ) + return False + + if func not in (os.rmdir, os.remove, os.unlink): + if func not in (os.open,): + warnings.warn( + PytestWarning( + f"(rm_rf) unknown function {func} when removing {path}:\n{type(exc)}: {exc}" + ) + ) + return False + + # Chmod + retry. + import stat + + def chmod_rw(p: str) -> None: + mode = os.stat(p).st_mode + os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR) + + # For files, we need to recursively go upwards in the directories to + # ensure they all are also writable. + p = Path(path) + if p.is_file(): + for parent in p.parents: + chmod_rw(str(parent)) + # Stop when we reach the original path passed to rm_rf. + if parent == start_path: + break + chmod_rw(str(path)) + + func(path) + return True + + +def ensure_extended_length_path(path: Path) -> Path: + """Get the extended-length version of a path (Windows). + + On Windows, by default, the maximum length of a path (MAX_PATH) is 260 + characters, and operations on paths longer than that fail. But it is possible + to overcome this by converting the path to "extended-length" form before + performing the operation: + https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation + + On Windows, this function returns the extended-length absolute version of path. + On other platforms it returns path unchanged. + """ + if sys.platform.startswith("win32"): + path = path.resolve() + path = Path(get_extended_length_path_str(str(path))) + return path + + +def get_extended_length_path_str(path: str) -> str: + """Convert a path to a Windows extended length path.""" + long_path_prefix = "\\\\?\\" + unc_long_path_prefix = "\\\\?\\UNC\\" + if path.startswith((long_path_prefix, unc_long_path_prefix)): + return path + # UNC + if path.startswith("\\\\"): + return unc_long_path_prefix + path[2:] + return long_path_prefix + path + + +def rm_rf(path: Path) -> None: + """Remove the path contents recursively, even if some elements + are read-only.""" + path = ensure_extended_length_path(path) + onerror = partial(on_rm_rf_error, start_path=path) + if sys.version_info >= (3, 12): + shutil.rmtree(str(path), onexc=onerror) + else: + shutil.rmtree(str(path), onerror=onerror) + + +def find_prefixed(root: Path, prefix: str) -> Iterator[os.DirEntry[str]]: + """Find all elements in root that begin with the prefix, case-insensitive.""" + l_prefix = prefix.lower() + for x in os.scandir(root): + if x.name.lower().startswith(l_prefix): + yield x + + +def extract_suffixes(iter: Iterable[os.DirEntry[str]], prefix: str) -> Iterator[str]: + """Return the parts of the paths following the prefix. + + :param iter: Iterator over path names. + :param prefix: Expected prefix of the path names. + """ + p_len = len(prefix) + for entry in iter: + yield entry.name[p_len:] + + +def find_suffixes(root: Path, prefix: str) -> Iterator[str]: + """Combine find_prefixes and extract_suffixes.""" + return extract_suffixes(find_prefixed(root, prefix), prefix) + + +def parse_num(maybe_num: str) -> int: + """Parse number path suffixes, returns -1 on error.""" + try: + return int(maybe_num) + except ValueError: + return -1 + + +def _force_symlink(root: Path, target: str | PurePath, link_to: str | Path) -> None: + """Helper to create the current symlink. + + It's full of race conditions that are reasonably OK to ignore + for the context of best effort linking to the latest test run. + + The presumption being that in case of much parallelism + the inaccuracy is going to be acceptable. + """ + current_symlink = root.joinpath(target) + try: + current_symlink.unlink() + except OSError: + pass + try: + current_symlink.symlink_to(link_to) + except Exception: + pass + + +def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path: + """Create a directory with an increased number as suffix for the given prefix.""" + for i in range(10): + # try up to 10 times to create the directory + max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) + new_number = max_existing + 1 + new_path = root.joinpath(f"{prefix}{new_number}") + try: + new_path.mkdir(mode=mode) + except Exception: + pass + else: + _force_symlink(root, prefix + "current", new_path) + return new_path + else: + raise OSError( + "could not create numbered dir with prefix " + f"{prefix} in {root} after 10 tries" + ) + + +def create_cleanup_lock(p: Path) -> Path: + """Create a lock to prevent premature directory cleanup.""" + lock_path = get_lock_path(p) + try: + fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + except FileExistsError as e: + raise OSError(f"cannot create lockfile in {p}") from e + else: + pid = os.getpid() + spid = str(pid).encode() + os.write(fd, spid) + os.close(fd) + if not lock_path.is_file(): + raise OSError("lock path got renamed after successful creation") + return lock_path + + +def register_cleanup_lock_removal(lock_path: Path, register: Any) -> Any: + """Register a cleanup function for removing a lock.""" + pid = os.getpid() + + def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None: + current_pid = os.getpid() + if current_pid != original_pid: + # fork + return + try: + lock_path.unlink() + except OSError: + pass + + return register(cleanup_on_exit) + + +def maybe_delete_a_numbered_dir(path: Path) -> None: + """Remove a numbered directory if its lock can be obtained and it does + not seem to be in use.""" + path = ensure_extended_length_path(path) + lock_path = None + try: + lock_path = create_cleanup_lock(path) + parent = path.parent + + garbage = parent.joinpath(f"garbage-{uuid.uuid4()}") + path.rename(garbage) + rm_rf(garbage) + except OSError: + # known races: + # * other process did a cleanup at the same time + # * deletable directory was found + # * process cwd (Windows) + return + finally: + # If we created the lock, ensure we remove it even if we failed + # to properly remove the numbered dir. + if lock_path is not None: + try: + lock_path.unlink() + except OSError: + pass + + +def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool: + """Check if `path` is deletable based on whether the lock file is expired.""" + if path.is_symlink(): + return False + lock = get_lock_path(path) + try: + if not lock.is_file(): + return True + except OSError: + # we might not have access to the lock file at all, in this case assume + # we don't have access to the entire directory (#7491). + return False + try: + lock_time = lock.stat().st_mtime + except Exception: + return False + else: + if lock_time < consider_lock_dead_if_created_before: + # We want to ignore any errors while trying to remove the lock such as: + # - PermissionDenied, like the file permissions have changed since the lock creation; + # - FileNotFoundError, in case another pytest process got here first; + # and any other cause of failure. + with contextlib.suppress(OSError): + lock.unlink() + return True + return False + + +def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None: + """Try to cleanup a directory if we can ensure it's deletable.""" + if ensure_deletable(path, consider_lock_dead_if_created_before): + maybe_delete_a_numbered_dir(path) + + +def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]: + """List candidates for numbered directories to be removed - follows py.path.""" + max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) + max_delete = max_existing - keep + entries = find_prefixed(root, prefix) + entries, entries2 = itertools.tee(entries) + numbers = map(parse_num, extract_suffixes(entries2, prefix)) + for entry, number in zip(entries, numbers, strict=True): + if number <= max_delete: + yield Path(entry) + + +def cleanup_dead_symlinks(root: Path) -> None: + for left_dir in root.iterdir(): + if left_dir.is_symlink(): + if not left_dir.resolve().exists(): + left_dir.unlink() + + +def cleanup_numbered_dir( + root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float +) -> None: + """Cleanup for lock driven numbered directories.""" + if not root.exists(): + return + for path in cleanup_candidates(root, prefix, keep): + try_cleanup(path, consider_lock_dead_if_created_before) + for path in root.glob("garbage-*"): + try_cleanup(path, consider_lock_dead_if_created_before) + + cleanup_dead_symlinks(root) + + +def make_numbered_dir_with_cleanup( + *, + root: Path, + prefix: str, + mode: int, + keep: int, + lock_timeout: float, + register: Any, +) -> Path: + """Create a numbered dir and register its cleanup. + + Similar to make_numbered_dir, but also maintains a lock file indicating that + the directory is currently in use, and registers the cleanup of the lock and + of stale numbered directories. + + :param keep: + The number of sessions to retain the directory. + :param lock_timeout: + In case of a crash, the lock remains "stuck". The timeout is a time + limit after which the lock is considered stale and can be removed. + :param register: + Called as register(cleanup_func, params...). Should schedule to call + passed cleanup functions on session finish. + """ + e = None + for i in range(10): + try: + p = make_numbered_dir(root, prefix, mode) + # Only lock the current dir when keep is not 0 + if keep != 0: + lock_path = create_cleanup_lock(p) + register_cleanup_lock_removal(lock_path, register) + except Exception as exc: + e = exc + else: + consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout + # Register a cleanup for program exit + register( + cleanup_numbered_dir, + root, + prefix, + keep, + consider_lock_dead_if_created_before, + ) + return p + assert e is not None + raise e + + +def resolve_from_str(input: str, rootpath: Path) -> Path: + input = expanduser(input) + input = expandvars(input) + if isabs(input): + return Path(input) + else: + return rootpath.joinpath(input) + + +def fnmatch_ex(pattern: str, path: str | os.PathLike[str]) -> bool: + """A port of FNMatcher from py.path.common which works with PurePath() instances. + + The difference between this algorithm and PurePath.match() is that the + latter matches "**" glob expressions for each part of the path, while + this algorithm uses the whole path instead. + + For example: + "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" + with this algorithm, but not with PurePath.match(). + + This algorithm was ported to keep backward-compatibility with existing + settings which assume paths match according this logic. + + References: + * https://bugs.python.org/issue29249 + * https://bugs.python.org/issue34731 + """ + path = PurePath(path) + iswin32 = sys.platform.startswith("win") + + if iswin32 and sep not in pattern and posix_sep in pattern: + # Running on Windows, the pattern has no Windows path separators, + # and the pattern has one or more Posix path separators. Replace + # the Posix path separators with the Windows path separator. + pattern = pattern.replace(posix_sep, sep) + + if sep not in pattern: + name = path.name + else: + name = str(path) + if path.is_absolute() and not os.path.isabs(pattern): + pattern = f"*{os.sep}{pattern}" + return fnmatch.fnmatch(name, pattern) + + +def parts(s: str) -> set[str]: + parts = s.split(sep) + return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} + + +def symlink_or_skip( + src: os.PathLike[str] | str, + dst: os.PathLike[str] | str, + **kwargs: Any, +) -> None: + """Make a symlink, or skip the test in case symlinks are not supported.""" + try: + os.symlink(src, dst, **kwargs) + except OSError as e: + skip(f"symlinks not supported: {e}") + + +class ImportMode(Enum): + """Possible values for `mode` parameter of `import_path`.""" + + prepend = "prepend" + append = "append" + importlib = "importlib" + + +class ImportPathMismatchError(ImportError): + """Raised on import_path() if there is a mismatch of __file__'s. + + This can happen when `import_path` is called multiple times with different filenames that has + the same basename but reside in packages + (for example "/tests1/test_foo.py" and "/tests2/test_foo.py"). + """ + + +def import_path( + path: str | os.PathLike[str], + *, + mode: str | ImportMode = ImportMode.prepend, + root: Path, + consider_namespace_packages: bool, +) -> ModuleType: + """ + Import and return a module from the given path, which can be a file (a module) or + a directory (a package). + + :param path: + Path to the file to import. + + :param mode: + Controls the underlying import mechanism that will be used: + + * ImportMode.prepend: the directory containing the module (or package, taking + `__init__.py` files into account) will be put at the *start* of `sys.path` before + being imported with `importlib.import_module`. + + * ImportMode.append: same as `prepend`, but the directory will be appended + to the end of `sys.path`, if not already in `sys.path`. + + * ImportMode.importlib: uses more fine control mechanisms provided by `importlib` + to import the module, which avoids having to muck with `sys.path` at all. It effectively + allows having same-named test modules in different places. + + :param root: + Used as an anchor when mode == ImportMode.importlib to obtain + a unique name for the module being imported so it can safely be stored + into ``sys.modules``. + + :param consider_namespace_packages: + If True, consider namespace packages when resolving module names. + + :raises ImportPathMismatchError: + If after importing the given `path` and the module `__file__` + are different. Only raised in `prepend` and `append` modes. + """ + path = Path(path) + mode = ImportMode(mode) + + if not path.exists(): + raise ImportError(path) + + if mode is ImportMode.importlib: + # Try to import this module using the standard import mechanisms, but + # without touching sys.path. + try: + _, module_name = resolve_pkg_root_and_module_name( + path, consider_namespace_packages=consider_namespace_packages + ) + except CouldNotResolvePathError: + pass + else: + # If the given module name is already in sys.modules, do not import it again. + with contextlib.suppress(KeyError): + return sys.modules[module_name] + + mod = _import_module_using_spec(module_name, path, insert_modules=False) + if mod is not None: + return mod + + # Could not import the module with the current sys.path, so we fall back + # to importing the file as a single module, not being a part of a package. + module_name = module_name_from_path(path, root) + with contextlib.suppress(KeyError): + return sys.modules[module_name] + + mod = _import_module_using_spec(module_name, path, insert_modules=True) + if mod is None: + raise ImportError(f"Can't find module {module_name} at location {path}") + return mod + + try: + pkg_root, module_name = resolve_pkg_root_and_module_name( + path, consider_namespace_packages=consider_namespace_packages + ) + except CouldNotResolvePathError: + pkg_root, module_name = path.parent, path.stem + + # Change sys.path permanently: restoring it at the end of this function would cause surprising + # problems because of delayed imports: for example, a conftest.py file imported by this function + # might have local imports, which would fail at runtime if we restored sys.path. + if mode is ImportMode.append: + if str(pkg_root) not in sys.path: + sys.path.append(str(pkg_root)) + elif mode is ImportMode.prepend: + if str(pkg_root) != sys.path[0]: + sys.path.insert(0, str(pkg_root)) + else: + assert_never(mode) + + importlib.import_module(module_name) + + mod = sys.modules[module_name] + if path.name == "__init__.py": + return mod + + ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "") + if ignore != "1": + module_file = mod.__file__ + if module_file is None: + raise ImportPathMismatchError(module_name, module_file, path) + + if module_file.endswith((".pyc", ".pyo")): + module_file = module_file[:-1] + if module_file.endswith(os.sep + "__init__.py"): + module_file = module_file[: -(len(os.sep + "__init__.py"))] + + try: + is_same = _is_same(str(path), module_file) + except FileNotFoundError: + is_same = False + + if not is_same: + raise ImportPathMismatchError(module_name, module_file, path) + + return mod + + +def _import_module_using_spec( + module_name: str, module_path: Path, *, insert_modules: bool +) -> ModuleType | None: + """ + Tries to import a module by its canonical name, path, and its parent location. + + :param module_name: + The expected module name, will become the key of `sys.modules`. + + :param module_path: + The file path of the module, for example `/foo/bar/test_demo.py`. + If module is a package, pass the path to the `__init__.py` of the package. + If module is a namespace package, pass directory path. + + :param insert_modules: + If True, will call `insert_missing_modules` to create empty intermediate modules + with made-up module names (when importing test files not reachable from `sys.path`). + + Example 1 of parent_module_*: + + module_name: "a.b.c.demo" + module_path: Path("a/b/c/demo.py") + if "a.b.c" is package ("a/b/c/__init__.py" exists), then + parent_module_name: "a.b.c" + parent_module_path: Path("a/b/c/__init__.py") + else: + parent_module_name: "a.b.c" + parent_module_path: Path("a/b/c") + + Example 2 of parent_module_*: + + module_name: "a.b.c" + module_path: Path("a/b/c/__init__.py") + if "a.b" is package ("a/b/__init__.py" exists), then + parent_module_name: "a.b" + parent_module_path: Path("a/b/__init__.py") + else: + parent_module_name: "a.b" + parent_module_path: Path("a/b/") + """ + # Attempt to import the parent module, seems is our responsibility: + # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311 + parent_module_name, _, name = module_name.rpartition(".") + parent_module: ModuleType | None = None + if parent_module_name: + parent_module = sys.modules.get(parent_module_name) + # If the parent_module lacks the `__path__` attribute, AttributeError when finding a submodule's spec, + # requiring re-import according to the path. + need_reimport = not hasattr(parent_module, "__path__") + if parent_module is None or need_reimport: + # Get parent_location based on location, get parent_path based on path. + if module_path.name == "__init__.py": + # If the current module is in a package, + # need to leave the package first and then enter the parent module. + parent_module_path = module_path.parent.parent + else: + parent_module_path = module_path.parent + + if (parent_module_path / "__init__.py").is_file(): + # If the parent module is a package, loading by __init__.py file. + parent_module_path = parent_module_path / "__init__.py" + + parent_module = _import_module_using_spec( + parent_module_name, + parent_module_path, + insert_modules=insert_modules, + ) + + # Checking with sys.meta_path first in case one of its hooks can import this module, + # such as our own assertion-rewrite hook. + find_spec_path = [str(module_path.parent)] + for meta_importer in sys.meta_path: + spec = meta_importer.find_spec(module_name, find_spec_path) + + if spec_matches_module_path(spec, module_path): + break + else: + loader = None + if module_path.is_dir(): + # The `spec_from_file_location` matches a loader based on the file extension by default. + # For a namespace package, need to manually specify a loader. + loader = NamespaceLoader(name, module_path, PathFinder()) # type: ignore[arg-type] + + spec = importlib.util.spec_from_file_location( + module_name, str(module_path), loader=loader + ) + + if spec_matches_module_path(spec, module_path): + assert spec is not None + # Find spec and import this module. + mod = importlib.util.module_from_spec(spec) + sys.modules[module_name] = mod + spec.loader.exec_module(mod) # type: ignore[union-attr] + + # Set this module as an attribute of the parent module (#12194). + if parent_module is not None: + setattr(parent_module, name, mod) + + if insert_modules: + insert_missing_modules(sys.modules, module_name) + return mod + + return None + + +def spec_matches_module_path(module_spec: ModuleSpec | None, module_path: Path) -> bool: + """Return true if the given ModuleSpec can be used to import the given module path.""" + if module_spec is None: + return False + + if module_spec.origin: + return Path(module_spec.origin) == module_path + + # Compare the path with the `module_spec.submodule_Search_Locations` in case + # the module is part of a namespace package. + # https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.submodule_search_locations + if module_spec.submodule_search_locations: # can be None. + for path in module_spec.submodule_search_locations: + if Path(path) == module_path: + return True + + return False + + +# Implement a special _is_same function on Windows which returns True if the two filenames +# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678). +if sys.platform.startswith("win"): + + def _is_same(f1: str, f2: str) -> bool: + return Path(f1) == Path(f2) or os.path.samefile(f1, f2) + +else: + + def _is_same(f1: str, f2: str) -> bool: + return os.path.samefile(f1, f2) + + +def module_name_from_path(path: Path, root: Path) -> str: + """ + Return a dotted module name based on the given path, anchored on root. + + For example: path="projects/src/tests/test_foo.py" and root="/projects", the + resulting module name will be "src.tests.test_foo". + """ + path = path.with_suffix("") + try: + relative_path = path.relative_to(root) + except ValueError: + # If we can't get a relative path to root, use the full path, except + # for the first part ("d:\\" or "/" depending on the platform, for example). + path_parts = path.parts[1:] + else: + # Use the parts for the relative path to the root path. + path_parts = relative_path.parts + + # Module name for packages do not contain the __init__ file, unless + # the `__init__.py` file is at the root. + if len(path_parts) >= 2 and path_parts[-1] == "__init__": + path_parts = path_parts[:-1] + + # Module names cannot contain ".", normalize them to "_". This prevents + # a directory having a "." in the name (".env.310" for example) causing extra intermediate modules. + # Also, important to replace "." at the start of paths, as those are considered relative imports. + path_parts = tuple(x.replace(".", "_") for x in path_parts) + + return ".".join(path_parts) + + +def insert_missing_modules(modules: dict[str, ModuleType], module_name: str) -> None: + """ + Used by ``import_path`` to create intermediate modules when using mode=importlib. + + When we want to import a module as "src.tests.test_foo" for example, we need + to create empty modules "src" and "src.tests" after inserting "src.tests.test_foo", + otherwise "src.tests.test_foo" is not importable by ``__import__``. + """ + module_parts = module_name.split(".") + while module_name: + parent_module_name, _, child_name = module_name.rpartition(".") + if parent_module_name: + parent_module = modules.get(parent_module_name) + if parent_module is None: + try: + # If sys.meta_path is empty, calling import_module will issue + # a warning and raise ModuleNotFoundError. To avoid the + # warning, we check sys.meta_path explicitly and raise the error + # ourselves to fall back to creating a dummy module. + if not sys.meta_path: + raise ModuleNotFoundError + parent_module = importlib.import_module(parent_module_name) + except ModuleNotFoundError: + parent_module = ModuleType( + module_name, + doc="Empty module created by pytest's importmode=importlib.", + ) + modules[parent_module_name] = parent_module + + # Add child attribute to the parent that can reference the child + # modules. + if not hasattr(parent_module, child_name): + setattr(parent_module, child_name, modules[module_name]) + + module_parts.pop(-1) + module_name = ".".join(module_parts) + + +def resolve_package_path(path: Path) -> Path | None: + """Return the Python package path by looking for the last + directory upwards which still contains an __init__.py. + + Returns None if it cannot be determined. + """ + result = None + for parent in itertools.chain((path,), path.parents): + if parent.is_dir(): + if not (parent / "__init__.py").is_file(): + break + if not parent.name.isidentifier(): + break + result = parent + return result + + +def resolve_pkg_root_and_module_name( + path: Path, *, consider_namespace_packages: bool = False +) -> tuple[Path, str]: + """ + Return the path to the directory of the root package that contains the + given Python file, and its module name: + + src/ + app/ + __init__.py + core/ + __init__.py + models.py + + Passing the full path to `models.py` will yield Path("src") and "app.core.models". + + If consider_namespace_packages is True, then we additionally check upwards in the hierarchy + for namespace packages: + + https://packaging.python.org/en/latest/guides/packaging-namespace-packages + + Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files). + """ + pkg_root: Path | None = None + pkg_path = resolve_package_path(path) + if pkg_path is not None: + pkg_root = pkg_path.parent + if consider_namespace_packages: + start = pkg_root if pkg_root is not None else path.parent + for candidate in (start, *start.parents): + module_name = compute_module_name(candidate, path) + if module_name and is_importable(module_name, path): + # Point the pkg_root to the root of the namespace package. + pkg_root = candidate + break + + if pkg_root is not None: + module_name = compute_module_name(pkg_root, path) + if module_name: + return pkg_root, module_name + + raise CouldNotResolvePathError(f"Could not resolve for {path}") + + +def is_importable(module_name: str, module_path: Path) -> bool: + """ + Return if the given module path could be imported normally by Python, akin to the user + entering the REPL and importing the corresponding module name directly, and corresponds + to the module_path specified. + + :param module_name: + Full module name that we want to check if is importable. + For example, "app.models". + + :param module_path: + Full path to the python module/package we want to check if is importable. + For example, "/projects/src/app/models.py". + """ + try: + # Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through + # sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``). + # Using importlib.util.find_spec() is different, it gives the same results as trying to import + # the module normally in the REPL. + spec = importlib.util.find_spec(module_name) + except (ImportError, ValueError, ImportWarning): + return False + else: + return spec_matches_module_path(spec, module_path) + + +def compute_module_name(root: Path, module_path: Path) -> str | None: + """Compute a module name based on a path and a root anchor.""" + try: + path_without_suffix = module_path.with_suffix("") + except ValueError: + # Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter). + return None + + try: + relative = path_without_suffix.relative_to(root) + except ValueError: # pragma: no cover + return None + names = list(relative.parts) + if not names: + return None + if names[-1] == "__init__": + names.pop() + return ".".join(names) + + +class CouldNotResolvePathError(Exception): + """Custom exception raised by resolve_pkg_root_and_module_name.""" + + +def scandir( + path: str | os.PathLike[str], + sort_key: Callable[[os.DirEntry[str]], object] = lambda entry: entry.name, +) -> list[os.DirEntry[str]]: + """Scan a directory recursively, in breadth-first order. + + The returned entries are sorted according to the given key. + The default is to sort by name. + If the directory does not exist, return an empty list. + """ + entries = [] + # Attempt to create a scandir iterator for the given path. + try: + scandir_iter = os.scandir(path) + except FileNotFoundError: + # If the directory does not exist, return an empty list. + return [] + # Use the scandir iterator in a context manager to ensure it is properly closed. + with scandir_iter as s: + for entry in s: + try: + entry.is_file() + except OSError as err: + if _ignore_error(err): + continue + # Reraise non-ignorable errors to avoid hiding issues. + raise + entries.append(entry) + entries.sort(key=sort_key) # type: ignore[arg-type] + return entries + + +def visit( + path: str | os.PathLike[str], recurse: Callable[[os.DirEntry[str]], bool] +) -> Iterator[os.DirEntry[str]]: + """Walk a directory recursively, in breadth-first order. + + The `recurse` predicate determines whether a directory is recursed. + + Entries at each directory level are sorted. + """ + entries = scandir(path) + yield from entries + for entry in entries: + if entry.is_dir() and recurse(entry): + yield from visit(entry.path, recurse) + + +def absolutepath(path: str | os.PathLike[str]) -> Path: + """Convert a path to an absolute path using os.path.abspath. + + Prefer this over Path.resolve() (see #6523). + Prefer this over Path.absolute() (not public, doesn't normalize). + """ + return Path(os.path.abspath(path)) + + +def commonpath(path1: Path, path2: Path) -> Path | None: + """Return the common part shared with the other path, or None if there is + no common part. + + If one path is relative and one is absolute, returns None. + """ + try: + return Path(os.path.commonpath((str(path1), str(path2)))) + except ValueError: + return None + + +def bestrelpath(directory: Path, dest: Path) -> str: + """Return a string which is a relative path from directory to dest such + that directory/bestrelpath == dest. + + The paths must be either both absolute or both relative. + + If no such path can be determined, returns dest. + """ + assert isinstance(directory, Path) + assert isinstance(dest, Path) + if dest == directory: + return os.curdir + # Find the longest common directory. + base = commonpath(directory, dest) + # Can be the case on Windows for two absolute paths on different drives. + # Can be the case for two relative paths without common prefix. + # Can be the case for a relative path and an absolute path. + if not base: + return str(dest) + reldirectory = directory.relative_to(base) + reldest = dest.relative_to(base) + return os.path.join( + # Back from directory to base. + *([os.pardir] * len(reldirectory.parts)), + # Forward from base to dest. + *reldest.parts, + ) + + +def safe_exists(p: Path) -> bool: + """Like Path.exists(), but account for input arguments that might be too long (#11394).""" + try: + return p.exists() + except (ValueError, OSError): + # ValueError: stat: path too long for Windows + # OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect + return False + + +def samefile_nofollow(p1: Path, p2: Path) -> bool: + """Test whether two paths reference the same actual file or directory. + + Unlike Path.samefile(), does not resolve symlinks. + """ + return os.path.samestat(p1.lstat(), p2.lstat()) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/py.typed b/.venv-test/lib/python3.12/site-packages/_pytest/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/pytester.py b/.venv-test/lib/python3.12/site-packages/_pytest/pytester.py new file mode 100644 index 0000000..b69b587 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/pytester.py @@ -0,0 +1,1803 @@ +# mypy: allow-untyped-defs +"""(Disabled by default) support for testing pytest and pytest plugins. + +PYTEST_DONT_REWRITE +""" + +from __future__ import annotations + +import collections.abc +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Mapping +from collections.abc import Sequence +import contextlib +from fnmatch import fnmatch +import gc +import importlib +from io import StringIO +import locale +import os +from pathlib import Path +import platform +import re +import shutil +import subprocess +import sys +import traceback +from typing import Any +from typing import Final +from typing import final +from typing import IO +from typing import Literal +from typing import overload +from typing import TextIO +from typing import TYPE_CHECKING +from weakref import WeakKeyDictionary + +from iniconfig import IniConfig +from iniconfig import SectionWrapper + +from _pytest import timing +from _pytest._code import Source +from _pytest.capture import _get_multicapture +from _pytest.compat import NOTSET +from _pytest.compat import NotSetType +from _pytest.config import _PluggyPlugin +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config import main +from _pytest.config import PytestPluginManager +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.main import Session +from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.outcomes import fail +from _pytest.outcomes import importorskip +from _pytest.outcomes import skip +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import make_numbered_dir +from _pytest.reports import CollectReport +from _pytest.reports import TestReport +from _pytest.tmpdir import TempPathFactory +from _pytest.warning_types import PytestFDWarning + + +if TYPE_CHECKING: + import pexpect + + +pytest_plugins = ["pytester_assertions"] + + +IGNORE_PAM = [ # filenames added when obtaining details about the current user + "/var/lib/sss/mc/passwd" +] + + +def pytest_addoption(parser: Parser) -> None: + parser.addoption( + "--lsof", + action="store_true", + dest="lsof", + default=False, + help="Run FD checks if lsof is available", + ) + + parser.addoption( + "--runpytest", + default="inprocess", + dest="runpytest", + choices=("inprocess", "subprocess"), + help=( + "Run pytest sub runs in tests using an 'inprocess' " + "or 'subprocess' (python -m main) method" + ), + ) + + parser.addini( + "pytester_example_dir", help="Directory to take the pytester example files from" + ) + + +def pytest_configure(config: Config) -> None: + if config.getvalue("lsof"): + checker = LsofFdLeakChecker() + if checker.matching_platform(): + config.pluginmanager.register(checker) + + config.addinivalue_line( + "markers", + "pytester_example_path(*path_segments): join the given path " + "segments to `pytester_example_dir` for this test.", + ) + + +class LsofFdLeakChecker: + def get_open_files(self) -> list[tuple[str, str]]: + if sys.version_info >= (3, 11): + # New in Python 3.11, ignores utf-8 mode + encoding = locale.getencoding() + else: + encoding = locale.getpreferredencoding(False) + out = subprocess.run( + ("lsof", "-Ffn0", "-p", str(os.getpid())), + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=True, + text=True, + encoding=encoding, + ).stdout + + def isopen(line: str) -> bool: + return line.startswith("f") and ( + "deleted" not in line + and "mem" not in line + and "txt" not in line + and "cwd" not in line + ) + + open_files = [] + + for line in out.split("\n"): + if isopen(line): + fields = line.split("\0") + fd = fields[0][1:] + filename = fields[1][1:] + if filename in IGNORE_PAM: + continue + if filename.startswith("/"): + open_files.append((fd, filename)) + + return open_files + + def matching_platform(self) -> bool: + try: + subprocess.run(("lsof", "-v"), check=True) + except (OSError, subprocess.CalledProcessError): + return False + else: + return True + + @hookimpl(wrapper=True, tryfirst=True) + def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]: + lines1 = self.get_open_files() + try: + return (yield) + finally: + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() + + new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [ + f"***** {len(leaked_files)} FD leakage detected", + *(str(f) for f in leaked_files), + "*** Before:", + *(str(f) for f in lines1), + "*** After:", + *(str(f) for f in lines2), + f"***** {len(leaked_files)} FD leakage detected", + "*** function {}:{}: {} ".format(*item.location), + "See issue #2366", + ] + item.warn(PytestFDWarning("\n".join(error))) + + +# used at least by pytest-xdist plugin + + +@fixture +def _pytest(request: FixtureRequest) -> PytestArg: + """Return a helper which offers a gethookrecorder(hook) method which + returns a HookRecorder instance which helps to make assertions about called + hooks.""" + return PytestArg(request) + + +class PytestArg: + def __init__(self, request: FixtureRequest) -> None: + self._request = request + + def gethookrecorder(self, hook) -> HookRecorder: + hookrecorder = HookRecorder(hook._pm) + self._request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + + +def get_public_names(values: Iterable[str]) -> list[str]: + """Only return names from iterator values without a leading underscore.""" + return [x for x in values if x[0] != "_"] + + +@final +class RecordedHookCall: + """A recorded call to a hook. + + The arguments to the hook call are set as attributes. + For example: + + .. code-block:: python + + calls = hook_recorder.getcalls("pytest_runtest_setup") + # Suppose pytest_runtest_setup was called once with `item=an_item`. + assert calls[0].item is an_item + """ + + def __init__(self, name: str, kwargs) -> None: + self.__dict__.update(kwargs) + self._name = name + + def __repr__(self) -> str: + d = self.__dict__.copy() + del d["_name"] + return f"" + + if TYPE_CHECKING: + # The class has undetermined attributes, this tells mypy about it. + def __getattr__(self, key: str): ... + + +@final +class HookRecorder: + """Record all hooks called in a plugin manager. + + Hook recorders are created by :class:`Pytester`. + + This wraps all the hook calls in the plugin manager, recording each call + before propagating the normal calls. + """ + + def __init__( + self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + + self._pluginmanager = pluginmanager + self.calls: list[RecordedHookCall] = [] + self.ret: int | ExitCode | None = None + + def before(hook_name: str, hook_impls, kwargs) -> None: + self.calls.append(RecordedHookCall(hook_name, kwargs)) + + def after(outcome, hook_name: str, hook_impls, kwargs) -> None: + pass + + self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after) + + def finish_recording(self) -> None: + self._undo_wrapping() + + def getcalls(self, names: str | Iterable[str]) -> list[RecordedHookCall]: + """Get all recorded calls to hooks with the given names (or name).""" + if isinstance(names, str): + names = names.split() + return [call for call in self.calls if call._name in names] + + def assert_contains(self, entries: Sequence[tuple[str, str]]) -> None: + __tracebackhide__ = True + i = 0 + entries = list(entries) + # Since Python 3.13, f_locals is not a dict, but eval requires a dict. + backlocals = dict(sys._getframe(1).f_locals) + while entries: + name, check = entries.pop(0) + for ind, call in enumerate(self.calls[i:]): + if call._name == name: + print("NAMEMATCH", name, call) + if eval(check, backlocals, call.__dict__): + print("CHECKERMATCH", repr(check), "->", call) + else: + print("NOCHECKERMATCH", repr(check), "-", call) + continue + i += ind + 1 + break + print("NONAMEMATCH", name, "with", call) + else: + fail(f"could not find {name!r} check {check!r}") + + def popcall(self, name: str) -> RecordedHookCall: + __tracebackhide__ = True + for i, call in enumerate(self.calls): + if call._name == name: + del self.calls[i] + return call + lines = [f"could not find call {name!r}, in:"] + lines.extend([f" {x}" for x in self.calls]) + fail("\n".join(lines)) + + def getcall(self, name: str) -> RecordedHookCall: + values = self.getcalls(name) + assert len(values) == 1, (name, values) + return values[0] + + # functionality for test reports + + @overload + def getreports( + self, + names: Literal["pytest_collectreport"], + ) -> Sequence[CollectReport]: ... + + @overload + def getreports( + self, + names: Literal["pytest_runtest_logreport"], + ) -> Sequence[TestReport]: ... + + @overload + def getreports( + self, + names: str | Iterable[str] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[CollectReport | TestReport]: ... + + def getreports( + self, + names: str | Iterable[str] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[CollectReport | TestReport]: + return [x.report for x in self.getcalls(names)] + + def matchreport( + self, + inamepart: str = "", + names: str | Iterable[str] = ( + "pytest_runtest_logreport", + "pytest_collectreport", + ), + when: str | None = None, + ) -> CollectReport | TestReport: + """Return a testreport whose dotted import path matches.""" + values = [] + for rep in self.getreports(names=names): + if not when and rep.when != "call" and rep.passed: + # setup/teardown passing reports - let's ignore those + continue + if when and rep.when != when: + continue + if not inamepart or inamepart in rep.nodeid.split("::"): + values.append(rep) + if not values: + raise ValueError( + f"could not find test report matching {inamepart!r}: " + "no test reports at all!" + ) + if len(values) > 1: + raise ValueError( + f"found 2 or more testreports matching {inamepart!r}: {values}" + ) + return values[0] + + @overload + def getfailures( + self, + names: Literal["pytest_collectreport"], + ) -> Sequence[CollectReport]: ... + + @overload + def getfailures( + self, + names: Literal["pytest_runtest_logreport"], + ) -> Sequence[TestReport]: ... + + @overload + def getfailures( + self, + names: str | Iterable[str] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[CollectReport | TestReport]: ... + + def getfailures( + self, + names: str | Iterable[str] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[CollectReport | TestReport]: + return [rep for rep in self.getreports(names) if rep.failed] + + def getfailedcollections(self) -> Sequence[CollectReport]: + return self.getfailures("pytest_collectreport") + + def listoutcomes( + self, + ) -> tuple[ + Sequence[TestReport], + Sequence[CollectReport | TestReport], + Sequence[CollectReport | TestReport], + ]: + passed = [] + skipped = [] + failed = [] + for rep in self.getreports( + ("pytest_collectreport", "pytest_runtest_logreport") + ): + if rep.passed: + if rep.when == "call": + assert isinstance(rep, TestReport) + passed.append(rep) + elif rep.skipped: + skipped.append(rep) + else: + assert rep.failed, f"Unexpected outcome: {rep!r}" + failed.append(rep) + return passed, skipped, failed + + def countoutcomes(self) -> list[int]: + return [len(x) for x in self.listoutcomes()] + + def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: + __tracebackhide__ = True + from _pytest.pytester_assertions import assertoutcome + + outcomes = self.listoutcomes() + assertoutcome( + outcomes, + passed=passed, + skipped=skipped, + failed=failed, + ) + + def clear(self) -> None: + self.calls[:] = [] + + +@fixture +def linecomp() -> LineComp: + """A :class: `LineComp` instance for checking that an input linearly + contains a sequence of strings.""" + return LineComp() + + +@fixture(name="LineMatcher") +def LineMatcher_fixture(request: FixtureRequest) -> type[LineMatcher]: + """A reference to the :class: `LineMatcher`. + + This is instantiable with a list of lines (without their trailing newlines). + This is useful for testing large texts, such as the output of commands. + """ + return LineMatcher + + +@fixture +def pytester( + request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch +) -> Pytester: + """ + Facilities to write tests/configuration files, execute pytest in isolation, and match + against expected output, perfect for black-box testing of pytest plugins. + + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to ``path`` and environment variables during initialization. + + It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path` + fixture but provides methods which aid in testing pytest itself. + """ + return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True) + + +@fixture +def _sys_snapshot() -> Generator[None]: + snappaths = SysPathsSnapshot() + snapmods = SysModulesSnapshot() + yield + snapmods.restore() + snappaths.restore() + + +@fixture +def _config_for_test() -> Generator[Config]: + from _pytest.config import get_config + + config = get_config() + yield config + config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles. + + +# Regex to match the session duration string in the summary: "74.34s". +rex_session_duration = re.compile(r"\d+\.\d\ds") +# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped". +rex_outcome = re.compile(r"(\d+) (\w+)") + + +@final +class RunResult: + """The result of running a command from :class:`~pytest.Pytester`.""" + + def __init__( + self, + ret: int | ExitCode, + outlines: list[str], + errlines: list[str], + duration: float, + ) -> None: + try: + self.ret: int | ExitCode = ExitCode(ret) + """The return value.""" + except ValueError: + self.ret = ret + self.outlines = outlines + """List of lines captured from stdout.""" + self.errlines = errlines + """List of lines captured from stderr.""" + self.stdout = LineMatcher(outlines) + """:class:`~pytest.LineMatcher` of stdout. + + Use e.g. :func:`str(stdout) ` to reconstruct stdout, or the commonly used + :func:`stdout.fnmatch_lines() ` method. + """ + self.stderr = LineMatcher(errlines) + """:class:`~pytest.LineMatcher` of stderr.""" + self.duration = duration + """Duration in seconds.""" + + def __repr__(self) -> str: + return ( + f"" + ) + + def parseoutcomes(self) -> dict[str, int]: + """Return a dictionary of outcome noun -> count from parsing the terminal + output that the test process produced. + + The returned nouns will always be in plural form:: + + ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== + + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``. + """ + return self.parse_summary_nouns(self.outlines) + + @classmethod + def parse_summary_nouns(cls, lines) -> dict[str, int]: + """Extract the nouns from a pytest terminal summary line. + + It always returns the plural noun for consistency:: + + ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== + + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``. + """ + for line in reversed(lines): + if rex_session_duration.search(line): + outcomes = rex_outcome.findall(line) + ret = {noun: int(count) for (count, noun) in outcomes} + break + else: + raise ValueError("Pytest terminal summary report not found") + + to_plural = { + "warning": "warnings", + "error": "errors", + } + return {to_plural.get(k, k): v for k, v in ret.items()} + + def assert_outcomes( + self, + passed: int = 0, + skipped: int = 0, + failed: int = 0, + errors: int = 0, + xpassed: int = 0, + xfailed: int = 0, + warnings: int | None = None, + deselected: int | None = None, + ) -> None: + """ + Assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run. + + ``warnings`` and ``deselected`` are only checked if not None. + """ + __tracebackhide__ = True + from _pytest.pytester_assertions import assert_outcomes + + outcomes = self.parseoutcomes() + assert_outcomes( + outcomes, + passed=passed, + skipped=skipped, + failed=failed, + errors=errors, + xpassed=xpassed, + xfailed=xfailed, + warnings=warnings, + deselected=deselected, + ) + + +class SysModulesSnapshot: + def __init__(self, preserve: Callable[[str], bool] | None = None) -> None: + self.__preserve = preserve + self.__saved = dict(sys.modules) + + def restore(self) -> None: + if self.__preserve: + self.__saved.update( + (k, m) for k, m in sys.modules.items() if self.__preserve(k) + ) + sys.modules.clear() + sys.modules.update(self.__saved) + + +class SysPathsSnapshot: + def __init__(self) -> None: + self.__saved = list(sys.path), list(sys.meta_path) + + def restore(self) -> None: + sys.path[:], sys.meta_path[:] = self.__saved + + +_FileContent = tuple[str | bytes, ...] | list[str | bytes] | str | bytes + + +@final +class Pytester: + """ + Facilities to write tests/configuration files, execute pytest in isolation, and match + against expected output, perfect for black-box testing of pytest plugins. + + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to :attr:`path` and environment variables during initialization. + """ + + __test__ = False + + CLOSE_STDIN: Final = NOTSET + + class TimeoutExpired(Exception): + pass + + def __init__( + self, + request: FixtureRequest, + tmp_path_factory: TempPathFactory, + monkeypatch: MonkeyPatch, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._request = request + self._mod_collections: WeakKeyDictionary[Collector, list[Item | Collector]] = ( + WeakKeyDictionary() + ) + if request.function: + name: str = request.function.__name__ + else: + name = request.node.name + self._name = name + self._path: Path = tmp_path_factory.mktemp(name, numbered=True) + #: A list of plugins to use with :py:meth:`parseconfig` and + #: :py:meth:`runpytest`. Initially this is an empty list but plugins can + #: be added to the list. + #: + #: When running in subprocess mode, specify plugins by name (str) - adding + #: plugin objects directly is not supported. + self.plugins: list[str | _PluggyPlugin] = [] + self._sys_path_snapshot = SysPathsSnapshot() + self._sys_modules_snapshot = self.__take_sys_modules_snapshot() + self._request.addfinalizer(self._finalize) + self._method = self._request.config.getoption("--runpytest") + self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) + + self._monkeypatch = mp = monkeypatch + self.chdir() + mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot)) + # Ensure no unexpected caching via tox. + mp.delenv("TOX_ENV_DIR", raising=False) + # Discard outer pytest options. + mp.delenv("PYTEST_ADDOPTS", raising=False) + # Ensure no user config is used. + tmphome = str(self.path) + mp.setenv("HOME", tmphome) + mp.setenv("USERPROFILE", tmphome) + # Do not use colors for inner runs by default. + mp.setenv("PY_COLORS", "0") + + @property + def path(self) -> Path: + """Temporary directory path used to create files/run tests from, etc.""" + return self._path + + def __repr__(self) -> str: + return f"" + + def _finalize(self) -> None: + """ + Clean up global state artifacts. + + Some methods modify the global interpreter state and this tries to + clean this up. It does not remove the temporary directory however so + it can be looked at after the test run has finished. + """ + self._sys_modules_snapshot.restore() + self._sys_path_snapshot.restore() + + def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: + # Some zope modules used by twisted-related tests keep internal state + # and can't be deleted; we had some trouble in the past with + # `zope.interface` for example. + # + # Preserve readline due to https://bugs.python.org/issue41033. + # pexpect issues a SIGWINCH. + def preserve_module(name): + return name.startswith(("zope", "readline")) + + return SysModulesSnapshot(preserve=preserve_module) + + def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: + """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`.""" + pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined] + self._request.addfinalizer(reprec.finish_recording) + return reprec + + def chdir(self) -> None: + """Cd into the temporary directory. + + This is done automatically upon instantiation. + """ + self._monkeypatch.chdir(self.path) + + def _makefile( + self, + ext: str, + lines: Sequence[Any | bytes], + files: Mapping[str, _FileContent], + encoding: str = "utf-8", + ) -> Path: + items = list(files.items()) + + if ext is None: + raise TypeError("ext must not be None") + + if ext and not ext.startswith("."): + raise ValueError( + f"pytester.makefile expects a file extension, try .{ext} instead of {ext}" + ) + + def to_text(s: Any | bytes) -> str: + return s.decode(encoding) if isinstance(s, bytes) else str(s) + + if lines: + source = "\n".join(to_text(x) for x in lines) + basename = self._name + items.insert(0, (basename, source)) + + ret = None + for basename, value in items: + p = self.path.joinpath(basename).with_suffix(ext) + p.parent.mkdir(parents=True, exist_ok=True) + source_ = Source(value) + source = "\n".join(to_text(line) for line in source_.lines) + p.write_text(source.strip(), encoding=encoding) + if ret is None: + ret = p + assert ret is not None + return ret + + def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: + r"""Create new text file(s) in the test directory. + + :param ext: + The extension the file(s) should use, including the dot, e.g. `.py`. + :param args: + All args are treated as strings and joined using newlines. + The result is written as contents to the file. The name of the + file is based on the test function requesting this fixture. + :param kwargs: + Each keyword is the name of a file, while the value of it will + be written as contents of the file. + :returns: + The first created file. + + Examples: + + .. code-block:: python + + pytester.makefile(".txt", "line1", "line2") + + pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") + + To create binary files, use :meth:`pathlib.Path.write_bytes` directly: + + .. code-block:: python + + filename = pytester.path.joinpath("foo.bin") + filename.write_bytes(b"...") + """ + return self._makefile(ext, args, kwargs) + + def makeconftest(self, source: str) -> Path: + """Write a conftest.py file. + + :param source: The contents. + :returns: The conftest.py file. + """ + return self.makepyfile(conftest=source) + + def makeini(self, source: str) -> Path: + """Write a tox.ini file. + + :param source: The contents. + :returns: The tox.ini file. + """ + return self.makefile(".ini", tox=source) + + def maketoml(self, source: str) -> Path: + """Write a pytest.toml file. + + :param source: The contents. + :returns: The pytest.toml file. + + .. versionadded:: 9.0 + """ + return self.makefile(".toml", pytest=source) + + def getinicfg(self, source: str) -> SectionWrapper: + """Return the pytest section from the tox.ini config file.""" + p = self.makeini(source) + return IniConfig(str(p))["pytest"] + + def makepyprojecttoml(self, source: str) -> Path: + """Write a pyproject.toml file. + + :param source: The contents. + :returns: The pyproject.ini file. + + .. versionadded:: 6.0 + """ + return self.makefile(".toml", pyproject=source) + + def makepyfile(self, *args: _FileContent, **kwargs: _FileContent) -> Path: + r"""Shortcut for .makefile() with a .py extension. + + Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting + existing files. + + Examples: + + .. code-block:: python + + def test_something(pytester): + # Initial file is created test_something.py. + pytester.makepyfile("foobar") + # To create multiple files, pass kwargs accordingly. + pytester.makepyfile(custom="foobar") + # At this point, both 'test_something.py' & 'custom.py' exist in the test directory. + + """ + return self._makefile(".py", args, kwargs) + + def maketxtfile(self, *args: _FileContent, **kwargs: _FileContent) -> Path: + r"""Shortcut for .makefile() with a .txt extension. + + Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting + existing files. + + Examples: + + .. code-block:: python + + def test_something(pytester): + # Initial file is created test_something.txt. + pytester.maketxtfile("foobar") + # To create multiple files, pass kwargs accordingly. + pytester.maketxtfile(custom="foobar") + # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory. + + """ + return self._makefile(".txt", args, kwargs) + + def syspathinsert(self, path: str | os.PathLike[str] | None = None) -> None: + """Prepend a directory to sys.path, defaults to :attr:`path`. + + This is undone automatically when this object dies at the end of each + test. + + :param path: + The path. + """ + if path is None: + path = self.path + + self._monkeypatch.syspath_prepend(str(path)) + + def mkdir(self, name: str | os.PathLike[str]) -> Path: + """Create a new (sub)directory. + + :param name: + The name of the directory, relative to the pytester path. + :returns: + The created directory. + :rtype: pathlib.Path + """ + p = self.path / name + p.mkdir() + return p + + def mkpydir(self, name: str | os.PathLike[str]) -> Path: + """Create a new python package. + + This creates a (sub)directory with an empty ``__init__.py`` file so it + gets recognised as a Python package. + """ + p = self.path / name + p.mkdir() + p.joinpath("__init__.py").touch() + return p + + def copy_example(self, name: str | None = None) -> Path: + """Copy file from project's directory into the testdir. + + :param name: + The name of the file to copy. + :return: + Path to the copied directory (inside ``self.path``). + :rtype: pathlib.Path + """ + example_dir_ = self._request.config.getini("pytester_example_dir") + if example_dir_ is None: + raise ValueError("pytester_example_dir is unset, can't copy examples") + example_dir: Path = self._request.config.rootpath / example_dir_ + + for extra_element in self._request.node.iter_markers("pytester_example_path"): + assert extra_element.args + example_dir = example_dir.joinpath(*extra_element.args) + + if name is None: + func_name = self._name + maybe_dir = example_dir / func_name + maybe_file = example_dir / (func_name + ".py") + + if maybe_dir.is_dir(): + example_path = maybe_dir + elif maybe_file.is_file(): + example_path = maybe_file + else: + raise LookupError( + f"{func_name} can't be found as module or package in {example_dir}" + ) + else: + example_path = example_dir.joinpath(name) + + if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): + shutil.copytree(example_path, self.path, symlinks=True, dirs_exist_ok=True) + return self.path + elif example_path.is_file(): + result = self.path.joinpath(example_path.name) + shutil.copy(example_path, result) + return result + else: + raise LookupError( + f'example "{example_path}" is not found as a file or directory' + ) + + def getnode(self, config: Config, arg: str | os.PathLike[str]) -> Collector | Item: + """Get the collection node of a file. + + :param config: + A pytest config. + See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it. + :param arg: + Path to the file. + :returns: + The node. + """ + session = Session.from_config(config) + assert "::" not in str(arg) + p = Path(os.path.abspath(arg)) + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([str(p)], genitems=False)[0] + config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) + return res + + def getpathnode(self, path: str | os.PathLike[str]) -> Collector | Item: + """Return the collection node of a file. + + This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to + create the (configured) pytest Config instance. + + :param path: + Path to the file. + :returns: + The node. + """ + path = Path(path) + config = self.parseconfigure(path) + session = Session.from_config(config) + x = bestrelpath(session.path, path) + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) + return res + + def genitems(self, colitems: Sequence[Item | Collector]) -> list[Item]: + """Generate all test items from a collection node. + + This recurses into the collection node and returns a list of all the + test items contained within. + + :param colitems: + The collection nodes. + :returns: + The collected items. + """ + session = colitems[0].session + result: list[Item] = [] + for colitem in colitems: + result.extend(session.genitems(colitem)) + return result + + def runitem(self, source: str) -> Any: + """Run the "test_func" Item. + + The calling test instance (class containing the test method) must + provide a ``.getrunner()`` method which should return a runner which + can run the test protocol for a single item, e.g. + ``_pytest.runner.runtestprotocol``. + """ + # used from runner functional tests + item = self.getitem(source) + # the test class where we are called from wants to provide the runner + testclassinstance = self._request.instance + runner = testclassinstance.getrunner() + return runner(item) + + def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder: + """Run a test module in process using ``pytest.main()``. + + This run writes "source" into a temporary file and runs + ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance + for the result. + + :param source: The source code of the test module. + :param cmdlineargs: Any extra command line arguments to use. + """ + p = self.makepyfile(source) + values = [*list(cmdlineargs), p] + return self.inline_run(*values) + + def inline_genitems(self, *args) -> tuple[list[Item], HookRecorder]: + """Run ``pytest.main(['--collect-only'])`` in-process. + + Runs the :py:func:`pytest.main` function to run all of pytest inside + the test process itself like :py:meth:`inline_run`, but returns a + tuple of the collected items and a :py:class:`HookRecorder` instance. + """ + rec = self.inline_run("--collect-only", *args) + items = [x.item for x in rec.getcalls("pytest_itemcollected")] + return items, rec + + def inline_run( + self, + *args: str | os.PathLike[str], + plugins=(), + no_reraise_ctrlc: bool = False, + ) -> HookRecorder: + """Run ``pytest.main()`` in-process, returning a HookRecorder. + + Runs the :py:func:`pytest.main` function to run all of pytest inside + the test process itself. This means it can return a + :py:class:`HookRecorder` instance which gives more detailed results + from that run than can be done by matching stdout/stderr from + :py:meth:`runpytest`. + + :param args: + Command line arguments to pass to :py:func:`pytest.main`. + :param plugins: + Extra plugin instances the ``pytest.main()`` instance should use. + :param no_reraise_ctrlc: + Typically we reraise keyboard interrupts from the child run. If + True, the KeyboardInterrupt exception is captured. + """ + from _pytest.unraisableexception import gc_collect_iterations_key + + # (maybe a cpython bug?) the importlib cache sometimes isn't updated + # properly between file creation and inline_run (especially if imports + # are interspersed with file creation) + importlib.invalidate_caches() + + plugins = list(plugins) + finalizers = [] + try: + # Any sys.module or sys.path changes done while running pytest + # inline should be reverted after the test run completes to avoid + # clashing with later inline tests run within the same pytest test, + # e.g. just because they use matching test module names. + finalizers.append(self.__take_sys_modules_snapshot().restore) + finalizers.append(SysPathsSnapshot().restore) + + # Important note: + # - our tests should not leave any other references/registrations + # laying around other than possibly loaded test modules + # referenced from sys.modules, as nothing will clean those up + # automatically + + rec = [] + + class PytesterHelperPlugin: + @staticmethod + def pytest_configure(config: Config) -> None: + rec.append(self.make_hook_recorder(config.pluginmanager)) + + # The unraisable plugin GC collect slows down inline + # pytester runs too much. + config.stash[gc_collect_iterations_key] = 0 + + plugins.append(PytesterHelperPlugin()) + ret = main([str(x) for x in args], plugins=plugins) + if len(rec) == 1: + reprec = rec.pop() + else: + + class reprec: # type: ignore + pass + + reprec.ret = ret + + # Typically we reraise keyboard interrupts from the child run + # because it's our user requesting interruption of the testing. + if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc: + calls = reprec.getcalls("pytest_keyboard_interrupt") + if calls and calls[-1].excinfo.type == KeyboardInterrupt: + raise KeyboardInterrupt() + return reprec + finally: + for finalizer in finalizers: + finalizer() + + def runpytest_inprocess( + self, *args: str | os.PathLike[str], **kwargs: Any + ) -> RunResult: + """Return result of running pytest in-process, providing a similar + interface to what self.runpytest() provides.""" + syspathinsert = kwargs.pop("syspathinsert", False) + + if syspathinsert: + self.syspathinsert() + instant = timing.Instant() + capture = _get_multicapture("sys") + capture.start_capturing() + try: + try: + reprec = self.inline_run(*args, **kwargs) + except SystemExit as e: + ret = e.args[0] + try: + ret = ExitCode(e.args[0]) + except ValueError: + pass + + class reprec: # type: ignore + ret = ret + + except Exception: + traceback.print_exc() + + class reprec: # type: ignore + ret = ExitCode(3) + + finally: + out, err = capture.readouterr() + capture.stop_capturing() + sys.stdout.write(out) + sys.stderr.write(err) + + assert reprec.ret is not None + res = RunResult( + reprec.ret, out.splitlines(), err.splitlines(), instant.elapsed().seconds + ) + res.reprec = reprec # type: ignore + return res + + def runpytest(self, *args: str | os.PathLike[str], **kwargs: Any) -> RunResult: + """Run pytest inline or in a subprocess, depending on the command line + option "--runpytest" and return a :py:class:`~pytest.RunResult`.""" + new_args = self._ensure_basetemp(args) + if self._method == "inprocess": + return self.runpytest_inprocess(*new_args, **kwargs) + elif self._method == "subprocess": + return self.runpytest_subprocess(*new_args, **kwargs) + raise RuntimeError(f"Unrecognized runpytest option: {self._method}") + + def _ensure_basetemp( + self, args: Sequence[str | os.PathLike[str]] + ) -> list[str | os.PathLike[str]]: + new_args = list(args) + for x in new_args: + if str(x).startswith("--basetemp"): + break + else: + new_args.append( + "--basetemp={}".format(self.path.parent.joinpath("basetemp")) + ) + return new_args + + def parseconfig(self, *args: str | os.PathLike[str]) -> Config: + """Return a new pytest :class:`pytest.Config` instance from given + commandline args. + + This invokes the pytest bootstrapping code in _pytest.config to create a + new :py:class:`pytest.PytestPluginManager` and call the + :hook:`pytest_cmdline_parse` hook to create a new :class:`pytest.Config` + instance. + + If :attr:`plugins` has been populated they should be plugin modules + to be registered with the plugin manager. + """ + import _pytest.config + + new_args = [str(x) for x in self._ensure_basetemp(args)] + + config = _pytest.config._prepareconfig(new_args, self.plugins) + # we don't know what the test will do with this half-setup config + # object and thus we make sure it gets unconfigured properly in any + # case (otherwise capturing could still be active, for example) + self._request.addfinalizer(config._ensure_unconfigure) + return config + + def parseconfigure(self, *args: str | os.PathLike[str]) -> Config: + """Return a new pytest configured Config instance. + + Returns a new :py:class:`pytest.Config` instance like + :py:meth:`parseconfig`, but also calls the :hook:`pytest_configure` + hook. + """ + config = self.parseconfig(*args) + config._do_configure() + return config + + def getitem( + self, source: str | os.PathLike[str], funcname: str = "test_func" + ) -> Item: + """Return the test item for a test function. + + Writes the source to a python file and runs pytest's collection on + the resulting module, returning the test item for the requested + function name. + + :param source: + The module source. + :param funcname: + The name of the test function for which to return a test item. + :returns: + The test item. + """ + items = self.getitems(source) + for item in items: + if item.name == funcname: + return item + assert 0, f"{funcname!r} item not found in module:\n{source}\nitems: {items}" + + def getitems(self, source: str | os.PathLike[str]) -> list[Item]: + """Return all test items collected from the module. + + Writes the source to a Python file and runs pytest's collection on + the resulting module, returning all test items contained within. + """ + modcol = self.getmodulecol(source) + return self.genitems([modcol]) + + def getmodulecol( + self, + source: str | os.PathLike[str], + configargs=(), + *, + withinit: bool = False, + ): + """Return the module collection node for ``source``. + + Writes ``source`` to a file using :py:meth:`makepyfile` and then + runs the pytest collection on it, returning the collection node for the + test module. + + :param source: + The source code of the module to collect. + + :param configargs: + Any extra arguments to pass to :py:meth:`parseconfigure`. + + :param withinit: + Whether to also write an ``__init__.py`` file to the same + directory to ensure it is a package. + """ + if isinstance(source, os.PathLike): + path = self.path.joinpath(source) + assert not withinit, "not supported for paths" + else: + kw = {self._name: str(source)} + path = self.makepyfile(**kw) + if withinit: + self.makepyfile(__init__="#") + self.config = config = self.parseconfigure(path, *configargs) + return self.getnode(config, path) + + def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None: + """Return the collection node for name from the module collection. + + Searches a module collection node for a collection node matching the + given name. + + :param modcol: A module collection node; see :py:meth:`getmodulecol`. + :param name: The name of the node to return. + """ + if modcol not in self._mod_collections: + self._mod_collections[modcol] = list(modcol.collect()) + for colitem in self._mod_collections[modcol]: + if colitem.name == name: + return colitem + return None + + def popen( + self, + cmdargs: Sequence[str | os.PathLike[str]], + stdout: int | TextIO = subprocess.PIPE, + stderr: int | TextIO = subprocess.PIPE, + stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN, + **kw, + ): + """Invoke :py:class:`subprocess.Popen`. + + Calls :py:class:`subprocess.Popen` making sure the current working + directory is in ``PYTHONPATH``. + + You probably want to use :py:meth:`run` instead. + """ + env = os.environ.copy() + env["PYTHONPATH"] = os.pathsep.join( + filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) + ) + kw["env"] = env + + if stdin is self.CLOSE_STDIN: + kw["stdin"] = subprocess.PIPE + elif isinstance(stdin, bytes): + kw["stdin"] = subprocess.PIPE + else: + kw["stdin"] = stdin + + popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + if stdin is self.CLOSE_STDIN: + assert popen.stdin is not None + popen.stdin.close() + elif isinstance(stdin, bytes): + assert popen.stdin is not None + popen.stdin.write(stdin) + + return popen + + def run( + self, + *cmdargs: str | os.PathLike[str], + timeout: float | None = None, + stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN, + ) -> RunResult: + """Run a command with arguments. + + Run a process using :py:class:`subprocess.Popen` saving the stdout and + stderr. + + :param cmdargs: + The sequence of arguments to pass to :py:class:`subprocess.Popen`, + with path-like objects being converted to :py:class:`str` + automatically. + :param timeout: + The period in seconds after which to timeout and raise + :py:class:`Pytester.TimeoutExpired`. + :param stdin: + Optional standard input. + + - If it is ``CLOSE_STDIN`` (Default), then this method calls + :py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and + the standard input is closed immediately after the new command is + started. + + - If it is of type :py:class:`bytes`, these bytes are sent to the + standard input of the command. + + - Otherwise, it is passed through to :py:class:`subprocess.Popen`. + For further information in this case, consult the document of the + ``stdin`` parameter in :py:class:`subprocess.Popen`. + :type stdin: _pytest.compat.NotSetType | bytes | IO[Any] | int + :returns: + The result. + + """ + __tracebackhide__ = True + + cmdargs = tuple(os.fspath(arg) for arg in cmdargs) + p1 = self.path.joinpath("stdout") + p2 = self.path.joinpath("stderr") + print("running:", *cmdargs) + print(" in:", Path.cwd()) + + with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2: + instant = timing.Instant() + popen = self.popen( + cmdargs, + stdin=stdin, + stdout=f1, + stderr=f2, + ) + if popen.stdin is not None: + popen.stdin.close() + + def handle_timeout() -> None: + __tracebackhide__ = True + + timeout_message = f"{timeout} second timeout expired running: {cmdargs}" + + popen.kill() + popen.wait() + raise self.TimeoutExpired(timeout_message) + + if timeout is None: + ret = popen.wait() + else: + try: + ret = popen.wait(timeout) + except subprocess.TimeoutExpired: + handle_timeout() + f1.flush() + f2.flush() + + with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2: + out = f1.read().splitlines() + err = f2.read().splitlines() + + self._dump_lines(out, sys.stdout) + self._dump_lines(err, sys.stderr) + + with contextlib.suppress(ValueError): + ret = ExitCode(ret) + return RunResult(ret, out, err, instant.elapsed().seconds) + + def _dump_lines(self, lines, fp): + try: + for line in lines: + print(line, file=fp) + except UnicodeEncodeError: + print(f"couldn't print to {fp} because of encoding") + + def _getpytestargs(self) -> tuple[str, ...]: + return sys.executable, "-mpytest" + + def runpython(self, script: os.PathLike[str]) -> RunResult: + """Run a python script using sys.executable as interpreter.""" + return self.run(sys.executable, script) + + def runpython_c(self, command: str) -> RunResult: + """Run ``python -c "command"``.""" + return self.run(sys.executable, "-c", command) + + def runpytest_subprocess( + self, *args: str | os.PathLike[str], timeout: float | None = None + ) -> RunResult: + """Run pytest as a subprocess with given arguments. + + Any plugins added to the :py:attr:`plugins` list will be added using the + ``-p`` command line option. Additionally ``--basetemp`` is used to put + any temporary files and directories in a numbered directory prefixed + with "runpytest-" to not conflict with the normal numbered pytest + location for temporary files and directories. + + :param args: + The sequence of arguments to pass to the pytest subprocess. + :param timeout: + The period in seconds after which to timeout and raise + :py:class:`Pytester.TimeoutExpired`. + :returns: + The result. + """ + __tracebackhide__ = True + p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) + args = (f"--basetemp={p}", *args) + for plugin in self.plugins: + if not isinstance(plugin, str): + raise ValueError( + f"Specifying plugins as objects is not supported in pytester subprocess mode; " + f"specify by name instead: {plugin}" + ) + args = ("-p", plugin, *args) + args = self._getpytestargs() + args + return self.run(*args, timeout=timeout) + + def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn: + """Run pytest using pexpect. + + This makes sure to use the right pytest and sets up the temporary + directory locations. + + The pexpect child is returned. + """ + basetemp = self.path / "temp-pexpect" + basetemp.mkdir(mode=0o700) + invoke = " ".join(map(str, self._getpytestargs())) + cmd = f"{invoke} --basetemp={basetemp} {string}" + return self.spawn(cmd, expect_timeout=expect_timeout) + + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn: + """Run a command using pexpect. + + The pexpect child is returned. + """ + pexpect = importorskip("pexpect", "3.0") + if hasattr(sys, "pypy_version_info") and "64" in platform.machine(): + skip("pypy-64 bit not supported") + if not hasattr(pexpect, "spawn"): + skip("pexpect.spawn not available") + logfile = self.path.joinpath("spawn.out").open("wb") + + child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout) + self._request.addfinalizer(logfile.close) + return child + + +class LineComp: + def __init__(self) -> None: + self.stringio = StringIO() + """:class:`python:io.StringIO()` instance used for input.""" + + def assert_contains_lines(self, lines2: Sequence[str]) -> None: + """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value. + + Lines are matched using :func:`LineMatcher.fnmatch_lines `. + """ + __tracebackhide__ = True + val = self.stringio.getvalue() + self.stringio.truncate(0) + self.stringio.seek(0) + lines1 = val.split("\n") + LineMatcher(lines1).fnmatch_lines(lines2) + + +class LineMatcher: + """Flexible matching of text. + + This is a convenience class to test large texts like the output of + commands. + + The constructor takes a list of lines without their trailing newlines, i.e. + ``text.splitlines()``. + """ + + def __init__(self, lines: list[str]) -> None: + self.lines = lines + self._log_output: list[str] = [] + + def __str__(self) -> str: + """Return the entire original text. + + .. versionadded:: 6.2 + You can use :meth:`str` in older versions. + """ + return "\n".join(self.lines) + + def _getlines(self, lines2: str | Sequence[str] | Source) -> Sequence[str]: + if isinstance(lines2, str): + lines2 = Source(lines2) + if isinstance(lines2, Source): + lines2 = lines2.strip().lines + return lines2 + + def fnmatch_lines_random(self, lines2: Sequence[str]) -> None: + """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`).""" + __tracebackhide__ = True + self._match_lines_random(lines2, fnmatch) + + def re_match_lines_random(self, lines2: Sequence[str]) -> None: + """Check lines exist in the output in any order (using :func:`python:re.match`).""" + __tracebackhide__ = True + self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name))) + + def _match_lines_random( + self, lines2: Sequence[str], match_func: Callable[[str, str], bool] + ) -> None: + __tracebackhide__ = True + lines2 = self._getlines(lines2) + for line in lines2: + for x in self.lines: + if line == x or match_func(x, line): + self._log("matched: ", repr(line)) + break + else: + msg = f"line {line!r} not found in output" + self._log(msg) + self._fail(msg) + + def get_lines_after(self, fnline: str) -> Sequence[str]: + """Return all lines following the given line in the text. + + The given line can contain glob wildcards. + """ + for i, line in enumerate(self.lines): + if fnline == line or fnmatch(line, fnline): + return self.lines[i + 1 :] + raise ValueError(f"line {fnline!r} not found in output") + + def _log(self, *args) -> None: + self._log_output.append(" ".join(str(x) for x in args)) + + @property + def _log_text(self) -> str: + return "\n".join(self._log_output) + + def fnmatch_lines( + self, lines2: Sequence[str], *, consecutive: bool = False + ) -> None: + """Check lines exist in the output (using :func:`python:fnmatch.fnmatch`). + + The argument is a list of lines which have to match and can use glob + wildcards. If they do not match a pytest.fail() is called. The + matches and non-matches are also shown as part of the error message. + + :param lines2: String patterns to match. + :param consecutive: Match lines consecutively? + """ + __tracebackhide__ = True + self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive) + + def re_match_lines( + self, lines2: Sequence[str], *, consecutive: bool = False + ) -> None: + """Check lines exist in the output (using :func:`python:re.match`). + + The argument is a list of lines which have to match using ``re.match``. + If they do not match a pytest.fail() is called. + + The matches and non-matches are also shown as part of the error message. + + :param lines2: string patterns to match. + :param consecutive: match lines consecutively? + """ + __tracebackhide__ = True + self._match_lines( + lines2, + lambda name, pat: bool(re.match(pat, name)), + "re.match", + consecutive=consecutive, + ) + + def _match_lines( + self, + lines2: Sequence[str], + match_func: Callable[[str, str], bool], + match_nickname: str, + *, + consecutive: bool = False, + ) -> None: + """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. + + :param Sequence[str] lines2: + List of string patterns to match. The actual format depends on + ``match_func``. + :param match_func: + A callable ``match_func(line, pattern)`` where line is the + captured line from stdout/stderr and pattern is the matching + pattern. + :param str match_nickname: + The nickname for the match function that will be logged to stdout + when a match occurs. + :param consecutive: + Match lines consecutively? + """ + if not isinstance(lines2, collections.abc.Sequence): + raise TypeError(f"invalid type for lines2: {type(lines2).__name__}") + lines2 = self._getlines(lines2) + lines1 = self.lines[:] + extralines = [] + __tracebackhide__ = True + wnick = len(match_nickname) + 1 + started = False + for line in lines2: + nomatchprinted = False + while lines1: + nextline = lines1.pop(0) + if line == nextline: + self._log("exact match:", repr(line)) + started = True + break + elif match_func(nextline, line): + self._log(f"{match_nickname}:", repr(line)) + self._log( + "{:>{width}}".format("with:", width=wnick), repr(nextline) + ) + started = True + break + else: + if consecutive and started: + msg = f"no consecutive match: {line!r}" + self._log(msg) + self._log( + "{:>{width}}".format("with:", width=wnick), repr(nextline) + ) + self._fail(msg) + if not nomatchprinted: + self._log( + "{:>{width}}".format("nomatch:", width=wnick), repr(line) + ) + nomatchprinted = True + self._log("{:>{width}}".format("and:", width=wnick), repr(nextline)) + extralines.append(nextline) + else: + msg = f"remains unmatched: {line!r}" + self._log(msg) + self._fail(msg) + self._log_output = [] + + def no_fnmatch_line(self, pat: str) -> None: + """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. + + :param str pat: The pattern to match lines. + """ + __tracebackhide__ = True + self._no_match_line(pat, fnmatch, "fnmatch") + + def no_re_match_line(self, pat: str) -> None: + """Ensure captured lines do not match the given pattern, using ``re.match``. + + :param str pat: The regular expression to match lines. + """ + __tracebackhide__ = True + self._no_match_line( + pat, lambda name, pat: bool(re.match(pat, name)), "re.match" + ) + + def _no_match_line( + self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str + ) -> None: + """Underlying implementation of ``no_fnmatch_line`` and ``no_re_match_line``. + + :param str pat: + The pattern to match lines. + :param match_func: + A callable ``match_func(line, pattern)`` where line is the + captured line from stdout/stderr and pattern is the matching + pattern. + :param match_nickname: + The nickname for the match function that will be logged to stdout + when a match occurs. + """ + __tracebackhide__ = True + nomatch_printed = False + wnick = len(match_nickname) + 1 + for line in self.lines: + if match_func(line, pat): + msg = f"{match_nickname}: {pat!r}" + self._log(msg) + self._log("{:>{width}}".format("with:", width=wnick), repr(line)) + self._fail(msg) + else: + if not nomatch_printed: + self._log("{:>{width}}".format("nomatch:", width=wnick), repr(pat)) + nomatch_printed = True + self._log("{:>{width}}".format("and:", width=wnick), repr(line)) + self._log_output = [] + + def _fail(self, msg: str) -> None: + __tracebackhide__ = True + log_text = self._log_text + self._log_output = [] + fail(log_text) + + def str(self) -> str: + """Return the entire original text.""" + return str(self) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/pytester_assertions.py b/.venv-test/lib/python3.12/site-packages/_pytest/pytester_assertions.py new file mode 100644 index 0000000..b8d8a19 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/pytester_assertions.py @@ -0,0 +1,74 @@ +"""Helper plugin for pytester; should not be loaded on its own.""" + +# This plugin contains assertions used by pytester. pytester cannot +# contain them itself, since it is imported by the `pytest` module, +# hence cannot be subject to assertion rewriting, which requires a +# module to not be already imported. +from __future__ import annotations + +from collections.abc import Sequence + +from _pytest.reports import CollectReport +from _pytest.reports import TestReport + + +def assertoutcome( + outcomes: tuple[ + Sequence[TestReport], + Sequence[CollectReport | TestReport], + Sequence[CollectReport | TestReport], + ], + passed: int = 0, + skipped: int = 0, + failed: int = 0, +) -> None: + __tracebackhide__ = True + + realpassed, realskipped, realfailed = outcomes + obtained = { + "failed": len(realfailed), + "passed": len(realpassed), + "skipped": len(realskipped), + } + expected = {"failed": failed, "passed": passed, "skipped": skipped} + assert obtained == expected, outcomes + + +def assert_outcomes( + outcomes: dict[str, int], + passed: int = 0, + skipped: int = 0, + failed: int = 0, + errors: int = 0, + xpassed: int = 0, + xfailed: int = 0, + warnings: int | None = None, + deselected: int | None = None, +) -> None: + """Assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run.""" + __tracebackhide__ = True + + obtained = { + "passed": outcomes.get("passed", 0), + "skipped": outcomes.get("skipped", 0), + "failed": outcomes.get("failed", 0), + "errors": outcomes.get("errors", 0), + "xpassed": outcomes.get("xpassed", 0), + "xfailed": outcomes.get("xfailed", 0), + } + expected = { + "passed": passed, + "skipped": skipped, + "failed": failed, + "errors": errors, + "xpassed": xpassed, + "xfailed": xfailed, + } + if warnings is not None: + obtained["warnings"] = outcomes.get("warnings", 0) + expected["warnings"] = warnings + if deselected is not None: + obtained["deselected"] = outcomes.get("deselected", 0) + expected["deselected"] = deselected + assert obtained == expected diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/python.py b/.venv-test/lib/python3.12/site-packages/_pytest/python.py new file mode 100644 index 0000000..45ff185 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/python.py @@ -0,0 +1,1759 @@ +# mypy: allow-untyped-defs +"""Python test discovery, setup and run of test functions.""" + +from __future__ import annotations + +import abc +from collections import Counter +from collections import defaultdict +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +import enum +import fnmatch +from functools import partial +import inspect +import itertools +import os +from pathlib import Path +import re +import textwrap +import types +from typing import Any +from typing import cast +from typing import final +from typing import Literal +from typing import NoReturn +from typing import TYPE_CHECKING +import warnings + +import _pytest +from _pytest import fixtures +from _pytest import nodes +from _pytest._code import filter_traceback +from _pytest._code import getfslineno +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import TerminalRepr +from _pytest._code.code import Traceback +from _pytest._io.saferepr import saferepr +from _pytest.compat import ascii_escaped +from _pytest.compat import get_default_arg_names +from _pytest.compat import get_real_func +from _pytest.compat import getimfunc +from _pytest.compat import is_async_function +from _pytest.compat import NOTSET +from _pytest.compat import safe_getattr +from _pytest.compat import safe_isclass +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import _resolve_args_directness +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import FixtureValue +from _pytest.fixtures import FuncFixtureInfo +from _pytest.fixtures import get_scope_node +from _pytest.main import Session +from _pytest.mark import ParameterSet +from _pytest.mark.structures import _HiddenParam +from _pytest.mark.structures import get_unpacked_marks +from _pytest.mark.structures import HIDDEN_PARAM +from _pytest.mark.structures import Mark +from _pytest.mark.structures import MarkDecorator +from _pytest.mark.structures import normalize_mark_list +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import import_path +from _pytest.pathlib import ImportPathMismatchError +from _pytest.pathlib import scandir +from _pytest.scope import Scope +from _pytest.scope import ScopeName +from _pytest.stash import StashKey +from _pytest.warning_types import PytestCollectionWarning +from _pytest.warning_types import PytestReturnNotNoneWarning + + +if TYPE_CHECKING: + from typing_extensions import Self + + +def pytest_addoption(parser: Parser) -> None: + parser.addini( + "python_files", + type="args", + # NOTE: default is also used in AssertionRewritingHook. + default=["test_*.py", "*_test.py"], + help="Glob-style file patterns for Python test module discovery", + ) + parser.addini( + "python_classes", + type="args", + default=["Test"], + help="Prefixes or glob names for Python test class discovery", + ) + parser.addini( + "python_functions", + type="args", + default=["test"], + help="Prefixes or glob names for Python test function and method discovery", + ) + parser.addini( + "disable_test_id_escaping_and_forfeit_all_rights_to_community_support", + type="bool", + default=False, + help="Disable string escape non-ASCII characters, might cause unwanted " + "side effects(use at your own risk)", + ) + parser.addini( + "strict_parametrization_ids", + type="bool", + # None => fallback to `strict`. + default=None, + help="Emit an error if non-unique parameter set IDs are detected", + ) + + +def pytest_generate_tests(metafunc: Metafunc) -> None: + for marker in metafunc.definition.iter_markers(name="parametrize"): + metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) + + +def pytest_configure(config: Config) -> None: + config.addinivalue_line( + "markers", + "parametrize(argnames, argvalues): call a test function multiple " + "times passing in different arguments in turn. argvalues generally " + "needs to be a list of values if argnames specifies only one name " + "or a list of tuples of values if argnames specifies multiple names. " + "Example: @parametrize('arg1', [1,2]) would lead to two calls of the " + "decorated test function, one with arg1=1 and another with arg1=2." + "see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info " + "and examples.", + ) + config.addinivalue_line( + "markers", + "usefixtures(fixturename1, fixturename2, ...): mark tests as needing " + "all of the specified fixtures. see " + "https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures ", + ) + + +def async_fail(nodeid: str) -> None: + msg = ( + "async def functions are not natively supported.\n" + "You need to install a suitable plugin for your async framework, for example:\n" + " - anyio\n" + " - pytest-asyncio\n" + " - pytest-tornasync\n" + " - pytest-trio\n" + " - pytest-twisted" + ) + fail(msg, pytrace=False) + + +@hookimpl(trylast=True) +def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: + testfunction = pyfuncitem.obj + if is_async_function(testfunction): + async_fail(pyfuncitem.nodeid) + funcargs = pyfuncitem.funcargs + testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} + result = testfunction(**testargs) + if hasattr(result, "__await__") or hasattr(result, "__aiter__"): + async_fail(pyfuncitem.nodeid) + elif result is not None: + warnings.warn( + PytestReturnNotNoneWarning( + f"Test functions should return None, but {pyfuncitem.nodeid} returned {type(result)!r}.\n" + "Did you mean to use `assert` instead of `return`?\n" + "See https://docs.pytest.org/en/stable/how-to/assert.html#return-not-none for more information." + ) + ) + return True + + +def pytest_collect_directory( + path: Path, parent: nodes.Collector +) -> nodes.Collector | None: + pkginit = path / "__init__.py" + try: + has_pkginit = pkginit.is_file() + except PermissionError: + # See https://github.com/pytest-dev/pytest/issues/12120#issuecomment-2106349096. + return None + if has_pkginit: + return Package.from_parent(parent, path=path) + return None + + +def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Module | None: + if file_path.suffix == ".py": + if not parent.session.isinitpath(file_path): + if not path_matches_patterns( + file_path, parent.config.getini("python_files") + ): + return None + ihook = parent.session.gethookproxy(file_path) + module: Module = ihook.pytest_pycollect_makemodule( + module_path=file_path, parent=parent + ) + return module + return None + + +def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: + """Return whether path matches any of the patterns in the list of globs given.""" + return any(fnmatch_ex(pattern, path) for pattern in patterns) + + +def pytest_pycollect_makemodule(module_path: Path, parent) -> Module: + return Module.from_parent(parent, path=module_path) + + +@hookimpl(trylast=True) +def pytest_pycollect_makeitem( + collector: Module | Class, name: str, obj: object +) -> None | nodes.Item | nodes.Collector | list[nodes.Item | nodes.Collector]: + assert isinstance(collector, Class | Module), type(collector) + # Nothing was collected elsewhere, let's do it here. + if safe_isclass(obj): + if collector.istestclass(obj, name): + return Class.from_parent(collector, name=name, obj=obj) + elif collector.istestfunction(obj, name): + # mock seems to store unbound methods (issue473), normalize it. + obj = getattr(obj, "__func__", obj) + # We need to try and unwrap the function if it's a functools.partial + # or a functools.wrapped. + # We mustn't if it's been wrapped with mock.patch (python 2 only). + if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))): + filename, lineno = getfslineno(obj) + warnings.warn_explicit( + message=PytestCollectionWarning( + f"cannot collect {name!r} because it is not a function." + ), + category=None, + filename=str(filename), + lineno=lineno + 1, + ) + elif getattr(obj, "__test__", True): + if inspect.isgeneratorfunction(obj): + fail( + f"'yield' keyword is allowed in fixtures, but not in tests ({name})", + pytrace=False, + ) + return list(collector._genfunctions(name, obj)) + return None + return None + + +class PyobjMixin(nodes.Node): + """this mix-in inherits from Node to carry over the typing information + + as its intended to always mix in before a node + its position in the mro is unaffected""" + + _ALLOW_MARKERS = True + + @property + def module(self): + """Python module object this node was collected from (can be None).""" + node = self.getparent(Module) + return node.obj if node is not None else None + + @property + def cls(self): + """Python class object this node was collected from (can be None).""" + node = self.getparent(Class) + return node.obj if node is not None else None + + @property + def instance(self): + """Python instance object the function is bound to. + + Returns None if not a test method, e.g. for a standalone test function, + a class or a module. + """ + # Overridden by Function. + return None + + @property + def obj(self): + """Underlying Python object.""" + obj = getattr(self, "_obj", None) + if obj is None: + self._obj = obj = self._getobj() + # XXX evil hack + # used to avoid Function marker duplication + if self._ALLOW_MARKERS: + self.own_markers.extend(get_unpacked_marks(self.obj)) + # This assumes that `obj` is called before there is a chance + # to add custom keys to `self.keywords`, so no fear of overriding. + self.keywords.update((mark.name, mark) for mark in self.own_markers) + return obj + + @obj.setter + def obj(self, value): + self._obj = value + + def _getobj(self): + """Get the underlying Python object. May be overwritten by subclasses.""" + # TODO: Improve the type of `parent` such that assert/ignore aren't needed. + assert self.parent is not None + obj = self.parent.obj # type: ignore[attr-defined] + return getattr(obj, self.name) + + def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str: + """Return Python path relative to the containing module.""" + parts = [] + for node in self.iter_parents(): + name = node.name + if isinstance(node, Module): + name = os.path.splitext(name)[0] + if stopatmodule: + if includemodule: + parts.append(name) + break + parts.append(name) + parts.reverse() + return ".".join(parts) + + def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: + # XXX caching? + path, lineno = getfslineno(self.obj) + modpath = self.getmodpath() + return path, lineno, modpath + + +# As an optimization, these builtin attribute names are pre-ignored when +# iterating over an object during collection -- the pytest_pycollect_makeitem +# hook is not called for them. +# fmt: off +class _EmptyClass: pass # noqa: E701 +IGNORED_ATTRIBUTES = frozenset.union( + frozenset(), + # Module. + dir(types.ModuleType("empty_module")), + # Some extra module attributes the above doesn't catch. + {"__builtins__", "__file__", "__cached__"}, + # Class. + dir(_EmptyClass), + # Instance. + dir(_EmptyClass()), +) +del _EmptyClass +# fmt: on + + +class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): + def funcnamefilter(self, name: str) -> bool: + return self._matches_prefix_or_glob_option("python_functions", name) + + def isnosetest(self, obj: object) -> bool: + """Look for the __test__ attribute, which is applied by the + @nose.tools.istest decorator. + """ + # We explicitly check for "is True" here to not mistakenly treat + # classes with a custom __getattr__ returning something truthy (like a + # function) as test classes. + return safe_getattr(obj, "__test__", False) is True + + def classnamefilter(self, name: str) -> bool: + return self._matches_prefix_or_glob_option("python_classes", name) + + def istestfunction(self, obj: object, name: str) -> bool: + if self.funcnamefilter(name) or self.isnosetest(obj): + if isinstance(obj, staticmethod | classmethod): + # staticmethods and classmethods need to be unwrapped. + obj = safe_getattr(obj, "__func__", False) + return callable(obj) and fixtures.getfixturemarker(obj) is None + else: + return False + + def istestclass(self, obj: object, name: str) -> bool: + if not (self.classnamefilter(name) or self.isnosetest(obj)): + return False + if inspect.isabstract(obj): + return False + return True + + def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool: + """Check if the given name matches the prefix or glob-pattern defined + in configuration.""" + for option in self.config.getini(option_name): + if name.startswith(option): + return True + # Check that name looks like a glob-string before calling fnmatch + # because this is called for every name in each collected module, + # and fnmatch is somewhat expensive to call. + elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch( + name, option + ): + return True + return False + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + if not getattr(self.obj, "__test__", True): + return [] + + # Avoid random getattrs and peek in the __dict__ instead. + dicts = [getattr(self.obj, "__dict__", {})] + if isinstance(self.obj, type): + for basecls in self.obj.__mro__: + dicts.append(basecls.__dict__) + + # In each class, nodes should be definition ordered. + # __dict__ is definition ordered. + seen: set[str] = set() + dict_values: list[list[nodes.Item | nodes.Collector]] = [] + collect_imported_tests = self.session.config.getini("collect_imported_tests") + ihook = self.ihook + for dic in dicts: + values: list[nodes.Item | nodes.Collector] = [] + # Note: seems like the dict can change during iteration - + # be careful not to remove the list() without consideration. + for name, obj in list(dic.items()): + if name in IGNORED_ATTRIBUTES: + continue + if name in seen: + continue + seen.add(name) + + if not collect_imported_tests and isinstance(self, Module): + # Do not collect functions and classes from other modules. + if inspect.isfunction(obj) or inspect.isclass(obj): + if obj.__module__ != self._getobj().__name__: + continue + + res = ihook.pytest_pycollect_makeitem( + collector=self, name=name, obj=obj + ) + if res is None: + continue + elif isinstance(res, list): + values.extend(res) + else: + values.append(res) + dict_values.append(values) + + # Between classes in the class hierarchy, reverse-MRO order -- nodes + # inherited from base classes should come before subclasses. + result = [] + for values in reversed(dict_values): + result.extend(values) + return result + + def _genfunctions(self, name: str, funcobj) -> Iterator[Function]: + modulecol = self.getparent(Module) + assert modulecol is not None + module = modulecol.obj + clscol = self.getparent(Class) + cls = (clscol and clscol.obj) or None + + definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj) + fixtureinfo = definition._fixtureinfo + + # pytest_generate_tests impls call metafunc.parametrize() which fills + # metafunc._calls, the outcome of the hook. + metafunc = Metafunc( + definition=definition, + fixtureinfo=fixtureinfo, + config=self.config, + cls=cls, + module=module, + _ispytest=True, + ) + methods = [] + if hasattr(module, "pytest_generate_tests"): + methods.append(module.pytest_generate_tests) + if cls is not None and hasattr(cls, "pytest_generate_tests"): + methods.append(cls().pytest_generate_tests) + self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) + + if not metafunc._calls: + yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) + else: + metafunc._recompute_direct_params_indices() + # Direct parametrizations taking place in module/class-specific + # `metafunc.parametrize` calls may have shadowed some fixtures, so make sure + # we update what the function really needs a.k.a its fixture closure. Note that + # direct parametrizations using `@pytest.mark.parametrize` have already been considered + # into making the closure using `ignore_args` arg to `getfixtureclosure`. + fixtureinfo.prune_dependency_tree() + + for callspec in metafunc._calls: + subname = f"{name}[{callspec.id}]" if callspec._idlist else name + yield Function.from_parent( + self, + name=subname, + callspec=callspec, + fixtureinfo=fixtureinfo, + keywords={callspec.id: True}, + originalname=name, + ) + + +def importtestmodule( + path: Path, + config: Config, +): + # We assume we are only called once per module. + importmode = config.getoption("--import-mode") + try: + mod = import_path( + path, + mode=importmode, + root=config.rootpath, + consider_namespace_packages=config.getini("consider_namespace_packages"), + ) + except SyntaxError as e: + raise nodes.Collector.CollectError( + ExceptionInfo.from_current().getrepr(style="short") + ) from e + except ImportPathMismatchError as e: + raise nodes.Collector.CollectError( + "import file mismatch:\n" + "imported module {!r} has this __file__ attribute:\n" + " {}\n" + "which is not the same as the test file we want to collect:\n" + " {}\n" + "HINT: remove __pycache__ / .pyc files and/or use a " + "unique basename for your test file modules".format(*e.args) + ) from e + except ImportError as e: + exc_info = ExceptionInfo.from_current() + if config.get_verbosity() < 2: + exc_info.traceback = exc_info.traceback.filter(filter_traceback) + exc_repr = ( + exc_info.getrepr(style="short") + if exc_info.traceback + else exc_info.exconly() + ) + formatted_tb = str(exc_repr) + raise nodes.Collector.CollectError( + f"ImportError while importing test module '{path}'.\n" + "Hint: make sure your test modules/packages have valid Python names.\n" + "Traceback:\n" + f"{formatted_tb}" + ) from e + except skip.Exception as e: + if e.allow_module_level: + raise + raise nodes.Collector.CollectError( + "Using pytest.skip outside of a test will skip the entire module. " + "If that's your intention, pass `allow_module_level=True`. " + "If you want to skip a specific test or an entire class, " + "use the @pytest.mark.skip or @pytest.mark.skipif decorators." + ) from e + config.pluginmanager.consider_module(mod) + return mod + + +class Module(nodes.File, PyCollector): + """Collector for test classes and functions in a Python module.""" + + def _getobj(self): + return importtestmodule(self.path, self.config) + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + self._register_setup_module_fixture() + self._register_setup_function_fixture() + self.session._fixturemanager.parsefactories(self) + return super().collect() + + def _register_setup_module_fixture(self) -> None: + """Register an autouse, module-scoped fixture for the collected module object + that invokes setUpModule/tearDownModule if either or both are available. + + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_module = _get_first_non_fixture_func( + self.obj, ("setUpModule", "setup_module") + ) + teardown_module = _get_first_non_fixture_func( + self.obj, ("tearDownModule", "teardown_module") + ) + + if setup_module is None and teardown_module is None: + return + + def xunit_setup_module_fixture(request) -> Generator[None]: + module = request.module + if setup_module is not None: + _call_with_optional_argument(setup_module, module) + yield + if teardown_module is not None: + _call_with_optional_argument(teardown_module, module) + + fixtures.register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_module_fixture_{self.obj.__name__}", + func=xunit_setup_module_fixture, + node=self, + scope="module", + autouse=True, + ) + + def _register_setup_function_fixture(self) -> None: + """Register an autouse, function-scoped fixture for the collected module object + that invokes setup_function/teardown_function if either or both are available. + + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",)) + teardown_function = _get_first_non_fixture_func( + self.obj, ("teardown_function",) + ) + if setup_function is None and teardown_function is None: + return + + def xunit_setup_function_fixture(request) -> Generator[None]: + if request.instance is not None: + # in this case we are bound to an instance, so we need to let + # setup_method handle this + yield + return + function = request.function + if setup_function is not None: + _call_with_optional_argument(setup_function, function) + yield + if teardown_function is not None: + _call_with_optional_argument(teardown_function, function) + + fixtures.register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_function_fixture_{self.obj.__name__}", + func=xunit_setup_function_fixture, + node=self, + scope="function", + autouse=True, + ) + + +class Package(nodes.Directory): + """Collector for files and directories in a Python packages -- directories + with an `__init__.py` file. + + .. note:: + + Directories without an `__init__.py` file are instead collected by + :class:`~pytest.Dir` by default. Both are :class:`~pytest.Directory` + collectors. + + .. versionchanged:: 8.0 + + Now inherits from :class:`~pytest.Directory`. + """ + + def __init__( + self, + fspath: None, + parent: nodes.Collector, + # NOTE: following args are unused: + config=None, + session=None, + nodeid=None, + path: Path | None = None, + ) -> None: + # NOTE: Could be just the following, but kept as-is for compat. + # super().__init__(self, fspath, parent=parent) + session = parent.session + super().__init__( + fspath=fspath, + path=path, + parent=parent, + config=config, + session=session, + nodeid=nodeid, + ) + + def setup(self) -> None: + init_mod = importtestmodule(self.path / "__init__.py", self.config) + + # Not using fixtures to call setup_module here because autouse fixtures + # from packages are not called automatically (#4085). + setup_module = _get_first_non_fixture_func( + init_mod, ("setUpModule", "setup_module") + ) + if setup_module is not None: + _call_with_optional_argument(setup_module, init_mod) + + teardown_module = _get_first_non_fixture_func( + init_mod, ("tearDownModule", "teardown_module") + ) + if teardown_module is not None: + func = partial(_call_with_optional_argument, teardown_module, init_mod) + self.addfinalizer(func) + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + # Always collect __init__.py first. + def sort_key(entry: os.DirEntry[str]) -> object: + return (entry.name != "__init__.py", entry.name) + + config = self.config + col: nodes.Collector | None + cols: Sequence[nodes.Collector] + ihook = self.ihook + for direntry in scandir(self.path, sort_key): + if direntry.is_dir(): + path = Path(direntry.path) + if not self.session.isinitpath(path, with_parents=True): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + col = ihook.pytest_collect_directory(path=path, parent=self) + if col is not None: + yield col + + elif direntry.is_file(): + path = Path(direntry.path) + if not self.session.isinitpath(path): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + cols = ihook.pytest_collect_file(file_path=path, parent=self) + yield from cols + + +def _call_with_optional_argument(func, arg) -> None: + """Call the given function with the given argument if func accepts one argument, otherwise + calls func without arguments.""" + arg_count = func.__code__.co_argcount + if inspect.ismethod(func): + arg_count -= 1 + if arg_count: + func(arg) + else: + func() + + +def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> object | None: + """Return the attribute from the given object to be used as a setup/teardown + xunit-style function, but only if not marked as a fixture to avoid calling it twice. + """ + for name in names: + meth: object | None = getattr(obj, name, None) + if meth is not None and fixtures.getfixturemarker(meth) is None: + return meth + return None + + +class Class(PyCollector): + """Collector for test methods (and nested classes) in a Python class.""" + + @classmethod + def from_parent(cls, parent, *, name, obj=None, **kw) -> Self: # type: ignore[override] + """The public constructor.""" + return super().from_parent(name=name, parent=parent, **kw) + + def newinstance(self): + return self.obj() + + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + if not safe_getattr(self.obj, "__test__", True): + return [] + if hasinit(self.obj): + assert self.parent is not None + self.warn( + PytestCollectionWarning( + f"cannot collect test class {self.obj.__name__!r} because it has a " + f"__init__ constructor (from: {self.parent.nodeid})" + ) + ) + return [] + elif hasnew(self.obj): + assert self.parent is not None + self.warn( + PytestCollectionWarning( + f"cannot collect test class {self.obj.__name__!r} because it has a " + f"__new__ constructor (from: {self.parent.nodeid})" + ) + ) + return [] + + self._register_setup_class_fixture() + self._register_setup_method_fixture() + + self.session._fixturemanager.parsefactories( + holder=self.newinstance(), node=self + ) + + return super().collect() + + def _register_setup_class_fixture(self) -> None: + """Register an autouse, class scoped fixture into the collected class object + that invokes setup_class/teardown_class if either or both are available. + + Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",)) + teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",)) + if setup_class is None and teardown_class is None: + return + + def xunit_setup_class_fixture(request) -> Generator[None]: + cls = request.cls + if setup_class is not None: + func = getimfunc(setup_class) + _call_with_optional_argument(func, cls) + yield + if teardown_class is not None: + func = getimfunc(teardown_class) + _call_with_optional_argument(func, cls) + + fixtures.register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}", + func=xunit_setup_class_fixture, + node=self, + scope="class", + autouse=True, + ) + + def _register_setup_method_fixture(self) -> None: + """Register an autouse, function scoped fixture into the collected class object + that invokes setup_method/teardown_method if either or both are available. + + Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with + other fixtures (#517). + """ + setup_name = "setup_method" + setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) + teardown_name = "teardown_method" + teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,)) + if setup_method is None and teardown_method is None: + return + + def xunit_setup_method_fixture(request) -> Generator[None]: + instance = request.instance + method = request.function + if setup_method is not None: + func = getattr(instance, setup_name) + _call_with_optional_argument(func, method) + yield + if teardown_method is not None: + func = getattr(instance, teardown_name) + _call_with_optional_argument(func, method) + + fixtures.register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}", + func=xunit_setup_method_fixture, + node=self, + scope="function", + autouse=True, + ) + + +def hasinit(obj: object) -> bool: + init: object = getattr(obj, "__init__", None) + if init: + return init != object.__init__ + return False + + +def hasnew(obj: object) -> bool: + new: object = getattr(obj, "__new__", None) + if new: + return new != object.__new__ + return False + + +@final +@dataclasses.dataclass(frozen=True) +class IdMaker: + """Make IDs for a parametrization.""" + + __slots__ = ( + "argnames", + "config", + "idfn", + "ids", + "nodeid", + "parametersets", + ) + + # The argnames of the parametrization. + argnames: Sequence[str] + # The ParameterSets of the parametrization. + parametersets: Sequence[ParameterSet] + # Optionally, a user-provided callable to make IDs for parameters in a + # ParameterSet. + idfn: Callable[[Any], object | None] | None + # Optionally, explicit IDs for ParameterSets by index. + ids: Sequence[object | None] | None + # Optionally, the pytest config. + # Used for controlling ASCII escaping, determining parametrization ID + # strictness, and for calling the :hook:`pytest_make_parametrize_id` hook. + config: Config | None + # Optionally, the ID of the node being parametrized. + # Used only for clearer error messages. + nodeid: str | None + + def make_unique_parameterset_ids(self) -> list[str | _HiddenParam]: + """Make a unique identifier for each ParameterSet, that may be used to + identify the parametrization in a node ID. + + If strict_parametrization_ids is enabled, and duplicates are detected, + raises CollectError. Otherwise makes the IDs unique as follows: + + Format is -...-[counter], where prm_x_token is + - user-provided id, if given + - else an id derived from the value, applicable for certain types + - else + The counter suffix is appended only in case a string wouldn't be unique + otherwise. + """ + resolved_ids = list(self._resolve_ids()) + # All IDs must be unique! + if len(resolved_ids) != len(set(resolved_ids)): + # Record the number of occurrences of each ID. + id_counts = Counter(resolved_ids) + + if self._strict_parametrization_ids_enabled(): + parameters = ", ".join(self.argnames) + parametersets = ", ".join( + [saferepr(list(param.values)) for param in self.parametersets] + ) + ids = ", ".join( + id if id is not HIDDEN_PARAM else "" for id in resolved_ids + ) + duplicates = ", ".join( + id if id is not HIDDEN_PARAM else "" + for id, count in id_counts.items() + if count > 1 + ) + msg = textwrap.dedent(f""" + Duplicate parametrization IDs detected, but strict_parametrization_ids is set. + + Test name: {self.nodeid} + Parameters: {parameters} + Parameter sets: {parametersets} + IDs: {ids} + Duplicates: {duplicates} + + You can fix this problem using `@pytest.mark.parametrize(..., ids=...)` or `pytest.param(..., id=...)`. + """).strip() # noqa: E501 + raise nodes.Collector.CollectError(msg) + + # Map the ID to its next suffix. + id_suffixes: dict[str, int] = defaultdict(int) + # Suffix non-unique IDs to make them unique. + for index, id in enumerate(resolved_ids): + if id_counts[id] > 1: + if id is HIDDEN_PARAM: + self._complain_multiple_hidden_parameter_sets() + suffix = "" + if id and id[-1].isdigit(): + suffix = "_" + new_id = f"{id}{suffix}{id_suffixes[id]}" + while new_id in set(resolved_ids): + id_suffixes[id] += 1 + new_id = f"{id}{suffix}{id_suffixes[id]}" + resolved_ids[index] = new_id + id_suffixes[id] += 1 + assert len(resolved_ids) == len(set(resolved_ids)), ( + f"Internal error: {resolved_ids=}" + ) + return resolved_ids + + def _strict_parametrization_ids_enabled(self) -> bool: + if self.config is None: + return False + strict_parametrization_ids = self.config.getini("strict_parametrization_ids") + if strict_parametrization_ids is None: + strict_parametrization_ids = self.config.getini("strict") + return cast(bool, strict_parametrization_ids) + + def _resolve_ids(self) -> Iterable[str | _HiddenParam]: + """Resolve IDs for all ParameterSets (may contain duplicates).""" + for idx, parameterset in enumerate(self.parametersets): + if parameterset.id is not None: + # ID provided directly - pytest.param(..., id="...") + if parameterset.id is HIDDEN_PARAM: + yield HIDDEN_PARAM + else: + yield _ascii_escaped_by_config(parameterset.id, self.config) + elif self.ids and idx < len(self.ids) and self.ids[idx] is not None: + # ID provided in the IDs list - parametrize(..., ids=[...]). + if self.ids[idx] is HIDDEN_PARAM: + yield HIDDEN_PARAM + else: + yield self._idval_from_value_required(self.ids[idx], idx) + else: + # ID not provided - generate it. + yield "-".join( + self._idval(val, argname, idx) + for val, argname in zip( + parameterset.values, self.argnames, strict=True + ) + ) + + def _idval(self, val: object, argname: str, idx: int) -> str: + """Make an ID for a parameter in a ParameterSet.""" + idval = self._idval_from_function(val, argname, idx) + if idval is not None: + return idval + idval = self._idval_from_hook(val, argname) + if idval is not None: + return idval + idval = self._idval_from_value(val) + if idval is not None: + return idval + return self._idval_from_argname(argname, idx) + + def _idval_from_function(self, val: object, argname: str, idx: int) -> str | None: + """Try to make an ID for a parameter in a ParameterSet using the + user-provided id callable, if given.""" + if self.idfn is None: + return None + try: + id = self.idfn(val) + except Exception as e: + prefix = f"{self.nodeid}: " if self.nodeid is not None else "" + msg = "error raised while trying to determine id of parameter '{}' at position {}" + msg = prefix + msg.format(argname, idx) + raise ValueError(msg) from e + if id is None: + return None + return self._idval_from_value(id) + + def _idval_from_hook(self, val: object, argname: str) -> str | None: + """Try to make an ID for a parameter in a ParameterSet by calling the + :hook:`pytest_make_parametrize_id` hook.""" + if self.config: + id: str | None = self.config.hook.pytest_make_parametrize_id( + config=self.config, val=val, argname=argname + ) + return id + return None + + def _idval_from_value(self, val: object) -> str | None: + """Try to make an ID for a parameter in a ParameterSet from its value, + if the value type is supported.""" + if isinstance(val, str | bytes): + return _ascii_escaped_by_config(val, self.config) + elif val is None or isinstance(val, float | int | bool | complex): + return str(val) + elif isinstance(val, re.Pattern): + return ascii_escaped(val.pattern) + elif val is NOTSET: + # Fallback to default. Note that NOTSET is an enum.Enum. + pass + elif isinstance(val, enum.Enum): + return str(val) + elif isinstance(getattr(val, "__name__", None), str): + # Name of a class, function, module, etc. + name: str = getattr(val, "__name__") + return name + return None + + def _idval_from_value_required(self, val: object, idx: int) -> str: + """Like _idval_from_value(), but fails if the type is not supported.""" + id = self._idval_from_value(val) + if id is not None: + return id + + # Fail. + prefix = self._make_error_prefix() + msg = ( + f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. " + "Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__." + ) + fail(msg, pytrace=False) + + @staticmethod + def _idval_from_argname(argname: str, idx: int) -> str: + """Make an ID for a parameter in a ParameterSet from the argument name + and the index of the ParameterSet.""" + return str(argname) + str(idx) + + def _complain_multiple_hidden_parameter_sets(self) -> NoReturn: + fail( + f"{self._make_error_prefix()}multiple instances of HIDDEN_PARAM " + "cannot be used in the same parametrize call, " + "because the tests names need to be unique." + ) + + def _make_error_prefix(self) -> str: + if self.nodeid is not None: + return f"In {self.nodeid}: " + else: + return "" + + +@final +@dataclasses.dataclass(frozen=True) +class CallSpec2: + """A planned parameterized invocation of a test function. + + Calculated during collection for a given test function's Metafunc. + Once collection is over, each callspec is turned into a single Item + and stored in item.callspec. + """ + + # arg name -> arg value which will be passed to a fixture of the same name. + params: dict[str, object] = dataclasses.field(default_factory=dict) + # arg name -> arg index. + indices: dict[str, int] = dataclasses.field(default_factory=dict) + # arg name -> parameter scope. + # Used for sorting parametrized resources. + _arg2scope: Mapping[str, Scope] = dataclasses.field(default_factory=dict) + # Parts which will be added to the item's name in `[..]` separated by "-". + _idlist: Sequence[str] = dataclasses.field(default_factory=tuple) + # Marks which will be applied to the item. + marks: list[Mark] = dataclasses.field(default_factory=list) + + def setmulti( + self, + *, + argnames: Iterable[str], + valset: Iterable[object], + id: str | _HiddenParam, + marks: Iterable[Mark | MarkDecorator], + scope: Scope, + param_index: int, + nodeid: str, + ) -> CallSpec2: + params = self.params.copy() + indices = self.indices.copy() + arg2scope = dict(self._arg2scope) + for arg, val in zip(argnames, valset, strict=True): + if arg in params: + raise nodes.Collector.CollectError( + f"{nodeid}: duplicate parametrization of {arg!r}" + ) + params[arg] = val + indices[arg] = param_index + arg2scope[arg] = scope + return CallSpec2( + params=params, + indices=indices, + _arg2scope=arg2scope, + _idlist=self._idlist if id is HIDDEN_PARAM else [*self._idlist, id], + marks=[*self.marks, *normalize_mark_list(marks)], + ) + + def getparam(self, name: str) -> object: + try: + return self.params[name] + except KeyError as e: + raise ValueError(name) from e + + @property + def id(self) -> str: + return "-".join(self._idlist) + + +def get_direct_param_fixture_func(request: FixtureRequest) -> Any: + return request.param + + +class DirectParamFixtureDef(FixtureDef[FixtureValue]): + """A custom FixtureDef for direct parametrization fixtures. + + Each parameter in direct parametrization is desugared to a parametrized + fixture which returns the direct parameterization value as its param. + We use this custom type as a "marker" for this type of FixtureDef, but + usually behaves like any other FixtureDef. + """ + + def __init__(self, *, node: nodes.Node, argname: str, scope: Scope) -> None: + super().__init__( + config=node.config, + baseid=NOTSET, + argname=argname, + func=get_direct_param_fixture_func, + scope=scope, + params=None, + ids=None, + node=node, + _ispytest=True, + ) + + +# Used for storing fixturedefs for direct parametrization. +name2directparamfixturedef_key = StashKey[dict[str, DirectParamFixtureDef[object]]]() + + +@final +class Metafunc: + """Objects passed to the :hook:`pytest_generate_tests` hook. + + They help to inspect a test function and to generate tests according to + test configuration or values specified in the class or module where a + test function is defined. + """ + + def __init__( + self, + definition: FunctionDefinition, + fixtureinfo: fixtures.FuncFixtureInfo, + config: Config, + cls=None, + module=None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + + #: Access to the underlying :class:`_pytest.python.FunctionDefinition`. + self.definition = definition + + #: Access to the :class:`pytest.Config` object for the test session. + self.config = config + + #: The module object where the test function is defined in. + self.module = module + + #: Underlying Python test function. + self.function = definition.obj + + #: Set of fixture names required by the test function. + self.fixturenames = fixtureinfo.names_closure + + #: Class object where the test function is defined in or ``None``. + self.cls = cls + + self._arg2fixturedefs = fixtureinfo.name2fixturedefs + + # Result of parametrize(). + self._calls: list[CallSpec2] = [] + + self._params_directness: dict[str, Literal["indirect", "direct"]] = {} + + def parametrize( + self, + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], + indirect: bool | Sequence[str] = False, + ids: Iterable[object | None] | Callable[[Any], object | None] | None = None, + scope: ScopeName | None = None, + *, + _param_mark: Mark | None = None, + ) -> None: + """Add new invocations to the underlying test function using the list + of argvalues for the given argnames. Parametrization is performed + during the collection phase. If you need to setup expensive resources + see about setting ``indirect`` to do it at test setup time instead. + + Can be called multiple times per test function (but only on different + argument names), in which case each call parametrizes all previous + parametrizations, e.g. + + :: + + unparametrized: t + parametrize ["x", "y"]: t[x], t[y] + parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2] + + :param argnames: + A comma-separated string denoting one or more argument names, or + a list/tuple of argument strings. + + :param argvalues: + The list of argvalues determines how often a test is invoked with + different argument values. + + If only one argname was specified argvalues is a list of values. + If N argnames were specified, argvalues must be a list of + N-tuples, where each tuple-element specifies a value for its + respective argname. + + .. versionchanged:: 9.1 + + Passing a non-:class:`~collections.abc.Collection` iterable + (such as a generator or iterator) is deprecated. See + :ref:`parametrize-iterators` for details. + + :param indirect: + A list of arguments' names (subset of argnames) or a boolean. + If True the list contains all names from the argnames. Each + argvalue corresponding to an argname in this list will + be passed as request.param to its respective argname fixture + function so that it can perform more expensive setups during the + setup phase of a test rather than at collection time. + + :param ids: + Sequence of (or generator for) ids for ``argvalues``, + or a callable to return part of the id for each argvalue. + + With sequences (and generators like ``itertools.count()``) the + returned ids should be of type ``string``, ``int``, ``float``, + ``bool``, or ``None``. + They are mapped to the corresponding index in ``argvalues``. + ``None`` means to use the auto-generated id. + + .. versionadded:: 8.4 + :ref:`hidden-param` means to hide the parameter set + from the test name. Can only be used at most 1 time, as + test names need to be unique. + + If it is a callable it will be called for each entry in + ``argvalues``, and the return value is used as part of the + auto-generated id for the whole set (where parts are joined with + dashes ("-")). + This is useful to provide more specific ids for certain items, e.g. + dates. Returning ``None`` will use an auto-generated id. + + If no ids are provided they will be generated automatically from + the argvalues. + + :param scope: + If specified it denotes the scope of the parameters. + The scope is used for grouping tests by parameter instances. + It will also override any fixture-function defined scope, allowing + to set a dynamic scope using test context or configuration. + """ + nodeid = self.definition.nodeid + + argnames, parametersets = ParameterSet._for_parametrize( + argnames, + argvalues, + self.function, + self.config, + nodeid=self.definition.nodeid, + ) + del argvalues + + if "request" in argnames: + fail( + f"{nodeid}: 'request' is a reserved name and cannot be used in @pytest.mark.parametrize", + pytrace=False, + ) + + if scope is not None: + scope_ = Scope.from_user( + scope, descr=f"parametrize() call in {self.function.__name__}" + ) + else: + scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) + + self._validate_if_using_arg_names(argnames, indirect) + + # Use any already (possibly) generated ids with parametrize Marks. + if _param_mark and _param_mark._param_ids_from: + generated_ids = _param_mark._param_ids_from._param_ids_generated + if generated_ids is not None: + ids = generated_ids + + ids = self._resolve_parameter_set_ids( + argnames, ids, parametersets, nodeid=self.definition.nodeid + ) + + # Store used (possibly generated) ids with parametrize Marks. + if _param_mark and _param_mark._param_ids_from and generated_ids is None: + object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids) + + # Calculate directness. + arg_directness = _resolve_args_directness( + argnames, indirect, self.definition.nodeid + ) + self._params_directness.update(arg_directness) + + # Add direct parametrizations as fixturedefs to arg2fixturedefs by + # registering artificial DirectParamFixtureDef's such that later at test + # setup time we can rely on FixtureDefs to exist for all argnames. + node = None + # For scopes higher than function, a DirectParamFixtureDef might have + # already been created for the scope. We thus store and cache the + # DirectParamFixtureDef on the node related to the scope. + if scope_ is Scope.Function: + name2directparamfixturedef = None + else: + collector = self.definition.parent + assert collector is not None + node = get_scope_node(collector, scope_) + if node is None: + # If used class scope and there is no class, use module-level + # collector (for now). + if scope_ is Scope.Class: + assert isinstance(collector, Module) + node = collector + # If used package scope and there is no package, use session + # (for now). + elif scope_ is Scope.Package: + node = collector.session + else: + assert False, f"Unhandled missing scope: {scope}" + default: dict[str, DirectParamFixtureDef[object]] = {} + name2directparamfixturedef = node.stash.setdefault( + name2directparamfixturedef_key, default + ) + for argname in argnames: + if arg_directness[argname] == "indirect": + continue + if ( + name2directparamfixturedef is not None + and argname in name2directparamfixturedef + ): + fixturedef = name2directparamfixturedef[argname] + else: + fixturedef = DirectParamFixtureDef( + node=self.definition.session, + argname=argname, + scope=scope_, + ) + if name2directparamfixturedef is not None: + name2directparamfixturedef[argname] = fixturedef + self._arg2fixturedefs[argname] = [fixturedef] + + # Create the new calls: if we are parametrize() multiple times (by applying the decorator + # more than once) then we accumulate those calls generating the cartesian product + # of all calls. + newcalls = [] + for callspec in self._calls or [CallSpec2()]: + for param_index, (param_id, param_set) in enumerate( + zip(ids, parametersets, strict=True) + ): + newcallspec = callspec.setmulti( + argnames=argnames, + valset=param_set.values, + id=param_id, + marks=param_set.marks, + scope=scope_, + param_index=param_index, + nodeid=nodeid, + ) + newcalls.append(newcallspec) + self._calls = newcalls + + def _resolve_parameter_set_ids( + self, + argnames: Sequence[str], + ids: Iterable[object | None] | Callable[[Any], object | None] | None, + parametersets: Sequence[ParameterSet], + nodeid: str, + ) -> list[str | _HiddenParam]: + """Resolve the actual ids for the given parameter sets. + + :param argnames: + Argument names passed to ``parametrize()``. + :param ids: + The `ids` parameter of the ``parametrize()`` call (see docs). + :param parametersets: + The parameter sets, each containing a set of values corresponding + to ``argnames``. + :param nodeid str: + The nodeid of the definition item that generated this + parametrization. + :returns: + List with ids for each parameter set given. + """ + if ids is None: + idfn = None + ids_ = None + elif callable(ids): + idfn = ids + ids_ = None + else: + idfn = None + ids_ = self._validate_ids(ids, parametersets) + id_maker = IdMaker( + argnames, + parametersets, + idfn, + ids_, + self.config, + nodeid=nodeid, + ) + return id_maker.make_unique_parameterset_ids() + + def _validate_ids( + self, + ids: Iterable[object | None], + parametersets: Sequence[ParameterSet], + ) -> list[object | None]: + try: + num_ids = len(ids) # type: ignore[arg-type] + except TypeError: + try: + iter(ids) + except TypeError as e: + raise TypeError("ids must be a callable or an iterable") from e + num_ids = len(parametersets) + + # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849 + if num_ids != len(parametersets) and num_ids != 0: + nodeid = self.definition.nodeid + fail( + f"In {nodeid}: {len(parametersets)} parameter sets specified, with different number of ids: {num_ids}", + pytrace=False, + ) + + return list(itertools.islice(ids, num_ids)) + + def _validate_if_using_arg_names( + self, + argnames: Sequence[str], + indirect: bool | Sequence[str], + ) -> None: + """Check if all argnames are being used, by default values, or directly/indirectly. + + :param List[str] argnames: List of argument names passed to ``parametrize()``. + :param indirect: Same as the ``indirect`` parameter of ``parametrize()``. + :raises ValueError: If validation fails. + """ + default_arg_names = set(get_default_arg_names(self.function)) + nodeid = self.definition.nodeid + for arg in argnames: + if arg not in self.fixturenames: + if arg in default_arg_names: + fail( + f"In {nodeid}: function already takes an argument '{arg}' with a default value", + pytrace=False, + ) + else: + if isinstance(indirect, Sequence): + name = "fixture" if arg in indirect else "argument" + else: + name = "fixture" if indirect else "argument" + fail( + f"In {nodeid}: function uses no {name} '{arg}'", + pytrace=False, + ) + + def _recompute_direct_params_indices(self) -> None: + for argname, param_type in self._params_directness.items(): + if param_type == "direct": + for i, callspec in enumerate(self._calls): + callspec.indices[argname] = i + + +def _find_parametrized_scope( + argnames: Sequence[str], + arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]], + indirect: bool | Sequence[str], +) -> Scope: + """Find the most appropriate scope for a parametrized call based on its arguments. + + When there's at least one direct argument, always use "function" scope. + + When a test function is parametrized and all its arguments are indirect + (e.g. fixtures), return the most narrow scope based on the fixtures used. + + Related to issue #1832, based on code posted by @Kingdread. + """ + if isinstance(indirect, Sequence): + all_arguments_are_fixtures = len(indirect) == len(argnames) + else: + all_arguments_are_fixtures = bool(indirect) + + if all_arguments_are_fixtures: + fixturedefs = arg2fixturedefs or {} + used_scopes = [ + fixturedef[-1]._scope + for name, fixturedef in fixturedefs.items() + if name in argnames + ] + # Takes the most narrow scope from used fixtures. + return min(used_scopes, default=Scope.Function) + + return Scope.Function + + +def _ascii_escaped_by_config(val: str | bytes, config: Config | None) -> str: + if config is None: + escape_option = False + else: + escape_option = config.getini( + "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" + ) + # TODO: If escaping is turned off and the user passes bytes, + # will return a bytes. For now we ignore this but the + # code *probably* doesn't handle this case. + return val if escape_option else ascii_escaped(val) # type: ignore + + +class Function(PyobjMixin, nodes.Item): + """Item responsible for setting up and executing a Python test function. + + :param name: + The full function name, including any decorations like those + added by parametrization (``my_func[my_param]``). + :param parent: + The parent Node. + :param config: + The pytest Config object. + :param callspec: + If given, this function has been parametrized and the callspec contains + meta information about the parametrization. + :param callobj: + If given, the object which will be called when the Function is invoked, + otherwise the callobj will be obtained from ``parent`` using ``originalname``. + :param keywords: + Keywords bound to the function object for "-k" matching. + :param session: + The pytest Session object. + :param fixtureinfo: + Fixture information already resolved at this fixture node.. + :param originalname: + The attribute name to use for accessing the underlying function object. + Defaults to ``name``. Set this if name is different from the original name, + for example when it contains decorations like those added by parametrization + (``my_func[my_param]``). + """ + + # Disable since functions handle it themselves. + _ALLOW_MARKERS = False + + def __init__( + self, + name: str, + parent, + config: Config | None = None, + callspec: CallSpec2 | None = None, + callobj=NOTSET, + keywords: Mapping[str, Any] | None = None, + session: Session | None = None, + fixtureinfo: FuncFixtureInfo | None = None, + originalname: str | None = None, + ) -> None: + super().__init__(name, parent, config=config, session=session) + + if callobj is not NOTSET: + self._obj = callobj + self._instance = getattr(callobj, "__self__", None) + + #: Original function name, without any decorations (for example + #: parametrization adds a ``"[...]"`` suffix to function names), used to access + #: the underlying function object from ``parent`` (in case ``callobj`` is not given + #: explicitly). + #: + #: .. versionadded:: 3.0 + self.originalname = originalname or name + + # Note: when FunctionDefinition is introduced, we should change ``originalname`` + # to a readonly property that returns FunctionDefinition.name. + + self.own_markers.extend(get_unpacked_marks(self.obj)) + if callspec: + self.callspec = callspec + self.own_markers.extend(callspec.marks) + + # todo: this is a hell of a hack + # https://github.com/pytest-dev/pytest/issues/4569 + # Note: the order of the updates is important here; indicates what + # takes priority (ctor argument over function attributes over markers). + # Take own_markers only; NodeKeywords handles parent traversal on its own. + self.keywords.update((mark.name, mark) for mark in self.own_markers) + self.keywords.update(self.obj.__dict__) + if keywords: + self.keywords.update(keywords) + + if fixtureinfo is None: + fm = self.session._fixturemanager + fixtureinfo = fm.getfixtureinfo(self, self.obj, self.cls) + self._fixtureinfo: FuncFixtureInfo = fixtureinfo + self.fixturenames = fixtureinfo.names_closure + self._initrequest() + + # todo: determine sound type limitations + @classmethod + def from_parent(cls, parent, **kw) -> Self: + """The public constructor.""" + return super().from_parent(parent=parent, **kw) + + def _initrequest(self) -> None: + self.funcargs: dict[str, object] = {} + self._request = fixtures.TopRequest(self, _ispytest=True) + + @property + def function(self): + """Underlying python 'function' object.""" + return getimfunc(self.obj) + + @property + def instance(self): + try: + return self._instance + except AttributeError: + if isinstance(self.parent, Class): + # Each Function gets a fresh class instance. + self._instance = self._getinstance() + else: + self._instance = None + return self._instance + + def _getinstance(self): + if isinstance(self.parent, Class): + # Each Function gets a fresh class instance. + return self.parent.newinstance() + else: + return None + + def _getobj(self): + instance = self.instance + if instance is not None: + parent_obj = instance + else: + assert self.parent is not None + parent_obj = self.parent.obj # type: ignore[attr-defined] + return getattr(parent_obj, self.originalname) + + @property + def _pyfuncitem(self): + """(compatonly) for code expecting pytest-2.2 style request objects.""" + return self + + def runtest(self) -> None: + """Execute the underlying test function.""" + self.ihook.pytest_pyfunc_call(pyfuncitem=self) + + def setup(self) -> None: + self._request._fillfixtures() + + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: + if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): + code = _pytest._code.Code.from_function(get_real_func(self.obj)) + path, firstlineno = code.path, code.firstlineno + traceback = excinfo.traceback + ntraceback = traceback.cut(path=path, firstlineno=firstlineno) + if ntraceback == traceback: + ntraceback = ntraceback.cut(path=path) + if ntraceback == traceback: + ntraceback = ntraceback.filter(filter_traceback) + if not ntraceback: + ntraceback = traceback + ntraceback = ntraceback.filter(excinfo) + + # issue364: mark all but first and last frames to + # only show a single-line message for each frame. + if self.config.getoption("tbstyle", "auto") == "auto": + if len(ntraceback) > 2: + ntraceback = Traceback( + ( + ntraceback[0], + *(t.with_repr_style("short") for t in ntraceback[1:-1]), + ntraceback[-1], + ) + ) + + return ntraceback + return excinfo.traceback + + # TODO: Type ignored -- breaks Liskov Substitution. + def repr_failure( # type: ignore[override] + self, + excinfo: ExceptionInfo[BaseException], + ) -> str | TerminalRepr: + style = self.config.getoption("tbstyle", "auto") + if style == "auto": + style = "long" + return self._repr_failure_py(excinfo, style=style) + + +class FunctionDefinition(Function): + """This class is a stop gap solution until we evolve to have actual function + definition nodes and manage to get rid of ``metafunc``.""" + + def runtest(self) -> None: + raise RuntimeError("function definitions are not supposed to be run as tests") + + setup = runtest diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/python_api.py b/.venv-test/lib/python3.12/site-packages/_pytest/python_api.py new file mode 100644 index 0000000..d7f4c2d --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/python_api.py @@ -0,0 +1,922 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import builtins +from collections.abc import Collection +from collections.abc import Mapping +from collections.abc import Sequence +from collections.abc import Sized +from datetime import datetime +from datetime import timedelta +from decimal import Decimal +import math +from numbers import Complex +import pprint +import sys +from typing import Any +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from numpy import ndarray + + +def _compare_approx( + full_object: object, + message_data: Sequence[tuple[str, str, str]], + number_of_elements: int, + different_ids: Sequence[object], + max_abs_diff: float, + max_rel_diff: float, +) -> list[str]: + message_list = list(message_data) + message_list.insert(0, ("Index", "Obtained", "Expected")) + max_sizes = [0, 0, 0] + for index, obtained, expected in message_list: + max_sizes[0] = max(max_sizes[0], len(index)) + max_sizes[1] = max(max_sizes[1], len(obtained)) + max_sizes[2] = max(max_sizes[2], len(expected)) + explanation = [ + f"comparison failed. Mismatched elements: {len(different_ids)} / {number_of_elements}:", + f"Max absolute difference: {max_abs_diff}", + f"Max relative difference: {max_rel_diff}", + ] + [ + f"{indexes:<{max_sizes[0]}} | {obtained:<{max_sizes[1]}} | {expected:<{max_sizes[2]}}" + for indexes, obtained, expected in message_list + ] + return explanation + + +# builtin pytest.approx helper + + +class ApproxBase: + """Provide shared utilities for making approximate comparisons between + numbers or sequences of numbers.""" + + # Tell numpy to use our `__eq__` operator instead of its. + __array_ufunc__ = None + __array_priority__ = 100 + + def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None: + __tracebackhide__ = True + self.expected = expected + self.abs = abs + self.rel = rel + self.nan_ok = nan_ok + self._check_type() + + def __repr__(self) -> str: + raise NotImplementedError + + def _repr_compare(self, other_side: Any) -> list[str]: + return [ + "comparison failed", + f"Obtained: {other_side}", + f"Expected: {self}", + ] + + def __eq__(self, actual) -> bool: + return all( + a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual) + ) + + def __bool__(self): + __tracebackhide__ = True + raise AssertionError( + "approx() is not supported in a boolean context.\nDid you mean: `assert a == approx(b)`?" + ) + + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore + + def __ne__(self, actual) -> bool: + return not (actual == self) + + def _approx_scalar(self, x) -> ApproxBase: + if isinstance(x, Decimal): + return ApproxDecimal(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) + if isinstance(x, (datetime, timedelta)): + return ApproxTimedelta(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) + return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) + + def _yield_comparisons(self, actual): + """Yield all the pairs of numbers to be compared. + + This is used to implement the `__eq__` method. + """ + raise NotImplementedError + + def _check_type(self) -> None: + """Raise a TypeError if the expected value is not a valid type.""" + # This is only a concern if the expected value is a sequence. In every + # other case, the approx() function ensures that the expected value has + # a numeric type. For this reason, the default is to do nothing. The + # classes that deal with sequences should reimplement this method to + # raise if there are any non-numeric elements in the sequence. + + +def _recursive_sequence_map(f, x): + """Recursively map a function over a sequence of arbitrary depth""" + if isinstance(x, list | tuple): + seq_type = type(x) + return seq_type(_recursive_sequence_map(f, xi) for xi in x) + elif _is_sequence_like(x): + return [_recursive_sequence_map(f, xi) for xi in x] + else: + return f(x) + + +class ApproxNumpy(ApproxBase): + """Perform approximate comparisons where the expected value is numpy array.""" + + def __repr__(self) -> str: + list_scalars = _recursive_sequence_map( + self._approx_scalar, self.expected.tolist() + ) + return f"approx({list_scalars!r})" + + def _repr_compare(self, other_side: ndarray | list[Any]) -> list[str]: + import itertools + import math + + def get_value_from_nested_list( + nested_list: list[Any], nd_index: tuple[Any, ...] + ) -> Any: + """ + Helper function to get the value out of a nested list, given an n-dimensional index. + This mimics numpy's indexing, but for raw nested python lists. + """ + value: Any = nested_list + for i in nd_index: + value = value[i] + return value + + np_array_shape = self.expected.shape + approx_side_as_seq = _recursive_sequence_map( + self._approx_scalar, self.expected.tolist() + ) + + # convert other_side to numpy array to ensure shape attribute is available + other_side_as_array = _as_numpy_array(other_side) + assert other_side_as_array is not None + + if np_array_shape != other_side_as_array.shape: + return [ + "Impossible to compare arrays with different shapes.", + f"Shapes: {np_array_shape} and {other_side_as_array.shape}", + ] + + number_of_elements = self.expected.size + max_abs_diff = -math.inf + max_rel_diff = -math.inf + different_ids = [] + for index in itertools.product(*(range(i) for i in np_array_shape)): + approx_value = get_value_from_nested_list(approx_side_as_seq, index) + other_value = get_value_from_nested_list(other_side_as_array, index) + if approx_value != other_value: + abs_diff = abs(approx_value.expected - other_value) + max_abs_diff = max(max_abs_diff, abs_diff) + if other_value == 0.0: + max_rel_diff = math.inf + else: + max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value)) + different_ids.append(index) + + message_data = [ + ( + str(index), + str(get_value_from_nested_list(other_side_as_array, index)), + str(get_value_from_nested_list(approx_side_as_seq, index)), + ) + for index in different_ids + ] + return _compare_approx( + self.expected, + message_data, + number_of_elements, + different_ids, + max_abs_diff, + max_rel_diff, + ) + + def __eq__(self, actual) -> bool: + import numpy as np + + # self.expected is supposed to always be an array here. + + if not np.isscalar(actual): + try: + actual = np.asarray(actual) + except Exception as e: + raise TypeError(f"cannot compare '{actual}' to numpy.ndarray") from e + + if not np.isscalar(actual) and actual.shape != self.expected.shape: + return False + + return super().__eq__(actual) + + def _yield_comparisons(self, actual): + import numpy as np + + # `actual` can either be a numpy array or a scalar, it is treated in + # `__eq__` before being passed to `ApproxBase.__eq__`, which is the + # only method that calls this one. + + if np.isscalar(actual): + for i in np.ndindex(self.expected.shape): + yield actual, self.expected[i].item() + else: + for i in np.ndindex(self.expected.shape): + yield actual[i].item(), self.expected[i].item() + + +class ApproxMapping(ApproxBase): + """Perform approximate comparisons where the expected value is a mapping + with numeric values (the keys can be anything).""" + + def __repr__(self) -> str: + return f"approx({ ({k: self._approx_scalar(v) for k, v in self.expected.items()})!r})" + + def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]: + import math + + if len(self.expected) != len(other_side): + return [ + "Impossible to compare mappings with different sizes.", + f"Lengths: {len(self.expected)} and {len(other_side)}", + ] + + if self.expected.keys() != other_side.keys(): + return [ + "comparison failed.", + f"Mappings has different keys: expected {self.expected.keys()} but got {other_side.keys()}", + ] + + approx_side_as_map = { + k: self._approx_scalar(v) for k, v in self.expected.items() + } + + number_of_elements = len(approx_side_as_map) + max_abs_diff = -math.inf + max_rel_diff = -math.inf + different_ids = [] + for approx_key, approx_value in approx_side_as_map.items(): + other_value = other_side[approx_key] + if approx_value != other_value: + if approx_value.expected is not None and other_value is not None: + try: + max_abs_diff = max( + max_abs_diff, abs(approx_value.expected - other_value) + ) + if approx_value.expected == 0.0: + max_rel_diff = math.inf + else: + max_rel_diff = max( + max_rel_diff, + abs( + (approx_value.expected - other_value) + / approx_value.expected + ), + ) + except ZeroDivisionError: + pass + different_ids.append(approx_key) + + message_data = [ + (str(key), str(other_side[key]), str(approx_side_as_map[key])) + for key in different_ids + ] + + return _compare_approx( + self.expected, + message_data, + number_of_elements, + different_ids, + max_abs_diff, + max_rel_diff, + ) + + def __eq__(self, actual) -> bool: + try: + if set(actual.keys()) != set(self.expected.keys()): + return False + except AttributeError: + return False + + return super().__eq__(actual) + + def _yield_comparisons(self, actual): + for k in self.expected.keys(): + yield actual[k], self.expected[k] + + def _check_type(self) -> None: + __tracebackhide__ = True + for key, value in self.expected.items(): + if isinstance(value, type(self.expected)): + msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}" + raise TypeError(msg.format(key, value, pprint.pformat(self.expected))) + + +class ApproxSequenceLike(ApproxBase): + """Perform approximate comparisons where the expected value is a sequence of numbers.""" + + def __repr__(self) -> str: + seq_type = type(self.expected) + if seq_type not in (tuple, list): + seq_type = list + return f"approx({seq_type(self._approx_scalar(x) for x in self.expected)!r})" + + def _repr_compare(self, other_side: Sequence[float]) -> list[str]: + import math + + if len(self.expected) != len(other_side): + return [ + "Impossible to compare lists with different sizes.", + f"Lengths: {len(self.expected)} and {len(other_side)}", + ] + + approx_side_as_map = _recursive_sequence_map(self._approx_scalar, self.expected) + + number_of_elements = len(approx_side_as_map) + max_abs_diff = -math.inf + max_rel_diff = -math.inf + different_ids = [] + for i, (approx_value, other_value) in enumerate( + zip(approx_side_as_map, other_side, strict=True) + ): + if approx_value != other_value: + try: + abs_diff = abs(approx_value.expected - other_value) + max_abs_diff = max(max_abs_diff, abs_diff) + # Ignore non-numbers for the diff calculations (#13012). + except TypeError: + pass + else: + if other_value == 0.0: + max_rel_diff = math.inf + else: + max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value)) + different_ids.append(i) + message_data = [ + (str(i), str(other_side[i]), str(approx_side_as_map[i])) + for i in different_ids + ] + + return _compare_approx( + self.expected, + message_data, + number_of_elements, + different_ids, + max_abs_diff, + max_rel_diff, + ) + + def __eq__(self, actual) -> bool: + try: + if len(actual) != len(self.expected): + return False + except TypeError: + return False + return super().__eq__(actual) + + def _yield_comparisons(self, actual): + return zip(actual, self.expected, strict=True) + + def _check_type(self) -> None: + __tracebackhide__ = True + for index, x in enumerate(self.expected): + if isinstance(x, type(self.expected)): + msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}" + raise TypeError(msg.format(x, index, pprint.pformat(self.expected))) + + +class ApproxScalar(ApproxBase): + """Perform approximate comparisons where the expected value is a single number.""" + + # Using Real should be better than this Union, but not possible yet: + # https://github.com/python/typeshed/pull/3108 + DEFAULT_ABSOLUTE_TOLERANCE: float | Decimal = 1e-12 + DEFAULT_RELATIVE_TOLERANCE: float | Decimal = 1e-6 + + def __repr__(self) -> str: + """Return a string communicating both the expected value and the + tolerance for the comparison being made. + + For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``. + """ + # Don't show a tolerance for values that aren't compared using + # tolerances, i.e. non-numerics and infinities. Need to call abs to + # handle complex numbers, e.g. (inf + 1j). + if ( + isinstance(self.expected, bool) + or (not isinstance(self.expected, Complex | Decimal)) + or math.isinf(abs(self.expected) or isinstance(self.expected, bool)) + ): + return str(self.expected) + + # If a sensible tolerance can't be calculated, self.tolerance will + # raise a ValueError. In this case, display '???'. + try: + if 1e-3 <= self.tolerance < 1e3: + vetted_tolerance = f"{self.tolerance:n}" + else: + vetted_tolerance = f"{self.tolerance:.1e}" + + if ( + isinstance(self.expected, Complex) + and self.expected.imag + and not math.isinf(self.tolerance) + ): + vetted_tolerance += " ∠ ±180°" + except ValueError: + vetted_tolerance = "???" + + return f"{self.expected} ± {vetted_tolerance}" + + def __eq__(self, actual) -> bool: + """Return whether the given value is equal to the expected value + within the pre-specified tolerance.""" + + def is_bool(val: Any) -> bool: + # Check if `val` is a native bool or numpy bool. + if isinstance(val, bool): + return True + if np := sys.modules.get("numpy"): + return isinstance(val, np.bool_) + return False + + asarray = _as_numpy_array(actual) + if asarray is not None: + # Call ``__eq__()`` manually to prevent infinite-recursion with + # numpy<1.13. See #3748. + return all(self.__eq__(a) for a in asarray.flat) + + # Short-circuit exact equality, except for bool and np.bool_ + if is_bool(self.expected) and not is_bool(actual): + return False + elif actual == self.expected: + return True + + # If either type is non-numeric, fall back to strict equality. + # NB: we need Complex, rather than just Number, to ensure that __abs__, + # __sub__, and __float__ are defined. Also, consider bool to be + # non-numeric, even though it has the required arithmetic. + if is_bool(self.expected) or not ( + isinstance(self.expected, Complex | Decimal) + and isinstance(actual, Complex | Decimal) + ): + return False + + # Allow the user to control whether NaNs are considered equal to each + # other or not. The abs() calls are for compatibility with complex + # numbers. + if math.isnan(abs(self.expected)): + return self.nan_ok and math.isnan(abs(actual)) + + # Infinity shouldn't be approximately equal to anything but itself, but + # if there's a relative tolerance, it will be infinite and infinity + # will seem approximately equal to everything. The equal-to-itself + # case would have been short circuited above, so here we can just + # return false if the expected value is infinite. The abs() call is + # for compatibility with complex numbers. + if math.isinf(abs(self.expected)): + return False + + # Return true if the two numbers are within the tolerance. + result: bool = abs(self.expected - actual) <= self.tolerance + return result + + __hash__ = None + + @property + def tolerance(self): + """Return the tolerance for the comparison. + + This could be either an absolute tolerance or a relative tolerance, + depending on what the user specified or which would be larger. + """ + + def set_default(x, default): + return x if x is not None else default + + # Figure out what the absolute tolerance should be. ``self.abs`` is + # either None or a value specified by the user. + absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE) + + if absolute_tolerance < 0: + raise ValueError( + f"absolute tolerance can't be negative: {absolute_tolerance}" + ) + if math.isnan(absolute_tolerance): + raise ValueError("absolute tolerance can't be NaN.") + + # If the user specified an absolute tolerance but not a relative one, + # just return the absolute tolerance. + if self.rel is None: + if self.abs is not None: + return absolute_tolerance + + # Figure out what the relative tolerance should be. ``self.rel`` is + # either None or a value specified by the user. This is done after + # we've made sure the user didn't ask for an absolute tolerance only, + # because we don't want to raise errors about the relative tolerance if + # we aren't even going to use it. + relative_tolerance = set_default( + self.rel, self.DEFAULT_RELATIVE_TOLERANCE + ) * abs(self.expected) + + if relative_tolerance < 0: + raise ValueError( + f"relative tolerance can't be negative: {relative_tolerance}" + ) + if math.isnan(relative_tolerance): + raise ValueError("relative tolerance can't be NaN.") + + # Return the larger of the relative and absolute tolerances. + return max(relative_tolerance, absolute_tolerance) + + +class ApproxDecimal(ApproxScalar): + """Perform approximate comparisons where the expected value is a Decimal.""" + + DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") + DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") + + def __repr__(self) -> str: + if isinstance(self.rel, float): + rel = Decimal.from_float(self.rel) + else: + rel = self.rel + + if isinstance(self.abs, float): + abs_ = Decimal.from_float(self.abs) + else: + abs_ = self.abs + + tol_str = "???" + if rel is not None and Decimal("1e-3") <= rel <= Decimal("1e3"): + tol_str = f"{rel:.1e}" + elif abs_ is not None: + tol_str = f"{abs_:.1e}" + + return f"{self.expected} ± {tol_str}" + + +class ApproxTimedelta(ApproxBase): + """Perform approximate comparisons where the expected value is a + datetime or timedelta. + + Requires an explicit tolerance as a timedelta for abs, or a float for rel. + Relative tolerance is not supported for datetime comparisons. + """ + + def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None: + __tracebackhide__ = True + if isinstance(expected, datetime) and rel is not None: + raise TypeError( + "pytest.approx() does not support relative tolerance for " + "datetime comparisons. Use abs=timedelta(...) instead." + ) + if nan_ok: + raise TypeError( + "pytest.approx() does not support nan_ok for " + "datetime/timedelta comparisons." + ) + if abs is None and rel is None: + raise TypeError( + "pytest.approx() requires an explicit tolerance for " + "datetime/timedelta comparisons: " + "e.g. approx(expected, abs=timedelta(seconds=1)) " + "or approx(expected, rel=0.01)" + ) + if abs is not None and not isinstance(abs, timedelta): + raise TypeError( + f"absolute tolerance for datetime/timedelta must be a " + f"timedelta, got {type(abs).__name__}" + ) + if abs is not None and abs < timedelta(0): + raise ValueError(f"absolute tolerance can't be negative: {abs}") + if rel is not None: + if not isinstance(rel, (int, float)): + raise TypeError( + f"relative tolerance for timedelta must be a " + f"number, got {type(rel).__name__}" + ) + if rel < 0: + raise ValueError(f"relative tolerance can't be negative: {rel}") + if math.isnan(rel): + raise ValueError("relative tolerance can't be NaN.") + # Compute the effective tolerance. abs_tolerance is a timedelta, rel * expected + # gives a timedelta (timedelta * float works in Python). + abs_tolerance = abs + rel_tolerance = rel * builtins.abs(expected) if rel is not None else None + if abs_tolerance is not None and rel_tolerance is not None: + tolerance = max(abs_tolerance, rel_tolerance) + else: + tolerance = abs_tolerance if abs_tolerance is not None else rel_tolerance + super().__init__(expected, rel=rel, abs=tolerance, nan_ok=False) + + def __repr__(self) -> str: + return f"{self.expected} ± {self.abs}" + + def __eq__(self, actual) -> bool: + try: + return bool(builtins.abs(self.expected - actual) <= self.abs) + except (TypeError, OverflowError): + return False + + def _yield_comparisons(self, actual): + yield actual, self.expected + + def _repr_compare(self, other_side: Any) -> list[str]: + try: + abs_diff = builtins.abs(self.expected - other_side) + except (TypeError, OverflowError): + abs_diff = "N/A" + return [ + "comparison failed", + f"Obtained: {other_side}", + f"Expected: {self.expected} ± {self.abs}", + f"Absolute difference: {abs_diff}", + f"Tolerance: {self.abs}", + ] + + +def approx( + expected: Any, + rel: float | Decimal | timedelta | None = None, + abs: float | Decimal | timedelta | None = None, + nan_ok: bool = False, +) -> ApproxBase: + """Assert that two numbers (or two ordered sequences of numbers) are equal to each other + within some tolerance. + + Due to the :doc:`python:tutorial/floatingpoint`, numbers that we + would intuitively expect to be equal are not always so:: + + >>> 0.1 + 0.2 == 0.3 + False + + This problem is commonly encountered when writing tests, e.g. when making + sure that floating-point values are what you expect them to be. One way to + deal with this problem is to assert that two floating-point numbers are + equal to within some appropriate tolerance:: + + >>> abs((0.1 + 0.2) - 0.3) < 1e-6 + True + + However, comparisons like this are tedious to write and difficult to + understand. Furthermore, absolute comparisons like the one above are + usually discouraged because there's no tolerance that works well for all + situations. ``1e-6`` is good for numbers around ``1``, but too small for + very big numbers and too big for very small ones. It's better to express + the tolerance as a fraction of the expected value, but relative comparisons + like that are even more difficult to write correctly and concisely. + + The ``approx`` class performs floating-point comparisons using a syntax + that's as intuitive as possible:: + + >>> from pytest import approx + >>> 0.1 + 0.2 == approx(0.3) + True + + The same syntax also works for ordered sequences of numbers:: + + >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6)) + True + + ``numpy`` arrays:: + + >>> import numpy as np # doctest: +SKIP + >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP + True + + And for a ``numpy`` array against a scalar:: + + >>> import numpy as np # doctest: +SKIP + >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP + True + + Only ordered sequences are supported, because ``approx`` needs + to infer the relative position of the sequences without ambiguity. This means + ``sets`` and other unordered sequences are not supported. + + Finally, dictionary *values* can also be compared:: + + >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6}) + True + + The comparison will be true if both mappings have the same keys and their + respective values match the expected tolerances. + + **Tolerances** + + By default, ``approx`` considers numbers within a relative tolerance of + ``1e-6`` (i.e. one part in a million) of its expected value to be equal. + This treatment would lead to surprising results if the expected value was + ``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``. + To handle this case less surprisingly, ``approx`` also considers numbers + within an absolute tolerance of ``1e-12`` of its expected value to be + equal. Infinity and NaN are special cases. Infinity is only considered + equal to itself, regardless of the relative tolerance. NaN is not + considered equal to anything by default, but you can make it be equal to + itself by setting the ``nan_ok`` argument to True. (This is meant to + facilitate comparing arrays that use NaN to mean "no data".) + + Both the relative and absolute tolerances can be changed by passing + arguments to the ``approx`` constructor:: + + >>> 1.0001 == approx(1) + False + >>> 1.0001 == approx(1, rel=1e-3) + True + >>> 1.0001 == approx(1, abs=1e-3) + True + + If you specify ``abs`` but not ``rel``, the comparison will not consider + the relative tolerance at all. In other words, two numbers that are within + the default relative tolerance of ``1e-6`` will still be considered unequal + if they exceed the specified absolute tolerance. If you specify both + ``abs`` and ``rel``, the numbers will be considered equal if either + tolerance is met:: + + >>> 1 + 1e-8 == approx(1) + True + >>> 1 + 1e-8 == approx(1, abs=1e-12) + False + >>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12) + True + + **Non-numeric types** + + You can also use ``approx`` to compare non-numeric types, or dicts and + sequences containing non-numeric types, in which case it falls back to + strict equality. This can be useful for comparing dicts and sequences that + can contain optional values:: + + >>> {"required": 1.0000005, "optional": None} == approx({"required": 1, "optional": None}) + True + >>> [None, 1.0000005] == approx([None,1]) + True + >>> ["foo", 1.0000005] == approx([None,1]) + False + + **datetime and timedelta** + + You can also use ``approx`` to compare :class:`~datetime.datetime` and + :class:`~datetime.timedelta` objects by specifying an absolute tolerance + as a :class:`~datetime.timedelta`:: + + >>> from datetime import datetime, timedelta + >>> dt1 = datetime(2024, 1, 1, 12, 0, 0) + >>> dt2 = datetime(2024, 1, 1, 12, 0, 0, 500000) + >>> dt1 == approx(dt2, abs=timedelta(seconds=1)) + True + + Note that ``rel`` is not supported for datetime comparisons. + For timedelta comparisons, ``rel`` is a number (not a timedelta) that + represents a relative tolerance -- a fraction of the expected value. + ``abs`` must be a ``timedelta`` object in both cases. + + .. versionadded:: 8.4 + + If you're thinking about using ``approx``, then you might want to know how + it compares to other good ways of comparing floating-point numbers. All of + these algorithms are based on relative and absolute tolerances and should + agree for the most part, but they do have meaningful differences: + + - ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative + tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute + tolerance is met. Because the relative tolerance is calculated w.r.t. + both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor + ``b`` is a "reference value"). You have to specify an absolute tolerance + if you want to compare to ``0.0`` because there is no tolerance by + default. More information: :py:func:`math.isclose`. + + - ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference + between ``a`` and ``b`` is less that the sum of the relative tolerance + w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance + is only calculated w.r.t. ``b``, this test is asymmetric and you can + think of ``b`` as the reference value. Support for comparing sequences + is provided by :py:func:`numpy.allclose`. More information: + :std:doc:`numpy:reference/generated/numpy.isclose`. + + - ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b`` + are within an absolute tolerance of ``1e-7``. No relative tolerance is + considered , so this function is not appropriate for very large or very + small numbers. Also, it's only available in subclasses of ``unittest.TestCase`` + and it's ugly because it doesn't follow PEP8. More information: + :py:meth:`unittest.TestCase.assertAlmostEqual`. + + - ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative + tolerance is met w.r.t. ``b`` or if the absolute tolerance is met. + Because the relative tolerance is only calculated w.r.t. ``b``, this test + is asymmetric and you can think of ``b`` as the reference value. In the + special case that you explicitly specify an absolute tolerance but not a + relative tolerance, only the absolute tolerance is considered. + + .. note:: + + ``approx`` can handle numpy arrays, but we recommend the + specialised test helpers in :std:doc:`numpy:reference/routines.testing` + if you need support for comparisons, NaNs, or ULP-based tolerances. + + To match strings using regex, you can use + `Matches `_ + from the + `re_assert package `_. + + + .. note:: + + Unlike built-in equality, this function considers + booleans unequal to numeric zero or one. For example:: + + >>> 1 == approx(True) + False + + .. warning:: + + .. versionchanged:: 3.2 + + In order to avoid inconsistent behavior, :py:exc:`TypeError` is + raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons. + The example below illustrates the problem:: + + assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10) + assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10) + + In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)`` + to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to + comparison. This is because the call hierarchy of rich comparisons + follows a fixed behavior. More information: :py:meth:`object.__ge__` + + .. versionchanged:: 3.7.1 + ``approx`` raises ``TypeError`` when it encounters a dict value or + sequence element of non-numeric type. + + .. versionchanged:: 6.1.0 + ``approx`` falls back to strict equality for non-numeric types instead + of raising ``TypeError``. + """ + # Delegate the comparison to a class that knows how to deal with the type + # of the expected value (e.g. int, float, list, dict, numpy.array, etc). + # + # The primary responsibility of these classes is to implement ``__eq__()`` + # and ``__repr__()``. The former is used to actually check if some + # "actual" value is equivalent to the given expected value within the + # allowed tolerance. The latter is used to show the user the expected + # value and tolerance, in the case that a test failed. + # + # The actual logic for making approximate comparisons can be found in + # ApproxScalar, which is used to compare individual numbers. All of the + # other Approx classes eventually delegate to this class. The ApproxBase + # class provides some convenient methods and overloads, but isn't really + # essential. + + __tracebackhide__ = True + + if isinstance(expected, Decimal): + cls: type[ApproxBase] = ApproxDecimal + elif isinstance(expected, Mapping): + cls = ApproxMapping + elif (np_array := _as_numpy_array(expected)) is not None: + expected = np_array + cls = ApproxNumpy + elif _is_sequence_like(expected): + cls = ApproxSequenceLike + elif isinstance(expected, Collection) and not isinstance(expected, str | bytes): + msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}" + raise TypeError(msg) + elif isinstance(expected, (datetime, timedelta)): + cls = ApproxTimedelta + else: + cls = ApproxScalar + + return cls(expected, rel, abs, nan_ok) + + +def _is_sequence_like(expected: object) -> bool: + return ( + hasattr(expected, "__getitem__") + and isinstance(expected, Sized) + and not isinstance(expected, str | bytes) + ) + + +def _as_numpy_array(obj: object) -> ndarray | None: + """ + Return an ndarray if the given object is implicitly convertible to ndarray, + and numpy is already imported, otherwise None. + """ + np: Any = sys.modules.get("numpy") + if np is not None: + # avoid infinite recursion on numpy scalars, which have __array__ + if np.isscalar(obj): + return None + elif isinstance(obj, np.ndarray): + return obj + elif hasattr(obj, "__array__") or hasattr(obj, "__array_interface__"): + return np.asarray(obj) + return None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/raises.py b/.venv-test/lib/python3.12/site-packages/_pytest/raises.py new file mode 100644 index 0000000..ef91cc2 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/raises.py @@ -0,0 +1,1505 @@ +from __future__ import annotations + +from abc import ABC +from abc import abstractmethod +import re +from re import Pattern +import sys +from textwrap import indent +from typing import Any +from typing import cast +from typing import final +from typing import Generic +from typing import get_args +from typing import get_origin +from typing import Literal +from typing import overload +from typing import TYPE_CHECKING +import warnings + +from _pytest._code import ExceptionInfo +from _pytest._code.code import stringify_exception +from _pytest.outcomes import fail +from _pytest.warning_types import PytestWarning + + +if TYPE_CHECKING: + from collections.abc import Callable + from collections.abc import Sequence + + # for some reason Sphinx does not play well with 'from types import TracebackType' + import types + from typing import TypeGuard + + from typing_extensions import ParamSpec + from typing_extensions import TypeVar + + P = ParamSpec("P") + + # this conditional definition is because we want to allow a TypeVar default + BaseExcT_co_default = TypeVar( + "BaseExcT_co_default", + bound=BaseException, + default=BaseException, + covariant=True, + ) + + # Use short name because it shows up in docs. + E = TypeVar("E", bound=BaseException, default=BaseException) +else: + from typing import TypeVar + + BaseExcT_co_default = TypeVar( + "BaseExcT_co_default", bound=BaseException, covariant=True + ) + +# RaisesGroup doesn't work with a default. +BaseExcT_co = TypeVar("BaseExcT_co", bound=BaseException, covariant=True) +BaseExcT_1 = TypeVar("BaseExcT_1", bound=BaseException) +BaseExcT_2 = TypeVar("BaseExcT_2", bound=BaseException) +ExcT_1 = TypeVar("ExcT_1", bound=Exception) +ExcT_2 = TypeVar("ExcT_2", bound=Exception) + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + from exceptiongroup import ExceptionGroup + + +# String patterns default to including the unicode flag. +_REGEX_NO_FLAGS = re.compile(r"").flags + + +# pytest.raises helper +@overload +def raises( + expected_exception: type[E] | tuple[type[E], ...], + *, + match: str | re.Pattern[str] | None = ..., + check: Callable[[E], bool] = ..., +) -> RaisesExc[E]: ... + + +@overload +def raises( + *, + match: str | re.Pattern[str], + # If exception_type is not provided, check() must do any typechecks itself. + check: Callable[[BaseException], bool] = ..., +) -> RaisesExc[BaseException]: ... + + +@overload +def raises(*, check: Callable[[BaseException], bool]) -> RaisesExc[BaseException]: ... + + +@overload +def raises( + expected_exception: type[E] | tuple[type[E], ...], + func: Callable[P, object], + *args: P.args, + **kwargs: P.kwargs, +) -> ExceptionInfo[E]: ... + + +def raises( + expected_exception: type[E] | tuple[type[E], ...] | None = None, + func: Callable[P, object] | None = None, + *args: Any, + **kwargs: Any, +) -> RaisesExc[BaseException] | ExceptionInfo[E]: + r"""Assert that a code block/function call raises an exception type, or one of its subclasses. + + :param expected_exception: + The expected exception type, or a tuple if one of multiple possible + exception types are expected. Note that subclasses of the passed exceptions + will also match. + + This is not a required parameter, you may opt to only use ``match`` and/or + ``check`` for verifying the raised exception. + + :kwparam str | re.Pattern[str] | None match: + If specified, a string containing a regular expression, + or a regular expression object, that is tested against the string + representation of the exception and its :pep:`678` `__notes__` + using :func:`re.search`. + + To match a literal string that may contain :ref:`special characters + `, the pattern can first be escaped with :func:`re.escape`. + + (This is only used when ``pytest.raises`` is used as a context manager, + and passed through to the function otherwise. + When using ``pytest.raises`` as a function, you can use: + ``pytest.raises(Exc, func, match="passed on").match("my pattern")``.) + + :kwparam Callable[[BaseException], bool] check: + + .. versionadded:: 8.4 + + If specified, a callable that will be called with the exception as a parameter + after checking the type and the match regex if specified. + If it returns ``True`` it will be considered a match, if not it will + be considered a failed match. + + + Use ``pytest.raises`` as a context manager, which will capture the exception of the given + type, or any of its subclasses:: + + >>> import pytest + >>> with pytest.raises(ZeroDivisionError): + ... 1/0 + + If the code block does not raise the expected exception (:class:`ZeroDivisionError` in the example + above), or no exception at all, the check will fail instead. + + You can also use the keyword argument ``match`` to assert that the + exception matches a text or regex:: + + >>> with pytest.raises(ValueError, match='must be 0 or None'): + ... raise ValueError("value must be 0 or None") + + >>> with pytest.raises(ValueError, match=r'must be \d+$'): + ... raise ValueError("value must be 42") + + The ``match`` argument searches the formatted exception string, which includes any + `PEP-678 `__ ``__notes__``: + + >>> with pytest.raises(ValueError, match=r"had a note added"): # doctest: +SKIP + ... e = ValueError("value must be 42") + ... e.add_note("had a note added") + ... raise e + + The ``check`` argument, if provided, must return True when passed the raised exception + for the match to be successful, otherwise an :exc:`AssertionError` is raised. + + >>> import errno + >>> with pytest.raises(OSError, check=lambda e: e.errno == errno.EACCES): + ... raise OSError(errno.EACCES, "no permission to view") + + The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the + details of the captured exception:: + + >>> with pytest.raises(ValueError) as exc_info: + ... raise ValueError("value must be 42") + >>> assert exc_info.type is ValueError + >>> assert exc_info.value.args[0] == "value must be 42" + + .. warning:: + + Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this:: + + # Careful, this will catch ANY exception raised. + with pytest.raises(Exception): + some_function() + + Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide + real bugs, where the user wrote this expecting a specific exception, but some other exception is being + raised due to a bug introduced during a refactoring. + + Avoid using ``pytest.raises`` to catch :class:`Exception` unless certain that you really want to catch + **any** exception raised. + + .. note:: + + When using ``pytest.raises`` as a context manager, it's worthwhile to + note that normal context manager rules apply and that the exception + raised *must* be the final line in the scope of the context manager. + Lines of code after that, within the scope of the context manager will + not be executed. For example:: + + >>> value = 15 + >>> with pytest.raises(ValueError) as exc_info: + ... if value > 10: + ... raise ValueError("value must be <= 10") + ... assert exc_info.type is ValueError # This will not execute. + + Instead, the following approach must be taken (note the difference in + scope):: + + >>> with pytest.raises(ValueError) as exc_info: + ... if value > 10: + ... raise ValueError("value must be <= 10") + ... + >>> assert exc_info.type is ValueError + + **Expecting exception groups** + + When expecting exceptions wrapped in :exc:`BaseExceptionGroup` or + :exc:`ExceptionGroup`, you should instead use :class:`pytest.RaisesGroup`. + + **Using with** ``pytest.mark.parametrize`` + + When using :ref:`pytest.mark.parametrize ref` + it is possible to parametrize tests such that + some runs raise an exception and others do not. + + See :ref:`parametrizing_conditional_raising` for an example. + + .. seealso:: + + :ref:`assertraises` for more examples and detailed discussion. + + .. note:: + Similar to caught exception objects in Python, explicitly clearing + local references to returned ``ExceptionInfo`` objects can + help the Python interpreter speed up its garbage collection. + + Clearing those references breaks a reference cycle + (``ExceptionInfo`` --> caught exception --> frame stack raising + the exception --> current frame stack --> local variables --> + ``ExceptionInfo``) which makes Python keep all objects referenced + from that cycle (including all local variables in the current + frame) alive until the next cyclic garbage collection run. + More detailed information can be found in the official Python + documentation for :ref:`the try statement `. + """ + __tracebackhide__ = True + + if func is None and not args: + if set(kwargs) - {"match", "check", "expected_exception"}: + msg = "Unexpected keyword arguments passed to pytest.raises: " + msg += ", ".join(sorted(kwargs)) + msg += "\nUse context-manager form instead?" + raise TypeError(msg) + + if expected_exception is None: + return RaisesExc(**kwargs) + return RaisesExc(expected_exception, **kwargs) + + if not expected_exception: + raise ValueError( + f"Expected an exception type or a tuple of exception types, but got `{expected_exception!r}`. " + f"Raising exceptions is already understood as failing the test, so you don't need " + f"any special code to say 'this should never raise an exception'." + ) + if not callable(func): + raise TypeError(f"{func!r} object (type: {type(func)}) must be callable") + with RaisesExc(expected_exception) as excinfo: + func(*args, **kwargs) + try: + return excinfo + finally: + del excinfo + + +# note: RaisesExc/RaisesGroup uses fail() internally, so this alias +# indicates (to [internal] plugins?) that `pytest.raises` will +# raise `_pytest.outcomes.Failed`, where +# `outcomes.Failed is outcomes.fail.Exception is raises.Exception` +# note: this is *not* the same as `_pytest.main.Failed` +# note: mypy does not recognize this attribute, and it's not possible +# to use a protocol/decorator like the others in outcomes due to +# https://github.com/python/mypy/issues/18715 +raises.Exception = fail.Exception # type: ignore[attr-defined] + + +def _match_pattern(match: Pattern[str]) -> str | Pattern[str]: + """Helper function to remove redundant `re.compile` calls when printing regex""" + return match.pattern if match.flags == _REGEX_NO_FLAGS else match + + +def repr_callable(fun: Callable[[BaseExcT_1], bool]) -> str: + """Get the repr of a ``check`` parameter. + + Split out so it can be monkeypatched (e.g. by hypothesis) + """ + return repr(fun) + + +def backquote(s: str) -> str: + return "`" + s + "`" + + +def _exception_type_name( + e: type[BaseException] | tuple[type[BaseException], ...], +) -> str: + if isinstance(e, type): + return e.__name__ + if len(e) == 1: + return e[0].__name__ + return "(" + ", ".join(ee.__name__ for ee in e) + ")" + + +def _check_raw_type( + expected_type: type[BaseException] | tuple[type[BaseException], ...] | None, + exception: BaseException, +) -> str | None: + if expected_type is None or expected_type == (): + return None + + if not isinstance( + exception, + expected_type, + ): + actual_type_str = backquote(_exception_type_name(type(exception)) + "()") + expected_type_str = backquote(_exception_type_name(expected_type)) + if ( + isinstance(exception, BaseExceptionGroup) + and isinstance(expected_type, type) + and not issubclass(expected_type, BaseExceptionGroup) + ): + return f"Unexpected nested {actual_type_str}, expected {expected_type_str}" + return f"{actual_type_str} is not an instance of {expected_type_str}" + return None + + +def is_fully_escaped(s: str) -> bool: + # we know we won't compile with re.VERBOSE, so whitespace doesn't need to be escaped + metacharacters = "{}()+.*?^$[]|" + # Strip all escape sequences (backslash + any char), then check if any + # metacharacter remains unescaped in the resulting string. + stripped = re.sub(r"\\.", "", s) + return not any(c in metacharacters for c in stripped) + + +def unescape(s: str) -> str: + return re.sub(r"\\([{}()+-.*?^$\[\]\s\\|])", r"\1", s) + + +# These classes conceptually differ from ExceptionInfo in that ExceptionInfo is tied, and +# constructed from, a particular exception - whereas these are constructed with expected +# exceptions, and later allow matching towards particular exceptions. +# But there's overlap in `ExceptionInfo.match` and `AbstractRaises._check_match`, as with +# `AbstractRaises.matches` and `ExceptionInfo.errisinstance`+`ExceptionInfo.group_contains`. +# The interaction between these classes should perhaps be improved. +class AbstractRaises(ABC, Generic[BaseExcT_co]): + """ABC with common functionality shared between RaisesExc and RaisesGroup""" + + def __init__( + self, + *, + match: str | Pattern[str] | None, + check: Callable[[BaseExcT_co], bool] | None, + ) -> None: + if isinstance(match, str): + # juggle error in order to avoid context to fail (necessary?) + re_error = None + try: + self.match: Pattern[str] | None = re.compile(match) + except re.error as e: + re_error = e + if re_error is not None: + fail(f"Invalid regex pattern provided to 'match': {re_error}") + if match == "": + warnings.warn( + PytestWarning( + "matching against an empty string will *always* pass. If you want " + "to check for an empty message you need to pass '^$'. If you don't " + "want to match you should pass `None` or leave out the parameter." + ), + stacklevel=2, + ) + else: + self.match = match + + # check if this is a fully escaped regex and has ^$ to match fully + # in which case we can do a proper diff on error + self.rawmatch: str | None = None + if isinstance(match, str) or ( + isinstance(match, Pattern) and match.flags == _REGEX_NO_FLAGS + ): + if isinstance(match, Pattern): + match = match.pattern + if ( + match + and match[0] == "^" + and match[-1] == "$" + and is_fully_escaped(match[1:-1]) + ): + self.rawmatch = unescape(match[1:-1]) + + self.check = check + self._fail_reason: str | None = None + + # used to suppress repeated printing of `repr(self.check)` + self._nested: bool = False + + # set in self._parse_exc + self.is_baseexception = False + + def _parse_exc( + self, exc: type[BaseExcT_1] | types.GenericAlias, expected: str + ) -> type[BaseExcT_1]: + if isinstance(exc, type) and issubclass(exc, BaseException): + if not issubclass(exc, Exception): + self.is_baseexception = True + return exc + # because RaisesGroup does not support variable number of exceptions there's + # still a use for RaisesExc(ExceptionGroup[Exception]). + origin_exc: type[BaseException] | None = get_origin(exc) + if origin_exc and issubclass(origin_exc, BaseExceptionGroup): + exc_type = get_args(exc)[0] + if ( + issubclass(origin_exc, ExceptionGroup) and exc_type in (Exception, Any) + ) or ( + issubclass(origin_exc, BaseExceptionGroup) + and exc_type in (BaseException, Any) + ): + if not issubclass(origin_exc, ExceptionGroup): + self.is_baseexception = True + return cast(type[BaseExcT_1], origin_exc) + else: + raise ValueError( + f"Only `ExceptionGroup[Exception]` or `BaseExceptionGroup[BaseException]` " + f"are accepted as generic types but got `{exc}`. " + f"As `raises` will catch all instances of the specified group regardless of the " + f"generic argument specific nested exceptions has to be checked " + f"with `RaisesGroup`." + ) + # unclear if the Type/ValueError distinction is even helpful here + msg = f"Expected {expected}, but got " + if isinstance(exc, type): # type: ignore[unreachable] + raise ValueError(msg + f"{exc.__name__!r}") + if isinstance(exc, BaseException): # type: ignore[unreachable] + raise TypeError(msg + f"an exception instance: {type(exc).__name__}") + raise TypeError(msg + repr(type(exc).__name__)) + + @property + def fail_reason(self) -> str | None: + """Set after a call to :meth:`matches` to give a human-readable reason for why the match failed. + When used as a context manager the string will be printed as the reason for the + test failing.""" + return self._fail_reason + + def _check_check( + self: AbstractRaises[BaseExcT_1], + exception: BaseExcT_1, + ) -> bool: + if self.check is None: + return True + + if self.check(exception): + return True + + check_repr = "" if self._nested else " " + repr_callable(self.check) + self._fail_reason = f"check{check_repr} did not return True" + return False + + # TODO: harmonize with ExceptionInfo.match + def _check_match(self, e: BaseException) -> bool: + if self.match is None or re.search( + self.match, + stringified_exception := stringify_exception( + e, include_subexception_msg=False + ), + ): + return True + + # if we're matching a group, make sure we're explicit to reduce confusion + # if they're trying to match an exception contained within the group + maybe_specify_type = ( + f" the `{_exception_type_name(type(e))}()`" + if isinstance(e, BaseExceptionGroup) + else "" + ) + if isinstance(self.rawmatch, str): + from _pytest.assertion.compare_text import _diff_text + from _pytest.assertion.highlight import dummy_highlighter + from _pytest.assertion.util import _config + from _pytest.config import Config + + verbose = ( + _config.get_verbosity(Config.VERBOSITY_ASSERTIONS) + if _config is not None + else 0 + ) + diff = list( + _diff_text( + self.rawmatch, stringified_exception, dummy_highlighter, verbose + ) + ) + self._fail_reason = ("\n" if diff[0][0] == "-" else "") + "\n".join(diff) + return False + + self._fail_reason = ( + f"Regex pattern did not match{maybe_specify_type}.\n" + f" Expected regex: {_match_pattern(self.match)!r}\n" + f" Actual message: {stringified_exception!r}" + ) + if _match_pattern(self.match) == stringified_exception: + self._fail_reason += "\n Did you mean to `re.escape()` the regex?" + return False + + @abstractmethod + def matches( + self: AbstractRaises[BaseExcT_1], exception: BaseException + ) -> TypeGuard[BaseExcT_1]: + """Check if an exception matches the requirements of this AbstractRaises. + If it fails, :meth:`AbstractRaises.fail_reason` should be set. + """ + + +@final +class RaisesExc(AbstractRaises[BaseExcT_co_default]): + """ + .. versionadded:: 8.4 + + + This is the class constructed when calling :func:`pytest.raises`, but may be used + directly as a helper class with :class:`RaisesGroup` when you want to specify + requirements on sub-exceptions. + + You don't need this if you only want to specify the type, since :class:`RaisesGroup` + accepts ``type[BaseException]``. + + :param type[BaseException] | tuple[type[BaseException]] | None expected_exception: + The expected type, or one of several possible types. + May be ``None`` in order to only make use of ``match`` and/or ``check`` + + The type is checked with :func:`isinstance`, and does not need to be an exact match. + If that is wanted you can use the ``check`` parameter. + + :kwparam str | Pattern[str] match: + A regex to match. + + :kwparam Callable[[BaseException], bool] check: + If specified, a callable that will be called with the exception as a parameter + after checking the type and the match regex if specified. + If it returns ``True`` it will be considered a match, if not it will + be considered a failed match. + + :meth:`RaisesExc.matches` can also be used standalone to check individual exceptions. + + Examples:: + + with RaisesGroup(RaisesExc(ValueError, match="string")) + ... + with RaisesGroup(RaisesExc(check=lambda x: x.args == (3, "hello"))): + ... + with RaisesGroup(RaisesExc(check=lambda x: type(x) is ValueError)): + ... + """ + + # Trio bundled hypothesis monkeypatching, we will probably instead assume that + # hypothesis will handle that in their pytest plugin by the time this is released. + # Alternatively we could add a version of get_pretty_function_description ourselves + # https://github.com/HypothesisWorks/hypothesis/blob/8ced2f59f5c7bea3344e35d2d53e1f8f8eb9fcd8/hypothesis-python/src/hypothesis/internal/reflection.py#L439 + + # At least one of the three parameters must be passed. + @overload + def __init__( + self, + expected_exception: ( + type[BaseExcT_co_default] | tuple[type[BaseExcT_co_default], ...] + ), + /, + *, + match: str | Pattern[str] | None = ..., + check: Callable[[BaseExcT_co_default], bool] | None = ..., + ) -> None: ... + + @overload + def __init__( + self: RaisesExc[BaseException], # Give E a value. + /, + *, + match: str | Pattern[str] | None, + # If exception_type is not provided, check() must do any typechecks itself. + check: Callable[[BaseException], bool] | None = ..., + ) -> None: ... + + @overload + def __init__(self, /, *, check: Callable[[BaseException], bool]) -> None: ... + + def __init__( + self, + expected_exception: ( + type[BaseExcT_co_default] | tuple[type[BaseExcT_co_default], ...] | None + ) = None, + /, + *, + match: str | Pattern[str] | None = None, + check: Callable[[BaseExcT_co_default], bool] | None = None, + ): + super().__init__(match=match, check=check) + if isinstance(expected_exception, tuple): + expected_exceptions = expected_exception + elif expected_exception is None: + expected_exceptions = () + else: + expected_exceptions = (expected_exception,) + + if (expected_exceptions == ()) and match is None and check is None: + raise ValueError("You must specify at least one parameter to match on.") + + self.expected_exceptions = tuple( + self._parse_exc(e, expected="a BaseException type") + for e in expected_exceptions + ) + + self._just_propagate = False + + def matches( + self, + exception: BaseException | None, + ) -> TypeGuard[BaseExcT_co_default]: + """Check if an exception matches the requirements of this :class:`RaisesExc`. + If it fails, :attr:`RaisesExc.fail_reason` will be set. + + Examples:: + + assert RaisesExc(ValueError).matches(my_exception): + # is equivalent to + assert isinstance(my_exception, ValueError) + + # this can be useful when checking e.g. the ``__cause__`` of an exception. + with pytest.raises(ValueError) as excinfo: + ... + assert RaisesExc(SyntaxError, match="foo").matches(excinfo.value.__cause__) + # above line is equivalent to + assert isinstance(excinfo.value.__cause__, SyntaxError) + assert re.search("foo", str(excinfo.value.__cause__) + + """ + self._just_propagate = False + if exception is None: + self._fail_reason = "exception is None" + return False + if not self._check_type(exception): + self._just_propagate = True + return False + + if not self._check_match(exception): + return False + + return self._check_check(exception) + + def __repr__(self) -> str: + parameters = [] + if self.expected_exceptions: + parameters.append(_exception_type_name(self.expected_exceptions)) + if self.match is not None: + # If no flags were specified, discard the redundant re.compile() here. + parameters.append( + f"match={_match_pattern(self.match)!r}", + ) + if self.check is not None: + parameters.append(f"check={repr_callable(self.check)}") + return f"RaisesExc({', '.join(parameters)})" + + def _check_type(self, exception: BaseException) -> TypeGuard[BaseExcT_co_default]: + self._fail_reason = _check_raw_type(self.expected_exceptions, exception) + return self._fail_reason is None + + def __enter__(self) -> ExceptionInfo[BaseExcT_co_default]: + self.excinfo: ExceptionInfo[BaseExcT_co_default] = ExceptionInfo.for_later() + return self.excinfo + + # TODO: move common code into superclass + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> bool: + __tracebackhide__ = True + if exc_type is None: + if not self.expected_exceptions: + fail("DID NOT RAISE any exception") + if len(self.expected_exceptions) == 1: + fail(f"DID NOT RAISE {self.expected_exceptions[0].__name__}") + else: + names = ", ".join(x.__name__ for x in self.expected_exceptions) + fail(f"DID NOT RAISE any of ({names})") + + assert self.excinfo is not None, ( + "Internal error - should have been constructed in __enter__" + ) + + if not self.matches(exc_val): + if self._just_propagate: + return False + raise AssertionError(self._fail_reason) from None + + # Cast to narrow the exception type now that it's verified.... + # even though the TypeGuard in self.matches should be narrowing + exc_info = cast( + "tuple[type[BaseExcT_co_default], BaseExcT_co_default, types.TracebackType]", + (exc_type, exc_val, exc_tb), + ) + self.excinfo.fill_unfilled(exc_info) + return True + + +@final +class RaisesGroup(AbstractRaises[BaseExceptionGroup[BaseExcT_co]]): + """ + .. versionadded:: 8.4 + + Contextmanager for checking for an expected :exc:`ExceptionGroup`. + This works similar to :func:`pytest.raises`, but allows for specifying the structure of an :exc:`ExceptionGroup`. + :meth:`ExceptionInfo.group_contains` also tries to handle exception groups, + but it is very bad at checking that you *didn't* get unexpected exceptions. + + The catching behaviour differs from :ref:`except* `, being much + stricter about the structure by default. + By using ``allow_unwrapped=True`` and ``flatten_subgroups=True`` you can match + :ref:`except* ` fully when expecting a single exception. + + :param args: + Any number of exception types, :class:`RaisesGroup` or :class:`RaisesExc` + to specify the exceptions contained in this exception. + All specified exceptions must be present in the raised group, *and no others*. + + If you expect a variable number of exceptions you need to use + :func:`pytest.raises(ExceptionGroup) ` and manually check + the contained exceptions. Consider making use of :meth:`RaisesExc.matches`. + + It does not care about the order of the exceptions, so + ``RaisesGroup(ValueError, TypeError)`` + is equivalent to + ``RaisesGroup(TypeError, ValueError)``. + :kwparam str | re.Pattern[str] | None match: + If specified, a string containing a regular expression, + or a regular expression object, that is tested against the string + representation of the exception group and its :pep:`678` `__notes__` + using :func:`re.search`. + + To match a literal string that may contain :ref:`special characters + `, the pattern can first be escaped with :func:`re.escape`. + + Note that " (5 subgroups)" will be stripped from the ``repr`` before matching. + :kwparam Callable[[E], bool] check: + If specified, a callable that will be called with the group as a parameter + after successfully matching the expected exceptions. If it returns ``True`` + it will be considered a match, if not it will be considered a failed match. + :kwparam bool allow_unwrapped: + If expecting a single exception or :class:`RaisesExc` it will match even + if the exception is not inside an exceptiongroup. + + Using this together with ``match``, ``check`` or expecting multiple exceptions + will raise an error. + :kwparam bool flatten_subgroups: + "flatten" any groups inside the raised exception group, extracting all exceptions + inside any nested groups, before matching. Without this it expects you to + fully specify the nesting structure by passing :class:`RaisesGroup` as expected + parameter. + + Examples:: + + with RaisesGroup(ValueError): + raise ExceptionGroup("", (ValueError(),)) + # match + with RaisesGroup( + ValueError, + ValueError, + RaisesExc(TypeError, match="^expected int$"), + match="^my group$", + ): + raise ExceptionGroup( + "my group", + [ + ValueError(), + TypeError("expected int"), + ValueError(), + ], + ) + # check + with RaisesGroup( + KeyboardInterrupt, + match="^hello$", + check=lambda x: isinstance(x.__cause__, ValueError), + ): + raise BaseExceptionGroup("hello", [KeyboardInterrupt()]) from ValueError + # nested groups + with RaisesGroup(RaisesGroup(ValueError)): + raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)) + + # flatten_subgroups + with RaisesGroup(ValueError, flatten_subgroups=True): + raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)) + + # allow_unwrapped + with RaisesGroup(ValueError, allow_unwrapped=True): + raise ValueError + + + :meth:`RaisesGroup.matches` can also be used directly to check a standalone exception group. + + + The matching algorithm is greedy, which means cases such as this may fail:: + + with RaisesGroup(ValueError, RaisesExc(ValueError, match="hello")): + raise ExceptionGroup("", (ValueError("hello"), ValueError("goodbye"))) + + even though it generally does not care about the order of the exceptions in the group. + To avoid the above you should specify the first :exc:`ValueError` with a :class:`RaisesExc` as well. + + .. note:: + When raised exceptions don't match the expected ones, you'll get a detailed error + message explaining why. This includes ``repr(check)`` if set, which in Python can be + overly verbose, showing memory locations etc etc. + + If installed and imported (in e.g. ``conftest.py``), the ``hypothesis`` library will + monkeypatch this output to provide shorter & more readable repr's. + """ + + # allow_unwrapped=True requires: singular exception, exception not being + # RaisesGroup instance, match is None, check is None + @overload + def __init__( + self, + expected_exception: type[BaseExcT_co] | RaisesExc[BaseExcT_co], + /, + *, + allow_unwrapped: Literal[True], + flatten_subgroups: bool = False, + ) -> None: ... + + # flatten_subgroups = True also requires no nested RaisesGroup + @overload + def __init__( + self, + expected_exception: type[BaseExcT_co] | RaisesExc[BaseExcT_co], + /, + *other_exceptions: type[BaseExcT_co] | RaisesExc[BaseExcT_co], + flatten_subgroups: Literal[True], + match: str | Pattern[str] | None = None, + check: Callable[[BaseExceptionGroup[BaseExcT_co]], bool] | None = None, + ) -> None: ... + + # simplify the typevars if possible (the following 3 are equivalent but go simpler->complicated) + # ... the first handles RaisesGroup[ValueError], the second RaisesGroup[ExceptionGroup[ValueError]], + # the third RaisesGroup[ValueError | ExceptionGroup[ValueError]]. + # ... otherwise, we will get results like RaisesGroup[ValueError | ExceptionGroup[Never]] (I think) + # (technically correct but misleading) + @overload + def __init__( + self: RaisesGroup[ExcT_1], + expected_exception: type[ExcT_1] | RaisesExc[ExcT_1], + /, + *other_exceptions: type[ExcT_1] | RaisesExc[ExcT_1], + match: str | Pattern[str] | None = None, + check: Callable[[ExceptionGroup[ExcT_1]], bool] | None = None, + ) -> None: ... + + @overload + def __init__( + self: RaisesGroup[ExceptionGroup[ExcT_2]], + expected_exception: RaisesGroup[ExcT_2], + /, + *other_exceptions: RaisesGroup[ExcT_2], + match: str | Pattern[str] | None = None, + check: Callable[[ExceptionGroup[ExceptionGroup[ExcT_2]]], bool] | None = None, + ) -> None: ... + + @overload + def __init__( + self: RaisesGroup[ExcT_1 | ExceptionGroup[ExcT_2]], + expected_exception: type[ExcT_1] | RaisesExc[ExcT_1] | RaisesGroup[ExcT_2], + /, + *other_exceptions: type[ExcT_1] | RaisesExc[ExcT_1] | RaisesGroup[ExcT_2], + match: str | Pattern[str] | None = None, + check: ( + Callable[[ExceptionGroup[ExcT_1 | ExceptionGroup[ExcT_2]]], bool] | None + ) = None, + ) -> None: ... + + # same as the above 3 but handling BaseException + @overload + def __init__( + self: RaisesGroup[BaseExcT_1], + expected_exception: type[BaseExcT_1] | RaisesExc[BaseExcT_1], + /, + *other_exceptions: type[BaseExcT_1] | RaisesExc[BaseExcT_1], + match: str | Pattern[str] | None = None, + check: Callable[[BaseExceptionGroup[BaseExcT_1]], bool] | None = None, + ) -> None: ... + + @overload + def __init__( + self: RaisesGroup[BaseExceptionGroup[BaseExcT_2]], + expected_exception: RaisesGroup[BaseExcT_2], + /, + *other_exceptions: RaisesGroup[BaseExcT_2], + match: str | Pattern[str] | None = None, + check: ( + Callable[[BaseExceptionGroup[BaseExceptionGroup[BaseExcT_2]]], bool] | None + ) = None, + ) -> None: ... + + @overload + def __init__( + self: RaisesGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]], + expected_exception: type[BaseExcT_1] + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2], + /, + *other_exceptions: type[BaseExcT_1] + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2], + match: str | Pattern[str] | None = None, + check: ( + Callable[ + [BaseExceptionGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]]], + bool, + ] + | None + ) = None, + ) -> None: ... + + def __init__( + self: RaisesGroup[ExcT_1 | BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]], + expected_exception: type[BaseExcT_1] + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2], + /, + *other_exceptions: type[BaseExcT_1] + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2], + allow_unwrapped: bool = False, + flatten_subgroups: bool = False, + match: str | Pattern[str] | None = None, + check: ( + Callable[[BaseExceptionGroup[BaseExcT_1]], bool] + | Callable[[ExceptionGroup[ExcT_1]], bool] + | None + ) = None, + ): + # The type hint on the `self` and `check` parameters uses different formats + # that are *very* hard to reconcile while adhering to the overloads, so we cast + # it to avoid an error when passing it to super().__init__ + check = cast( + "Callable[[BaseExceptionGroup[ExcT_1|BaseExcT_1|BaseExceptionGroup[BaseExcT_2]]], bool]", + check, + ) + super().__init__(match=match, check=check) + self.allow_unwrapped = allow_unwrapped + self.flatten_subgroups: bool = flatten_subgroups + self.is_baseexception = False + + if allow_unwrapped and other_exceptions: + raise ValueError( + "You cannot specify multiple exceptions with `allow_unwrapped=True.`" + " If you want to match one of multiple possible exceptions you should" + " use a `RaisesExc`." + " E.g. `RaisesExc(check=lambda e: isinstance(e, (...)))`", + ) + if allow_unwrapped and isinstance(expected_exception, RaisesGroup): + raise ValueError( + "`allow_unwrapped=True` has no effect when expecting a `RaisesGroup`." + " You might want it in the expected `RaisesGroup`, or" + " `flatten_subgroups=True` if you don't care about the structure.", + ) + if allow_unwrapped and (match is not None or check is not None): + raise ValueError( + "`allow_unwrapped=True` bypasses the `match` and `check` parameters" + " if the exception is unwrapped. If you intended to match/check the" + " exception you should use a `RaisesExc` object. If you want to match/check" + " the exceptiongroup when the exception *is* wrapped you need to" + " do e.g. `if isinstance(exc.value, ExceptionGroup):" + " assert RaisesGroup(...).matches(exc.value)` afterwards.", + ) + + self.expected_exceptions: tuple[ + type[BaseExcT_co] | RaisesExc[BaseExcT_co] | RaisesGroup[BaseException], ... + ] = tuple( + self._parse_excgroup(e, "a BaseException type, RaisesExc, or RaisesGroup") + for e in ( + expected_exception, + *other_exceptions, + ) + ) + + def _parse_excgroup( + self, + exc: ( + type[BaseExcT_co] + | types.GenericAlias + | RaisesExc[BaseExcT_1] + | RaisesGroup[BaseExcT_2] + ), + expected: str, + ) -> type[BaseExcT_co] | RaisesExc[BaseExcT_1] | RaisesGroup[BaseExcT_2]: + # verify exception type and set `self.is_baseexception` + match exc: + case RaisesGroup() if self.flatten_subgroups: + raise ValueError( + "You cannot specify a nested structure inside a RaisesGroup with" + " `flatten_subgroups=True`. The parameter will flatten subgroups" + " in the raised exceptiongroup before matching, which would never" + " match a nested structure.", + ) + case RaisesGroup() | RaisesExc(): + self.is_baseexception |= exc.is_baseexception + exc._nested = True + return exc + case tuple(): + raise TypeError( + f"Expected {expected}, but got {type(exc).__name__!r}.\n" + "RaisesGroup does not support tuples of exception types when expecting one of " + "several possible exception types like RaisesExc.\n" + "If you meant to expect a group with multiple exceptions, list them as separate arguments." + ) + case _: + return super()._parse_exc(exc, expected) + + @overload + def __enter__( + self: RaisesGroup[ExcT_1], + ) -> ExceptionInfo[ExceptionGroup[ExcT_1]]: ... + @overload + def __enter__( + self: RaisesGroup[BaseExcT_1], + ) -> ExceptionInfo[BaseExceptionGroup[BaseExcT_1]]: ... + + def __enter__(self) -> ExceptionInfo[BaseExceptionGroup[BaseException]]: + self.excinfo: ExceptionInfo[BaseExceptionGroup[BaseExcT_co]] = ( + ExceptionInfo.for_later() + ) + return self.excinfo + + def __repr__(self) -> str: + reqs = [ + e.__name__ if isinstance(e, type) else repr(e) + for e in self.expected_exceptions + ] + if self.allow_unwrapped: + reqs.append(f"allow_unwrapped={self.allow_unwrapped}") + if self.flatten_subgroups: + reqs.append(f"flatten_subgroups={self.flatten_subgroups}") + if self.match is not None: + # If no flags were specified, discard the redundant re.compile() here. + reqs.append(f"match={_match_pattern(self.match)!r}") + if self.check is not None: + reqs.append(f"check={repr_callable(self.check)}") + return f"RaisesGroup({', '.join(reqs)})" + + def _unroll_exceptions( + self, + exceptions: Sequence[BaseException], + ) -> Sequence[BaseException]: + """Used if `flatten_subgroups=True`.""" + res: list[BaseException] = [] + for exc in exceptions: + if isinstance(exc, BaseExceptionGroup): + res.extend(self._unroll_exceptions(exc.exceptions)) + + else: + res.append(exc) + return res + + @overload + def matches( + self: RaisesGroup[ExcT_1], + exception: BaseException | None, + ) -> TypeGuard[ExceptionGroup[ExcT_1]]: ... + @overload + def matches( + self: RaisesGroup[BaseExcT_1], + exception: BaseException | None, + ) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ... + + def matches( + self, + exception: BaseException | None, + ) -> bool: + """Check if an exception matches the requirements of this RaisesGroup. + If it fails, `RaisesGroup.fail_reason` will be set. + + Example:: + + with pytest.raises(TypeError) as excinfo: + ... + assert RaisesGroup(ValueError).matches(excinfo.value.__cause__) + # the above line is equivalent to + myexc = excinfo.value.__cause + assert isinstance(myexc, BaseExceptionGroup) + assert len(myexc.exceptions) == 1 + assert isinstance(myexc.exceptions[0], ValueError) + """ + self._fail_reason = None + if exception is None: + self._fail_reason = "exception is None" + return False + if not isinstance(exception, BaseExceptionGroup): + # we opt to only print type of the exception here, as the repr would + # likely be quite long + not_group_msg = f"`{type(exception).__name__}()` is not an exception group" + if len(self.expected_exceptions) > 1: + self._fail_reason = not_group_msg + return False + # if we have 1 expected exception, check if it would work even if + # allow_unwrapped is not set + res = self._check_expected(self.expected_exceptions[0], exception) + if res is None and self.allow_unwrapped: + return True + + if res is None: + self._fail_reason = ( + f"{not_group_msg}, but would match with `allow_unwrapped=True`" + ) + elif self.allow_unwrapped: + self._fail_reason = res + else: + self._fail_reason = not_group_msg + return False + + actual_exceptions: Sequence[BaseException] = exception.exceptions + if self.flatten_subgroups: + actual_exceptions = self._unroll_exceptions(actual_exceptions) + + if not self._check_match(exception): + self._fail_reason = cast(str, self._fail_reason) + old_reason = self._fail_reason + if ( + len(actual_exceptions) == len(self.expected_exceptions) == 1 + and isinstance(expected := self.expected_exceptions[0], type) + and isinstance(actual := actual_exceptions[0], expected) + and self._check_match(actual) + ): + assert self.match is not None, "can't be None if _check_match failed" + assert self._fail_reason is old_reason is not None + self._fail_reason += ( + f"\n" + f" but matched the expected `{self._repr_expected(expected)}`.\n" + f" You might want " + f"`RaisesGroup(RaisesExc({expected.__name__}, match={_match_pattern(self.match)!r}))`" + ) + else: + self._fail_reason = old_reason + return False + + # do the full check on expected exceptions + if not self._check_exceptions( + exception, + actual_exceptions, + ): + self._fail_reason = cast(str, self._fail_reason) + assert self._fail_reason is not None + old_reason = self._fail_reason + # if we're not expecting a nested structure, and there is one, do a second + # pass where we try flattening it + if ( + not self.flatten_subgroups + and not any( + isinstance(e, RaisesGroup) for e in self.expected_exceptions + ) + and any(isinstance(e, BaseExceptionGroup) for e in actual_exceptions) + and self._check_exceptions( + exception, + self._unroll_exceptions(exception.exceptions), + ) + ): + # only indent if it's a single-line reason. In a multi-line there's already + # indented lines that this does not belong to. + indent = " " if "\n" not in self._fail_reason else "" + self._fail_reason = ( + old_reason + + f"\n{indent}Did you mean to use `flatten_subgroups=True`?" + ) + else: + self._fail_reason = old_reason + return False + + # Only run `self.check` once we know `exception` is of the correct type. + if not self._check_check(exception): + reason = ( + cast(str, self._fail_reason) + f" on the {type(exception).__name__}" + ) + if ( + len(actual_exceptions) == len(self.expected_exceptions) == 1 + and isinstance(expected := self.expected_exceptions[0], type) + # we explicitly break typing here :) + and self._check_check(actual_exceptions[0]) # type: ignore[arg-type] + ): + self._fail_reason = reason + ( + f", but did return True for the expected {self._repr_expected(expected)}." + f" You might want RaisesGroup(RaisesExc({expected.__name__}, check=<...>))" + ) + else: + self._fail_reason = reason + return False + + return True + + @staticmethod + def _check_expected( + expected_type: ( + type[BaseException] | RaisesExc[BaseException] | RaisesGroup[BaseException] + ), + exception: BaseException, + ) -> str | None: + """Helper method for `RaisesGroup.matches` and `RaisesGroup._check_exceptions` + to check one of potentially several expected exceptions.""" + if isinstance(expected_type, type): + return _check_raw_type(expected_type, exception) + res = expected_type.matches(exception) + if res: + return None + assert expected_type.fail_reason is not None + if expected_type.fail_reason.startswith("\n"): + return f"\n{expected_type!r}: {indent(expected_type.fail_reason, ' ')}" + return f"{expected_type!r}: {expected_type.fail_reason}" + + @staticmethod + def _repr_expected(e: type[BaseException] | AbstractRaises[BaseException]) -> str: + """Get the repr of an expected type/RaisesExc/RaisesGroup, but we only want + the name if it's a type""" + if isinstance(e, type): + return _exception_type_name(e) + return repr(e) + + @overload + def _check_exceptions( + self: RaisesGroup[ExcT_1], + _exception: Exception, + actual_exceptions: Sequence[Exception], + ) -> TypeGuard[ExceptionGroup[ExcT_1]]: ... + @overload + def _check_exceptions( + self: RaisesGroup[BaseExcT_1], + _exception: BaseException, + actual_exceptions: Sequence[BaseException], + ) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ... + + def _check_exceptions( + self, + _exception: BaseException, + actual_exceptions: Sequence[BaseException], + ) -> bool: + """Helper method for RaisesGroup.matches that attempts to pair up expected and actual exceptions""" + # The _exception parameter is not used, but necessary for the TypeGuard + + # full table with all results + results = ResultHolder(self.expected_exceptions, actual_exceptions) + + # (indexes of) raised exceptions that haven't (yet) found an expected + remaining_actual = list(range(len(actual_exceptions))) + # (indexes of) expected exceptions that haven't found a matching raised + failed_expected: list[int] = [] + # successful greedy matches + matches: dict[int, int] = {} + + # loop over expected exceptions first to get a more predictable result + for i_exp, expected in enumerate(self.expected_exceptions): + for i_rem in remaining_actual: + res = self._check_expected(expected, actual_exceptions[i_rem]) + results.set_result(i_exp, i_rem, res) + if res is None: + remaining_actual.remove(i_rem) + matches[i_exp] = i_rem + break + else: + failed_expected.append(i_exp) + + # All exceptions matched up successfully + if not remaining_actual and not failed_expected: + return True + + # in case of a single expected and single raised we simplify the output + if 1 == len(actual_exceptions) == len(self.expected_exceptions): + assert not matches + self._fail_reason = res + return False + + # The test case is failing, so we can do a slow and exhaustive check to find + # duplicate matches etc that will be helpful in debugging + for i_exp, expected in enumerate(self.expected_exceptions): + for i_actual, actual in enumerate(actual_exceptions): + if results.has_result(i_exp, i_actual): + continue + results.set_result( + i_exp, i_actual, self._check_expected(expected, actual) + ) + + successful_str = ( + f"{len(matches)} matched exception{'s' if len(matches) > 1 else ''}. " + if matches + else "" + ) + + # all expected were found + if not failed_expected and results.no_match_for_actual(remaining_actual): + self._fail_reason = ( + f"{successful_str}Unexpected exception(s):" + f" {[actual_exceptions[i] for i in remaining_actual]!r}" + ) + return False + # all raised exceptions were expected + if not remaining_actual and results.no_match_for_expected(failed_expected): + no_match_for_str = ", ".join( + self._repr_expected(self.expected_exceptions[i]) + for i in failed_expected + ) + self._fail_reason = f"{successful_str}Too few exceptions raised, found no match for: [{no_match_for_str}]" + return False + + # if there's only one remaining and one failed, and the unmatched didn't match anything else, + # we elect to only print why the remaining and the failed didn't match. + if ( + 1 == len(remaining_actual) == len(failed_expected) + and results.no_match_for_actual(remaining_actual) + and results.no_match_for_expected(failed_expected) + ): + self._fail_reason = f"{successful_str}{results.get_result(failed_expected[0], remaining_actual[0])}" + return False + + # there's both expected and raised exceptions without matches + s = "" + if matches: + s += f"\n{successful_str}" + indent_1 = " " * 2 + indent_2 = " " * 4 + + if not remaining_actual: + s += "\nToo few exceptions raised!" + elif not failed_expected: + s += "\nUnexpected exception(s)!" + + if failed_expected: + s += "\nThe following expected exceptions did not find a match:" + rev_matches = {v: k for k, v in matches.items()} + for i_failed in failed_expected: + s += ( + f"\n{indent_1}{self._repr_expected(self.expected_exceptions[i_failed])}" + ) + for i_actual, actual in enumerate(actual_exceptions): + if results.get_result(i_failed, i_actual) is None: + # we print full repr of match target + s += ( + f"\n{indent_2}It matches {backquote(repr(actual))} which was paired with " + + backquote( + self._repr_expected( + self.expected_exceptions[rev_matches[i_actual]] + ) + ) + ) + + if remaining_actual: + s += "\nThe following raised exceptions did not find a match" + for i_actual in remaining_actual: + s += f"\n{indent_1}{actual_exceptions[i_actual]!r}:" + for i_exp, expected in enumerate(self.expected_exceptions): + res = results.get_result(i_exp, i_actual) + if i_exp in failed_expected: + assert res is not None + if res[0] != "\n": + s += "\n" + s += indent(res, indent_2) + if res is None: + # we print full repr of match target + s += ( + f"\n{indent_2}It matches {backquote(self._repr_expected(expected))} " + f"which was paired with {backquote(repr(actual_exceptions[matches[i_exp]]))}" + ) + + if len(self.expected_exceptions) == len(actual_exceptions) and possible_match( + results + ): + s += ( + "\nThere exist a possible match when attempting an exhaustive check," + " but RaisesGroup uses a greedy algorithm. " + "Please make your expected exceptions more stringent with `RaisesExc` etc" + " so the greedy algorithm can function." + ) + self._fail_reason = s + return False + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> bool: + __tracebackhide__ = True + if exc_type is None: + fail(f"DID NOT RAISE any exception, expected `{self.expected_type()}`") + + assert self.excinfo is not None, ( + "Internal error - should have been constructed in __enter__" + ) + + # group_str is the only thing that differs between RaisesExc and RaisesGroup... + # I might just scrap it? Or make it part of fail_reason + group_str = ( + "(group)" + if self.allow_unwrapped and not issubclass(exc_type, BaseExceptionGroup) + else "group" + ) + + if not self.matches(exc_val): + fail(f"Raised exception {group_str} did not match: {self._fail_reason}") + + # Cast to narrow the exception type now that it's verified.... + # even though the TypeGuard in self.matches should be narrowing + exc_info = cast( + "tuple[type[BaseExceptionGroup[BaseExcT_co]], BaseExceptionGroup[BaseExcT_co], types.TracebackType]", + (exc_type, exc_val, exc_tb), + ) + self.excinfo.fill_unfilled(exc_info) + return True + + def expected_type(self) -> str: + subexcs = [] + for e in self.expected_exceptions: + if isinstance(e, RaisesExc): + subexcs.append(repr(e)) + elif isinstance(e, RaisesGroup): + subexcs.append(e.expected_type()) + elif isinstance(e, type): + subexcs.append(e.__name__) + else: # pragma: no cover + raise AssertionError("unknown type") + group_type = "Base" if self.is_baseexception else "" + return f"{group_type}ExceptionGroup({', '.join(subexcs)})" + + +@final +class NotChecked: + """Singleton for unchecked values in ResultHolder""" + + +class ResultHolder: + """Container for results of checking exceptions. + Used in RaisesGroup._check_exceptions and possible_match. + """ + + def __init__( + self, + expected_exceptions: tuple[ + type[BaseException] | AbstractRaises[BaseException], ... + ], + actual_exceptions: Sequence[BaseException], + ) -> None: + self.results: list[list[str | type[NotChecked] | None]] = [ + [NotChecked for _ in expected_exceptions] for _ in actual_exceptions + ] + + def set_result(self, expected: int, actual: int, result: str | None) -> None: + self.results[actual][expected] = result + + def get_result(self, expected: int, actual: int) -> str | None: + res = self.results[actual][expected] + assert res is not NotChecked + return res + + def has_result(self, expected: int, actual: int) -> bool: + return self.results[actual][expected] is not NotChecked + + def no_match_for_expected(self, expected: list[int]) -> bool: + for i in expected: + for actual_results in self.results: + assert actual_results[i] is not NotChecked + if actual_results[i] is None: + return False + return True + + def no_match_for_actual(self, actual: list[int]) -> bool: + for i in actual: + for res in self.results[i]: + assert res is not NotChecked + if res is None: + return False + return True + + +def possible_match(results: ResultHolder, used: set[int] | None = None) -> bool: + if used is None: + used = set() + curr_row = len(used) + if curr_row == len(results.results): + return True + return any( + val is None and i not in used and possible_match(results, used | {i}) + for (i, val) in enumerate(results.results[curr_row]) + ) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/recwarn.py b/.venv-test/lib/python3.12/site-packages/_pytest/recwarn.py new file mode 100644 index 0000000..c3cb10b --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/recwarn.py @@ -0,0 +1,379 @@ +# mypy: allow-untyped-defs +"""Record warnings during test function execution.""" + +from __future__ import annotations + +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterator +from pprint import pformat +import re +from types import TracebackType +from typing import Any +from typing import final +from typing import overload +from typing import TYPE_CHECKING +from typing import TypeVar + + +if TYPE_CHECKING: + from typing_extensions import ParamSpec + from typing_extensions import Self + + P = ParamSpec("P") + +import warnings + +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.outcomes import Exit +from _pytest.outcomes import fail + + +T = TypeVar("T") + + +@fixture +def recwarn() -> Generator[WarningsRecorder]: + """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. + + See :ref:`warnings` for information on warning categories. + """ + wrec = WarningsRecorder(_ispytest=True) + with wrec: + warnings.simplefilter("default") + yield wrec + + +@overload +def deprecated_call( + *, match: str | re.Pattern[str] | None = ... +) -> WarningsRecorder: ... + + +@overload +def deprecated_call(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ... + + +def deprecated_call( + func: Callable[..., Any] | None = None, *args: Any, **kwargs: Any +) -> WarningsRecorder | Any: + """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``. + + This function can be used as a context manager:: + + >>> import warnings + >>> def api_call_v2(): + ... warnings.warn('use v3 of this api', DeprecationWarning) + ... return 200 + + >>> import pytest + >>> with pytest.deprecated_call(): + ... assert api_call_v2() == 200 + >>> with pytest.deprecated_call(match="^use v3 of this api$") as warning_messages: + ... assert api_call_v2() == 200 + + You may use the keyword argument ``match`` to assert + that the warning matches a text or regex. + + The return value is a list of :class:`warnings.WarningMessage` objects, + one for each warning emitted + (regardless of whether it is an ``expected_warning`` or not). + """ + __tracebackhide__ = True + dep_warnings = (DeprecationWarning, PendingDeprecationWarning, FutureWarning) + if func is None: + return warns(dep_warnings, *args, **kwargs) + + with warns(dep_warnings): + return func(*args, **kwargs) + + +@overload +def warns( + expected_warning: type[Warning] | tuple[type[Warning], ...] = ..., + *, + match: str | re.Pattern[str] | None = ..., +) -> WarningsChecker: ... + + +@overload +def warns( + expected_warning: type[Warning] | tuple[type[Warning], ...], + func: Callable[P, T], + *args: P.args, + **kwargs: P.kwargs, +) -> T: ... + + +def warns( + expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning, + func: Callable[..., object] | None = None, + *args: Any, + **kwargs: Any, +) -> WarningsChecker | Any: + r"""Assert that code raises a particular class of warning. + + Specifically, the parameter ``expected_warning`` can be a warning class or tuple + of warning classes, and the code inside the ``with`` block must issue at least one + warning of that class or classes. + + This helper produces a list of :class:`warnings.WarningMessage` objects, one for + each warning emitted (regardless of whether it is an ``expected_warning`` or not). + Since pytest 8.0, unmatched warnings are also re-emitted when the context closes. + + This function should be used as a context manager:: + + >>> import pytest + >>> with pytest.warns(RuntimeWarning): + ... warnings.warn("my warning", RuntimeWarning) + + The ``match`` keyword argument can be used to assert + that the warning matches a text or regex:: + + >>> with pytest.warns(UserWarning, match='must be 0 or None'): + ... warnings.warn("value must be 0 or None", UserWarning) + + >>> with pytest.warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("value must be 42", UserWarning) + + >>> with pytest.warns(UserWarning): # catch re-emitted warning + ... with pytest.warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("this is not here", UserWarning) + Traceback (most recent call last): + ... + Failed: Regex pattern did not match any of the 1 warnings emitted. + Regex: ... + Emitted warnings: ...UserWarning... + + **Using with** ``pytest.mark.parametrize`` + + When using :ref:`pytest.mark.parametrize ref` it is possible to parametrize tests + such that some runs raise a warning and others do not. + + This could be achieved in the same way as with exceptions, see + :ref:`parametrizing_conditional_raising` for an example. + + """ + __tracebackhide__ = True + if func is None and not args: + match: str | re.Pattern[str] | None = kwargs.pop("match", None) + if kwargs: + argnames = ", ".join(sorted(kwargs)) + raise TypeError( + f"Unexpected keyword arguments passed to pytest.warns: {argnames}" + "\nUse context-manager form instead?" + ) + return WarningsChecker(expected_warning, match_expr=match, _ispytest=True) + else: + if not callable(func): + raise TypeError(f"{func!r} object (type: {type(func)}) must be callable") + with WarningsChecker(expected_warning, _ispytest=True): + return func(*args, **kwargs) + + +class WarningsRecorder(warnings.catch_warnings): + """A context manager to record raised warnings. + + Each recorded warning is an instance of :class:`warnings.WarningMessage`. + + Adapted from `warnings.catch_warnings`. + + .. note:: + ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated + differently; see :ref:`ensuring_function_triggers`. + + """ + + def __init__(self, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + super().__init__(record=True) + self._entered = False + self._list: list[warnings.WarningMessage] = [] + + @property + def list(self) -> list[warnings.WarningMessage]: + """The list of recorded warnings.""" + return self._list + + def __getitem__(self, i: int) -> warnings.WarningMessage: + """Get a recorded warning by index.""" + return self._list[i] + + def __iter__(self) -> Iterator[warnings.WarningMessage]: + """Iterate through the recorded warnings.""" + return iter(self._list) + + def __len__(self) -> int: + """The number of recorded warnings.""" + return len(self._list) + + def pop(self, cls: type[Warning] = Warning) -> warnings.WarningMessage: + """Pop the first recorded warning which is an instance of ``cls``, + but not an instance of a child class of any other match. + Raises ``AssertionError`` if there is no match. + """ + best_idx: int | None = None + for i, w in enumerate(self._list): + if w.category == cls: + return self._list.pop(i) # exact match, stop looking + if issubclass(w.category, cls) and ( + best_idx is None + or not issubclass(w.category, self._list[best_idx].category) + ): + best_idx = i + if best_idx is not None: + return self._list.pop(best_idx) + __tracebackhide__ = True + raise AssertionError(f"{cls!r} not found in warning list") + + def clear(self) -> None: + """Clear the list of recorded warnings.""" + self._list[:] = [] + + # Type ignored because we basically want the `catch_warnings` generic type + # parameter to be ourselves but that is not possible(?). + def __enter__(self) -> Self: # type: ignore[override] + if self._entered: + __tracebackhide__ = True + raise RuntimeError(f"Cannot enter {self!r} twice") + _list = super().__enter__() + # record=True means it's None. + assert _list is not None + self._list = _list + warnings.simplefilter("always") + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if not self._entered: + __tracebackhide__ = True + raise RuntimeError(f"Cannot exit {self!r} without entering first") + + super().__exit__(exc_type, exc_val, exc_tb) + + # Built-in catch_warnings does not reset entered state so we do it + # manually here for this context manager to become reusable. + self._entered = False + + +@final +class WarningsChecker(WarningsRecorder): + def __init__( + self, + expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning, + match_expr: str | re.Pattern[str] | None = None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + super().__init__(_ispytest=True) + + msg = "exceptions must be derived from Warning, not %s" + if isinstance(expected_warning, tuple): + for exc in expected_warning: + if not issubclass(exc, Warning): + raise TypeError(msg % type(exc)) + expected_warning_tup = expected_warning + elif isinstance(expected_warning, type) and issubclass( + expected_warning, Warning + ): + expected_warning_tup = (expected_warning,) + else: + raise TypeError(msg % type(expected_warning)) + + self.expected_warning = expected_warning_tup + self.match_expr = match_expr + + def matches(self, warning: warnings.WarningMessage) -> bool: + assert self.expected_warning is not None + return issubclass(warning.category, self.expected_warning) and bool( + self.match_expr is None or re.search(self.match_expr, str(warning.message)) + ) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + super().__exit__(exc_type, exc_val, exc_tb) + + __tracebackhide__ = True + + # BaseExceptions like pytest.{skip,fail,xfail,exit} or Ctrl-C within + # pytest.warns should *not* trigger "DID NOT WARN" and get suppressed + # when the warning doesn't happen. Control-flow exceptions should always + # propagate. + if exc_val is not None and ( + not isinstance(exc_val, Exception) + # Exit is an Exception, not a BaseException, for some reason. + or isinstance(exc_val, Exit) + ): + return + + def found_str() -> str: + return pformat([record.message for record in self], indent=2) + + try: + if not any(issubclass(w.category, self.expected_warning) for w in self): + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" + f" Emitted warnings: {found_str()}." + ) + elif not any(self.matches(w) for w in self): + escape_hint = "" + if isinstance(self.match_expr, str) and any( + self.match_expr == str(w.message) + for w in self + if issubclass(w.category, self.expected_warning) + ): + escape_hint = "\n Did you mean to `re.escape()` the regex?" + fail( + f"Regex pattern did not match any of the {len(self)} warnings emitted.\n" + f" Regex: {self.match_expr!r}\n" + f" Emitted warnings: {found_str()}.{escape_hint}" + ) + finally: + # Whether or not any warnings matched, we want to re-emit all unmatched warnings. + for w in self: + if not self.matches(w): + warnings.warn_explicit( + message=w.message, + category=w.category, + filename=w.filename, + lineno=w.lineno, + module=w.__module__, + source=w.source, + ) + + # Currently in Python it is possible to pass other types than an + # `str` message when creating `Warning` instances, however this + # causes an exception when :func:`warnings.filterwarnings` is used + # to filter those warnings. See + # https://github.com/python/cpython/issues/103577 for a discussion. + # While this can be considered a bug in CPython, we put guards in + # pytest as the error message produced without this check in place + # is confusing (#10865). + for w in self: + if type(w.message) is not UserWarning: + # If the warning was of an incorrect type then `warnings.warn()` + # creates a UserWarning. Any other warning must have been specified + # explicitly. + continue + if not w.message.args: + # UserWarning() without arguments must have been specified explicitly. + continue + msg = w.message.args[0] + if isinstance(msg, str): + continue + # It's possible that UserWarning was explicitly specified, and + # its first argument was not a string. But that case can't be + # distinguished from an invalid type. + raise TypeError( + f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})" + ) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/reports.py b/.venv-test/lib/python3.12/site-packages/_pytest/reports.py new file mode 100644 index 0000000..011a69d --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/reports.py @@ -0,0 +1,694 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +from io import StringIO +import os +from pprint import pprint +import sys +from typing import Any +from typing import cast +from typing import final +from typing import Literal +from typing import NoReturn +from typing import TYPE_CHECKING + +from _pytest._code.code import ExceptionChainRepr +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ExceptionRepr +from _pytest._code.code import ReprEntry +from _pytest._code.code import ReprEntryNative +from _pytest._code.code import ReprExceptionInfo +from _pytest._code.code import ReprFileLocation +from _pytest._code.code import ReprFuncArgs +from _pytest._code.code import ReprLocals +from _pytest._code.code import ReprTraceback +from _pytest._code.code import TerminalRepr +from _pytest._io import TerminalWriter +from _pytest.config import Config +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.outcomes import fail +from _pytest.outcomes import skip + + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + + +if TYPE_CHECKING: + from typing_extensions import Self + + from _pytest.runner import CallInfo + + +def getworkerinfoline(node): + try: + return node._workerinfocache + except AttributeError: + d = node.workerinfo + ver = "{}.{}.{}".format(*d["version_info"][:3]) + node._workerinfocache = s = "[{}] {} -- Python {} {}".format( + d["id"], d["sysplatform"], ver, d["executable"] + ) + return s + + +class BaseReport: + when: str | None + location: tuple[str, int | None, str] | None + longrepr: ( + None | ExceptionInfo[BaseException] | tuple[str, int, str] | str | TerminalRepr + ) + sections: list[tuple[str, str]] + nodeid: str + outcome: Literal["passed", "failed", "skipped"] + + def __init__(self, **kw: Any) -> None: + self.__dict__.update(kw) + + if TYPE_CHECKING: + # Can have arbitrary fields given to __init__(). + def __getattr__(self, key: str) -> Any: ... + + def toterminal(self, out: TerminalWriter) -> None: + if hasattr(self, "node"): + worker_info = getworkerinfoline(self.node) + if worker_info: + out.line(worker_info) + + longrepr = self.longrepr + if longrepr is None: + return + + if hasattr(longrepr, "toterminal"): + longrepr_terminal = cast(TerminalRepr, longrepr) + longrepr_terminal.toterminal(out) + else: + try: + s = str(longrepr) + except UnicodeEncodeError: + s = "" + out.line(s) + + def get_sections(self, prefix: str) -> Iterator[tuple[str, str]]: + for name, content in self.sections: + if name.startswith(prefix): + yield prefix, content + + @property + def longreprtext(self) -> str: + """Read-only property that returns the full string representation of + ``longrepr``. + + .. versionadded:: 3.0 + """ + file = StringIO() + tw = TerminalWriter(file) + tw.hasmarkup = False + self.toterminal(tw) + exc = file.getvalue() + return exc.strip() + + @property + def caplog(self) -> str: + """Return captured log lines, if log capturing is enabled. + + .. versionadded:: 3.5 + """ + return "\n".join( + content for (prefix, content) in self.get_sections("Captured log") + ) + + @property + def capstdout(self) -> str: + """Return captured text from stdout, if capturing is enabled. + + .. versionadded:: 3.0 + """ + return "".join( + content for (prefix, content) in self.get_sections("Captured stdout") + ) + + @property + def capstderr(self) -> str: + """Return captured text from stderr, if capturing is enabled. + + .. versionadded:: 3.0 + """ + return "".join( + content for (prefix, content) in self.get_sections("Captured stderr") + ) + + @property + def passed(self) -> bool: + """Whether the outcome is passed.""" + return self.outcome == "passed" + + @property + def failed(self) -> bool: + """Whether the outcome is failed.""" + return self.outcome == "failed" + + @property + def skipped(self) -> bool: + """Whether the outcome is skipped.""" + return self.outcome == "skipped" + + @property + def fspath(self) -> str: + """The path portion of the reported node, as a string.""" + return self.nodeid.split("::")[0] + + @property + def count_towards_summary(self) -> bool: + """**Experimental** Whether this report should be counted towards the + totals shown at the end of the test session: "1 passed, 1 failure, etc". + + .. note:: + + This function is considered **experimental**, so beware that it is subject to changes + even in patch releases. + """ + return True + + @property + def head_line(self) -> str | None: + """**Experimental** The head line shown with longrepr output for this + report, more commonly during traceback representation during + failures:: + + ________ Test.foo ________ + + + In the example above, the head_line is "Test.foo". + + .. note:: + + This function is considered **experimental**, so beware that it is subject to changes + even in patch releases. + """ + if self.location is not None: + _fspath, _lineno, domain = self.location + return domain + return None + + def _get_verbose_word_with_markup( + self, config: Config, default_markup: Mapping[str, bool] + ) -> tuple[str, Mapping[str, bool]]: + _category, _short, verbose = config.hook.pytest_report_teststatus( + report=self, config=config + ) + + if isinstance(verbose, str): + return verbose, default_markup + + if isinstance(verbose, Sequence) and len(verbose) == 2: + word, markup = verbose + if isinstance(word, str) and isinstance(markup, Mapping): + return word, markup + + fail( # pragma: no cover + "pytest_report_teststatus() hook (from a plugin) returned " + f"an invalid verbose value: {verbose!r}.\nExpected either a string " + "or a tuple of (word, markup)." + ) + + def _to_json(self) -> dict[str, Any]: + """Return the contents of this report as a dict of builtin entries, + suitable for serialization. + + This was originally the serialize_report() function from xdist (ca03269). + + Experimental method. + """ + return _report_to_json(self) + + @classmethod + def _from_json(cls, reportdict: dict[str, object]) -> Self: + """Create either a TestReport or CollectReport, depending on the calling class. + + It is the callers responsibility to know which class to pass here. + + This was originally the serialize_report() function from xdist (ca03269). + + Experimental method. + """ + kwargs = _report_kwargs_from_json(reportdict) + return cls(**kwargs) + + +def _report_unserialization_failure( + type_name: str, report_class: type[BaseReport], reportdict +) -> NoReturn: + url = "https://github.com/pytest-dev/pytest/issues" + stream = StringIO() + pprint("-" * 100, stream=stream) + pprint(f"INTERNALERROR: Unknown entry type returned: {type_name}", stream=stream) + pprint(f"report_name: {report_class}", stream=stream) + pprint(reportdict, stream=stream) + pprint(f"Please report this bug at {url}", stream=stream) + pprint("-" * 100, stream=stream) + raise RuntimeError(stream.getvalue()) + + +def _format_failed_longrepr( + item: Item, call: CallInfo[None], excinfo: ExceptionInfo[BaseException] +): + if call.when == "call": + longrepr = item.repr_failure(excinfo) + else: + # Exception in setup or teardown. + longrepr = item._repr_failure_py( + excinfo, style=item.config.getoption("tbstyle", "auto") + ) + return longrepr + + +def _format_exception_group_all_skipped_longrepr( + item: Item, + excinfo: ExceptionInfo[BaseExceptionGroup[BaseException | BaseExceptionGroup]], +) -> tuple[str, int, str]: + r = excinfo._getreprcrash() + assert r is not None, ( + "There should always be a traceback entry for skipping a test." + ) + if all( + getattr(skip, "_use_item_location", False) for skip in excinfo.value.exceptions + ): + path, line = item.reportinfo()[:2] + assert line is not None + loc = (os.fspath(path), line + 1) + default_msg = "skipped" + else: + loc = (str(r.path), r.lineno) + default_msg = r.message + + # Get all unique skip messages. + msgs: list[str] = [] + for exception in excinfo.value.exceptions: + m = getattr(exception, "msg", None) or ( + exception.args[0] if exception.args else None + ) + if m and m not in msgs: + msgs.append(m) + + reason = "; ".join(msgs) if msgs else default_msg + longrepr = (*loc, reason) + return longrepr + + +class TestReport(BaseReport): + """Basic test report object (also used for setup and teardown calls if + they fail). + + Reports can contain arbitrary extra attributes. + """ + + __test__ = False + + # Defined by skipping plugin. + # xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish. + wasxfail: str + + def __init__( + self, + nodeid: str, + location: tuple[str, int | None, str], + keywords: Mapping[str, Any], + outcome: Literal["passed", "failed", "skipped"], + longrepr: None + | ExceptionInfo[BaseException] + | tuple[str, int, str] + | str + | TerminalRepr, + when: Literal["setup", "call", "teardown"], + sections: Iterable[tuple[str, str]] = (), + duration: float = 0, + start: float = 0, + stop: float = 0, + user_properties: Iterable[tuple[str, object]] | None = None, + **extra, + ) -> None: + #: Normalized collection nodeid. + self.nodeid = nodeid + + #: A (filesystempath, lineno, domaininfo) tuple indicating the + #: actual location of a test item - it might be different from the + #: collected one e.g. if a method is inherited from a different module. + #: The filesystempath may be relative to ``config.rootdir``. + #: The line number is 0-based. + self.location: tuple[str, int | None, str] = location + + #: A name -> value dictionary containing all keywords and + #: markers associated with a test invocation. + self.keywords: Mapping[str, Any] = keywords + + #: Test outcome, always one of "passed", "failed", "skipped". + self.outcome = outcome + + #: None or a failure representation. + self.longrepr = longrepr + + #: One of 'setup', 'call', 'teardown' to indicate runtest phase. + self.when: Literal["setup", "call", "teardown"] = when + + #: User properties is a list of tuples (name, value) that holds user + #: defined properties of the test. + self.user_properties = list(user_properties or []) + + #: Tuples of str ``(heading, content)`` with extra information + #: for the test report. Used by pytest to add text captured + #: from ``stdout``, ``stderr``, and intercepted logging events. May + #: be used by other plugins to add arbitrary information to reports. + self.sections = list(sections) + + #: Time it took to run just the test. + self.duration: float = duration + + #: The system time when the call started, in seconds since the epoch. + self.start: float = start + #: The system time when the call ended, in seconds since the epoch. + self.stop: float = stop + + self.__dict__.update(extra) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.nodeid!r} when={self.when!r} outcome={self.outcome!r}>" + + @classmethod + def from_item_and_call(cls, item: Item, call: CallInfo[None]) -> TestReport: + """Create and fill a TestReport with standard item and call info. + + :param item: The item. + :param call: The call info. + """ + when = call.when + # Remove "collect" from the Literal type -- only for collection calls. + assert when != "collect" + duration = call.duration + start = call.start + stop = call.stop + keywords = {x: 1 for x in item.keywords} + excinfo = call.excinfo + sections = [] + if not call.excinfo: + outcome: Literal["passed", "failed", "skipped"] = "passed" + longrepr: ( + None + | ExceptionInfo[BaseException] + | tuple[str, int, str] + | str + | TerminalRepr + ) = None + else: + if not isinstance(excinfo, ExceptionInfo): + outcome = "failed" + longrepr = excinfo + elif isinstance(excinfo.value, skip.Exception): + outcome = "skipped" + r = excinfo._getreprcrash() + assert r is not None, ( + "There should always be a traceback entry for skipping a test." + ) + if excinfo.value._use_item_location: + path, line = item.reportinfo()[:2] + assert line is not None + longrepr = (os.fspath(path), line + 1, r.message) + else: + longrepr = (str(r.path), r.lineno, r.message) + elif isinstance(excinfo.value, BaseExceptionGroup) and ( + excinfo.value.split(skip.Exception)[1] is None + ): + # All exceptions in the group are skip exceptions. + outcome = "skipped" + excinfo = cast( + ExceptionInfo[ + BaseExceptionGroup[BaseException | BaseExceptionGroup] + ], + excinfo, + ) + longrepr = _format_exception_group_all_skipped_longrepr(item, excinfo) + else: + outcome = "failed" + longrepr = _format_failed_longrepr(item, call, excinfo) + for rwhen, key, content in item._report_sections: + sections.append((f"Captured {key} {rwhen}", content)) + return cls( + item.nodeid, + item.location, + keywords, + outcome, + longrepr, + when, + sections, + duration, + start, + stop, + user_properties=item.user_properties, + ) + + +@final +class CollectReport(BaseReport): + """Collection report object. + + Reports can contain arbitrary extra attributes. + """ + + when = "collect" + + def __init__( + self, + nodeid: str, + outcome: Literal["passed", "failed", "skipped"], + longrepr: None + | ExceptionInfo[BaseException] + | tuple[str, int, str] + | str + | TerminalRepr, + result: list[Item | Collector] | None, + sections: Iterable[tuple[str, str]] = (), + **extra, + ) -> None: + #: Normalized collection nodeid. + self.nodeid = nodeid + + #: Test outcome, always one of "passed", "failed", "skipped". + self.outcome = outcome + + #: None or a failure representation. + self.longrepr = longrepr + + #: The collected items and collection nodes. + self.result = result or [] + + #: Tuples of str ``(heading, content)`` with extra information + #: for the test report. Used by pytest to add text captured + #: from ``stdout``, ``stderr``, and intercepted logging events. May + #: be used by other plugins to add arbitrary information to reports. + self.sections = list(sections) + + self.__dict__.update(extra) + + @property + def location( # type:ignore[override] + self, + ) -> tuple[str, int | None, str] | None: + return (self.fspath, None, self.fspath) + + def __repr__(self) -> str: + return f"" + + +class CollectErrorRepr(TerminalRepr): + def __init__(self, msg: str) -> None: + self.longrepr = msg + + def toterminal(self, out: TerminalWriter) -> None: + out.line(self.longrepr, red=True) + + +def pytest_report_to_serializable( + report: CollectReport | TestReport, +) -> dict[str, Any] | None: + if isinstance(report, TestReport | CollectReport): + data = report._to_json() + data["$report_type"] = report.__class__.__name__ + return data + # TODO: Check if this is actually reachable. + return None # type: ignore[unreachable] + + +def pytest_report_from_serializable( + data: dict[str, Any], +) -> CollectReport | TestReport | None: + if "$report_type" in data: + if data["$report_type"] == "TestReport": + return TestReport._from_json(data) + elif data["$report_type"] == "CollectReport": + return CollectReport._from_json(data) + assert False, "Unknown report_type unserialize data: {}".format( + data["$report_type"] + ) + return None + + +def _report_to_json(report: BaseReport) -> dict[str, Any]: + """Return the contents of this report as a dict of builtin entries, + suitable for serialization. + + This was originally the serialize_report() function from xdist (ca03269). + """ + + def serialize_repr_entry( + entry: ReprEntry | ReprEntryNative, + ) -> dict[str, Any]: + data = dataclasses.asdict(entry) + for key, value in data.items(): + if hasattr(value, "__dict__"): + data[key] = dataclasses.asdict(value) + entry_data = {"type": type(entry).__name__, "data": data} + return entry_data + + def serialize_repr_traceback(reprtraceback: ReprTraceback) -> dict[str, Any]: + result = dataclasses.asdict(reprtraceback) + result["reprentries"] = [ + serialize_repr_entry(x) for x in reprtraceback.reprentries + ] + return result + + def serialize_repr_crash( + reprcrash: ReprFileLocation | None, + ) -> dict[str, Any] | None: + if reprcrash is not None: + return dataclasses.asdict(reprcrash) + else: + return None + + def serialize_exception_longrepr(rep: BaseReport) -> dict[str, Any]: + assert rep.longrepr is not None + # TODO: Investigate whether the duck typing is really necessary here. + longrepr = cast(ExceptionRepr, rep.longrepr) + result: dict[str, Any] = { + "reprcrash": serialize_repr_crash(longrepr.reprcrash), + "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback), + "sections": longrepr.sections, + } + if isinstance(longrepr, ExceptionChainRepr): + result["chain"] = [] + for repr_traceback, repr_crash, description in longrepr.chain: + result["chain"].append( + ( + serialize_repr_traceback(repr_traceback), + serialize_repr_crash(repr_crash), + description, + ) + ) + else: + result["chain"] = None + return result + + d = report.__dict__.copy() + if hasattr(report.longrepr, "toterminal"): + if hasattr(report.longrepr, "reprtraceback") and hasattr( + report.longrepr, "reprcrash" + ): + d["longrepr"] = serialize_exception_longrepr(report) + else: + d["longrepr"] = str(report.longrepr) + else: + d["longrepr"] = report.longrepr + for name in d: + if isinstance(d[name], os.PathLike): + d[name] = os.fspath(d[name]) + elif name == "result": + d[name] = None # for now + return d + + +def _report_kwargs_from_json(reportdict: dict[str, Any]) -> dict[str, Any]: + """Return **kwargs that can be used to construct a TestReport or + CollectReport instance. + + This was originally the serialize_report() function from xdist (ca03269). + """ + + def deserialize_repr_entry(entry_data): + data = entry_data["data"] + entry_type = entry_data["type"] + if entry_type == "ReprEntry": + reprfuncargs = None + reprfileloc = None + reprlocals = None + if data["reprfuncargs"]: + reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) + if data["reprfileloc"]: + reprfileloc = ReprFileLocation(**data["reprfileloc"]) + if data["reprlocals"]: + reprlocals = ReprLocals(data["reprlocals"]["lines"]) + + reprentry: ReprEntry | ReprEntryNative = ReprEntry( + lines=data["lines"], + reprfuncargs=reprfuncargs, + reprlocals=reprlocals, + reprfileloc=reprfileloc, + style=data["style"], + ) + elif entry_type == "ReprEntryNative": + reprentry = ReprEntryNative(data["lines"]) + else: + _report_unserialization_failure(entry_type, TestReport, reportdict) + return reprentry + + def deserialize_repr_traceback(repr_traceback_dict): + repr_traceback_dict["reprentries"] = [ + deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"] + ] + return ReprTraceback(**repr_traceback_dict) + + def deserialize_repr_crash(repr_crash_dict: dict[str, Any] | None): + if repr_crash_dict is not None: + return ReprFileLocation(**repr_crash_dict) + else: + return None + + if ( + reportdict["longrepr"] + and "reprcrash" in reportdict["longrepr"] + and "reprtraceback" in reportdict["longrepr"] + ): + reprtraceback = deserialize_repr_traceback( + reportdict["longrepr"]["reprtraceback"] + ) + reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"]) + if reportdict["longrepr"]["chain"]: + chain = [] + for repr_traceback_data, repr_crash_data, description in reportdict[ + "longrepr" + ]["chain"]: + chain.append( + ( + deserialize_repr_traceback(repr_traceback_data), + deserialize_repr_crash(repr_crash_data), + description, + ) + ) + exception_info: ExceptionChainRepr | ReprExceptionInfo = ExceptionChainRepr( + chain + ) + else: + exception_info = ReprExceptionInfo( + reprtraceback=reprtraceback, + reprcrash=reprcrash, + ) + + for section in reportdict["longrepr"]["sections"]: + exception_info.addsection(*section) + reportdict["longrepr"] = exception_info + + return reportdict diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/runner.py b/.venv-test/lib/python3.12/site-packages/_pytest/runner.py new file mode 100644 index 0000000..3f03cfa --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/runner.py @@ -0,0 +1,593 @@ +# mypy: allow-untyped-defs +"""Basic collect and runtest protocol implementations.""" + +from __future__ import annotations + +import bdb +from collections.abc import Callable +import dataclasses +import os +import sys +import types +from typing import cast +from typing import final +from typing import Generic +from typing import Literal +from typing import TYPE_CHECKING +from typing import TypeVar + +from .config import Config +from .reports import BaseReport +from .reports import CollectErrorRepr +from .reports import CollectReport +from .reports import TestReport +from _pytest import timing +from _pytest._code.code import ExceptionChainRepr +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import TerminalRepr +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.nodes import Collector +from _pytest.nodes import Directory +from _pytest.nodes import Item +from _pytest.nodes import Node +from _pytest.outcomes import Exit +from _pytest.outcomes import OutcomeException +from _pytest.outcomes import Skipped +from _pytest.outcomes import TEST_OUTCOME + + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + +if TYPE_CHECKING: + from _pytest.main import Session + from _pytest.terminal import TerminalReporter + +# +# pytest plugin hooks. + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("terminal reporting", "Reporting", after="general") + group.addoption( + "--durations", + action="store", + type=int, + default=None, + metavar="N", + help="Show N slowest setup/test durations (N=0 for all)", + ) + group.addoption( + "--durations-min", + action="store", + type=float, + default=None, + metavar="N", + help="Minimal duration in seconds for inclusion in slowest list. " + "Default: 0.005 (or 0.0 if -vv is given).", + ) + + +def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None: + durations = terminalreporter.config.option.durations + durations_min = terminalreporter.config.option.durations_min + verbose = terminalreporter.config.get_verbosity() + if durations is None: + return + if durations_min is None: + durations_min = 0.005 if verbose < 2 else 0.0 + tr = terminalreporter + dlist = [] + for replist in tr.stats.values(): + for rep in replist: + if hasattr(rep, "duration"): + dlist.append(rep) + if not dlist: + return + dlist.sort(key=lambda x: x.duration, reverse=True) + if not durations: + tr.write_sep("=", "slowest durations") + else: + tr.write_sep("=", f"slowest {durations} durations") + dlist = dlist[:durations] + + for i, rep in enumerate(dlist): + if rep.duration < durations_min: + tr.write_line("") + message = f"({len(dlist) - i} durations < {durations_min:g}s hidden." + if terminalreporter.config.option.durations_min is None: + message += " Use -vv to show these durations." + message += ")" + tr.write_line(message) + break + tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}") + + +def pytest_sessionstart(session: Session) -> None: + session._setupstate = SetupState() + + +def pytest_sessionfinish(session: Session) -> None: + session._setupstate.teardown_exact(None) + + +def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> bool: + ihook = item.ihook + ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) + runtestprotocol(item, nextitem=nextitem) + ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) + return True + + +def runtestprotocol( + item: Item, log: bool = True, nextitem: Item | None = None +) -> list[TestReport]: + hasrequest = hasattr(item, "_request") + if hasrequest and not item._request: # type: ignore[attr-defined] + # This only happens if the item is re-run, as is done by + # pytest-rerunfailures. + item._initrequest() # type: ignore[attr-defined] + try: + rep = call_and_report(item, "setup", log) + reports = [rep] + if rep.passed: + setup_only = item.config.getoption("setuponly", False) + if item.config.getoption("setupshow", False): + show_test_item(item, add_space=not setup_only) + if not setup_only: + reports.append(call_and_report(item, "call", log)) + # If the session is about to fail or stop, teardown everything - this is + # necessary to correctly report fixture teardown errors (see #11706) + if item.session.shouldfail or item.session.shouldstop: + nextitem = None + reports.append(call_and_report(item, "teardown", log, nextitem=nextitem)) + finally: + # After all teardown hooks have been called (or an exception was reraised) + # want funcargs and request info to go away. + if hasrequest: + item._request = False # type: ignore[attr-defined] + item.funcargs = None # type: ignore[attr-defined] + return reports + + +def show_test_item(item: Item, *, add_space: bool) -> None: + """Show test function, parameters and the fixtures of the test item.""" + tw = item.config.get_terminal_writer() + tw.line() + tw.write(" " * 8) + tw.write(item.nodeid) + used_fixtures = sorted(getattr(item, "fixturenames", [])) + if used_fixtures: + tw.write(f" (fixtures used: {', '.join(used_fixtures)})") + if add_space: + tw.write(" ") + tw.flush() + + +def pytest_runtest_setup(item: Item) -> None: + _update_current_test_var(item, "setup") + item.session._setupstate.setup(item) + + +def pytest_runtest_call(item: Item) -> None: + _update_current_test_var(item, "call") + try: + del sys.last_type + del sys.last_value + del sys.last_traceback + if sys.version_info >= (3, 12, 0): + del sys.last_exc # type:ignore[attr-defined] + except AttributeError: + pass + try: + item.runtest() + except Exception as e: + # Store trace info to allow postmortem debugging + sys.last_type = type(e) + sys.last_value = e + if sys.version_info >= (3, 12, 0): + sys.last_exc = e # type:ignore[attr-defined] + assert e.__traceback__ is not None + # Skip *this* frame + sys.last_traceback = e.__traceback__.tb_next + raise + + +def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None: + _update_current_test_var(item, "teardown") + item.session._setupstate.teardown_exact(nextitem) + _update_current_test_var(item, None) + + +def _update_current_test_var( + item: Item, when: Literal["setup", "call", "teardown"] | None +) -> None: + """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. + + If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment. + """ + var_name = "PYTEST_CURRENT_TEST" + if when: + value = f"{item.nodeid} ({when})" + # don't allow null bytes on environment variables (see #2644, #2957) + value = value.replace("\x00", "(null)") + os.environ[var_name] = value + else: + os.environ.pop(var_name) + + +def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None: + if report.when in ("setup", "teardown"): + if report.failed: + # category, shortletter, verbose-word + return "error", "E", "ERROR" + elif report.skipped: + return "skipped", "s", "SKIPPED" + else: + return "", "", "" + return None + + +# +# Implementation + + +def call_and_report( + item: Item, when: Literal["setup", "call", "teardown"], log: bool = True, **kwds +) -> TestReport: + ihook = item.ihook + if when == "setup": + runtest_hook: Callable[..., None] = ihook.pytest_runtest_setup + elif when == "call": + runtest_hook = ihook.pytest_runtest_call + elif when == "teardown": + runtest_hook = ihook.pytest_runtest_teardown + else: + assert False, f"Unhandled runtest hook case: {when}" + + call = CallInfo.from_call( + lambda: runtest_hook(item=item, **kwds), + when=when, + reraise=get_reraise_exceptions(item.config), + ) + report: TestReport = ihook.pytest_runtest_makereport(item=item, call=call) + if log: + ihook.pytest_runtest_logreport(report=report) + if check_interactive_exception(call, report): + ihook.pytest_exception_interact(node=item, call=call, report=report) + return report + + +def get_reraise_exceptions(config: Config) -> tuple[type[BaseException], ...]: + """Return exception types that should not be suppressed in general.""" + reraise: tuple[type[BaseException], ...] = (Exit,) + if not config.getoption("usepdb", False): + reraise += (KeyboardInterrupt,) + return reraise + + +def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> bool: + """Check whether the call raised an exception that should be reported as + interactive.""" + if call.excinfo is None: + # Didn't raise. + return False + if hasattr(report, "wasxfail"): + # Exception was expected. + return False + unittest = sys.modules.get("unittest") + if isinstance(call.excinfo.value, Skipped | bdb.BdbQuit) or ( + unittest is not None and isinstance(call.excinfo.value, unittest.SkipTest) + ): + # Special control flow exception. + return False + return True + + +TResult = TypeVar("TResult", covariant=True) + + +@final +@dataclasses.dataclass +class CallInfo(Generic[TResult]): + """Result/Exception info of a function invocation.""" + + _result: TResult | None + #: The captured exception of the call, if it raised. + excinfo: ExceptionInfo[BaseException] | None + #: The system time when the call started, in seconds since the epoch. + start: float + #: The system time when the call ended, in seconds since the epoch. + stop: float + #: The call duration, in seconds. + duration: float + #: The context of invocation: "collect", "setup", "call" or "teardown". + when: Literal["collect", "setup", "call", "teardown"] + + def __init__( + self, + result: TResult | None, + excinfo: ExceptionInfo[BaseException] | None, + start: float, + stop: float, + duration: float, + when: Literal["collect", "setup", "call", "teardown"], + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._result = result + self.excinfo = excinfo + self.start = start + self.stop = stop + self.duration = duration + self.when = when + + @property + def result(self) -> TResult: + """The return value of the call, if it didn't raise. + + Can only be accessed if excinfo is None. + """ + if self.excinfo is not None: + raise AttributeError(f"{self!r} has no valid result") + # The cast is safe because an exception wasn't raised, hence + # _result has the expected function return type (which may be + # None, that's why a cast and not an assert). + return cast(TResult, self._result) + + @classmethod + def from_call( + cls, + func: Callable[[], TResult], + when: Literal["collect", "setup", "call", "teardown"], + reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None, + ) -> CallInfo[TResult]: + """Call func, wrapping the result in a CallInfo. + + :param func: + The function to call. Called without arguments. + :type func: Callable[[], _pytest.runner.TResult] + :param when: + The phase in which the function is called. + :param reraise: + Exception or exceptions that shall propagate if raised by the + function, instead of being wrapped in the CallInfo. + """ + excinfo = None + instant = timing.Instant() + try: + result: TResult | None = func() + except BaseException: + excinfo = ExceptionInfo.from_current() + if reraise is not None and isinstance(excinfo.value, reraise): + raise + result = None + duration = instant.elapsed() + return cls( + start=duration.start.time, + stop=duration.stop.time, + duration=duration.seconds, + when=when, + result=result, + excinfo=excinfo, + _ispytest=True, + ) + + def __repr__(self) -> str: + if self.excinfo is None: + return f"" + return f"" + + +def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: + return TestReport.from_item_and_call(item, call) + + +def pytest_make_collect_report(collector: Collector) -> CollectReport: + def collect() -> list[Item | Collector]: + # Before collecting, if this is a Directory, load the conftests. + # If a conftest import fails to load, it is considered a collection + # error of the Directory collector. This is why it's done inside of the + # CallInfo wrapper. + # + # Note: initial conftests are loaded early, not here. + if isinstance(collector, Directory): + collector.config.pluginmanager._loadconftestmodules( + collector.path, + collector.config.getoption("importmode"), + rootpath=collector.config.rootpath, + consider_namespace_packages=collector.config.getini( + "consider_namespace_packages" + ), + ) + + return list(collector.collect()) + + call = CallInfo.from_call( + collect, "collect", reraise=(KeyboardInterrupt, SystemExit) + ) + longrepr: None | tuple[str, int, str] | str | TerminalRepr = None + if not call.excinfo: + outcome: Literal["passed", "skipped", "failed"] = "passed" + else: + skip_exceptions = [Skipped] + unittest = sys.modules.get("unittest") + if unittest is not None: + skip_exceptions.append(unittest.SkipTest) + if isinstance(call.excinfo.value, tuple(skip_exceptions)): + outcome = "skipped" + r_ = collector._repr_failure_py(call.excinfo, "line") + assert isinstance(r_, ExceptionChainRepr), repr(r_) + r = r_.reprcrash + assert r + longrepr = (str(r.path), r.lineno, r.message) + else: + outcome = "failed" + errorinfo = collector.repr_failure(call.excinfo) + if not hasattr(errorinfo, "toterminal"): + assert isinstance(errorinfo, str) + errorinfo = CollectErrorRepr(errorinfo) + longrepr = errorinfo + result = call.result if not call.excinfo else None + rep = CollectReport(collector.nodeid, outcome, longrepr, result) + rep.call = call # type: ignore # see collect_one_node + return rep + + +class SetupState: + """Shared state for setting up/tearing down test items or collectors + in a session. + + Suppose we have a collection tree as follows: + + + + + + + + The SetupState maintains a stack. The stack starts out empty: + + [] + + During the setup phase of item1, setup(item1) is called. What it does + is: + + push session to stack, run session.setup() + push mod1 to stack, run mod1.setup() + push item1 to stack, run item1.setup() + + The stack is: + + [session, mod1, item1] + + While the stack is in this shape, it is allowed to add finalizers to + each of session, mod1, item1 using addfinalizer(). + + During the teardown phase of item1, teardown_exact(item2) is called, + where item2 is the next item to item1. What it does is: + + pop item1 from stack, run its teardowns + pop mod1 from stack, run its teardowns + + mod1 was popped because it ended its purpose with item1. The stack is: + + [session] + + During the setup phase of item2, setup(item2) is called. What it does + is: + + push mod2 to stack, run mod2.setup() + push item2 to stack, run item2.setup() + + Stack: + + [session, mod2, item2] + + During the teardown phase of item2, teardown_exact(None) is called, + because item2 is the last item. What it does is: + + pop item2 from stack, run its teardowns + pop mod2 from stack, run its teardowns + pop session from stack, run its teardowns + + Stack: + + [] + + The end! + """ + + def __init__(self) -> None: + # The stack is in the dict insertion order. + self.stack: dict[ + Node, + tuple[ + # Node's finalizers. + list[Callable[[], object]], + # Node's exception and original traceback, if its setup raised. + tuple[OutcomeException | Exception, types.TracebackType | None] | None, + ], + ] = {} + + def is_node_active(self, node: Node) -> bool: + """Check if a node is currently active in the stack -- set up and not + torn down yet.""" + return node in self.stack + + def setup(self, item: Item) -> None: + """Setup objects along the collector chain to the item.""" + needed_collectors = item.listchain() + + # If a collector fails its setup, fail its entire subtree of items. + # The setup is not retried for each item - the same exception is used. + for col, (finalizers, exc) in self.stack.items(): + assert col in needed_collectors, "previous item was not torn down properly" + if exc: + raise exc[0].with_traceback(exc[1]) + + for col in needed_collectors[len(self.stack) :]: + assert col not in self.stack + # Push onto the stack. + self.stack[col] = ([col.teardown], None) + try: + col.setup() + except TEST_OUTCOME as exc: + self.stack[col] = (self.stack[col][0], (exc, exc.__traceback__)) + raise + + def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: + """Attach a finalizer to the given node. + + The node must be currently active in the stack. + """ + assert node and not isinstance(node, tuple) + assert callable(finalizer) + assert node in self.stack, (node, self.stack) + self.stack[node][0].append(finalizer) + + def teardown_exact(self, nextitem: Item | None) -> None: + """Teardown the current stack up until reaching nodes that nextitem + also descends from. + + When nextitem is None (meaning we're at the last item), the entire + stack is torn down. + """ + needed_collectors = (nextitem and nextitem.listchain()) or [] + exceptions: list[BaseException] = [] + while self.stack: + if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: + break + node, (finalizers, _) = self.stack.popitem() + these_exceptions = [] + while finalizers: + fin = finalizers.pop() + try: + fin() + except TEST_OUTCOME as e: + these_exceptions.append(e) + + if len(these_exceptions) == 1: + exceptions.extend(these_exceptions) + elif these_exceptions: + msg = f"errors while tearing down {node!r}" + exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1])) + + if len(exceptions) == 1: + raise exceptions[0] + elif exceptions: + raise BaseExceptionGroup("errors during test teardown", exceptions[::-1]) + if nextitem is None: + assert not self.stack + + +def collect_one_node(collector: Collector) -> CollectReport: + ihook = collector.ihook + ihook.pytest_collectstart(collector=collector) + rep: CollectReport = ihook.pytest_make_collect_report(collector=collector) + call = rep.__dict__.pop("call", None) + if call and check_interactive_exception(call, rep): + ihook.pytest_exception_interact(node=collector, call=call, report=rep) + return rep diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/scope.py b/.venv-test/lib/python3.12/site-packages/_pytest/scope.py new file mode 100644 index 0000000..68a9da0 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/scope.py @@ -0,0 +1,91 @@ +""" +Scope definition and related utilities. + +Those are defined here, instead of in the 'fixtures' module because +their use is spread across many other pytest modules, and centralizing it in 'fixtures' +would cause circular references. + +Also this makes the module light to import, as it should. +""" + +from __future__ import annotations + +from enum import Enum +from functools import total_ordering +from typing import Literal + + +ScopeName = Literal["session", "package", "module", "class", "function"] + + +@total_ordering +class Scope(Enum): + """ + Represents one of the possible fixture scopes in pytest. + + Scopes are ordered from lower to higher, that is: + + ->>> higher ->>> + + Function < Class < Module < Package < Session + + <<<- lower <<<- + """ + + # Scopes need to be listed from lower to higher. + Function = "function" + Class = "class" + Module = "module" + Package = "package" + Session = "session" + + def next_lower(self) -> Scope: + """Return the next lower scope.""" + index = _SCOPE_INDICES[self] + if index == 0: + raise ValueError(f"{self} is the lower-most scope") + return _ALL_SCOPES[index - 1] + + def next_higher(self) -> Scope: + """Return the next higher scope.""" + index = _SCOPE_INDICES[self] + if index == len(_SCOPE_INDICES) - 1: + raise ValueError(f"{self} is the upper-most scope") + return _ALL_SCOPES[index + 1] + + def __lt__(self, other: Scope) -> bool: + self_index = _SCOPE_INDICES[self] + other_index = _SCOPE_INDICES[other] + return self_index < other_index + + @classmethod + def from_user( + cls, scope_name: ScopeName, descr: str, where: str | None = None + ) -> Scope: + """ + Given a scope name from the user, return the equivalent Scope enum. Should be used + whenever we want to convert a user provided scope name to its enum object. + + If the scope name is invalid, construct a user friendly message and call pytest.fail. + """ + from _pytest.outcomes import fail + + try: + # Holding this reference is necessary for mypy at the moment. + scope = Scope(scope_name) + except ValueError: + fail( + "{} {}got an unexpected scope value '{}'".format( + descr, f"from {where} " if where else "", scope_name + ), + pytrace=False, + ) + return scope + + +_ALL_SCOPES = list(Scope) +_SCOPE_INDICES = {scope: index for index, scope in enumerate(_ALL_SCOPES)} + + +# Ordered list of scopes which can contain many tests (in practice all except Function). +HIGH_SCOPES = [x for x in Scope if x is not Scope.Function] diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/setuponly.py b/.venv-test/lib/python3.12/site-packages/_pytest/setuponly.py new file mode 100644 index 0000000..7e6b46b --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/setuponly.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from collections.abc import Generator + +from _pytest._io.saferepr import saferepr +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import SubRequest +from _pytest.scope import Scope +import pytest + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("debugconfig") + group.addoption( + "--setuponly", + "--setup-only", + action="store_true", + help="Only setup fixtures, do not execute tests", + ) + group.addoption( + "--setupshow", + "--setup-show", + action="store_true", + help="Show setup of fixtures while executing tests", + ) + + +@pytest.hookimpl(wrapper=True) +def pytest_fixture_setup( + fixturedef: FixtureDef[object], request: SubRequest +) -> Generator[None, object, object]: + try: + return (yield) + finally: + if request.config.option.setupshow: + if hasattr(request, "param"): + # Save the fixture parameter so ._show_fixture_action() can + # display it now and during the teardown (in .finish()). + if fixturedef.ids: + if callable(fixturedef.ids): + param = fixturedef.ids(request.param) + else: + param = fixturedef.ids[request.param_index] + else: + param = request.param + fixturedef.cached_param = param # type: ignore[attr-defined] + _show_fixture_action(fixturedef, request.config, "SETUP") + + +def pytest_fixture_post_finalizer( + fixturedef: FixtureDef[object], request: SubRequest +) -> None: + if fixturedef.cached_result is not None: + config = request.config + if config.option.setupshow: + _show_fixture_action(fixturedef, request.config, "TEARDOWN") + if hasattr(fixturedef, "cached_param"): + del fixturedef.cached_param + + +def _show_fixture_action( + fixturedef: FixtureDef[object], config: Config, msg: str +) -> None: + capman = config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture() + + tw = config.get_terminal_writer() + tw.line() + # Use smaller indentation the higher the scope: Session = 0, Package = 1, etc. + scope_indent = list(reversed(Scope)).index(fixturedef._scope) + tw.write(" " * 2 * scope_indent) + + scopename = fixturedef.scope[0].upper() + tw.write(f"{msg:<8} {scopename} {fixturedef.argname}") + + if msg == "SETUP": + deps = sorted(arg for arg in fixturedef.argnames if arg != "request") + if deps: + tw.write(" (fixtures used: {})".format(", ".join(deps))) + + if hasattr(fixturedef, "cached_param"): + tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") + + tw.flush() + + if capman: + capman.resume_global_capture() + + +@pytest.hookimpl(tryfirst=True) +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + if config.option.setuponly: + config.option.setupshow = True + return None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/setupplan.py b/.venv-test/lib/python3.12/site-packages/_pytest/setupplan.py new file mode 100644 index 0000000..4e124cc --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/setupplan.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import SubRequest +import pytest + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("debugconfig") + group.addoption( + "--setupplan", + "--setup-plan", + action="store_true", + help="Show what fixtures and tests would be executed but " + "don't execute anything", + ) + + +@pytest.hookimpl(tryfirst=True) +def pytest_fixture_setup( + fixturedef: FixtureDef[object], request: SubRequest +) -> object | None: + # Will return a dummy fixture if the setuponly option is provided. + if request.config.option.setupplan: + my_cache_key = fixturedef.cache_key(request) + fixturedef.cached_result = (None, my_cache_key, None) + return fixturedef.cached_result + return None + + +@pytest.hookimpl(tryfirst=True) +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: + if config.option.setupplan: + config.option.setuponly = True + config.option.setupshow = True + return None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/skipping.py b/.venv-test/lib/python3.12/site-packages/_pytest/skipping.py new file mode 100644 index 0000000..f7a4c4c --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/skipping.py @@ -0,0 +1,321 @@ +# mypy: allow-untyped-defs +"""Support for skip/xfail functions and markers.""" + +from __future__ import annotations + +from collections.abc import Generator +from collections.abc import Mapping +import dataclasses +import os +import platform +import sys +import traceback + +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.mark.structures import Mark +from _pytest.nodes import Item +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.outcomes import xfail +from _pytest.raises import AbstractRaises +from _pytest.reports import BaseReport +from _pytest.reports import TestReport +from _pytest.runner import CallInfo +from _pytest.stash import StashKey + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group.addoption( + "--runxfail", + action="store_true", + dest="runxfail", + default=False, + help="Report the results of xfail tests as if they were not marked", + ) + + parser.addini( + "strict_xfail", + "Default for the strict parameter of xfail " + "markers when not given explicitly (default: False) (alias: xfail_strict)", + type="bool", + # None => fallback to `strict`. + default=None, + aliases=["xfail_strict"], + ) + + +def pytest_configure(config: Config) -> None: + if config.option.runxfail: + # yay a hack + import pytest + + old = pytest.xfail + config.add_cleanup(lambda: setattr(pytest, "xfail", old)) + + def nop(*args, **kwargs): + pass + + nop.Exception = xfail.Exception # type: ignore[attr-defined] + setattr(pytest, "xfail", nop) + + config.addinivalue_line( + "markers", + "skip(reason=None): skip the given test function with an optional reason. " + 'Example: skip(reason="no way of currently testing this") skips the ' + "test.", + ) + config.addinivalue_line( + "markers", + "skipif(condition, ..., *, reason=...): " + "skip the given test function if any of the conditions evaluate to True. " + "Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. " + "See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-skipif", + ) + config.addinivalue_line( + "markers", + "xfail(condition, ..., *, reason=..., run=True, raises=None, strict=strict_xfail): " + "mark the test function as an expected failure if any of the conditions " + "evaluate to True. Optionally specify a reason for better reporting " + "and run=False if you don't even want to execute the test function. " + "If only specific exception(s) are expected, you can list them in " + "raises, and if the test fails in other ways, it will be reported as " + "a true failure. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-xfail", + ) + + +def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool, str]: + """Evaluate a single skipif/xfail condition. + + If an old-style string condition is given, it is eval()'d, otherwise the + condition is bool()'d. If this fails, an appropriately formatted pytest.fail + is raised. + + Returns (result, reason). The reason is only relevant if the result is True. + """ + # String condition. + if isinstance(condition, str): + globals_ = { + "os": os, + "sys": sys, + "platform": platform, + "config": item.config, + } + for dictionary in reversed( + item.ihook.pytest_markeval_namespace(config=item.config) + ): + if not isinstance(dictionary, Mapping): + raise ValueError( + f"pytest_markeval_namespace() needs to return a dict, got {dictionary!r}" + ) + globals_.update(dictionary) + if hasattr(item, "obj"): + globals_.update(item.obj.__globals__) + try: + filename = f"<{mark.name} condition>" + condition_code = compile(condition, filename, "eval") + result = eval(condition_code, globals_) + except SyntaxError as exc: + msglines = [ + f"Error evaluating {mark.name!r} condition", + " " + condition, + " " + " " * (exc.offset or 0) + "^", + "SyntaxError: invalid syntax", + ] + fail("\n".join(msglines), pytrace=False) + except Exception as exc: + msglines = [ + f"Error evaluating {mark.name!r} condition", + " " + condition, + *traceback.format_exception_only(exc), + ] + fail("\n".join(msglines), pytrace=False) + + # Boolean condition. + else: + try: + result = bool(condition) + except Exception as exc: + msglines = [ + f"Error evaluating {mark.name!r} condition as a boolean", + *traceback.format_exception_only(exc), + ] + fail("\n".join(msglines), pytrace=False) + + reason = mark.kwargs.get("reason", None) + if reason is None: + if isinstance(condition, str): + reason = "condition: " + condition + else: + # XXX better be checked at collection time + msg = ( + f"Error evaluating {mark.name!r}: " + + "you need to specify reason=STRING when using booleans as conditions." + ) + fail(msg, pytrace=False) + + return result, reason + + +@dataclasses.dataclass(frozen=True) +class Skip: + """The result of evaluate_skip_marks().""" + + reason: str = "unconditional skip" + + +def evaluate_skip_marks(item: Item) -> Skip | None: + """Evaluate skip and skipif marks on item, returning Skip if triggered.""" + for mark in item.iter_markers(name="skipif"): + if "condition" not in mark.kwargs: + conditions = mark.args + else: + conditions = (mark.kwargs["condition"],) + + # Unconditional. + if not conditions: + reason = mark.kwargs.get("reason", "") + return Skip(reason) + + # If any of the conditions are true. + for condition in conditions: + result, reason = evaluate_condition(item, mark, condition) + if result: + return Skip(reason) + + for mark in item.iter_markers(name="skip"): + try: + return Skip(*mark.args, **mark.kwargs) + except TypeError as e: + raise TypeError(str(e) + " - maybe you meant pytest.mark.skipif?") from None + + return None + + +@dataclasses.dataclass(frozen=True) +class Xfail: + """The result of evaluate_xfail_marks().""" + + __slots__ = ("raises", "reason", "run", "strict") + + reason: str + run: bool + strict: bool + raises: ( + type[BaseException] + | tuple[type[BaseException], ...] + | AbstractRaises[BaseException] + | None + ) + + +def evaluate_xfail_marks(item: Item) -> Xfail | None: + """Evaluate xfail marks on item, returning Xfail if triggered.""" + for mark in item.iter_markers(name="xfail"): + run = mark.kwargs.get("run", True) + strict = mark.kwargs.get("strict") + if strict is None: + strict = item.config.getini("strict_xfail") + if strict is None: + strict = item.config.getini("strict") + raises = mark.kwargs.get("raises", None) + if "condition" not in mark.kwargs: + conditions = mark.args + else: + conditions = (mark.kwargs["condition"],) + + # Unconditional. + if not conditions: + reason = mark.kwargs.get("reason", "") + return Xfail(reason, run, strict, raises) + + # If any of the conditions are true. + for condition in conditions: + result, reason = evaluate_condition(item, mark, condition) + if result: + return Xfail(reason, run, strict, raises) + + return None + + +# Saves the xfail mark evaluation. Can be refreshed during call if None. +xfailed_key = StashKey[Xfail | None]() + + +@hookimpl(tryfirst=True) +def pytest_runtest_setup(item: Item) -> None: + skipped = evaluate_skip_marks(item) + if skipped: + raise skip.Exception(skipped.reason, _use_item_location=True) + + item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) + if xfailed and not item.config.option.runxfail and not xfailed.run: + xfail("[NOTRUN] " + xfailed.reason) + + +@hookimpl(wrapper=True) +def pytest_runtest_call(item: Item) -> Generator[None]: + xfailed = item.stash.get(xfailed_key, None) + if xfailed is None: + item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) + + if xfailed and not item.config.option.runxfail and not xfailed.run: + xfail("[NOTRUN] " + xfailed.reason) + + try: + return (yield) + finally: + # The test run may have added an xfail mark dynamically. + xfailed = item.stash.get(xfailed_key, None) + if xfailed is None: + item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) + + +@hookimpl(wrapper=True) +def pytest_runtest_makereport( + item: Item, call: CallInfo[None] +) -> Generator[None, TestReport, TestReport]: + rep = yield + xfailed = item.stash.get(xfailed_key, None) + if item.config.option.runxfail: + pass # don't interfere + elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception): + assert call.excinfo.value.msg is not None + rep.wasxfail = call.excinfo.value.msg + rep.outcome = "skipped" + elif not rep.skipped and xfailed: + if call.excinfo: + raises = xfailed.raises + if raises is None or ( + ( + isinstance(raises, type | tuple) + and isinstance(call.excinfo.value, raises) + ) + or ( + isinstance(raises, AbstractRaises) + and raises.matches(call.excinfo.value) + ) + ): + rep.outcome = "skipped" + rep.wasxfail = xfailed.reason + else: + rep.outcome = "failed" + elif call.when == "call": + if xfailed.strict: + rep.outcome = "failed" + rep.longrepr = "[XPASS(strict)] " + xfailed.reason + else: + rep.outcome = "passed" + rep.wasxfail = xfailed.reason + return rep + + +def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None: + if hasattr(report, "wasxfail"): + if report.skipped: + return "xfailed", "x", "XFAIL" + elif report.passed: + return "xpassed", "X", "XPASS" + return None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/stash.py b/.venv-test/lib/python3.12/site-packages/_pytest/stash.py new file mode 100644 index 0000000..6a9ff88 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/stash.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from typing import Any +from typing import cast +from typing import Generic +from typing import TypeVar + + +__all__ = ["Stash", "StashKey"] + + +T = TypeVar("T") +D = TypeVar("D") + + +class StashKey(Generic[T]): + """``StashKey`` is an object used as a key to a :class:`Stash`. + + A ``StashKey`` is associated with the type ``T`` of the value of the key. + + A ``StashKey`` is unique and cannot conflict with another key. + + .. versionadded:: 7.0 + """ + + __slots__ = () + + +class Stash: + r"""``Stash`` is a type-safe heterogeneous mutable mapping that + allows keys and value types to be defined separately from + where it (the ``Stash``) is created. + + Usually you will be given an object which has a ``Stash``, for example + :class:`~pytest.Config` or a :class:`~_pytest.nodes.Node`: + + .. code-block:: python + + stash: Stash = some_object.stash + + If a module or plugin wants to store data in this ``Stash``, it creates + :class:`StashKey`\s for its keys (at the module level): + + .. code-block:: python + + # At the top-level of the module + some_str_key = StashKey[str]() + some_bool_key = StashKey[bool]() + + To store information: + + .. code-block:: python + + # Value type must match the key. + stash[some_str_key] = "value" + stash[some_bool_key] = True + + To retrieve the information: + + .. code-block:: python + + # The static type of some_str is str. + some_str = stash[some_str_key] + # The static type of some_bool is bool. + some_bool = stash[some_bool_key] + + .. versionadded:: 7.0 + """ + + __slots__ = ("_storage",) + + def __init__(self) -> None: + self._storage: dict[StashKey[Any], object] = {} + + def __setitem__(self, key: StashKey[T], value: T) -> None: + """Set a value for key.""" + self._storage[key] = value + + def __getitem__(self, key: StashKey[T]) -> T: + """Get the value for key. + + Raises ``KeyError`` if the key wasn't set before. + """ + return cast(T, self._storage[key]) + + def get(self, key: StashKey[T], default: D) -> T | D: + """Get the value for key, or return default if the key wasn't set + before.""" + try: + return self[key] + except KeyError: + return default + + def setdefault(self, key: StashKey[T], default: T) -> T: + """Return the value of key if already set, otherwise set the value + of key to default and return default.""" + try: + return self[key] + except KeyError: + self[key] = default + return default + + def __delitem__(self, key: StashKey[T]) -> None: + """Delete the value for key. + + Raises ``KeyError`` if the key wasn't set before. + """ + del self._storage[key] + + def __contains__(self, key: StashKey[T]) -> bool: + """Return whether key was set.""" + return key in self._storage + + def __len__(self) -> int: + """Return how many items exist in the stash.""" + return len(self._storage) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/stepwise.py b/.venv-test/lib/python3.12/site-packages/_pytest/stepwise.py new file mode 100644 index 0000000..8901540 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/stepwise.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import dataclasses +from datetime import datetime +from datetime import timedelta +from typing import Any +from typing import TYPE_CHECKING + +from _pytest import nodes +from _pytest.cacheprovider import Cache +from _pytest.config import Config +from _pytest.config.argparsing import Parser +from _pytest.main import Session +from _pytest.reports import TestReport + + +if TYPE_CHECKING: + from typing_extensions import Self + +STEPWISE_CACHE_DIR = "cache/stepwise" + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("general") + group.addoption( + "--sw", + "--stepwise", + action="store_true", + default=False, + dest="stepwise", + help="Exit on test failure and continue from last failing test next time", + ) + group.addoption( + "--sw-skip", + "--stepwise-skip", + action="store_true", + default=False, + dest="stepwise_skip", + help="Ignore the first failing test but stop on the next failing test. " + "Implicitly enables --stepwise.", + ) + group.addoption( + "--sw-reset", + "--stepwise-reset", + action="store_true", + default=False, + dest="stepwise_reset", + help="Resets stepwise state, restarting the stepwise workflow. " + "Implicitly enables --stepwise.", + ) + + +def pytest_configure(config: Config) -> None: + # --stepwise-skip/--stepwise-reset implies stepwise. + if config.option.stepwise_skip or config.option.stepwise_reset: + config.option.stepwise = True + if config.getoption("stepwise"): + config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") + + +def pytest_sessionfinish(session: Session) -> None: + if not session.config.getoption("stepwise"): + assert session.config.cache is not None + if hasattr(session.config, "workerinput"): + # Do not update cache if this process is a xdist worker to prevent + # race conditions (#10641). + return + + +@dataclasses.dataclass +class StepwiseCacheInfo: + # The nodeid of the last failed test. + last_failed: str | None + + # The number of tests in the last time --stepwise was run. + # We use this information as a simple way to invalidate the cache information, avoiding + # confusing behavior in case the cache is stale. + last_test_count: int | None + + # The date when the cache was last updated, for information purposes only. + last_cache_date_str: str + + @property + def last_cache_date(self) -> datetime: + return datetime.fromisoformat(self.last_cache_date_str) + + @classmethod + def empty(cls) -> Self: + return cls( + last_failed=None, + last_test_count=None, + last_cache_date_str=datetime.now().isoformat(), + ) + + def update_date_to_now(self) -> None: + self.last_cache_date_str = datetime.now().isoformat() + + +class StepwisePlugin: + def __init__(self, config: Config) -> None: + self.config = config + self.session: Session | None = None + self.report_status: list[str] = [] + assert config.cache is not None + self.cache: Cache = config.cache + self.skip: bool = config.getoption("stepwise_skip") + self.reset: bool = config.getoption("stepwise_reset") + self.cached_info = self._load_cached_info() + + def _load_cached_info(self) -> StepwiseCacheInfo: + cached_dict: dict[str, Any] | None = self.cache.get(STEPWISE_CACHE_DIR, None) + if cached_dict: + try: + return StepwiseCacheInfo( + cached_dict["last_failed"], + cached_dict["last_test_count"], + cached_dict["last_cache_date_str"], + ) + except (KeyError, TypeError) as e: + error = f"{type(e).__name__}: {e}" + self.report_status.append(f"error reading cache, discarding ({error})") + + # Cache not found or error during load, return a new cache. + return StepwiseCacheInfo.empty() + + def pytest_sessionstart(self, session: Session) -> None: + self.session = session + + def pytest_collection_modifyitems( + self, config: Config, items: list[nodes.Item] + ) -> None: + last_test_count = self.cached_info.last_test_count + self.cached_info.last_test_count = len(items) + + if self.reset: + self.report_status.append("resetting state, not skipping.") + self.cached_info.last_failed = None + return + + if not self.cached_info.last_failed: + self.report_status.append("no previously failed tests, not skipping.") + return + + if last_test_count is not None and last_test_count != len(items): + self.report_status.append( + f"test count changed, not skipping (now {len(items)} tests, previously {last_test_count})." + ) + self.cached_info.last_failed = None + return + + # Check all item nodes until we find a match on last failed. + failed_index = None + for index, item in enumerate(items): + if item.nodeid == self.cached_info.last_failed: + failed_index = index + break + + # If the previously failed test was not found among the test items, + # do not skip any tests. + if failed_index is None: + self.report_status.append("previously failed test not found, not skipping.") + else: + cache_age = datetime.now() - self.cached_info.last_cache_date + # Round up to avoid showing microseconds. + cache_age = timedelta(seconds=int(cache_age.total_seconds())) + self.report_status.append( + f"skipping {failed_index} already passed items (cache from {cache_age} ago," + f" use --sw-reset to discard)." + ) + deselected = items[:failed_index] + del items[:failed_index] + config.hook.pytest_deselected(items=deselected) + + def pytest_runtest_logreport(self, report: TestReport) -> None: + if report.failed: + if self.skip: + # Remove test from the failed ones (if it exists) and unset the skip option + # to make sure the following tests will not be skipped. + if report.nodeid == self.cached_info.last_failed: + self.cached_info.last_failed = None + + self.skip = False + else: + # Mark test as the last failing and interrupt the test session. + self.cached_info.last_failed = report.nodeid + assert self.session is not None + self.session.shouldstop = ( + "Test failed, continuing from this test next run." + ) + + else: + # If the test was actually run and did pass. + if report.when == "call": + # Remove test from the failed ones, if exists. + if report.nodeid == self.cached_info.last_failed: + self.cached_info.last_failed = None + + def pytest_report_collectionfinish(self) -> list[str] | None: + if self.config.get_verbosity() >= 0 and self.report_status: + return [f"stepwise: {x}" for x in self.report_status] + return None + + def pytest_sessionfinish(self) -> None: + if hasattr(self.config, "workerinput"): + # Do not update cache if this process is a xdist worker to prevent + # race conditions (#10641). + return + self.cached_info.update_date_to_now() + self.cache.set(STEPWISE_CACHE_DIR, dataclasses.asdict(self.cached_info)) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/subtests.py b/.venv-test/lib/python3.12/site-packages/_pytest/subtests.py new file mode 100644 index 0000000..6ac3b5c --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/subtests.py @@ -0,0 +1,418 @@ +"""Builtin plugin that adds subtests support.""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Callable +from collections.abc import Iterator +from collections.abc import Mapping +from contextlib import AbstractContextManager +from contextlib import contextmanager +from contextlib import ExitStack +from contextlib import nullcontext +import dataclasses +import time +from types import TracebackType +from typing import Any +from typing import TYPE_CHECKING + +import pluggy + +from _pytest._code import ExceptionInfo +from _pytest._io.saferepr import saferepr +from _pytest.capture import CaptureFixture +from _pytest.capture import FDCapture +from _pytest.capture import SysCapture +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import SubRequest +from _pytest.logging import catching_logs +from _pytest.logging import LogCaptureHandler +from _pytest.logging import LoggingPlugin +from _pytest.reports import TestReport +from _pytest.runner import CallInfo +from _pytest.runner import check_interactive_exception +from _pytest.runner import get_reraise_exceptions +from _pytest.stash import StashKey + + +if TYPE_CHECKING: + from typing_extensions import Self + + +def pytest_addoption(parser: Parser) -> None: + Config._add_verbosity_ini( + parser, + Config.VERBOSITY_SUBTESTS, + help=( + "Specify verbosity level for subtests. " + "Higher levels will generate output for passed subtests. Failed subtests are always reported." + ), + ) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class SubtestContext: + """The values passed to Subtests.test() that are included in the test report.""" + + msg: str | None + kwargs: Mapping[str, Any] + + def __post_init__(self) -> None: + # Brute-force the returned kwargs dict to be JSON serializable (pytest-dev/pytest-xdist#1273). + object.__setattr__( + self, "kwargs", {k: saferepr(v) for (k, v) in self.kwargs.items()} + ) + + def _to_json(self) -> dict[str, Any]: + result = dataclasses.asdict(self) + return result + + @classmethod + def _from_json(cls, d: dict[str, Any]) -> Self: + return cls(msg=d["msg"], kwargs=d["kwargs"]) + + +@dataclasses.dataclass(init=False) +class SubtestReport(TestReport): + context: SubtestContext + + @property + def head_line(self) -> str: + _, _, domain = self.location + return f"{domain} {self._sub_test_description()}" + + def _sub_test_description(self) -> str: + parts = [] + if self.context.msg is not None: + parts.append(f"[{self.context.msg}]") + if self.context.kwargs: + params_desc = ", ".join( + f"{k}={v}" for (k, v) in self.context.kwargs.items() + ) + parts.append(f"({params_desc})") + return " ".join(parts) or "()" + + def _to_json(self) -> dict[str, Any]: + data = super()._to_json() + del data["context"] + data["_report_type"] = "SubTestReport" + data["_subtest.context"] = self.context._to_json() + return data + + @classmethod + def _from_json(cls, reportdict: dict[str, Any]) -> SubtestReport: + report = super()._from_json(reportdict) + report.context = SubtestContext._from_json(reportdict["_subtest.context"]) + return report + + @classmethod + def _new( + cls, + test_report: TestReport, + context: SubtestContext, + captured_output: Captured | None, + captured_logs: CapturedLogs | None, + ) -> Self: + result = super()._from_json(test_report._to_json()) + result.context = context + + if captured_output: + if captured_output.out: + result.sections.append(("Captured stdout call", captured_output.out)) + if captured_output.err: + result.sections.append(("Captured stderr call", captured_output.err)) + + if captured_logs and (log := captured_logs.handler.stream.getvalue()): + result.sections.append(("Captured log call", log)) + + return result + + +@fixture +def subtests(request: SubRequest) -> Subtests: + """Provides subtests functionality.""" + capmam = request.node.config.pluginmanager.get_plugin("capturemanager") + suspend_capture_ctx = ( + capmam.global_and_fixture_disabled if capmam is not None else nullcontext + ) + return Subtests(request.node.ihook, suspend_capture_ctx, request, _ispytest=True) + + +class Subtests: + """Subtests fixture, enables declaring subtests inside test functions via the :meth:`test` method.""" + + def __init__( + self, + ihook: pluggy.HookRelay, + suspend_capture_ctx: Callable[[], AbstractContextManager[None]], + request: SubRequest, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._ihook = ihook + self._suspend_capture_ctx = suspend_capture_ctx + self._request = request + + def test( + self, + msg: str | None = None, + **kwargs: Any, + ) -> _SubTestContextManager: + """ + Context manager for subtests, capturing exceptions raised inside the subtest scope and + reporting assertion failures and errors individually. + + Usage + ----- + + .. code-block:: python + + def test(subtests): + for i in range(5): + with subtests.test("custom message", i=i): + assert i % 2 == 0 + + :param msg: + If given, the message will be shown in the test report in case of subtest failure. + + :param kwargs: + Arbitrary values that are also added to the subtest report. + """ + return _SubTestContextManager( + self._ihook, + msg, + kwargs, + request=self._request, + suspend_capture_ctx=self._suspend_capture_ctx, + config=self._request.config, + ) + + +@dataclasses.dataclass +class _SubTestContextManager: + """ + Context manager for subtests, capturing exceptions raised inside the subtest scope and handling + them through the pytest machinery. + """ + + # Note: initially the logic for this context manager was implemented directly + # in Subtests.test() as a @contextmanager, however, it is not possible to control the output fully when + # exiting from it due to an exception when in `--exitfirst` mode, so this was refactored into an + # explicit context manager class (pytest-dev/pytest-subtests#134). + + ihook: pluggy.HookRelay + msg: str | None + kwargs: dict[str, Any] + suspend_capture_ctx: Callable[[], AbstractContextManager[None]] + request: SubRequest + config: Config + + def __enter__(self) -> None: + __tracebackhide__ = True + + self._start = time.time() + self._precise_start = time.perf_counter() + self._exc_info = None + + self._exit_stack = ExitStack() + self._captured_output = self._exit_stack.enter_context( + capturing_output(self.request) + ) + self._captured_logs = self._exit_stack.enter_context( + capturing_logs(self.request) + ) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + __tracebackhide__ = True + if exc_val is not None: + exc_info = ExceptionInfo.from_exception(exc_val) + else: + exc_info = None + + self._exit_stack.close() + + precise_stop = time.perf_counter() + duration = precise_stop - self._precise_start + stop = time.time() + + call_info = CallInfo[None]( + None, + exc_info, + start=self._start, + stop=stop, + duration=duration, + when="call", + _ispytest=True, + ) + report = self.ihook.pytest_runtest_makereport( + item=self.request.node, call=call_info + ) + sub_report = SubtestReport._new( + report, + SubtestContext(msg=self.msg, kwargs=self.kwargs), + captured_output=self._captured_output, + captured_logs=self._captured_logs, + ) + + if sub_report.failed: + failed_subtests = self.config.stash[failed_subtests_key] + failed_subtests[self.request.node.nodeid] += 1 + + with self.suspend_capture_ctx(): + self.ihook.pytest_runtest_logreport(report=sub_report) + + if check_interactive_exception(call_info, sub_report): + self.ihook.pytest_exception_interact( + node=self.request.node, call=call_info, report=sub_report + ) + + if exc_val is not None: + if isinstance(exc_val, get_reraise_exceptions(self.config)): + return False + if self.request.session.shouldfail: + return False + return True + + +@contextmanager +def capturing_output(request: SubRequest) -> Iterator[Captured]: + option = request.config.getoption("capture", None) + + capman = request.config.pluginmanager.getplugin("capturemanager") + if getattr(capman, "_capture_fixture", None): + # capsys or capfd are active, subtest should not capture. + fixture = None + elif option == "sys": + fixture = CaptureFixture(SysCapture, request, _ispytest=True) + elif option == "fd": + fixture = CaptureFixture(FDCapture, request, _ispytest=True) + else: + fixture = None + + if fixture is not None: + fixture._start() + + captured = Captured() + try: + yield captured + finally: + if fixture is not None: + out, err = fixture.readouterr() + fixture.close() + captured.out = out + captured.err = err + + +@contextmanager +def capturing_logs( + request: SubRequest, +) -> Iterator[CapturedLogs | None]: + logging_plugin: LoggingPlugin | None = request.config.pluginmanager.getplugin( + "logging-plugin" + ) + if logging_plugin is None: + yield None + else: + handler = LogCaptureHandler() + handler.setFormatter(logging_plugin.formatter) + + captured_logs = CapturedLogs(handler) + with catching_logs(handler, level=logging_plugin.log_level): + yield captured_logs + + +@dataclasses.dataclass +class Captured: + out: str = "" + err: str = "" + + +@dataclasses.dataclass +class CapturedLogs: + handler: LogCaptureHandler + + +def pytest_report_to_serializable(report: TestReport) -> dict[str, Any] | None: + if isinstance(report, SubtestReport): + return report._to_json() + return None + + +def pytest_report_from_serializable(data: dict[str, Any]) -> SubtestReport | None: + if data.get("_report_type") == "SubTestReport": + return SubtestReport._from_json(data) + return None + + +# Dict of nodeid -> number of failed subtests. +# Used to fail top-level tests that passed but contain failed subtests. +failed_subtests_key = StashKey[defaultdict[str, int]]() + + +def pytest_configure(config: Config) -> None: + config.stash[failed_subtests_key] = defaultdict(int) + + +@hookimpl(tryfirst=True) +def pytest_report_teststatus( + report: TestReport, + config: Config, +) -> tuple[str, str, str | Mapping[str, bool]] | None: + if report.when != "call": + return None + + quiet = config.get_verbosity(Config.VERBOSITY_SUBTESTS) == 0 + if isinstance(report, SubtestReport): + outcome = report.outcome + description = report._sub_test_description() + + if hasattr(report, "wasxfail"): + if quiet: + return "", "", "" + elif outcome == "skipped": + category = "xfailed" + short = "y" # x letter is used for regular xfail, y for subtest xfail + status = "SUBXFAIL" + # outcome == "passed" in an xfail is only possible via a @pytest.mark.xfail mark, which + # is not applicable to a subtest, which only handles pytest.xfail(). + else: # pragma: no cover + # This should not normally happen, unless some plugin is setting wasxfail without + # the correct outcome. Pytest expects the call outcome to be either skipped or + # passed in case of xfail. + # Let's pass this report to the next hook. + return None + return category, short, f"{status}{description}" + + if report.failed: + return outcome, "u", f"SUBFAILED{description}" + else: + if report.passed: + if quiet: + return "", "", "" + else: + return f"subtests {outcome}", "u", f"SUBPASSED{description}" + elif report.skipped: + if quiet: + return "", "", "" + else: + return outcome, "-", f"SUBSKIPPED{description}" + + else: + failed_subtests_count = config.stash[failed_subtests_key][report.nodeid] + # Top-level test, fail if it contains failed subtests and it has passed. + if report.passed and failed_subtests_count > 0: + report.outcome = "failed" + suffix = "s" if failed_subtests_count > 1 else "" + report.longrepr = f"contains {failed_subtests_count} failed subtest{suffix}" + + return None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/terminal.py b/.venv-test/lib/python3.12/site-packages/_pytest/terminal.py new file mode 100644 index 0000000..b9a65ff --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/terminal.py @@ -0,0 +1,1786 @@ +# mypy: allow-untyped-defs +"""Terminal reporting of the full testing process. + +This is a good source for looking at the various reporting hooks. +""" + +from __future__ import annotations + +import argparse +from collections import Counter +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Mapping +from collections.abc import Sequence +import dataclasses +import datetime +from functools import partial +import inspect +from pathlib import Path +import platform +import sys +import textwrap +from typing import Any +from typing import ClassVar +from typing import final +from typing import Literal +from typing import NamedTuple +from typing import TextIO +from typing import TYPE_CHECKING +import warnings + +import pluggy + +from _pytest import compat +from _pytest import nodes +from _pytest import timing +from _pytest._code import ExceptionInfo +from _pytest._code.code import ExceptionRepr +from _pytest._io import TerminalWriter +from _pytest._io.wcwidth import wcswidth +import _pytest._version +from _pytest.compat import running_on_ci +from _pytest.config import _PluggyPlugin +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.nodes import Item +from _pytest.nodes import Node +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath +from _pytest.reports import BaseReport +from _pytest.reports import CollectReport +from _pytest.reports import TestReport + + +if TYPE_CHECKING: + from _pytest.main import Session + + +REPORT_COLLECTING_RESOLUTION = 0.5 + +KNOWN_TYPES = ( + "failed", + "passed", + "skipped", + "deselected", + "xfailed", + "xpassed", + "warnings", + "error", + "subtests passed", + "subtests failed", + "subtests skipped", +) + +_REPORTCHARS_DEFAULT = "fE" + + +class MoreQuietAction(argparse.Action): + """A modified copy of the argparse count action which counts down and updates + the legacy quiet attribute at the same time. + + Used to unify verbosity handling. + """ + + def __init__( + self, + option_strings: Sequence[str], + dest: str, + default: object = None, + required: bool = False, + help: str | None = None, + ) -> None: + super().__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + default=default, + required=required, + help=help, + ) + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: str | Sequence[object] | None, + option_string: str | None = None, + ) -> None: + new_count = getattr(namespace, self.dest, 0) - 1 + setattr(namespace, self.dest, new_count) + # todo Deprecate config.quiet + namespace.quiet = getattr(namespace, "quiet", 0) + 1 + + +class TestShortLogReport(NamedTuple): + """Used to store the test status result category, shortletter and verbose word. + For example ``"rerun", "R", ("RERUN", {"yellow": True})``. + + :ivar category: + The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string. + + :ivar letter: + The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string. + + :ivar word: + Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``, + ``"ERROR"``, or the empty string. + """ + + category: str + letter: str + word: str | tuple[str, Mapping[str, bool]] + + +def pytest_addoption(parser: Parser) -> None: + group = parser.getgroup("terminal reporting", "Reporting", after="general") + group._addoption( # private to use reserved lower-case short option + "-v", + "--verbose", + action="count", + default=0, + dest="verbose", + help="Increase verbosity", + ) + group.addoption( + "--no-header", + action="store_true", + default=False, + dest="no_header", + help="Disable header", + ) + group.addoption( + "--no-summary", + action="store_true", + default=False, + dest="no_summary", + help="Disable summary", + ) + group.addoption( + "--no-fold-skipped", + action="store_false", + dest="fold_skipped", + default=True, + help="Do not fold skipped tests in short summary.", + ) + group.addoption( + "--force-short-summary", + action="store_true", + dest="force_short_summary", + default=False, + help="Force condensed summary output regardless of verbosity level.", + ) + group._addoption( # private to use reserved lower-case short option + "-q", + "--quiet", + action=MoreQuietAction, + default=0, + dest="verbose", + help="Decrease verbosity", + ) + group.addoption( + "--verbosity", + dest="verbose", + type=int, + default=0, + help="Set verbosity. Default: 0.", + ) + group._addoption( # private to use reserved lower-case short option + "-r", + "--report-chars", + action="store", + dest="reportchars", + default=_REPORTCHARS_DEFAULT, + metavar="chars", + help="Show extra test summary info as specified by chars: (f)ailed, " + "(E)rror, (s)kipped, (x)failed, (X)passed, " + "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. " + "(w)arnings are enabled by default (see --disable-warnings), " + "'N' can be used to reset the list. (default: 'fE').", + ) + group.addoption( + "--disable-warnings", + "--disable-pytest-warnings", + default=False, + dest="disable_warnings", + action="store_true", + help="Disable warnings summary", + ) + group._addoption( # private to use reserved lower-case short option + "-l", + "--showlocals", + action="store_true", + dest="showlocals", + default=False, + help="Show locals in tracebacks (disabled by default)", + ) + group.addoption( + "--no-showlocals", + action="store_false", + dest="showlocals", + help="Hide locals in tracebacks (negate --showlocals passed through addopts)", + ) + group.addoption( + "--tb", + metavar="style", + action="store", + dest="tbstyle", + default="auto", + choices=["auto", "long", "short", "no", "line", "native"], + help="Traceback print mode (auto/long/short/line/native/no)", + ) + group.addoption( + "--xfail-tb", + action="store_true", + dest="xfail_tb", + default=False, + help="Show tracebacks for xfail (as long as --tb != no)", + ) + group.addoption( + "--show-capture", + action="store", + dest="showcapture", + choices=["no", "stdout", "stderr", "log", "all"], + default="all", + help="Controls how captured stdout/stderr/log is shown on failed tests. " + "Default: all.", + ) + group.addoption( + "--fulltrace", + "--full-trace", + action="store_true", + default=False, + help="Don't cut any tracebacks (default is to cut)", + ) + group.addoption( + "--color", + metavar="color", + action="store", + dest="color", + default="auto", + choices=["yes", "no", "auto"], + help="Color terminal output (yes/no/auto)", + ) + group.addoption( + "--code-highlight", + default="yes", + choices=["yes", "no"], + help="Whether code should be highlighted (only if --color is also enabled). " + "Default: yes.", + ) + + parser.addini( + "console_output_style", + help='Console output: "classic", or with additional progress information ' + '("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces ' + "progress even when capture=no)", + default="progress", + ) + Config._add_verbosity_ini( + parser, + Config.VERBOSITY_TEST_CASES, + help=( + "Specify a verbosity level for test case execution, overriding the main level. " + "Higher levels will provide more detailed information about each test case executed." + ), + ) + + +def pytest_configure(config: Config) -> None: + reporter = TerminalReporter(config, sys.stdout) + config.pluginmanager.register(reporter, "terminalreporter") + if config.option.debug or config.option.traceconfig: + + def mywriter(tags, args): + msg = " ".join(map(str, args)) + reporter.write_line("[traceconfig] " + msg) + + config.trace.root.setprocessor("pytest:config", mywriter) + + # See terminalprogress.py. + # On Windows it's safe to load by default. + if sys.platform == "win32": + config.pluginmanager.import_plugin("terminalprogress") + + +def getreportopt(config: Config) -> str: + reportchars: str = config.option.reportchars + + old_aliases = {"F", "S"} + reportopts = "" + for char in reportchars: + if char in old_aliases: + char = char.lower() + if char == "a": + reportopts = "sxXEf" + elif char == "A": + reportopts = "PpsxXEf" + elif char == "N": + reportopts = "" + elif char not in reportopts: + reportopts += char + + if not config.option.disable_warnings and "w" not in reportopts: + reportopts = "w" + reportopts + elif config.option.disable_warnings and "w" in reportopts: + reportopts = reportopts.replace("w", "") + + return reportopts + + +@hookimpl(trylast=True) # after _pytest.runner +def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str]: + letter = "F" + if report.passed: + letter = "." + elif report.skipped: + letter = "s" + + outcome: str = report.outcome + if report.when in ("collect", "setup", "teardown") and outcome == "failed": + outcome = "error" + letter = "E" + + return outcome, letter, outcome.upper() + + +@dataclasses.dataclass +class WarningReport: + """Simple structure to hold warnings information captured by ``pytest_warning_recorded``. + + :ivar str message: + User friendly message about the warning. + :ivar str|None nodeid: + nodeid that generated the warning (see ``get_location``). + :ivar tuple fslocation: + File system location of the source of the warning (see ``get_location``). + """ + + message: str + nodeid: str | None = None + fslocation: tuple[str, int] | None = None + + count_towards_summary: ClassVar = True + + def get_location(self, config: Config) -> str | None: + """Return the more user-friendly information about the location of a warning, or None.""" + if self.nodeid: + return self.nodeid + if self.fslocation: + filename, linenum = self.fslocation + relpath = bestrelpath(config.invocation_params.dir, absolutepath(filename)) + return f"{relpath}:{linenum}" + return None + + +@final +class TerminalReporter: + def __init__(self, config: Config, file: TextIO | None = None) -> None: + import _pytest.config + + self.config = config + self._numcollected = 0 + self._session: Session | None = None + self._showfspath: bool | None = None + + self.stats: dict[str, list[Any]] = {} + self._main_color: str | None = None + self._known_types: list[str] | None = None + self.startpath = config.invocation_params.dir + if file is None: + file = sys.stdout + self._tw = _pytest.config.create_terminal_writer(config, file) + self._screen_width = self._tw.fullwidth + self.currentfspath: None | Path | str | int = None + self.reportchars = getreportopt(config) + self.foldskipped = config.option.fold_skipped + self.hasmarkup = self._tw.hasmarkup + # isatty should be a method but was wrongly implemented as a boolean. + # We use CallableBool here to support both. + self.isatty = compat.CallableBool(file.isatty()) + self._progress_nodeids_reported: set[str] = set() + self._timing_nodeids_reported: set[str] = set() + self._show_progress_info = self._determine_show_progress_info() + self._collect_report_last_write = timing.Instant() + self._already_displayed_warnings: int | None = None + self._keyboardinterrupt_memo: ExceptionRepr | None = None + + def _determine_show_progress_info( + self, + ) -> Literal["progress", "count", "times", False]: + """Return whether we should display progress information based on the current config.""" + # do not show progress if we are not capturing output (#3038) unless explicitly + # overridden by progress-even-when-capture-no + if ( + self.config.getoption("capture", "no") == "no" + and self.config.getini("console_output_style") + != "progress-even-when-capture-no" + ): + return False + # do not show progress if we are showing fixture setup/teardown + if self.config.getoption("setupshow", False): + return False + cfg: str = self.config.getini("console_output_style") + if cfg in {"progress", "progress-even-when-capture-no"}: + return "progress" + elif cfg == "count": + return "count" + elif cfg == "times": + return "times" + else: + return False + + @property + def verbosity(self) -> int: + verbosity: int = self.config.option.verbose + return verbosity + + @property + def showheader(self) -> bool: + return self.verbosity >= 0 + + @property + def no_header(self) -> bool: + return bool(self.config.option.no_header) + + @property + def no_summary(self) -> bool: + return bool(self.config.option.no_summary) + + @property + def showfspath(self) -> bool: + if self._showfspath is None: + return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) >= 0 + return self._showfspath + + @showfspath.setter + def showfspath(self, value: bool | None) -> None: + self._showfspath = value + + @property + def showlongtestinfo(self) -> bool: + return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) > 0 + + @property + def reported_progress(self) -> int: + """The amount of items reported in the progress so far. + + :meta private: + """ + return len(self._progress_nodeids_reported) + + def hasopt(self, char: str) -> bool: + char = {"xfailed": "x", "skipped": "s"}.get(char, char) + return char in self.reportchars + + def write_fspath_result(self, nodeid: str, res: str, **markup: bool) -> None: + fspath = self.config.rootpath / nodeid.split("::", maxsplit=1)[0] + if self.currentfspath is None or fspath != self.currentfspath: + if self.currentfspath is not None and self._show_progress_info: + self._write_progress_information_filling_space() + self.currentfspath = fspath + relfspath = bestrelpath(self.startpath, fspath) + self._tw.line() + self._tw.write(relfspath + " ") + self._tw.write(res, flush=True, **markup) + + def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None: + if self.currentfspath != prefix: + self._tw.line() + self.currentfspath = prefix + self._tw.write(prefix) + if extra: + self._tw.write(extra, **kwargs) + self.currentfspath = -2 + + def ensure_newline(self) -> None: + if self.currentfspath: + self._tw.line() + self.currentfspath = None + + def wrap_write( + self, + content: str, + *, + flush: bool = False, + margin: int = 8, + line_sep: str = "\n", + **markup: bool, + ) -> None: + """Wrap message with margin for progress info.""" + width_of_current_line = self._tw.width_of_current_line + wrapped = line_sep.join( + textwrap.wrap( + " " * width_of_current_line + content, + width=self._screen_width - margin, + drop_whitespace=True, + replace_whitespace=False, + ), + ) + wrapped = wrapped[width_of_current_line:] + self._tw.write(wrapped, flush=flush, **markup) + + def write(self, content: str, *, flush: bool = False, **markup: bool) -> None: + self._tw.write(content, flush=flush, **markup) + + def write_raw(self, content: str, *, flush: bool = False) -> None: + self._tw.write_raw(content, flush=flush) + + def flush(self) -> None: + self._tw.flush() + + def write_line(self, line: str | bytes, **markup: bool) -> None: + if not isinstance(line, str): + line = str(line, errors="replace") + self.ensure_newline() + self._tw.line(line, **markup) + + def rewrite(self, line: str, **markup: bool) -> None: + """Rewinds the terminal cursor to the beginning and writes the given line. + + :param erase: + If True, will also add spaces until the full terminal width to ensure + previous lines are properly erased. + + The rest of the keyword arguments are markup instructions. + """ + erase = markup.pop("erase", False) + if erase: + fill_count = self._tw.fullwidth - len(line) - 1 + fill = " " * fill_count + else: + fill = "" + line = str(line) + self._tw.write("\r" + line + fill, **markup) + + def write_sep( + self, + sep: str, + title: str | None = None, + fullwidth: int | None = None, + **markup: bool, + ) -> None: + self.ensure_newline() + self._tw.sep(sep, title, fullwidth, **markup) + + def section(self, title: str, sep: str = "=", **kw: bool) -> None: + self._tw.sep(sep, title, **kw) + + def line(self, msg: str, **kw: bool) -> None: + self._tw.line(msg, **kw) + + def _add_stats(self, category: str, items: Sequence[Any]) -> None: + set_main_color = category not in self.stats + self.stats.setdefault(category, []).extend(items) + if set_main_color: + self._set_main_color() + + def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool: + for line in str(excrepr).split("\n"): + self.write_line("INTERNALERROR> " + line) + return True + + def pytest_warning_recorded( + self, + warning_message: warnings.WarningMessage, + nodeid: str, + ) -> None: + from _pytest.warnings import warning_record_to_str + + fslocation = warning_message.filename, warning_message.lineno + message = warning_record_to_str(warning_message) + + warning_report = WarningReport( + fslocation=fslocation, message=message, nodeid=nodeid + ) + self._add_stats("warnings", [warning_report]) + + def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: + if self.config.option.traceconfig: + msg = f"PLUGIN registered: {plugin}" + # XXX This event may happen during setup/teardown time + # which unfortunately captures our output here + # which garbles our output if we use self.write_line. + self.write_line(msg) + + def pytest_deselected(self, items: Sequence[Item]) -> None: + self._add_stats("deselected", items) + + def pytest_runtest_logstart( + self, nodeid: str, location: tuple[str, int | None, str] + ) -> None: + fspath, lineno, domain = location + # Ensure that the path is printed before the + # 1st test of a module starts running. + if self.showlongtestinfo: + line = self._locationline(nodeid, fspath, lineno, domain) + self.write_ensure_prefix(line, "") + self.flush() + elif self.showfspath: + self.write_fspath_result(nodeid, "") + self.flush() + + def pytest_runtest_logreport(self, report: TestReport) -> None: + self._tests_ran = True + rep = report + + res = TestShortLogReport( + *self.config.hook.pytest_report_teststatus(report=rep, config=self.config) + ) + category, letter, word = res.category, res.letter, res.word + if not isinstance(word, tuple): + markup = None + else: + word, markup = word + self._add_stats(category, [rep]) + if not letter and not word: + # Probably passed setup/teardown. + return + if markup is None: + was_xfail = hasattr(report, "wasxfail") + if rep.passed and not was_xfail: + markup = {"green": True} + elif rep.passed and was_xfail: + markup = {"yellow": True} + elif rep.failed: + markup = {"red": True} + elif rep.skipped: + markup = {"yellow": True} + else: + markup = {} + self._progress_nodeids_reported.add(rep.nodeid) + if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0: + self._tw.write(letter, **markup) + # When running in xdist, the logreport and logfinish of multiple + # items are interspersed, e.g. `logreport`, `logreport`, + # `logfinish`, `logfinish`. To avoid the "past edge" calculation + # from getting confused and overflowing (#7166), do the past edge + # printing here and not in logfinish, except for the 100% which + # should only be printed after all teardowns are finished. + if self._show_progress_info and not self._is_last_item: + self._write_progress_information_if_past_edge() + else: + line = self._locationline(rep.nodeid, *rep.location) + running_xdist = hasattr(rep, "node") + if not running_xdist: + self.write_ensure_prefix(line, word, **markup) + if rep.skipped or hasattr(report, "wasxfail"): + reason = _get_raw_skip_reason(rep) + if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) < 2: + available_width = ( + (self._tw.fullwidth - self._tw.width_of_current_line) + - len(" [100%]") + - 1 + ) + formatted_reason = _format_trimmed( + " ({})", reason, available_width + ) + else: + formatted_reason = f" ({reason})" + + if reason and formatted_reason is not None: + self.wrap_write(formatted_reason) + if self._show_progress_info: + self._write_progress_information_filling_space() + else: + self.ensure_newline() + self._tw.write(f"[{rep.node.gateway.id}]") + if self._show_progress_info: + self._tw.write( + self._get_progress_information_message() + " ", cyan=True + ) + else: + self._tw.write(" ") + self._tw.write(word, **markup) + self._tw.write(" " + line) + self.currentfspath = -2 + self.flush() + + @property + def _is_last_item(self) -> bool: + assert self._session is not None + return self.reported_progress == self._session.testscollected + + @hookimpl(wrapper=True) + def pytest_runtestloop(self) -> Generator[None, object, object]: + result = yield + + # Write the final/100% progress -- deferred until the loop is complete. + if ( + self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0 + and self._show_progress_info + and self.reported_progress + ): + self._write_progress_information_filling_space() + + return result + + def _get_progress_information_message(self) -> str: + assert self._session + collected = self._session.testscollected + if self._show_progress_info == "count": + if collected: + progress = self.reported_progress + counter_format = f"{{:{len(str(collected))}d}}" + format_string = f" [{counter_format}/{{}}]" + return format_string.format(progress, collected) + return f" [ {collected} / {collected} ]" + if self._show_progress_info == "times": + if not collected: + return "" + all_reports = ( + self._get_reports_to_display("passed") + + self._get_reports_to_display("xpassed") + + self._get_reports_to_display("failed") + + self._get_reports_to_display("xfailed") + + self._get_reports_to_display("skipped") + + self._get_reports_to_display("error") + + self._get_reports_to_display("") + ) + current_location = all_reports[-1].location[0] + not_reported = [ + r for r in all_reports if r.nodeid not in self._timing_nodeids_reported + ] + tests_in_module = sum( + i.location[0] == current_location for i in self._session.items + ) + tests_completed = sum( + r.when == "setup" + for r in not_reported + if r.location[0] == current_location + ) + last_in_module = tests_completed == tests_in_module + if self.showlongtestinfo or last_in_module: + self._timing_nodeids_reported.update(r.nodeid for r in not_reported) + return format_node_duration( + sum(r.duration for r in not_reported if isinstance(r, TestReport)) + ) + return "" + if collected: + return f" [{self.reported_progress * 100 // collected:3d}%]" + return " [100%]" + + def _write_progress_information_if_past_edge(self) -> None: + w = self._width_of_current_line + if self._show_progress_info == "count": + assert self._session + num_tests = self._session.testscollected + progress_length = len(f" [{num_tests}/{num_tests}]") + elif self._show_progress_info == "times": + progress_length = len(" 99h 59m") + else: + progress_length = len(" [100%]") + past_edge = w + progress_length + 1 >= self._screen_width + if past_edge: + main_color, _ = self._get_main_color() + msg = self._get_progress_information_message() + self._tw.write(msg + "\n", **{main_color: True}) + + def _write_progress_information_filling_space(self) -> None: + color, _ = self._get_main_color() + msg = self._get_progress_information_message() + w = self._width_of_current_line + fill = self._tw.fullwidth - w - 1 + self.write(msg.rjust(fill), flush=True, **{color: True}) + + @property + def _width_of_current_line(self) -> int: + """Return the width of the current line.""" + return self._tw.width_of_current_line + + def pytest_collection(self) -> None: + if self.isatty(): + if self.config.option.verbose >= 0: + self.write("collecting ... ", flush=True, bold=True) + elif self.config.option.verbose >= 1: + self.write("collecting ... ", flush=True, bold=True) + + def pytest_collectreport(self, report: CollectReport) -> None: + if report.failed: + self._add_stats("error", [report]) + elif report.skipped: + self._add_stats("skipped", [report]) + items = [x for x in report.result if isinstance(x, Item)] + self._numcollected += len(items) + if self.isatty(): + self.report_collect() + + def report_collect(self, final: bool = False) -> None: + if self.config.option.verbose < 0: + return + + if not final: + # Only write the "collecting" report every `REPORT_COLLECTING_RESOLUTION`. + if ( + self._collect_report_last_write.elapsed().seconds + < REPORT_COLLECTING_RESOLUTION + ): + return + self._collect_report_last_write = timing.Instant() + + errors = len(self.stats.get("error", [])) + skipped = len(self.stats.get("skipped", [])) + deselected = len(self.stats.get("deselected", [])) + selected = self._numcollected - deselected + line = "collected " if final else "collecting " + line += ( + str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") + ) + if errors: + line += f" / {errors} error{'s' if errors != 1 else ''}" + if deselected: + line += f" / {deselected} deselected" + if skipped: + line += f" / {skipped} skipped" + if self._numcollected > selected: + line += f" / {selected} selected" + if self.isatty(): + self.rewrite(line, bold=True, erase=True) + if final: + self.write("\n") + else: + self.write_line(line) + + @hookimpl(trylast=True) + def pytest_sessionstart(self, session: Session) -> None: + self._session = session + self._session_start = timing.Instant() + if not self.showheader: + return + self.write_sep("=", "test session starts", bold=True) + verinfo = platform.python_version() + if not self.no_header: + msg = f"platform {sys.platform} -- Python {verinfo}" + pypy_version_info = getattr(sys, "pypy_version_info", None) + if pypy_version_info: + verinfo = ".".join(map(str, pypy_version_info[:3])) + msg += f"[pypy-{verinfo}-{pypy_version_info[3]}]" + msg += f", pytest-{_pytest._version.version}, pluggy-{pluggy.__version__}" + if ( + self.verbosity > 0 + or self.config.option.debug + or getattr(self.config.option, "pastebin", None) + ): + msg += " -- " + str(sys.executable) + self.write_line(msg) + lines = self.config.hook.pytest_report_header( + config=self.config, start_path=self.startpath + ) + self._write_report_lines_from_hooks(lines) + + def _write_report_lines_from_hooks( + self, lines: Sequence[str | Sequence[str]] + ) -> None: + for line_or_lines in reversed(lines): + if isinstance(line_or_lines, str): + self.write_line(line_or_lines) + else: + for line in line_or_lines: + self.write_line(line) + + def pytest_report_header(self, config: Config) -> list[str]: + result = [f"rootdir: {config.rootpath}"] + + if config.inipath: + warning = "" + if config._ignored_config_files: + warning = f" (WARNING: ignoring pytest config in {', '.join(config._ignored_config_files)}!)" + result.append( + "configfile: " + bestrelpath(config.rootpath, config.inipath) + warning + ) + + if config.args_source == Config.ArgsSource.TESTPATHS: + testpaths: list[str] = config.getini("testpaths") + result.append("testpaths: {}".format(", ".join(testpaths))) + + plugininfo = config.pluginmanager.list_plugin_distinfo() + if plugininfo: + result.append( + "plugins: {}".format(", ".join(_plugin_nameversions(plugininfo))) + ) + return result + + def pytest_collection_finish(self, session: Session) -> None: + self.report_collect(True) + + lines = self.config.hook.pytest_report_collectionfinish( + config=self.config, + start_path=self.startpath, + items=session.items, + ) + self._write_report_lines_from_hooks(lines) + + if self.config.getoption("collectonly"): + if session.items: + if self.config.option.verbose > -1: + self._tw.line("") + self._printcollecteditems(session.items) + + failed = self.stats.get("failed") + if failed: + self._tw.sep("!", "collection failures") + for rep in failed: + rep.toterminal(self._tw) + + def _printcollecteditems(self, items: Sequence[Item]) -> None: + test_cases_verbosity = self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) + if test_cases_verbosity < 0: + if test_cases_verbosity < -1: + counts = Counter(item.nodeid.split("::", 1)[0] for item in items) + for name, count in sorted(counts.items()): + self._tw.line(f"{name}: {count}") + else: + for item in items: + self._tw.line(item.nodeid) + return + stack: list[Node] = [] + indent = "" + for item in items: + needed_collectors = item.listchain()[1:] # strip root node + while stack: + if stack == needed_collectors[: len(stack)]: + break + stack.pop() + for col in needed_collectors[len(stack) :]: + stack.append(col) + indent = (len(stack) - 1) * " " + self._tw.line(f"{indent}{col}") + if test_cases_verbosity >= 1: + obj = getattr(col, "obj", None) + doc = inspect.getdoc(obj) if obj else None + if doc: + for line in doc.splitlines(): + self._tw.line("{}{}".format(indent + " ", line)) + + @hookimpl(wrapper=True) + def pytest_sessionfinish( + self, session: Session, exitstatus: int | ExitCode + ) -> Generator[None]: + result = yield + self._tw.line("") + summary_exit_codes = ( + ExitCode.OK, + ExitCode.TESTS_FAILED, + ExitCode.INTERRUPTED, + ExitCode.USAGE_ERROR, + ExitCode.NO_TESTS_COLLECTED, + ExitCode.MAX_WARNINGS_ERROR, + ) + if exitstatus in summary_exit_codes and not self.no_summary: + self.config.hook.pytest_terminal_summary( + terminalreporter=self, exitstatus=exitstatus, config=self.config + ) + # Check --max-warnings threshold after all warnings have been collected. + max_warnings = self._get_max_warnings() + if max_warnings is not None and session.exitstatus == ExitCode.OK: + warning_count = len(self.stats.get("warnings", [])) + if warning_count > max_warnings: + session.exitstatus = ExitCode.MAX_WARNINGS_ERROR + self.write_line( + "Tests pass, but maximum allowed warnings exceeded: " + f"{warning_count} > {max_warnings}", + red=True, + ) + if session.shouldfail: + self.write_sep("!", str(session.shouldfail), red=True) + if exitstatus == ExitCode.INTERRUPTED: + self._report_keyboardinterrupt() + self._keyboardinterrupt_memo = None + elif session.shouldstop: + self.write_sep("!", str(session.shouldstop), red=True) + self.summary_stats() + return result + + @hookimpl(wrapper=True) + def pytest_terminal_summary(self) -> Generator[None]: + self.summary_errors() + self.summary_failures() + self.summary_xfailures() + self.summary_warnings() + self.summary_passes() + self.summary_xpasses() + try: + return (yield) + finally: + self.short_test_summary() + # Display any extra warnings from teardown here (if any). + self.summary_warnings() + + def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None: + self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) + + def pytest_unconfigure(self) -> None: + if self._keyboardinterrupt_memo is not None: + self._report_keyboardinterrupt() + + def _report_keyboardinterrupt(self) -> None: + excrepr = self._keyboardinterrupt_memo + assert excrepr is not None + assert excrepr.reprcrash is not None + msg = excrepr.reprcrash.message + self.write_sep("!", msg) + if "KeyboardInterrupt" in msg: + if self.config.option.fulltrace: + excrepr.toterminal(self._tw) + else: + excrepr.reprcrash.toterminal(self._tw) + self._tw.line( + "(to show a full traceback on KeyboardInterrupt use --full-trace)", + yellow=True, + ) + + def _locationline( + self, nodeid: str, fspath: str, lineno: int | None, domain: str + ) -> str: + def mkrel(nodeid: str) -> str: + line = self.config.cwd_relative_nodeid(nodeid) + if domain and line.endswith(domain): + line = line[: -len(domain)] + values = domain.split("[") + values[0] = values[0].replace(".", "::") # don't replace '.' in params + line += "[".join(values) + return line + + # fspath comes from testid which has a "/"-normalized path. + if fspath: + res = mkrel(nodeid) + if self.verbosity >= 2 and ( + nodeid.split("::", maxsplit=1)[0] != nodes.norm_sep(fspath) + ): + res += " <- " + bestrelpath(self.startpath, Path(fspath)) + else: + res = "[location]" + return res + " " + + def _getfailureheadline(self, rep): + head_line = rep.head_line + if head_line: + return head_line + return "test session" # XXX? + + def _getcrashline(self, rep): + try: + return str(rep.longrepr.reprcrash) + except AttributeError: + try: + return str(rep.longrepr)[:50] + except AttributeError: + return "" + + def _get_max_warnings(self) -> int | None: + """Return the max_warnings threshold, from CLI or INI, or None if unset.""" + value = self.config.option.max_warnings + if value is not None: + return int(value) + ini_value = self.config.getini("max_warnings") + if ini_value: + return int(ini_value) + return None + + # + # Summaries for sessionfinish. + # + def getreports(self, name: str): + return [x for x in self.stats.get(name, ()) if not hasattr(x, "_pdbshown")] + + def summary_warnings(self) -> None: + if self.hasopt("w"): + all_warnings: list[WarningReport] | None = self.stats.get("warnings") + if not all_warnings: + return + + final = self._already_displayed_warnings is not None + if final: + warning_reports = all_warnings[self._already_displayed_warnings :] + else: + warning_reports = all_warnings + self._already_displayed_warnings = len(warning_reports) + if not warning_reports: + return + + reports_grouped_by_message: dict[str, list[WarningReport]] = {} + for wr in warning_reports: + reports_grouped_by_message.setdefault(wr.message, []).append(wr) + + def collapsed_location_report(reports: list[WarningReport]) -> str: + locations = [] + for w in reports: + location = w.get_location(self.config) + if location: + locations.append(location) + + if len(locations) < 10: + return "\n".join(map(str, locations)) + + counts_by_filename = Counter( + str(loc).split("::", 1)[0] for loc in locations + ) + return "\n".join( + "{}: {} warning{}".format(k, v, "s" if v > 1 else "") + for k, v in counts_by_filename.items() + ) + + title = "warnings summary (final)" if final else "warnings summary" + self.write_sep("=", title, yellow=True, bold=False) + for message, message_reports in reports_grouped_by_message.items(): + maybe_location = collapsed_location_report(message_reports) + if maybe_location: + self._tw.line(maybe_location) + lines = message.splitlines() + indented = "\n".join(" " + x for x in lines) + message = indented.rstrip() + else: + message = message.rstrip() + self._tw.line(message) + self._tw.line() + self._tw.line( + "-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html" + ) + + def summary_passes(self) -> None: + self.summary_passes_combined("passed", "PASSES", "P") + + def summary_xpasses(self) -> None: + self.summary_passes_combined("xpassed", "XPASSES", "X") + + def summary_passes_combined( + self, which_reports: str, sep_title: str, needed_opt: str + ) -> None: + if self.config.option.tbstyle != "no": + if self.hasopt(needed_opt): + reports: list[TestReport] = self.getreports(which_reports) + if not reports: + return + self.write_sep("=", sep_title) + for rep in reports: + if rep.sections: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg, green=True, bold=True) + self._outrep_summary(rep) + self._handle_teardown_sections(rep.nodeid) + + def _get_teardown_reports(self, nodeid: str) -> list[TestReport]: + reports = self.getreports("") + return [ + report + for report in reports + if report.when == "teardown" and report.nodeid == nodeid + ] + + def _handle_teardown_sections(self, nodeid: str) -> None: + for report in self._get_teardown_reports(nodeid): + self.print_teardown_sections(report) + + def print_teardown_sections(self, rep: TestReport) -> None: + showcapture = self.config.option.showcapture + if showcapture == "no": + return + for secname, content in rep.sections: + if showcapture != "all" and showcapture not in secname: + continue + if "teardown" in secname: + self._tw.sep("-", secname) + if content[-1:] == "\n": + content = content[:-1] + self._tw.line(content) + + def summary_failures(self) -> None: + style = self.config.option.tbstyle + self.summary_failures_combined("failed", "FAILURES", style=style) + + def summary_xfailures(self) -> None: + show_tb = self.config.option.xfail_tb + style = self.config.option.tbstyle if show_tb else "no" + self.summary_failures_combined("xfailed", "XFAILURES", style=style) + + def summary_failures_combined( + self, + which_reports: str, + sep_title: str, + *, + style: str, + needed_opt: str | None = None, + ) -> None: + if style != "no": + if not needed_opt or self.hasopt(needed_opt): + reports: list[BaseReport] = self.getreports(which_reports) + if not reports: + return + self.write_sep("=", sep_title) + if style == "line": + for rep in reports: + line = self._getcrashline(rep) + self._outrep_summary(rep) + self.write_line(line) + else: + for rep in reports: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) + self._handle_teardown_sections(rep.nodeid) + + def summary_errors(self) -> None: + if self.config.option.tbstyle != "no": + reports: list[BaseReport] = self.getreports("error") + if not reports: + return + self.write_sep("=", "ERRORS") + for rep in self.stats["error"]: + msg = self._getfailureheadline(rep) + if rep.when == "collect": + msg = "ERROR collecting " + msg + else: + msg = f"ERROR at {rep.when} of {msg}" + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) + + def _outrep_summary(self, rep: BaseReport) -> None: + rep.toterminal(self._tw) + showcapture = self.config.option.showcapture + if showcapture == "no": + return + for secname, content in rep.sections: + if showcapture != "all" and showcapture not in secname: + continue + self._tw.sep("-", secname) + if content[-1:] == "\n": + content = content[:-1] + self._tw.line(content) + + def summary_stats(self) -> None: + if self.verbosity < -1: + return + + session_duration = self._session_start.elapsed() + (parts, main_color) = self.build_summary_stats_line() + line_parts = [] + + display_sep = self.verbosity >= 0 + if display_sep: + fullwidth = self._tw.fullwidth + for text, markup in parts: + with_markup = self._tw.markup(text, **markup) + if display_sep: + fullwidth += len(with_markup) - len(text) + line_parts.append(with_markup) + msg = ", ".join(line_parts) + + main_markup = {main_color: True} + duration = f" in {format_session_duration(session_duration.seconds)}" + duration_with_markup = self._tw.markup(duration, **main_markup) + if display_sep: + fullwidth += len(duration_with_markup) - len(duration) + msg += duration_with_markup + + if display_sep: + markup_for_end_sep = self._tw.markup("", **main_markup) + if markup_for_end_sep.endswith("\x1b[0m"): + markup_for_end_sep = markup_for_end_sep[:-4] + fullwidth += len(markup_for_end_sep) + msg += markup_for_end_sep + + if display_sep: + self.write_sep("=", msg, fullwidth=fullwidth, **main_markup) + else: + self.write_line(msg, **main_markup) + + def short_test_summary(self) -> None: + if not self.reportchars: + return + + def show_simple(lines: list[str], *, stat: str) -> None: + failed = self.stats.get(stat, []) + if not failed: + return + config = self.config + for rep in failed: + color = _color_for_type.get(stat, _color_for_type_default) + line = _get_line_with_reprcrash_message( + config, rep, self._tw, {color: True} + ) + lines.append(line) + + def show_xfailed(lines: list[str]) -> None: + xfailed = self.stats.get("xfailed", []) + for rep in xfailed: + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} + ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) + nodeid = _get_node_id_with_markup(self._tw, self.config, rep) + line = f"{markup_word} {nodeid}" + reason = rep.wasxfail + if reason: + line += " - " + str(reason) + + lines.append(line) + + def show_xpassed(lines: list[str]) -> None: + xpassed = self.stats.get("xpassed", []) + for rep in xpassed: + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} + ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) + nodeid = _get_node_id_with_markup(self._tw, self.config, rep) + line = f"{markup_word} {nodeid}" + reason = rep.wasxfail + if reason: + line += " - " + str(reason) + lines.append(line) + + def show_skipped_folded(lines: list[str]) -> None: + skipped: list[CollectReport] = self.stats.get("skipped", []) + fskips = _folded_skips(self.startpath, skipped) if skipped else [] + if not fskips: + return + verbose_word, verbose_markup = skipped[0]._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} + ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) + prefix = "Skipped: " + for num, fspath, lineno, reason in fskips: + if reason.startswith(prefix): + reason = reason[len(prefix) :] + if lineno is not None: + lines.append(f"{markup_word} [{num}] {fspath}:{lineno}: {reason}") + else: + lines.append(f"{markup_word} [{num}] {fspath}: {reason}") + + def show_skipped_unfolded(lines: list[str]) -> None: + skipped: list[CollectReport] = self.stats.get("skipped", []) + + for rep in skipped: + assert rep.longrepr is not None + assert isinstance(rep.longrepr, tuple), (rep, rep.longrepr) + assert len(rep.longrepr) == 3, (rep, rep.longrepr) + + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} + ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) + nodeid = _get_node_id_with_markup(self._tw, self.config, rep) + line = f"{markup_word} {nodeid}" + reason = rep.longrepr[2] + if reason: + line += " - " + str(reason) + lines.append(line) + + def show_skipped(lines: list[str]) -> None: + if self.foldskipped: + show_skipped_folded(lines) + else: + show_skipped_unfolded(lines) + + REPORTCHAR_ACTIONS: Mapping[str, Callable[[list[str]], None]] = { + "x": show_xfailed, + "X": show_xpassed, + "f": partial(show_simple, stat="failed"), + "s": show_skipped, + "p": partial(show_simple, stat="passed"), + "E": partial(show_simple, stat="error"), + } + + lines: list[str] = [] + for char in self.reportchars: + action = REPORTCHAR_ACTIONS.get(char) + if action: # skipping e.g. "P" (passed with output) here. + action(lines) + + if lines: + self.write_sep("=", "short test summary info", cyan=True, bold=True) + for line in lines: + self.write_line(line) + + def _get_main_color(self) -> tuple[str, list[str]]: + if self._main_color is None or self._known_types is None or self._is_last_item: + self._set_main_color() + assert self._main_color + assert self._known_types + return self._main_color, self._known_types + + def _determine_main_color(self, unknown_type_seen: bool) -> str: + stats = self.stats + if "failed" in stats or "error" in stats: + main_color = "red" + elif "warnings" in stats or "xpassed" in stats or unknown_type_seen: + main_color = "yellow" + elif "passed" in stats or not self._is_last_item: + main_color = "green" + else: + main_color = "yellow" + return main_color + + def _set_main_color(self) -> None: + unknown_types: list[str] = [] + for found_type in self.stats: + if found_type: # setup/teardown reports have an empty key, ignore them + if found_type not in KNOWN_TYPES and found_type not in unknown_types: + unknown_types.append(found_type) + self._known_types = list(KNOWN_TYPES) + unknown_types + self._main_color = self._determine_main_color(bool(unknown_types)) + + def build_summary_stats_line(self) -> tuple[list[tuple[str, dict[str, bool]]], str]: + """ + Build the parts used in the last summary stats line. + + The summary stats line is the line shown at the end, "=== 12 passed, 2 errors in Xs===". + + This function builds a list of the "parts" that make up for the text in that line, in + the example above it would be:: + + [ + ("12 passed", {"green": True}), + ("2 errors", {"red": True} + ] + + That last dict for each line is a "markup dictionary", used by TerminalWriter to + color output. + + The final color of the line is also determined by this function, and is the second + element of the returned tuple. + """ + if self.config.getoption("collectonly"): + return self._build_collect_only_summary_stats_line() + else: + return self._build_normal_summary_stats_line() + + def _get_reports_to_display(self, key: str) -> list[Any]: + """Get test/collection reports for the given status key, such as `passed` or `error`.""" + reports = self.stats.get(key, []) + return [x for x in reports if getattr(x, "count_towards_summary", True)] + + def _build_normal_summary_stats_line( + self, + ) -> tuple[list[tuple[str, dict[str, bool]]], str]: + main_color, known_types = self._get_main_color() + parts = [] + + for key in known_types: + reports = self._get_reports_to_display(key) + if reports: + count = len(reports) + color = _color_for_type.get(key, _color_for_type_default) + markup = {color: True, "bold": color == main_color} + parts.append(("%d %s" % pluralize(count, key), markup)) # noqa: UP031 + + if not parts: + parts = [("no tests ran", {_color_for_type_default: True})] + + return parts, main_color + + def _build_collect_only_summary_stats_line( + self, + ) -> tuple[list[tuple[str, dict[str, bool]]], str]: + deselected = len(self._get_reports_to_display("deselected")) + errors = len(self._get_reports_to_display("error")) + + if self._numcollected == 0: + parts = [("no tests collected", {"yellow": True})] + main_color = "yellow" + + elif deselected == 0: + main_color = "green" + collected_output = "%d %s collected" % pluralize(self._numcollected, "test") # noqa: UP031 + parts = [(collected_output, {main_color: True})] + else: + all_tests_were_deselected = self._numcollected == deselected + if all_tests_were_deselected: + main_color = "yellow" + collected_output = f"no tests collected ({deselected} deselected)" + else: + main_color = "green" + selected = self._numcollected - deselected + collected_output = f"{selected}/{self._numcollected} tests collected ({deselected} deselected)" + + parts = [(collected_output, {main_color: True})] + + if errors: + main_color = _color_for_type["error"] + parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})] # noqa: UP031 + + return parts, main_color + + +def _get_node_id_with_markup(tw: TerminalWriter, config: Config, rep: BaseReport): + nodeid = config.cwd_relative_nodeid(rep.nodeid) + path, *parts = nodeid.split("::") + if parts: + parts_markup = tw.markup("::".join(parts), bold=True) + return path + "::" + parts_markup + else: + return path + + +def _format_trimmed(format: str, msg: str, available_width: int) -> str | None: + """Format msg into format, ellipsizing it if doesn't fit in available_width. + + Returns None if even the ellipsis can't fit. + """ + # Only use the first line. + i = msg.find("\n") + if i != -1: + msg = msg[:i] + + ellipsis = "..." + format_width = wcswidth(format.format("")) + if format_width + len(ellipsis) > available_width: + return None + + if format_width + wcswidth(msg) > available_width: + available_width -= len(ellipsis) + msg = msg[:available_width] + while format_width + wcswidth(msg) > available_width: + msg = msg[:-1] + msg += ellipsis + + return format.format(msg) + + +def _get_line_with_reprcrash_message( + config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool] +) -> str: + """Get summary line for a report, trying to add reprcrash message.""" + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + config, word_markup + ) + word = tw.markup(verbose_word, **verbose_markup) + node = _get_node_id_with_markup(tw, config, rep) + + line = f"{word} {node}" + line_width = wcswidth(line) + + msg: str | None + try: + if isinstance(rep.longrepr, str): + msg = rep.longrepr + else: + # Type ignored intentionally -- possible AttributeError expected. + msg = rep.longrepr.reprcrash.message # type: ignore[union-attr] + except AttributeError: + pass + else: + if ( + running_on_ci() or config.option.verbose >= 2 + ) and not config.option.force_short_summary: + msg = f" - {msg}" + else: + available_width = tw.fullwidth - line_width + msg = _format_trimmed(" - {}", msg, available_width) + if msg is not None: + line += msg + + return line + + +def _folded_skips( + startpath: Path, + skipped: Sequence[CollectReport], +) -> list[tuple[int, str, int | None, str]]: + d: dict[tuple[str, int | None, str], list[CollectReport]] = {} + for event in skipped: + assert event.longrepr is not None + assert isinstance(event.longrepr, tuple), (event, event.longrepr) + assert len(event.longrepr) == 3, (event, event.longrepr) + fspath, lineno, reason = event.longrepr + # For consistency, report all fspaths in relative form. + fspath = bestrelpath(startpath, Path(fspath)) + keywords = getattr(event, "keywords", {}) + # Folding reports with global pytestmark variable. + # This is a workaround, because for now we cannot identify the scope of a skip marker + # TODO: Revisit after marks scope would be fixed. + if ( + event.when == "setup" + and "skip" in keywords + and "pytestmark" not in keywords + ): + key: tuple[str, int | None, str] = (fspath, None, reason) + else: + key = (fspath, lineno, reason) + d.setdefault(key, []).append(event) + values: list[tuple[int, str, int | None, str]] = [] + for key, events in d.items(): + values.append((len(events), *key)) + return values + + +_color_for_type = { + "failed": "red", + "error": "red", + "warnings": "yellow", + "passed": "green", + "subtests passed": "green", + "subtests failed": "red", +} +_color_for_type_default = "yellow" + + +def pluralize(count: int, noun: str) -> tuple[int, str]: + # No need to pluralize words such as `failed` or `passed`. + if noun not in ["error", "warnings", "test"]: + return count, noun + + # The `warnings` key is plural. To avoid API breakage, we keep it that way but + # set it to singular here so we can determine plurality in the same way as we do + # for `error`. + noun = noun.replace("warnings", "warning") + + return count, noun + "s" if count != 1 else noun + + +def _plugin_nameversions(plugininfo) -> list[str]: + values: list[str] = [] + for plugin, dist in plugininfo: + # Gets us name and version! + name = f"{dist.project_name}-{dist.version}" + # Questionable convenience, but it keeps things short. + if name.startswith("pytest-"): + name = name[7:] + # We decided to print python package names they can have more than one plugin. + if name not in values: + values.append(name) + return values + + +def format_session_duration(seconds: float) -> str: + """Format the given seconds in a human readable manner to show in the final summary.""" + if seconds < 60: + return f"{seconds:.2f}s" + else: + dt = datetime.timedelta(seconds=int(seconds)) + return f"{seconds:.2f}s ({dt})" + + +def format_node_duration(seconds: float) -> str: + """Format the given seconds in a human readable manner to show in the test progress.""" + # The formatting is designed to be compact and readable, with at most 7 characters + # for durations below 100 hours. + if seconds < 0.00001: + return f" {seconds * 1000000:.3f}us" + if seconds < 0.0001: + return f" {seconds * 1000000:.2f}us" + if seconds < 0.001: + return f" {seconds * 1000000:.1f}us" + if seconds < 0.01: + return f" {seconds * 1000:.3f}ms" + if seconds < 0.1: + return f" {seconds * 1000:.2f}ms" + if seconds < 1: + return f" {seconds * 1000:.1f}ms" + if seconds < 60: + return f" {seconds:.3f}s" + if seconds < 3600: + return f" {seconds // 60:.0f}m {seconds % 60:.0f}s" + return f" {seconds // 3600:.0f}h {(seconds % 3600) // 60:.0f}m" + + +def _get_raw_skip_reason(report: TestReport) -> str: + """Get the reason string of a skip/xfail/xpass test report. + + The string is just the part given by the user. + """ + if hasattr(report, "wasxfail"): + reason = report.wasxfail + if reason.startswith("reason: "): + reason = reason[len("reason: ") :] + return reason + else: + assert report.skipped + assert isinstance(report.longrepr, tuple) + _, _, reason = report.longrepr + if reason.startswith("Skipped: "): + reason = reason[len("Skipped: ") :] + elif reason == "Skipped": + reason = "" + return reason + + +class TerminalProgressPlugin: + """Terminal progress reporting plugin using OSC 9;4 ANSI sequences. + + Emits OSC 9;4 sequences to indicate test progress to terminal + tabs/windows/etc. + + Not all terminal emulators support this feature. + + Ref: https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC + """ + + def __init__(self, tr: TerminalReporter) -> None: + self._tr = tr + self._session: Session | None = None + self._has_failures = False + + def _emit_progress( + self, + state: Literal["remove", "normal", "error", "indeterminate", "paused"], + progress: int | None = None, + ) -> None: + """Emit OSC 9;4 sequence for indicating progress to the terminal. + + :param state: + Progress state to set. + :param progress: + Progress value 0-100. Required for "normal", optional for "error" + and "paused", otherwise ignored. + """ + assert progress is None or 0 <= progress <= 100 + + # OSC 9;4 sequence: ESC ] 9 ; 4 ; state ; progress ST + # ST can be ESC \ or BEL. ESC \ seems better supported. + match state: + case "remove": + sequence = "\x1b]9;4;0;\x1b\\" + case "normal": + assert progress is not None + sequence = f"\x1b]9;4;1;{progress}\x1b\\" + case "error": + if progress is not None: + sequence = f"\x1b]9;4;2;{progress}\x1b\\" + else: + sequence = "\x1b]9;4;2;\x1b\\" + case "indeterminate": + sequence = "\x1b]9;4;3;\x1b\\" + case "paused": + if progress is not None: + sequence = f"\x1b]9;4;4;{progress}\x1b\\" + else: + sequence = "\x1b]9;4;4;\x1b\\" + + self._tr.write_raw(sequence, flush=True) + + @hookimpl + def pytest_sessionstart(self, session: Session) -> None: + self._session = session + # Show indeterminate progress during collection. + self._emit_progress("indeterminate") + + @hookimpl + def pytest_collection_finish(self) -> None: + assert self._session is not None + if self._session.testscollected > 0: + # Switch from indeterminate to 0% progress. + self._emit_progress("normal", 0) + + @hookimpl + def pytest_runtest_logreport(self, report: TestReport) -> None: + if report.failed: + self._has_failures = True + + # Let's consider the "call" phase for progress. + if report.when != "call": + return + + # Calculate and emit progress. + assert self._session is not None + collected = self._session.testscollected + if collected > 0: + reported = self._tr.reported_progress + progress = min(reported * 100 // collected, 100) + self._emit_progress("error" if self._has_failures else "normal", progress) + + @hookimpl + def pytest_sessionfinish(self) -> None: + self._emit_progress("remove") diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/terminalprogress.py b/.venv-test/lib/python3.12/site-packages/_pytest/terminalprogress.py new file mode 100644 index 0000000..287f0d5 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/terminalprogress.py @@ -0,0 +1,30 @@ +# A plugin to register the TerminalProgressPlugin plugin. +# +# This plugin is not loaded by default due to compatibility issues (#13896), +# but can be enabled in one of these ways: +# - The terminal plugin enables it in a few cases where it's safe, and not +# blocked by the user (using e.g. `-p no:terminalprogress`). +# - The user explicitly requests it, e.g. using `-p terminalprogress`. +# +# In a few years, if it's safe, we can consider enabling it by default. Then, +# this file will become unnecessary and can be inlined into terminal.py. + +from __future__ import annotations + +import os + +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.terminal import TerminalProgressPlugin +from _pytest.terminal import TerminalReporter + + +@hookimpl(trylast=True) +def pytest_configure(config: Config) -> None: + reporter: TerminalReporter | None = config.pluginmanager.get_plugin( + "terminalreporter" + ) + + if reporter is not None and reporter.isatty() and os.environ.get("TERM") != "dumb": + plugin = TerminalProgressPlugin(reporter) + config.pluginmanager.register(plugin, name="terminalprogress-plugin") diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/threadexception.py b/.venv-test/lib/python3.12/site-packages/_pytest/threadexception.py new file mode 100644 index 0000000..eb57783 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/threadexception.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import collections +from collections.abc import Callable +import functools +import sys +import threading +import traceback +from typing import NamedTuple +from typing import TYPE_CHECKING +import warnings + +from _pytest.config import Config +from _pytest.nodes import Item +from _pytest.stash import StashKey +from _pytest.tracemalloc import tracemalloc_message +import pytest + + +if TYPE_CHECKING: + pass + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + + +class ThreadExceptionMeta(NamedTuple): + msg: str + cause_msg: str + exc_value: BaseException | None + + +thread_exceptions: StashKey[collections.deque[ThreadExceptionMeta | BaseException]] = ( + StashKey() +) + + +def collect_thread_exception(config: Config) -> None: + pop_thread_exception = config.stash[thread_exceptions].pop + errors: list[pytest.PytestUnhandledThreadExceptionWarning | RuntimeError] = [] + meta = None + hook_error = None + try: + while True: + try: + meta = pop_thread_exception() + except IndexError: + break + + if isinstance(meta, BaseException): + hook_error = RuntimeError("Failed to process thread exception") + hook_error.__cause__ = meta + errors.append(hook_error) + continue + + msg = meta.msg + try: + warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) + except pytest.PytestUnhandledThreadExceptionWarning as e: + # This except happens when the warning is treated as an error (e.g. `-Werror`). + if meta.exc_value is not None: + # Exceptions have a better way to show the traceback, but + # warnings do not, so hide the traceback from the msg and + # set the cause so the traceback shows up in the right place. + e.args = (meta.cause_msg,) + e.__cause__ = meta.exc_value + errors.append(e) + + if len(errors) == 1: + raise errors[0] + if errors: + raise ExceptionGroup("multiple thread exception warnings", errors) + finally: + del errors, meta, hook_error + + +def cleanup( + *, config: Config, prev_hook: Callable[[threading.ExceptHookArgs], object] +) -> None: + try: + try: + # We don't join threads here, so exceptions raised from any + # threads still running by the time _threading_atexits joins them + # do not get captured (see #13027). + collect_thread_exception(config) + finally: + threading.excepthook = prev_hook + finally: + del config.stash[thread_exceptions] + + +def thread_exception_hook( + args: threading.ExceptHookArgs, + /, + *, + append: Callable[[ThreadExceptionMeta | BaseException], object], +) -> None: + try: + # we need to compute these strings here as they might change after + # the excepthook finishes and before the metadata object is + # collected by a pytest hook + thread_name = "" if args.thread is None else args.thread.name + summary = f"Exception in thread {thread_name}" + traceback_message = "\n\n" + "".join( + traceback.format_exception( + args.exc_type, + args.exc_value, + args.exc_traceback, + ) + ) + tracemalloc_tb = "\n" + tracemalloc_message(args.thread) + msg = summary + traceback_message + tracemalloc_tb + cause_msg = summary + tracemalloc_tb + + append( + ThreadExceptionMeta( + # Compute these strings here as they might change later + msg=msg, + cause_msg=cause_msg, + exc_value=args.exc_value, + ) + ) + except BaseException as e: + append(e) + # Raising this will cause the exception to be logged twice, once in our + # collect_thread_exception and once by sys.excepthook + # which is fine - this should never happen anyway and if it does + # it should probably be reported as a pytest bug. + raise + + +def pytest_configure(config: Config) -> None: + prev_hook = threading.excepthook + deque: collections.deque[ThreadExceptionMeta | BaseException] = collections.deque() + config.stash[thread_exceptions] = deque + config.add_cleanup(functools.partial(cleanup, config=config, prev_hook=prev_hook)) + threading.excepthook = functools.partial(thread_exception_hook, append=deque.append) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_setup(item: Item) -> None: + collect_thread_exception(item.config) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_call(item: Item) -> None: + collect_thread_exception(item.config) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_teardown(item: Item) -> None: + collect_thread_exception(item.config) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/timing.py b/.venv-test/lib/python3.12/site-packages/_pytest/timing.py new file mode 100644 index 0000000..639bd99 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/timing.py @@ -0,0 +1,97 @@ +"""Indirection for time functions. + +We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect +pytest runtime information (issue #185). + +Fixture "mock_timing" also interacts with this module for pytest's own tests. +""" + +from __future__ import annotations + +import dataclasses +from datetime import datetime +from datetime import timezone +from time import perf_counter +from time import sleep +from time import time +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from pytest import MonkeyPatch + + +@dataclasses.dataclass(frozen=True) +class Instant: + """ + Represents an instant in time, used to both get the timestamp value and to measure + the duration of a time span. + + Inspired by Rust's `std::time::Instant`. + """ + + # Creation time of this instant, using time.time(), to measure actual time. + # Note: using a `lambda` to correctly get the mocked time via `MockTiming`. + # pylint: disable-next=lambda-assignment + time: float = dataclasses.field(default_factory=lambda: time(), init=False) # noqa: PLW0108 + + # Performance counter tick of the instant, used to measure precise elapsed time. + # Note: using a `lambda` to correctly get the mocked time via `MockTiming`. + perf_count: float = dataclasses.field( + default_factory=lambda: perf_counter(), # noqa: PLW0108 + init=False, + ) + + def elapsed(self) -> Duration: + """Measure the duration since `Instant` was created.""" + return Duration(start=self, stop=Instant()) + + def as_utc(self) -> datetime: + """Instant as UTC datetime.""" + return datetime.fromtimestamp(self.time, timezone.utc) + + +@dataclasses.dataclass(frozen=True) +class Duration: + """A span of time as measured by `Instant.elapsed()`.""" + + start: Instant + stop: Instant + + @property + def seconds(self) -> float: + """Elapsed time of the duration in seconds, measured using a performance counter for precise timing.""" + return self.stop.perf_count - self.start.perf_count + + +@dataclasses.dataclass +class MockTiming: + """Mocks _pytest.timing with a known object that can be used to control timing in tests + deterministically. + + pytest itself should always use functions from `_pytest.timing` instead of `time` directly. + + This then allows us more control over time during testing, if testing code also + uses `_pytest.timing` functions. + + Time is static, and only advances through `sleep` calls, thus tests might sleep over large + numbers and obtain accurate time() calls at the end, making tests reliable and instant.""" + + _current_time: float = datetime(2020, 5, 22, 14, 20, 50).timestamp() + + def sleep(self, seconds: float) -> None: + self._current_time += seconds + + def time(self) -> float: + return self._current_time + + def patch(self, monkeypatch: MonkeyPatch) -> None: + # pylint: disable-next=import-self + from _pytest import timing # noqa: PLW0406 + + monkeypatch.setattr(timing, "sleep", self.sleep) + monkeypatch.setattr(timing, "time", self.time) + monkeypatch.setattr(timing, "perf_counter", self.time) + + +__all__ = ["perf_counter", "sleep", "time"] diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/tmpdir.py b/.venv-test/lib/python3.12/site-packages/_pytest/tmpdir.py new file mode 100644 index 0000000..9196006 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/tmpdir.py @@ -0,0 +1,351 @@ +# mypy: allow-untyped-defs +"""Support for providing temporary directories to test functions.""" + +from __future__ import annotations + +import atexit +from collections.abc import Generator +from contextlib import ExitStack +import dataclasses +import os +from pathlib import Path +import re +from shutil import rmtree +import stat +import tempfile +from typing import Any +from typing import final +from typing import Literal + +from .pathlib import cleanup_dead_symlinks +from .pathlib import LOCK_TIMEOUT +from .pathlib import make_numbered_dir +from .pathlib import make_numbered_dir_with_cleanup +from .pathlib import rm_rf +from _pytest.compat import get_user_id +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Item +from _pytest.reports import TestReport +from _pytest.stash import StashKey + + +tmppath_result_key = StashKey[dict[str, bool]]() +RetentionType = Literal["all", "failed", "none"] + + +@final +@dataclasses.dataclass +class TempPathFactory: + """Factory for temporary directories under the common base temp directory, + as discussed at :ref:`temporary directory location and retention`. + """ + + _given_basetemp: Path | None + # pluggy TagTracerSub, not currently exposed, so Any. + _trace: Any + _basetemp: Path | None + _retention_count: int + _retention_policy: RetentionType + + def __init__( + self, + given_basetemp: Path | None, + retention_count: int, + retention_policy: RetentionType, + trace, + basetemp: Path | None = None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + if given_basetemp is None: + self._given_basetemp = None + else: + # Use os.path.abspath() to get absolute path instead of resolve() as it + # does not work the same in all platforms (see #4427). + # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012). + self._given_basetemp = Path(os.path.abspath(str(given_basetemp))) + self._trace = trace + self._retention_count = retention_count + self._retention_policy = retention_policy + self._basetemp = basetemp + # Register cleanups for session finish. Also called atexit as a last + # resort if sessionfinish for some reason doesn't happen. + self._exit_stack = ExitStack() + + @classmethod + def from_config( + cls, + config: Config, + *, + _ispytest: bool = False, + ) -> TempPathFactory: + """Create a factory according to pytest configuration. + + :meta private: + """ + check_ispytest(_ispytest) + count = int(config.getini("tmp_path_retention_count")) + if count < 0: + raise ValueError( + f"tmp_path_retention_count must be >= 0. Current input: {count}." + ) + + policy = config.getini("tmp_path_retention_policy") + if policy not in ("all", "failed", "none"): + raise ValueError( + f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}." + ) + + return cls( + given_basetemp=config.option.basetemp, + trace=config.trace.get("tmpdir"), + retention_count=count, + retention_policy=policy, + _ispytest=True, + ) + + def _ensure_relative_to_basetemp(self, basename: str) -> str: + basename = os.path.normpath(basename) + if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp(): + raise ValueError(f"{basename} is not a normalized and relative path") + return basename + + def mktemp(self, basename: str, numbered: bool = True) -> Path: + """Create a new temporary directory managed by the factory. + + :param basename: + Directory base name, must be a relative path. + + :param numbered: + If ``True``, ensure the directory is unique by adding a numbered + suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True`` + means that this function will create directories named ``"foo-0"``, + ``"foo-1"``, ``"foo-2"`` and so on. + + :returns: + The path to the new directory. + """ + basename = self._ensure_relative_to_basetemp(basename) + if not numbered: + p = self.getbasetemp().joinpath(basename) + p.mkdir(mode=0o700) + else: + p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700) + self._trace("mktemp", p) + return p + + def getbasetemp(self) -> Path: + """Return the base temporary directory, creating it if needed. + + :returns: + The base temporary directory. + """ + if self._basetemp is not None: + return self._basetemp + + if self._given_basetemp is not None: + basetemp = self._given_basetemp + if basetemp.exists(): + rm_rf(basetemp) + basetemp.mkdir(mode=0o700) + basetemp = basetemp.resolve() + else: + from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") + temproot = Path(from_env or tempfile.gettempdir()).resolve() + user = get_user() or "unknown" + # use a sub-directory in the temproot to speed-up + # make_numbered_dir() call + rootdir = temproot.joinpath(f"pytest-of-{user}") + try: + rootdir.mkdir(mode=0o700, exist_ok=True) + except OSError: + # getuser() likely returned illegal characters for the platform, use unknown back off mechanism + rootdir = temproot.joinpath("pytest-of-unknown") + rootdir.mkdir(mode=0o700, exist_ok=True) + # Because we use exist_ok=True with a predictable name, make sure + # we are the owners, to prevent any funny business (on unix, where + # temproot is usually shared). + # Also, to keep things private, fixup any world-readable temp + # rootdir's permissions. Historically 0o755 was used, so we can't + # just error out on this, at least for a while. + # Don't follow symlinks, otherwise we're open to symlink-swapping + # TOCTOU vulnerability. + # This check makes us vulnerable to a DoS - a user can `mkdir + # /tmp/pytest-of-otheruser` and then `otheruser` will fail this + # check. For now we don't consider it a real problem. otheruser can + # change their TMPDIR or --basetemp, and maybe give the prankster a + # good scolding. + uid = get_user_id() + if uid is not None: + stat_follow_symlinks = ( + False if os.stat in os.supports_follow_symlinks else True + ) + rootdir_stat = rootdir.stat(follow_symlinks=stat_follow_symlinks) + if stat.S_ISLNK(rootdir_stat.st_mode): + raise OSError( + f"The temporary directory {rootdir} is a symbolic link. " + "Fix this and try again." + ) + if rootdir_stat.st_uid != uid: + raise OSError( + f"The temporary directory {rootdir} is not owned by the current user. " + "Fix this and try again." + ) + if (rootdir_stat.st_mode & 0o077) != 0: + chmod_follow_symlinks = ( + False if os.chmod in os.supports_follow_symlinks else True + ) + rootdir.chmod( + rootdir_stat.st_mode & ~0o077, + follow_symlinks=chmod_follow_symlinks, + ) + keep = self._retention_count + if self._retention_policy == "none": + keep = 0 + basetemp = make_numbered_dir_with_cleanup( + prefix="pytest-", + root=rootdir, + keep=keep, + lock_timeout=LOCK_TIMEOUT, + mode=0o700, + register=self._exit_stack.callback, + ) + # Ensure that the cleanup is called on exit (#1120 possibly?). + # But if the exit stack is closed manually (as it normally should), + # unregister the atexit to avoid pile up. + atexit.register(self._exit_stack.close) + self._exit_stack.callback(atexit.unregister, self._exit_stack.close) + assert basetemp is not None, basetemp + self._basetemp = basetemp + self._trace("new basetemp", basetemp) + return basetemp + + +def get_user() -> str | None: + """Return the current user name, or None if getuser() does not work + in the current environment (see #1010).""" + try: + # In some exotic environments, getpass may not be importable. + import getpass + + return getpass.getuser() + except (ImportError, OSError, KeyError): + return None + + +def pytest_configure(config: Config) -> None: + """Create a TempPathFactory and attach it to the config object. + + This is to comply with existing plugins which expect the handler to be + available at pytest_configure time, but ideally should be moved entirely + to the tmp_path_factory session fixture. + """ + mp = MonkeyPatch() + config.add_cleanup(mp.undo) + _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True) + mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False) + + +def pytest_addoption(parser: Parser) -> None: + parser.addini( + "tmp_path_retention_count", + help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.", + default="3", + # NOTE: Would have been better as an `int` but can't change it now. + type="string", + ) + + parser.addini( + "tmp_path_retention_policy", + help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. " + "(all/failed/none)", + type="string", + default="all", + ) + + +@fixture(scope="session") +def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: + """Return a :class:`pytest.TempPathFactory` instance for the test session.""" + # Set dynamically by pytest_configure() above. + return request.config._tmp_path_factory # type: ignore + + +def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: + name = request.node.name + name = re.sub(r"[\W]", "_", name) + MAXVAL = 30 + name = name[:MAXVAL] + return factory.mktemp(name, numbered=True) + + +@fixture +def tmp_path( + request: FixtureRequest, tmp_path_factory: TempPathFactory +) -> Generator[Path]: + """Return a temporary directory (as :class:`pathlib.Path` object) + which is unique to each test function invocation. + The temporary directory is created as a subdirectory + of the base temporary directory, with configurable retention, + as discussed in :ref:`temporary directory location and retention`. + """ + path = _mk_tmp(request, tmp_path_factory) + yield path + + # Remove the tmpdir if the policy is "failed" and the test passed. + policy = tmp_path_factory._retention_policy + result_dict = request.node.stash[tmppath_result_key] + + if policy == "failed" and result_dict.get("call", True): + # We do a "best effort" to remove files, but it might not be possible due to some leaked resource, + # permissions, etc, in which case we ignore it. + rmtree(path, ignore_errors=True) + + del request.node.stash[tmppath_result_key] + + +def pytest_sessionfinish(session, exitstatus: int | ExitCode): + """After each session, remove base directory if all the tests passed, + the policy is "failed", and the basetemp is not specified by a user. + """ + tmp_path_factory: TempPathFactory = session.config._tmp_path_factory + basetemp = tmp_path_factory._basetemp + if basetemp is None: + return + + policy = tmp_path_factory._retention_policy + if ( + exitstatus == 0 + and policy == "failed" + and tmp_path_factory._given_basetemp is None + ): + if basetemp.is_dir(): + # We do a "best effort" to remove files, but it might not be possible due to some leaked resource, + # permissions, etc, in which case we ignore it. + rmtree(basetemp, ignore_errors=True) + + # Remove dead symlinks. + if basetemp.is_dir(): + cleanup_dead_symlinks(basetemp) + + # Run the numbered dirs and lock file cleanups registered on the ExitStack. + tmp_path_factory._exit_stack.close() + + +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_makereport( + item: Item, call +) -> Generator[None, TestReport, TestReport]: + rep = yield + assert rep.when is not None + empty: dict[str, bool] = {} + item.stash.setdefault(tmppath_result_key, empty)[rep.when] = rep.passed + return rep diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/tracemalloc.py b/.venv-test/lib/python3.12/site-packages/_pytest/tracemalloc.py new file mode 100644 index 0000000..5d0b198 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/tracemalloc.py @@ -0,0 +1,24 @@ +from __future__ import annotations + + +def tracemalloc_message(source: object) -> str: + if source is None: + return "" + + try: + import tracemalloc + except ImportError: + return "" + + tb = tracemalloc.get_object_traceback(source) + if tb is not None: + formatted_tb = "\n".join(tb.format()) + # Use a leading new line to better separate the (large) output + # from the traceback to the previous warning text. + return f"\nObject allocated at:\n{formatted_tb}" + # No need for a leading new line. + url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings" + return ( + "Enable tracemalloc to get traceback where the object was allocated.\n" + f"See {url} for more info." + ) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/unittest.py b/.venv-test/lib/python3.12/site-packages/_pytest/unittest.py new file mode 100644 index 0000000..d5286af --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/unittest.py @@ -0,0 +1,653 @@ +# mypy: allow-untyped-defs +"""Discover and run std-library "unittest" style tests.""" + +from __future__ import annotations + +from collections.abc import Callable +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +from enum import auto +from enum import Enum +import inspect +import sys +import traceback +import types +from typing import Any +from typing import TYPE_CHECKING +from unittest import TestCase + +from _pytest import fixtures +import _pytest._code +from _pytest._code import ExceptionInfo +from _pytest.compat import assert_never +from _pytest.compat import is_async_function +from _pytest.config import hookimpl +from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Collector +from _pytest.nodes import Item +from _pytest.outcomes import exit +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.outcomes import xfail +from _pytest.python import Class +from _pytest.python import Function +from _pytest.python import Module +from _pytest.runner import CallInfo +from _pytest.runner import check_interactive_exception +from _pytest.subtests import SubtestContext +from _pytest.subtests import SubtestReport + + +if sys.version_info[:2] < (3, 11): + from exceptiongroup import ExceptionGroup + +if TYPE_CHECKING: + from types import TracebackType + import unittest + + import twisted.trial.unittest + + +_SysExcInfoType = ( + tuple[type[BaseException], BaseException, types.TracebackType] + | tuple[None, None, None] +) + + +def pytest_pycollect_makeitem( + collector: Module | Class, name: str, obj: object +) -> UnitTestCase | None: + try: + # Has unittest been imported? + ut = sys.modules["unittest"] + # Is obj a subclass of unittest.TestCase? + # Type ignored because `ut` is an opaque module. + if not issubclass(obj, ut.TestCase): # type: ignore + return None + except Exception: + return None + # Is obj a concrete class? + # Abstract classes can't be instantiated so no point collecting them. + if inspect.isabstract(obj): + return None + # Yes, so let's collect it. + return UnitTestCase.from_parent(collector, name=name, obj=obj) + + +class UnitTestCase(Class): + # Marker for fixturemanger.getfixtureinfo() + # to declare that our children do not support funcargs. + nofuncargs = True + + def newinstance(self): + # TestCase __init__ takes the method (test) name. The TestCase + # constructor treats the name "runTest" as a special no-op, so it can be + # used when a dummy instance is needed. While unittest.TestCase has a + # default, some subclasses omit the default (#9610), so always supply + # it. + return self.obj("runTest") + + def collect(self) -> Iterable[Item | Collector]: + from unittest import TestLoader + + cls = self.obj + if not getattr(cls, "__test__", True): + return + + skipped = _is_skipped(cls) + if not skipped: + self._register_unittest_setup_method_fixture(cls) + self._register_unittest_setup_class_fixture(cls) + self._register_setup_class_fixture() + else: + self._register_unittest_skip_fixture(cls) + + self.session._fixturemanager.parsefactories( + holder=self.newinstance(), node=self + ) + + loader = TestLoader() + foundsomething = False + for name in loader.getTestCaseNames(self.obj): + x = getattr(self.obj, name) + if not getattr(x, "__test__", True): + continue + yield TestCaseFunction.from_parent(self, name=name) + foundsomething = True + + if not foundsomething: + runtest = getattr(self.obj, "runTest", None) + if runtest is not None: + ut = sys.modules.get("twisted.trial.unittest", None) + if ut is None or runtest != ut.TestCase.runTest: + yield TestCaseFunction.from_parent(self, name="runTest") + + def _register_unittest_setup_class_fixture(self, cls: type) -> None: + """Register an auto-use fixture to invoke setUpClass and + tearDownClass (#517).""" + setup = getattr(cls, "setUpClass", None) + teardown = getattr(cls, "tearDownClass", None) + if setup is None and teardown is None: + return None + cleanup = getattr(cls, "doClassCleanups", lambda: None) + + def process_teardown_exceptions() -> None: + # tearDown_exceptions is a list set in the class containing exc_infos for errors during + # teardown for the class. + exc_infos = getattr(cls, "tearDown_exceptions", None) + if not exc_infos: + return + exceptions = [exc for (_, exc, _) in exc_infos] + # If a single exception, raise it directly as this provides a more readable + # error (hopefully this will improve in #12255). + if len(exceptions) == 1: + raise exceptions[0] + else: + raise ExceptionGroup("Unittest class cleanup errors", exceptions) + + def unittest_setup_class_fixture( + request: FixtureRequest, + ) -> Generator[None]: + cls = request.cls + if _is_skipped(cls): + reason = cls.__unittest_skip_why__ + raise skip.Exception(reason, _use_item_location=True) + if setup is not None: + try: + setup() + # unittest does not call the cleanup function for every BaseException, so we + # follow this here. + except Exception: + cleanup() + process_teardown_exceptions() + raise + yield + try: + if teardown is not None: + teardown() + finally: + cleanup() + process_teardown_exceptions() + + fixtures.register_fixture( + # Use a unique name to speed up lookup. + name=f"_unittest_setUpClass_fixture_{cls.__qualname__}", + func=unittest_setup_class_fixture, + node=self, + scope="class", + autouse=True, + ) + + def _register_unittest_skip_fixture(self, cls: type) -> None: + """Register an auto-use fixture to skip tests for a class decorated + with @unittest.skip or @unittest.skipIf (#13885).""" + + def unittest_skip_fixture(request: FixtureRequest) -> None: + reason = getattr(cls, "__unittest_skip_why__", "") + raise skip.Exception(reason, _use_item_location=True) + + fixtures.register_fixture( + name=f"_unittest_skip_fixture_{cls.__qualname__}", + func=unittest_skip_fixture, + node=self, + scope="class", + autouse=True, + ) + + def _register_unittest_setup_method_fixture(self, cls: type) -> None: + """Register an auto-use fixture to invoke setup_method and + teardown_method (#517).""" + setup = getattr(cls, "setup_method", None) + teardown = getattr(cls, "teardown_method", None) + if setup is None and teardown is None: + return None + + def unittest_setup_method_fixture( + request: FixtureRequest, + ) -> Generator[None]: + self = request.instance + if _is_skipped(self): + reason = self.__unittest_skip_why__ + raise skip.Exception(reason, _use_item_location=True) + if setup is not None: + setup(self, request.function) + yield + if teardown is not None: + teardown(self, request.function) + + fixtures.register_fixture( + # Use a unique name to speed up lookup. + name=f"_unittest_setup_method_fixture_{cls.__qualname__}", + func=unittest_setup_method_fixture, + node=self, + scope="function", + autouse=True, + ) + + +class TestCaseFunction(Function): + nofuncargs = True + failfast = False + _excinfo: list[_pytest._code.ExceptionInfo[BaseException]] | None = None + + def _getinstance(self): + assert isinstance(self.parent, UnitTestCase) + return self.parent.obj(self.name) + + # Backward compat for pytest-django; can be removed after pytest-django + # updates + some slack. + @property + def _testcase(self): + return self.instance + + def setup(self) -> None: + # A bound method to be called during teardown() if set (see 'runtest()'). + self._explicit_tearDown: Callable[[], None] | None = None + super().setup() + if sys.version_info < (3, 11): + # A cache of the subTest errors and non-subtest skips in self._outcome. + # Compute and cache these lists once, instead of computing them again and again for each subtest (#13965). + self._cached_errors_and_skips: tuple[list[Any], list[Any]] | None = None + + def teardown(self) -> None: + if self._explicit_tearDown is not None: + self._explicit_tearDown() + self._explicit_tearDown = None + self._obj = None + del self._instance + super().teardown() + + def startTest(self, testcase: unittest.TestCase) -> None: + pass + + def _addexcinfo(self, rawexcinfo: _SysExcInfoType) -> None: + rawexcinfo = _handle_twisted_exc_info(rawexcinfo) + try: + excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info( + rawexcinfo # type: ignore[arg-type] + ) + # Invoke the attributes to trigger storing the traceback + # trial causes some issue there. + _ = excinfo.value + _ = excinfo.traceback + except TypeError: + try: + try: + values = traceback.format_exception(*rawexcinfo) + values.insert( + 0, + "NOTE: Incompatible Exception Representation, " + "displaying natively:\n\n", + ) + fail("".join(values), pytrace=False) + except (fail.Exception, KeyboardInterrupt): + raise + except BaseException: + fail( + "ERROR: Unknown Incompatible Exception " + f"representation:\n{rawexcinfo!r}", + pytrace=False, + ) + except KeyboardInterrupt: + raise + except fail.Exception: + excinfo = _pytest._code.ExceptionInfo.from_current() + self.__dict__.setdefault("_excinfo", []).append(excinfo) + + def addError( + self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType + ) -> None: + try: + if isinstance(rawexcinfo[1], exit.Exception): + exit(rawexcinfo[1].msg) + except TypeError: + pass + self._addexcinfo(rawexcinfo) + + def addFailure( + self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType + ) -> None: + self._addexcinfo(rawexcinfo) + + def addSkip( + self, testcase: unittest.TestCase, reason: str, *, handle_subtests: bool = True + ) -> None: + from unittest.case import _SubTest # type: ignore[attr-defined] + + def add_skip() -> None: + try: + raise skip.Exception(reason, _use_item_location=True) + except skip.Exception: + self._addexcinfo(sys.exc_info()) + + if not handle_subtests: + add_skip() + return + + if isinstance(testcase, _SubTest): + add_skip() + if self._excinfo is not None: + exc_info = self._excinfo[-1] + self.addSubTest(testcase.test_case, testcase, exc_info) + else: + # For python < 3.11: the non-subtest skips have to be added by `add_skip` only after all subtest + # failures are processed by `_addSubTest`: `self.instance._outcome` has no attribute + # `skipped/errors` anymore. + # We also need to check if `self.instance._outcome` is `None` (this happens if the test + # class/method is decorated with `unittest.skip`, see pytest-dev/pytest-subtests#173). + if sys.version_info < (3, 11) and self.instance._outcome is not None: + subtest_errors, _ = self._obtain_errors_and_skips() + if len(subtest_errors) == 0: + add_skip() + else: + add_skip() + + def addExpectedFailure( + self, + testcase: unittest.TestCase, + rawexcinfo: _SysExcInfoType, + reason: str = "", + ) -> None: + try: + xfail(str(reason)) + except xfail.Exception: + self._addexcinfo(sys.exc_info()) + + def addUnexpectedSuccess( + self, + testcase: unittest.TestCase, + reason: twisted.trial.unittest.Todo | None = None, + ) -> None: + msg = "Unexpected success" + if reason: + msg += f": {reason.reason}" + # Preserve unittest behaviour - fail the test. Explicitly not an XPASS. + try: + fail(msg, pytrace=False) + except fail.Exception: + self._addexcinfo(sys.exc_info()) + + def addSuccess(self, testcase: unittest.TestCase) -> None: + pass + + def stopTest(self, testcase: unittest.TestCase) -> None: + pass + + def addDuration(self, testcase: unittest.TestCase, elapsed: float) -> None: + pass + + def runtest(self) -> None: + from _pytest.debugging import maybe_wrap_pytest_function_for_tracing + + testcase = self.instance + assert testcase is not None + + maybe_wrap_pytest_function_for_tracing(self) + + # Let the unittest framework handle async functions. + if is_async_function(self.obj): + testcase(result=self) + else: + # When --pdb is given, we want to postpone calling tearDown() otherwise + # when entering the pdb prompt, tearDown() would have probably cleaned up + # instance variables, which makes it difficult to debug. + # Arguably we could always postpone tearDown(), but this changes the moment where the + # TestCase instance interacts with the results object, so better to only do it + # when absolutely needed. + # We need to consider if the test itself is skipped, or the whole class. + assert isinstance(self.parent, UnitTestCase) + skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj) + if self.config.getoption("usepdb") and not skipped: + self._explicit_tearDown = testcase.tearDown + setattr(testcase, "tearDown", lambda *args: None) + + # We need to update the actual bound method with self.obj, because + # wrap_pytest_function_for_tracing replaces self.obj by a wrapper. + setattr(testcase, self.name, self.obj) + try: + testcase(result=self) + finally: + delattr(testcase, self.name) + + def _traceback_filter( + self, excinfo: _pytest._code.ExceptionInfo[BaseException] + ) -> _pytest._code.Traceback: + traceback = super()._traceback_filter(excinfo) + ntraceback = traceback.filter( + lambda x: not x.frame.f_globals.get("__unittest"), + ) + if not ntraceback: + ntraceback = traceback + return ntraceback + + def addSubTest( + self, + test_case: Any, + test: TestCase, + exc_info: ExceptionInfo[BaseException] + | tuple[type[BaseException], BaseException, TracebackType] + | None, + ) -> None: + # Importing this private symbol locally in case this symbol is renamed/removed in the future; importing + # it globally would break pytest entirely, importing it locally only will break unittests using `addSubTest`. + from unittest.case import _subtest_msg_sentinel # type: ignore[attr-defined] + + exception_info: ExceptionInfo[BaseException] | None + match exc_info: + case tuple(): + exception_info = ExceptionInfo(exc_info, _ispytest=True) + case ExceptionInfo() | None: + exception_info = exc_info + case unreachable: + assert_never(unreachable) + + call_info = CallInfo[None]( + None, + exception_info, + start=0, + stop=0, + duration=0, + when="call", + _ispytest=True, + ) + msg = None if test._message is _subtest_msg_sentinel else str(test._message) # type: ignore[attr-defined] + report = self.ihook.pytest_runtest_makereport(item=self, call=call_info) + sub_report = SubtestReport._new( + report, + SubtestContext(msg=msg, kwargs=dict(test.params)), # type: ignore[attr-defined] + captured_output=None, + captured_logs=None, + ) + self.ihook.pytest_runtest_logreport(report=sub_report) + if check_interactive_exception(call_info, sub_report): + self.ihook.pytest_exception_interact( + node=self, call=call_info, report=sub_report + ) + + # For python < 3.11: add non-subtest skips once all subtest failures are processed by # `_addSubTest`. + if sys.version_info < (3, 11): + subtest_errors, non_subtest_skip = self._obtain_errors_and_skips() + + # Check if we have non-subtest skips: if there are also sub failures, non-subtest skips are not treated in + # `_addSubTest` and have to be added using `add_skip` after all subtest failures are processed. + if len(non_subtest_skip) > 0 and len(subtest_errors) > 0: + # Make sure we have processed the last subtest failure + last_subset_error = subtest_errors[-1] + if exc_info is last_subset_error[-1]: + # Add non-subtest skips (as they could not be treated in `_addSkip`) + for testcase, reason in non_subtest_skip: + self.addSkip(testcase, reason, handle_subtests=False) + + def _obtain_errors_and_skips(self) -> tuple[list[Any], list[Any]]: + """Compute or obtain the cached values for subtest errors and non-subtest skips.""" + from unittest.case import _SubTest # type: ignore[attr-defined] + + assert sys.version_info < (3, 11), ( + "This workaround only should be used in Python 3.10" + ) + if self._cached_errors_and_skips is not None: + return self._cached_errors_and_skips + + subtest_errors = [ + (x, y) + for x, y in self.instance._outcome.errors + if isinstance(x, _SubTest) and y is not None + ] + + non_subtest_skips = [ + (x, y) + for x, y in self.instance._outcome.skipped + if not isinstance(x, _SubTest) + ] + self._cached_errors_and_skips = (subtest_errors, non_subtest_skips) + return subtest_errors, non_subtest_skips + + +@hookimpl(tryfirst=True) +def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: + if isinstance(item, TestCaseFunction): + if item._excinfo: + call.excinfo = item._excinfo.pop(0) + try: + del call.result + except AttributeError: + pass + + # Convert unittest.SkipTest to pytest.skip. + # This covers explicit `raise unittest.SkipTest`. + unittest = sys.modules.get("unittest") + if unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest): + excinfo = call.excinfo + call2 = CallInfo[None].from_call(lambda: skip(str(excinfo.value)), call.when) + call.excinfo = call2.excinfo + + +def _is_skipped(obj) -> bool: + """Return True if the given object has been marked with @unittest.skip.""" + return bool(getattr(obj, "__unittest_skip__", False)) + + +def pytest_configure() -> None: + """Register the TestCaseFunction class as an IReporter if twisted.trial is available.""" + if _get_twisted_version() is not TwistedVersion.NotInstalled: + from twisted.trial.itrial import IReporter + from zope.interface import classImplements + + classImplements(TestCaseFunction, IReporter) + + +class TwistedVersion(Enum): + """ + The Twisted version installed in the environment. + + We have different workarounds in place for different versions of Twisted. + """ + + # Twisted version 24 or prior. + Version24 = auto() + # Twisted version 25 or later. + Version25 = auto() + # Twisted version is not available. + NotInstalled = auto() + + +def _get_twisted_version() -> TwistedVersion: + # We need to check if "twisted.trial.unittest" is specifically present in sys.modules. + # This is because we intend to integrate with Trial only when it's actively running + # the test suite, but not needed when only other Twisted components are in use. + if "twisted.trial.unittest" not in sys.modules: + return TwistedVersion.NotInstalled + + import importlib.metadata + + import packaging.version + + version_str = importlib.metadata.version("twisted") + version = packaging.version.parse(version_str) + if version.major <= 24: + return TwistedVersion.Version24 + else: + return TwistedVersion.Version25 + + +# Name of the attribute in `twisted.python.Failure` instances that stores +# the `sys.exc_info()` tuple. +# See twisted.trial support in `pytest_runtest_protocol`. +TWISTED_RAW_EXCINFO_ATTR = "_twisted_raw_excinfo" + + +@hookimpl(wrapper=True) +def pytest_runtest_protocol(item: Item) -> Iterator[None]: + if _get_twisted_version() is TwistedVersion.Version24: + import twisted.python.failure as ut + + # Monkeypatch `Failure.__init__` to store the raw exception info. + original__init__ = ut.Failure.__init__ + + def store_raw_exception_info( + self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None + ): # pragma: no cover + if exc_value is None: + raw_exc_info = sys.exc_info() + else: + if exc_type is None: + exc_type = type(exc_value) + if exc_tb is None: + exc_tb = sys.exc_info()[2] + raw_exc_info = (exc_type, exc_value, exc_tb) + setattr(self, TWISTED_RAW_EXCINFO_ATTR, tuple(raw_exc_info)) + try: + original__init__( + self, exc_value, exc_type, exc_tb, captureVars=captureVars + ) + except TypeError: # pragma: no cover + original__init__(self, exc_value, exc_type, exc_tb) + + with MonkeyPatch.context() as patcher: + patcher.setattr(ut.Failure, "__init__", store_raw_exception_info) + return (yield) + else: + return (yield) + + +def _handle_twisted_exc_info( + rawexcinfo: _SysExcInfoType | BaseException, +) -> _SysExcInfoType: + """ + Twisted passes a custom Failure instance to `addError()` instead of using `sys.exc_info()`. + Therefore, if `rawexcinfo` is a `Failure` instance, convert it into the equivalent `sys.exc_info()` tuple + as expected by pytest. + """ + twisted_version = _get_twisted_version() + if twisted_version is TwistedVersion.NotInstalled: + # Unfortunately, because we cannot import `twisted.python.failure` at the top of the file + # and use it in the signature, we need to use `type:ignore` here because we cannot narrow + # the type properly in the `if` statement above. + return rawexcinfo # type:ignore[return-value] + elif twisted_version is TwistedVersion.Version24: + # Twisted calls addError() passing its own classes (like `twisted.python.Failure`), which violates + # the `addError()` signature, so we extract the original `sys.exc_info()` tuple which is stored + # in the object. + if hasattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR): + saved_exc_info = getattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR) + # Delete the attribute from the original object to avoid leaks. + delattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR) + return saved_exc_info # type:ignore[no-any-return] + return rawexcinfo # type:ignore[return-value] + elif twisted_version is TwistedVersion.Version25: + if isinstance(rawexcinfo, BaseException): + import twisted.python.failure + + if isinstance(rawexcinfo, twisted.python.failure.Failure): + tb = rawexcinfo.__traceback__ + if tb is None: + tb = sys.exc_info()[2] + return type(rawexcinfo.value), rawexcinfo.value, tb + + return rawexcinfo # type:ignore[return-value] + else: + # Ideally we would use assert_never() here, but it is not available in all Python versions + # we support, plus we do not require `type_extensions` currently. + assert False, f"Unexpected Twisted version: {twisted_version}" diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/unraisableexception.py b/.venv-test/lib/python3.12/site-packages/_pytest/unraisableexception.py new file mode 100644 index 0000000..6c092fb --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/unraisableexception.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import collections +from collections.abc import Callable +import functools +import gc +import sys +import traceback +from typing import NamedTuple +from typing import TYPE_CHECKING +import warnings + +from _pytest.config import Config +from _pytest.nodes import Item +from _pytest.stash import StashKey +from _pytest.tracemalloc import tracemalloc_message +import pytest + + +if TYPE_CHECKING: + pass + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + + +# This is a stash item and not a simple constant to allow pytester to override it. +gc_collect_iterations_key = StashKey[int]() + + +def gc_collect_harder(iterations: int) -> None: + for _ in range(iterations): + gc.collect() + + +class UnraisableMeta(NamedTuple): + msg: str + cause_msg: str + exc_value: BaseException | None + + +unraisable_exceptions: StashKey[collections.deque[UnraisableMeta | BaseException]] = ( + StashKey() +) + + +def collect_unraisable(config: Config) -> None: + pop_unraisable = config.stash[unraisable_exceptions].pop + errors: list[pytest.PytestUnraisableExceptionWarning | RuntimeError] = [] + meta = None + hook_error = None + try: + while True: + try: + meta = pop_unraisable() + except IndexError: + break + + if isinstance(meta, BaseException): + hook_error = RuntimeError("Failed to process unraisable exception") + hook_error.__cause__ = meta + errors.append(hook_error) + continue + + msg = meta.msg + try: + warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) + except pytest.PytestUnraisableExceptionWarning as e: + # This except happens when the warning is treated as an error (e.g. `-Werror`). + if meta.exc_value is not None: + # Exceptions have a better way to show the traceback, but + # warnings do not, so hide the traceback from the msg and + # set the cause so the traceback shows up in the right place. + e.args = (meta.cause_msg,) + e.__cause__ = meta.exc_value + errors.append(e) + + if len(errors) == 1: + raise errors[0] + if errors: + raise ExceptionGroup("multiple unraisable exception warnings", errors) + finally: + del errors, meta, hook_error + + +def cleanup( + *, config: Config, prev_hook: Callable[[sys.UnraisableHookArgs], object] +) -> None: + # On PyPy, objects (e.g. coroutines) can survive GC rounds because executing + # their __del__ can resurrect them. The Trio project determined experimentally + # that 5 passes are needed on PyPy to flush everything. On CPython, reference + # counting handles most cleanup immediately, so 1 pass is sufficient. + _default_gc_collect_iterations = 5 if sys.implementation.name == "pypy" else 1 + gc_collect_iterations = config.stash.get( + gc_collect_iterations_key, _default_gc_collect_iterations + ) + try: + try: + gc_collect_harder(gc_collect_iterations) + collect_unraisable(config) + finally: + sys.unraisablehook = prev_hook + finally: + del config.stash[unraisable_exceptions] + + +def unraisable_hook( + unraisable: sys.UnraisableHookArgs, + /, + *, + append: Callable[[UnraisableMeta | BaseException], object], +) -> None: + try: + # we need to compute these strings here as they might change after + # the unraisablehook finishes and before the metadata object is + # collected by a pytest hook + err_msg = ( + "Exception ignored in" if unraisable.err_msg is None else unraisable.err_msg + ) + summary = f"{err_msg}: {unraisable.object!r}" + traceback_message = "\n\n" + "".join( + traceback.format_exception( + unraisable.exc_type, + unraisable.exc_value, + unraisable.exc_traceback, + ) + ) + tracemalloc_tb = "\n" + tracemalloc_message(unraisable.object) + msg = summary + traceback_message + tracemalloc_tb + cause_msg = summary + tracemalloc_tb + + append( + UnraisableMeta( + msg=msg, + cause_msg=cause_msg, + exc_value=unraisable.exc_value, + ) + ) + except BaseException as e: + append(e) + # Raising this will cause the exception to be logged twice, once in our + # collect_unraisable and once by the unraisablehook calling machinery + # which is fine - this should never happen anyway and if it does + # it should probably be reported as a pytest bug. + raise + + +def pytest_configure(config: Config) -> None: + prev_hook = sys.unraisablehook + deque: collections.deque[UnraisableMeta | BaseException] = collections.deque() + config.stash[unraisable_exceptions] = deque + config.add_cleanup(functools.partial(cleanup, config=config, prev_hook=prev_hook)) + sys.unraisablehook = functools.partial(unraisable_hook, append=deque.append) + + +def pytest_unconfigure(config: Config) -> None: + # Runs before ``_cleanup_stack.close()``, so warning filters from + # cleanup-stack-managed contexts (notably the ``warnings`` plugin's + # ``catch_warnings``) are still installed when garbage-collected + # finalizers fire. A ``config.add_cleanup`` callback would instead + # couple correctness to LIFO pop order across plugins' cleanups. + if unraisable_exceptions not in config.stash: + # ``pytest_configure`` did not complete (e.g. a usage error raised + # in another plugin's configure), so the queue stash was never set. + return + # PyPy can resurrect objects in __del__, so it needs several GC passes + # (5, per the Trio project); CPython frees cycles in one pass. See #14441. + _default_gc_collect_iterations = 5 if sys.implementation.name == "pypy" else 1 + gc_collect_iterations = config.stash.get( + gc_collect_iterations_key, _default_gc_collect_iterations + ) + gc_collect_harder(gc_collect_iterations) + collect_unraisable(config) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_setup(item: Item) -> None: + collect_unraisable(item.config) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_call(item: Item) -> None: + collect_unraisable(item.config) + + +@pytest.hookimpl(trylast=True) +def pytest_runtest_teardown(item: Item) -> None: + collect_unraisable(item.config) diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/warning_types.py b/.venv-test/lib/python3.12/site-packages/_pytest/warning_types.py new file mode 100644 index 0000000..8fd62ff --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/warning_types.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import dataclasses +import inspect +from types import FunctionType +from typing import Any +from typing import final +from typing import Generic +from typing import TypeVar +import warnings + + +class PytestWarning(UserWarning): + """Base class for all warnings emitted by pytest.""" + + __module__ = "pytest" + + +@final +class PytestAssertRewriteWarning(PytestWarning): + """Warning emitted by the pytest assert rewrite module.""" + + __module__ = "pytest" + + +@final +class PytestCacheWarning(PytestWarning): + """Warning emitted by the cache plugin in various situations.""" + + __module__ = "pytest" + + +@final +class PytestConfigWarning(PytestWarning): + """Warning emitted for configuration issues.""" + + __module__ = "pytest" + + +@final +class PytestCollectionWarning(PytestWarning): + """Warning emitted when pytest is not able to collect a file or symbol in a module.""" + + __module__ = "pytest" + + +class PytestDeprecationWarning(PytestWarning, DeprecationWarning): + """Warning class for features that will be removed in a future version.""" + + __module__ = "pytest" + + +class PytestRemovedIn10Warning(PytestDeprecationWarning): + """Warning class for features that will be removed in pytest 10.""" + + __module__ = "pytest" + + +@final +class PytestExperimentalApiWarning(PytestWarning, FutureWarning): + """Warning category used to denote experiments in pytest. + + Use sparingly as the API might change or even be removed completely in a + future version. + """ + + __module__ = "pytest" + + @classmethod + def simple(cls, apiname: str) -> PytestExperimentalApiWarning: + return cls(f"{apiname} is an experimental api that may change over time") + + +@final +class PytestReturnNotNoneWarning(PytestWarning): + """ + Warning emitted when a test function returns a value other than ``None``. + + See :ref:`return-not-none` for details. + """ + + __module__ = "pytest" + + +@final +class PytestUnknownMarkWarning(PytestWarning): + """Warning emitted on use of unknown markers. + + See :ref:`mark` for details. + """ + + __module__ = "pytest" + + +@final +class PytestUnraisableExceptionWarning(PytestWarning): + """An unraisable exception was reported. + + Unraisable exceptions are exceptions raised in :meth:`__del__ ` + implementations and similar situations when the exception cannot be raised + as normal. + """ + + __module__ = "pytest" + + +@final +class PytestUnhandledThreadExceptionWarning(PytestWarning): + """An unhandled exception occurred in a :class:`~threading.Thread`. + + Such exceptions don't propagate normally. + """ + + __module__ = "pytest" + + +_W = TypeVar("_W", bound=PytestWarning) + + +@final +@dataclasses.dataclass +class UnformattedWarning(Generic[_W]): + """A warning meant to be formatted during runtime. + + This is used to hold warnings that need to format their message at runtime, + as opposed to a direct message. + """ + + category: type[_W] + template: str + + def format(self, **kwargs: Any) -> _W: + """Return an instance of the warning category, formatted with given kwargs.""" + return self.category(self.template.format(**kwargs)) + + +@final +class PytestFDWarning(PytestWarning): + """When the lsof plugin finds leaked fds.""" + + __module__ = "pytest" + + +def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None: + """ + Issue the warning :param:`message` for the definition of the given :param:`method` + + this helps to log warnings for functions defined prior to finding an issue with them + (like hook wrappers being marked in a legacy mechanism) + """ + lineno = method.__code__.co_firstlineno + filename = inspect.getfile(method) + module = method.__module__ + mod_globals = method.__globals__ + try: + warnings.warn_explicit( + message, + type(message), + filename=filename, + module=module, + registry=mod_globals.setdefault("__warningregistry__", {}), + lineno=lineno, + ) + except Warning as w: + # If warnings are errors (e.g. -Werror), location information gets lost, so we add it to the message. + raise type(w)(f"{w}\n at {filename}:{lineno}") from None diff --git a/.venv-test/lib/python3.12/site-packages/_pytest/warnings.py b/.venv-test/lib/python3.12/site-packages/_pytest/warnings.py new file mode 100644 index 0000000..d599d6c --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/_pytest/warnings.py @@ -0,0 +1,152 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +from collections.abc import Generator +from contextlib import contextmanager +from contextlib import ExitStack +import sys +from typing import Literal +import warnings + +from _pytest.config import apply_warning_filters +from _pytest.config import Config +from _pytest.config import parse_warning_filter +from _pytest.main import Session +from _pytest.nodes import Item +from _pytest.terminal import TerminalReporter +from _pytest.tracemalloc import tracemalloc_message +import pytest + + +@contextmanager +def catch_warnings_for_item( + config: Config, + ihook, + when: Literal["config", "collect", "runtest"], + item: Item | None, + *, + record: bool = True, +) -> Generator[None]: + """Context manager that catches warnings generated in the contained execution block. + + ``item`` can be None if we are not in the context of an item execution. + + Each warning captured triggers the ``pytest_warning_recorded`` hook. + """ + config_filters = config.getini("filterwarnings") + cmdline_filters = config.known_args_namespace.pythonwarnings or [] + with warnings.catch_warnings(record=record) as log: + if not sys.warnoptions: + # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908). + warnings.filterwarnings("always", category=DeprecationWarning) + warnings.filterwarnings("always", category=PendingDeprecationWarning) + + # To be enabled in pytest 10.0.0. + # warnings.filterwarnings("error", category=pytest.PytestRemovedIn10Warning) + + apply_warning_filters(config_filters, cmdline_filters) + + # apply filters from "filterwarnings" marks + nodeid = "" if item is None else item.nodeid + if item is not None: + for mark in item.iter_markers(name="filterwarnings"): + for arg in mark.args: + warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) + + try: + yield + finally: + if record: + # mypy can't infer that record=True means log is not None; help it. + assert log is not None + + for warning_message in log: + ihook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=warning_message, + nodeid=nodeid, + when=when, + location=None, + ) + ) + + +def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: + """Convert a warnings.WarningMessage to a string.""" + return warnings.formatwarning( + str(warning_message.message), + warning_message.category, + warning_message.filename, + warning_message.lineno, + warning_message.line, + ) + tracemalloc_message(warning_message.source) + + +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: + with catch_warnings_for_item( + config=item.config, ihook=item.ihook, when="runtest", item=item + ): + return (yield) + + +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_collection(session: Session) -> Generator[None, object, object]: + config = session.config + with catch_warnings_for_item( + config=config, ihook=config.hook, when="collect", item=None + ): + return (yield) + + +@pytest.hookimpl(wrapper=True) +def pytest_terminal_summary( + terminalreporter: TerminalReporter, +) -> Generator[None]: + config = terminalreporter.config + with catch_warnings_for_item( + config=config, ihook=config.hook, when="config", item=None + ): + return (yield) + + +@pytest.hookimpl(wrapper=True) +def pytest_sessionfinish(session: Session) -> Generator[None]: + config = session.config + with catch_warnings_for_item( + config=config, ihook=config.hook, when="config", item=None + ): + return (yield) + + +@pytest.hookimpl(wrapper=True) +def pytest_load_initial_conftests( + early_config: Config, +) -> Generator[None]: + with catch_warnings_for_item( + config=early_config, ihook=early_config.hook, when="config", item=None + ): + return (yield) + + +def pytest_configure(config: Config) -> None: + with ExitStack() as stack: + stack.enter_context( + catch_warnings_for_item( + config=config, + ihook=config.hook, + when="config", + item=None, + # this disables recording because the terminalreporter has + # finished by the time it comes to reporting logged warnings + # from the end of config cleanup. So for now, this is only + # useful for setting a warning filter with an 'error' action. + record=False, + ) + ) + config.addinivalue_line( + "markers", + "filterwarnings(warning): add a warning filter to the given test. " + "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ", + ) + config.add_cleanup(stack.pop_all().close) diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/INSTALLER b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/METADATA b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/METADATA new file mode 100644 index 0000000..d5356ba --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/METADATA @@ -0,0 +1,123 @@ +Metadata-Version: 2.4 +Name: aiosqlite +Version: 0.22.1 +Summary: asyncio bridge to the standard sqlite3 module +Author-email: Amethyst Reese +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: AsyncIO +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Topic :: Software Development :: Libraries +License-File: LICENSE +Requires-Dist: attribution==1.8.0 ; extra == "dev" +Requires-Dist: black==25.11.0 ; extra == "dev" +Requires-Dist: build>=1.2 ; extra == "dev" +Requires-Dist: coverage[toml]==7.10.7 ; extra == "dev" +Requires-Dist: flake8==7.3.0 ; extra == "dev" +Requires-Dist: flake8-bugbear==24.12.12 ; extra == "dev" +Requires-Dist: flit==3.12.0 ; extra == "dev" +Requires-Dist: mypy==1.19.0 ; extra == "dev" +Requires-Dist: ufmt==2.8.0 ; extra == "dev" +Requires-Dist: usort==1.0.8.post1 ; extra == "dev" +Requires-Dist: sphinx==8.1.3 ; extra == "docs" +Requires-Dist: sphinx-mdinclude==0.6.2 ; extra == "docs" +Project-URL: Documentation, https://aiosqlite.omnilib.dev +Project-URL: Github, https://github.com/omnilib/aiosqlite +Provides-Extra: dev +Provides-Extra: docs + +aiosqlite\: Sqlite for AsyncIO +============================== + +.. image:: https://readthedocs.org/projects/aiosqlite/badge/?version=latest + :target: https://aiosqlite.omnilib.dev/en/latest/?badge=latest + :alt: Documentation Status +.. image:: https://img.shields.io/pypi/v/aiosqlite.svg + :target: https://pypi.org/project/aiosqlite + :alt: PyPI Release +.. image:: https://img.shields.io/badge/change-log-blue + :target: https://github.com/omnilib/aiosqlite/blob/master/CHANGELOG.md + :alt: Changelog +.. image:: https://img.shields.io/pypi/l/aiosqlite.svg + :target: https://github.com/omnilib/aiosqlite/blob/master/LICENSE + :alt: MIT Licensed + +aiosqlite provides a friendly, async interface to sqlite databases. + +It replicates the standard ``sqlite3`` module, but with async versions +of all the standard connection and cursor methods, plus context managers for +automatically closing connections and cursors: + +.. code-block:: python + + async with aiosqlite.connect(...) as db: + await db.execute("INSERT INTO some_table ...") + await db.commit() + + async with db.execute("SELECT * FROM some_table") as cursor: + async for row in cursor: + ... + +It can also be used in the traditional, procedural manner: + +.. code-block:: python + + db = await aiosqlite.connect(...) + cursor = await db.execute('SELECT * FROM some_table') + row = await cursor.fetchone() + rows = await cursor.fetchall() + await cursor.close() + await db.close() + +aiosqlite also replicates most of the advanced features of ``sqlite3``: + +.. code-block:: python + + async with aiosqlite.connect(...) as db: + db.row_factory = aiosqlite.Row + async with db.execute('SELECT * FROM some_table') as cursor: + async for row in cursor: + value = row['column'] + + await db.execute('INSERT INTO foo some_table') + assert db.total_changes > 0 + + +Install +------- + +aiosqlite is compatible with Python 3.8 and newer. +You can install it from PyPI: + +.. code-block:: console + + $ pip install aiosqlite + + +Details +------- + +aiosqlite allows interaction with SQLite databases on the main AsyncIO event +loop without blocking execution of other coroutines while waiting for queries +or data fetches. It does this by using a single, shared thread per connection. +This thread executes all actions within a shared request queue to prevent +overlapping actions. + +Connection objects are proxies to the real connections, contain the shared +execution thread, and provide context managers to handle automatically closing +connections. Cursors are similarly proxies to the real cursors, and provide +async iterators to query results. + + +License +------- + +aiosqlite is copyright `Amethyst Reese `_, and licensed under the +MIT license. I am providing code in this repository to you under an open source +license. This is my personal repository; the license you receive to my code +is from me and not from my employer. See the `LICENSE`_ file for details. + +.. _LICENSE: https://github.com/omnilib/aiosqlite/blob/master/LICENSE + diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/RECORD b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/RECORD new file mode 100644 index 0000000..bc3589d --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/RECORD @@ -0,0 +1,27 @@ +aiosqlite-0.22.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +aiosqlite-0.22.1.dist-info/METADATA,sha256=zzyMxzl2h_dGAlV6Pk9c4YBlkaYsgv6UybOW_YDRs5o,4311 +aiosqlite-0.22.1.dist-info/RECORD,, +aiosqlite-0.22.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +aiosqlite-0.22.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +aiosqlite-0.22.1.dist-info/licenses/LICENSE,sha256=qwwXHcPvi_MlqEu3fYVUIfJhEzXd9uCIFrKSLE7cD3Y,1071 +aiosqlite/__init__.py,sha256=kjZKcYP2eZ3IbBEHQ0D_Owsk_-FlRGEjQWlbybOs8jk,888 +aiosqlite/__pycache__/__init__.cpython-312.pyc,, +aiosqlite/__pycache__/__version__.cpython-312.pyc,, +aiosqlite/__pycache__/context.cpython-312.pyc,, +aiosqlite/__pycache__/core.cpython-312.pyc,, +aiosqlite/__pycache__/cursor.cpython-312.pyc,, +aiosqlite/__version__.py,sha256=sEM7xBU6e8WQYHe3ESoQmkEbXGKrqXjrQ5ujl5zpyV0,157 +aiosqlite/context.py,sha256=9jJcPG_SGSshzNUwXy87C1__mrKGFbToX0UuOQ1uItQ,1448 +aiosqlite/core.py,sha256=eXar7Bxr1pQz3VShCD0t6zGpp_bwwvFtVzVdzS2opII,15095 +aiosqlite/cursor.py,sha256=X3k2gYJeo3yB84scDEAPFZpsC_rzjT8dT4p6W3MeezM,3476 +aiosqlite/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +aiosqlite/tests/__init__.py,sha256=sp0-HYboM6gOYrUxWy8xna-hdJyMUtKBvAKrpRBcDCE,90 +aiosqlite/tests/__main__.py,sha256=eZRuAxr1bwF9xAAqVjCi4vd1WFsFO35uyhtuVO0GjmY,162 +aiosqlite/tests/__pycache__/__init__.cpython-312.pyc,, +aiosqlite/tests/__pycache__/__main__.cpython-312.pyc,, +aiosqlite/tests/__pycache__/helpers.cpython-312.pyc,, +aiosqlite/tests/__pycache__/perf.cpython-312.pyc,, +aiosqlite/tests/__pycache__/smoke.cpython-312.pyc,, +aiosqlite/tests/helpers.py,sha256=MWC839FiX63TBmFiIjabXNx-4G5eWYnE5MiInKIAdJw,722 +aiosqlite/tests/perf.py,sha256=-ipnXSHidO6VBKEdLAOcGa3cKHU5ul1w8-ifDNtGbfA,7249 +aiosqlite/tests/smoke.py,sha256=k5mp4AOHheOO6wKL_bgdH1fenY6-ve6aew19ifmcIWA,19851 diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/REQUESTED b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/WHEEL b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/licenses/LICENSE b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/licenses/LICENSE new file mode 100644 index 0000000..bbb5ba0 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite-0.22.1.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Amethyst Reese + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/__init__.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/__init__.py new file mode 100644 index 0000000..6e4ccb2 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/__init__.py @@ -0,0 +1,44 @@ +# Copyright Amethyst Reese +# Licensed under the MIT license + +"""asyncio bridge to the standard sqlite3 module""" + +from sqlite3 import ( # pylint: disable=redefined-builtin + DatabaseError, + Error, + IntegrityError, + NotSupportedError, + OperationalError, + paramstyle, + ProgrammingError, + register_adapter, + register_converter, + Row, + sqlite_version, + sqlite_version_info, + Warning, +) + +__author__ = "Amethyst Reese" +from .__version__ import __version__ +from .core import connect, Connection, Cursor + +__all__ = [ + "__version__", + "paramstyle", + "register_adapter", + "register_converter", + "sqlite_version", + "sqlite_version_info", + "connect", + "Connection", + "Cursor", + "Row", + "Warning", + "Error", + "DatabaseError", + "IntegrityError", + "ProgrammingError", + "OperationalError", + "NotSupportedError", +] diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/__version__.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/__version__.py new file mode 100644 index 0000000..4adc274 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/__version__.py @@ -0,0 +1,7 @@ +""" +This file is automatically generated by attribution. + +Do not edit manually. Get more info at https://attribution.omnilib.dev +""" + +__version__ = "0.22.1" diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/context.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/context.py new file mode 100644 index 0000000..5afbaef --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/context.py @@ -0,0 +1,56 @@ +# Copyright Amethyst Reese +# Licensed under the MIT license + + +from collections.abc import Coroutine, Generator +from contextlib import AbstractAsyncContextManager +from functools import wraps +from typing import Any, Callable, TypeVar + +from .cursor import Cursor + +_T = TypeVar("_T") + + +class Result(AbstractAsyncContextManager[_T], Coroutine[Any, Any, _T]): + __slots__ = ("_coro", "_obj") + + def __init__(self, coro: Coroutine[Any, Any, _T]): + self._coro = coro + self._obj: _T + + def send(self, value) -> None: + return self._coro.send(value) + + def throw(self, typ, val=None, tb=None) -> None: + if val is None: + return self._coro.throw(typ) + + if tb is None: + return self._coro.throw(typ, val) + + return self._coro.throw(typ, val, tb) + + def close(self) -> None: + return self._coro.close() + + def __await__(self) -> Generator[Any, None, _T]: + return self._coro.__await__() + + async def __aenter__(self) -> _T: + self._obj = await self._coro + return self._obj + + async def __aexit__(self, exc_type, exc, tb) -> None: + if isinstance(self._obj, Cursor): + await self._obj.close() + + +def contextmanager( + method: Callable[..., Coroutine[Any, Any, _T]], +) -> Callable[..., Result[_T]]: + @wraps(method) + def wrapper(self, *args, **kwargs) -> Result[_T]: + return Result(method(self, *args, **kwargs)) + + return wrapper diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/core.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/core.py new file mode 100644 index 0000000..fc2b79b --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/core.py @@ -0,0 +1,468 @@ +# Copyright Amethyst Reese +# Licensed under the MIT license + +""" +Core implementation of aiosqlite proxies +""" + +import asyncio +import logging +import sqlite3 +from collections.abc import AsyncIterator, Generator, Iterable +from functools import partial +from pathlib import Path +from queue import Empty, Queue, SimpleQueue +from threading import Thread +from typing import Any, Callable, Literal, Optional, Union +from warnings import warn + +from .context import contextmanager +from .cursor import Cursor + +__all__ = ["connect", "Connection", "Cursor"] + +AuthorizerCallback = Callable[[int, str, str, str, str], int] + +LOG = logging.getLogger("aiosqlite") + + +IsolationLevel = Optional[Literal["DEFERRED", "IMMEDIATE", "EXCLUSIVE"]] + + +def set_result(fut: asyncio.Future, result: Any) -> None: + """Set the result of a future if it hasn't been set already.""" + if not fut.done(): + fut.set_result(result) + + +def set_exception(fut: asyncio.Future, e: BaseException) -> None: + """Set the exception of a future if it hasn't been set already.""" + if not fut.done(): + fut.set_exception(e) + + +_STOP_RUNNING_SENTINEL = object() +_TxQueue = SimpleQueue[tuple[Optional[asyncio.Future], Callable[[], Any]]] + + +def _connection_worker_thread(tx: _TxQueue): + """ + Execute function calls on a separate thread. + + :meta private: + """ + while True: + # Continues running until all queue items are processed, + # even after connection is closed (so we can finalize all + # futures) + + future, function = tx.get() + + try: + LOG.debug("executing %s", function) + result = function() + + if future: + future.get_loop().call_soon_threadsafe(set_result, future, result) + LOG.debug("operation %s completed", function) + + if result is _STOP_RUNNING_SENTINEL: + break + + except BaseException as e: # noqa B036 + LOG.debug("returning exception %s", e) + if future: + future.get_loop().call_soon_threadsafe(set_exception, future, e) + + +class Connection: + def __init__( + self, + connector: Callable[[], sqlite3.Connection], + iter_chunk_size: int, + loop: Optional[asyncio.AbstractEventLoop] = None, + ) -> None: + self._running = True + self._connection: Optional[sqlite3.Connection] = None + self._connector = connector + self._tx: _TxQueue = SimpleQueue() + self._iter_chunk_size = iter_chunk_size + self._thread = Thread(target=_connection_worker_thread, args=(self._tx,)) + + if loop is not None: + warn( + "aiosqlite.Connection no longer uses the `loop` parameter", + DeprecationWarning, + ) + + def __del__(self): + if self._connection is None: + return + + warn( + ( + f"{self!r} was deleted before being closed. " + "Please use 'async with' or '.close()' to close the connection properly." + ), + ResourceWarning, + stacklevel=1, + ) + + # Don't try to be creative here, the event loop may have already been closed. + # Simply stop the worker thread, and let the underlying sqlite3 connection + # be finalized by its own __del__. + self.stop() + + def stop(self) -> Optional[asyncio.Future]: + """Stop the background thread. Prefer `async with` or `await close()`""" + self._running = False + + def close_and_stop(): + if self._connection is not None: + self._connection.close() + self._connection = None + return _STOP_RUNNING_SENTINEL + + try: + future = asyncio.get_event_loop().create_future() + except Exception: + future = None + + self._tx.put_nowait((future, close_and_stop)) + return future + + @property + def _conn(self) -> sqlite3.Connection: + if self._connection is None: + raise ValueError("no active connection") + + return self._connection + + def _execute_insert(self, sql: str, parameters: Any) -> Optional[sqlite3.Row]: + cursor = self._conn.execute(sql, parameters) + cursor.execute("SELECT last_insert_rowid()") + return cursor.fetchone() + + def _execute_fetchall(self, sql: str, parameters: Any) -> Iterable[sqlite3.Row]: + cursor = self._conn.execute(sql, parameters) + return cursor.fetchall() + + async def _execute(self, fn, *args, **kwargs): + """Queue a function with the given arguments for execution.""" + if not self._running or not self._connection: + raise ValueError("Connection closed") + + function = partial(fn, *args, **kwargs) + future = asyncio.get_event_loop().create_future() + + self._tx.put_nowait((future, function)) + + return await future + + async def _connect(self) -> "Connection": + """Connect to the actual sqlite database.""" + if self._connection is None: + try: + future = asyncio.get_event_loop().create_future() + self._tx.put_nowait((future, self._connector)) + self._connection = await future + except BaseException: + self.stop() + self._connection = None + raise + + return self + + def __await__(self) -> Generator[Any, None, "Connection"]: + self._thread.start() + return self._connect().__await__() + + async def __aenter__(self) -> "Connection": + return await self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + await self.close() + + @contextmanager + async def cursor(self) -> Cursor: + """Create an aiosqlite cursor wrapping a sqlite3 cursor object.""" + return Cursor(self, await self._execute(self._conn.cursor)) + + async def commit(self) -> None: + """Commit the current transaction.""" + await self._execute(self._conn.commit) + + async def rollback(self) -> None: + """Roll back the current transaction.""" + await self._execute(self._conn.rollback) + + async def close(self) -> None: + """Complete queued queries/cursors and close the connection.""" + + if self._connection is None: + return + + try: + await self._execute(self._conn.close) + except Exception: + LOG.info("exception occurred while closing connection") + raise + finally: + self._connection = None + future = self.stop() + if future: + await future + + @contextmanager + async def execute( + self, sql: str, parameters: Optional[Iterable[Any]] = None + ) -> Cursor: + """Helper to create a cursor and execute the given query.""" + if parameters is None: + parameters = [] + cursor = await self._execute(self._conn.execute, sql, parameters) + return Cursor(self, cursor) + + @contextmanager + async def execute_insert( + self, sql: str, parameters: Optional[Iterable[Any]] = None + ) -> Optional[sqlite3.Row]: + """Helper to insert and get the last_insert_rowid.""" + if parameters is None: + parameters = [] + return await self._execute(self._execute_insert, sql, parameters) + + @contextmanager + async def execute_fetchall( + self, sql: str, parameters: Optional[Iterable[Any]] = None + ) -> Iterable[sqlite3.Row]: + """Helper to execute a query and return all the data.""" + if parameters is None: + parameters = [] + return await self._execute(self._execute_fetchall, sql, parameters) + + @contextmanager + async def executemany( + self, sql: str, parameters: Iterable[Iterable[Any]] + ) -> Cursor: + """Helper to create a cursor and execute the given multiquery.""" + cursor = await self._execute(self._conn.executemany, sql, parameters) + return Cursor(self, cursor) + + @contextmanager + async def executescript(self, sql_script: str) -> Cursor: + """Helper to create a cursor and execute a user script.""" + cursor = await self._execute(self._conn.executescript, sql_script) + return Cursor(self, cursor) + + async def interrupt(self) -> None: + """Interrupt pending queries.""" + return self._conn.interrupt() + + async def create_function( + self, name: str, num_params: int, func: Callable, deterministic: bool = False + ) -> None: + """ + Create user-defined function that can be later used + within SQL statements. Must be run within the same thread + that query executions take place so instead of executing directly + against the connection, we defer this to `run` function. + + If ``deterministic`` is true, the created function is marked as deterministic, + which allows SQLite to perform additional optimizations. This flag is supported + by SQLite 3.8.3 or higher, ``NotSupportedError`` will be raised if used with + older versions. + """ + await self._execute( + self._conn.create_function, + name, + num_params, + func, + deterministic=deterministic, + ) + + @property + def in_transaction(self) -> bool: + return self._conn.in_transaction + + @property + def isolation_level(self) -> Optional[str]: + return self._conn.isolation_level + + @isolation_level.setter + def isolation_level(self, value: IsolationLevel) -> None: + self._conn.isolation_level = value + + @property + def row_factory(self) -> Optional[type]: + return self._conn.row_factory + + @row_factory.setter + def row_factory(self, factory: Optional[type]) -> None: + self._conn.row_factory = factory + + @property + def text_factory(self) -> Callable[[bytes], Any]: + return self._conn.text_factory + + @text_factory.setter + def text_factory(self, factory: Callable[[bytes], Any]) -> None: + self._conn.text_factory = factory + + @property + def total_changes(self) -> int: + return self._conn.total_changes + + async def enable_load_extension(self, value: bool) -> None: + await self._execute(self._conn.enable_load_extension, value) # type: ignore + + async def load_extension(self, path: str): + await self._execute(self._conn.load_extension, path) # type: ignore + + async def set_progress_handler( + self, handler: Callable[[], Optional[int]], n: int + ) -> None: + await self._execute(self._conn.set_progress_handler, handler, n) + + async def set_trace_callback(self, handler: Callable) -> None: + await self._execute(self._conn.set_trace_callback, handler) + + async def set_authorizer( + self, authorizer_callback: Optional[AuthorizerCallback] + ) -> None: + """ + Set an authorizer callback to control database access. + + The authorizer callback is invoked for each SQL statement that is prepared, + and controls whether specific operations are permitted. + + Example:: + + import sqlite3 + + def restrict_drops(action_code, arg1, arg2, db_name, trigger_name): + # Deny all DROP operations + if action_code == sqlite3.SQLITE_DROP_TABLE: + return sqlite3.SQLITE_DENY + # Allow everything else + return sqlite3.SQLITE_OK + + await conn.set_authorizer(restrict_drops) + + See ``sqlite3`` documentation for details: + https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.set_authorizer + + :param authorizer_callback: An optional callable that receives five arguments: + + - ``action_code`` (int): The action to be authorized (e.g., ``SQLITE_READ``) + - ``arg1`` (str): First argument, meaning depends on ``action_code`` + - ``arg2`` (str): Second argument, meaning depends on ``action_code`` + - ``db_name`` (str): Database name (e.g., ``"main"``, ``"temp"``) + - ``trigger_name`` (str): Name of trigger or view that is doing the access, + or ``None`` + + The callback should return: + + - ``SQLITE_OK`` (0): Allow the operation + - ``SQLITE_DENY`` (1): Deny the operation, raise ``sqlite3.DatabaseError`` + - ``SQLITE_IGNORE`` (2): Treat operation as no-op + + Pass ``None`` to remove the authorizer. + """ + await self._execute(self._conn.set_authorizer, authorizer_callback) + + async def iterdump(self) -> AsyncIterator[str]: + """ + Return an async iterator to dump the database in SQL text format. + + Example:: + + async for line in db.iterdump(): + ... + + """ + dump_queue: Queue = Queue() + + def dumper(): + try: + for line in self._conn.iterdump(): + dump_queue.put_nowait(line) + dump_queue.put_nowait(None) + + except Exception: + LOG.exception("exception while dumping db") + dump_queue.put_nowait(None) + raise + + fut = self._execute(dumper) + task = asyncio.ensure_future(fut) + + while True: + try: + line: Optional[str] = dump_queue.get_nowait() + if line is None: + break + yield line + + except Empty: + if task.done(): + LOG.warning("iterdump completed unexpectedly") + break + + await asyncio.sleep(0.01) + + await task + + async def backup( + self, + target: Union["Connection", sqlite3.Connection], + *, + pages: int = 0, + progress: Optional[Callable[[int, int, int], None]] = None, + name: str = "main", + sleep: float = 0.250, + ) -> None: + """ + Make a backup of the current database to the target database. + + Takes either a standard sqlite3 or aiosqlite Connection object as the target. + """ + if isinstance(target, Connection): + target = target._conn + + await self._execute( + self._conn.backup, + target, + pages=pages, + progress=progress, + name=name, + sleep=sleep, + ) + + +def connect( + database: Union[str, Path], + *, + iter_chunk_size=64, + loop: Optional[asyncio.AbstractEventLoop] = None, + **kwargs: Any, +) -> Connection: + """Create and return a connection proxy to the sqlite database.""" + + if loop is not None: + warn( + "aiosqlite.connect() no longer uses the `loop` parameter", + DeprecationWarning, + ) + + def connector() -> sqlite3.Connection: + if isinstance(database, str): + loc = database + elif isinstance(database, bytes): + loc = database.decode("utf-8") + else: + loc = str(database) + + return sqlite3.connect(loc, **kwargs) + + return Connection(connector, iter_chunk_size) diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/cursor.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/cursor.py new file mode 100644 index 0000000..6932874 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/cursor.py @@ -0,0 +1,110 @@ +# Copyright Amethyst Reese +# Licensed under the MIT license + +import sqlite3 +from collections.abc import AsyncIterator, Iterable +from typing import Any, Callable, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from .core import Connection + + +class Cursor: + def __init__(self, conn: "Connection", cursor: sqlite3.Cursor) -> None: + self.iter_chunk_size = conn._iter_chunk_size + self._conn = conn + self._cursor = cursor + + def __aiter__(self) -> AsyncIterator[sqlite3.Row]: + """The cursor proxy is also an async iterator.""" + return self._fetch_chunked() + + async def _fetch_chunked(self): + while True: + rows = await self.fetchmany(self.iter_chunk_size) + if not rows: + return + for row in rows: + yield row + + async def _execute(self, fn, *args, **kwargs): + """Execute the given function on the shared connection's thread.""" + return await self._conn._execute(fn, *args, **kwargs) + + async def execute( + self, sql: str, parameters: Optional[Iterable[Any]] = None + ) -> "Cursor": + """Execute the given query.""" + if parameters is None: + parameters = [] + await self._execute(self._cursor.execute, sql, parameters) + return self + + async def executemany( + self, sql: str, parameters: Iterable[Iterable[Any]] + ) -> "Cursor": + """Execute the given multiquery.""" + await self._execute(self._cursor.executemany, sql, parameters) + return self + + async def executescript(self, sql_script: str) -> "Cursor": + """Execute a user script.""" + await self._execute(self._cursor.executescript, sql_script) + return self + + async def fetchone(self) -> Optional[sqlite3.Row]: + """Fetch a single row.""" + return await self._execute(self._cursor.fetchone) + + async def fetchmany(self, size: Optional[int] = None) -> Iterable[sqlite3.Row]: + """Fetch up to `cursor.arraysize` number of rows.""" + args: tuple[int, ...] = () + if size is not None: + args = (size,) + return await self._execute(self._cursor.fetchmany, *args) + + async def fetchall(self) -> Iterable[sqlite3.Row]: + """Fetch all remaining rows.""" + return await self._execute(self._cursor.fetchall) + + async def close(self) -> None: + """Close the cursor.""" + await self._execute(self._cursor.close) + + @property + def rowcount(self) -> int: + return self._cursor.rowcount + + @property + def lastrowid(self) -> Optional[int]: + return self._cursor.lastrowid + + @property + def arraysize(self) -> int: + return self._cursor.arraysize + + @arraysize.setter + def arraysize(self, value: int) -> None: + self._cursor.arraysize = value + + @property + def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...]: + return self._cursor.description + + @property + def row_factory(self) -> Optional[Callable[[sqlite3.Cursor, sqlite3.Row], object]]: + return self._cursor.row_factory + + @row_factory.setter + def row_factory(self, factory: Optional[type]) -> None: + self._cursor.row_factory = factory + + @property + def connection(self) -> sqlite3.Connection: + return self._cursor.connection + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/py.typed b/.venv-test/lib/python3.12/site-packages/aiosqlite/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/__init__.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/__init__.py new file mode 100644 index 0000000..8fc25e7 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright Amethyst Reese +# Licensed under the MIT license + +from .smoke import SmokeTest diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/__main__.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/__main__.py new file mode 100644 index 0000000..6c37ff1 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/__main__.py @@ -0,0 +1,7 @@ +# Copyright Amethyst Reese +# Licensed under the MIT license + +import unittest + +if __name__ == "__main__": + unittest.main(module="aiosqlite.tests", verbosity=2) diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/helpers.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/helpers.py new file mode 100644 index 0000000..06f32dc --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/helpers.py @@ -0,0 +1,29 @@ +# Copyright Amethyst Reese +# Licensed under the MIT license + +import logging +import sys + + +def setup_logger(): + log = logging.getLogger("") + log.setLevel(logging.INFO) + + logging.addLevelName(logging.ERROR, "E") + logging.addLevelName(logging.WARNING, "W") + logging.addLevelName(logging.INFO, "I") + logging.addLevelName(logging.DEBUG, "V") + + date_fmt = r"%H:%M:%S" + verbose_fmt = ( + "%(asctime)s,%(msecs)d %(levelname)s " + "%(module)s:%(funcName)s():%(lineno)d " + "%(message)s" + ) + + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter(verbose_fmt, date_fmt)) + log.addHandler(handler) + + return log diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/perf.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/perf.py new file mode 100644 index 0000000..5cd13db --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/perf.py @@ -0,0 +1,221 @@ +# Copyright Amethyst Reese +# Licensed under the MIT license + +""" +Simple perf tests for aiosqlite and the asyncio run loop. +""" +import sqlite3 +import string +import tempfile +import time + +from unittest import IsolatedAsyncioTestCase as TestCase + +import aiosqlite +from .smoke import setup_logger + +TEST_DB = ":memory:" +TARGET = 2.0 +RESULTS = {} + + +def timed(fn, name=None): + """ + Decorator for perf testing a block of async code. + + Expects the wrapped function to return an async generator. + The generator should do setup, then yield when ready to start perf testing. + The decorator will then pump the generator repeatedly until the target + time has been reached, then close the generator and print perf results. + """ + + name = name or fn.__name__ + + async def wrapper(*args, **kwargs): + gen = fn(*args, **kwargs) + + await gen.asend(None) + count = 0 + before = time.time() + + while True: + count += 1 + value = time.time() - before < TARGET + try: + if value: + await gen.asend(value) + else: + await gen.aclose() + break + + except StopAsyncIteration: + break + + except Exception as e: + print(f"exception occurred: {e}") + return + + duration = time.time() - before + + RESULTS[name] = (count, duration) + + return wrapper + + +class PerfTest(TestCase): + @classmethod + def setUpClass(cls): + print(f"Running perf tests for at least {TARGET:.1f}s each...") + setup_logger() + + @classmethod + def tearDownClass(cls): + print(f"\n{'Perf Test':<25} Iterations Duration {'Rate':>11}") + for name in sorted(RESULTS): + count, duration = RESULTS[name] + rate = count / duration + name = name.replace("test_", "") + print(f"{name:<25} {count:>10} {duration:>7.1f}s {rate:>9.1f}/s") + + @timed + async def test_connection_memory(self): + while True: + yield + async with aiosqlite.connect(TEST_DB): + pass + + @timed + async def test_connection_file(self): + with tempfile.NamedTemporaryFile(delete=False) as tf: + path = tf.name + tf.close() + + async with aiosqlite.connect(path) as db: + await db.execute( + "create table perf (i integer primary key asc, k integer)" + ) + await db.execute("insert into perf (k) values (2), (3)") + await db.commit() + + while True: + yield + async with aiosqlite.connect(path): + pass + + @timed + async def test_atomics(self): + async with aiosqlite.connect(TEST_DB) as db: + await db.execute("create table perf (i integer primary key asc, k integer)") + await db.execute("insert into perf (k) values (2), (3)") + await db.commit() + + while True: + yield + async with db.execute("select last_insert_rowid()") as cursor: + await cursor.fetchone() + + @timed + async def test_inserts(self): + async with aiosqlite.connect(TEST_DB) as db: + await db.execute("create table perf (i integer primary key asc, k integer)") + await db.commit() + + while True: + yield + await db.execute("insert into perf (k) values (1), (2), (3)") + await db.commit() + + @timed + async def test_inserts_authorized(self): + def deny_drops(action_code, arg1, arg2, db_name, trigger_name): + if action_code == sqlite3.SQLITE_DROP_TABLE: + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + async with aiosqlite.connect(TEST_DB) as db: + await db.execute("create table perf (i integer primary key asc, k integer)") + await db.set_authorizer(deny_drops) + await db.commit() + + while True: + yield + await db.execute("insert into perf (k) values (1), (2), (3)") + await db.commit() + + @timed + async def test_insert_ids(self): + async with aiosqlite.connect(TEST_DB) as db: + await db.execute("create table perf (i integer primary key asc, k integer)") + await db.commit() + + while True: + yield + cursor = await db.execute("insert into perf (k) values (1)") + await cursor.execute("select last_insert_rowid()") + await cursor.fetchone() + await db.commit() + + @timed + async def test_insert_macro_ids(self): + async with aiosqlite.connect(TEST_DB) as db: + await db.execute("create table perf (i integer primary key asc, k integer)") + await db.commit() + + while True: + yield + await db.execute_insert("insert into perf (k) values (1)") + await db.commit() + + @timed + async def test_select(self): + async with aiosqlite.connect(TEST_DB) as db: + await db.execute("create table perf (i integer primary key asc, k integer)") + for i in range(100): + await db.execute("insert into perf (k) values (%d)" % (i,)) + await db.commit() + + while True: + yield + cursor = await db.execute("select i, k from perf") + assert len(await cursor.fetchall()) == 100 + + @timed + async def test_select_macro(self): + async with aiosqlite.connect(TEST_DB) as db: + await db.execute("create table perf (i integer primary key asc, k integer)") + for i in range(100): + await db.execute("insert into perf (k) values (%d)" % (i,)) + await db.commit() + + while True: + yield + assert len(await db.execute_fetchall("select i, k from perf")) == 100 + + async def test_iterable_cursor_perf(self): + async with aiosqlite.connect(TEST_DB) as db: + await db.execute( + "create table ic_perf (" + "i integer primary key asc, k integer, a integer, b integer, c char(16))" + ) + for batch in range(128): # add 128k rows + r_start = batch * 1024 + await db.executemany( + "insert into ic_perf (k, a, b, c) values(?, 1, 2, ?)", + [ + *[ + (i, string.ascii_lowercase) + for i in range(r_start, r_start + 1024) + ] + ], + ) + await db.commit() + + async def test_perf(chunk_size: int): + while True: + async with db.execute("SELECT * FROM ic_perf") as cursor: + cursor.iter_chunk_size = chunk_size + async for _ in cursor: + yield + + for chunk_size in [2**i for i in range(4, 11)]: + await timed(test_perf, f"iterable_cursor @ {chunk_size}")(chunk_size) diff --git a/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/smoke.py b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/smoke.py new file mode 100644 index 0000000..046f9b7 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/aiosqlite/tests/smoke.py @@ -0,0 +1,537 @@ +# Copyright Amethyst Reese +# Licensed under the MIT license + +import asyncio +import sqlite3 +import sys +from pathlib import Path +from sqlite3 import OperationalError +from tempfile import TemporaryDirectory +from threading import Thread +from unittest import IsolatedAsyncioTestCase, SkipTest +from unittest.mock import patch + +import aiosqlite +from .helpers import setup_logger + + +class SmokeTest(IsolatedAsyncioTestCase): + @classmethod + def setUpClass(cls): + setup_logger() + + def setUp(self): + td = TemporaryDirectory() + self.addCleanup(td.cleanup) + self.db = Path(td.name).resolve() / "test.db" + + async def test_connection_await(self): + db = await aiosqlite.connect(self.db) + self.assertIsInstance(db, aiosqlite.Connection) + + async with db.execute("select 1, 2") as cursor: + rows = await cursor.fetchall() + self.assertEqual(rows, [(1, 2)]) + + await db.close() + + async def test_connection_context(self): + async with aiosqlite.connect(self.db) as db: + self.assertIsInstance(db, aiosqlite.Connection) + + async with db.execute("select 1, 2") as cursor: + rows = await cursor.fetchall() + self.assertEqual(rows, [(1, 2)]) + + async def test_connection_locations(self): + TEST_DB = self.db.as_posix() + + class Fake: # pylint: disable=too-few-public-methods + def __str__(self): + return TEST_DB + + locs = (Path(TEST_DB), TEST_DB, TEST_DB.encode(), Fake()) + + async with aiosqlite.connect(locs[0]) as db: + await db.execute("create table foo (i integer, k integer)") + await db.execute("insert into foo (i, k) values (1, 5)") + await db.commit() + + cursor = await db.execute("select * from foo") + rows = await cursor.fetchall() + + for loc in locs: + async with aiosqlite.connect(loc) as db: + cursor = await db.execute("select * from foo") + self.assertEqual(await cursor.fetchall(), rows) + + async def test_multiple_connections(self): + async with aiosqlite.connect(self.db) as db: + await db.execute( + "create table multiple_connections " + "(i integer primary key asc, k integer)" + ) + + async def do_one_conn(i): + async with aiosqlite.connect(self.db) as db: + await db.execute("insert into multiple_connections (k) values (?)", [i]) + await db.commit() + + await asyncio.gather(*[do_one_conn(i) for i in range(10)]) + + async with aiosqlite.connect(self.db) as db: + cursor = await db.execute("select * from multiple_connections") + rows = await cursor.fetchall() + + assert len(rows) == 10 + + async def test_multiple_queries(self): + async with aiosqlite.connect(self.db) as db: + await db.execute( + "create table multiple_queries " + "(i integer primary key asc, k integer)" + ) + + await asyncio.gather( + *[ + db.execute("insert into multiple_queries (k) values (?)", [i]) + for i in range(10) + ] + ) + + await db.commit() + + async with aiosqlite.connect(self.db) as db: + cursor = await db.execute("select * from multiple_queries") + rows = await cursor.fetchall() + + assert len(rows) == 10 + + async def test_iterable_cursor(self): + async with aiosqlite.connect(self.db) as db: + cursor = await db.cursor() + await cursor.execute( + "create table iterable_cursor " "(i integer primary key asc, k integer)" + ) + await cursor.executemany( + "insert into iterable_cursor (k) values (?)", [[i] for i in range(10)] + ) + await db.commit() + + async with aiosqlite.connect(self.db) as db: + cursor = await db.execute("select * from iterable_cursor") + rows = [] + async for row in cursor: + rows.append(row) + + assert len(rows) == 10 + + async def test_multi_loop_usage(self): + results = {} + + def runner(k, conn): + async def query(): + async with conn.execute("select * from foo") as cursor: + rows = await cursor.fetchall() + self.assertEqual(len(rows), 2) + return rows + + with self.subTest(k): + loop = asyncio.new_event_loop() + rows = loop.run_until_complete(query()) + loop.close() + results[k] = rows + + async with aiosqlite.connect(":memory:") as db: + await db.execute("create table foo (id int, name varchar)") + await db.execute( + "insert into foo values (?, ?), (?, ?)", (1, "Sally", 2, "Janet") + ) + await db.commit() + + threads = [Thread(target=runner, args=(k, db)) for k in range(4)] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + self.assertEqual(len(results), 4) + for rows in results.values(): + self.assertEqual(len(rows), 2) + + async def test_context_cursor(self): + async with aiosqlite.connect(self.db) as db: + async with db.cursor() as cursor: + await cursor.execute( + "create table context_cursor " + "(i integer primary key asc, k integer)" + ) + await cursor.executemany( + "insert into context_cursor (k) values (?)", + [[i] for i in range(10)], + ) + await db.commit() + + async with aiosqlite.connect(self.db) as db: + async with db.execute("select * from context_cursor") as cursor: + rows = [] + async for row in cursor: + rows.append(row) + + assert len(rows) == 10 + + async def test_cursor_return_self(self): + async with aiosqlite.connect(self.db) as db: + cursor = await db.cursor() + + result = await cursor.execute( + "create table test_cursor_return_self (i integer, k integer)" + ) + self.assertEqual(result, cursor, "cursor execute returns itself") + + result = await cursor.executemany( + "insert into test_cursor_return_self values (?, ?)", [(1, 1), (2, 2)] + ) + self.assertEqual(result, cursor) + + result = await cursor.executescript( + "insert into test_cursor_return_self values (3, 3);" + "insert into test_cursor_return_self values (4, 4);" + "insert into test_cursor_return_self values (5, 5);" + ) + self.assertEqual(result, cursor) + + async def test_connection_properties(self): + async with aiosqlite.connect(self.db) as db: + self.assertEqual(db.total_changes, 0) + + async with db.cursor() as cursor: + self.assertFalse(db.in_transaction) + await cursor.execute( + "create table test_properties " + "(i integer primary key asc, k integer, d text)" + ) + await cursor.execute( + "insert into test_properties (k, d) values (1, 'hi')" + ) + self.assertTrue(db.in_transaction) + await db.commit() + self.assertFalse(db.in_transaction) + + self.assertEqual(db.total_changes, 1) + + self.assertIsNone(db.row_factory) + self.assertEqual(db.text_factory, str) + + async with db.cursor() as cursor: + await cursor.execute("select * from test_properties") + row = await cursor.fetchone() + self.assertIsInstance(row, tuple) + self.assertEqual(row, (1, 1, "hi")) + with self.assertRaises(TypeError): + _ = row["k"] + + async with db.cursor() as cursor: + cursor.row_factory = aiosqlite.Row + self.assertEqual(cursor.row_factory, aiosqlite.Row) + await cursor.execute("select * from test_properties") + row = await cursor.fetchone() + self.assertIsInstance(row, aiosqlite.Row) + self.assertEqual(row[1], 1) + self.assertEqual(row[2], "hi") + self.assertEqual(row["k"], 1) + self.assertEqual(row["d"], "hi") + + db.row_factory = aiosqlite.Row + db.text_factory = bytes + self.assertEqual(db.row_factory, aiosqlite.Row) + self.assertEqual(db.text_factory, bytes) + + async with db.cursor() as cursor: + await cursor.execute("select * from test_properties") + row = await cursor.fetchone() + self.assertIsInstance(row, aiosqlite.Row) + self.assertEqual(row[1], 1) + self.assertEqual(row[2], b"hi") + self.assertEqual(row["k"], 1) + self.assertEqual(row["d"], b"hi") + + async def test_fetch_all(self): + async with aiosqlite.connect(self.db) as db: + await db.execute( + "create table test_fetch_all (i integer primary key asc, k integer)" + ) + await db.execute( + "insert into test_fetch_all (k) values (10), (24), (16), (32)" + ) + await db.commit() + + async with aiosqlite.connect(self.db) as db: + cursor = await db.execute("select k from test_fetch_all where k < 30") + rows = await cursor.fetchall() + self.assertEqual(rows, [(10,), (24,), (16,)]) + + async def test_enable_load_extension(self): + """Assert that after enabling extension loading, they can be loaded""" + async with aiosqlite.connect(self.db) as db: + try: + await db.enable_load_extension(True) + await db.load_extension("test") + except OperationalError as e: + assert "not authorized" not in e.args + except AttributeError as e: + raise SkipTest( + "python was not compiled with sqlite3 " + "extension support, so we can't test it" + ) from e + + async def test_set_progress_handler(self): + """ + Assert that after setting a progress handler returning 1, DB operations are aborted + """ + async with aiosqlite.connect(self.db) as db: + await db.set_progress_handler(lambda: 1, 1) + with self.assertRaises(OperationalError): + await db.execute( + "create table test_progress_handler (i integer primary key asc, k integer)" + ) + + async def test_create_function(self): + """Assert that after creating a custom function, it can be used""" + + def no_arg(): + return "no arg" + + def one_arg(num): + return num * 2 + + async with aiosqlite.connect(self.db) as db: + await db.create_function("no_arg", 0, no_arg) + await db.create_function("one_arg", 1, one_arg) + + async with db.execute("SELECT no_arg();") as res: + row = await res.fetchone() + self.assertEqual(row[0], "no arg") + + async with db.execute("SELECT one_arg(10);") as res: + row = await res.fetchone() + self.assertEqual(row[0], 20) + + async def test_create_function_deterministic(self): + """Assert that after creating a deterministic custom function, it can be used. + + https://sqlite.org/deterministic.html + """ + + def one_arg(num): + return num * 2 + + async with aiosqlite.connect(self.db) as db: + await db.create_function("one_arg", 1, one_arg, deterministic=True) + await db.execute("create table foo (id int, bar int)") + + # Non-deterministic functions cannot be used in indexes + await db.execute("create index t on foo(one_arg(bar))") + + async def test_set_trace_callback(self): + statements = [] + + def callback(statement: str): + statements.append(statement) + + async with aiosqlite.connect(self.db) as db: + await db.set_trace_callback(callback) + + await db.execute("select 10") + self.assertIn("select 10", statements) + + async def test_set_authorizer_deny_drops(self): + """Test authorizer that denies DROP operations""" + + def deny_drops(action_code, arg1, arg2, db_name, trigger_name): + if action_code == sqlite3.SQLITE_DROP_TABLE: + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + async with aiosqlite.connect(self.db) as db: + await db.set_authorizer(deny_drops) + + # Other operations should succeed + await db.execute("CREATE TABLE test_drop (id INTEGER)") + await db.execute("INSERT INTO test_drop VALUES (1)") + await db.execute("SELECT * FROM test_drop") + + # DROP should fail + with self.assertRaises(sqlite3.DatabaseError): + await db.execute("DROP TABLE test_drop") + + if sys.version_info >= (3, 11): + # Disabling the authorizer re-enables DROP + await db.set_authorizer(None) + await db.execute("DROP TABLE test_drop") + + async def test_set_authorizer_exception_propagation(self): + """Test that exceptions raised in authorizer callback are caught by SQLite""" + + def raise_exception(action_code, arg1, arg2, db_name, trigger_name): + raise ValueError("Test exception from authorizer") + + async with aiosqlite.connect(self.db) as db: + await db.set_authorizer(raise_exception) + with self.assertRaises(sqlite3.DatabaseError): + await db.execute("CREATE TABLE test_exception (id INTEGER)") + + async def test_connect_error(self): + bad_db = Path("/something/that/shouldnt/exist.db") + with self.assertRaisesRegex(OperationalError, "unable to open database"): + async with aiosqlite.connect(bad_db) as db: + self.assertIsNone(db) # should never be reached + + with self.assertRaisesRegex(OperationalError, "unable to open database"): + await aiosqlite.connect(bad_db) + + async def test_connect_base_exception(self): + # Check if connect task is cancelled, thread is properly closed. + def _raise_cancelled_error(*_, **__): + raise asyncio.CancelledError("I changed my mind") + + connection = aiosqlite.Connection(lambda: sqlite3.connect(":memory:"), 64) + with ( + patch.object(sqlite3, "connect", side_effect=_raise_cancelled_error), + self.assertRaisesRegex(asyncio.CancelledError, "I changed my mind"), + ): + async with connection: + ... + # Terminate the thread here if the test fails to have a clear error. + if connection._running: + connection.stop() + raise AssertionError("connection thread was not stopped") + + async def test_iterdump(self): + async with aiosqlite.connect(":memory:") as db: + await db.execute("create table foo (i integer, k charvar(250))") + await db.executemany( + "insert into foo values (?, ?)", [(1, "hello"), (2, "world")] + ) + + lines = [line async for line in db.iterdump()] + self.assertEqual( + lines, + [ + "BEGIN TRANSACTION;", + "CREATE TABLE foo (i integer, k charvar(250));", + "INSERT INTO \"foo\" VALUES(1,'hello');", + "INSERT INTO \"foo\" VALUES(2,'world');", + "COMMIT;", + ], + ) + + async def test_cursor_on_closed_connection(self): + db = await aiosqlite.connect(self.db) + + cursor = await db.execute("select 1, 2") + await db.close() + with self.assertRaisesRegex(ValueError, "Connection closed"): + await cursor.fetchall() + with self.assertRaisesRegex(ValueError, "Connection closed"): + await cursor.fetchall() + + async def test_cursor_on_closed_connection_loop(self): + db = await aiosqlite.connect(self.db) + + cursor = await db.execute("select 1, 2") + tasks = [] + for i in range(100): + if i == 50: + tasks.append(asyncio.ensure_future(db.close())) + tasks.append(asyncio.ensure_future(cursor.fetchall())) + for task in tasks: + try: + await task + except sqlite3.ProgrammingError: + pass + + async def test_close_blocking_until_transaction_queue_empty(self): + db = await aiosqlite.connect(self.db) + # Insert transactions into the + # transaction queue '_tx' + for i in range(1000): + await db.execute(f"select 1, {i}") + # Wait for all transactions to complete + await db.close() + # Check no more transaction pending + self.assertEqual(db._tx.empty(), True) + + async def test_close_twice(self): + db = await aiosqlite.connect(self.db) + + await db.close() + + # no error + await db.close() + + async def test_backup_aiosqlite(self): + def progress(a, b, c): + print(a, b, c) + + async with ( + aiosqlite.connect(":memory:") as db1, + aiosqlite.connect(":memory:") as db2, + ): + await db1.execute("create table foo (i integer, k charvar(250))") + await db1.executemany( + "insert into foo values (?, ?)", [(1, "hello"), (2, "world")] + ) + await db1.commit() + + with self.assertRaisesRegex(OperationalError, "no such table: foo"): + await db2.execute("select * from foo") + + await db1.backup(db2, progress=progress) + + async with db2.execute("select * from foo") as cursor: + rows = await cursor.fetchall() + self.assertEqual(rows, [(1, "hello"), (2, "world")]) + + async def test_backup_sqlite(self): + async with aiosqlite.connect(":memory:") as db1: + with sqlite3.connect(":memory:") as db2: + await db1.execute("create table foo (i integer, k charvar(250))") + await db1.executemany( + "insert into foo values (?, ?)", [(1, "hello"), (2, "world")] + ) + await db1.commit() + + with self.assertRaisesRegex(OperationalError, "no such table: foo"): + db2.execute("select * from foo") + + await db1.backup(db2) + + cursor = db2.execute("select * from foo") + rows = cursor.fetchall() + self.assertEqual(rows, [(1, "hello"), (2, "world")]) + + async def test_emits_warning_when_left_open(self): + db = await aiosqlite.connect(":memory:") + + with self.assertWarnsRegex( + ResourceWarning, r".*was deleted before being closed.*" + ): + del db + + async def test_stop_without_close(self): + db = await aiosqlite.connect(":memory:") + await db.stop() + + def test_stop_after_event_loop_closed(self): + db = None + + async def inner(): + nonlocal db + db = await aiosqlite.connect(":memory:") + + loop = asyncio.new_event_loop() + loop.run_until_complete(inner()) + loop.close() + + db.stop() diff --git a/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/INSTALLER b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/METADATA b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/METADATA new file mode 100644 index 0000000..d162dc8 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/METADATA @@ -0,0 +1,144 @@ +Metadata-Version: 2.4 +Name: annotated-doc +Version: 0.0.5 +Summary: Document parameters, class attributes, return types, and variables inline, with Annotated. +Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= +License-Expression: MIT +License-File: LICENSE +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: System Administrators +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Software Development +Classifier: Typing :: Typed +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Project-URL: Homepage, https://github.com/fastapi/annotated-doc +Project-URL: Documentation, https://github.com/fastapi/annotated-doc +Project-URL: Repository, https://github.com/fastapi/annotated-doc +Project-URL: Issues, https://github.com/fastapi/annotated-doc/issues +Project-URL: Changelog, https://github.com/fastapi/annotated-doc/release-notes.md +Requires-Python: >=3.9 +Description-Content-Type: text/markdown + +# Annotated Doc + +Document parameters, class attributes, return types, and variables inline, with `Annotated`. + + + Test + + + Coverage + + + Package version + + + Supported Python versions + + +## Installation + +```bash +pip install annotated-doc +``` + +Or with `uv`: + +```Python +uv add annotated-doc +``` + +## Usage + +Import `Doc` and pass a single literal string with the documentation for the specific parameter, class attribute, return type, or variable. + +For example, to document a parameter `name` in a function `hi` you could do: + +```Python +from typing import Annotated + +from annotated_doc import Doc + +def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None: + print(f"Hi, {name}!") +``` + +You can also use it to document class attributes: + +```Python +from typing import Annotated + +from annotated_doc import Doc + +class User: + name: Annotated[str, Doc("The user's name")] + age: Annotated[int, Doc("The user's age")] +``` + +The same way, you could document return types and variables, or anything that could have a type annotation with `Annotated`. + +## Who Uses This + +`annotated-doc` was made for: + +* [FastAPI](https://fastapi.tiangolo.com/) +* [Typer](https://typer.tiangolo.com/) +* [SQLModel](https://sqlmodel.tiangolo.com/) +* [Asyncer](https://asyncer.tiangolo.com/) + +`annotated-doc` is supported by [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc), which powers reference documentation like the one in the [FastAPI Reference](https://fastapi.tiangolo.com/reference/). + +## Reasons not to use `annotated-doc` + +You are already comfortable with one of the existing docstring formats, like: + +* Sphinx +* numpydoc +* Google +* Keras + +Your team is already comfortable using them. + +You prefer having the documentation about parameters all together in a docstring, separated from the code defining them. + +You care about a specific set of users, using one specific editor, and that editor already has support for the specific docstring format you use. + +## Reasons to use `annotated-doc` + +* No micro-syntax to learn for newcomers, it’s **just Python** syntax. +* **Editing** would be already fully supported by default by any editor (current or future) supporting Python syntax, including syntax errors, syntax highlighting, etc. +* **Rendering** would be relatively straightforward to implement by static tools (tools that don't need runtime execution), as the information can be extracted from the AST they normally already create. +* **Deduplication of information**: the name of a parameter would be defined in a single place, not duplicated inside of a docstring. +* **Elimination** of the possibility of having **inconsistencies** when removing a parameter or class variable and **forgetting to remove** its documentation. +* **Minimization** of the probability of adding a new parameter or class variable and **forgetting to add its documentation**. +* **Elimination** of the possibility of having **inconsistencies** between the **name** of a parameter in the **signature** and the name in the docstring when it is renamed. +* **Access** to the documentation string for each symbol at **runtime**, including existing (older) Python versions. +* A more formalized way to document other symbols, like type aliases, that could use Annotated. +* **Support** for apps using FastAPI, Typer and others. +* **AI Accessibility**: AI tools will have an easier way understanding each parameter as the distance from documentation to parameter is much closer. + +## History + +I ([@tiangolo](https://github.com/tiangolo)) originally wanted for this to be part of the Python standard library (in [PEP 727](https://peps.python.org/pep-0727/)), but the proposal was withdrawn as there was a fair amount of negative feedback and opposition. + +The conclusion was that this was better done as an external effort, in a third-party library. + +So, here it is, with a simpler approach, as a third-party library, in a way that can be used by others, starting with FastAPI and friends. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/RECORD b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/RECORD new file mode 100644 index 0000000..cdbf84e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/RECORD @@ -0,0 +1,11 @@ +annotated_doc-0.0.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +annotated_doc-0.0.5.dist-info/METADATA,sha256=jCTXDF2RU9IhIzU6kGBC_XYD3y0VwhlikLD4qWJVjEY,6516 +annotated_doc-0.0.5.dist-info/RECORD,, +annotated_doc-0.0.5.dist-info/WHEEL,sha256=VP-D4TPS230sME9Z3vb3INXvo1yt0924YRm5AOsk_dE,90 +annotated_doc-0.0.5.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34 +annotated_doc-0.0.5.dist-info/licenses/LICENSE,sha256=__Fwd5pqy_ZavbQFwIfxzuF4ZpHkqWpANFF-SlBKDN8,1086 +annotated_doc/__init__.py,sha256=UqEV5evgeySQ5yvJQwfwTqwvMGfIwbxRPP4gf-o2-LA,52 +annotated_doc/__pycache__/__init__.cpython-312.pyc,, +annotated_doc/__pycache__/main.cpython-312.pyc,, +annotated_doc/main.py,sha256=5Zfvxv80SwwLqpRW73AZyZyiM4bWma9QWRbp_cgD20s,1075 +annotated_doc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/WHEEL b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/WHEEL new file mode 100644 index 0000000..e651d8e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: pdm-backend (2.4.9) +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/entry_points.txt b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/entry_points.txt new file mode 100644 index 0000000..c3ad472 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/entry_points.txt @@ -0,0 +1,4 @@ +[console_scripts] + +[gui_scripts] + diff --git a/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/licenses/LICENSE b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/licenses/LICENSE new file mode 100644 index 0000000..7a25446 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_doc-0.0.5.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 Sebastián Ramírez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/.venv-test/lib/python3.12/site-packages/annotated_doc/__init__.py b/.venv-test/lib/python3.12/site-packages/annotated_doc/__init__.py new file mode 100644 index 0000000..d6f7292 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_doc/__init__.py @@ -0,0 +1,3 @@ +from .main import Doc as Doc + +__version__ = "0.0.5" diff --git a/.venv-test/lib/python3.12/site-packages/annotated_doc/main.py b/.venv-test/lib/python3.12/site-packages/annotated_doc/main.py new file mode 100644 index 0000000..7063c59 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_doc/main.py @@ -0,0 +1,36 @@ +class Doc: + """Define the documentation of a type annotation using `Annotated`, to be + used in class attributes, function and method parameters, return values, + and variables. + + The value should be a positional-only string literal to allow static tools + like editors and documentation generators to use it. + + This complements docstrings. + + The string value passed is available in the attribute `documentation`. + + Example: + + ```Python + from typing import Annotated + from annotated_doc import Doc + + def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None: + print(f"Hi, {name}!") + ``` + """ + + def __init__(self, documentation: str, /) -> None: + self.documentation = documentation + + def __repr__(self) -> str: + return f"Doc({self.documentation!r})" + + def __hash__(self) -> int: + return hash(self.documentation) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Doc): + return NotImplemented + return self.documentation == other.documentation diff --git a/.venv-test/lib/python3.12/site-packages/annotated_doc/py.typed b/.venv-test/lib/python3.12/site-packages/annotated_doc/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/INSTALLER b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/METADATA b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/METADATA new file mode 100644 index 0000000..eb5a088 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/METADATA @@ -0,0 +1,295 @@ +Metadata-Version: 2.4 +Name: annotated-types +Version: 0.8.0 +Summary: Reusable constraint types to use with typing.Annotated +Project-URL: Homepage, https://github.com/annotated-types/annotated-types +Project-URL: Source, https://github.com/annotated-types/annotated-types +Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases +Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin , Zac Hatfield-Dodds +License-Expression: MIT +License-File: LICENSE +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console +Classifier: Environment :: MacOS X +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Unix +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Typing :: Typed +Requires-Python: >=3.10 +Description-Content-Type: text/markdown + +# annotated-types + +[![CI](https://github.com/annotated-types/annotated-types/workflows/CI/badge.svg?event=push)](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) +[![pypi](https://img.shields.io/pypi/v/annotated-types.svg)](https://pypi.python.org/pypi/annotated-types) +[![versions](https://img.shields.io/pypi/pyversions/annotated-types.svg)](https://github.com/annotated-types/annotated-types) +[![license](https://img.shields.io/github/license/annotated-types/annotated-types.svg)](https://github.com/annotated-types/annotated-types/blob/main/LICENSE) + +[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of +adding context-specific metadata to existing types, and specifies that +`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special +logic for `x`. + +This package provides metadata objects which can be used to represent common +constraints such as upper and lower bounds on scalar values and collection sizes, +a `Predicate` marker for runtime checks, and +descriptions of how we intend these metadata to be interpreted. In some cases, +we also note alternative representations which do not require this package. + +## Install + +```bash +pip install annotated-types +``` + +## Examples + +```python +from typing import Annotated +from annotated_types import Gt, Len, Predicate + +class MyClass: + age: Annotated[int, Gt(18)] # Valid: 19, 20, ... + # Invalid: 17, 18, "19", 19.0, ... + factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ... + # Invalid: 4, 8, -2, 5.0, "prime", ... + + my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50] + # Invalid: (1, 2), ["abc"], [0] * 20 +``` + +## Documentation + +_While `annotated-types` avoids runtime checks for performance, users should not +construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`. +Downstream implementors may choose to raise an error, emit a warning, silently ignore +a metadata item, etc., if the metadata objects described below are used with an +incompatible type - or for any other reason!_ + +### Gt, Ge, Lt, Le + +Express inclusive and/or exclusive bounds on orderable values - which may be numbers, +dates, times, strings, sets, etc. Note that the boundary value need not be of the +same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]` +is fine, for example, and implies that the value is an integer x such that `x > 1.5`. + +We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)` +as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on +the `annotated-types` package. + +To be explicit, these types have the following meanings: + +* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum +* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum +* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum +* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum + +### Interval + +`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single +metadata object. `None` attributes should be ignored, and non-`None` attributes +treated as per the single bounds above. + +### MultipleOf + +`MultipleOf(multiple_of=x)` might be interpreted in two ways: + +1. Python semantics, implying `value % multiple_of == 0`, or +2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1), + where `int(value / multiple_of) == value / multiple_of`. + +We encourage users to be aware of these two common interpretations and their +distinct behaviours, especially since very large or non-integer numbers make +it easy to cause silent data corruption due to floating-point imprecision. + +We encourage libraries to carefully document which interpretation they implement. + +### MinLen, MaxLen, Len + +`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive. + +As well as `Len()` which can optionally include upper and lower bounds, we also +provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)` +and `Len(max_length=y)` respectively. + +`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`. + +Examples of usage: + +* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10)]`) - list must have a length of 10 or less +* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less +* `Annotated[list, MinLen(3)]` (or `Annotated[list, Len(min_length=3)]`) - list must have a length of 3 or more +* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6 +* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8 + +#### Changed in v0.4.0 + +* `min_inclusive` has been renamed to `min_length`, no change in meaning +* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive** +* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic + meaning of the upper bound in slices vs. `Len` + +See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion. + +### Timezone + +`Timezone` can be used with a `datetime` or a `time` to express which timezones +are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime. +`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis)) +expresses that any timezone-aware datetime is allowed. You may also pass a specific +timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) +object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only +allow a specific timezone, though we note that this is often a symptom of fragile design. + +#### Changed in v0.x.x + +* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of + `timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries. + +### Unit + +`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of +a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]` +would be a float representing a velocity in meters per second. + +Please note that `annotated_types` itself makes no attempt to parse or validate +the unit string in any way. That is left entirely to downstream libraries, +such as [`pint`](https://pint.readthedocs.io) or +[`astropy.units`](https://docs.astropy.org/en/stable/units/). + +An example of how a library might use this metadata: + +```python +from annotated_types import Unit +from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args + +# given a type annotated with a unit: +Meters = Annotated[float, Unit("m")] + + +# you can cast the annotation to a specific unit type with any +# callable that accepts a string and returns the desired type +T = TypeVar("T") +def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None: + if get_origin(tp) is Annotated: + for arg in get_args(tp): + if isinstance(arg, Unit): + return unit_cls(arg.unit) + return None + + +# using `pint` +import pint +pint_unit = cast_unit(Meters, pint.Unit) + + +# using `astropy.units` +import astropy.units as u +astropy_unit = cast_unit(Meters, u.Unit) +``` + +### Predicate + +`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values. +Users should prefer the statically inspectable metadata above, but if you need +the full power and flexibility of arbitrary runtime predicates... here it is. + +For some common constraints, we provide generic types: + +* `LowerCase = Annotated[T, Predicate(str.islower)]` +* `UpperCase = Annotated[T, Predicate(str.isupper)]` +* `IsDigit = Annotated[T, Predicate(str.isdigit)]` +* `IsFinite = Annotated[T, Predicate(math.isfinite)]` +* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]` +* `IsNan = Annotated[T, Predicate(math.isnan)]` +* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]` +* `IsInfinite = Annotated[T, Predicate(math.isinf)]` +* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]` + +so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer +(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`. + +Some libraries might have special logic to handle known or understandable predicates, +for example by checking for `str.isdigit` and using its presence to both call custom +logic to enforce digit-only strings, and customise some generated external schema. +Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in +favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`. + +To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner. + +We do not specify what behaviour should be expected for predicates that raise +an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently +skip invalid constraints, or statically raise an error; or it might try calling it +and then propagate or discard the resulting +`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object` +exception. We encourage libraries to document the behaviour they choose. + +### Doc + +`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used. + +It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools. + +It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`. + +This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md). + +### Integrating downstream types with `GroupedMetadata` + +Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata. +This can help reduce verbosity and cognitive overhead for users. +For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata: + +```python +from dataclasses import dataclass +from typing import Iterator +from annotated_types import GroupedMetadata, Ge + +@dataclass +class Field(GroupedMetadata): + ge: int | None = None + description: str | None = None + + def __iter__(self) -> Iterator[object]: + # Iterating over a GroupedMetadata object should yield annotated-types + # constraint metadata objects which describe it as fully as possible, + # and may include other unknown objects too. + if self.ge is not None: + yield Ge(self.ge) + if self.description is not None: + yield Description(self.description) +``` + +Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently. + +Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself. + +Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`. + +### Consuming metadata + +We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103). + +It is up to the implementer to determine how this metadata is used. +You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases. + +## Design & History + +This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic +and Hypothesis, with the goal of making it as easy as possible for end-users to +provide more informative annotations for use by runtime libraries. + +It is deliberately minimal, and following PEP-593 allows considerable downstream +discretion in what (if anything!) they choose to support. Nonetheless, we expect +that staying simple and covering _only_ the most common use-cases will give users +and maintainers the best experience we can. If you'd like more constraints for your +types - follow our lead, by defining them and documenting them downstream! diff --git a/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/RECORD b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/RECORD new file mode 100644 index 0000000..3bcb37f --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/RECORD @@ -0,0 +1,10 @@ +annotated_types-0.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +annotated_types-0.8.0.dist-info/METADATA,sha256=YUmFsnj2Abjvhj-CeDuZHMOtpLifWS63HHxIxfLvIpg,15009 +annotated_types-0.8.0.dist-info/RECORD,, +annotated_types-0.8.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87 +annotated_types-0.8.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083 +annotated_types/__init__.py,sha256=pxBKTUObJ6n3T8C-I2ubobeDHmBEAmgCogWrwSmKm8g,13273 +annotated_types/__pycache__/__init__.cpython-312.pyc,, +annotated_types/__pycache__/test_cases.cpython-312.pyc,, +annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +annotated_types/test_cases.py,sha256=2GdHKstXuBzpf3iXKCjmvFZpyk2zP9Hm9kXGGDhGBo4,6310 diff --git a/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/WHEEL b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/WHEEL new file mode 100644 index 0000000..7401812 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.31.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/licenses/LICENSE b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..d99323a --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_types-0.8.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 the contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.venv-test/lib/python3.12/site-packages/annotated_types/__init__.py b/.venv-test/lib/python3.12/site-packages/annotated_types/__init__.py new file mode 100644 index 0000000..dcb35c5 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_types/__init__.py @@ -0,0 +1,416 @@ +import math +import types +from collections.abc import Callable, Iterator +from dataclasses import dataclass +from datetime import tzinfo +from types import EllipsisType +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Literal, + Protocol, + SupportsFloat, + SupportsIndex, + TypeVar, + Union, + runtime_checkable, +) + +__all__ = ( + 'BaseMetadata', + 'GroupedMetadata', + 'Gt', + 'Ge', + 'Lt', + 'Le', + 'Interval', + 'MultipleOf', + 'MinLen', + 'MaxLen', + 'Len', + 'Timezone', + 'Predicate', + 'LowerCase', + 'UpperCase', + 'IsDigits', + 'IsFinite', + 'IsNotFinite', + 'IsNan', + 'IsNotNan', + 'IsInfinite', + 'IsNotInfinite', + 'doc', + 'DocInfo', + '__version__', +) + +__version__ = '0.8.0' + + +T = TypeVar('T') + + +# arguments that start with __ are considered +# positional only +# see https://peps.python.org/pep-0484/#positional-only-arguments + + +class SupportsGt(Protocol): + def __gt__(self: T, __other: T) -> bool: + ... + + +class SupportsGe(Protocol): + def __ge__(self: T, __other: T) -> bool: + ... + + +class SupportsLt(Protocol): + def __lt__(self: T, __other: T) -> bool: + ... + + +class SupportsLe(Protocol): + def __le__(self: T, __other: T) -> bool: + ... + + +class SupportsMod(Protocol): + def __mod__(self: T, __other: T) -> T: + ... + + +class SupportsDiv(Protocol): + def __div__(self: T, __other: T) -> T: + ... + + +class BaseMetadata: + """Base class for all metadata. + + This exists mainly so that implementers + can do `isinstance(..., BaseMetadata)` while traversing field annotations. + """ + + __slots__ = () + + +@dataclass(frozen=True, slots=True) +class Gt(BaseMetadata): + """Gt(gt=x) implies that the value must be greater than x. + + It can be used with any type that supports the ``>`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + gt: SupportsGt + + +@dataclass(frozen=True, slots=True) +class Ge(BaseMetadata): + """Ge(ge=x) implies that the value must be greater than or equal to x. + + It can be used with any type that supports the ``>=`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + ge: SupportsGe + + +@dataclass(frozen=True, slots=True) +class Lt(BaseMetadata): + """Lt(lt=x) implies that the value must be less than x. + + It can be used with any type that supports the ``<`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + lt: SupportsLt + + +@dataclass(frozen=True, slots=True) +class Le(BaseMetadata): + """Le(le=x) implies that the value must be less than or equal to x. + + It can be used with any type that supports the ``<=`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + le: SupportsLe + + +@runtime_checkable +class GroupedMetadata(Protocol): + """A grouping of multiple objects, like typing.Unpack. + + `GroupedMetadata` on its own is not metadata and has no meaning. + All of the constraints and metadata should be fully expressable + in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`. + + Concrete implementations should override `GroupedMetadata.__iter__()` + to add their own metadata. + For example: + + >>> @dataclass + >>> class Field(GroupedMetadata): + >>> gt: float | None = None + >>> description: str | None = None + ... + >>> def __iter__(self) -> Iterable[object]: + >>> if self.gt is not None: + >>> yield Gt(self.gt) + >>> if self.description is not None: + >>> yield Description(self.gt) + + Also see the implementation of `Interval` below for an example. + + Parsers should recognize this and unpack it so that it can be used + both with and without unpacking: + + - `Annotated[int, Field(...)]` (parser must unpack Field) + - `Annotated[int, *Field(...)]` (PEP-646) + """ # noqa: trailing-whitespace + + @property + def __is_annotated_types_grouped_metadata__(self) -> Literal[True]: + return True + + def __iter__(self) -> Iterator[object]: + ... + + if not TYPE_CHECKING: + __slots__ = () # allow subclasses to use slots + + def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: + # Basic ABC like functionality without the complexity of an ABC + super().__init_subclass__(*args, **kwargs) + if cls.__iter__ is GroupedMetadata.__iter__: + raise TypeError("Can't subclass GroupedMetadata without implementing __iter__") + + def __iter__(self) -> Iterator[object]: # noqa: F811 + raise NotImplementedError # more helpful than "None has no attribute..." type errors + + +@dataclass(frozen=True, kw_only=True, slots=True) +class Interval(GroupedMetadata): + """Interval can express inclusive or exclusive bounds with a single object. + + It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which + are interpreted the same way as the single-bound constraints. + """ + + gt: SupportsGt | None = None + ge: SupportsGe | None = None + lt: SupportsLt | None = None + le: SupportsLe | None = None + + def __iter__(self) -> Iterator[BaseMetadata]: + """Unpack an Interval into zero or more single-bounds.""" + if self.gt is not None: + yield Gt(self.gt) + if self.ge is not None: + yield Ge(self.ge) + if self.lt is not None: + yield Lt(self.lt) + if self.le is not None: + yield Le(self.le) + + +@dataclass(frozen=True, slots=True) +class MultipleOf(BaseMetadata): + """MultipleOf(multiple_of=x) might be interpreted in two ways: + + 1. Python semantics, implying ``value % multiple_of == 0``, or + 2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of`` + + We encourage users to be aware of these two common interpretations, + and libraries to carefully document which they implement. + """ + + multiple_of: SupportsDiv | SupportsMod + + +@dataclass(frozen=True, slots=True) +class MinLen(BaseMetadata): + """ + MinLen() implies minimum inclusive length, + e.g. ``len(value) >= min_length``. + """ + + min_length: Annotated[int, Ge(0)] + + +@dataclass(frozen=True, slots=True) +class MaxLen(BaseMetadata): + """ + MaxLen() implies maximum inclusive length, + e.g. ``len(value) <= max_length``. + """ + + max_length: Annotated[int, Ge(0)] + + +@dataclass(frozen=True, slots=True) +class Len(GroupedMetadata): + """ + Len() implies that ``min_length <= len(value) <= max_length``. + + Upper bound may be omitted or ``None`` to indicate no upper length bound. + """ + + min_length: Annotated[int, Ge(0)] = 0 + max_length: Annotated[int, Ge(0)] | None = None + + def __iter__(self) -> Iterator[BaseMetadata]: + """Unpack a Len into zero or more single-bounds.""" + if self.min_length > 0: + yield MinLen(self.min_length) + if self.max_length is not None: + yield MaxLen(self.max_length) + + +@dataclass(frozen=True, slots=True) +class Timezone(BaseMetadata): + """Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive). + + ``Annotated[datetime, Timezone(None)]`` must be a naive datetime. + ``Timezone(...)`` (the ellipsis literal) expresses that the datetime must be + tz-aware but any timezone is allowed. + + You may also pass a specific timezone string or tzinfo object such as + ``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that + you only allow a specific timezone, though we note that this is often + a symptom of poor design. + """ + + tz: str | tzinfo | EllipsisType | None + + +@dataclass(frozen=True, slots=True) +class Unit(BaseMetadata): + """Indicates that the value is a physical quantity with the specified unit. + + It is intended for usage with numeric types, where the value represents the + magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]`` + or ``speed: Annotated[float, Unit('m/s')]``. + + Interpretation of the unit string is left to the discretion of the consumer. + It is suggested to follow conventions established by python libraries that work + with physical quantities, such as + + - ``pint`` : + - ``astropy.units``: + + For indicating a quantity with a certain dimensionality but without a specific unit + it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`. + Note, however, ``annotated_types`` itself makes no use of the unit string. + """ + + unit: str + + +@dataclass(frozen=True, slots=True) +class Predicate(BaseMetadata): + """``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values. + + Users should prefer statically inspectable metadata, but if you need the full + power and flexibility of arbitrary runtime predicates... here it is. + + We provide a few predefined predicates for common string constraints: + ``LowerCase = Predicate(str.islower)``, ``UpperCase = Predicate(str.isupper)``, and + ``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which + can be given special handling, and avoid indirection like ``lambda s: s.lower()``. + + Some libraries might have special logic to handle certain predicates, e.g. by + checking for `str.isdigit` and using its presence to both call custom logic to + enforce digit-only strings, and customise some generated external schema. + + We do not specify what behaviour should be expected for predicates that raise + an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently + skip invalid constraints, or statically raise an error; or it might try calling it + and then propagate or discard the resulting exception. + """ + + func: Callable[[Any], bool] + + def __repr__(self) -> str: + if getattr(self.func, "__name__", "") == "": + return f"{self.__class__.__name__}({self.func!r})" + if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and ( + namespace := getattr(self.func.__self__, "__name__", None) + ): + return f"{self.__class__.__name__}({namespace}.{self.func.__name__})" + if isinstance(self.func, type(str.isascii)): # method descriptor + return f"{self.__class__.__name__}({self.func.__qualname__})" + return f"{self.__class__.__name__}({self.func.__name__})" + + +@dataclass +class Not: + func: Callable[[Any], bool] + + def __call__(self, __v: Any) -> bool: + return not self.func(__v) + + +_StrType = TypeVar("_StrType", bound=str) + +LowerCase = Annotated[_StrType, Predicate(str.islower)] +""" +Return True if the string is a lowercase string, False otherwise. + +A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string. +""" # noqa: E501 +UpperCase = Annotated[_StrType, Predicate(str.isupper)] +""" +Return True if the string is an uppercase string, False otherwise. + +A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string. +""" # noqa: E501 +IsDigit = Annotated[_StrType, Predicate(str.isdigit)] +IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63 +""" +Return True if the string is a digit string, False otherwise. + +A string is a digit string if all characters in the string are digits and there is at least one character in the string. +""" # noqa: E501 +IsAscii = Annotated[_StrType, Predicate(str.isascii)] +""" +Return True if all characters in the string are ASCII, False otherwise. + +ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too. +""" + +_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex]) +IsFinite = Annotated[_NumericType, Predicate(math.isfinite)] +"""Return True if x is neither an infinity nor a NaN, and False otherwise.""" +IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))] +"""Return True if x is one of infinity or NaN, and False otherwise""" +IsNan = Annotated[_NumericType, Predicate(math.isnan)] +"""Return True if x is a NaN (not a number), and False otherwise.""" +IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))] +"""Return True if x is anything but NaN (not a number), and False otherwise.""" +IsInfinite = Annotated[_NumericType, Predicate(math.isinf)] +"""Return True if x is a positive or negative infinity, and False otherwise.""" +IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))] +"""Return True if x is neither a positive or negative infinity, and False otherwise.""" + +try: + # PEP 727 – Documentation in Annotated Metadata + from typing_extensions import Doc # type: ignore[attr-defined] +except ImportError: + + @dataclass(frozen=True, slots=True) + class Doc: # type: ignore [no-redef] + """ " + The return value of doc(), mainly to be used by tools that want to extract the + Annotated documentation at runtime. + """ + + documentation: str + """The documentation string passed to doc().""" + + +DocInfo = Doc # backwards compatibility +doc = Doc diff --git a/.venv-test/lib/python3.12/site-packages/annotated_types/py.typed b/.venv-test/lib/python3.12/site-packages/annotated_types/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/annotated_types/test_cases.py b/.venv-test/lib/python3.12/site-packages/annotated_types/test_cases.py new file mode 100644 index 0000000..fba3c25 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/annotated_types/test_cases.py @@ -0,0 +1,146 @@ +import math +from collections.abc import Iterable, Iterator +from datetime import date, datetime, timedelta, timezone +from decimal import Decimal +from typing import Annotated, Any, NamedTuple + +import annotated_types as at + + +class Case(NamedTuple): + """ + A test case for `annotated_types`. + """ + + annotation: Any + valid_cases: Iterable[Any] + invalid_cases: Iterable[Any] + + +def cases() -> Iterable[Case]: + # Gt, Ge, Lt, Le + yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1)) + yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1)) + yield Case( + Annotated[datetime, at.Gt(datetime(2000, 1, 1))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(2000, 1, 1), datetime(1999, 12, 31)], + ) + yield Case( + Annotated[datetime, at.Gt(date(2000, 1, 1))], + [date(2000, 1, 2), date(2000, 1, 3)], + [date(2000, 1, 1), date(1999, 12, 31)], + ) + yield Case( + Annotated[datetime, at.Gt(Decimal('1.123'))], + [Decimal('1.1231'), Decimal('123')], + [Decimal('1.123'), Decimal('0')], + ) + + yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1)) + yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1)) + yield Case( + Annotated[datetime, at.Ge(datetime(2000, 1, 1))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(1998, 1, 1), datetime(1999, 12, 31)], + ) + + yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4)) + yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9)) + yield Case( + Annotated[datetime, at.Lt(datetime(2000, 1, 1))], + [datetime(1999, 12, 31), datetime(1999, 12, 31)], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + ) + + yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000)) + yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9)) + yield Case( + Annotated[datetime, at.Le(datetime(2000, 1, 1))], + [datetime(2000, 1, 1), datetime(1999, 12, 31)], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + ) + + # Interval + yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1)) + yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1)) + yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1)) + yield Case( + Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(2000, 1, 1), datetime(2000, 1, 4)], + ) + + yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4)) + yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1)) + + # lengths + + yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) + yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) + yield Case(Annotated[list[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) + yield Case(Annotated[list[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) + + yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10)) + yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10)) + yield Case(Annotated[list[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) + yield Case(Annotated[list[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) + + yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10)) + yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234')) + + yield Case(Annotated[dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}]) + yield Case(Annotated[set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4})) + yield Case(Annotated[tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4))) + + # Timezone + + yield Case( + Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)] + ) + yield Case( + Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)] + ) + yield Case( + Annotated[datetime, at.Timezone(timezone.utc)], + [datetime(2000, 1, 1, tzinfo=timezone.utc)], + [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], + ) + yield Case( + Annotated[datetime, at.Timezone('Europe/London')], + [datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))], + [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], + ) + + # Quantity + + yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m')) + + # predicate types + + yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom']) + yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC']) + yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2']) + yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀']) + + yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5]) + + yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf]) + yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23]) + yield Case(at.IsNan[float], [math.nan], [1.23, math.inf]) + yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan]) + yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23]) + yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf]) + + # check stacked predicates + yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan]) + + # doc + yield Case(Annotated[int, at.doc("A number")], [1, 2], []) + + # custom GroupedMetadata + class MyCustomGroupedMetadata(at.GroupedMetadata): + def __iter__(self) -> Iterator[at.Predicate]: + yield at.Predicate(lambda x: float(x).is_integer()) + + yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5]) diff --git a/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/INSTALLER b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/METADATA b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/METADATA new file mode 100644 index 0000000..13734ec --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/METADATA @@ -0,0 +1,108 @@ +Metadata-Version: 2.4 +Name: anyio +Version: 4.15.1 +Summary: High-level concurrency and networking framework on top of asyncio or Trio +Author-email: Alex Grönholm +License-Expression: MIT +Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/ +Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html +Project-URL: Source code, https://github.com/agronholm/anyio +Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Framework :: AnyIO +Classifier: Typing :: Typed +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta +Requires-Python: >=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11" +Requires-Dist: idna>=2.8 +Requires-Dist: typing_extensions>=4.16.0; python_version < "3.15" +Provides-Extra: trio +Requires-Dist: trio>=0.32.0; extra == "trio" +Dynamic: license-file + +.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg + :target: https://github.com/agronholm/anyio/actions/workflows/test.yml + :alt: Build Status +.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master + :target: https://coveralls.io/github/agronholm/anyio?branch=master + :alt: Code Coverage +.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest + :target: https://anyio.readthedocs.io/en/latest/?badge=latest + :alt: Documentation +.. image:: https://badges.gitter.im/gitterHQ/gitter.svg + :target: https://gitter.im/python-trio/AnyIO + :alt: Gitter chat +.. image:: https://tidelift.com/badges/package/pypi/anyio + :target: https://tidelift.com/subscription/pkg/pypi-anyio + :alt: Tidelift + +AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or +Trio_. It implements Trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony +with the native SC of Trio itself. + +Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or +Trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full +refactoring necessary. It will blend in with the native libraries of your chosen backend. + +To find out why you might want to use AnyIO's APIs instead of asyncio's, you can read about it +`here `_. + +Documentation +------------- + +View full documentation at: https://anyio.readthedocs.io/ + +Features +-------- + +AnyIO offers the following functionality: + +* Task groups (nurseries_ in trio terminology) +* High-level networking (TCP, UDP and UNIX sockets) + + * `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python + 3.8) + * async/await style UDP sockets (unlike asyncio where you still have to use Transports and + Protocols) + +* A versatile API for byte streams and object streams +* Inter-task synchronization and communication (locks, conditions, events, semaphores, object + streams and futures) +* Worker threads +* Subprocesses +* Subinterpreter support for code parallelization (on Python 3.13 and later) +* Asynchronous file I/O (using worker threads) +* Signal handling +* Asynchronous versions of the functools_ and itertools_ modules + +AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures. +It even works with the popular Hypothesis_ library. + +.. _asyncio: https://docs.python.org/3/library/asyncio.html +.. _Trio: https://github.com/python-trio/trio +.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency +.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning +.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs +.. _pytest: https://docs.pytest.org/en/latest/ +.. _functools: https://docs.python.org/3/library/functools.html +.. _itertools: https://docs.python.org/3/library/itertools.html +.. _Hypothesis: https://hypothesis.works/ + +Security contact information +---------------------------- + +To report a security vulnerability, please use the `Tidelift security contact`_. +Tidelift will coordinate the fix and disclosure. + +.. _Tidelift security contact: https://tidelift.com/security diff --git a/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/RECORD b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/RECORD new file mode 100644 index 0000000..3946aab --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/RECORD @@ -0,0 +1,100 @@ +anyio-4.15.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +anyio-4.15.1.dist-info/METADATA,sha256=BDrstXGvcYnopsUo9djjJUXxss36r3YdUebVtn6jyoE,4733 +anyio-4.15.1.dist-info/RECORD,, +anyio-4.15.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91 +anyio-4.15.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39 +anyio-4.15.1.dist-info/licenses/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081 +anyio-4.15.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6 +anyio/__init__.py,sha256=OfroYRjIUw_JFj0Zwh1JAaajMQg4VqtoAt0XoOHDRwE,7602 +anyio/__pycache__/__init__.cpython-312.pyc,, +anyio/__pycache__/_lazyimport.cpython-312.pyc,, +anyio/__pycache__/from_thread.cpython-312.pyc,, +anyio/__pycache__/functools.cpython-312.pyc,, +anyio/__pycache__/itertools.cpython-312.pyc,, +anyio/__pycache__/lowlevel.cpython-312.pyc,, +anyio/__pycache__/pytest_plugin.cpython-312.pyc,, +anyio/__pycache__/to_interpreter.cpython-312.pyc,, +anyio/__pycache__/to_process.cpython-312.pyc,, +anyio/__pycache__/to_thread.cpython-312.pyc,, +anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/_backends/__pycache__/__init__.cpython-312.pyc,, +anyio/_backends/__pycache__/_asyncio.cpython-312.pyc,, +anyio/_backends/__pycache__/_trio.cpython-312.pyc,, +anyio/_backends/_asyncio.py,sha256=Vqy7O1ymmEpxTiPzehSuBCVTziGALNV-BbsIRaKXsg4,107767 +anyio/_backends/_trio.py,sha256=Gs8XzX0IeBvje8IEZtJmpNylKCUAChKv7I_YZi3wlmg,45715 +anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/_core/__pycache__/__init__.cpython-312.pyc,, +anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc,, +anyio/_core/__pycache__/_concurrency_utils.cpython-312.pyc,, +anyio/_core/__pycache__/_contextmanagers.cpython-312.pyc,, +anyio/_core/__pycache__/_eventloop.cpython-312.pyc,, +anyio/_core/__pycache__/_exceptions.cpython-312.pyc,, +anyio/_core/__pycache__/_fileio.cpython-312.pyc,, +anyio/_core/__pycache__/_futures.cpython-312.pyc,, +anyio/_core/__pycache__/_resources.cpython-312.pyc,, +anyio/_core/__pycache__/_signals.cpython-312.pyc,, +anyio/_core/__pycache__/_sockets.cpython-312.pyc,, +anyio/_core/__pycache__/_streams.cpython-312.pyc,, +anyio/_core/__pycache__/_subprocesses.cpython-312.pyc,, +anyio/_core/__pycache__/_synchronization.cpython-312.pyc,, +anyio/_core/__pycache__/_tasks.cpython-312.pyc,, +anyio/_core/__pycache__/_tempfile.cpython-312.pyc,, +anyio/_core/__pycache__/_testing.cpython-312.pyc,, +anyio/_core/__pycache__/_typedattr.cpython-312.pyc,, +anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626 +anyio/_core/_concurrency_utils.py,sha256=kvRXMrnDqgQTBkZc-f279-lBnZM4YbSToXqhnAUCfSc,4522 +anyio/_core/_contextmanagers.py,sha256=YInBCabiEeS-UaP_Jdxa1CaFC71ETPW8HZTHIM8Rsc8,7215 +anyio/_core/_eventloop.py,sha256=ByZUeJD9alMfcyTseRo5IzTO0IltEul_Gyq9iqSjqDk,6658 +anyio/_core/_exceptions.py,sha256=B4ZhLkBQT4tI6vShv4-GJui492IJsoIDTw1FQJEkmAI,5618 +anyio/_core/_fileio.py,sha256=q--ft9h5Y_FRLzhAnoYrXf3tOuZ8ZyIN2A9mCfMbZRc,34163 +anyio/_core/_futures.py,sha256=YQq5uhnCnKQMQD62tOLfzEm7dUXY3GQorJdH6ghfgIQ,6177 +anyio/_core/_resources.py,sha256=Ld5duCe9NwgPfI7EIOh3CMIBYQ7rq6ZQEa7CFS0u9Ec,450 +anyio/_core/_signals.py,sha256=mjTBB2hTKNPRlU0IhnijeQedpWOGERDiMjSlJQsFrug,1016 +anyio/_core/_sockets.py,sha256=C8b-3w5H9n0Fo4g7X8WJvk0m6CGh6NVAAFtieBGHK3g,35289 +anyio/_core/_streams.py,sha256=FczFwIgDpnkK0bODWJXMpsUJYdvAD04kaUaGzJU8DK0,1806 +anyio/_core/_subprocesses.py,sha256=TMBuGNILz-47w5t9vayJ1GpCpDT6LxsMohJDFCewIRc,7938 +anyio/_core/_synchronization.py,sha256=ggsd74QPd846faA2HQeT07ONQ5HQ1S78gy8mvB3qCrw,21658 +anyio/_core/_tasks.py,sha256=J2dhM_Jq3Grsfg_enNWpRz2SxfM15uLt-pEfvoQq3uA,15391 +anyio/_core/_tempfile.py,sha256=IgoBCFaPhrFvsF9KHcCcrnCRn-UqMfw0WRdSDWgxAiI,19726 +anyio/_core/_testing.py,sha256=ZRucLh5kqhiFWAJMMbd8yZ6uIvz0rq3WwL72ldosxAU,2340 +anyio/_core/_typedattr.py,sha256=gt9Dzn-IL0QW14aHPDD8PAtq95FYKNNupHyf37xZGPQ,2607 +anyio/_lazyimport.py,sha256=LCjnYanIfYE9G-Dv_EsqNLDuVRppNicWZjCTfS9-sJU,6082 +anyio/abc/__init__.py,sha256=XIVvCI2c6oyaOcqXe94k2rzU5_s7-ZNnlxGTk34H4T0,3050 +anyio/abc/__pycache__/__init__.cpython-312.pyc,, +anyio/abc/__pycache__/_eventloop.cpython-312.pyc,, +anyio/abc/__pycache__/_resources.cpython-312.pyc,, +anyio/abc/__pycache__/_sockets.cpython-312.pyc,, +anyio/abc/__pycache__/_streams.cpython-312.pyc,, +anyio/abc/__pycache__/_subprocesses.cpython-312.pyc,, +anyio/abc/__pycache__/_tasks.cpython-312.pyc,, +anyio/abc/__pycache__/_testing.cpython-312.pyc,, +anyio/abc/_eventloop.py,sha256=0-UwgB351rkocCt8m3fjTmcNx2Fxn52nUoujG5DzMvU,11127 +anyio/abc/_resources.py,sha256=hrb0LXUgHSp3i8G6W8JJchdzKXgg1m0T-L7udXmv2y0,900 +anyio/abc/_sockets.py,sha256=jQWnPX7iDIDyvyvVkbuFhjezw4oI2WggzbW6Vl2-eqs,13162 +anyio/abc/_streams.py,sha256=IMeOPPutFXRbyeAutxV1HJe7FUQZYp2qKrJJpP1DCv0,7620 +anyio/abc/_subprocesses.py,sha256=CDqU1p0HuBS0pXUzBW_hbfhihTJRM67n__7qZmoPU84,2123 +anyio/abc/_tasks.py,sha256=HBPpT72aOy0bwMAxBCKmnXsR328wCYRjYm3iaVw2fQo,7070 +anyio/abc/_testing.py,sha256=A9Y8RAClIPn1A0aNHBN8YoaRy_MKOpbFGEbTHAXgB6U,2145 +anyio/from_thread.py,sha256=iZZbfcNySi4wAcx1_cxAQE4J0YZC88qRMIYqQ6SF9sc,19415 +anyio/functools.py,sha256=T4JS8IXq-x1S0Lbo2owF8l9fza2KypO147QLeyz4cjs,11797 +anyio/itertools.py,sha256=TS7_UCa2Y3cggtaLlWHbjOyf6QhPfQUdPduNW-ddXTE,16255 +anyio/lowlevel.py,sha256=QyWkvfetbCH0mHkqv1yxGwOaiZ7fyxhpZkHWOXPV5JU,6280 +anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/pytest_plugin.py,sha256=oLXyOstvRZW3VVbqIODhZsPt4jSz2UGh44YeG2nJXx0,14516 +anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/streams/__pycache__/__init__.cpython-312.pyc,, +anyio/streams/__pycache__/buffered.cpython-312.pyc,, +anyio/streams/__pycache__/file.cpython-312.pyc,, +anyio/streams/__pycache__/memory.cpython-312.pyc,, +anyio/streams/__pycache__/stapled.cpython-312.pyc,, +anyio/streams/__pycache__/text.cpython-312.pyc,, +anyio/streams/__pycache__/tls.cpython-312.pyc,, +anyio/streams/buffered.py,sha256=u7hCD8SNrYHcutG6K5wEiy1F88-pizgjvEFM22Kq2Cw,6746 +anyio/streams/file.py,sha256=6jujI2m-QJITqqKFamrupX_DNsU7y2Fz3omLZxOLuY0,4524 +anyio/streams/memory.py,sha256=rpxNzDMag9PCOynBqN64KyuAfAPB41vFyRP-Npo-lE0,10793 +anyio/streams/stapled.py,sha256=UJ2Rgt0NUmM1VovRNOoZoic960lcbDjb3vhfZg3b_Z0,4845 +anyio/streams/text.py,sha256=BcVAGJw1VRvtIqnv-o0Rb0pwH7p8vwlvl21xHq522ag,5765 +anyio/streams/tls.py,sha256=fqLmxvULFO9rvxGyJ33VPrOIN7E3eiU1q90BkmKigQ8,15818 +anyio/to_interpreter.py,sha256=ATnoT6kei9wDmQZtXTA3SsY4CK7XKr8Ta4_x5i6pbTw,7100 +anyio/to_process.py,sha256=p6n8j0nreJUl1DEkR_9PL6KRoKtNqFxORTn8qZBvn94,9850 +anyio/to_thread.py,sha256=8MPVrrQq8ttGvV21cSd68rMto5bhxJ0-i9CupXag5Gs,2750 diff --git a/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/WHEEL b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/WHEEL new file mode 100644 index 0000000..13013d7 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (84.0.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/entry_points.txt b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/entry_points.txt new file mode 100644 index 0000000..44dd9bd --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[pytest11] +anyio = anyio.pytest_plugin diff --git a/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/licenses/LICENSE b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/licenses/LICENSE new file mode 100644 index 0000000..104eebf --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2018 Alex Grönholm + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/top_level.txt b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/top_level.txt new file mode 100644 index 0000000..c77c069 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio-4.15.1.dist-info/top_level.txt @@ -0,0 +1 @@ +anyio diff --git a/.venv-test/lib/python3.12/site-packages/anyio/__init__.py b/.venv-test/lib/python3.12/site-packages/anyio/__init__.py new file mode 100644 index 0000000..30a3a16 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/__init__.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ._lazyimport import ( + fix_package_names, + install_lazy_importer, + set_deprecated_aliases, +) + +if TYPE_CHECKING or not install_lazy_importer(): + from ._core._concurrency_utils import amap as amap + from ._core._concurrency_utils import as_completed as as_completed + from ._core._concurrency_utils import gather as gather + from ._core._contextmanagers import ( + AsyncContextManagerMixin as AsyncContextManagerMixin, + ) + from ._core._contextmanagers import ContextManagerMixin as ContextManagerMixin + from ._core._eventloop import current_time as current_time + from ._core._eventloop import get_all_backends as get_all_backends + from ._core._eventloop import get_available_backends as get_available_backends + from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class + from ._core._eventloop import run as run + from ._core._eventloop import sleep as sleep + from ._core._eventloop import sleep_forever as sleep_forever + from ._core._eventloop import sleep_until as sleep_until + from ._core._exceptions import BrokenResourceError as BrokenResourceError + from ._core._exceptions import BrokenWorkerInterpreter as BrokenWorkerInterpreter + from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess + from ._core._exceptions import BusyResourceError as BusyResourceError + from ._core._exceptions import ClosedResourceError as ClosedResourceError + from ._core._exceptions import ConnectionFailed as ConnectionFailed + from ._core._exceptions import DelimiterNotFound as DelimiterNotFound + from ._core._exceptions import EndOfStream as EndOfStream + from ._core._exceptions import FutureAlreadyFinished as FutureAlreadyFinished + from ._core._exceptions import FutureCancelled as FutureCancelled + from ._core._exceptions import FutureFailed as FutureFailed + from ._core._exceptions import FutureNotFinished as FutureNotFinished + from ._core._exceptions import IncompleteRead as IncompleteRead + from ._core._exceptions import NoEventLoopError as NoEventLoopError + from ._core._exceptions import RunFinishedError as RunFinishedError + from ._core._exceptions import TaskCancelled as TaskCancelled + from ._core._exceptions import TaskFailed as TaskFailed + from ._core._exceptions import TaskNotFinished as TaskNotFinished + from ._core._exceptions import ( + TypedAttributeLookupError as TypedAttributeLookupError, + ) + from ._core._exceptions import WouldBlock as WouldBlock + from ._core._fileio import AsyncFile as AsyncFile + from ._core._fileio import Path as Path + from ._core._fileio import open_file as open_file + from ._core._fileio import wrap_file as wrap_file + from ._core._futures import Future as Future + from ._core._resources import aclose_forcefully as aclose_forcefully + from ._core._signals import open_signal_receiver as open_signal_receiver + from ._core._sockets import TCPConnectable as TCPConnectable + from ._core._sockets import UNIXConnectable as UNIXConnectable + from ._core._sockets import as_connectable as as_connectable + from ._core._sockets import connect_tcp as connect_tcp + from ._core._sockets import connect_unix as connect_unix + from ._core._sockets import ( + create_connected_udp_socket as create_connected_udp_socket, + ) + from ._core._sockets import ( + create_connected_unix_datagram_socket as create_connected_unix_datagram_socket, + ) + from ._core._sockets import create_tcp_listener as create_tcp_listener + from ._core._sockets import create_udp_socket as create_udp_socket + from ._core._sockets import ( + create_unix_datagram_socket as create_unix_datagram_socket, + ) + from ._core._sockets import create_unix_listener as create_unix_listener + from ._core._sockets import getaddrinfo as getaddrinfo + from ._core._sockets import getnameinfo as getnameinfo + from ._core._sockets import notify_closing as notify_closing + from ._core._sockets import wait_readable as wait_readable + from ._core._sockets import wait_socket_readable as wait_socket_readable + from ._core._sockets import wait_socket_writable as wait_socket_writable + from ._core._sockets import wait_writable as wait_writable + from ._core._streams import ( + create_memory_object_stream as create_memory_object_stream, + ) + from ._core._subprocesses import open_process as open_process + from ._core._subprocesses import run_process as run_process + from ._core._synchronization import CapacityLimiter as CapacityLimiter + from ._core._synchronization import ( + CapacityLimiterStatistics as CapacityLimiterStatistics, + ) + from ._core._synchronization import Condition as Condition + from ._core._synchronization import ConditionStatistics as ConditionStatistics + from ._core._synchronization import Event as Event + from ._core._synchronization import EventStatistics as EventStatistics + from ._core._synchronization import Lock as Lock + from ._core._synchronization import LockStatistics as LockStatistics + from ._core._synchronization import ResourceGuard as ResourceGuard + from ._core._synchronization import Semaphore as Semaphore + from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics + from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED + from ._core._tasks import CancelScope as CancelScope + from ._core._tasks import TaskHandle as TaskHandle + from ._core._tasks import create_task_group as create_task_group + from ._core._tasks import current_effective_deadline as current_effective_deadline + from ._core._tasks import fail_after as fail_after + from ._core._tasks import fail_at as fail_at + from ._core._tasks import move_on_after as move_on_after + from ._core._tasks import move_on_at as move_on_at + from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile + from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile + from ._core._tempfile import TemporaryDirectory as TemporaryDirectory + from ._core._tempfile import TemporaryFile as TemporaryFile + from ._core._tempfile import gettempdir as gettempdir + from ._core._tempfile import gettempdirb as gettempdirb + from ._core._tempfile import mkdtemp as mkdtemp + from ._core._tempfile import mkstemp as mkstemp + from ._core._testing import TaskInfo as TaskInfo + from ._core._testing import get_current_task as get_current_task + from ._core._testing import get_running_tasks as get_running_tasks + from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked + from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider + from ._core._typedattr import TypedAttributeSet as TypedAttributeSet + from ._core._typedattr import typed_attribute as typed_attribute + + # ruff: isort: off + from . import ( + abc as abc, + ) + from . import ( + from_thread as from_thread, + ) + from . import ( + functools as functools, + ) + from . import ( + itertools as itertools, + ) + from . import ( + lowlevel as lowlevel, + ) + from . import ( + to_interpreter as to_interpreter, + ) + from . import ( + to_process as to_process, + ) + from . import ( + to_thread as to_thread, + ) + + fix_package_names() + set_deprecated_aliases( + { + "BrokenWorkerIntepreter": "anyio.BrokenWorkerInterpreter", + } + ) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_backends/__init__.py b/.venv-test/lib/python3.12/site-packages/anyio/_backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_backends/_asyncio.py b/.venv-test/lib/python3.12/site-packages/anyio/_backends/_asyncio.py new file mode 100644 index 0000000..7eba257 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_backends/_asyncio.py @@ -0,0 +1,3201 @@ +from __future__ import annotations + +import array +import asyncio +import concurrent.futures +import math +import os +import socket +import sys +import threading +import weakref +from asyncio import ( + AbstractEventLoop, + CancelledError, + all_tasks, + create_task, + current_task, + get_running_loop, + sleep, +) +from asyncio.base_events import _run_until_complete_cb # type: ignore[attr-defined] +from collections import OrderedDict, deque +from collections.abc import ( + AsyncGenerator, + AsyncIterator, + Awaitable, + Callable, + Collection, + Coroutine, + Iterable, + Sequence, +) +from concurrent.futures import Future +from contextlib import AbstractContextManager, suppress +from contextvars import Context, copy_context +from dataclasses import dataclass, field +from functools import partial, wraps +from inspect import ( + CORO_RUNNING, + CORO_SUSPENDED, + getcoroutinestate, +) +from io import IOBase +from os import PathLike +from queue import Queue +from signal import Signals +from socket import AddressFamily, SocketKind +from threading import Thread +from types import CodeType, TracebackType +from typing import ( + IO, + TYPE_CHECKING, + Any, + Literal, + ParamSpec, + TypeVar, + cast, +) +from weakref import WeakKeyDictionary + +from .. import ( + CapacityLimiterStatistics, + EventStatistics, + LockStatistics, + TaskInfo, + abc, +) +from .._core._eventloop import ( + claim_worker_thread, + set_current_async_library, + threadlocals, +) +from .._core._exceptions import ( + BrokenResourceError, + BusyResourceError, + ClosedResourceError, + EndOfStream, + RunFinishedError, + WouldBlock, +) +from .._core._sockets import convert_ipv6_sockaddr +from .._core._streams import create_memory_object_stream +from .._core._synchronization import ( + CapacityLimiter as BaseCapacityLimiter, +) +from .._core._synchronization import Event as BaseEvent +from .._core._synchronization import Lock as BaseLock +from .._core._synchronization import ( + ResourceGuard, + SemaphoreStatistics, +) +from .._core._synchronization import Semaphore as BaseSemaphore +from .._core._tasks import CancelScope as BaseCancelScope +from .._core._tasks import TaskHandle +from ..abc import ( + AsyncBackend, + IPSockAddrType, + SocketListener, + UDPPacketType, + UNIXDatagramPacketType, +) +from ..abc._tasks import call_for_coroutine, get_callable_name, get_coro_name +from ..lowlevel import RunVar, _run_vars + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + + from ..abc._eventloop import StrOrBytesPath + from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +else: + FileDescriptorLike = object + +if sys.version_info >= (3, 11): + from asyncio import Runner + from typing import Self, TypeVarTuple, Unpack +else: + import contextvars + import enum + import signal + from asyncio import coroutines, events, exceptions, tasks + + from exceptiongroup import BaseExceptionGroup + from typing_extensions import Self, TypeVarTuple, Unpack + + class _State(enum.Enum): + CREATED = "created" + INITIALIZED = "initialized" + CLOSED = "closed" + + class Runner: + # Copied from CPython 3.11 + def __init__( + self, + *, + debug: bool | None = None, + loop_factory: Callable[[], AbstractEventLoop] | None = None, + ): + self._state = _State.CREATED + self._debug = debug + self._loop_factory = loop_factory + self._loop: AbstractEventLoop | None = None + self._context = None + self._interrupt_count = 0 + self._set_event_loop = False + + def __enter__(self) -> Self: + self._lazy_init() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """Shutdown and close event loop.""" + loop = self._loop + if self._state is not _State.INITIALIZED or loop is None: + return + try: + _cancel_all_tasks(loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + if hasattr(loop, "shutdown_default_executor"): + loop.run_until_complete(loop.shutdown_default_executor()) + else: + loop.run_until_complete(_shutdown_default_executor(loop)) + finally: + if self._set_event_loop: + events.set_event_loop(None) + loop.close() + self._loop = None + self._state = _State.CLOSED + + def get_loop(self) -> AbstractEventLoop: + """Return embedded event loop.""" + self._lazy_init() + return self._loop + + def run(self, coro: Coroutine[T_Retval], *, context=None) -> T_Retval: + """Run a coroutine inside the embedded event loop.""" + if not coroutines.iscoroutine(coro): + raise ValueError(f"a coroutine was expected, got {coro!r}") + + if events._get_running_loop() is not None: + # fail fast with short traceback + raise RuntimeError( + "Runner.run() cannot be called from a running event loop" + ) + + self._lazy_init() + + if context is None: + context = self._context + task = context.run(self._loop.create_task, coro) + + if ( + threading.current_thread() is threading.main_thread() + and signal.getsignal(signal.SIGINT) is signal.default_int_handler + ): + sigint_handler = partial(self._on_sigint, main_task=task) + try: + signal.signal(signal.SIGINT, sigint_handler) + except ValueError: + # `signal.signal` may throw if `threading.main_thread` does + # not support signals (e.g. embedded interpreter with signals + # not registered - see gh-91880) + sigint_handler = None + else: + sigint_handler = None + + self._interrupt_count = 0 + try: + return self._loop.run_until_complete(task) + except exceptions.CancelledError: + if self._interrupt_count > 0: + uncancel = getattr(task, "uncancel", None) + if uncancel is not None and uncancel() == 0: + raise KeyboardInterrupt # noqa: B904 + raise # CancelledError + finally: + if ( + sigint_handler is not None + and signal.getsignal(signal.SIGINT) is sigint_handler + ): + signal.signal(signal.SIGINT, signal.default_int_handler) + + def _lazy_init(self) -> None: + if self._state is _State.CLOSED: + raise RuntimeError("Runner is closed") + if self._state is _State.INITIALIZED: + return + if self._loop_factory is None: + self._loop = events.new_event_loop() + if not self._set_event_loop: + # Call set_event_loop only once to avoid calling + # attach_loop multiple times on child watchers + events.set_event_loop(self._loop) + self._set_event_loop = True + else: + self._loop = self._loop_factory() + if self._debug is not None: + self._loop.set_debug(self._debug) + self._context = contextvars.copy_context() + self._state = _State.INITIALIZED + + def _on_sigint(self, signum, frame, main_task: asyncio.Task) -> None: + self._interrupt_count += 1 + if self._interrupt_count == 1 and not main_task.done(): + main_task.cancel() + # wakeup loop if it is blocked by select() with long timeout + self._loop.call_soon_threadsafe(lambda: None) + return + raise KeyboardInterrupt() + + def _cancel_all_tasks(loop: AbstractEventLoop) -> None: + to_cancel = tasks.all_tasks(loop) + if not to_cancel: + return + + for task in to_cancel: + task.cancel() + + loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True)) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "unhandled exception during asyncio.run() shutdown", + "exception": task.exception(), + "task": task, + } + ) + + async def _shutdown_default_executor(loop: AbstractEventLoop) -> None: + """Schedule the shutdown of the default executor.""" + + def _do_shutdown(future: asyncio.futures.Future) -> None: + try: + loop._default_executor.shutdown(wait=True) # type: ignore[attr-defined] + loop.call_soon_threadsafe(future.set_result, None) + except Exception as ex: + loop.call_soon_threadsafe(future.set_exception, ex) + + loop._executor_shutdown_called = True + if loop._default_executor is None: + return + future = loop.create_future() + thread = threading.Thread(target=_do_shutdown, args=(future,)) + thread.start() + try: + await future + finally: + thread.join() + + +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) +PosArgsT = TypeVarTuple("PosArgsT") +P = ParamSpec("P") + +_root_task: RunVar[asyncio.Task[Any] | None] = RunVar("_root_task") + + +def find_root_task() -> asyncio.Task: + root_task = _root_task.get(None) + if root_task is not None and not root_task.done(): + return root_task + + # Look for a task that has been started via run_until_complete() + for task in all_tasks(): + if task._callbacks and not task.done(): + for cb, context in task._callbacks: + if ( + cb is _run_until_complete_cb + or getattr(cb, "__module__", None) == "uvloop.loop" + ): + _root_task.set(task) + + def _unset(t: asyncio.Task[Any]) -> None: + if vars := _run_vars.get(t.get_loop()): + vars.pop(_root_task, None) + + # Register a callback to break the task -> loop -> _run_var[loop][_root_task] -> task cycle + # Also run it in its own context to not create another reference. + # We can't use RunVar.reset() here since these are called synchronously + # and thus lowlevel.current_token() (which RunVar.reset() depends on) fails. + task.add_done_callback(_unset, context=context) + return task + + # Look up the topmost task in the AnyIO task tree, if possible + task = cast(asyncio.Task, current_task()) + state = _task_states.get(task) + if state: + cancel_scope = state.cancel_scope + while cancel_scope and cancel_scope._parent_scope is not None: + cancel_scope = cancel_scope._parent_scope + + if cancel_scope is not None: + return cast(asyncio.Task, cancel_scope._host_task) + + return task + + +def _task_started(task: asyncio.Task) -> bool: + """Return ``True`` if the task has been started and has not finished.""" + # The task coro should never be None here, as we never add finished tasks to the + # task list + coro = task.get_coro() + assert coro is not None + return getcoroutinestate(coro) in (CORO_RUNNING, CORO_SUSPENDED) + + +# +# Timeouts and cancellation +# + + +def is_anyio_cancellation(exc: CancelledError) -> bool: + # Sometimes third party frameworks catch a CancelledError and raise a new one, so as + # a workaround we have to look at the previous ones in __context__ too for a + # matching cancel message + while True: + if ( + exc.args + and isinstance(exc.args[0], str) + and exc.args[0].startswith("Cancelled via cancel scope ") + ): + return True + + if isinstance(exc.__context__, CancelledError): + exc = exc.__context__ + continue + + return False + + +class CancelScope(BaseCancelScope): + __slots__ = ( + "_active", + "_cancel_called", + "_cancel_handle", + "_cancel_reason", + "_cancelled_caught", + "_child_scopes", + "_deadline", + "_host_task", + "_parent_scope", + "_pending_uncancellations", + "_shield", + "_tasks", + "_timeout_handle", + ) + + def __new__(cls, *, deadline: float = math.inf, shield: bool = False) -> Self: + return object.__new__(cls) + + def __init__(self, deadline: float = math.inf, shield: bool = False): + self._deadline = deadline + self._shield = shield + self._parent_scope: CancelScope | None = None + self._child_scopes: set[CancelScope] = set() + self._cancel_called = False + self._cancel_reason: str | None = None + self._cancelled_caught = False + self._active = False + self._timeout_handle: asyncio.TimerHandle | None = None + self._cancel_handle: asyncio.Handle | None = None + self._tasks: set[asyncio.Task] = set() + self._host_task: asyncio.Task | None = None + if sys.version_info >= (3, 11): + self._pending_uncancellations: int | None = 0 + else: + self._pending_uncancellations = None + + def __enter__(self) -> Self: + if self._active: + raise RuntimeError( + "Each CancelScope may only be used for a single 'with' block" + ) + + self._host_task = host_task = cast(asyncio.Task, current_task()) + self._tasks.add(host_task) + try: + task_state = _task_states[host_task] + except KeyError: + task_state = TaskState(None, self) + _task_states[host_task] = task_state + else: + self._parent_scope = task_state.cancel_scope + task_state.cancel_scope = self + if self._parent_scope is not None: + # If using an eager task factory, the parent scope may not even contain + # the host task + self._parent_scope._child_scopes.add(self) + self._parent_scope._tasks.discard(host_task) + + self._timeout() + self._active = True + + # Start cancelling the host task if the scope was cancelled before entering + if self._cancel_called: + self._deliver_cancellation(self) + + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + del exc_tb + + if not self._active: + raise RuntimeError("This cancel scope is not active") + if current_task() is not self._host_task: + raise RuntimeError( + "Attempted to exit cancel scope in a different task than it was " + "entered in" + ) + + assert self._host_task is not None + host_task_state = _task_states.get(self._host_task) + if host_task_state is None or host_task_state.cancel_scope is not self: + raise RuntimeError( + "Attempted to exit a cancel scope that isn't the current tasks's " + "current cancel scope" + ) + + try: + self._active = False + if self._timeout_handle: + self._timeout_handle.cancel() + self._timeout_handle = None + + self._tasks.remove(self._host_task) + if self._parent_scope is not None: + self._parent_scope._child_scopes.remove(self) + self._parent_scope._tasks.add(self._host_task) + + host_task_state.cancel_scope = self._parent_scope + + # Restart the cancellation effort in the closest visible, cancelled parent + # scope if necessary + self._restart_cancellation_in_parent() + + # We only swallow the exception iff it was an AnyIO CancelledError, either + # directly as exc_val or inside an exception group and there are no cancelled + # parent cancel scopes visible to us here + if self._cancel_called and not self._parent_cancellation_is_visible_to_us: + # For each level-cancel() call made on the host task, call uncancel() + while self._pending_uncancellations: + self._host_task.uncancel() + self._pending_uncancellations -= 1 + + # Update cancelled_caught and check for exceptions we must not swallow + if isinstance(exc_val, BaseExceptionGroup): + cancelleds_caught, remaining = exc_val.split( + lambda exc: ( + isinstance(exc, CancelledError) + and is_anyio_cancellation(exc) + ) + ) + + if cancelleds_caught is None: + return False + + self._cancelled_caught = True + + if remaining is None: + return True + + context = remaining.__context__ + try: + # Preserve __cause__ and __suppress_context__ by avoiding `raise + # ... from ...` + raise remaining + finally: + # Preserve __context__ + remaining.__context__ = context + del context + else: + if isinstance(exc_val, CancelledError) and is_anyio_cancellation( + exc_val + ): + self._cancelled_caught = True + return True + else: + return False + else: + if self._pending_uncancellations: + assert self._parent_scope is not None + assert self._parent_scope._pending_uncancellations is not None + self._parent_scope._pending_uncancellations += ( + self._pending_uncancellations + ) + self._pending_uncancellations = 0 + + return False + finally: + self._host_task = None + del exc_val + + @property + def _effectively_cancelled(self) -> bool: + cancel_scope: CancelScope | None = self + while cancel_scope is not None: + if cancel_scope._cancel_called: + return True + + if cancel_scope.shield: + return False + + cancel_scope = cancel_scope._parent_scope + + return False + + @property + def _parent_cancellation_is_visible_to_us(self) -> bool: + return ( + self._parent_scope is not None + and not self.shield + and self._parent_scope._effectively_cancelled + ) + + def _timeout(self) -> None: + if self._deadline != math.inf: + loop = get_running_loop() + if loop.time() >= self._deadline: + self.cancel("deadline exceeded") + else: + self._timeout_handle = loop.call_at(self._deadline, self._timeout) + + def _deliver_cancellation(self, origin: CancelScope) -> bool: + """ + Deliver cancellation to directly contained tasks and nested cancel scopes. + + Schedule another run at the end if we still have tasks eligible for + cancellation. + + :param origin: the cancel scope that originated the cancellation + :return: ``True`` if the delivery needs to be retried on the next cycle + + """ + should_retry = False + current = current_task() + for task in self._tasks: + # Always skip tasks that are already done (see issue #1111) + if task.done(): + continue + + should_retry = True + if task._must_cancel: # type: ignore[attr-defined] + continue + + # The task is eligible for cancellation if it has started + if task is not current and (task is self._host_task or _task_started(task)): + waiter = task._fut_waiter # type: ignore[attr-defined] + if not isinstance(waiter, asyncio.Future) or not waiter.done(): + task.cancel(origin._cancel_reason) + if ( + task is origin._host_task + and origin._pending_uncancellations is not None + ): + origin._pending_uncancellations += 1 + + # Deliver cancellation to child scopes that aren't shielded or running their own + # cancellation callbacks + for scope in self._child_scopes: + if not scope._shield and not scope.cancel_called: + should_retry = scope._deliver_cancellation(origin) or should_retry + + # Schedule another callback if there are still tasks left + if origin is self: + if should_retry: + self._cancel_handle = get_running_loop().call_soon( + self._deliver_cancellation, origin + ) + else: + self._cancel_handle = None + + return should_retry + + def _restart_cancellation_in_parent(self) -> None: + """ + Restart the cancellation effort in the closest directly cancelled parent scope. + + """ + scope = self._parent_scope + while scope is not None: + if scope._cancel_called: + if scope._cancel_handle is None: + scope._deliver_cancellation(scope) + + break + + # No point in looking beyond any shielded scope + if scope._shield: + break + + scope = scope._parent_scope + + def _reparent(self, new_parent: CancelScope) -> None: + """ + Move this active scope from its current parent to ``new_parent``. + + Used by :meth:`TaskGroup.start` to move a task that has just called + ``task_status.started()`` into the target task group's cancel scope. + """ + if self._parent_scope is new_parent: + return + + if self._parent_scope is not None: + self._parent_scope._child_scopes.discard(self) + + self._parent_scope = new_parent + new_parent._child_scopes.add(self) + + # If the new parent (or an ancestor) is already cancelled, (re)start the + # delivery loop to ensure we're cancelled at next checkpoint like Trio. + self._restart_cancellation_in_parent() + + def cancel(self, reason: str | None = None) -> None: + if not self._cancel_called: + if self._timeout_handle: + self._timeout_handle.cancel() + self._timeout_handle = None + + self._cancel_called = True + self._cancel_reason = f"Cancelled via cancel scope {id(self):x}" + if task := current_task(): + self._cancel_reason += f" by {task}" + + if reason: + self._cancel_reason += f"; reason: {reason}" + + if self._host_task is not None: + self._deliver_cancellation(self) + + @property + def deadline(self) -> float: + return self._deadline + + @deadline.setter + def deadline(self, value: float) -> None: + self._deadline = float(value) + if self._timeout_handle is not None: + self._timeout_handle.cancel() + self._timeout_handle = None + + if self._active and not self._cancel_called: + self._timeout() + + @property + def cancel_called(self) -> bool: + return self._cancel_called + + @property + def cancelled_caught(self) -> bool: + return self._cancelled_caught + + @property + def shield(self) -> bool: + return self._shield + + @shield.setter + def shield(self, value: bool) -> None: + if self._shield != value: + self._shield = value + if not value: + self._restart_cancellation_in_parent() + + +# +# Task states +# + + +class TaskState: + """ + Encapsulates auxiliary task information that cannot be added to the Task instance + itself because there are no guarantees about its implementation. + """ + + __slots__ = "__weakref__", "cancel_scope", "parent_id" + + def __init__(self, parent_id: int | None, cancel_scope: CancelScope | None): + self.parent_id = parent_id + self.cancel_scope = cancel_scope + + +_task_states: WeakKeyDictionary[asyncio.Task, TaskState] = WeakKeyDictionary() + + +# +# Task groups +# + + +class _AsyncioTaskStatus(abc.TaskStatus[T_contra]): + def __init__( + self, + future: asyncio.Future, + parent_id: int, + target_scope: CancelScope, + spawn_scope: CancelScope, + ): + self._future = future + self._parent_id = parent_id + # The eventual parent scope for this spawn_scope + # (after task_status.started() has been called) + self._target_scope = target_scope + # The task's own cancel scope, also held by its TaskHandle + self._spawn_scope = spawn_scope + + def started(self, value: T_contra | None = None) -> None: + task = cast(asyncio.Task, current_task()) + _task_states[task].parent_id = self._parent_id + if self._future.done(): + if not self._future.cancelled(): + raise RuntimeError("called 'started' twice on the same task status") + else: + # Caller of start() was cancelled, nothing to reparent + return + + self._future.set_result(value) + self._spawn_scope._reparent(self._target_scope) + + +if sys.version_info >= (3, 12): + _eager_task_factory_code: CodeType | None = asyncio.eager_task_factory.__code__ +else: + _eager_task_factory_code = None + + +class TaskGroup(abc.TaskGroup): + def __init__(self) -> None: + self.cancel_scope: CancelScope = CancelScope() + self._entered = False + self._exceptions: list[BaseException] = [] + self._tasks: set[asyncio.Task] = set() + self._on_completed_fut: asyncio.Future[None] | None = None + + async def __aenter__(self) -> Self: + if self._entered: + raise RuntimeError("TaskGroup cannot be entered more than once") + + self._entered = True + + self.cancel_scope.__enter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + try: + if exc_val is not None: + self.cancel_scope.cancel() + if not isinstance(exc_val, CancelledError): + self._exceptions.append(exc_val) + + loop = get_running_loop() + try: + if self._tasks: + with CancelScope() as wait_scope: + while self._tasks: + self._on_completed_fut = loop.create_future() + + try: + await self._on_completed_fut + except CancelledError as exc: + # Shield the scope against further cancellation attempts, + # as they're not productive (#695) + wait_scope.shield = True + self.cancel_scope.cancel() + + # Set exc_val from the cancellation exception if it was + # previously unset. However, we should not replace a native + # cancellation exception with one raise by a cancel scope. + if exc_val is None or ( + isinstance(exc_val, CancelledError) + and not is_anyio_cancellation(exc) + ): + exc_val = exc + + self._on_completed_fut = None + else: + # If there are no child tasks to wait on, run at least one checkpoint + # anyway + await AsyncIOBackend.cancel_shielded_checkpoint() + + if self._exceptions: + # The exception that got us here should already have been + # added to self._exceptions so it's ok to break exception + # chaining and avoid adding a "During handling of above..." + # for each nesting level. + raise BaseExceptionGroup( + "unhandled errors in a TaskGroup", self._exceptions + ) from None + elif exc_val: + raise exc_val + except BaseException as exc: + if self.cancel_scope.__exit__(type(exc), exc, exc.__traceback__): + return True + + raise + + return self.cancel_scope.__exit__(exc_type, exc_val, exc_tb) + finally: + del exc_val, exc_tb, self._exceptions + + def _spawn( + self, + coro: Coroutine[Any, Any, T_co], + name: object, + task_status: _AsyncioTaskStatus | None = None, + ) -> TaskHandle[T_co]: + task_status_future = task_status._future if task_status is not None else None + + def task_done(_task: asyncio.Task) -> None: + if sys.version_info >= (3, 14) and self.cancel_scope._host_task is not None: + asyncio.future_discard_from_awaited_by( + _task, self.cancel_scope._host_task + ) + + task_state = _task_states[_task] + assert task_state.cancel_scope is not None + assert _task in task_state.cancel_scope._tasks + task_state.cancel_scope._tasks.remove(_task) + self._tasks.remove(task) + del _task_states[_task] + + if self._on_completed_fut is not None and not self._tasks: + try: + self._on_completed_fut.set_result(None) + except asyncio.InvalidStateError: + pass + + try: + exc = _task.exception() + except CancelledError as e: + while isinstance(e.__context__, CancelledError): + e = e.__context__ + + exc = e + + if exc is not None: + # The future can only be in the cancelled state if the host task was + # cancelled, so return immediately instead of adding one more + # CancelledError to the exceptions list + if task_status_future is not None and task_status_future.cancelled(): + return + + if task_status_future is None or task_status_future.done(): + if not isinstance(exc, CancelledError): + self._exceptions.append(exc) + + if not self.cancel_scope._effectively_cancelled: + self.cancel_scope.cancel() + else: + task_status_future.set_exception(exc) + elif task_status_future is not None and not task_status_future.done(): + task_status_future.set_exception( + RuntimeError("Child exited without calling task_status.started()") + ) + + if task_status_future is not None: + parent_id = id(current_task()) + caller_state = _task_states.get(cast(asyncio.Task, current_task())) + if caller_state is not None and caller_state.cancel_scope is not None: + initial_scope = caller_state.cancel_scope + else: + # The caller is an unmanaged task (no task state) + initial_scope = self.cancel_scope + else: + parent_id = id(self.cancel_scope._host_task) + initial_scope = self.cancel_scope + + spawn_scope = task_status._spawn_scope if task_status is not None else None + handle = TaskHandle(coro, name, cancel_scope=spawn_scope) + loop = asyncio.get_running_loop() + wrapper_coro = handle._run_coro() + try: + if ( + (factory := loop.get_task_factory()) + and getattr(factory, "__code__", None) is _eager_task_factory_code + and (closure := getattr(factory, "__closure__", None)) + ): + custom_task_constructor = closure[0].cell_contents + task = custom_task_constructor( + wrapper_coro, loop=loop, name=handle.name + ) + else: + task = loop.create_task(wrapper_coro, name=handle.name) + except BaseException: + with suppress(BaseException): + wrapper_coro.close() + + with suppress(BaseException): + coro.close() + + raise + + # Make the spawned task inherit the initial cancel scope + _task_states[task] = TaskState(parent_id=parent_id, cancel_scope=initial_scope) + initial_scope._tasks.add(task) + self._tasks.add(task) + if sys.version_info >= (3, 14) and self.cancel_scope._host_task is not None: + asyncio.future_add_to_awaited_by(task, self.cancel_scope._host_task) + + task.add_done_callback(task_done) + return handle + + def create_task( + self, + coro: Coroutine[Any, Any, T_co], + *, + name: object = None, + context: Context | None = None, + ) -> TaskHandle[T_co]: + if not isinstance(coro, Coroutine): + raise TypeError(f"expected a coroutine, got {coro.__class__.__qualname__}") + + if not self._entered or not self.cancel_scope._active: + coro.close() + raise RuntimeError( + "This task group is not active; no new tasks can be started." + ) + + final_name = get_coro_name(coro, name) + + if context is not None: + return context.run(self._spawn, coro, name=final_name) + else: + return self._spawn(coro, name=final_name) + + async def start( + self, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + name: object = None, + return_handle: Literal[False, True] = False, + ) -> Any: + if not self._entered or not self.cancel_scope._active: + raise RuntimeError( + "This task group is not active; no new tasks can be started." + ) + + # Until task_status.started() is called, a task spawned via start() belongs to + # the *caller's* cancel scope, not the target group's, so cancelling the group + # does not cancel a task that hasn't reported startup yet. The + # task_status.started() call moves the task's cancel scope to this task group's + # cancel scope. + # + # The caller may be an unmanaged task (no _task_states entry), in which case + # fall back to the group's own scope. + future: asyncio.Future = asyncio.Future() + final_name = get_callable_name(func, name) + task_status: _AsyncioTaskStatus[Any] = _AsyncioTaskStatus( + future, id(self.cancel_scope._host_task), self.cancel_scope, CancelScope() + ) + coro = call_for_coroutine(func, args, task_status=task_status) + handle = self._spawn(coro, final_name, task_status) + + # If the task raises an exception after sending a start value without a switch + # point between, the task group is cancelled and this method never proceeds to + # process the completed future. That's why we have to have a shielded cancel + # scope here. + try: + await future + except BaseException: + if handle.status is TaskHandle.Status.PENDING: + # Cancel the task and wait for it to exit before returning + handle.cancel() + with CancelScope(shield=True): + await handle.wait() + + raise + + if return_handle: + handle._start_value = future.result() + return handle + else: + return future.result() + + +# +# Threads +# + +_Retval_Queue_Type = tuple[T_Retval | None, BaseException | None] + + +class WorkerThread(Thread): + MAX_IDLE_TIME = 10 # seconds + + def __init__( + self, + root_task: asyncio.Task, + workers: set[WorkerThread], + idle_workers: deque[WorkerThread], + ): + kwargs: dict[str, Any] = {} + if sys.version_info >= (3, 14): + kwargs["context"] = Context() + + super().__init__(name="AnyIO worker thread", **kwargs) + self.root_task = root_task + self.workers = workers + self.idle_workers = idle_workers + self.loop = root_task._loop + self.queue: Queue[ + tuple[Context, Callable, tuple, asyncio.Future, CancelScope] | None + ] = Queue(2) + self.idle_since = AsyncIOBackend.current_time() + self.stopping = False + + def _report_result( + self, future: asyncio.Future, result: Any, exc: BaseException | None + ) -> None: + self.idle_since = AsyncIOBackend.current_time() + if not self.stopping: + self.idle_workers.append(self) + + if not future.cancelled(): + if exc is not None: + if isinstance(exc, StopIteration): + new_exc = RuntimeError("coroutine raised StopIteration") + new_exc.__cause__ = exc + exc = new_exc + + future.set_exception(exc) + else: + future.set_result(result) + + def run(self) -> None: + with claim_worker_thread(AsyncIOBackend, self.loop): + while True: + item = self.queue.get() + if item is None: + # Shutdown command received + return + + context, func, args, future, cancel_scope = item + if not future.cancelled(): + result = None + exception: BaseException | None = None + threadlocals.current_cancel_scope = cancel_scope + try: + result = context.run(func, *args) + except BaseException as exc: + exception = exc + finally: + del threadlocals.current_cancel_scope + + try: + self.loop.call_soon_threadsafe( + self._report_result, future, result, exception + ) + except RuntimeError: + if not self.loop.is_closed(): + raise + + del result, exception + + self.queue.task_done() + del item, context, func, args, future, cancel_scope + + def stop(self, f: asyncio.Task | None = None) -> None: + self.stopping = True + self.queue.put_nowait(None) + self.workers.discard(self) + try: + self.idle_workers.remove(self) + except ValueError: + pass + + +_threadpool_idle_workers: RunVar[deque[WorkerThread]] = RunVar( + "_threadpool_idle_workers" +) +_threadpool_workers: RunVar[set[WorkerThread]] = RunVar("_threadpool_workers") + + +# +# Subprocesses +# + + +@dataclass(eq=False) +class StreamReaderWrapper(abc.ByteReceiveStream): + _stream: asyncio.StreamReader + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + data = await self._stream.read(max_bytes) + if data: + return data + else: + raise EndOfStream + + async def aclose(self) -> None: + self._stream.set_exception(ClosedResourceError()) + await AsyncIOBackend.checkpoint() + + +@dataclass(eq=False) +class StreamWriterWrapper(abc.ByteSendStream): + _stream: asyncio.StreamWriter + _closed: bool = field(init=False, default=False) + + async def send(self, item: bytes) -> None: + await AsyncIOBackend.checkpoint_if_cancelled() + stream_paused = self._stream._protocol._paused # type: ignore[attr-defined] + try: + self._stream.write(item) + await self._stream.drain() + except (ConnectionResetError, BrokenPipeError, RuntimeError) as exc: + # If closed by us and/or the peer: + # * on stdlib, drain() raises ConnectionResetError or BrokenPipeError + # * on uvloop and Winloop, write() eventually starts raising RuntimeError + if self._closed: + raise ClosedResourceError from exc + elif self._stream.is_closing(): + raise BrokenResourceError from exc + + raise + + if not stream_paused: + await AsyncIOBackend.cancel_shielded_checkpoint() + + async def aclose(self) -> None: + self._closed = True + self._stream.close() + await AsyncIOBackend.checkpoint() + + +@dataclass(eq=False) +class Process(abc.Process): + _process: asyncio.subprocess.Process + _stdin: StreamWriterWrapper | None + _stdout: StreamReaderWrapper | None + _stderr: StreamReaderWrapper | None + _exited: asyncio.Event + _transport: asyncio.SubprocessTransport + + async def aclose(self) -> None: + with CancelScope(shield=True) as scope: + # We need to close the underlying pipe_transports as well to allow a + # process blocking on full buffers to receive SIGPIPE and exit. + if self._stdin: + await self._stdin.aclose() + if pipe := self._transport.get_pipe_transport(0): + pipe.close() + if self._stdout: + await self._stdout.aclose() + if pipe := self._transport.get_pipe_transport(1): + pipe.close() + if self._stderr: + await self._stderr.aclose() + if pipe := self._transport.get_pipe_transport(2): + pipe.close() + + scope.shield = False + try: + await self.wait() + except BaseException: + scope.shield = True + # Closing the transport on asyncio also handles sending kill + self._transport.close() + await self.wait() + raise + + async def wait(self) -> int: + await self._exited.wait() + assert self._process.returncode is not None + return self._process.returncode + + def terminate(self) -> None: + self._process.terminate() + + def kill(self) -> None: + self._process.kill() + + def send_signal(self, signal: int) -> None: + self._process.send_signal(signal) + + @property + def pid(self) -> int: + return self._process.pid + + @property + def returncode(self) -> int | None: + return self._process.returncode + + @property + def stdin(self) -> abc.ByteSendStream | None: + return self._stdin + + @property + def stdout(self) -> abc.ByteReceiveStream | None: + return self._stdout + + @property + def stderr(self) -> abc.ByteReceiveStream | None: + return self._stderr + + +def _forcibly_shutdown_process_pool_on_exit( + workers: set[Process], _task: object +) -> None: + """ + Forcibly shuts down worker processes belonging to this event loop.""" + child_watcher: asyncio.AbstractChildWatcher | None = None # type: ignore[name-defined] + if sys.version_info < (3, 12): + try: + child_watcher = asyncio.get_event_loop_policy().get_child_watcher() + except NotImplementedError: + pass + + # Close as much as possible (w/o async/await) to avoid warnings + for process in workers.copy(): + if process.returncode is not None: + continue + + process._stdin._stream._transport.close() # type: ignore[union-attr] + process._stdout._stream._transport.close() # type: ignore[union-attr] + process._stderr._stream._transport.close() # type: ignore[union-attr] + process.kill() + if child_watcher: + child_watcher.remove_child_handler(process.pid) + + +async def _shutdown_process_pool_on_exit(workers: set[abc.Process]) -> None: + """ + Shuts down worker processes belonging to this event loop. + + NOTE: this only works when the event loop was started using asyncio.run() or + anyio.run(). + + """ + process: abc.Process + try: + await sleep(math.inf) + except asyncio.CancelledError: + workers = workers.copy() + for process in workers: + if process.returncode is None: + process.kill() + + for process in workers: + await process.aclose() + + +# +# Sockets and networking +# + + +class StreamProtocol(asyncio.Protocol): + read_queue: deque[bytes] + read_event: asyncio.Event + write_event: asyncio.Event + exception: Exception | None = None + is_at_eof: bool = False + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + self.read_queue = deque() + self.read_event = asyncio.Event() + self.write_event = asyncio.Event() + self.write_event.set() + cast(asyncio.Transport, transport).set_write_buffer_limits(0) + + def connection_lost(self, exc: Exception | None) -> None: + if exc: + self.exception = exc + + self.read_event.set() + self.write_event.set() + + def data_received(self, data: bytes) -> None: + # ProactorEventloop sometimes sends bytearray instead of bytes + self.read_queue.append(bytes(data)) + self.read_event.set() + + def eof_received(self) -> bool | None: + self.is_at_eof = True + self.read_event.set() + return True + + def pause_writing(self) -> None: + self.write_event = asyncio.Event() + + def resume_writing(self) -> None: + self.write_event.set() + + +class DatagramProtocol(asyncio.DatagramProtocol): + read_queue: deque[tuple[bytes, IPSockAddrType]] + read_event: asyncio.Event + write_event: asyncio.Event + closed_event: asyncio.Event + exception: Exception | None = None + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + self.read_queue = deque(maxlen=100) # arbitrary value + self.read_event = asyncio.Event() + self.write_event = asyncio.Event() + self.closed_event = asyncio.Event() + self.write_event.set() + + def connection_lost(self, exc: Exception | None) -> None: + self.read_event.set() + self.write_event.set() + self.closed_event.set() + + def datagram_received(self, data: bytes, addr: IPSockAddrType) -> None: + addr = convert_ipv6_sockaddr(addr) + self.read_queue.append((data, addr)) + self.read_event.set() + + def error_received(self, exc: Exception) -> None: + self.exception = exc + + def pause_writing(self) -> None: + self.write_event.clear() + + def resume_writing(self) -> None: + self.write_event.set() + + +class SocketStream(abc.SocketStream): + def __init__(self, transport: asyncio.Transport, protocol: StreamProtocol): + self._transport = transport + self._protocol = protocol + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + self._closed = False + + @property + def _raw_socket(self) -> socket.socket: + return self._transport.get_extra_info("socket") + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + with self._receive_guard: + if ( + not self._protocol.read_event.is_set() + and not self._transport.is_closing() + and not self._protocol.is_at_eof + ): + self._transport.resume_reading() + await self._protocol.read_event.wait() + self._transport.pause_reading() + else: + await AsyncIOBackend.checkpoint() + + try: + chunk = self._protocol.read_queue.popleft() + except IndexError: + if self._closed: + raise ClosedResourceError from None + elif self._protocol.exception: + raise BrokenResourceError from self._protocol.exception + else: + raise EndOfStream from None + + if len(chunk) > max_bytes: + # Split the oversized chunk + chunk, leftover = chunk[:max_bytes], chunk[max_bytes:] + self._protocol.read_queue.appendleft(leftover) + + # If the read queue is empty, clear the flag so that the next call will + # block until data is available + if not self._protocol.read_queue: + self._protocol.read_event.clear() + + return chunk + + async def send(self, item: bytes) -> None: + with self._send_guard: + await AsyncIOBackend.checkpoint() + + if self._closed: + raise ClosedResourceError + elif self._protocol.exception is not None: + raise BrokenResourceError from self._protocol.exception + + try: + self._transport.write(item) + except RuntimeError as exc: + if self._transport.is_closing(): + raise BrokenResourceError from exc + else: + raise + + await self._protocol.write_event.wait() + + async def send_eof(self) -> None: + try: + self._transport.write_eof() + except OSError: + pass + + async def aclose(self) -> None: + self._closed = True + if not self._transport.is_closing(): + try: + self._transport.write_eof() + except OSError: + pass + + self._transport.close() + await sleep(0) + self._transport.abort() + + +class _RawSocketMixin: + _receive_future: asyncio.Future | None = None + _send_future: asyncio.Future | None = None + _closing = False + + def __init__(self, raw_socket: socket.socket): + self.__raw_socket = raw_socket + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + @property + def _raw_socket(self) -> socket.socket: + return self.__raw_socket + + def _wait_until_readable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: + def callback(f: object) -> None: + del self._receive_future + loop.remove_reader(self.__raw_socket) + + f = self._receive_future = asyncio.Future() + loop.add_reader(self.__raw_socket, f.set_result, None) + f.add_done_callback(callback) + return f + + def _wait_until_writable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: + def callback(f: object) -> None: + del self._send_future + loop.remove_writer(self.__raw_socket) + + f = self._send_future = asyncio.Future() + loop.add_writer(self.__raw_socket, f.set_result, None) + f.add_done_callback(callback) + return f + + async def aclose(self) -> None: + if not self._closing: + self._closing = True + if self.__raw_socket.fileno() != -1: + self.__raw_socket.close() + + if self._receive_future and not self._receive_future.done(): + self._receive_future.set_result(None) + if self._send_future and not self._send_future.done(): + self._send_future.set_result(None) + + +class UNIXSocketStream(_RawSocketMixin, abc.UNIXSocketStream): + async def send_eof(self) -> None: + with self._send_guard: + self._raw_socket.shutdown(socket.SHUT_WR) + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + data = self._raw_socket.recv(max_bytes) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + if not data: + raise EndOfStream + + return data + + async def send(self, item: bytes) -> None: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._send_guard: + view = memoryview(item) + while view: + try: + bytes_sent = self._raw_socket.send(view) + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + view = view[bytes_sent:] + + async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: + if not isinstance(msglen, int) or msglen < 0: + raise ValueError("msglen must be a non-negative integer") + if not isinstance(maxfds, int) or maxfds < 1: + raise ValueError("maxfds must be a positive integer") + + loop = get_running_loop() + fds = array.array("i") + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + message, ancdata, _flags, _addr = self._raw_socket.recvmsg( + msglen, socket.CMSG_LEN(maxfds * fds.itemsize) + ) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + if not message and not ancdata: + raise EndOfStream + + break + + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: + raise RuntimeError( + f"Received unexpected ancillary data; message = {message!r}, " + f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" + ) + + fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) + + return message, list(fds) + + async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: + if not message: + raise ValueError("message must not be empty") + if not fds: + raise ValueError("fds must not be empty") + + loop = get_running_loop() + filenos: list[int] = [] + for fd in fds: + if isinstance(fd, int): + filenos.append(fd) + elif isinstance(fd, IOBase): + filenos.append(fd.fileno()) + + fdarray = array.array("i", filenos) + await AsyncIOBackend.checkpoint() + with self._send_guard: + while True: + try: + # The ignore can be removed after mypy picks up + # https://github.com/python/typeshed/pull/5545 + self._raw_socket.sendmsg( + [message], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fdarray)] + ) + break + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + + +class TCPSocketListener(abc.SocketListener): + _accept_scope: CancelScope | None = None + _closed = False + + def __init__(self, raw_socket: socket.socket): + self.__raw_socket = raw_socket + self._loop = cast(asyncio.BaseEventLoop, get_running_loop()) + self._accept_guard = ResourceGuard("accepting connections from") + + @property + def _raw_socket(self) -> socket.socket: + return self.__raw_socket + + async def accept(self) -> abc.SocketStream: + if self._closed: + raise ClosedResourceError + + with self._accept_guard: + await AsyncIOBackend.checkpoint() + with CancelScope() as self._accept_scope: + try: + client_sock, _addr = await self._loop.sock_accept(self._raw_socket) + except asyncio.CancelledError: + # Workaround for https://bugs.python.org/issue41317 + try: + self._loop.remove_reader(self._raw_socket) + except (ValueError, NotImplementedError): + pass + + if self._closed: + raise ClosedResourceError from None + + raise + finally: + self._accept_scope = None + + client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + transport, protocol = await self._loop.connect_accepted_socket( + StreamProtocol, client_sock + ) + return SocketStream(transport, protocol) + + async def aclose(self) -> None: + if self._closed: + return + + self._closed = True + if self._accept_scope: + # Workaround for https://bugs.python.org/issue41317 + try: + self._loop.remove_reader(self._raw_socket) + except (ValueError, NotImplementedError): + pass + + self._accept_scope.cancel() + await sleep(0) + + self._raw_socket.close() + + +class UNIXSocketListener(abc.SocketListener): + def __init__(self, raw_socket: socket.socket): + self.__raw_socket = raw_socket + self._loop = get_running_loop() + self._accept_guard = ResourceGuard("accepting connections from") + self._closed = False + + async def accept(self) -> abc.SocketStream: + await AsyncIOBackend.checkpoint() + with self._accept_guard: + while True: + try: + client_sock, _ = self.__raw_socket.accept() + client_sock.setblocking(False) + return UNIXSocketStream(client_sock) + except BlockingIOError: + f: asyncio.Future = asyncio.Future() + self._loop.add_reader(self.__raw_socket, f.set_result, None) + f.add_done_callback( + lambda _: self._loop.remove_reader(self.__raw_socket) + ) + await f + except OSError as exc: + if self._closed: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + + async def aclose(self) -> None: + self._closed = True + self.__raw_socket.close() + + @property + def _raw_socket(self) -> socket.socket: + return self.__raw_socket + + +class UDPSocket(abc.UDPSocket): + def __init__( + self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol + ): + self._transport = transport + self._protocol = protocol + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + self._closed = False + + @property + def _raw_socket(self) -> socket.socket: + return self._transport.get_extra_info("socket") + + async def aclose(self) -> None: + self._closed = True + if not self._transport.is_closing(): + self._transport.close() + + await self._protocol.closed_event.wait() + + async def receive(self) -> tuple[bytes, IPSockAddrType]: + with self._receive_guard: + await AsyncIOBackend.checkpoint() + + # If the buffer is empty, ask for more data + if not self._protocol.read_queue and not self._transport.is_closing(): + self._protocol.read_event.clear() + await self._protocol.read_event.wait() + + try: + return self._protocol.read_queue.popleft() + except IndexError: + if self._closed: + raise ClosedResourceError from None + else: + raise BrokenResourceError from None + + async def send(self, item: UDPPacketType) -> None: + with self._send_guard: + await AsyncIOBackend.checkpoint() + await self._protocol.write_event.wait() + if self._closed: + raise ClosedResourceError + elif self._transport.is_closing(): + raise BrokenResourceError + else: + self._transport.sendto(*item) + + +class ConnectedUDPSocket(abc.ConnectedUDPSocket): + def __init__( + self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol + ): + self._transport = transport + self._protocol = protocol + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + self._closed = False + + @property + def _raw_socket(self) -> socket.socket: + return self._transport.get_extra_info("socket") + + async def aclose(self) -> None: + self._closed = True + if not self._transport.is_closing(): + self._transport.close() + + await self._protocol.closed_event.wait() + + async def receive(self) -> bytes: + with self._receive_guard: + await AsyncIOBackend.checkpoint() + + # If the buffer is empty, ask for more data + if not self._protocol.read_queue and not self._transport.is_closing(): + self._protocol.read_event.clear() + await self._protocol.read_event.wait() + + try: + packet = self._protocol.read_queue.popleft() + except IndexError: + if self._closed: + raise ClosedResourceError from None + else: + raise BrokenResourceError from None + + return packet[0] + + async def send(self, item: bytes) -> None: + with self._send_guard: + await AsyncIOBackend.checkpoint() + await self._protocol.write_event.wait() + if self._closed: + raise ClosedResourceError + elif self._transport.is_closing(): + raise BrokenResourceError + else: + self._transport.sendto(item) + + +class UNIXDatagramSocket(_RawSocketMixin, abc.UNIXDatagramSocket): + async def receive(self) -> UNIXDatagramPacketType: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + data = self._raw_socket.recvfrom(65536) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return data + + async def send(self, item: UNIXDatagramPacketType) -> None: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._send_guard: + while True: + try: + self._raw_socket.sendto(*item) + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return + + +class ConnectedUNIXDatagramSocket(_RawSocketMixin, abc.ConnectedUNIXDatagramSocket): + async def receive(self) -> bytes: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + data = self._raw_socket.recv(65536) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return data + + async def send(self, item: bytes) -> None: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._send_guard: + while True: + try: + self._raw_socket.send(item) + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return + + +_read_events: RunVar[dict[int, asyncio.Future[bool]]] = RunVar("read_events") +_write_events: RunVar[dict[int, asyncio.Future[bool]]] = RunVar("write_events") + + +# +# Synchronization +# + + +class Event(BaseEvent): + __slots__ = ("_event",) + + def __new__(cls) -> Self: + return object.__new__(cls) + + def __init__(self) -> None: + self._event = asyncio.Event() + + def set(self) -> None: + self._event.set() + + def is_set(self) -> bool: + return self._event.is_set() + + async def wait(self) -> None: + if self.is_set(): + await AsyncIOBackend.checkpoint() + else: + await self._event.wait() + + def statistics(self) -> EventStatistics: + return EventStatistics(len(self._event._waiters)) + + +class Lock(BaseLock): + __slots__ = "_fast_acquire", "_owner_task", "_waiters" + + def __new__(cls, *, fast_acquire: bool = False) -> Self: + return object.__new__(cls) + + def __init__(self, *, fast_acquire: bool = False) -> None: + self._fast_acquire = fast_acquire + self._owner_task: asyncio.Task | None = None + self._waiters: deque[tuple[asyncio.Task, asyncio.Future]] = deque() + + async def acquire(self) -> None: + task = cast(asyncio.Task, current_task()) + if self._owner_task is None and not self._waiters: + await AsyncIOBackend.checkpoint_if_cancelled() + self._owner_task = task + + # Unless on the "fast path", yield control of the event loop so that other + # tasks can run too + if not self._fast_acquire: + try: + await AsyncIOBackend.cancel_shielded_checkpoint() + except CancelledError: + self.release() + raise + + return + + if self._owner_task == task: + raise RuntimeError("Attempted to acquire an already held Lock") + + fut: asyncio.Future[None] = asyncio.Future() + item = task, fut + self._waiters.append(item) + try: + await fut + except CancelledError: + if fut.cancelled(): + try: + self._waiters.remove(item) + except ValueError: + pass + else: + self.release() + + raise + + def acquire_nowait(self) -> None: + task = cast(asyncio.Task, current_task()) + if self._owner_task is None and not self._waiters: + self._owner_task = task + return + + if self._owner_task is task: + raise RuntimeError("Attempted to acquire an already held Lock") + + raise WouldBlock + + def locked(self) -> bool: + return self._owner_task is not None + + def release(self) -> None: + if self._owner_task != current_task(): + raise RuntimeError("The current task is not holding this lock") + + # A cancelled waiter that already received ownership removes itself from + # _waiters before calling release(); any cancelled waiter still queued here + # was cancelled before being woken, so drop it. + while self._waiters: + task, fut = self._waiters.popleft() + if fut.cancelled(): + continue + + self._owner_task = task + fut.set_result(None) + return + + self._owner_task = None + + def statistics(self) -> LockStatistics: + task_info = AsyncIOTaskInfo(self._owner_task) if self._owner_task else None + return LockStatistics(self.locked(), task_info, len(self._waiters)) + + +class Semaphore(BaseSemaphore): + __slots__ = "_fast_acquire", "_max_value", "_value", "_waiters" + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Self: + return object.__new__(cls) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ): + super().__init__(initial_value, max_value=max_value) + self._value = initial_value + self._max_value = max_value + self._fast_acquire = fast_acquire + self._waiters: deque[asyncio.Future[None]] = deque() + + async def acquire(self) -> None: + if self._value > 0 and not self._waiters: + await AsyncIOBackend.checkpoint_if_cancelled() + self._value -= 1 + + # Unless on the "fast path", yield control of the event loop so that other + # tasks can run too + if not self._fast_acquire: + try: + await AsyncIOBackend.cancel_shielded_checkpoint() + except CancelledError: + self.release() + raise + + return + + fut: asyncio.Future[None] = asyncio.Future() + self._waiters.append(fut) + try: + await fut + except CancelledError: + if fut.cancelled(): + try: + self._waiters.remove(fut) + except ValueError: + pass + else: + self.release() + + raise + + def acquire_nowait(self) -> None: + if self._value == 0: + raise WouldBlock + + self._value -= 1 + + def release(self) -> None: + if self._max_value is not None and self._value == self._max_value: + raise ValueError("semaphore released too many times") + + while self._waiters: + fut = self._waiters.popleft() + if fut.cancelled(): + continue + + fut.set_result(None) + return + + self._value += 1 + + @property + def value(self) -> int: + return self._value + + @property + def max_value(self) -> int | None: + return self._max_value + + def statistics(self) -> SemaphoreStatistics: + return SemaphoreStatistics(len(self._waiters)) + + +class CapacityLimiter(BaseCapacityLimiter): + __slots__ = "_borrowers", "_total_tokens", "_wait_queue" + + def __new__(cls, total_tokens: float) -> Self: + return object.__new__(cls) + + def __init__(self, total_tokens: float): + self._total_tokens: float = 0 + self._borrowers: set[Any] = set() + self._wait_queue: OrderedDict[Any, asyncio.Event] = OrderedDict() + self.total_tokens = total_tokens + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + @property + def total_tokens(self) -> float: + return self._total_tokens + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + if not isinstance(value, int) and not math.isinf(value): + raise TypeError("total_tokens must be an int or math.inf") + + if value < 0: + raise ValueError("total_tokens must be >= 0") + + self._total_tokens = value + + # Notify waiting tasks that they have acquired the limiter while + # there is spare capacity. + while self._wait_queue and len(self._borrowers) < self._total_tokens: + borrower, event = self._wait_queue.popitem(last=False) + self._borrowers.add(borrower) + event.set() + + @property + def borrowed_tokens(self) -> int: + return len(self._borrowers) + + @property + def available_tokens(self) -> float: + return self._total_tokens - len(self._borrowers) + + def _notify_next_waiter(self) -> None: + """Hand a free token to the next task in line, if any.""" + if self._wait_queue and len(self._borrowers) < self._total_tokens: + borrower, event = self._wait_queue.popitem(last=False) + self._borrowers.add(borrower) + event.set() + + def acquire_nowait(self) -> None: + self.acquire_on_behalf_of_nowait(current_task()) + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + if borrower in self._borrowers: + raise RuntimeError( + "this borrower is already holding one of this CapacityLimiter's tokens" + ) + + if self._wait_queue or len(self._borrowers) >= self._total_tokens: + raise WouldBlock + + self._borrowers.add(borrower) + + async def acquire(self) -> None: + return await self.acquire_on_behalf_of(current_task()) + + async def acquire_on_behalf_of(self, borrower: object) -> None: + await AsyncIOBackend.checkpoint_if_cancelled() + try: + self.acquire_on_behalf_of_nowait(borrower) + except WouldBlock: + event = asyncio.Event() + self._wait_queue[borrower] = event + try: + await event.wait() + except BaseException: + self._wait_queue.pop(borrower, None) + if event.is_set(): + self._borrowers.discard(borrower) + self._notify_next_waiter() + + raise + else: + try: + await AsyncIOBackend.cancel_shielded_checkpoint() + except BaseException: + self.release() + raise + + def release(self) -> None: + self.release_on_behalf_of(current_task()) + + def release_on_behalf_of(self, borrower: object) -> None: + try: + self._borrowers.remove(borrower) + except KeyError: + raise RuntimeError( + "this borrower isn't holding any of this CapacityLimiter's tokens" + ) from None + + self._notify_next_waiter() + + def statistics(self) -> CapacityLimiterStatistics: + return CapacityLimiterStatistics( + self.borrowed_tokens, + self.total_tokens, + tuple(self._borrowers), + len(self._wait_queue), + ) + + +_default_thread_limiter: RunVar[CapacityLimiter] = RunVar("_default_thread_limiter") + + +# +# Operating system signals +# + + +class _SignalReceiver: + def __init__(self, signals: tuple[Signals, ...]): + self._signals = signals + self._loop = get_running_loop() + self._signal_queue: deque[Signals] = deque() + self._future: asyncio.Future = asyncio.Future() + self._handled_signals: set[Signals] = set() + + def _deliver(self, signum: Signals) -> None: + self._signal_queue.append(signum) + if not self._future.done(): + self._future.set_result(None) + + def __enter__(self) -> Self: + for sig in set(self._signals): + self._loop.add_signal_handler(sig, self._deliver, sig) + self._handled_signals.add(sig) + + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + for sig in self._handled_signals: + self._loop.remove_signal_handler(sig) + + def __aiter__(self) -> _SignalReceiver: + return self + + async def __anext__(self) -> Signals: + await AsyncIOBackend.checkpoint() + if not self._signal_queue: + self._future = asyncio.Future() + await self._future + + return self._signal_queue.popleft() + + +# +# Testing and debugging +# + + +class AsyncIOTaskInfo(TaskInfo): + def __init__(self, task: asyncio.Task): + task_state = _task_states.get(task) + if task_state is None: + parent_id = None + else: + parent_id = task_state.parent_id + + coro = task.get_coro() + assert coro is not None, "created TaskInfo from a completed Task" + super().__init__(id(task), parent_id, task.get_name(), coro) + self._task = weakref.ref(task) + + def has_pending_cancellation(self) -> bool: + if not (task := self._task()): + # If the task isn't around anymore, it won't have a pending cancellation + return False + + if task._must_cancel or ( # type: ignore[attr-defined] + isinstance(task._fut_waiter, asyncio.Future) # type: ignore[attr-defined] + and task._fut_waiter.cancelled() # type: ignore[attr-defined] + ): + return True + + if task_state := _task_states.get(task): + if cancel_scope := task_state.cancel_scope: + return cancel_scope._effectively_cancelled + + return False + + +class TestRunner(abc.TestRunner): + _send_stream: MemoryObjectSendStream[tuple[Awaitable[Any], asyncio.Future[Any]]] + + def __init__( + self, + *, + debug: bool | None = None, + use_uvloop: bool = False, + loop_factory: Callable[[], AbstractEventLoop] | None = None, + ) -> None: + if use_uvloop and loop_factory is None: + if sys.platform != "win32": + import uvloop + + loop_factory = uvloop.new_event_loop + else: + import winloop + + loop_factory = winloop.new_event_loop + + self._runner = Runner(debug=debug, loop_factory=loop_factory) + self._exceptions: list[BaseException] = [] + self._runner_task: asyncio.Task | None = None + + def __enter__(self) -> Self: + self._runner.__enter__() + self.get_loop().set_exception_handler(self._exception_handler) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._runner.__exit__(exc_type, exc_val, exc_tb) + + def get_loop(self) -> AbstractEventLoop: + return self._runner.get_loop() + + def is_running(self) -> bool: + try: + asyncio.get_running_loop() + return True + except RuntimeError: + return False + + def _exception_handler( + self, loop: asyncio.AbstractEventLoop, context: dict[str, Any] + ) -> None: + if isinstance(context.get("exception"), Exception): + self._exceptions.append(context["exception"]) + else: + loop.default_exception_handler(context) + + def _raise_async_exceptions(self) -> None: + # Re-raise any exceptions raised in asynchronous callbacks + if self._exceptions: + exceptions, self._exceptions = self._exceptions, [] + if len(exceptions) == 1: + raise exceptions[0] + elif exceptions: + raise BaseExceptionGroup( + "Multiple exceptions occurred in asynchronous callbacks", exceptions + ) + + async def _run_tests_and_fixtures( + self, + receive_stream: MemoryObjectReceiveStream[ + tuple[Awaitable[T_Retval], asyncio.Future[T_Retval]] + ], + ) -> None: + from _pytest.outcomes import OutcomeException + + with receive_stream, self._send_stream: + async for coro, future in receive_stream: + try: + retval = await coro + except CancelledError as exc: + if not future.cancelled(): + future.cancel(*exc.args) + + raise + except BaseException as exc: + if not future.cancelled(): + future.set_exception(exc) + + if not isinstance(exc, (Exception, OutcomeException)): + raise + else: + if not future.cancelled(): + future.set_result(retval) + + async def _call_in_runner_task( + self, + func: Callable[P, Awaitable[T_Retval]], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> T_Retval: + if not self._runner_task: + self._send_stream, receive_stream = create_memory_object_stream[ + tuple[Awaitable[Any], asyncio.Future] + ](1) + self._runner_task = self.get_loop().create_task( + self._run_tests_and_fixtures(receive_stream) + ) + + coro = func(*args, **kwargs) + future: asyncio.Future[T_Retval] = self.get_loop().create_future() + self._send_stream.send_nowait((coro, future)) + return await future + + def run_asyncgen_fixture( + self, + fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], + kwargs: dict[str, Any], + ) -> Iterable[T_Retval]: + asyncgen = fixture_func(**kwargs) + fixturevalue: T_Retval = self.get_loop().run_until_complete( + self._call_in_runner_task(asyncgen.asend, None) + ) + self._raise_async_exceptions() + + yield fixturevalue + + try: + self.get_loop().run_until_complete( + self._call_in_runner_task(asyncgen.asend, None) + ) + except StopAsyncIteration: + self._raise_async_exceptions() + else: + self.get_loop().run_until_complete(asyncgen.aclose()) + raise RuntimeError("Async generator fixture did not stop") + + def run_fixture( + self, + fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], + kwargs: dict[str, Any], + ) -> T_Retval: + retval = self.get_loop().run_until_complete( + self._call_in_runner_task(fixture_func, **kwargs) + ) + self._raise_async_exceptions() + return retval + + def run_test( + self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] + ) -> None: + from _pytest.outcomes import OutcomeException + + try: + self.get_loop().run_until_complete( + self._call_in_runner_task(test_func, **kwargs) + ) + except Exception as exc: + self._exceptions.append(exc) + except OutcomeException: + raise + except BaseException: + # A BaseException (e.g. KeyboardInterrupt, SystemExit) interrupted the event loop before + # the test completed. Cancel _runner_task so it does not resume when the event + # loop is re-entered during async generator fixture teardown. + if self._runner_task is not None and not self._runner_task.done(): + self._runner_task.cancel() + self._send_stream.close() + try: + self.get_loop().run_until_complete(self._runner_task) + except CancelledError: + pass + finally: + self._runner_task = None + raise + self._raise_async_exceptions() + + +class _ProcessStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol): + """ + A subprocess protocol that allows us to be notified of ``process_exited`` + + asyncio's own ``Process.wait()`` only resolves once every pipe transport has + disconnected so to get same semantics as on trio and uvloop we need this. + """ + + def __init__(self) -> None: + # Match the standard factory for asyncio.create_process + super().__init__(limit=2**16, loop=asyncio.get_running_loop()) + self.exited = asyncio.Event() + + def process_exited(self) -> None: + super().process_exited() + self.exited.set() + + +class AsyncIOBackend(AsyncBackend): + @classmethod + def run( + cls, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + options: dict[str, Any], + ) -> T_Retval: + @wraps(func) + async def wrapper() -> T_Retval: + task = cast(asyncio.Task, current_task()) + task.set_name(get_callable_name(func)) + _task_states[task] = TaskState(None, None) + + try: + return await func(*args) + finally: + del _task_states[task] + + debug = options.get("debug", None) + loop_factory = options.get("loop_factory", None) + if loop_factory is None and options.get("use_uvloop", False): + if sys.platform != "win32": + import uvloop + + loop_factory = uvloop.new_event_loop + else: + import winloop + + loop_factory = winloop.new_event_loop + + with Runner(debug=debug, loop_factory=loop_factory) as runner: + return runner.run(wrapper()) + + @classmethod + def current_token(cls) -> object: + return get_running_loop() + + @classmethod + def current_time(cls) -> float: + return get_running_loop().time() + + @classmethod + def cancelled_exception_class(cls) -> type[BaseException]: + return CancelledError + + @classmethod + async def checkpoint(cls) -> None: + await sleep(0) + + @classmethod + async def checkpoint_if_cancelled(cls) -> None: + task = current_task() + if task is None: + return + + try: + cancel_scope = _task_states[task].cancel_scope + except KeyError: + return + + while cancel_scope: + if cancel_scope.cancel_called: + await sleep(0) + elif cancel_scope.shield: + break + else: + cancel_scope = cancel_scope._parent_scope + + @classmethod + async def cancel_shielded_checkpoint(cls) -> None: + with CancelScope(shield=True): + await sleep(0) + + @classmethod + async def sleep(cls, delay: float) -> None: + await sleep(delay) + + @classmethod + def create_cancel_scope( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + return CancelScope(deadline=deadline, shield=shield) + + @classmethod + def current_effective_deadline(cls) -> float: + if (task := current_task()) is None: + return math.inf + + try: + cancel_scope = _task_states[task].cancel_scope + except KeyError: + return math.inf + + deadline = math.inf + while cancel_scope: + deadline = min(deadline, cancel_scope.deadline) + if cancel_scope._cancel_called: + deadline = -math.inf + break + elif cancel_scope.shield: + break + else: + cancel_scope = cancel_scope._parent_scope + + return deadline + + @classmethod + def create_task_group(cls) -> abc.TaskGroup: + return TaskGroup() + + @classmethod + def create_event(cls) -> BaseEvent: + return Event() + + @classmethod + def create_lock(cls, *, fast_acquire: bool) -> BaseLock: + return Lock(fast_acquire=fast_acquire) + + @classmethod + def create_semaphore( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> BaseSemaphore: + return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire) + + @classmethod + def create_capacity_limiter(cls, total_tokens: float) -> BaseCapacityLimiter: + return CapacityLimiter(total_tokens) + + @classmethod + async def run_sync_in_worker_thread( # type: ignore[return] + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + abandon_on_cancel: bool = False, + limiter: BaseCapacityLimiter | None = None, + ) -> T_Retval: + await cls.checkpoint() + + # If this is the first run in this event loop thread, set up the necessary + # variables + try: + idle_workers = _threadpool_idle_workers.get() + workers = _threadpool_workers.get() + except LookupError: + idle_workers = deque() + workers = set() + _threadpool_idle_workers.set(idle_workers) + _threadpool_workers.set(workers) + + async with limiter or cls.current_default_thread_limiter(): + with CancelScope(shield=not abandon_on_cancel) as scope: + future = asyncio.Future[T_Retval]() + root_task = find_root_task() + if not idle_workers: + worker = WorkerThread(root_task, workers, idle_workers) + worker.start() + workers.add(worker) + root_task.add_done_callback(worker.stop, context=Context()) + else: + worker = idle_workers.pop() + + # Prune any other workers that have been idle for MAX_IDLE_TIME + # seconds or longer + now = cls.current_time() + while idle_workers: + if ( + now - idle_workers[0].idle_since + < WorkerThread.MAX_IDLE_TIME + ): + break + + expired_worker = idle_workers.popleft() + expired_worker.root_task.remove_done_callback( + expired_worker.stop + ) + expired_worker.stop() + + context = copy_context() + context.run(set_current_async_library, None) + if abandon_on_cancel or scope._parent_scope is None: + worker_scope = scope + else: + worker_scope = scope._parent_scope + + worker.queue.put_nowait((context, func, args, future, worker_scope)) + return await future + + @classmethod + def check_cancelled(cls) -> None: + scope: CancelScope | None = threadlocals.current_cancel_scope + while scope is not None: + if scope.cancel_called: + raise CancelledError(f"Cancelled via cancel scope {id(scope):x}") + + if scope.shield: + return + + scope = scope._parent_scope + + @classmethod + def run_async_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_co: + async def task_wrapper() -> T_co: + __tracebackhide__ = True + if scope is not None: + task = cast(asyncio.Task, current_task()) + _task_states[task] = TaskState(None, scope) + scope._tasks.add(task) + try: + return await func(*args) + except CancelledError as exc: + raise concurrent.futures.CancelledError(str(exc)) from None + finally: + if scope is not None: + scope._tasks.discard(task) + + loop = cast( + "AbstractEventLoop", token or threadlocals.current_token.native_token + ) + if loop.is_closed(): + raise RunFinishedError + + context = copy_context() + context.run(set_current_async_library, "asyncio") + scope = getattr(threadlocals, "current_cancel_scope", None) + f: concurrent.futures.Future[T_co] = context.run( + asyncio.run_coroutine_threadsafe, task_wrapper(), loop=loop + ) + return f.result() + + @classmethod + def run_sync_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_Retval: + @wraps(func) + def wrapper() -> None: + try: + set_current_async_library("asyncio") + f.set_result(func(*args)) + except BaseException as exc: + f.set_exception(exc) + if not isinstance(exc, Exception): + raise + + loop = cast( + "AbstractEventLoop", token or threadlocals.current_token.native_token + ) + if loop.is_closed(): + raise RunFinishedError + + f: concurrent.futures.Future[T_Retval] = Future() + loop.call_soon_threadsafe(wrapper) + return f.result() + + @classmethod + async def open_process( + cls, + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None, + stdout: int | IO[Any] | None, + stderr: int | IO[Any] | None, + **kwargs: Any, + ) -> Process: + await cls.checkpoint() + if isinstance(command, PathLike): + command = os.fspath(command) + + # Use loop.subprocess_shell()/subprocess_exec() rather than their + # asyncio.create_subprocess_*() counterparts to get access to + # transport/protocol. + loop = asyncio.get_running_loop() + if isinstance(command, (str, bytes)): + transport, protocol = await loop.subprocess_shell( + _ProcessStreamProtocol, + command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + **kwargs, + ) + else: + transport, protocol = await loop.subprocess_exec( + _ProcessStreamProtocol, + *command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + **kwargs, + ) + + process = asyncio.subprocess.Process(transport, protocol, loop) + stdin_stream = StreamWriterWrapper(process.stdin) if process.stdin else None + stdout_stream = StreamReaderWrapper(process.stdout) if process.stdout else None + stderr_stream = StreamReaderWrapper(process.stderr) if process.stderr else None + return Process( + process, + stdin_stream, + stdout_stream, + stderr_stream, + protocol.exited, + transport, + ) + + @classmethod + def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: + create_task( + _shutdown_process_pool_on_exit(workers), + name="AnyIO process pool shutdown task", + ) + find_root_task().add_done_callback( + partial(_forcibly_shutdown_process_pool_on_exit, workers) # type:ignore[arg-type] + ) + + @classmethod + async def connect_tcp( + cls, host: str, port: int, local_address: IPSockAddrType | None = None + ) -> abc.SocketStream: + transport, protocol = cast( + tuple[asyncio.Transport, StreamProtocol], + await get_running_loop().create_connection( + StreamProtocol, host, port, local_addr=local_address + ), + ) + transport.pause_reading() + return SocketStream(transport, protocol) + + @classmethod + async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: + await cls.checkpoint() + loop = get_running_loop() + raw_socket = socket.socket(socket.AF_UNIX) + raw_socket.setblocking(False) + while True: + try: + raw_socket.connect(path) + except BlockingIOError: + f: asyncio.Future = asyncio.Future() + loop.add_writer(raw_socket, f.set_result, None) + f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) + await f + except BaseException: + raw_socket.close() + raise + else: + return UNIXSocketStream(raw_socket) + + @classmethod + def create_tcp_listener(cls, sock: socket.socket) -> SocketListener: + return TCPSocketListener(sock) + + @classmethod + def create_unix_listener(cls, sock: socket.socket) -> SocketListener: + return UNIXSocketListener(sock) + + @classmethod + async def create_udp_socket( + cls, + family: AddressFamily, + local_address: IPSockAddrType | None, + remote_address: IPSockAddrType | None, + reuse_port: bool, + ) -> UDPSocket | ConnectedUDPSocket: + transport, protocol = await get_running_loop().create_datagram_endpoint( + DatagramProtocol, + local_addr=local_address, + remote_addr=remote_address, + family=family, + reuse_port=reuse_port, + ) + if protocol.exception: + transport.close() + raise protocol.exception + + if not remote_address: + return UDPSocket(transport, protocol) + else: + return ConnectedUDPSocket(transport, protocol) + + @classmethod + async def create_unix_datagram_socket( # type: ignore[override] + cls, raw_socket: socket.socket, remote_path: str | bytes | None + ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: + await cls.checkpoint() + loop = get_running_loop() + + if remote_path: + while True: + try: + raw_socket.connect(remote_path) + except BlockingIOError: + f: asyncio.Future = asyncio.Future() + loop.add_writer(raw_socket, f.set_result, None) + f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) + await f + except BaseException: + raw_socket.close() + raise + else: + return ConnectedUNIXDatagramSocket(raw_socket) + else: + return UNIXDatagramSocket(raw_socket) + + @classmethod + async def getaddrinfo( + cls, + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, + ) -> Sequence[ + tuple[ + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], + ] + ]: + return await get_running_loop().getaddrinfo( + host, port, family=family, type=type, proto=proto, flags=flags + ) + + @classmethod + async def getnameinfo( + cls, sockaddr: IPSockAddrType, flags: int = 0 + ) -> tuple[str, str]: + return await get_running_loop().getnameinfo(sockaddr, flags) + + @classmethod + async def wait_readable(cls, obj: FileDescriptorLike) -> None: + try: + read_events = _read_events.get() + except LookupError: + read_events = {} + _read_events.set(read_events) + + fd = obj if isinstance(obj, int) else obj.fileno() + if read_events.get(fd): + raise BusyResourceError("reading from") + + loop = get_running_loop() + fut: asyncio.Future[bool] = loop.create_future() + + def cb() -> None: + try: + del read_events[fd] + except KeyError: + pass + else: + remove_reader(fd) + + try: + fut.set_result(True) + except asyncio.InvalidStateError: + pass + + try: + loop.add_reader(fd, cb) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + selector = get_selector() + selector.add_reader(fd, cb) + remove_reader = selector.remove_reader + else: + remove_reader = loop.remove_reader + + read_events[fd] = fut + try: + success = await fut + finally: + try: + del read_events[fd] + except KeyError: + pass + else: + remove_reader(fd) + + if not success: + raise ClosedResourceError + + @classmethod + async def wait_writable(cls, obj: FileDescriptorLike) -> None: + try: + write_events = _write_events.get() + except LookupError: + write_events = {} + _write_events.set(write_events) + + fd = obj if isinstance(obj, int) else obj.fileno() + if write_events.get(fd): + raise BusyResourceError("writing to") + + loop = get_running_loop() + fut: asyncio.Future[bool] = loop.create_future() + + def cb() -> None: + try: + del write_events[fd] + except KeyError: + pass + else: + remove_writer(fd) + + try: + fut.set_result(True) + except asyncio.InvalidStateError: + pass + + try: + loop.add_writer(fd, cb) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + selector = get_selector() + selector.add_writer(fd, cb) + remove_writer = selector.remove_writer + else: + remove_writer = loop.remove_writer + + write_events[fd] = fut + try: + success = await fut + finally: + try: + del write_events[fd] + except KeyError: + pass + else: + remove_writer(fd) + + if not success: + raise ClosedResourceError + + @classmethod + def notify_closing(cls, obj: FileDescriptorLike) -> None: + fd = obj if isinstance(obj, int) else obj.fileno() + loop = get_running_loop() + + try: + write_events = _write_events.get() + except LookupError: + pass + else: + try: + fut = write_events.pop(fd) + except KeyError: + pass + else: + try: + fut.set_result(False) + except asyncio.InvalidStateError: + pass + + try: + loop.remove_writer(fd) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + get_selector().remove_writer(fd) + + try: + read_events = _read_events.get() + except LookupError: + pass + else: + try: + fut = read_events.pop(fd) + except KeyError: + pass + else: + try: + fut.set_result(False) + except asyncio.InvalidStateError: + pass + + try: + loop.remove_reader(fd) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + get_selector().remove_reader(fd) + + @classmethod + async def wrap_listener_socket(cls, sock: socket.socket) -> SocketListener: + if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX: + return UNIXSocketListener(sock) + + return TCPSocketListener(sock) + + @classmethod + async def wrap_stream_socket(cls, sock: socket.socket) -> SocketStream: + transport, protocol = await get_running_loop().create_connection( + StreamProtocol, sock=sock + ) + return SocketStream(transport, protocol) + + @classmethod + async def wrap_unix_stream_socket(cls, sock: socket.socket) -> UNIXSocketStream: + return UNIXSocketStream(sock) + + @classmethod + async def wrap_udp_socket(cls, sock: socket.socket) -> UDPSocket: + transport, protocol = await get_running_loop().create_datagram_endpoint( + DatagramProtocol, sock=sock + ) + return UDPSocket(transport, protocol) + + @classmethod + async def wrap_connected_udp_socket(cls, sock: socket.socket) -> ConnectedUDPSocket: + transport, protocol = await get_running_loop().create_datagram_endpoint( + DatagramProtocol, sock=sock + ) + return ConnectedUDPSocket(transport, protocol) + + @classmethod + async def wrap_unix_datagram_socket(cls, sock: socket.socket) -> UNIXDatagramSocket: + return UNIXDatagramSocket(sock) + + @classmethod + async def wrap_connected_unix_datagram_socket( + cls, sock: socket.socket + ) -> ConnectedUNIXDatagramSocket: + return ConnectedUNIXDatagramSocket(sock) + + @classmethod + def current_default_thread_limiter(cls) -> CapacityLimiter: + try: + return _default_thread_limiter.get() + except LookupError: + limiter = CapacityLimiter(40) + _default_thread_limiter.set(limiter) + return limiter + + @classmethod + def open_signal_receiver( + cls, *signals: Signals + ) -> AbstractContextManager[AsyncIterator[Signals]]: + return _SignalReceiver(signals) + + @classmethod + def get_current_task(cls) -> TaskInfo: + return AsyncIOTaskInfo(current_task()) # type: ignore[arg-type] + + @classmethod + def get_running_tasks(cls) -> Sequence[TaskInfo]: + return [AsyncIOTaskInfo(task) for task in all_tasks() if not task.done()] + + @classmethod + async def wait_all_tasks_blocked(cls) -> None: + await cls.checkpoint() + this_task = current_task() + while True: + for task in all_tasks(): + if task is this_task: + continue + + waiter = task._fut_waiter # type: ignore[attr-defined] + if waiter is None or waiter.done(): + await sleep(0.1) + break + else: + return + + @classmethod + def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: + return TestRunner(**options) + + +backend_class = AsyncIOBackend diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_backends/_trio.py b/.venv-test/lib/python3.12/site-packages/anyio/_backends/_trio.py new file mode 100644 index 0000000..2e67786 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_backends/_trio.py @@ -0,0 +1,1473 @@ +from __future__ import annotations + +import array +import math +import os +import socket +import sys +import types +import weakref +from collections.abc import ( + AsyncGenerator, + AsyncIterator, + Awaitable, + Callable, + Collection, + Coroutine, + Iterable, + Sequence, +) +from contextlib import AbstractContextManager +from contextvars import Context +from dataclasses import dataclass +from functools import partial, wraps +from io import IOBase +from os import PathLike +from signal import Signals +from socket import AddressFamily, SocketKind +from types import TracebackType +from typing import ( + IO, + TYPE_CHECKING, + Any, + Generic, + Literal, + NoReturn, + ParamSpec, + TypeVar, + cast, + overload, +) + +import trio.from_thread +import trio.lowlevel +from outcome import Error, Outcome, Value +from trio.lowlevel import ( + current_root_task, + current_task, + notify_closing, + wait_readable, + wait_writable, +) +from trio.socket import SocketType as TrioSocketType +from trio.to_thread import run_sync + +from .. import ( + CapacityLimiterStatistics, + EventStatistics, + LockStatistics, + RunFinishedError, + TaskInfo, + WouldBlock, + abc, +) +from .._core._eventloop import claim_worker_thread +from .._core._exceptions import ( + BrokenResourceError, + BusyResourceError, + ClosedResourceError, + EndOfStream, +) +from .._core._sockets import convert_ipv6_sockaddr +from .._core._streams import create_memory_object_stream +from .._core._synchronization import ( + CapacityLimiter as BaseCapacityLimiter, +) +from .._core._synchronization import Event as BaseEvent +from .._core._synchronization import Lock as BaseLock +from .._core._synchronization import ( + ResourceGuard, + SemaphoreStatistics, +) +from .._core._synchronization import Semaphore as BaseSemaphore +from .._core._tasks import CancelScope as BaseCancelScope +from .._core._tasks import TaskHandle +from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType +from ..abc._eventloop import AsyncBackend, StrOrBytesPath +from ..abc._tasks import T_contra, call_for_coroutine, get_callable_name, get_coro_name + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + + from ..streams.memory import MemoryObjectSendStream + +if sys.version_info < (3, 15): + from typing_extensions import sentinel + +if sys.version_info >= (3, 11): + from typing import Self, TypeVarTuple, Unpack +else: + from exceptiongroup import BaseExceptionGroup + from typing_extensions import Self, TypeVarTuple, Unpack + +T = TypeVar("T") +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType) +PosArgsT = TypeVarTuple("PosArgsT") +P = ParamSpec("P") + + +def ensure_returns_coro( + func: Callable[P, Awaitable[T_Retval]], +) -> Callable[P, Coroutine[Any, Any, T_Retval]]: + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> Coroutine[Any, Any, T_Retval]: + awaitable = func(*args, **kwargs) + # Check the common case first. + if isinstance(awaitable, Coroutine): + return awaitable + elif not isinstance(awaitable, Awaitable): + # The user violated the type annotations. Still, we should pass this on to + # Trio so it can raise with an appropriate message. + return awaitable + else: + + @wraps(func) + async def inner_wrapper() -> T_Retval: + return await awaitable + + return inner_wrapper() + + return wrapper + + +# +# Event loop +# + +RunVar = trio.lowlevel.RunVar + + +# +# Timeouts and cancellation +# + + +class CancelScope(BaseCancelScope): + __slots__ = ("__original",) + + def __new__( + cls, original: trio.CancelScope | None = None, **kwargs: object + ) -> Self: + return object.__new__(cls) + + def __init__(self, original: trio.CancelScope | None = None, **kwargs: Any) -> None: + self.__original = original or trio.CancelScope(**kwargs) + + def __enter__(self) -> Self: + self.__original.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + return self.__original.__exit__(exc_type, exc_val, exc_tb) + + def cancel(self, reason: str | None = None) -> None: + self.__original.cancel(reason) + + @property + def deadline(self) -> float: + return self.__original.deadline + + @deadline.setter + def deadline(self, value: float) -> None: + self.__original.deadline = value + + @property + def cancel_called(self) -> bool: + return self.__original.cancel_called + + @property + def cancelled_caught(self) -> bool: + return self.__original.cancelled_caught + + @property + def shield(self) -> bool: + return self.__original.shield + + @shield.setter + def shield(self, value: bool) -> None: + self.__original.shield = value + + +# +# Task groups +# + +empty_start_value = sentinel("empty_start_value") + + +class _TrioTaskStatus(abc.TaskStatus[T_contra], Generic[T_contra]): + early_start_value: T_contra | object = empty_start_value + real_task_status: trio.TaskStatus[T_contra | None] | None = None + + def started(self, value: T_contra | None = None) -> None: + if self.real_task_status is None: + if self.early_start_value is not empty_start_value: + raise RuntimeError("called 'started' twice on the same task status") + + self.early_start_value = value + else: + self.real_task_status.started(value) + + +class TaskGroup(abc.TaskGroup): + def __init__(self) -> None: + self._entered = False + self._active = False + self._nursery_manager = trio.open_nursery(strict_exception_groups=True) + self.cancel_scope = None # type: ignore[assignment] + + async def __aenter__(self) -> Self: + if self._entered: + raise RuntimeError("TaskGroup cannot be entered more than once") + + self._entered = True + self._active = True + self._nursery = await self._nursery_manager.__aenter__() + self.cancel_scope = CancelScope(self._nursery.cancel_scope) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + try: + # trio.Nursery.__exit__ returns bool; .open_nursery has wrong type + return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb) # type: ignore[return-value] + except BaseExceptionGroup as exc: + if not exc.split(trio.Cancelled)[1]: + raise trio.Cancelled._create() from exc + + raise + finally: + del exc_val, exc_tb + self._active = False + + def _check_active(self, coro: Coroutine | None = None) -> None: + if not self._active: + if coro is not None: + coro.close() + + raise RuntimeError( + "This task group is not active; no new tasks can be started." + ) + + def create_task( + self, + coro: Coroutine[Any, Any, T_co], + *, + name: object = None, + context: Context | None = None, + ) -> TaskHandle[T_co]: + if not isinstance(coro, Coroutine): + raise TypeError(f"expected a coroutine, got {coro.__class__.__qualname__}") + + self._check_active(coro) + final_name = get_coro_name(coro, name) + handle = TaskHandle(coro, final_name) + if context is not None: + context.run( + partial(self._nursery.start_soon, handle._run_coro, name=handle.name) + ) + else: + self._nursery.start_soon(handle._run_coro, name=handle.name) + + return handle + + async def start( + self, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + name: object = None, + return_handle: Literal[False, True] = False, + ) -> Any: + handle: TaskHandle[T_co] + + async def run_coro_with_task_status( + *, task_status: trio.TaskStatus[Any] + ) -> None: + nonlocal handle + wrapper_task_status = _TrioTaskStatus() + coro = call_for_coroutine(func, args, task_status=wrapper_task_status) + if wrapper_task_status.early_start_value is not empty_start_value: + task_status.started(wrapper_task_status.early_start_value) + else: + wrapper_task_status.real_task_status = task_status + + handle = TaskHandle(coro, final_name) + await handle._run_coro() + + self._check_active() + final_name = get_callable_name(func, name) + start_value = await self._nursery.start( + run_coro_with_task_status, name=final_name + ) + if return_handle: + handle._start_value = start_value + return handle + else: + return start_value + + +# +# Subprocesses +# + + +@dataclass(eq=False) +class ReceiveStreamWrapper(abc.ByteReceiveStream): + _stream: trio.abc.ReceiveStream + + async def receive(self, max_bytes: int | None = None) -> bytes: + if max_bytes is not None and max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + try: + data = await self._stream.receive_some(max_bytes) + except trio.ClosedResourceError as exc: + raise ClosedResourceError from exc.__cause__ + except trio.BrokenResourceError as exc: + raise BrokenResourceError from exc.__cause__ + + if data: + return bytes(data) + else: + raise EndOfStream + + async def aclose(self) -> None: + await self._stream.aclose() + + +@dataclass(eq=False) +class SendStreamWrapper(abc.ByteSendStream): + _stream: trio.abc.SendStream + + async def send(self, item: bytes) -> None: + try: + await self._stream.send_all(item) + except trio.ClosedResourceError as exc: + raise ClosedResourceError from exc.__cause__ + except trio.BrokenResourceError as exc: + raise BrokenResourceError from exc.__cause__ + + async def aclose(self) -> None: + await self._stream.aclose() + + +@dataclass(eq=False) +class Process(abc.Process): + _process: trio.Process + _stdin: abc.ByteSendStream | None + _stdout: abc.ByteReceiveStream | None + _stderr: abc.ByteReceiveStream | None + + async def aclose(self) -> None: + with CancelScope(shield=True): + if self._stdin: + await self._stdin.aclose() + if self._stdout: + await self._stdout.aclose() + if self._stderr: + await self._stderr.aclose() + + try: + await self.wait() + except BaseException: + self.kill() + with CancelScope(shield=True): + await self.wait() + raise + + async def wait(self) -> int: + return await self._process.wait() + + def terminate(self) -> None: + self._process.terminate() + + def kill(self) -> None: + self._process.kill() + + def send_signal(self, signal: Signals) -> None: + self._process.send_signal(signal) + + @property + def pid(self) -> int: + return self._process.pid + + @property + def returncode(self) -> int | None: + return self._process.returncode + + @property + def stdin(self) -> abc.ByteSendStream | None: + return self._stdin + + @property + def stdout(self) -> abc.ByteReceiveStream | None: + return self._stdout + + @property + def stderr(self) -> abc.ByteReceiveStream | None: + return self._stderr + + +class _ProcessPoolShutdownInstrument(trio.abc.Instrument): + def after_run(self) -> None: + super().after_run() + + +current_default_worker_process_limiter: trio.lowlevel.RunVar = RunVar( + "current_default_worker_process_limiter" +) + + +async def _shutdown_process_pool(workers: set[abc.Process]) -> None: + try: + await trio.sleep(math.inf) + except trio.Cancelled: + for process in workers: + if process.returncode is None: + process.kill() + + with CancelScope(shield=True): + for process in workers: + await process.aclose() + + +# +# Sockets and networking +# + + +class _TrioSocketMixin(Generic[T_SockAddr]): + def __init__(self, trio_socket: TrioSocketType) -> None: + self._trio_socket = trio_socket + self._closed = False + + def _check_closed(self) -> None: + if self._closed: + raise ClosedResourceError + if self._trio_socket.fileno() < 0: + raise BrokenResourceError + + @property + def _raw_socket(self) -> socket.socket: + return self._trio_socket._sock # type: ignore[attr-defined] + + async def aclose(self) -> None: + if self._trio_socket.fileno() >= 0: + self._closed = True + self._trio_socket.close() + + def _convert_socket_error(self, exc: BaseException) -> NoReturn: + if isinstance(exc, trio.ClosedResourceError): + raise ClosedResourceError from exc + elif self._trio_socket.fileno() < 0 and self._closed: + raise ClosedResourceError from None + elif isinstance(exc, OSError): + raise BrokenResourceError from exc + else: + raise exc + + +class SocketStream(_TrioSocketMixin, abc.SocketStream): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + with self._receive_guard: + try: + data = await self._trio_socket.recv(max_bytes) + except BaseException as exc: + self._convert_socket_error(exc) + + if data: + return data + else: + raise EndOfStream + + async def send(self, item: bytes) -> None: + with self._send_guard: + view = memoryview(item) + while view: + try: + bytes_sent = await self._trio_socket.send(view) + except BaseException as exc: + self._convert_socket_error(exc) + + view = view[bytes_sent:] + + async def send_eof(self) -> None: + self._trio_socket.shutdown(socket.SHUT_WR) + + +class UNIXSocketStream(SocketStream, abc.UNIXSocketStream): + async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: + if not isinstance(msglen, int) or msglen < 0: + raise ValueError("msglen must be a non-negative integer") + if not isinstance(maxfds, int) or maxfds < 1: + raise ValueError("maxfds must be a positive integer") + + fds = array.array("i") + await trio.lowlevel.checkpoint() + with self._receive_guard: + while True: + try: + message, ancdata, _flags, _addr = await self._trio_socket.recvmsg( + msglen, socket.CMSG_LEN(maxfds * fds.itemsize) + ) + except BaseException as exc: + self._convert_socket_error(exc) + else: + if not message and not ancdata: + raise EndOfStream + + break + + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: + raise RuntimeError( + f"Received unexpected ancillary data; message = {message!r}, " + f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" + ) + + fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) + + return message, list(fds) + + async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: + if not message: + raise ValueError("message must not be empty") + if not fds: + raise ValueError("fds must not be empty") + + filenos: list[int] = [] + for fd in fds: + if isinstance(fd, int): + filenos.append(fd) + elif isinstance(fd, IOBase): + filenos.append(fd.fileno()) + + fdarray = array.array("i", filenos) + await trio.lowlevel.checkpoint() + with self._send_guard: + while True: + try: + await self._trio_socket.sendmsg( + [message], + [ + ( + socket.SOL_SOCKET, + socket.SCM_RIGHTS, + fdarray, + ) + ], + ) + break + except BaseException as exc: + self._convert_socket_error(exc) + + +class TCPSocketListener(_TrioSocketMixin, abc.SocketListener): + def __init__(self, raw_socket: socket.socket): + super().__init__(trio.socket.from_stdlib_socket(raw_socket)) + self._accept_guard = ResourceGuard("accepting connections from") + + async def accept(self) -> SocketStream: + with self._accept_guard: + try: + trio_socket, _addr = await self._trio_socket.accept() + except BaseException as exc: + self._convert_socket_error(exc) + + trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + return SocketStream(trio_socket) + + +class UNIXSocketListener(_TrioSocketMixin, abc.SocketListener): + def __init__(self, raw_socket: socket.socket): + super().__init__(trio.socket.from_stdlib_socket(raw_socket)) + self._accept_guard = ResourceGuard("accepting connections from") + + async def accept(self) -> UNIXSocketStream: + with self._accept_guard: + try: + trio_socket, _addr = await self._trio_socket.accept() + except BaseException as exc: + self._convert_socket_error(exc) + + return UNIXSocketStream(trio_socket) + + +class UDPSocket(_TrioSocketMixin[IPSockAddrType], abc.UDPSocket): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> tuple[bytes, IPSockAddrType]: + with self._receive_guard: + try: + data, addr = await self._trio_socket.recvfrom(65536) + return data, convert_ipv6_sockaddr(addr) + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: UDPPacketType) -> None: + with self._send_guard: + try: + await self._trio_socket.sendto(*item) + except BaseException as exc: + self._convert_socket_error(exc) + + +class ConnectedUDPSocket(_TrioSocketMixin[IPSockAddrType], abc.ConnectedUDPSocket): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> bytes: + with self._receive_guard: + try: + return await self._trio_socket.recv(65536) + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: bytes) -> None: + with self._send_guard: + try: + await self._trio_socket.send(item) + except BaseException as exc: + self._convert_socket_error(exc) + + +class UNIXDatagramSocket(_TrioSocketMixin[str], abc.UNIXDatagramSocket): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> UNIXDatagramPacketType: + with self._receive_guard: + try: + data, addr = await self._trio_socket.recvfrom(65536) + return data, addr + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: UNIXDatagramPacketType) -> None: + with self._send_guard: + try: + await self._trio_socket.sendto(*item) + except BaseException as exc: + self._convert_socket_error(exc) + + +class ConnectedUNIXDatagramSocket( + _TrioSocketMixin[str], abc.ConnectedUNIXDatagramSocket +): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> bytes: + with self._receive_guard: + try: + return await self._trio_socket.recv(65536) + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: bytes) -> None: + with self._send_guard: + try: + await self._trio_socket.send(item) + except BaseException as exc: + self._convert_socket_error(exc) + + +# +# Synchronization +# + + +class Event(BaseEvent): + __slots__ = ("__original",) + + def __new__(cls) -> Self: + return object.__new__(cls) + + def __init__(self) -> None: + self.__original = trio.Event() + + def is_set(self) -> bool: + return self.__original.is_set() + + async def wait(self) -> None: + return await self.__original.wait() + + def statistics(self) -> EventStatistics: + orig_statistics = self.__original.statistics() + return EventStatistics(tasks_waiting=orig_statistics.tasks_waiting) + + def set(self) -> None: + self.__original.set() + + +class Lock(BaseLock): + __slots__ = "__original", "_fast_acquire" + + def __new__(cls, *, fast_acquire: bool = False) -> Self: + return object.__new__(cls) + + def __init__(self, *, fast_acquire: bool = False) -> None: + self._fast_acquire = fast_acquire + self.__original = trio.Lock() + + @staticmethod + def _convert_runtime_error_msg(exc: RuntimeError) -> None: + if exc.args == ("attempt to re-acquire an already held Lock",): + exc.args = ("Attempted to acquire an already held Lock",) + + async def acquire(self) -> None: + if not self._fast_acquire: + try: + await self.__original.acquire() + except RuntimeError as exc: + self._convert_runtime_error_msg(exc) + raise + + return + + # This is the "fast path" where we don't let other tasks run + await trio.lowlevel.checkpoint_if_cancelled() + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + await self.__original._lot.park() + except RuntimeError as exc: + self._convert_runtime_error_msg(exc) + raise + + def acquire_nowait(self) -> None: + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + raise WouldBlock from None + except RuntimeError as exc: + self._convert_runtime_error_msg(exc) + raise + + def locked(self) -> bool: + return self.__original.locked() + + def release(self) -> None: + self.__original.release() + + def statistics(self) -> LockStatistics: + orig_statistics = self.__original.statistics() + owner = TrioTaskInfo(orig_statistics.owner) if orig_statistics.owner else None + return LockStatistics( + orig_statistics.locked, owner, orig_statistics.tasks_waiting + ) + + +class Semaphore(BaseSemaphore): + __slots__ = ("__original",) + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Self: + return object.__new__(cls) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> None: + super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire) + self.__original = trio.Semaphore(initial_value, max_value=max_value) + + async def acquire(self) -> None: + if not self._fast_acquire: + await self.__original.acquire() + return + + # This is the "fast path" where we don't let other tasks run + await trio.lowlevel.checkpoint_if_cancelled() + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + await self.__original._lot.park() + + def acquire_nowait(self) -> None: + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + raise WouldBlock from None + + @property + def max_value(self) -> int | None: + return self.__original.max_value + + @property + def value(self) -> int: + return self.__original.value + + def release(self) -> None: + self.__original.release() + + def statistics(self) -> SemaphoreStatistics: + orig_statistics = self.__original.statistics() + return SemaphoreStatistics(orig_statistics.tasks_waiting) + + +class CapacityLimiter(BaseCapacityLimiter): + __slots__ = ("__original",) + + def __new__( + cls, + total_tokens: float | None = None, + *, + original: trio.CapacityLimiter | None = None, + ) -> Self: + return object.__new__(cls) + + def __init__( + self, + total_tokens: float | None = None, + *, + original: trio.CapacityLimiter | None = None, + ) -> None: + if original is not None: + self.__original = original + else: + assert total_tokens is not None + self.__original = trio.CapacityLimiter(total_tokens) + + async def __aenter__(self) -> None: + return await self.__original.__aenter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.__original.__aexit__(exc_type, exc_val, exc_tb) + + @property + def total_tokens(self) -> float: + return self.__original.total_tokens + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + self.__original.total_tokens = value + + @property + def borrowed_tokens(self) -> int: + return self.__original.borrowed_tokens + + @property + def available_tokens(self) -> float: + return self.__original.available_tokens + + def acquire_nowait(self) -> None: + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + raise WouldBlock from None + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + try: + self.__original.acquire_on_behalf_of_nowait(borrower) + except trio.WouldBlock: + raise WouldBlock from None + + async def acquire(self) -> None: + await self.__original.acquire() + + async def acquire_on_behalf_of(self, borrower: object) -> None: + await self.__original.acquire_on_behalf_of(borrower) + + def release(self) -> None: + return self.__original.release() + + def release_on_behalf_of(self, borrower: object) -> None: + return self.__original.release_on_behalf_of(borrower) + + def statistics(self) -> CapacityLimiterStatistics: + orig = self.__original.statistics() + return CapacityLimiterStatistics( + borrowed_tokens=orig.borrowed_tokens, + total_tokens=orig.total_tokens, + borrowers=tuple(orig.borrowers), + tasks_waiting=orig.tasks_waiting, + ) + + +_capacity_limiter_wrapper: trio.lowlevel.RunVar = RunVar("_capacity_limiter_wrapper") + + +# +# Signal handling +# + + +class _SignalReceiver: + _iterator: AsyncIterator[int] + + def __init__(self, signals: tuple[Signals, ...]): + self._signals = signals + + def __enter__(self) -> Self: + self._cm = trio.open_signal_receiver(*self._signals) + self._iterator = self._cm.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + return self._cm.__exit__(exc_type, exc_val, exc_tb) + + def __aiter__(self) -> _SignalReceiver: + return self + + async def __anext__(self) -> Signals: + signum = await self._iterator.__anext__() + return Signals(signum) + + +# +# Testing and debugging +# + + +class TestRunner(abc.TestRunner): + def __init__(self, **options: Any) -> None: + from queue import Queue + + self._call_queue: Queue[Callable[[], object]] = Queue() + self._send_stream: ( + MemoryObjectSendStream[tuple[Awaitable[Any], list[Outcome]]] | None + ) = None + self._options = options + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + if self._send_stream: + self._send_stream.close() + while self._send_stream is not None: + self._call_queue.get()() + + def is_running(self) -> bool: + return trio.lowlevel.in_trio_task() + + async def _run_tests_and_fixtures(self) -> None: + self._send_stream, receive_stream = create_memory_object_stream[ + tuple[Awaitable[Any], list[Outcome]] + ](1) + with receive_stream: + async for awaitable, outcome_holder in receive_stream: + try: + retval = await awaitable + except BaseException as exc: + outcome_holder.append(Error(exc)) + else: + outcome_holder.append(Value(retval)) + + def _main_task_finished(self, outcome: object) -> None: + self._send_stream = None + + def _call_in_runner_task( + self, + func: Callable[P, Awaitable[T_Retval]], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> T_Retval: + if self._send_stream is None: + trio.lowlevel.start_guest_run( + self._run_tests_and_fixtures, + run_sync_soon_threadsafe=self._call_queue.put, + done_callback=self._main_task_finished, + **self._options, + ) + while self._send_stream is None: + self._call_queue.get()() + + outcome_holder: list[Outcome] = [] + self._send_stream.send_nowait((func(*args, **kwargs), outcome_holder)) + while not outcome_holder: + self._call_queue.get()() + + return outcome_holder[0].unwrap() + + def run_asyncgen_fixture( + self, + fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], + kwargs: dict[str, Any], + ) -> Iterable[T_Retval]: + asyncgen = fixture_func(**kwargs) + fixturevalue: T_Retval = self._call_in_runner_task(asyncgen.asend, None) + + yield fixturevalue + + try: + self._call_in_runner_task(asyncgen.asend, None) + except StopAsyncIteration: + pass + else: + self._call_in_runner_task(asyncgen.aclose) + raise RuntimeError("Async generator fixture did not stop") + + def run_fixture( + self, + fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], + kwargs: dict[str, Any], + ) -> T_Retval: + return self._call_in_runner_task(fixture_func, **kwargs) + + def run_test( + self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] + ) -> None: + self._call_in_runner_task(test_func, **kwargs) + + +class TrioTaskInfo(TaskInfo): + def __init__(self, task: trio.lowlevel.Task): + parent_id = None + if task.parent_nursery and task.parent_nursery.parent_task: + parent_id = id(task.parent_nursery.parent_task) + + super().__init__(id(task), parent_id, task.name, task.coro) + self._task = weakref.proxy(task) + + def has_pending_cancellation(self) -> bool: + try: + return self._task._cancel_status.effectively_cancelled + except ReferenceError: + # If the task is no longer around, it surely doesn't have a cancellation + # pending + return False + + +class TrioBackend(AsyncBackend): + @classmethod + def run( + cls, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + options: dict[str, Any], + ) -> T_Retval: + assert not kwargs, "unreachable, and not supported by Trio" + return trio.run(ensure_returns_coro(func), *args, **options) + + @classmethod + def current_token(cls) -> object: + return trio.lowlevel.current_trio_token() + + @classmethod + def current_time(cls) -> float: + return trio.current_time() + + @classmethod + def cancelled_exception_class(cls) -> type[BaseException]: + return trio.Cancelled + + @classmethod + async def checkpoint(cls) -> None: + await trio.lowlevel.checkpoint() + + @classmethod + async def checkpoint_if_cancelled(cls) -> None: + await trio.lowlevel.checkpoint_if_cancelled() + + @classmethod + async def cancel_shielded_checkpoint(cls) -> None: + await trio.lowlevel.cancel_shielded_checkpoint() + + @classmethod + async def sleep(cls, delay: float) -> None: + await trio.sleep(delay) + + @classmethod + def create_cancel_scope( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> BaseCancelScope: + return CancelScope(deadline=deadline, shield=shield) + + @classmethod + def current_effective_deadline(cls) -> float: + return trio.current_effective_deadline() + + @classmethod + def create_task_group(cls) -> abc.TaskGroup: + return TaskGroup() + + @classmethod + def create_event(cls) -> BaseEvent: + return Event() + + @classmethod + def create_lock(cls, *, fast_acquire: bool) -> Lock: + return Lock(fast_acquire=fast_acquire) + + @classmethod + def create_semaphore( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> BaseSemaphore: + return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire) + + @classmethod + def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: + return CapacityLimiter(total_tokens) + + @classmethod + async def run_sync_in_worker_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + abandon_on_cancel: bool = False, + limiter: BaseCapacityLimiter | None = None, + ) -> T_Retval: + def wrapper() -> T_Retval: + with claim_worker_thread(TrioBackend, token): + return func(*args) + + token = TrioBackend.current_token() + return await run_sync( + wrapper, + abandon_on_cancel=abandon_on_cancel, + limiter=cast(trio.CapacityLimiter, limiter), + ) + + @classmethod + def check_cancelled(cls) -> None: + trio.from_thread.check_cancelled() + + @classmethod + def run_async_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_co: + trio_token = cast("trio.lowlevel.TrioToken | None", token) + try: + return trio.from_thread.run(func, *args, trio_token=trio_token) + except trio.RunFinishedError: + raise RunFinishedError from None + + @classmethod + def run_sync_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_Retval: + trio_token = cast("trio.lowlevel.TrioToken | None", token) + try: + return trio.from_thread.run_sync(func, *args, trio_token=trio_token) + except trio.RunFinishedError: + raise RunFinishedError from None + + @classmethod + async def open_process( + cls, + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None, + stdout: int | IO[Any] | None, + stderr: int | IO[Any] | None, + **kwargs: Any, + ) -> Process: + def convert_item(item: StrOrBytesPath) -> str: + str_or_bytes = os.fspath(item) + if isinstance(str_or_bytes, str): + return str_or_bytes + else: + return os.fsdecode(str_or_bytes) + + if isinstance(command, (str, bytes, PathLike)): + process = await trio.lowlevel.open_process( + convert_item(command), + stdin=stdin, + stdout=stdout, + stderr=stderr, + shell=True, + **kwargs, + ) + else: + process = await trio.lowlevel.open_process( + [convert_item(item) for item in command], + stdin=stdin, + stdout=stdout, + stderr=stderr, + shell=False, + **kwargs, + ) + + stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None + stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None + stderr_stream = ReceiveStreamWrapper(process.stderr) if process.stderr else None + return Process(process, stdin_stream, stdout_stream, stderr_stream) + + @classmethod + def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: + trio.lowlevel.spawn_system_task(_shutdown_process_pool, workers) + + @classmethod + async def connect_tcp( + cls, host: str, port: int, local_address: IPSockAddrType | None = None + ) -> SocketStream: + family = socket.AF_INET6 if ":" in host else socket.AF_INET + trio_socket = trio.socket.socket(family) + trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + if local_address: + await trio_socket.bind(local_address) + + try: + await trio_socket.connect((host, port)) + except BaseException: + trio_socket.close() + raise + + return SocketStream(trio_socket) + + @classmethod + async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: + trio_socket = trio.socket.socket(socket.AF_UNIX) + try: + await trio_socket.connect(path) + except BaseException: + trio_socket.close() + raise + + return UNIXSocketStream(trio_socket) + + @classmethod + def create_tcp_listener(cls, sock: socket.socket) -> abc.SocketListener: + return TCPSocketListener(sock) + + @classmethod + def create_unix_listener(cls, sock: socket.socket) -> abc.SocketListener: + return UNIXSocketListener(sock) + + @classmethod + async def create_udp_socket( + cls, + family: socket.AddressFamily, + local_address: IPSockAddrType | None, + remote_address: IPSockAddrType | None, + reuse_port: bool, + ) -> UDPSocket | ConnectedUDPSocket: + trio_socket = trio.socket.socket(family=family, type=socket.SOCK_DGRAM) + + if reuse_port: + trio_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + + if local_address: + await trio_socket.bind(local_address) + + if remote_address: + await trio_socket.connect(remote_address) + return ConnectedUDPSocket(trio_socket) + else: + return UDPSocket(trio_socket) + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket.socket, remote_path: None + ) -> abc.UNIXDatagramSocket: ... + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket.socket, remote_path: str | bytes + ) -> abc.ConnectedUNIXDatagramSocket: ... + + @classmethod + async def create_unix_datagram_socket( + cls, raw_socket: socket.socket, remote_path: str | bytes | None + ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: + trio_socket = trio.socket.from_stdlib_socket(raw_socket) + + if remote_path: + await trio_socket.connect(remote_path) + return ConnectedUNIXDatagramSocket(trio_socket) + else: + return UNIXDatagramSocket(trio_socket) + + @classmethod + async def getaddrinfo( + cls, + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, + ) -> Sequence[ + tuple[ + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], + ] + ]: + return await trio.socket.getaddrinfo(host, port, family, type, proto, flags) + + @classmethod + async def getnameinfo( + cls, sockaddr: IPSockAddrType, flags: int = 0 + ) -> tuple[str, str]: + return await trio.socket.getnameinfo(sockaddr, flags) + + @classmethod + async def wait_readable(cls, obj: FileDescriptorLike) -> None: + try: + await wait_readable(obj) + except trio.ClosedResourceError as exc: + raise ClosedResourceError().with_traceback(exc.__traceback__) from None + except trio.BusyResourceError: + raise BusyResourceError("reading from") from None + + @classmethod + async def wait_writable(cls, obj: FileDescriptorLike) -> None: + try: + await wait_writable(obj) + except trio.ClosedResourceError as exc: + raise ClosedResourceError().with_traceback(exc.__traceback__) from None + except trio.BusyResourceError: + raise BusyResourceError("writing to") from None + + @classmethod + def notify_closing(cls, obj: FileDescriptorLike) -> None: + notify_closing(obj) + + @classmethod + async def wrap_listener_socket(cls, sock: socket.socket) -> abc.SocketListener: + if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX: + return UNIXSocketListener(sock) + + return TCPSocketListener(sock) + + @classmethod + async def wrap_stream_socket(cls, sock: socket.socket) -> SocketStream: + trio_sock = trio.socket.from_stdlib_socket(sock) + return SocketStream(trio_sock) + + @classmethod + async def wrap_unix_stream_socket(cls, sock: socket.socket) -> UNIXSocketStream: + trio_sock = trio.socket.from_stdlib_socket(sock) + return UNIXSocketStream(trio_sock) + + @classmethod + async def wrap_udp_socket(cls, sock: socket.socket) -> UDPSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return UDPSocket(trio_sock) + + @classmethod + async def wrap_connected_udp_socket(cls, sock: socket.socket) -> ConnectedUDPSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return ConnectedUDPSocket(trio_sock) + + @classmethod + async def wrap_unix_datagram_socket(cls, sock: socket.socket) -> UNIXDatagramSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return UNIXDatagramSocket(trio_sock) + + @classmethod + async def wrap_connected_unix_datagram_socket( + cls, sock: socket.socket + ) -> ConnectedUNIXDatagramSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return ConnectedUNIXDatagramSocket(trio_sock) + + @classmethod + def current_default_thread_limiter(cls) -> CapacityLimiter: + try: + return _capacity_limiter_wrapper.get() + except LookupError: + limiter = CapacityLimiter( + original=trio.to_thread.current_default_thread_limiter() + ) + _capacity_limiter_wrapper.set(limiter) + return limiter + + @classmethod + def open_signal_receiver( + cls, *signals: Signals + ) -> AbstractContextManager[AsyncIterator[Signals]]: + return _SignalReceiver(signals) + + @classmethod + def get_current_task(cls) -> TaskInfo: + task = current_task() + return TrioTaskInfo(task) + + @classmethod + def get_running_tasks(cls) -> Sequence[TaskInfo]: + root_task = current_root_task() + assert root_task + task_infos = [TrioTaskInfo(root_task)] + nurseries = root_task.child_nurseries + while nurseries: + new_nurseries: list[trio.Nursery] = [] + for nursery in nurseries: + for task in nursery.child_tasks: + task_infos.append(TrioTaskInfo(task)) + new_nurseries.extend(task.child_nurseries) + + nurseries = new_nurseries + + return task_infos + + @classmethod + async def wait_all_tasks_blocked(cls) -> None: + from trio.testing import wait_all_tasks_blocked + + await wait_all_tasks_blocked() + + @classmethod + def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: + return TestRunner(**options) + + +backend_class = TrioBackend diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/__init__.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py new file mode 100644 index 0000000..9f35bae --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import asyncio +import socket +import threading +from collections.abc import Callable +from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + +_selector_lock = threading.Lock() +_selector: Selector | None = None + + +class Selector: + def __init__(self) -> None: + self._thread = threading.Thread(target=self.run, name="AnyIO socket selector") + self._selector = DefaultSelector() + self._send, self._receive = socket.socketpair() + self._send.setblocking(False) + self._receive.setblocking(False) + # This somewhat reduces the amount of memory wasted queueing up data + # for wakeups. With these settings, maximum number of 1-byte sends + # before getting BlockingIOError: + # Linux 4.8: 6 + # macOS (darwin 15.5): 1 + # Windows 10: 525347 + # Windows you're weird. (And on Windows setting SNDBUF to 0 makes send + # blocking, even on non-blocking sockets, so don't do that.) + self._receive.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1) + self._send.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1) + # On Windows this is a TCP socket so this might matter. On other + # platforms this fails b/c AF_UNIX sockets aren't actually TCP. + try: + self._send.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except OSError: + pass + + self._selector.register(self._receive, EVENT_READ) + self._closed = False + + def start(self) -> None: + self._thread.start() + threading._register_atexit(self._stop) # type: ignore[attr-defined] + + def _stop(self) -> None: + global _selector + self._closed = True + self._notify_self() + self._send.close() + self._thread.join() + self._selector.unregister(self._receive) + self._receive.close() + self._selector.close() + _selector = None + assert not self._selector.get_map(), ( + "selector still has registered file descriptors after shutdown" + ) + + def _notify_self(self) -> None: + try: + self._send.send(b"\x00") + except BlockingIOError: + pass + + def add_reader(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: + loop = asyncio.get_running_loop() + try: + key = self._selector.get_key(fd) + except KeyError: + self._selector.register(fd, EVENT_READ, {EVENT_READ: (loop, callback)}) + else: + if EVENT_READ in key.data: + raise ValueError( + "this file descriptor is already registered for reading" + ) + + key.data[EVENT_READ] = loop, callback + self._selector.modify(fd, key.events | EVENT_READ, key.data) + + self._notify_self() + + def add_writer(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: + loop = asyncio.get_running_loop() + try: + key = self._selector.get_key(fd) + except KeyError: + self._selector.register(fd, EVENT_WRITE, {EVENT_WRITE: (loop, callback)}) + else: + if EVENT_WRITE in key.data: + raise ValueError( + "this file descriptor is already registered for writing" + ) + + key.data[EVENT_WRITE] = loop, callback + self._selector.modify(fd, key.events | EVENT_WRITE, key.data) + + self._notify_self() + + def remove_reader(self, fd: FileDescriptorLike) -> bool: + try: + key = self._selector.get_key(fd) + except KeyError: + return False + + if new_events := key.events ^ EVENT_READ: + del key.data[EVENT_READ] + self._selector.modify(fd, new_events, key.data) + else: + self._selector.unregister(fd) + + return True + + def remove_writer(self, fd: FileDescriptorLike) -> bool: + try: + key = self._selector.get_key(fd) + except KeyError: + return False + + if new_events := key.events ^ EVENT_WRITE: + del key.data[EVENT_WRITE] + self._selector.modify(fd, new_events, key.data) + else: + self._selector.unregister(fd) + + return True + + def run(self) -> None: + while not self._closed: + for key, events in self._selector.select(): + if key.fileobj is self._receive: + try: + while self._receive.recv(4096): + pass + except BlockingIOError: + pass + + continue + + if events & EVENT_READ: + loop, callback = key.data[EVENT_READ] + self.remove_reader(key.fd) + try: + loop.call_soon_threadsafe(callback) + except RuntimeError: + pass # the loop was already closed + + if events & EVENT_WRITE: + loop, callback = key.data[EVENT_WRITE] + self.remove_writer(key.fd) + try: + loop.call_soon_threadsafe(callback) + except RuntimeError: + pass # the loop was already closed + + +def get_selector() -> Selector: + global _selector + + with _selector_lock: + if _selector is None: + _selector = Selector() + _selector.start() + + return _selector diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_concurrency_utils.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_concurrency_utils.py new file mode 100644 index 0000000..f66229a --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_concurrency_utils.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from inspect import iscoroutine + +__all__ = ("amap", "as_completed", "gather") + +from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine, Iterable +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Any, TypeVar, overload + +from ..abc._tasks import get_coro_name +from ._exceptions import BrokenResourceError +from ._streams import create_memory_object_stream +from ._tasks import TaskHandle, create_task_group + +if TYPE_CHECKING: + from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream + +R = TypeVar("R") +S = TypeVar("S") +T = TypeVar("T") +U = TypeVar("U") +V = TypeVar("V") +W = TypeVar("W") + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], coro2: Coroutine[Any, Any, S], / +) -> tuple[R, S]: ... + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], + coro2: Coroutine[Any, Any, S], + coro3: Coroutine[Any, Any, T], + /, +) -> tuple[R, S, T]: ... + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], + coro2: Coroutine[Any, Any, S], + coro3: Coroutine[Any, Any, T], + coro4: Coroutine[Any, Any, U], + /, +) -> tuple[R, S, T, U]: ... + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], + coro2: Coroutine[Any, Any, S], + coro3: Coroutine[Any, Any, T], + coro4: Coroutine[Any, Any, U], + coro5: Coroutine[Any, Any, V], + /, +) -> tuple[R, S, T, U, V]: ... + + +@overload +async def gather( + coro1: Coroutine[Any, Any, R], + coro2: Coroutine[Any, Any, S], + coro3: Coroutine[Any, Any, T], + coro4: Coroutine[Any, Any, U], + coro5: Coroutine[Any, Any, V], + coro6: Coroutine[Any, Any, W], + /, +) -> tuple[R, S, T, U, V, W]: ... + + +# handle arbitrary length if awaitables are all of the same type +@overload +async def gather(*coros: Coroutine[Any, Any, R]) -> tuple[R, ...]: ... + + +async def gather(*coros: Coroutine[Any, Any, Any]) -> tuple[Any, ...]: + """ + Run coroutines concurrently in a task group. The order of result values corresponds + to the order of coroutines passed. + + :param coros: coroutine objects to run as tasks + :return: task results for each argument in the same order as they were passed + + """ + handles: list[TaskHandle[Any]] = [] + async with create_task_group() as tg: + handles.extend(tg.create_task(coro) for coro in coros) + + return tuple(h.return_value for h in handles) + + +@asynccontextmanager +async def as_completed( + *awaitables: Awaitable[R], +) -> AsyncGenerator[MemoryObjectReceiveStream[TaskHandle[R]]]: + """ + Run awaitable objects concurrently in a task group, returning an iterator which can + be used to get finished task handles in the order they complete. + + :param awaitables: awaitable objects to run as tasks + :return: MemoryObjectReceiveStream for iterating over task handles as tasks complete. + + """ + if not awaitables: + raise ValueError("as_completed() takes at least one awaitable") + + send, recv = create_memory_object_stream[TaskHandle[R]](len(awaitables)) + task_handles: list[TaskHandle[R]] = [] + + async def runner( + awaitable: Awaitable[R], + index: int, + _send: MemoryObjectSendStream[TaskHandle[R]], + ) -> R: + async with _send: + try: + return await awaitable + finally: + try: + _send.send_nowait(task_handles[index]) + except BrokenResourceError: + pass + + async with recv, create_task_group() as tg: + async with send: + for i, awaitable in enumerate(awaitables): + name = get_coro_name(awaitable) if iscoroutine(awaitable) else None + task_handles.append( + tg.start_soon(runner, awaitable, i, send.clone(), name=name) + ) + try: + yield recv + finally: + tg.cancel_scope.cancel() + + +async def amap( + func: Callable[[T], Coroutine[Any, Any, R]], args: Iterable[T] +) -> list[R]: + """ + Run the given coroutine function concurrently for multiple argument values. + + :param func: a coroutine function that takes a single argument + :param args: a sequence of argument values to pass to ``func`` + :return: task results for each argument in the same order as they were passed + + """ + handles: list[TaskHandle[R]] = [] + async with create_task_group() as tg: + handles.extend(tg.start_soon(func, arg) for arg in args) + + return [h.return_value for h in handles] diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_contextmanagers.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_contextmanagers.py new file mode 100644 index 0000000..302f32b --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_contextmanagers.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +from abc import abstractmethod +from contextlib import AbstractAsyncContextManager, AbstractContextManager +from inspect import isasyncgen, iscoroutine, isgenerator +from types import TracebackType +from typing import Protocol, TypeVar, cast, final + +_T_co = TypeVar("_T_co", covariant=True) +_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound="bool | None") + + +class _SupportsCtxMgr(Protocol[_T_co, _ExitT_co]): + def __contextmanager__(self) -> AbstractContextManager[_T_co, _ExitT_co]: ... + + +class _SupportsAsyncCtxMgr(Protocol[_T_co, _ExitT_co]): + def __asynccontextmanager__( + self, + ) -> AbstractAsyncContextManager[_T_co, _ExitT_co]: ... + + +class ContextManagerMixin: + """ + Mixin class providing context manager functionality via a generator-based + implementation. + + This class allows you to implement a context manager via :meth:`__contextmanager__` + which should return a generator. The mechanics are meant to mirror those of + :func:`@contextmanager `. + + .. note:: Classes using this mix-in are not reentrant as context managers, meaning + that once you enter it, you can't re-enter before first exiting it. + + .. seealso:: :doc:`contextmanagers` + """ + + __cm: AbstractContextManager[object, bool | None] | None = None + + @final + def __enter__(self: _SupportsCtxMgr[_T_co, bool | None]) -> _T_co: + # Needed for mypy to assume self still has the __cm member + assert isinstance(self, ContextManagerMixin) + if self.__cm is not None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has already been entered" + ) + + cm = self.__contextmanager__() + if not isinstance(cm, AbstractContextManager): + if isgenerator(cm): + raise TypeError( + "__contextmanager__() returned a generator object instead of " + "a context manager. Did you forget to add the @contextmanager " + "decorator?" + ) + + raise TypeError( + f"__contextmanager__() did not return a context manager object, " + f"but {cm.__class__!r}" + ) + + if cm is self: + raise TypeError( + f"{self.__class__.__qualname__}.__contextmanager__() returned " + f"self. Did you forget to add the @contextmanager decorator and a " + f"'yield' statement?" + ) + + value = cm.__enter__() + self.__cm = cm + return value + + @final + def __exit__( + self: _SupportsCtxMgr[object, _ExitT_co], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> _ExitT_co: + # Needed for mypy to assume self still has the __cm member + assert isinstance(self, ContextManagerMixin) + if self.__cm is None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has not been entered yet" + ) + + # Prevent circular references + cm = self.__cm + del self.__cm + + return cast(_ExitT_co, cm.__exit__(exc_type, exc_val, exc_tb)) + + @abstractmethod + def __contextmanager__(self) -> AbstractContextManager[object, bool | None]: + """ + Implement your context manager logic here. + + This method **must** be decorated with + :func:`@contextmanager `. + + .. note:: Remember that the ``yield`` will raise any exception raised in the + enclosed context block, so use a ``finally:`` block to clean up resources! + + :return: a context manager object + """ + + +class AsyncContextManagerMixin: + """ + Mixin class providing async context manager functionality via a generator-based + implementation. + + This class allows you to implement a context manager via + :meth:`__asynccontextmanager__`. The mechanics are meant to mirror those of + :func:`@asynccontextmanager `. + + .. note:: Classes using this mix-in are not reentrant as context managers, meaning + that once you enter it, you can't re-enter before first exiting it. + + .. seealso:: :doc:`contextmanagers` + """ + + __cm: AbstractAsyncContextManager[object, bool | None] | None = None + + @final + async def __aenter__(self: _SupportsAsyncCtxMgr[_T_co, bool | None]) -> _T_co: + # Needed for mypy to assume self still has the __cm member + assert isinstance(self, AsyncContextManagerMixin) + if self.__cm is not None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has already been entered" + ) + + cm = self.__asynccontextmanager__() + if not isinstance(cm, AbstractAsyncContextManager): + if isasyncgen(cm): + raise TypeError( + "__asynccontextmanager__() returned an async generator instead of " + "an async context manager. Did you forget to add the " + "@asynccontextmanager decorator?" + ) + elif iscoroutine(cm): + cm.close() + raise TypeError( + "__asynccontextmanager__() returned a coroutine object instead of " + "an async context manager. Did you forget to add the " + "@asynccontextmanager decorator and a 'yield' statement?" + ) + + raise TypeError( + f"__asynccontextmanager__() did not return an async context manager, " + f"but {cm.__class__!r}" + ) + + if cm is self: + raise TypeError( + f"{self.__class__.__qualname__}.__asynccontextmanager__() returned " + f"self. Did you forget to add the @asynccontextmanager decorator and a " + f"'yield' statement?" + ) + + value = await cm.__aenter__() + self.__cm = cm + return value + + @final + async def __aexit__( + self: _SupportsAsyncCtxMgr[object, _ExitT_co], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> _ExitT_co: + assert isinstance(self, AsyncContextManagerMixin) + if self.__cm is None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has not been entered yet" + ) + + # Prevent circular references + cm = self.__cm + del self.__cm + + return cast(_ExitT_co, await cm.__aexit__(exc_type, exc_val, exc_tb)) + + @abstractmethod + def __asynccontextmanager__( + self, + ) -> AbstractAsyncContextManager[object, bool | None]: + """ + Implement your async context manager logic here. + + This method **must** be decorated with + :func:`@asynccontextmanager `. + + .. note:: Remember that the ``yield`` will raise any exception raised in the + enclosed context block, so use a ``finally:`` block to clean up resources! + + :return: an async context manager object + """ diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_eventloop.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_eventloop.py new file mode 100644 index 0000000..a3e2ab1 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_eventloop.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +import math +import sys +import threading +from collections.abc import Awaitable, Callable, Generator +from contextlib import contextmanager +from contextvars import Token +from importlib import import_module +from typing import TYPE_CHECKING, Any, TypeVar + +from ._exceptions import NoEventLoopError + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +sniffio: Any +try: + import sniffio +except ModuleNotFoundError: + sniffio = None + +if TYPE_CHECKING: + from ..abc import AsyncBackend + +# This must be updated when new backends are introduced +BACKENDS = "asyncio", "trio" + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + +threadlocals = threading.local() +loaded_backends: dict[str, type[AsyncBackend]] = {} + + +def run( + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + *args: Unpack[PosArgsT], + backend: str = "asyncio", + backend_options: dict[str, Any] | None = None, +) -> T_Retval: + """ + Run the given coroutine function in an asynchronous event loop. + + The current thread must not be already running an event loop. + + :param func: a coroutine function + :param args: positional arguments to ``func`` + :param backend: name of the asynchronous event loop implementation – currently + either ``asyncio`` or ``trio`` + :param backend_options: keyword arguments to call the backend ``run()`` + implementation with (documented :ref:`here `) + :return: the return value of the coroutine function + :raises RuntimeError: if an asynchronous event loop is already running in this + thread + :raises LookupError: if the named backend is not found + + """ + if asynclib_name := current_async_library(): + raise RuntimeError(f"Already running {asynclib_name} in this thread") + + try: + async_backend = get_async_backend(backend) + except ImportError as exc: + if backend in BACKENDS: + raise LookupError( + f"Backend {backend!r} is not available. " + f"Install it with: pip install anyio[{backend}]" + ) from exc + + raise LookupError(f"No such backend: {backend}") from exc + + token = None + if asynclib_name is None: + # Since we're in control of the event loop, we can cache the name of the async + # library + token = set_current_async_library(backend) + + try: + backend_options = backend_options or {} + return async_backend.run(func, args, {}, backend_options) + finally: + reset_current_async_library(token) + + +async def sleep(delay: float) -> None: + """ + Pause the current task for the specified duration. + + :param delay: the duration, in seconds + + """ + return await get_async_backend().sleep(delay) + + +async def sleep_forever() -> None: + """ + Pause the current task until it's cancelled. + + This is a shortcut for ``sleep(math.inf)``. + + .. versionadded:: 3.1 + + """ + await sleep(math.inf) + + +async def sleep_until(deadline: float) -> None: + """ + Pause the current task until the given time. + + :param deadline: the absolute time to wake up at (according to the internal + monotonic clock of the event loop) + + .. versionadded:: 3.1 + + """ + now = current_time() + await sleep(max(deadline - now, 0)) + + +def current_time() -> float: + """ + Return the current value of the event loop's internal clock. + + :return: the clock value (seconds) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().current_time() + + +def get_all_backends() -> tuple[str, ...]: + """Return a tuple of the names of all built-in backends.""" + return BACKENDS + + +def get_available_backends() -> tuple[str, ...]: + """ + Test for the availability of built-in backends. + + :return a tuple of the built-in backend names that were successfully imported + + .. versionadded:: 4.12 + + """ + available_backends: list[str] = [] + for backend_name in get_all_backends(): + try: + get_async_backend(backend_name) + except ImportError: + continue + + available_backends.append(backend_name) + + return tuple(available_backends) + + +def get_cancelled_exc_class() -> type[BaseException]: + """ + Return the current async library's cancellation exception class. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().cancelled_exception_class() + + +# +# Private API +# + + +@contextmanager +def claim_worker_thread( + backend_class: type[AsyncBackend], token: object +) -> Generator[Any, None, None]: + from ..lowlevel import EventLoopToken + + threadlocals.current_token = EventLoopToken(backend_class, token) + try: + yield + finally: + del threadlocals.current_token + + +def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]: + if asynclib_name is None: + asynclib_name = current_async_library() + if not asynclib_name: + raise NoEventLoopError( + f"Not currently running on any asynchronous event loop. " + f"Available async backends: {', '.join(get_all_backends())}" + ) + + # We use our own dict instead of sys.modules to get the already imported back-end + # class because the appropriate modules in sys.modules could potentially be only + # partially initialized + try: + return loaded_backends[asynclib_name] + except KeyError: + module = import_module(f"anyio._backends._{asynclib_name}") + loaded_backends[asynclib_name] = module.backend_class + return module.backend_class + + +def current_async_library() -> str | None: + if sniffio is None: + # If sniffio is not installed, we assume we're either running asyncio or nothing + import asyncio + + try: + asyncio.get_running_loop() + return "asyncio" + except RuntimeError: + pass + else: + try: + return sniffio.current_async_library() + except sniffio.AsyncLibraryNotFoundError: + pass + + return None + + +def set_current_async_library(asynclib_name: str | None) -> Token | None: + # no-op if sniffio is not installed + if sniffio is None: + return None + + return sniffio.current_async_library_cvar.set(asynclib_name) + + +def reset_current_async_library(token: Token | None) -> None: + if token is not None: + sniffio.current_async_library_cvar.reset(token) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_exceptions.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_exceptions.py new file mode 100644 index 0000000..7d9165f --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_exceptions.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +import sys +from collections.abc import Generator +from textwrap import dedent +from typing import Any + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + + +class BrokenResourceError(Exception): + """ + Raised when trying to use a resource that has been rendered unusable due to external + causes (e.g. a send stream whose peer has disconnected). + """ + + +class BrokenWorkerProcess(Exception): + """ + Raised by :meth:`~anyio.to_process.run_sync` if the worker process terminates abruptly or + otherwise misbehaves. + """ + + +class BrokenWorkerInterpreter(Exception): + """ + Raised by :meth:`~anyio.to_interpreter.run_sync` if an unexpected exception is + raised in the subinterpreter. + """ + + def __init__(self, excinfo: Any): + # This was adapted from concurrent.futures.interpreter.ExecutionFailed + msg = excinfo.formatted + if not msg: + if excinfo.type and excinfo.msg: + msg = f"{excinfo.type.__name__}: {excinfo.msg}" + else: + msg = excinfo.type.__name__ or excinfo.msg + + super().__init__(msg) + self.excinfo = excinfo + + def __str__(self) -> str: + try: + formatted = self.excinfo.errdisplay + except Exception: + return super().__str__() + else: + return dedent( + f""" + {super().__str__()} + + Uncaught in the interpreter: + + {formatted} + """.strip() + ) + + +class BusyResourceError(Exception): + """ + Raised when two tasks are trying to read from or write to the same resource + concurrently. + """ + + def __init__(self, action: str): + super().__init__(f"Another task is already {action} this resource") + + +class ClosedResourceError(Exception): + """Raised when trying to use a resource that has been closed.""" + + +class ConnectionFailed(OSError): + """ + Raised when a connection attempt fails. + + .. note:: This class inherits from :exc:`OSError` for backwards compatibility. + """ + + +def iterate_exceptions( + exception: BaseException, +) -> Generator[BaseException, None, None]: + if isinstance(exception, BaseExceptionGroup): + for exc in exception.exceptions: + yield from iterate_exceptions(exc) + else: + yield exception + + +class DelimiterNotFound(Exception): + """ + Raised during + :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the + maximum number of bytes has been read without the delimiter being found. + """ + + def __init__(self, max_bytes: int) -> None: + super().__init__( + f"The delimiter was not found among the first {max_bytes} bytes" + ) + + +class EndOfStream(Exception): + """ + Raised when trying to read from a stream that has been closed from the other end. + """ + + +class IncompleteRead(Exception): + """ + Raised during + :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_exactly` or + :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the + connection is closed before the requested amount of bytes has been read. + """ + + def __init__(self) -> None: + super().__init__( + "The stream was closed before the read operation could be completed" + ) + + +class TypedAttributeLookupError(LookupError): + """ + Raised by :meth:`~anyio.TypedAttributeProvider.extra` when the given typed attribute + is not found and no default value has been given. + """ + + +class WouldBlock(Exception): + """Raised by ``X_nowait`` functions if ``X()`` would block.""" + + +class NoEventLoopError(RuntimeError): + """ + Raised by several functions that require an event loop to be running in the current + thread when there is no running event loop. + + This is also raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` + if not calling from an AnyIO worker thread, and no ``token`` was passed. + """ + + +class RunFinishedError(RuntimeError): + """ + Raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` if the event + loop associated with the explicitly passed token has already finished. + """ + + def __init__(self) -> None: + super().__init__( + "The event loop associated with the given token has already finished" + ) + + +class TaskFailed(Exception): + """ + Raised when awaiting on, or attempting to access the return value of, a + :class:`.TaskHandle` that raised an exception. + """ + + +class TaskCancelled(TaskFailed): + """ + Raised when awaiting on, or attempting to access the return value of, a + :class:`.TaskHandle` that was cancelled. + """ + + +class TaskNotFinished(Exception): + """ + Raised when attempting to access the return value or exception of a + :class:`.TaskHandle` that is still pending completion. + """ + + +class FutureFailed(Exception): + """ + Raised when awaiting on, or attempting to access the return value of, a + :class:`.Future` that raised an exception. + """ + + +class FutureCancelled(FutureFailed): + """ + Raised when attempting to access the return value or exception of a + :class:`.Future` that was cancelled. + """ + + +class FutureNotFinished(Exception): + """ + Raised when attempting to access the return value or exception of a + :class:`.Future` that is still pending completion. + """ + + +class FutureAlreadyFinished(Exception): + """ + Raised when attempting set a result of or await a + :class:`.Future` that has already completed. + """ diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_fileio.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_fileio.py new file mode 100644 index 0000000..498eef4 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_fileio.py @@ -0,0 +1,1045 @@ +from __future__ import annotations + +import os +import pathlib +import sys +from collections.abc import ( + AsyncIterator, + Callable, + Iterable, + Iterator, + Sequence, +) +from dataclasses import dataclass +from functools import partial +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + AnyStr, + ClassVar, + Final, + Generic, + TypeVar, + overload, +) + +from .. import to_thread +from ..abc import AsyncResource +from ._synchronization import CapacityLimiter + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +if sys.version_info >= (3, 14): + from pathlib.types import PathInfo + +if TYPE_CHECKING: + from types import ModuleType + + from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer +else: + ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object + + +T = TypeVar("T", bound="Path") + + +class AsyncFile(AsyncResource, Generic[AnyStr]): + """ + An asynchronous file object. + + This class wraps a standard file object and provides async friendly versions of the + following blocking methods (where available on the original file object): + + * read + * read1 + * readline + * readlines + * readinto + * readinto1 + * write + * writelines + * truncate + * seek + * tell + * flush + + All other methods are directly passed through. + + This class supports the asynchronous context manager protocol which closes the + underlying file at the end of the context block. + + This class also supports asynchronous iteration:: + + async with await open_file(...) as f: + async for line in f: + print(line) + """ + + def __init__( + self, fp: IO[AnyStr], *, limiter: CapacityLimiter | None = None + ) -> None: + if limiter is not None and not isinstance(limiter, CapacityLimiter): + raise TypeError( + f"limiter must be a CapacityLimiter or None, not " + f"{limiter.__class__.__name__}" + ) + + self._fp: Any = fp + self._limiter = limiter + + def __getattr__(self, name: str) -> object: + return getattr(self._fp, name) + + @property + def limiter(self) -> CapacityLimiter | None: + """The capacity limiter used by this file object, if not the global limiter.""" + return self._limiter + + @property + def wrapped(self) -> IO[AnyStr]: + """The wrapped file object.""" + return self._fp + + async def __aiter__(self) -> AsyncIterator[AnyStr]: + while True: + line = await self.readline() + if line: + yield line + else: + break + + async def aclose(self) -> None: + return await to_thread.run_sync(self._fp.close, limiter=self._limiter) + + async def read(self, size: int = -1) -> AnyStr: + return await to_thread.run_sync(self._fp.read, size, limiter=self._limiter) + + async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes: + return await to_thread.run_sync(self._fp.read1, size, limiter=self._limiter) + + async def readline(self) -> AnyStr: + return await to_thread.run_sync(self._fp.readline, limiter=self._limiter) + + async def readlines(self) -> list[AnyStr]: + return await to_thread.run_sync(self._fp.readlines, limiter=self._limiter) + + async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + return await to_thread.run_sync(self._fp.readinto, b, limiter=self._limiter) + + async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + return await to_thread.run_sync(self._fp.readinto1, b, limiter=self._limiter) + + @overload + async def write(self: AsyncFile[bytes], b: ReadableBuffer) -> int: ... + + @overload + async def write(self: AsyncFile[str], b: str) -> int: ... + + async def write(self, b: ReadableBuffer | str) -> int: + return await to_thread.run_sync(self._fp.write, b, limiter=self._limiter) + + @overload + async def writelines( + self: AsyncFile[bytes], lines: Iterable[ReadableBuffer] + ) -> None: ... + + @overload + async def writelines(self: AsyncFile[str], lines: Iterable[str]) -> None: ... + + async def writelines(self, lines: Iterable[ReadableBuffer] | Iterable[str]) -> None: + return await to_thread.run_sync( + self._fp.writelines, lines, limiter=self._limiter + ) + + async def truncate(self, size: int | None = None) -> int: + return await to_thread.run_sync(self._fp.truncate, size, limiter=self._limiter) + + async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: + return await to_thread.run_sync( + self._fp.seek, offset, whence, limiter=self._limiter + ) + + async def tell(self) -> int: + return await to_thread.run_sync(self._fp.tell, limiter=self._limiter) + + async def flush(self) -> None: + return await to_thread.run_sync(self._fp.flush, limiter=self._limiter) + + +@overload +async def open_file( + file: str | PathLike[str] | int, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] | None = ..., + *, + limiter: CapacityLimiter | None = ..., +) -> AsyncFile[bytes]: ... + + +@overload +async def open_file( + file: str | PathLike[str] | int, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] | None = ..., + *, + limiter: CapacityLimiter | None = ..., +) -> AsyncFile[str]: ... + + +async def open_file( + file: str | PathLike[str] | int, + mode: str = "r", + buffering: int = -1, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + closefd: bool = True, + opener: Callable[[str, int], int] | None = None, + *, + limiter: CapacityLimiter | None = None, +) -> AsyncFile[Any]: + """ + Open a file asynchronously. + + Except for ``limiter``, the arguments are exactly the same as for the builtin :func:`open`. + + :param limiter: an optional capacity limiter to use with the file + instead of the default one + :return: an asynchronous file object + + .. versionchanged:: 4.14.0 + Added the ``limiter`` keyword argument. + + """ + fp = await to_thread.run_sync( + open, + file, + mode, + buffering, + encoding, + errors, + newline, + closefd, + opener, + limiter=limiter, + ) + return AsyncFile(fp, limiter=limiter) + + +def wrap_file( + file: IO[AnyStr], *, limiter: CapacityLimiter | None = None +) -> AsyncFile[AnyStr]: + """ + Wrap an existing file as an asynchronous file. + + :param file: an existing file-like object + :param limiter: an optional capacity limiter to use with the file + instead of the default one + :return: an asynchronous file object + + .. versionchanged:: 4.14.0 + Added the ``limiter`` keyword argument. + + """ + return AsyncFile(file, limiter=limiter) + + +@dataclass(eq=False) +class _PathIterator(AsyncIterator[T]): + iterator: Iterator[PathLike[str]] + limiter: CapacityLimiter | None + # This was added to ensure that iterating over a subclass of Path yields instances + # of that subclass rather than the base Path class. + path_cls: type[T] + + async def __anext__(self) -> T: + nextval = await to_thread.run_sync( + next, self.iterator, None, abandon_on_cancel=True, limiter=self.limiter + ) + if nextval is None: + raise StopAsyncIteration from None + + return self.path_cls(nextval, limiter=self.limiter) + + +class Path: + """ + An asynchronous version of :class:`pathlib.Path`. + + This class cannot be substituted for :class:`pathlib.Path` or + :class:`pathlib.PurePath`, but it is compatible with the :class:`os.PathLike` + interface. + + It implements the Python 3.10 version of :class:`pathlib.Path` interface, except for + the deprecated :meth:`~pathlib.Path.link_to` method. + + Some methods may be unavailable or have limited functionality, based on the Python + version: + + * :meth:`~pathlib.Path.copy` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.copy_into` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.from_uri` (available on Python 3.13 or later) + * :meth:`~pathlib.PurePath.full_match` (available on Python 3.13 or later) + * :attr:`~pathlib.Path.info` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.is_junction` (available on Python 3.12 or later) + * :meth:`~pathlib.PurePath.match` (the ``case_sensitive`` parameter is only + available on Python 3.13 or later) + * :meth:`~pathlib.Path.move` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.move_into` (available on Python 3.14 or later) + * :meth:`~pathlib.PurePath.relative_to` (the ``walk_up`` parameter is only available + on Python 3.12 or later) + * :meth:`~pathlib.Path.walk` (available on Python 3.12 or later) + + Any methods that do disk I/O need to be awaited on. These methods are: + + * :meth:`~pathlib.Path.absolute` + * :meth:`~pathlib.Path.chmod` + * :meth:`~pathlib.Path.cwd` + * :meth:`~pathlib.Path.exists` + * :meth:`~pathlib.Path.expanduser` + * :meth:`~pathlib.Path.group` + * :meth:`~pathlib.Path.hardlink_to` + * :meth:`~pathlib.Path.home` + * :meth:`~pathlib.Path.is_block_device` + * :meth:`~pathlib.Path.is_char_device` + * :meth:`~pathlib.Path.is_dir` + * :meth:`~pathlib.Path.is_fifo` + * :meth:`~pathlib.Path.is_file` + * :meth:`~pathlib.Path.is_junction` + * :meth:`~pathlib.Path.is_mount` + * :meth:`~pathlib.Path.is_socket` + * :meth:`~pathlib.Path.is_symlink` + * :meth:`~pathlib.Path.lchmod` + * :meth:`~pathlib.Path.lstat` + * :meth:`~pathlib.Path.mkdir` + * :meth:`~pathlib.Path.open` + * :meth:`~pathlib.Path.owner` + * :meth:`~pathlib.Path.read_bytes` + * :meth:`~pathlib.Path.read_text` + * :meth:`~pathlib.Path.readlink` + * :meth:`~pathlib.Path.rename` + * :meth:`~pathlib.Path.replace` + * :meth:`~pathlib.Path.resolve` + * :meth:`~pathlib.Path.rmdir` + * :meth:`~pathlib.Path.samefile` + * :meth:`~pathlib.Path.stat` + * :meth:`~pathlib.Path.symlink_to` + * :meth:`~pathlib.Path.touch` + * :meth:`~pathlib.Path.unlink` + * :meth:`~pathlib.Path.walk` + * :meth:`~pathlib.Path.write_bytes` + * :meth:`~pathlib.Path.write_text` + + Additionally, the following methods return an async iterator yielding + :class:`~.Path` objects: + + * :meth:`~pathlib.Path.glob` + * :meth:`~pathlib.Path.iterdir` + * :meth:`~pathlib.Path.rglob` + + .. versionchanged:: 4.14.0 + Added the ``limiter`` keyword argument. + """ + + __slots__ = "__weakref__", "_limiter", "_path" + + __weakref__: Any + + def __init__( + self, *args: str | PathLike[str], limiter: CapacityLimiter | None = None + ) -> None: + if limiter is not None and not isinstance(limiter, CapacityLimiter): + raise TypeError( + f"limiter must be a CapacityLimiter or None, not " + f"{limiter.__class__.__name__}" + ) + + self._path: Final[pathlib.Path] = pathlib.Path(*args) + self._limiter = limiter + + def __fspath__(self) -> str: + return self._path.__fspath__() + + if sys.version_info >= (3, 15): + + def __vfspath__(self) -> str: + return self._path.__vfspath__() + + def __str__(self) -> str: + return self._path.__str__() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.as_posix()!r})" + + def __bytes__(self) -> bytes: + return self._path.__bytes__() + + def __hash__(self) -> int: + return self._path.__hash__() + + def __eq__(self, other: object) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__eq__(target) + + def __lt__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__lt__(target) + + def __le__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__le__(target) + + def __gt__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__gt__(target) + + def __ge__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__ge__(target) + + def __truediv__(self, other: str | PathLike[str]) -> Self: + return type(self)(self._path / other, limiter=self._limiter) + + def __rtruediv__(self, other: str | PathLike[str]) -> Self: + return type(self)(other, limiter=self._limiter) / self + + @property + def limiter(self) -> CapacityLimiter | None: + """The capacity limiter used by this path, if not the global limiter.""" + return self._limiter + + @property + def parts(self) -> tuple[str, ...]: + return self._path.parts + + @property + def drive(self) -> str: + return self._path.drive + + @property + def root(self) -> str: + return self._path.root + + @property + def anchor(self) -> str: + return self._path.anchor + + @property + def parents(self) -> Sequence[Self]: + return tuple(type(self)(p, limiter=self._limiter) for p in self._path.parents) + + @property + def parent(self) -> Self: + return type(self)(self._path.parent, limiter=self._limiter) + + @property + def name(self) -> str: + return self._path.name + + @property + def suffix(self) -> str: + return self._path.suffix + + @property + def suffixes(self) -> list[str]: + return self._path.suffixes + + @property + def stem(self) -> str: + return self._path.stem + + async def absolute(self) -> Self: + path = await to_thread.run_sync(self._path.absolute, limiter=self._limiter) + return type(self)(path, limiter=self._limiter) + + def as_posix(self) -> str: + return self._path.as_posix() + + def as_uri(self) -> str: + return self._path.as_uri() + + if sys.version_info >= (3, 13): + parser: ClassVar[ModuleType] = pathlib.Path.parser + + @classmethod + def from_uri(cls, uri: str, *, limiter: CapacityLimiter | None = None) -> Self: + return cls(pathlib.Path.from_uri(uri), limiter=limiter) + + def full_match( + self, path_pattern: str, *, case_sensitive: bool | None = None + ) -> bool: + return self._path.full_match(path_pattern, case_sensitive=case_sensitive) + + def match( + self, path_pattern: str, *, case_sensitive: bool | None = None + ) -> bool: + return self._path.match(path_pattern, case_sensitive=case_sensitive) + else: + + def match(self, path_pattern: str) -> bool: + return self._path.match(path_pattern) + + if sys.version_info >= (3, 14): + + @property + def info(self) -> PathInfo: + return self._path.info + + async def copy( + self, + target: str | os.PathLike[str], + *, + follow_symlinks: bool = True, + preserve_metadata: bool = False, + ) -> Self: + func = partial( + self._path.copy, + follow_symlinks=follow_symlinks, + preserve_metadata=preserve_metadata, + ) + return type(self)( + await to_thread.run_sync( + func, pathlib.Path(target), limiter=self._limiter + ), + limiter=self._limiter, + ) + + async def copy_into( + self, + target_dir: str | os.PathLike[str], + *, + follow_symlinks: bool = True, + preserve_metadata: bool = False, + ) -> Self: + func = partial( + self._path.copy_into, + follow_symlinks=follow_symlinks, + preserve_metadata=preserve_metadata, + ) + return type(self)( + await to_thread.run_sync( + func, pathlib.Path(target_dir), limiter=self._limiter + ), + limiter=self._limiter, + ) + + async def move(self, target: str | os.PathLike[str]) -> Self: + # Upstream does not handle anyio.Path properly as a PathLike + target = pathlib.Path(target) + return type(self)( + await to_thread.run_sync( + self._path.move, target, limiter=self._limiter + ), + limiter=self._limiter, + ) + + async def move_into( + self, + target_dir: str | os.PathLike[str], + ) -> Self: + return type(self)( + await to_thread.run_sync( + self._path.move_into, target_dir, limiter=self._limiter + ), + limiter=self._limiter, + ) + + def is_relative_to(self, other: str | PathLike[str]) -> bool: + try: + self.relative_to(other) + return True + except ValueError: + return False + + async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: + func = partial(os.chmod, follow_symlinks=follow_symlinks) + return await to_thread.run_sync(func, self._path, mode, limiter=self._limiter) + + @classmethod + async def cwd(cls, *, limiter: CapacityLimiter | None = None) -> Self: + path = await to_thread.run_sync(pathlib.Path.cwd, limiter=limiter) + return cls(path, limiter=limiter) + + if sys.version_info >= (3, 12): + + async def exists(self, *, follow_symlinks: bool = True) -> bool: + return await to_thread.run_sync( + partial(self._path.exists, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def exists(self) -> bool: + return await to_thread.run_sync( + self._path.exists, abandon_on_cancel=True, limiter=self._limiter + ) + + async def expanduser(self) -> Self: + return type(self)( + await to_thread.run_sync( + self._path.expanduser, abandon_on_cancel=True, limiter=self._limiter + ), + limiter=self._limiter, + ) + + if sys.version_info < (3, 12): + # Python 3.11 and earlier + def glob(self, pattern: str) -> AsyncIterator[Self]: + gen = self._path.glob(pattern) + return _PathIterator(gen, self._limiter, type(self)) + elif (3, 12) <= sys.version_info < (3, 13): + # changed in Python 3.12: + # - The case_sensitive parameter was added. + def glob( + self, + pattern: str, + *, + case_sensitive: bool | None = None, + ) -> AsyncIterator[Self]: + gen = self._path.glob(pattern, case_sensitive=case_sensitive) + return _PathIterator(gen, self._limiter, type(self)) + elif sys.version_info >= (3, 13): + # Changed in Python 3.13: + # - The recurse_symlinks parameter was added. + # - The pattern parameter accepts a path-like object. + def glob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block + self, + pattern: str | PathLike[str], + *, + case_sensitive: bool | None = None, + recurse_symlinks: bool = False, + ) -> AsyncIterator[Self]: + gen = self._path.glob( + pattern, # type: ignore[arg-type] + case_sensitive=case_sensitive, + recurse_symlinks=recurse_symlinks, + ) + return _PathIterator(gen, self._limiter, type(self)) + + if sys.version_info >= (3, 13): + + async def group(self, *, follow_symlinks: bool = True) -> str: + return await to_thread.run_sync( + partial(self._path.group, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def group(self) -> str: + return await to_thread.run_sync( + self._path.group, abandon_on_cancel=True, limiter=self._limiter + ) + + async def hardlink_to( + self, target: str | bytes | PathLike[str] | PathLike[bytes] + ) -> None: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync(os.link, target, self, limiter=self._limiter) + + @classmethod + async def home(cls, *, limiter: CapacityLimiter | None = None) -> Self: + home_path = await to_thread.run_sync(pathlib.Path.home, limiter=limiter) + return cls(home_path, limiter=limiter) + + def is_absolute(self) -> bool: + return self._path.is_absolute() + + async def is_block_device(self) -> bool: + return await to_thread.run_sync( + self._path.is_block_device, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_char_device(self) -> bool: + return await to_thread.run_sync( + self._path.is_char_device, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info >= (3, 13): + + async def is_dir(self, *, follow_symlinks: bool = True) -> bool: + return await to_thread.run_sync( + partial(self._path.is_dir, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def is_dir(self) -> bool: + return await to_thread.run_sync( + self._path.is_dir, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_fifo(self) -> bool: + return await to_thread.run_sync( + self._path.is_fifo, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info >= (3, 13): + + async def is_file(self, *, follow_symlinks: bool = True) -> bool: + return await to_thread.run_sync( + partial(self._path.is_file, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def is_file(self) -> bool: + return await to_thread.run_sync( + self._path.is_file, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info >= (3, 12): + + async def is_junction(self) -> bool: + return await to_thread.run_sync( + self._path.is_junction, limiter=self._limiter + ) + + async def is_mount(self) -> bool: + return await to_thread.run_sync( + os.path.ismount, self._path, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info < (3, 15): + + def is_reserved(self) -> bool: + return self._path.is_reserved() + + async def is_socket(self) -> bool: + return await to_thread.run_sync( + self._path.is_socket, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_symlink(self) -> bool: + return await to_thread.run_sync( + self._path.is_symlink, abandon_on_cancel=True, limiter=self._limiter + ) + + async def iterdir(self) -> AsyncIterator[Self]: + gen = ( + self._path.iterdir() + if sys.version_info < (3, 13) + else await to_thread.run_sync( + self._path.iterdir, abandon_on_cancel=True, limiter=self._limiter + ) + ) + async for path in _PathIterator(gen, self._limiter, type(self)): + yield path + + def joinpath(self, *args: str | PathLike[str]) -> Self: + return type(self)(self._path.joinpath(*args), limiter=self._limiter) + + async def lchmod(self, mode: int) -> None: + await to_thread.run_sync(self._path.lchmod, mode, limiter=self._limiter) + + async def lstat(self) -> os.stat_result: + return await to_thread.run_sync( + self._path.lstat, abandon_on_cancel=True, limiter=self._limiter + ) + + async def mkdir( + self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False + ) -> None: + await to_thread.run_sync( + self._path.mkdir, mode, parents, exist_ok, limiter=self._limiter + ) + + @overload + async def open( + self, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + ) -> AsyncFile[bytes]: ... + + @overload + async def open( + self, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + ) -> AsyncFile[str]: ... + + async def open( + self, + mode: str = "r", + buffering: int = -1, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> AsyncFile[Any]: + fp = await to_thread.run_sync( + self._path.open, + mode, + buffering, + encoding, + errors, + newline, + limiter=self._limiter, + ) + return AsyncFile(fp, limiter=self._limiter) + + if sys.version_info >= (3, 13): + + async def owner(self, *, follow_symlinks: bool = True) -> str: + return await to_thread.run_sync( + partial(self._path.owner, follow_symlinks=follow_symlinks), + abandon_on_cancel=True, + limiter=self._limiter, + ) + + else: + + async def owner(self) -> str: + return await to_thread.run_sync( + self._path.owner, abandon_on_cancel=True, limiter=self._limiter + ) + + async def read_bytes(self) -> bytes: + return await to_thread.run_sync(self._path.read_bytes, limiter=self._limiter) + + if sys.version_info >= (3, 13): + + async def read_text( + self, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> str: + return await to_thread.run_sync( + self._path.read_text, + encoding, + errors, + newline, + limiter=self._limiter, + ) + + else: + + async def read_text( + self, encoding: str | None = None, errors: str | None = None + ) -> str: + return await to_thread.run_sync( + self._path.read_text, encoding, errors, limiter=self._limiter + ) + + if sys.version_info >= (3, 12): + + def relative_to( + self, *other: str | PathLike[str], walk_up: bool = False + ) -> Self: + # relative_to() should work with any PathLike but it doesn't + others = [pathlib.Path(other) for other in other] + return type(self)( + self._path.relative_to(*others, walk_up=walk_up), limiter=self._limiter + ) + + else: + + def relative_to(self, *other: str | PathLike[str]) -> Self: + return type(self)(self._path.relative_to(*other), limiter=self._limiter) + + async def readlink(self) -> Self: + target = await to_thread.run_sync( + os.readlink, self._path, limiter=self._limiter + ) + return type(self)(target, limiter=self._limiter) + + async def rename(self, target: str | pathlib.PurePath | Path) -> Self: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync(self._path.rename, target, limiter=self._limiter) + return type(self)(target, limiter=self._limiter) + + async def replace(self, target: str | pathlib.PurePath | Path) -> Self: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync(self._path.replace, target, limiter=self._limiter) + return type(self)(target, limiter=self._limiter) + + async def resolve(self, strict: bool = False) -> Self: + func = partial(self._path.resolve, strict=strict) + return type(self)( + await to_thread.run_sync( + func, abandon_on_cancel=True, limiter=self._limiter + ), + limiter=self._limiter, + ) + + if sys.version_info < (3, 12): + # Pre Python 3.12 + def rglob(self, pattern: str) -> AsyncIterator[Self]: + gen = self._path.rglob(pattern) + return _PathIterator(gen, self._limiter, type(self)) + elif (3, 12) <= sys.version_info < (3, 13): + # Changed in Python 3.12: + # - The case_sensitive parameter was added. + def rglob( + self, pattern: str, *, case_sensitive: bool | None = None + ) -> AsyncIterator[Self]: + gen = self._path.rglob(pattern, case_sensitive=case_sensitive) + return _PathIterator(gen, self._limiter, type(self)) + elif sys.version_info >= (3, 13): + # Changed in Python 3.13: + # - The recurse_symlinks parameter was added. + # - The pattern parameter accepts a path-like object. + def rglob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block + self, + pattern: str | PathLike[str], + *, + case_sensitive: bool | None = None, + recurse_symlinks: bool = False, + ) -> AsyncIterator[Self]: + gen = self._path.rglob( + pattern, # type: ignore[arg-type] + case_sensitive=case_sensitive, + recurse_symlinks=recurse_symlinks, + ) + return _PathIterator(gen, self._limiter, type(self)) + + async def rmdir(self) -> None: + await to_thread.run_sync(self._path.rmdir, limiter=self._limiter) + + async def samefile(self, other_path: str | PathLike[str]) -> bool: + if isinstance(other_path, Path): + other_path = other_path._path + + return await to_thread.run_sync( + self._path.samefile, + other_path, + abandon_on_cancel=True, + limiter=self._limiter, + ) + + async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: + func = partial(os.stat, follow_symlinks=follow_symlinks) + return await to_thread.run_sync( + func, self._path, abandon_on_cancel=True, limiter=self._limiter + ) + + async def symlink_to( + self, + target: str | bytes | PathLike[str] | PathLike[bytes], + target_is_directory: bool = False, + ) -> None: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync( + self._path.symlink_to, target, target_is_directory, limiter=self._limiter + ) + + async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: + await to_thread.run_sync( + self._path.touch, mode, exist_ok, limiter=self._limiter + ) + + async def unlink(self, missing_ok: bool = False) -> None: + try: + await to_thread.run_sync(self._path.unlink, limiter=self._limiter) + except FileNotFoundError: + if not missing_ok: + raise + + if sys.version_info >= (3, 12): + + async def walk( + self, + top_down: bool = True, + on_error: Callable[[OSError], object] | None = None, + follow_symlinks: bool = False, + ) -> AsyncIterator[tuple[Self, list[str], list[str]]]: + def get_next_value() -> tuple[pathlib.Path, list[str], list[str]] | None: + try: + return next(gen) + except StopIteration: + return None + + gen = self._path.walk(top_down, on_error, follow_symlinks) + while True: + value = await to_thread.run_sync(get_next_value, limiter=self._limiter) + if value is None: + return + + root, dirs, paths = value + yield type(self)(root, limiter=self._limiter), dirs, paths + + def with_name(self, name: str) -> Self: + return type(self)(self._path.with_name(name), limiter=self._limiter) + + if sys.version_info < (3, 13): + # Backport pathlib's Python>=3.13 behavior for empty stems on paths with non-empty suffixes. + # See: https://github.com/python/cpython/pull/114612 + def with_stem(self, stem: str) -> Self: + suffix = self._path.suffix + if not suffix: + return self.with_name(stem) + elif not stem: + # If the suffix is non-empty, we can't make the stem empty. + raise ValueError(f"{self!r} has a non-empty suffix") + else: + return self.with_name(stem + suffix) + else: + + def with_stem(self, stem: str) -> Self: + return type(self)(self._path.with_stem(stem), limiter=self._limiter) + + def with_suffix(self, suffix: str) -> Self: + return type(self)(self._path.with_suffix(suffix), limiter=self._limiter) + + def with_segments(self, *pathsegments: str | PathLike[str]) -> Self: + return type(self)(*pathsegments, limiter=self._limiter) + + async def write_bytes(self, data: ReadableBuffer) -> int: + return await to_thread.run_sync( + self._path.write_bytes, data, limiter=self._limiter + ) + + async def write_text( + self, + data: str, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> int: + return await to_thread.run_sync( + self._path.write_text, + data, + encoding, + errors, + newline, + limiter=self._limiter, + ) + + +PathLike.register(Path) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_futures.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_futures.py new file mode 100644 index 0000000..7730c53 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_futures.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +from collections.abc import Generator +from enum import Enum, auto +from typing import Any, Generic, TypeVar + +from ._exceptions import ( + FutureAlreadyFinished, + FutureCancelled, + FutureFailed, + FutureNotFinished, +) +from ._synchronization import Event + +T = TypeVar("T") + + +class Future(Generic[T]): + """ + An awaitable object that works similar to a :class:`asyncio.Future` but + with similar characteristics to a :class:`.TaskHandle`. + """ + + class Status(Enum): + """ + The status of a future handle. + + .. attribute:: PENDING + + The future has not finished yet. + .. attribute:: FINISHED + + The future has finished with a return value. + .. attribute:: CANCELLED + + The future was cancelled and has finished since. + .. attribute:: FAILED + + The future raised an exception. + """ + + PENDING = auto() + FINISHED = auto() + CANCELLED = auto() + FAILED = auto() + + __slots__ = ( + "_cancelled", + "_exception", + "_finished_event", + "_name", + "_return_value", + ) + _return_value: T + + def __init__(self, *, name: str | None = None) -> None: + self._finished_event = Event() + self._exception: BaseException | None = None + self._cancelled: bool = False + self._name = name + + def _check_pending(self) -> None: + """Shortcut for checking if a Future is pending + + :raises FutureAlreadyFinished: if future was already given a result or exception. + :raises FutureCancelled: if future has been cancelled previously. + """ + match self.status: + case Future.Status.PENDING: + return + case Future.Status.FINISHED: + raise FutureAlreadyFinished("future has already finished") + case Future.Status.FAILED: + raise FutureAlreadyFinished("future already failed") + case Future.Status.CANCELLED: + raise FutureCancelled("future was cancelled") + + async def wait(self) -> None: + """ + Waits for the future to finish. + + This method will attempt to wait for a result or exception + """ + await self._finished_event.wait() + + def cancel(self) -> None: + """Cancels a pending `.Future` object + + Does nothing if the Future was already finished. + """ + if self.status is Future.Status.PENDING: + self._cancelled = True + self._finished_event.set() + + @property + def exception(self) -> BaseException | None: + """ + The exception value of a `.Future` + + :raises FutureNotFinished: if future is still pending + :raises FutureCancelled: if future was cancelled + :returns: None if future succeeds with a result sent instead + otherwise this will be an exception + """ + + match self.status: + case Future.Status.PENDING: + raise FutureNotFinished("the future has not finished yet") + case Future.Status.FINISHED: + return None + case Future.Status.CANCELLED: + raise FutureCancelled("the future was cancelled") + case Future.Status.FAILED: + return self._exception + + @exception.setter + def exception(self, exception: BaseException) -> None: + """Send exception for a `.Future` object + + :raises FutureAlreadyFinished: if future was already given a result or exception. + :raises FutureCancelled: if future has been cancelled previously. + """ + self._check_pending() + self._exception = exception + self._finished_event.set() + + @property + def return_value(self) -> T: + """ + The return value of the future. + + :raises FutureNotFinished: if the future has not finished yet + :raises FutureCancelled: if the future was cancelled + :raises FutureFailed: if the future raised an exception + + """ + match self.status: + case Future.Status.PENDING: + raise FutureNotFinished("the future has not finished yet") + case Future.Status.FINISHED: + return self._return_value + case Future.Status.CANCELLED: + raise FutureCancelled("the future was cancelled") + case Future.Status.FAILED: + raise FutureFailed( + "the future raised an exception" + ) from self._exception + + @return_value.setter + def return_value(self, value: T) -> None: + """ + Send pending result for a `.Future` object + + :raises FutureAlreadyFinished: if future was already given a result or exception. + :raises FutureCancelled: if future has been cancelled previously. + """ + self._check_pending() + self._return_value = value + self._finished_event.set() + + @property + def status(self) -> Future.Status: + """ + The current status of a future. + + Every future starts in the :attr:`~Future.Status.PENDING` state. + If a future is cancelled while in this state, it will transition to the + :attr:`Future.Status.CANCELLED` state. When the task finishes, it will + transition to one of the three final states ( + :attr:`Future.Status.FINISHED`, :attr:`Future.Status.FAILED`, or + :attr:`Future.Status.CANCELLED`) depending on the exception the task + raised, if any. No other status transitions will happen. + """ + if not self._finished_event.is_set(): + return Future.Status.PENDING + elif self._cancelled: + return Future.Status.CANCELLED + elif self._exception is not None: + return Future.Status.FAILED + else: + return Future.Status.FINISHED + + def __await__(self) -> Generator[Any, Any, T]: + yield from self.wait().__await__() + return self.return_value + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self.status.name.lower()} " + f"name={self._name!r}>" + ) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_resources.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_resources.py new file mode 100644 index 0000000..6ca35d9 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_resources.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from ..abc import AsyncResource # noqa: TC001 +from ._tasks import CancelScope + + +async def aclose_forcefully(resource: AsyncResource) -> None: + """ + Close an asynchronous resource in a cancelled scope. + + Doing this closes the resource without waiting on anything. + + :param resource: the resource to close + + """ + with CancelScope() as scope: + scope.cancel() + await resource.aclose() diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_signals.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_signals.py new file mode 100644 index 0000000..e24c79e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_signals.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import AbstractContextManager +from signal import Signals + +from ._eventloop import get_async_backend + + +def open_signal_receiver( + *signals: Signals, +) -> AbstractContextManager[AsyncIterator[Signals]]: + """ + Start receiving operating system signals. + + :param signals: signals to receive (e.g. ``signal.SIGINT``) + :return: an asynchronous context manager for an asynchronous iterator which yields + signal numbers + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. warning:: Windows does not support signals natively so it is best to avoid + relying on this in cross-platform applications. + + .. warning:: On asyncio, this permanently replaces any previous signal handler for + the given signals, as set via :meth:`~asyncio.loop.add_signal_handler`. + + """ + return get_async_backend().open_signal_receiver(*signals) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_sockets.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_sockets.py new file mode 100644 index 0000000..14ee6e4 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_sockets.py @@ -0,0 +1,1011 @@ +from __future__ import annotations + +import errno +import os +import socket +import ssl +import stat +import sys +from collections.abc import Awaitable +from dataclasses import dataclass +from ipaddress import IPv4Address, IPv6Address, ip_address +from os import PathLike, chmod +from socket import AddressFamily, SocketKind +from typing import TYPE_CHECKING, Any, Literal, cast, overload + +from .. import ConnectionFailed, to_thread +from ..abc import ( + ByteStreamConnectable, + ConnectedUDPSocket, + ConnectedUNIXDatagramSocket, + IPAddressType, + IPSockAddrType, + SocketListener, + SocketStream, + UDPSocket, + UNIXDatagramSocket, + UNIXSocketStream, +) +from ..streams.stapled import MultiListener +from ..streams.tls import TLSConnectable, TLSStream +from ._eventloop import get_async_backend +from ._resources import aclose_forcefully +from ._synchronization import Event +from ._tasks import create_task_group, move_on_after + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike +else: + FileDescriptorLike = object + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +if sys.version_info < (3, 13): + from typing_extensions import deprecated +else: + from warnings import deprecated + +IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) # https://bugs.python.org/issue29515 + +AnyIPAddressFamily = Literal[ + AddressFamily.AF_UNSPEC, AddressFamily.AF_INET, AddressFamily.AF_INET6 +] +IPAddressFamily = Literal[AddressFamily.AF_INET, AddressFamily.AF_INET6] + + +def idna2008_resolve(host: str) -> bytes: + try: + return host.encode("ascii") + except UnicodeEncodeError: + import idna + + return idna.encode(host, uts46=True) + + +# tls_hostname given +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + ssl_context: ssl.SSLContext | None = ..., + tls_standard_compatible: bool = ..., + tls_hostname: str, + happy_eyeballs_delay: float = ..., +) -> TLSStream: ... + + +# ssl_context given +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + ssl_context: ssl.SSLContext, + tls_standard_compatible: bool = ..., + tls_hostname: str | None = ..., + happy_eyeballs_delay: float = ..., +) -> TLSStream: ... + + +# tls=True +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + tls: Literal[True], + ssl_context: ssl.SSLContext | None = ..., + tls_standard_compatible: bool = ..., + tls_hostname: str | None = ..., + happy_eyeballs_delay: float = ..., +) -> TLSStream: ... + + +# tls=False +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + tls: Literal[False], + ssl_context: ssl.SSLContext | None = ..., + tls_standard_compatible: bool = ..., + tls_hostname: str | None = ..., + happy_eyeballs_delay: float = ..., +) -> SocketStream: ... + + +# No TLS arguments +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + happy_eyeballs_delay: float = ..., +) -> SocketStream: ... + + +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = None, + local_port: int | None = None, + tls: bool = False, + ssl_context: ssl.SSLContext | None = None, + tls_standard_compatible: bool = True, + tls_hostname: str | None = None, + happy_eyeballs_delay: float = 0.25, +) -> SocketStream | TLSStream: + """ + Connect to a host using the TCP protocol. + + This function implements the stateless version of the Happy Eyeballs algorithm (RFC + 6555). If ``remote_host`` is a host name that resolves to multiple IP addresses, + each one is tried until one connection attempt succeeds. If the first attempt does + not connected within 250 milliseconds, a second attempt is started using the next + address in the list, and so on. On IPv6 enabled systems, an IPv6 address (if + available) is tried first. + + When the connection has been established, a TLS handshake will be done if either + ``ssl_context`` or ``tls_hostname`` is not ``None``, or if ``tls`` is ``True``. + + :param remote_host: the IP address or host name to connect to + :param remote_port: port on the target host to connect to + :param local_host: the interface address or name to bind the socket to before + connecting + :param local_port: the local port to bind to (requires ``local_host`` to also be + set) + :param tls: ``True`` to do a TLS handshake with the connected stream and return a + :class:`~anyio.streams.tls.TLSStream` instead + :param ssl_context: the SSL context object to use (if omitted, a default context is + created) + :param tls_standard_compatible: If ``True``, performs the TLS shutdown handshake + before closing the stream and requires that the server does this as well. + Otherwise, :exc:`~ssl.SSLEOFError` may be raised during reads from the stream. + Some protocols, such as HTTP, require this option to be ``False``. + See :meth:`~ssl.SSLContext.wrap_socket` for details. + :param tls_hostname: host name to check the server certificate against (defaults to + the value of ``remote_host``) + :param happy_eyeballs_delay: delay (in seconds) before starting the next connection + attempt + :return: a socket stream object if no TLS handshake was done, otherwise a TLS stream + :raises ConnectionFailed: if the connection fails + + """ + # Placed here due to https://github.com/python/mypy/issues/7057 + connected_stream: SocketStream | None = None + + async def try_connect(remote_host: str, event: Event) -> None: + nonlocal connected_stream + try: + stream = await asynclib.connect_tcp(remote_host, remote_port, local_address) + except OSError as exc: + oserrors.append(exc) + return + else: + if connected_stream is None: + connected_stream = stream + tg.cancel_scope.cancel() + else: + await stream.aclose() + finally: + event.set() + + asynclib = get_async_backend() + local_address: IPSockAddrType | None = None + family = socket.AF_UNSPEC + if local_host: + gai_res = await getaddrinfo(str(local_host), local_port) + family, *_, local_address = gai_res[0] + + target_host = str(remote_host) + try: + addr_obj = ip_address(remote_host) + except ValueError: + addr_obj = None + + if addr_obj is not None: + if isinstance(addr_obj, IPv6Address): + target_addrs = [(socket.AF_INET6, addr_obj.compressed)] + else: + target_addrs = [(socket.AF_INET, addr_obj.compressed)] + else: + # getaddrinfo() will raise an exception if name resolution fails + gai_res = await getaddrinfo( + target_host, remote_port, family=family, type=socket.SOCK_STREAM + ) + + # Organize the list so that the first address is an IPv6 address (if available) + # and the second one is an IPv4 addresses. The rest can be in whatever order. + v6_found = v4_found = False + target_addrs = [] + for af, *_, sa in gai_res: + if af == socket.AF_INET6 and not v6_found: + v6_found = True + target_addrs.insert(0, (af, sa[0])) + elif af == socket.AF_INET and not v4_found and v6_found: + v4_found = True + target_addrs.insert(1, (af, sa[0])) + else: + target_addrs.append((af, sa[0])) + + oserrors: list[OSError] = [] + try: + async with create_task_group() as tg: + for _af, addr in target_addrs: + event = Event() + tg.start_soon(try_connect, addr, event) + with move_on_after(happy_eyeballs_delay): + await event.wait() + + if connected_stream is None: + cause = ( + oserrors[0] + if len(oserrors) == 1 + else ExceptionGroup("multiple connection attempts failed", oserrors) + ) + raise OSError("All connection attempts failed") from cause + finally: + oserrors.clear() + + if tls or tls_hostname or ssl_context: + try: + return await TLSStream.wrap( + connected_stream, + server_side=False, + hostname=tls_hostname or str(remote_host), + ssl_context=ssl_context, + standard_compatible=tls_standard_compatible, + ) + except BaseException: + await aclose_forcefully(connected_stream) + raise + + return connected_stream + + +async def connect_unix(path: str | bytes | PathLike[Any]) -> UNIXSocketStream: + """ + Connect to the given UNIX socket. + + Not available on Windows. + + :param path: path to the socket + :return: a socket stream object + :raises ConnectionFailed: if the connection fails + + """ + path = os.fspath(path) + return await get_async_backend().connect_unix(path) + + +async def create_tcp_listener( + *, + local_host: IPAddressType | None = None, + local_port: int = 0, + family: AnyIPAddressFamily = socket.AddressFamily.AF_UNSPEC, + backlog: int = 65536, + reuse_port: bool = False, +) -> MultiListener[SocketStream]: + """ + Create a TCP socket listener. + + :param local_port: port number to listen on + :param local_host: IP address of the interface to listen on. If omitted, listen on + all IPv4 and IPv6 interfaces. To listen on all interfaces on a specific address + family, use ``0.0.0.0`` for IPv4 or ``::`` for IPv6. + :param family: address family (used if ``local_host`` was omitted) + :param backlog: maximum number of queued incoming connections (up to a maximum of + 2**16, or 65536) + :param reuse_port: ``True`` to allow multiple sockets to bind to the same + address/port (not supported on Windows) + :return: a multi-listener object containing one or more socket listeners + :raises OSError: if there's an error creating a socket, or binding to one or more + interfaces failed + + """ + asynclib = get_async_backend() + backlog = min(backlog, 65536) + local_host = str(local_host) if local_host is not None else None + + def setup_raw_socket( + fam: AddressFamily, + bind_addr: tuple[str, int] | tuple[str, int, int, int], + *, + v6only: bool = True, + ) -> socket.socket: + sock = socket.socket(fam) + try: + sock.setblocking(False) + + if fam == AddressFamily.AF_INET6: + sock.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, v6only) + + # For Windows, enable exclusive address use. For others, enable address + # reuse. + if sys.platform == "win32": + sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) + else: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + if reuse_port: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + + # Workaround for #554 + if fam == socket.AF_INET6 and "%" in bind_addr[0]: + addr, scope_id = bind_addr[0].split("%", 1) + bind_addr = (addr, bind_addr[1], 0, int(scope_id)) + + sock.bind(bind_addr) + sock.listen(backlog) + except BaseException: + sock.close() + raise + + return sock + + # We passing type=0 on non-Windows platforms as a workaround for a uvloop bug + # where we don't get the correct scope ID for IPv6 link-local addresses when passing + # type=socket.SOCK_STREAM to getaddrinfo(): + # https://github.com/MagicStack/uvloop/issues/539 + gai_res = await getaddrinfo( + local_host, + local_port, + family=family, + type=socket.SOCK_STREAM if sys.platform == "win32" else 0, + flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, + ) + + # The set comprehension is here to work around a glibc bug: + # https://sourceware.org/bugzilla/show_bug.cgi?id=14969 + sockaddrs = sorted({res for res in gai_res if res[1] == SocketKind.SOCK_STREAM}) + + # Special case for dual-stack binding on the "any" interface + if ( + local_host is None + and family == AddressFamily.AF_UNSPEC + and socket.has_dualstack_ipv6() + and any(fam == AddressFamily.AF_INET6 for fam, *_ in gai_res) + ): + raw_socket = setup_raw_socket( + AddressFamily.AF_INET6, ("::", local_port), v6only=False + ) + listener = asynclib.create_tcp_listener(raw_socket) + return MultiListener([listener]) + + errors: list[OSError] = [] + try: + for _ in range(len(sockaddrs)): + listeners: list[SocketListener] = [] + bound_ephemeral_port = local_port + try: + for fam, *_, sockaddr in sockaddrs: + sockaddr = sockaddr[0], bound_ephemeral_port, *sockaddr[2:] + raw_socket = setup_raw_socket(fam, sockaddr) + + # Store the assigned port if an ephemeral port was requested, so + # we'll bind to the same port on all interfaces + if local_port == 0 and len(gai_res) > 1: + bound_ephemeral_port = raw_socket.getsockname()[1] + + listeners.append(asynclib.create_tcp_listener(raw_socket)) + except BaseException as exc: + for listener in listeners: + await listener.aclose() + + # If an ephemeral port was requested but binding the assigned port + # failed for another interface, rotate the address list and try again + if ( + isinstance(exc, OSError) + and exc.errno == errno.EADDRINUSE + and local_port == 0 + and bound_ephemeral_port + ): + errors.append(exc) + sockaddrs.append(sockaddrs.pop(0)) + continue + + raise + + return MultiListener(listeners) + + raise OSError( + f"Could not create {len(sockaddrs)} listeners with a consistent port" + ) from ExceptionGroup("Several bind attempts failed", errors) + finally: + del errors # Prevent reference cycles + + +async def create_unix_listener( + path: str | bytes | PathLike[Any], + *, + mode: int | None = None, + backlog: int = 65536, +) -> SocketListener: + """ + Create a UNIX socket listener. + + Not available on Windows. + + :param path: path of the socket + :param mode: permissions to set on the socket + :param backlog: maximum number of queued incoming connections (up to a maximum of + 2**16, or 65536) + :return: a listener object + + .. versionchanged:: 3.0 + If a socket already exists on the file system in the given path, it will be + removed first. + + """ + backlog = min(backlog, 65536) + raw_socket = await setup_unix_local_socket(path, mode, socket.SOCK_STREAM) + try: + raw_socket.listen(backlog) + return get_async_backend().create_unix_listener(raw_socket) + except BaseException: + raw_socket.close() + raise + + +async def create_udp_socket( + family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, + *, + local_host: IPAddressType | None = None, + local_port: int = 0, + reuse_port: bool = False, +) -> UDPSocket: + """ + Create a UDP socket. + + If ``port`` has been given, the socket will be bound to this port on the local + machine, making this socket suitable for providing UDP based services. + + :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically + determined from ``local_host`` if omitted + :param local_host: IP address or host name of the local interface to bind to + :param local_port: local port to bind to + :param reuse_port: ``True`` to allow multiple sockets to bind to the same + address/port (not supported on Windows) + :return: a UDP socket + + """ + if family is AddressFamily.AF_UNSPEC and not local_host: + raise ValueError('Either "family" or "local_host" must be given') + + if local_host: + gai_res = await getaddrinfo( + str(local_host), + local_port, + family=family, + type=socket.SOCK_DGRAM, + flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, + ) + family = cast(AnyIPAddressFamily, gai_res[0][0]) + local_address = gai_res[0][-1] + elif family is AddressFamily.AF_INET6: + local_address = ("::", 0) + else: + local_address = ("0.0.0.0", 0) + + sock = await get_async_backend().create_udp_socket( + family, local_address, None, reuse_port + ) + return cast(UDPSocket, sock) + + +async def create_connected_udp_socket( + remote_host: IPAddressType, + remote_port: int, + *, + family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, + local_host: IPAddressType | None = None, + local_port: int = 0, + reuse_port: bool = False, +) -> ConnectedUDPSocket: + """ + Create a connected UDP socket. + + Connected UDP sockets can only communicate with the specified remote host/port, an + any packets sent from other sources are dropped. + + :param remote_host: remote host to set as the default target + :param remote_port: port on the remote host to set as the default target + :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically + determined from ``local_host`` or ``remote_host`` if omitted + :param local_host: IP address or host name of the local interface to bind to + :param local_port: local port to bind to + :param reuse_port: ``True`` to allow multiple sockets to bind to the same + address/port (not supported on Windows) + :return: a connected UDP socket + + """ + local_address = None + if local_host: + gai_res = await getaddrinfo( + str(local_host), + local_port, + family=family, + type=socket.SOCK_DGRAM, + flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, + ) + family = cast(AnyIPAddressFamily, gai_res[0][0]) + local_address = gai_res[0][-1] + + gai_res = await getaddrinfo( + str(remote_host), remote_port, family=family, type=socket.SOCK_DGRAM + ) + family = cast(AnyIPAddressFamily, gai_res[0][0]) + remote_address = gai_res[0][-1] + + sock = await get_async_backend().create_udp_socket( + family, local_address, remote_address, reuse_port + ) + return cast(ConnectedUDPSocket, sock) + + +async def create_unix_datagram_socket( + *, + local_path: None | str | bytes | PathLike[Any] = None, + local_mode: int | None = None, +) -> UNIXDatagramSocket: + """ + Create a UNIX datagram socket. + + Not available on Windows. + + If ``local_path`` has been given, the socket will be bound to this path, making this + socket suitable for receiving datagrams from other processes. Other processes can + send datagrams to this socket only if ``local_path`` is set. + + If a socket already exists on the file system in the ``local_path``, it will be + removed first. + + :param local_path: the path on which to bind to + :param local_mode: permissions to set on the local socket + :return: a UNIX datagram socket + + """ + raw_socket = await setup_unix_local_socket( + local_path, local_mode, socket.SOCK_DGRAM + ) + return await get_async_backend().create_unix_datagram_socket(raw_socket, None) + + +async def create_connected_unix_datagram_socket( + remote_path: str | bytes | PathLike[Any], + *, + local_path: None | str | bytes | PathLike[Any] = None, + local_mode: int | None = None, +) -> ConnectedUNIXDatagramSocket: + """ + Create a connected UNIX datagram socket. + + Connected datagram sockets can only communicate with the specified remote path. + + If ``local_path`` has been given, the socket will be bound to this path, making + this socket suitable for receiving datagrams from other processes. Other processes + can send datagrams to this socket only if ``local_path`` is set. + + If a socket already exists on the file system in the ``local_path``, it will be + removed first. + + :param remote_path: the path to set as the default target + :param local_path: the path on which to bind to + :param local_mode: permissions to set on the local socket + :return: a connected UNIX datagram socket + + """ + remote_path = os.fspath(remote_path) + raw_socket = await setup_unix_local_socket( + local_path, local_mode, socket.SOCK_DGRAM + ) + return await get_async_backend().create_unix_datagram_socket( + raw_socket, remote_path + ) + + +async def getaddrinfo( + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, +) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int]]]: + """ + Look up a numeric IP address given a host name. + + Internationalized domain names are translated according to the (non-transitional) + IDNA 2008 standard. + + .. note:: 4-tuple IPv6 socket addresses are automatically converted to 2-tuples of + (host, port), unlike what :func:`socket.getaddrinfo` does. + + :param host: host name + :param port: port number + :param family: socket family (`'AF_INET``, ...) + :param type: socket type (``SOCK_STREAM``, ...) + :param proto: protocol number + :param flags: flags to pass to upstream ``getaddrinfo()`` + :return: list of tuples containing (family, type, proto, canonname, sockaddr) + + .. seealso:: :func:`socket.getaddrinfo` + + """ + # Handle unicode hostnames + encoded_host = idna2008_resolve(host) if isinstance(host, str) else host + gai_res = await get_async_backend().getaddrinfo( + encoded_host, port, family=family, type=type, proto=proto, flags=flags + ) + return [ + (family, type, proto, canonname, convert_ipv6_sockaddr(sockaddr)) + for family, type, proto, canonname, sockaddr in gai_res + # filter out IPv6 results when IPv6 is disabled + if not isinstance(sockaddr[0], int) + ] + + +def getnameinfo(sockaddr: IPSockAddrType, flags: int = 0) -> Awaitable[tuple[str, str]]: + """ + Look up the host name of an IP address. + + :param sockaddr: socket address (e.g. (ipaddress, port) for IPv4) + :param flags: flags to pass to upstream ``getnameinfo()`` + :return: a tuple of (host name, service name) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. seealso:: :func:`socket.getnameinfo` + + """ + return get_async_backend().getnameinfo(sockaddr, flags) + + +@deprecated("This function is deprecated; use `wait_readable` instead") +def wait_socket_readable(sock: socket.socket) -> Awaitable[None]: + """ + .. deprecated:: 4.7.0 + Use :func:`wait_readable` instead. + + Wait until the given socket has data to be read. + + .. warning:: Only use this on raw sockets that have not been wrapped by any higher + level constructs like socket streams! + + :param sock: a socket object + :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the + socket to become readable + :raises ~anyio.BusyResourceError: if another task is already waiting for the socket + to become readable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().wait_readable(sock.fileno()) + + +@deprecated("This function is deprecated; use `wait_writable` instead") +def wait_socket_writable(sock: socket.socket) -> Awaitable[None]: + """ + .. deprecated:: 4.7.0 + Use :func:`wait_writable` instead. + + Wait until the given socket can be written to. + + This does **NOT** work on Windows when using the asyncio backend with a proactor + event loop (default on py3.8+). + + .. warning:: Only use this on raw sockets that have not been wrapped by any higher + level constructs like socket streams! + + :param sock: a socket object + :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the + socket to become writable + :raises ~anyio.BusyResourceError: if another task is already waiting for the socket + to become writable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().wait_writable(sock.fileno()) + + +def wait_readable(obj: FileDescriptorLike) -> Awaitable[None]: + """ + Wait until the given object has data to be read. + + On Unix systems, ``obj`` must either be an integer file descriptor, or else an + object with a ``.fileno()`` method which returns an integer file descriptor. Any + kind of file descriptor can be passed, though the exact semantics will depend on + your kernel. For example, this probably won't do anything useful for on-disk files. + + On Windows systems, ``obj`` must either be an integer ``SOCKET`` handle, or else an + object with a ``.fileno()`` method which returns an integer ``SOCKET`` handle. File + descriptors aren't supported, and neither are handles that refer to anything besides + a ``SOCKET``. + + On backends where this functionality is not natively provided (asyncio + ``ProactorEventLoop`` on Windows), it is provided using a separate selector thread + which is set to shut down when the interpreter shuts down. + + .. warning:: Don't use this on raw sockets that have been wrapped by any higher + level constructs like socket streams! + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the + object to become readable + :raises ~anyio.BusyResourceError: if another task is already waiting for the object + to become readable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().wait_readable(obj) + + +def wait_writable(obj: FileDescriptorLike) -> Awaitable[None]: + """ + Wait until the given object can be written to. + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the + object to become writable + :raises ~anyio.BusyResourceError: if another task is already waiting for the object + to become writable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. seealso:: See the documentation of :func:`wait_readable` for the definition of + ``obj`` and notes on backend compatibility. + + .. warning:: Don't use this on raw sockets that have been wrapped by any higher + level constructs like socket streams! + + """ + return get_async_backend().wait_writable(obj) + + +def notify_closing(obj: FileDescriptorLike) -> None: + """ + Call this before closing a file descriptor (on Unix) or socket (on + Windows). This will cause any `wait_readable` or `wait_writable` + calls on the given object to immediately wake up and raise + `~anyio.ClosedResourceError`. + + This doesn't actually close the object – you still have to do that + yourself afterwards. Also, you want to be careful to make sure no + new tasks start waiting on the object in between when you call this + and when it's actually closed. So to close something properly, you + usually want to do these steps in order: + + 1. Explicitly mark the object as closed, so that any new attempts + to use it will abort before they start. + 2. Call `notify_closing` to wake up any already-existing users. + 3. Actually close the object. + + It's also possible to do them in a different order if that's more + convenient, *but only if* you make sure not to have any checkpoints in + between the steps. This way they all happen in a single atomic + step, so other tasks won't be able to tell what order they happened + in anyway. + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + get_async_backend().notify_closing(obj) + + +# +# Private API +# + + +def convert_ipv6_sockaddr( + sockaddr: tuple[str, int, int, int] | tuple[str, int], +) -> tuple[str, int]: + """ + Convert a 4-tuple IPv6 socket address to a 2-tuple (address, port) format. + + If the scope ID is nonzero, it is added to the address, separated with ``%``. + Otherwise the flow id and scope id are simply cut off from the tuple. + Any other kinds of socket addresses are returned as-is. + + :param sockaddr: the result of :meth:`~socket.socket.getsockname` + :return: the converted socket address + + """ + # This is more complicated than it should be because of MyPy + if isinstance(sockaddr, tuple) and len(sockaddr) == 4: + host, port, _flowinfo, scope_id = sockaddr + if scope_id: + # PyPy (as of v7.3.11) leaves the interface name in the result, so + # we discard it and only get the scope ID from the end + # (https://foss.heptapod.net/pypy/pypy/-/issues/3938) + host = host.split("%")[0] + + # Add scope_id to the address + return f"{host}%{scope_id}", port + else: + return host, port + else: + return sockaddr + + +async def setup_unix_local_socket( + path: None | str | bytes | PathLike[Any], + mode: int | None, + socktype: int, +) -> socket.socket: + """ + Create a UNIX local socket object, deleting the socket at the given path if it + exists. + + Not available on Windows. + + :param path: path of the socket + :param mode: permissions to set on the socket + :param socktype: socket.SOCK_STREAM or socket.SOCK_DGRAM + + """ + path_str: str | None + if path is not None: + path_str = os.fsdecode(path) + + # Linux abstract namespace sockets aren't backed by a concrete file so skip stat call + if not path_str.startswith("\0"): + # Copied from pathlib... + try: + stat_result = os.stat(path) + except OSError as e: + if e.errno not in ( + errno.ENOENT, + errno.ENOTDIR, + errno.EBADF, + errno.ELOOP, + ): + raise + else: + if stat.S_ISSOCK(stat_result.st_mode): + os.unlink(path) + else: + path_str = None + + raw_socket = socket.socket(socket.AF_UNIX, socktype) + raw_socket.setblocking(False) + + if path_str is not None: + try: + await to_thread.run_sync(raw_socket.bind, path_str, abandon_on_cancel=True) + if mode is not None: + await to_thread.run_sync(chmod, path_str, mode, abandon_on_cancel=True) + except BaseException: + raw_socket.close() + raise + + return raw_socket + + +@dataclass +class TCPConnectable(ByteStreamConnectable): + """ + Connects to a TCP server at the given host and port. + + :param host: host name or IP address of the server + :param port: TCP port number of the server + """ + + host: str | IPv4Address | IPv6Address + port: int + + def __post_init__(self) -> None: + if self.port < 1 or self.port > 65535: + raise ValueError("TCP port number out of range") + + @override + async def connect(self) -> SocketStream: + try: + return await connect_tcp(self.host, self.port) + except OSError as exc: + raise ConnectionFailed( + f"error connecting to {self.host}:{self.port}: {exc}" + ) from exc + + +@dataclass +class UNIXConnectable(ByteStreamConnectable): + """ + Connects to a UNIX domain socket at the given path. + + :param path: the file system path of the socket + """ + + path: str | bytes | PathLike[str] | PathLike[bytes] + + @override + async def connect(self) -> UNIXSocketStream: + try: + return await connect_unix(self.path) + except OSError as exc: + raise ConnectionFailed(f"error connecting to {self.path!r}: {exc}") from exc + + +def as_connectable( + remote: ByteStreamConnectable + | tuple[str | IPv4Address | IPv6Address, int] + | str + | bytes + | PathLike[str], + /, + *, + tls: bool = False, + ssl_context: ssl.SSLContext | None = None, + tls_hostname: str | None = None, + tls_standard_compatible: bool = True, +) -> ByteStreamConnectable: + """ + Return a byte stream connectable from the given object. + + If a bytestream connectable is given, it is returned unchanged. + If a tuple of (host, port) is given, a TCP connectable is returned. + If a string or bytes path is given, a UNIX connectable is returned. + + If ``tls=True``, the connectable will be wrapped in a + :class:`~.streams.tls.TLSConnectable`. + + :param remote: a connectable, a tuple of (host, port) or a path to a UNIX socket + :param tls: if ``True``, wrap the plaintext connectable in a + :class:`~.streams.tls.TLSConnectable`, using the provided TLS settings) + :param ssl_context: if ``tls=True``, the SSLContext object to use (if not provided, + a secure default will be created) + :param tls_hostname: if ``tls=True``, host name of the server to use for checking + the server certificate (defaults to the host portion of the address for TCP + connectables) + :param tls_standard_compatible: if ``False`` and ``tls=True``, makes the TLS stream + skip the closing handshake when closing the connection, so it won't raise an + exception if the server does the same + + """ + connectable: TCPConnectable | UNIXConnectable | TLSConnectable + if isinstance(remote, ByteStreamConnectable): + return remote + elif isinstance(remote, tuple) and len(remote) == 2: + connectable = TCPConnectable(*remote) + elif isinstance(remote, (str, bytes, PathLike)): + connectable = UNIXConnectable(remote) + else: + raise TypeError(f"cannot convert {remote!r} to a connectable") + + if tls: + if not tls_hostname and isinstance(connectable, TCPConnectable): + tls_hostname = str(connectable.host) + + connectable = TLSConnectable( + connectable, + ssl_context=ssl_context, + hostname=tls_hostname, + standard_compatible=tls_standard_compatible, + ) + + return connectable diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_streams.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_streams.py new file mode 100644 index 0000000..2b9c7df --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_streams.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import math +from typing import TypeVar +from warnings import warn + +from ..streams.memory import ( + MemoryObjectReceiveStream, + MemoryObjectSendStream, + _MemoryObjectStreamState, +) + +T_Item = TypeVar("T_Item") + + +class create_memory_object_stream( + tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]], +): + """ + Create a memory object stream. + + The stream's item type can be annotated like + :func:`create_memory_object_stream[T_Item]`. + + :param max_buffer_size: number of items held in the buffer until ``send()`` starts + blocking + :param item_type: old way of marking the streams with the right generic type for + static typing (does nothing on AnyIO 4) + + .. deprecated:: 4.0 + Use ``create_memory_object_stream[YourItemType](...)`` instead. + :return: a tuple of (send stream, receive stream) + + """ + + def __new__( # type: ignore[misc] + cls, max_buffer_size: float = 0, item_type: object = None + ) -> tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]]: + if max_buffer_size != math.inf and not isinstance(max_buffer_size, int): + raise ValueError("max_buffer_size must be either an integer or math.inf") + if max_buffer_size < 0: + raise ValueError("max_buffer_size cannot be negative") + if item_type is not None: + warn( + "The item_type argument has been deprecated in AnyIO 4.0. " + "Use create_memory_object_stream[YourItemType](...) instead.", + DeprecationWarning, + stacklevel=2, + ) + + state = _MemoryObjectStreamState[T_Item](max_buffer_size) + return (MemoryObjectSendStream(state), MemoryObjectReceiveStream(state)) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_subprocesses.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_subprocesses.py new file mode 100644 index 0000000..53184a2 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_subprocesses.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +from collections.abc import AsyncIterable, Iterable, Mapping, Sequence +from io import BytesIO +from os import PathLike +from subprocess import PIPE, CalledProcessError, CompletedProcess +from typing import IO, Any, TypeAlias, cast + +from ..abc import Process # noqa: TC001 +from ._eventloop import get_async_backend +from ._tasks import create_task_group + +StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes] + + +async def run_process( + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + input: bytes | None = None, + stdin: int | IO[Any] | None = None, + stdout: int | IO[Any] | None = PIPE, + stderr: int | IO[Any] | None = PIPE, + check: bool = True, + cwd: StrOrBytesPath | None = None, + env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, + start_new_session: bool = False, + pass_fds: Sequence[int] = (), + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, +) -> CompletedProcess[bytes]: + """ + Run an external command in a subprocess and wait until it completes. + + .. seealso:: :func:`subprocess.run` + + :param command: either a string to pass to the shell, or an iterable of strings + containing the executable name or path and its arguments + :param input: bytes passed to the standard input of the subprocess + :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + a file-like object, or `None`; ``input`` overrides this + :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + a file-like object, or `None` + :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + :data:`subprocess.STDOUT`, a file-like object, or `None` + :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the + process terminates with a return code other than 0 + :param cwd: If not ``None``, change the working directory to this before running the + command + :param env: if not ``None``, this mapping replaces the inherited environment + variables from the parent process + :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used + to specify process startup parameters (Windows only) + :param creationflags: flags that can be used to control the creation of the + subprocess (see :class:`subprocess.Popen` for the specifics) + :param start_new_session: if ``true`` the setsid() system call will be made in the + child process prior to the execution of the subprocess. (POSIX only) + :param pass_fds: sequence of file descriptors to keep open between the parent and + child processes. (POSIX only) + :param user: effective user to run the process as (Python >= 3.9, POSIX only) + :param group: effective group to run the process as (Python >= 3.9, POSIX only) + :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9, + POSIX only) + :param umask: if not negative, this umask is applied in the child process before + running the given command (Python >= 3.9, POSIX only) + :return: an object representing the completed process + :raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process + exits with a nonzero return code + + """ + + async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None: + buffer = BytesIO() + async for chunk in stream: + buffer.write(chunk) + + stream_contents[index] = buffer.getvalue() + + if stdin is not None and input is not None: + raise ValueError("only one of stdin and input is allowed") + + async with await open_process( + command, + stdin=PIPE if input else stdin, + stdout=stdout, + stderr=stderr, + cwd=cwd, + env=env, + startupinfo=startupinfo, + creationflags=creationflags, + start_new_session=start_new_session, + pass_fds=pass_fds, + user=user, + group=group, + extra_groups=extra_groups, + umask=umask, + ) as process: + stream_contents: list[bytes | None] = [None, None] + async with create_task_group() as tg: + if process.stdout: + tg.start_soon(drain_stream, process.stdout, 0) + + if process.stderr: + tg.start_soon(drain_stream, process.stderr, 1) + + if process.stdin and input: + await process.stdin.send(input) + await process.stdin.aclose() + + await process.wait() + + output, errors = stream_contents + if check and process.returncode != 0: + raise CalledProcessError(cast(int, process.returncode), command, output, errors) + + return CompletedProcess(command, cast(int, process.returncode), output, errors) + + +async def open_process( + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None = PIPE, + stdout: int | IO[Any] | None = PIPE, + stderr: int | IO[Any] | None = PIPE, + cwd: StrOrBytesPath | None = None, + env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, + start_new_session: bool = False, + pass_fds: Sequence[int] = (), + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, +) -> Process: + """ + Start an external command in a subprocess. + + .. seealso:: :class:`subprocess.Popen` + + :param command: either a string to pass to the shell, or an iterable of strings + containing the executable name or path and its arguments + :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a + file-like object, or ``None`` + :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + a file-like object, or ``None`` + :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + :data:`subprocess.STDOUT`, a file-like object, or ``None`` + :param cwd: If not ``None``, the working directory is changed before executing + :param env: If env is not ``None``, it must be a mapping that defines the + environment variables for the new process + :param creationflags: flags that can be used to control the creation of the + subprocess (see :class:`subprocess.Popen` for the specifics) + :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used + to specify process startup parameters (Windows only) + :param start_new_session: if ``true`` the setsid() system call will be made in the + child process prior to the execution of the subprocess. (POSIX only) + :param pass_fds: sequence of file descriptors to keep open between the parent and + child processes. (POSIX only) + :param user: effective user to run the process as (POSIX only) + :param group: effective group to run the process as (POSIX only) + :param extra_groups: supplementary groups to set in the subprocess (POSIX only) + :param umask: if not negative, this umask is applied in the child process before + running the given command (POSIX only) + :return: an asynchronous process object + + """ + kwargs: dict[str, Any] = {} + if user is not None: + kwargs["user"] = user + + if group is not None: + kwargs["group"] = group + + if extra_groups is not None: + kwargs["extra_groups"] = extra_groups + + if umask >= 0: + kwargs["umask"] = umask + + return await get_async_backend().open_process( + command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + cwd=cwd, + env=env, + startupinfo=startupinfo, + creationflags=creationflags, + start_new_session=start_new_session, + pass_fds=pass_fds, + **kwargs, + ) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_synchronization.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_synchronization.py new file mode 100644 index 0000000..cb988c6 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_synchronization.py @@ -0,0 +1,778 @@ +from __future__ import annotations + +import math +import sys +from collections import deque +from collections.abc import Callable +from dataclasses import dataclass +from types import TracebackType +from typing import TypeVar + +from ..lowlevel import checkpoint_if_cancelled +from ._eventloop import get_async_backend +from ._exceptions import BusyResourceError, NoEventLoopError +from ._tasks import CancelScope +from ._testing import TaskInfo, get_current_task + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +T = TypeVar("T") + + +@dataclass(frozen=True) +class EventStatistics: + """ + :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait` + """ + + tasks_waiting: int + + +@dataclass(frozen=True) +class CapacityLimiterStatistics: + """ + :ivar int borrowed_tokens: number of tokens currently borrowed by tasks + :ivar float total_tokens: total number of available tokens + :ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from + this limiter + :ivar int tasks_waiting: number of tasks waiting on + :meth:`~.CapacityLimiter.acquire` or + :meth:`~.CapacityLimiter.acquire_on_behalf_of` + """ + + borrowed_tokens: int + total_tokens: float + borrowers: tuple[object, ...] + tasks_waiting: int + + +@dataclass(frozen=True) +class LockStatistics: + """ + :ivar bool locked: flag indicating if this lock is locked or not + :ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the + lock is not held by any task) + :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire` + """ + + locked: bool + owner: TaskInfo | None + tasks_waiting: int + + +@dataclass(frozen=True) +class ConditionStatistics: + """ + :ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait` + :ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying + :class:`~.Lock` + """ + + tasks_waiting: int + lock_statistics: LockStatistics + + +@dataclass(frozen=True) +class SemaphoreStatistics: + """ + :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire` + + """ + + tasks_waiting: int + + +class Event: + __slots__ = ("__weakref__",) + + def __new__(cls) -> Event: + try: + return get_async_backend().create_event() + except NoEventLoopError: + return EventAdapter() + + def set(self) -> None: + """Set the flag, notifying all listeners.""" + raise NotImplementedError + + def is_set(self) -> bool: + """Return ``True`` if the flag is set, ``False`` if not.""" + raise NotImplementedError + + async def wait(self) -> None: + """ + Wait until the flag has been set. + + If the flag has already been set when this method is called, it returns + immediately. + + """ + raise NotImplementedError + + def statistics(self) -> EventStatistics: + """Return statistics about the current state of this event.""" + raise NotImplementedError + + +class EventAdapter(Event): + __slots__ = "_internal_event", "_is_set" + + def __new__(cls) -> Self: + return object.__new__(cls) + + def __init__(self) -> None: + self._internal_event: Event | None = None + self._is_set = False + + @property + def _event(self) -> Event: + if self._internal_event is None: + self._internal_event = get_async_backend().create_event() + if self._is_set: + self._internal_event.set() + + return self._internal_event + + def set(self) -> None: + if self._internal_event is None: + self._is_set = True + else: + self._event.set() + + def is_set(self) -> bool: + if self._internal_event is None: + return self._is_set + + return self._internal_event.is_set() + + async def wait(self) -> None: + await self._event.wait() + + def statistics(self) -> EventStatistics: + if self._internal_event is None: + return EventStatistics(tasks_waiting=0) + + return self._internal_event.statistics() + + +class Lock: + __slots__ = ("__weakref__",) + + def __new__(cls, *, fast_acquire: bool = False) -> Lock: + try: + return get_async_backend().create_lock(fast_acquire=fast_acquire) + except NoEventLoopError: + return LockAdapter(fast_acquire=fast_acquire) + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + async def acquire(self) -> None: + """Acquire the lock.""" + raise NotImplementedError + + def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + raise NotImplementedError + + def release(self) -> None: + """Release the lock.""" + raise NotImplementedError + + def locked(self) -> bool: + """Return True if the lock is currently held.""" + raise NotImplementedError + + def statistics(self) -> LockStatistics: + """ + Return statistics about the current state of this lock. + + .. versionadded:: 3.0 + """ + raise NotImplementedError + + +class LockAdapter(Lock): + __slots__ = "_fast_acquire", "_internal_lock" + + def __new__(cls, *, fast_acquire: bool = False) -> Self: + return object.__new__(cls) + + def __init__(self, *, fast_acquire: bool = False): + self._internal_lock: Lock | None = None + self._fast_acquire = fast_acquire + + @property + def _lock(self) -> Lock: + if self._internal_lock is None: + self._internal_lock = get_async_backend().create_lock( + fast_acquire=self._fast_acquire + ) + + return self._internal_lock + + async def __aenter__(self) -> None: + await self._lock.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self._internal_lock is not None: + self._internal_lock.release() + + async def acquire(self) -> None: + """Acquire the lock.""" + await self._lock.acquire() + + def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + self._lock.acquire_nowait() + + def release(self) -> None: + """Release the lock.""" + self._lock.release() + + def locked(self) -> bool: + """Return True if the lock is currently held.""" + return self._lock.locked() + + def statistics(self) -> LockStatistics: + """ + Return statistics about the current state of this lock. + + .. versionadded:: 3.0 + + """ + if self._internal_lock is None: + return LockStatistics(False, None, 0) + + return self._internal_lock.statistics() + + +class Condition: + __slots__ = "__weakref__", "_lock", "_owner_task", "_waiters" + + def __init__(self, lock: Lock | None = None): + self._owner_task: TaskInfo | None = None + self._lock = lock or Lock() + self._waiters: deque[Event] = deque() + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + def _check_acquired(self) -> None: + if self._owner_task != get_current_task(): + raise RuntimeError("The current task is not holding the underlying lock") + + async def acquire(self) -> None: + """Acquire the underlying lock.""" + await self._lock.acquire() + self._owner_task = get_current_task() + + def acquire_nowait(self) -> None: + """ + Acquire the underlying lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + self._lock.acquire_nowait() + self._owner_task = get_current_task() + + def release(self) -> None: + """Release the underlying lock.""" + self._lock.release() + + def locked(self) -> bool: + """Return True if the lock is set.""" + return self._lock.locked() + + def notify(self, n: int = 1) -> None: + """Notify exactly n listeners.""" + self._check_acquired() + for _ in range(n): + try: + event = self._waiters.popleft() + except IndexError: + break + + event.set() + + def notify_all(self) -> None: + """Notify all the listeners.""" + self._check_acquired() + for event in self._waiters: + event.set() + + self._waiters.clear() + + async def wait(self) -> None: + """Wait for a notification.""" + await checkpoint_if_cancelled() + self._check_acquired() + event = Event() + self._waiters.append(event) + self.release() + try: + await event.wait() + except BaseException: + if not event.is_set(): + self._waiters.remove(event) + elif self._waiters: + # This task was notified by could not act on it, so pass + # it on to the next task + self._waiters.popleft().set() + + raise + finally: + with CancelScope(shield=True): + await self.acquire() + + async def wait_for(self, predicate: Callable[[], T]) -> T: + """ + Wait until a predicate becomes true. + + :param predicate: a callable that returns a truthy value when the condition is + met + :return: the result of the predicate + + .. versionadded:: 4.11.0 + + """ + while not (result := predicate()): + await self.wait() + + return result + + def statistics(self) -> ConditionStatistics: + """ + Return statistics about the current state of this condition. + + .. versionadded:: 3.0 + """ + return ConditionStatistics(len(self._waiters), self._lock.statistics()) + + +class Semaphore: + __slots__ = "__weakref__", "_fast_acquire" + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Semaphore: + try: + return get_async_backend().create_semaphore( + initial_value, max_value=max_value, fast_acquire=fast_acquire + ) + except NoEventLoopError: + return SemaphoreAdapter(initial_value, max_value=max_value) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ): + if not isinstance(initial_value, int): + raise TypeError("initial_value must be an integer") + if initial_value < 0: + raise ValueError("initial_value must be >= 0") + if max_value is not None: + if not isinstance(max_value, int): + raise TypeError("max_value must be an integer or None") + if max_value < initial_value: + raise ValueError( + "max_value must be equal to or higher than initial_value" + ) + + self._fast_acquire = fast_acquire + + async def __aenter__(self) -> Self: + await self.acquire() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + async def acquire(self) -> None: + """Decrement the semaphore value, blocking if necessary.""" + raise NotImplementedError + + def acquire_nowait(self) -> None: + """ + Acquire the underlying lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + raise NotImplementedError + + def release(self) -> None: + """Increment the semaphore value.""" + raise NotImplementedError + + @property + def value(self) -> int: + """The current value of the semaphore.""" + raise NotImplementedError + + @property + def max_value(self) -> int | None: + """The maximum value of the semaphore.""" + raise NotImplementedError + + def statistics(self) -> SemaphoreStatistics: + """ + Return statistics about the current state of this semaphore. + + .. versionadded:: 3.0 + """ + raise NotImplementedError + + +class SemaphoreAdapter(Semaphore): + __slots__ = "_initial_value", "_internal_semaphore", "_max_value" + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Self: + return object.__new__(cls) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> None: + super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire) + self._internal_semaphore: Semaphore | None = None + self._initial_value = initial_value + self._max_value = max_value + + @property + def _semaphore(self) -> Semaphore: + if self._internal_semaphore is None: + self._internal_semaphore = get_async_backend().create_semaphore( + self._initial_value, max_value=self._max_value + ) + + return self._internal_semaphore + + async def acquire(self) -> None: + await self._semaphore.acquire() + + def acquire_nowait(self) -> None: + self._semaphore.acquire_nowait() + + def release(self) -> None: + self._semaphore.release() + + @property + def value(self) -> int: + if self._internal_semaphore is None: + return self._initial_value + + return self._semaphore.value + + @property + def max_value(self) -> int | None: + return self._max_value + + def statistics(self) -> SemaphoreStatistics: + if self._internal_semaphore is None: + return SemaphoreStatistics(tasks_waiting=0) + + return self._semaphore.statistics() + + +class CapacityLimiter: + __slots__ = ("__weakref__",) + + def __new__(cls, total_tokens: float) -> CapacityLimiter: + try: + return get_async_backend().create_capacity_limiter(total_tokens) + except NoEventLoopError: + return CapacityLimiterAdapter(total_tokens) + + async def __aenter__(self) -> None: + raise NotImplementedError + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + raise NotImplementedError + + @property + def total_tokens(self) -> float: + """ + The total number of tokens available for borrowing. + + This is a read-write property. If the total number of tokens is increased, the + proportionate number of tasks waiting on this limiter will be granted their + tokens. + + .. versionchanged:: 3.0 + The property is now writable. + .. versionchanged:: 4.12 + The value can now be set to 0. + + """ + raise NotImplementedError + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + raise NotImplementedError + + @property + def borrowed_tokens(self) -> int: + """The number of tokens that have currently been borrowed.""" + raise NotImplementedError + + @property + def available_tokens(self) -> float: + """The number of tokens currently available to be borrowed""" + raise NotImplementedError + + def acquire_nowait(self) -> None: + """ + Acquire a token for the current task without waiting for one to become + available. + + :raises ~anyio.WouldBlock: if there are no tokens available for borrowing + + """ + raise NotImplementedError + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + """ + Acquire a token without waiting for one to become available. + + :param borrower: the entity borrowing a token + :raises ~anyio.WouldBlock: if there are no tokens available for borrowing + + """ + raise NotImplementedError + + async def acquire(self) -> None: + """ + Acquire a token for the current task, waiting if necessary for one to become + available. + + """ + raise NotImplementedError + + async def acquire_on_behalf_of(self, borrower: object) -> None: + """ + Acquire a token, waiting if necessary for one to become available. + + :param borrower: the entity borrowing a token + + """ + raise NotImplementedError + + def release(self) -> None: + """ + Release the token held by the current task. + + :raises RuntimeError: if the current task has not borrowed a token from this + limiter. + + """ + raise NotImplementedError + + def release_on_behalf_of(self, borrower: object) -> None: + """ + Release the token held by the given borrower. + + :raises RuntimeError: if the borrower has not borrowed a token from this + limiter. + + """ + raise NotImplementedError + + def statistics(self) -> CapacityLimiterStatistics: + """ + Return statistics about the current state of this limiter. + + .. versionadded:: 3.0 + + """ + raise NotImplementedError + + +class CapacityLimiterAdapter(CapacityLimiter): + __slots__ = "_internal_limiter", "_total_tokens" + + def __new__(cls, total_tokens: float) -> Self: + return object.__new__(cls) + + def __init__(self, total_tokens: float) -> None: + self._internal_limiter: CapacityLimiter | None = None + self.total_tokens = total_tokens + + @property + def _limiter(self) -> CapacityLimiter: + if self._internal_limiter is None: + self._internal_limiter = get_async_backend().create_capacity_limiter( + self._total_tokens + ) + + return self._internal_limiter + + async def __aenter__(self) -> None: + await self._limiter.__aenter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + return await self._limiter.__aexit__(exc_type, exc_val, exc_tb) + + @property + def total_tokens(self) -> float: + if self._internal_limiter is None: + return self._total_tokens + + return self._internal_limiter.total_tokens + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + if not isinstance(value, int) and not math.isinf(value): + raise TypeError("total_tokens must be an int or math.inf") + elif value < 0: + raise ValueError("total_tokens must be >= 0") + + if self._internal_limiter is None: + self._total_tokens = value + return + + self._limiter.total_tokens = value + + @property + def borrowed_tokens(self) -> int: + if self._internal_limiter is None: + return 0 + + return self._internal_limiter.borrowed_tokens + + @property + def available_tokens(self) -> float: + if self._internal_limiter is None: + return self._total_tokens + + return self._internal_limiter.available_tokens + + def acquire_nowait(self) -> None: + self._limiter.acquire_nowait() + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + self._limiter.acquire_on_behalf_of_nowait(borrower) + + async def acquire(self) -> None: + await self._limiter.acquire() + + async def acquire_on_behalf_of(self, borrower: object) -> None: + await self._limiter.acquire_on_behalf_of(borrower) + + def release(self) -> None: + self._limiter.release() + + def release_on_behalf_of(self, borrower: object) -> None: + self._limiter.release_on_behalf_of(borrower) + + def statistics(self) -> CapacityLimiterStatistics: + if self._internal_limiter is None: + return CapacityLimiterStatistics( + borrowed_tokens=0, + total_tokens=self.total_tokens, + borrowers=(), + tasks_waiting=0, + ) + + return self._internal_limiter.statistics() + + +class ResourceGuard: + """ + A context manager for ensuring that a resource is only used by a single task at a + time. + + Entering this context manager while the previous has not exited it yet will trigger + :exc:`BusyResourceError`. + + :param action: the action to guard against (visible in the :exc:`BusyResourceError` + when triggered, e.g. "Another task is already {action} this resource") + + .. versionadded:: 4.1 + """ + + __slots__ = "__weakref__", "_guarded", "action" + + def __init__(self, action: str = "using"): + self.action: str = action + self._guarded = False + + def __enter__(self) -> None: + if self._guarded: + raise BusyResourceError(self.action) + + self._guarded = True + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._guarded = False diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_tasks.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_tasks.py new file mode 100644 index 0000000..044eda6 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_tasks.py @@ -0,0 +1,464 @@ +from __future__ import annotations + +import math +import sys +from collections.abc import ( + Coroutine, + Generator, +) +from contextlib import contextmanager +from enum import Enum, auto +from inspect import iscoroutine +from types import TracebackType +from typing import Any, Generic, final + +from ..abc import TaskGroup, TaskStatus +from ._eventloop import get_async_backend, get_cancelled_exc_class +from ._exceptions import TaskCancelled, TaskFailed, TaskNotFinished + +if sys.version_info >= (3, 13): + from typing import TypeVar +else: + from typing_extensions import TypeVar + +if sys.version_info >= (3, 11): + from typing import Never, Self, TypeVarTuple +else: + from typing_extensions import Never, Self, TypeVarTuple + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) +T_startval_co = TypeVar("T_startval_co", covariant=True, default=Never) +PosArgsT = TypeVarTuple("PosArgsT") + + +class _IgnoredTaskStatus(TaskStatus[object]): + def started(self, value: object = None) -> None: + pass + + +TASK_STATUS_IGNORED = _IgnoredTaskStatus() + + +class CancelScope: + """ + Wraps a unit of work that can be made separately cancellable. + + :param deadline: The time (clock value) when this scope is cancelled automatically + :param shield: ``True`` to shield the cancel scope from external cancellation + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + """ + + __slots__ = ("__weakref__",) + + def __new__( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + return get_async_backend().create_cancel_scope(shield=shield, deadline=deadline) + + def cancel(self, reason: str | None = None) -> None: + """ + Cancel this scope immediately. + + :param reason: a message describing the reason for the cancellation + + """ + raise NotImplementedError + + @property + def deadline(self) -> float: + """ + The time (clock value) when this scope is cancelled automatically. + + Will be ``float('inf')`` if no timeout has been set. + + """ + raise NotImplementedError + + @deadline.setter + def deadline(self, value: float) -> None: + raise NotImplementedError + + @property + def cancel_called(self) -> bool: + """``True`` if :meth:`cancel` has been called.""" + raise NotImplementedError + + @property + def cancelled_caught(self) -> bool: + """ + ``True`` if this scope suppressed a cancellation exception it itself raised. + + This is typically used to check if any work was interrupted, or to see if the + scope was cancelled due to its deadline being reached. The value will, however, + only be ``True`` if the cancellation was triggered by the scope itself (and not + an outer scope). + + """ + raise NotImplementedError + + @property + def shield(self) -> bool: + """ + ``True`` if this scope is shielded from external cancellation. + + While a scope is shielded, it will not receive cancellations from outside. + + """ + raise NotImplementedError + + @shield.setter + def shield(self, value: bool) -> None: + raise NotImplementedError + + def __enter__(self) -> Self: + raise NotImplementedError + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + raise NotImplementedError + + +@contextmanager +def fail_at( + deadline: float | None, shield: bool = False, reason: str | None = None +) -> Generator[CancelScope, None, None]: + """ + Create a context manager which raises a :class:`TimeoutError` if the code in the + enclosing context does not finish by the given deadline. + + :param deadline: the deadline before raising the exception, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :param reason: explanation for timeout to add to the message of a raised `TimeoutError` + :return: a context manager that yields a cancel scope + :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\] + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. versionadded:: 4.15.0 + + """ + current_time = get_async_backend().current_time + effective_deadline = math.inf if deadline is None else deadline + with get_async_backend().create_cancel_scope( + deadline=effective_deadline, shield=shield + ) as cancel_scope: + yield cancel_scope + + if cancel_scope.cancelled_caught and current_time() >= cancel_scope.deadline: + raise TimeoutError(reason) if reason else TimeoutError + + +@contextmanager +def fail_after( + delay: float | None, shield: bool = False, reason: str | None = None +) -> Generator[CancelScope, None, None]: + """ + Create a context manager which raises a :class:`TimeoutError` if the code in the + enclosing context block does not finish in time. + + :param delay: maximum allowed time (in seconds) before raising the exception, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :param reason: explanation for timeout to add to the message of a raised `TimeoutError` + :return: a context manager that yields a cancel scope + :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\] + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + current_time = get_async_backend().current_time + deadline = (current_time() + delay) if delay is not None else math.inf + with fail_at(deadline, shield=shield, reason=reason) as scope: + yield scope + + +def move_on_at(deadline: float | None, shield: bool = False) -> CancelScope: + """ + Create a cancel scope with a deadline that expires after the given delay. + + :param deadline: the deadline before exiting the context block, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :return: a cancel scope + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. versionadded:: 4.15.0 + + """ + return get_async_backend().create_cancel_scope( + deadline=deadline if deadline is not None else math.inf, shield=shield + ) + + +def move_on_after(delay: float | None, shield: bool = False) -> CancelScope: + """ + Create a cancel scope with a deadline that expires after the given delay. + + :param delay: maximum allowed time (in seconds) before exiting the context block, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :return: a cancel scope + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. note:: Unlike with :func:`fail_after`, the timer starts when this function is + called, not when the context manager is entered. This will be changed in v5.0. + + """ + deadline = ( + (get_async_backend().current_time() + delay) if delay is not None else math.inf + ) + return get_async_backend().create_cancel_scope(deadline=deadline, shield=shield) + + +def current_effective_deadline() -> float: + """ + Return the nearest deadline among all the cancel scopes effective for the current + task. + + :return: a clock value from the event loop's internal clock (or ``float('inf')`` if + there is no deadline in effect, or ``float('-inf')`` if the current scope has + been cancelled) + :rtype: float + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().current_effective_deadline() + + +def create_task_group() -> TaskGroup: + """ + Create a task group. + + :return: a task group + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().create_task_group() + + +@final +class TaskHandle(Generic[T_co, T_startval_co]): + """ + Returned from the task-spawning methods of :class:`TaskGroup`. Can be awaited on to + get the return value of the task (or the raised exception). If the task was + terminated by a :exc:`BaseException`, :exc:`TaskFailed` will be raised (or its + subclass :exc:`TaskCancelled` if the task was cancelled). + + .. versionadded:: 4.14.0 + """ + + class Status(Enum): + """ + The status of a task handle. + + .. attribute:: PENDING + + The task has not finished yet. + .. attribute:: FINISHED + + The task has finished with a return value. + .. attribute:: CANCELLING + + The task has been cancelled but has not finished yet. + .. attribute:: CANCELLED + + The task was cancelled and has finished since. + .. attribute:: FAILED + + The task raised an exception. + """ + + PENDING = auto() + FINISHED = auto() + CANCELLING = auto() + CANCELLED = auto() + FAILED = auto() + + __slots__ = ( + "__weakref__", + "_cancel_scope", + "_coro", + "_exception", + "_finished_event", + "_name", + "_return_value", + "_start_value", + ) + + _return_value: T_co + _start_value: T_startval_co + + def __init__( + self, + coro: Coroutine[Any, Any, T_co], + name: object, + cancel_scope: CancelScope | None = None, + ) -> None: + from ._synchronization import Event + + self._coro = coro + self._cancel_scope = cancel_scope if cancel_scope is not None else CancelScope() + self._finished_event = Event() + self._exception: BaseException | None = None + + if name is not None: + self._name = str(name) + elif iscoroutine(coro): + self._name = coro.__qualname__ + else: + self._name = str(coro) # coroutine-like object (e.g. asend() objects) + + async def _run_coro(self) -> None: + __tracebackhide__ = True + + with self._cancel_scope: + try: + retval = await self._coro + except BaseException as exc: + self._exception = exc + raise + else: + self._return_value = retval + finally: + self._finished_event.set() + del self # Break the reference cycle + + def cancel(self) -> None: + """ + Set the task to a cancelled state. + + This will interrupt any interruptible asynchronous operation, and will cause + any further awaits on this task to get immediately cancelled, unless done in + a shielded cancel scope. + + If the task has already finished, this method has no effect. + """ + if not self._finished_event.is_set(): + self._cancel_scope.cancel() + + @property + def coro(self) -> Coroutine[Any, Any, T_co]: + """ + The coroutine object that was passed to one of the task-spawning methods in + :class:`TaskGroup`. + """ + return self._coro + + @property + def status(self) -> TaskHandle.Status: + """ + The current status of the task. + + Every task starts in the :attr:`~TaskHandle.Status.PENDING` state. + If a task is cancelled while in this state, it will transition to the + :attr:`~TaskHandle.Status.CANCELLING` state. When the task finishes, it will + transition to one of the three final states ( + :attr:`~TaskHandle.Status.FINISHED`, :attr:`~TaskHandle.Status.FAILED`, or + :attr:`~TaskHandle.Status.CANCELLING`) depending on the exception the task + raised, if any. No other status transitions will happen. + """ + if not self._finished_event.is_set(): + if self._cancel_scope.cancel_called: + return TaskHandle.Status.CANCELLING + else: + return TaskHandle.Status.PENDING + elif self._exception is not None: + if isinstance(self._exception, get_cancelled_exc_class()): + return TaskHandle.Status.CANCELLED + else: + return TaskHandle.Status.FAILED + else: + return TaskHandle.Status.FINISHED + + @property + def name(self) -> str: + """The name of the task.""" + return self._name + + @property + def exception(self) -> BaseException | None: + """ + The exception raised by the task, or ``None`` if it finished without raising. + + :raises TaskNotFinished: if the task has not finished yet + :raises TaskCancelled: if the task was cancelled + + """ + match self.status: + case TaskHandle.Status.PENDING: + raise TaskNotFinished("the task has not finished yet") + case TaskHandle.Status.FINISHED: + return None + case TaskHandle.Status.CANCELLING: + raise TaskCancelled("the task was cancelled") + case TaskHandle.Status.CANCELLED: + raise TaskCancelled("the task was cancelled") from self._exception + case TaskHandle.Status.FAILED: + return self._exception + + @property + def return_value(self) -> T_co: + """ + The return value of the task. + + :raises TaskNotFinished: if the task has not finished yet + :raises TaskCancelled: if the task was cancelled + :raises TaskFailed: if the task raised an exception + + """ + match self.status: + case TaskHandle.Status.PENDING: + raise TaskNotFinished("the task has not finished yet") + case TaskHandle.Status.FINISHED: + return self._return_value + case TaskHandle.Status.CANCELLING: + raise TaskCancelled("the task was cancelled") + case TaskHandle.Status.CANCELLED: + raise TaskCancelled("the task was cancelled") from self._exception + case TaskHandle.Status.FAILED: + raise TaskFailed("the task raised an exception") from self._exception + + @property + def start_value(self) -> T_startval_co: + """ + The value passed to :meth:`task_status.started() <.abc.TaskStatus.started>`, + + :raises RuntimeError: if the task was not started with :meth:`TaskGroup.start() + <.abc.TaskGroup.start>` + """ + try: + return self._start_value + except AttributeError: + raise RuntimeError( + "the task was not started with TaskGroup.start()" + ) from None + + async def wait(self) -> None: + """ + Wait for the task to finish. + + This method will return as soon as the task has finished, no matter how it + happened. + """ + await self._finished_event.wait() + + def __await__(self) -> Generator[Any, Any, T_co]: + yield from self._finished_event.wait().__await__() + return self.return_value + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self.status.name.lower()} " + f"name={self._name!r} coro={self._coro!r}>" + ) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_tempfile.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_tempfile.py new file mode 100644 index 0000000..9603086 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_tempfile.py @@ -0,0 +1,615 @@ +from __future__ import annotations + +import os +import sys +import tempfile +from collections.abc import Iterable +from io import BytesIO, TextIOWrapper +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + AnyStr, + Generic, + overload, +) + +from .. import to_thread +from .._core._fileio import AsyncFile +from ..lowlevel import checkpoint_if_cancelled +from ._tasks import CancelScope + +if TYPE_CHECKING: + from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer + + +class TemporaryFile(Generic[AnyStr]): + """ + An asynchronous temporary file that is automatically created and cleaned up. + + This class provides an asynchronous context manager interface to a temporary file. + The file is created using Python's standard `tempfile.TemporaryFile` function in a + background thread, and is wrapped as an asynchronous file using `AsyncFile`. + + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file. Only applicable in + text mode. + :param newline: Controls how universal newlines mode works (only applicable in text + mode). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param errors: The error handling scheme used for encoding/decoding errors. + """ + + _async_file: AsyncFile[AnyStr] + + @overload + def __init__( + self: TemporaryFile[bytes], + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + @overload + def __init__( + self: TemporaryFile[str], + mode: OpenTextMode, + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + + def __init__( + self, + mode: OpenTextMode | OpenBinaryMode = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + *, + errors: str | None = None, + ) -> None: + self.mode = mode + self.buffering = buffering + self.encoding = encoding + self.newline = newline + self.suffix: str | None = suffix + self.prefix: str | None = prefix + self.dir: str | None = dir + self.errors = errors + + async def __aenter__(self) -> AsyncFile[AnyStr]: + fp = await to_thread.run_sync( + lambda: tempfile.TemporaryFile( + self.mode, + self.buffering, + self.encoding, + self.newline, + self.suffix, + self.prefix, + self.dir, + errors=self.errors, + ) + ) + self._async_file = AsyncFile(fp) + return self._async_file + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self._async_file.aclose() + + +class NamedTemporaryFile(Generic[AnyStr]): + """ + An asynchronous named temporary file that is automatically created and cleaned up. + + This class provides an asynchronous context manager for a temporary file with a + visible name in the file system. It uses Python's standard + :func:`~tempfile.NamedTemporaryFile` function and wraps the file object with + :class:`AsyncFile` for asynchronous operations. + + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file. Only applicable in + text mode. + :param newline: Controls how universal newlines mode works (only applicable in text + mode). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param delete: Whether to delete the file when it is closed. + :param errors: The error handling scheme used for encoding/decoding errors. + :param delete_on_close: (Python 3.12+) Whether to delete the file on close. + """ + + _async_file: AsyncFile[AnyStr] + + @overload + def __init__( + self: NamedTemporaryFile[bytes], + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + delete: bool = ..., + *, + errors: str | None = ..., + delete_on_close: bool = ..., + ): ... + @overload + def __init__( + self: NamedTemporaryFile[str], + mode: OpenTextMode, + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + delete: bool = ..., + *, + errors: str | None = ..., + delete_on_close: bool = ..., + ): ... + + def __init__( + self, + mode: OpenBinaryMode | OpenTextMode = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + delete: bool = True, + *, + errors: str | None = None, + delete_on_close: bool = True, + ) -> None: + self._params: dict[str, Any] = { + "mode": mode, + "buffering": buffering, + "encoding": encoding, + "newline": newline, + "suffix": suffix, + "prefix": prefix, + "dir": dir, + "delete": delete, + "errors": errors, + } + if sys.version_info >= (3, 12): + self._params["delete_on_close"] = delete_on_close + + async def __aenter__(self) -> AsyncFile[AnyStr]: + fp = await to_thread.run_sync( + lambda: tempfile.NamedTemporaryFile(**self._params) + ) + self._async_file = AsyncFile(fp) + return self._async_file + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self._async_file.aclose() + + +class SpooledTemporaryFile(AsyncFile[AnyStr]): + """ + An asynchronous spooled temporary file that starts in memory and is spooled to disk. + + This class provides an asynchronous interface to a spooled temporary file, much like + Python's standard :class:`~tempfile.SpooledTemporaryFile`. It supports asynchronous + write operations and provides a method to force a rollover to disk. + + :param max_size: Maximum size in bytes before the file is rolled over to disk. + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file (text mode only). + :param newline: Controls how universal newlines mode works (text mode only). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param errors: The error handling scheme used for encoding/decoding errors. + """ + + _rolled: bool = False + + @overload + def __init__( + self: SpooledTemporaryFile[bytes], + max_size: int = ..., + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + @overload + def __init__( + self: SpooledTemporaryFile[str], + max_size: int = ..., + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + + def __init__( + self, + max_size: int = 0, + mode: OpenBinaryMode | OpenTextMode = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + *, + errors: str | None = None, + ) -> None: + self._tempfile_params: dict[str, Any] = { + "mode": mode, + "buffering": buffering, + "encoding": encoding, + "newline": newline, + "suffix": suffix, + "prefix": prefix, + "dir": dir, + "errors": errors, + } + self._max_size = max_size + if "b" in mode: + super().__init__(BytesIO()) # type: ignore[arg-type] + else: + super().__init__( + TextIOWrapper( # type: ignore[arg-type] + BytesIO(), + encoding=encoding, + errors=errors, + newline=newline, + write_through=True, + ) + ) + + async def aclose(self) -> None: + if not self._rolled: + self._fp.close() + return + + await super().aclose() + + async def _check(self) -> None: + if self._rolled or self._fp.tell() <= self._max_size: + return + + await self.rollover() + + async def rollover(self) -> None: + if self._rolled: + return + + self._rolled = True + buffer = self._fp + buffer.seek(0) + self._fp = await to_thread.run_sync( + lambda: tempfile.TemporaryFile(**self._tempfile_params) + ) + await self.write(buffer.read()) + buffer.close() + + @property + def closed(self) -> bool: + return self._fp.closed + + async def read(self, size: int = -1) -> AnyStr: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.read(size) + + return await super().read(size) # type: ignore[return-value] + + async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.read1(size) + + return await super().read1(size) + + async def readline(self) -> AnyStr: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readline() + + return await super().readline() # type: ignore[return-value] + + async def readlines(self) -> list[AnyStr]: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readlines() + + return await super().readlines() # type: ignore[return-value] + + async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readinto(b) + + return await super().readinto(b) + + async def readinto1(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readinto1(b) + + return await super().readinto1(b) + + async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.seek(offset, whence) + + return await super().seek(offset, whence) + + async def tell(self) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.tell() + + return await super().tell() + + async def truncate(self, size: int | None = None) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.truncate(size) + + return await super().truncate(size) + + @overload + async def write(self: SpooledTemporaryFile[bytes], b: ReadableBuffer) -> int: ... + @overload + async def write(self: SpooledTemporaryFile[str], b: str) -> int: ... + + async def write(self, b: ReadableBuffer | str) -> int: + """ + Asynchronously write data to the spooled temporary file. + + If the file has not yet been rolled over, the data is written synchronously, + and a rollover is triggered if the size exceeds the maximum size. + + :param s: The data to write. + :return: The number of bytes written. + :raises RuntimeError: If the underlying file is not initialized. + + """ + if not self._rolled: + await checkpoint_if_cancelled() + result = self._fp.write(b) + await self._check() + return result + + return await super().write(b) # type: ignore[misc] + + @overload + async def writelines( + self: SpooledTemporaryFile[bytes], lines: Iterable[ReadableBuffer] + ) -> None: ... + @overload + async def writelines( + self: SpooledTemporaryFile[str], lines: Iterable[str] + ) -> None: ... + + async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> None: + """ + Asynchronously write a list of lines to the spooled temporary file. + + If the file has not yet been rolled over, the lines are written synchronously, + and a rollover is triggered if the size exceeds the maximum size. + + :param lines: An iterable of lines to write. + :raises RuntimeError: If the underlying file is not initialized. + + """ + if not self._rolled: + await checkpoint_if_cancelled() + result = self._fp.writelines(lines) + await self._check() + return result + + return await super().writelines(lines) # type: ignore[misc] + + +class TemporaryDirectory(Generic[AnyStr]): + """ + An asynchronous temporary directory that is created and cleaned up automatically. + + This class provides an asynchronous context manager for creating a temporary + directory. It wraps Python's standard :class:`~tempfile.TemporaryDirectory` to + perform directory creation and cleanup operations in a background thread. + + :param suffix: Suffix to be added to the temporary directory name. + :param prefix: Prefix to be added to the temporary directory name. + :param dir: The parent directory where the temporary directory is created. + :param ignore_cleanup_errors: Whether to ignore errors during cleanup + :param delete: Whether to delete the directory upon closing (Python 3.12+). + """ + + def __init__( + self, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + *, + ignore_cleanup_errors: bool = False, + delete: bool = True, + ) -> None: + self.suffix: AnyStr | None = suffix + self.prefix: AnyStr | None = prefix + self.dir: AnyStr | None = dir + self.ignore_cleanup_errors = ignore_cleanup_errors + self.delete = delete + + self._tempdir: tempfile.TemporaryDirectory | None = None + + async def __aenter__(self) -> str: + params: dict[str, Any] = { + "suffix": self.suffix, + "prefix": self.prefix, + "dir": self.dir, + "ignore_cleanup_errors": self.ignore_cleanup_errors, + } + if sys.version_info >= (3, 12): + params["delete"] = self.delete + + self._tempdir = await to_thread.run_sync( + lambda: tempfile.TemporaryDirectory(**params) + ) + return await to_thread.run_sync(self._tempdir.__enter__) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + if self._tempdir is not None: + with CancelScope(shield=True): + await to_thread.run_sync( + self._tempdir.__exit__, exc_type, exc_value, traceback + ) + + async def cleanup(self) -> None: + if self._tempdir is not None: + await to_thread.run_sync(self._tempdir.cleanup) + + +@overload +async def mkstemp( + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + text: bool = False, +) -> tuple[int, str]: ... + + +@overload +async def mkstemp( + suffix: bytes | None = None, + prefix: bytes | None = None, + dir: bytes | None = None, + text: bool = False, +) -> tuple[int, bytes]: ... + + +async def mkstemp( + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + text: bool = False, +) -> tuple[int, str | bytes]: + """ + Asynchronously create a temporary file and return an OS-level handle and the file + name. + + This function wraps `tempfile.mkstemp` and executes it in a background thread. + + :param suffix: Suffix to be added to the file name. + :param prefix: Prefix to be added to the file name. + :param dir: Directory in which the temporary file is created. + :param text: Whether the file is opened in text mode. + :return: A tuple containing the file descriptor and the file name. + + """ + return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text) + + +@overload +async def mkdtemp( + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, +) -> str: ... + + +@overload +async def mkdtemp( + suffix: bytes | None = None, + prefix: bytes | None = None, + dir: bytes | None = None, +) -> bytes: ... + + +async def mkdtemp( + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, +) -> str | bytes: + """ + Asynchronously create a temporary directory and return its path. + + This function wraps `tempfile.mkdtemp` and executes it in a background thread. + + :param suffix: Suffix to be added to the directory name. + :param prefix: Prefix to be added to the directory name. + :param dir: Parent directory where the temporary directory is created. + :return: The path of the created temporary directory. + + """ + return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir) + + +async def gettempdir() -> str: + """ + Asynchronously return the name of the directory used for temporary files. + + This function wraps `tempfile.gettempdir` and executes it in a background thread. + + :return: The path of the temporary directory as a string. + + """ + return await to_thread.run_sync(tempfile.gettempdir) + + +async def gettempdirb() -> bytes: + """ + Asynchronously return the name of the directory used for temporary files in bytes. + + This function wraps `tempfile.gettempdirb` and executes it in a background thread. + + :return: The path of the temporary directory as bytes. + + """ + return await to_thread.run_sync(tempfile.gettempdirb) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_testing.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_testing.py new file mode 100644 index 0000000..fa2c391 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_testing.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Generator +from typing import Any, cast + +from ._eventloop import get_async_backend + + +class TaskInfo: + """ + Represents an asynchronous task. + + :ivar int id: the unique identifier of the task + :ivar parent_id: the identifier of the parent task, if any + :vartype parent_id: Optional[int] + :ivar str name: the description of the task (if any) + :ivar ~collections.abc.Coroutine coro: the coroutine object of the task + """ + + __slots__ = "_name", "coro", "id", "name", "parent_id" + + def __init__( + self, + id: int, + parent_id: int | None, + name: str | None, + coro: Generator[Any, Any, Any] | Awaitable[Any], + ): + func = get_current_task + self._name = f"{func.__module__}.{func.__qualname__}" + self.id: int = id + self.parent_id: int | None = parent_id + self.name: str | None = name + self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro + + def __eq__(self, other: object) -> bool: + if isinstance(other, TaskInfo): + return self.id == other.id + + return NotImplemented + + def __hash__(self) -> int: + return hash(self.id) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(id={self.id!r}, name={self.name!r})" + + def has_pending_cancellation(self) -> bool: + """ + Return ``True`` if the task has a cancellation pending, ``False`` otherwise. + + """ + return False + + +def get_current_task() -> TaskInfo: + """ + Return the current task. + + :return: a representation of the current task + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().get_current_task() + + +def get_running_tasks() -> list[TaskInfo]: + """ + Return a list of running tasks in the current event loop. + + :return: a list of task info objects + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return cast("list[TaskInfo]", get_async_backend().get_running_tasks()) + + +async def wait_all_tasks_blocked() -> None: + """Wait until all other tasks are waiting for something.""" + await get_async_backend().wait_all_tasks_blocked() diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_core/_typedattr.py b/.venv-test/lib/python3.12/site-packages/anyio/_core/_typedattr.py new file mode 100644 index 0000000..59d3da9 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_core/_typedattr.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import sys +from collections.abc import Callable, Mapping +from typing import Any, TypeVar, final, overload + +from ._exceptions import TypedAttributeLookupError + +if sys.version_info < (3, 15): + from typing_extensions import sentinel + +T_Attr = TypeVar("T_Attr") +T_Default = TypeVar("T_Default") +undefined = sentinel("undefined") + + +def typed_attribute() -> Any: + """Return a unique object, used to mark typed attributes.""" + return object() + + +class TypedAttributeSet: + """ + Superclass for typed attribute collections. + + Checks that every public attribute of every subclass has a type annotation. + """ + + def __init_subclass__(cls) -> None: + annotations: dict[str, Any] = getattr(cls, "__annotations__", {}) + for attrname in dir(cls): + if not attrname.startswith("_") and attrname not in annotations: + raise TypeError( + f"Attribute {attrname!r} is missing its type annotation" + ) + + super().__init_subclass__() + + +class TypedAttributeProvider: + """Base class for classes that wish to provide typed extra attributes.""" + + @property + def extra_attributes(self) -> Mapping[T_Attr, Callable[[], T_Attr]]: + """ + A mapping of the extra attributes to callables that return the corresponding + values. + + If the provider wraps another provider, the attributes from that wrapper should + also be included in the returned mapping (but the wrapper may override the + callables from the wrapped instance). + + """ + return {} + + @overload + def extra(self, attribute: T_Attr) -> T_Attr: ... + + @overload + def extra(self, attribute: T_Attr, default: T_Default) -> T_Attr | T_Default: ... + + @final + def extra(self, attribute: Any, default: object = undefined) -> object: + """ + extra(attribute, default=undefined) + + Return the value of the given typed extra attribute. + + :param attribute: the attribute (member of a :class:`~TypedAttributeSet`) to + look for + :param default: the value that should be returned if no value is found for the + attribute + :raises ~anyio.TypedAttributeLookupError: if the search failed and no default + value was given + + """ + try: + getter = self.extra_attributes[attribute] + except KeyError: + if default is undefined: + raise TypedAttributeLookupError("Attribute not found") from None + else: + return default + + return getter() diff --git a/.venv-test/lib/python3.12/site-packages/anyio/_lazyimport.py b/.venv-test/lib/python3.12/site-packages/anyio/_lazyimport.py new file mode 100644 index 0000000..dc0a4fa --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/_lazyimport.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +__all__ = ( + "fix_package_names", + "install_lazy_importer", + "set_deprecated_aliases", +) + +import ast +import inspect +import sys +import warnings +from importlib import import_module +from types import ModuleType +from typing import Any + + +def install_lazy_importer() -> bool: + module_globals = sys._getframe(1).f_globals + module_name = module_globals["__name__"] + module_prefix = module_name + "." + module = sys.modules[module_name] + lazy_map, deprecated_aliases, submodule_names = _build_lazy_map(module) + names = sorted(lazy_map) + + # Delete symbols that are not part of the API + del module_globals["TYPE_CHECKING"] + del module_globals["install_lazy_importer"] + + if not lazy_map and not deprecated_aliases: + return False + + def __getattr__(name: str) -> Any: + if new_name := deprecated_aliases.get(name): + emit_deprecation_warning(module_name, name, new_name) + target_mod, target_attr = new_name.rsplit(".", 1) + elif name in submodule_names: + target_mod, target_attr = "." + name, "" + else: + try: + target_mod, target_attr = lazy_map[name] + except KeyError: + raise AttributeError( + f"module {module_name!r} has no attribute {name!r}" + ) from None + + imported = import_module(target_mod, module_name) + value = getattr(imported, target_attr) if target_attr else imported + + # patch the module name to match + if ( + getattr(value, "__module__", "").startswith(module_prefix) + and name not in deprecated_aliases + ): + value.__module__ = module_name + + module_globals[name] = value + return value + + def __dir__() -> list[str]: + return names + + module_globals["__dir__"] = __dir__ + module_globals["__getattr__"] = __getattr__ + module_globals.pop("fix_package_names", None) + module_globals.pop("set_deprecated_aliases", None) + return True + + +def fix_package_names() -> None: + module_globals = sys._getframe(1).f_globals + module_prefix = module_globals["__name__"] + "." + del module_globals[fix_package_names.__name__] + for value in module_globals.values(): + if modname := getattr(value, "__module__", ""): + if modname.startswith(module_prefix): + parts = modname.split(".") + value.__module__ = ".".join( + part for part in parts if not part.startswith("_") + ) + + +def emit_deprecation_warning(module_name: str, name: str, target: str) -> None: + warnings.warn( + f"The {module_name}.{name} alias is deprecated, use {target} instead.", + DeprecationWarning, + stacklevel=3, + ) + + +def set_deprecated_aliases(aliases: dict[str, str]) -> None: + module_globals = sys._getframe(1).f_globals + module_name = module_globals["__name__"] + del module_globals[set_deprecated_aliases.__name__] + + def __getattr__(name: str) -> Any: + try: + target = aliases[name] + except KeyError: + raise AttributeError( + f"module {module_name!r} has no attribute {name!r}" + ) from None + + emit_deprecation_warning(module_name, name, target) + target_modname, attrname = target.rsplit(".", 1) + module = import_module(target_modname) + return getattr(module, attrname) + + sys.modules[module_name].__dict__["__getattr__"] = __getattr__ + + +def _build_lazy_map( + module: ModuleType, +) -> tuple[dict[str, tuple[str, str]], dict[str, str], list[str]]: + try: + source = inspect.getsource(module) + except OSError: + return {}, {}, [] + + tree = compile(source, module.__file__ or "", "exec", ast.PyCF_ONLY_AST) + assert isinstance(tree, ast.Module) + out: dict[str, tuple[str, str]] = {} + deprecated_aliases: dict[str, str] = {} + submodule_names: list[str] = [] + + for node in tree.body: + if not isinstance(node, ast.If) or not _is_type_checking_block(node.test): + continue + + for stmt in node.body: + match stmt: + case ast.ImportFrom(): + if stmt.module is None: + submodule_names.extend(alias.name for alias in stmt.names) + else: + base = "." * stmt.level + (stmt.module or "") + for alias in stmt.names: + if alias.name == "*": + raise RuntimeError("star imports not supported") + + exported = alias.asname or alias.name + out[exported] = (base, alias.name) + case ast.Expr() if isinstance(stmt.value, ast.Call): + call = stmt.value + if ( + isinstance(call.func, ast.Name) + and call.func.id == "set_deprecated_aliases" + ): + arg0 = call.args[0] + assert isinstance(arg0, ast.Dict) + for key, value in zip(arg0.keys, arg0.values, strict=True): + assert isinstance(key, ast.Constant) + assert isinstance(key.value, str) + assert isinstance(value, ast.Constant) + assert isinstance(value.value, str) + deprecated_aliases[key.value] = value.value + + return out, deprecated_aliases, submodule_names + + +def _is_type_checking_block(test: ast.AST) -> bool: + if not isinstance(test, ast.BoolOp): + return False + + subtest = test.values[0] + match subtest: + case ast.Name(): + return subtest.id == "TYPE_CHECKING" + case ast.Attribute(): + return ( + isinstance(subtest.value, ast.Name) + and subtest.value.id == "typing" + and subtest.attr == "TYPE_CHECKING" + ) + case _: + return False diff --git a/.venv-test/lib/python3.12/site-packages/anyio/abc/__init__.py b/.venv-test/lib/python3.12/site-packages/anyio/abc/__init__.py new file mode 100644 index 0000000..ba738f8 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/abc/__init__.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._lazyimport import ( + fix_package_names, + install_lazy_importer, + set_deprecated_aliases, +) + +if TYPE_CHECKING or not install_lazy_importer(): + from ._eventloop import AsyncBackend as AsyncBackend + from ._resources import AsyncResource as AsyncResource + from ._sockets import ConnectedUDPSocket as ConnectedUDPSocket + from ._sockets import ConnectedUNIXDatagramSocket as ConnectedUNIXDatagramSocket + from ._sockets import IPAddressType as IPAddressType + from ._sockets import IPSockAddrType as IPSockAddrType + from ._sockets import SocketAttribute as SocketAttribute + from ._sockets import SocketListener as SocketListener + from ._sockets import SocketStream as SocketStream + from ._sockets import UDPPacketType as UDPPacketType + from ._sockets import UDPSocket as UDPSocket + from ._sockets import UNIXDatagramPacketType as UNIXDatagramPacketType + from ._sockets import UNIXDatagramSocket as UNIXDatagramSocket + from ._sockets import UNIXSocketStream as UNIXSocketStream + from ._streams import AnyByteReceiveStream as AnyByteReceiveStream + from ._streams import AnyByteSendStream as AnyByteSendStream + from ._streams import AnyByteStream as AnyByteStream + from ._streams import AnyByteStreamConnectable as AnyByteStreamConnectable + from ._streams import ( + AnyUnreliableByteReceiveStream as AnyUnreliableByteReceiveStream, + ) + from ._streams import AnyUnreliableByteSendStream as AnyUnreliableByteSendStream + from ._streams import AnyUnreliableByteStream as AnyUnreliableByteStream + from ._streams import ByteReceiveStream as ByteReceiveStream + from ._streams import ByteSendStream as ByteSendStream + from ._streams import ByteStream as ByteStream + from ._streams import ByteStreamConnectable as ByteStreamConnectable + from ._streams import Listener as Listener + from ._streams import ObjectReceiveStream as ObjectReceiveStream + from ._streams import ObjectSendStream as ObjectSendStream + from ._streams import ObjectStream as ObjectStream + from ._streams import ObjectStreamConnectable as ObjectStreamConnectable + from ._streams import UnreliableObjectReceiveStream as UnreliableObjectReceiveStream + from ._streams import UnreliableObjectSendStream as UnreliableObjectSendStream + from ._streams import UnreliableObjectStream as UnreliableObjectStream + from ._subprocesses import Process as Process + from ._tasks import TaskGroup as TaskGroup + from ._tasks import TaskStatus as TaskStatus + from ._testing import TestRunner as TestRunner + + fix_package_names() + set_deprecated_aliases( + { + "CapacityLimiter": "anyio.CapacityLimiter", + "Condition": "anyio.Condition", + "Event": "anyio.Event", + "Lock": "anyio.Lock", + "Semaphore": "anyio.Semaphore", + "CancelScope": "anyio.CancelScope", + "BlockingPortal": "anyio.from_thread.BlockingPortal", + } + ) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/abc/_eventloop.py b/.venv-test/lib/python3.12/site-packages/anyio/abc/_eventloop.py new file mode 100644 index 0000000..a3cbed2 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/abc/_eventloop.py @@ -0,0 +1,428 @@ +from __future__ import annotations + +import math +import sys +from abc import ABCMeta, abstractmethod +from collections.abc import ( + AsyncIterator, + Awaitable, + Callable, + Coroutine, + Iterable, + Mapping, + Sequence, +) +from contextlib import AbstractContextManager +from os import PathLike +from signal import Signals +from socket import AddressFamily, SocketKind, socket +from typing import ( + IO, + TYPE_CHECKING, + Any, + TypeAlias, + TypeVar, + overload, +) + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + + from .._core._synchronization import CapacityLimiter, Event, Lock, Semaphore + from .._core._tasks import CancelScope + from .._core._testing import TaskInfo + from ._sockets import ( + ConnectedUDPSocket, + ConnectedUNIXDatagramSocket, + IPSockAddrType, + SocketListener, + SocketStream, + UDPSocket, + UNIXDatagramSocket, + UNIXSocketStream, + ) + from ._subprocesses import Process + from ._tasks import TaskGroup + from ._testing import TestRunner + +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +PosArgsT = TypeVarTuple("PosArgsT") +StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes] + + +class AsyncBackend(metaclass=ABCMeta): + @classmethod + @abstractmethod + def run( + cls, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + options: dict[str, Any], + ) -> T_Retval: + """ + Run the given coroutine function in an asynchronous event loop. + + The current thread must not be already running an event loop. + + :param func: a coroutine function + :param args: positional arguments to ``func`` + :param kwargs: positional arguments to ``func`` + :param options: keyword arguments to call the backend ``run()`` implementation + with + :return: the return value of the coroutine function + """ + + @classmethod + @abstractmethod + def current_token(cls) -> object: + """ + Return an object that allows other threads to run code inside the event loop. + + :return: a token object, specific to the event loop running in the current + thread + """ + + @classmethod + @abstractmethod + def current_time(cls) -> float: + """ + Return the current value of the event loop's internal clock. + + :return: the clock value (seconds) + """ + + @classmethod + @abstractmethod + def cancelled_exception_class(cls) -> type[BaseException]: + """Return the exception class that is raised in a task if it's cancelled.""" + + @classmethod + @abstractmethod + async def checkpoint(cls) -> None: + """ + Check if the task has been cancelled, and allow rescheduling of other tasks. + + This is effectively the same as running :meth:`checkpoint_if_cancelled` and then + :meth:`cancel_shielded_checkpoint`. + """ + + @classmethod + async def checkpoint_if_cancelled(cls) -> None: + """ + Check if the current task group has been cancelled. + + This will check if the task has been cancelled, but will not allow other tasks + to be scheduled if not. + + """ + if cls.current_effective_deadline() == -math.inf: + await cls.checkpoint() + + @classmethod + async def cancel_shielded_checkpoint(cls) -> None: + """ + Allow the rescheduling of other tasks. + + This will give other tasks the opportunity to run, but without checking if the + current task group has been cancelled, unlike with :meth:`checkpoint`. + + """ + with cls.create_cancel_scope(shield=True): + await cls.sleep(0) + + @classmethod + @abstractmethod + async def sleep(cls, delay: float) -> None: + """ + Pause the current task for the specified duration. + + :param delay: the duration, in seconds + """ + + @classmethod + @abstractmethod + def create_cancel_scope( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + pass + + @classmethod + @abstractmethod + def current_effective_deadline(cls) -> float: + """ + Return the nearest deadline among all the cancel scopes effective for the + current task. + + :return: + - a clock value from the event loop's internal clock + - ``inf`` if there is no deadline in effect + - ``-inf`` if the current scope has been cancelled + :rtype: float + """ + + @classmethod + @abstractmethod + def create_task_group(cls) -> TaskGroup: + pass + + @classmethod + @abstractmethod + def create_event(cls) -> Event: + pass + + @classmethod + @abstractmethod + def create_lock(cls, *, fast_acquire: bool) -> Lock: + pass + + @classmethod + @abstractmethod + def create_semaphore( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Semaphore: + pass + + @classmethod + @abstractmethod + def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: + pass + + @classmethod + @abstractmethod + async def run_sync_in_worker_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + abandon_on_cancel: bool = False, + limiter: CapacityLimiter | None = None, + ) -> T_Retval: + pass + + @classmethod + @abstractmethod + def check_cancelled(cls) -> None: + pass + + @classmethod + @abstractmethod + def run_async_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_co: + pass + + @classmethod + @abstractmethod + def run_sync_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_Retval: + pass + + @classmethod + @abstractmethod + async def open_process( + cls, + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None, + stdout: int | IO[Any] | None, + stderr: int | IO[Any] | None, + cwd: StrOrBytesPath | None = None, + env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, + start_new_session: bool = False, + pass_fds: Sequence[int] = (), + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, + **kwargs: Any, + ) -> Process: + pass + + @classmethod + @abstractmethod + def setup_process_pool_exit_at_shutdown(cls, workers: set[Process]) -> None: + pass + + @classmethod + @abstractmethod + async def connect_tcp( + cls, host: str, port: int, local_address: IPSockAddrType | None = None + ) -> SocketStream: + pass + + @classmethod + @abstractmethod + async def connect_unix(cls, path: str | bytes) -> UNIXSocketStream: + pass + + @classmethod + @abstractmethod + def create_tcp_listener(cls, sock: socket) -> SocketListener: + pass + + @classmethod + @abstractmethod + def create_unix_listener(cls, sock: socket) -> SocketListener: + pass + + @classmethod + @abstractmethod + async def create_udp_socket( + cls, + family: AddressFamily, + local_address: IPSockAddrType | None, + remote_address: IPSockAddrType | None, + reuse_port: bool, + ) -> UDPSocket | ConnectedUDPSocket: + pass + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket, remote_path: None + ) -> UNIXDatagramSocket: ... + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket, remote_path: str | bytes + ) -> ConnectedUNIXDatagramSocket: ... + + @classmethod + @abstractmethod + async def create_unix_datagram_socket( + cls, raw_socket: socket, remote_path: str | bytes | None + ) -> UNIXDatagramSocket | ConnectedUNIXDatagramSocket: + pass + + @classmethod + @abstractmethod + async def getaddrinfo( + cls, + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, + ) -> Sequence[ + tuple[ + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], + ] + ]: + pass + + @classmethod + @abstractmethod + async def getnameinfo( + cls, sockaddr: IPSockAddrType, flags: int = 0 + ) -> tuple[str, str]: + pass + + @classmethod + @abstractmethod + async def wait_readable(cls, obj: FileDescriptorLike) -> None: + pass + + @classmethod + @abstractmethod + async def wait_writable(cls, obj: FileDescriptorLike) -> None: + pass + + @classmethod + @abstractmethod + def notify_closing(cls, obj: FileDescriptorLike) -> None: + pass + + @classmethod + @abstractmethod + async def wrap_listener_socket(cls, sock: socket) -> SocketListener: + pass + + @classmethod + @abstractmethod + async def wrap_stream_socket(cls, sock: socket) -> SocketStream: + pass + + @classmethod + @abstractmethod + async def wrap_unix_stream_socket(cls, sock: socket) -> UNIXSocketStream: + pass + + @classmethod + @abstractmethod + async def wrap_udp_socket(cls, sock: socket) -> UDPSocket: + pass + + @classmethod + @abstractmethod + async def wrap_connected_udp_socket(cls, sock: socket) -> ConnectedUDPSocket: + pass + + @classmethod + @abstractmethod + async def wrap_unix_datagram_socket(cls, sock: socket) -> UNIXDatagramSocket: + pass + + @classmethod + @abstractmethod + async def wrap_connected_unix_datagram_socket( + cls, sock: socket + ) -> ConnectedUNIXDatagramSocket: + pass + + @classmethod + @abstractmethod + def current_default_thread_limiter(cls) -> CapacityLimiter: + pass + + @classmethod + @abstractmethod + def open_signal_receiver( + cls, *signals: Signals + ) -> AbstractContextManager[AsyncIterator[Signals]]: + pass + + @classmethod + @abstractmethod + def get_current_task(cls) -> TaskInfo: + pass + + @classmethod + @abstractmethod + def get_running_tasks(cls) -> Sequence[TaskInfo]: + pass + + @classmethod + @abstractmethod + async def wait_all_tasks_blocked(cls) -> None: + pass + + @classmethod + @abstractmethod + def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: + pass diff --git a/.venv-test/lib/python3.12/site-packages/anyio/abc/_resources.py b/.venv-test/lib/python3.12/site-packages/anyio/abc/_resources.py new file mode 100644 index 0000000..9373f71 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/abc/_resources.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import sys +from abc import ABCMeta, abstractmethod +from types import TracebackType +from typing import TypeVar + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +T = TypeVar("T") + + +class AsyncResource(metaclass=ABCMeta): + """ + Abstract base class for all closeable asynchronous resources. + + Works as an asynchronous context manager which returns the instance itself on enter, + and calls :meth:`aclose` on exit. + """ + + __slots__ = () + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.aclose() + + @abstractmethod + async def aclose(self) -> None: + """Close the resource.""" diff --git a/.venv-test/lib/python3.12/site-packages/anyio/abc/_sockets.py b/.venv-test/lib/python3.12/site-packages/anyio/abc/_sockets.py new file mode 100644 index 0000000..d6a1139 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/abc/_sockets.py @@ -0,0 +1,401 @@ +from __future__ import annotations + +import errno +import socket +from abc import abstractmethod +from collections.abc import Callable, Collection, Mapping +from contextlib import AsyncExitStack +from io import IOBase +from ipaddress import IPv4Address, IPv6Address +from socket import AddressFamily +from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar + +from .._core._eventloop import get_async_backend +from .._core._typedattr import ( + TypedAttributeProvider, + TypedAttributeSet, + typed_attribute, +) +from ._streams import ByteStream, Listener, UnreliableObjectStream + +if TYPE_CHECKING: + from ._tasks import TaskGroup + +IPAddressType: TypeAlias = str | IPv4Address | IPv6Address +IPSockAddrType: TypeAlias = tuple[str, int] +SockAddrType: TypeAlias = IPSockAddrType | str +UDPPacketType: TypeAlias = tuple[bytes, IPSockAddrType] +UNIXDatagramPacketType: TypeAlias = tuple[bytes, str] +T_Retval = TypeVar("T_Retval") + + +def _validate_socket( + sock_or_fd: socket.socket | int, + sock_type: socket.SocketKind, + addr_family: socket.AddressFamily = socket.AF_UNSPEC, + *, + require_connected: bool = False, + require_bound: bool = False, +) -> socket.socket: + if isinstance(sock_or_fd, int): + try: + sock = socket.socket(fileno=sock_or_fd) + except OSError as exc: + if exc.errno == errno.ENOTSOCK: + raise ValueError( + "the file descriptor does not refer to a socket" + ) from exc + elif require_connected: + raise ValueError("the socket must be connected") from exc + elif require_bound: + raise ValueError("the socket must be bound to a local address") from exc + else: + raise + elif isinstance(sock_or_fd, socket.socket): + sock = sock_or_fd + else: + raise TypeError( + f"expected an int or socket, got {type(sock_or_fd).__qualname__} instead" + ) + + try: + if require_connected: + try: + sock.getpeername() + except OSError as exc: + raise ValueError("the socket must be connected") from exc + + if require_bound: + try: + if sock.family in (socket.AF_INET, socket.AF_INET6): + bound_addr = sock.getsockname()[1] + else: + bound_addr = sock.getsockname() + except OSError: + bound_addr = None + + if not bound_addr: + raise ValueError("the socket must be bound to a local address") + + if addr_family != socket.AF_UNSPEC and sock.family != addr_family: + raise ValueError( + f"address family mismatch: expected {addr_family.name}, got " + f"{sock.family.name}" + ) + + if sock.type != sock_type: + raise ValueError( + f"socket type mismatch: expected {sock_type.name}, got {sock.type.name}" + ) + except BaseException: + # Avoid ResourceWarning from the locally constructed socket object + if isinstance(sock_or_fd, int): + sock.detach() + + raise + + sock.setblocking(False) + return sock + + +class SocketAttribute(TypedAttributeSet): + """ + .. attribute:: family + :type: socket.AddressFamily + + the address family of the underlying socket + + .. attribute:: local_address + :type: tuple[str, int] | str + + the local address the underlying socket is connected to + + .. attribute:: local_port + :type: int + + for IP based sockets, the local port the underlying socket is bound to + + .. attribute:: raw_socket + :type: socket.socket + + the underlying stdlib socket object + + .. attribute:: remote_address + :type: tuple[str, int] | str + + the remote address the underlying socket is connected to + + .. attribute:: remote_port + :type: int + + for IP based sockets, the remote port the underlying socket is connected to + """ + + family: AddressFamily = typed_attribute() + local_address: SockAddrType = typed_attribute() + local_port: int = typed_attribute() + raw_socket: socket.socket = typed_attribute() + remote_address: SockAddrType = typed_attribute() + remote_port: int = typed_attribute() + + +class _SocketProvider(TypedAttributeProvider): + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + from .._core._sockets import convert_ipv6_sockaddr as convert + + attributes: dict[Any, Callable[[], Any]] = { + SocketAttribute.family: lambda: self._raw_socket.family, + SocketAttribute.local_address: lambda: convert( + self._raw_socket.getsockname() + ), + SocketAttribute.raw_socket: lambda: self._raw_socket, + } + try: + peername: tuple[str, int] | None = convert(self._raw_socket.getpeername()) + except OSError: + peername = None + + # Provide the remote address for connected sockets + if peername is not None: + attributes[SocketAttribute.remote_address] = lambda: peername + + # Provide local and remote ports for IP based sockets + if self._raw_socket.family in (AddressFamily.AF_INET, AddressFamily.AF_INET6): + attributes[SocketAttribute.local_port] = lambda: ( + self._raw_socket.getsockname()[1] + ) + if peername is not None: + remote_port = peername[1] + attributes[SocketAttribute.remote_port] = lambda: remote_port + + return attributes + + @property + @abstractmethod + def _raw_socket(self) -> socket.socket: + pass + + +class SocketStream(ByteStream, _SocketProvider): + """ + Transports bytes over a socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> SocketStream: + """ + Wrap an existing socket object or file descriptor as a socket stream. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a socket stream + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_connected=True) + return await get_async_backend().wrap_stream_socket(sock) + + +class UNIXSocketStream(SocketStream): + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> UNIXSocketStream: + """ + Wrap an existing socket object or file descriptor as a UNIX socket stream. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a UNIX socket stream + + """ + sock = _validate_socket( + sock_or_fd, socket.SOCK_STREAM, socket.AF_UNIX, require_connected=True + ) + return await get_async_backend().wrap_unix_stream_socket(sock) + + @abstractmethod + async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: + """ + Send file descriptors along with a message to the peer. + + :param message: a non-empty bytestring + :param fds: a collection of files (either numeric file descriptors or open file + or socket objects) + """ + + @abstractmethod + async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: + """ + Receive file descriptors along with a message from the peer. + + :param msglen: length of the message to expect from the peer + :param maxfds: maximum number of file descriptors to expect from the peer + :return: a tuple of (message, file descriptors) + """ + + +class SocketListener(Listener[SocketStream], _SocketProvider): + """ + Listens to incoming socket connections. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket( + cls, + sock_or_fd: socket.socket | int, + ) -> SocketListener: + """ + Wrap an existing socket object or file descriptor as a socket listener. + + The newly created listener takes ownership of the socket being passed in. + + :param sock_or_fd: a socket object or file descriptor + :return: a socket listener + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_bound=True) + return await get_async_backend().wrap_listener_socket(sock) + + @abstractmethod + async def accept(self) -> SocketStream: + """Accept an incoming connection.""" + + async def serve( + self, + handler: Callable[[SocketStream], Any], + task_group: TaskGroup | None = None, + ) -> None: + from .. import create_task_group + + async with AsyncExitStack() as stack: + if task_group is None: + task_group = await stack.enter_async_context(create_task_group()) + + while True: + stream = await self.accept() + task_group.start_soon(handler, stream) + + +class UDPSocket(UnreliableObjectStream[UDPPacketType], _SocketProvider): + """ + Represents an unconnected UDP socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> UDPSocket: + """ + Wrap an existing socket object or file descriptor as a UDP socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must be bound to a local address. + + :param sock_or_fd: a socket object or file descriptor + :return: a UDP socket + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, require_bound=True) + return await get_async_backend().wrap_udp_socket(sock) + + async def sendto(self, data: bytes, host: str, port: int) -> None: + """ + Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, (host, port))). + + """ + return await self.send((data, (host, port))) + + +class ConnectedUDPSocket(UnreliableObjectStream[bytes], _SocketProvider): + """ + Represents an connected UDP socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> ConnectedUDPSocket: + """ + Wrap an existing socket object or file descriptor as a connected UDP socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a connected UDP socket + + """ + sock = _validate_socket( + sock_or_fd, + socket.SOCK_DGRAM, + require_connected=True, + ) + return await get_async_backend().wrap_connected_udp_socket(sock) + + +class UNIXDatagramSocket( + UnreliableObjectStream[UNIXDatagramPacketType], _SocketProvider +): + """ + Represents an unconnected Unix datagram socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket( + cls, + sock_or_fd: socket.socket | int, + ) -> UNIXDatagramSocket: + """ + Wrap an existing socket object or file descriptor as a UNIX datagram + socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + + :param sock_or_fd: a socket object or file descriptor + :return: a UNIX datagram socket + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX) + return await get_async_backend().wrap_unix_datagram_socket(sock) + + async def sendto(self, data: bytes, path: str) -> None: + """Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, path)).""" + return await self.send((data, path)) + + +class ConnectedUNIXDatagramSocket(UnreliableObjectStream[bytes], _SocketProvider): + """ + Represents a connected Unix datagram socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket( + cls, + sock_or_fd: socket.socket | int, + ) -> ConnectedUNIXDatagramSocket: + """ + Wrap an existing socket object or file descriptor as a connected UNIX datagram + socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a connected UNIX datagram socket + + """ + sock = _validate_socket( + sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX, require_connected=True + ) + return await get_async_backend().wrap_connected_unix_datagram_socket(sock) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/abc/_streams.py b/.venv-test/lib/python3.12/site-packages/anyio/abc/_streams.py new file mode 100644 index 0000000..daf335d --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/abc/_streams.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar + +from .._core._exceptions import EndOfStream +from .._core._typedattr import TypedAttributeProvider +from ._resources import AsyncResource + +if TYPE_CHECKING: + from ._tasks import TaskGroup + +T_Item = TypeVar("T_Item") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) + + +class UnreliableObjectReceiveStream( + AsyncResource, TypedAttributeProvider, Generic[T_co] +): + """ + An interface for receiving objects. + + This interface makes no guarantees that the received messages arrive in the order in + which they were sent, or that no messages are missed. + + Asynchronously iterating over objects of this type will yield objects matching the + given type parameter. + """ + + def __aiter__(self) -> UnreliableObjectReceiveStream[T_co]: + return self + + async def __anext__(self) -> T_co: + try: + return await self.receive() + except EndOfStream: + raise StopAsyncIteration from None + + @abstractmethod + async def receive(self) -> T_co: + """ + Receive the next item. + + :raises ~anyio.ClosedResourceError: if the receive stream has been explicitly + closed + :raises ~anyio.EndOfStream: if this stream has been closed from the other end + :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable + due to external causes + """ + + +class UnreliableObjectSendStream( + AsyncResource, TypedAttributeProvider, Generic[T_contra] +): + """ + An interface for sending objects. + + This interface makes no guarantees that the messages sent will reach the + recipient(s) in the same order in which they were sent, or at all. + """ + + @abstractmethod + async def send(self, item: T_contra) -> None: + """ + Send an item to the peer(s). + + :param item: the item to send + :raises ~anyio.ClosedResourceError: if the send stream has been explicitly + closed + :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable + due to external causes + """ + + +class UnreliableObjectStream( + UnreliableObjectReceiveStream[T_Item], UnreliableObjectSendStream[T_Item] +): + """ + A bidirectional message stream which does not guarantee the order or reliability of + message delivery. + """ + + +class ObjectReceiveStream(UnreliableObjectReceiveStream[T_co]): + """ + A receive message stream which guarantees that messages are received in the same + order in which they were sent, and that no messages are missed. + """ + + +class ObjectSendStream(UnreliableObjectSendStream[T_contra]): + """ + A send message stream which guarantees that messages are delivered in the same order + in which they were sent, without missing any messages in the middle. + """ + + +class ObjectStream( + ObjectReceiveStream[T_Item], + ObjectSendStream[T_Item], + UnreliableObjectStream[T_Item], +): + """ + A bidirectional message stream which guarantees the order and reliability of message + delivery. + """ + + @abstractmethod + async def send_eof(self) -> None: + """ + Send an end-of-file indication to the peer. + + You should not try to send any further data to this stream after calling this + method. This method is idempotent (does nothing on successive calls). + """ + + +class ByteReceiveStream(AsyncResource, TypedAttributeProvider): + """ + An interface for receiving bytes from a single peer. + + Iterating this byte stream will yield a byte string of arbitrary length, but no more + than 65536 bytes. + """ + + def __aiter__(self) -> ByteReceiveStream: + return self + + async def __anext__(self) -> bytes: + try: + return await self.receive() + except EndOfStream: + raise StopAsyncIteration from None + + @abstractmethod + async def receive(self, max_bytes: int = 65536) -> bytes: + """ + Receive at most ``max_bytes`` bytes from the peer. + + .. note:: Implementers of this interface should not return an empty + :class:`bytes` object, and users should ignore them. + + :param max_bytes: maximum number of bytes to receive (must be a positive + integer) + :return: the received bytes + :raises ValueError: if ``max_bytes`` is less than 1 + :raises ~anyio.EndOfStream: if this stream has been closed from the other end + """ + + +class ByteSendStream(AsyncResource, TypedAttributeProvider): + """An interface for sending bytes to a single peer.""" + + @abstractmethod + async def send(self, item: bytes) -> None: + """ + Send the given bytes to the peer. + + :param item: the bytes to send + """ + + +class ByteStream(ByteReceiveStream, ByteSendStream): + """A bidirectional byte stream.""" + + @abstractmethod + async def send_eof(self) -> None: + """ + Send an end-of-file indication to the peer. + + You should not try to send any further data to this stream after calling this + method. This method is idempotent (does nothing on successive calls). + """ + + +#: Type alias for all unreliable bytes-oriented receive streams. +AnyUnreliableByteReceiveStream: TypeAlias = ( + UnreliableObjectReceiveStream[bytes] | ByteReceiveStream +) +#: Type alias for all unreliable bytes-oriented send streams. +AnyUnreliableByteSendStream: TypeAlias = ( + UnreliableObjectSendStream[bytes] | ByteSendStream +) +#: Type alias for all unreliable bytes-oriented streams. +AnyUnreliableByteStream: TypeAlias = UnreliableObjectStream[bytes] | ByteStream +#: Type alias for all bytes-oriented receive streams. +AnyByteReceiveStream: TypeAlias = ObjectReceiveStream[bytes] | ByteReceiveStream +#: Type alias for all bytes-oriented send streams. +AnyByteSendStream: TypeAlias = ObjectSendStream[bytes] | ByteSendStream +#: Type alias for all bytes-oriented streams. +AnyByteStream: TypeAlias = ObjectStream[bytes] | ByteStream + + +class Listener(AsyncResource, TypedAttributeProvider, Generic[T_co]): + """An interface for objects that let you accept incoming connections.""" + + @abstractmethod + async def serve( + self, handler: Callable[[T_co], Any], task_group: TaskGroup | None = None + ) -> None: + """ + Accept incoming connections as they come in and start tasks to handle them. + + :param handler: a callable that will be used to handle each accepted connection + :param task_group: the task group that will be used to start tasks for handling + each accepted connection (if omitted, an ad-hoc task group will be created) + """ + + +class ObjectStreamConnectable(Generic[T_co], metaclass=ABCMeta): + @abstractmethod + async def connect(self) -> ObjectStream[T_co]: + """ + Connect to the remote endpoint. + + :return: an object stream connected to the remote end + :raises ConnectionFailed: if the connection fails + """ + + +class ByteStreamConnectable(metaclass=ABCMeta): + @abstractmethod + async def connect(self) -> ByteStream: + """ + Connect to the remote endpoint. + + :return: a bytestream connected to the remote end + :raises ConnectionFailed: if the connection fails + """ + + +#: Type alias for all connectables returning bytestreams or bytes-oriented object streams +AnyByteStreamConnectable: TypeAlias = ( + ObjectStreamConnectable[bytes] | ByteStreamConnectable +) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/abc/_subprocesses.py b/.venv-test/lib/python3.12/site-packages/anyio/abc/_subprocesses.py new file mode 100644 index 0000000..555f15f --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/abc/_subprocesses.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from abc import abstractmethod +from signal import Signals +from typing import TYPE_CHECKING + +from ._resources import AsyncResource + +if TYPE_CHECKING: + from ._streams import ByteReceiveStream, ByteSendStream + + +class Process(AsyncResource): + """An asynchronous version of :class:`subprocess.Popen`.""" + + @abstractmethod + async def wait(self) -> int: + """ + Wait until the process exits. + + :return: the exit code of the process + """ + + @abstractmethod + def terminate(self) -> None: + """ + Terminates the process, gracefully if possible. + + On Windows, this calls ``TerminateProcess()``. + On POSIX systems, this sends ``SIGTERM`` to the process. + + .. seealso:: :meth:`subprocess.Popen.terminate` + """ + + @abstractmethod + def kill(self) -> None: + """ + Kills the process. + + On Windows, this calls ``TerminateProcess()``. + On POSIX systems, this sends ``SIGKILL`` to the process. + + .. seealso:: :meth:`subprocess.Popen.kill` + """ + + @abstractmethod + def send_signal(self, signal: Signals) -> None: + """ + Send a signal to the subprocess. + + .. seealso:: :meth:`subprocess.Popen.send_signal` + + :param signal: the signal number (e.g. :data:`signal.SIGHUP`) + """ + + @property + @abstractmethod + def pid(self) -> int: + """The process ID of the process.""" + + @property + @abstractmethod + def returncode(self) -> int | None: + """ + The return code of the process. If the process has not yet terminated, this will + be ``None``. + """ + + @property + @abstractmethod + def stdin(self) -> ByteSendStream | None: + """The stream for the standard input of the process.""" + + @property + @abstractmethod + def stdout(self) -> ByteReceiveStream | None: + """The stream for the standard output of the process.""" + + @property + @abstractmethod + def stderr(self) -> ByteReceiveStream | None: + """The stream for the standard error output of the process.""" diff --git a/.venv-test/lib/python3.12/site-packages/anyio/abc/_tasks.py b/.venv-test/lib/python3.12/site-packages/anyio/abc/_tasks.py new file mode 100644 index 0000000..0a019e2 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/abc/_tasks.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +import sys +from abc import ABCMeta, abstractmethod +from collections.abc import Callable, Coroutine +from contextvars import Context +from types import TracebackType +from typing import TYPE_CHECKING, Any, Literal, Protocol, final, overload + +if sys.version_info >= (3, 13): + from typing import TypeVar +else: + from typing_extensions import TypeVar + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if TYPE_CHECKING: + from .._core._tasks import CancelScope, TaskHandle + +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True, default=None) +PosArgsT = TypeVarTuple("PosArgsT") + + +def get_coro_name(coro: Coroutine[Any, Any, object], override: object = None) -> str: + if override is not None: + return str(override) + + try: + cr_frame = coro.cr_frame # type: ignore[attr-defined] + module = cr_frame.f_globals["__name__"] + except (AttributeError, KeyError): + module = None + + qualname = getattr(coro, "__qualname__", None) + return ".".join([x for x in (module, qualname) if x]) + + +def get_callable_name(func: Callable, override: object = None) -> str: + if override is not None: + return str(override) + + module = getattr(func, "__module__", None) + qualname = getattr(func, "__qualname__", None) + return ".".join([x for x in (module, qualname) if x]) + + +def call_for_coroutine( + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + **kwargs: Any, +) -> Coroutine[Any, Any, T_co]: + """ + Call the given function with the given positional and keyword arguments. + + :return: the resulting coroutine + :raises TypeError: if the return value was not a coroutine object + + """ + coro = func(*args, **kwargs) + if not isinstance(coro, Coroutine): + prefix = f"{func.__module__}." if hasattr(func, "__module__") else "" + raise TypeError( + f"Expected {prefix}{func.__qualname__}() to return a coroutine, but " + f"the return value ({coro!r}) is not a coroutine object" + ) + + return coro + + +class TaskStatus(Protocol[T_contra]): + @overload + def started(self: TaskStatus[None]) -> None: ... + + @overload + def started(self, value: T_contra) -> None: ... + + def started(self, value: T_contra | None = None) -> None: + """ + Signal that the task has started. + + :param value: object passed back to the starter of the task + """ + + +class TaskGroup(metaclass=ABCMeta): + """ + Groups several asynchronous tasks together. + + :ivar cancel_scope: the cancel scope inherited by all child tasks + :vartype cancel_scope: CancelScope + + .. note:: On asyncio, support for eager task factories is considered to be + **experimental**. In particular, they don't follow the usual semantics of new + tasks being scheduled on the next iteration of the event loop, and may thus + cause unexpected behavior in code that wasn't written with such semantics in + mind. + """ + + cancel_scope: CancelScope + + def cancel(self, reason: str | None = None) -> None: + """ + Cancel this task group's cancel scope immediately. + + This is a shortcut for calling ``.cancel_scope.cancel()`` on the task group. + + :param reason: a message describing the reason for the cancellation + + .. versionadded:: 4.14.0 + + """ + self.cancel_scope.cancel(reason) + + @abstractmethod + def create_task( + self, + coro: Coroutine[Any, Any, T_co], + *, + name: object = None, + context: Context | None = None, + ) -> TaskHandle[T_co]: + """ + Create a new task from a coroutine object and schedule it to run. + + :param coro: a coroutine object + :param name: optional name to give the task + :param context: optional context to run the task in + :return: a task handle + + .. versionadded:: 4.14.0 + """ + + @final + def start_soon( + self, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + name: object = None, + ) -> TaskHandle[T_co]: + """ + Start a new task in this task group. + + :param func: a coroutine function + :param args: positional arguments to call the function with + :param name: name of the task, for the purposes of introspection and debugging + :return: a task handle + + .. versionadded:: 3.0 + .. versionchanged:: 4.14.0 + This method now returns a task handle. + + """ + final_name = get_callable_name(func, name) + return self.create_task(call_for_coroutine(func, args), name=final_name) + + @overload + async def start( + self, + func: Callable[..., Coroutine[Any, Any, T_co]], + *args: object, + name: object = None, + return_handle: Literal[False] = ..., + ) -> Any: ... + + @overload + async def start( + self, + func: Callable[..., Coroutine[Any, Any, T_co]], + *args: object, + name: object = None, + return_handle: Literal[True], + ) -> TaskHandle[T_co, Any]: ... + + @abstractmethod + async def start( + self, + func: Callable[..., Coroutine[Any, Any, T_co]], + *args: object, + name: object = None, + return_handle: Literal[False, True] = False, + ) -> Any: + """ + Start a new task and wait until it signals for readiness. + + The target callable must accept a keyword argument ``task_status`` (of type + :class:`TaskStatus`). Awaiting on this method will return whatever was passed to + ``task_status.started()`` (``None`` by default). + + .. note:: The :class:`TaskStatus` class is generic, and the type argument should + indicate the type of the value that will be passed to + ``task_status.started()``. + + :param func: a coroutine function that accepts the ``task_status`` keyword + argument + :param args: positional arguments to call the function with + :param name: an optional name for the task, for introspection and debugging + :param return_handle: if ``True``, return a :class:`TaskHandle` which also + contains the start value in ``start_value`` + :return: the value passed to ``task_status.started()`` + :raises RuntimeError: if the task finishes without calling + ``task_status.started()`` + + .. seealso:: :ref:`start_initialize` + + .. versionadded:: 3.0 + """ + + @abstractmethod + async def __aenter__(self) -> TaskGroup: + """Enter the task group context and allow starting new tasks.""" + + @abstractmethod + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + """Exit the task group context waiting for all tasks to finish.""" diff --git a/.venv-test/lib/python3.12/site-packages/anyio/abc/_testing.py b/.venv-test/lib/python3.12/site-packages/anyio/abc/_testing.py new file mode 100644 index 0000000..b18d835 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/abc/_testing.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import sys +import types +from abc import ABCMeta, abstractmethod +from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable +from typing import Any, TypeVar + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +_T = TypeVar("_T") + + +class TestRunner(metaclass=ABCMeta): + """ + Encapsulates a running event loop. Every call made through this object will use the + same event loop. + """ + + def __enter__(self) -> Self: + return self + + @abstractmethod + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> bool | None: ... + + @abstractmethod + def run_asyncgen_fixture( + self, + fixture_func: Callable[..., AsyncGenerator[_T, Any]], + kwargs: dict[str, Any], + ) -> Iterable[_T]: + """ + Run an async generator fixture. + + :param fixture_func: the fixture function + :param kwargs: keyword arguments to call the fixture function with + :return: an iterator yielding the value yielded from the async generator + """ + + @abstractmethod + def run_fixture( + self, + fixture_func: Callable[..., Coroutine[Any, Any, _T]], + kwargs: dict[str, Any], + ) -> _T: + """ + Run an async fixture. + + :param fixture_func: the fixture function + :param kwargs: keyword arguments to call the fixture function with + :return: the return value of the fixture function + """ + + @abstractmethod + def run_test( + self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] + ) -> None: + """ + Run an async test function. + + :param test_func: the test function + :param kwargs: keyword arguments to call the test function with + """ + + @abstractmethod + def is_running(self) -> bool: + """ + Check if the test runner is running. + + :return: ``True`` if the coroutine is currently being run, ``False`` otherwise. + """ diff --git a/.venv-test/lib/python3.12/site-packages/anyio/from_thread.py b/.venv-test/lib/python3.12/site-packages/anyio/from_thread.py new file mode 100644 index 0000000..f163d81 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/from_thread.py @@ -0,0 +1,588 @@ +from __future__ import annotations + +__all__ = ( + "BlockingPortal", + "BlockingPortalProvider", + "check_cancelled", + "run", + "run_sync", + "start_blocking_portal", +) + +import sys +from collections.abc import Awaitable, Callable, Coroutine, Generator +from concurrent.futures import Future +from contextlib import ( + AbstractAsyncContextManager, + AbstractContextManager, + contextmanager, +) +from contextvars import Context +from dataclasses import dataclass, field +from functools import partial +from inspect import isawaitable +from threading import Lock, Thread, current_thread, get_ident +from types import TracebackType +from typing import ( + Any, + Generic, + TypeVar, + cast, + overload, +) + +from ._core._eventloop import ( + get_cancelled_exc_class, + threadlocals, +) +from ._core._eventloop import run as run_eventloop +from ._core._exceptions import NoEventLoopError +from ._core._synchronization import Event +from ._core._tasks import CancelScope, create_task_group +from .abc._tasks import TaskStatus +from .lowlevel import EventLoopToken, current_token + +if sys.version_info >= (3, 11): + from typing import Self, TypeVarTuple, Unpack +else: + from typing_extensions import Self, TypeVarTuple, Unpack + +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +PosArgsT = TypeVarTuple("PosArgsT") + + +def _token_or_error(token: EventLoopToken | None) -> EventLoopToken: + if token is not None: + return token + + try: + return threadlocals.current_token + except AttributeError: + raise NoEventLoopError( + "Not running inside an AnyIO worker thread, and no event loop token was " + "provided" + ) from None + + +def run( + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + token: EventLoopToken | None = None, +) -> T_co: + """ + Call a coroutine function from a worker thread. + + :param func: a coroutine function + :param args: positional arguments for the callable + :param token: an event loop token to use to get back to the event loop thread + (required if calling this function from outside an AnyIO worker thread) + :return: the return value of the coroutine function + :raises MissingTokenError: if no token was provided and called from outside an + AnyIO worker thread + :raises RunFinishedError: if the event loop tied to ``token`` is no longer running + + .. versionchanged:: 4.11.0 + Added the ``token`` parameter. + + """ + explicit_token = token is not None + token = _token_or_error(token) + return token.backend_class.run_async_from_thread( + func, args, token=token.native_token if explicit_token else None + ) + + +def run_sync( + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + token: EventLoopToken | None = None, +) -> T_Retval: + """ + Call a function in the event loop thread from a worker thread. + + :param func: a callable + :param args: positional arguments for the callable + :param token: an event loop token to use to get back to the event loop thread + (required if calling this function from outside an AnyIO worker thread) + :return: the return value of the callable + :raises MissingTokenError: if no token was provided and called from outside an + AnyIO worker thread + :raises RunFinishedError: if the event loop tied to ``token`` is no longer running + + .. versionchanged:: 4.11.0 + Added the ``token`` parameter. + + """ + explicit_token = token is not None + token = _token_or_error(token) + return token.backend_class.run_sync_from_thread( + func, args, token=token.native_token if explicit_token else None + ) + + +class _BlockingAsyncContextManager(AbstractContextManager, Generic[T_co]): + _enter_future: Future[T_co] + _exit_future: Future[bool | None] + _exit_event: Event + _exit_exc_info: tuple[ + type[BaseException] | None, BaseException | None, TracebackType | None + ] = (None, None, None) + + def __init__( + self, async_cm: AbstractAsyncContextManager[T_co], portal: BlockingPortal + ): + self._async_cm = async_cm + self._portal = portal + + async def run_async_cm(self) -> bool | None: + try: + self._exit_event = Event() + value = await self._async_cm.__aenter__() + except BaseException as exc: + self._enter_future.set_exception(exc) + raise + else: + self._enter_future.set_result(value) + + try: + # Wait for the sync context manager to exit. + # This next statement can raise `get_cancelled_exc_class()` if + # something went wrong in a task group in this async context + # manager. + await self._exit_event.wait() + finally: + # In case of cancellation, it could be that we end up here before + # `_BlockingAsyncContextManager.__exit__` is called, and an + # `_exit_exc_info` has been set. + result = await self._async_cm.__aexit__(*self._exit_exc_info) + + return result + + def __enter__(self) -> T_co: + self._enter_future = Future() + self._exit_future = self._portal.start_task_soon(self.run_async_cm) + return self._enter_future.result() + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + /, + ) -> bool | None: + self._exit_exc_info = exc_type, exc_value, traceback + self._portal.call(self._exit_event.set) + return self._exit_future.result() + + +class _BlockingPortalTaskStatus(TaskStatus): + def __init__(self, future: Future): + self._future = future + + def started(self, value: object = None) -> None: + self._future.set_result(value) + + +class BlockingPortal: + """ + An object that lets external threads run code in an asynchronous event loop. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + """ + + def __init__(self) -> None: + self._token = current_token() + self._event_loop_thread_id: int | None = get_ident() + self._stop_event = Event() + self._task_group = create_task_group() + + async def __aenter__(self) -> Self: + await self._task_group.__aenter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + await self.stop() + return await self._task_group.__aexit__(exc_type, exc_val, exc_tb) + + def _check_running(self) -> None: + if self._event_loop_thread_id is None: + raise RuntimeError("This portal is not running") + if self._event_loop_thread_id == get_ident(): + raise RuntimeError( + "This method cannot be called from the event loop thread" + ) + + async def sleep_until_stopped(self) -> None: + """Sleep until :meth:`stop` is called.""" + await self._stop_event.wait() + + async def stop(self, cancel_remaining: bool = False) -> None: + """ + Signal the portal to shut down. + + This marks the portal as no longer accepting new calls and exits from + :meth:`sleep_until_stopped`. + + :param cancel_remaining: ``True`` to cancel all the remaining tasks, ``False`` + to let them finish before returning + + """ + self._event_loop_thread_id = None + self._stop_event.set() + if cancel_remaining: + self._task_group.cancel_scope.cancel("the blocking portal is shutting down") + + async def _call_func( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + future: Future[T_Retval], + ) -> None: + event_loop_thread_id = self._event_loop_thread_id + + def callback(f: Future[T_Retval]) -> None: + if f.cancelled(): + if event_loop_thread_id == get_ident(): + scope.cancel("the future was cancelled") + elif event_loop_thread_id is not None: + run_sync( + scope.cancel, "the future was cancelled", token=self._token + ) + + try: + retval_or_awaitable = func(*args, **kwargs) + if isawaitable(retval_or_awaitable): + with CancelScope() as scope: + future.add_done_callback(callback) + retval = await retval_or_awaitable + else: + retval = retval_or_awaitable + except get_cancelled_exc_class(): + future.cancel() + future.set_running_or_notify_cancel() + except BaseException as exc: + if not future.cancelled(): + future.set_exception(exc) + + # Let base exceptions fall through + if not isinstance(exc, Exception): + raise + else: + if not future.cancelled(): + future.set_result(retval) + finally: + scope = None # type: ignore[assignment] + + def _spawn_task_from_thread( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + name: object, + future: Future[T_Retval], + ) -> None: + """ + Spawn a new task using the given callable. + + :param func: a callable + :param args: positional arguments to be passed to the callable + :param kwargs: keyword arguments to be passed to the callable + :param name: name of the task (will be coerced to a string if not ``None``) + :param future: a future that will resolve to the return value of the callable, + or the exception raised during its execution + + """ + run_sync( + partial(self._task_group.start_soon, name=name), + self._call_func, + func, + args, + kwargs, + future, + token=self._token, + ) + + @overload + def call( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + *args: Unpack[PosArgsT], + ) -> T_Retval: ... + + @overload + def call( + self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] + ) -> T_Retval: ... + + def call( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + *args: Unpack[PosArgsT], + ) -> T_Retval: + """ + Call the given function in the event loop thread. + + If the callable returns a coroutine object, it is awaited on. + + :param func: any callable + :raises RuntimeError: if the portal is not running or if this method is called + from within the event loop thread + + """ + return cast(T_Retval, self.start_task_soon(func, *args).result()) + + @overload + def start_task_soon( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + *args: Unpack[PosArgsT], + name: object = None, + ) -> Future[T_Retval]: ... + + @overload + def start_task_soon( + self, + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + name: object = None, + ) -> Future[T_Retval]: ... + + def start_task_soon( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + *args: Unpack[PosArgsT], + name: object = None, + ) -> Future[T_Retval]: + """ + Start a task in the portal's task group. + + The task will be run inside a cancel scope which can be cancelled by cancelling + the returned future. + + :param func: the target function + :param args: positional arguments passed to ``func`` + :param name: name of the task (will be coerced to a string if not ``None``) + :return: a future that resolves with the return value of the callable if the + task completes successfully, or with the exception raised in the task + :raises RuntimeError: if the portal is not running or if this method is called + from within the event loop thread + :rtype: concurrent.futures.Future[T_Retval] + + .. versionadded:: 3.0 + + """ + self._check_running() + f: Future[T_Retval] = Future() + self._spawn_task_from_thread(func, args, {}, name, f) + return f + + def start_task( + self, + func: Callable[..., Awaitable[T_Retval]], + *args: object, + name: object = None, + ) -> tuple[Future[T_Retval], Any]: + """ + Start a task in the portal's task group and wait until it signals for readiness. + + This method works the same way as :meth:`.abc.TaskGroup.start`. + + :param func: the target function + :param args: positional arguments passed to ``func`` + :param name: name of the task (will be coerced to a string if not ``None``) + :return: a tuple of (future, task_status_value) where the ``task_status_value`` + is the value passed to ``task_status.started()`` from within the target + function + :rtype: tuple[concurrent.futures.Future[T_Retval], Any] + + .. versionadded:: 3.0 + + """ + + def task_done(future: Future[T_Retval]) -> None: + if not task_status_future.done(): + if future.cancelled(): + task_status_future.cancel() + elif future.exception(): + task_status_future.set_exception(future.exception()) + else: + exc = RuntimeError( + "Task exited without calling task_status.started()" + ) + task_status_future.set_exception(exc) + + self._check_running() + task_status_future: Future = Future() + task_status = _BlockingPortalTaskStatus(task_status_future) + f: Future = Future() + f.add_done_callback(task_done) + self._spawn_task_from_thread(func, args, {"task_status": task_status}, name, f) + return f, task_status_future.result() + + def wrap_async_context_manager( + self, cm: AbstractAsyncContextManager[T_co] + ) -> AbstractContextManager[T_co]: + """ + Wrap an async context manager as a synchronous context manager via this portal. + + Spawns a task that will call both ``__aenter__()`` and ``__aexit__()``, stopping + in the middle until the synchronous context manager exits. + + :param cm: an asynchronous context manager + :return: a synchronous context manager + + .. versionadded:: 2.1 + + """ + return _BlockingAsyncContextManager(cm, self) + + +@dataclass +class BlockingPortalProvider: + """ + A manager for a blocking portal. Used as a context manager. The first thread to + enter this context manager causes a blocking portal to be started with the specific + parameters, and the last thread to exit causes the portal to be shut down. Thus, + there will be exactly one blocking portal running in this context as long as at + least one thread has entered this context manager. + + The parameters are the same as for :func:`~anyio.run`. + + :param backend: name of the backend + :param backend_options: backend options + + .. versionadded:: 4.4 + """ + + backend: str = "asyncio" + backend_options: dict[str, Any] | None = None + _lock: Lock = field(init=False, default_factory=Lock) + _leases: int = field(init=False, default=0) + _portal: BlockingPortal = field(init=False) + _portal_cm: AbstractContextManager[BlockingPortal] | None = field( + init=False, default=None + ) + + def __enter__(self) -> BlockingPortal: + with self._lock: + if self._portal_cm is None: + self._portal_cm = start_blocking_portal( + self.backend, self.backend_options + ) + self._portal = self._portal_cm.__enter__() + + self._leases += 1 + return self._portal + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + portal_cm: AbstractContextManager[BlockingPortal] | None = None + with self._lock: + assert self._portal_cm + assert self._leases > 0 + self._leases -= 1 + if not self._leases: + portal_cm = self._portal_cm + self._portal_cm = None + del self._portal + + if portal_cm: + portal_cm.__exit__(None, None, None) + + +@contextmanager +def start_blocking_portal( + backend: str = "asyncio", + backend_options: dict[str, Any] | None = None, + *, + name: str | None = None, +) -> Generator[BlockingPortal, Any, None]: + """ + Start a new event loop in a new thread and run a blocking portal in its main task. + + The parameters are the same as for :func:`~anyio.run`. + + :param backend: name of the backend + :param backend_options: backend options + :param name: name of the thread + :return: a context manager that yields a blocking portal + + .. versionchanged:: 3.0 + Usage as a context manager is now required. + + """ + + async def run_portal() -> None: + async with BlockingPortal() as portal_: + if name is None: + current_thread().name = f"{backend}-portal-{id(portal_):x}" + + future.set_result(portal_) + await portal_.sleep_until_stopped() + + def run_blocking_portal() -> None: + if future.set_running_or_notify_cancel(): + try: + run_eventloop( + run_portal, backend=backend, backend_options=backend_options + ) + except BaseException as exc: + if not future.done(): + future.set_exception(exc) + + future: Future[BlockingPortal] = Future() + kwargs: dict[str, Any] = {} + if sys.version_info >= (3, 14): + kwargs["context"] = Context() + + thread = Thread(target=run_blocking_portal, daemon=True, name=name, **kwargs) + thread.start() + try: + cancel_remaining_tasks = False + portal = future.result() + try: + yield portal + except BaseException: + cancel_remaining_tasks = True + raise + finally: + try: + portal.call(portal.stop, cancel_remaining_tasks) + except RuntimeError: + pass + finally: + thread.join() + + +def check_cancelled() -> None: + """ + Check if the cancel scope of the host task's running the current worker thread has + been cancelled. + + If the host task's current cancel scope has indeed been cancelled, the + backend-specific cancellation exception will be raised. + + :raises RuntimeError: if the current thread was not spawned by + :func:`.to_thread.run_sync` + + """ + try: + token: EventLoopToken = threadlocals.current_token + except AttributeError: + raise NoEventLoopError( + "This function can only be called inside an AnyIO worker thread" + ) from None + + token.backend_class.check_cancelled() diff --git a/.venv-test/lib/python3.12/site-packages/anyio/functools.py b/.venv-test/lib/python3.12/site-packages/anyio/functools.py new file mode 100644 index 0000000..b0bdfb4 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/functools.py @@ -0,0 +1,400 @@ +from __future__ import annotations + +__all__ = ( + "AsyncCacheInfo", + "AsyncCacheParameters", + "AsyncLRUCacheWrapper", + "cache", + "lru_cache", + "reduce", +) + +import functools +from collections import OrderedDict +from collections.abc import ( + AsyncIterable, + Awaitable, + Callable, + Coroutine, + Hashable, + Iterable, +) +from functools import update_wrapper +from inspect import iscoroutinefunction +from typing import ( + Any, + Generic, + NamedTuple, + ParamSpec, + TypedDict, + TypeVar, + cast, + final, + overload, +) +from weakref import WeakKeyDictionary + +from ._core._eventloop import current_time +from ._core._synchronization import Lock +from .lowlevel import RunVar, checkpoint + +T = TypeVar("T") +S = TypeVar("S") +P = ParamSpec("P") +lru_cache_items: RunVar[ + WeakKeyDictionary[ + AsyncLRUCacheWrapper[Any, Any], + OrderedDict[ + Hashable, + tuple[_InitialMissingType, Lock, float | None] + | tuple[Any, None, float | None], + ], + ] +] = RunVar("lru_cache_items") + + +class _InitialMissingType: + pass + + +initial_missing: _InitialMissingType = _InitialMissingType() + + +class AsyncCacheInfo(NamedTuple): + hits: int + misses: int + maxsize: int | None + currsize: int + ttl: int | None + + +class AsyncCacheParameters(TypedDict): + maxsize: int | None + typed: bool + always_checkpoint: bool + ttl: int | None + + +class _LRUMethodWrapper(Generic[T]): + def __init__(self, wrapper: AsyncLRUCacheWrapper[..., T], instance: object): + self.__wrapper = wrapper + self.__instance = instance + + def cache_info(self) -> AsyncCacheInfo: + return self.__wrapper.cache_info() + + def cache_parameters(self) -> AsyncCacheParameters: + return self.__wrapper.cache_parameters() + + def cache_clear(self) -> None: + self.__wrapper.cache_clear() + + async def __call__(self, *args: Any, **kwargs: Any) -> T: + if self.__instance is None: + return await self.__wrapper(*args, **kwargs) + + return await self.__wrapper(self.__instance, *args, **kwargs) + + +@final +class AsyncLRUCacheWrapper(Generic[P, T]): + def __init__( + self, + func: Callable[P, Awaitable[T]], + maxsize: int | None, + typed: bool, + always_checkpoint: bool, + ttl: int | None, + ): + self.__wrapped__ = func + self._hits: int = 0 + self._misses: int = 0 + self._maxsize = max(maxsize, 0) if maxsize is not None else None + self._currsize: int = 0 + self._typed = typed + self._always_checkpoint = always_checkpoint + self._ttl = ttl + update_wrapper(self, func) + + def cache_info(self) -> AsyncCacheInfo: + return AsyncCacheInfo( + self._hits, self._misses, self._maxsize, self._currsize, self._ttl + ) + + def cache_parameters(self) -> AsyncCacheParameters: + return { + "maxsize": self._maxsize, + "typed": self._typed, + "always_checkpoint": self._always_checkpoint, + "ttl": self._ttl, + } + + def cache_clear(self) -> None: + if cache := lru_cache_items.get(None): + cache.pop(self, None) + self._hits = self._misses = self._currsize = 0 + + async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: + # Easy case first: if maxsize == 0, no caching is done + if self._maxsize == 0: + value = await self.__wrapped__(*args, **kwargs) + self._misses += 1 + return value + + # The key is constructed as a flat tuple to avoid memory overhead + key: tuple[Any, ...] = args + if kwargs: + # initial_missing is used as a separator + key += (initial_missing,) + sum(kwargs.items(), ()) + + if self._typed: + key += tuple(type(arg) for arg in args) + if kwargs: + key += (initial_missing,) + tuple(type(val) for val in kwargs.values()) + + try: + cache = lru_cache_items.get() + except LookupError: + cache = WeakKeyDictionary() + lru_cache_items.set(cache) + + try: + cache_entry = cache[self] + except KeyError: + cache_entry = cache[self] = OrderedDict() + + cached_value: T | _InitialMissingType + try: + cached_value, lock, expires_at = cache_entry[key] + except KeyError: + # We're the first task to call this function + cached_value, lock, expires_at = ( + initial_missing, + Lock(fast_acquire=not self._always_checkpoint), + None, + ) + cache_entry[key] = cached_value, lock, expires_at + + if lock is None: + if expires_at is not None and current_time() >= expires_at: + self._currsize -= 1 + cached_value, lock, expires_at = ( + initial_missing, + Lock(fast_acquire=not self._always_checkpoint), + None, + ) + cache_entry[key] = cached_value, lock, expires_at + else: + # The value was already cached + self._hits += 1 + cache_entry.move_to_end(key) + if self._always_checkpoint: + await checkpoint() + + return cast(T, cached_value) + + async with lock: + # Check if another task filled the cache while we acquired the lock + if (cached_value := cache_entry[key][0]) is initial_missing: + self._misses += 1 + if self._maxsize is not None and self._currsize >= self._maxsize: + cache_entry.popitem(last=False) + else: + self._currsize += 1 + + value = await self.__wrapped__(*args, **kwargs) + expires_at = ( + current_time() + self._ttl if self._ttl is not None else None + ) + cache_entry[key] = value, None, expires_at + else: + # Another task filled the cache while we were waiting for the lock + self._hits += 1 + cache_entry.move_to_end(key) + value = cast(T, cached_value) + + return value + + def __get__( + self, instance: object, owner: type | None = None + ) -> _LRUMethodWrapper[T]: + wrapper = _LRUMethodWrapper(self, instance) + update_wrapper(wrapper, self.__wrapped__) + return wrapper + + +class _LRUCacheWrapper: + def __init__( + self, maxsize: int | None, typed: bool, always_checkpoint: bool, ttl: int | None + ): + self._maxsize = maxsize + self._typed = typed + self._always_checkpoint = always_checkpoint + self._ttl = ttl + + @overload + def __call__( # type: ignore[overload-overlap] + self, func: Callable[P, Coroutine[Any, Any, T]], / + ) -> AsyncLRUCacheWrapper[P, T]: ... + + @overload + def __call__( + self, func: Callable[..., T], / + ) -> functools._lru_cache_wrapper[T]: ... + + def __call__( + self, f: Callable[P, Coroutine[Any, Any, T]] | Callable[..., T], / + ) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]: + if iscoroutinefunction(f): + return AsyncLRUCacheWrapper( + f, self._maxsize, self._typed, self._always_checkpoint, self._ttl + ) + + return functools.lru_cache(maxsize=self._maxsize, typed=self._typed)(f) # type: ignore[arg-type] + + +@overload +def cache( # type: ignore[overload-overlap] + func: Callable[P, Coroutine[Any, Any, T]], / +) -> AsyncLRUCacheWrapper[P, T]: ... + + +@overload +def cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ... + + +def cache(func: Callable[..., Any] | Callable[P, Coroutine[Any, Any, Any]], /) -> Any: + """ + A convenient shortcut for :func:`lru_cache` with ``maxsize=None``. + + This is the asynchronous equivalent to :func:`functools.cache`. + + """ + return lru_cache(maxsize=None)(func) + + +@overload +def lru_cache( + *, + maxsize: int | None = ..., + typed: bool = ..., + always_checkpoint: bool = ..., + ttl: int | None = ..., +) -> _LRUCacheWrapper: ... + + +@overload +def lru_cache( # type: ignore[overload-overlap] + func: Callable[P, Coroutine[Any, Any, T]], / +) -> AsyncLRUCacheWrapper[P, T]: ... + + +@overload +def lru_cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ... + + +def lru_cache( + func: Callable[..., Coroutine[Any, Any, Any]] | Callable[..., Any] | None = None, + /, + *, + maxsize: int | None = 128, + typed: bool = False, + always_checkpoint: bool = False, + ttl: int | None = None, +) -> Any: + """ + An asynchronous version of :func:`functools.lru_cache`. + + If a synchronous function is passed, the standard library + :func:`functools.lru_cache` is applied instead. + + :param always_checkpoint: if ``True``, every call to the cached function will be + guaranteed to yield control to the event loop at least once + :param ttl: time in seconds after which to invalidate cache entries + + .. note:: Caches and locks are managed on a per-event loop basis. + + """ + if func is None: + return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl) + + if not callable(func): + raise TypeError("the first argument must be callable") + + return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl)(func) + + +@overload +async def reduce( + function: Callable[[T, S], Awaitable[T]], + iterable: Iterable[S] | AsyncIterable[S], + /, + initial: T, +) -> T: ... + + +@overload +async def reduce( + function: Callable[[T, T], Awaitable[T]], + iterable: Iterable[T] | AsyncIterable[T], + /, +) -> T: ... + + +async def reduce( # type: ignore[misc] + function: Callable[[T, T], Awaitable[T]] | Callable[[T, S], Awaitable[T]], + iterable: Iterable[T] | Iterable[S] | AsyncIterable[T] | AsyncIterable[S], + /, + initial: T | _InitialMissingType = initial_missing, +) -> T: + """ + Asynchronous version of :func:`functools.reduce`. + + :param function: a coroutine function that takes two arguments: the accumulated + value and the next element from the iterable + :param iterable: an iterable or async iterable + :param initial: the initial value (if missing, the first element of the iterable is + used as the initial value) + + """ + element: Any + function_called = False + if isinstance(iterable, AsyncIterable): + async_it = iterable.__aiter__() + if initial is initial_missing: + try: + value = cast(T, await async_it.__anext__()) + except StopAsyncIteration: + raise TypeError( + "reduce() of empty sequence with no initial value" + ) from None + else: + value = cast(T, initial) + + async for element in async_it: + value = await function(value, element) + function_called = True + elif isinstance(iterable, Iterable): + it = iter(iterable) + if initial is initial_missing: + try: + value = cast(T, next(it)) + except StopIteration: + raise TypeError( + "reduce() of empty sequence with no initial value" + ) from None + else: + value = cast(T, initial) + + for element in it: + value = await function(value, element) + function_called = True + else: + raise TypeError("reduce() argument 2 must be an iterable or async iterable") + + # Make sure there is at least one checkpoint, even if an empty iterable and an + # initial value were given + if not function_called: + await checkpoint() + + return value diff --git a/.venv-test/lib/python3.12/site-packages/anyio/itertools.py b/.venv-test/lib/python3.12/site-packages/anyio/itertools.py new file mode 100644 index 0000000..2a10cfd --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/itertools.py @@ -0,0 +1,629 @@ +from __future__ import annotations + +__all__ = ( + "Chain", + "accumulate", + "batched", + "combinations", + "combinations_with_replacement", + "compress", + "count", + "cycle", + "dropwhile", + "filterfalse", + "groupby", + "islice", + "pairwise", + "permutations", + "product", + "repeat", + "starmap", + "takewhile", + "tee", + "zip_longest", +) + +import itertools +import operator +import sys +from collections.abc import ( + AsyncGenerator, + AsyncIterable, + AsyncIterator, + Awaitable, + Callable, + Iterable, + Iterator, +) +from dataclasses import dataclass, field +from typing import Any, Generic, TypeVar, cast, overload + +from ._core._synchronization import Lock +from ._core._tasks import CancelScope +from .lowlevel import cancel_shielded_checkpoint, checkpoint, checkpoint_if_cancelled + +if sys.version_info < (3, 15): + from typing_extensions import sentinel + +T = TypeVar("T") +R = TypeVar("R") +_tee_end = sentinel("_tee_end") + + +@dataclass(eq=False) +class _IterableAsyncIterator(AsyncIterator[T]): + iterator: Iterator[T] + + async def __anext__(self) -> T: + await checkpoint_if_cancelled() + try: + result = next(self.iterator) + except StopIteration: + await cancel_shielded_checkpoint() + raise StopAsyncIteration from None + + await cancel_shielded_checkpoint() + return result + + +def _iterate(iterable: Iterable[T] | AsyncIterable[T]) -> AsyncIterator[T]: + if isinstance(iterable, AsyncIterator): + return iterable + + if isinstance(iterable, AsyncIterable): + return iterable.__aiter__() + + return _IterableAsyncIterator(iter(iterable)) + + +@dataclass(eq=False) +class _TeeLink(Generic[T]): + value: object | None = None + next: _TeeLink[T] | None = None + filled: bool = False + + +@dataclass(eq=False) +class _TeeState(Generic[T]): + iterator: AsyncIterator[T] + lock: Lock = field(default_factory=Lock) + + async def fill(self, link: _TeeLink[T]) -> bool: + if link.filled: + return False + + async with self.lock: + if link.filled: + return True + + link.value = await anext(self.iterator, _tee_end) + if link.value is not _tee_end: + link.next = _TeeLink() + + link.filled = True + return True + + +class _TeeAsyncIterator(AsyncIterator[T]): + _state: _TeeState[T] + _link: _TeeLink[T] + _element_yielded: bool + + def __init__( + self, iterable: Iterable[T] | AsyncIterable[T] | _TeeAsyncIterator[T] + ) -> None: + if isinstance(iterable, _TeeAsyncIterator): + self._state = iterable._state + self._link = iterable._link + else: + self._state = _TeeState(_iterate(iterable)) + self._link = _TeeLink() + + self._element_yielded = False + + async def __anext__(self) -> T: + had_yieldpoint = await self._state.fill(self._link) + if self._link.value is _tee_end: + if not self._element_yielded: + await checkpoint() + + raise StopAsyncIteration + + if not had_yieldpoint: + await checkpoint_if_cancelled() + + self._element_yielded = True + value = cast(T, self._link.value) + next_link = self._link.next + assert next_link is not None + self._link = next_link + if not had_yieldpoint: + await cancel_shielded_checkpoint() + + return value + + +async def _operator_add(x: T, y: T) -> T: + return operator.add(x, y) + + +async def accumulate( + iterable: Iterable[T] | AsyncIterable[T], + function: Callable[[T, T], Awaitable[T]] = _operator_add, + *, + initial: T | None = None, +) -> AsyncGenerator[T, None]: + iterator = _iterate(iterable) + if initial is None: + try: + total = await anext(iterator) + except StopAsyncIteration: + await checkpoint() + return + else: + await checkpoint_if_cancelled() + total = initial + await cancel_shielded_checkpoint() + + yield total + + async for element in iterator: + total = await function(total, element) + yield total + + +async def batched( + iterable: Iterable[T] | AsyncIterable[T], n: int, *, strict: bool = False +) -> AsyncGenerator[tuple[T, ...], None]: + if n < 1: + raise ValueError("n must be at least one") + + iterator = _iterate(iterable) + + while True: + batch: list[T] = [] + for _ in range(n): + try: + batch.append(await anext(iterator)) + except StopAsyncIteration: + if not batch: + await checkpoint() + return + if strict: + raise ValueError("batched(): incomplete batch") from None + + yield tuple(batch) + return + + yield tuple(batch) + + +class Chain: + def __call__( + self, *iterables: Iterable[T] | AsyncIterable[T] + ) -> AsyncGenerator[T, None]: + return self.from_iterable(iterables) + + async def from_iterable( + self, + iterables: ( + Iterable[Iterable[T] | AsyncIterable[T]] + | AsyncIterable[Iterable[T] | AsyncIterable[T]] + ), + ) -> AsyncGenerator[T, None]: + element_yielded = False + outer_iter = _iterate(iterables) + + try: + async for iterable in outer_iter: + async for element in _iterate(iterable): + element_yielded = True + yield element + finally: + aclose = getattr(outer_iter, "aclose", None) + if aclose is not None: + with CancelScope(shield=True): + await aclose() + + if not element_yielded: + await checkpoint() + + +chain: Chain = Chain() + + +async def combinations( + iterable: Iterable[T] | AsyncIterable[T], r: int +) -> AsyncGenerator[tuple[T, ...], None]: + pool: list[T] = [element async for element in _iterate(iterable)] + async for combination in _iterate(itertools.combinations(pool, r)): + yield combination + + +async def combinations_with_replacement( + iterable: Iterable[T] | AsyncIterable[T], r: int +) -> AsyncGenerator[tuple[T, ...], None]: + pool: list[T] = [element async for element in _iterate(iterable)] + async for combination in _iterate(itertools.combinations_with_replacement(pool, r)): + yield combination + + +async def compress( + data: Iterable[T] | AsyncIterable[T], + selectors: Iterable[object] | AsyncIterable[object], +) -> AsyncGenerator[T, None]: + data_iterator = _iterate(data) + selector_iterator = _iterate(selectors) + element_yielded = False + + while True: + try: + datum = await anext(data_iterator) + selector = await anext(selector_iterator) + except StopAsyncIteration: + if not element_yielded: + await checkpoint() + + return + + if selector: + element_yielded = True + yield datum + + +async def count(start: int = 0, step: int = 1) -> AsyncGenerator[int, None]: + n = start + while True: + await checkpoint_if_cancelled() + value = n + n += step + await cancel_shielded_checkpoint() + yield value + + +async def cycle( + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + saved: list[T] = [] + async for element in _iterate(iterable): + saved.append(element) + yield element + + if not saved: + await checkpoint() + return + + while True: + for element in saved: + await checkpoint() + yield element + + +async def dropwhile( + predicate: Callable[[T], Awaitable[object]], + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + element_yielded = False + dropping = True + + async for element in _iterate(iterable): + if dropping and await predicate(element): + continue + + dropping = False + element_yielded = True + yield element + + if not element_yielded: + await checkpoint() + + +async def filterfalse( + predicate: Callable[[T], Awaitable[object]], + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + element_yielded = False + + async for element in _iterate(iterable): + if not await predicate(element): + element_yielded = True + yield element + + if not element_yielded: + await checkpoint() + + +@overload +def groupby( + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[tuple[T, list[T]], None]: ... + + +@overload +def groupby( + iterable: Iterable[T] | AsyncIterable[T], + key: Callable[[T], Awaitable[R]], +) -> AsyncGenerator[tuple[R, list[T]], None]: ... + + +async def groupby( + iterable: Iterable[T] | AsyncIterable[T], + key: Callable[[T], Awaitable[object]] | None = None, +) -> AsyncGenerator[tuple[object, list[T]], None]: + iterator = _iterate(iterable) + try: + element = await anext(iterator) + except StopAsyncIteration: + await checkpoint() + return + + group_key = element if key is None else await key(element) + values = [element] + + async for element in iterator: + next_key = element if key is None else await key(element) + if next_key != group_key: + completed_group = group_key, values + group_key = next_key + values = [element] + yield completed_group + else: + values.append(element) + + yield group_key, values + + +@overload +def islice( + iterable: Iterable[T] | AsyncIterable[T], + stop: int | None, + /, +) -> AsyncGenerator[T, None]: ... + + +@overload +def islice( + iterable: Iterable[T] | AsyncIterable[T], + start: int | None, + stop: int | None, + step: int | None = 1, + /, +) -> AsyncGenerator[T, None]: ... + + +async def islice( + iterable: Iterable[T] | AsyncIterable[T], + *args: int | None, +) -> AsyncGenerator[T, None]: + if not args: + raise TypeError("islice expected at least 2 arguments, got 1") + if len(args) > 3: + raise TypeError(f"islice expected at most 4 arguments, got {len(args) + 1}") + + slice_args = slice(*args) + + start_message = ( + "Indices for islice() must be None or an integer: 0 <= x <= sys.maxsize." + ) + stop_message = ( + "Stop argument for islice() must be None or an integer: 0 <= x <= sys.maxsize." + ) + step_message = "Step for islice() must be a positive integer or None." + + def normalize_index(value: object, message: str) -> int: + try: + index = operator.index(cast(Any, value)) + except TypeError: + raise ValueError(message) from None + + if index < 0 or index > sys.maxsize: + raise ValueError(message) + + return index + + start = ( + 0 + if slice_args.start is None + else normalize_index(slice_args.start, start_message) + ) + stop = ( + None + if slice_args.stop is None + else normalize_index(slice_args.stop, stop_message) + ) + step = ( + 1 if slice_args.step is None else normalize_index(slice_args.step, step_message) + ) + + if step <= 0: + raise ValueError(step_message) + + if stop == 0 or start == stop: + await checkpoint() + return + + iterator = _iterate(iterable) + index = 0 + element_yielded = False + + while stop is None or index < stop: + try: + element = await anext(iterator) + except StopAsyncIteration: + if not element_yielded: + await checkpoint() + + return + + if index >= start and (index - start) % step == 0: + index += 1 + element_yielded = True + yield element + else: + index += 1 + + if not element_yielded: + await checkpoint() + + +async def pairwise( + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[tuple[T, T], None]: + iterator = _iterate(iterable) + try: + previous = await anext(iterator) + except StopAsyncIteration: + await checkpoint() + return + + element_yielded = False + async for element in iterator: + element_yielded = True + pair = (previous, element) + previous = element + yield pair + + if not element_yielded: + await checkpoint() + + +async def permutations( + iterable: Iterable[T] | AsyncIterable[T], r: int | None = None +) -> AsyncGenerator[tuple[T, ...], None]: + pool: list[T] = [element async for element in _iterate(iterable)] + n = len(pool) + if r is None: + r = n + elif not isinstance(r, int): + raise TypeError("Expected int as r") + elif r < 0: + raise ValueError("r must be non-negative") + + async for permutation in _iterate(itertools.permutations(pool, r)): + yield permutation + + +async def product( + *iterables: Iterable[T] | AsyncIterable[T], repeat: int = 1 +) -> AsyncGenerator[tuple[T, ...], None]: + repeat = operator.index(repeat) + if repeat < 0: + raise ValueError("repeat argument cannot be negative") + + pools: list[tuple[T, ...]] = [] + for iterable in iterables: + pool: list[T] = [element async for element in _iterate(iterable)] + pools.append(tuple(pool)) + + async for value in _iterate(itertools.product(*pools, repeat=repeat)): + yield value + + +async def repeat(element: T, times: int | None = None) -> AsyncGenerator[T, None]: + if times is None: + while True: + await checkpoint() + yield element + + remaining = operator.index(cast(Any, times)) + if remaining <= 0: + await checkpoint() + return + + while remaining > 0: + await checkpoint_if_cancelled() + remaining -= 1 + await cancel_shielded_checkpoint() + yield element + + +async def starmap( + function: Callable[..., Awaitable[R]], + iterable: ( + Iterable[Iterable[object] | AsyncIterable[object]] + | AsyncIterable[Iterable[object] | AsyncIterable[object]] + ), +) -> AsyncGenerator[R, None]: + result_yielded = False + + async for args_iterable in _iterate(iterable): + args = [element async for element in _iterate(args_iterable)] + result_yielded = True + yield await function(*args) + + if not result_yielded: + await checkpoint() + + +def tee( + iterable: Iterable[T] | AsyncIterable[T], n: int = 2 +) -> tuple[AsyncIterator[T], ...]: + n = operator.index(cast(Any, n)) + if n < 0: + raise ValueError("n must be >= 0") + if n == 0: + return () + + iterator = _TeeAsyncIterator(iterable) + iterators: list[AsyncIterator[T]] = [iterator] + iterators.extend(_TeeAsyncIterator(iterator) for _ in range(n - 1)) + return tuple(iterators) + + +async def takewhile( + predicate: Callable[[T], Awaitable[object]], + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + element_yielded = False + + async for element in _iterate(iterable): + if not await predicate(element): + if not element_yielded: + await checkpoint() + + return + + element_yielded = True + yield element + + if not element_yielded: + await checkpoint() + + +async def zip_longest( + *iterables: Iterable[object] | AsyncIterable[object], + fillvalue: object = None, +) -> AsyncGenerator[tuple[object, ...], None]: + iterators = [_iterate(iterable) for iterable in iterables] + num_active = len(iterators) + if not num_active: + await checkpoint() + return + + active = [True] * num_active + tuple_yielded = False + + while True: + values: list[object] = [] + for index, iterator in enumerate(iterators): + if not active[index]: + values.append(fillvalue) + continue + + try: + value = await anext(iterator) + except StopAsyncIteration: + active[index] = False + num_active -= 1 + if not num_active: + if not tuple_yielded: + await checkpoint() + + return + + value = fillvalue + + values.append(value) + + tuple_yielded = True + yield tuple(values) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/lowlevel.py b/.venv-test/lib/python3.12/site-packages/anyio/lowlevel.py new file mode 100644 index 0000000..361b2eb --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/lowlevel.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +__all__ = ( + "EventLoopToken", + "RunVar", + "RunvarToken", + "cancel_shielded_checkpoint", + "checkpoint", + "checkpoint_if_cancelled", + "current_token", +) + +import enum +from dataclasses import dataclass +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, final, overload +from weakref import WeakKeyDictionary + +from ._core._eventloop import get_async_backend + +if TYPE_CHECKING: + from .abc import AsyncBackend + +T = TypeVar("T") +D = TypeVar("D") + + +async def checkpoint() -> None: + """ + Check for cancellation and allow the scheduler to switch to another task. + + Equivalent to (but more efficient than):: + + await checkpoint_if_cancelled() + await cancel_shielded_checkpoint() + + .. versionadded:: 3.0 + + """ + await get_async_backend().checkpoint() + + +async def checkpoint_if_cancelled() -> None: + """ + Enter a checkpoint if the enclosing cancel scope has been cancelled. + + This does not allow the scheduler to switch to a different task. + + .. versionadded:: 3.0 + + """ + await get_async_backend().checkpoint_if_cancelled() + + +async def cancel_shielded_checkpoint() -> None: + """ + Allow the scheduler to switch to another task but without checking for cancellation. + + Equivalent to (but potentially more efficient than):: + + with CancelScope(shield=True): + await checkpoint() + + .. versionadded:: 3.0 + + """ + await get_async_backend().cancel_shielded_checkpoint() + + +@final +@dataclass(frozen=True, repr=False) +class EventLoopToken: + """ + An opaque object that holds a reference to an event loop. + + .. versionadded:: 4.11.0 + """ + + backend_class: type[AsyncBackend] + native_token: object + + +def current_token() -> EventLoopToken: + """ + Return a token object that can be used to call code in the current event loop from + another thread. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. versionadded:: 4.11.0 + + """ + backend_class = get_async_backend() + raw_token = backend_class.current_token() + return EventLoopToken(backend_class, raw_token) + + +_run_vars: WeakKeyDictionary[object, dict[RunVar[Any], Any]] = WeakKeyDictionary() + + +class _NoValueSet(enum.Enum): + NO_VALUE_SET = enum.auto() + + +class RunvarToken(Generic[T]): + """ + A token that can be used to restore a :class:`RunVar` to its previous value. + + Returned by :meth:`RunVar.set`. Can be used as a context manager to automatically + reset the variable on exit, or passed directly to :meth:`RunVar.reset`. + """ + + __slots__ = "_redeemed", "_value", "_var" + + def __init__(self, var: RunVar[T], value: T | Literal[_NoValueSet.NO_VALUE_SET]): + self._var = var + self._value: T | Literal[_NoValueSet.NO_VALUE_SET] = value + self._redeemed = False + + def __enter__(self) -> RunvarToken[T]: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._var.reset(self) + + +class RunVar(Generic[T]): + """ + Like a :class:`~contextvars.ContextVar`, except scoped to the running event loop. + + Can be used as a context manager, Just like :class:`~contextvars.ContextVar`, that + will reset the variable to its previous value when the context block is exited. + """ + + __slots__ = "_default", "_name" + + NO_VALUE_SET: Literal[_NoValueSet.NO_VALUE_SET] = _NoValueSet.NO_VALUE_SET + + def __init__( + self, name: str, default: T | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET + ): + self._name = name + self._default = default + + @property + def _current_vars(self) -> dict[RunVar[T], T]: + native_token = current_token().native_token + try: + return _run_vars[native_token] + except KeyError: + run_vars = _run_vars[native_token] = {} + return run_vars + + @overload + def get(self, default: D) -> T | D: ... + + @overload + def get(self) -> T: ... + + def get( + self, default: D | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET + ) -> T | D: + """ + Return the current value of this run variable. + + :param default: a fallback value to return if no value has been set + :return: the current value, the provided default, or the variable's own default + :raises LookupError: if no value is set and no default is available + + """ + try: + return self._current_vars[self] + except KeyError: + if default is not RunVar.NO_VALUE_SET: + return default + elif self._default is not RunVar.NO_VALUE_SET: + return self._default + + raise LookupError( + f'Run variable "{self._name}" has no value and no default set' + ) + + def set(self, value: T) -> RunvarToken[T]: + """ + Set the value of this run variable for the current event loop. + + :param value: the new value + :return: a token that can be used to restore the previous value + + """ + current_vars = self._current_vars + token = RunvarToken(self, current_vars.get(self, RunVar.NO_VALUE_SET)) + current_vars[self] = value + return token + + def reset(self, token: RunvarToken[T]) -> None: + """ + Restore this run variable to the value it held before the matching :meth:`set`. + + :param token: the token returned by :meth:`set` + :raises ValueError: if the token belongs to a different :class:`RunVar` or the token + has already been used + + """ + if token._var is not self: + raise ValueError("This token does not belong to this RunVar") + + if token._redeemed: + raise ValueError("This token has already been used") + + if token._value is _NoValueSet.NO_VALUE_SET: + try: + del self._current_vars[self] + except KeyError: + pass + else: + self._current_vars[self] = token._value + + token._redeemed = True + + def __repr__(self) -> str: + return f"" diff --git a/.venv-test/lib/python3.12/site-packages/anyio/py.typed b/.venv-test/lib/python3.12/site-packages/anyio/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/anyio/pytest_plugin.py b/.venv-test/lib/python3.12/site-packages/anyio/pytest_plugin.py new file mode 100644 index 0000000..d6238cd --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/pytest_plugin.py @@ -0,0 +1,404 @@ +from __future__ import annotations + +import dataclasses +import socket +import sys +from collections.abc import Callable, Generator, Iterator +from contextlib import ExitStack, contextmanager +from inspect import isasyncgenfunction, iscoroutinefunction, ismethod +from typing import TYPE_CHECKING, Any, cast + +import pytest +from _pytest.fixtures import FuncFixtureInfo, SubRequest +from _pytest.outcomes import Exit +from _pytest.scope import Scope + +from . import get_available_backends +from ._core._eventloop import ( + current_async_library, + get_async_backend, + reset_current_async_library, + set_current_async_library, +) +from ._core._exceptions import iterate_exceptions + +if TYPE_CHECKING: + from .abc import TestRunner + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + +if TYPE_CHECKING: + # pytest >= 9.2 keeps CallSpec2 as a TYPE_CHECKING-only alias of CallSpec + from _pytest.python import CallSpec2 as CallSpec +else: + try: + # c.f. https://github.com/pytest-dev/pytest/pull/14742 + # pytest >= 9.2 + from _pytest.python import CallSpec + except ImportError: + from _pytest.python import CallSpec2 as CallSpec + +_current_runner: TestRunner | None = None +_runner_stack: ExitStack | None = None +_runner_leases = 0 + + +def extract_backend_and_options(backend: object) -> tuple[str, dict[str, Any]]: + if isinstance(backend, str): + return backend, {} + elif isinstance(backend, tuple) and len(backend) == 2: + if isinstance(backend[0], str) and isinstance(backend[1], dict): + return cast(tuple[str, dict[str, Any]], backend) + + raise TypeError("anyio_backend must be either a string or tuple of (string, dict)") + + +@contextmanager +def get_runner( + backend_name: str, backend_options: dict[str, Any] +) -> Iterator[TestRunner]: + global _current_runner, _runner_leases, _runner_stack + if _current_runner is None: + asynclib = get_async_backend(backend_name) + _runner_stack = ExitStack() + if current_async_library() is None: + # Since we're in control of the event loop, we can cache the name of the + # async library + token = set_current_async_library(backend_name) + _runner_stack.callback(reset_current_async_library, token) + + backend_options = backend_options or {} + _current_runner = _runner_stack.enter_context( + asynclib.create_test_runner(backend_options) + ) + + _runner_leases += 1 + try: + yield _current_runner + finally: + _runner_leases -= 1 + if not _runner_leases: + assert _runner_stack is not None + _runner_stack.close() + _runner_stack = _current_runner = None + + +def pytest_addoption(parser: pytest.Parser) -> None: + group = parser.getgroup("anyio") + parser.addini( + "anyio_mode", + default="strict", + help='AnyIO plugin mode (either "strict" or "auto")', + ) + group.addoption( + "--anyio-mode", + default=None, + dest="anyio_mode", + help=""" + 'auto' - All async test functions will be handled by AnyIO pytest plugin + 'strict' - Disabling autoprocessing(useful when anyio tests + need to coexist with other async test plugins) + """, + ) + + +def _is_auto_mode(config: pytest.Config, mode: str) -> bool: + return ( + config.getoption(mode, default=None) == "auto" or config.getini(mode) == "auto" + ) + + +def pytest_configure(config: pytest.Config) -> None: + config.addinivalue_line( + "markers", + "anyio: mark the (coroutine function) test to be run asynchronously via anyio.", + ) + if ( + _is_auto_mode(config, "anyio_mode") + and config.pluginmanager.has_plugin("asyncio") + and _is_auto_mode(config, "asyncio_mode") + ): + config.issue_config_time_warning( + pytest.PytestConfigWarning( + "AnyIO auto mode has been enabled together with pytest-asyncio auto " + "mode. This may cause unexpected behavior." + ), + 1, + ) + + +@pytest.hookimpl(hookwrapper=True) +def pytest_fixture_setup(fixturedef: Any, request: Any) -> Generator[Any]: + def wrapper(anyio_backend: Any, request: SubRequest, **kwargs: Any) -> Any: + # Rebind any fixture methods to the request instance + if ( + request.instance + and ismethod(func) + and type(func.__self__) is type(request.instance) + ): + local_func = func.__func__.__get__(request.instance) + else: + local_func = func + + backend_name, backend_options = extract_backend_and_options(anyio_backend) + if has_backend_arg: + kwargs["anyio_backend"] = anyio_backend + + if has_request_arg: + kwargs["request"] = request + + with get_runner(backend_name, backend_options) as runner: + # re-entrant call into the test runner detected. this happens when an async fixture + # is dynamically requested via request.getfixturevalue() from inside a running async + # test or fixture. on asyncio this raises RuntimeError: This event loop is already + # running, on trio the runner deadlocks - the host loop blocks waiting for the + # coroutine to return, but the coroutine is waiting for the host loop. raising here + # prevents the hang and gives a consistent error across backends. + if runner.is_running(): + raise RuntimeError( + "Cannot schedule a coroutine in the test runner while another is already running; " + "likely caused by request.getfixturevalue() on an async fixture." + ) + + if isasyncgenfunction(local_func): + yield from runner.run_asyncgen_fixture(local_func, kwargs) + else: + yield runner.run_fixture(local_func, kwargs) + + # Only apply this to coroutine functions and async generator functions in requests + # that involve the anyio_backend fixture + func = fixturedef.func + if isasyncgenfunction(func) or iscoroutinefunction(func): + if "anyio_backend" in request.fixturenames: + fixturedef.func = wrapper + original_argname = fixturedef.argnames + + if not (has_backend_arg := "anyio_backend" in fixturedef.argnames): + fixturedef.argnames += ("anyio_backend",) + + if not (has_request_arg := "request" in fixturedef.argnames): + fixturedef.argnames += ("request",) + + try: + return (yield) + finally: + fixturedef.func = func + fixturedef.argnames = original_argname + + return (yield) + + +@pytest.hookimpl(tryfirst=True) +def pytest_pycollect_makeitem( + collector: pytest.Module | pytest.Class, name: str, obj: object +) -> None: + if collector.istestfunction(obj, name): + inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj + if iscoroutinefunction(inner_func): + anyio_auto_mode = _is_auto_mode(collector.config, "anyio_mode") + marker = collector.get_closest_marker("anyio") + own_markers = getattr(obj, "pytestmark", ()) + if ( + anyio_auto_mode + or marker + or any(marker.name == "anyio" for marker in own_markers) + ): + pytest.mark.usefixtures("anyio_backend")(obj) + + +def pytest_collection_finish(session: pytest.Session) -> None: + for i, item in reversed(list(enumerate(session.items))): + if ( + isinstance(item, pytest.Function) + and iscoroutinefunction(item.function) + and item.get_closest_marker("anyio") is not None + and "anyio_backend" not in item.fixturenames + ): + new_items = [] + try: + cs_fields = {f.name for f in dataclasses.fields(CallSpec)} + except TypeError: + cs_fields = set() + + for param_index, backend in enumerate(get_available_backends()): + if "_arg2scope" in cs_fields: # pytest >= 8 + callspec = CallSpec( + params={"anyio_backend": backend}, + indices={"anyio_backend": param_index}, + _arg2scope={"anyio_backend": Scope.Module}, + _idlist=[backend], + marks=[], + ) + else: # pytest 7.x + callspec = CallSpec( # type: ignore[call-arg] + funcargs={}, + params={"anyio_backend": backend}, + indices={"anyio_backend": param_index}, + arg2scope={"anyio_backend": Scope.Module}, + idlist=[backend], + marks=[], + ) + + fi = item._fixtureinfo + new_names_closure = list(fi.names_closure) + if "anyio_backend" not in new_names_closure: + new_names_closure.append("anyio_backend") + + new_fixtureinfo = FuncFixtureInfo( + argnames=fi.argnames, + initialnames=fi.initialnames, + names_closure=new_names_closure, + name2fixturedefs=fi.name2fixturedefs, + ) + new_item = pytest.Function.from_parent( + item.parent, + name=f"{item.originalname}[{backend}]", + callspec=callspec, + callobj=item.obj, + fixtureinfo=new_fixtureinfo, + keywords=item.keywords, + originalname=item.originalname, + ) + new_items.append(new_item) + + session.items[i : i + 1] = new_items + + +@pytest.hookimpl(tryfirst=True) +def pytest_pyfunc_call(pyfuncitem: Any) -> bool | None: + def run_with_hypothesis(**kwargs: Any) -> None: + with get_runner(backend_name, backend_options) as runner: + runner.run_test(original_func, kwargs) + + backend = pyfuncitem.funcargs.get("anyio_backend") + if backend: + backend_name, backend_options = extract_backend_and_options(backend) + + if hasattr(pyfuncitem.obj, "hypothesis"): + # Wrap the inner test function unless it's already wrapped + original_func = pyfuncitem.obj.hypothesis.inner_test + if original_func.__qualname__ != run_with_hypothesis.__qualname__: + if iscoroutinefunction(original_func): + pyfuncitem.obj.hypothesis.inner_test = run_with_hypothesis + + return None + + if iscoroutinefunction(pyfuncitem.obj): + funcargs = pyfuncitem.funcargs + testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} + with get_runner(backend_name, backend_options) as runner: + try: + runner.run_test(pyfuncitem.obj, testargs) + except ExceptionGroup as excgrp: + for exc in iterate_exceptions(excgrp): + if isinstance(exc, (Exit, KeyboardInterrupt, SystemExit)): + raise exc from excgrp + + raise + + return True + + return None + + +@pytest.fixture(scope="module", params=get_available_backends()) +def anyio_backend(request: Any) -> Any: + return request.param + + +@pytest.fixture +def anyio_backend_name(anyio_backend: Any) -> str: + if isinstance(anyio_backend, str): + return anyio_backend + else: + return anyio_backend[0] + + +@pytest.fixture +def anyio_backend_options(anyio_backend: Any) -> dict[str, Any]: + if isinstance(anyio_backend, str): + return {} + else: + return anyio_backend[1] + + +class FreePortFactory: + """ + Manages port generation based on specified socket kind, ensuring no duplicate + ports are generated. + + This class provides functionality for generating available free ports on the + system. It is initialized with a specific socket kind and can generate ports + for given address families while avoiding reuse of previously generated ports. + + Users should not instantiate this class directly, but use the + ``free_tcp_port_factory`` and ``free_udp_port_factory`` fixtures instead. For simple + uses cases, ``free_tcp_port`` and ``free_udp_port`` can be used instead. + """ + + def __init__(self, kind: socket.SocketKind) -> None: + self._kind = kind + self._generated = set[int]() + + @property + def kind(self) -> socket.SocketKind: + """ + The type of socket connection (e.g., :data:`~socket.SOCK_STREAM` or + :data:`~socket.SOCK_DGRAM`) used to bind for checking port availability + + """ + return self._kind + + def __call__(self, family: socket.AddressFamily | None = None) -> int: + """ + Return an unbound port for the given address family. + + :param family: if omitted, both IPv4 and IPv6 addresses will be tried + :return: a port number + + """ + if family is not None: + families = [family] + else: + families = [socket.AF_INET] + if socket.has_ipv6: + families.append(socket.AF_INET6) + + while True: + port = 0 + with ExitStack() as stack: + for fam in families: + sock = stack.enter_context(socket.socket(fam, self._kind)) + addr = "::1" if fam == socket.AF_INET6 else "127.0.0.1" + try: + sock.bind((addr, port)) + except OSError: + break + + if not port: + port = sock.getsockname()[1] + else: + if port not in self._generated: + self._generated.add(port) + return port + + +@pytest.fixture(scope="session") +def free_tcp_port_factory() -> FreePortFactory: + return FreePortFactory(socket.SOCK_STREAM) + + +@pytest.fixture(scope="session") +def free_udp_port_factory() -> FreePortFactory: + return FreePortFactory(socket.SOCK_DGRAM) + + +@pytest.fixture +def free_tcp_port(free_tcp_port_factory: Callable[[], int]) -> int: + return free_tcp_port_factory() + + +@pytest.fixture +def free_udp_port(free_udp_port_factory: Callable[[], int]) -> int: + return free_udp_port_factory() diff --git a/.venv-test/lib/python3.12/site-packages/anyio/streams/__init__.py b/.venv-test/lib/python3.12/site-packages/anyio/streams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/anyio/streams/buffered.py b/.venv-test/lib/python3.12/site-packages/anyio/streams/buffered.py new file mode 100644 index 0000000..a3b07f7 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/streams/buffered.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +__all__ = ( + "BufferedByteReceiveStream", + "BufferedByteStream", + "BufferedConnectable", +) + +import sys +from collections.abc import Callable, Iterable, Mapping +from dataclasses import dataclass, field +from typing import Any, SupportsIndex + +from .. import ClosedResourceError, DelimiterNotFound, EndOfStream, IncompleteRead +from ..abc import ( + AnyByteReceiveStream, + AnyByteStream, + AnyByteStreamConnectable, + ByteReceiveStream, + ByteStream, + ByteStreamConnectable, +) + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + +@dataclass(eq=False) +class BufferedByteReceiveStream(ByteReceiveStream): + """ + Wraps any bytes-based receive stream and uses a buffer to provide sophisticated + receiving capabilities in the form of a byte stream. + """ + + receive_stream: AnyByteReceiveStream + _buffer: bytearray = field(init=False, default_factory=bytearray) + _closed: bool = field(init=False, default=False) + + async def aclose(self) -> None: + await self.receive_stream.aclose() + self._closed = True + + @property + def buffer(self) -> bytes: + """The bytes currently in the buffer.""" + return bytes(self._buffer) + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return self.receive_stream.extra_attributes + + def feed_data(self, data: Iterable[SupportsIndex], /) -> None: + """ + Append data directly into the buffer. + + Any data in the buffer will be consumed by receive operations before receiving + anything from the wrapped stream. + + :param data: the data to append to the buffer (can be bytes or anything else + that supports ``__index__()``) + + """ + self._buffer.extend(data) + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + if self._closed: + raise ClosedResourceError + + if self._buffer: + chunk = bytes(self._buffer[:max_bytes]) + del self._buffer[:max_bytes] + return chunk + elif isinstance(self.receive_stream, ByteReceiveStream): + return await self.receive_stream.receive(max_bytes) + else: + # With a bytes-oriented object stream, we need to handle any surplus bytes + # we get from the receive() call + chunk = await self.receive_stream.receive() + if len(chunk) > max_bytes: + # Save the surplus bytes in the buffer + self._buffer.extend(chunk[max_bytes:]) + return chunk[:max_bytes] + else: + return chunk + + async def receive_exactly(self, nbytes: int) -> bytes: + """ + Read exactly the given amount of bytes from the stream. + + :param nbytes: the number of bytes to read + :return: the bytes read + :raises ~anyio.IncompleteRead: if the stream was closed before the requested + amount of bytes could be read from the stream + + """ + while True: + remaining = nbytes - len(self._buffer) + if remaining <= 0: + retval = self._buffer[:nbytes] + del self._buffer[:nbytes] + return bytes(retval) + + try: + if isinstance(self.receive_stream, ByteReceiveStream): + chunk = await self.receive_stream.receive(remaining) + else: + chunk = await self.receive_stream.receive() + except EndOfStream as exc: + raise IncompleteRead from exc + + self._buffer.extend(chunk) + + async def receive_until(self, delimiter: bytes, max_bytes: int) -> bytes: + """ + Read from the stream until the delimiter is found or max_bytes have been read. + + :param delimiter: the marker to look for in the stream + :param max_bytes: maximum number of bytes that will be read before raising + :exc:`~anyio.DelimiterNotFound` + :return: the bytes read (not including the delimiter) + :raises ~anyio.IncompleteRead: if the stream was closed before the delimiter + was found + :raises ~anyio.DelimiterNotFound: if the delimiter is not found within the + bytes read up to the maximum allowed + + """ + delimiter_size = len(delimiter) + offset = 0 + while True: + # Check if the delimiter can be found in the current buffer + index = self._buffer.find(delimiter, offset) + if index >= 0: + found = self._buffer[:index] + del self._buffer[: index + len(delimiter) :] + return bytes(found) + + # Check if the buffer is already at or over the limit + if len(self._buffer) >= max_bytes: + raise DelimiterNotFound(max_bytes) + + # Read more data into the buffer from the socket + try: + data = await self.receive_stream.receive() + except EndOfStream as exc: + raise IncompleteRead from exc + + # Move the offset forward and add the new data to the buffer + offset = max(len(self._buffer) - delimiter_size + 1, 0) + self._buffer.extend(data) + + +class BufferedByteStream(BufferedByteReceiveStream, ByteStream): + """ + A full-duplex variant of :class:`BufferedByteReceiveStream`. All writes are passed + through to the wrapped stream as-is. + """ + + def __init__(self, stream: AnyByteStream): + """ + :param stream: the stream to be wrapped + + """ + super().__init__(stream) + self._stream = stream + + @override + async def send_eof(self) -> None: + await self._stream.send_eof() + + @override + async def send(self, item: bytes) -> None: + await self._stream.send(item) + + +class BufferedConnectable(ByteStreamConnectable): + """ + Wraps a byte stream connectable to produce :class:`BufferedByteStream` connections. + + Use this when you want the streams returned by :meth:`connect` to have the buffered + receive API (e.g. :meth:`~BufferedByteReceiveStream.receive_exactly` and + :meth:`~BufferedByteReceiveStream.receive_until`). + + :param connectable: the byte stream connectable to wrap + """ + + def __init__(self, connectable: AnyByteStreamConnectable): + """ + :param connectable: the connectable to wrap + + """ + self.connectable = connectable + + @override + async def connect(self) -> BufferedByteStream: + stream = await self.connectable.connect() + return BufferedByteStream(stream) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/streams/file.py b/.venv-test/lib/python3.12/site-packages/anyio/streams/file.py new file mode 100644 index 0000000..b9d7ca4 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/streams/file.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +__all__ = ( + "FileReadStream", + "FileStreamAttribute", + "FileWriteStream", +) + +from collections.abc import Callable, Mapping +from io import SEEK_SET, UnsupportedOperation +from os import PathLike +from pathlib import Path +from typing import IO, Any + +from .. import ( + BrokenResourceError, + ClosedResourceError, + EndOfStream, + TypedAttributeSet, + to_thread, + typed_attribute, +) +from ..abc import ByteReceiveStream, ByteSendStream + + +class FileStreamAttribute(TypedAttributeSet): + #: the open file descriptor + file: IO[bytes] = typed_attribute() + #: the path of the file on the file system, if available (file must be a real file) + path: Path = typed_attribute() + #: the file number, if available (file must be a real file or a TTY) + fileno: int = typed_attribute() + + +class _BaseFileStream: + def __init__(self, file: IO[bytes]): + self._file = file + + async def aclose(self) -> None: + await to_thread.run_sync(self._file.close) + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + attributes: dict[Any, Callable[[], Any]] = { + FileStreamAttribute.file: lambda: self._file, + } + + if hasattr(self._file, "name"): + attributes[FileStreamAttribute.path] = lambda: Path(self._file.name) + + try: + self._file.fileno() + except UnsupportedOperation: + pass + else: + attributes[FileStreamAttribute.fileno] = lambda: self._file.fileno() + + return attributes + + +class FileReadStream(_BaseFileStream, ByteReceiveStream): + """ + A byte stream that reads from a file in the file system. + + :param file: a file that has been opened for reading in binary mode + + .. versionadded:: 3.0 + """ + + @classmethod + async def from_path(cls, path: str | PathLike[str]) -> FileReadStream: + """ + Create a file read stream by opening the given file. + + :param path: path of the file to read from + + """ + file = await to_thread.run_sync(Path(path).open, "rb") + return cls(file) + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + try: + data = await to_thread.run_sync(self._file.read, max_bytes) + except ValueError: + raise ClosedResourceError from None + except OSError as exc: + raise BrokenResourceError from exc + + if data: + return data + else: + raise EndOfStream + + async def seek(self, position: int, whence: int = SEEK_SET) -> int: + """ + Seek the file to the given position. + + .. seealso:: :meth:`io.IOBase.seek` + + .. note:: Not all file descriptors are seekable. + + :param position: position to seek the file to + :param whence: controls how ``position`` is interpreted + :return: the new absolute position + :raises OSError: if the file is not seekable + + """ + return await to_thread.run_sync(self._file.seek, position, whence) + + async def tell(self) -> int: + """ + Return the current stream position. + + .. note:: Not all file descriptors are seekable. + + :return: the current absolute position + :raises OSError: if the file is not seekable + + """ + return await to_thread.run_sync(self._file.tell) + + +class FileWriteStream(_BaseFileStream, ByteSendStream): + """ + A byte stream that writes to a file in the file system. + + :param file: a file that has been opened for writing in binary mode + + .. versionadded:: 3.0 + """ + + @classmethod + async def from_path( + cls, path: str | PathLike[str], append: bool = False + ) -> FileWriteStream: + """ + Create a file write stream by opening the given file for writing. + + :param path: path of the file to write to + :param append: if ``True``, open the file for appending; if ``False``, any + existing file at the given path will be truncated + + """ + mode = "ab" if append else "wb" + file = await to_thread.run_sync(Path(path).open, mode) + return cls(file) + + async def send(self, item: bytes) -> None: + try: + await to_thread.run_sync(self._file.write, item) + except ValueError: + raise ClosedResourceError from None + except OSError as exc: + raise BrokenResourceError from exc diff --git a/.venv-test/lib/python3.12/site-packages/anyio/streams/memory.py b/.venv-test/lib/python3.12/site-packages/anyio/streams/memory.py new file mode 100644 index 0000000..329a75e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/streams/memory.py @@ -0,0 +1,326 @@ +from __future__ import annotations + +__all__ = ( + "MemoryObjectReceiveStream", + "MemoryObjectSendStream", + "MemoryObjectStreamStatistics", +) + +import warnings +from collections import OrderedDict, deque +from dataclasses import dataclass, field +from types import TracebackType +from typing import Generic, NamedTuple, TypeVar + +from .._core._exceptions import ( + BrokenResourceError, + ClosedResourceError, + EndOfStream, + WouldBlock, +) +from .._core._synchronization import Event +from .._core._testing import TaskInfo, get_current_task +from ..abc import ObjectReceiveStream, ObjectSendStream +from ..lowlevel import checkpoint + +T_Item = TypeVar("T_Item") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) + + +class MemoryObjectStreamStatistics(NamedTuple): + current_buffer_used: int #: number of items stored in the buffer + #: maximum number of items that can be stored on this stream (or :data:`math.inf`) + max_buffer_size: float + open_send_streams: int #: number of unclosed clones of the send stream + open_receive_streams: int #: number of unclosed clones of the receive stream + #: number of tasks blocked on :meth:`MemoryObjectSendStream.send` + tasks_waiting_send: int + #: number of tasks blocked on :meth:`MemoryObjectReceiveStream.receive` + tasks_waiting_receive: int + + +@dataclass(eq=False) +class _MemoryObjectItemReceiver(Generic[T_Item]): + task_info: TaskInfo = field(init=False, default_factory=get_current_task) + item: T_Item = field(init=False) + + def __repr__(self) -> str: + # When item is not defined, we get following error with default __repr__: + # AttributeError: 'MemoryObjectItemReceiver' object has no attribute 'item' + item = getattr(self, "item", None) + return f"{self.__class__.__name__}(task_info={self.task_info}, item={item!r})" + + +@dataclass(eq=False) +class _MemoryObjectStreamState(Generic[T_Item]): + max_buffer_size: float = field() + buffer: deque[T_Item] = field(init=False, default_factory=deque) + open_send_channels: int = field(init=False, default=0) + open_receive_channels: int = field(init=False, default=0) + waiting_receivers: OrderedDict[Event, _MemoryObjectItemReceiver[T_Item]] = field( + init=False, default_factory=OrderedDict + ) + waiting_senders: OrderedDict[Event, T_Item] = field( + init=False, default_factory=OrderedDict + ) + + def statistics(self) -> MemoryObjectStreamStatistics: + return MemoryObjectStreamStatistics( + len(self.buffer), + self.max_buffer_size, + self.open_send_channels, + self.open_receive_channels, + len(self.waiting_senders), + len(self.waiting_receivers), + ) + + +@dataclass(eq=False) +class MemoryObjectReceiveStream(ObjectReceiveStream[T_co], Generic[T_co]): + _state: _MemoryObjectStreamState[T_co] + _closed: bool = field(init=False, default=False) + + def __post_init__(self) -> None: + self._state.open_receive_channels += 1 + + def receive_nowait(self) -> T_co: + """ + Receive the next item if it can be done without waiting. + + :return: the received item + :raises ~anyio.ClosedResourceError: if this send stream has been closed + :raises ~anyio.EndOfStream: if the buffer is empty and this stream has been + closed from the sending end + :raises ~anyio.WouldBlock: if there are no items in the buffer and no tasks + waiting to send + + """ + if self._closed: + raise ClosedResourceError + + if self._state.waiting_senders: + # Get the item from the next sender + send_event, item = self._state.waiting_senders.popitem(last=False) + self._state.buffer.append(item) + send_event.set() + + if self._state.buffer: + return self._state.buffer.popleft() + elif not self._state.open_send_channels: + raise EndOfStream + + raise WouldBlock + + async def receive(self) -> T_co: + await checkpoint() + try: + return self.receive_nowait() + except WouldBlock: + # Add ourselves in the queue + receive_event = Event() + receiver = _MemoryObjectItemReceiver[T_co]() + self._state.waiting_receivers[receive_event] = receiver + + try: + await receive_event.wait() + finally: + self._state.waiting_receivers.pop(receive_event, None) + + try: + return receiver.item + except AttributeError: + raise EndOfStream from None + + def clone(self) -> MemoryObjectReceiveStream[T_co]: + """ + Create a clone of this receive stream. + + Each clone can be closed separately. Only when all clones have been closed will + the receiving end of the memory stream be considered closed by the sending ends. + + :return: the cloned stream + + """ + if self._closed: + raise ClosedResourceError + + return MemoryObjectReceiveStream(_state=self._state) + + def close(self) -> None: + """ + Close the stream. + + This works the exact same way as :meth:`aclose`, but is provided as a special + case for the benefit of synchronous callbacks. + + """ + if not self._closed: + self._closed = True + self._state.open_receive_channels -= 1 + if self._state.open_receive_channels == 0: + send_events = list(self._state.waiting_senders.keys()) + for event in send_events: + event.set() + + async def aclose(self) -> None: + self.close() + + def statistics(self) -> MemoryObjectStreamStatistics: + """ + Return statistics about the current state of this stream. + + .. versionadded:: 3.0 + """ + return self._state.statistics() + + def __enter__(self) -> MemoryObjectReceiveStream[T_co]: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def __del__(self) -> None: + if not self._closed: + warnings.warn( + f"Unclosed <{self.__class__.__name__} at {id(self):x}>", + ResourceWarning, + stacklevel=1, + source=self, + ) + + +@dataclass(eq=False) +class MemoryObjectSendStream(ObjectSendStream[T_contra], Generic[T_contra]): + _state: _MemoryObjectStreamState[T_contra] + _closed: bool = field(init=False, default=False) + + def __post_init__(self) -> None: + self._state.open_send_channels += 1 + + def send_nowait(self, item: T_contra) -> None: + """ + Send an item immediately if it can be done without waiting. + + :param item: the item to send + :raises ~anyio.ClosedResourceError: if this send stream has been closed + :raises ~anyio.BrokenResourceError: if the stream has been closed from the + receiving end + :raises ~anyio.WouldBlock: if the buffer is full and there are no tasks waiting + to receive + + """ + if self._closed: + raise ClosedResourceError + if not self._state.open_receive_channels: + raise BrokenResourceError + + while self._state.waiting_receivers: + receive_event, receiver = self._state.waiting_receivers.popitem(last=False) + if not receiver.task_info.has_pending_cancellation(): + receiver.item = item + receive_event.set() + return + + if len(self._state.buffer) < self._state.max_buffer_size: + self._state.buffer.append(item) + else: + raise WouldBlock + + async def send(self, item: T_contra) -> None: + """ + Send an item to the stream. + + If the buffer is full, this method blocks until there is again room in the + buffer or the item can be sent directly to a receiver. + + :param item: the item to send + :raises ~anyio.ClosedResourceError: if this send stream has been closed + :raises ~anyio.BrokenResourceError: if the stream has been closed from the + receiving end + + """ + await checkpoint() + try: + self.send_nowait(item) + except WouldBlock: + # Wait until there's someone on the receiving end + send_event = Event() + self._state.waiting_senders[send_event] = item + try: + await send_event.wait() + except BaseException: + self._state.waiting_senders.pop(send_event, None) + raise + + if send_event in self._state.waiting_senders: + del self._state.waiting_senders[send_event] + raise BrokenResourceError from None + + def clone(self) -> MemoryObjectSendStream[T_contra]: + """ + Create a clone of this send stream. + + Each clone can be closed separately. Only when all clones have been closed will + the sending end of the memory stream be considered closed by the receiving ends. + + :return: the cloned stream + + """ + if self._closed: + raise ClosedResourceError + + return MemoryObjectSendStream(_state=self._state) + + def close(self) -> None: + """ + Close the stream. + + This works the exact same way as :meth:`aclose`, but is provided as a special + case for the benefit of synchronous callbacks. + + """ + if not self._closed: + self._closed = True + self._state.open_send_channels -= 1 + if self._state.open_send_channels == 0: + receive_events = list(self._state.waiting_receivers.keys()) + self._state.waiting_receivers.clear() + for event in receive_events: + event.set() + + async def aclose(self) -> None: + self.close() + + def statistics(self) -> MemoryObjectStreamStatistics: + """ + Return statistics about the current state of this stream. + + .. versionadded:: 3.0 + """ + return self._state.statistics() + + def __enter__(self) -> MemoryObjectSendStream[T_contra]: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def __del__(self) -> None: + if not self._closed: + warnings.warn( + f"Unclosed <{self.__class__.__name__} at {id(self):x}>", + ResourceWarning, + stacklevel=1, + source=self, + ) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/streams/stapled.py b/.venv-test/lib/python3.12/site-packages/anyio/streams/stapled.py new file mode 100644 index 0000000..9a75345 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/streams/stapled.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +__all__ = ( + "MultiListener", + "StapledByteStream", + "StapledObjectStream", +) + +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +from ..abc import ( + ByteReceiveStream, + ByteSendStream, + ByteStream, + Listener, + ObjectReceiveStream, + ObjectSendStream, + ObjectStream, + TaskGroup, +) + +T_Item = TypeVar("T_Item") +T_Stream = TypeVar("T_Stream") + + +@dataclass(eq=False) +class StapledByteStream(ByteStream): + """ + Combines two byte streams into a single, bidirectional byte stream. + + Extra attributes will be provided from both streams, with the receive stream + providing the values in case of a conflict. + + :param ByteSendStream send_stream: the sending byte stream + :param ByteReceiveStream receive_stream: the receiving byte stream + """ + + send_stream: ByteSendStream + receive_stream: ByteReceiveStream + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + return await self.receive_stream.receive(max_bytes) + + async def send(self, item: bytes) -> None: + await self.send_stream.send(item) + + async def send_eof(self) -> None: + await self.send_stream.aclose() + + async def aclose(self) -> None: + await self.send_stream.aclose() + await self.receive_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self.send_stream.extra_attributes, + **self.receive_stream.extra_attributes, + } + + +@dataclass(eq=False) +class StapledObjectStream(ObjectStream[T_Item], Generic[T_Item]): + """ + Combines two object streams into a single, bidirectional object stream. + + Extra attributes will be provided from both streams, with the receive stream + providing the values in case of a conflict. + + :param ObjectSendStream send_stream: the sending object stream + :param ObjectReceiveStream receive_stream: the receiving object stream + """ + + send_stream: ObjectSendStream[T_Item] + receive_stream: ObjectReceiveStream[T_Item] + + async def receive(self) -> T_Item: + return await self.receive_stream.receive() + + async def send(self, item: T_Item) -> None: + await self.send_stream.send(item) + + def send_nowait(self, item: T_Item) -> None: + try: + send_nowait = self.send_stream.send_nowait # type: ignore[attr-defined] + except AttributeError as exc: + raise NotImplementedError( + f"'send_nowait' method not implemented in {type(self.send_stream)}" + ) from exc + + send_nowait(item) + + async def send_eof(self) -> None: + await self.send_stream.aclose() + + async def aclose(self) -> None: + await self.send_stream.aclose() + await self.receive_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self.send_stream.extra_attributes, + **self.receive_stream.extra_attributes, + } + + +@dataclass(eq=False) +class MultiListener(Listener[T_Stream], Generic[T_Stream]): + """ + Combines multiple listeners into one, serving connections from all of them at once. + + Any MultiListeners in the given collection of listeners will have their listeners + moved into this one. + + Extra attributes are provided from each listener, with each successive listener + overriding any conflicting attributes from the previous one. + + :param listeners: listeners to serve + :type listeners: Sequence[Listener[T_Stream]] + """ + + listeners: Sequence[Listener[T_Stream]] + + def __post_init__(self) -> None: + listeners: list[Listener[T_Stream]] = [] + for listener in self.listeners: + if isinstance(listener, MultiListener): + listeners.extend(listener.listeners) + del listener.listeners[:] # type: ignore[attr-defined] + else: + listeners.append(listener) + + self.listeners = listeners + + async def serve( + self, handler: Callable[[T_Stream], Any], task_group: TaskGroup | None = None + ) -> None: + from .. import create_task_group + + async with create_task_group() as tg: + for listener in self.listeners: + tg.start_soon(listener.serve, handler, task_group) + + async def aclose(self) -> None: + for listener in self.listeners: + await listener.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + attributes: dict = {} + for listener in self.listeners: + attributes.update(listener.extra_attributes) + + return attributes diff --git a/.venv-test/lib/python3.12/site-packages/anyio/streams/text.py b/.venv-test/lib/python3.12/site-packages/anyio/streams/text.py new file mode 100644 index 0000000..296cd25 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/streams/text.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +__all__ = ( + "TextConnectable", + "TextReceiveStream", + "TextSendStream", + "TextStream", +) + +import codecs +import sys +from collections.abc import Callable, Mapping +from dataclasses import InitVar, dataclass, field +from typing import Any + +from ..abc import ( + AnyByteReceiveStream, + AnyByteSendStream, + AnyByteStream, + AnyByteStreamConnectable, + ObjectReceiveStream, + ObjectSendStream, + ObjectStream, + ObjectStreamConnectable, +) + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + +@dataclass(eq=False) +class TextReceiveStream(ObjectReceiveStream[str]): + """ + Stream wrapper that decodes bytes to strings using the given encoding. + + Decoding is done using :class:`~codecs.IncrementalDecoder` which returns any + completely received unicode characters as soon as they come in. + + :param transport_stream: any bytes-based receive stream + :param encoding: character encoding to use for decoding bytes to strings (defaults + to ``utf-8``) + :param errors: handling scheme for decoding errors (defaults to ``strict``; see the + `codecs module documentation`_ for a comprehensive list of options) + + .. _codecs module documentation: + https://docs.python.org/3/library/codecs.html#codec-objects + """ + + transport_stream: AnyByteReceiveStream + encoding: InitVar[str] = "utf-8" + errors: InitVar[str] = "strict" + _decoder: codecs.IncrementalDecoder = field(init=False) + + def __post_init__(self, encoding: str, errors: str) -> None: + decoder_class = codecs.getincrementaldecoder(encoding) + self._decoder = decoder_class(errors=errors) + + async def receive(self) -> str: + while True: + chunk = await self.transport_stream.receive() + decoded = self._decoder.decode(chunk) + if decoded: + return decoded + + async def aclose(self) -> None: + await self.transport_stream.aclose() + self._decoder.reset() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return self.transport_stream.extra_attributes + + +@dataclass(eq=False) +class TextSendStream(ObjectSendStream[str]): + """ + Sends strings to the wrapped stream as bytes using the given encoding. + + :param AnyByteSendStream transport_stream: any bytes-based send stream + :param str encoding: character encoding to use for encoding strings to bytes + (defaults to ``utf-8``) + :param str errors: handling scheme for encoding errors (defaults to ``strict``; see + the `codecs module documentation`_ for a comprehensive list of options) + + .. _codecs module documentation: + https://docs.python.org/3/library/codecs.html#codec-objects + """ + + transport_stream: AnyByteSendStream + encoding: InitVar[str] = "utf-8" + errors: str = "strict" + _encoder: Callable[..., tuple[bytes, int]] = field(init=False) + + def __post_init__(self, encoding: str) -> None: + self._encoder = codecs.getencoder(encoding) + + async def send(self, item: str) -> None: + encoded = self._encoder(item, self.errors)[0] + await self.transport_stream.send(encoded) + + async def aclose(self) -> None: + await self.transport_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return self.transport_stream.extra_attributes + + +@dataclass(eq=False) +class TextStream(ObjectStream[str]): + """ + A bidirectional stream that decodes bytes to strings on receive and encodes strings + to bytes on send. + + Extra attributes will be provided from both streams, with the receive stream + providing the values in case of a conflict. + + :param AnyByteStream transport_stream: any bytes-based stream + :param str encoding: character encoding to use for encoding/decoding strings to/from + bytes (defaults to ``utf-8``) + :param str errors: handling scheme for encoding errors (defaults to ``strict``; see + the `codecs module documentation`_ for a comprehensive list of options) + + .. _codecs module documentation: + https://docs.python.org/3/library/codecs.html#codec-objects + """ + + transport_stream: AnyByteStream + encoding: InitVar[str] = "utf-8" + errors: InitVar[str] = "strict" + _receive_stream: TextReceiveStream = field(init=False) + _send_stream: TextSendStream = field(init=False) + + def __post_init__(self, encoding: str, errors: str) -> None: + self._receive_stream = TextReceiveStream( + self.transport_stream, encoding=encoding, errors=errors + ) + self._send_stream = TextSendStream( + self.transport_stream, encoding=encoding, errors=errors + ) + + async def receive(self) -> str: + return await self._receive_stream.receive() + + async def send(self, item: str) -> None: + await self._send_stream.send(item) + + async def send_eof(self) -> None: + await self.transport_stream.send_eof() + + async def aclose(self) -> None: + await self._send_stream.aclose() + await self._receive_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self._send_stream.extra_attributes, + **self._receive_stream.extra_attributes, + } + + +class TextConnectable(ObjectStreamConnectable[str]): + def __init__(self, connectable: AnyByteStreamConnectable): + """ + :param connectable: the bytestream endpoint to wrap + + """ + self.connectable = connectable + + @override + async def connect(self) -> TextStream: + stream = await self.connectable.connect() + return TextStream(stream) diff --git a/.venv-test/lib/python3.12/site-packages/anyio/streams/tls.py b/.venv-test/lib/python3.12/site-packages/anyio/streams/tls.py new file mode 100644 index 0000000..b9ddf1d --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/streams/tls.py @@ -0,0 +1,436 @@ +from __future__ import annotations + +__all__ = ( + "TLSAttribute", + "TLSConnectable", + "TLSListener", + "TLSStream", +) + +import logging +import re +import ssl +import sys +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from functools import wraps +from ssl import SSLContext +from typing import Any, TypeAlias, TypeVar + +from .. import ( + BrokenResourceError, + EndOfStream, + aclose_forcefully, + get_cancelled_exc_class, + to_thread, +) +from .._core._typedattr import TypedAttributeSet, typed_attribute +from ..abc import ( + AnyByteStream, + AnyByteStreamConnectable, + ByteStream, + ByteStreamConnectable, + Listener, + TaskGroup, +) + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") +_PCTRTT: TypeAlias = tuple[tuple[str, str], ...] +_PCTRTTT: TypeAlias = tuple[_PCTRTT, ...] + + +class TLSAttribute(TypedAttributeSet): + """Contains Transport Layer Security related attributes.""" + + #: the selected ALPN protocol + alpn_protocol: str | None = typed_attribute() + #: the channel binding for type ``tls-unique`` + channel_binding_tls_unique: bytes = typed_attribute() + #: the selected cipher + cipher: tuple[str, str, int] = typed_attribute() + #: the peer certificate in dictionary form (see :meth:`ssl.SSLSocket.getpeercert` + # for more information) + peer_certificate: None | (dict[str, str | _PCTRTTT | _PCTRTT]) = typed_attribute() + #: the peer certificate in binary form + peer_certificate_binary: bytes | None = typed_attribute() + #: ``True`` if this is the server side of the connection + server_side: bool = typed_attribute() + #: ciphers shared by the client during the TLS handshake (``None`` if this is the + #: client side) + shared_ciphers: list[tuple[str, str, int]] | None = typed_attribute() + #: the :class:`~ssl.SSLObject` used for encryption + ssl_object: ssl.SSLObject = typed_attribute() + #: ``True`` if this stream does (and expects) a closing TLS handshake when the + #: stream is being closed + standard_compatible: bool = typed_attribute() + #: the TLS protocol version (e.g. ``TLSv1.2``) + tls_version: str = typed_attribute() + + +@dataclass(eq=False) +class TLSStream(ByteStream): + """ + A stream wrapper that encrypts all sent data and decrypts received data. + + This class has no public initializer; use :meth:`wrap` instead. + All extra attributes from :class:`~TLSAttribute` are supported. + + :var AnyByteStream transport_stream: the wrapped stream + + """ + + transport_stream: AnyByteStream + standard_compatible: bool + _ssl_object: ssl.SSLObject + _read_bio: ssl.MemoryBIO + _write_bio: ssl.MemoryBIO + + @classmethod + async def wrap( + cls, + transport_stream: AnyByteStream, + *, + server_side: bool | None = None, + hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + standard_compatible: bool = True, + ) -> TLSStream: + """ + Wrap an existing stream with Transport Layer Security. + + This performs a TLS handshake with the peer. + + :param transport_stream: a bytes-transporting stream to wrap + :param server_side: ``True`` if this is the server side of the connection, + ``False`` if this is the client side (if omitted, will be set to ``False`` + if ``hostname`` has been provided, ``False`` otherwise). Used only to create + a default context when an explicit context has not been provided. + :param hostname: host name of the peer (if host name checking is desired) + :param ssl_context: the SSLContext object to use (if not provided, a secure + default will be created) + :param standard_compatible: if ``False``, skip the closing handshake when + closing the connection, and don't raise an exception if the peer does the + same + :raises ~ssl.SSLError: if the TLS handshake fails + + """ + if server_side is None: + server_side = not hostname + + if not ssl_context: + purpose = ( + ssl.Purpose.CLIENT_AUTH if server_side else ssl.Purpose.SERVER_AUTH + ) + ssl_context = ssl.create_default_context(purpose) + + # Re-enable detection of unexpected EOFs if it was disabled by Python + if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): + ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF + + bio_in = ssl.MemoryBIO() + bio_out = ssl.MemoryBIO() + + # Resolve international host names using IDNA 2008. + # Otherwise wrap_bio() would resolve them with IDNA 2003. + if hostname is not None: + from .._core._sockets import idna2008_resolve + + server_hostname: bytes | None = idna2008_resolve(hostname) + else: + server_hostname = None + + # External SSLContext implementations may do blocking I/O in wrap_bio(), + # but the standard library implementation won't + if type(ssl_context) is ssl.SSLContext: + ssl_object = ssl_context.wrap_bio( + bio_in, + bio_out, + server_side=server_side, + server_hostname=server_hostname, + ) + else: + ssl_object = await to_thread.run_sync( + ssl_context.wrap_bio, + bio_in, + bio_out, + server_side, + server_hostname, + None, + ) + + wrapper = cls( + transport_stream=transport_stream, + standard_compatible=standard_compatible, + _ssl_object=ssl_object, + _read_bio=bio_in, + _write_bio=bio_out, + ) + await wrapper._call_sslobject_method(ssl_object.do_handshake) + return wrapper + + async def _call_sslobject_method( + self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] + ) -> T_Retval: + while True: + try: + result = func(*args) + except ssl.SSLWantReadError: + try: + # Flush any pending writes first + if self._write_bio.pending: + await self.transport_stream.send(self._write_bio.read()) + + data = await self.transport_stream.receive() + except EndOfStream: + self._read_bio.write_eof() + except OSError as exc: + self._read_bio.write_eof() + self._write_bio.write_eof() + raise BrokenResourceError from exc + else: + self._read_bio.write(data) + except ssl.SSLWantWriteError: + await self.transport_stream.send(self._write_bio.read()) + except ssl.SSLSyscallError as exc: + self._read_bio.write_eof() + self._write_bio.write_eof() + raise BrokenResourceError from exc + except ssl.SSLError as exc: + self._read_bio.write_eof() + self._write_bio.write_eof() + if isinstance(exc, ssl.SSLEOFError) or ( + exc.strerror and "UNEXPECTED_EOF_WHILE_READING" in exc.strerror + ): + if self.standard_compatible: + raise BrokenResourceError from exc + else: + raise EndOfStream from None + + raise + else: + # Flush any pending writes first + if self._write_bio.pending: + await self.transport_stream.send(self._write_bio.read()) + + return result + + async def unwrap(self) -> tuple[AnyByteStream, bytes]: + """ + Does the TLS closing handshake. + + :return: a tuple of (wrapped byte stream, bytes left in the read buffer) + + """ + await self._call_sslobject_method(self._ssl_object.unwrap) + self._read_bio.write_eof() + self._write_bio.write_eof() + return self.transport_stream, self._read_bio.read() + + async def aclose(self) -> None: + if self.standard_compatible: + try: + await self.unwrap() + except BaseException: + await aclose_forcefully(self.transport_stream) + raise + + await self.transport_stream.aclose() + + async def receive(self, max_bytes: int = 65536) -> bytes: + if max_bytes < 1: + raise ValueError("max_bytes must be a positive integer") + + data = await self._call_sslobject_method(self._ssl_object.read, max_bytes) + if not data: + raise EndOfStream + + return data + + async def send(self, item: bytes) -> None: + await self._call_sslobject_method(self._ssl_object.write, item) + + async def send_eof(self) -> None: + tls_version = self.extra(TLSAttribute.tls_version) + match = re.match(r"TLSv(\d+)(?:\.(\d+))?", tls_version) + if match: + major, minor = int(match.group(1)), int(match.group(2) or 0) + if (major, minor) < (1, 3): + raise NotImplementedError( + f"send_eof() requires at least TLSv1.3; current " + f"session uses {tls_version}" + ) + + raise NotImplementedError( + "send_eof() has not yet been implemented for TLS streams" + ) + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self.transport_stream.extra_attributes, + TLSAttribute.alpn_protocol: self._ssl_object.selected_alpn_protocol, + TLSAttribute.channel_binding_tls_unique: ( + self._ssl_object.get_channel_binding + ), + TLSAttribute.cipher: self._ssl_object.cipher, + TLSAttribute.peer_certificate: lambda: self._ssl_object.getpeercert(False), + TLSAttribute.peer_certificate_binary: lambda: self._ssl_object.getpeercert( + True + ), + TLSAttribute.server_side: lambda: self._ssl_object.server_side, + TLSAttribute.shared_ciphers: lambda: ( + self._ssl_object.shared_ciphers() + if self._ssl_object.server_side + else None + ), + TLSAttribute.standard_compatible: lambda: self.standard_compatible, + TLSAttribute.ssl_object: lambda: self._ssl_object, + TLSAttribute.tls_version: self._ssl_object.version, + } + + +@dataclass(eq=False) +class TLSListener(Listener[TLSStream]): + """ + A convenience listener that wraps another listener and auto-negotiates a TLS session + on every accepted connection. + + If the TLS handshake times out or raises an exception, + :meth:`handle_handshake_error` is called to do whatever post-mortem processing is + deemed necessary. + + Supports only the :attr:`~TLSAttribute.standard_compatible` extra attribute. + + :param Listener listener: the listener to wrap + :param ssl_context: the SSL context object + :param standard_compatible: a flag passed through to :meth:`TLSStream.wrap` + :param handshake_timeout: time limit for the TLS handshake + (passed to :func:`~anyio.fail_after`) + """ + + listener: Listener[Any] + ssl_context: ssl.SSLContext + standard_compatible: bool = True + handshake_timeout: float = 30 + + @staticmethod + async def handle_handshake_error(exc: BaseException, stream: AnyByteStream) -> None: + """ + Handle an exception raised during the TLS handshake. + + This method does 3 things: + + #. Forcefully closes the original stream + #. Logs the exception (unless it was a cancellation exception) using the + ``anyio.streams.tls`` logger + #. Reraises the exception if it was a base exception or a cancellation exception + + :param exc: the exception + :param stream: the original stream + + """ + await aclose_forcefully(stream) + + # Log all except cancellation exceptions + if not isinstance(exc, get_cancelled_exc_class()): + # CPython (as of 3.11.5) returns incorrect `sys.exc_info()` here when using + # any asyncio implementation, so we explicitly pass the exception to log + # (https://github.com/python/cpython/issues/108668). Trio does not have this + # issue because it works around the CPython bug. + logging.getLogger(__name__).exception( + "Error during TLS handshake", exc_info=exc + ) + + # Only reraise base exceptions and cancellation exceptions + if not isinstance(exc, Exception) or isinstance(exc, get_cancelled_exc_class()): + raise # noqa: PLE0704 + + async def serve( + self, + handler: Callable[[TLSStream], Any], + task_group: TaskGroup | None = None, + ) -> None: + @wraps(handler) + async def handler_wrapper(stream: AnyByteStream) -> None: + from .. import fail_after + + try: + with fail_after(self.handshake_timeout): + wrapped_stream = await TLSStream.wrap( + stream, + ssl_context=self.ssl_context, + standard_compatible=self.standard_compatible, + ) + except BaseException as exc: + await self.handle_handshake_error(exc, stream) + else: + await handler(wrapped_stream) + + await self.listener.serve(handler_wrapper, task_group) + + async def aclose(self) -> None: + await self.listener.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + TLSAttribute.standard_compatible: lambda: self.standard_compatible, + } + + +class TLSConnectable(ByteStreamConnectable): + """ + Wraps another connectable and does TLS negotiation after a successful connection. + + :param connectable: the connectable to wrap + :param hostname: host name of the server (if host name checking is desired) + :param ssl_context: the SSLContext object to use (if not provided, a secure default + will be created) + :param standard_compatible: if ``False``, skip the closing handshake when closing + the connection, and don't raise an exception if the server does the same + """ + + def __init__( + self, + connectable: AnyByteStreamConnectable, + *, + hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + standard_compatible: bool = True, + ) -> None: + self.connectable = connectable + self.ssl_context: SSLContext = ssl_context or ssl.create_default_context( + ssl.Purpose.SERVER_AUTH + ) + if not isinstance(self.ssl_context, ssl.SSLContext): + raise TypeError( + "ssl_context must be an instance of ssl.SSLContext, not " + f"{type(self.ssl_context).__name__}" + ) + self.hostname = hostname + self.standard_compatible = standard_compatible + + @override + async def connect(self) -> TLSStream: + stream = await self.connectable.connect() + try: + return await TLSStream.wrap( + stream, + hostname=self.hostname, + ssl_context=self.ssl_context, + standard_compatible=self.standard_compatible, + ) + except BaseException: + await aclose_forcefully(stream) + raise diff --git a/.venv-test/lib/python3.12/site-packages/anyio/to_interpreter.py b/.venv-test/lib/python3.12/site-packages/anyio/to_interpreter.py new file mode 100644 index 0000000..54ef4a8 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/to_interpreter.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +__all__ = ( + "current_default_interpreter_limiter", + "run_sync", +) + +import atexit +import os +import sys +from collections import deque +from collections.abc import Callable +from typing import Any, Final, TypeVar + +from . import current_time, to_thread +from ._core._exceptions import BrokenWorkerInterpreter +from ._core._synchronization import CapacityLimiter +from .lowlevel import RunVar + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if sys.version_info >= (3, 14): + from concurrent.interpreters import ExecutionFailed, create + + def _interp_call( + func: Callable[..., Any], args: tuple[Any, ...] + ) -> tuple[Any, bool]: + try: + retval = func(*args) + except BaseException as exc: + return exc, True + else: + return retval, False + + class _Worker: + last_used: float = 0 + + def __init__(self) -> None: + self._interpreter = create() + + def destroy(self) -> None: + self._interpreter.close() + + def call( + self, + func: Callable[..., T_Retval], + args: tuple[Any, ...], + ) -> T_Retval: + try: + res, is_exception = self._interpreter.call(_interp_call, func, args) + except ExecutionFailed as exc: + raise BrokenWorkerInterpreter(exc.excinfo) from exc + + if is_exception: + raise res + + return res +elif sys.version_info >= (3, 13): + import _interpqueues + import _interpreters + + UNBOUND: Final = 2 # I have no clue how this works, but it was used in the stdlib + FMT_UNPICKLED: Final = 0 + FMT_PICKLED: Final = 1 + QUEUE_PICKLE_ARGS: Final = (FMT_PICKLED, UNBOUND) + QUEUE_UNPICKLE_ARGS: Final = (FMT_UNPICKLED, UNBOUND) + + _run_func = compile( + """ +import _interpqueues +from _interpreters import NotShareableError +from pickle import loads, dumps, HIGHEST_PROTOCOL + +QUEUE_PICKLE_ARGS = (1, 2) +QUEUE_UNPICKLE_ARGS = (0, 2) + +item = _interpqueues.get(queue_id)[0] +try: + func, args = loads(item) + retval = func(*args) +except BaseException as exc: + is_exception = True + retval = exc +else: + is_exception = False + +try: + _interpqueues.put(queue_id, (retval, is_exception), *QUEUE_UNPICKLE_ARGS) +except NotShareableError: + retval = dumps(retval, HIGHEST_PROTOCOL) + _interpqueues.put(queue_id, (retval, is_exception), *QUEUE_PICKLE_ARGS) + """, + "", + "exec", + ) + + class _Worker: + last_used: float = 0 + + def __init__(self) -> None: + self._interpreter_id = _interpreters.create() + self._queue_id = _interpqueues.create(1, *QUEUE_UNPICKLE_ARGS) + _interpreters.set___main___attrs( + self._interpreter_id, {"queue_id": self._queue_id} + ) + + def destroy(self) -> None: + _interpqueues.destroy(self._queue_id) + _interpreters.destroy(self._interpreter_id) + + def call( + self, + func: Callable[..., T_Retval], + args: tuple[Any, ...], + ) -> T_Retval: + import pickle + + item = pickle.dumps((func, args), pickle.HIGHEST_PROTOCOL) + _interpqueues.put(self._queue_id, item, *QUEUE_PICKLE_ARGS) + exc_info = _interpreters.exec(self._interpreter_id, _run_func) + if exc_info: + raise BrokenWorkerInterpreter(exc_info) + + res = _interpqueues.get(self._queue_id) + (res, is_exception), fmt = res[:2] + if fmt == FMT_PICKLED: + res = pickle.loads(res) + + if is_exception: + raise res + + return res +else: + + class _Worker: + last_used: float = 0 + + def __init__(self) -> None: + raise RuntimeError("subinterpreters require at least Python 3.13") + + def call( + self, + func: Callable[..., T_Retval], + args: tuple[Any, ...], + ) -> T_Retval: + raise NotImplementedError + + def destroy(self) -> None: + pass + + +DEFAULT_CPU_COUNT: Final = 8 # this is just an arbitrarily selected value +MAX_WORKER_IDLE_TIME = ( + 30 # seconds a subinterpreter can be idle before becoming eligible for pruning +) + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + +_idle_workers = RunVar[deque[_Worker]]("_available_workers") +_default_interpreter_limiter = RunVar[CapacityLimiter]("_default_interpreter_limiter") + + +def _stop_workers(workers: deque[_Worker]) -> None: + for worker in workers: + worker.destroy() + + workers.clear() + + +async def run_sync( + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + limiter: CapacityLimiter | None = None, +) -> T_Retval: + """ + Call the given function with the given arguments in a subinterpreter. + + .. warning:: On Python 3.13, the :mod:`concurrent.interpreters` module was not yet + available, so the code path for that Python version relies on an undocumented, + private API. As such, it is recommended to not rely on this function for anything + mission-critical on Python 3.13. + + :param func: a callable + :param args: the positional arguments for the callable + :param limiter: capacity limiter to use to limit the total number of subinterpreters + running (if omitted, the default limiter is used) + :return: the result of the call + :raises BrokenWorkerInterpreter: if there's an internal error in a subinterpreter + + """ + if limiter is None: + limiter = current_default_interpreter_limiter() + + try: + idle_workers = _idle_workers.get() + except LookupError: + idle_workers = deque() + _idle_workers.set(idle_workers) + atexit.register(_stop_workers, idle_workers) + + async with limiter: + try: + worker = idle_workers.pop() + except IndexError: + worker = _Worker() + + try: + return await to_thread.run_sync( + worker.call, + func, + args, + limiter=limiter, + ) + finally: + # Prune workers that have been idle for too long + now = current_time() + while idle_workers: + if now - idle_workers[0].last_used <= MAX_WORKER_IDLE_TIME: + break + + await to_thread.run_sync(idle_workers.popleft().destroy, limiter=limiter) + + worker.last_used = current_time() + idle_workers.append(worker) + + +def current_default_interpreter_limiter() -> CapacityLimiter: + """ + Return the capacity limiter used by default to limit the number of concurrently + running subinterpreters. + + Defaults to the number of CPU cores. + + :return: a capacity limiter object + + """ + try: + return _default_interpreter_limiter.get() + except LookupError: + limiter = CapacityLimiter(os.cpu_count() or DEFAULT_CPU_COUNT) + _default_interpreter_limiter.set(limiter) + return limiter diff --git a/.venv-test/lib/python3.12/site-packages/anyio/to_process.py b/.venv-test/lib/python3.12/site-packages/anyio/to_process.py new file mode 100644 index 0000000..584128e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/to_process.py @@ -0,0 +1,269 @@ +from __future__ import annotations + +__all__ = ( + "current_default_process_limiter", + "process_worker", + "run_sync", +) + +import os +import pickle +import runpy +import subprocess +import sys +from collections import deque +from collections.abc import Callable +from types import ModuleType +from typing import TypeVar, cast + +from ._core._eventloop import current_time, get_async_backend, get_cancelled_exc_class +from ._core._exceptions import BrokenWorkerProcess +from ._core._subprocesses import open_process +from ._core._synchronization import CapacityLimiter +from ._core._tasks import CancelScope, fail_after +from .abc import ByteReceiveStream, ByteSendStream, Process +from .lowlevel import RunVar, checkpoint_if_cancelled +from .streams.buffered import BufferedByteReceiveStream + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +WORKER_MAX_IDLE_TIME = 300 # 5 minutes + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + +_process_pool_workers: RunVar[set[Process]] = RunVar("_process_pool_workers") +_process_pool_idle_workers: RunVar[deque[tuple[Process, float]]] = RunVar( + "_process_pool_idle_workers" +) +_default_process_limiter: RunVar[CapacityLimiter] = RunVar("_default_process_limiter") + + +async def run_sync( # type: ignore[return] + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + cancellable: bool = False, + limiter: CapacityLimiter | None = None, +) -> T_Retval: + """ + Call the given function with the given arguments in a worker process. + + If the ``cancellable`` option is enabled and the task waiting for its completion is + cancelled, the worker process running it will be abruptly terminated using SIGKILL + (or ``terminateProcess()`` on Windows). + + :param func: a callable + :param args: positional arguments for the callable + :param cancellable: ``True`` to allow cancellation of the operation while it's + running + :param limiter: capacity limiter to use to limit the total amount of processes + running (if omitted, the default limiter is used) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + :return: an awaitable that yields the return value of the function. + + """ + + async def send_raw_command(pickled_cmd: bytes) -> object: + try: + await stdin.send(pickled_cmd) + response = await buffered.receive_until(b"\n", 50) + status, length = response.split(b" ") + if status not in (b"RETURN", b"EXCEPTION"): + raise RuntimeError( + f"Worker process returned unexpected response: {response!r}" + ) + + pickled_response = await buffered.receive_exactly(int(length)) + except BaseException as exc: + workers.discard(process) + try: + process.kill() + with CancelScope(shield=True): + await process.aclose() + except ProcessLookupError: + pass + + if isinstance(exc, get_cancelled_exc_class()): + raise + else: + raise BrokenWorkerProcess from exc + + retval = pickle.loads(pickled_response) + if status == b"EXCEPTION": + assert isinstance(retval, BaseException) + raise retval + else: + return retval + + # First pickle the request before trying to reserve a worker process + await checkpoint_if_cancelled() + request = pickle.dumps(("run", func, args), protocol=pickle.HIGHEST_PROTOCOL) + + # If this is the first run in this event loop thread, set up the necessary variables + try: + workers = _process_pool_workers.get() + idle_workers = _process_pool_idle_workers.get() + except LookupError: + workers = set() + idle_workers = deque() + _process_pool_workers.set(workers) + _process_pool_idle_workers.set(idle_workers) + get_async_backend().setup_process_pool_exit_at_shutdown(workers) + + async with limiter or current_default_process_limiter(): + # Pop processes from the pool (starting from the most recently used) until we + # find one that hasn't exited yet + process: Process + while idle_workers: + process, _idle_since = idle_workers.pop() + if process.returncode is None: + stdin = cast(ByteSendStream, process.stdin) + buffered = BufferedByteReceiveStream( + cast(ByteReceiveStream, process.stdout) + ) + + # Prune any other workers that have been idle for WORKER_MAX_IDLE_TIME + # seconds or longer + now = current_time() + killed_processes: list[Process] = [] + while idle_workers: + if now - idle_workers[0][1] < WORKER_MAX_IDLE_TIME: + break + + process_to_kill, _idle_since = idle_workers.popleft() + process_to_kill.kill() + workers.remove(process_to_kill) + killed_processes.append(process_to_kill) + + with CancelScope(shield=True): + for killed_process in killed_processes: + await killed_process.aclose() + + break + + workers.remove(process) + else: + command = [sys.executable, "-u", "-m", __name__] + process = await open_process( + command, stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) + try: + stdin = cast(ByteSendStream, process.stdin) + buffered = BufferedByteReceiveStream( + cast(ByteReceiveStream, process.stdout) + ) + with fail_after(20): + message = await buffered.receive(6) + + if message != b"READY\n": + raise BrokenWorkerProcess( + f"Worker process returned unexpected response: {message!r}" + ) + + main_module_path = getattr(sys.modules["__main__"], "__file__", None) + pickled = pickle.dumps( + ("init", sys.path, main_module_path), + protocol=pickle.HIGHEST_PROTOCOL, + ) + await send_raw_command(pickled) + except (BrokenWorkerProcess, get_cancelled_exc_class()): + raise + except BaseException as exc: + process.kill() + raise BrokenWorkerProcess( + "Error during worker process initialization" + ) from exc + + workers.add(process) + + with CancelScope(shield=not cancellable): + try: + return cast(T_Retval, await send_raw_command(request)) + finally: + if process in workers: + idle_workers.append((process, current_time())) + + +def current_default_process_limiter() -> CapacityLimiter: + """ + Return the capacity limiter that is used by default to limit the number of worker + processes. + + :return: a capacity limiter object + + """ + try: + return _default_process_limiter.get() + except LookupError: + limiter = CapacityLimiter(os.cpu_count() or 2) + _default_process_limiter.set(limiter) + return limiter + + +def process_worker() -> None: + # Redirect standard streams to os.devnull so that user code won't interfere with the + # parent-worker communication + stdin = sys.stdin + stdout = sys.stdout + sys.stdin = open(os.devnull) + sys.stdout = open(os.devnull, "w") + sys.stderr = open(os.devnull, "w") + + stdout.buffer.write(b"READY\n") + while True: + retval = exception = None + try: + command, *args = pickle.load(stdin.buffer) + except EOFError: + return + except BaseException as exc: + exception = exc + else: + if command == "run": + func, args = args + try: + retval = func(*args) + except BaseException as exc: + exception = exc + elif command == "init": + main_module_path: str | None + sys.path, main_module_path = args + del sys.modules["__main__"] + if main_module_path and os.path.isfile(main_module_path): + # Load the parent's main module but as __mp_main__ instead of + # __main__ (like multiprocessing does) to avoid infinite recursion + try: + main = ModuleType("__mp_main__") + main_content = runpy.run_path( + main_module_path, run_name="__mp_main__" + ) + main.__dict__.update(main_content) + sys.modules["__main__"] = sys.modules["__mp_main__"] = main + except BaseException as exc: + exception = exc + try: + if exception is not None: + status = b"EXCEPTION" + pickled = pickle.dumps(exception, pickle.HIGHEST_PROTOCOL) + else: + status = b"RETURN" + pickled = pickle.dumps(retval, pickle.HIGHEST_PROTOCOL) + except BaseException as exc: + exception = exc + status = b"EXCEPTION" + pickled = pickle.dumps(exc, pickle.HIGHEST_PROTOCOL) + + stdout.buffer.write(b"%s %d\n" % (status, len(pickled))) + stdout.buffer.write(pickled) + + # Respect SIGTERM + if isinstance(exception, SystemExit): + raise exception + + +if __name__ == "__main__": + process_worker() diff --git a/.venv-test/lib/python3.12/site-packages/anyio/to_thread.py b/.venv-test/lib/python3.12/site-packages/anyio/to_thread.py new file mode 100644 index 0000000..5cdb725 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/anyio/to_thread.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +__all__ = ( + "current_default_thread_limiter", + "run_sync", +) + +import sys +from collections.abc import Callable +from typing import TYPE_CHECKING, TypeVar +from warnings import warn + +from ._core._eventloop import get_async_backend + +if TYPE_CHECKING: + from ._core._synchronization import CapacityLimiter + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + + +async def run_sync( + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + abandon_on_cancel: bool = False, + cancellable: bool | None = None, + limiter: CapacityLimiter | None = None, +) -> T_Retval: + """ + Call the given function with the given arguments in a worker thread. + + If the ``abandon_on_cancel`` option is enabled and the task waiting for its + completion is cancelled, the thread will still run its course but its + return value (or any raised exception) will be ignored. + + :param func: a callable + :param args: positional arguments for the callable + :param abandon_on_cancel: ``True`` to abandon the thread (leaving it to run + unchecked on own) if the host task is cancelled, ``False`` to ignore + cancellations in the host task until the operation has completed in the worker + thread + :param cancellable: deprecated alias of ``abandon_on_cancel``; will override + ``abandon_on_cancel`` if both parameters are passed + :param limiter: capacity limiter to use to limit the total amount of threads running + (if omitted, the default limiter is used) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + :return: an awaitable that yields the return value of the function. + + """ + if cancellable is not None: + abandon_on_cancel = cancellable + warn( + "The `cancellable=` keyword argument to `anyio.to_thread.run_sync` is " + "deprecated since AnyIO 4.1.0; use `abandon_on_cancel=` instead", + DeprecationWarning, + stacklevel=2, + ) + + return await get_async_backend().run_sync_in_worker_thread( + func, args, abandon_on_cancel=abandon_on_cancel, limiter=limiter + ) + + +def current_default_thread_limiter() -> CapacityLimiter: + """ + Return the capacity limiter that is used by default to limit the number of + concurrent threads. + + :return: a capacity limiter object + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().current_default_thread_limiter() diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/INSTALLER b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/LICENSE b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/LICENSE new file mode 100644 index 0000000..11069ed --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/METADATA b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/METADATA new file mode 100644 index 0000000..86f46ae --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/METADATA @@ -0,0 +1,342 @@ +Metadata-Version: 2.1 +Name: bcrypt +Version: 5.0.0 +Summary: Modern password hashing for your software and your servers +Author-email: The Python Cryptographic Authority developers +License: Apache-2.0 +Project-URL: homepage, https://github.com/pyca/bcrypt/ +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +License-File: LICENSE +Provides-Extra: tests +Requires-Dist: pytest!=3.3.0,>=3.2.1; extra == "tests" +Provides-Extra: typecheck +Requires-Dist: mypy; extra == "typecheck" + +bcrypt +====== + +.. image:: https://img.shields.io/pypi/v/bcrypt.svg + :target: https://pypi.org/project/bcrypt/ + :alt: Latest Version + +.. image:: https://github.com/pyca/bcrypt/workflows/CI/badge.svg?branch=main + :target: https://github.com/pyca/bcrypt/actions?query=workflow%3ACI+branch%3Amain + +Acceptable password hashing for your software and your servers (but you should +really use argon2id or scrypt) + + +Installation +============ + +To install bcrypt, simply: + +.. code:: console + + $ pip install bcrypt + +Note that bcrypt should build very easily on Linux provided you have a C +compiler and a Rust compiler (the minimum supported Rust version is 1.56.0). + +For Debian and Ubuntu, the following command will ensure that the required dependencies are installed: + +.. code:: console + + $ sudo apt-get install build-essential cargo + +For Fedora and RHEL-derivatives, the following command will ensure that the required dependencies are installed: + +.. code:: console + + $ sudo yum install gcc cargo + +For Alpine, the following command will ensure that the required dependencies are installed: + +.. code:: console + + $ apk add --update musl-dev gcc cargo + + +Alternatives +============ + +While bcrypt remains an acceptable choice for password storage, depending on your specific use case you may also want to consider using scrypt (either via `standard library`_ or `cryptography`_) or argon2id via `argon2_cffi`_. + +Changelog +========= + +5.0.0 +----- + +* Bumped MSRV to 1.74. +* Added support for Python 3.14 and free-threaded Python 3.14. +* Added support for Windows on ARM. +* Passing ``hashpw`` a password longer than 72 bytes now raises a + ``ValueError``. Previously the password was silently truncated, following the + behavior of the original OpenBSD ``bcrypt`` implementation. + +4.3.0 +----- + +* Dropped support for Python 3.7. +* We now support free-threaded Python 3.13. +* We now support PyPy 3.11. +* We now publish wheels for free-threaded Python 3.13, for PyPy 3.11 on + ``manylinux``, and for ARMv7l on ``manylinux``. + +4.2.1 +----- + +* Bump Rust dependency versions - this should resolve crashes on Python 3.13 + free-threaded builds. +* We no longer build ``manylinux`` wheels for PyPy 3.9. + +4.2.0 +----- + +* Bump Rust dependency versions +* Removed the ``BCRYPT_ALLOW_RUST_163`` environment variable. + +4.1.3 +----- + +* Bump Rust dependency versions + +4.1.2 +----- + +* Publish both ``py37`` and ``py39`` wheels. This should resolve some errors + relating to initializing a module multiple times per process. + +4.1.1 +----- + +* Fixed the type signature on the ``kdf`` method. +* Fixed packaging bug on Windows. +* Fixed incompatibility with passlib package detection assumptions. + +4.1.0 +----- + +* Dropped support for Python 3.6. +* Bumped MSRV to 1.64. (Note: Rust 1.63 can be used by setting the ``BCRYPT_ALLOW_RUST_163`` environment variable) + +4.0.1 +----- + +* We now build PyPy ``manylinux`` wheels. +* Fixed a bug where passing an invalid ``salt`` to ``checkpw`` could result in + a ``pyo3_runtime.PanicException``. It now correctly raises a ``ValueError``. + +4.0.0 +----- + +* ``bcrypt`` is now implemented in Rust. Users building from source will need + to have a Rust compiler available. Nothing will change for users downloading + wheels. +* We no longer ship ``manylinux2010`` wheels. Users should upgrade to the latest + ``pip`` to ensure this doesn’t cause issues downloading wheels on their + platform. We now ship ``manylinux_2_28`` wheels for users on new enough platforms. +* ``NUL`` bytes are now allowed in inputs. + + +3.2.2 +----- + +* Fixed packaging of ``py.typed`` files in wheels so that ``mypy`` works. + +3.2.1 +----- + +* Added support for compilation on z/OS +* The next release of ``bcrypt`` with be 4.0 and it will require Rust at + compile time, for users building from source. There will be no additional + requirement for users who are installing from wheels. Users on most + platforms will be able to obtain a wheel by making sure they have an up to + date ``pip``. The minimum supported Rust version will be 1.56.0. +* This will be the final release for which we ship ``manylinux2010`` wheels. + Going forward the minimum supported manylinux ABI for our wheels will be + ``manylinux2014``. The vast majority of users will continue to receive + ``manylinux`` wheels provided they have an up to date ``pip``. + + +3.2.0 +----- + +* Added typehints for library functions. +* Dropped support for Python versions less than 3.6 (2.7, 3.4, 3.5). +* Shipped ``abi3`` Windows wheels (requires pip >= 20). + +3.1.7 +----- + +* Set a ``setuptools`` lower bound for PEP517 wheel building. +* We no longer distribute 32-bit ``manylinux1`` wheels. Continuing to produce + them was a maintenance burden. + +3.1.6 +----- + +* Added support for compilation on Haiku. + +3.1.5 +----- + +* Added support for compilation on AIX. +* Dropped Python 2.6 and 3.3 support. +* Switched to using ``abi3`` wheels for Python 3. If you are not getting a + wheel on a compatible platform please upgrade your ``pip`` version. + +3.1.4 +----- + +* Fixed compilation with mingw and on illumos. + +3.1.3 +----- +* Fixed a compilation issue on Solaris. +* Added a warning when using too few rounds with ``kdf``. + +3.1.2 +----- +* Fixed a compile issue affecting big endian platforms. +* Fixed invalid escape sequence warnings on Python 3.6. +* Fixed building in non-UTF8 environments on Python 2. + +3.1.1 +----- +* Resolved a ``UserWarning`` when used with ``cffi`` 1.8.3. + +3.1.0 +----- +* Added support for ``checkpw``, a convenience method for verifying a password. +* Ensure that you get a ``$2y$`` hash when you input a ``$2y$`` salt. +* Fixed a regression where ``$2a`` hashes were vulnerable to a wraparound bug. +* Fixed compilation under Alpine Linux. + +3.0.0 +----- +* Switched the C backend to code obtained from the OpenBSD project rather than + openwall. +* Added support for ``bcrypt_pbkdf`` via the ``kdf`` function. + +2.0.0 +----- +* Added support for an adjustible prefix when calling ``gensalt``. +* Switched to CFFI 1.0+ + +Usage +----- + +Password Hashing +~~~~~~~~~~~~~~~~ + +Hashing and then later checking that a password matches the previous hashed +password is very simple: + +.. code:: pycon + + >>> import bcrypt + >>> password = b"super secret password" + >>> # Hash a password for the first time, with a randomly-generated salt + >>> hashed = bcrypt.hashpw(password, bcrypt.gensalt()) + >>> # Check that an unhashed password matches one that has previously been + >>> # hashed + >>> if bcrypt.checkpw(password, hashed): + ... print("It Matches!") + ... else: + ... print("It Does not Match :(") + +KDF +~~~ + +As of 3.0.0 ``bcrypt`` now offers a ``kdf`` function which does ``bcrypt_pbkdf``. +This KDF is used in OpenSSH's newer encrypted private key format. + +.. code:: pycon + + >>> import bcrypt + >>> key = bcrypt.kdf( + ... password=b'password', + ... salt=b'salt', + ... desired_key_bytes=32, + ... rounds=100) + + +Adjustable Work Factor +~~~~~~~~~~~~~~~~~~~~~~ +One of bcrypt's features is an adjustable logarithmic work factor. To adjust +the work factor merely pass the desired number of rounds to +``bcrypt.gensalt(rounds=12)`` which defaults to 12): + +.. code:: pycon + + >>> import bcrypt + >>> password = b"super secret password" + >>> # Hash a password for the first time, with a certain number of rounds + >>> hashed = bcrypt.hashpw(password, bcrypt.gensalt(14)) + >>> # Check that a unhashed password matches one that has previously been + >>> # hashed + >>> if bcrypt.checkpw(password, hashed): + ... print("It Matches!") + ... else: + ... print("It Does not Match :(") + + +Adjustable Prefix +~~~~~~~~~~~~~~~~~ + +Another one of bcrypt's features is an adjustable prefix to let you define what +libraries you'll remain compatible with. To adjust this, pass either ``2a`` or +``2b`` (the default) to ``bcrypt.gensalt(prefix=b"2b")`` as a bytes object. + +As of 3.0.0 the ``$2y$`` prefix is still supported in ``hashpw`` but deprecated. + +Maximum Password Length +~~~~~~~~~~~~~~~~~~~~~~~ + +The bcrypt algorithm only handles passwords up to 72 characters, any characters +beyond that are ignored. To work around this, a common approach is to hash a +password with a cryptographic hash (such as ``sha256``) and then base64 +encode it to prevent NULL byte problems before hashing the result with +``bcrypt``: + +.. code:: pycon + + >>> password = b"an incredibly long password" * 10 + >>> hashed = bcrypt.hashpw( + ... base64.b64encode(hashlib.sha256(password).digest()), + ... bcrypt.gensalt() + ... ) + +Compatibility +------------- + +This library should be compatible with py-bcrypt and it will run on Python +3.8+ (including free-threaded builds), and PyPy 3. + +Security +-------- + +``bcrypt`` follows the `same security policy as cryptography`_, if you +identify a vulnerability, we ask you to contact us privately. + +.. _`same security policy as cryptography`: https://cryptography.io/en/latest/security.html +.. _`standard library`: https://docs.python.org/3/library/hashlib.html#hashlib.scrypt +.. _`argon2_cffi`: https://argon2-cffi.readthedocs.io +.. _`cryptography`: https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#cryptography.hazmat.primitives.kdf.scrypt.Scrypt diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/RECORD b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/RECORD new file mode 100644 index 0000000..3266ac6 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/RECORD @@ -0,0 +1,12 @@ +bcrypt-5.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +bcrypt-5.0.0.dist-info/LICENSE,sha256=gXPVwptPlW1TJ4HSuG5OMPg-a3h43OGMkZRR1rpwfJA,10850 +bcrypt-5.0.0.dist-info/METADATA,sha256=i0d9VpcvYkuW9auAAC8TC5fGEFIS3SEm6I24DcbuiE0,10502 +bcrypt-5.0.0.dist-info/RECORD,, +bcrypt-5.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +bcrypt-5.0.0.dist-info/WHEEL,sha256=TPSuvIjdmY_fZ57irZdtZVUvrlrOM05O6Px8Mk8bzIc,114 +bcrypt-5.0.0.dist-info/top_level.txt,sha256=BkR_qBzDbSuycMzHWE1vzXrfYecAzUVmQs6G2CukqNI,7 +bcrypt/__init__.py,sha256=cv-NupIX6P7o6A4PK_F0ur6IZoDr3GnvyzFO9k16wKQ,1000 +bcrypt/__init__.pyi,sha256=ITUCB9mPVU8sKUbJQMDUH5YfQXZb1O55F9qvKZR_o8I,333 +bcrypt/__pycache__/__init__.cpython-312.pyc,, +bcrypt/_bcrypt.abi3.so,sha256=W22eAZGZwn0KnLuNoPYfBYhZO29Q9l67TOmjuLbXEYw,1172304 +bcrypt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/REQUESTED b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/WHEEL b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/WHEEL new file mode 100644 index 0000000..eabad67 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.45.1) +Root-Is-Purelib: false +Tag: cp39-abi3-macosx_10_12_universal2 + diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/top_level.txt b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/top_level.txt new file mode 100644 index 0000000..7f0b6e7 --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/bcrypt-5.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +bcrypt diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt/__init__.py b/.venv-test/lib/python3.12/site-packages/bcrypt/__init__.py new file mode 100644 index 0000000..81a92fd --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/bcrypt/__init__.py @@ -0,0 +1,43 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._bcrypt import ( + __author__, + __copyright__, + __email__, + __license__, + __summary__, + __title__, + __uri__, + checkpw, + gensalt, + hashpw, + kdf, +) +from ._bcrypt import ( + __version_ex__ as __version__, +) + +__all__ = [ + "__author__", + "__copyright__", + "__email__", + "__license__", + "__summary__", + "__title__", + "__uri__", + "__version__", + "checkpw", + "gensalt", + "hashpw", + "kdf", +] diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt/__init__.pyi b/.venv-test/lib/python3.12/site-packages/bcrypt/__init__.pyi new file mode 100644 index 0000000..12e4a2e --- /dev/null +++ b/.venv-test/lib/python3.12/site-packages/bcrypt/__init__.pyi @@ -0,0 +1,10 @@ +def gensalt(rounds: int = 12, prefix: bytes = b"2b") -> bytes: ... +def hashpw(password: bytes, salt: bytes) -> bytes: ... +def checkpw(password: bytes, hashed_password: bytes) -> bool: ... +def kdf( + password: bytes, + salt: bytes, + desired_key_bytes: int, + rounds: int, + ignore_few_rounds: bool = False, +) -> bytes: ... diff --git a/.venv-test/lib/python3.12/site-packages/bcrypt/_bcrypt.abi3.so b/.venv-test/lib/python3.12/site-packages/bcrypt/_bcrypt.abi3.so new file mode 100755 index 0000000000000000000000000000000000000000..ec26ceb93735597d3cb035930defc452e48a4bf8 GIT binary patch literal 1172304 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сFD)uduQbceW|{y<9clse9nI^c6T_LJ1XM;YmJ$?Y}Y z);ee&8{m>CHa)-jwNn^@FXDladY~yjlNwTxRgfsxcxlMI8OYiIheOP=O5)O7RRZS{ zM$s3W%z|e!_bQ0VfUV6l{6qrgQPf@r78wB7+Rx(B-o2H6uyqL2;X4z#Db=j7|8s$b z)}w{TVgJk4rBO5zKvGEeG|VoDUn&XCFbPs#rveSWnJLv>uo*ct&ZsvO>}phv=y?LC zRk1;Oj4y0Xydw#M{by~MZg*Y+=Imk;5QbHl;^K!HvFyg4lo61bZuiMF_ItRKRPqxo zxCQ?|B}rUz)H?ATRf&g1g@<*bXh5tJ!CDbdtP_2vN471&8nt|Gza&gKEW1U&<-1@F z@AMQ57iOyw+vz#I8OC8itwz`nzn8|FAuKaXY#w4Fr)m+j$?awuZR0bAJ|L4rkJB)s zK#yol^(%!3NTB+%Y?4wE!>SmOuBS8gNZ%hPdv?ZF&_#o1q%&?oB9bBgM?MJNK0@v) zZO4HR&sJ#>2FjgVdRG69BJuu`%B|yKO{tKAFN##x>Y`jRkm{c3f&WgoR!30*>oCWN zAc^t8b5i*P4XcC{)PRmApHta8WR?0)SHB4-^X}BlBbOd?cP)?+kkyBw=usTX^m+^1MP*yvNs!|NF35FE7I;*$V~rC;TipP@ zj22Is>G&kok$GL+q;OUF@X&a13}G**SX+$zR)t*PmAF%d{#XdzB3)Cc5H#1yNGegd zeJyByLG7~$*MPr-hQ#lr1dOA>QE(P+7UuI^#e2HS42|77Us<1r3*?d63v-P6K|Io0 z=$9OvD;3M9$a_P0fw(5f=-r!X>2j&WyrYe+AxmGKChi=!q4-IN#M{`>8d=J3fA7*M z6qbLa3}_xXh|4TSR=aZS+lVHmRHJ+aj=qTWWCd=s-JOPn!WK$7ix3Xh^VJZ@i{gxg zU&xo8mWwP7TV&@CV2W->!0`( zFn92^GSLwPi*LC_xQ*4En%9+z(cw=beM_k>pAk{g0x<16RJFkZP#;%jB}aXc!TFeW zHJy$mmoqrZ&x{4dja<@6lJJ4Aq(4qMp>IETUTGF$sM?MCwJL{L$AW#--XfMUMP4{W zIOG-o%TcwA71B^Ma1}yF@c4+r+a_GDgmAOeHjDTNEZ66!fRH_rO#lHbLvlro>~qYz zdJS*SqC(Z3P*s{0vNTT{mPN|vQBd=kQR&>>vetRbgc>ehIvN(-zlOM$FkSZ>5n05WIKD&<_>AZayhIKRbIibc-a-GVecLlWVnEft?YmNkb_$R?52y}W zm9xyu-2bC}+bcy|kZZgC!@ez!F0vXNEF*6=dvY8-S#PO2!>F^ANR}lXpvW}i6V3U% zEUz_T?0ZX5^Q*EneH!t|7K8h6bK-BQ5gafNzXhwPH-kVqxj#!wva~zF^Qr>qc1ef0 zTDhh6ppU=z%dCixp^Iq_cy-3nNi)B1WL0d!oa!)H3IE$|M5Z&T**EQnd-6CK-BrcJ z@JzOt_m#{|9&4MqQ#z+?GrfS#nJsc8BttZ=csJhN*8TmSjU@ZeobI>spCQ3_bJbG6 zt0lRUhyrz&hO4^Y8M-jVM^gxq`8X0ZX!O1CX1axCqF9=nWRo@@)x~M|Pm-!uy}#y3 zWYS~&it=bgUzeUn4nbw}AsrM4qOd!7I#5u#4`ock3AvD7K)0g-q@_{~|MxSpD^-T8} zqKMI#I57y4;)8ciHWsc*W8t|s(pY#g(*zvGs(3S*>!9k9Os2>qBTjiJcsY|z4o>4ma4HM5=W4DDOenpWV%H7))!@5dnNqMQ_|Jd!E- zn1WNULj_4sOZAKzVl0#CF3H3I>_Q2lMd9z)OOoh$=Q_aYgDcci~Bp7RQ#bq6I444n6hv}2m=?;D-rBb+mODtzvEqG7TwON=QCTDYyb47V6!)B!Y zDkEA1u#xtwF|Uw4(mp|-=|}^{C~^Aal1yOx*Fu`Qo8qrc3r2mFQfr;1 z`o;2MRd~{s{F6%J-AFwKnoH8q%+EBWR6R#Vi+J0VuITnurqE1*gh0r8+(XmSNV`MN z*}taCTqcc=B^9Z=K1H!2b$>t>YH#^kqP-m@bvvXRO36`faVl`+XcmrB#ug)bIeII# zT`%pzCs~#WAWkntplqWZjnk*GKOk=YDP5@DrkPZbd7JoMcK#Bn2~-o{N#~DB{##gC z37rQswHwiUG@--~Q38_6QRk)6$)^L;IR4!T>SaYEzg09Ozu)HH_4r=(~W>w5ijmlBR z8CT2UT8iTd5*i`hJwTd2b!Rt z?Bk>%8644MpGp@P$g4UP^r`)05+RGT-1|nH6b_XbOTMN8y5Ky&Ihg1mEHQ4!<$HJusj&0mrP2){#_OL?a*PYxaqS+q2gHQ@3%;bspq&@z zbWFK|&+v_;BeW>r@v$_nN#@hEqyAE~OhbEIxv>560P;fkGKt5h{SMpTYnfaGTx z6E(*e&8!X6_@%FjsZ9^%spNZyNHdzH_Tq~G6&jZsx#kfCIf-g)77J1(`ez#~E~N=- z_$f6w28(qoohpi)Dm)Dj6gY82vw&FPCSU}ePW$4i-7ehhLL7WjM~G4`d_c22{vka= z^Etv(?{*S0o_g}8J@w8CUc+<7Q}0U0Q%`&B)h662Pd#O_+=m;Yh(EccP)wL3v;j9- zuilUAv3J&g*lWQxYGeEpc!;1uws-sd1O9EU zbcUH-`((U_VrANq@6d}Ua^#B>LlSN~Q-Shl$uL#V)ol4_(@PLJnveQCo6wWSJ$$0x@#? zEw7ky7II_fB46Zda+Hg7)R+GeG^fy8x!+*xSkj)DP8I&x)05PX3ZTdmg5W(%17-Y# z8oA=|aR#TUT5Ylp&aTi=jeaAukznUs-m2_94B1F!l5NN7YXv*h>C_%S<7@ii)?eJaJb| zDrCSYb;QXnF$1n3%dqStPNeRxm7s9rvmyrQAREiwg$JD=8$k}}F3(H!;jeSt$*m#? zM3y_}ojm4pw{>rGB)PBJc(jGE8!vL|_kf&6c1PPom#7dMV)f`L$0<%{#JX^t!jHj= zlWvKV-Tz`dIZY7f&IP(QP|_oZbL%)>B_Rjpf?|$u559Jc5tZAr;vo{;#tXR>Q?8YV zC8Ik;_KcZY*c2S4vjy*v;zm>q%aofkg_89p@*=B5;!uml7F*@v{Mzf>;rsHI&AA%N z+4|emLT!UAAeV`LL{@Yf$(kD+K-S!4bIQoNQ?knU_}?Tehui7SFH|X!UzCE?Bz>;0 z0A@>3lDACfsp_SVhd;CUuYT8oZEtKn&jmS^w^}M`O z^EiuTM54TSEfpVnuTjjwTyqdHW7+!|S8SZjYfE)0IdWR6`^yiOO`t1wpxXc@kAW_D z@BbNOVs;RHhMO~uhh%RiTy-jZP!@iWXcZ(XXQ=<#g@fg4u3)xx=E`{HiVmc`?8B)ELz zGC8DW5e!uBApIQ^r@WE7;^g!`*So@|i- z8#P39DqNm8gHLMy5N3gP6Ww?xlEP*pK5-H6>Qr!x0&FA6M*tQ) z9$@a{0am2|J1$VEMrTvIK>ptW=}op8^*<8ek}G5$*Z*7;ng{vA9LtqCCI$!QVJ>qF zR3_uUpk|}~OP;;1_q}Cn7-l&$FM&CFGg$8M>>L8ToW`Tiy1FRG{oF-d-^@8)lXwZM zW)*VLxx{Ho+)qR$cX+F?q_zCZx|p|ySA8+B4%wt{=DmVcxs`V-1F@BX*yfBB*Yd6* zOoX!gsCW}ZEmgg4RKw{h-0Lyd$j~%0G+P;(ceuA#+Mc_0Q?f7G>hfhsl-@{`XhJd6NI`LMZT1hHZ z=#ffPDrry>F0yyn11O@ll~a0Ds;9LgBx0)w8fcl0^Om-Hj(uzUr>#9=uk?ZiRLnp^ z0$xZIxo8UkFHDF+&i;h3S z66UK&nC|SUk50QlA2ANn?r!^VOYZ5N+y)W$1$u^Wo*~^?_F`kYQ7@9c`cR0uC2ePO zxA~{aWCttv9-}NC^x}0}Z!XUGG3nqDy=G-_`T_TTIdRd}l0(#9i|iIQgPP?vvnp>G z>rx2;PhzUyT%c=j+X9#si)4{kQftb0j+Cz(SpV!+Wl=XQ+@Y{*f6ZELfCJc#vMprK zWf84|WnZ*#+6(&{gxeEl)&eqDnlS9!HLOf!;mVkTis-5th5d--x8No;s%(Cn%11|O z2O*av&y%qgv%X-P$M*WkuhdBuEQ#(l*1Cd&%i}$4)vOmE&5XOzMarV>Uv2(IV#?r{ z^NxbVOjKPbOWfTWDtE?St9gELqTap6c4gewrtUf_W6sUeTJfc! zKN1gcFz!3QKf=!7oR#zmT$RzG??QsV^4+Rasgz9<5{WS3UA3a3bi@>tvT6r~nT?Y< zacsHOq;KW@#%*uXld9M1i^wefINV_!sWbAz9VC*&SE;a*c@Ne3oBLn)Sjk|=&u`o9?k5_WWi$8!dwAb;rPkW@D+|16BX$%fX!4>P zIK&=DYOyWMjyB@$P?)?Rwm9=rwVX?jIZu7oD@j>(YWsO5jrd8HDf8otpVQ*|3*3E# z6~k-26`%Uu;4Sx`_ubLDVUH9>vr@Gj6LE-q&LkiAU;z09@l;TRN@s1YSMr8e{6bLj0#1q5B(l?Ed!dz2U%&6H?~lFc7TrYF za0)p)Tc~g^Tey4R3SS?T9P(87hM@ScSE5e>{?8A|SgQ(9#jpa}aKlxAjq3c8{juT{ ze?1P*fUvR1wpe7pxAMHQ*lQc#u3hd&*87oEEdI138Wmtm3TwT4pbAhat3b0KKNKrI zqCZsF zPFk3NO>XpnO0W(c&P!Zb4} z*o|rt*YZ*gRBNMqK(*-29YM)HuULO$5e^pX@SYgZ)a#M`C++_NH+}@V@8@H>Wws2_0SM_`RZt{#WUk$TWXJ)jq>tOnjEPyG~Jc=;mVar(e9EqGAym8l$1N zl>54ef8hd3S~B4~m3?+d6FVJOJ-8Zv!Ska}nto~ZRC+6(qYqSAx%%dr<04v>NFC58Ag&t(2!b{va*^9zfc3K8= zf=w`v@Yj#L>qp-5R^Ri-mnW`XSOVqW3I)UXAtM-4&Z!1Su#t+?g7pwhFmJn>c5N+h-RV8?hyVC^*^xhNpKrapk)r7vnh`2Xw2GdoM~o@= zqbxZaK=NaP;d3aX2}Z?Ciit(27_24J1Cxj~oM7kyM1is4$em{QSk_XqGZv{=|F0Z= za>J(|I^nNB=EjfIzmH^yU+K|egr82?qH0lKWFPz#;B#29>UUS{QD);d_(rT-vOOr? z%U?h86p+y-+TeI#9X98 zbq-3lQMFnXl4=;?OOqxUO(eyAj%%AJ#Vq} z31l(DXF(udFLUXax}PQd1d&>v%WAW23r!or6(t7paFG+*U&MD_`vg%{v>+AogCvIE_(J=-X5D;O)r7SpWIh?T636~Dw^0ilSUG4Ht(WF1{j zFv&@(BfRGIjw|Tmoz&rcTC8rdCd7A9=x2kHXLK4a!!M}@6xjw39*n%`t$em}|4&ml z{`h4x>6|(D6DR81$OYT_0-p-sIE;W21w}YTq8_cDkAw%sFNluC2mF%Fe&iV+Vb7no zB-HBQnic3&o!vkkP%h5;y9&T7J2CIh$jgYWeCgAZMtpnxYLsE4AF0)fICAZ-(1EZ!fB@9>K|{SqO9n9yFk(_1KSnPjI$Gw45~ym4Jn+y>-UE`R=>#JZoP zq8Y`lj-uhx{H{oK8AU>Z{fN0~{sUlTv*()wneKPr*L1JRnr=3zlkx-?rq&GdR$TwU zZ{|Gpvp!g>x)9K!&a;3P%Yf5|TEW7uWYxUU)U7F|EHC4WxKqQ}i(Mz?xgurp5=qlkR_bCaM5U@o(^;-a zqR&!5<`5os?HuYFj|7=9hwH6As6%srOoAX#Gq56vOuZjRh=P~U1Y`v>ABJ@^dX%#e zoGSHGZNzk-RqfDN34mJk%SYu%tFp;eEWVS3=hJ*AT`(2)r-kH#?#7KkV6I zSNHgrFWh`?>(CGjL$OXVxam&@NX?`7`^F6~edmSafR(AMVpk^OS8 zu~vd^LbM1BdI9TYnc-(a7@28eKD2PLr(M+@$1D)}zgCC|mz4v-BUu`X)} zFym&h6i!IEQzfB_e4Q6OLILu1X#egY!h(xUiZ?rC@7#462loK(00hFH+7S>5B%WcI zg(KIwF@Rrq{-1~5w60`n#!je-*a=x%C(e2xan?Y!>;$kG)xG0`0(^Ok7_?T_9zYIM zKTbMMa~N3|5O1jHzrGHpFUAm{&3j5F_x+kNQ@ zlz{`fC`5z6^2OEqc7sW#dew?|(!;jUASFk}=E5Lb$i>8Y z4KfC=9s-^bnlx*IZ)COGV4*WanF(0J67_Xf{Lj%{gh6yWskrlUJH>@`7U_etJZvP>BVn;w1pjcv6I9X z4UstAjiI2)5CYCvgRR&Q%9X{n4#$e$ke9@!)3BE$ga5aI_vSl!~MEvZZ_f;6=j&kbgZYLv|L6 z1!p;>q#J;e>unEYPdzAMUo_dzFs6dgbFOD z2T0U*q6@+%^aoDye4Ky3_{CWKpkLfTAp|9)L8SQAf9E@di|r(4q6a`uEJGIi5#F1+ z%;7(8<=N}czGg)6>7Oz$E}cViv^u`DT!}^7YTuees=}&W*`f zTA^nG4;_zC{BxoKiVyQ3BSnOoK^b&6Eo??r#v-k*sUrZ7C~05q4h4$6vLbG;0ugBH zd_nsR_PaobmI>=2P7I0(t%m_rK#jpzGfrEB;@X|RJPC?Zh&41EVFF+?4)VpNb!D%^zdU|)s$G<;#vcnlGZlhmW+pA91J z^m0=Ol6Yq2$RfGn6j`KDA_+Py@goQHx*4#H!k6L0&p`ubGGohkAQB=um3Wa58-|FA znT3S05Le(h3=Se+D-qK8U~^-T|4Rvy)A=Ll42wpDFo4=Kltyen>A-+cz~oFd*8%vX zyQEB3NPILr65mC=5FaLLN`GNGEZQI-iOc7t3zjA9i~_|HO+yhA$(EMn!i>@OYk!}( zV)lvG>n1{drO&Yiw8(6MV??M=(?P$|=jM29@EBv^VFcj&0%Gsv3we2g8X+%I|4v>| zfLmi28jTR`@rz&Juli@z(m&1_`RIxNeyeVz_#|EktOijKSiaW-1eWg!!#wF1V_A38 z;#l!UC#VD*bPkM+kZOCr!^IdOfVm!EgaH3LoDE@Aiqsg824+wiiX?6zG(5gc25@4r;va8l+*f2$7Ky$c|3_9AFg!2uD z(r8!()8srvM2YUSs6u9>3poPGqOZWAY7Pp^d0;9wM&0p9_8~_G!zd3))EF{ciYIuR z@h0X#S(+oOWEMp2$`42tStu;!BAzlK%F^oI0@E!EV|ZckP8i=ZfUBEhk%OA-BeY(!$Hw#bV70tekzu1Bn=s=O0>K7Q z4C6bIc9=vDr~!In3jr!bLRh&N??FBSCR=SMZ^2@X(g_S8PgGqGC>ai5cz8!pOk@nf zVoZ_o7X%52DW@1e>HGiL(KItLf$=SDk4zb|gzd3{#eT4k(6eM|g)t%WC<54U5TieI zLDmY1-Xntin;2v07EH6SuL}i>x2)5%hDhT>PL$vn=~1GhN1387QHn+0WF*#GF>Lh} zC;Z~FuM@z;pAJpXlQFrz;W!q|By8vd_034Uj9#{B(kC-69fZ8 zBFP>V*e}uHe4;LQ=%BZ{@ZsyH4m#mS_%nb%I87x}0ToLH_*f69z^c6+Yw}|O3^Z$0)fMw|B4Dwcgrs$Td77qW|-?NU9q{Vp@ zl*e=qIugYxCkO}fS=Fg{onDBit>YqloqWsSSgAm3MqUU9MBE^nOsmw5@q+Vj2qX|n zv}q8T!7Dk52G`_&N=9>syz3;%gw9v3XnyqknM?k?uq%$9jhRCzOrl^JRt9H-R%7s_ zA_2aNKoUI0VA5fR{jI9&#d$PP0z0QcE2vPCc_`e`eagE>0@QGx`SQGFZIpYrR9%L=}J5xhuEd2I^OX?VIhd;ovqqak`2 zF$$;TH>p1gl*vyq$%DkW%ZBik9;isOA-m0n?8e711EPuz@>l(+?SJhE=6L5QqHRc$ zIuI9BWnn7rZOeu)&>{0kepWgCC`d=-Hc zqGs2Aaee)t`kYb9jYZ9la#Wlegex&uxQW|wRIm?lh-v9(iMSF@f1fznu?)kL->rD5 z@>g3YhEV|v?ttiIcd-D|b+9!Fd^K*>A~%4LYC&WZBvQBE%uYzvd;VJJbLLJmtfKvrS`2By!w% z?2`){;CKK?LY;u|k#LPL`vKt){fsAXA%?8`LKmAzm|<`Tm2qRCDC)Nzc?Y$-DH^i1 zCJu%^@qEFE5}wf1$b?xpk$fG}0H_c+5^j=UcHt)LSU*4u0p(lU_9AJSEEEM1C0Tv3=ihpsK@D=A$QWxj|M!mv3aDm87?5&l_G+Ji_H=pgay(@%7?;sFy>ypE++24INteG7;H5DOXT@u!*L|X(N5effB?q-A zmz}USvAh_J)HPykad<`n%oX{GoSmGpV?$MbF`{OWDptZEM6(UUY;u|j&TO}jK?|(_ z`DR>fnZ1}A5~I#xD?+cGnmVrl&)0?p*4SVS!`1TR5E~S|zCeX&#DxLW6$<|lO$fo1 zQ^A0#YNE%;VJ$SJZOmDFCmfuXCAn$7(+#*TO>#n=C|bOqz$?1l)~UFh?{}jj2Bs{U zrb%G(Hn8;~I9_@<>WXo-k!Qu&4celx8U&EsFen3Q=5)yQFsEbinb)3mxN|iIXsw1> z<0co*g+aqBFof=+6!|o0_ztGh$jQx2DYGb0EdEricoU%k3_nK@`jNky^~KZ*_()x# z@7GxvLO`P3u-0CwsNw_s^#HwS;MJ{<-@U!<>ysZeD-#w;9i$bg1K)>r&@?PbuW5er zcvb!2N581q5cN%vQr? zv_K7WrU`z<6CrCEAS}SUF6|TwhxO3f)=;}UP$+x zkA26Z*FYCMh`38ype4W!n2A3MvJ$>x$kto+z*o-R^UNjZ06}qq;T5S97!cXjP!?)I z51=AY?OCQ3$XKER03D16#fq8Mi53U;zsZya`45qBI;l~`#GC4D031tWZGyo3KKnTl1 ziX%)b&B}o+iK7FRQjw|+lizH@55wk)&DtF5m1nlKJiU4D3wtb%N3W@h!~#@x73&_j zVz+6&k=$ENqp=|ryky34u%W8k`Hr_$Y}VbP*jzqlKoq{k+a=tv>bB-@?437hF8&bl z0V#g@KH=a8LV(4k8u*J6c}4+>x2hF_L24Wm(qLi%OF(5V;;>u+t@!Kj{(*+>&tIVq}C=F-G?zTCRe(uED8X4_DI}9p8mpK-dIm zQ^~ z0KKuuc4=0|W5PJJbN}Eu7{GWAVO{Es4mKHX?45kHdNpb{bm zl>pV~0o|Tm=t5^Oq(C?ugHG5VN&;8a6J1E(z_N=vEq8hW%x&LIXw%b;5K+s*+Fag4Ag}mBBbe$_) zHnjJm^n%9GQVobysLMj6EBZn&6l#a*dM2)jx2Mtvl5FU|&}1xhX=9)p6Hamd6mNK`SLOwdnG(b`YA;@Fn(|@GXKO(<-(CpWxYxt#C{-1KUz; z__+yNCW^llLq!(v(O+gYu6ktjB~Py2R3GR@^NJM+gwLeQ>6 zoMgM^Bp2J{8%U9`<+w2nI;%bDXH9w%*V~-5Lq-H@32&-*%#gc{_4vwr28{i>3nK}Zh4|ON&Kp6(76JRkaElBxtO>Dg4RC-zkgz4L9xz*V z#NA~rCuvNaaj<<;z+Z;OIbUfJ>%-b5EJVoMTD*n#(04v|bNdAu};`vpO~ zqA-Zx*q_DXMM3-vgM;`j!-Du_BiP?U3!iiLx5%uKp318DuHzWlGg@1D#M!u{_Q0A* zxF&)XbaED>3+&Vt1~u7HnuXH^ESN6z5?A$SwR927-v`$pzB#}C(9IVYd^10UI0?S+Jj&H?029yoKd#CqSYxg?hWAe&?oMH}O_)CWmi|^roTy(5AlEx;IH} ze8)EvhSlx5C2wQ>{!K%_pf`tazP7w>*BA13*KgZ2{3@6Bw>l1i+?LOG-+XOy-LA{> z4%V;VG;~~;VsZFpy58)ozid;lGQIicyoF2j=36(9nOV21G_SM1VpFdY-gt>QBlLC| zZ!1=6lK`A*FgiAzt^z^X_|@E{#3L8$R0gI~EW?LgG9x-r8>+A&MJMJ-Q~RH+OkSwX z-!{)|<0iLS==FLy?Zw>!8^Ka{n&t^VfjI38Cd$MCt$E4_YPGKVw=1W53 zxA>Q$VlVQ$kAGw_ZxjD;rOi?>OOVgD)~zjxb`;cIJ0!Zkp#I_6TOxH8rJGXqf0(_c zcU{Ghed~F4a76vjW^XCb?Q?Y%CHpqir)D15k;~4atoEdDFP$j10pnHIPq8Jn~M)|7f;w^^B4h9hhT^O8OQ&;@Wd9JE{ z&DtLC*NNPtFX6oU=)ke+PV6YCfavPGoVhKWCCDR! zoiA-^7*{O=Ys$c45zy}X8^OamJTd>?rybp7S-RR|nABlRj}nMc7Pny?HJh4?HjO|}^~tY$sd~gc`SX$eNZr3x=QY+Z-_-Y1K0{U6I>Wk`=9VVaixH9s z`Gqe1VoSfPIK27z%-PHHxFukm$rI#*s^(lAhYcuJ$*8g%--%Vk9nOaLf zKbwBCOW$PassGmcS2p#n3)2^p9yNPi{`9(QKRM#T{1!{4`b#xKpHJqs*C#h^R9lNO zc`wQ4J=5jgp2@qh{^CtL(R&sJsJnJ<=`W?JBYr1AxM_>SA_3Q6rH7uNEIj>X5d z6&1M3AoZMB9g`dYHjFMPa*l3hSnmd|grwp(!P_VVn^=mKuFdLqzmqv08WSaelN|Xj zC86Nwa`@!y^>c6D#lN@t_Znj@vG^tdAO^MLsT_VmgRo#m^Dpb?&U=%8oA|efZ}!(! z%)J?nJon}uJXx=cr-%qGX1Bs2{N7)g?9=KcPkZWN$A^PXth;u2w6&maOt0wSg1Xey z+0RGn7j9Z#m+Ce9`QG&l_r0#Gw?@>ZVzZww;Qm1U^ZWMd?yXUotIa3WrB0iDAiw^; zeFt>4`4oL~0DXVjzJ1w;7wfA2txeDH@9FxVY-+7c-1B-ZY*U$d;B_tFtozwF=l#Z> zNbKu=F}L*GJrS>_p-1%YF@`SYE^*EYh)~`NIu4B|Zv`EyB6n>m68*)WWQL zizGon;1Hd12w*R0Yu*Pw1o}%u)%wlYc^4Ji7y5vdWdLh#YMKx5UV)wHrFbw6U&7g=Xf3oApTT0-2e2460sjwd8H*E* z#0a#wATD-Q?lUG2V&E-XBq|nvEmpEAR$QmQTBBGuG5^88se3|)Dc4(4R1p$DZSor*zsuEp(^XJF>cc5KXdGfMMW@|5o+BA>&5@?Fq7Tw0rd(=u@F)yNMU z;Kk=IVe=zRHx@?1*wJM+#TXBF?)?Cw#eV!P*Iq4|iD~FUF_vjqoCEkRZuTVXq6yqM z5C`1@90;uS76`}O?!Wpaf7e)G@l#w-ad}hu5B4gT(AU%!XbaQST6*&c!#MONlQ8uF zVDYBvqcI&i6yxh$UQPdDaE{68jr7ejdI1skE}qX$Q_*lJlR)CLK|ZU-^KV+LN$iXe z!K(p#f?@)fFcn6a+4h~AT)^IQg|rOwenWp2)36O)QDEE8dBfO-ZUk(wD7Ns$?7pX* z<`zF3UARp9SKU=7NZy6q>pSM?5Q@KE(KC5LnXPw`9|7{f?E|dbz|id`D$|!(&TAjauiSFI{_#pE$m?+s`kCVGSVbFDx7&8J#>#@Ec> z6-|C?7<)=yHUIF5<E$Ih)!pc~1_DyMKSt`+h$$ zIks<}e{dW6qWRnTd8Q?p)$Mk=8*|+eDI2S8j*f8h^ad;r28OR2rnQ#dd_=@qK)dqX zW^9@E_fAYKJecAsyW46R9k>dGy|f1?!{trOFo^svZ!1WCKkoRI*2c z%E+R7dd*W`b~aY5R0;kN&N&&|YrfmnJZxXd`t(<>&1qru5jF{>GipBw{YQ%Y&$nHY z`6#FVyMKQi`n&IQ$bb35qsd?QZ#k9A$=~Q-^V&@70QS7QwZKd8|75x%)=s3M!%KRF z7`Rit;P-8FPY$SHj&NK)?y_M2Y9kX>Oy7 zGx<3D*ZuqBFj)5c93IDY0>T{_aQ@xUo8wD4`KY$47xC!MCckEGL0%Q`ZFjoYxx_K! zo#}V~B|oo?b4bQD=DbYvP%m5TcQyjb)|g-6#_U5k`gSL7RK#_M>9tB@W~qC|v8Y?| zn1Eedbr*R*{l4U_&|%eiZR0*1eJdt~^sRVI1ZLOF7(riIB^jqo%Vp{2UKa-!w4l*$g+vDZzPZC70?bahjXrijiq>089kb+&+ zkLnn7n=}^?Tpcu%&N(6Mn@;u3ovv>>(>GF*k$gHAZ4cu-<-80sFURO2*{erbqB?c; zklf&nIMhie|#; zrjFqDo`Z7P{fk|IW152U~LI~+pOPF?B!Fnxvne$A<; zHD^PM&|i830Q^E_PjKqFS7Xex!wTY^OZow+SuLVFd*($PnzFg|qwAtPLa zgFC!v1(PEh(x-92pWpndI=C+|Q{+-tPYI_7zq^h?DR9C9|k%Q}5TepR>w z^3_sy{F|Q30tcn>GF^HweJgpPe=}dT&fs2q0pui^;*z8%@GIlgJSbz2pdW-w^ltY$ z3#uX6j`X=v!DRMSFGq3RmT#1CDbj!AiAYjS-s#&P?qTh- zv#3s0^Q?dqiTxE!Yn??A&fhovLHkvQ@94}GOV-CrKB}Aak4RW^`}FgwuJ&pkT_GJ@ z)yGRbTFXUx9_8?~Df@J*p5Ex5rn67m^>m7R3JM8xC{XSfyQhtM8eO0x5Q4;?zzjN) zHoD+0yMJ`6-FH?Ek0oamw4K@|5Q}}}Ro?9>qI5@I)jmHla!@*b7J~S)2(g{3An|BA z%-}*YZ_%~#=>vY|`=apsk%RK8+WiFCq?cyD|0!2A5PqW4xAS#}<#eFxW^&C#^$)d%z&XwG-D@04BZf%cK0Vz8ETry6+UkCpR#czW zmDPD|Bk5&PJAKM0EU|0o-*~Eqrr#xtZ1Sq8mn|?|LqV$XM?VFpokWDBOWrC%em({- z#hOE2x`yf;#Y-2E6HkxgrBAx2NAXf`_tf!HA>1flGWmGDINvTzVbrXhE@^5YpryKg znRi{hlhqGd`#G6_npZPVYMq_a(?2JJ@~aHL?||RliBz2$B>$p=5d6-k z(vK3*B=0tKS7+77N#cz7H5JoLeY8#^lkShGSwq%-lSS33vE=WRovQeCvLgW6#sak+ z>0-W>rg*3b8vpi}(6~E(oy-y3(FI%J6}O!|2f~cNEcy-gOK`v|o|p+y+Rn*sah`vd zOLAAerM#;_+~IdVQ*~!$qGy>2{zn+nC`51%XGnpbsVW#W#q`RS?l@Q$*@NckLI=;{6GQ9NyRqdG?u zZV<2DOE}094B3}{i=$rba(8a)(D!P}NkQ@0`VZrC6H3sxBNrg*eDAKc9{u9{0e{HfW63#@wom4km=`8d=uMn4G${Sk3QE0$ zgQOqTMLJPPO%u1*qQD!|_fR&X*_5tkcl@@s8L4%~ElhJmCe3A};pm=f+?|8z2hW5T z5B|07^xV>^f42Omspt#|2saaY6dpCry7` z(4{{v=+Yk-nEn_|_0S&|bm@-^On-bPeBPx$E|{M_%^u!yL5}{o;Qn;y=^6c@U3_as z%1c{5}pWH^d z(45d!XVn!3{Eot-{}}&KPB(g&@*u>2R)yR9=T)7A(daq{xgXEFD4*@(5D#2ArF?$p z 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