Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .venv/lib/python3.11/site-packages/blake3-1.0.4.dist-info/INSTALLER +1 -0
- .venv/lib/python3.11/site-packages/blake3-1.0.4.dist-info/METADATA +108 -0
- .venv/lib/python3.11/site-packages/blake3-1.0.4.dist-info/RECORD +9 -0
- .venv/lib/python3.11/site-packages/blake3-1.0.4.dist-info/WHEEL +4 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/__init__.py +1132 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_adapters.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_collections.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_compat.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_functools.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_itertools.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_meta.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/diagnose.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/_adapters.py +135 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/_collections.py +30 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/_compat.py +56 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/_functools.py +104 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/_itertools.py +171 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/_meta.py +75 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/_text.py +99 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/compat/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/compat/__pycache__/py39.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/compat/py311.py +22 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/compat/py39.py +36 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/diagnose.py +21 -0
- .venv/lib/python3.11/site-packages/importlib_metadata/py.typed +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/__main__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/_subprocess.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/_types.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/config.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/importer.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/logging.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/main.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/server.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/__pycache__/workers.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/lifespan/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/lifespan/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/lifespan/__pycache__/off.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/lifespan/__pycache__/on.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/lifespan/off.py +17 -0
- .venv/lib/python3.11/site-packages/uvicorn/lifespan/on.py +137 -0
- .venv/lib/python3.11/site-packages/uvicorn/loops/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/protocols/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/protocols/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/protocols/__pycache__/utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/protocols/http/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/protocols/http/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/protocols/http/__pycache__/auto.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/uvicorn/protocols/http/__pycache__/flow_control.cpython-311.pyc +0 -0
.venv/lib/python3.11/site-packages/blake3-1.0.4.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
.venv/lib/python3.11/site-packages/blake3-1.0.4.dist-info/METADATA
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: blake3
|
| 3 |
+
Version: 1.0.4
|
| 4 |
+
Summary: Python bindings for the Rust blake3 crate
|
| 5 |
+
Home-Page: https://github.com/oconnor663/blake3-py
|
| 6 |
+
Author: Jack O'Connor <oconnor663@gmail.com>
|
| 7 |
+
Author-email: Jack O'Connor <oconnor663@gmail.com>
|
| 8 |
+
License: CC0-1.0 OR Apache-2.0
|
| 9 |
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
| 10 |
+
Project-URL: Source Code, https://github.com/oconnor663/blake3-py
|
| 11 |
+
|
| 12 |
+
# blake3-py [](https://github.com/oconnor663/blake3-py/actions/workflows/tests.yml) [](https://pypi.python.org/pypi/blake3)
|
| 13 |
+
|
| 14 |
+
Python bindings for the [official Rust implementation of
|
| 15 |
+
BLAKE3](https://github.com/BLAKE3-team/BLAKE3), based on
|
| 16 |
+
[PyO3](https://github.com/PyO3/pyo3). These bindings expose all the features of
|
| 17 |
+
BLAKE3, including extendable output, keying, and multithreading. The basic API
|
| 18 |
+
matches that of Python's standard
|
| 19 |
+
[`hashlib`](https://docs.python.org/3/library/hashlib.html) module.
|
| 20 |
+
|
| 21 |
+
## Examples
|
| 22 |
+
|
| 23 |
+
```python
|
| 24 |
+
from blake3 import blake3
|
| 25 |
+
|
| 26 |
+
# Hash some input all at once. The input can be bytes, a bytearray, or a memoryview.
|
| 27 |
+
hash1 = blake3(b"foobarbaz").digest()
|
| 28 |
+
|
| 29 |
+
# Hash the same input incrementally.
|
| 30 |
+
hasher = blake3()
|
| 31 |
+
hasher.update(b"foo")
|
| 32 |
+
hasher.update(b"bar")
|
| 33 |
+
hasher.update(b"baz")
|
| 34 |
+
hash2 = hasher.digest()
|
| 35 |
+
assert hash1 == hash2
|
| 36 |
+
|
| 37 |
+
# Hash the same input fluently.
|
| 38 |
+
assert hash1 == blake3(b"foo").update(b"bar").update(b"baz").digest()
|
| 39 |
+
|
| 40 |
+
# Hexadecimal output.
|
| 41 |
+
print("The hash of 'hello world' is", blake3(b"hello world").hexdigest())
|
| 42 |
+
|
| 43 |
+
# Use the keyed hashing mode, which takes a 32-byte key.
|
| 44 |
+
import secrets
|
| 45 |
+
random_key = secrets.token_bytes(32)
|
| 46 |
+
message = b"a message to authenticate"
|
| 47 |
+
mac = blake3(message, key=random_key).digest()
|
| 48 |
+
|
| 49 |
+
# Use the key derivation mode, which takes a context string. Context strings
|
| 50 |
+
# should be hardcoded, globally unique, and application-specific.
|
| 51 |
+
context = "blake3-py 2020-03-04 11:13:10 example context"
|
| 52 |
+
key_material = b"usually at least 32 random bytes, not a password"
|
| 53 |
+
derived_key = blake3(key_material, derive_key_context=context).digest()
|
| 54 |
+
|
| 55 |
+
# Extendable output. The default digest size is 32 bytes.
|
| 56 |
+
extended = blake3(b"foo").digest(length=100)
|
| 57 |
+
assert extended[:32] == blake3(b"foo").digest()
|
| 58 |
+
assert extended[75:100] == blake3(b"foo").digest(length=25, seek=75)
|
| 59 |
+
|
| 60 |
+
# Hash a large input using multiple threads. Note that this can be slower for
|
| 61 |
+
# inputs shorter than ~1 MB, and it's a good idea to benchmark it for your use
|
| 62 |
+
# case on your platform.
|
| 63 |
+
large_input = bytearray(1_000_000)
|
| 64 |
+
hash_single = blake3(large_input).digest()
|
| 65 |
+
hash_two = blake3(large_input, max_threads=2).digest()
|
| 66 |
+
hash_many = blake3(large_input, max_threads=blake3.AUTO).digest()
|
| 67 |
+
assert hash_single == hash_two == hash_many
|
| 68 |
+
|
| 69 |
+
# Hash a file with multiple threads using memory mapping. This is what b3sum
|
| 70 |
+
# does by default.
|
| 71 |
+
file_hasher = blake3(max_threads=blake3.AUTO)
|
| 72 |
+
file_hasher.update_mmap("/big/file.txt")
|
| 73 |
+
file_hash = file_hasher.digest()
|
| 74 |
+
|
| 75 |
+
# Copy a hasher that's already accepted some input.
|
| 76 |
+
hasher1 = blake3(b"foo")
|
| 77 |
+
hasher2 = hasher1.copy()
|
| 78 |
+
hasher1.update(b"bar")
|
| 79 |
+
hasher2.update(b"baz")
|
| 80 |
+
assert hasher1.digest() == blake3(b"foobar").digest()
|
| 81 |
+
assert hasher2.digest() == blake3(b"foobaz").digest()
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
## Installation
|
| 85 |
+
|
| 86 |
+
```
|
| 87 |
+
pip install blake3
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
As usual with Pip, you might need to use `sudo` or the `--user` flag
|
| 91 |
+
with the command above, depending on how you installed Python on your
|
| 92 |
+
system.
|
| 93 |
+
|
| 94 |
+
There are binary wheels [available on
|
| 95 |
+
PyPI](https://pypi.org/project/blake3/#files) for most environments. But
|
| 96 |
+
if you're building the source distribution, or if a binary wheel isn't
|
| 97 |
+
available for your environment, you'll need to [install the Rust
|
| 98 |
+
toolchain](https://rustup.rs).
|
| 99 |
+
|
| 100 |
+
## C Bindings
|
| 101 |
+
|
| 102 |
+
Experimental bindings for the official BLAKE3 C implementation are available in
|
| 103 |
+
the [`c_impl`](c_impl) directory. These will probably not be published on PyPI,
|
| 104 |
+
and most applications should prefer the Rust-based bindings. But if you can't
|
| 105 |
+
depend on the Rust toolchain, and you're on some platform that this project
|
| 106 |
+
doesn't provide binary wheels for, the C-based bindings might be an
|
| 107 |
+
alternative.
|
| 108 |
+
|
.venv/lib/python3.11/site-packages/blake3-1.0.4.dist-info/RECORD
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blake3-1.0.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
blake3-1.0.4.dist-info/METADATA,sha256=t8zfV6RI_1YtG_N6JgFUG1Z73sK6n31N9bv0rejh4eY,4166
|
| 3 |
+
blake3-1.0.4.dist-info/RECORD,,
|
| 4 |
+
blake3-1.0.4.dist-info/WHEEL,sha256=eKn-h6LbuPin9BQdctwIkEq1OLRtDcdOVrhrYyXn53g,129
|
| 5 |
+
blake3/__init__.py,sha256=i5GXKa35g4Dt_hOK8OmCFGY-6xDtzmTAGlepSFv_0ns,107
|
| 6 |
+
blake3/__init__.pyi,sha256=Ngl-UCmwX3q3E9IWmQGCqbEQhfdkaoSVd1G1QaHtQNg,750
|
| 7 |
+
blake3/__pycache__/__init__.cpython-311.pyc,,
|
| 8 |
+
blake3/blake3.cpython-311-x86_64-linux-gnu.so,sha256=hZ7I6d_EChuUSlcKn5QfaNN0TDjYNNp0S9xedZe5vec,964720
|
| 9 |
+
blake3/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
.venv/lib/python3.11/site-packages/blake3-1.0.4.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: maturin (1.8.1)
|
| 3 |
+
Root-Is-Purelib: false
|
| 4 |
+
Tag: cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64
|
.venv/lib/python3.11/site-packages/importlib_metadata/__init__.py
ADDED
|
@@ -0,0 +1,1132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
APIs exposing metadata from third-party Python packages.
|
| 3 |
+
|
| 4 |
+
This codebase is shared between importlib.metadata in the stdlib
|
| 5 |
+
and importlib_metadata in PyPI. See
|
| 6 |
+
https://github.com/python/importlib_metadata/wiki/Development-Methodology
|
| 7 |
+
for more detail.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import abc
|
| 13 |
+
import collections
|
| 14 |
+
import email
|
| 15 |
+
import functools
|
| 16 |
+
import itertools
|
| 17 |
+
import operator
|
| 18 |
+
import os
|
| 19 |
+
import pathlib
|
| 20 |
+
import posixpath
|
| 21 |
+
import re
|
| 22 |
+
import sys
|
| 23 |
+
import textwrap
|
| 24 |
+
import types
|
| 25 |
+
from contextlib import suppress
|
| 26 |
+
from importlib import import_module
|
| 27 |
+
from importlib.abc import MetaPathFinder
|
| 28 |
+
from itertools import starmap
|
| 29 |
+
from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast
|
| 30 |
+
|
| 31 |
+
from . import _meta
|
| 32 |
+
from ._collections import FreezableDefaultDict, Pair
|
| 33 |
+
from ._compat import (
|
| 34 |
+
NullFinder,
|
| 35 |
+
install,
|
| 36 |
+
)
|
| 37 |
+
from ._functools import method_cache, pass_none
|
| 38 |
+
from ._itertools import always_iterable, bucket, unique_everseen
|
| 39 |
+
from ._meta import PackageMetadata, SimplePath
|
| 40 |
+
from .compat import py39, py311
|
| 41 |
+
|
| 42 |
+
__all__ = [
|
| 43 |
+
'Distribution',
|
| 44 |
+
'DistributionFinder',
|
| 45 |
+
'PackageMetadata',
|
| 46 |
+
'PackageNotFoundError',
|
| 47 |
+
'SimplePath',
|
| 48 |
+
'distribution',
|
| 49 |
+
'distributions',
|
| 50 |
+
'entry_points',
|
| 51 |
+
'files',
|
| 52 |
+
'metadata',
|
| 53 |
+
'packages_distributions',
|
| 54 |
+
'requires',
|
| 55 |
+
'version',
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class PackageNotFoundError(ModuleNotFoundError):
|
| 60 |
+
"""The package was not found."""
|
| 61 |
+
|
| 62 |
+
def __str__(self) -> str:
|
| 63 |
+
return f"No package metadata was found for {self.name}"
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def name(self) -> str: # type: ignore[override] # make readonly
|
| 67 |
+
(name,) = self.args
|
| 68 |
+
return name
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class Sectioned:
|
| 72 |
+
"""
|
| 73 |
+
A simple entry point config parser for performance
|
| 74 |
+
|
| 75 |
+
>>> for item in Sectioned.read(Sectioned._sample):
|
| 76 |
+
... print(item)
|
| 77 |
+
Pair(name='sec1', value='# comments ignored')
|
| 78 |
+
Pair(name='sec1', value='a = 1')
|
| 79 |
+
Pair(name='sec1', value='b = 2')
|
| 80 |
+
Pair(name='sec2', value='a = 2')
|
| 81 |
+
|
| 82 |
+
>>> res = Sectioned.section_pairs(Sectioned._sample)
|
| 83 |
+
>>> item = next(res)
|
| 84 |
+
>>> item.name
|
| 85 |
+
'sec1'
|
| 86 |
+
>>> item.value
|
| 87 |
+
Pair(name='a', value='1')
|
| 88 |
+
>>> item = next(res)
|
| 89 |
+
>>> item.value
|
| 90 |
+
Pair(name='b', value='2')
|
| 91 |
+
>>> item = next(res)
|
| 92 |
+
>>> item.name
|
| 93 |
+
'sec2'
|
| 94 |
+
>>> item.value
|
| 95 |
+
Pair(name='a', value='2')
|
| 96 |
+
>>> list(res)
|
| 97 |
+
[]
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
_sample = textwrap.dedent(
|
| 101 |
+
"""
|
| 102 |
+
[sec1]
|
| 103 |
+
# comments ignored
|
| 104 |
+
a = 1
|
| 105 |
+
b = 2
|
| 106 |
+
|
| 107 |
+
[sec2]
|
| 108 |
+
a = 2
|
| 109 |
+
"""
|
| 110 |
+
).lstrip()
|
| 111 |
+
|
| 112 |
+
@classmethod
|
| 113 |
+
def section_pairs(cls, text):
|
| 114 |
+
return (
|
| 115 |
+
section._replace(value=Pair.parse(section.value))
|
| 116 |
+
for section in cls.read(text, filter_=cls.valid)
|
| 117 |
+
if section.name is not None
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
@staticmethod
|
| 121 |
+
def read(text, filter_=None):
|
| 122 |
+
lines = filter(filter_, map(str.strip, text.splitlines()))
|
| 123 |
+
name = None
|
| 124 |
+
for value in lines:
|
| 125 |
+
section_match = value.startswith('[') and value.endswith(']')
|
| 126 |
+
if section_match:
|
| 127 |
+
name = value.strip('[]')
|
| 128 |
+
continue
|
| 129 |
+
yield Pair(name, value)
|
| 130 |
+
|
| 131 |
+
@staticmethod
|
| 132 |
+
def valid(line: str):
|
| 133 |
+
return line and not line.startswith('#')
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class EntryPoint:
|
| 137 |
+
"""An entry point as defined by Python packaging conventions.
|
| 138 |
+
|
| 139 |
+
See `the packaging docs on entry points
|
| 140 |
+
<https://packaging.python.org/specifications/entry-points/>`_
|
| 141 |
+
for more information.
|
| 142 |
+
|
| 143 |
+
>>> ep = EntryPoint(
|
| 144 |
+
... name=None, group=None, value='package.module:attr [extra1, extra2]')
|
| 145 |
+
>>> ep.module
|
| 146 |
+
'package.module'
|
| 147 |
+
>>> ep.attr
|
| 148 |
+
'attr'
|
| 149 |
+
>>> ep.extras
|
| 150 |
+
['extra1', 'extra2']
|
| 151 |
+
"""
|
| 152 |
+
|
| 153 |
+
pattern = re.compile(
|
| 154 |
+
r'(?P<module>[\w.]+)\s*'
|
| 155 |
+
r'(:\s*(?P<attr>[\w.]+)\s*)?'
|
| 156 |
+
r'((?P<extras>\[.*\])\s*)?$'
|
| 157 |
+
)
|
| 158 |
+
"""
|
| 159 |
+
A regular expression describing the syntax for an entry point,
|
| 160 |
+
which might look like:
|
| 161 |
+
|
| 162 |
+
- module
|
| 163 |
+
- package.module
|
| 164 |
+
- package.module:attribute
|
| 165 |
+
- package.module:object.attribute
|
| 166 |
+
- package.module:attr [extra1, extra2]
|
| 167 |
+
|
| 168 |
+
Other combinations are possible as well.
|
| 169 |
+
|
| 170 |
+
The expression is lenient about whitespace around the ':',
|
| 171 |
+
following the attr, and following any extras.
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
name: str
|
| 175 |
+
value: str
|
| 176 |
+
group: str
|
| 177 |
+
|
| 178 |
+
dist: Optional[Distribution] = None
|
| 179 |
+
|
| 180 |
+
def __init__(self, name: str, value: str, group: str) -> None:
|
| 181 |
+
vars(self).update(name=name, value=value, group=group)
|
| 182 |
+
|
| 183 |
+
def load(self) -> Any:
|
| 184 |
+
"""Load the entry point from its definition. If only a module
|
| 185 |
+
is indicated by the value, return that module. Otherwise,
|
| 186 |
+
return the named object.
|
| 187 |
+
"""
|
| 188 |
+
match = cast(Match, self.pattern.match(self.value))
|
| 189 |
+
module = import_module(match.group('module'))
|
| 190 |
+
attrs = filter(None, (match.group('attr') or '').split('.'))
|
| 191 |
+
return functools.reduce(getattr, attrs, module)
|
| 192 |
+
|
| 193 |
+
@property
|
| 194 |
+
def module(self) -> str:
|
| 195 |
+
match = self.pattern.match(self.value)
|
| 196 |
+
assert match is not None
|
| 197 |
+
return match.group('module')
|
| 198 |
+
|
| 199 |
+
@property
|
| 200 |
+
def attr(self) -> str:
|
| 201 |
+
match = self.pattern.match(self.value)
|
| 202 |
+
assert match is not None
|
| 203 |
+
return match.group('attr')
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def extras(self) -> List[str]:
|
| 207 |
+
match = self.pattern.match(self.value)
|
| 208 |
+
assert match is not None
|
| 209 |
+
return re.findall(r'\w+', match.group('extras') or '')
|
| 210 |
+
|
| 211 |
+
def _for(self, dist):
|
| 212 |
+
vars(self).update(dist=dist)
|
| 213 |
+
return self
|
| 214 |
+
|
| 215 |
+
def matches(self, **params):
|
| 216 |
+
"""
|
| 217 |
+
EntryPoint matches the given parameters.
|
| 218 |
+
|
| 219 |
+
>>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
|
| 220 |
+
>>> ep.matches(group='foo')
|
| 221 |
+
True
|
| 222 |
+
>>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
|
| 223 |
+
True
|
| 224 |
+
>>> ep.matches(group='foo', name='other')
|
| 225 |
+
False
|
| 226 |
+
>>> ep.matches()
|
| 227 |
+
True
|
| 228 |
+
>>> ep.matches(extras=['extra1', 'extra2'])
|
| 229 |
+
True
|
| 230 |
+
>>> ep.matches(module='bing')
|
| 231 |
+
True
|
| 232 |
+
>>> ep.matches(attr='bong')
|
| 233 |
+
True
|
| 234 |
+
"""
|
| 235 |
+
self._disallow_dist(params)
|
| 236 |
+
attrs = (getattr(self, param) for param in params)
|
| 237 |
+
return all(map(operator.eq, params.values(), attrs))
|
| 238 |
+
|
| 239 |
+
@staticmethod
|
| 240 |
+
def _disallow_dist(params):
|
| 241 |
+
"""
|
| 242 |
+
Querying by dist is not allowed (dist objects are not comparable).
|
| 243 |
+
>>> EntryPoint(name='fan', value='fav', group='fag').matches(dist='foo')
|
| 244 |
+
Traceback (most recent call last):
|
| 245 |
+
...
|
| 246 |
+
ValueError: "dist" is not suitable for matching...
|
| 247 |
+
"""
|
| 248 |
+
if "dist" in params:
|
| 249 |
+
raise ValueError(
|
| 250 |
+
'"dist" is not suitable for matching. '
|
| 251 |
+
"Instead, use Distribution.entry_points.select() on a "
|
| 252 |
+
"located distribution."
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
def _key(self):
|
| 256 |
+
return self.name, self.value, self.group
|
| 257 |
+
|
| 258 |
+
def __lt__(self, other):
|
| 259 |
+
return self._key() < other._key()
|
| 260 |
+
|
| 261 |
+
def __eq__(self, other):
|
| 262 |
+
return self._key() == other._key()
|
| 263 |
+
|
| 264 |
+
def __setattr__(self, name, value):
|
| 265 |
+
raise AttributeError("EntryPoint objects are immutable.")
|
| 266 |
+
|
| 267 |
+
def __repr__(self):
|
| 268 |
+
return (
|
| 269 |
+
f'EntryPoint(name={self.name!r}, value={self.value!r}, '
|
| 270 |
+
f'group={self.group!r})'
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
def __hash__(self) -> int:
|
| 274 |
+
return hash(self._key())
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
class EntryPoints(tuple):
|
| 278 |
+
"""
|
| 279 |
+
An immutable collection of selectable EntryPoint objects.
|
| 280 |
+
"""
|
| 281 |
+
|
| 282 |
+
__slots__ = ()
|
| 283 |
+
|
| 284 |
+
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int
|
| 285 |
+
"""
|
| 286 |
+
Get the EntryPoint in self matching name.
|
| 287 |
+
"""
|
| 288 |
+
try:
|
| 289 |
+
return next(iter(self.select(name=name)))
|
| 290 |
+
except StopIteration:
|
| 291 |
+
raise KeyError(name)
|
| 292 |
+
|
| 293 |
+
def __repr__(self):
|
| 294 |
+
"""
|
| 295 |
+
Repr with classname and tuple constructor to
|
| 296 |
+
signal that we deviate from regular tuple behavior.
|
| 297 |
+
"""
|
| 298 |
+
return '%s(%r)' % (self.__class__.__name__, tuple(self))
|
| 299 |
+
|
| 300 |
+
def select(self, **params) -> EntryPoints:
|
| 301 |
+
"""
|
| 302 |
+
Select entry points from self that match the
|
| 303 |
+
given parameters (typically group and/or name).
|
| 304 |
+
"""
|
| 305 |
+
return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params))
|
| 306 |
+
|
| 307 |
+
@property
|
| 308 |
+
def names(self) -> Set[str]:
|
| 309 |
+
"""
|
| 310 |
+
Return the set of all names of all entry points.
|
| 311 |
+
"""
|
| 312 |
+
return {ep.name for ep in self}
|
| 313 |
+
|
| 314 |
+
@property
|
| 315 |
+
def groups(self) -> Set[str]:
|
| 316 |
+
"""
|
| 317 |
+
Return the set of all groups of all entry points.
|
| 318 |
+
"""
|
| 319 |
+
return {ep.group for ep in self}
|
| 320 |
+
|
| 321 |
+
@classmethod
|
| 322 |
+
def _from_text_for(cls, text, dist):
|
| 323 |
+
return cls(ep._for(dist) for ep in cls._from_text(text))
|
| 324 |
+
|
| 325 |
+
@staticmethod
|
| 326 |
+
def _from_text(text):
|
| 327 |
+
return (
|
| 328 |
+
EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
|
| 329 |
+
for item in Sectioned.section_pairs(text or '')
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
class PackagePath(pathlib.PurePosixPath):
|
| 334 |
+
"""A reference to a path in a package"""
|
| 335 |
+
|
| 336 |
+
hash: Optional[FileHash]
|
| 337 |
+
size: int
|
| 338 |
+
dist: Distribution
|
| 339 |
+
|
| 340 |
+
def read_text(self, encoding: str = 'utf-8') -> str:
|
| 341 |
+
return self.locate().read_text(encoding=encoding)
|
| 342 |
+
|
| 343 |
+
def read_binary(self) -> bytes:
|
| 344 |
+
return self.locate().read_bytes()
|
| 345 |
+
|
| 346 |
+
def locate(self) -> SimplePath:
|
| 347 |
+
"""Return a path-like object for this path"""
|
| 348 |
+
return self.dist.locate_file(self)
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
class FileHash:
|
| 352 |
+
def __init__(self, spec: str) -> None:
|
| 353 |
+
self.mode, _, self.value = spec.partition('=')
|
| 354 |
+
|
| 355 |
+
def __repr__(self) -> str:
|
| 356 |
+
return f'<FileHash mode: {self.mode} value: {self.value}>'
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class Distribution(metaclass=abc.ABCMeta):
|
| 360 |
+
"""
|
| 361 |
+
An abstract Python distribution package.
|
| 362 |
+
|
| 363 |
+
Custom providers may derive from this class and define
|
| 364 |
+
the abstract methods to provide a concrete implementation
|
| 365 |
+
for their environment. Some providers may opt to override
|
| 366 |
+
the default implementation of some properties to bypass
|
| 367 |
+
the file-reading mechanism.
|
| 368 |
+
"""
|
| 369 |
+
|
| 370 |
+
@abc.abstractmethod
|
| 371 |
+
def read_text(self, filename) -> Optional[str]:
|
| 372 |
+
"""Attempt to load metadata file given by the name.
|
| 373 |
+
|
| 374 |
+
Python distribution metadata is organized by blobs of text
|
| 375 |
+
typically represented as "files" in the metadata directory
|
| 376 |
+
(e.g. package-1.0.dist-info). These files include things
|
| 377 |
+
like:
|
| 378 |
+
|
| 379 |
+
- METADATA: The distribution metadata including fields
|
| 380 |
+
like Name and Version and Description.
|
| 381 |
+
- entry_points.txt: A series of entry points as defined in
|
| 382 |
+
`the entry points spec <https://packaging.python.org/en/latest/specifications/entry-points/#file-format>`_.
|
| 383 |
+
- RECORD: A record of files according to
|
| 384 |
+
`this recording spec <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-record-file>`_.
|
| 385 |
+
|
| 386 |
+
A package may provide any set of files, including those
|
| 387 |
+
not listed here or none at all.
|
| 388 |
+
|
| 389 |
+
:param filename: The name of the file in the distribution info.
|
| 390 |
+
:return: The text if found, otherwise None.
|
| 391 |
+
"""
|
| 392 |
+
|
| 393 |
+
@abc.abstractmethod
|
| 394 |
+
def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
|
| 395 |
+
"""
|
| 396 |
+
Given a path to a file in this distribution, return a SimplePath
|
| 397 |
+
to it.
|
| 398 |
+
|
| 399 |
+
This method is used by callers of ``Distribution.files()`` to
|
| 400 |
+
locate files within the distribution. If it's possible for a
|
| 401 |
+
Distribution to represent files in the distribution as
|
| 402 |
+
``SimplePath`` objects, it should implement this method
|
| 403 |
+
to resolve such objects.
|
| 404 |
+
|
| 405 |
+
Some Distribution providers may elect not to resolve SimplePath
|
| 406 |
+
objects within the distribution by raising a
|
| 407 |
+
NotImplementedError, but consumers of such a Distribution would
|
| 408 |
+
be unable to invoke ``Distribution.files()``.
|
| 409 |
+
"""
|
| 410 |
+
|
| 411 |
+
@classmethod
|
| 412 |
+
def from_name(cls, name: str) -> Distribution:
|
| 413 |
+
"""Return the Distribution for the given package name.
|
| 414 |
+
|
| 415 |
+
:param name: The name of the distribution package to search for.
|
| 416 |
+
:return: The Distribution instance (or subclass thereof) for the named
|
| 417 |
+
package, if found.
|
| 418 |
+
:raises PackageNotFoundError: When the named package's distribution
|
| 419 |
+
metadata cannot be found.
|
| 420 |
+
:raises ValueError: When an invalid value is supplied for name.
|
| 421 |
+
"""
|
| 422 |
+
if not name:
|
| 423 |
+
raise ValueError("A distribution name is required.")
|
| 424 |
+
try:
|
| 425 |
+
return next(iter(cls._prefer_valid(cls.discover(name=name))))
|
| 426 |
+
except StopIteration:
|
| 427 |
+
raise PackageNotFoundError(name)
|
| 428 |
+
|
| 429 |
+
@classmethod
|
| 430 |
+
def discover(
|
| 431 |
+
cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
|
| 432 |
+
) -> Iterable[Distribution]:
|
| 433 |
+
"""Return an iterable of Distribution objects for all packages.
|
| 434 |
+
|
| 435 |
+
Pass a ``context`` or pass keyword arguments for constructing
|
| 436 |
+
a context.
|
| 437 |
+
|
| 438 |
+
:context: A ``DistributionFinder.Context`` object.
|
| 439 |
+
:return: Iterable of Distribution objects for packages matching
|
| 440 |
+
the context.
|
| 441 |
+
"""
|
| 442 |
+
if context and kwargs:
|
| 443 |
+
raise ValueError("cannot accept context and kwargs")
|
| 444 |
+
context = context or DistributionFinder.Context(**kwargs)
|
| 445 |
+
return itertools.chain.from_iterable(
|
| 446 |
+
resolver(context) for resolver in cls._discover_resolvers()
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
@staticmethod
|
| 450 |
+
def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]:
|
| 451 |
+
"""
|
| 452 |
+
Prefer (move to the front) distributions that have metadata.
|
| 453 |
+
|
| 454 |
+
Ref python/importlib_resources#489.
|
| 455 |
+
"""
|
| 456 |
+
buckets = bucket(dists, lambda dist: bool(dist.metadata))
|
| 457 |
+
return itertools.chain(buckets[True], buckets[False])
|
| 458 |
+
|
| 459 |
+
@staticmethod
|
| 460 |
+
def at(path: str | os.PathLike[str]) -> Distribution:
|
| 461 |
+
"""Return a Distribution for the indicated metadata path.
|
| 462 |
+
|
| 463 |
+
:param path: a string or path-like object
|
| 464 |
+
:return: a concrete Distribution instance for the path
|
| 465 |
+
"""
|
| 466 |
+
return PathDistribution(pathlib.Path(path))
|
| 467 |
+
|
| 468 |
+
@staticmethod
|
| 469 |
+
def _discover_resolvers():
|
| 470 |
+
"""Search the meta_path for resolvers (MetadataPathFinders)."""
|
| 471 |
+
declared = (
|
| 472 |
+
getattr(finder, 'find_distributions', None) for finder in sys.meta_path
|
| 473 |
+
)
|
| 474 |
+
return filter(None, declared)
|
| 475 |
+
|
| 476 |
+
@property
|
| 477 |
+
def metadata(self) -> _meta.PackageMetadata:
|
| 478 |
+
"""Return the parsed metadata for this Distribution.
|
| 479 |
+
|
| 480 |
+
The returned object will have keys that name the various bits of
|
| 481 |
+
metadata per the
|
| 482 |
+
`Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_.
|
| 483 |
+
|
| 484 |
+
Custom providers may provide the METADATA file or override this
|
| 485 |
+
property.
|
| 486 |
+
"""
|
| 487 |
+
# deferred for performance (python/cpython#109829)
|
| 488 |
+
from . import _adapters
|
| 489 |
+
|
| 490 |
+
opt_text = (
|
| 491 |
+
self.read_text('METADATA')
|
| 492 |
+
or self.read_text('PKG-INFO')
|
| 493 |
+
# This last clause is here to support old egg-info files. Its
|
| 494 |
+
# effect is to just end up using the PathDistribution's self._path
|
| 495 |
+
# (which points to the egg-info file) attribute unchanged.
|
| 496 |
+
or self.read_text('')
|
| 497 |
+
)
|
| 498 |
+
text = cast(str, opt_text)
|
| 499 |
+
return _adapters.Message(email.message_from_string(text))
|
| 500 |
+
|
| 501 |
+
@property
|
| 502 |
+
def name(self) -> str:
|
| 503 |
+
"""Return the 'Name' metadata for the distribution package."""
|
| 504 |
+
return self.metadata['Name']
|
| 505 |
+
|
| 506 |
+
@property
|
| 507 |
+
def _normalized_name(self):
|
| 508 |
+
"""Return a normalized version of the name."""
|
| 509 |
+
return Prepared.normalize(self.name)
|
| 510 |
+
|
| 511 |
+
@property
|
| 512 |
+
def version(self) -> str:
|
| 513 |
+
"""Return the 'Version' metadata for the distribution package."""
|
| 514 |
+
return self.metadata['Version']
|
| 515 |
+
|
| 516 |
+
@property
|
| 517 |
+
def entry_points(self) -> EntryPoints:
|
| 518 |
+
"""
|
| 519 |
+
Return EntryPoints for this distribution.
|
| 520 |
+
|
| 521 |
+
Custom providers may provide the ``entry_points.txt`` file
|
| 522 |
+
or override this property.
|
| 523 |
+
"""
|
| 524 |
+
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
|
| 525 |
+
|
| 526 |
+
@property
|
| 527 |
+
def files(self) -> Optional[List[PackagePath]]:
|
| 528 |
+
"""Files in this distribution.
|
| 529 |
+
|
| 530 |
+
:return: List of PackagePath for this distribution or None
|
| 531 |
+
|
| 532 |
+
Result is `None` if the metadata file that enumerates files
|
| 533 |
+
(i.e. RECORD for dist-info, or installed-files.txt or
|
| 534 |
+
SOURCES.txt for egg-info) is missing.
|
| 535 |
+
Result may be empty if the metadata exists but is empty.
|
| 536 |
+
|
| 537 |
+
Custom providers are recommended to provide a "RECORD" file (in
|
| 538 |
+
``read_text``) or override this property to allow for callers to be
|
| 539 |
+
able to resolve filenames provided by the package.
|
| 540 |
+
"""
|
| 541 |
+
|
| 542 |
+
def make_file(name, hash=None, size_str=None):
|
| 543 |
+
result = PackagePath(name)
|
| 544 |
+
result.hash = FileHash(hash) if hash else None
|
| 545 |
+
result.size = int(size_str) if size_str else None
|
| 546 |
+
result.dist = self
|
| 547 |
+
return result
|
| 548 |
+
|
| 549 |
+
@pass_none
|
| 550 |
+
def make_files(lines):
|
| 551 |
+
# Delay csv import, since Distribution.files is not as widely used
|
| 552 |
+
# as other parts of importlib.metadata
|
| 553 |
+
import csv
|
| 554 |
+
|
| 555 |
+
return starmap(make_file, csv.reader(lines))
|
| 556 |
+
|
| 557 |
+
@pass_none
|
| 558 |
+
def skip_missing_files(package_paths):
|
| 559 |
+
return list(filter(lambda path: path.locate().exists(), package_paths))
|
| 560 |
+
|
| 561 |
+
return skip_missing_files(
|
| 562 |
+
make_files(
|
| 563 |
+
self._read_files_distinfo()
|
| 564 |
+
or self._read_files_egginfo_installed()
|
| 565 |
+
or self._read_files_egginfo_sources()
|
| 566 |
+
)
|
| 567 |
+
)
|
| 568 |
+
|
| 569 |
+
def _read_files_distinfo(self):
|
| 570 |
+
"""
|
| 571 |
+
Read the lines of RECORD.
|
| 572 |
+
"""
|
| 573 |
+
text = self.read_text('RECORD')
|
| 574 |
+
return text and text.splitlines()
|
| 575 |
+
|
| 576 |
+
def _read_files_egginfo_installed(self):
|
| 577 |
+
"""
|
| 578 |
+
Read installed-files.txt and return lines in a similar
|
| 579 |
+
CSV-parsable format as RECORD: each file must be placed
|
| 580 |
+
relative to the site-packages directory and must also be
|
| 581 |
+
quoted (since file names can contain literal commas).
|
| 582 |
+
|
| 583 |
+
This file is written when the package is installed by pip,
|
| 584 |
+
but it might not be written for other installation methods.
|
| 585 |
+
Assume the file is accurate if it exists.
|
| 586 |
+
"""
|
| 587 |
+
text = self.read_text('installed-files.txt')
|
| 588 |
+
# Prepend the .egg-info/ subdir to the lines in this file.
|
| 589 |
+
# But this subdir is only available from PathDistribution's
|
| 590 |
+
# self._path.
|
| 591 |
+
subdir = getattr(self, '_path', None)
|
| 592 |
+
if not text or not subdir:
|
| 593 |
+
return
|
| 594 |
+
|
| 595 |
+
paths = (
|
| 596 |
+
py311.relative_fix((subdir / name).resolve())
|
| 597 |
+
.relative_to(self.locate_file('').resolve(), walk_up=True)
|
| 598 |
+
.as_posix()
|
| 599 |
+
for name in text.splitlines()
|
| 600 |
+
)
|
| 601 |
+
return map('"{}"'.format, paths)
|
| 602 |
+
|
| 603 |
+
def _read_files_egginfo_sources(self):
|
| 604 |
+
"""
|
| 605 |
+
Read SOURCES.txt and return lines in a similar CSV-parsable
|
| 606 |
+
format as RECORD: each file name must be quoted (since it
|
| 607 |
+
might contain literal commas).
|
| 608 |
+
|
| 609 |
+
Note that SOURCES.txt is not a reliable source for what
|
| 610 |
+
files are installed by a package. This file is generated
|
| 611 |
+
for a source archive, and the files that are present
|
| 612 |
+
there (e.g. setup.py) may not correctly reflect the files
|
| 613 |
+
that are present after the package has been installed.
|
| 614 |
+
"""
|
| 615 |
+
text = self.read_text('SOURCES.txt')
|
| 616 |
+
return text and map('"{}"'.format, text.splitlines())
|
| 617 |
+
|
| 618 |
+
@property
|
| 619 |
+
def requires(self) -> Optional[List[str]]:
|
| 620 |
+
"""Generated requirements specified for this Distribution"""
|
| 621 |
+
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
|
| 622 |
+
return reqs and list(reqs)
|
| 623 |
+
|
| 624 |
+
def _read_dist_info_reqs(self):
|
| 625 |
+
return self.metadata.get_all('Requires-Dist')
|
| 626 |
+
|
| 627 |
+
def _read_egg_info_reqs(self):
|
| 628 |
+
source = self.read_text('requires.txt')
|
| 629 |
+
return pass_none(self._deps_from_requires_text)(source)
|
| 630 |
+
|
| 631 |
+
@classmethod
|
| 632 |
+
def _deps_from_requires_text(cls, source):
|
| 633 |
+
return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
|
| 634 |
+
|
| 635 |
+
@staticmethod
|
| 636 |
+
def _convert_egg_info_reqs_to_simple_reqs(sections):
|
| 637 |
+
"""
|
| 638 |
+
Historically, setuptools would solicit and store 'extra'
|
| 639 |
+
requirements, including those with environment markers,
|
| 640 |
+
in separate sections. More modern tools expect each
|
| 641 |
+
dependency to be defined separately, with any relevant
|
| 642 |
+
extras and environment markers attached directly to that
|
| 643 |
+
requirement. This method converts the former to the
|
| 644 |
+
latter. See _test_deps_from_requires_text for an example.
|
| 645 |
+
"""
|
| 646 |
+
|
| 647 |
+
def make_condition(name):
|
| 648 |
+
return name and f'extra == "{name}"'
|
| 649 |
+
|
| 650 |
+
def quoted_marker(section):
|
| 651 |
+
section = section or ''
|
| 652 |
+
extra, sep, markers = section.partition(':')
|
| 653 |
+
if extra and markers:
|
| 654 |
+
markers = f'({markers})'
|
| 655 |
+
conditions = list(filter(None, [markers, make_condition(extra)]))
|
| 656 |
+
return '; ' + ' and '.join(conditions) if conditions else ''
|
| 657 |
+
|
| 658 |
+
def url_req_space(req):
|
| 659 |
+
"""
|
| 660 |
+
PEP 508 requires a space between the url_spec and the quoted_marker.
|
| 661 |
+
Ref python/importlib_metadata#357.
|
| 662 |
+
"""
|
| 663 |
+
# '@' is uniquely indicative of a url_req.
|
| 664 |
+
return ' ' * ('@' in req)
|
| 665 |
+
|
| 666 |
+
for section in sections:
|
| 667 |
+
space = url_req_space(section.value)
|
| 668 |
+
yield section.value + space + quoted_marker(section.name)
|
| 669 |
+
|
| 670 |
+
@property
|
| 671 |
+
def origin(self):
|
| 672 |
+
return self._load_json('direct_url.json')
|
| 673 |
+
|
| 674 |
+
def _load_json(self, filename):
|
| 675 |
+
# Deferred for performance (python/importlib_metadata#503)
|
| 676 |
+
import json
|
| 677 |
+
|
| 678 |
+
return pass_none(json.loads)(
|
| 679 |
+
self.read_text(filename),
|
| 680 |
+
object_hook=lambda data: types.SimpleNamespace(**data),
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
class DistributionFinder(MetaPathFinder):
|
| 685 |
+
"""
|
| 686 |
+
A MetaPathFinder capable of discovering installed distributions.
|
| 687 |
+
|
| 688 |
+
Custom providers should implement this interface in order to
|
| 689 |
+
supply metadata.
|
| 690 |
+
"""
|
| 691 |
+
|
| 692 |
+
class Context:
|
| 693 |
+
"""
|
| 694 |
+
Keyword arguments presented by the caller to
|
| 695 |
+
``distributions()`` or ``Distribution.discover()``
|
| 696 |
+
to narrow the scope of a search for distributions
|
| 697 |
+
in all DistributionFinders.
|
| 698 |
+
|
| 699 |
+
Each DistributionFinder may expect any parameters
|
| 700 |
+
and should attempt to honor the canonical
|
| 701 |
+
parameters defined below when appropriate.
|
| 702 |
+
|
| 703 |
+
This mechanism gives a custom provider a means to
|
| 704 |
+
solicit additional details from the caller beyond
|
| 705 |
+
"name" and "path" when searching distributions.
|
| 706 |
+
For example, imagine a provider that exposes suites
|
| 707 |
+
of packages in either a "public" or "private" ``realm``.
|
| 708 |
+
A caller may wish to query only for distributions in
|
| 709 |
+
a particular realm and could call
|
| 710 |
+
``distributions(realm="private")`` to signal to the
|
| 711 |
+
custom provider to only include distributions from that
|
| 712 |
+
realm.
|
| 713 |
+
"""
|
| 714 |
+
|
| 715 |
+
name = None
|
| 716 |
+
"""
|
| 717 |
+
Specific name for which a distribution finder should match.
|
| 718 |
+
A name of ``None`` matches all distributions.
|
| 719 |
+
"""
|
| 720 |
+
|
| 721 |
+
def __init__(self, **kwargs):
|
| 722 |
+
vars(self).update(kwargs)
|
| 723 |
+
|
| 724 |
+
@property
|
| 725 |
+
def path(self) -> List[str]:
|
| 726 |
+
"""
|
| 727 |
+
The sequence of directory path that a distribution finder
|
| 728 |
+
should search.
|
| 729 |
+
|
| 730 |
+
Typically refers to Python installed package paths such as
|
| 731 |
+
"site-packages" directories and defaults to ``sys.path``.
|
| 732 |
+
"""
|
| 733 |
+
return vars(self).get('path', sys.path)
|
| 734 |
+
|
| 735 |
+
@abc.abstractmethod
|
| 736 |
+
def find_distributions(self, context=Context()) -> Iterable[Distribution]:
|
| 737 |
+
"""
|
| 738 |
+
Find distributions.
|
| 739 |
+
|
| 740 |
+
Return an iterable of all Distribution instances capable of
|
| 741 |
+
loading the metadata for packages matching the ``context``,
|
| 742 |
+
a DistributionFinder.Context instance.
|
| 743 |
+
"""
|
| 744 |
+
|
| 745 |
+
|
| 746 |
+
class FastPath:
|
| 747 |
+
"""
|
| 748 |
+
Micro-optimized class for searching a root for children.
|
| 749 |
+
|
| 750 |
+
Root is a path on the file system that may contain metadata
|
| 751 |
+
directories either as natural directories or within a zip file.
|
| 752 |
+
|
| 753 |
+
>>> FastPath('').children()
|
| 754 |
+
['...']
|
| 755 |
+
|
| 756 |
+
FastPath objects are cached and recycled for any given root.
|
| 757 |
+
|
| 758 |
+
>>> FastPath('foobar') is FastPath('foobar')
|
| 759 |
+
True
|
| 760 |
+
"""
|
| 761 |
+
|
| 762 |
+
@functools.lru_cache() # type: ignore[misc]
|
| 763 |
+
def __new__(cls, root):
|
| 764 |
+
return super().__new__(cls)
|
| 765 |
+
|
| 766 |
+
def __init__(self, root):
|
| 767 |
+
self.root = root
|
| 768 |
+
|
| 769 |
+
def joinpath(self, child):
|
| 770 |
+
return pathlib.Path(self.root, child)
|
| 771 |
+
|
| 772 |
+
def children(self):
|
| 773 |
+
with suppress(Exception):
|
| 774 |
+
return os.listdir(self.root or '.')
|
| 775 |
+
with suppress(Exception):
|
| 776 |
+
return self.zip_children()
|
| 777 |
+
return []
|
| 778 |
+
|
| 779 |
+
def zip_children(self):
|
| 780 |
+
# deferred for performance (python/importlib_metadata#502)
|
| 781 |
+
from zipp.compat.overlay import zipfile
|
| 782 |
+
|
| 783 |
+
zip_path = zipfile.Path(self.root)
|
| 784 |
+
names = zip_path.root.namelist()
|
| 785 |
+
self.joinpath = zip_path.joinpath
|
| 786 |
+
|
| 787 |
+
return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
|
| 788 |
+
|
| 789 |
+
def search(self, name):
|
| 790 |
+
return self.lookup(self.mtime).search(name)
|
| 791 |
+
|
| 792 |
+
@property
|
| 793 |
+
def mtime(self):
|
| 794 |
+
with suppress(OSError):
|
| 795 |
+
return os.stat(self.root).st_mtime
|
| 796 |
+
self.lookup.cache_clear()
|
| 797 |
+
|
| 798 |
+
@method_cache
|
| 799 |
+
def lookup(self, mtime):
|
| 800 |
+
return Lookup(self)
|
| 801 |
+
|
| 802 |
+
|
| 803 |
+
class Lookup:
|
| 804 |
+
"""
|
| 805 |
+
A micro-optimized class for searching a (fast) path for metadata.
|
| 806 |
+
"""
|
| 807 |
+
|
| 808 |
+
def __init__(self, path: FastPath):
|
| 809 |
+
"""
|
| 810 |
+
Calculate all of the children representing metadata.
|
| 811 |
+
|
| 812 |
+
From the children in the path, calculate early all of the
|
| 813 |
+
children that appear to represent metadata (infos) or legacy
|
| 814 |
+
metadata (eggs).
|
| 815 |
+
"""
|
| 816 |
+
|
| 817 |
+
base = os.path.basename(path.root).lower()
|
| 818 |
+
base_is_egg = base.endswith(".egg")
|
| 819 |
+
self.infos = FreezableDefaultDict(list)
|
| 820 |
+
self.eggs = FreezableDefaultDict(list)
|
| 821 |
+
|
| 822 |
+
for child in path.children():
|
| 823 |
+
low = child.lower()
|
| 824 |
+
if low.endswith((".dist-info", ".egg-info")):
|
| 825 |
+
# rpartition is faster than splitext and suitable for this purpose.
|
| 826 |
+
name = low.rpartition(".")[0].partition("-")[0]
|
| 827 |
+
normalized = Prepared.normalize(name)
|
| 828 |
+
self.infos[normalized].append(path.joinpath(child))
|
| 829 |
+
elif base_is_egg and low == "egg-info":
|
| 830 |
+
name = base.rpartition(".")[0].partition("-")[0]
|
| 831 |
+
legacy_normalized = Prepared.legacy_normalize(name)
|
| 832 |
+
self.eggs[legacy_normalized].append(path.joinpath(child))
|
| 833 |
+
|
| 834 |
+
self.infos.freeze()
|
| 835 |
+
self.eggs.freeze()
|
| 836 |
+
|
| 837 |
+
def search(self, prepared: Prepared):
|
| 838 |
+
"""
|
| 839 |
+
Yield all infos and eggs matching the Prepared query.
|
| 840 |
+
"""
|
| 841 |
+
infos = (
|
| 842 |
+
self.infos[prepared.normalized]
|
| 843 |
+
if prepared
|
| 844 |
+
else itertools.chain.from_iterable(self.infos.values())
|
| 845 |
+
)
|
| 846 |
+
eggs = (
|
| 847 |
+
self.eggs[prepared.legacy_normalized]
|
| 848 |
+
if prepared
|
| 849 |
+
else itertools.chain.from_iterable(self.eggs.values())
|
| 850 |
+
)
|
| 851 |
+
return itertools.chain(infos, eggs)
|
| 852 |
+
|
| 853 |
+
|
| 854 |
+
class Prepared:
|
| 855 |
+
"""
|
| 856 |
+
A prepared search query for metadata on a possibly-named package.
|
| 857 |
+
|
| 858 |
+
Pre-calculates the normalization to prevent repeated operations.
|
| 859 |
+
|
| 860 |
+
>>> none = Prepared(None)
|
| 861 |
+
>>> none.normalized
|
| 862 |
+
>>> none.legacy_normalized
|
| 863 |
+
>>> bool(none)
|
| 864 |
+
False
|
| 865 |
+
>>> sample = Prepared('Sample__Pkg-name.foo')
|
| 866 |
+
>>> sample.normalized
|
| 867 |
+
'sample_pkg_name_foo'
|
| 868 |
+
>>> sample.legacy_normalized
|
| 869 |
+
'sample__pkg_name.foo'
|
| 870 |
+
>>> bool(sample)
|
| 871 |
+
True
|
| 872 |
+
"""
|
| 873 |
+
|
| 874 |
+
normalized = None
|
| 875 |
+
legacy_normalized = None
|
| 876 |
+
|
| 877 |
+
def __init__(self, name: Optional[str]):
|
| 878 |
+
self.name = name
|
| 879 |
+
if name is None:
|
| 880 |
+
return
|
| 881 |
+
self.normalized = self.normalize(name)
|
| 882 |
+
self.legacy_normalized = self.legacy_normalize(name)
|
| 883 |
+
|
| 884 |
+
@staticmethod
|
| 885 |
+
def normalize(name):
|
| 886 |
+
"""
|
| 887 |
+
PEP 503 normalization plus dashes as underscores.
|
| 888 |
+
"""
|
| 889 |
+
return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
|
| 890 |
+
|
| 891 |
+
@staticmethod
|
| 892 |
+
def legacy_normalize(name):
|
| 893 |
+
"""
|
| 894 |
+
Normalize the package name as found in the convention in
|
| 895 |
+
older packaging tools versions and specs.
|
| 896 |
+
"""
|
| 897 |
+
return name.lower().replace('-', '_')
|
| 898 |
+
|
| 899 |
+
def __bool__(self):
|
| 900 |
+
return bool(self.name)
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
@install
|
| 904 |
+
class MetadataPathFinder(NullFinder, DistributionFinder):
|
| 905 |
+
"""A degenerate finder for distribution packages on the file system.
|
| 906 |
+
|
| 907 |
+
This finder supplies only a find_distributions() method for versions
|
| 908 |
+
of Python that do not have a PathFinder find_distributions().
|
| 909 |
+
"""
|
| 910 |
+
|
| 911 |
+
@classmethod
|
| 912 |
+
def find_distributions(
|
| 913 |
+
cls, context=DistributionFinder.Context()
|
| 914 |
+
) -> Iterable[PathDistribution]:
|
| 915 |
+
"""
|
| 916 |
+
Find distributions.
|
| 917 |
+
|
| 918 |
+
Return an iterable of all Distribution instances capable of
|
| 919 |
+
loading the metadata for packages matching ``context.name``
|
| 920 |
+
(or all names if ``None`` indicated) along the paths in the list
|
| 921 |
+
of directories ``context.path``.
|
| 922 |
+
"""
|
| 923 |
+
found = cls._search_paths(context.name, context.path)
|
| 924 |
+
return map(PathDistribution, found)
|
| 925 |
+
|
| 926 |
+
@classmethod
|
| 927 |
+
def _search_paths(cls, name, paths):
|
| 928 |
+
"""Find metadata directories in paths heuristically."""
|
| 929 |
+
prepared = Prepared(name)
|
| 930 |
+
return itertools.chain.from_iterable(
|
| 931 |
+
path.search(prepared) for path in map(FastPath, paths)
|
| 932 |
+
)
|
| 933 |
+
|
| 934 |
+
@classmethod
|
| 935 |
+
def invalidate_caches(cls) -> None:
|
| 936 |
+
FastPath.__new__.cache_clear()
|
| 937 |
+
|
| 938 |
+
|
| 939 |
+
class PathDistribution(Distribution):
|
| 940 |
+
def __init__(self, path: SimplePath) -> None:
|
| 941 |
+
"""Construct a distribution.
|
| 942 |
+
|
| 943 |
+
:param path: SimplePath indicating the metadata directory.
|
| 944 |
+
"""
|
| 945 |
+
self._path = path
|
| 946 |
+
|
| 947 |
+
def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
|
| 948 |
+
with suppress(
|
| 949 |
+
FileNotFoundError,
|
| 950 |
+
IsADirectoryError,
|
| 951 |
+
KeyError,
|
| 952 |
+
NotADirectoryError,
|
| 953 |
+
PermissionError,
|
| 954 |
+
):
|
| 955 |
+
return self._path.joinpath(filename).read_text(encoding='utf-8')
|
| 956 |
+
|
| 957 |
+
return None
|
| 958 |
+
|
| 959 |
+
read_text.__doc__ = Distribution.read_text.__doc__
|
| 960 |
+
|
| 961 |
+
def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
|
| 962 |
+
return self._path.parent / path
|
| 963 |
+
|
| 964 |
+
@property
|
| 965 |
+
def _normalized_name(self):
|
| 966 |
+
"""
|
| 967 |
+
Performance optimization: where possible, resolve the
|
| 968 |
+
normalized name from the file system path.
|
| 969 |
+
"""
|
| 970 |
+
stem = os.path.basename(str(self._path))
|
| 971 |
+
return (
|
| 972 |
+
pass_none(Prepared.normalize)(self._name_from_stem(stem))
|
| 973 |
+
or super()._normalized_name
|
| 974 |
+
)
|
| 975 |
+
|
| 976 |
+
@staticmethod
|
| 977 |
+
def _name_from_stem(stem):
|
| 978 |
+
"""
|
| 979 |
+
>>> PathDistribution._name_from_stem('foo-3.0.egg-info')
|
| 980 |
+
'foo'
|
| 981 |
+
>>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
|
| 982 |
+
'CherryPy'
|
| 983 |
+
>>> PathDistribution._name_from_stem('face.egg-info')
|
| 984 |
+
'face'
|
| 985 |
+
>>> PathDistribution._name_from_stem('foo.bar')
|
| 986 |
+
"""
|
| 987 |
+
filename, ext = os.path.splitext(stem)
|
| 988 |
+
if ext not in ('.dist-info', '.egg-info'):
|
| 989 |
+
return
|
| 990 |
+
name, sep, rest = filename.partition('-')
|
| 991 |
+
return name
|
| 992 |
+
|
| 993 |
+
|
| 994 |
+
def distribution(distribution_name: str) -> Distribution:
|
| 995 |
+
"""Get the ``Distribution`` instance for the named package.
|
| 996 |
+
|
| 997 |
+
:param distribution_name: The name of the distribution package as a string.
|
| 998 |
+
:return: A ``Distribution`` instance (or subclass thereof).
|
| 999 |
+
"""
|
| 1000 |
+
return Distribution.from_name(distribution_name)
|
| 1001 |
+
|
| 1002 |
+
|
| 1003 |
+
def distributions(**kwargs) -> Iterable[Distribution]:
|
| 1004 |
+
"""Get all ``Distribution`` instances in the current environment.
|
| 1005 |
+
|
| 1006 |
+
:return: An iterable of ``Distribution`` instances.
|
| 1007 |
+
"""
|
| 1008 |
+
return Distribution.discover(**kwargs)
|
| 1009 |
+
|
| 1010 |
+
|
| 1011 |
+
def metadata(distribution_name: str) -> _meta.PackageMetadata:
|
| 1012 |
+
"""Get the metadata for the named package.
|
| 1013 |
+
|
| 1014 |
+
:param distribution_name: The name of the distribution package to query.
|
| 1015 |
+
:return: A PackageMetadata containing the parsed metadata.
|
| 1016 |
+
"""
|
| 1017 |
+
return Distribution.from_name(distribution_name).metadata
|
| 1018 |
+
|
| 1019 |
+
|
| 1020 |
+
def version(distribution_name: str) -> str:
|
| 1021 |
+
"""Get the version string for the named package.
|
| 1022 |
+
|
| 1023 |
+
:param distribution_name: The name of the distribution package to query.
|
| 1024 |
+
:return: The version string for the package as defined in the package's
|
| 1025 |
+
"Version" metadata key.
|
| 1026 |
+
"""
|
| 1027 |
+
return distribution(distribution_name).version
|
| 1028 |
+
|
| 1029 |
+
|
| 1030 |
+
_unique = functools.partial(
|
| 1031 |
+
unique_everseen,
|
| 1032 |
+
key=py39.normalized_name,
|
| 1033 |
+
)
|
| 1034 |
+
"""
|
| 1035 |
+
Wrapper for ``distributions`` to return unique distributions by name.
|
| 1036 |
+
"""
|
| 1037 |
+
|
| 1038 |
+
|
| 1039 |
+
def entry_points(**params) -> EntryPoints:
|
| 1040 |
+
"""Return EntryPoint objects for all installed packages.
|
| 1041 |
+
|
| 1042 |
+
Pass selection parameters (group or name) to filter the
|
| 1043 |
+
result to entry points matching those properties (see
|
| 1044 |
+
EntryPoints.select()).
|
| 1045 |
+
|
| 1046 |
+
:return: EntryPoints for all installed packages.
|
| 1047 |
+
"""
|
| 1048 |
+
eps = itertools.chain.from_iterable(
|
| 1049 |
+
dist.entry_points for dist in _unique(distributions())
|
| 1050 |
+
)
|
| 1051 |
+
return EntryPoints(eps).select(**params)
|
| 1052 |
+
|
| 1053 |
+
|
| 1054 |
+
def files(distribution_name: str) -> Optional[List[PackagePath]]:
|
| 1055 |
+
"""Return a list of files for the named package.
|
| 1056 |
+
|
| 1057 |
+
:param distribution_name: The name of the distribution package to query.
|
| 1058 |
+
:return: List of files composing the distribution.
|
| 1059 |
+
"""
|
| 1060 |
+
return distribution(distribution_name).files
|
| 1061 |
+
|
| 1062 |
+
|
| 1063 |
+
def requires(distribution_name: str) -> Optional[List[str]]:
|
| 1064 |
+
"""
|
| 1065 |
+
Return a list of requirements for the named package.
|
| 1066 |
+
|
| 1067 |
+
:return: An iterable of requirements, suitable for
|
| 1068 |
+
packaging.requirement.Requirement.
|
| 1069 |
+
"""
|
| 1070 |
+
return distribution(distribution_name).requires
|
| 1071 |
+
|
| 1072 |
+
|
| 1073 |
+
def packages_distributions() -> Mapping[str, List[str]]:
|
| 1074 |
+
"""
|
| 1075 |
+
Return a mapping of top-level packages to their
|
| 1076 |
+
distributions.
|
| 1077 |
+
|
| 1078 |
+
>>> import collections.abc
|
| 1079 |
+
>>> pkgs = packages_distributions()
|
| 1080 |
+
>>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
|
| 1081 |
+
True
|
| 1082 |
+
"""
|
| 1083 |
+
pkg_to_dist = collections.defaultdict(list)
|
| 1084 |
+
for dist in distributions():
|
| 1085 |
+
for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
|
| 1086 |
+
pkg_to_dist[pkg].append(dist.metadata['Name'])
|
| 1087 |
+
return dict(pkg_to_dist)
|
| 1088 |
+
|
| 1089 |
+
|
| 1090 |
+
def _top_level_declared(dist):
|
| 1091 |
+
return (dist.read_text('top_level.txt') or '').split()
|
| 1092 |
+
|
| 1093 |
+
|
| 1094 |
+
def _topmost(name: PackagePath) -> Optional[str]:
|
| 1095 |
+
"""
|
| 1096 |
+
Return the top-most parent as long as there is a parent.
|
| 1097 |
+
"""
|
| 1098 |
+
top, *rest = name.parts
|
| 1099 |
+
return top if rest else None
|
| 1100 |
+
|
| 1101 |
+
|
| 1102 |
+
def _get_toplevel_name(name: PackagePath) -> str:
|
| 1103 |
+
"""
|
| 1104 |
+
Infer a possibly importable module name from a name presumed on
|
| 1105 |
+
sys.path.
|
| 1106 |
+
|
| 1107 |
+
>>> _get_toplevel_name(PackagePath('foo.py'))
|
| 1108 |
+
'foo'
|
| 1109 |
+
>>> _get_toplevel_name(PackagePath('foo'))
|
| 1110 |
+
'foo'
|
| 1111 |
+
>>> _get_toplevel_name(PackagePath('foo.pyc'))
|
| 1112 |
+
'foo'
|
| 1113 |
+
>>> _get_toplevel_name(PackagePath('foo/__init__.py'))
|
| 1114 |
+
'foo'
|
| 1115 |
+
>>> _get_toplevel_name(PackagePath('foo.pth'))
|
| 1116 |
+
'foo.pth'
|
| 1117 |
+
>>> _get_toplevel_name(PackagePath('foo.dist-info'))
|
| 1118 |
+
'foo.dist-info'
|
| 1119 |
+
"""
|
| 1120 |
+
# Defer import of inspect for performance (python/cpython#118761)
|
| 1121 |
+
import inspect
|
| 1122 |
+
|
| 1123 |
+
return _topmost(name) or inspect.getmodulename(name) or str(name)
|
| 1124 |
+
|
| 1125 |
+
|
| 1126 |
+
def _top_level_inferred(dist):
|
| 1127 |
+
opt_names = set(map(_get_toplevel_name, always_iterable(dist.files)))
|
| 1128 |
+
|
| 1129 |
+
def importable_name(name):
|
| 1130 |
+
return '.' not in name
|
| 1131 |
+
|
| 1132 |
+
return filter(importable_name, opt_names)
|
.venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (61.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_adapters.cpython-311.pyc
ADDED
|
Binary file (6.78 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_collections.cpython-311.pyc
ADDED
|
Binary file (2.17 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_compat.cpython-311.pyc
ADDED
|
Binary file (2.46 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_functools.cpython-311.pyc
ADDED
|
Binary file (3.61 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_itertools.cpython-311.pyc
ADDED
|
Binary file (7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/_meta.cpython-311.pyc
ADDED
|
Binary file (4.11 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/importlib_metadata/__pycache__/diagnose.cpython-311.pyc
ADDED
|
Binary file (1.37 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/importlib_metadata/_adapters.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import email.message
|
| 2 |
+
import email.policy
|
| 3 |
+
import re
|
| 4 |
+
import textwrap
|
| 5 |
+
|
| 6 |
+
from ._text import FoldedCase
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class RawPolicy(email.policy.EmailPolicy):
|
| 10 |
+
def fold(self, name, value):
|
| 11 |
+
folded = self.linesep.join(
|
| 12 |
+
textwrap.indent(value, prefix=' ' * 8, predicate=lambda line: True)
|
| 13 |
+
.lstrip()
|
| 14 |
+
.splitlines()
|
| 15 |
+
)
|
| 16 |
+
return f'{name}: {folded}{self.linesep}'
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Message(email.message.Message):
|
| 20 |
+
r"""
|
| 21 |
+
Specialized Message subclass to handle metadata naturally.
|
| 22 |
+
|
| 23 |
+
Reads values that may have newlines in them and converts the
|
| 24 |
+
payload to the Description.
|
| 25 |
+
|
| 26 |
+
>>> msg_text = textwrap.dedent('''
|
| 27 |
+
... Name: Foo
|
| 28 |
+
... Version: 3.0
|
| 29 |
+
... License: blah
|
| 30 |
+
... de-blah
|
| 31 |
+
... <BLANKLINE>
|
| 32 |
+
... First line of description.
|
| 33 |
+
... Second line of description.
|
| 34 |
+
... <BLANKLINE>
|
| 35 |
+
... Fourth line!
|
| 36 |
+
... ''').lstrip().replace('<BLANKLINE>', '')
|
| 37 |
+
>>> msg = Message(email.message_from_string(msg_text))
|
| 38 |
+
>>> msg['Description']
|
| 39 |
+
'First line of description.\nSecond line of description.\n\nFourth line!\n'
|
| 40 |
+
|
| 41 |
+
Message should render even if values contain newlines.
|
| 42 |
+
|
| 43 |
+
>>> print(msg)
|
| 44 |
+
Name: Foo
|
| 45 |
+
Version: 3.0
|
| 46 |
+
License: blah
|
| 47 |
+
de-blah
|
| 48 |
+
Description: First line of description.
|
| 49 |
+
Second line of description.
|
| 50 |
+
<BLANKLINE>
|
| 51 |
+
Fourth line!
|
| 52 |
+
<BLANKLINE>
|
| 53 |
+
<BLANKLINE>
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
multiple_use_keys = set(
|
| 57 |
+
map(
|
| 58 |
+
FoldedCase,
|
| 59 |
+
[
|
| 60 |
+
'Classifier',
|
| 61 |
+
'Obsoletes-Dist',
|
| 62 |
+
'Platform',
|
| 63 |
+
'Project-URL',
|
| 64 |
+
'Provides-Dist',
|
| 65 |
+
'Provides-Extra',
|
| 66 |
+
'Requires-Dist',
|
| 67 |
+
'Requires-External',
|
| 68 |
+
'Supported-Platform',
|
| 69 |
+
'Dynamic',
|
| 70 |
+
],
|
| 71 |
+
)
|
| 72 |
+
)
|
| 73 |
+
"""
|
| 74 |
+
Keys that may be indicated multiple times per PEP 566.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
def __new__(cls, orig: email.message.Message):
|
| 78 |
+
res = super().__new__(cls)
|
| 79 |
+
vars(res).update(vars(orig))
|
| 80 |
+
return res
|
| 81 |
+
|
| 82 |
+
def __init__(self, *args, **kwargs):
|
| 83 |
+
self._headers = self._repair_headers()
|
| 84 |
+
|
| 85 |
+
# suppress spurious error from mypy
|
| 86 |
+
def __iter__(self):
|
| 87 |
+
return super().__iter__()
|
| 88 |
+
|
| 89 |
+
def __getitem__(self, item):
|
| 90 |
+
"""
|
| 91 |
+
Override parent behavior to typical dict behavior.
|
| 92 |
+
|
| 93 |
+
``email.message.Message`` will emit None values for missing
|
| 94 |
+
keys. Typical mappings, including this ``Message``, will raise
|
| 95 |
+
a key error for missing keys.
|
| 96 |
+
|
| 97 |
+
Ref python/importlib_metadata#371.
|
| 98 |
+
"""
|
| 99 |
+
res = super().__getitem__(item)
|
| 100 |
+
if res is None:
|
| 101 |
+
raise KeyError(item)
|
| 102 |
+
return res
|
| 103 |
+
|
| 104 |
+
def _repair_headers(self):
|
| 105 |
+
def redent(value):
|
| 106 |
+
"Correct for RFC822 indentation"
|
| 107 |
+
indent = ' ' * 8
|
| 108 |
+
if not value or '\n' + indent not in value:
|
| 109 |
+
return value
|
| 110 |
+
return textwrap.dedent(indent + value)
|
| 111 |
+
|
| 112 |
+
headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
|
| 113 |
+
if self._payload:
|
| 114 |
+
headers.append(('Description', self.get_payload()))
|
| 115 |
+
self.set_payload('')
|
| 116 |
+
return headers
|
| 117 |
+
|
| 118 |
+
def as_string(self):
|
| 119 |
+
return super().as_string(policy=RawPolicy())
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def json(self):
|
| 123 |
+
"""
|
| 124 |
+
Convert PackageMetadata to a JSON-compatible format
|
| 125 |
+
per PEP 0566.
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
def transform(key):
|
| 129 |
+
value = self.get_all(key) if key in self.multiple_use_keys else self[key]
|
| 130 |
+
if key == 'Keywords':
|
| 131 |
+
value = re.split(r'\s+', value)
|
| 132 |
+
tk = key.lower().replace('-', '_')
|
| 133 |
+
return tk, value
|
| 134 |
+
|
| 135 |
+
return dict(map(transform, map(FoldedCase, self)))
|
.venv/lib/python3.11/site-packages/importlib_metadata/_collections.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
# from jaraco.collections 3.3
|
| 5 |
+
class FreezableDefaultDict(collections.defaultdict):
|
| 6 |
+
"""
|
| 7 |
+
Often it is desirable to prevent the mutation of
|
| 8 |
+
a default dict after its initial construction, such
|
| 9 |
+
as to prevent mutation during iteration.
|
| 10 |
+
|
| 11 |
+
>>> dd = FreezableDefaultDict(list)
|
| 12 |
+
>>> dd[0].append('1')
|
| 13 |
+
>>> dd.freeze()
|
| 14 |
+
>>> dd[1]
|
| 15 |
+
[]
|
| 16 |
+
>>> len(dd)
|
| 17 |
+
1
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __missing__(self, key):
|
| 21 |
+
return getattr(self, '_frozen', super().__missing__)(key)
|
| 22 |
+
|
| 23 |
+
def freeze(self):
|
| 24 |
+
self._frozen = lambda key: self.default_factory()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class Pair(collections.namedtuple('Pair', 'name value')):
|
| 28 |
+
@classmethod
|
| 29 |
+
def parse(cls, text):
|
| 30 |
+
return cls(*map(str.strip, text.split("=", 1)))
|
.venv/lib/python3.11/site-packages/importlib_metadata/_compat.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import platform
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
__all__ = ['install', 'NullFinder']
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def install(cls):
|
| 8 |
+
"""
|
| 9 |
+
Class decorator for installation on sys.meta_path.
|
| 10 |
+
|
| 11 |
+
Adds the backport DistributionFinder to sys.meta_path and
|
| 12 |
+
attempts to disable the finder functionality of the stdlib
|
| 13 |
+
DistributionFinder.
|
| 14 |
+
"""
|
| 15 |
+
sys.meta_path.append(cls())
|
| 16 |
+
disable_stdlib_finder()
|
| 17 |
+
return cls
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def disable_stdlib_finder():
|
| 21 |
+
"""
|
| 22 |
+
Give the backport primacy for discovering path-based distributions
|
| 23 |
+
by monkey-patching the stdlib O_O.
|
| 24 |
+
|
| 25 |
+
See #91 for more background for rationale on this sketchy
|
| 26 |
+
behavior.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def matches(finder):
|
| 30 |
+
return getattr(
|
| 31 |
+
finder, '__module__', None
|
| 32 |
+
) == '_frozen_importlib_external' and hasattr(finder, 'find_distributions')
|
| 33 |
+
|
| 34 |
+
for finder in filter(matches, sys.meta_path): # pragma: nocover
|
| 35 |
+
del finder.find_distributions
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class NullFinder:
|
| 39 |
+
"""
|
| 40 |
+
A "Finder" (aka "MetaPathFinder") that never finds any modules,
|
| 41 |
+
but may find distributions.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
@staticmethod
|
| 45 |
+
def find_spec(*args, **kwargs):
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def pypy_partial(val):
|
| 50 |
+
"""
|
| 51 |
+
Adjust for variable stacklevel on partial under PyPy.
|
| 52 |
+
|
| 53 |
+
Workaround for #327.
|
| 54 |
+
"""
|
| 55 |
+
is_pypy = platform.python_implementation() == 'PyPy'
|
| 56 |
+
return val + is_pypy
|
.venv/lib/python3.11/site-packages/importlib_metadata/_functools.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import types
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
# from jaraco.functools 3.3
|
| 6 |
+
def method_cache(method, cache_wrapper=None):
|
| 7 |
+
"""
|
| 8 |
+
Wrap lru_cache to support storing the cache data in the object instances.
|
| 9 |
+
|
| 10 |
+
Abstracts the common paradigm where the method explicitly saves an
|
| 11 |
+
underscore-prefixed protected property on first call and returns that
|
| 12 |
+
subsequently.
|
| 13 |
+
|
| 14 |
+
>>> class MyClass:
|
| 15 |
+
... calls = 0
|
| 16 |
+
...
|
| 17 |
+
... @method_cache
|
| 18 |
+
... def method(self, value):
|
| 19 |
+
... self.calls += 1
|
| 20 |
+
... return value
|
| 21 |
+
|
| 22 |
+
>>> a = MyClass()
|
| 23 |
+
>>> a.method(3)
|
| 24 |
+
3
|
| 25 |
+
>>> for x in range(75):
|
| 26 |
+
... res = a.method(x)
|
| 27 |
+
>>> a.calls
|
| 28 |
+
75
|
| 29 |
+
|
| 30 |
+
Note that the apparent behavior will be exactly like that of lru_cache
|
| 31 |
+
except that the cache is stored on each instance, so values in one
|
| 32 |
+
instance will not flush values from another, and when an instance is
|
| 33 |
+
deleted, so are the cached values for that instance.
|
| 34 |
+
|
| 35 |
+
>>> b = MyClass()
|
| 36 |
+
>>> for x in range(35):
|
| 37 |
+
... res = b.method(x)
|
| 38 |
+
>>> b.calls
|
| 39 |
+
35
|
| 40 |
+
>>> a.method(0)
|
| 41 |
+
0
|
| 42 |
+
>>> a.calls
|
| 43 |
+
75
|
| 44 |
+
|
| 45 |
+
Note that if method had been decorated with ``functools.lru_cache()``,
|
| 46 |
+
a.calls would have been 76 (due to the cached value of 0 having been
|
| 47 |
+
flushed by the 'b' instance).
|
| 48 |
+
|
| 49 |
+
Clear the cache with ``.cache_clear()``
|
| 50 |
+
|
| 51 |
+
>>> a.method.cache_clear()
|
| 52 |
+
|
| 53 |
+
Same for a method that hasn't yet been called.
|
| 54 |
+
|
| 55 |
+
>>> c = MyClass()
|
| 56 |
+
>>> c.method.cache_clear()
|
| 57 |
+
|
| 58 |
+
Another cache wrapper may be supplied:
|
| 59 |
+
|
| 60 |
+
>>> cache = functools.lru_cache(maxsize=2)
|
| 61 |
+
>>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
|
| 62 |
+
>>> a = MyClass()
|
| 63 |
+
>>> a.method2()
|
| 64 |
+
3
|
| 65 |
+
|
| 66 |
+
Caution - do not subsequently wrap the method with another decorator, such
|
| 67 |
+
as ``@property``, which changes the semantics of the function.
|
| 68 |
+
|
| 69 |
+
See also
|
| 70 |
+
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
|
| 71 |
+
for another implementation and additional justification.
|
| 72 |
+
"""
|
| 73 |
+
cache_wrapper = cache_wrapper or functools.lru_cache()
|
| 74 |
+
|
| 75 |
+
def wrapper(self, *args, **kwargs):
|
| 76 |
+
# it's the first call, replace the method with a cached, bound method
|
| 77 |
+
bound_method = types.MethodType(method, self)
|
| 78 |
+
cached_method = cache_wrapper(bound_method)
|
| 79 |
+
setattr(self, method.__name__, cached_method)
|
| 80 |
+
return cached_method(*args, **kwargs)
|
| 81 |
+
|
| 82 |
+
# Support cache clear even before cache has been created.
|
| 83 |
+
wrapper.cache_clear = lambda: None
|
| 84 |
+
|
| 85 |
+
return wrapper
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# From jaraco.functools 3.3
|
| 89 |
+
def pass_none(func):
|
| 90 |
+
"""
|
| 91 |
+
Wrap func so it's not called if its first param is None
|
| 92 |
+
|
| 93 |
+
>>> print_text = pass_none(print)
|
| 94 |
+
>>> print_text('text')
|
| 95 |
+
text
|
| 96 |
+
>>> print_text(None)
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
@functools.wraps(func)
|
| 100 |
+
def wrapper(param, *args, **kwargs):
|
| 101 |
+
if param is not None:
|
| 102 |
+
return func(param, *args, **kwargs)
|
| 103 |
+
|
| 104 |
+
return wrapper
|
.venv/lib/python3.11/site-packages/importlib_metadata/_itertools.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import defaultdict, deque
|
| 2 |
+
from itertools import filterfalse
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def unique_everseen(iterable, key=None):
|
| 6 |
+
"List unique elements, preserving order. Remember all elements ever seen."
|
| 7 |
+
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
| 8 |
+
# unique_everseen('ABBCcAD', str.lower) --> A B C D
|
| 9 |
+
seen = set()
|
| 10 |
+
seen_add = seen.add
|
| 11 |
+
if key is None:
|
| 12 |
+
for element in filterfalse(seen.__contains__, iterable):
|
| 13 |
+
seen_add(element)
|
| 14 |
+
yield element
|
| 15 |
+
else:
|
| 16 |
+
for element in iterable:
|
| 17 |
+
k = key(element)
|
| 18 |
+
if k not in seen:
|
| 19 |
+
seen_add(k)
|
| 20 |
+
yield element
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# copied from more_itertools 8.8
|
| 24 |
+
def always_iterable(obj, base_type=(str, bytes)):
|
| 25 |
+
"""If *obj* is iterable, return an iterator over its items::
|
| 26 |
+
|
| 27 |
+
>>> obj = (1, 2, 3)
|
| 28 |
+
>>> list(always_iterable(obj))
|
| 29 |
+
[1, 2, 3]
|
| 30 |
+
|
| 31 |
+
If *obj* is not iterable, return a one-item iterable containing *obj*::
|
| 32 |
+
|
| 33 |
+
>>> obj = 1
|
| 34 |
+
>>> list(always_iterable(obj))
|
| 35 |
+
[1]
|
| 36 |
+
|
| 37 |
+
If *obj* is ``None``, return an empty iterable:
|
| 38 |
+
|
| 39 |
+
>>> obj = None
|
| 40 |
+
>>> list(always_iterable(None))
|
| 41 |
+
[]
|
| 42 |
+
|
| 43 |
+
By default, binary and text strings are not considered iterable::
|
| 44 |
+
|
| 45 |
+
>>> obj = 'foo'
|
| 46 |
+
>>> list(always_iterable(obj))
|
| 47 |
+
['foo']
|
| 48 |
+
|
| 49 |
+
If *base_type* is set, objects for which ``isinstance(obj, base_type)``
|
| 50 |
+
returns ``True`` won't be considered iterable.
|
| 51 |
+
|
| 52 |
+
>>> obj = {'a': 1}
|
| 53 |
+
>>> list(always_iterable(obj)) # Iterate over the dict's keys
|
| 54 |
+
['a']
|
| 55 |
+
>>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
|
| 56 |
+
[{'a': 1}]
|
| 57 |
+
|
| 58 |
+
Set *base_type* to ``None`` to avoid any special handling and treat objects
|
| 59 |
+
Python considers iterable as iterable:
|
| 60 |
+
|
| 61 |
+
>>> obj = 'foo'
|
| 62 |
+
>>> list(always_iterable(obj, base_type=None))
|
| 63 |
+
['f', 'o', 'o']
|
| 64 |
+
"""
|
| 65 |
+
if obj is None:
|
| 66 |
+
return iter(())
|
| 67 |
+
|
| 68 |
+
if (base_type is not None) and isinstance(obj, base_type):
|
| 69 |
+
return iter((obj,))
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
return iter(obj)
|
| 73 |
+
except TypeError:
|
| 74 |
+
return iter((obj,))
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# Copied from more_itertools 10.3
|
| 78 |
+
class bucket:
|
| 79 |
+
"""Wrap *iterable* and return an object that buckets the iterable into
|
| 80 |
+
child iterables based on a *key* function.
|
| 81 |
+
|
| 82 |
+
>>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3']
|
| 83 |
+
>>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character
|
| 84 |
+
>>> sorted(list(s)) # Get the keys
|
| 85 |
+
['a', 'b', 'c']
|
| 86 |
+
>>> a_iterable = s['a']
|
| 87 |
+
>>> next(a_iterable)
|
| 88 |
+
'a1'
|
| 89 |
+
>>> next(a_iterable)
|
| 90 |
+
'a2'
|
| 91 |
+
>>> list(s['b'])
|
| 92 |
+
['b1', 'b2', 'b3']
|
| 93 |
+
|
| 94 |
+
The original iterable will be advanced and its items will be cached until
|
| 95 |
+
they are used by the child iterables. This may require significant storage.
|
| 96 |
+
|
| 97 |
+
By default, attempting to select a bucket to which no items belong will
|
| 98 |
+
exhaust the iterable and cache all values.
|
| 99 |
+
If you specify a *validator* function, selected buckets will instead be
|
| 100 |
+
checked against it.
|
| 101 |
+
|
| 102 |
+
>>> from itertools import count
|
| 103 |
+
>>> it = count(1, 2) # Infinite sequence of odd numbers
|
| 104 |
+
>>> key = lambda x: x % 10 # Bucket by last digit
|
| 105 |
+
>>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only
|
| 106 |
+
>>> s = bucket(it, key=key, validator=validator)
|
| 107 |
+
>>> 2 in s
|
| 108 |
+
False
|
| 109 |
+
>>> list(s[2])
|
| 110 |
+
[]
|
| 111 |
+
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
def __init__(self, iterable, key, validator=None):
|
| 115 |
+
self._it = iter(iterable)
|
| 116 |
+
self._key = key
|
| 117 |
+
self._cache = defaultdict(deque)
|
| 118 |
+
self._validator = validator or (lambda x: True)
|
| 119 |
+
|
| 120 |
+
def __contains__(self, value):
|
| 121 |
+
if not self._validator(value):
|
| 122 |
+
return False
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
item = next(self[value])
|
| 126 |
+
except StopIteration:
|
| 127 |
+
return False
|
| 128 |
+
else:
|
| 129 |
+
self._cache[value].appendleft(item)
|
| 130 |
+
|
| 131 |
+
return True
|
| 132 |
+
|
| 133 |
+
def _get_values(self, value):
|
| 134 |
+
"""
|
| 135 |
+
Helper to yield items from the parent iterator that match *value*.
|
| 136 |
+
Items that don't match are stored in the local cache as they
|
| 137 |
+
are encountered.
|
| 138 |
+
"""
|
| 139 |
+
while True:
|
| 140 |
+
# If we've cached some items that match the target value, emit
|
| 141 |
+
# the first one and evict it from the cache.
|
| 142 |
+
if self._cache[value]:
|
| 143 |
+
yield self._cache[value].popleft()
|
| 144 |
+
# Otherwise we need to advance the parent iterator to search for
|
| 145 |
+
# a matching item, caching the rest.
|
| 146 |
+
else:
|
| 147 |
+
while True:
|
| 148 |
+
try:
|
| 149 |
+
item = next(self._it)
|
| 150 |
+
except StopIteration:
|
| 151 |
+
return
|
| 152 |
+
item_value = self._key(item)
|
| 153 |
+
if item_value == value:
|
| 154 |
+
yield item
|
| 155 |
+
break
|
| 156 |
+
elif self._validator(item_value):
|
| 157 |
+
self._cache[item_value].append(item)
|
| 158 |
+
|
| 159 |
+
def __iter__(self):
|
| 160 |
+
for item in self._it:
|
| 161 |
+
item_value = self._key(item)
|
| 162 |
+
if self._validator(item_value):
|
| 163 |
+
self._cache[item_value].append(item)
|
| 164 |
+
|
| 165 |
+
yield from self._cache.keys()
|
| 166 |
+
|
| 167 |
+
def __getitem__(self, value):
|
| 168 |
+
if not self._validator(value):
|
| 169 |
+
return iter(())
|
| 170 |
+
|
| 171 |
+
return self._get_values(value)
|
.venv/lib/python3.11/site-packages/importlib_metadata/_meta.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from typing import (
|
| 5 |
+
Any,
|
| 6 |
+
Dict,
|
| 7 |
+
Iterator,
|
| 8 |
+
List,
|
| 9 |
+
Optional,
|
| 10 |
+
Protocol,
|
| 11 |
+
TypeVar,
|
| 12 |
+
Union,
|
| 13 |
+
overload,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
_T = TypeVar("_T")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class PackageMetadata(Protocol):
|
| 20 |
+
def __len__(self) -> int: ... # pragma: no cover
|
| 21 |
+
|
| 22 |
+
def __contains__(self, item: str) -> bool: ... # pragma: no cover
|
| 23 |
+
|
| 24 |
+
def __getitem__(self, key: str) -> str: ... # pragma: no cover
|
| 25 |
+
|
| 26 |
+
def __iter__(self) -> Iterator[str]: ... # pragma: no cover
|
| 27 |
+
|
| 28 |
+
@overload
|
| 29 |
+
def get(
|
| 30 |
+
self, name: str, failobj: None = None
|
| 31 |
+
) -> Optional[str]: ... # pragma: no cover
|
| 32 |
+
|
| 33 |
+
@overload
|
| 34 |
+
def get(self, name: str, failobj: _T) -> Union[str, _T]: ... # pragma: no cover
|
| 35 |
+
|
| 36 |
+
# overload per python/importlib_metadata#435
|
| 37 |
+
@overload
|
| 38 |
+
def get_all(
|
| 39 |
+
self, name: str, failobj: None = None
|
| 40 |
+
) -> Optional[List[Any]]: ... # pragma: no cover
|
| 41 |
+
|
| 42 |
+
@overload
|
| 43 |
+
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
|
| 44 |
+
"""
|
| 45 |
+
Return all values associated with a possibly multi-valued key.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
@property
|
| 49 |
+
def json(self) -> Dict[str, Union[str, List[str]]]:
|
| 50 |
+
"""
|
| 51 |
+
A JSON-compatible form of the metadata.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class SimplePath(Protocol):
|
| 56 |
+
"""
|
| 57 |
+
A minimal subset of pathlib.Path required by Distribution.
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def joinpath(
|
| 61 |
+
self, other: Union[str, os.PathLike[str]]
|
| 62 |
+
) -> SimplePath: ... # pragma: no cover
|
| 63 |
+
|
| 64 |
+
def __truediv__(
|
| 65 |
+
self, other: Union[str, os.PathLike[str]]
|
| 66 |
+
) -> SimplePath: ... # pragma: no cover
|
| 67 |
+
|
| 68 |
+
@property
|
| 69 |
+
def parent(self) -> SimplePath: ... # pragma: no cover
|
| 70 |
+
|
| 71 |
+
def read_text(self, encoding=None) -> str: ... # pragma: no cover
|
| 72 |
+
|
| 73 |
+
def read_bytes(self) -> bytes: ... # pragma: no cover
|
| 74 |
+
|
| 75 |
+
def exists(self) -> bool: ... # pragma: no cover
|
.venv/lib/python3.11/site-packages/importlib_metadata/_text.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
from ._functools import method_cache
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# from jaraco.text 3.5
|
| 7 |
+
class FoldedCase(str):
|
| 8 |
+
"""
|
| 9 |
+
A case insensitive string class; behaves just like str
|
| 10 |
+
except compares equal when the only variation is case.
|
| 11 |
+
|
| 12 |
+
>>> s = FoldedCase('hello world')
|
| 13 |
+
|
| 14 |
+
>>> s == 'Hello World'
|
| 15 |
+
True
|
| 16 |
+
|
| 17 |
+
>>> 'Hello World' == s
|
| 18 |
+
True
|
| 19 |
+
|
| 20 |
+
>>> s != 'Hello World'
|
| 21 |
+
False
|
| 22 |
+
|
| 23 |
+
>>> s.index('O')
|
| 24 |
+
4
|
| 25 |
+
|
| 26 |
+
>>> s.split('O')
|
| 27 |
+
['hell', ' w', 'rld']
|
| 28 |
+
|
| 29 |
+
>>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta']))
|
| 30 |
+
['alpha', 'Beta', 'GAMMA']
|
| 31 |
+
|
| 32 |
+
Sequence membership is straightforward.
|
| 33 |
+
|
| 34 |
+
>>> "Hello World" in [s]
|
| 35 |
+
True
|
| 36 |
+
>>> s in ["Hello World"]
|
| 37 |
+
True
|
| 38 |
+
|
| 39 |
+
You may test for set inclusion, but candidate and elements
|
| 40 |
+
must both be folded.
|
| 41 |
+
|
| 42 |
+
>>> FoldedCase("Hello World") in {s}
|
| 43 |
+
True
|
| 44 |
+
>>> s in {FoldedCase("Hello World")}
|
| 45 |
+
True
|
| 46 |
+
|
| 47 |
+
String inclusion works as long as the FoldedCase object
|
| 48 |
+
is on the right.
|
| 49 |
+
|
| 50 |
+
>>> "hello" in FoldedCase("Hello World")
|
| 51 |
+
True
|
| 52 |
+
|
| 53 |
+
But not if the FoldedCase object is on the left:
|
| 54 |
+
|
| 55 |
+
>>> FoldedCase('hello') in 'Hello World'
|
| 56 |
+
False
|
| 57 |
+
|
| 58 |
+
In that case, use in_:
|
| 59 |
+
|
| 60 |
+
>>> FoldedCase('hello').in_('Hello World')
|
| 61 |
+
True
|
| 62 |
+
|
| 63 |
+
>>> FoldedCase('hello') > FoldedCase('Hello')
|
| 64 |
+
False
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def __lt__(self, other):
|
| 68 |
+
return self.lower() < other.lower()
|
| 69 |
+
|
| 70 |
+
def __gt__(self, other):
|
| 71 |
+
return self.lower() > other.lower()
|
| 72 |
+
|
| 73 |
+
def __eq__(self, other):
|
| 74 |
+
return self.lower() == other.lower()
|
| 75 |
+
|
| 76 |
+
def __ne__(self, other):
|
| 77 |
+
return self.lower() != other.lower()
|
| 78 |
+
|
| 79 |
+
def __hash__(self):
|
| 80 |
+
return hash(self.lower())
|
| 81 |
+
|
| 82 |
+
def __contains__(self, other):
|
| 83 |
+
return super().lower().__contains__(other.lower())
|
| 84 |
+
|
| 85 |
+
def in_(self, other):
|
| 86 |
+
"Does self appear in other?"
|
| 87 |
+
return self in FoldedCase(other)
|
| 88 |
+
|
| 89 |
+
# cache lower since it's likely to be called frequently.
|
| 90 |
+
@method_cache
|
| 91 |
+
def lower(self):
|
| 92 |
+
return super().lower()
|
| 93 |
+
|
| 94 |
+
def index(self, sub):
|
| 95 |
+
return self.lower().index(sub.lower())
|
| 96 |
+
|
| 97 |
+
def split(self, splitter=' ', maxsplit=0):
|
| 98 |
+
pattern = re.compile(re.escape(splitter), re.I)
|
| 99 |
+
return pattern.split(self, maxsplit)
|
.venv/lib/python3.11/site-packages/importlib_metadata/compat/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/importlib_metadata/compat/__pycache__/py39.cpython-311.pyc
ADDED
|
Binary file (1.78 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/importlib_metadata/compat/py311.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pathlib
|
| 3 |
+
import sys
|
| 4 |
+
import types
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def wrap(path): # pragma: no cover
|
| 8 |
+
"""
|
| 9 |
+
Workaround for https://github.com/python/cpython/issues/84538
|
| 10 |
+
to add backward compatibility for walk_up=True.
|
| 11 |
+
An example affected package is dask-labextension, which uses
|
| 12 |
+
jupyter-packaging to install JupyterLab javascript files outside
|
| 13 |
+
of site-packages.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
def relative_to(root, *, walk_up=False):
|
| 17 |
+
return pathlib.Path(os.path.relpath(path, root))
|
| 18 |
+
|
| 19 |
+
return types.SimpleNamespace(relative_to=relative_to)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
relative_fix = wrap if sys.version_info < (3, 12) else lambda x: x
|
.venv/lib/python3.11/site-packages/importlib_metadata/compat/py39.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Compatibility layer with Python 3.8/3.9
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import TYPE_CHECKING, Any, Optional
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING: # pragma: no cover
|
| 8 |
+
# Prevent circular imports on runtime.
|
| 9 |
+
from .. import Distribution, EntryPoint
|
| 10 |
+
else:
|
| 11 |
+
Distribution = EntryPoint = Any
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def normalized_name(dist: Distribution) -> Optional[str]:
|
| 15 |
+
"""
|
| 16 |
+
Honor name normalization for distributions that don't provide ``_normalized_name``.
|
| 17 |
+
"""
|
| 18 |
+
try:
|
| 19 |
+
return dist._normalized_name
|
| 20 |
+
except AttributeError:
|
| 21 |
+
from .. import Prepared # -> delay to prevent circular imports.
|
| 22 |
+
|
| 23 |
+
return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name'])
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def ep_matches(ep: EntryPoint, **params) -> bool:
|
| 27 |
+
"""
|
| 28 |
+
Workaround for ``EntryPoint`` objects without the ``matches`` method.
|
| 29 |
+
"""
|
| 30 |
+
try:
|
| 31 |
+
return ep.matches(**params)
|
| 32 |
+
except AttributeError:
|
| 33 |
+
from .. import EntryPoint # -> delay to prevent circular imports.
|
| 34 |
+
|
| 35 |
+
# Reconstruct the EntryPoint object to make sure it is compatible.
|
| 36 |
+
return EntryPoint(ep.name, ep.value, ep.group).matches(**params)
|
.venv/lib/python3.11/site-packages/importlib_metadata/diagnose.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
from . import Distribution
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def inspect(path):
|
| 7 |
+
print("Inspecting", path)
|
| 8 |
+
dists = list(Distribution.discover(path=[path]))
|
| 9 |
+
if not dists:
|
| 10 |
+
return
|
| 11 |
+
print("Found", len(dists), "packages:", end=' ')
|
| 12 |
+
print(', '.join(dist.name for dist in dists))
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def run():
|
| 16 |
+
for path in sys.path:
|
| 17 |
+
inspect(path)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
if __name__ == '__main__':
|
| 21 |
+
run()
|
.venv/lib/python3.11/site-packages/importlib_metadata/py.typed
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (417 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/__main__.cpython-311.pyc
ADDED
|
Binary file (306 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/_subprocess.cpython-311.pyc
ADDED
|
Binary file (3.09 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/_types.cpython-311.pyc
ADDED
|
Binary file (13.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (26.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/importer.cpython-311.pyc
ADDED
|
Binary file (2.02 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/logging.cpython-311.pyc
ADDED
|
Binary file (8.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (20.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/server.cpython-311.pyc
ADDED
|
Binary file (16.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/__pycache__/workers.cpython-311.pyc
ADDED
|
Binary file (6.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/lifespan/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/uvicorn/lifespan/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (189 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/lifespan/__pycache__/off.cpython-311.pyc
ADDED
|
Binary file (1.14 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/lifespan/__pycache__/on.cpython-311.pyc
ADDED
|
Binary file (8.38 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/lifespan/off.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
from uvicorn import Config
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class LifespanOff:
|
| 9 |
+
def __init__(self, config: Config) -> None:
|
| 10 |
+
self.should_exit = False
|
| 11 |
+
self.state: dict[str, Any] = {}
|
| 12 |
+
|
| 13 |
+
async def startup(self) -> None:
|
| 14 |
+
pass
|
| 15 |
+
|
| 16 |
+
async def shutdown(self) -> None:
|
| 17 |
+
pass
|
.venv/lib/python3.11/site-packages/uvicorn/lifespan/on.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
from asyncio import Queue
|
| 6 |
+
from typing import Any, Union
|
| 7 |
+
|
| 8 |
+
from uvicorn import Config
|
| 9 |
+
from uvicorn._types import (
|
| 10 |
+
LifespanScope,
|
| 11 |
+
LifespanShutdownCompleteEvent,
|
| 12 |
+
LifespanShutdownEvent,
|
| 13 |
+
LifespanShutdownFailedEvent,
|
| 14 |
+
LifespanStartupCompleteEvent,
|
| 15 |
+
LifespanStartupEvent,
|
| 16 |
+
LifespanStartupFailedEvent,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
LifespanReceiveMessage = Union[LifespanStartupEvent, LifespanShutdownEvent]
|
| 20 |
+
LifespanSendMessage = Union[
|
| 21 |
+
LifespanStartupFailedEvent,
|
| 22 |
+
LifespanShutdownFailedEvent,
|
| 23 |
+
LifespanStartupCompleteEvent,
|
| 24 |
+
LifespanShutdownCompleteEvent,
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
STATE_TRANSITION_ERROR = "Got invalid state transition on lifespan protocol."
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class LifespanOn:
|
| 32 |
+
def __init__(self, config: Config) -> None:
|
| 33 |
+
if not config.loaded:
|
| 34 |
+
config.load()
|
| 35 |
+
|
| 36 |
+
self.config = config
|
| 37 |
+
self.logger = logging.getLogger("uvicorn.error")
|
| 38 |
+
self.startup_event = asyncio.Event()
|
| 39 |
+
self.shutdown_event = asyncio.Event()
|
| 40 |
+
self.receive_queue: Queue[LifespanReceiveMessage] = asyncio.Queue()
|
| 41 |
+
self.error_occured = False
|
| 42 |
+
self.startup_failed = False
|
| 43 |
+
self.shutdown_failed = False
|
| 44 |
+
self.should_exit = False
|
| 45 |
+
self.state: dict[str, Any] = {}
|
| 46 |
+
|
| 47 |
+
async def startup(self) -> None:
|
| 48 |
+
self.logger.info("Waiting for application startup.")
|
| 49 |
+
|
| 50 |
+
loop = asyncio.get_event_loop()
|
| 51 |
+
main_lifespan_task = loop.create_task(self.main()) # noqa: F841
|
| 52 |
+
# Keep a hard reference to prevent garbage collection
|
| 53 |
+
# See https://github.com/encode/uvicorn/pull/972
|
| 54 |
+
startup_event: LifespanStartupEvent = {"type": "lifespan.startup"}
|
| 55 |
+
await self.receive_queue.put(startup_event)
|
| 56 |
+
await self.startup_event.wait()
|
| 57 |
+
|
| 58 |
+
if self.startup_failed or (self.error_occured and self.config.lifespan == "on"):
|
| 59 |
+
self.logger.error("Application startup failed. Exiting.")
|
| 60 |
+
self.should_exit = True
|
| 61 |
+
else:
|
| 62 |
+
self.logger.info("Application startup complete.")
|
| 63 |
+
|
| 64 |
+
async def shutdown(self) -> None:
|
| 65 |
+
if self.error_occured:
|
| 66 |
+
return
|
| 67 |
+
self.logger.info("Waiting for application shutdown.")
|
| 68 |
+
shutdown_event: LifespanShutdownEvent = {"type": "lifespan.shutdown"}
|
| 69 |
+
await self.receive_queue.put(shutdown_event)
|
| 70 |
+
await self.shutdown_event.wait()
|
| 71 |
+
|
| 72 |
+
if self.shutdown_failed or (self.error_occured and self.config.lifespan == "on"):
|
| 73 |
+
self.logger.error("Application shutdown failed. Exiting.")
|
| 74 |
+
self.should_exit = True
|
| 75 |
+
else:
|
| 76 |
+
self.logger.info("Application shutdown complete.")
|
| 77 |
+
|
| 78 |
+
async def main(self) -> None:
|
| 79 |
+
try:
|
| 80 |
+
app = self.config.loaded_app
|
| 81 |
+
scope: LifespanScope = {
|
| 82 |
+
"type": "lifespan",
|
| 83 |
+
"asgi": {"version": self.config.asgi_version, "spec_version": "2.0"},
|
| 84 |
+
"state": self.state,
|
| 85 |
+
}
|
| 86 |
+
await app(scope, self.receive, self.send)
|
| 87 |
+
except BaseException as exc:
|
| 88 |
+
self.asgi = None
|
| 89 |
+
self.error_occured = True
|
| 90 |
+
if self.startup_failed or self.shutdown_failed:
|
| 91 |
+
return
|
| 92 |
+
if self.config.lifespan == "auto":
|
| 93 |
+
msg = "ASGI 'lifespan' protocol appears unsupported."
|
| 94 |
+
self.logger.info(msg)
|
| 95 |
+
else:
|
| 96 |
+
msg = "Exception in 'lifespan' protocol\n"
|
| 97 |
+
self.logger.error(msg, exc_info=exc)
|
| 98 |
+
finally:
|
| 99 |
+
self.startup_event.set()
|
| 100 |
+
self.shutdown_event.set()
|
| 101 |
+
|
| 102 |
+
async def send(self, message: LifespanSendMessage) -> None:
|
| 103 |
+
assert message["type"] in (
|
| 104 |
+
"lifespan.startup.complete",
|
| 105 |
+
"lifespan.startup.failed",
|
| 106 |
+
"lifespan.shutdown.complete",
|
| 107 |
+
"lifespan.shutdown.failed",
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
if message["type"] == "lifespan.startup.complete":
|
| 111 |
+
assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR
|
| 112 |
+
assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
|
| 113 |
+
self.startup_event.set()
|
| 114 |
+
|
| 115 |
+
elif message["type"] == "lifespan.startup.failed":
|
| 116 |
+
assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR
|
| 117 |
+
assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
|
| 118 |
+
self.startup_event.set()
|
| 119 |
+
self.startup_failed = True
|
| 120 |
+
if message.get("message"):
|
| 121 |
+
self.logger.error(message["message"])
|
| 122 |
+
|
| 123 |
+
elif message["type"] == "lifespan.shutdown.complete":
|
| 124 |
+
assert self.startup_event.is_set(), STATE_TRANSITION_ERROR
|
| 125 |
+
assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
|
| 126 |
+
self.shutdown_event.set()
|
| 127 |
+
|
| 128 |
+
elif message["type"] == "lifespan.shutdown.failed":
|
| 129 |
+
assert self.startup_event.is_set(), STATE_TRANSITION_ERROR
|
| 130 |
+
assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
|
| 131 |
+
self.shutdown_event.set()
|
| 132 |
+
self.shutdown_failed = True
|
| 133 |
+
if message.get("message"):
|
| 134 |
+
self.logger.error(message["message"])
|
| 135 |
+
|
| 136 |
+
async def receive(self) -> LifespanReceiveMessage:
|
| 137 |
+
return await self.receive_queue.get()
|
.venv/lib/python3.11/site-packages/uvicorn/loops/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (186 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/protocols/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/uvicorn/protocols/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (190 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/protocols/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (3.56 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/protocols/http/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/uvicorn/protocols/http/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (195 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/protocols/http/__pycache__/auto.cpython-311.pyc
ADDED
|
Binary file (694 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/uvicorn/protocols/http/__pycache__/flow_control.cpython-311.pyc
ADDED
|
Binary file (3.29 kB). View file
|
|
|