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/pip/_internal/metadata/__init__.py +128 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/_json.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/base.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/pkg_resources.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/_json.py +84 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/base.py +702 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/importlib/__pycache__/_dists.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_compat.py +55 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_dists.py +227 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_envs.py +189 -0
- .venv/lib/python3.11/site-packages/pip/_internal/metadata/pkg_resources.py +278 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/__init__.py +2 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/auth.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/cache.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/download.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/lazy_wheel.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/session.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/xmlrpc.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/auth.py +561 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/cache.py +106 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/download.py +186 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/lazy_wheel.py +210 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/session.py +520 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/utils.py +96 -0
- .venv/lib/python3.11/site-packages/pip/_internal/network/xmlrpc.py +62 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/_jaraco_text.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/_log.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/compat.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/compatibility_tags.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/datetime.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/deprecation.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/direct_url_helpers.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/egg_link.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/encoding.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/entrypoints.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/filesystem.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/filetypes.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/glibc.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/hashes.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/logging.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/misc.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/models.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/packaging.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/setuptools_build.cpython-311.pyc +0 -0
.venv/lib/python3.11/site-packages/pip/_internal/metadata/__init__.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import contextlib
|
| 2 |
+
import functools
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
from typing import TYPE_CHECKING, List, Optional, Type, cast
|
| 6 |
+
|
| 7 |
+
from pip._internal.utils.misc import strtobool
|
| 8 |
+
|
| 9 |
+
from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
|
| 10 |
+
|
| 11 |
+
if TYPE_CHECKING:
|
| 12 |
+
from typing import Literal, Protocol
|
| 13 |
+
else:
|
| 14 |
+
Protocol = object
|
| 15 |
+
|
| 16 |
+
__all__ = [
|
| 17 |
+
"BaseDistribution",
|
| 18 |
+
"BaseEnvironment",
|
| 19 |
+
"FilesystemWheel",
|
| 20 |
+
"MemoryWheel",
|
| 21 |
+
"Wheel",
|
| 22 |
+
"get_default_environment",
|
| 23 |
+
"get_environment",
|
| 24 |
+
"get_wheel_distribution",
|
| 25 |
+
"select_backend",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _should_use_importlib_metadata() -> bool:
|
| 30 |
+
"""Whether to use the ``importlib.metadata`` or ``pkg_resources`` backend.
|
| 31 |
+
|
| 32 |
+
By default, pip uses ``importlib.metadata`` on Python 3.11+, and
|
| 33 |
+
``pkg_resourcess`` otherwise. This can be overridden by a couple of ways:
|
| 34 |
+
|
| 35 |
+
* If environment variable ``_PIP_USE_IMPORTLIB_METADATA`` is set, it
|
| 36 |
+
dictates whether ``importlib.metadata`` is used, regardless of Python
|
| 37 |
+
version.
|
| 38 |
+
* On Python 3.11+, Python distributors can patch ``importlib.metadata``
|
| 39 |
+
to add a global constant ``_PIP_USE_IMPORTLIB_METADATA = False``. This
|
| 40 |
+
makes pip use ``pkg_resources`` (unless the user set the aforementioned
|
| 41 |
+
environment variable to *True*).
|
| 42 |
+
"""
|
| 43 |
+
with contextlib.suppress(KeyError, ValueError):
|
| 44 |
+
return bool(strtobool(os.environ["_PIP_USE_IMPORTLIB_METADATA"]))
|
| 45 |
+
if sys.version_info < (3, 11):
|
| 46 |
+
return False
|
| 47 |
+
import importlib.metadata
|
| 48 |
+
|
| 49 |
+
return bool(getattr(importlib.metadata, "_PIP_USE_IMPORTLIB_METADATA", True))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class Backend(Protocol):
|
| 53 |
+
NAME: 'Literal["importlib", "pkg_resources"]'
|
| 54 |
+
Distribution: Type[BaseDistribution]
|
| 55 |
+
Environment: Type[BaseEnvironment]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@functools.lru_cache(maxsize=None)
|
| 59 |
+
def select_backend() -> Backend:
|
| 60 |
+
if _should_use_importlib_metadata():
|
| 61 |
+
from . import importlib
|
| 62 |
+
|
| 63 |
+
return cast(Backend, importlib)
|
| 64 |
+
from . import pkg_resources
|
| 65 |
+
|
| 66 |
+
return cast(Backend, pkg_resources)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def get_default_environment() -> BaseEnvironment:
|
| 70 |
+
"""Get the default representation for the current environment.
|
| 71 |
+
|
| 72 |
+
This returns an Environment instance from the chosen backend. The default
|
| 73 |
+
Environment instance should be built from ``sys.path`` and may use caching
|
| 74 |
+
to share instance state accorss calls.
|
| 75 |
+
"""
|
| 76 |
+
return select_backend().Environment.default()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
|
| 80 |
+
"""Get a representation of the environment specified by ``paths``.
|
| 81 |
+
|
| 82 |
+
This returns an Environment instance from the chosen backend based on the
|
| 83 |
+
given import paths. The backend must build a fresh instance representing
|
| 84 |
+
the state of installed distributions when this function is called.
|
| 85 |
+
"""
|
| 86 |
+
return select_backend().Environment.from_paths(paths)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def get_directory_distribution(directory: str) -> BaseDistribution:
|
| 90 |
+
"""Get the distribution metadata representation in the specified directory.
|
| 91 |
+
|
| 92 |
+
This returns a Distribution instance from the chosen backend based on
|
| 93 |
+
the given on-disk ``.dist-info`` directory.
|
| 94 |
+
"""
|
| 95 |
+
return select_backend().Distribution.from_directory(directory)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistribution:
|
| 99 |
+
"""Get the representation of the specified wheel's distribution metadata.
|
| 100 |
+
|
| 101 |
+
This returns a Distribution instance from the chosen backend based on
|
| 102 |
+
the given wheel's ``.dist-info`` directory.
|
| 103 |
+
|
| 104 |
+
:param canonical_name: Normalized project name of the given wheel.
|
| 105 |
+
"""
|
| 106 |
+
return select_backend().Distribution.from_wheel(wheel, canonical_name)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def get_metadata_distribution(
|
| 110 |
+
metadata_contents: bytes,
|
| 111 |
+
filename: str,
|
| 112 |
+
canonical_name: str,
|
| 113 |
+
) -> BaseDistribution:
|
| 114 |
+
"""Get the dist representation of the specified METADATA file contents.
|
| 115 |
+
|
| 116 |
+
This returns a Distribution instance from the chosen backend sourced from the data
|
| 117 |
+
in `metadata_contents`.
|
| 118 |
+
|
| 119 |
+
:param metadata_contents: Contents of a METADATA file within a dist, or one served
|
| 120 |
+
via PEP 658.
|
| 121 |
+
:param filename: Filename for the dist this metadata represents.
|
| 122 |
+
:param canonical_name: Normalized project name of the given dist.
|
| 123 |
+
"""
|
| 124 |
+
return select_backend().Distribution.from_metadata_file_contents(
|
| 125 |
+
metadata_contents,
|
| 126 |
+
filename,
|
| 127 |
+
canonical_name,
|
| 128 |
+
)
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (6.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/_json.cpython-311.pyc
ADDED
|
Binary file (3.56 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/base.cpython-311.pyc
ADDED
|
Binary file (38.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/__pycache__/pkg_resources.cpython-311.pyc
ADDED
|
Binary file (17.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/_json.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Extracted from https://github.com/pfmoore/pkg_metadata
|
| 2 |
+
|
| 3 |
+
from email.header import Header, decode_header, make_header
|
| 4 |
+
from email.message import Message
|
| 5 |
+
from typing import Any, Dict, List, Union
|
| 6 |
+
|
| 7 |
+
METADATA_FIELDS = [
|
| 8 |
+
# Name, Multiple-Use
|
| 9 |
+
("Metadata-Version", False),
|
| 10 |
+
("Name", False),
|
| 11 |
+
("Version", False),
|
| 12 |
+
("Dynamic", True),
|
| 13 |
+
("Platform", True),
|
| 14 |
+
("Supported-Platform", True),
|
| 15 |
+
("Summary", False),
|
| 16 |
+
("Description", False),
|
| 17 |
+
("Description-Content-Type", False),
|
| 18 |
+
("Keywords", False),
|
| 19 |
+
("Home-page", False),
|
| 20 |
+
("Download-URL", False),
|
| 21 |
+
("Author", False),
|
| 22 |
+
("Author-email", False),
|
| 23 |
+
("Maintainer", False),
|
| 24 |
+
("Maintainer-email", False),
|
| 25 |
+
("License", False),
|
| 26 |
+
("Classifier", True),
|
| 27 |
+
("Requires-Dist", True),
|
| 28 |
+
("Requires-Python", False),
|
| 29 |
+
("Requires-External", True),
|
| 30 |
+
("Project-URL", True),
|
| 31 |
+
("Provides-Extra", True),
|
| 32 |
+
("Provides-Dist", True),
|
| 33 |
+
("Obsoletes-Dist", True),
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def json_name(field: str) -> str:
|
| 38 |
+
return field.lower().replace("-", "_")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def msg_to_json(msg: Message) -> Dict[str, Any]:
|
| 42 |
+
"""Convert a Message object into a JSON-compatible dictionary."""
|
| 43 |
+
|
| 44 |
+
def sanitise_header(h: Union[Header, str]) -> str:
|
| 45 |
+
if isinstance(h, Header):
|
| 46 |
+
chunks = []
|
| 47 |
+
for bytes, encoding in decode_header(h):
|
| 48 |
+
if encoding == "unknown-8bit":
|
| 49 |
+
try:
|
| 50 |
+
# See if UTF-8 works
|
| 51 |
+
bytes.decode("utf-8")
|
| 52 |
+
encoding = "utf-8"
|
| 53 |
+
except UnicodeDecodeError:
|
| 54 |
+
# If not, latin1 at least won't fail
|
| 55 |
+
encoding = "latin1"
|
| 56 |
+
chunks.append((bytes, encoding))
|
| 57 |
+
return str(make_header(chunks))
|
| 58 |
+
return str(h)
|
| 59 |
+
|
| 60 |
+
result = {}
|
| 61 |
+
for field, multi in METADATA_FIELDS:
|
| 62 |
+
if field not in msg:
|
| 63 |
+
continue
|
| 64 |
+
key = json_name(field)
|
| 65 |
+
if multi:
|
| 66 |
+
value: Union[str, List[str]] = [
|
| 67 |
+
sanitise_header(v) for v in msg.get_all(field) # type: ignore
|
| 68 |
+
]
|
| 69 |
+
else:
|
| 70 |
+
value = sanitise_header(msg.get(field)) # type: ignore
|
| 71 |
+
if key == "keywords":
|
| 72 |
+
# Accept both comma-separated and space-separated
|
| 73 |
+
# forms, for better compatibility with old data.
|
| 74 |
+
if "," in value:
|
| 75 |
+
value = [v.strip() for v in value.split(",")]
|
| 76 |
+
else:
|
| 77 |
+
value = value.split()
|
| 78 |
+
result[key] = value
|
| 79 |
+
|
| 80 |
+
payload = msg.get_payload()
|
| 81 |
+
if payload:
|
| 82 |
+
result["description"] = payload
|
| 83 |
+
|
| 84 |
+
return result
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/base.py
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv
|
| 2 |
+
import email.message
|
| 3 |
+
import functools
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
import pathlib
|
| 7 |
+
import re
|
| 8 |
+
import zipfile
|
| 9 |
+
from typing import (
|
| 10 |
+
IO,
|
| 11 |
+
TYPE_CHECKING,
|
| 12 |
+
Any,
|
| 13 |
+
Collection,
|
| 14 |
+
Container,
|
| 15 |
+
Dict,
|
| 16 |
+
Iterable,
|
| 17 |
+
Iterator,
|
| 18 |
+
List,
|
| 19 |
+
NamedTuple,
|
| 20 |
+
Optional,
|
| 21 |
+
Tuple,
|
| 22 |
+
Union,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
from pip._vendor.packaging.requirements import Requirement
|
| 26 |
+
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
|
| 27 |
+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
| 28 |
+
from pip._vendor.packaging.version import LegacyVersion, Version
|
| 29 |
+
|
| 30 |
+
from pip._internal.exceptions import NoneMetadataError
|
| 31 |
+
from pip._internal.locations import site_packages, user_site
|
| 32 |
+
from pip._internal.models.direct_url import (
|
| 33 |
+
DIRECT_URL_METADATA_NAME,
|
| 34 |
+
DirectUrl,
|
| 35 |
+
DirectUrlValidationError,
|
| 36 |
+
)
|
| 37 |
+
from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here.
|
| 38 |
+
from pip._internal.utils.egg_link import egg_link_path_from_sys_path
|
| 39 |
+
from pip._internal.utils.misc import is_local, normalize_path
|
| 40 |
+
from pip._internal.utils.urls import url_to_path
|
| 41 |
+
|
| 42 |
+
from ._json import msg_to_json
|
| 43 |
+
|
| 44 |
+
if TYPE_CHECKING:
|
| 45 |
+
from typing import Protocol
|
| 46 |
+
else:
|
| 47 |
+
Protocol = object
|
| 48 |
+
|
| 49 |
+
DistributionVersion = Union[LegacyVersion, Version]
|
| 50 |
+
|
| 51 |
+
InfoPath = Union[str, pathlib.PurePath]
|
| 52 |
+
|
| 53 |
+
logger = logging.getLogger(__name__)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class BaseEntryPoint(Protocol):
|
| 57 |
+
@property
|
| 58 |
+
def name(self) -> str:
|
| 59 |
+
raise NotImplementedError()
|
| 60 |
+
|
| 61 |
+
@property
|
| 62 |
+
def value(self) -> str:
|
| 63 |
+
raise NotImplementedError()
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def group(self) -> str:
|
| 67 |
+
raise NotImplementedError()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _convert_installed_files_path(
|
| 71 |
+
entry: Tuple[str, ...],
|
| 72 |
+
info: Tuple[str, ...],
|
| 73 |
+
) -> str:
|
| 74 |
+
"""Convert a legacy installed-files.txt path into modern RECORD path.
|
| 75 |
+
|
| 76 |
+
The legacy format stores paths relative to the info directory, while the
|
| 77 |
+
modern format stores paths relative to the package root, e.g. the
|
| 78 |
+
site-packages directory.
|
| 79 |
+
|
| 80 |
+
:param entry: Path parts of the installed-files.txt entry.
|
| 81 |
+
:param info: Path parts of the egg-info directory relative to package root.
|
| 82 |
+
:returns: The converted entry.
|
| 83 |
+
|
| 84 |
+
For best compatibility with symlinks, this does not use ``abspath()`` or
|
| 85 |
+
``Path.resolve()``, but tries to work with path parts:
|
| 86 |
+
|
| 87 |
+
1. While ``entry`` starts with ``..``, remove the equal amounts of parts
|
| 88 |
+
from ``info``; if ``info`` is empty, start appending ``..`` instead.
|
| 89 |
+
2. Join the two directly.
|
| 90 |
+
"""
|
| 91 |
+
while entry and entry[0] == "..":
|
| 92 |
+
if not info or info[-1] == "..":
|
| 93 |
+
info += ("..",)
|
| 94 |
+
else:
|
| 95 |
+
info = info[:-1]
|
| 96 |
+
entry = entry[1:]
|
| 97 |
+
return str(pathlib.Path(*info, *entry))
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class RequiresEntry(NamedTuple):
|
| 101 |
+
requirement: str
|
| 102 |
+
extra: str
|
| 103 |
+
marker: str
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class BaseDistribution(Protocol):
|
| 107 |
+
@classmethod
|
| 108 |
+
def from_directory(cls, directory: str) -> "BaseDistribution":
|
| 109 |
+
"""Load the distribution from a metadata directory.
|
| 110 |
+
|
| 111 |
+
:param directory: Path to a metadata directory, e.g. ``.dist-info``.
|
| 112 |
+
"""
|
| 113 |
+
raise NotImplementedError()
|
| 114 |
+
|
| 115 |
+
@classmethod
|
| 116 |
+
def from_metadata_file_contents(
|
| 117 |
+
cls,
|
| 118 |
+
metadata_contents: bytes,
|
| 119 |
+
filename: str,
|
| 120 |
+
project_name: str,
|
| 121 |
+
) -> "BaseDistribution":
|
| 122 |
+
"""Load the distribution from the contents of a METADATA file.
|
| 123 |
+
|
| 124 |
+
This is used to implement PEP 658 by generating a "shallow" dist object that can
|
| 125 |
+
be used for resolution without downloading or building the actual dist yet.
|
| 126 |
+
|
| 127 |
+
:param metadata_contents: The contents of a METADATA file.
|
| 128 |
+
:param filename: File name for the dist with this metadata.
|
| 129 |
+
:param project_name: Name of the project this dist represents.
|
| 130 |
+
"""
|
| 131 |
+
raise NotImplementedError()
|
| 132 |
+
|
| 133 |
+
@classmethod
|
| 134 |
+
def from_wheel(cls, wheel: "Wheel", name: str) -> "BaseDistribution":
|
| 135 |
+
"""Load the distribution from a given wheel.
|
| 136 |
+
|
| 137 |
+
:param wheel: A concrete wheel definition.
|
| 138 |
+
:param name: File name of the wheel.
|
| 139 |
+
|
| 140 |
+
:raises InvalidWheel: Whenever loading of the wheel causes a
|
| 141 |
+
:py:exc:`zipfile.BadZipFile` exception to be thrown.
|
| 142 |
+
:raises UnsupportedWheel: If the wheel is a valid zip, but malformed
|
| 143 |
+
internally.
|
| 144 |
+
"""
|
| 145 |
+
raise NotImplementedError()
|
| 146 |
+
|
| 147 |
+
def __repr__(self) -> str:
|
| 148 |
+
return f"{self.raw_name} {self.version} ({self.location})"
|
| 149 |
+
|
| 150 |
+
def __str__(self) -> str:
|
| 151 |
+
return f"{self.raw_name} {self.version}"
|
| 152 |
+
|
| 153 |
+
@property
|
| 154 |
+
def location(self) -> Optional[str]:
|
| 155 |
+
"""Where the distribution is loaded from.
|
| 156 |
+
|
| 157 |
+
A string value is not necessarily a filesystem path, since distributions
|
| 158 |
+
can be loaded from other sources, e.g. arbitrary zip archives. ``None``
|
| 159 |
+
means the distribution is created in-memory.
|
| 160 |
+
|
| 161 |
+
Do not canonicalize this value with e.g. ``pathlib.Path.resolve()``. If
|
| 162 |
+
this is a symbolic link, we want to preserve the relative path between
|
| 163 |
+
it and files in the distribution.
|
| 164 |
+
"""
|
| 165 |
+
raise NotImplementedError()
|
| 166 |
+
|
| 167 |
+
@property
|
| 168 |
+
def editable_project_location(self) -> Optional[str]:
|
| 169 |
+
"""The project location for editable distributions.
|
| 170 |
+
|
| 171 |
+
This is the directory where pyproject.toml or setup.py is located.
|
| 172 |
+
None if the distribution is not installed in editable mode.
|
| 173 |
+
"""
|
| 174 |
+
# TODO: this property is relatively costly to compute, memoize it ?
|
| 175 |
+
direct_url = self.direct_url
|
| 176 |
+
if direct_url:
|
| 177 |
+
if direct_url.is_local_editable():
|
| 178 |
+
return url_to_path(direct_url.url)
|
| 179 |
+
else:
|
| 180 |
+
# Search for an .egg-link file by walking sys.path, as it was
|
| 181 |
+
# done before by dist_is_editable().
|
| 182 |
+
egg_link_path = egg_link_path_from_sys_path(self.raw_name)
|
| 183 |
+
if egg_link_path:
|
| 184 |
+
# TODO: get project location from second line of egg_link file
|
| 185 |
+
# (https://github.com/pypa/pip/issues/10243)
|
| 186 |
+
return self.location
|
| 187 |
+
return None
|
| 188 |
+
|
| 189 |
+
@property
|
| 190 |
+
def installed_location(self) -> Optional[str]:
|
| 191 |
+
"""The distribution's "installed" location.
|
| 192 |
+
|
| 193 |
+
This should generally be a ``site-packages`` directory. This is
|
| 194 |
+
usually ``dist.location``, except for legacy develop-installed packages,
|
| 195 |
+
where ``dist.location`` is the source code location, and this is where
|
| 196 |
+
the ``.egg-link`` file is.
|
| 197 |
+
|
| 198 |
+
The returned location is normalized (in particular, with symlinks removed).
|
| 199 |
+
"""
|
| 200 |
+
raise NotImplementedError()
|
| 201 |
+
|
| 202 |
+
@property
|
| 203 |
+
def info_location(self) -> Optional[str]:
|
| 204 |
+
"""Location of the .[egg|dist]-info directory or file.
|
| 205 |
+
|
| 206 |
+
Similarly to ``location``, a string value is not necessarily a
|
| 207 |
+
filesystem path. ``None`` means the distribution is created in-memory.
|
| 208 |
+
|
| 209 |
+
For a modern .dist-info installation on disk, this should be something
|
| 210 |
+
like ``{location}/{raw_name}-{version}.dist-info``.
|
| 211 |
+
|
| 212 |
+
Do not canonicalize this value with e.g. ``pathlib.Path.resolve()``. If
|
| 213 |
+
this is a symbolic link, we want to preserve the relative path between
|
| 214 |
+
it and other files in the distribution.
|
| 215 |
+
"""
|
| 216 |
+
raise NotImplementedError()
|
| 217 |
+
|
| 218 |
+
@property
|
| 219 |
+
def installed_by_distutils(self) -> bool:
|
| 220 |
+
"""Whether this distribution is installed with legacy distutils format.
|
| 221 |
+
|
| 222 |
+
A distribution installed with "raw" distutils not patched by setuptools
|
| 223 |
+
uses one single file at ``info_location`` to store metadata. We need to
|
| 224 |
+
treat this specially on uninstallation.
|
| 225 |
+
"""
|
| 226 |
+
info_location = self.info_location
|
| 227 |
+
if not info_location:
|
| 228 |
+
return False
|
| 229 |
+
return pathlib.Path(info_location).is_file()
|
| 230 |
+
|
| 231 |
+
@property
|
| 232 |
+
def installed_as_egg(self) -> bool:
|
| 233 |
+
"""Whether this distribution is installed as an egg.
|
| 234 |
+
|
| 235 |
+
This usually indicates the distribution was installed by (older versions
|
| 236 |
+
of) easy_install.
|
| 237 |
+
"""
|
| 238 |
+
location = self.location
|
| 239 |
+
if not location:
|
| 240 |
+
return False
|
| 241 |
+
return location.endswith(".egg")
|
| 242 |
+
|
| 243 |
+
@property
|
| 244 |
+
def installed_with_setuptools_egg_info(self) -> bool:
|
| 245 |
+
"""Whether this distribution is installed with the ``.egg-info`` format.
|
| 246 |
+
|
| 247 |
+
This usually indicates the distribution was installed with setuptools
|
| 248 |
+
with an old pip version or with ``single-version-externally-managed``.
|
| 249 |
+
|
| 250 |
+
Note that this ensure the metadata store is a directory. distutils can
|
| 251 |
+
also installs an ``.egg-info``, but as a file, not a directory. This
|
| 252 |
+
property is *False* for that case. Also see ``installed_by_distutils``.
|
| 253 |
+
"""
|
| 254 |
+
info_location = self.info_location
|
| 255 |
+
if not info_location:
|
| 256 |
+
return False
|
| 257 |
+
if not info_location.endswith(".egg-info"):
|
| 258 |
+
return False
|
| 259 |
+
return pathlib.Path(info_location).is_dir()
|
| 260 |
+
|
| 261 |
+
@property
|
| 262 |
+
def installed_with_dist_info(self) -> bool:
|
| 263 |
+
"""Whether this distribution is installed with the "modern format".
|
| 264 |
+
|
| 265 |
+
This indicates a "modern" installation, e.g. storing metadata in the
|
| 266 |
+
``.dist-info`` directory. This applies to installations made by
|
| 267 |
+
setuptools (but through pip, not directly), or anything using the
|
| 268 |
+
standardized build backend interface (PEP 517).
|
| 269 |
+
"""
|
| 270 |
+
info_location = self.info_location
|
| 271 |
+
if not info_location:
|
| 272 |
+
return False
|
| 273 |
+
if not info_location.endswith(".dist-info"):
|
| 274 |
+
return False
|
| 275 |
+
return pathlib.Path(info_location).is_dir()
|
| 276 |
+
|
| 277 |
+
@property
|
| 278 |
+
def canonical_name(self) -> NormalizedName:
|
| 279 |
+
raise NotImplementedError()
|
| 280 |
+
|
| 281 |
+
@property
|
| 282 |
+
def version(self) -> DistributionVersion:
|
| 283 |
+
raise NotImplementedError()
|
| 284 |
+
|
| 285 |
+
@property
|
| 286 |
+
def setuptools_filename(self) -> str:
|
| 287 |
+
"""Convert a project name to its setuptools-compatible filename.
|
| 288 |
+
|
| 289 |
+
This is a copy of ``pkg_resources.to_filename()`` for compatibility.
|
| 290 |
+
"""
|
| 291 |
+
return self.raw_name.replace("-", "_")
|
| 292 |
+
|
| 293 |
+
@property
|
| 294 |
+
def direct_url(self) -> Optional[DirectUrl]:
|
| 295 |
+
"""Obtain a DirectUrl from this distribution.
|
| 296 |
+
|
| 297 |
+
Returns None if the distribution has no `direct_url.json` metadata,
|
| 298 |
+
or if `direct_url.json` is invalid.
|
| 299 |
+
"""
|
| 300 |
+
try:
|
| 301 |
+
content = self.read_text(DIRECT_URL_METADATA_NAME)
|
| 302 |
+
except FileNotFoundError:
|
| 303 |
+
return None
|
| 304 |
+
try:
|
| 305 |
+
return DirectUrl.from_json(content)
|
| 306 |
+
except (
|
| 307 |
+
UnicodeDecodeError,
|
| 308 |
+
json.JSONDecodeError,
|
| 309 |
+
DirectUrlValidationError,
|
| 310 |
+
) as e:
|
| 311 |
+
logger.warning(
|
| 312 |
+
"Error parsing %s for %s: %s",
|
| 313 |
+
DIRECT_URL_METADATA_NAME,
|
| 314 |
+
self.canonical_name,
|
| 315 |
+
e,
|
| 316 |
+
)
|
| 317 |
+
return None
|
| 318 |
+
|
| 319 |
+
@property
|
| 320 |
+
def installer(self) -> str:
|
| 321 |
+
try:
|
| 322 |
+
installer_text = self.read_text("INSTALLER")
|
| 323 |
+
except (OSError, ValueError, NoneMetadataError):
|
| 324 |
+
return "" # Fail silently if the installer file cannot be read.
|
| 325 |
+
for line in installer_text.splitlines():
|
| 326 |
+
cleaned_line = line.strip()
|
| 327 |
+
if cleaned_line:
|
| 328 |
+
return cleaned_line
|
| 329 |
+
return ""
|
| 330 |
+
|
| 331 |
+
@property
|
| 332 |
+
def requested(self) -> bool:
|
| 333 |
+
return self.is_file("REQUESTED")
|
| 334 |
+
|
| 335 |
+
@property
|
| 336 |
+
def editable(self) -> bool:
|
| 337 |
+
return bool(self.editable_project_location)
|
| 338 |
+
|
| 339 |
+
@property
|
| 340 |
+
def local(self) -> bool:
|
| 341 |
+
"""If distribution is installed in the current virtual environment.
|
| 342 |
+
|
| 343 |
+
Always True if we're not in a virtualenv.
|
| 344 |
+
"""
|
| 345 |
+
if self.installed_location is None:
|
| 346 |
+
return False
|
| 347 |
+
return is_local(self.installed_location)
|
| 348 |
+
|
| 349 |
+
@property
|
| 350 |
+
def in_usersite(self) -> bool:
|
| 351 |
+
if self.installed_location is None or user_site is None:
|
| 352 |
+
return False
|
| 353 |
+
return self.installed_location.startswith(normalize_path(user_site))
|
| 354 |
+
|
| 355 |
+
@property
|
| 356 |
+
def in_site_packages(self) -> bool:
|
| 357 |
+
if self.installed_location is None or site_packages is None:
|
| 358 |
+
return False
|
| 359 |
+
return self.installed_location.startswith(normalize_path(site_packages))
|
| 360 |
+
|
| 361 |
+
def is_file(self, path: InfoPath) -> bool:
|
| 362 |
+
"""Check whether an entry in the info directory is a file."""
|
| 363 |
+
raise NotImplementedError()
|
| 364 |
+
|
| 365 |
+
def iter_distutils_script_names(self) -> Iterator[str]:
|
| 366 |
+
"""Find distutils 'scripts' entries metadata.
|
| 367 |
+
|
| 368 |
+
If 'scripts' is supplied in ``setup.py``, distutils records those in the
|
| 369 |
+
installed distribution's ``scripts`` directory, a file for each script.
|
| 370 |
+
"""
|
| 371 |
+
raise NotImplementedError()
|
| 372 |
+
|
| 373 |
+
def read_text(self, path: InfoPath) -> str:
|
| 374 |
+
"""Read a file in the info directory.
|
| 375 |
+
|
| 376 |
+
:raise FileNotFoundError: If ``path`` does not exist in the directory.
|
| 377 |
+
:raise NoneMetadataError: If ``path`` exists in the info directory, but
|
| 378 |
+
cannot be read.
|
| 379 |
+
"""
|
| 380 |
+
raise NotImplementedError()
|
| 381 |
+
|
| 382 |
+
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
|
| 383 |
+
raise NotImplementedError()
|
| 384 |
+
|
| 385 |
+
def _metadata_impl(self) -> email.message.Message:
|
| 386 |
+
raise NotImplementedError()
|
| 387 |
+
|
| 388 |
+
@functools.lru_cache(maxsize=1)
|
| 389 |
+
def _metadata_cached(self) -> email.message.Message:
|
| 390 |
+
# When we drop python 3.7 support, move this to the metadata property and use
|
| 391 |
+
# functools.cached_property instead of lru_cache.
|
| 392 |
+
metadata = self._metadata_impl()
|
| 393 |
+
self._add_egg_info_requires(metadata)
|
| 394 |
+
return metadata
|
| 395 |
+
|
| 396 |
+
@property
|
| 397 |
+
def metadata(self) -> email.message.Message:
|
| 398 |
+
"""Metadata of distribution parsed from e.g. METADATA or PKG-INFO.
|
| 399 |
+
|
| 400 |
+
This should return an empty message if the metadata file is unavailable.
|
| 401 |
+
|
| 402 |
+
:raises NoneMetadataError: If the metadata file is available, but does
|
| 403 |
+
not contain valid metadata.
|
| 404 |
+
"""
|
| 405 |
+
return self._metadata_cached()
|
| 406 |
+
|
| 407 |
+
@property
|
| 408 |
+
def metadata_dict(self) -> Dict[str, Any]:
|
| 409 |
+
"""PEP 566 compliant JSON-serializable representation of METADATA or PKG-INFO.
|
| 410 |
+
|
| 411 |
+
This should return an empty dict if the metadata file is unavailable.
|
| 412 |
+
|
| 413 |
+
:raises NoneMetadataError: If the metadata file is available, but does
|
| 414 |
+
not contain valid metadata.
|
| 415 |
+
"""
|
| 416 |
+
return msg_to_json(self.metadata)
|
| 417 |
+
|
| 418 |
+
@property
|
| 419 |
+
def metadata_version(self) -> Optional[str]:
|
| 420 |
+
"""Value of "Metadata-Version:" in distribution metadata, if available."""
|
| 421 |
+
return self.metadata.get("Metadata-Version")
|
| 422 |
+
|
| 423 |
+
@property
|
| 424 |
+
def raw_name(self) -> str:
|
| 425 |
+
"""Value of "Name:" in distribution metadata."""
|
| 426 |
+
# The metadata should NEVER be missing the Name: key, but if it somehow
|
| 427 |
+
# does, fall back to the known canonical name.
|
| 428 |
+
return self.metadata.get("Name", self.canonical_name)
|
| 429 |
+
|
| 430 |
+
@property
|
| 431 |
+
def requires_python(self) -> SpecifierSet:
|
| 432 |
+
"""Value of "Requires-Python:" in distribution metadata.
|
| 433 |
+
|
| 434 |
+
If the key does not exist or contains an invalid value, an empty
|
| 435 |
+
SpecifierSet should be returned.
|
| 436 |
+
"""
|
| 437 |
+
value = self.metadata.get("Requires-Python")
|
| 438 |
+
if value is None:
|
| 439 |
+
return SpecifierSet()
|
| 440 |
+
try:
|
| 441 |
+
# Convert to str to satisfy the type checker; this can be a Header object.
|
| 442 |
+
spec = SpecifierSet(str(value))
|
| 443 |
+
except InvalidSpecifier as e:
|
| 444 |
+
message = "Package %r has an invalid Requires-Python: %s"
|
| 445 |
+
logger.warning(message, self.raw_name, e)
|
| 446 |
+
return SpecifierSet()
|
| 447 |
+
return spec
|
| 448 |
+
|
| 449 |
+
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
|
| 450 |
+
"""Dependencies of this distribution.
|
| 451 |
+
|
| 452 |
+
For modern .dist-info distributions, this is the collection of
|
| 453 |
+
"Requires-Dist:" entries in distribution metadata.
|
| 454 |
+
"""
|
| 455 |
+
raise NotImplementedError()
|
| 456 |
+
|
| 457 |
+
def iter_provided_extras(self) -> Iterable[str]:
|
| 458 |
+
"""Extras provided by this distribution.
|
| 459 |
+
|
| 460 |
+
For modern .dist-info distributions, this is the collection of
|
| 461 |
+
"Provides-Extra:" entries in distribution metadata.
|
| 462 |
+
|
| 463 |
+
The return value of this function is not particularly useful other than
|
| 464 |
+
display purposes due to backward compatibility issues and the extra
|
| 465 |
+
names being poorly normalized prior to PEP 685. If you want to perform
|
| 466 |
+
logic operations on extras, use :func:`is_extra_provided` instead.
|
| 467 |
+
"""
|
| 468 |
+
raise NotImplementedError()
|
| 469 |
+
|
| 470 |
+
def is_extra_provided(self, extra: str) -> bool:
|
| 471 |
+
"""Check whether an extra is provided by this distribution.
|
| 472 |
+
|
| 473 |
+
This is needed mostly for compatibility issues with pkg_resources not
|
| 474 |
+
following the extra normalization rules defined in PEP 685.
|
| 475 |
+
"""
|
| 476 |
+
raise NotImplementedError()
|
| 477 |
+
|
| 478 |
+
def _iter_declared_entries_from_record(self) -> Optional[Iterator[str]]:
|
| 479 |
+
try:
|
| 480 |
+
text = self.read_text("RECORD")
|
| 481 |
+
except FileNotFoundError:
|
| 482 |
+
return None
|
| 483 |
+
# This extra Path-str cast normalizes entries.
|
| 484 |
+
return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
|
| 485 |
+
|
| 486 |
+
def _iter_declared_entries_from_legacy(self) -> Optional[Iterator[str]]:
|
| 487 |
+
try:
|
| 488 |
+
text = self.read_text("installed-files.txt")
|
| 489 |
+
except FileNotFoundError:
|
| 490 |
+
return None
|
| 491 |
+
paths = (p for p in text.splitlines(keepends=False) if p)
|
| 492 |
+
root = self.location
|
| 493 |
+
info = self.info_location
|
| 494 |
+
if root is None or info is None:
|
| 495 |
+
return paths
|
| 496 |
+
try:
|
| 497 |
+
info_rel = pathlib.Path(info).relative_to(root)
|
| 498 |
+
except ValueError: # info is not relative to root.
|
| 499 |
+
return paths
|
| 500 |
+
if not info_rel.parts: # info *is* root.
|
| 501 |
+
return paths
|
| 502 |
+
return (
|
| 503 |
+
_convert_installed_files_path(pathlib.Path(p).parts, info_rel.parts)
|
| 504 |
+
for p in paths
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
def iter_declared_entries(self) -> Optional[Iterator[str]]:
|
| 508 |
+
"""Iterate through file entries declared in this distribution.
|
| 509 |
+
|
| 510 |
+
For modern .dist-info distributions, this is the files listed in the
|
| 511 |
+
``RECORD`` metadata file. For legacy setuptools distributions, this
|
| 512 |
+
comes from ``installed-files.txt``, with entries normalized to be
|
| 513 |
+
compatible with the format used by ``RECORD``.
|
| 514 |
+
|
| 515 |
+
:return: An iterator for listed entries, or None if the distribution
|
| 516 |
+
contains neither ``RECORD`` nor ``installed-files.txt``.
|
| 517 |
+
"""
|
| 518 |
+
return (
|
| 519 |
+
self._iter_declared_entries_from_record()
|
| 520 |
+
or self._iter_declared_entries_from_legacy()
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
def _iter_requires_txt_entries(self) -> Iterator[RequiresEntry]:
|
| 524 |
+
"""Parse a ``requires.txt`` in an egg-info directory.
|
| 525 |
+
|
| 526 |
+
This is an INI-ish format where an egg-info stores dependencies. A
|
| 527 |
+
section name describes extra other environment markers, while each entry
|
| 528 |
+
is an arbitrary string (not a key-value pair) representing a dependency
|
| 529 |
+
as a requirement string (no markers).
|
| 530 |
+
|
| 531 |
+
There is a construct in ``importlib.metadata`` called ``Sectioned`` that
|
| 532 |
+
does mostly the same, but the format is currently considered private.
|
| 533 |
+
"""
|
| 534 |
+
try:
|
| 535 |
+
content = self.read_text("requires.txt")
|
| 536 |
+
except FileNotFoundError:
|
| 537 |
+
return
|
| 538 |
+
extra = marker = "" # Section-less entries don't have markers.
|
| 539 |
+
for line in content.splitlines():
|
| 540 |
+
line = line.strip()
|
| 541 |
+
if not line or line.startswith("#"): # Comment; ignored.
|
| 542 |
+
continue
|
| 543 |
+
if line.startswith("[") and line.endswith("]"): # A section header.
|
| 544 |
+
extra, _, marker = line.strip("[]").partition(":")
|
| 545 |
+
continue
|
| 546 |
+
yield RequiresEntry(requirement=line, extra=extra, marker=marker)
|
| 547 |
+
|
| 548 |
+
def _iter_egg_info_extras(self) -> Iterable[str]:
|
| 549 |
+
"""Get extras from the egg-info directory."""
|
| 550 |
+
known_extras = {""}
|
| 551 |
+
for entry in self._iter_requires_txt_entries():
|
| 552 |
+
extra = canonicalize_name(entry.extra)
|
| 553 |
+
if extra in known_extras:
|
| 554 |
+
continue
|
| 555 |
+
known_extras.add(extra)
|
| 556 |
+
yield extra
|
| 557 |
+
|
| 558 |
+
def _iter_egg_info_dependencies(self) -> Iterable[str]:
|
| 559 |
+
"""Get distribution dependencies from the egg-info directory.
|
| 560 |
+
|
| 561 |
+
To ease parsing, this converts a legacy dependency entry into a PEP 508
|
| 562 |
+
requirement string. Like ``_iter_requires_txt_entries()``, there is code
|
| 563 |
+
in ``importlib.metadata`` that does mostly the same, but not do exactly
|
| 564 |
+
what we need.
|
| 565 |
+
|
| 566 |
+
Namely, ``importlib.metadata`` does not normalize the extra name before
|
| 567 |
+
putting it into the requirement string, which causes marker comparison
|
| 568 |
+
to fail because the dist-info format do normalize. This is consistent in
|
| 569 |
+
all currently available PEP 517 backends, although not standardized.
|
| 570 |
+
"""
|
| 571 |
+
for entry in self._iter_requires_txt_entries():
|
| 572 |
+
extra = canonicalize_name(entry.extra)
|
| 573 |
+
if extra and entry.marker:
|
| 574 |
+
marker = f'({entry.marker}) and extra == "{extra}"'
|
| 575 |
+
elif extra:
|
| 576 |
+
marker = f'extra == "{extra}"'
|
| 577 |
+
elif entry.marker:
|
| 578 |
+
marker = entry.marker
|
| 579 |
+
else:
|
| 580 |
+
marker = ""
|
| 581 |
+
if marker:
|
| 582 |
+
yield f"{entry.requirement} ; {marker}"
|
| 583 |
+
else:
|
| 584 |
+
yield entry.requirement
|
| 585 |
+
|
| 586 |
+
def _add_egg_info_requires(self, metadata: email.message.Message) -> None:
|
| 587 |
+
"""Add egg-info requires.txt information to the metadata."""
|
| 588 |
+
if not metadata.get_all("Requires-Dist"):
|
| 589 |
+
for dep in self._iter_egg_info_dependencies():
|
| 590 |
+
metadata["Requires-Dist"] = dep
|
| 591 |
+
if not metadata.get_all("Provides-Extra"):
|
| 592 |
+
for extra in self._iter_egg_info_extras():
|
| 593 |
+
metadata["Provides-Extra"] = extra
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
class BaseEnvironment:
|
| 597 |
+
"""An environment containing distributions to introspect."""
|
| 598 |
+
|
| 599 |
+
@classmethod
|
| 600 |
+
def default(cls) -> "BaseEnvironment":
|
| 601 |
+
raise NotImplementedError()
|
| 602 |
+
|
| 603 |
+
@classmethod
|
| 604 |
+
def from_paths(cls, paths: Optional[List[str]]) -> "BaseEnvironment":
|
| 605 |
+
raise NotImplementedError()
|
| 606 |
+
|
| 607 |
+
def get_distribution(self, name: str) -> Optional["BaseDistribution"]:
|
| 608 |
+
"""Given a requirement name, return the installed distributions.
|
| 609 |
+
|
| 610 |
+
The name may not be normalized. The implementation must canonicalize
|
| 611 |
+
it for lookup.
|
| 612 |
+
"""
|
| 613 |
+
raise NotImplementedError()
|
| 614 |
+
|
| 615 |
+
def _iter_distributions(self) -> Iterator["BaseDistribution"]:
|
| 616 |
+
"""Iterate through installed distributions.
|
| 617 |
+
|
| 618 |
+
This function should be implemented by subclass, but never called
|
| 619 |
+
directly. Use the public ``iter_distribution()`` instead, which
|
| 620 |
+
implements additional logic to make sure the distributions are valid.
|
| 621 |
+
"""
|
| 622 |
+
raise NotImplementedError()
|
| 623 |
+
|
| 624 |
+
def iter_all_distributions(self) -> Iterator[BaseDistribution]:
|
| 625 |
+
"""Iterate through all installed distributions without any filtering."""
|
| 626 |
+
for dist in self._iter_distributions():
|
| 627 |
+
# Make sure the distribution actually comes from a valid Python
|
| 628 |
+
# packaging distribution. Pip's AdjacentTempDirectory leaves folders
|
| 629 |
+
# e.g. ``~atplotlib.dist-info`` if cleanup was interrupted. The
|
| 630 |
+
# valid project name pattern is taken from PEP 508.
|
| 631 |
+
project_name_valid = re.match(
|
| 632 |
+
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$",
|
| 633 |
+
dist.canonical_name,
|
| 634 |
+
flags=re.IGNORECASE,
|
| 635 |
+
)
|
| 636 |
+
if not project_name_valid:
|
| 637 |
+
logger.warning(
|
| 638 |
+
"Ignoring invalid distribution %s (%s)",
|
| 639 |
+
dist.canonical_name,
|
| 640 |
+
dist.location,
|
| 641 |
+
)
|
| 642 |
+
continue
|
| 643 |
+
yield dist
|
| 644 |
+
|
| 645 |
+
def iter_installed_distributions(
|
| 646 |
+
self,
|
| 647 |
+
local_only: bool = True,
|
| 648 |
+
skip: Container[str] = stdlib_pkgs,
|
| 649 |
+
include_editables: bool = True,
|
| 650 |
+
editables_only: bool = False,
|
| 651 |
+
user_only: bool = False,
|
| 652 |
+
) -> Iterator[BaseDistribution]:
|
| 653 |
+
"""Return a list of installed distributions.
|
| 654 |
+
|
| 655 |
+
This is based on ``iter_all_distributions()`` with additional filtering
|
| 656 |
+
options. Note that ``iter_installed_distributions()`` without arguments
|
| 657 |
+
is *not* equal to ``iter_all_distributions()``, since some of the
|
| 658 |
+
configurations exclude packages by default.
|
| 659 |
+
|
| 660 |
+
:param local_only: If True (default), only return installations
|
| 661 |
+
local to the current virtualenv, if in a virtualenv.
|
| 662 |
+
:param skip: An iterable of canonicalized project names to ignore;
|
| 663 |
+
defaults to ``stdlib_pkgs``.
|
| 664 |
+
:param include_editables: If False, don't report editables.
|
| 665 |
+
:param editables_only: If True, only report editables.
|
| 666 |
+
:param user_only: If True, only report installations in the user
|
| 667 |
+
site directory.
|
| 668 |
+
"""
|
| 669 |
+
it = self.iter_all_distributions()
|
| 670 |
+
if local_only:
|
| 671 |
+
it = (d for d in it if d.local)
|
| 672 |
+
if not include_editables:
|
| 673 |
+
it = (d for d in it if not d.editable)
|
| 674 |
+
if editables_only:
|
| 675 |
+
it = (d for d in it if d.editable)
|
| 676 |
+
if user_only:
|
| 677 |
+
it = (d for d in it if d.in_usersite)
|
| 678 |
+
return (d for d in it if d.canonical_name not in skip)
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
class Wheel(Protocol):
|
| 682 |
+
location: str
|
| 683 |
+
|
| 684 |
+
def as_zipfile(self) -> zipfile.ZipFile:
|
| 685 |
+
raise NotImplementedError()
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
class FilesystemWheel(Wheel):
|
| 689 |
+
def __init__(self, location: str) -> None:
|
| 690 |
+
self.location = location
|
| 691 |
+
|
| 692 |
+
def as_zipfile(self) -> zipfile.ZipFile:
|
| 693 |
+
return zipfile.ZipFile(self.location, allowZip64=True)
|
| 694 |
+
|
| 695 |
+
|
| 696 |
+
class MemoryWheel(Wheel):
|
| 697 |
+
def __init__(self, location: str, stream: IO[bytes]) -> None:
|
| 698 |
+
self.location = location
|
| 699 |
+
self.stream = stream
|
| 700 |
+
|
| 701 |
+
def as_zipfile(self) -> zipfile.ZipFile:
|
| 702 |
+
return zipfile.ZipFile(self.stream, allowZip64=True)
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/importlib/__pycache__/_dists.cpython-311.pyc
ADDED
|
Binary file (14.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_compat.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.metadata
|
| 2 |
+
from typing import Any, Optional, Protocol, cast
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class BadMetadata(ValueError):
|
| 6 |
+
def __init__(self, dist: importlib.metadata.Distribution, *, reason: str) -> None:
|
| 7 |
+
self.dist = dist
|
| 8 |
+
self.reason = reason
|
| 9 |
+
|
| 10 |
+
def __str__(self) -> str:
|
| 11 |
+
return f"Bad metadata in {self.dist} ({self.reason})"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class BasePath(Protocol):
|
| 15 |
+
"""A protocol that various path objects conform.
|
| 16 |
+
|
| 17 |
+
This exists because importlib.metadata uses both ``pathlib.Path`` and
|
| 18 |
+
``zipfile.Path``, and we need a common base for type hints (Union does not
|
| 19 |
+
work well since ``zipfile.Path`` is too new for our linter setup).
|
| 20 |
+
|
| 21 |
+
This does not mean to be exhaustive, but only contains things that present
|
| 22 |
+
in both classes *that we need*.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
@property
|
| 26 |
+
def name(self) -> str:
|
| 27 |
+
raise NotImplementedError()
|
| 28 |
+
|
| 29 |
+
@property
|
| 30 |
+
def parent(self) -> "BasePath":
|
| 31 |
+
raise NotImplementedError()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_info_location(d: importlib.metadata.Distribution) -> Optional[BasePath]:
|
| 35 |
+
"""Find the path to the distribution's metadata directory.
|
| 36 |
+
|
| 37 |
+
HACK: This relies on importlib.metadata's private ``_path`` attribute. Not
|
| 38 |
+
all distributions exist on disk, so importlib.metadata is correct to not
|
| 39 |
+
expose the attribute as public. But pip's code base is old and not as clean,
|
| 40 |
+
so we do this to avoid having to rewrite too many things. Hopefully we can
|
| 41 |
+
eliminate this some day.
|
| 42 |
+
"""
|
| 43 |
+
return getattr(d, "_path", None)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def get_dist_name(dist: importlib.metadata.Distribution) -> str:
|
| 47 |
+
"""Get the distribution's project name.
|
| 48 |
+
|
| 49 |
+
The ``name`` attribute is only available in Python 3.10 or later. We are
|
| 50 |
+
targeting exactly that, but Mypy does not know this.
|
| 51 |
+
"""
|
| 52 |
+
name = cast(Any, dist).name
|
| 53 |
+
if not isinstance(name, str):
|
| 54 |
+
raise BadMetadata(dist, reason="invalid metadata entry 'name'")
|
| 55 |
+
return name
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_dists.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import email.message
|
| 2 |
+
import importlib.metadata
|
| 3 |
+
import os
|
| 4 |
+
import pathlib
|
| 5 |
+
import zipfile
|
| 6 |
+
from typing import (
|
| 7 |
+
Collection,
|
| 8 |
+
Dict,
|
| 9 |
+
Iterable,
|
| 10 |
+
Iterator,
|
| 11 |
+
Mapping,
|
| 12 |
+
Optional,
|
| 13 |
+
Sequence,
|
| 14 |
+
cast,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
from pip._vendor.packaging.requirements import Requirement
|
| 18 |
+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
| 19 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 20 |
+
|
| 21 |
+
from pip._internal.exceptions import InvalidWheel, UnsupportedWheel
|
| 22 |
+
from pip._internal.metadata.base import (
|
| 23 |
+
BaseDistribution,
|
| 24 |
+
BaseEntryPoint,
|
| 25 |
+
DistributionVersion,
|
| 26 |
+
InfoPath,
|
| 27 |
+
Wheel,
|
| 28 |
+
)
|
| 29 |
+
from pip._internal.utils.misc import normalize_path
|
| 30 |
+
from pip._internal.utils.temp_dir import TempDirectory
|
| 31 |
+
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
|
| 32 |
+
|
| 33 |
+
from ._compat import BasePath, get_dist_name
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class WheelDistribution(importlib.metadata.Distribution):
|
| 37 |
+
"""An ``importlib.metadata.Distribution`` read from a wheel.
|
| 38 |
+
|
| 39 |
+
Although ``importlib.metadata.PathDistribution`` accepts ``zipfile.Path``,
|
| 40 |
+
its implementation is too "lazy" for pip's needs (we can't keep the ZipFile
|
| 41 |
+
handle open for the entire lifetime of the distribution object).
|
| 42 |
+
|
| 43 |
+
This implementation eagerly reads the entire metadata directory into the
|
| 44 |
+
memory instead, and operates from that.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def __init__(
|
| 48 |
+
self,
|
| 49 |
+
files: Mapping[pathlib.PurePosixPath, bytes],
|
| 50 |
+
info_location: pathlib.PurePosixPath,
|
| 51 |
+
) -> None:
|
| 52 |
+
self._files = files
|
| 53 |
+
self.info_location = info_location
|
| 54 |
+
|
| 55 |
+
@classmethod
|
| 56 |
+
def from_zipfile(
|
| 57 |
+
cls,
|
| 58 |
+
zf: zipfile.ZipFile,
|
| 59 |
+
name: str,
|
| 60 |
+
location: str,
|
| 61 |
+
) -> "WheelDistribution":
|
| 62 |
+
info_dir, _ = parse_wheel(zf, name)
|
| 63 |
+
paths = (
|
| 64 |
+
(name, pathlib.PurePosixPath(name.split("/", 1)[-1]))
|
| 65 |
+
for name in zf.namelist()
|
| 66 |
+
if name.startswith(f"{info_dir}/")
|
| 67 |
+
)
|
| 68 |
+
files = {
|
| 69 |
+
relpath: read_wheel_metadata_file(zf, fullpath)
|
| 70 |
+
for fullpath, relpath in paths
|
| 71 |
+
}
|
| 72 |
+
info_location = pathlib.PurePosixPath(location, info_dir)
|
| 73 |
+
return cls(files, info_location)
|
| 74 |
+
|
| 75 |
+
def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
|
| 76 |
+
# Only allow iterating through the metadata directory.
|
| 77 |
+
if pathlib.PurePosixPath(str(path)) in self._files:
|
| 78 |
+
return iter(self._files)
|
| 79 |
+
raise FileNotFoundError(path)
|
| 80 |
+
|
| 81 |
+
def read_text(self, filename: str) -> Optional[str]:
|
| 82 |
+
try:
|
| 83 |
+
data = self._files[pathlib.PurePosixPath(filename)]
|
| 84 |
+
except KeyError:
|
| 85 |
+
return None
|
| 86 |
+
try:
|
| 87 |
+
text = data.decode("utf-8")
|
| 88 |
+
except UnicodeDecodeError as e:
|
| 89 |
+
wheel = self.info_location.parent
|
| 90 |
+
error = f"Error decoding metadata for {wheel}: {e} in {filename} file"
|
| 91 |
+
raise UnsupportedWheel(error)
|
| 92 |
+
return text
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class Distribution(BaseDistribution):
|
| 96 |
+
def __init__(
|
| 97 |
+
self,
|
| 98 |
+
dist: importlib.metadata.Distribution,
|
| 99 |
+
info_location: Optional[BasePath],
|
| 100 |
+
installed_location: Optional[BasePath],
|
| 101 |
+
) -> None:
|
| 102 |
+
self._dist = dist
|
| 103 |
+
self._info_location = info_location
|
| 104 |
+
self._installed_location = installed_location
|
| 105 |
+
|
| 106 |
+
@classmethod
|
| 107 |
+
def from_directory(cls, directory: str) -> BaseDistribution:
|
| 108 |
+
info_location = pathlib.Path(directory)
|
| 109 |
+
dist = importlib.metadata.Distribution.at(info_location)
|
| 110 |
+
return cls(dist, info_location, info_location.parent)
|
| 111 |
+
|
| 112 |
+
@classmethod
|
| 113 |
+
def from_metadata_file_contents(
|
| 114 |
+
cls,
|
| 115 |
+
metadata_contents: bytes,
|
| 116 |
+
filename: str,
|
| 117 |
+
project_name: str,
|
| 118 |
+
) -> BaseDistribution:
|
| 119 |
+
# Generate temp dir to contain the metadata file, and write the file contents.
|
| 120 |
+
temp_dir = pathlib.Path(
|
| 121 |
+
TempDirectory(kind="metadata", globally_managed=True).path
|
| 122 |
+
)
|
| 123 |
+
metadata_path = temp_dir / "METADATA"
|
| 124 |
+
metadata_path.write_bytes(metadata_contents)
|
| 125 |
+
# Construct dist pointing to the newly created directory.
|
| 126 |
+
dist = importlib.metadata.Distribution.at(metadata_path.parent)
|
| 127 |
+
return cls(dist, metadata_path.parent, None)
|
| 128 |
+
|
| 129 |
+
@classmethod
|
| 130 |
+
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
|
| 131 |
+
try:
|
| 132 |
+
with wheel.as_zipfile() as zf:
|
| 133 |
+
dist = WheelDistribution.from_zipfile(zf, name, wheel.location)
|
| 134 |
+
except zipfile.BadZipFile as e:
|
| 135 |
+
raise InvalidWheel(wheel.location, name) from e
|
| 136 |
+
except UnsupportedWheel as e:
|
| 137 |
+
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
|
| 138 |
+
return cls(dist, dist.info_location, pathlib.PurePosixPath(wheel.location))
|
| 139 |
+
|
| 140 |
+
@property
|
| 141 |
+
def location(self) -> Optional[str]:
|
| 142 |
+
if self._info_location is None:
|
| 143 |
+
return None
|
| 144 |
+
return str(self._info_location.parent)
|
| 145 |
+
|
| 146 |
+
@property
|
| 147 |
+
def info_location(self) -> Optional[str]:
|
| 148 |
+
if self._info_location is None:
|
| 149 |
+
return None
|
| 150 |
+
return str(self._info_location)
|
| 151 |
+
|
| 152 |
+
@property
|
| 153 |
+
def installed_location(self) -> Optional[str]:
|
| 154 |
+
if self._installed_location is None:
|
| 155 |
+
return None
|
| 156 |
+
return normalize_path(str(self._installed_location))
|
| 157 |
+
|
| 158 |
+
def _get_dist_name_from_location(self) -> Optional[str]:
|
| 159 |
+
"""Try to get the name from the metadata directory name.
|
| 160 |
+
|
| 161 |
+
This is much faster than reading metadata.
|
| 162 |
+
"""
|
| 163 |
+
if self._info_location is None:
|
| 164 |
+
return None
|
| 165 |
+
stem, suffix = os.path.splitext(self._info_location.name)
|
| 166 |
+
if suffix not in (".dist-info", ".egg-info"):
|
| 167 |
+
return None
|
| 168 |
+
return stem.split("-", 1)[0]
|
| 169 |
+
|
| 170 |
+
@property
|
| 171 |
+
def canonical_name(self) -> NormalizedName:
|
| 172 |
+
name = self._get_dist_name_from_location() or get_dist_name(self._dist)
|
| 173 |
+
return canonicalize_name(name)
|
| 174 |
+
|
| 175 |
+
@property
|
| 176 |
+
def version(self) -> DistributionVersion:
|
| 177 |
+
return parse_version(self._dist.version)
|
| 178 |
+
|
| 179 |
+
def is_file(self, path: InfoPath) -> bool:
|
| 180 |
+
return self._dist.read_text(str(path)) is not None
|
| 181 |
+
|
| 182 |
+
def iter_distutils_script_names(self) -> Iterator[str]:
|
| 183 |
+
# A distutils installation is always "flat" (not in e.g. egg form), so
|
| 184 |
+
# if this distribution's info location is NOT a pathlib.Path (but e.g.
|
| 185 |
+
# zipfile.Path), it can never contain any distutils scripts.
|
| 186 |
+
if not isinstance(self._info_location, pathlib.Path):
|
| 187 |
+
return
|
| 188 |
+
for child in self._info_location.joinpath("scripts").iterdir():
|
| 189 |
+
yield child.name
|
| 190 |
+
|
| 191 |
+
def read_text(self, path: InfoPath) -> str:
|
| 192 |
+
content = self._dist.read_text(str(path))
|
| 193 |
+
if content is None:
|
| 194 |
+
raise FileNotFoundError(path)
|
| 195 |
+
return content
|
| 196 |
+
|
| 197 |
+
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
|
| 198 |
+
# importlib.metadata's EntryPoint structure sasitfies BaseEntryPoint.
|
| 199 |
+
return self._dist.entry_points
|
| 200 |
+
|
| 201 |
+
def _metadata_impl(self) -> email.message.Message:
|
| 202 |
+
# From Python 3.10+, importlib.metadata declares PackageMetadata as the
|
| 203 |
+
# return type. This protocol is unfortunately a disaster now and misses
|
| 204 |
+
# a ton of fields that we need, including get() and get_payload(). We
|
| 205 |
+
# rely on the implementation that the object is actually a Message now,
|
| 206 |
+
# until upstream can improve the protocol. (python/cpython#94952)
|
| 207 |
+
return cast(email.message.Message, self._dist.metadata)
|
| 208 |
+
|
| 209 |
+
def iter_provided_extras(self) -> Iterable[str]:
|
| 210 |
+
return self.metadata.get_all("Provides-Extra", [])
|
| 211 |
+
|
| 212 |
+
def is_extra_provided(self, extra: str) -> bool:
|
| 213 |
+
return any(
|
| 214 |
+
canonicalize_name(provided_extra) == canonicalize_name(extra)
|
| 215 |
+
for provided_extra in self.metadata.get_all("Provides-Extra", [])
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
|
| 219 |
+
contexts: Sequence[Dict[str, str]] = [{"extra": e} for e in extras]
|
| 220 |
+
for req_string in self.metadata.get_all("Requires-Dist", []):
|
| 221 |
+
req = Requirement(req_string)
|
| 222 |
+
if not req.marker:
|
| 223 |
+
yield req
|
| 224 |
+
elif not extras and req.marker.evaluate({"extra": ""}):
|
| 225 |
+
yield req
|
| 226 |
+
elif any(req.marker.evaluate(context) for context in contexts):
|
| 227 |
+
yield req
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_envs.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import importlib.metadata
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import pathlib
|
| 6 |
+
import sys
|
| 7 |
+
import zipfile
|
| 8 |
+
import zipimport
|
| 9 |
+
from typing import Iterator, List, Optional, Sequence, Set, Tuple
|
| 10 |
+
|
| 11 |
+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
| 12 |
+
|
| 13 |
+
from pip._internal.metadata.base import BaseDistribution, BaseEnvironment
|
| 14 |
+
from pip._internal.models.wheel import Wheel
|
| 15 |
+
from pip._internal.utils.deprecation import deprecated
|
| 16 |
+
from pip._internal.utils.filetypes import WHEEL_EXTENSION
|
| 17 |
+
|
| 18 |
+
from ._compat import BadMetadata, BasePath, get_dist_name, get_info_location
|
| 19 |
+
from ._dists import Distribution
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _looks_like_wheel(location: str) -> bool:
|
| 25 |
+
if not location.endswith(WHEEL_EXTENSION):
|
| 26 |
+
return False
|
| 27 |
+
if not os.path.isfile(location):
|
| 28 |
+
return False
|
| 29 |
+
if not Wheel.wheel_file_re.match(os.path.basename(location)):
|
| 30 |
+
return False
|
| 31 |
+
return zipfile.is_zipfile(location)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class _DistributionFinder:
|
| 35 |
+
"""Finder to locate distributions.
|
| 36 |
+
|
| 37 |
+
The main purpose of this class is to memoize found distributions' names, so
|
| 38 |
+
only one distribution is returned for each package name. At lot of pip code
|
| 39 |
+
assumes this (because it is setuptools's behavior), and not doing the same
|
| 40 |
+
can potentially cause a distribution in lower precedence path to override a
|
| 41 |
+
higher precedence one if the caller is not careful.
|
| 42 |
+
|
| 43 |
+
Eventually we probably want to make it possible to see lower precedence
|
| 44 |
+
installations as well. It's useful feature, after all.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
FoundResult = Tuple[importlib.metadata.Distribution, Optional[BasePath]]
|
| 48 |
+
|
| 49 |
+
def __init__(self) -> None:
|
| 50 |
+
self._found_names: Set[NormalizedName] = set()
|
| 51 |
+
|
| 52 |
+
def _find_impl(self, location: str) -> Iterator[FoundResult]:
|
| 53 |
+
"""Find distributions in a location."""
|
| 54 |
+
# Skip looking inside a wheel. Since a package inside a wheel is not
|
| 55 |
+
# always valid (due to .data directories etc.), its .dist-info entry
|
| 56 |
+
# should not be considered an installed distribution.
|
| 57 |
+
if _looks_like_wheel(location):
|
| 58 |
+
return
|
| 59 |
+
# To know exactly where we find a distribution, we have to feed in the
|
| 60 |
+
# paths one by one, instead of dumping the list to importlib.metadata.
|
| 61 |
+
for dist in importlib.metadata.distributions(path=[location]):
|
| 62 |
+
info_location = get_info_location(dist)
|
| 63 |
+
try:
|
| 64 |
+
raw_name = get_dist_name(dist)
|
| 65 |
+
except BadMetadata as e:
|
| 66 |
+
logger.warning("Skipping %s due to %s", info_location, e.reason)
|
| 67 |
+
continue
|
| 68 |
+
normalized_name = canonicalize_name(raw_name)
|
| 69 |
+
if normalized_name in self._found_names:
|
| 70 |
+
continue
|
| 71 |
+
self._found_names.add(normalized_name)
|
| 72 |
+
yield dist, info_location
|
| 73 |
+
|
| 74 |
+
def find(self, location: str) -> Iterator[BaseDistribution]:
|
| 75 |
+
"""Find distributions in a location.
|
| 76 |
+
|
| 77 |
+
The path can be either a directory, or a ZIP archive.
|
| 78 |
+
"""
|
| 79 |
+
for dist, info_location in self._find_impl(location):
|
| 80 |
+
if info_location is None:
|
| 81 |
+
installed_location: Optional[BasePath] = None
|
| 82 |
+
else:
|
| 83 |
+
installed_location = info_location.parent
|
| 84 |
+
yield Distribution(dist, info_location, installed_location)
|
| 85 |
+
|
| 86 |
+
def find_linked(self, location: str) -> Iterator[BaseDistribution]:
|
| 87 |
+
"""Read location in egg-link files and return distributions in there.
|
| 88 |
+
|
| 89 |
+
The path should be a directory; otherwise this returns nothing. This
|
| 90 |
+
follows how setuptools does this for compatibility. The first non-empty
|
| 91 |
+
line in the egg-link is read as a path (resolved against the egg-link's
|
| 92 |
+
containing directory if relative). Distributions found at that linked
|
| 93 |
+
location are returned.
|
| 94 |
+
"""
|
| 95 |
+
path = pathlib.Path(location)
|
| 96 |
+
if not path.is_dir():
|
| 97 |
+
return
|
| 98 |
+
for child in path.iterdir():
|
| 99 |
+
if child.suffix != ".egg-link":
|
| 100 |
+
continue
|
| 101 |
+
with child.open() as f:
|
| 102 |
+
lines = (line.strip() for line in f)
|
| 103 |
+
target_rel = next((line for line in lines if line), "")
|
| 104 |
+
if not target_rel:
|
| 105 |
+
continue
|
| 106 |
+
target_location = str(path.joinpath(target_rel))
|
| 107 |
+
for dist, info_location in self._find_impl(target_location):
|
| 108 |
+
yield Distribution(dist, info_location, path)
|
| 109 |
+
|
| 110 |
+
def _find_eggs_in_dir(self, location: str) -> Iterator[BaseDistribution]:
|
| 111 |
+
from pip._vendor.pkg_resources import find_distributions
|
| 112 |
+
|
| 113 |
+
from pip._internal.metadata import pkg_resources as legacy
|
| 114 |
+
|
| 115 |
+
with os.scandir(location) as it:
|
| 116 |
+
for entry in it:
|
| 117 |
+
if not entry.name.endswith(".egg"):
|
| 118 |
+
continue
|
| 119 |
+
for dist in find_distributions(entry.path):
|
| 120 |
+
yield legacy.Distribution(dist)
|
| 121 |
+
|
| 122 |
+
def _find_eggs_in_zip(self, location: str) -> Iterator[BaseDistribution]:
|
| 123 |
+
from pip._vendor.pkg_resources import find_eggs_in_zip
|
| 124 |
+
|
| 125 |
+
from pip._internal.metadata import pkg_resources as legacy
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
importer = zipimport.zipimporter(location)
|
| 129 |
+
except zipimport.ZipImportError:
|
| 130 |
+
return
|
| 131 |
+
for dist in find_eggs_in_zip(importer, location):
|
| 132 |
+
yield legacy.Distribution(dist)
|
| 133 |
+
|
| 134 |
+
def find_eggs(self, location: str) -> Iterator[BaseDistribution]:
|
| 135 |
+
"""Find eggs in a location.
|
| 136 |
+
|
| 137 |
+
This actually uses the old *pkg_resources* backend. We likely want to
|
| 138 |
+
deprecate this so we can eventually remove the *pkg_resources*
|
| 139 |
+
dependency entirely. Before that, this should first emit a deprecation
|
| 140 |
+
warning for some versions when using the fallback since importing
|
| 141 |
+
*pkg_resources* is slow for those who don't need it.
|
| 142 |
+
"""
|
| 143 |
+
if os.path.isdir(location):
|
| 144 |
+
yield from self._find_eggs_in_dir(location)
|
| 145 |
+
if zipfile.is_zipfile(location):
|
| 146 |
+
yield from self._find_eggs_in_zip(location)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@functools.lru_cache(maxsize=None) # Warn a distribution exactly once.
|
| 150 |
+
def _emit_egg_deprecation(location: Optional[str]) -> None:
|
| 151 |
+
deprecated(
|
| 152 |
+
reason=f"Loading egg at {location} is deprecated.",
|
| 153 |
+
replacement="to use pip for package installation.",
|
| 154 |
+
gone_in="24.3",
|
| 155 |
+
issue=12330,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class Environment(BaseEnvironment):
|
| 160 |
+
def __init__(self, paths: Sequence[str]) -> None:
|
| 161 |
+
self._paths = paths
|
| 162 |
+
|
| 163 |
+
@classmethod
|
| 164 |
+
def default(cls) -> BaseEnvironment:
|
| 165 |
+
return cls(sys.path)
|
| 166 |
+
|
| 167 |
+
@classmethod
|
| 168 |
+
def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
|
| 169 |
+
if paths is None:
|
| 170 |
+
return cls(sys.path)
|
| 171 |
+
return cls(paths)
|
| 172 |
+
|
| 173 |
+
def _iter_distributions(self) -> Iterator[BaseDistribution]:
|
| 174 |
+
finder = _DistributionFinder()
|
| 175 |
+
for location in self._paths:
|
| 176 |
+
yield from finder.find(location)
|
| 177 |
+
for dist in finder.find_eggs(location):
|
| 178 |
+
_emit_egg_deprecation(dist.location)
|
| 179 |
+
yield dist
|
| 180 |
+
# This must go last because that's how pkg_resources tie-breaks.
|
| 181 |
+
yield from finder.find_linked(location)
|
| 182 |
+
|
| 183 |
+
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
|
| 184 |
+
matches = (
|
| 185 |
+
distribution
|
| 186 |
+
for distribution in self.iter_all_distributions()
|
| 187 |
+
if distribution.canonical_name == canonicalize_name(name)
|
| 188 |
+
)
|
| 189 |
+
return next(matches, None)
|
.venv/lib/python3.11/site-packages/pip/_internal/metadata/pkg_resources.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import email.message
|
| 2 |
+
import email.parser
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import zipfile
|
| 6 |
+
from typing import Collection, Iterable, Iterator, List, Mapping, NamedTuple, Optional
|
| 7 |
+
|
| 8 |
+
from pip._vendor import pkg_resources
|
| 9 |
+
from pip._vendor.packaging.requirements import Requirement
|
| 10 |
+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
| 11 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 12 |
+
|
| 13 |
+
from pip._internal.exceptions import InvalidWheel, NoneMetadataError, UnsupportedWheel
|
| 14 |
+
from pip._internal.utils.egg_link import egg_link_path_from_location
|
| 15 |
+
from pip._internal.utils.misc import display_path, normalize_path
|
| 16 |
+
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
|
| 17 |
+
|
| 18 |
+
from .base import (
|
| 19 |
+
BaseDistribution,
|
| 20 |
+
BaseEntryPoint,
|
| 21 |
+
BaseEnvironment,
|
| 22 |
+
DistributionVersion,
|
| 23 |
+
InfoPath,
|
| 24 |
+
Wheel,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
__all__ = ["NAME", "Distribution", "Environment"]
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
NAME = "pkg_resources"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class EntryPoint(NamedTuple):
|
| 35 |
+
name: str
|
| 36 |
+
value: str
|
| 37 |
+
group: str
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class InMemoryMetadata:
|
| 41 |
+
"""IMetadataProvider that reads metadata files from a dictionary.
|
| 42 |
+
|
| 43 |
+
This also maps metadata decoding exceptions to our internal exception type.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, metadata: Mapping[str, bytes], wheel_name: str) -> None:
|
| 47 |
+
self._metadata = metadata
|
| 48 |
+
self._wheel_name = wheel_name
|
| 49 |
+
|
| 50 |
+
def has_metadata(self, name: str) -> bool:
|
| 51 |
+
return name in self._metadata
|
| 52 |
+
|
| 53 |
+
def get_metadata(self, name: str) -> str:
|
| 54 |
+
try:
|
| 55 |
+
return self._metadata[name].decode()
|
| 56 |
+
except UnicodeDecodeError as e:
|
| 57 |
+
# Augment the default error with the origin of the file.
|
| 58 |
+
raise UnsupportedWheel(
|
| 59 |
+
f"Error decoding metadata for {self._wheel_name}: {e} in {name} file"
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def get_metadata_lines(self, name: str) -> Iterable[str]:
|
| 63 |
+
return pkg_resources.yield_lines(self.get_metadata(name))
|
| 64 |
+
|
| 65 |
+
def metadata_isdir(self, name: str) -> bool:
|
| 66 |
+
return False
|
| 67 |
+
|
| 68 |
+
def metadata_listdir(self, name: str) -> List[str]:
|
| 69 |
+
return []
|
| 70 |
+
|
| 71 |
+
def run_script(self, script_name: str, namespace: str) -> None:
|
| 72 |
+
pass
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class Distribution(BaseDistribution):
|
| 76 |
+
def __init__(self, dist: pkg_resources.Distribution) -> None:
|
| 77 |
+
self._dist = dist
|
| 78 |
+
|
| 79 |
+
@classmethod
|
| 80 |
+
def from_directory(cls, directory: str) -> BaseDistribution:
|
| 81 |
+
dist_dir = directory.rstrip(os.sep)
|
| 82 |
+
|
| 83 |
+
# Build a PathMetadata object, from path to metadata. :wink:
|
| 84 |
+
base_dir, dist_dir_name = os.path.split(dist_dir)
|
| 85 |
+
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
|
| 86 |
+
|
| 87 |
+
# Determine the correct Distribution object type.
|
| 88 |
+
if dist_dir.endswith(".egg-info"):
|
| 89 |
+
dist_cls = pkg_resources.Distribution
|
| 90 |
+
dist_name = os.path.splitext(dist_dir_name)[0]
|
| 91 |
+
else:
|
| 92 |
+
assert dist_dir.endswith(".dist-info")
|
| 93 |
+
dist_cls = pkg_resources.DistInfoDistribution
|
| 94 |
+
dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
|
| 95 |
+
|
| 96 |
+
dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
|
| 97 |
+
return cls(dist)
|
| 98 |
+
|
| 99 |
+
@classmethod
|
| 100 |
+
def from_metadata_file_contents(
|
| 101 |
+
cls,
|
| 102 |
+
metadata_contents: bytes,
|
| 103 |
+
filename: str,
|
| 104 |
+
project_name: str,
|
| 105 |
+
) -> BaseDistribution:
|
| 106 |
+
metadata_dict = {
|
| 107 |
+
"METADATA": metadata_contents,
|
| 108 |
+
}
|
| 109 |
+
dist = pkg_resources.DistInfoDistribution(
|
| 110 |
+
location=filename,
|
| 111 |
+
metadata=InMemoryMetadata(metadata_dict, filename),
|
| 112 |
+
project_name=project_name,
|
| 113 |
+
)
|
| 114 |
+
return cls(dist)
|
| 115 |
+
|
| 116 |
+
@classmethod
|
| 117 |
+
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
|
| 118 |
+
try:
|
| 119 |
+
with wheel.as_zipfile() as zf:
|
| 120 |
+
info_dir, _ = parse_wheel(zf, name)
|
| 121 |
+
metadata_dict = {
|
| 122 |
+
path.split("/", 1)[-1]: read_wheel_metadata_file(zf, path)
|
| 123 |
+
for path in zf.namelist()
|
| 124 |
+
if path.startswith(f"{info_dir}/")
|
| 125 |
+
}
|
| 126 |
+
except zipfile.BadZipFile as e:
|
| 127 |
+
raise InvalidWheel(wheel.location, name) from e
|
| 128 |
+
except UnsupportedWheel as e:
|
| 129 |
+
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
|
| 130 |
+
dist = pkg_resources.DistInfoDistribution(
|
| 131 |
+
location=wheel.location,
|
| 132 |
+
metadata=InMemoryMetadata(metadata_dict, wheel.location),
|
| 133 |
+
project_name=name,
|
| 134 |
+
)
|
| 135 |
+
return cls(dist)
|
| 136 |
+
|
| 137 |
+
@property
|
| 138 |
+
def location(self) -> Optional[str]:
|
| 139 |
+
return self._dist.location
|
| 140 |
+
|
| 141 |
+
@property
|
| 142 |
+
def installed_location(self) -> Optional[str]:
|
| 143 |
+
egg_link = egg_link_path_from_location(self.raw_name)
|
| 144 |
+
if egg_link:
|
| 145 |
+
location = egg_link
|
| 146 |
+
elif self.location:
|
| 147 |
+
location = self.location
|
| 148 |
+
else:
|
| 149 |
+
return None
|
| 150 |
+
return normalize_path(location)
|
| 151 |
+
|
| 152 |
+
@property
|
| 153 |
+
def info_location(self) -> Optional[str]:
|
| 154 |
+
return self._dist.egg_info
|
| 155 |
+
|
| 156 |
+
@property
|
| 157 |
+
def installed_by_distutils(self) -> bool:
|
| 158 |
+
# A distutils-installed distribution is provided by FileMetadata. This
|
| 159 |
+
# provider has a "path" attribute not present anywhere else. Not the
|
| 160 |
+
# best introspection logic, but pip has been doing this for a long time.
|
| 161 |
+
try:
|
| 162 |
+
return bool(self._dist._provider.path)
|
| 163 |
+
except AttributeError:
|
| 164 |
+
return False
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def canonical_name(self) -> NormalizedName:
|
| 168 |
+
return canonicalize_name(self._dist.project_name)
|
| 169 |
+
|
| 170 |
+
@property
|
| 171 |
+
def version(self) -> DistributionVersion:
|
| 172 |
+
return parse_version(self._dist.version)
|
| 173 |
+
|
| 174 |
+
def is_file(self, path: InfoPath) -> bool:
|
| 175 |
+
return self._dist.has_metadata(str(path))
|
| 176 |
+
|
| 177 |
+
def iter_distutils_script_names(self) -> Iterator[str]:
|
| 178 |
+
yield from self._dist.metadata_listdir("scripts")
|
| 179 |
+
|
| 180 |
+
def read_text(self, path: InfoPath) -> str:
|
| 181 |
+
name = str(path)
|
| 182 |
+
if not self._dist.has_metadata(name):
|
| 183 |
+
raise FileNotFoundError(name)
|
| 184 |
+
content = self._dist.get_metadata(name)
|
| 185 |
+
if content is None:
|
| 186 |
+
raise NoneMetadataError(self, name)
|
| 187 |
+
return content
|
| 188 |
+
|
| 189 |
+
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
|
| 190 |
+
for group, entries in self._dist.get_entry_map().items():
|
| 191 |
+
for name, entry_point in entries.items():
|
| 192 |
+
name, _, value = str(entry_point).partition("=")
|
| 193 |
+
yield EntryPoint(name=name.strip(), value=value.strip(), group=group)
|
| 194 |
+
|
| 195 |
+
def _metadata_impl(self) -> email.message.Message:
|
| 196 |
+
"""
|
| 197 |
+
:raises NoneMetadataError: if the distribution reports `has_metadata()`
|
| 198 |
+
True but `get_metadata()` returns None.
|
| 199 |
+
"""
|
| 200 |
+
if isinstance(self._dist, pkg_resources.DistInfoDistribution):
|
| 201 |
+
metadata_name = "METADATA"
|
| 202 |
+
else:
|
| 203 |
+
metadata_name = "PKG-INFO"
|
| 204 |
+
try:
|
| 205 |
+
metadata = self.read_text(metadata_name)
|
| 206 |
+
except FileNotFoundError:
|
| 207 |
+
if self.location:
|
| 208 |
+
displaying_path = display_path(self.location)
|
| 209 |
+
else:
|
| 210 |
+
displaying_path = repr(self.location)
|
| 211 |
+
logger.warning("No metadata found in %s", displaying_path)
|
| 212 |
+
metadata = ""
|
| 213 |
+
feed_parser = email.parser.FeedParser()
|
| 214 |
+
feed_parser.feed(metadata)
|
| 215 |
+
return feed_parser.close()
|
| 216 |
+
|
| 217 |
+
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
|
| 218 |
+
if extras: # pkg_resources raises on invalid extras, so we sanitize.
|
| 219 |
+
extras = frozenset(pkg_resources.safe_extra(e) for e in extras)
|
| 220 |
+
extras = extras.intersection(self._dist.extras)
|
| 221 |
+
return self._dist.requires(extras)
|
| 222 |
+
|
| 223 |
+
def iter_provided_extras(self) -> Iterable[str]:
|
| 224 |
+
return self._dist.extras
|
| 225 |
+
|
| 226 |
+
def is_extra_provided(self, extra: str) -> bool:
|
| 227 |
+
return pkg_resources.safe_extra(extra) in self._dist.extras
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
class Environment(BaseEnvironment):
|
| 231 |
+
def __init__(self, ws: pkg_resources.WorkingSet) -> None:
|
| 232 |
+
self._ws = ws
|
| 233 |
+
|
| 234 |
+
@classmethod
|
| 235 |
+
def default(cls) -> BaseEnvironment:
|
| 236 |
+
return cls(pkg_resources.working_set)
|
| 237 |
+
|
| 238 |
+
@classmethod
|
| 239 |
+
def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
|
| 240 |
+
return cls(pkg_resources.WorkingSet(paths))
|
| 241 |
+
|
| 242 |
+
def _iter_distributions(self) -> Iterator[BaseDistribution]:
|
| 243 |
+
for dist in self._ws:
|
| 244 |
+
yield Distribution(dist)
|
| 245 |
+
|
| 246 |
+
def _search_distribution(self, name: str) -> Optional[BaseDistribution]:
|
| 247 |
+
"""Find a distribution matching the ``name`` in the environment.
|
| 248 |
+
|
| 249 |
+
This searches from *all* distributions available in the environment, to
|
| 250 |
+
match the behavior of ``pkg_resources.get_distribution()``.
|
| 251 |
+
"""
|
| 252 |
+
canonical_name = canonicalize_name(name)
|
| 253 |
+
for dist in self.iter_all_distributions():
|
| 254 |
+
if dist.canonical_name == canonical_name:
|
| 255 |
+
return dist
|
| 256 |
+
return None
|
| 257 |
+
|
| 258 |
+
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
|
| 259 |
+
# Search the distribution by looking through the working set.
|
| 260 |
+
dist = self._search_distribution(name)
|
| 261 |
+
if dist:
|
| 262 |
+
return dist
|
| 263 |
+
|
| 264 |
+
# If distribution could not be found, call working_set.require to
|
| 265 |
+
# update the working set, and try to find the distribution again.
|
| 266 |
+
# This might happen for e.g. when you install a package twice, once
|
| 267 |
+
# using setup.py develop and again using setup.py install. Now when
|
| 268 |
+
# running pip uninstall twice, the package gets removed from the
|
| 269 |
+
# working set in the first uninstall, so we have to populate the
|
| 270 |
+
# working set again so that pip knows about it and the packages gets
|
| 271 |
+
# picked up and is successfully uninstalled the second time too.
|
| 272 |
+
try:
|
| 273 |
+
# We didn't pass in any version specifiers, so this can never
|
| 274 |
+
# raise pkg_resources.VersionConflict.
|
| 275 |
+
self._ws.require(name)
|
| 276 |
+
except pkg_resources.DistributionNotFound:
|
| 277 |
+
return None
|
| 278 |
+
return self._search_distribution(name)
|
.venv/lib/python3.11/site-packages/pip/_internal/network/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Contains purely network-related utilities.
|
| 2 |
+
"""
|
.venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (259 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/auth.cpython-311.pyc
ADDED
|
Binary file (24 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/cache.cpython-311.pyc
ADDED
|
Binary file (7.93 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/download.cpython-311.pyc
ADDED
|
Binary file (9.54 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/lazy_wheel.cpython-311.pyc
ADDED
|
Binary file (13 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/session.cpython-311.pyc
ADDED
|
Binary file (21.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (2.41 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/xmlrpc.cpython-311.pyc
ADDED
|
Binary file (3.25 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/network/auth.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Network Authentication Helpers
|
| 2 |
+
|
| 3 |
+
Contains interface (MultiDomainBasicAuth) and associated glue code for
|
| 4 |
+
providing credentials in the context of network requests.
|
| 5 |
+
"""
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import shutil
|
| 9 |
+
import subprocess
|
| 10 |
+
import sysconfig
|
| 11 |
+
import typing
|
| 12 |
+
import urllib.parse
|
| 13 |
+
from abc import ABC, abstractmethod
|
| 14 |
+
from functools import lru_cache
|
| 15 |
+
from os.path import commonprefix
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
|
| 18 |
+
|
| 19 |
+
from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
|
| 20 |
+
from pip._vendor.requests.models import Request, Response
|
| 21 |
+
from pip._vendor.requests.utils import get_netrc_auth
|
| 22 |
+
|
| 23 |
+
from pip._internal.utils.logging import getLogger
|
| 24 |
+
from pip._internal.utils.misc import (
|
| 25 |
+
ask,
|
| 26 |
+
ask_input,
|
| 27 |
+
ask_password,
|
| 28 |
+
remove_auth_from_url,
|
| 29 |
+
split_auth_netloc_from_url,
|
| 30 |
+
)
|
| 31 |
+
from pip._internal.vcs.versioncontrol import AuthInfo
|
| 32 |
+
|
| 33 |
+
logger = getLogger(__name__)
|
| 34 |
+
|
| 35 |
+
KEYRING_DISABLED = False
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class Credentials(NamedTuple):
|
| 39 |
+
url: str
|
| 40 |
+
username: str
|
| 41 |
+
password: str
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class KeyRingBaseProvider(ABC):
|
| 45 |
+
"""Keyring base provider interface"""
|
| 46 |
+
|
| 47 |
+
has_keyring: bool
|
| 48 |
+
|
| 49 |
+
@abstractmethod
|
| 50 |
+
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
| 51 |
+
...
|
| 52 |
+
|
| 53 |
+
@abstractmethod
|
| 54 |
+
def save_auth_info(self, url: str, username: str, password: str) -> None:
|
| 55 |
+
...
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class KeyRingNullProvider(KeyRingBaseProvider):
|
| 59 |
+
"""Keyring null provider"""
|
| 60 |
+
|
| 61 |
+
has_keyring = False
|
| 62 |
+
|
| 63 |
+
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
| 64 |
+
return None
|
| 65 |
+
|
| 66 |
+
def save_auth_info(self, url: str, username: str, password: str) -> None:
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class KeyRingPythonProvider(KeyRingBaseProvider):
|
| 71 |
+
"""Keyring interface which uses locally imported `keyring`"""
|
| 72 |
+
|
| 73 |
+
has_keyring = True
|
| 74 |
+
|
| 75 |
+
def __init__(self) -> None:
|
| 76 |
+
import keyring
|
| 77 |
+
|
| 78 |
+
self.keyring = keyring
|
| 79 |
+
|
| 80 |
+
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
| 81 |
+
# Support keyring's get_credential interface which supports getting
|
| 82 |
+
# credentials without a username. This is only available for
|
| 83 |
+
# keyring>=15.2.0.
|
| 84 |
+
if hasattr(self.keyring, "get_credential"):
|
| 85 |
+
logger.debug("Getting credentials from keyring for %s", url)
|
| 86 |
+
cred = self.keyring.get_credential(url, username)
|
| 87 |
+
if cred is not None:
|
| 88 |
+
return cred.username, cred.password
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
if username is not None:
|
| 92 |
+
logger.debug("Getting password from keyring for %s", url)
|
| 93 |
+
password = self.keyring.get_password(url, username)
|
| 94 |
+
if password:
|
| 95 |
+
return username, password
|
| 96 |
+
return None
|
| 97 |
+
|
| 98 |
+
def save_auth_info(self, url: str, username: str, password: str) -> None:
|
| 99 |
+
self.keyring.set_password(url, username, password)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class KeyRingCliProvider(KeyRingBaseProvider):
|
| 103 |
+
"""Provider which uses `keyring` cli
|
| 104 |
+
|
| 105 |
+
Instead of calling the keyring package installed alongside pip
|
| 106 |
+
we call keyring on the command line which will enable pip to
|
| 107 |
+
use which ever installation of keyring is available first in
|
| 108 |
+
PATH.
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
has_keyring = True
|
| 112 |
+
|
| 113 |
+
def __init__(self, cmd: str) -> None:
|
| 114 |
+
self.keyring = cmd
|
| 115 |
+
|
| 116 |
+
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
| 117 |
+
# This is the default implementation of keyring.get_credential
|
| 118 |
+
# https://github.com/jaraco/keyring/blob/97689324abcf01bd1793d49063e7ca01e03d7d07/keyring/backend.py#L134-L139
|
| 119 |
+
if username is not None:
|
| 120 |
+
password = self._get_password(url, username)
|
| 121 |
+
if password is not None:
|
| 122 |
+
return username, password
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
def save_auth_info(self, url: str, username: str, password: str) -> None:
|
| 126 |
+
return self._set_password(url, username, password)
|
| 127 |
+
|
| 128 |
+
def _get_password(self, service_name: str, username: str) -> Optional[str]:
|
| 129 |
+
"""Mirror the implementation of keyring.get_password using cli"""
|
| 130 |
+
if self.keyring is None:
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
cmd = [self.keyring, "get", service_name, username]
|
| 134 |
+
env = os.environ.copy()
|
| 135 |
+
env["PYTHONIOENCODING"] = "utf-8"
|
| 136 |
+
res = subprocess.run(
|
| 137 |
+
cmd,
|
| 138 |
+
stdin=subprocess.DEVNULL,
|
| 139 |
+
stdout=subprocess.PIPE,
|
| 140 |
+
env=env,
|
| 141 |
+
)
|
| 142 |
+
if res.returncode:
|
| 143 |
+
return None
|
| 144 |
+
return res.stdout.decode("utf-8").strip(os.linesep)
|
| 145 |
+
|
| 146 |
+
def _set_password(self, service_name: str, username: str, password: str) -> None:
|
| 147 |
+
"""Mirror the implementation of keyring.set_password using cli"""
|
| 148 |
+
if self.keyring is None:
|
| 149 |
+
return None
|
| 150 |
+
env = os.environ.copy()
|
| 151 |
+
env["PYTHONIOENCODING"] = "utf-8"
|
| 152 |
+
subprocess.run(
|
| 153 |
+
[self.keyring, "set", service_name, username],
|
| 154 |
+
input=f"{password}{os.linesep}".encode("utf-8"),
|
| 155 |
+
env=env,
|
| 156 |
+
check=True,
|
| 157 |
+
)
|
| 158 |
+
return None
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
@lru_cache(maxsize=None)
|
| 162 |
+
def get_keyring_provider(provider: str) -> KeyRingBaseProvider:
|
| 163 |
+
logger.verbose("Keyring provider requested: %s", provider)
|
| 164 |
+
|
| 165 |
+
# keyring has previously failed and been disabled
|
| 166 |
+
if KEYRING_DISABLED:
|
| 167 |
+
provider = "disabled"
|
| 168 |
+
if provider in ["import", "auto"]:
|
| 169 |
+
try:
|
| 170 |
+
impl = KeyRingPythonProvider()
|
| 171 |
+
logger.verbose("Keyring provider set: import")
|
| 172 |
+
return impl
|
| 173 |
+
except ImportError:
|
| 174 |
+
pass
|
| 175 |
+
except Exception as exc:
|
| 176 |
+
# In the event of an unexpected exception
|
| 177 |
+
# we should warn the user
|
| 178 |
+
msg = "Installed copy of keyring fails with exception %s"
|
| 179 |
+
if provider == "auto":
|
| 180 |
+
msg = msg + ", trying to find a keyring executable as a fallback"
|
| 181 |
+
logger.warning(msg, exc, exc_info=logger.isEnabledFor(logging.DEBUG))
|
| 182 |
+
if provider in ["subprocess", "auto"]:
|
| 183 |
+
cli = shutil.which("keyring")
|
| 184 |
+
if cli and cli.startswith(sysconfig.get_path("scripts")):
|
| 185 |
+
# all code within this function is stolen from shutil.which implementation
|
| 186 |
+
@typing.no_type_check
|
| 187 |
+
def PATH_as_shutil_which_determines_it() -> str:
|
| 188 |
+
path = os.environ.get("PATH", None)
|
| 189 |
+
if path is None:
|
| 190 |
+
try:
|
| 191 |
+
path = os.confstr("CS_PATH")
|
| 192 |
+
except (AttributeError, ValueError):
|
| 193 |
+
# os.confstr() or CS_PATH is not available
|
| 194 |
+
path = os.defpath
|
| 195 |
+
# bpo-35755: Don't use os.defpath if the PATH environment variable is
|
| 196 |
+
# set to an empty string
|
| 197 |
+
|
| 198 |
+
return path
|
| 199 |
+
|
| 200 |
+
scripts = Path(sysconfig.get_path("scripts"))
|
| 201 |
+
|
| 202 |
+
paths = []
|
| 203 |
+
for path in PATH_as_shutil_which_determines_it().split(os.pathsep):
|
| 204 |
+
p = Path(path)
|
| 205 |
+
try:
|
| 206 |
+
if not p.samefile(scripts):
|
| 207 |
+
paths.append(path)
|
| 208 |
+
except FileNotFoundError:
|
| 209 |
+
pass
|
| 210 |
+
|
| 211 |
+
path = os.pathsep.join(paths)
|
| 212 |
+
|
| 213 |
+
cli = shutil.which("keyring", path=path)
|
| 214 |
+
|
| 215 |
+
if cli:
|
| 216 |
+
logger.verbose("Keyring provider set: subprocess with executable %s", cli)
|
| 217 |
+
return KeyRingCliProvider(cli)
|
| 218 |
+
|
| 219 |
+
logger.verbose("Keyring provider set: disabled")
|
| 220 |
+
return KeyRingNullProvider()
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
class MultiDomainBasicAuth(AuthBase):
|
| 224 |
+
def __init__(
|
| 225 |
+
self,
|
| 226 |
+
prompting: bool = True,
|
| 227 |
+
index_urls: Optional[List[str]] = None,
|
| 228 |
+
keyring_provider: str = "auto",
|
| 229 |
+
) -> None:
|
| 230 |
+
self.prompting = prompting
|
| 231 |
+
self.index_urls = index_urls
|
| 232 |
+
self.keyring_provider = keyring_provider # type: ignore[assignment]
|
| 233 |
+
self.passwords: Dict[str, AuthInfo] = {}
|
| 234 |
+
# When the user is prompted to enter credentials and keyring is
|
| 235 |
+
# available, we will offer to save them. If the user accepts,
|
| 236 |
+
# this value is set to the credentials they entered. After the
|
| 237 |
+
# request authenticates, the caller should call
|
| 238 |
+
# ``save_credentials`` to save these.
|
| 239 |
+
self._credentials_to_save: Optional[Credentials] = None
|
| 240 |
+
|
| 241 |
+
@property
|
| 242 |
+
def keyring_provider(self) -> KeyRingBaseProvider:
|
| 243 |
+
return get_keyring_provider(self._keyring_provider)
|
| 244 |
+
|
| 245 |
+
@keyring_provider.setter
|
| 246 |
+
def keyring_provider(self, provider: str) -> None:
|
| 247 |
+
# The free function get_keyring_provider has been decorated with
|
| 248 |
+
# functools.cache. If an exception occurs in get_keyring_auth that
|
| 249 |
+
# cache will be cleared and keyring disabled, take that into account
|
| 250 |
+
# if you want to remove this indirection.
|
| 251 |
+
self._keyring_provider = provider
|
| 252 |
+
|
| 253 |
+
@property
|
| 254 |
+
def use_keyring(self) -> bool:
|
| 255 |
+
# We won't use keyring when --no-input is passed unless
|
| 256 |
+
# a specific provider is requested because it might require
|
| 257 |
+
# user interaction
|
| 258 |
+
return self.prompting or self._keyring_provider not in ["auto", "disabled"]
|
| 259 |
+
|
| 260 |
+
def _get_keyring_auth(
|
| 261 |
+
self,
|
| 262 |
+
url: Optional[str],
|
| 263 |
+
username: Optional[str],
|
| 264 |
+
) -> Optional[AuthInfo]:
|
| 265 |
+
"""Return the tuple auth for a given url from keyring."""
|
| 266 |
+
# Do nothing if no url was provided
|
| 267 |
+
if not url:
|
| 268 |
+
return None
|
| 269 |
+
|
| 270 |
+
try:
|
| 271 |
+
return self.keyring_provider.get_auth_info(url, username)
|
| 272 |
+
except Exception as exc:
|
| 273 |
+
logger.warning(
|
| 274 |
+
"Keyring is skipped due to an exception: %s",
|
| 275 |
+
str(exc),
|
| 276 |
+
)
|
| 277 |
+
global KEYRING_DISABLED
|
| 278 |
+
KEYRING_DISABLED = True
|
| 279 |
+
get_keyring_provider.cache_clear()
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
def _get_index_url(self, url: str) -> Optional[str]:
|
| 283 |
+
"""Return the original index URL matching the requested URL.
|
| 284 |
+
|
| 285 |
+
Cached or dynamically generated credentials may work against
|
| 286 |
+
the original index URL rather than just the netloc.
|
| 287 |
+
|
| 288 |
+
The provided url should have had its username and password
|
| 289 |
+
removed already. If the original index url had credentials then
|
| 290 |
+
they will be included in the return value.
|
| 291 |
+
|
| 292 |
+
Returns None if no matching index was found, or if --no-index
|
| 293 |
+
was specified by the user.
|
| 294 |
+
"""
|
| 295 |
+
if not url or not self.index_urls:
|
| 296 |
+
return None
|
| 297 |
+
|
| 298 |
+
url = remove_auth_from_url(url).rstrip("/") + "/"
|
| 299 |
+
parsed_url = urllib.parse.urlsplit(url)
|
| 300 |
+
|
| 301 |
+
candidates = []
|
| 302 |
+
|
| 303 |
+
for index in self.index_urls:
|
| 304 |
+
index = index.rstrip("/") + "/"
|
| 305 |
+
parsed_index = urllib.parse.urlsplit(remove_auth_from_url(index))
|
| 306 |
+
if parsed_url == parsed_index:
|
| 307 |
+
return index
|
| 308 |
+
|
| 309 |
+
if parsed_url.netloc != parsed_index.netloc:
|
| 310 |
+
continue
|
| 311 |
+
|
| 312 |
+
candidate = urllib.parse.urlsplit(index)
|
| 313 |
+
candidates.append(candidate)
|
| 314 |
+
|
| 315 |
+
if not candidates:
|
| 316 |
+
return None
|
| 317 |
+
|
| 318 |
+
candidates.sort(
|
| 319 |
+
reverse=True,
|
| 320 |
+
key=lambda candidate: commonprefix(
|
| 321 |
+
[
|
| 322 |
+
parsed_url.path,
|
| 323 |
+
candidate.path,
|
| 324 |
+
]
|
| 325 |
+
).rfind("/"),
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
return urllib.parse.urlunsplit(candidates[0])
|
| 329 |
+
|
| 330 |
+
def _get_new_credentials(
|
| 331 |
+
self,
|
| 332 |
+
original_url: str,
|
| 333 |
+
*,
|
| 334 |
+
allow_netrc: bool = True,
|
| 335 |
+
allow_keyring: bool = False,
|
| 336 |
+
) -> AuthInfo:
|
| 337 |
+
"""Find and return credentials for the specified URL."""
|
| 338 |
+
# Split the credentials and netloc from the url.
|
| 339 |
+
url, netloc, url_user_password = split_auth_netloc_from_url(
|
| 340 |
+
original_url,
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
# Start with the credentials embedded in the url
|
| 344 |
+
username, password = url_user_password
|
| 345 |
+
if username is not None and password is not None:
|
| 346 |
+
logger.debug("Found credentials in url for %s", netloc)
|
| 347 |
+
return url_user_password
|
| 348 |
+
|
| 349 |
+
# Find a matching index url for this request
|
| 350 |
+
index_url = self._get_index_url(url)
|
| 351 |
+
if index_url:
|
| 352 |
+
# Split the credentials from the url.
|
| 353 |
+
index_info = split_auth_netloc_from_url(index_url)
|
| 354 |
+
if index_info:
|
| 355 |
+
index_url, _, index_url_user_password = index_info
|
| 356 |
+
logger.debug("Found index url %s", index_url)
|
| 357 |
+
|
| 358 |
+
# If an index URL was found, try its embedded credentials
|
| 359 |
+
if index_url and index_url_user_password[0] is not None:
|
| 360 |
+
username, password = index_url_user_password
|
| 361 |
+
if username is not None and password is not None:
|
| 362 |
+
logger.debug("Found credentials in index url for %s", netloc)
|
| 363 |
+
return index_url_user_password
|
| 364 |
+
|
| 365 |
+
# Get creds from netrc if we still don't have them
|
| 366 |
+
if allow_netrc:
|
| 367 |
+
netrc_auth = get_netrc_auth(original_url)
|
| 368 |
+
if netrc_auth:
|
| 369 |
+
logger.debug("Found credentials in netrc for %s", netloc)
|
| 370 |
+
return netrc_auth
|
| 371 |
+
|
| 372 |
+
# If we don't have a password and keyring is available, use it.
|
| 373 |
+
if allow_keyring:
|
| 374 |
+
# The index url is more specific than the netloc, so try it first
|
| 375 |
+
# fmt: off
|
| 376 |
+
kr_auth = (
|
| 377 |
+
self._get_keyring_auth(index_url, username) or
|
| 378 |
+
self._get_keyring_auth(netloc, username)
|
| 379 |
+
)
|
| 380 |
+
# fmt: on
|
| 381 |
+
if kr_auth:
|
| 382 |
+
logger.debug("Found credentials in keyring for %s", netloc)
|
| 383 |
+
return kr_auth
|
| 384 |
+
|
| 385 |
+
return username, password
|
| 386 |
+
|
| 387 |
+
def _get_url_and_credentials(
|
| 388 |
+
self, original_url: str
|
| 389 |
+
) -> Tuple[str, Optional[str], Optional[str]]:
|
| 390 |
+
"""Return the credentials to use for the provided URL.
|
| 391 |
+
|
| 392 |
+
If allowed, netrc and keyring may be used to obtain the
|
| 393 |
+
correct credentials.
|
| 394 |
+
|
| 395 |
+
Returns (url_without_credentials, username, password). Note
|
| 396 |
+
that even if the original URL contains credentials, this
|
| 397 |
+
function may return a different username and password.
|
| 398 |
+
"""
|
| 399 |
+
url, netloc, _ = split_auth_netloc_from_url(original_url)
|
| 400 |
+
|
| 401 |
+
# Try to get credentials from original url
|
| 402 |
+
username, password = self._get_new_credentials(original_url)
|
| 403 |
+
|
| 404 |
+
# If credentials not found, use any stored credentials for this netloc.
|
| 405 |
+
# Do this if either the username or the password is missing.
|
| 406 |
+
# This accounts for the situation in which the user has specified
|
| 407 |
+
# the username in the index url, but the password comes from keyring.
|
| 408 |
+
if (username is None or password is None) and netloc in self.passwords:
|
| 409 |
+
un, pw = self.passwords[netloc]
|
| 410 |
+
# It is possible that the cached credentials are for a different username,
|
| 411 |
+
# in which case the cache should be ignored.
|
| 412 |
+
if username is None or username == un:
|
| 413 |
+
username, password = un, pw
|
| 414 |
+
|
| 415 |
+
if username is not None or password is not None:
|
| 416 |
+
# Convert the username and password if they're None, so that
|
| 417 |
+
# this netloc will show up as "cached" in the conditional above.
|
| 418 |
+
# Further, HTTPBasicAuth doesn't accept None, so it makes sense to
|
| 419 |
+
# cache the value that is going to be used.
|
| 420 |
+
username = username or ""
|
| 421 |
+
password = password or ""
|
| 422 |
+
|
| 423 |
+
# Store any acquired credentials.
|
| 424 |
+
self.passwords[netloc] = (username, password)
|
| 425 |
+
|
| 426 |
+
assert (
|
| 427 |
+
# Credentials were found
|
| 428 |
+
(username is not None and password is not None)
|
| 429 |
+
# Credentials were not found
|
| 430 |
+
or (username is None and password is None)
|
| 431 |
+
), f"Could not load credentials from url: {original_url}"
|
| 432 |
+
|
| 433 |
+
return url, username, password
|
| 434 |
+
|
| 435 |
+
def __call__(self, req: Request) -> Request:
|
| 436 |
+
# Get credentials for this request
|
| 437 |
+
url, username, password = self._get_url_and_credentials(req.url)
|
| 438 |
+
|
| 439 |
+
# Set the url of the request to the url without any credentials
|
| 440 |
+
req.url = url
|
| 441 |
+
|
| 442 |
+
if username is not None and password is not None:
|
| 443 |
+
# Send the basic auth with this request
|
| 444 |
+
req = HTTPBasicAuth(username, password)(req)
|
| 445 |
+
|
| 446 |
+
# Attach a hook to handle 401 responses
|
| 447 |
+
req.register_hook("response", self.handle_401)
|
| 448 |
+
|
| 449 |
+
return req
|
| 450 |
+
|
| 451 |
+
# Factored out to allow for easy patching in tests
|
| 452 |
+
def _prompt_for_password(
|
| 453 |
+
self, netloc: str
|
| 454 |
+
) -> Tuple[Optional[str], Optional[str], bool]:
|
| 455 |
+
username = ask_input(f"User for {netloc}: ") if self.prompting else None
|
| 456 |
+
if not username:
|
| 457 |
+
return None, None, False
|
| 458 |
+
if self.use_keyring:
|
| 459 |
+
auth = self._get_keyring_auth(netloc, username)
|
| 460 |
+
if auth and auth[0] is not None and auth[1] is not None:
|
| 461 |
+
return auth[0], auth[1], False
|
| 462 |
+
password = ask_password("Password: ")
|
| 463 |
+
return username, password, True
|
| 464 |
+
|
| 465 |
+
# Factored out to allow for easy patching in tests
|
| 466 |
+
def _should_save_password_to_keyring(self) -> bool:
|
| 467 |
+
if (
|
| 468 |
+
not self.prompting
|
| 469 |
+
or not self.use_keyring
|
| 470 |
+
or not self.keyring_provider.has_keyring
|
| 471 |
+
):
|
| 472 |
+
return False
|
| 473 |
+
return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"
|
| 474 |
+
|
| 475 |
+
def handle_401(self, resp: Response, **kwargs: Any) -> Response:
|
| 476 |
+
# We only care about 401 responses, anything else we want to just
|
| 477 |
+
# pass through the actual response
|
| 478 |
+
if resp.status_code != 401:
|
| 479 |
+
return resp
|
| 480 |
+
|
| 481 |
+
username, password = None, None
|
| 482 |
+
|
| 483 |
+
# Query the keyring for credentials:
|
| 484 |
+
if self.use_keyring:
|
| 485 |
+
username, password = self._get_new_credentials(
|
| 486 |
+
resp.url,
|
| 487 |
+
allow_netrc=False,
|
| 488 |
+
allow_keyring=True,
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
# We are not able to prompt the user so simply return the response
|
| 492 |
+
if not self.prompting and not username and not password:
|
| 493 |
+
return resp
|
| 494 |
+
|
| 495 |
+
parsed = urllib.parse.urlparse(resp.url)
|
| 496 |
+
|
| 497 |
+
# Prompt the user for a new username and password
|
| 498 |
+
save = False
|
| 499 |
+
if not username and not password:
|
| 500 |
+
username, password, save = self._prompt_for_password(parsed.netloc)
|
| 501 |
+
|
| 502 |
+
# Store the new username and password to use for future requests
|
| 503 |
+
self._credentials_to_save = None
|
| 504 |
+
if username is not None and password is not None:
|
| 505 |
+
self.passwords[parsed.netloc] = (username, password)
|
| 506 |
+
|
| 507 |
+
# Prompt to save the password to keyring
|
| 508 |
+
if save and self._should_save_password_to_keyring():
|
| 509 |
+
self._credentials_to_save = Credentials(
|
| 510 |
+
url=parsed.netloc,
|
| 511 |
+
username=username,
|
| 512 |
+
password=password,
|
| 513 |
+
)
|
| 514 |
+
|
| 515 |
+
# Consume content and release the original connection to allow our new
|
| 516 |
+
# request to reuse the same one.
|
| 517 |
+
# The result of the assignment isn't used, it's just needed to consume
|
| 518 |
+
# the content.
|
| 519 |
+
_ = resp.content
|
| 520 |
+
resp.raw.release_conn()
|
| 521 |
+
|
| 522 |
+
# Add our new username and password to the request
|
| 523 |
+
req = HTTPBasicAuth(username or "", password or "")(resp.request)
|
| 524 |
+
req.register_hook("response", self.warn_on_401)
|
| 525 |
+
|
| 526 |
+
# On successful request, save the credentials that were used to
|
| 527 |
+
# keyring. (Note that if the user responded "no" above, this member
|
| 528 |
+
# is not set and nothing will be saved.)
|
| 529 |
+
if self._credentials_to_save:
|
| 530 |
+
req.register_hook("response", self.save_credentials)
|
| 531 |
+
|
| 532 |
+
# Send our new request
|
| 533 |
+
new_resp = resp.connection.send(req, **kwargs)
|
| 534 |
+
new_resp.history.append(resp)
|
| 535 |
+
|
| 536 |
+
return new_resp
|
| 537 |
+
|
| 538 |
+
def warn_on_401(self, resp: Response, **kwargs: Any) -> None:
|
| 539 |
+
"""Response callback to warn about incorrect credentials."""
|
| 540 |
+
if resp.status_code == 401:
|
| 541 |
+
logger.warning(
|
| 542 |
+
"401 Error, Credentials not correct for %s",
|
| 543 |
+
resp.request.url,
|
| 544 |
+
)
|
| 545 |
+
|
| 546 |
+
def save_credentials(self, resp: Response, **kwargs: Any) -> None:
|
| 547 |
+
"""Response callback to save credentials on success."""
|
| 548 |
+
assert (
|
| 549 |
+
self.keyring_provider.has_keyring
|
| 550 |
+
), "should never reach here without keyring"
|
| 551 |
+
|
| 552 |
+
creds = self._credentials_to_save
|
| 553 |
+
self._credentials_to_save = None
|
| 554 |
+
if creds and resp.status_code < 400:
|
| 555 |
+
try:
|
| 556 |
+
logger.info("Saving credentials to keyring")
|
| 557 |
+
self.keyring_provider.save_auth_info(
|
| 558 |
+
creds.url, creds.username, creds.password
|
| 559 |
+
)
|
| 560 |
+
except Exception:
|
| 561 |
+
logger.exception("Failed to save credentials")
|
.venv/lib/python3.11/site-packages/pip/_internal/network/cache.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HTTP cache implementation.
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
from contextlib import contextmanager
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import BinaryIO, Generator, Optional, Union
|
| 8 |
+
|
| 9 |
+
from pip._vendor.cachecontrol.cache import SeparateBodyBaseCache
|
| 10 |
+
from pip._vendor.cachecontrol.caches import SeparateBodyFileCache
|
| 11 |
+
from pip._vendor.requests.models import Response
|
| 12 |
+
|
| 13 |
+
from pip._internal.utils.filesystem import adjacent_tmp_file, replace
|
| 14 |
+
from pip._internal.utils.misc import ensure_dir
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def is_from_cache(response: Response) -> bool:
|
| 18 |
+
return getattr(response, "from_cache", False)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@contextmanager
|
| 22 |
+
def suppressed_cache_errors() -> Generator[None, None, None]:
|
| 23 |
+
"""If we can't access the cache then we can just skip caching and process
|
| 24 |
+
requests as if caching wasn't enabled.
|
| 25 |
+
"""
|
| 26 |
+
try:
|
| 27 |
+
yield
|
| 28 |
+
except OSError:
|
| 29 |
+
pass
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class SafeFileCache(SeparateBodyBaseCache):
|
| 33 |
+
"""
|
| 34 |
+
A file based cache which is safe to use even when the target directory may
|
| 35 |
+
not be accessible or writable.
|
| 36 |
+
|
| 37 |
+
There is a race condition when two processes try to write and/or read the
|
| 38 |
+
same entry at the same time, since each entry consists of two separate
|
| 39 |
+
files (https://github.com/psf/cachecontrol/issues/324). We therefore have
|
| 40 |
+
additional logic that makes sure that both files to be present before
|
| 41 |
+
returning an entry; this fixes the read side of the race condition.
|
| 42 |
+
|
| 43 |
+
For the write side, we assume that the server will only ever return the
|
| 44 |
+
same data for the same URL, which ought to be the case for files pip is
|
| 45 |
+
downloading. PyPI does not have a mechanism to swap out a wheel for
|
| 46 |
+
another wheel, for example. If this assumption is not true, the
|
| 47 |
+
CacheControl issue will need to be fixed.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(self, directory: str) -> None:
|
| 51 |
+
assert directory is not None, "Cache directory must not be None."
|
| 52 |
+
super().__init__()
|
| 53 |
+
self.directory = directory
|
| 54 |
+
|
| 55 |
+
def _get_cache_path(self, name: str) -> str:
|
| 56 |
+
# From cachecontrol.caches.file_cache.FileCache._fn, brought into our
|
| 57 |
+
# class for backwards-compatibility and to avoid using a non-public
|
| 58 |
+
# method.
|
| 59 |
+
hashed = SeparateBodyFileCache.encode(name)
|
| 60 |
+
parts = list(hashed[:5]) + [hashed]
|
| 61 |
+
return os.path.join(self.directory, *parts)
|
| 62 |
+
|
| 63 |
+
def get(self, key: str) -> Optional[bytes]:
|
| 64 |
+
# The cache entry is only valid if both metadata and body exist.
|
| 65 |
+
metadata_path = self._get_cache_path(key)
|
| 66 |
+
body_path = metadata_path + ".body"
|
| 67 |
+
if not (os.path.exists(metadata_path) and os.path.exists(body_path)):
|
| 68 |
+
return None
|
| 69 |
+
with suppressed_cache_errors():
|
| 70 |
+
with open(metadata_path, "rb") as f:
|
| 71 |
+
return f.read()
|
| 72 |
+
|
| 73 |
+
def _write(self, path: str, data: bytes) -> None:
|
| 74 |
+
with suppressed_cache_errors():
|
| 75 |
+
ensure_dir(os.path.dirname(path))
|
| 76 |
+
|
| 77 |
+
with adjacent_tmp_file(path) as f:
|
| 78 |
+
f.write(data)
|
| 79 |
+
|
| 80 |
+
replace(f.name, path)
|
| 81 |
+
|
| 82 |
+
def set(
|
| 83 |
+
self, key: str, value: bytes, expires: Union[int, datetime, None] = None
|
| 84 |
+
) -> None:
|
| 85 |
+
path = self._get_cache_path(key)
|
| 86 |
+
self._write(path, value)
|
| 87 |
+
|
| 88 |
+
def delete(self, key: str) -> None:
|
| 89 |
+
path = self._get_cache_path(key)
|
| 90 |
+
with suppressed_cache_errors():
|
| 91 |
+
os.remove(path)
|
| 92 |
+
with suppressed_cache_errors():
|
| 93 |
+
os.remove(path + ".body")
|
| 94 |
+
|
| 95 |
+
def get_body(self, key: str) -> Optional[BinaryIO]:
|
| 96 |
+
# The cache entry is only valid if both metadata and body exist.
|
| 97 |
+
metadata_path = self._get_cache_path(key)
|
| 98 |
+
body_path = metadata_path + ".body"
|
| 99 |
+
if not (os.path.exists(metadata_path) and os.path.exists(body_path)):
|
| 100 |
+
return None
|
| 101 |
+
with suppressed_cache_errors():
|
| 102 |
+
return open(body_path, "rb")
|
| 103 |
+
|
| 104 |
+
def set_body(self, key: str, body: bytes) -> None:
|
| 105 |
+
path = self._get_cache_path(key) + ".body"
|
| 106 |
+
self._write(path, body)
|
.venv/lib/python3.11/site-packages/pip/_internal/network/download.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Download files with progress indicators.
|
| 2 |
+
"""
|
| 3 |
+
import email.message
|
| 4 |
+
import logging
|
| 5 |
+
import mimetypes
|
| 6 |
+
import os
|
| 7 |
+
from typing import Iterable, Optional, Tuple
|
| 8 |
+
|
| 9 |
+
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
|
| 10 |
+
|
| 11 |
+
from pip._internal.cli.progress_bars import get_download_progress_renderer
|
| 12 |
+
from pip._internal.exceptions import NetworkConnectionError
|
| 13 |
+
from pip._internal.models.index import PyPI
|
| 14 |
+
from pip._internal.models.link import Link
|
| 15 |
+
from pip._internal.network.cache import is_from_cache
|
| 16 |
+
from pip._internal.network.session import PipSession
|
| 17 |
+
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
|
| 18 |
+
from pip._internal.utils.misc import format_size, redact_auth_from_url, splitext
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _get_http_response_size(resp: Response) -> Optional[int]:
|
| 24 |
+
try:
|
| 25 |
+
return int(resp.headers["content-length"])
|
| 26 |
+
except (ValueError, KeyError, TypeError):
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _prepare_download(
|
| 31 |
+
resp: Response,
|
| 32 |
+
link: Link,
|
| 33 |
+
progress_bar: str,
|
| 34 |
+
) -> Iterable[bytes]:
|
| 35 |
+
total_length = _get_http_response_size(resp)
|
| 36 |
+
|
| 37 |
+
if link.netloc == PyPI.file_storage_domain:
|
| 38 |
+
url = link.show_url
|
| 39 |
+
else:
|
| 40 |
+
url = link.url_without_fragment
|
| 41 |
+
|
| 42 |
+
logged_url = redact_auth_from_url(url)
|
| 43 |
+
|
| 44 |
+
if total_length:
|
| 45 |
+
logged_url = f"{logged_url} ({format_size(total_length)})"
|
| 46 |
+
|
| 47 |
+
if is_from_cache(resp):
|
| 48 |
+
logger.info("Using cached %s", logged_url)
|
| 49 |
+
else:
|
| 50 |
+
logger.info("Downloading %s", logged_url)
|
| 51 |
+
|
| 52 |
+
if logger.getEffectiveLevel() > logging.INFO:
|
| 53 |
+
show_progress = False
|
| 54 |
+
elif is_from_cache(resp):
|
| 55 |
+
show_progress = False
|
| 56 |
+
elif not total_length:
|
| 57 |
+
show_progress = True
|
| 58 |
+
elif total_length > (40 * 1000):
|
| 59 |
+
show_progress = True
|
| 60 |
+
else:
|
| 61 |
+
show_progress = False
|
| 62 |
+
|
| 63 |
+
chunks = response_chunks(resp, CONTENT_CHUNK_SIZE)
|
| 64 |
+
|
| 65 |
+
if not show_progress:
|
| 66 |
+
return chunks
|
| 67 |
+
|
| 68 |
+
renderer = get_download_progress_renderer(bar_type=progress_bar, size=total_length)
|
| 69 |
+
return renderer(chunks)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def sanitize_content_filename(filename: str) -> str:
|
| 73 |
+
"""
|
| 74 |
+
Sanitize the "filename" value from a Content-Disposition header.
|
| 75 |
+
"""
|
| 76 |
+
return os.path.basename(filename)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def parse_content_disposition(content_disposition: str, default_filename: str) -> str:
|
| 80 |
+
"""
|
| 81 |
+
Parse the "filename" value from a Content-Disposition header, and
|
| 82 |
+
return the default filename if the result is empty.
|
| 83 |
+
"""
|
| 84 |
+
m = email.message.Message()
|
| 85 |
+
m["content-type"] = content_disposition
|
| 86 |
+
filename = m.get_param("filename")
|
| 87 |
+
if filename:
|
| 88 |
+
# We need to sanitize the filename to prevent directory traversal
|
| 89 |
+
# in case the filename contains ".." path parts.
|
| 90 |
+
filename = sanitize_content_filename(str(filename))
|
| 91 |
+
return filename or default_filename
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _get_http_response_filename(resp: Response, link: Link) -> str:
|
| 95 |
+
"""Get an ideal filename from the given HTTP response, falling back to
|
| 96 |
+
the link filename if not provided.
|
| 97 |
+
"""
|
| 98 |
+
filename = link.filename # fallback
|
| 99 |
+
# Have a look at the Content-Disposition header for a better guess
|
| 100 |
+
content_disposition = resp.headers.get("content-disposition")
|
| 101 |
+
if content_disposition:
|
| 102 |
+
filename = parse_content_disposition(content_disposition, filename)
|
| 103 |
+
ext: Optional[str] = splitext(filename)[1]
|
| 104 |
+
if not ext:
|
| 105 |
+
ext = mimetypes.guess_extension(resp.headers.get("content-type", ""))
|
| 106 |
+
if ext:
|
| 107 |
+
filename += ext
|
| 108 |
+
if not ext and link.url != resp.url:
|
| 109 |
+
ext = os.path.splitext(resp.url)[1]
|
| 110 |
+
if ext:
|
| 111 |
+
filename += ext
|
| 112 |
+
return filename
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _http_get_download(session: PipSession, link: Link) -> Response:
|
| 116 |
+
target_url = link.url.split("#", 1)[0]
|
| 117 |
+
resp = session.get(target_url, headers=HEADERS, stream=True)
|
| 118 |
+
raise_for_status(resp)
|
| 119 |
+
return resp
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class Downloader:
|
| 123 |
+
def __init__(
|
| 124 |
+
self,
|
| 125 |
+
session: PipSession,
|
| 126 |
+
progress_bar: str,
|
| 127 |
+
) -> None:
|
| 128 |
+
self._session = session
|
| 129 |
+
self._progress_bar = progress_bar
|
| 130 |
+
|
| 131 |
+
def __call__(self, link: Link, location: str) -> Tuple[str, str]:
|
| 132 |
+
"""Download the file given by link into location."""
|
| 133 |
+
try:
|
| 134 |
+
resp = _http_get_download(self._session, link)
|
| 135 |
+
except NetworkConnectionError as e:
|
| 136 |
+
assert e.response is not None
|
| 137 |
+
logger.critical(
|
| 138 |
+
"HTTP error %s while getting %s", e.response.status_code, link
|
| 139 |
+
)
|
| 140 |
+
raise
|
| 141 |
+
|
| 142 |
+
filename = _get_http_response_filename(resp, link)
|
| 143 |
+
filepath = os.path.join(location, filename)
|
| 144 |
+
|
| 145 |
+
chunks = _prepare_download(resp, link, self._progress_bar)
|
| 146 |
+
with open(filepath, "wb") as content_file:
|
| 147 |
+
for chunk in chunks:
|
| 148 |
+
content_file.write(chunk)
|
| 149 |
+
content_type = resp.headers.get("Content-Type", "")
|
| 150 |
+
return filepath, content_type
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class BatchDownloader:
|
| 154 |
+
def __init__(
|
| 155 |
+
self,
|
| 156 |
+
session: PipSession,
|
| 157 |
+
progress_bar: str,
|
| 158 |
+
) -> None:
|
| 159 |
+
self._session = session
|
| 160 |
+
self._progress_bar = progress_bar
|
| 161 |
+
|
| 162 |
+
def __call__(
|
| 163 |
+
self, links: Iterable[Link], location: str
|
| 164 |
+
) -> Iterable[Tuple[Link, Tuple[str, str]]]:
|
| 165 |
+
"""Download the files given by links into location."""
|
| 166 |
+
for link in links:
|
| 167 |
+
try:
|
| 168 |
+
resp = _http_get_download(self._session, link)
|
| 169 |
+
except NetworkConnectionError as e:
|
| 170 |
+
assert e.response is not None
|
| 171 |
+
logger.critical(
|
| 172 |
+
"HTTP error %s while getting %s",
|
| 173 |
+
e.response.status_code,
|
| 174 |
+
link,
|
| 175 |
+
)
|
| 176 |
+
raise
|
| 177 |
+
|
| 178 |
+
filename = _get_http_response_filename(resp, link)
|
| 179 |
+
filepath = os.path.join(location, filename)
|
| 180 |
+
|
| 181 |
+
chunks = _prepare_download(resp, link, self._progress_bar)
|
| 182 |
+
with open(filepath, "wb") as content_file:
|
| 183 |
+
for chunk in chunks:
|
| 184 |
+
content_file.write(chunk)
|
| 185 |
+
content_type = resp.headers.get("Content-Type", "")
|
| 186 |
+
yield link, (filepath, content_type)
|
.venv/lib/python3.11/site-packages/pip/_internal/network/lazy_wheel.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lazy ZIP over HTTP"""
|
| 2 |
+
|
| 3 |
+
__all__ = ["HTTPRangeRequestUnsupported", "dist_from_wheel_url"]
|
| 4 |
+
|
| 5 |
+
from bisect import bisect_left, bisect_right
|
| 6 |
+
from contextlib import contextmanager
|
| 7 |
+
from tempfile import NamedTemporaryFile
|
| 8 |
+
from typing import Any, Dict, Generator, List, Optional, Tuple
|
| 9 |
+
from zipfile import BadZipFile, ZipFile
|
| 10 |
+
|
| 11 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 12 |
+
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
|
| 13 |
+
|
| 14 |
+
from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution
|
| 15 |
+
from pip._internal.network.session import PipSession
|
| 16 |
+
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class HTTPRangeRequestUnsupported(Exception):
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def dist_from_wheel_url(name: str, url: str, session: PipSession) -> BaseDistribution:
|
| 24 |
+
"""Return a distribution object from the given wheel URL.
|
| 25 |
+
|
| 26 |
+
This uses HTTP range requests to only fetch the portion of the wheel
|
| 27 |
+
containing metadata, just enough for the object to be constructed.
|
| 28 |
+
If such requests are not supported, HTTPRangeRequestUnsupported
|
| 29 |
+
is raised.
|
| 30 |
+
"""
|
| 31 |
+
with LazyZipOverHTTP(url, session) as zf:
|
| 32 |
+
# For read-only ZIP files, ZipFile only needs methods read,
|
| 33 |
+
# seek, seekable and tell, not the whole IO protocol.
|
| 34 |
+
wheel = MemoryWheel(zf.name, zf) # type: ignore
|
| 35 |
+
# After context manager exit, wheel.name
|
| 36 |
+
# is an invalid file by intention.
|
| 37 |
+
return get_wheel_distribution(wheel, canonicalize_name(name))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class LazyZipOverHTTP:
|
| 41 |
+
"""File-like object mapped to a ZIP file over HTTP.
|
| 42 |
+
|
| 43 |
+
This uses HTTP range requests to lazily fetch the file's content,
|
| 44 |
+
which is supposed to be fed to ZipFile. If such requests are not
|
| 45 |
+
supported by the server, raise HTTPRangeRequestUnsupported
|
| 46 |
+
during initialization.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(
|
| 50 |
+
self, url: str, session: PipSession, chunk_size: int = CONTENT_CHUNK_SIZE
|
| 51 |
+
) -> None:
|
| 52 |
+
head = session.head(url, headers=HEADERS)
|
| 53 |
+
raise_for_status(head)
|
| 54 |
+
assert head.status_code == 200
|
| 55 |
+
self._session, self._url, self._chunk_size = session, url, chunk_size
|
| 56 |
+
self._length = int(head.headers["Content-Length"])
|
| 57 |
+
self._file = NamedTemporaryFile()
|
| 58 |
+
self.truncate(self._length)
|
| 59 |
+
self._left: List[int] = []
|
| 60 |
+
self._right: List[int] = []
|
| 61 |
+
if "bytes" not in head.headers.get("Accept-Ranges", "none"):
|
| 62 |
+
raise HTTPRangeRequestUnsupported("range request is not supported")
|
| 63 |
+
self._check_zip()
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def mode(self) -> str:
|
| 67 |
+
"""Opening mode, which is always rb."""
|
| 68 |
+
return "rb"
|
| 69 |
+
|
| 70 |
+
@property
|
| 71 |
+
def name(self) -> str:
|
| 72 |
+
"""Path to the underlying file."""
|
| 73 |
+
return self._file.name
|
| 74 |
+
|
| 75 |
+
def seekable(self) -> bool:
|
| 76 |
+
"""Return whether random access is supported, which is True."""
|
| 77 |
+
return True
|
| 78 |
+
|
| 79 |
+
def close(self) -> None:
|
| 80 |
+
"""Close the file."""
|
| 81 |
+
self._file.close()
|
| 82 |
+
|
| 83 |
+
@property
|
| 84 |
+
def closed(self) -> bool:
|
| 85 |
+
"""Whether the file is closed."""
|
| 86 |
+
return self._file.closed
|
| 87 |
+
|
| 88 |
+
def read(self, size: int = -1) -> bytes:
|
| 89 |
+
"""Read up to size bytes from the object and return them.
|
| 90 |
+
|
| 91 |
+
As a convenience, if size is unspecified or -1,
|
| 92 |
+
all bytes until EOF are returned. Fewer than
|
| 93 |
+
size bytes may be returned if EOF is reached.
|
| 94 |
+
"""
|
| 95 |
+
download_size = max(size, self._chunk_size)
|
| 96 |
+
start, length = self.tell(), self._length
|
| 97 |
+
stop = length if size < 0 else min(start + download_size, length)
|
| 98 |
+
start = max(0, stop - download_size)
|
| 99 |
+
self._download(start, stop - 1)
|
| 100 |
+
return self._file.read(size)
|
| 101 |
+
|
| 102 |
+
def readable(self) -> bool:
|
| 103 |
+
"""Return whether the file is readable, which is True."""
|
| 104 |
+
return True
|
| 105 |
+
|
| 106 |
+
def seek(self, offset: int, whence: int = 0) -> int:
|
| 107 |
+
"""Change stream position and return the new absolute position.
|
| 108 |
+
|
| 109 |
+
Seek to offset relative position indicated by whence:
|
| 110 |
+
* 0: Start of stream (the default). pos should be >= 0;
|
| 111 |
+
* 1: Current position - pos may be negative;
|
| 112 |
+
* 2: End of stream - pos usually negative.
|
| 113 |
+
"""
|
| 114 |
+
return self._file.seek(offset, whence)
|
| 115 |
+
|
| 116 |
+
def tell(self) -> int:
|
| 117 |
+
"""Return the current position."""
|
| 118 |
+
return self._file.tell()
|
| 119 |
+
|
| 120 |
+
def truncate(self, size: Optional[int] = None) -> int:
|
| 121 |
+
"""Resize the stream to the given size in bytes.
|
| 122 |
+
|
| 123 |
+
If size is unspecified resize to the current position.
|
| 124 |
+
The current stream position isn't changed.
|
| 125 |
+
|
| 126 |
+
Return the new file size.
|
| 127 |
+
"""
|
| 128 |
+
return self._file.truncate(size)
|
| 129 |
+
|
| 130 |
+
def writable(self) -> bool:
|
| 131 |
+
"""Return False."""
|
| 132 |
+
return False
|
| 133 |
+
|
| 134 |
+
def __enter__(self) -> "LazyZipOverHTTP":
|
| 135 |
+
self._file.__enter__()
|
| 136 |
+
return self
|
| 137 |
+
|
| 138 |
+
def __exit__(self, *exc: Any) -> None:
|
| 139 |
+
self._file.__exit__(*exc)
|
| 140 |
+
|
| 141 |
+
@contextmanager
|
| 142 |
+
def _stay(self) -> Generator[None, None, None]:
|
| 143 |
+
"""Return a context manager keeping the position.
|
| 144 |
+
|
| 145 |
+
At the end of the block, seek back to original position.
|
| 146 |
+
"""
|
| 147 |
+
pos = self.tell()
|
| 148 |
+
try:
|
| 149 |
+
yield
|
| 150 |
+
finally:
|
| 151 |
+
self.seek(pos)
|
| 152 |
+
|
| 153 |
+
def _check_zip(self) -> None:
|
| 154 |
+
"""Check and download until the file is a valid ZIP."""
|
| 155 |
+
end = self._length - 1
|
| 156 |
+
for start in reversed(range(0, end, self._chunk_size)):
|
| 157 |
+
self._download(start, end)
|
| 158 |
+
with self._stay():
|
| 159 |
+
try:
|
| 160 |
+
# For read-only ZIP files, ZipFile only needs
|
| 161 |
+
# methods read, seek, seekable and tell.
|
| 162 |
+
ZipFile(self) # type: ignore
|
| 163 |
+
except BadZipFile:
|
| 164 |
+
pass
|
| 165 |
+
else:
|
| 166 |
+
break
|
| 167 |
+
|
| 168 |
+
def _stream_response(
|
| 169 |
+
self, start: int, end: int, base_headers: Dict[str, str] = HEADERS
|
| 170 |
+
) -> Response:
|
| 171 |
+
"""Return HTTP response to a range request from start to end."""
|
| 172 |
+
headers = base_headers.copy()
|
| 173 |
+
headers["Range"] = f"bytes={start}-{end}"
|
| 174 |
+
# TODO: Get range requests to be correctly cached
|
| 175 |
+
headers["Cache-Control"] = "no-cache"
|
| 176 |
+
return self._session.get(self._url, headers=headers, stream=True)
|
| 177 |
+
|
| 178 |
+
def _merge(
|
| 179 |
+
self, start: int, end: int, left: int, right: int
|
| 180 |
+
) -> Generator[Tuple[int, int], None, None]:
|
| 181 |
+
"""Return a generator of intervals to be fetched.
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
start (int): Start of needed interval
|
| 185 |
+
end (int): End of needed interval
|
| 186 |
+
left (int): Index of first overlapping downloaded data
|
| 187 |
+
right (int): Index after last overlapping downloaded data
|
| 188 |
+
"""
|
| 189 |
+
lslice, rslice = self._left[left:right], self._right[left:right]
|
| 190 |
+
i = start = min([start] + lslice[:1])
|
| 191 |
+
end = max([end] + rslice[-1:])
|
| 192 |
+
for j, k in zip(lslice, rslice):
|
| 193 |
+
if j > i:
|
| 194 |
+
yield i, j - 1
|
| 195 |
+
i = k + 1
|
| 196 |
+
if i <= end:
|
| 197 |
+
yield i, end
|
| 198 |
+
self._left[left:right], self._right[left:right] = [start], [end]
|
| 199 |
+
|
| 200 |
+
def _download(self, start: int, end: int) -> None:
|
| 201 |
+
"""Download bytes from start to end inclusively."""
|
| 202 |
+
with self._stay():
|
| 203 |
+
left = bisect_left(self._right, start)
|
| 204 |
+
right = bisect_right(self._left, end)
|
| 205 |
+
for start, end in self._merge(start, end, left, right):
|
| 206 |
+
response = self._stream_response(start, end)
|
| 207 |
+
response.raise_for_status()
|
| 208 |
+
self.seek(start)
|
| 209 |
+
for chunk in response_chunks(response, self._chunk_size):
|
| 210 |
+
self._file.write(chunk)
|
.venv/lib/python3.11/site-packages/pip/_internal/network/session.py
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PipSession and supporting code, containing all pip-specific
|
| 2 |
+
network request configuration and behavior.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import email.utils
|
| 6 |
+
import io
|
| 7 |
+
import ipaddress
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
import mimetypes
|
| 11 |
+
import os
|
| 12 |
+
import platform
|
| 13 |
+
import shutil
|
| 14 |
+
import subprocess
|
| 15 |
+
import sys
|
| 16 |
+
import urllib.parse
|
| 17 |
+
import warnings
|
| 18 |
+
from typing import (
|
| 19 |
+
TYPE_CHECKING,
|
| 20 |
+
Any,
|
| 21 |
+
Dict,
|
| 22 |
+
Generator,
|
| 23 |
+
List,
|
| 24 |
+
Mapping,
|
| 25 |
+
Optional,
|
| 26 |
+
Sequence,
|
| 27 |
+
Tuple,
|
| 28 |
+
Union,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
from pip._vendor import requests, urllib3
|
| 32 |
+
from pip._vendor.cachecontrol import CacheControlAdapter as _BaseCacheControlAdapter
|
| 33 |
+
from pip._vendor.requests.adapters import DEFAULT_POOLBLOCK, BaseAdapter
|
| 34 |
+
from pip._vendor.requests.adapters import HTTPAdapter as _BaseHTTPAdapter
|
| 35 |
+
from pip._vendor.requests.models import PreparedRequest, Response
|
| 36 |
+
from pip._vendor.requests.structures import CaseInsensitiveDict
|
| 37 |
+
from pip._vendor.urllib3.connectionpool import ConnectionPool
|
| 38 |
+
from pip._vendor.urllib3.exceptions import InsecureRequestWarning
|
| 39 |
+
|
| 40 |
+
from pip import __version__
|
| 41 |
+
from pip._internal.metadata import get_default_environment
|
| 42 |
+
from pip._internal.models.link import Link
|
| 43 |
+
from pip._internal.network.auth import MultiDomainBasicAuth
|
| 44 |
+
from pip._internal.network.cache import SafeFileCache
|
| 45 |
+
|
| 46 |
+
# Import ssl from compat so the initial import occurs in only one place.
|
| 47 |
+
from pip._internal.utils.compat import has_tls
|
| 48 |
+
from pip._internal.utils.glibc import libc_ver
|
| 49 |
+
from pip._internal.utils.misc import build_url_from_netloc, parse_netloc
|
| 50 |
+
from pip._internal.utils.urls import url_to_path
|
| 51 |
+
|
| 52 |
+
if TYPE_CHECKING:
|
| 53 |
+
from ssl import SSLContext
|
| 54 |
+
|
| 55 |
+
from pip._vendor.urllib3.poolmanager import PoolManager
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
logger = logging.getLogger(__name__)
|
| 59 |
+
|
| 60 |
+
SecureOrigin = Tuple[str, str, Optional[Union[int, str]]]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# Ignore warning raised when using --trusted-host.
|
| 64 |
+
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
SECURE_ORIGINS: List[SecureOrigin] = [
|
| 68 |
+
# protocol, hostname, port
|
| 69 |
+
# Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC)
|
| 70 |
+
("https", "*", "*"),
|
| 71 |
+
("*", "localhost", "*"),
|
| 72 |
+
("*", "127.0.0.0/8", "*"),
|
| 73 |
+
("*", "::1/128", "*"),
|
| 74 |
+
("file", "*", None),
|
| 75 |
+
# ssh is always secure.
|
| 76 |
+
("ssh", "*", "*"),
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# These are environment variables present when running under various
|
| 81 |
+
# CI systems. For each variable, some CI systems that use the variable
|
| 82 |
+
# are indicated. The collection was chosen so that for each of a number
|
| 83 |
+
# of popular systems, at least one of the environment variables is used.
|
| 84 |
+
# This list is used to provide some indication of and lower bound for
|
| 85 |
+
# CI traffic to PyPI. Thus, it is okay if the list is not comprehensive.
|
| 86 |
+
# For more background, see: https://github.com/pypa/pip/issues/5499
|
| 87 |
+
CI_ENVIRONMENT_VARIABLES = (
|
| 88 |
+
# Azure Pipelines
|
| 89 |
+
"BUILD_BUILDID",
|
| 90 |
+
# Jenkins
|
| 91 |
+
"BUILD_ID",
|
| 92 |
+
# AppVeyor, CircleCI, Codeship, Gitlab CI, Shippable, Travis CI
|
| 93 |
+
"CI",
|
| 94 |
+
# Explicit environment variable.
|
| 95 |
+
"PIP_IS_CI",
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def looks_like_ci() -> bool:
|
| 100 |
+
"""
|
| 101 |
+
Return whether it looks like pip is running under CI.
|
| 102 |
+
"""
|
| 103 |
+
# We don't use the method of checking for a tty (e.g. using isatty())
|
| 104 |
+
# because some CI systems mimic a tty (e.g. Travis CI). Thus that
|
| 105 |
+
# method doesn't provide definitive information in either direction.
|
| 106 |
+
return any(name in os.environ for name in CI_ENVIRONMENT_VARIABLES)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def user_agent() -> str:
|
| 110 |
+
"""
|
| 111 |
+
Return a string representing the user agent.
|
| 112 |
+
"""
|
| 113 |
+
data: Dict[str, Any] = {
|
| 114 |
+
"installer": {"name": "pip", "version": __version__},
|
| 115 |
+
"python": platform.python_version(),
|
| 116 |
+
"implementation": {
|
| 117 |
+
"name": platform.python_implementation(),
|
| 118 |
+
},
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if data["implementation"]["name"] == "CPython":
|
| 122 |
+
data["implementation"]["version"] = platform.python_version()
|
| 123 |
+
elif data["implementation"]["name"] == "PyPy":
|
| 124 |
+
pypy_version_info = sys.pypy_version_info # type: ignore
|
| 125 |
+
if pypy_version_info.releaselevel == "final":
|
| 126 |
+
pypy_version_info = pypy_version_info[:3]
|
| 127 |
+
data["implementation"]["version"] = ".".join(
|
| 128 |
+
[str(x) for x in pypy_version_info]
|
| 129 |
+
)
|
| 130 |
+
elif data["implementation"]["name"] == "Jython":
|
| 131 |
+
# Complete Guess
|
| 132 |
+
data["implementation"]["version"] = platform.python_version()
|
| 133 |
+
elif data["implementation"]["name"] == "IronPython":
|
| 134 |
+
# Complete Guess
|
| 135 |
+
data["implementation"]["version"] = platform.python_version()
|
| 136 |
+
|
| 137 |
+
if sys.platform.startswith("linux"):
|
| 138 |
+
from pip._vendor import distro
|
| 139 |
+
|
| 140 |
+
linux_distribution = distro.name(), distro.version(), distro.codename()
|
| 141 |
+
distro_infos: Dict[str, Any] = dict(
|
| 142 |
+
filter(
|
| 143 |
+
lambda x: x[1],
|
| 144 |
+
zip(["name", "version", "id"], linux_distribution),
|
| 145 |
+
)
|
| 146 |
+
)
|
| 147 |
+
libc = dict(
|
| 148 |
+
filter(
|
| 149 |
+
lambda x: x[1],
|
| 150 |
+
zip(["lib", "version"], libc_ver()),
|
| 151 |
+
)
|
| 152 |
+
)
|
| 153 |
+
if libc:
|
| 154 |
+
distro_infos["libc"] = libc
|
| 155 |
+
if distro_infos:
|
| 156 |
+
data["distro"] = distro_infos
|
| 157 |
+
|
| 158 |
+
if sys.platform.startswith("darwin") and platform.mac_ver()[0]:
|
| 159 |
+
data["distro"] = {"name": "macOS", "version": platform.mac_ver()[0]}
|
| 160 |
+
|
| 161 |
+
if platform.system():
|
| 162 |
+
data.setdefault("system", {})["name"] = platform.system()
|
| 163 |
+
|
| 164 |
+
if platform.release():
|
| 165 |
+
data.setdefault("system", {})["release"] = platform.release()
|
| 166 |
+
|
| 167 |
+
if platform.machine():
|
| 168 |
+
data["cpu"] = platform.machine()
|
| 169 |
+
|
| 170 |
+
if has_tls():
|
| 171 |
+
import _ssl as ssl
|
| 172 |
+
|
| 173 |
+
data["openssl_version"] = ssl.OPENSSL_VERSION
|
| 174 |
+
|
| 175 |
+
setuptools_dist = get_default_environment().get_distribution("setuptools")
|
| 176 |
+
if setuptools_dist is not None:
|
| 177 |
+
data["setuptools_version"] = str(setuptools_dist.version)
|
| 178 |
+
|
| 179 |
+
if shutil.which("rustc") is not None:
|
| 180 |
+
# If for any reason `rustc --version` fails, silently ignore it
|
| 181 |
+
try:
|
| 182 |
+
rustc_output = subprocess.check_output(
|
| 183 |
+
["rustc", "--version"], stderr=subprocess.STDOUT, timeout=0.5
|
| 184 |
+
)
|
| 185 |
+
except Exception:
|
| 186 |
+
pass
|
| 187 |
+
else:
|
| 188 |
+
if rustc_output.startswith(b"rustc "):
|
| 189 |
+
# The format of `rustc --version` is:
|
| 190 |
+
# `b'rustc 1.52.1 (9bc8c42bb 2021-05-09)\n'`
|
| 191 |
+
# We extract just the middle (1.52.1) part
|
| 192 |
+
data["rustc_version"] = rustc_output.split(b" ")[1].decode()
|
| 193 |
+
|
| 194 |
+
# Use None rather than False so as not to give the impression that
|
| 195 |
+
# pip knows it is not being run under CI. Rather, it is a null or
|
| 196 |
+
# inconclusive result. Also, we include some value rather than no
|
| 197 |
+
# value to make it easier to know that the check has been run.
|
| 198 |
+
data["ci"] = True if looks_like_ci() else None
|
| 199 |
+
|
| 200 |
+
user_data = os.environ.get("PIP_USER_AGENT_USER_DATA")
|
| 201 |
+
if user_data is not None:
|
| 202 |
+
data["user_data"] = user_data
|
| 203 |
+
|
| 204 |
+
return "{data[installer][name]}/{data[installer][version]} {json}".format(
|
| 205 |
+
data=data,
|
| 206 |
+
json=json.dumps(data, separators=(",", ":"), sort_keys=True),
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class LocalFSAdapter(BaseAdapter):
|
| 211 |
+
def send(
|
| 212 |
+
self,
|
| 213 |
+
request: PreparedRequest,
|
| 214 |
+
stream: bool = False,
|
| 215 |
+
timeout: Optional[Union[float, Tuple[float, float]]] = None,
|
| 216 |
+
verify: Union[bool, str] = True,
|
| 217 |
+
cert: Optional[Union[str, Tuple[str, str]]] = None,
|
| 218 |
+
proxies: Optional[Mapping[str, str]] = None,
|
| 219 |
+
) -> Response:
|
| 220 |
+
pathname = url_to_path(request.url)
|
| 221 |
+
|
| 222 |
+
resp = Response()
|
| 223 |
+
resp.status_code = 200
|
| 224 |
+
resp.url = request.url
|
| 225 |
+
|
| 226 |
+
try:
|
| 227 |
+
stats = os.stat(pathname)
|
| 228 |
+
except OSError as exc:
|
| 229 |
+
# format the exception raised as a io.BytesIO object,
|
| 230 |
+
# to return a better error message:
|
| 231 |
+
resp.status_code = 404
|
| 232 |
+
resp.reason = type(exc).__name__
|
| 233 |
+
resp.raw = io.BytesIO(f"{resp.reason}: {exc}".encode("utf8"))
|
| 234 |
+
else:
|
| 235 |
+
modified = email.utils.formatdate(stats.st_mtime, usegmt=True)
|
| 236 |
+
content_type = mimetypes.guess_type(pathname)[0] or "text/plain"
|
| 237 |
+
resp.headers = CaseInsensitiveDict(
|
| 238 |
+
{
|
| 239 |
+
"Content-Type": content_type,
|
| 240 |
+
"Content-Length": stats.st_size,
|
| 241 |
+
"Last-Modified": modified,
|
| 242 |
+
}
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
resp.raw = open(pathname, "rb")
|
| 246 |
+
resp.close = resp.raw.close
|
| 247 |
+
|
| 248 |
+
return resp
|
| 249 |
+
|
| 250 |
+
def close(self) -> None:
|
| 251 |
+
pass
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
class _SSLContextAdapterMixin:
|
| 255 |
+
"""Mixin to add the ``ssl_context`` constructor argument to HTTP adapters.
|
| 256 |
+
|
| 257 |
+
The additional argument is forwarded directly to the pool manager. This allows us
|
| 258 |
+
to dynamically decide what SSL store to use at runtime, which is used to implement
|
| 259 |
+
the optional ``truststore`` backend.
|
| 260 |
+
"""
|
| 261 |
+
|
| 262 |
+
def __init__(
|
| 263 |
+
self,
|
| 264 |
+
*,
|
| 265 |
+
ssl_context: Optional["SSLContext"] = None,
|
| 266 |
+
**kwargs: Any,
|
| 267 |
+
) -> None:
|
| 268 |
+
self._ssl_context = ssl_context
|
| 269 |
+
super().__init__(**kwargs)
|
| 270 |
+
|
| 271 |
+
def init_poolmanager(
|
| 272 |
+
self,
|
| 273 |
+
connections: int,
|
| 274 |
+
maxsize: int,
|
| 275 |
+
block: bool = DEFAULT_POOLBLOCK,
|
| 276 |
+
**pool_kwargs: Any,
|
| 277 |
+
) -> "PoolManager":
|
| 278 |
+
if self._ssl_context is not None:
|
| 279 |
+
pool_kwargs.setdefault("ssl_context", self._ssl_context)
|
| 280 |
+
return super().init_poolmanager( # type: ignore[misc]
|
| 281 |
+
connections=connections,
|
| 282 |
+
maxsize=maxsize,
|
| 283 |
+
block=block,
|
| 284 |
+
**pool_kwargs,
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
class HTTPAdapter(_SSLContextAdapterMixin, _BaseHTTPAdapter):
|
| 289 |
+
pass
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
class CacheControlAdapter(_SSLContextAdapterMixin, _BaseCacheControlAdapter):
|
| 293 |
+
pass
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
class InsecureHTTPAdapter(HTTPAdapter):
|
| 297 |
+
def cert_verify(
|
| 298 |
+
self,
|
| 299 |
+
conn: ConnectionPool,
|
| 300 |
+
url: str,
|
| 301 |
+
verify: Union[bool, str],
|
| 302 |
+
cert: Optional[Union[str, Tuple[str, str]]],
|
| 303 |
+
) -> None:
|
| 304 |
+
super().cert_verify(conn=conn, url=url, verify=False, cert=cert)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
class InsecureCacheControlAdapter(CacheControlAdapter):
|
| 308 |
+
def cert_verify(
|
| 309 |
+
self,
|
| 310 |
+
conn: ConnectionPool,
|
| 311 |
+
url: str,
|
| 312 |
+
verify: Union[bool, str],
|
| 313 |
+
cert: Optional[Union[str, Tuple[str, str]]],
|
| 314 |
+
) -> None:
|
| 315 |
+
super().cert_verify(conn=conn, url=url, verify=False, cert=cert)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
class PipSession(requests.Session):
|
| 319 |
+
timeout: Optional[int] = None
|
| 320 |
+
|
| 321 |
+
def __init__(
|
| 322 |
+
self,
|
| 323 |
+
*args: Any,
|
| 324 |
+
retries: int = 0,
|
| 325 |
+
cache: Optional[str] = None,
|
| 326 |
+
trusted_hosts: Sequence[str] = (),
|
| 327 |
+
index_urls: Optional[List[str]] = None,
|
| 328 |
+
ssl_context: Optional["SSLContext"] = None,
|
| 329 |
+
**kwargs: Any,
|
| 330 |
+
) -> None:
|
| 331 |
+
"""
|
| 332 |
+
:param trusted_hosts: Domains not to emit warnings for when not using
|
| 333 |
+
HTTPS.
|
| 334 |
+
"""
|
| 335 |
+
super().__init__(*args, **kwargs)
|
| 336 |
+
|
| 337 |
+
# Namespace the attribute with "pip_" just in case to prevent
|
| 338 |
+
# possible conflicts with the base class.
|
| 339 |
+
self.pip_trusted_origins: List[Tuple[str, Optional[int]]] = []
|
| 340 |
+
|
| 341 |
+
# Attach our User Agent to the request
|
| 342 |
+
self.headers["User-Agent"] = user_agent()
|
| 343 |
+
|
| 344 |
+
# Attach our Authentication handler to the session
|
| 345 |
+
self.auth = MultiDomainBasicAuth(index_urls=index_urls)
|
| 346 |
+
|
| 347 |
+
# Create our urllib3.Retry instance which will allow us to customize
|
| 348 |
+
# how we handle retries.
|
| 349 |
+
retries = urllib3.Retry(
|
| 350 |
+
# Set the total number of retries that a particular request can
|
| 351 |
+
# have.
|
| 352 |
+
total=retries,
|
| 353 |
+
# A 503 error from PyPI typically means that the Fastly -> Origin
|
| 354 |
+
# connection got interrupted in some way. A 503 error in general
|
| 355 |
+
# is typically considered a transient error so we'll go ahead and
|
| 356 |
+
# retry it.
|
| 357 |
+
# A 500 may indicate transient error in Amazon S3
|
| 358 |
+
# A 502 may be a transient error from a CDN like CloudFlare or CloudFront
|
| 359 |
+
# A 520 or 527 - may indicate transient error in CloudFlare
|
| 360 |
+
status_forcelist=[500, 502, 503, 520, 527],
|
| 361 |
+
# Add a small amount of back off between failed requests in
|
| 362 |
+
# order to prevent hammering the service.
|
| 363 |
+
backoff_factor=0.25,
|
| 364 |
+
) # type: ignore
|
| 365 |
+
|
| 366 |
+
# Our Insecure HTTPAdapter disables HTTPS validation. It does not
|
| 367 |
+
# support caching so we'll use it for all http:// URLs.
|
| 368 |
+
# If caching is disabled, we will also use it for
|
| 369 |
+
# https:// hosts that we've marked as ignoring
|
| 370 |
+
# TLS errors for (trusted-hosts).
|
| 371 |
+
insecure_adapter = InsecureHTTPAdapter(max_retries=retries)
|
| 372 |
+
|
| 373 |
+
# We want to _only_ cache responses on securely fetched origins or when
|
| 374 |
+
# the host is specified as trusted. We do this because
|
| 375 |
+
# we can't validate the response of an insecurely/untrusted fetched
|
| 376 |
+
# origin, and we don't want someone to be able to poison the cache and
|
| 377 |
+
# require manual eviction from the cache to fix it.
|
| 378 |
+
if cache:
|
| 379 |
+
secure_adapter = CacheControlAdapter(
|
| 380 |
+
cache=SafeFileCache(cache),
|
| 381 |
+
max_retries=retries,
|
| 382 |
+
ssl_context=ssl_context,
|
| 383 |
+
)
|
| 384 |
+
self._trusted_host_adapter = InsecureCacheControlAdapter(
|
| 385 |
+
cache=SafeFileCache(cache),
|
| 386 |
+
max_retries=retries,
|
| 387 |
+
)
|
| 388 |
+
else:
|
| 389 |
+
secure_adapter = HTTPAdapter(max_retries=retries, ssl_context=ssl_context)
|
| 390 |
+
self._trusted_host_adapter = insecure_adapter
|
| 391 |
+
|
| 392 |
+
self.mount("https://", secure_adapter)
|
| 393 |
+
self.mount("http://", insecure_adapter)
|
| 394 |
+
|
| 395 |
+
# Enable file:// urls
|
| 396 |
+
self.mount("file://", LocalFSAdapter())
|
| 397 |
+
|
| 398 |
+
for host in trusted_hosts:
|
| 399 |
+
self.add_trusted_host(host, suppress_logging=True)
|
| 400 |
+
|
| 401 |
+
def update_index_urls(self, new_index_urls: List[str]) -> None:
|
| 402 |
+
"""
|
| 403 |
+
:param new_index_urls: New index urls to update the authentication
|
| 404 |
+
handler with.
|
| 405 |
+
"""
|
| 406 |
+
self.auth.index_urls = new_index_urls
|
| 407 |
+
|
| 408 |
+
def add_trusted_host(
|
| 409 |
+
self, host: str, source: Optional[str] = None, suppress_logging: bool = False
|
| 410 |
+
) -> None:
|
| 411 |
+
"""
|
| 412 |
+
:param host: It is okay to provide a host that has previously been
|
| 413 |
+
added.
|
| 414 |
+
:param source: An optional source string, for logging where the host
|
| 415 |
+
string came from.
|
| 416 |
+
"""
|
| 417 |
+
if not suppress_logging:
|
| 418 |
+
msg = f"adding trusted host: {host!r}"
|
| 419 |
+
if source is not None:
|
| 420 |
+
msg += f" (from {source})"
|
| 421 |
+
logger.info(msg)
|
| 422 |
+
|
| 423 |
+
parsed_host, parsed_port = parse_netloc(host)
|
| 424 |
+
if parsed_host is None:
|
| 425 |
+
raise ValueError(f"Trusted host URL must include a host part: {host!r}")
|
| 426 |
+
if (parsed_host, parsed_port) not in self.pip_trusted_origins:
|
| 427 |
+
self.pip_trusted_origins.append((parsed_host, parsed_port))
|
| 428 |
+
|
| 429 |
+
self.mount(
|
| 430 |
+
build_url_from_netloc(host, scheme="http") + "/", self._trusted_host_adapter
|
| 431 |
+
)
|
| 432 |
+
self.mount(build_url_from_netloc(host) + "/", self._trusted_host_adapter)
|
| 433 |
+
if not parsed_port:
|
| 434 |
+
self.mount(
|
| 435 |
+
build_url_from_netloc(host, scheme="http") + ":",
|
| 436 |
+
self._trusted_host_adapter,
|
| 437 |
+
)
|
| 438 |
+
# Mount wildcard ports for the same host.
|
| 439 |
+
self.mount(build_url_from_netloc(host) + ":", self._trusted_host_adapter)
|
| 440 |
+
|
| 441 |
+
def iter_secure_origins(self) -> Generator[SecureOrigin, None, None]:
|
| 442 |
+
yield from SECURE_ORIGINS
|
| 443 |
+
for host, port in self.pip_trusted_origins:
|
| 444 |
+
yield ("*", host, "*" if port is None else port)
|
| 445 |
+
|
| 446 |
+
def is_secure_origin(self, location: Link) -> bool:
|
| 447 |
+
# Determine if this url used a secure transport mechanism
|
| 448 |
+
parsed = urllib.parse.urlparse(str(location))
|
| 449 |
+
origin_protocol, origin_host, origin_port = (
|
| 450 |
+
parsed.scheme,
|
| 451 |
+
parsed.hostname,
|
| 452 |
+
parsed.port,
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
# The protocol to use to see if the protocol matches.
|
| 456 |
+
# Don't count the repository type as part of the protocol: in
|
| 457 |
+
# cases such as "git+ssh", only use "ssh". (I.e., Only verify against
|
| 458 |
+
# the last scheme.)
|
| 459 |
+
origin_protocol = origin_protocol.rsplit("+", 1)[-1]
|
| 460 |
+
|
| 461 |
+
# Determine if our origin is a secure origin by looking through our
|
| 462 |
+
# hardcoded list of secure origins, as well as any additional ones
|
| 463 |
+
# configured on this PackageFinder instance.
|
| 464 |
+
for secure_origin in self.iter_secure_origins():
|
| 465 |
+
secure_protocol, secure_host, secure_port = secure_origin
|
| 466 |
+
if origin_protocol != secure_protocol and secure_protocol != "*":
|
| 467 |
+
continue
|
| 468 |
+
|
| 469 |
+
try:
|
| 470 |
+
addr = ipaddress.ip_address(origin_host or "")
|
| 471 |
+
network = ipaddress.ip_network(secure_host)
|
| 472 |
+
except ValueError:
|
| 473 |
+
# We don't have both a valid address or a valid network, so
|
| 474 |
+
# we'll check this origin against hostnames.
|
| 475 |
+
if (
|
| 476 |
+
origin_host
|
| 477 |
+
and origin_host.lower() != secure_host.lower()
|
| 478 |
+
and secure_host != "*"
|
| 479 |
+
):
|
| 480 |
+
continue
|
| 481 |
+
else:
|
| 482 |
+
# We have a valid address and network, so see if the address
|
| 483 |
+
# is contained within the network.
|
| 484 |
+
if addr not in network:
|
| 485 |
+
continue
|
| 486 |
+
|
| 487 |
+
# Check to see if the port matches.
|
| 488 |
+
if (
|
| 489 |
+
origin_port != secure_port
|
| 490 |
+
and secure_port != "*"
|
| 491 |
+
and secure_port is not None
|
| 492 |
+
):
|
| 493 |
+
continue
|
| 494 |
+
|
| 495 |
+
# If we've gotten here, then this origin matches the current
|
| 496 |
+
# secure origin and we should return True
|
| 497 |
+
return True
|
| 498 |
+
|
| 499 |
+
# If we've gotten to this point, then the origin isn't secure and we
|
| 500 |
+
# will not accept it as a valid location to search. We will however
|
| 501 |
+
# log a warning that we are ignoring it.
|
| 502 |
+
logger.warning(
|
| 503 |
+
"The repository located at %s is not a trusted or secure host and "
|
| 504 |
+
"is being ignored. If this repository is available via HTTPS we "
|
| 505 |
+
"recommend you use HTTPS instead, otherwise you may silence "
|
| 506 |
+
"this warning and allow it anyway with '--trusted-host %s'.",
|
| 507 |
+
origin_host,
|
| 508 |
+
origin_host,
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
return False
|
| 512 |
+
|
| 513 |
+
def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Response:
|
| 514 |
+
# Allow setting a default timeout on a session
|
| 515 |
+
kwargs.setdefault("timeout", self.timeout)
|
| 516 |
+
# Allow setting a default proxies on a session
|
| 517 |
+
kwargs.setdefault("proxies", self.proxies)
|
| 518 |
+
|
| 519 |
+
# Dispatch the actual request
|
| 520 |
+
return super().request(method, url, *args, **kwargs)
|
.venv/lib/python3.11/site-packages/pip/_internal/network/utils.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Generator
|
| 2 |
+
|
| 3 |
+
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
|
| 4 |
+
|
| 5 |
+
from pip._internal.exceptions import NetworkConnectionError
|
| 6 |
+
|
| 7 |
+
# The following comments and HTTP headers were originally added by
|
| 8 |
+
# Donald Stufft in git commit 22c562429a61bb77172039e480873fb239dd8c03.
|
| 9 |
+
#
|
| 10 |
+
# We use Accept-Encoding: identity here because requests defaults to
|
| 11 |
+
# accepting compressed responses. This breaks in a variety of ways
|
| 12 |
+
# depending on how the server is configured.
|
| 13 |
+
# - Some servers will notice that the file isn't a compressible file
|
| 14 |
+
# and will leave the file alone and with an empty Content-Encoding
|
| 15 |
+
# - Some servers will notice that the file is already compressed and
|
| 16 |
+
# will leave the file alone, adding a Content-Encoding: gzip header
|
| 17 |
+
# - Some servers won't notice anything at all and will take a file
|
| 18 |
+
# that's already been compressed and compress it again, and set
|
| 19 |
+
# the Content-Encoding: gzip header
|
| 20 |
+
# By setting this to request only the identity encoding we're hoping
|
| 21 |
+
# to eliminate the third case. Hopefully there does not exist a server
|
| 22 |
+
# which when given a file will notice it is already compressed and that
|
| 23 |
+
# you're not asking for a compressed file and will then decompress it
|
| 24 |
+
# before sending because if that's the case I don't think it'll ever be
|
| 25 |
+
# possible to make this work.
|
| 26 |
+
HEADERS: Dict[str, str] = {"Accept-Encoding": "identity"}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def raise_for_status(resp: Response) -> None:
|
| 30 |
+
http_error_msg = ""
|
| 31 |
+
if isinstance(resp.reason, bytes):
|
| 32 |
+
# We attempt to decode utf-8 first because some servers
|
| 33 |
+
# choose to localize their reason strings. If the string
|
| 34 |
+
# isn't utf-8, we fall back to iso-8859-1 for all other
|
| 35 |
+
# encodings.
|
| 36 |
+
try:
|
| 37 |
+
reason = resp.reason.decode("utf-8")
|
| 38 |
+
except UnicodeDecodeError:
|
| 39 |
+
reason = resp.reason.decode("iso-8859-1")
|
| 40 |
+
else:
|
| 41 |
+
reason = resp.reason
|
| 42 |
+
|
| 43 |
+
if 400 <= resp.status_code < 500:
|
| 44 |
+
http_error_msg = (
|
| 45 |
+
f"{resp.status_code} Client Error: {reason} for url: {resp.url}"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
elif 500 <= resp.status_code < 600:
|
| 49 |
+
http_error_msg = (
|
| 50 |
+
f"{resp.status_code} Server Error: {reason} for url: {resp.url}"
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
if http_error_msg:
|
| 54 |
+
raise NetworkConnectionError(http_error_msg, response=resp)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def response_chunks(
|
| 58 |
+
response: Response, chunk_size: int = CONTENT_CHUNK_SIZE
|
| 59 |
+
) -> Generator[bytes, None, None]:
|
| 60 |
+
"""Given a requests Response, provide the data chunks."""
|
| 61 |
+
try:
|
| 62 |
+
# Special case for urllib3.
|
| 63 |
+
for chunk in response.raw.stream(
|
| 64 |
+
chunk_size,
|
| 65 |
+
# We use decode_content=False here because we don't
|
| 66 |
+
# want urllib3 to mess with the raw bytes we get
|
| 67 |
+
# from the server. If we decompress inside of
|
| 68 |
+
# urllib3 then we cannot verify the checksum
|
| 69 |
+
# because the checksum will be of the compressed
|
| 70 |
+
# file. This breakage will only occur if the
|
| 71 |
+
# server adds a Content-Encoding header, which
|
| 72 |
+
# depends on how the server was configured:
|
| 73 |
+
# - Some servers will notice that the file isn't a
|
| 74 |
+
# compressible file and will leave the file alone
|
| 75 |
+
# and with an empty Content-Encoding
|
| 76 |
+
# - Some servers will notice that the file is
|
| 77 |
+
# already compressed and will leave the file
|
| 78 |
+
# alone and will add a Content-Encoding: gzip
|
| 79 |
+
# header
|
| 80 |
+
# - Some servers won't notice anything at all and
|
| 81 |
+
# will take a file that's already been compressed
|
| 82 |
+
# and compress it again and set the
|
| 83 |
+
# Content-Encoding: gzip header
|
| 84 |
+
#
|
| 85 |
+
# By setting this not to decode automatically we
|
| 86 |
+
# hope to eliminate problems with the second case.
|
| 87 |
+
decode_content=False,
|
| 88 |
+
):
|
| 89 |
+
yield chunk
|
| 90 |
+
except AttributeError:
|
| 91 |
+
# Standard file-like object.
|
| 92 |
+
while True:
|
| 93 |
+
chunk = response.raw.read(chunk_size)
|
| 94 |
+
if not chunk:
|
| 95 |
+
break
|
| 96 |
+
yield chunk
|
.venv/lib/python3.11/site-packages/pip/_internal/network/xmlrpc.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""xmlrpclib.Transport implementation
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
import urllib.parse
|
| 6 |
+
import xmlrpc.client
|
| 7 |
+
from typing import TYPE_CHECKING, Tuple
|
| 8 |
+
|
| 9 |
+
from pip._internal.exceptions import NetworkConnectionError
|
| 10 |
+
from pip._internal.network.session import PipSession
|
| 11 |
+
from pip._internal.network.utils import raise_for_status
|
| 12 |
+
|
| 13 |
+
if TYPE_CHECKING:
|
| 14 |
+
from xmlrpc.client import _HostType, _Marshallable
|
| 15 |
+
|
| 16 |
+
from _typeshed import SizedBuffer
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class PipXmlrpcTransport(xmlrpc.client.Transport):
|
| 22 |
+
"""Provide a `xmlrpclib.Transport` implementation via a `PipSession`
|
| 23 |
+
object.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(
|
| 27 |
+
self, index_url: str, session: PipSession, use_datetime: bool = False
|
| 28 |
+
) -> None:
|
| 29 |
+
super().__init__(use_datetime)
|
| 30 |
+
index_parts = urllib.parse.urlparse(index_url)
|
| 31 |
+
self._scheme = index_parts.scheme
|
| 32 |
+
self._session = session
|
| 33 |
+
|
| 34 |
+
def request(
|
| 35 |
+
self,
|
| 36 |
+
host: "_HostType",
|
| 37 |
+
handler: str,
|
| 38 |
+
request_body: "SizedBuffer",
|
| 39 |
+
verbose: bool = False,
|
| 40 |
+
) -> Tuple["_Marshallable", ...]:
|
| 41 |
+
assert isinstance(host, str)
|
| 42 |
+
parts = (self._scheme, host, handler, None, None, None)
|
| 43 |
+
url = urllib.parse.urlunparse(parts)
|
| 44 |
+
try:
|
| 45 |
+
headers = {"Content-Type": "text/xml"}
|
| 46 |
+
response = self._session.post(
|
| 47 |
+
url,
|
| 48 |
+
data=request_body,
|
| 49 |
+
headers=headers,
|
| 50 |
+
stream=True,
|
| 51 |
+
)
|
| 52 |
+
raise_for_status(response)
|
| 53 |
+
self.verbose = verbose
|
| 54 |
+
return self.parse_response(response.raw)
|
| 55 |
+
except NetworkConnectionError as exc:
|
| 56 |
+
assert exc.response
|
| 57 |
+
logger.critical(
|
| 58 |
+
"HTTP error %s while getting %s",
|
| 59 |
+
exc.response.status_code,
|
| 60 |
+
url,
|
| 61 |
+
)
|
| 62 |
+
raise
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (192 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/_jaraco_text.cpython-311.pyc
ADDED
|
Binary file (4.76 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/_log.cpython-311.pyc
ADDED
|
Binary file (2.01 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-311.pyc
ADDED
|
Binary file (2.55 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/compat.cpython-311.pyc
ADDED
|
Binary file (2.26 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/compatibility_tags.cpython-311.pyc
ADDED
|
Binary file (6.75 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/datetime.cpython-311.pyc
ADDED
|
Binary file (709 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/deprecation.cpython-311.pyc
ADDED
|
Binary file (4.68 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/direct_url_helpers.cpython-311.pyc
ADDED
|
Binary file (3.71 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/egg_link.cpython-311.pyc
ADDED
|
Binary file (3.55 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/encoding.cpython-311.pyc
ADDED
|
Binary file (2.32 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/entrypoints.cpython-311.pyc
ADDED
|
Binary file (4.24 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/filesystem.cpython-311.pyc
ADDED
|
Binary file (8.22 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/filetypes.cpython-311.pyc
ADDED
|
Binary file (1.31 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/glibc.cpython-311.pyc
ADDED
|
Binary file (2.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/hashes.cpython-311.pyc
ADDED
|
Binary file (8.76 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/logging.cpython-311.pyc
ADDED
|
Binary file (15.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/misc.cpython-311.pyc
ADDED
|
Binary file (38.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (2.93 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/packaging.cpython-311.pyc
ADDED
|
Binary file (2.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/pip/_internal/utils/__pycache__/setuptools_build.cpython-311.pyc
ADDED
|
Binary file (4.86 kB). View file
|
|
|