Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/pip/__init__.py +13 -0
- lib/python3.12/site-packages/pip/__main__.py +24 -0
- lib/python3.12/site-packages/pip/__pip-runner__.py +50 -0
- lib/python3.12/site-packages/pip/_internal/__init__.py +18 -0
- lib/python3.12/site-packages/pip/_internal/build_env.py +322 -0
- lib/python3.12/site-packages/pip/_internal/cache.py +290 -0
- lib/python3.12/site-packages/pip/_internal/configuration.py +383 -0
- lib/python3.12/site-packages/pip/_internal/exceptions.py +809 -0
- lib/python3.12/site-packages/pip/_internal/main.py +12 -0
- lib/python3.12/site-packages/pip/_internal/metadata/__init__.py +128 -0
- lib/python3.12/site-packages/pip/_internal/metadata/_json.py +86 -0
- lib/python3.12/site-packages/pip/_internal/metadata/base.py +688 -0
- lib/python3.12/site-packages/pip/_internal/metadata/importlib/__init__.py +6 -0
- lib/python3.12/site-packages/pip/_internal/metadata/importlib/__pycache__/_compat.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/metadata/importlib/__pycache__/_dists.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/metadata/importlib/__pycache__/_envs.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/metadata/importlib/_compat.py +85 -0
- lib/python3.12/site-packages/pip/_internal/metadata/importlib/_dists.py +228 -0
- lib/python3.12/site-packages/pip/_internal/metadata/importlib/_envs.py +189 -0
- lib/python3.12/site-packages/pip/_internal/metadata/pkg_resources.py +301 -0
- lib/python3.12/site-packages/pip/_internal/pyproject.py +185 -0
- lib/python3.12/site-packages/pip/_internal/self_outdated_check.py +252 -0
- lib/python3.12/site-packages/pip/_internal/wheel_builder.py +354 -0
- lib/python3.12/site-packages/pip/py.typed +4 -0
- lib/python3.12/site-packages/pygments/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/__main__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/cmdline.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/console.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/filter.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/formatter.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/lexer.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/modeline.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/plugin.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/regexopt.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/scanner.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/sphinxext.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/style.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/token.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/unistring.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/__pycache__/util.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/formatters/__init__.py +157 -0
- lib/python3.12/site-packages/pygments/formatters/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/formatters/__pycache__/_mapping.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/formatters/__pycache__/bbcode.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/formatters/__pycache__/groff.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/formatters/__pycache__/html.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/formatters/__pycache__/img.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/formatters/__pycache__/irc.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/formatters/__pycache__/latex.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pygments/formatters/__pycache__/other.cpython-312.pyc +0 -0
lib/python3.12/site-packages/pip/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
|
| 3 |
+
__version__ = "25.0.1"
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def main(args: Optional[List[str]] = None) -> int:
|
| 7 |
+
"""This is an internal API only meant for use by pip's own console scripts.
|
| 8 |
+
|
| 9 |
+
For additional details, see https://github.com/pypa/pip/issues/7498.
|
| 10 |
+
"""
|
| 11 |
+
from pip._internal.utils.entrypoints import _wrapper
|
| 12 |
+
|
| 13 |
+
return _wrapper(args)
|
lib/python3.12/site-packages/pip/__main__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Remove '' and current working directory from the first entry
|
| 5 |
+
# of sys.path, if present to avoid using current directory
|
| 6 |
+
# in pip commands check, freeze, install, list and show,
|
| 7 |
+
# when invoked as python -m pip <command>
|
| 8 |
+
if sys.path[0] in ("", os.getcwd()):
|
| 9 |
+
sys.path.pop(0)
|
| 10 |
+
|
| 11 |
+
# If we are running from a wheel, add the wheel to sys.path
|
| 12 |
+
# This allows the usage python pip-*.whl/pip install pip-*.whl
|
| 13 |
+
if __package__ == "":
|
| 14 |
+
# __file__ is pip-*.whl/pip/__main__.py
|
| 15 |
+
# first dirname call strips of '/__main__.py', second strips off '/pip'
|
| 16 |
+
# Resulting path is the name of the wheel itself
|
| 17 |
+
# Add that to sys.path so we can import pip
|
| 18 |
+
path = os.path.dirname(os.path.dirname(__file__))
|
| 19 |
+
sys.path.insert(0, path)
|
| 20 |
+
|
| 21 |
+
if __name__ == "__main__":
|
| 22 |
+
from pip._internal.cli.main import main as _main
|
| 23 |
+
|
| 24 |
+
sys.exit(_main())
|
lib/python3.12/site-packages/pip/__pip-runner__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Execute exactly this copy of pip, within a different environment.
|
| 2 |
+
|
| 3 |
+
This file is named as it is, to ensure that this module can't be imported via
|
| 4 |
+
an import statement.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# /!\ This version compatibility check section must be Python 2 compatible. /!\
|
| 8 |
+
|
| 9 |
+
import sys
|
| 10 |
+
|
| 11 |
+
# Copied from pyproject.toml
|
| 12 |
+
PYTHON_REQUIRES = (3, 8)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def version_str(version): # type: ignore
|
| 16 |
+
return ".".join(str(v) for v in version)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
if sys.version_info[:2] < PYTHON_REQUIRES:
|
| 20 |
+
raise SystemExit(
|
| 21 |
+
"This version of pip does not support python {} (requires >={}).".format(
|
| 22 |
+
version_str(sys.version_info[:2]), version_str(PYTHON_REQUIRES)
|
| 23 |
+
)
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# From here on, we can use Python 3 features, but the syntax must remain
|
| 27 |
+
# Python 2 compatible.
|
| 28 |
+
|
| 29 |
+
import runpy # noqa: E402
|
| 30 |
+
from importlib.machinery import PathFinder # noqa: E402
|
| 31 |
+
from os.path import dirname # noqa: E402
|
| 32 |
+
|
| 33 |
+
PIP_SOURCES_ROOT = dirname(dirname(__file__))
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class PipImportRedirectingFinder:
|
| 37 |
+
@classmethod
|
| 38 |
+
def find_spec(self, fullname, path=None, target=None): # type: ignore
|
| 39 |
+
if fullname != "pip":
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
spec = PathFinder.find_spec(fullname, [PIP_SOURCES_ROOT], target)
|
| 43 |
+
assert spec, (PIP_SOURCES_ROOT, fullname)
|
| 44 |
+
return spec
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
sys.meta_path.insert(0, PipImportRedirectingFinder())
|
| 48 |
+
|
| 49 |
+
assert __name__ == "__main__", "Cannot run __pip-runner__.py as a non-main module"
|
| 50 |
+
runpy.run_module("pip", run_name="__main__", alter_sys=True)
|
lib/python3.12/site-packages/pip/_internal/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
|
| 3 |
+
from pip._internal.utils import _log
|
| 4 |
+
|
| 5 |
+
# init_logging() must be called before any call to logging.getLogger()
|
| 6 |
+
# which happens at import of most modules.
|
| 7 |
+
_log.init_logging()
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main(args: Optional[List[str]] = None) -> int:
|
| 11 |
+
"""This is preserved for old console scripts that may still be referencing
|
| 12 |
+
it.
|
| 13 |
+
|
| 14 |
+
For additional details, see https://github.com/pypa/pip/issues/7498.
|
| 15 |
+
"""
|
| 16 |
+
from pip._internal.utils.entrypoints import _wrapper
|
| 17 |
+
|
| 18 |
+
return _wrapper(args)
|
lib/python3.12/site-packages/pip/_internal/build_env.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build Environment used for isolation during sdist building
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
import os
|
| 6 |
+
import pathlib
|
| 7 |
+
import site
|
| 8 |
+
import sys
|
| 9 |
+
import textwrap
|
| 10 |
+
from collections import OrderedDict
|
| 11 |
+
from types import TracebackType
|
| 12 |
+
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
|
| 13 |
+
|
| 14 |
+
from pip._vendor.packaging.version import Version
|
| 15 |
+
|
| 16 |
+
from pip import __file__ as pip_location
|
| 17 |
+
from pip._internal.cli.spinners import open_spinner
|
| 18 |
+
from pip._internal.locations import get_platlib, get_purelib, get_scheme
|
| 19 |
+
from pip._internal.metadata import get_default_environment, get_environment
|
| 20 |
+
from pip._internal.utils.logging import VERBOSE
|
| 21 |
+
from pip._internal.utils.packaging import get_requirement
|
| 22 |
+
from pip._internal.utils.subprocess import call_subprocess
|
| 23 |
+
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
|
| 24 |
+
|
| 25 |
+
if TYPE_CHECKING:
|
| 26 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 27 |
+
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _dedup(a: str, b: str) -> Union[Tuple[str], Tuple[str, str]]:
|
| 32 |
+
return (a, b) if a != b else (a,)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class _Prefix:
|
| 36 |
+
def __init__(self, path: str) -> None:
|
| 37 |
+
self.path = path
|
| 38 |
+
self.setup = False
|
| 39 |
+
scheme = get_scheme("", prefix=path)
|
| 40 |
+
self.bin_dir = scheme.scripts
|
| 41 |
+
self.lib_dirs = _dedup(scheme.purelib, scheme.platlib)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def get_runnable_pip() -> str:
|
| 45 |
+
"""Get a file to pass to a Python executable, to run the currently-running pip.
|
| 46 |
+
|
| 47 |
+
This is used to run a pip subprocess, for installing requirements into the build
|
| 48 |
+
environment.
|
| 49 |
+
"""
|
| 50 |
+
source = pathlib.Path(pip_location).resolve().parent
|
| 51 |
+
|
| 52 |
+
if not source.is_dir():
|
| 53 |
+
# This would happen if someone is using pip from inside a zip file. In that
|
| 54 |
+
# case, we can use that directly.
|
| 55 |
+
return str(source)
|
| 56 |
+
|
| 57 |
+
return os.fsdecode(source / "__pip-runner__.py")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _get_system_sitepackages() -> Set[str]:
|
| 61 |
+
"""Get system site packages
|
| 62 |
+
|
| 63 |
+
Usually from site.getsitepackages,
|
| 64 |
+
but fallback on `get_purelib()/get_platlib()` if unavailable
|
| 65 |
+
(e.g. in a virtualenv created by virtualenv<20)
|
| 66 |
+
|
| 67 |
+
Returns normalized set of strings.
|
| 68 |
+
"""
|
| 69 |
+
if hasattr(site, "getsitepackages"):
|
| 70 |
+
system_sites = site.getsitepackages()
|
| 71 |
+
else:
|
| 72 |
+
# virtualenv < 20 overwrites site.py without getsitepackages
|
| 73 |
+
# fallback on get_purelib/get_platlib.
|
| 74 |
+
# this is known to miss things, but shouldn't in the cases
|
| 75 |
+
# where getsitepackages() has been removed (inside a virtualenv)
|
| 76 |
+
system_sites = [get_purelib(), get_platlib()]
|
| 77 |
+
return {os.path.normcase(path) for path in system_sites}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class BuildEnvironment:
|
| 81 |
+
"""Creates and manages an isolated environment to install build deps"""
|
| 82 |
+
|
| 83 |
+
def __init__(self) -> None:
|
| 84 |
+
temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)
|
| 85 |
+
|
| 86 |
+
self._prefixes = OrderedDict(
|
| 87 |
+
(name, _Prefix(os.path.join(temp_dir.path, name)))
|
| 88 |
+
for name in ("normal", "overlay")
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
self._bin_dirs: List[str] = []
|
| 92 |
+
self._lib_dirs: List[str] = []
|
| 93 |
+
for prefix in reversed(list(self._prefixes.values())):
|
| 94 |
+
self._bin_dirs.append(prefix.bin_dir)
|
| 95 |
+
self._lib_dirs.extend(prefix.lib_dirs)
|
| 96 |
+
|
| 97 |
+
# Customize site to:
|
| 98 |
+
# - ensure .pth files are honored
|
| 99 |
+
# - prevent access to system site packages
|
| 100 |
+
system_sites = _get_system_sitepackages()
|
| 101 |
+
|
| 102 |
+
self._site_dir = os.path.join(temp_dir.path, "site")
|
| 103 |
+
if not os.path.exists(self._site_dir):
|
| 104 |
+
os.mkdir(self._site_dir)
|
| 105 |
+
with open(
|
| 106 |
+
os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8"
|
| 107 |
+
) as fp:
|
| 108 |
+
fp.write(
|
| 109 |
+
textwrap.dedent(
|
| 110 |
+
"""
|
| 111 |
+
import os, site, sys
|
| 112 |
+
|
| 113 |
+
# First, drop system-sites related paths.
|
| 114 |
+
original_sys_path = sys.path[:]
|
| 115 |
+
known_paths = set()
|
| 116 |
+
for path in {system_sites!r}:
|
| 117 |
+
site.addsitedir(path, known_paths=known_paths)
|
| 118 |
+
system_paths = set(
|
| 119 |
+
os.path.normcase(path)
|
| 120 |
+
for path in sys.path[len(original_sys_path):]
|
| 121 |
+
)
|
| 122 |
+
original_sys_path = [
|
| 123 |
+
path for path in original_sys_path
|
| 124 |
+
if os.path.normcase(path) not in system_paths
|
| 125 |
+
]
|
| 126 |
+
sys.path = original_sys_path
|
| 127 |
+
|
| 128 |
+
# Second, add lib directories.
|
| 129 |
+
# ensuring .pth file are processed.
|
| 130 |
+
for path in {lib_dirs!r}:
|
| 131 |
+
assert not path in sys.path
|
| 132 |
+
site.addsitedir(path)
|
| 133 |
+
"""
|
| 134 |
+
).format(system_sites=system_sites, lib_dirs=self._lib_dirs)
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
def __enter__(self) -> None:
|
| 138 |
+
self._save_env = {
|
| 139 |
+
name: os.environ.get(name, None)
|
| 140 |
+
for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
path = self._bin_dirs[:]
|
| 144 |
+
old_path = self._save_env["PATH"]
|
| 145 |
+
if old_path:
|
| 146 |
+
path.extend(old_path.split(os.pathsep))
|
| 147 |
+
|
| 148 |
+
pythonpath = [self._site_dir]
|
| 149 |
+
|
| 150 |
+
os.environ.update(
|
| 151 |
+
{
|
| 152 |
+
"PATH": os.pathsep.join(path),
|
| 153 |
+
"PYTHONNOUSERSITE": "1",
|
| 154 |
+
"PYTHONPATH": os.pathsep.join(pythonpath),
|
| 155 |
+
}
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
def __exit__(
|
| 159 |
+
self,
|
| 160 |
+
exc_type: Optional[Type[BaseException]],
|
| 161 |
+
exc_val: Optional[BaseException],
|
| 162 |
+
exc_tb: Optional[TracebackType],
|
| 163 |
+
) -> None:
|
| 164 |
+
for varname, old_value in self._save_env.items():
|
| 165 |
+
if old_value is None:
|
| 166 |
+
os.environ.pop(varname, None)
|
| 167 |
+
else:
|
| 168 |
+
os.environ[varname] = old_value
|
| 169 |
+
|
| 170 |
+
def check_requirements(
|
| 171 |
+
self, reqs: Iterable[str]
|
| 172 |
+
) -> Tuple[Set[Tuple[str, str]], Set[str]]:
|
| 173 |
+
"""Return 2 sets:
|
| 174 |
+
- conflicting requirements: set of (installed, wanted) reqs tuples
|
| 175 |
+
- missing requirements: set of reqs
|
| 176 |
+
"""
|
| 177 |
+
missing = set()
|
| 178 |
+
conflicting = set()
|
| 179 |
+
if reqs:
|
| 180 |
+
env = (
|
| 181 |
+
get_environment(self._lib_dirs)
|
| 182 |
+
if hasattr(self, "_lib_dirs")
|
| 183 |
+
else get_default_environment()
|
| 184 |
+
)
|
| 185 |
+
for req_str in reqs:
|
| 186 |
+
req = get_requirement(req_str)
|
| 187 |
+
# We're explicitly evaluating with an empty extra value, since build
|
| 188 |
+
# environments are not provided any mechanism to select specific extras.
|
| 189 |
+
if req.marker is not None and not req.marker.evaluate({"extra": ""}):
|
| 190 |
+
continue
|
| 191 |
+
dist = env.get_distribution(req.name)
|
| 192 |
+
if not dist:
|
| 193 |
+
missing.add(req_str)
|
| 194 |
+
continue
|
| 195 |
+
if isinstance(dist.version, Version):
|
| 196 |
+
installed_req_str = f"{req.name}=={dist.version}"
|
| 197 |
+
else:
|
| 198 |
+
installed_req_str = f"{req.name}==={dist.version}"
|
| 199 |
+
if not req.specifier.contains(dist.version, prereleases=True):
|
| 200 |
+
conflicting.add((installed_req_str, req_str))
|
| 201 |
+
# FIXME: Consider direct URL?
|
| 202 |
+
return conflicting, missing
|
| 203 |
+
|
| 204 |
+
def install_requirements(
|
| 205 |
+
self,
|
| 206 |
+
finder: "PackageFinder",
|
| 207 |
+
requirements: Iterable[str],
|
| 208 |
+
prefix_as_string: str,
|
| 209 |
+
*,
|
| 210 |
+
kind: str,
|
| 211 |
+
) -> None:
|
| 212 |
+
prefix = self._prefixes[prefix_as_string]
|
| 213 |
+
assert not prefix.setup
|
| 214 |
+
prefix.setup = True
|
| 215 |
+
if not requirements:
|
| 216 |
+
return
|
| 217 |
+
self._install_requirements(
|
| 218 |
+
get_runnable_pip(),
|
| 219 |
+
finder,
|
| 220 |
+
requirements,
|
| 221 |
+
prefix,
|
| 222 |
+
kind=kind,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
@staticmethod
|
| 226 |
+
def _install_requirements(
|
| 227 |
+
pip_runnable: str,
|
| 228 |
+
finder: "PackageFinder",
|
| 229 |
+
requirements: Iterable[str],
|
| 230 |
+
prefix: _Prefix,
|
| 231 |
+
*,
|
| 232 |
+
kind: str,
|
| 233 |
+
) -> None:
|
| 234 |
+
args: List[str] = [
|
| 235 |
+
sys.executable,
|
| 236 |
+
pip_runnable,
|
| 237 |
+
"install",
|
| 238 |
+
"--ignore-installed",
|
| 239 |
+
"--no-user",
|
| 240 |
+
"--prefix",
|
| 241 |
+
prefix.path,
|
| 242 |
+
"--no-warn-script-location",
|
| 243 |
+
"--disable-pip-version-check",
|
| 244 |
+
# The prefix specified two lines above, thus
|
| 245 |
+
# target from config file or env var should be ignored
|
| 246 |
+
"--target",
|
| 247 |
+
"",
|
| 248 |
+
]
|
| 249 |
+
if logger.getEffectiveLevel() <= logging.DEBUG:
|
| 250 |
+
args.append("-vv")
|
| 251 |
+
elif logger.getEffectiveLevel() <= VERBOSE:
|
| 252 |
+
args.append("-v")
|
| 253 |
+
for format_control in ("no_binary", "only_binary"):
|
| 254 |
+
formats = getattr(finder.format_control, format_control)
|
| 255 |
+
args.extend(
|
| 256 |
+
(
|
| 257 |
+
"--" + format_control.replace("_", "-"),
|
| 258 |
+
",".join(sorted(formats or {":none:"})),
|
| 259 |
+
)
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
index_urls = finder.index_urls
|
| 263 |
+
if index_urls:
|
| 264 |
+
args.extend(["-i", index_urls[0]])
|
| 265 |
+
for extra_index in index_urls[1:]:
|
| 266 |
+
args.extend(["--extra-index-url", extra_index])
|
| 267 |
+
else:
|
| 268 |
+
args.append("--no-index")
|
| 269 |
+
for link in finder.find_links:
|
| 270 |
+
args.extend(["--find-links", link])
|
| 271 |
+
|
| 272 |
+
if finder.proxy:
|
| 273 |
+
args.extend(["--proxy", finder.proxy])
|
| 274 |
+
for host in finder.trusted_hosts:
|
| 275 |
+
args.extend(["--trusted-host", host])
|
| 276 |
+
if finder.custom_cert:
|
| 277 |
+
args.extend(["--cert", finder.custom_cert])
|
| 278 |
+
if finder.client_cert:
|
| 279 |
+
args.extend(["--client-cert", finder.client_cert])
|
| 280 |
+
if finder.allow_all_prereleases:
|
| 281 |
+
args.append("--pre")
|
| 282 |
+
if finder.prefer_binary:
|
| 283 |
+
args.append("--prefer-binary")
|
| 284 |
+
args.append("--")
|
| 285 |
+
args.extend(requirements)
|
| 286 |
+
with open_spinner(f"Installing {kind}") as spinner:
|
| 287 |
+
call_subprocess(
|
| 288 |
+
args,
|
| 289 |
+
command_desc=f"pip subprocess to install {kind}",
|
| 290 |
+
spinner=spinner,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
class NoOpBuildEnvironment(BuildEnvironment):
|
| 295 |
+
"""A no-op drop-in replacement for BuildEnvironment"""
|
| 296 |
+
|
| 297 |
+
def __init__(self) -> None:
|
| 298 |
+
pass
|
| 299 |
+
|
| 300 |
+
def __enter__(self) -> None:
|
| 301 |
+
pass
|
| 302 |
+
|
| 303 |
+
def __exit__(
|
| 304 |
+
self,
|
| 305 |
+
exc_type: Optional[Type[BaseException]],
|
| 306 |
+
exc_val: Optional[BaseException],
|
| 307 |
+
exc_tb: Optional[TracebackType],
|
| 308 |
+
) -> None:
|
| 309 |
+
pass
|
| 310 |
+
|
| 311 |
+
def cleanup(self) -> None:
|
| 312 |
+
pass
|
| 313 |
+
|
| 314 |
+
def install_requirements(
|
| 315 |
+
self,
|
| 316 |
+
finder: "PackageFinder",
|
| 317 |
+
requirements: Iterable[str],
|
| 318 |
+
prefix_as_string: str,
|
| 319 |
+
*,
|
| 320 |
+
kind: str,
|
| 321 |
+
) -> None:
|
| 322 |
+
raise NotImplementedError()
|
lib/python3.12/site-packages/pip/_internal/cache.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cache Management
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any, Dict, List, Optional
|
| 10 |
+
|
| 11 |
+
from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version
|
| 12 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 13 |
+
|
| 14 |
+
from pip._internal.exceptions import InvalidWheelFilename
|
| 15 |
+
from pip._internal.models.direct_url import DirectUrl
|
| 16 |
+
from pip._internal.models.link import Link
|
| 17 |
+
from pip._internal.models.wheel import Wheel
|
| 18 |
+
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
|
| 19 |
+
from pip._internal.utils.urls import path_to_url
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
ORIGIN_JSON_NAME = "origin.json"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _hash_dict(d: Dict[str, str]) -> str:
|
| 27 |
+
"""Return a stable sha224 of a dictionary."""
|
| 28 |
+
s = json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
| 29 |
+
return hashlib.sha224(s.encode("ascii")).hexdigest()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class Cache:
|
| 33 |
+
"""An abstract class - provides cache directories for data from links
|
| 34 |
+
|
| 35 |
+
:param cache_dir: The root of the cache.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def __init__(self, cache_dir: str) -> None:
|
| 39 |
+
super().__init__()
|
| 40 |
+
assert not cache_dir or os.path.isabs(cache_dir)
|
| 41 |
+
self.cache_dir = cache_dir or None
|
| 42 |
+
|
| 43 |
+
def _get_cache_path_parts(self, link: Link) -> List[str]:
|
| 44 |
+
"""Get parts of part that must be os.path.joined with cache_dir"""
|
| 45 |
+
|
| 46 |
+
# We want to generate an url to use as our cache key, we don't want to
|
| 47 |
+
# just reuse the URL because it might have other items in the fragment
|
| 48 |
+
# and we don't care about those.
|
| 49 |
+
key_parts = {"url": link.url_without_fragment}
|
| 50 |
+
if link.hash_name is not None and link.hash is not None:
|
| 51 |
+
key_parts[link.hash_name] = link.hash
|
| 52 |
+
if link.subdirectory_fragment:
|
| 53 |
+
key_parts["subdirectory"] = link.subdirectory_fragment
|
| 54 |
+
|
| 55 |
+
# Include interpreter name, major and minor version in cache key
|
| 56 |
+
# to cope with ill-behaved sdists that build a different wheel
|
| 57 |
+
# depending on the python version their setup.py is being run on,
|
| 58 |
+
# and don't encode the difference in compatibility tags.
|
| 59 |
+
# https://github.com/pypa/pip/issues/7296
|
| 60 |
+
key_parts["interpreter_name"] = interpreter_name()
|
| 61 |
+
key_parts["interpreter_version"] = interpreter_version()
|
| 62 |
+
|
| 63 |
+
# Encode our key url with sha224, we'll use this because it has similar
|
| 64 |
+
# security properties to sha256, but with a shorter total output (and
|
| 65 |
+
# thus less secure). However the differences don't make a lot of
|
| 66 |
+
# difference for our use case here.
|
| 67 |
+
hashed = _hash_dict(key_parts)
|
| 68 |
+
|
| 69 |
+
# We want to nest the directories some to prevent having a ton of top
|
| 70 |
+
# level directories where we might run out of sub directories on some
|
| 71 |
+
# FS.
|
| 72 |
+
parts = [hashed[:2], hashed[2:4], hashed[4:6], hashed[6:]]
|
| 73 |
+
|
| 74 |
+
return parts
|
| 75 |
+
|
| 76 |
+
def _get_candidates(self, link: Link, canonical_package_name: str) -> List[Any]:
|
| 77 |
+
can_not_cache = not self.cache_dir or not canonical_package_name or not link
|
| 78 |
+
if can_not_cache:
|
| 79 |
+
return []
|
| 80 |
+
|
| 81 |
+
path = self.get_path_for_link(link)
|
| 82 |
+
if os.path.isdir(path):
|
| 83 |
+
return [(candidate, path) for candidate in os.listdir(path)]
|
| 84 |
+
return []
|
| 85 |
+
|
| 86 |
+
def get_path_for_link(self, link: Link) -> str:
|
| 87 |
+
"""Return a directory to store cached items in for link."""
|
| 88 |
+
raise NotImplementedError()
|
| 89 |
+
|
| 90 |
+
def get(
|
| 91 |
+
self,
|
| 92 |
+
link: Link,
|
| 93 |
+
package_name: Optional[str],
|
| 94 |
+
supported_tags: List[Tag],
|
| 95 |
+
) -> Link:
|
| 96 |
+
"""Returns a link to a cached item if it exists, otherwise returns the
|
| 97 |
+
passed link.
|
| 98 |
+
"""
|
| 99 |
+
raise NotImplementedError()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class SimpleWheelCache(Cache):
|
| 103 |
+
"""A cache of wheels for future installs."""
|
| 104 |
+
|
| 105 |
+
def __init__(self, cache_dir: str) -> None:
|
| 106 |
+
super().__init__(cache_dir)
|
| 107 |
+
|
| 108 |
+
def get_path_for_link(self, link: Link) -> str:
|
| 109 |
+
"""Return a directory to store cached wheels for link
|
| 110 |
+
|
| 111 |
+
Because there are M wheels for any one sdist, we provide a directory
|
| 112 |
+
to cache them in, and then consult that directory when looking up
|
| 113 |
+
cache hits.
|
| 114 |
+
|
| 115 |
+
We only insert things into the cache if they have plausible version
|
| 116 |
+
numbers, so that we don't contaminate the cache with things that were
|
| 117 |
+
not unique. E.g. ./package might have dozens of installs done for it
|
| 118 |
+
and build a version of 0.0...and if we built and cached a wheel, we'd
|
| 119 |
+
end up using the same wheel even if the source has been edited.
|
| 120 |
+
|
| 121 |
+
:param link: The link of the sdist for which this will cache wheels.
|
| 122 |
+
"""
|
| 123 |
+
parts = self._get_cache_path_parts(link)
|
| 124 |
+
assert self.cache_dir
|
| 125 |
+
# Store wheels within the root cache_dir
|
| 126 |
+
return os.path.join(self.cache_dir, "wheels", *parts)
|
| 127 |
+
|
| 128 |
+
def get(
|
| 129 |
+
self,
|
| 130 |
+
link: Link,
|
| 131 |
+
package_name: Optional[str],
|
| 132 |
+
supported_tags: List[Tag],
|
| 133 |
+
) -> Link:
|
| 134 |
+
candidates = []
|
| 135 |
+
|
| 136 |
+
if not package_name:
|
| 137 |
+
return link
|
| 138 |
+
|
| 139 |
+
canonical_package_name = canonicalize_name(package_name)
|
| 140 |
+
for wheel_name, wheel_dir in self._get_candidates(link, canonical_package_name):
|
| 141 |
+
try:
|
| 142 |
+
wheel = Wheel(wheel_name)
|
| 143 |
+
except InvalidWheelFilename:
|
| 144 |
+
continue
|
| 145 |
+
if canonicalize_name(wheel.name) != canonical_package_name:
|
| 146 |
+
logger.debug(
|
| 147 |
+
"Ignoring cached wheel %s for %s as it "
|
| 148 |
+
"does not match the expected distribution name %s.",
|
| 149 |
+
wheel_name,
|
| 150 |
+
link,
|
| 151 |
+
package_name,
|
| 152 |
+
)
|
| 153 |
+
continue
|
| 154 |
+
if not wheel.supported(supported_tags):
|
| 155 |
+
# Built for a different python/arch/etc
|
| 156 |
+
continue
|
| 157 |
+
candidates.append(
|
| 158 |
+
(
|
| 159 |
+
wheel.support_index_min(supported_tags),
|
| 160 |
+
wheel_name,
|
| 161 |
+
wheel_dir,
|
| 162 |
+
)
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
if not candidates:
|
| 166 |
+
return link
|
| 167 |
+
|
| 168 |
+
_, wheel_name, wheel_dir = min(candidates)
|
| 169 |
+
return Link(path_to_url(os.path.join(wheel_dir, wheel_name)))
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class EphemWheelCache(SimpleWheelCache):
|
| 173 |
+
"""A SimpleWheelCache that creates it's own temporary cache directory"""
|
| 174 |
+
|
| 175 |
+
def __init__(self) -> None:
|
| 176 |
+
self._temp_dir = TempDirectory(
|
| 177 |
+
kind=tempdir_kinds.EPHEM_WHEEL_CACHE,
|
| 178 |
+
globally_managed=True,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
super().__init__(self._temp_dir.path)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
class CacheEntry:
|
| 185 |
+
def __init__(
|
| 186 |
+
self,
|
| 187 |
+
link: Link,
|
| 188 |
+
persistent: bool,
|
| 189 |
+
):
|
| 190 |
+
self.link = link
|
| 191 |
+
self.persistent = persistent
|
| 192 |
+
self.origin: Optional[DirectUrl] = None
|
| 193 |
+
origin_direct_url_path = Path(self.link.file_path).parent / ORIGIN_JSON_NAME
|
| 194 |
+
if origin_direct_url_path.exists():
|
| 195 |
+
try:
|
| 196 |
+
self.origin = DirectUrl.from_json(
|
| 197 |
+
origin_direct_url_path.read_text(encoding="utf-8")
|
| 198 |
+
)
|
| 199 |
+
except Exception as e:
|
| 200 |
+
logger.warning(
|
| 201 |
+
"Ignoring invalid cache entry origin file %s for %s (%s)",
|
| 202 |
+
origin_direct_url_path,
|
| 203 |
+
link.filename,
|
| 204 |
+
e,
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
class WheelCache(Cache):
|
| 209 |
+
"""Wraps EphemWheelCache and SimpleWheelCache into a single Cache
|
| 210 |
+
|
| 211 |
+
This Cache allows for gracefully degradation, using the ephem wheel cache
|
| 212 |
+
when a certain link is not found in the simple wheel cache first.
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
def __init__(self, cache_dir: str) -> None:
|
| 216 |
+
super().__init__(cache_dir)
|
| 217 |
+
self._wheel_cache = SimpleWheelCache(cache_dir)
|
| 218 |
+
self._ephem_cache = EphemWheelCache()
|
| 219 |
+
|
| 220 |
+
def get_path_for_link(self, link: Link) -> str:
|
| 221 |
+
return self._wheel_cache.get_path_for_link(link)
|
| 222 |
+
|
| 223 |
+
def get_ephem_path_for_link(self, link: Link) -> str:
|
| 224 |
+
return self._ephem_cache.get_path_for_link(link)
|
| 225 |
+
|
| 226 |
+
def get(
|
| 227 |
+
self,
|
| 228 |
+
link: Link,
|
| 229 |
+
package_name: Optional[str],
|
| 230 |
+
supported_tags: List[Tag],
|
| 231 |
+
) -> Link:
|
| 232 |
+
cache_entry = self.get_cache_entry(link, package_name, supported_tags)
|
| 233 |
+
if cache_entry is None:
|
| 234 |
+
return link
|
| 235 |
+
return cache_entry.link
|
| 236 |
+
|
| 237 |
+
def get_cache_entry(
|
| 238 |
+
self,
|
| 239 |
+
link: Link,
|
| 240 |
+
package_name: Optional[str],
|
| 241 |
+
supported_tags: List[Tag],
|
| 242 |
+
) -> Optional[CacheEntry]:
|
| 243 |
+
"""Returns a CacheEntry with a link to a cached item if it exists or
|
| 244 |
+
None. The cache entry indicates if the item was found in the persistent
|
| 245 |
+
or ephemeral cache.
|
| 246 |
+
"""
|
| 247 |
+
retval = self._wheel_cache.get(
|
| 248 |
+
link=link,
|
| 249 |
+
package_name=package_name,
|
| 250 |
+
supported_tags=supported_tags,
|
| 251 |
+
)
|
| 252 |
+
if retval is not link:
|
| 253 |
+
return CacheEntry(retval, persistent=True)
|
| 254 |
+
|
| 255 |
+
retval = self._ephem_cache.get(
|
| 256 |
+
link=link,
|
| 257 |
+
package_name=package_name,
|
| 258 |
+
supported_tags=supported_tags,
|
| 259 |
+
)
|
| 260 |
+
if retval is not link:
|
| 261 |
+
return CacheEntry(retval, persistent=False)
|
| 262 |
+
|
| 263 |
+
return None
|
| 264 |
+
|
| 265 |
+
@staticmethod
|
| 266 |
+
def record_download_origin(cache_dir: str, download_info: DirectUrl) -> None:
|
| 267 |
+
origin_path = Path(cache_dir) / ORIGIN_JSON_NAME
|
| 268 |
+
if origin_path.exists():
|
| 269 |
+
try:
|
| 270 |
+
origin = DirectUrl.from_json(origin_path.read_text(encoding="utf-8"))
|
| 271 |
+
except Exception as e:
|
| 272 |
+
logger.warning(
|
| 273 |
+
"Could not read origin file %s in cache entry (%s). "
|
| 274 |
+
"Will attempt to overwrite it.",
|
| 275 |
+
origin_path,
|
| 276 |
+
e,
|
| 277 |
+
)
|
| 278 |
+
else:
|
| 279 |
+
# TODO: use DirectUrl.equivalent when
|
| 280 |
+
# https://github.com/pypa/pip/pull/10564 is merged.
|
| 281 |
+
if origin.url != download_info.url:
|
| 282 |
+
logger.warning(
|
| 283 |
+
"Origin URL %s in cache entry %s does not match download URL "
|
| 284 |
+
"%s. This is likely a pip bug or a cache corruption issue. "
|
| 285 |
+
"Will overwrite it with the new value.",
|
| 286 |
+
origin.url,
|
| 287 |
+
cache_dir,
|
| 288 |
+
download_info.url,
|
| 289 |
+
)
|
| 290 |
+
origin_path.write_text(download_info.to_json(), encoding="utf-8")
|
lib/python3.12/site-packages/pip/_internal/configuration.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration management setup
|
| 2 |
+
|
| 3 |
+
Some terminology:
|
| 4 |
+
- name
|
| 5 |
+
As written in config files.
|
| 6 |
+
- value
|
| 7 |
+
Value associated with a name
|
| 8 |
+
- key
|
| 9 |
+
Name combined with it's section (section.name)
|
| 10 |
+
- variant
|
| 11 |
+
A single word describing where the configuration key-value pair came from
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import configparser
|
| 15 |
+
import locale
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple
|
| 19 |
+
|
| 20 |
+
from pip._internal.exceptions import (
|
| 21 |
+
ConfigurationError,
|
| 22 |
+
ConfigurationFileCouldNotBeLoaded,
|
| 23 |
+
)
|
| 24 |
+
from pip._internal.utils import appdirs
|
| 25 |
+
from pip._internal.utils.compat import WINDOWS
|
| 26 |
+
from pip._internal.utils.logging import getLogger
|
| 27 |
+
from pip._internal.utils.misc import ensure_dir, enum
|
| 28 |
+
|
| 29 |
+
RawConfigParser = configparser.RawConfigParser # Shorthand
|
| 30 |
+
Kind = NewType("Kind", str)
|
| 31 |
+
|
| 32 |
+
CONFIG_BASENAME = "pip.ini" if WINDOWS else "pip.conf"
|
| 33 |
+
ENV_NAMES_IGNORED = "version", "help"
|
| 34 |
+
|
| 35 |
+
# The kinds of configurations there are.
|
| 36 |
+
kinds = enum(
|
| 37 |
+
USER="user", # User Specific
|
| 38 |
+
GLOBAL="global", # System Wide
|
| 39 |
+
SITE="site", # [Virtual] Environment Specific
|
| 40 |
+
ENV="env", # from PIP_CONFIG_FILE
|
| 41 |
+
ENV_VAR="env-var", # from Environment Variables
|
| 42 |
+
)
|
| 43 |
+
OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR
|
| 44 |
+
VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE
|
| 45 |
+
|
| 46 |
+
logger = getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# NOTE: Maybe use the optionx attribute to normalize keynames.
|
| 50 |
+
def _normalize_name(name: str) -> str:
|
| 51 |
+
"""Make a name consistent regardless of source (environment or file)"""
|
| 52 |
+
name = name.lower().replace("_", "-")
|
| 53 |
+
if name.startswith("--"):
|
| 54 |
+
name = name[2:] # only prefer long opts
|
| 55 |
+
return name
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _disassemble_key(name: str) -> List[str]:
|
| 59 |
+
if "." not in name:
|
| 60 |
+
error_message = (
|
| 61 |
+
"Key does not contain dot separated section and key. "
|
| 62 |
+
f"Perhaps you wanted to use 'global.{name}' instead?"
|
| 63 |
+
)
|
| 64 |
+
raise ConfigurationError(error_message)
|
| 65 |
+
return name.split(".", 1)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def get_configuration_files() -> Dict[Kind, List[str]]:
|
| 69 |
+
global_config_files = [
|
| 70 |
+
os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs("pip")
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
site_config_file = os.path.join(sys.prefix, CONFIG_BASENAME)
|
| 74 |
+
legacy_config_file = os.path.join(
|
| 75 |
+
os.path.expanduser("~"),
|
| 76 |
+
"pip" if WINDOWS else ".pip",
|
| 77 |
+
CONFIG_BASENAME,
|
| 78 |
+
)
|
| 79 |
+
new_config_file = os.path.join(appdirs.user_config_dir("pip"), CONFIG_BASENAME)
|
| 80 |
+
return {
|
| 81 |
+
kinds.GLOBAL: global_config_files,
|
| 82 |
+
kinds.SITE: [site_config_file],
|
| 83 |
+
kinds.USER: [legacy_config_file, new_config_file],
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class Configuration:
|
| 88 |
+
"""Handles management of configuration.
|
| 89 |
+
|
| 90 |
+
Provides an interface to accessing and managing configuration files.
|
| 91 |
+
|
| 92 |
+
This class converts provides an API that takes "section.key-name" style
|
| 93 |
+
keys and stores the value associated with it as "key-name" under the
|
| 94 |
+
section "section".
|
| 95 |
+
|
| 96 |
+
This allows for a clean interface wherein the both the section and the
|
| 97 |
+
key-name are preserved in an easy to manage form in the configuration files
|
| 98 |
+
and the data stored is also nice.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __init__(self, isolated: bool, load_only: Optional[Kind] = None) -> None:
|
| 102 |
+
super().__init__()
|
| 103 |
+
|
| 104 |
+
if load_only is not None and load_only not in VALID_LOAD_ONLY:
|
| 105 |
+
raise ConfigurationError(
|
| 106 |
+
"Got invalid value for load_only - should be one of {}".format(
|
| 107 |
+
", ".join(map(repr, VALID_LOAD_ONLY))
|
| 108 |
+
)
|
| 109 |
+
)
|
| 110 |
+
self.isolated = isolated
|
| 111 |
+
self.load_only = load_only
|
| 112 |
+
|
| 113 |
+
# Because we keep track of where we got the data from
|
| 114 |
+
self._parsers: Dict[Kind, List[Tuple[str, RawConfigParser]]] = {
|
| 115 |
+
variant: [] for variant in OVERRIDE_ORDER
|
| 116 |
+
}
|
| 117 |
+
self._config: Dict[Kind, Dict[str, Any]] = {
|
| 118 |
+
variant: {} for variant in OVERRIDE_ORDER
|
| 119 |
+
}
|
| 120 |
+
self._modified_parsers: List[Tuple[str, RawConfigParser]] = []
|
| 121 |
+
|
| 122 |
+
def load(self) -> None:
|
| 123 |
+
"""Loads configuration from configuration files and environment"""
|
| 124 |
+
self._load_config_files()
|
| 125 |
+
if not self.isolated:
|
| 126 |
+
self._load_environment_vars()
|
| 127 |
+
|
| 128 |
+
def get_file_to_edit(self) -> Optional[str]:
|
| 129 |
+
"""Returns the file with highest priority in configuration"""
|
| 130 |
+
assert self.load_only is not None, "Need to be specified a file to be editing"
|
| 131 |
+
|
| 132 |
+
try:
|
| 133 |
+
return self._get_parser_to_modify()[0]
|
| 134 |
+
except IndexError:
|
| 135 |
+
return None
|
| 136 |
+
|
| 137 |
+
def items(self) -> Iterable[Tuple[str, Any]]:
|
| 138 |
+
"""Returns key-value pairs like dict.items() representing the loaded
|
| 139 |
+
configuration
|
| 140 |
+
"""
|
| 141 |
+
return self._dictionary.items()
|
| 142 |
+
|
| 143 |
+
def get_value(self, key: str) -> Any:
|
| 144 |
+
"""Get a value from the configuration."""
|
| 145 |
+
orig_key = key
|
| 146 |
+
key = _normalize_name(key)
|
| 147 |
+
try:
|
| 148 |
+
return self._dictionary[key]
|
| 149 |
+
except KeyError:
|
| 150 |
+
# disassembling triggers a more useful error message than simply
|
| 151 |
+
# "No such key" in the case that the key isn't in the form command.option
|
| 152 |
+
_disassemble_key(key)
|
| 153 |
+
raise ConfigurationError(f"No such key - {orig_key}")
|
| 154 |
+
|
| 155 |
+
def set_value(self, key: str, value: Any) -> None:
|
| 156 |
+
"""Modify a value in the configuration."""
|
| 157 |
+
key = _normalize_name(key)
|
| 158 |
+
self._ensure_have_load_only()
|
| 159 |
+
|
| 160 |
+
assert self.load_only
|
| 161 |
+
fname, parser = self._get_parser_to_modify()
|
| 162 |
+
|
| 163 |
+
if parser is not None:
|
| 164 |
+
section, name = _disassemble_key(key)
|
| 165 |
+
|
| 166 |
+
# Modify the parser and the configuration
|
| 167 |
+
if not parser.has_section(section):
|
| 168 |
+
parser.add_section(section)
|
| 169 |
+
parser.set(section, name, value)
|
| 170 |
+
|
| 171 |
+
self._config[self.load_only][key] = value
|
| 172 |
+
self._mark_as_modified(fname, parser)
|
| 173 |
+
|
| 174 |
+
def unset_value(self, key: str) -> None:
|
| 175 |
+
"""Unset a value in the configuration."""
|
| 176 |
+
orig_key = key
|
| 177 |
+
key = _normalize_name(key)
|
| 178 |
+
self._ensure_have_load_only()
|
| 179 |
+
|
| 180 |
+
assert self.load_only
|
| 181 |
+
if key not in self._config[self.load_only]:
|
| 182 |
+
raise ConfigurationError(f"No such key - {orig_key}")
|
| 183 |
+
|
| 184 |
+
fname, parser = self._get_parser_to_modify()
|
| 185 |
+
|
| 186 |
+
if parser is not None:
|
| 187 |
+
section, name = _disassemble_key(key)
|
| 188 |
+
if not (
|
| 189 |
+
parser.has_section(section) and parser.remove_option(section, name)
|
| 190 |
+
):
|
| 191 |
+
# The option was not removed.
|
| 192 |
+
raise ConfigurationError(
|
| 193 |
+
"Fatal Internal error [id=1]. Please report as a bug."
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# The section may be empty after the option was removed.
|
| 197 |
+
if not parser.items(section):
|
| 198 |
+
parser.remove_section(section)
|
| 199 |
+
self._mark_as_modified(fname, parser)
|
| 200 |
+
|
| 201 |
+
del self._config[self.load_only][key]
|
| 202 |
+
|
| 203 |
+
def save(self) -> None:
|
| 204 |
+
"""Save the current in-memory state."""
|
| 205 |
+
self._ensure_have_load_only()
|
| 206 |
+
|
| 207 |
+
for fname, parser in self._modified_parsers:
|
| 208 |
+
logger.info("Writing to %s", fname)
|
| 209 |
+
|
| 210 |
+
# Ensure directory exists.
|
| 211 |
+
ensure_dir(os.path.dirname(fname))
|
| 212 |
+
|
| 213 |
+
# Ensure directory's permission(need to be writeable)
|
| 214 |
+
try:
|
| 215 |
+
with open(fname, "w") as f:
|
| 216 |
+
parser.write(f)
|
| 217 |
+
except OSError as error:
|
| 218 |
+
raise ConfigurationError(
|
| 219 |
+
f"An error occurred while writing to the configuration file "
|
| 220 |
+
f"{fname}: {error}"
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
#
|
| 224 |
+
# Private routines
|
| 225 |
+
#
|
| 226 |
+
|
| 227 |
+
def _ensure_have_load_only(self) -> None:
|
| 228 |
+
if self.load_only is None:
|
| 229 |
+
raise ConfigurationError("Needed a specific file to be modifying.")
|
| 230 |
+
logger.debug("Will be working with %s variant only", self.load_only)
|
| 231 |
+
|
| 232 |
+
@property
|
| 233 |
+
def _dictionary(self) -> Dict[str, Any]:
|
| 234 |
+
"""A dictionary representing the loaded configuration."""
|
| 235 |
+
# NOTE: Dictionaries are not populated if not loaded. So, conditionals
|
| 236 |
+
# are not needed here.
|
| 237 |
+
retval = {}
|
| 238 |
+
|
| 239 |
+
for variant in OVERRIDE_ORDER:
|
| 240 |
+
retval.update(self._config[variant])
|
| 241 |
+
|
| 242 |
+
return retval
|
| 243 |
+
|
| 244 |
+
def _load_config_files(self) -> None:
|
| 245 |
+
"""Loads configuration from configuration files"""
|
| 246 |
+
config_files = dict(self.iter_config_files())
|
| 247 |
+
if config_files[kinds.ENV][0:1] == [os.devnull]:
|
| 248 |
+
logger.debug(
|
| 249 |
+
"Skipping loading configuration files due to "
|
| 250 |
+
"environment's PIP_CONFIG_FILE being os.devnull"
|
| 251 |
+
)
|
| 252 |
+
return
|
| 253 |
+
|
| 254 |
+
for variant, files in config_files.items():
|
| 255 |
+
for fname in files:
|
| 256 |
+
# If there's specific variant set in `load_only`, load only
|
| 257 |
+
# that variant, not the others.
|
| 258 |
+
if self.load_only is not None and variant != self.load_only:
|
| 259 |
+
logger.debug("Skipping file '%s' (variant: %s)", fname, variant)
|
| 260 |
+
continue
|
| 261 |
+
|
| 262 |
+
parser = self._load_file(variant, fname)
|
| 263 |
+
|
| 264 |
+
# Keeping track of the parsers used
|
| 265 |
+
self._parsers[variant].append((fname, parser))
|
| 266 |
+
|
| 267 |
+
def _load_file(self, variant: Kind, fname: str) -> RawConfigParser:
|
| 268 |
+
logger.verbose("For variant '%s', will try loading '%s'", variant, fname)
|
| 269 |
+
parser = self._construct_parser(fname)
|
| 270 |
+
|
| 271 |
+
for section in parser.sections():
|
| 272 |
+
items = parser.items(section)
|
| 273 |
+
self._config[variant].update(self._normalized_keys(section, items))
|
| 274 |
+
|
| 275 |
+
return parser
|
| 276 |
+
|
| 277 |
+
def _construct_parser(self, fname: str) -> RawConfigParser:
|
| 278 |
+
parser = configparser.RawConfigParser()
|
| 279 |
+
# If there is no such file, don't bother reading it but create the
|
| 280 |
+
# parser anyway, to hold the data.
|
| 281 |
+
# Doing this is useful when modifying and saving files, where we don't
|
| 282 |
+
# need to construct a parser.
|
| 283 |
+
if os.path.exists(fname):
|
| 284 |
+
locale_encoding = locale.getpreferredencoding(False)
|
| 285 |
+
try:
|
| 286 |
+
parser.read(fname, encoding=locale_encoding)
|
| 287 |
+
except UnicodeDecodeError:
|
| 288 |
+
# See https://github.com/pypa/pip/issues/4963
|
| 289 |
+
raise ConfigurationFileCouldNotBeLoaded(
|
| 290 |
+
reason=f"contains invalid {locale_encoding} characters",
|
| 291 |
+
fname=fname,
|
| 292 |
+
)
|
| 293 |
+
except configparser.Error as error:
|
| 294 |
+
# See https://github.com/pypa/pip/issues/4893
|
| 295 |
+
raise ConfigurationFileCouldNotBeLoaded(error=error)
|
| 296 |
+
return parser
|
| 297 |
+
|
| 298 |
+
def _load_environment_vars(self) -> None:
|
| 299 |
+
"""Loads configuration from environment variables"""
|
| 300 |
+
self._config[kinds.ENV_VAR].update(
|
| 301 |
+
self._normalized_keys(":env:", self.get_environ_vars())
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
def _normalized_keys(
|
| 305 |
+
self, section: str, items: Iterable[Tuple[str, Any]]
|
| 306 |
+
) -> Dict[str, Any]:
|
| 307 |
+
"""Normalizes items to construct a dictionary with normalized keys.
|
| 308 |
+
|
| 309 |
+
This routine is where the names become keys and are made the same
|
| 310 |
+
regardless of source - configuration files or environment.
|
| 311 |
+
"""
|
| 312 |
+
normalized = {}
|
| 313 |
+
for name, val in items:
|
| 314 |
+
key = section + "." + _normalize_name(name)
|
| 315 |
+
normalized[key] = val
|
| 316 |
+
return normalized
|
| 317 |
+
|
| 318 |
+
def get_environ_vars(self) -> Iterable[Tuple[str, str]]:
|
| 319 |
+
"""Returns a generator with all environmental vars with prefix PIP_"""
|
| 320 |
+
for key, val in os.environ.items():
|
| 321 |
+
if key.startswith("PIP_"):
|
| 322 |
+
name = key[4:].lower()
|
| 323 |
+
if name not in ENV_NAMES_IGNORED:
|
| 324 |
+
yield name, val
|
| 325 |
+
|
| 326 |
+
# XXX: This is patched in the tests.
|
| 327 |
+
def iter_config_files(self) -> Iterable[Tuple[Kind, List[str]]]:
|
| 328 |
+
"""Yields variant and configuration files associated with it.
|
| 329 |
+
|
| 330 |
+
This should be treated like items of a dictionary. The order
|
| 331 |
+
here doesn't affect what gets overridden. That is controlled
|
| 332 |
+
by OVERRIDE_ORDER. However this does control the order they are
|
| 333 |
+
displayed to the user. It's probably most ergonomic to display
|
| 334 |
+
things in the same order as OVERRIDE_ORDER
|
| 335 |
+
"""
|
| 336 |
+
# SMELL: Move the conditions out of this function
|
| 337 |
+
|
| 338 |
+
env_config_file = os.environ.get("PIP_CONFIG_FILE", None)
|
| 339 |
+
config_files = get_configuration_files()
|
| 340 |
+
|
| 341 |
+
yield kinds.GLOBAL, config_files[kinds.GLOBAL]
|
| 342 |
+
|
| 343 |
+
# per-user config is not loaded when env_config_file exists
|
| 344 |
+
should_load_user_config = not self.isolated and not (
|
| 345 |
+
env_config_file and os.path.exists(env_config_file)
|
| 346 |
+
)
|
| 347 |
+
if should_load_user_config:
|
| 348 |
+
# The legacy config file is overridden by the new config file
|
| 349 |
+
yield kinds.USER, config_files[kinds.USER]
|
| 350 |
+
|
| 351 |
+
# virtualenv config
|
| 352 |
+
yield kinds.SITE, config_files[kinds.SITE]
|
| 353 |
+
|
| 354 |
+
if env_config_file is not None:
|
| 355 |
+
yield kinds.ENV, [env_config_file]
|
| 356 |
+
else:
|
| 357 |
+
yield kinds.ENV, []
|
| 358 |
+
|
| 359 |
+
def get_values_in_config(self, variant: Kind) -> Dict[str, Any]:
|
| 360 |
+
"""Get values present in a config file"""
|
| 361 |
+
return self._config[variant]
|
| 362 |
+
|
| 363 |
+
def _get_parser_to_modify(self) -> Tuple[str, RawConfigParser]:
|
| 364 |
+
# Determine which parser to modify
|
| 365 |
+
assert self.load_only
|
| 366 |
+
parsers = self._parsers[self.load_only]
|
| 367 |
+
if not parsers:
|
| 368 |
+
# This should not happen if everything works correctly.
|
| 369 |
+
raise ConfigurationError(
|
| 370 |
+
"Fatal Internal error [id=2]. Please report as a bug."
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
# Use the highest priority parser.
|
| 374 |
+
return parsers[-1]
|
| 375 |
+
|
| 376 |
+
# XXX: This is patched in the tests.
|
| 377 |
+
def _mark_as_modified(self, fname: str, parser: RawConfigParser) -> None:
|
| 378 |
+
file_parser_tuple = (fname, parser)
|
| 379 |
+
if file_parser_tuple not in self._modified_parsers:
|
| 380 |
+
self._modified_parsers.append(file_parser_tuple)
|
| 381 |
+
|
| 382 |
+
def __repr__(self) -> str:
|
| 383 |
+
return f"{self.__class__.__name__}({self._dictionary!r})"
|
lib/python3.12/site-packages/pip/_internal/exceptions.py
ADDED
|
@@ -0,0 +1,809 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Exceptions used throughout package.
|
| 2 |
+
|
| 3 |
+
This module MUST NOT try to import from anything within `pip._internal` to
|
| 4 |
+
operate. This is expected to be importable from any/all files within the
|
| 5 |
+
subpackage and, thus, should not depend on them.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import configparser
|
| 9 |
+
import contextlib
|
| 10 |
+
import locale
|
| 11 |
+
import logging
|
| 12 |
+
import pathlib
|
| 13 |
+
import re
|
| 14 |
+
import sys
|
| 15 |
+
from itertools import chain, groupby, repeat
|
| 16 |
+
from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union
|
| 17 |
+
|
| 18 |
+
from pip._vendor.packaging.requirements import InvalidRequirement
|
| 19 |
+
from pip._vendor.packaging.version import InvalidVersion
|
| 20 |
+
from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult
|
| 21 |
+
from pip._vendor.rich.markup import escape
|
| 22 |
+
from pip._vendor.rich.text import Text
|
| 23 |
+
|
| 24 |
+
if TYPE_CHECKING:
|
| 25 |
+
from hashlib import _Hash
|
| 26 |
+
|
| 27 |
+
from pip._vendor.requests.models import Request, Response
|
| 28 |
+
|
| 29 |
+
from pip._internal.metadata import BaseDistribution
|
| 30 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
#
|
| 36 |
+
# Scaffolding
|
| 37 |
+
#
|
| 38 |
+
def _is_kebab_case(s: str) -> bool:
|
| 39 |
+
return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _prefix_with_indent(
|
| 43 |
+
s: Union[Text, str],
|
| 44 |
+
console: Console,
|
| 45 |
+
*,
|
| 46 |
+
prefix: str,
|
| 47 |
+
indent: str,
|
| 48 |
+
) -> Text:
|
| 49 |
+
if isinstance(s, Text):
|
| 50 |
+
text = s
|
| 51 |
+
else:
|
| 52 |
+
text = console.render_str(s)
|
| 53 |
+
|
| 54 |
+
return console.render_str(prefix, overflow="ignore") + console.render_str(
|
| 55 |
+
f"\n{indent}", overflow="ignore"
|
| 56 |
+
).join(text.split(allow_blank=True))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class PipError(Exception):
|
| 60 |
+
"""The base pip error."""
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class DiagnosticPipError(PipError):
|
| 64 |
+
"""An error, that presents diagnostic information to the user.
|
| 65 |
+
|
| 66 |
+
This contains a bunch of logic, to enable pretty presentation of our error
|
| 67 |
+
messages. Each error gets a unique reference. Each error can also include
|
| 68 |
+
additional context, a hint and/or a note -- which are presented with the
|
| 69 |
+
main error message in a consistent style.
|
| 70 |
+
|
| 71 |
+
This is adapted from the error output styling in `sphinx-theme-builder`.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
reference: str
|
| 75 |
+
|
| 76 |
+
def __init__(
|
| 77 |
+
self,
|
| 78 |
+
*,
|
| 79 |
+
kind: 'Literal["error", "warning"]' = "error",
|
| 80 |
+
reference: Optional[str] = None,
|
| 81 |
+
message: Union[str, Text],
|
| 82 |
+
context: Optional[Union[str, Text]],
|
| 83 |
+
hint_stmt: Optional[Union[str, Text]],
|
| 84 |
+
note_stmt: Optional[Union[str, Text]] = None,
|
| 85 |
+
link: Optional[str] = None,
|
| 86 |
+
) -> None:
|
| 87 |
+
# Ensure a proper reference is provided.
|
| 88 |
+
if reference is None:
|
| 89 |
+
assert hasattr(self, "reference"), "error reference not provided!"
|
| 90 |
+
reference = self.reference
|
| 91 |
+
assert _is_kebab_case(reference), "error reference must be kebab-case!"
|
| 92 |
+
|
| 93 |
+
self.kind = kind
|
| 94 |
+
self.reference = reference
|
| 95 |
+
|
| 96 |
+
self.message = message
|
| 97 |
+
self.context = context
|
| 98 |
+
|
| 99 |
+
self.note_stmt = note_stmt
|
| 100 |
+
self.hint_stmt = hint_stmt
|
| 101 |
+
|
| 102 |
+
self.link = link
|
| 103 |
+
|
| 104 |
+
super().__init__(f"<{self.__class__.__name__}: {self.reference}>")
|
| 105 |
+
|
| 106 |
+
def __repr__(self) -> str:
|
| 107 |
+
return (
|
| 108 |
+
f"<{self.__class__.__name__}("
|
| 109 |
+
f"reference={self.reference!r}, "
|
| 110 |
+
f"message={self.message!r}, "
|
| 111 |
+
f"context={self.context!r}, "
|
| 112 |
+
f"note_stmt={self.note_stmt!r}, "
|
| 113 |
+
f"hint_stmt={self.hint_stmt!r}"
|
| 114 |
+
")>"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
def __rich_console__(
|
| 118 |
+
self,
|
| 119 |
+
console: Console,
|
| 120 |
+
options: ConsoleOptions,
|
| 121 |
+
) -> RenderResult:
|
| 122 |
+
colour = "red" if self.kind == "error" else "yellow"
|
| 123 |
+
|
| 124 |
+
yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]"
|
| 125 |
+
yield ""
|
| 126 |
+
|
| 127 |
+
if not options.ascii_only:
|
| 128 |
+
# Present the main message, with relevant context indented.
|
| 129 |
+
if self.context is not None:
|
| 130 |
+
yield _prefix_with_indent(
|
| 131 |
+
self.message,
|
| 132 |
+
console,
|
| 133 |
+
prefix=f"[{colour}]×[/] ",
|
| 134 |
+
indent=f"[{colour}]│[/] ",
|
| 135 |
+
)
|
| 136 |
+
yield _prefix_with_indent(
|
| 137 |
+
self.context,
|
| 138 |
+
console,
|
| 139 |
+
prefix=f"[{colour}]╰─>[/] ",
|
| 140 |
+
indent=f"[{colour}] [/] ",
|
| 141 |
+
)
|
| 142 |
+
else:
|
| 143 |
+
yield _prefix_with_indent(
|
| 144 |
+
self.message,
|
| 145 |
+
console,
|
| 146 |
+
prefix="[red]×[/] ",
|
| 147 |
+
indent=" ",
|
| 148 |
+
)
|
| 149 |
+
else:
|
| 150 |
+
yield self.message
|
| 151 |
+
if self.context is not None:
|
| 152 |
+
yield ""
|
| 153 |
+
yield self.context
|
| 154 |
+
|
| 155 |
+
if self.note_stmt is not None or self.hint_stmt is not None:
|
| 156 |
+
yield ""
|
| 157 |
+
|
| 158 |
+
if self.note_stmt is not None:
|
| 159 |
+
yield _prefix_with_indent(
|
| 160 |
+
self.note_stmt,
|
| 161 |
+
console,
|
| 162 |
+
prefix="[magenta bold]note[/]: ",
|
| 163 |
+
indent=" ",
|
| 164 |
+
)
|
| 165 |
+
if self.hint_stmt is not None:
|
| 166 |
+
yield _prefix_with_indent(
|
| 167 |
+
self.hint_stmt,
|
| 168 |
+
console,
|
| 169 |
+
prefix="[cyan bold]hint[/]: ",
|
| 170 |
+
indent=" ",
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
if self.link is not None:
|
| 174 |
+
yield ""
|
| 175 |
+
yield f"Link: {self.link}"
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
#
|
| 179 |
+
# Actual Errors
|
| 180 |
+
#
|
| 181 |
+
class ConfigurationError(PipError):
|
| 182 |
+
"""General exception in configuration"""
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class InstallationError(PipError):
|
| 186 |
+
"""General exception during installation"""
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class MissingPyProjectBuildRequires(DiagnosticPipError):
|
| 190 |
+
"""Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
|
| 191 |
+
|
| 192 |
+
reference = "missing-pyproject-build-system-requires"
|
| 193 |
+
|
| 194 |
+
def __init__(self, *, package: str) -> None:
|
| 195 |
+
super().__init__(
|
| 196 |
+
message=f"Can not process {escape(package)}",
|
| 197 |
+
context=Text(
|
| 198 |
+
"This package has an invalid pyproject.toml file.\n"
|
| 199 |
+
"The [build-system] table is missing the mandatory `requires` key."
|
| 200 |
+
),
|
| 201 |
+
note_stmt="This is an issue with the package mentioned above, not pip.",
|
| 202 |
+
hint_stmt=Text("See PEP 518 for the detailed specification."),
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class InvalidPyProjectBuildRequires(DiagnosticPipError):
|
| 207 |
+
"""Raised when pyproject.toml an invalid `build-system.requires`."""
|
| 208 |
+
|
| 209 |
+
reference = "invalid-pyproject-build-system-requires"
|
| 210 |
+
|
| 211 |
+
def __init__(self, *, package: str, reason: str) -> None:
|
| 212 |
+
super().__init__(
|
| 213 |
+
message=f"Can not process {escape(package)}",
|
| 214 |
+
context=Text(
|
| 215 |
+
"This package has an invalid `build-system.requires` key in "
|
| 216 |
+
f"pyproject.toml.\n{reason}"
|
| 217 |
+
),
|
| 218 |
+
note_stmt="This is an issue with the package mentioned above, not pip.",
|
| 219 |
+
hint_stmt=Text("See PEP 518 for the detailed specification."),
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
class NoneMetadataError(PipError):
|
| 224 |
+
"""Raised when accessing a Distribution's "METADATA" or "PKG-INFO".
|
| 225 |
+
|
| 226 |
+
This signifies an inconsistency, when the Distribution claims to have
|
| 227 |
+
the metadata file (if not, raise ``FileNotFoundError`` instead), but is
|
| 228 |
+
not actually able to produce its content. This may be due to permission
|
| 229 |
+
errors.
|
| 230 |
+
"""
|
| 231 |
+
|
| 232 |
+
def __init__(
|
| 233 |
+
self,
|
| 234 |
+
dist: "BaseDistribution",
|
| 235 |
+
metadata_name: str,
|
| 236 |
+
) -> None:
|
| 237 |
+
"""
|
| 238 |
+
:param dist: A Distribution object.
|
| 239 |
+
:param metadata_name: The name of the metadata being accessed
|
| 240 |
+
(can be "METADATA" or "PKG-INFO").
|
| 241 |
+
"""
|
| 242 |
+
self.dist = dist
|
| 243 |
+
self.metadata_name = metadata_name
|
| 244 |
+
|
| 245 |
+
def __str__(self) -> str:
|
| 246 |
+
# Use `dist` in the error message because its stringification
|
| 247 |
+
# includes more information, like the version and location.
|
| 248 |
+
return f"None {self.metadata_name} metadata found for distribution: {self.dist}"
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
class UserInstallationInvalid(InstallationError):
|
| 252 |
+
"""A --user install is requested on an environment without user site."""
|
| 253 |
+
|
| 254 |
+
def __str__(self) -> str:
|
| 255 |
+
return "User base directory is not specified"
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
class InvalidSchemeCombination(InstallationError):
|
| 259 |
+
def __str__(self) -> str:
|
| 260 |
+
before = ", ".join(str(a) for a in self.args[:-1])
|
| 261 |
+
return f"Cannot set {before} and {self.args[-1]} together"
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
class DistributionNotFound(InstallationError):
|
| 265 |
+
"""Raised when a distribution cannot be found to satisfy a requirement"""
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
class RequirementsFileParseError(InstallationError):
|
| 269 |
+
"""Raised when a general error occurs parsing a requirements file line."""
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
class BestVersionAlreadyInstalled(PipError):
|
| 273 |
+
"""Raised when the most up-to-date version of a package is already
|
| 274 |
+
installed."""
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
class BadCommand(PipError):
|
| 278 |
+
"""Raised when virtualenv or a command is not found"""
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
class CommandError(PipError):
|
| 282 |
+
"""Raised when there is an error in command-line arguments"""
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
class PreviousBuildDirError(PipError):
|
| 286 |
+
"""Raised when there's a previous conflicting build directory"""
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
class NetworkConnectionError(PipError):
|
| 290 |
+
"""HTTP connection error"""
|
| 291 |
+
|
| 292 |
+
def __init__(
|
| 293 |
+
self,
|
| 294 |
+
error_msg: str,
|
| 295 |
+
response: Optional["Response"] = None,
|
| 296 |
+
request: Optional["Request"] = None,
|
| 297 |
+
) -> None:
|
| 298 |
+
"""
|
| 299 |
+
Initialize NetworkConnectionError with `request` and `response`
|
| 300 |
+
objects.
|
| 301 |
+
"""
|
| 302 |
+
self.response = response
|
| 303 |
+
self.request = request
|
| 304 |
+
self.error_msg = error_msg
|
| 305 |
+
if (
|
| 306 |
+
self.response is not None
|
| 307 |
+
and not self.request
|
| 308 |
+
and hasattr(response, "request")
|
| 309 |
+
):
|
| 310 |
+
self.request = self.response.request
|
| 311 |
+
super().__init__(error_msg, response, request)
|
| 312 |
+
|
| 313 |
+
def __str__(self) -> str:
|
| 314 |
+
return str(self.error_msg)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
class InvalidWheelFilename(InstallationError):
|
| 318 |
+
"""Invalid wheel filename."""
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
class UnsupportedWheel(InstallationError):
|
| 322 |
+
"""Unsupported wheel."""
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
class InvalidWheel(InstallationError):
|
| 326 |
+
"""Invalid (e.g. corrupt) wheel."""
|
| 327 |
+
|
| 328 |
+
def __init__(self, location: str, name: str):
|
| 329 |
+
self.location = location
|
| 330 |
+
self.name = name
|
| 331 |
+
|
| 332 |
+
def __str__(self) -> str:
|
| 333 |
+
return f"Wheel '{self.name}' located at {self.location} is invalid."
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
class MetadataInconsistent(InstallationError):
|
| 337 |
+
"""Built metadata contains inconsistent information.
|
| 338 |
+
|
| 339 |
+
This is raised when the metadata contains values (e.g. name and version)
|
| 340 |
+
that do not match the information previously obtained from sdist filename,
|
| 341 |
+
user-supplied ``#egg=`` value, or an install requirement name.
|
| 342 |
+
"""
|
| 343 |
+
|
| 344 |
+
def __init__(
|
| 345 |
+
self, ireq: "InstallRequirement", field: str, f_val: str, m_val: str
|
| 346 |
+
) -> None:
|
| 347 |
+
self.ireq = ireq
|
| 348 |
+
self.field = field
|
| 349 |
+
self.f_val = f_val
|
| 350 |
+
self.m_val = m_val
|
| 351 |
+
|
| 352 |
+
def __str__(self) -> str:
|
| 353 |
+
return (
|
| 354 |
+
f"Requested {self.ireq} has inconsistent {self.field}: "
|
| 355 |
+
f"expected {self.f_val!r}, but metadata has {self.m_val!r}"
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class MetadataInvalid(InstallationError):
|
| 360 |
+
"""Metadata is invalid."""
|
| 361 |
+
|
| 362 |
+
def __init__(self, ireq: "InstallRequirement", error: str) -> None:
|
| 363 |
+
self.ireq = ireq
|
| 364 |
+
self.error = error
|
| 365 |
+
|
| 366 |
+
def __str__(self) -> str:
|
| 367 |
+
return f"Requested {self.ireq} has invalid metadata: {self.error}"
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
class InstallationSubprocessError(DiagnosticPipError, InstallationError):
|
| 371 |
+
"""A subprocess call failed."""
|
| 372 |
+
|
| 373 |
+
reference = "subprocess-exited-with-error"
|
| 374 |
+
|
| 375 |
+
def __init__(
|
| 376 |
+
self,
|
| 377 |
+
*,
|
| 378 |
+
command_description: str,
|
| 379 |
+
exit_code: int,
|
| 380 |
+
output_lines: Optional[List[str]],
|
| 381 |
+
) -> None:
|
| 382 |
+
if output_lines is None:
|
| 383 |
+
output_prompt = Text("See above for output.")
|
| 384 |
+
else:
|
| 385 |
+
output_prompt = (
|
| 386 |
+
Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
|
| 387 |
+
+ Text("".join(output_lines))
|
| 388 |
+
+ Text.from_markup(R"[red]\[end of output][/]")
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
super().__init__(
|
| 392 |
+
message=(
|
| 393 |
+
f"[green]{escape(command_description)}[/] did not run successfully.\n"
|
| 394 |
+
f"exit code: {exit_code}"
|
| 395 |
+
),
|
| 396 |
+
context=output_prompt,
|
| 397 |
+
hint_stmt=None,
|
| 398 |
+
note_stmt=(
|
| 399 |
+
"This error originates from a subprocess, and is likely not a "
|
| 400 |
+
"problem with pip."
|
| 401 |
+
),
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
self.command_description = command_description
|
| 405 |
+
self.exit_code = exit_code
|
| 406 |
+
|
| 407 |
+
def __str__(self) -> str:
|
| 408 |
+
return f"{self.command_description} exited with {self.exit_code}"
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
|
| 412 |
+
reference = "metadata-generation-failed"
|
| 413 |
+
|
| 414 |
+
def __init__(
|
| 415 |
+
self,
|
| 416 |
+
*,
|
| 417 |
+
package_details: str,
|
| 418 |
+
) -> None:
|
| 419 |
+
super(InstallationSubprocessError, self).__init__(
|
| 420 |
+
message="Encountered error while generating package metadata.",
|
| 421 |
+
context=escape(package_details),
|
| 422 |
+
hint_stmt="See above for details.",
|
| 423 |
+
note_stmt="This is an issue with the package mentioned above, not pip.",
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
def __str__(self) -> str:
|
| 427 |
+
return "metadata generation failed"
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
class HashErrors(InstallationError):
|
| 431 |
+
"""Multiple HashError instances rolled into one for reporting"""
|
| 432 |
+
|
| 433 |
+
def __init__(self) -> None:
|
| 434 |
+
self.errors: List[HashError] = []
|
| 435 |
+
|
| 436 |
+
def append(self, error: "HashError") -> None:
|
| 437 |
+
self.errors.append(error)
|
| 438 |
+
|
| 439 |
+
def __str__(self) -> str:
|
| 440 |
+
lines = []
|
| 441 |
+
self.errors.sort(key=lambda e: e.order)
|
| 442 |
+
for cls, errors_of_cls in groupby(self.errors, lambda e: e.__class__):
|
| 443 |
+
lines.append(cls.head)
|
| 444 |
+
lines.extend(e.body() for e in errors_of_cls)
|
| 445 |
+
if lines:
|
| 446 |
+
return "\n".join(lines)
|
| 447 |
+
return ""
|
| 448 |
+
|
| 449 |
+
def __bool__(self) -> bool:
|
| 450 |
+
return bool(self.errors)
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
class HashError(InstallationError):
|
| 454 |
+
"""
|
| 455 |
+
A failure to verify a package against known-good hashes
|
| 456 |
+
|
| 457 |
+
:cvar order: An int sorting hash exception classes by difficulty of
|
| 458 |
+
recovery (lower being harder), so the user doesn't bother fretting
|
| 459 |
+
about unpinned packages when he has deeper issues, like VCS
|
| 460 |
+
dependencies, to deal with. Also keeps error reports in a
|
| 461 |
+
deterministic order.
|
| 462 |
+
:cvar head: A section heading for display above potentially many
|
| 463 |
+
exceptions of this kind
|
| 464 |
+
:ivar req: The InstallRequirement that triggered this error. This is
|
| 465 |
+
pasted on after the exception is instantiated, because it's not
|
| 466 |
+
typically available earlier.
|
| 467 |
+
|
| 468 |
+
"""
|
| 469 |
+
|
| 470 |
+
req: Optional["InstallRequirement"] = None
|
| 471 |
+
head = ""
|
| 472 |
+
order: int = -1
|
| 473 |
+
|
| 474 |
+
def body(self) -> str:
|
| 475 |
+
"""Return a summary of me for display under the heading.
|
| 476 |
+
|
| 477 |
+
This default implementation simply prints a description of the
|
| 478 |
+
triggering requirement.
|
| 479 |
+
|
| 480 |
+
:param req: The InstallRequirement that provoked this error, with
|
| 481 |
+
its link already populated by the resolver's _populate_link().
|
| 482 |
+
|
| 483 |
+
"""
|
| 484 |
+
return f" {self._requirement_name()}"
|
| 485 |
+
|
| 486 |
+
def __str__(self) -> str:
|
| 487 |
+
return f"{self.head}\n{self.body()}"
|
| 488 |
+
|
| 489 |
+
def _requirement_name(self) -> str:
|
| 490 |
+
"""Return a description of the requirement that triggered me.
|
| 491 |
+
|
| 492 |
+
This default implementation returns long description of the req, with
|
| 493 |
+
line numbers
|
| 494 |
+
|
| 495 |
+
"""
|
| 496 |
+
return str(self.req) if self.req else "unknown package"
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
class VcsHashUnsupported(HashError):
|
| 500 |
+
"""A hash was provided for a version-control-system-based requirement, but
|
| 501 |
+
we don't have a method for hashing those."""
|
| 502 |
+
|
| 503 |
+
order = 0
|
| 504 |
+
head = (
|
| 505 |
+
"Can't verify hashes for these requirements because we don't "
|
| 506 |
+
"have a way to hash version control repositories:"
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
class DirectoryUrlHashUnsupported(HashError):
|
| 511 |
+
"""A hash was provided for a version-control-system-based requirement, but
|
| 512 |
+
we don't have a method for hashing those."""
|
| 513 |
+
|
| 514 |
+
order = 1
|
| 515 |
+
head = (
|
| 516 |
+
"Can't verify hashes for these file:// requirements because they "
|
| 517 |
+
"point to directories:"
|
| 518 |
+
)
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
class HashMissing(HashError):
|
| 522 |
+
"""A hash was needed for a requirement but is absent."""
|
| 523 |
+
|
| 524 |
+
order = 2
|
| 525 |
+
head = (
|
| 526 |
+
"Hashes are required in --require-hashes mode, but they are "
|
| 527 |
+
"missing from some requirements. Here is a list of those "
|
| 528 |
+
"requirements along with the hashes their downloaded archives "
|
| 529 |
+
"actually had. Add lines like these to your requirements files to "
|
| 530 |
+
"prevent tampering. (If you did not enable --require-hashes "
|
| 531 |
+
"manually, note that it turns on automatically when any package "
|
| 532 |
+
"has a hash.)"
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
def __init__(self, gotten_hash: str) -> None:
|
| 536 |
+
"""
|
| 537 |
+
:param gotten_hash: The hash of the (possibly malicious) archive we
|
| 538 |
+
just downloaded
|
| 539 |
+
"""
|
| 540 |
+
self.gotten_hash = gotten_hash
|
| 541 |
+
|
| 542 |
+
def body(self) -> str:
|
| 543 |
+
# Dodge circular import.
|
| 544 |
+
from pip._internal.utils.hashes import FAVORITE_HASH
|
| 545 |
+
|
| 546 |
+
package = None
|
| 547 |
+
if self.req:
|
| 548 |
+
# In the case of URL-based requirements, display the original URL
|
| 549 |
+
# seen in the requirements file rather than the package name,
|
| 550 |
+
# so the output can be directly copied into the requirements file.
|
| 551 |
+
package = (
|
| 552 |
+
self.req.original_link
|
| 553 |
+
if self.req.is_direct
|
| 554 |
+
# In case someone feeds something downright stupid
|
| 555 |
+
# to InstallRequirement's constructor.
|
| 556 |
+
else getattr(self.req, "req", None)
|
| 557 |
+
)
|
| 558 |
+
return " {} --hash={}:{}".format(
|
| 559 |
+
package or "unknown package", FAVORITE_HASH, self.gotten_hash
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
class HashUnpinned(HashError):
|
| 564 |
+
"""A requirement had a hash specified but was not pinned to a specific
|
| 565 |
+
version."""
|
| 566 |
+
|
| 567 |
+
order = 3
|
| 568 |
+
head = (
|
| 569 |
+
"In --require-hashes mode, all requirements must have their "
|
| 570 |
+
"versions pinned with ==. These do not:"
|
| 571 |
+
)
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
class HashMismatch(HashError):
|
| 575 |
+
"""
|
| 576 |
+
Distribution file hash values don't match.
|
| 577 |
+
|
| 578 |
+
:ivar package_name: The name of the package that triggered the hash
|
| 579 |
+
mismatch. Feel free to write to this after the exception is raise to
|
| 580 |
+
improve its error message.
|
| 581 |
+
|
| 582 |
+
"""
|
| 583 |
+
|
| 584 |
+
order = 4
|
| 585 |
+
head = (
|
| 586 |
+
"THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS "
|
| 587 |
+
"FILE. If you have updated the package versions, please update "
|
| 588 |
+
"the hashes. Otherwise, examine the package contents carefully; "
|
| 589 |
+
"someone may have tampered with them."
|
| 590 |
+
)
|
| 591 |
+
|
| 592 |
+
def __init__(self, allowed: Dict[str, List[str]], gots: Dict[str, "_Hash"]) -> None:
|
| 593 |
+
"""
|
| 594 |
+
:param allowed: A dict of algorithm names pointing to lists of allowed
|
| 595 |
+
hex digests
|
| 596 |
+
:param gots: A dict of algorithm names pointing to hashes we
|
| 597 |
+
actually got from the files under suspicion
|
| 598 |
+
"""
|
| 599 |
+
self.allowed = allowed
|
| 600 |
+
self.gots = gots
|
| 601 |
+
|
| 602 |
+
def body(self) -> str:
|
| 603 |
+
return f" {self._requirement_name()}:\n{self._hash_comparison()}"
|
| 604 |
+
|
| 605 |
+
def _hash_comparison(self) -> str:
|
| 606 |
+
"""
|
| 607 |
+
Return a comparison of actual and expected hash values.
|
| 608 |
+
|
| 609 |
+
Example::
|
| 610 |
+
|
| 611 |
+
Expected sha256 abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde
|
| 612 |
+
or 123451234512345123451234512345123451234512345
|
| 613 |
+
Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef
|
| 614 |
+
|
| 615 |
+
"""
|
| 616 |
+
|
| 617 |
+
def hash_then_or(hash_name: str) -> "chain[str]":
|
| 618 |
+
# For now, all the decent hashes have 6-char names, so we can get
|
| 619 |
+
# away with hard-coding space literals.
|
| 620 |
+
return chain([hash_name], repeat(" or"))
|
| 621 |
+
|
| 622 |
+
lines: List[str] = []
|
| 623 |
+
for hash_name, expecteds in self.allowed.items():
|
| 624 |
+
prefix = hash_then_or(hash_name)
|
| 625 |
+
lines.extend((f" Expected {next(prefix)} {e}") for e in expecteds)
|
| 626 |
+
lines.append(
|
| 627 |
+
f" Got {self.gots[hash_name].hexdigest()}\n"
|
| 628 |
+
)
|
| 629 |
+
return "\n".join(lines)
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
class UnsupportedPythonVersion(InstallationError):
|
| 633 |
+
"""Unsupported python version according to Requires-Python package
|
| 634 |
+
metadata."""
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
|
| 638 |
+
"""When there are errors while loading a configuration file"""
|
| 639 |
+
|
| 640 |
+
def __init__(
|
| 641 |
+
self,
|
| 642 |
+
reason: str = "could not be loaded",
|
| 643 |
+
fname: Optional[str] = None,
|
| 644 |
+
error: Optional[configparser.Error] = None,
|
| 645 |
+
) -> None:
|
| 646 |
+
super().__init__(error)
|
| 647 |
+
self.reason = reason
|
| 648 |
+
self.fname = fname
|
| 649 |
+
self.error = error
|
| 650 |
+
|
| 651 |
+
def __str__(self) -> str:
|
| 652 |
+
if self.fname is not None:
|
| 653 |
+
message_part = f" in {self.fname}."
|
| 654 |
+
else:
|
| 655 |
+
assert self.error is not None
|
| 656 |
+
message_part = f".\n{self.error}\n"
|
| 657 |
+
return f"Configuration file {self.reason}{message_part}"
|
| 658 |
+
|
| 659 |
+
|
| 660 |
+
_DEFAULT_EXTERNALLY_MANAGED_ERROR = f"""\
|
| 661 |
+
The Python environment under {sys.prefix} is managed externally, and may not be
|
| 662 |
+
manipulated by the user. Please use specific tooling from the distributor of
|
| 663 |
+
the Python installation to interact with this environment instead.
|
| 664 |
+
"""
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
class ExternallyManagedEnvironment(DiagnosticPipError):
|
| 668 |
+
"""The current environment is externally managed.
|
| 669 |
+
|
| 670 |
+
This is raised when the current environment is externally managed, as
|
| 671 |
+
defined by `PEP 668`_. The ``EXTERNALLY-MANAGED`` configuration is checked
|
| 672 |
+
and displayed when the error is bubbled up to the user.
|
| 673 |
+
|
| 674 |
+
:param error: The error message read from ``EXTERNALLY-MANAGED``.
|
| 675 |
+
"""
|
| 676 |
+
|
| 677 |
+
reference = "externally-managed-environment"
|
| 678 |
+
|
| 679 |
+
def __init__(self, error: Optional[str]) -> None:
|
| 680 |
+
if error is None:
|
| 681 |
+
context = Text(_DEFAULT_EXTERNALLY_MANAGED_ERROR)
|
| 682 |
+
else:
|
| 683 |
+
context = Text(error)
|
| 684 |
+
super().__init__(
|
| 685 |
+
message="This environment is externally managed",
|
| 686 |
+
context=context,
|
| 687 |
+
note_stmt=(
|
| 688 |
+
"If you believe this is a mistake, please contact your "
|
| 689 |
+
"Python installation or OS distribution provider. "
|
| 690 |
+
"You can override this, at the risk of breaking your Python "
|
| 691 |
+
"installation or OS, by passing --break-system-packages."
|
| 692 |
+
),
|
| 693 |
+
hint_stmt=Text("See PEP 668 for the detailed specification."),
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
@staticmethod
|
| 697 |
+
def _iter_externally_managed_error_keys() -> Iterator[str]:
|
| 698 |
+
# LC_MESSAGES is in POSIX, but not the C standard. The most common
|
| 699 |
+
# platform that does not implement this category is Windows, where
|
| 700 |
+
# using other categories for console message localization is equally
|
| 701 |
+
# unreliable, so we fall back to the locale-less vendor message. This
|
| 702 |
+
# can always be re-evaluated when a vendor proposes a new alternative.
|
| 703 |
+
try:
|
| 704 |
+
category = locale.LC_MESSAGES
|
| 705 |
+
except AttributeError:
|
| 706 |
+
lang: Optional[str] = None
|
| 707 |
+
else:
|
| 708 |
+
lang, _ = locale.getlocale(category)
|
| 709 |
+
if lang is not None:
|
| 710 |
+
yield f"Error-{lang}"
|
| 711 |
+
for sep in ("-", "_"):
|
| 712 |
+
before, found, _ = lang.partition(sep)
|
| 713 |
+
if not found:
|
| 714 |
+
continue
|
| 715 |
+
yield f"Error-{before}"
|
| 716 |
+
yield "Error"
|
| 717 |
+
|
| 718 |
+
@classmethod
|
| 719 |
+
def from_config(
|
| 720 |
+
cls,
|
| 721 |
+
config: Union[pathlib.Path, str],
|
| 722 |
+
) -> "ExternallyManagedEnvironment":
|
| 723 |
+
parser = configparser.ConfigParser(interpolation=None)
|
| 724 |
+
try:
|
| 725 |
+
parser.read(config, encoding="utf-8")
|
| 726 |
+
section = parser["externally-managed"]
|
| 727 |
+
for key in cls._iter_externally_managed_error_keys():
|
| 728 |
+
with contextlib.suppress(KeyError):
|
| 729 |
+
return cls(section[key])
|
| 730 |
+
except KeyError:
|
| 731 |
+
pass
|
| 732 |
+
except (OSError, UnicodeDecodeError, configparser.ParsingError):
|
| 733 |
+
from pip._internal.utils._log import VERBOSE
|
| 734 |
+
|
| 735 |
+
exc_info = logger.isEnabledFor(VERBOSE)
|
| 736 |
+
logger.warning("Failed to read %s", config, exc_info=exc_info)
|
| 737 |
+
return cls(None)
|
| 738 |
+
|
| 739 |
+
|
| 740 |
+
class UninstallMissingRecord(DiagnosticPipError):
|
| 741 |
+
reference = "uninstall-no-record-file"
|
| 742 |
+
|
| 743 |
+
def __init__(self, *, distribution: "BaseDistribution") -> None:
|
| 744 |
+
installer = distribution.installer
|
| 745 |
+
if not installer or installer == "pip":
|
| 746 |
+
dep = f"{distribution.raw_name}=={distribution.version}"
|
| 747 |
+
hint = Text.assemble(
|
| 748 |
+
"You might be able to recover from this via: ",
|
| 749 |
+
(f"pip install --force-reinstall --no-deps {dep}", "green"),
|
| 750 |
+
)
|
| 751 |
+
else:
|
| 752 |
+
hint = Text(
|
| 753 |
+
f"The package was installed by {installer}. "
|
| 754 |
+
"You should check if it can uninstall the package."
|
| 755 |
+
)
|
| 756 |
+
|
| 757 |
+
super().__init__(
|
| 758 |
+
message=Text(f"Cannot uninstall {distribution}"),
|
| 759 |
+
context=(
|
| 760 |
+
"The package's contents are unknown: "
|
| 761 |
+
f"no RECORD file was found for {distribution.raw_name}."
|
| 762 |
+
),
|
| 763 |
+
hint_stmt=hint,
|
| 764 |
+
)
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
class LegacyDistutilsInstall(DiagnosticPipError):
|
| 768 |
+
reference = "uninstall-distutils-installed-package"
|
| 769 |
+
|
| 770 |
+
def __init__(self, *, distribution: "BaseDistribution") -> None:
|
| 771 |
+
super().__init__(
|
| 772 |
+
message=Text(f"Cannot uninstall {distribution}"),
|
| 773 |
+
context=(
|
| 774 |
+
"It is a distutils installed project and thus we cannot accurately "
|
| 775 |
+
"determine which files belong to it which would lead to only a partial "
|
| 776 |
+
"uninstall."
|
| 777 |
+
),
|
| 778 |
+
hint_stmt=None,
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
class InvalidInstalledPackage(DiagnosticPipError):
|
| 783 |
+
reference = "invalid-installed-package"
|
| 784 |
+
|
| 785 |
+
def __init__(
|
| 786 |
+
self,
|
| 787 |
+
*,
|
| 788 |
+
dist: "BaseDistribution",
|
| 789 |
+
invalid_exc: Union[InvalidRequirement, InvalidVersion],
|
| 790 |
+
) -> None:
|
| 791 |
+
installed_location = dist.installed_location
|
| 792 |
+
|
| 793 |
+
if isinstance(invalid_exc, InvalidRequirement):
|
| 794 |
+
invalid_type = "requirement"
|
| 795 |
+
else:
|
| 796 |
+
invalid_type = "version"
|
| 797 |
+
|
| 798 |
+
super().__init__(
|
| 799 |
+
message=Text(
|
| 800 |
+
f"Cannot process installed package {dist} "
|
| 801 |
+
+ (f"in {installed_location!r} " if installed_location else "")
|
| 802 |
+
+ f"because it has an invalid {invalid_type}:\n{invalid_exc.args[0]}"
|
| 803 |
+
),
|
| 804 |
+
context=(
|
| 805 |
+
"Starting with pip 24.1, packages with invalid "
|
| 806 |
+
f"{invalid_type}s can not be processed."
|
| 807 |
+
),
|
| 808 |
+
hint_stmt="To proceed this package must be uninstalled.",
|
| 809 |
+
)
|
lib/python3.12/site-packages/pip/_internal/main.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def main(args: Optional[List[str]] = None) -> int:
|
| 5 |
+
"""This is preserved for old console scripts that may still be referencing
|
| 6 |
+
it.
|
| 7 |
+
|
| 8 |
+
For additional details, see https://github.com/pypa/pip/issues/7498.
|
| 9 |
+
"""
|
| 10 |
+
from pip._internal.utils.entrypoints import _wrapper
|
| 11 |
+
|
| 12 |
+
return _wrapper(args)
|
lib/python3.12/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_resources`` 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 across 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 |
+
)
|
lib/python3.12/site-packages/pip/_internal/metadata/_json.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, cast
|
| 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 |
+
("License-Expression", False),
|
| 27 |
+
("License-File", True),
|
| 28 |
+
("Classifier", True),
|
| 29 |
+
("Requires-Dist", True),
|
| 30 |
+
("Requires-Python", False),
|
| 31 |
+
("Requires-External", True),
|
| 32 |
+
("Project-URL", True),
|
| 33 |
+
("Provides-Extra", True),
|
| 34 |
+
("Provides-Dist", True),
|
| 35 |
+
("Obsoletes-Dist", True),
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def json_name(field: str) -> str:
|
| 40 |
+
return field.lower().replace("-", "_")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def msg_to_json(msg: Message) -> Dict[str, Any]:
|
| 44 |
+
"""Convert a Message object into a JSON-compatible dictionary."""
|
| 45 |
+
|
| 46 |
+
def sanitise_header(h: Union[Header, str]) -> str:
|
| 47 |
+
if isinstance(h, Header):
|
| 48 |
+
chunks = []
|
| 49 |
+
for bytes, encoding in decode_header(h):
|
| 50 |
+
if encoding == "unknown-8bit":
|
| 51 |
+
try:
|
| 52 |
+
# See if UTF-8 works
|
| 53 |
+
bytes.decode("utf-8")
|
| 54 |
+
encoding = "utf-8"
|
| 55 |
+
except UnicodeDecodeError:
|
| 56 |
+
# If not, latin1 at least won't fail
|
| 57 |
+
encoding = "latin1"
|
| 58 |
+
chunks.append((bytes, encoding))
|
| 59 |
+
return str(make_header(chunks))
|
| 60 |
+
return str(h)
|
| 61 |
+
|
| 62 |
+
result = {}
|
| 63 |
+
for field, multi in METADATA_FIELDS:
|
| 64 |
+
if field not in msg:
|
| 65 |
+
continue
|
| 66 |
+
key = json_name(field)
|
| 67 |
+
if multi:
|
| 68 |
+
value: Union[str, List[str]] = [
|
| 69 |
+
sanitise_header(v) for v in msg.get_all(field) # type: ignore
|
| 70 |
+
]
|
| 71 |
+
else:
|
| 72 |
+
value = sanitise_header(msg.get(field)) # type: ignore
|
| 73 |
+
if key == "keywords":
|
| 74 |
+
# Accept both comma-separated and space-separated
|
| 75 |
+
# forms, for better compatibility with old data.
|
| 76 |
+
if "," in value:
|
| 77 |
+
value = [v.strip() for v in value.split(",")]
|
| 78 |
+
else:
|
| 79 |
+
value = value.split()
|
| 80 |
+
result[key] = value
|
| 81 |
+
|
| 82 |
+
payload = cast(str, msg.get_payload())
|
| 83 |
+
if payload:
|
| 84 |
+
result["description"] = payload
|
| 85 |
+
|
| 86 |
+
return result
|
lib/python3.12/site-packages/pip/_internal/metadata/base.py
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
Any,
|
| 12 |
+
Collection,
|
| 13 |
+
Container,
|
| 14 |
+
Dict,
|
| 15 |
+
Iterable,
|
| 16 |
+
Iterator,
|
| 17 |
+
List,
|
| 18 |
+
NamedTuple,
|
| 19 |
+
Optional,
|
| 20 |
+
Protocol,
|
| 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 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 |
+
InfoPath = Union[str, pathlib.PurePath]
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class BaseEntryPoint(Protocol):
|
| 50 |
+
@property
|
| 51 |
+
def name(self) -> str:
|
| 52 |
+
raise NotImplementedError()
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def value(self) -> str:
|
| 56 |
+
raise NotImplementedError()
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def group(self) -> str:
|
| 60 |
+
raise NotImplementedError()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _convert_installed_files_path(
|
| 64 |
+
entry: Tuple[str, ...],
|
| 65 |
+
info: Tuple[str, ...],
|
| 66 |
+
) -> str:
|
| 67 |
+
"""Convert a legacy installed-files.txt path into modern RECORD path.
|
| 68 |
+
|
| 69 |
+
The legacy format stores paths relative to the info directory, while the
|
| 70 |
+
modern format stores paths relative to the package root, e.g. the
|
| 71 |
+
site-packages directory.
|
| 72 |
+
|
| 73 |
+
:param entry: Path parts of the installed-files.txt entry.
|
| 74 |
+
:param info: Path parts of the egg-info directory relative to package root.
|
| 75 |
+
:returns: The converted entry.
|
| 76 |
+
|
| 77 |
+
For best compatibility with symlinks, this does not use ``abspath()`` or
|
| 78 |
+
``Path.resolve()``, but tries to work with path parts:
|
| 79 |
+
|
| 80 |
+
1. While ``entry`` starts with ``..``, remove the equal amounts of parts
|
| 81 |
+
from ``info``; if ``info`` is empty, start appending ``..`` instead.
|
| 82 |
+
2. Join the two directly.
|
| 83 |
+
"""
|
| 84 |
+
while entry and entry[0] == "..":
|
| 85 |
+
if not info or info[-1] == "..":
|
| 86 |
+
info += ("..",)
|
| 87 |
+
else:
|
| 88 |
+
info = info[:-1]
|
| 89 |
+
entry = entry[1:]
|
| 90 |
+
return str(pathlib.Path(*info, *entry))
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class RequiresEntry(NamedTuple):
|
| 94 |
+
requirement: str
|
| 95 |
+
extra: str
|
| 96 |
+
marker: str
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class BaseDistribution(Protocol):
|
| 100 |
+
@classmethod
|
| 101 |
+
def from_directory(cls, directory: str) -> "BaseDistribution":
|
| 102 |
+
"""Load the distribution from a metadata directory.
|
| 103 |
+
|
| 104 |
+
:param directory: Path to a metadata directory, e.g. ``.dist-info``.
|
| 105 |
+
"""
|
| 106 |
+
raise NotImplementedError()
|
| 107 |
+
|
| 108 |
+
@classmethod
|
| 109 |
+
def from_metadata_file_contents(
|
| 110 |
+
cls,
|
| 111 |
+
metadata_contents: bytes,
|
| 112 |
+
filename: str,
|
| 113 |
+
project_name: str,
|
| 114 |
+
) -> "BaseDistribution":
|
| 115 |
+
"""Load the distribution from the contents of a METADATA file.
|
| 116 |
+
|
| 117 |
+
This is used to implement PEP 658 by generating a "shallow" dist object that can
|
| 118 |
+
be used for resolution without downloading or building the actual dist yet.
|
| 119 |
+
|
| 120 |
+
:param metadata_contents: The contents of a METADATA file.
|
| 121 |
+
:param filename: File name for the dist with this metadata.
|
| 122 |
+
:param project_name: Name of the project this dist represents.
|
| 123 |
+
"""
|
| 124 |
+
raise NotImplementedError()
|
| 125 |
+
|
| 126 |
+
@classmethod
|
| 127 |
+
def from_wheel(cls, wheel: "Wheel", name: str) -> "BaseDistribution":
|
| 128 |
+
"""Load the distribution from a given wheel.
|
| 129 |
+
|
| 130 |
+
:param wheel: A concrete wheel definition.
|
| 131 |
+
:param name: File name of the wheel.
|
| 132 |
+
|
| 133 |
+
:raises InvalidWheel: Whenever loading of the wheel causes a
|
| 134 |
+
:py:exc:`zipfile.BadZipFile` exception to be thrown.
|
| 135 |
+
:raises UnsupportedWheel: If the wheel is a valid zip, but malformed
|
| 136 |
+
internally.
|
| 137 |
+
"""
|
| 138 |
+
raise NotImplementedError()
|
| 139 |
+
|
| 140 |
+
def __repr__(self) -> str:
|
| 141 |
+
return f"{self.raw_name} {self.raw_version} ({self.location})"
|
| 142 |
+
|
| 143 |
+
def __str__(self) -> str:
|
| 144 |
+
return f"{self.raw_name} {self.raw_version}"
|
| 145 |
+
|
| 146 |
+
@property
|
| 147 |
+
def location(self) -> Optional[str]:
|
| 148 |
+
"""Where the distribution is loaded from.
|
| 149 |
+
|
| 150 |
+
A string value is not necessarily a filesystem path, since distributions
|
| 151 |
+
can be loaded from other sources, e.g. arbitrary zip archives. ``None``
|
| 152 |
+
means the distribution is created in-memory.
|
| 153 |
+
|
| 154 |
+
Do not canonicalize this value with e.g. ``pathlib.Path.resolve()``. If
|
| 155 |
+
this is a symbolic link, we want to preserve the relative path between
|
| 156 |
+
it and files in the distribution.
|
| 157 |
+
"""
|
| 158 |
+
raise NotImplementedError()
|
| 159 |
+
|
| 160 |
+
@property
|
| 161 |
+
def editable_project_location(self) -> Optional[str]:
|
| 162 |
+
"""The project location for editable distributions.
|
| 163 |
+
|
| 164 |
+
This is the directory where pyproject.toml or setup.py is located.
|
| 165 |
+
None if the distribution is not installed in editable mode.
|
| 166 |
+
"""
|
| 167 |
+
# TODO: this property is relatively costly to compute, memoize it ?
|
| 168 |
+
direct_url = self.direct_url
|
| 169 |
+
if direct_url:
|
| 170 |
+
if direct_url.is_local_editable():
|
| 171 |
+
return url_to_path(direct_url.url)
|
| 172 |
+
else:
|
| 173 |
+
# Search for an .egg-link file by walking sys.path, as it was
|
| 174 |
+
# done before by dist_is_editable().
|
| 175 |
+
egg_link_path = egg_link_path_from_sys_path(self.raw_name)
|
| 176 |
+
if egg_link_path:
|
| 177 |
+
# TODO: get project location from second line of egg_link file
|
| 178 |
+
# (https://github.com/pypa/pip/issues/10243)
|
| 179 |
+
return self.location
|
| 180 |
+
return None
|
| 181 |
+
|
| 182 |
+
@property
|
| 183 |
+
def installed_location(self) -> Optional[str]:
|
| 184 |
+
"""The distribution's "installed" location.
|
| 185 |
+
|
| 186 |
+
This should generally be a ``site-packages`` directory. This is
|
| 187 |
+
usually ``dist.location``, except for legacy develop-installed packages,
|
| 188 |
+
where ``dist.location`` is the source code location, and this is where
|
| 189 |
+
the ``.egg-link`` file is.
|
| 190 |
+
|
| 191 |
+
The returned location is normalized (in particular, with symlinks removed).
|
| 192 |
+
"""
|
| 193 |
+
raise NotImplementedError()
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def info_location(self) -> Optional[str]:
|
| 197 |
+
"""Location of the .[egg|dist]-info directory or file.
|
| 198 |
+
|
| 199 |
+
Similarly to ``location``, a string value is not necessarily a
|
| 200 |
+
filesystem path. ``None`` means the distribution is created in-memory.
|
| 201 |
+
|
| 202 |
+
For a modern .dist-info installation on disk, this should be something
|
| 203 |
+
like ``{location}/{raw_name}-{version}.dist-info``.
|
| 204 |
+
|
| 205 |
+
Do not canonicalize this value with e.g. ``pathlib.Path.resolve()``. If
|
| 206 |
+
this is a symbolic link, we want to preserve the relative path between
|
| 207 |
+
it and other files in the distribution.
|
| 208 |
+
"""
|
| 209 |
+
raise NotImplementedError()
|
| 210 |
+
|
| 211 |
+
@property
|
| 212 |
+
def installed_by_distutils(self) -> bool:
|
| 213 |
+
"""Whether this distribution is installed with legacy distutils format.
|
| 214 |
+
|
| 215 |
+
A distribution installed with "raw" distutils not patched by setuptools
|
| 216 |
+
uses one single file at ``info_location`` to store metadata. We need to
|
| 217 |
+
treat this specially on uninstallation.
|
| 218 |
+
"""
|
| 219 |
+
info_location = self.info_location
|
| 220 |
+
if not info_location:
|
| 221 |
+
return False
|
| 222 |
+
return pathlib.Path(info_location).is_file()
|
| 223 |
+
|
| 224 |
+
@property
|
| 225 |
+
def installed_as_egg(self) -> bool:
|
| 226 |
+
"""Whether this distribution is installed as an egg.
|
| 227 |
+
|
| 228 |
+
This usually indicates the distribution was installed by (older versions
|
| 229 |
+
of) easy_install.
|
| 230 |
+
"""
|
| 231 |
+
location = self.location
|
| 232 |
+
if not location:
|
| 233 |
+
return False
|
| 234 |
+
return location.endswith(".egg")
|
| 235 |
+
|
| 236 |
+
@property
|
| 237 |
+
def installed_with_setuptools_egg_info(self) -> bool:
|
| 238 |
+
"""Whether this distribution is installed with the ``.egg-info`` format.
|
| 239 |
+
|
| 240 |
+
This usually indicates the distribution was installed with setuptools
|
| 241 |
+
with an old pip version or with ``single-version-externally-managed``.
|
| 242 |
+
|
| 243 |
+
Note that this ensure the metadata store is a directory. distutils can
|
| 244 |
+
also installs an ``.egg-info``, but as a file, not a directory. This
|
| 245 |
+
property is *False* for that case. Also see ``installed_by_distutils``.
|
| 246 |
+
"""
|
| 247 |
+
info_location = self.info_location
|
| 248 |
+
if not info_location:
|
| 249 |
+
return False
|
| 250 |
+
if not info_location.endswith(".egg-info"):
|
| 251 |
+
return False
|
| 252 |
+
return pathlib.Path(info_location).is_dir()
|
| 253 |
+
|
| 254 |
+
@property
|
| 255 |
+
def installed_with_dist_info(self) -> bool:
|
| 256 |
+
"""Whether this distribution is installed with the "modern format".
|
| 257 |
+
|
| 258 |
+
This indicates a "modern" installation, e.g. storing metadata in the
|
| 259 |
+
``.dist-info`` directory. This applies to installations made by
|
| 260 |
+
setuptools (but through pip, not directly), or anything using the
|
| 261 |
+
standardized build backend interface (PEP 517).
|
| 262 |
+
"""
|
| 263 |
+
info_location = self.info_location
|
| 264 |
+
if not info_location:
|
| 265 |
+
return False
|
| 266 |
+
if not info_location.endswith(".dist-info"):
|
| 267 |
+
return False
|
| 268 |
+
return pathlib.Path(info_location).is_dir()
|
| 269 |
+
|
| 270 |
+
@property
|
| 271 |
+
def canonical_name(self) -> NormalizedName:
|
| 272 |
+
raise NotImplementedError()
|
| 273 |
+
|
| 274 |
+
@property
|
| 275 |
+
def version(self) -> Version:
|
| 276 |
+
raise NotImplementedError()
|
| 277 |
+
|
| 278 |
+
@property
|
| 279 |
+
def raw_version(self) -> str:
|
| 280 |
+
raise NotImplementedError()
|
| 281 |
+
|
| 282 |
+
@property
|
| 283 |
+
def setuptools_filename(self) -> str:
|
| 284 |
+
"""Convert a project name to its setuptools-compatible filename.
|
| 285 |
+
|
| 286 |
+
This is a copy of ``pkg_resources.to_filename()`` for compatibility.
|
| 287 |
+
"""
|
| 288 |
+
return self.raw_name.replace("-", "_")
|
| 289 |
+
|
| 290 |
+
@property
|
| 291 |
+
def direct_url(self) -> Optional[DirectUrl]:
|
| 292 |
+
"""Obtain a DirectUrl from this distribution.
|
| 293 |
+
|
| 294 |
+
Returns None if the distribution has no `direct_url.json` metadata,
|
| 295 |
+
or if `direct_url.json` is invalid.
|
| 296 |
+
"""
|
| 297 |
+
try:
|
| 298 |
+
content = self.read_text(DIRECT_URL_METADATA_NAME)
|
| 299 |
+
except FileNotFoundError:
|
| 300 |
+
return None
|
| 301 |
+
try:
|
| 302 |
+
return DirectUrl.from_json(content)
|
| 303 |
+
except (
|
| 304 |
+
UnicodeDecodeError,
|
| 305 |
+
json.JSONDecodeError,
|
| 306 |
+
DirectUrlValidationError,
|
| 307 |
+
) as e:
|
| 308 |
+
logger.warning(
|
| 309 |
+
"Error parsing %s for %s: %s",
|
| 310 |
+
DIRECT_URL_METADATA_NAME,
|
| 311 |
+
self.canonical_name,
|
| 312 |
+
e,
|
| 313 |
+
)
|
| 314 |
+
return None
|
| 315 |
+
|
| 316 |
+
@property
|
| 317 |
+
def installer(self) -> str:
|
| 318 |
+
try:
|
| 319 |
+
installer_text = self.read_text("INSTALLER")
|
| 320 |
+
except (OSError, ValueError, NoneMetadataError):
|
| 321 |
+
return "" # Fail silently if the installer file cannot be read.
|
| 322 |
+
for line in installer_text.splitlines():
|
| 323 |
+
cleaned_line = line.strip()
|
| 324 |
+
if cleaned_line:
|
| 325 |
+
return cleaned_line
|
| 326 |
+
return ""
|
| 327 |
+
|
| 328 |
+
@property
|
| 329 |
+
def requested(self) -> bool:
|
| 330 |
+
return self.is_file("REQUESTED")
|
| 331 |
+
|
| 332 |
+
@property
|
| 333 |
+
def editable(self) -> bool:
|
| 334 |
+
return bool(self.editable_project_location)
|
| 335 |
+
|
| 336 |
+
@property
|
| 337 |
+
def local(self) -> bool:
|
| 338 |
+
"""If distribution is installed in the current virtual environment.
|
| 339 |
+
|
| 340 |
+
Always True if we're not in a virtualenv.
|
| 341 |
+
"""
|
| 342 |
+
if self.installed_location is None:
|
| 343 |
+
return False
|
| 344 |
+
return is_local(self.installed_location)
|
| 345 |
+
|
| 346 |
+
@property
|
| 347 |
+
def in_usersite(self) -> bool:
|
| 348 |
+
if self.installed_location is None or user_site is None:
|
| 349 |
+
return False
|
| 350 |
+
return self.installed_location.startswith(normalize_path(user_site))
|
| 351 |
+
|
| 352 |
+
@property
|
| 353 |
+
def in_site_packages(self) -> bool:
|
| 354 |
+
if self.installed_location is None or site_packages is None:
|
| 355 |
+
return False
|
| 356 |
+
return self.installed_location.startswith(normalize_path(site_packages))
|
| 357 |
+
|
| 358 |
+
def is_file(self, path: InfoPath) -> bool:
|
| 359 |
+
"""Check whether an entry in the info directory is a file."""
|
| 360 |
+
raise NotImplementedError()
|
| 361 |
+
|
| 362 |
+
def iter_distutils_script_names(self) -> Iterator[str]:
|
| 363 |
+
"""Find distutils 'scripts' entries metadata.
|
| 364 |
+
|
| 365 |
+
If 'scripts' is supplied in ``setup.py``, distutils records those in the
|
| 366 |
+
installed distribution's ``scripts`` directory, a file for each script.
|
| 367 |
+
"""
|
| 368 |
+
raise NotImplementedError()
|
| 369 |
+
|
| 370 |
+
def read_text(self, path: InfoPath) -> str:
|
| 371 |
+
"""Read a file in the info directory.
|
| 372 |
+
|
| 373 |
+
:raise FileNotFoundError: If ``path`` does not exist in the directory.
|
| 374 |
+
:raise NoneMetadataError: If ``path`` exists in the info directory, but
|
| 375 |
+
cannot be read.
|
| 376 |
+
"""
|
| 377 |
+
raise NotImplementedError()
|
| 378 |
+
|
| 379 |
+
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
|
| 380 |
+
raise NotImplementedError()
|
| 381 |
+
|
| 382 |
+
def _metadata_impl(self) -> email.message.Message:
|
| 383 |
+
raise NotImplementedError()
|
| 384 |
+
|
| 385 |
+
@functools.cached_property
|
| 386 |
+
def metadata(self) -> email.message.Message:
|
| 387 |
+
"""Metadata of distribution parsed from e.g. METADATA or PKG-INFO.
|
| 388 |
+
|
| 389 |
+
This should return an empty message if the metadata file is unavailable.
|
| 390 |
+
|
| 391 |
+
:raises NoneMetadataError: If the metadata file is available, but does
|
| 392 |
+
not contain valid metadata.
|
| 393 |
+
"""
|
| 394 |
+
metadata = self._metadata_impl()
|
| 395 |
+
self._add_egg_info_requires(metadata)
|
| 396 |
+
return metadata
|
| 397 |
+
|
| 398 |
+
@property
|
| 399 |
+
def metadata_dict(self) -> Dict[str, Any]:
|
| 400 |
+
"""PEP 566 compliant JSON-serializable representation of METADATA or PKG-INFO.
|
| 401 |
+
|
| 402 |
+
This should return an empty dict if the metadata file is unavailable.
|
| 403 |
+
|
| 404 |
+
:raises NoneMetadataError: If the metadata file is available, but does
|
| 405 |
+
not contain valid metadata.
|
| 406 |
+
"""
|
| 407 |
+
return msg_to_json(self.metadata)
|
| 408 |
+
|
| 409 |
+
@property
|
| 410 |
+
def metadata_version(self) -> Optional[str]:
|
| 411 |
+
"""Value of "Metadata-Version:" in distribution metadata, if available."""
|
| 412 |
+
return self.metadata.get("Metadata-Version")
|
| 413 |
+
|
| 414 |
+
@property
|
| 415 |
+
def raw_name(self) -> str:
|
| 416 |
+
"""Value of "Name:" in distribution metadata."""
|
| 417 |
+
# The metadata should NEVER be missing the Name: key, but if it somehow
|
| 418 |
+
# does, fall back to the known canonical name.
|
| 419 |
+
return self.metadata.get("Name", self.canonical_name)
|
| 420 |
+
|
| 421 |
+
@property
|
| 422 |
+
def requires_python(self) -> SpecifierSet:
|
| 423 |
+
"""Value of "Requires-Python:" in distribution metadata.
|
| 424 |
+
|
| 425 |
+
If the key does not exist or contains an invalid value, an empty
|
| 426 |
+
SpecifierSet should be returned.
|
| 427 |
+
"""
|
| 428 |
+
value = self.metadata.get("Requires-Python")
|
| 429 |
+
if value is None:
|
| 430 |
+
return SpecifierSet()
|
| 431 |
+
try:
|
| 432 |
+
# Convert to str to satisfy the type checker; this can be a Header object.
|
| 433 |
+
spec = SpecifierSet(str(value))
|
| 434 |
+
except InvalidSpecifier as e:
|
| 435 |
+
message = "Package %r has an invalid Requires-Python: %s"
|
| 436 |
+
logger.warning(message, self.raw_name, e)
|
| 437 |
+
return SpecifierSet()
|
| 438 |
+
return spec
|
| 439 |
+
|
| 440 |
+
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
|
| 441 |
+
"""Dependencies of this distribution.
|
| 442 |
+
|
| 443 |
+
For modern .dist-info distributions, this is the collection of
|
| 444 |
+
"Requires-Dist:" entries in distribution metadata.
|
| 445 |
+
"""
|
| 446 |
+
raise NotImplementedError()
|
| 447 |
+
|
| 448 |
+
def iter_raw_dependencies(self) -> Iterable[str]:
|
| 449 |
+
"""Raw Requires-Dist metadata."""
|
| 450 |
+
return self.metadata.get_all("Requires-Dist", [])
|
| 451 |
+
|
| 452 |
+
def iter_provided_extras(self) -> Iterable[NormalizedName]:
|
| 453 |
+
"""Extras provided by this distribution.
|
| 454 |
+
|
| 455 |
+
For modern .dist-info distributions, this is the collection of
|
| 456 |
+
"Provides-Extra:" entries in distribution metadata.
|
| 457 |
+
|
| 458 |
+
The return value of this function is expected to be normalised names,
|
| 459 |
+
per PEP 685, with the returned value being handled appropriately by
|
| 460 |
+
`iter_dependencies`.
|
| 461 |
+
"""
|
| 462 |
+
raise NotImplementedError()
|
| 463 |
+
|
| 464 |
+
def _iter_declared_entries_from_record(self) -> Optional[Iterator[str]]:
|
| 465 |
+
try:
|
| 466 |
+
text = self.read_text("RECORD")
|
| 467 |
+
except FileNotFoundError:
|
| 468 |
+
return None
|
| 469 |
+
# This extra Path-str cast normalizes entries.
|
| 470 |
+
return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
|
| 471 |
+
|
| 472 |
+
def _iter_declared_entries_from_legacy(self) -> Optional[Iterator[str]]:
|
| 473 |
+
try:
|
| 474 |
+
text = self.read_text("installed-files.txt")
|
| 475 |
+
except FileNotFoundError:
|
| 476 |
+
return None
|
| 477 |
+
paths = (p for p in text.splitlines(keepends=False) if p)
|
| 478 |
+
root = self.location
|
| 479 |
+
info = self.info_location
|
| 480 |
+
if root is None or info is None:
|
| 481 |
+
return paths
|
| 482 |
+
try:
|
| 483 |
+
info_rel = pathlib.Path(info).relative_to(root)
|
| 484 |
+
except ValueError: # info is not relative to root.
|
| 485 |
+
return paths
|
| 486 |
+
if not info_rel.parts: # info *is* root.
|
| 487 |
+
return paths
|
| 488 |
+
return (
|
| 489 |
+
_convert_installed_files_path(pathlib.Path(p).parts, info_rel.parts)
|
| 490 |
+
for p in paths
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
def iter_declared_entries(self) -> Optional[Iterator[str]]:
|
| 494 |
+
"""Iterate through file entries declared in this distribution.
|
| 495 |
+
|
| 496 |
+
For modern .dist-info distributions, this is the files listed in the
|
| 497 |
+
``RECORD`` metadata file. For legacy setuptools distributions, this
|
| 498 |
+
comes from ``installed-files.txt``, with entries normalized to be
|
| 499 |
+
compatible with the format used by ``RECORD``.
|
| 500 |
+
|
| 501 |
+
:return: An iterator for listed entries, or None if the distribution
|
| 502 |
+
contains neither ``RECORD`` nor ``installed-files.txt``.
|
| 503 |
+
"""
|
| 504 |
+
return (
|
| 505 |
+
self._iter_declared_entries_from_record()
|
| 506 |
+
or self._iter_declared_entries_from_legacy()
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
def _iter_requires_txt_entries(self) -> Iterator[RequiresEntry]:
|
| 510 |
+
"""Parse a ``requires.txt`` in an egg-info directory.
|
| 511 |
+
|
| 512 |
+
This is an INI-ish format where an egg-info stores dependencies. A
|
| 513 |
+
section name describes extra other environment markers, while each entry
|
| 514 |
+
is an arbitrary string (not a key-value pair) representing a dependency
|
| 515 |
+
as a requirement string (no markers).
|
| 516 |
+
|
| 517 |
+
There is a construct in ``importlib.metadata`` called ``Sectioned`` that
|
| 518 |
+
does mostly the same, but the format is currently considered private.
|
| 519 |
+
"""
|
| 520 |
+
try:
|
| 521 |
+
content = self.read_text("requires.txt")
|
| 522 |
+
except FileNotFoundError:
|
| 523 |
+
return
|
| 524 |
+
extra = marker = "" # Section-less entries don't have markers.
|
| 525 |
+
for line in content.splitlines():
|
| 526 |
+
line = line.strip()
|
| 527 |
+
if not line or line.startswith("#"): # Comment; ignored.
|
| 528 |
+
continue
|
| 529 |
+
if line.startswith("[") and line.endswith("]"): # A section header.
|
| 530 |
+
extra, _, marker = line.strip("[]").partition(":")
|
| 531 |
+
continue
|
| 532 |
+
yield RequiresEntry(requirement=line, extra=extra, marker=marker)
|
| 533 |
+
|
| 534 |
+
def _iter_egg_info_extras(self) -> Iterable[str]:
|
| 535 |
+
"""Get extras from the egg-info directory."""
|
| 536 |
+
known_extras = {""}
|
| 537 |
+
for entry in self._iter_requires_txt_entries():
|
| 538 |
+
extra = canonicalize_name(entry.extra)
|
| 539 |
+
if extra in known_extras:
|
| 540 |
+
continue
|
| 541 |
+
known_extras.add(extra)
|
| 542 |
+
yield extra
|
| 543 |
+
|
| 544 |
+
def _iter_egg_info_dependencies(self) -> Iterable[str]:
|
| 545 |
+
"""Get distribution dependencies from the egg-info directory.
|
| 546 |
+
|
| 547 |
+
To ease parsing, this converts a legacy dependency entry into a PEP 508
|
| 548 |
+
requirement string. Like ``_iter_requires_txt_entries()``, there is code
|
| 549 |
+
in ``importlib.metadata`` that does mostly the same, but not do exactly
|
| 550 |
+
what we need.
|
| 551 |
+
|
| 552 |
+
Namely, ``importlib.metadata`` does not normalize the extra name before
|
| 553 |
+
putting it into the requirement string, which causes marker comparison
|
| 554 |
+
to fail because the dist-info format do normalize. This is consistent in
|
| 555 |
+
all currently available PEP 517 backends, although not standardized.
|
| 556 |
+
"""
|
| 557 |
+
for entry in self._iter_requires_txt_entries():
|
| 558 |
+
extra = canonicalize_name(entry.extra)
|
| 559 |
+
if extra and entry.marker:
|
| 560 |
+
marker = f'({entry.marker}) and extra == "{extra}"'
|
| 561 |
+
elif extra:
|
| 562 |
+
marker = f'extra == "{extra}"'
|
| 563 |
+
elif entry.marker:
|
| 564 |
+
marker = entry.marker
|
| 565 |
+
else:
|
| 566 |
+
marker = ""
|
| 567 |
+
if marker:
|
| 568 |
+
yield f"{entry.requirement} ; {marker}"
|
| 569 |
+
else:
|
| 570 |
+
yield entry.requirement
|
| 571 |
+
|
| 572 |
+
def _add_egg_info_requires(self, metadata: email.message.Message) -> None:
|
| 573 |
+
"""Add egg-info requires.txt information to the metadata."""
|
| 574 |
+
if not metadata.get_all("Requires-Dist"):
|
| 575 |
+
for dep in self._iter_egg_info_dependencies():
|
| 576 |
+
metadata["Requires-Dist"] = dep
|
| 577 |
+
if not metadata.get_all("Provides-Extra"):
|
| 578 |
+
for extra in self._iter_egg_info_extras():
|
| 579 |
+
metadata["Provides-Extra"] = extra
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
class BaseEnvironment:
|
| 583 |
+
"""An environment containing distributions to introspect."""
|
| 584 |
+
|
| 585 |
+
@classmethod
|
| 586 |
+
def default(cls) -> "BaseEnvironment":
|
| 587 |
+
raise NotImplementedError()
|
| 588 |
+
|
| 589 |
+
@classmethod
|
| 590 |
+
def from_paths(cls, paths: Optional[List[str]]) -> "BaseEnvironment":
|
| 591 |
+
raise NotImplementedError()
|
| 592 |
+
|
| 593 |
+
def get_distribution(self, name: str) -> Optional["BaseDistribution"]:
|
| 594 |
+
"""Given a requirement name, return the installed distributions.
|
| 595 |
+
|
| 596 |
+
The name may not be normalized. The implementation must canonicalize
|
| 597 |
+
it for lookup.
|
| 598 |
+
"""
|
| 599 |
+
raise NotImplementedError()
|
| 600 |
+
|
| 601 |
+
def _iter_distributions(self) -> Iterator["BaseDistribution"]:
|
| 602 |
+
"""Iterate through installed distributions.
|
| 603 |
+
|
| 604 |
+
This function should be implemented by subclass, but never called
|
| 605 |
+
directly. Use the public ``iter_distribution()`` instead, which
|
| 606 |
+
implements additional logic to make sure the distributions are valid.
|
| 607 |
+
"""
|
| 608 |
+
raise NotImplementedError()
|
| 609 |
+
|
| 610 |
+
def iter_all_distributions(self) -> Iterator[BaseDistribution]:
|
| 611 |
+
"""Iterate through all installed distributions without any filtering."""
|
| 612 |
+
for dist in self._iter_distributions():
|
| 613 |
+
# Make sure the distribution actually comes from a valid Python
|
| 614 |
+
# packaging distribution. Pip's AdjacentTempDirectory leaves folders
|
| 615 |
+
# e.g. ``~atplotlib.dist-info`` if cleanup was interrupted. The
|
| 616 |
+
# valid project name pattern is taken from PEP 508.
|
| 617 |
+
project_name_valid = re.match(
|
| 618 |
+
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$",
|
| 619 |
+
dist.canonical_name,
|
| 620 |
+
flags=re.IGNORECASE,
|
| 621 |
+
)
|
| 622 |
+
if not project_name_valid:
|
| 623 |
+
logger.warning(
|
| 624 |
+
"Ignoring invalid distribution %s (%s)",
|
| 625 |
+
dist.canonical_name,
|
| 626 |
+
dist.location,
|
| 627 |
+
)
|
| 628 |
+
continue
|
| 629 |
+
yield dist
|
| 630 |
+
|
| 631 |
+
def iter_installed_distributions(
|
| 632 |
+
self,
|
| 633 |
+
local_only: bool = True,
|
| 634 |
+
skip: Container[str] = stdlib_pkgs,
|
| 635 |
+
include_editables: bool = True,
|
| 636 |
+
editables_only: bool = False,
|
| 637 |
+
user_only: bool = False,
|
| 638 |
+
) -> Iterator[BaseDistribution]:
|
| 639 |
+
"""Return a list of installed distributions.
|
| 640 |
+
|
| 641 |
+
This is based on ``iter_all_distributions()`` with additional filtering
|
| 642 |
+
options. Note that ``iter_installed_distributions()`` without arguments
|
| 643 |
+
is *not* equal to ``iter_all_distributions()``, since some of the
|
| 644 |
+
configurations exclude packages by default.
|
| 645 |
+
|
| 646 |
+
:param local_only: If True (default), only return installations
|
| 647 |
+
local to the current virtualenv, if in a virtualenv.
|
| 648 |
+
:param skip: An iterable of canonicalized project names to ignore;
|
| 649 |
+
defaults to ``stdlib_pkgs``.
|
| 650 |
+
:param include_editables: If False, don't report editables.
|
| 651 |
+
:param editables_only: If True, only report editables.
|
| 652 |
+
:param user_only: If True, only report installations in the user
|
| 653 |
+
site directory.
|
| 654 |
+
"""
|
| 655 |
+
it = self.iter_all_distributions()
|
| 656 |
+
if local_only:
|
| 657 |
+
it = (d for d in it if d.local)
|
| 658 |
+
if not include_editables:
|
| 659 |
+
it = (d for d in it if not d.editable)
|
| 660 |
+
if editables_only:
|
| 661 |
+
it = (d for d in it if d.editable)
|
| 662 |
+
if user_only:
|
| 663 |
+
it = (d for d in it if d.in_usersite)
|
| 664 |
+
return (d for d in it if d.canonical_name not in skip)
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
class Wheel(Protocol):
|
| 668 |
+
location: str
|
| 669 |
+
|
| 670 |
+
def as_zipfile(self) -> zipfile.ZipFile:
|
| 671 |
+
raise NotImplementedError()
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
class FilesystemWheel(Wheel):
|
| 675 |
+
def __init__(self, location: str) -> None:
|
| 676 |
+
self.location = location
|
| 677 |
+
|
| 678 |
+
def as_zipfile(self) -> zipfile.ZipFile:
|
| 679 |
+
return zipfile.ZipFile(self.location, allowZip64=True)
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
class MemoryWheel(Wheel):
|
| 683 |
+
def __init__(self, location: str, stream: IO[bytes]) -> None:
|
| 684 |
+
self.location = location
|
| 685 |
+
self.stream = stream
|
| 686 |
+
|
| 687 |
+
def as_zipfile(self) -> zipfile.ZipFile:
|
| 688 |
+
return zipfile.ZipFile(self.stream, allowZip64=True)
|
lib/python3.12/site-packages/pip/_internal/metadata/importlib/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ._dists import Distribution
|
| 2 |
+
from ._envs import Environment
|
| 3 |
+
|
| 4 |
+
__all__ = ["NAME", "Distribution", "Environment"]
|
| 5 |
+
|
| 6 |
+
NAME = "importlib"
|
lib/python3.12/site-packages/pip/_internal/metadata/importlib/__pycache__/_compat.cpython-312.pyc
ADDED
|
Binary file (4.5 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/metadata/importlib/__pycache__/_dists.cpython-312.pyc
ADDED
|
Binary file (12.9 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/metadata/importlib/__pycache__/_envs.cpython-312.pyc
ADDED
|
Binary file (11.1 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/metadata/importlib/_compat.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.metadata
|
| 2 |
+
import os
|
| 3 |
+
from typing import Any, Optional, Protocol, Tuple, cast
|
| 4 |
+
|
| 5 |
+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class BadMetadata(ValueError):
|
| 9 |
+
def __init__(self, dist: importlib.metadata.Distribution, *, reason: str) -> None:
|
| 10 |
+
self.dist = dist
|
| 11 |
+
self.reason = reason
|
| 12 |
+
|
| 13 |
+
def __str__(self) -> str:
|
| 14 |
+
return f"Bad metadata in {self.dist} ({self.reason})"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class BasePath(Protocol):
|
| 18 |
+
"""A protocol that various path objects conform.
|
| 19 |
+
|
| 20 |
+
This exists because importlib.metadata uses both ``pathlib.Path`` and
|
| 21 |
+
``zipfile.Path``, and we need a common base for type hints (Union does not
|
| 22 |
+
work well since ``zipfile.Path`` is too new for our linter setup).
|
| 23 |
+
|
| 24 |
+
This does not mean to be exhaustive, but only contains things that present
|
| 25 |
+
in both classes *that we need*.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def name(self) -> str:
|
| 30 |
+
raise NotImplementedError()
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def parent(self) -> "BasePath":
|
| 34 |
+
raise NotImplementedError()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def get_info_location(d: importlib.metadata.Distribution) -> Optional[BasePath]:
|
| 38 |
+
"""Find the path to the distribution's metadata directory.
|
| 39 |
+
|
| 40 |
+
HACK: This relies on importlib.metadata's private ``_path`` attribute. Not
|
| 41 |
+
all distributions exist on disk, so importlib.metadata is correct to not
|
| 42 |
+
expose the attribute as public. But pip's code base is old and not as clean,
|
| 43 |
+
so we do this to avoid having to rewrite too many things. Hopefully we can
|
| 44 |
+
eliminate this some day.
|
| 45 |
+
"""
|
| 46 |
+
return getattr(d, "_path", None)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def parse_name_and_version_from_info_directory(
|
| 50 |
+
dist: importlib.metadata.Distribution,
|
| 51 |
+
) -> Tuple[Optional[str], Optional[str]]:
|
| 52 |
+
"""Get a name and version from the metadata directory name.
|
| 53 |
+
|
| 54 |
+
This is much faster than reading distribution metadata.
|
| 55 |
+
"""
|
| 56 |
+
info_location = get_info_location(dist)
|
| 57 |
+
if info_location is None:
|
| 58 |
+
return None, None
|
| 59 |
+
|
| 60 |
+
stem, suffix = os.path.splitext(info_location.name)
|
| 61 |
+
if suffix == ".dist-info":
|
| 62 |
+
name, sep, version = stem.partition("-")
|
| 63 |
+
if sep:
|
| 64 |
+
return name, version
|
| 65 |
+
|
| 66 |
+
if suffix == ".egg-info":
|
| 67 |
+
name = stem.split("-", 1)[0]
|
| 68 |
+
return name, None
|
| 69 |
+
|
| 70 |
+
return None, None
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def get_dist_canonical_name(dist: importlib.metadata.Distribution) -> NormalizedName:
|
| 74 |
+
"""Get the distribution's normalized name.
|
| 75 |
+
|
| 76 |
+
The ``name`` attribute is only available in Python 3.10 or later. We are
|
| 77 |
+
targeting exactly that, but Mypy does not know this.
|
| 78 |
+
"""
|
| 79 |
+
if name := parse_name_and_version_from_info_directory(dist)[0]:
|
| 80 |
+
return canonicalize_name(name)
|
| 81 |
+
|
| 82 |
+
name = cast(Any, dist).name
|
| 83 |
+
if not isinstance(name, str):
|
| 84 |
+
raise BadMetadata(dist, reason="invalid metadata entry 'name'")
|
| 85 |
+
return canonicalize_name(name)
|
lib/python3.12/site-packages/pip/_internal/metadata/importlib/_dists.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import email.message
|
| 2 |
+
import importlib.metadata
|
| 3 |
+
import pathlib
|
| 4 |
+
import zipfile
|
| 5 |
+
from os import PathLike
|
| 6 |
+
from typing import (
|
| 7 |
+
Collection,
|
| 8 |
+
Dict,
|
| 9 |
+
Iterable,
|
| 10 |
+
Iterator,
|
| 11 |
+
Mapping,
|
| 12 |
+
Optional,
|
| 13 |
+
Sequence,
|
| 14 |
+
Union,
|
| 15 |
+
cast,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
from pip._vendor.packaging.requirements import Requirement
|
| 19 |
+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
| 20 |
+
from pip._vendor.packaging.version import Version
|
| 21 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 22 |
+
|
| 23 |
+
from pip._internal.exceptions import InvalidWheel, UnsupportedWheel
|
| 24 |
+
from pip._internal.metadata.base import (
|
| 25 |
+
BaseDistribution,
|
| 26 |
+
BaseEntryPoint,
|
| 27 |
+
InfoPath,
|
| 28 |
+
Wheel,
|
| 29 |
+
)
|
| 30 |
+
from pip._internal.utils.misc import normalize_path
|
| 31 |
+
from pip._internal.utils.packaging import get_requirement
|
| 32 |
+
from pip._internal.utils.temp_dir import TempDirectory
|
| 33 |
+
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
|
| 34 |
+
|
| 35 |
+
from ._compat import (
|
| 36 |
+
BasePath,
|
| 37 |
+
get_dist_canonical_name,
|
| 38 |
+
parse_name_and_version_from_info_directory,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class WheelDistribution(importlib.metadata.Distribution):
|
| 43 |
+
"""An ``importlib.metadata.Distribution`` read from a wheel.
|
| 44 |
+
|
| 45 |
+
Although ``importlib.metadata.PathDistribution`` accepts ``zipfile.Path``,
|
| 46 |
+
its implementation is too "lazy" for pip's needs (we can't keep the ZipFile
|
| 47 |
+
handle open for the entire lifetime of the distribution object).
|
| 48 |
+
|
| 49 |
+
This implementation eagerly reads the entire metadata directory into the
|
| 50 |
+
memory instead, and operates from that.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
files: Mapping[pathlib.PurePosixPath, bytes],
|
| 56 |
+
info_location: pathlib.PurePosixPath,
|
| 57 |
+
) -> None:
|
| 58 |
+
self._files = files
|
| 59 |
+
self.info_location = info_location
|
| 60 |
+
|
| 61 |
+
@classmethod
|
| 62 |
+
def from_zipfile(
|
| 63 |
+
cls,
|
| 64 |
+
zf: zipfile.ZipFile,
|
| 65 |
+
name: str,
|
| 66 |
+
location: str,
|
| 67 |
+
) -> "WheelDistribution":
|
| 68 |
+
info_dir, _ = parse_wheel(zf, name)
|
| 69 |
+
paths = (
|
| 70 |
+
(name, pathlib.PurePosixPath(name.split("/", 1)[-1]))
|
| 71 |
+
for name in zf.namelist()
|
| 72 |
+
if name.startswith(f"{info_dir}/")
|
| 73 |
+
)
|
| 74 |
+
files = {
|
| 75 |
+
relpath: read_wheel_metadata_file(zf, fullpath)
|
| 76 |
+
for fullpath, relpath in paths
|
| 77 |
+
}
|
| 78 |
+
info_location = pathlib.PurePosixPath(location, info_dir)
|
| 79 |
+
return cls(files, info_location)
|
| 80 |
+
|
| 81 |
+
def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
|
| 82 |
+
# Only allow iterating through the metadata directory.
|
| 83 |
+
if pathlib.PurePosixPath(str(path)) in self._files:
|
| 84 |
+
return iter(self._files)
|
| 85 |
+
raise FileNotFoundError(path)
|
| 86 |
+
|
| 87 |
+
def read_text(self, filename: str) -> Optional[str]:
|
| 88 |
+
try:
|
| 89 |
+
data = self._files[pathlib.PurePosixPath(filename)]
|
| 90 |
+
except KeyError:
|
| 91 |
+
return None
|
| 92 |
+
try:
|
| 93 |
+
text = data.decode("utf-8")
|
| 94 |
+
except UnicodeDecodeError as e:
|
| 95 |
+
wheel = self.info_location.parent
|
| 96 |
+
error = f"Error decoding metadata for {wheel}: {e} in {filename} file"
|
| 97 |
+
raise UnsupportedWheel(error)
|
| 98 |
+
return text
|
| 99 |
+
|
| 100 |
+
def locate_file(self, path: Union[str, "PathLike[str]"]) -> pathlib.Path:
|
| 101 |
+
# This method doesn't make sense for our in-memory wheel, but the API
|
| 102 |
+
# requires us to define it.
|
| 103 |
+
raise NotImplementedError
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class Distribution(BaseDistribution):
|
| 107 |
+
def __init__(
|
| 108 |
+
self,
|
| 109 |
+
dist: importlib.metadata.Distribution,
|
| 110 |
+
info_location: Optional[BasePath],
|
| 111 |
+
installed_location: Optional[BasePath],
|
| 112 |
+
) -> None:
|
| 113 |
+
self._dist = dist
|
| 114 |
+
self._info_location = info_location
|
| 115 |
+
self._installed_location = installed_location
|
| 116 |
+
|
| 117 |
+
@classmethod
|
| 118 |
+
def from_directory(cls, directory: str) -> BaseDistribution:
|
| 119 |
+
info_location = pathlib.Path(directory)
|
| 120 |
+
dist = importlib.metadata.Distribution.at(info_location)
|
| 121 |
+
return cls(dist, info_location, info_location.parent)
|
| 122 |
+
|
| 123 |
+
@classmethod
|
| 124 |
+
def from_metadata_file_contents(
|
| 125 |
+
cls,
|
| 126 |
+
metadata_contents: bytes,
|
| 127 |
+
filename: str,
|
| 128 |
+
project_name: str,
|
| 129 |
+
) -> BaseDistribution:
|
| 130 |
+
# Generate temp dir to contain the metadata file, and write the file contents.
|
| 131 |
+
temp_dir = pathlib.Path(
|
| 132 |
+
TempDirectory(kind="metadata", globally_managed=True).path
|
| 133 |
+
)
|
| 134 |
+
metadata_path = temp_dir / "METADATA"
|
| 135 |
+
metadata_path.write_bytes(metadata_contents)
|
| 136 |
+
# Construct dist pointing to the newly created directory.
|
| 137 |
+
dist = importlib.metadata.Distribution.at(metadata_path.parent)
|
| 138 |
+
return cls(dist, metadata_path.parent, None)
|
| 139 |
+
|
| 140 |
+
@classmethod
|
| 141 |
+
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
|
| 142 |
+
try:
|
| 143 |
+
with wheel.as_zipfile() as zf:
|
| 144 |
+
dist = WheelDistribution.from_zipfile(zf, name, wheel.location)
|
| 145 |
+
except zipfile.BadZipFile as e:
|
| 146 |
+
raise InvalidWheel(wheel.location, name) from e
|
| 147 |
+
return cls(dist, dist.info_location, pathlib.PurePosixPath(wheel.location))
|
| 148 |
+
|
| 149 |
+
@property
|
| 150 |
+
def location(self) -> Optional[str]:
|
| 151 |
+
if self._info_location is None:
|
| 152 |
+
return None
|
| 153 |
+
return str(self._info_location.parent)
|
| 154 |
+
|
| 155 |
+
@property
|
| 156 |
+
def info_location(self) -> Optional[str]:
|
| 157 |
+
if self._info_location is None:
|
| 158 |
+
return None
|
| 159 |
+
return str(self._info_location)
|
| 160 |
+
|
| 161 |
+
@property
|
| 162 |
+
def installed_location(self) -> Optional[str]:
|
| 163 |
+
if self._installed_location is None:
|
| 164 |
+
return None
|
| 165 |
+
return normalize_path(str(self._installed_location))
|
| 166 |
+
|
| 167 |
+
@property
|
| 168 |
+
def canonical_name(self) -> NormalizedName:
|
| 169 |
+
return get_dist_canonical_name(self._dist)
|
| 170 |
+
|
| 171 |
+
@property
|
| 172 |
+
def version(self) -> Version:
|
| 173 |
+
if version := parse_name_and_version_from_info_directory(self._dist)[1]:
|
| 174 |
+
return parse_version(version)
|
| 175 |
+
return parse_version(self._dist.version)
|
| 176 |
+
|
| 177 |
+
@property
|
| 178 |
+
def raw_version(self) -> str:
|
| 179 |
+
return self._dist.version
|
| 180 |
+
|
| 181 |
+
def is_file(self, path: InfoPath) -> bool:
|
| 182 |
+
return self._dist.read_text(str(path)) is not None
|
| 183 |
+
|
| 184 |
+
def iter_distutils_script_names(self) -> Iterator[str]:
|
| 185 |
+
# A distutils installation is always "flat" (not in e.g. egg form), so
|
| 186 |
+
# if this distribution's info location is NOT a pathlib.Path (but e.g.
|
| 187 |
+
# zipfile.Path), it can never contain any distutils scripts.
|
| 188 |
+
if not isinstance(self._info_location, pathlib.Path):
|
| 189 |
+
return
|
| 190 |
+
for child in self._info_location.joinpath("scripts").iterdir():
|
| 191 |
+
yield child.name
|
| 192 |
+
|
| 193 |
+
def read_text(self, path: InfoPath) -> str:
|
| 194 |
+
content = self._dist.read_text(str(path))
|
| 195 |
+
if content is None:
|
| 196 |
+
raise FileNotFoundError(path)
|
| 197 |
+
return content
|
| 198 |
+
|
| 199 |
+
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
|
| 200 |
+
# importlib.metadata's EntryPoint structure satisfies BaseEntryPoint.
|
| 201 |
+
return self._dist.entry_points
|
| 202 |
+
|
| 203 |
+
def _metadata_impl(self) -> email.message.Message:
|
| 204 |
+
# From Python 3.10+, importlib.metadata declares PackageMetadata as the
|
| 205 |
+
# return type. This protocol is unfortunately a disaster now and misses
|
| 206 |
+
# a ton of fields that we need, including get() and get_payload(). We
|
| 207 |
+
# rely on the implementation that the object is actually a Message now,
|
| 208 |
+
# until upstream can improve the protocol. (python/cpython#94952)
|
| 209 |
+
return cast(email.message.Message, self._dist.metadata)
|
| 210 |
+
|
| 211 |
+
def iter_provided_extras(self) -> Iterable[NormalizedName]:
|
| 212 |
+
return [
|
| 213 |
+
canonicalize_name(extra)
|
| 214 |
+
for extra in self.metadata.get_all("Provides-Extra", [])
|
| 215 |
+
]
|
| 216 |
+
|
| 217 |
+
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
|
| 218 |
+
contexts: Sequence[Dict[str, str]] = [{"extra": e} for e in extras]
|
| 219 |
+
for req_string in self.metadata.get_all("Requires-Dist", []):
|
| 220 |
+
# strip() because email.message.Message.get_all() may return a leading \n
|
| 221 |
+
# in case a long header was wrapped.
|
| 222 |
+
req = get_requirement(req_string.strip())
|
| 223 |
+
if not req.marker:
|
| 224 |
+
yield req
|
| 225 |
+
elif not extras and req.marker.evaluate({"extra": ""}):
|
| 226 |
+
yield req
|
| 227 |
+
elif any(req.marker.evaluate(context) for context in contexts):
|
| 228 |
+
yield req
|
lib/python3.12/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_canonical_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 |
+
name = get_dist_canonical_name(dist)
|
| 65 |
+
except BadMetadata as e:
|
| 66 |
+
logger.warning("Skipping %s due to %s", info_location, e.reason)
|
| 67 |
+
continue
|
| 68 |
+
if name in self._found_names:
|
| 69 |
+
continue
|
| 70 |
+
self._found_names.add(name)
|
| 71 |
+
yield dist, info_location
|
| 72 |
+
|
| 73 |
+
def find(self, location: str) -> Iterator[BaseDistribution]:
|
| 74 |
+
"""Find distributions in a location.
|
| 75 |
+
|
| 76 |
+
The path can be either a directory, or a ZIP archive.
|
| 77 |
+
"""
|
| 78 |
+
for dist, info_location in self._find_impl(location):
|
| 79 |
+
if info_location is None:
|
| 80 |
+
installed_location: Optional[BasePath] = None
|
| 81 |
+
else:
|
| 82 |
+
installed_location = info_location.parent
|
| 83 |
+
yield Distribution(dist, info_location, installed_location)
|
| 84 |
+
|
| 85 |
+
def find_linked(self, location: str) -> Iterator[BaseDistribution]:
|
| 86 |
+
"""Read location in egg-link files and return distributions in there.
|
| 87 |
+
|
| 88 |
+
The path should be a directory; otherwise this returns nothing. This
|
| 89 |
+
follows how setuptools does this for compatibility. The first non-empty
|
| 90 |
+
line in the egg-link is read as a path (resolved against the egg-link's
|
| 91 |
+
containing directory if relative). Distributions found at that linked
|
| 92 |
+
location are returned.
|
| 93 |
+
"""
|
| 94 |
+
path = pathlib.Path(location)
|
| 95 |
+
if not path.is_dir():
|
| 96 |
+
return
|
| 97 |
+
for child in path.iterdir():
|
| 98 |
+
if child.suffix != ".egg-link":
|
| 99 |
+
continue
|
| 100 |
+
with child.open() as f:
|
| 101 |
+
lines = (line.strip() for line in f)
|
| 102 |
+
target_rel = next((line for line in lines if line), "")
|
| 103 |
+
if not target_rel:
|
| 104 |
+
continue
|
| 105 |
+
target_location = str(path.joinpath(target_rel))
|
| 106 |
+
for dist, info_location in self._find_impl(target_location):
|
| 107 |
+
yield Distribution(dist, info_location, path)
|
| 108 |
+
|
| 109 |
+
def _find_eggs_in_dir(self, location: str) -> Iterator[BaseDistribution]:
|
| 110 |
+
from pip._vendor.pkg_resources import find_distributions
|
| 111 |
+
|
| 112 |
+
from pip._internal.metadata import pkg_resources as legacy
|
| 113 |
+
|
| 114 |
+
with os.scandir(location) as it:
|
| 115 |
+
for entry in it:
|
| 116 |
+
if not entry.name.endswith(".egg"):
|
| 117 |
+
continue
|
| 118 |
+
for dist in find_distributions(entry.path):
|
| 119 |
+
yield legacy.Distribution(dist)
|
| 120 |
+
|
| 121 |
+
def _find_eggs_in_zip(self, location: str) -> Iterator[BaseDistribution]:
|
| 122 |
+
from pip._vendor.pkg_resources import find_eggs_in_zip
|
| 123 |
+
|
| 124 |
+
from pip._internal.metadata import pkg_resources as legacy
|
| 125 |
+
|
| 126 |
+
try:
|
| 127 |
+
importer = zipimport.zipimporter(location)
|
| 128 |
+
except zipimport.ZipImportError:
|
| 129 |
+
return
|
| 130 |
+
for dist in find_eggs_in_zip(importer, location):
|
| 131 |
+
yield legacy.Distribution(dist)
|
| 132 |
+
|
| 133 |
+
def find_eggs(self, location: str) -> Iterator[BaseDistribution]:
|
| 134 |
+
"""Find eggs in a location.
|
| 135 |
+
|
| 136 |
+
This actually uses the old *pkg_resources* backend. We likely want to
|
| 137 |
+
deprecate this so we can eventually remove the *pkg_resources*
|
| 138 |
+
dependency entirely. Before that, this should first emit a deprecation
|
| 139 |
+
warning for some versions when using the fallback since importing
|
| 140 |
+
*pkg_resources* is slow for those who don't need it.
|
| 141 |
+
"""
|
| 142 |
+
if os.path.isdir(location):
|
| 143 |
+
yield from self._find_eggs_in_dir(location)
|
| 144 |
+
if zipfile.is_zipfile(location):
|
| 145 |
+
yield from self._find_eggs_in_zip(location)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@functools.lru_cache(maxsize=None) # Warn a distribution exactly once.
|
| 149 |
+
def _emit_egg_deprecation(location: Optional[str]) -> None:
|
| 150 |
+
deprecated(
|
| 151 |
+
reason=f"Loading egg at {location} is deprecated.",
|
| 152 |
+
replacement="to use pip for package installation",
|
| 153 |
+
gone_in="25.1",
|
| 154 |
+
issue=12330,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class Environment(BaseEnvironment):
|
| 159 |
+
def __init__(self, paths: Sequence[str]) -> None:
|
| 160 |
+
self._paths = paths
|
| 161 |
+
|
| 162 |
+
@classmethod
|
| 163 |
+
def default(cls) -> BaseEnvironment:
|
| 164 |
+
return cls(sys.path)
|
| 165 |
+
|
| 166 |
+
@classmethod
|
| 167 |
+
def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
|
| 168 |
+
if paths is None:
|
| 169 |
+
return cls(sys.path)
|
| 170 |
+
return cls(paths)
|
| 171 |
+
|
| 172 |
+
def _iter_distributions(self) -> Iterator[BaseDistribution]:
|
| 173 |
+
finder = _DistributionFinder()
|
| 174 |
+
for location in self._paths:
|
| 175 |
+
yield from finder.find(location)
|
| 176 |
+
for dist in finder.find_eggs(location):
|
| 177 |
+
_emit_egg_deprecation(dist.location)
|
| 178 |
+
yield dist
|
| 179 |
+
# This must go last because that's how pkg_resources tie-breaks.
|
| 180 |
+
yield from finder.find_linked(location)
|
| 181 |
+
|
| 182 |
+
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
|
| 183 |
+
canonical_name = canonicalize_name(name)
|
| 184 |
+
matches = (
|
| 185 |
+
distribution
|
| 186 |
+
for distribution in self.iter_all_distributions()
|
| 187 |
+
if distribution.canonical_name == canonical_name
|
| 188 |
+
)
|
| 189 |
+
return next(matches, None)
|
lib/python3.12/site-packages/pip/_internal/metadata/pkg_resources.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import email.message
|
| 2 |
+
import email.parser
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import zipfile
|
| 6 |
+
from typing import (
|
| 7 |
+
Collection,
|
| 8 |
+
Iterable,
|
| 9 |
+
Iterator,
|
| 10 |
+
List,
|
| 11 |
+
Mapping,
|
| 12 |
+
NamedTuple,
|
| 13 |
+
Optional,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
from pip._vendor import pkg_resources
|
| 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 Version
|
| 20 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 21 |
+
|
| 22 |
+
from pip._internal.exceptions import InvalidWheel, NoneMetadataError, UnsupportedWheel
|
| 23 |
+
from pip._internal.utils.egg_link import egg_link_path_from_location
|
| 24 |
+
from pip._internal.utils.misc import display_path, normalize_path
|
| 25 |
+
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
|
| 26 |
+
|
| 27 |
+
from .base import (
|
| 28 |
+
BaseDistribution,
|
| 29 |
+
BaseEntryPoint,
|
| 30 |
+
BaseEnvironment,
|
| 31 |
+
InfoPath,
|
| 32 |
+
Wheel,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
__all__ = ["NAME", "Distribution", "Environment"]
|
| 36 |
+
|
| 37 |
+
logger = logging.getLogger(__name__)
|
| 38 |
+
|
| 39 |
+
NAME = "pkg_resources"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class EntryPoint(NamedTuple):
|
| 43 |
+
name: str
|
| 44 |
+
value: str
|
| 45 |
+
group: str
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class InMemoryMetadata:
|
| 49 |
+
"""IMetadataProvider that reads metadata files from a dictionary.
|
| 50 |
+
|
| 51 |
+
This also maps metadata decoding exceptions to our internal exception type.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(self, metadata: Mapping[str, bytes], wheel_name: str) -> None:
|
| 55 |
+
self._metadata = metadata
|
| 56 |
+
self._wheel_name = wheel_name
|
| 57 |
+
|
| 58 |
+
def has_metadata(self, name: str) -> bool:
|
| 59 |
+
return name in self._metadata
|
| 60 |
+
|
| 61 |
+
def get_metadata(self, name: str) -> str:
|
| 62 |
+
try:
|
| 63 |
+
return self._metadata[name].decode()
|
| 64 |
+
except UnicodeDecodeError as e:
|
| 65 |
+
# Augment the default error with the origin of the file.
|
| 66 |
+
raise UnsupportedWheel(
|
| 67 |
+
f"Error decoding metadata for {self._wheel_name}: {e} in {name} file"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def get_metadata_lines(self, name: str) -> Iterable[str]:
|
| 71 |
+
return pkg_resources.yield_lines(self.get_metadata(name))
|
| 72 |
+
|
| 73 |
+
def metadata_isdir(self, name: str) -> bool:
|
| 74 |
+
return False
|
| 75 |
+
|
| 76 |
+
def metadata_listdir(self, name: str) -> List[str]:
|
| 77 |
+
return []
|
| 78 |
+
|
| 79 |
+
def run_script(self, script_name: str, namespace: str) -> None:
|
| 80 |
+
pass
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class Distribution(BaseDistribution):
|
| 84 |
+
def __init__(self, dist: pkg_resources.Distribution) -> None:
|
| 85 |
+
self._dist = dist
|
| 86 |
+
# This is populated lazily, to avoid loading metadata for all possible
|
| 87 |
+
# distributions eagerly.
|
| 88 |
+
self.__extra_mapping: Optional[Mapping[NormalizedName, str]] = None
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def _extra_mapping(self) -> Mapping[NormalizedName, str]:
|
| 92 |
+
if self.__extra_mapping is None:
|
| 93 |
+
self.__extra_mapping = {
|
| 94 |
+
canonicalize_name(extra): extra for extra in self._dist.extras
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
return self.__extra_mapping
|
| 98 |
+
|
| 99 |
+
@classmethod
|
| 100 |
+
def from_directory(cls, directory: str) -> BaseDistribution:
|
| 101 |
+
dist_dir = directory.rstrip(os.sep)
|
| 102 |
+
|
| 103 |
+
# Build a PathMetadata object, from path to metadata. :wink:
|
| 104 |
+
base_dir, dist_dir_name = os.path.split(dist_dir)
|
| 105 |
+
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
|
| 106 |
+
|
| 107 |
+
# Determine the correct Distribution object type.
|
| 108 |
+
if dist_dir.endswith(".egg-info"):
|
| 109 |
+
dist_cls = pkg_resources.Distribution
|
| 110 |
+
dist_name = os.path.splitext(dist_dir_name)[0]
|
| 111 |
+
else:
|
| 112 |
+
assert dist_dir.endswith(".dist-info")
|
| 113 |
+
dist_cls = pkg_resources.DistInfoDistribution
|
| 114 |
+
dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
|
| 115 |
+
|
| 116 |
+
dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
|
| 117 |
+
return cls(dist)
|
| 118 |
+
|
| 119 |
+
@classmethod
|
| 120 |
+
def from_metadata_file_contents(
|
| 121 |
+
cls,
|
| 122 |
+
metadata_contents: bytes,
|
| 123 |
+
filename: str,
|
| 124 |
+
project_name: str,
|
| 125 |
+
) -> BaseDistribution:
|
| 126 |
+
metadata_dict = {
|
| 127 |
+
"METADATA": metadata_contents,
|
| 128 |
+
}
|
| 129 |
+
dist = pkg_resources.DistInfoDistribution(
|
| 130 |
+
location=filename,
|
| 131 |
+
metadata=InMemoryMetadata(metadata_dict, filename),
|
| 132 |
+
project_name=project_name,
|
| 133 |
+
)
|
| 134 |
+
return cls(dist)
|
| 135 |
+
|
| 136 |
+
@classmethod
|
| 137 |
+
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
|
| 138 |
+
try:
|
| 139 |
+
with wheel.as_zipfile() as zf:
|
| 140 |
+
info_dir, _ = parse_wheel(zf, name)
|
| 141 |
+
metadata_dict = {
|
| 142 |
+
path.split("/", 1)[-1]: read_wheel_metadata_file(zf, path)
|
| 143 |
+
for path in zf.namelist()
|
| 144 |
+
if path.startswith(f"{info_dir}/")
|
| 145 |
+
}
|
| 146 |
+
except zipfile.BadZipFile as e:
|
| 147 |
+
raise InvalidWheel(wheel.location, name) from e
|
| 148 |
+
except UnsupportedWheel as e:
|
| 149 |
+
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
|
| 150 |
+
dist = pkg_resources.DistInfoDistribution(
|
| 151 |
+
location=wheel.location,
|
| 152 |
+
metadata=InMemoryMetadata(metadata_dict, wheel.location),
|
| 153 |
+
project_name=name,
|
| 154 |
+
)
|
| 155 |
+
return cls(dist)
|
| 156 |
+
|
| 157 |
+
@property
|
| 158 |
+
def location(self) -> Optional[str]:
|
| 159 |
+
return self._dist.location
|
| 160 |
+
|
| 161 |
+
@property
|
| 162 |
+
def installed_location(self) -> Optional[str]:
|
| 163 |
+
egg_link = egg_link_path_from_location(self.raw_name)
|
| 164 |
+
if egg_link:
|
| 165 |
+
location = egg_link
|
| 166 |
+
elif self.location:
|
| 167 |
+
location = self.location
|
| 168 |
+
else:
|
| 169 |
+
return None
|
| 170 |
+
return normalize_path(location)
|
| 171 |
+
|
| 172 |
+
@property
|
| 173 |
+
def info_location(self) -> Optional[str]:
|
| 174 |
+
return self._dist.egg_info
|
| 175 |
+
|
| 176 |
+
@property
|
| 177 |
+
def installed_by_distutils(self) -> bool:
|
| 178 |
+
# A distutils-installed distribution is provided by FileMetadata. This
|
| 179 |
+
# provider has a "path" attribute not present anywhere else. Not the
|
| 180 |
+
# best introspection logic, but pip has been doing this for a long time.
|
| 181 |
+
try:
|
| 182 |
+
return bool(self._dist._provider.path)
|
| 183 |
+
except AttributeError:
|
| 184 |
+
return False
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def canonical_name(self) -> NormalizedName:
|
| 188 |
+
return canonicalize_name(self._dist.project_name)
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def version(self) -> Version:
|
| 192 |
+
return parse_version(self._dist.version)
|
| 193 |
+
|
| 194 |
+
@property
|
| 195 |
+
def raw_version(self) -> str:
|
| 196 |
+
return self._dist.version
|
| 197 |
+
|
| 198 |
+
def is_file(self, path: InfoPath) -> bool:
|
| 199 |
+
return self._dist.has_metadata(str(path))
|
| 200 |
+
|
| 201 |
+
def iter_distutils_script_names(self) -> Iterator[str]:
|
| 202 |
+
yield from self._dist.metadata_listdir("scripts")
|
| 203 |
+
|
| 204 |
+
def read_text(self, path: InfoPath) -> str:
|
| 205 |
+
name = str(path)
|
| 206 |
+
if not self._dist.has_metadata(name):
|
| 207 |
+
raise FileNotFoundError(name)
|
| 208 |
+
content = self._dist.get_metadata(name)
|
| 209 |
+
if content is None:
|
| 210 |
+
raise NoneMetadataError(self, name)
|
| 211 |
+
return content
|
| 212 |
+
|
| 213 |
+
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
|
| 214 |
+
for group, entries in self._dist.get_entry_map().items():
|
| 215 |
+
for name, entry_point in entries.items():
|
| 216 |
+
name, _, value = str(entry_point).partition("=")
|
| 217 |
+
yield EntryPoint(name=name.strip(), value=value.strip(), group=group)
|
| 218 |
+
|
| 219 |
+
def _metadata_impl(self) -> email.message.Message:
|
| 220 |
+
"""
|
| 221 |
+
:raises NoneMetadataError: if the distribution reports `has_metadata()`
|
| 222 |
+
True but `get_metadata()` returns None.
|
| 223 |
+
"""
|
| 224 |
+
if isinstance(self._dist, pkg_resources.DistInfoDistribution):
|
| 225 |
+
metadata_name = "METADATA"
|
| 226 |
+
else:
|
| 227 |
+
metadata_name = "PKG-INFO"
|
| 228 |
+
try:
|
| 229 |
+
metadata = self.read_text(metadata_name)
|
| 230 |
+
except FileNotFoundError:
|
| 231 |
+
if self.location:
|
| 232 |
+
displaying_path = display_path(self.location)
|
| 233 |
+
else:
|
| 234 |
+
displaying_path = repr(self.location)
|
| 235 |
+
logger.warning("No metadata found in %s", displaying_path)
|
| 236 |
+
metadata = ""
|
| 237 |
+
feed_parser = email.parser.FeedParser()
|
| 238 |
+
feed_parser.feed(metadata)
|
| 239 |
+
return feed_parser.close()
|
| 240 |
+
|
| 241 |
+
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
|
| 242 |
+
if extras:
|
| 243 |
+
relevant_extras = set(self._extra_mapping) & set(
|
| 244 |
+
map(canonicalize_name, extras)
|
| 245 |
+
)
|
| 246 |
+
extras = [self._extra_mapping[extra] for extra in relevant_extras]
|
| 247 |
+
return self._dist.requires(extras)
|
| 248 |
+
|
| 249 |
+
def iter_provided_extras(self) -> Iterable[NormalizedName]:
|
| 250 |
+
return self._extra_mapping.keys()
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
class Environment(BaseEnvironment):
|
| 254 |
+
def __init__(self, ws: pkg_resources.WorkingSet) -> None:
|
| 255 |
+
self._ws = ws
|
| 256 |
+
|
| 257 |
+
@classmethod
|
| 258 |
+
def default(cls) -> BaseEnvironment:
|
| 259 |
+
return cls(pkg_resources.working_set)
|
| 260 |
+
|
| 261 |
+
@classmethod
|
| 262 |
+
def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
|
| 263 |
+
return cls(pkg_resources.WorkingSet(paths))
|
| 264 |
+
|
| 265 |
+
def _iter_distributions(self) -> Iterator[BaseDistribution]:
|
| 266 |
+
for dist in self._ws:
|
| 267 |
+
yield Distribution(dist)
|
| 268 |
+
|
| 269 |
+
def _search_distribution(self, name: str) -> Optional[BaseDistribution]:
|
| 270 |
+
"""Find a distribution matching the ``name`` in the environment.
|
| 271 |
+
|
| 272 |
+
This searches from *all* distributions available in the environment, to
|
| 273 |
+
match the behavior of ``pkg_resources.get_distribution()``.
|
| 274 |
+
"""
|
| 275 |
+
canonical_name = canonicalize_name(name)
|
| 276 |
+
for dist in self.iter_all_distributions():
|
| 277 |
+
if dist.canonical_name == canonical_name:
|
| 278 |
+
return dist
|
| 279 |
+
return None
|
| 280 |
+
|
| 281 |
+
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
|
| 282 |
+
# Search the distribution by looking through the working set.
|
| 283 |
+
dist = self._search_distribution(name)
|
| 284 |
+
if dist:
|
| 285 |
+
return dist
|
| 286 |
+
|
| 287 |
+
# If distribution could not be found, call working_set.require to
|
| 288 |
+
# update the working set, and try to find the distribution again.
|
| 289 |
+
# This might happen for e.g. when you install a package twice, once
|
| 290 |
+
# using setup.py develop and again using setup.py install. Now when
|
| 291 |
+
# running pip uninstall twice, the package gets removed from the
|
| 292 |
+
# working set in the first uninstall, so we have to populate the
|
| 293 |
+
# working set again so that pip knows about it and the packages gets
|
| 294 |
+
# picked up and is successfully uninstalled the second time too.
|
| 295 |
+
try:
|
| 296 |
+
# We didn't pass in any version specifiers, so this can never
|
| 297 |
+
# raise pkg_resources.VersionConflict.
|
| 298 |
+
self._ws.require(name)
|
| 299 |
+
except pkg_resources.DistributionNotFound:
|
| 300 |
+
return None
|
| 301 |
+
return self._search_distribution(name)
|
lib/python3.12/site-packages/pip/_internal/pyproject.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.util
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
from collections import namedtuple
|
| 5 |
+
from typing import Any, List, Optional
|
| 6 |
+
|
| 7 |
+
if sys.version_info >= (3, 11):
|
| 8 |
+
import tomllib
|
| 9 |
+
else:
|
| 10 |
+
from pip._vendor import tomli as tomllib
|
| 11 |
+
|
| 12 |
+
from pip._vendor.packaging.requirements import InvalidRequirement
|
| 13 |
+
|
| 14 |
+
from pip._internal.exceptions import (
|
| 15 |
+
InstallationError,
|
| 16 |
+
InvalidPyProjectBuildRequires,
|
| 17 |
+
MissingPyProjectBuildRequires,
|
| 18 |
+
)
|
| 19 |
+
from pip._internal.utils.packaging import get_requirement
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _is_list_of_str(obj: Any) -> bool:
|
| 23 |
+
return isinstance(obj, list) and all(isinstance(item, str) for item in obj)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def make_pyproject_path(unpacked_source_directory: str) -> str:
|
| 27 |
+
return os.path.join(unpacked_source_directory, "pyproject.toml")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
BuildSystemDetails = namedtuple(
|
| 31 |
+
"BuildSystemDetails", ["requires", "backend", "check", "backend_path"]
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_pyproject_toml(
|
| 36 |
+
use_pep517: Optional[bool], pyproject_toml: str, setup_py: str, req_name: str
|
| 37 |
+
) -> Optional[BuildSystemDetails]:
|
| 38 |
+
"""Load the pyproject.toml file.
|
| 39 |
+
|
| 40 |
+
Parameters:
|
| 41 |
+
use_pep517 - Has the user requested PEP 517 processing? None
|
| 42 |
+
means the user hasn't explicitly specified.
|
| 43 |
+
pyproject_toml - Location of the project's pyproject.toml file
|
| 44 |
+
setup_py - Location of the project's setup.py file
|
| 45 |
+
req_name - The name of the requirement we're processing (for
|
| 46 |
+
error reporting)
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
None if we should use the legacy code path, otherwise a tuple
|
| 50 |
+
(
|
| 51 |
+
requirements from pyproject.toml,
|
| 52 |
+
name of PEP 517 backend,
|
| 53 |
+
requirements we should check are installed after setting
|
| 54 |
+
up the build environment
|
| 55 |
+
directory paths to import the backend from (backend-path),
|
| 56 |
+
relative to the project root.
|
| 57 |
+
)
|
| 58 |
+
"""
|
| 59 |
+
has_pyproject = os.path.isfile(pyproject_toml)
|
| 60 |
+
has_setup = os.path.isfile(setup_py)
|
| 61 |
+
|
| 62 |
+
if not has_pyproject and not has_setup:
|
| 63 |
+
raise InstallationError(
|
| 64 |
+
f"{req_name} does not appear to be a Python project: "
|
| 65 |
+
f"neither 'setup.py' nor 'pyproject.toml' found."
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
if has_pyproject:
|
| 69 |
+
with open(pyproject_toml, encoding="utf-8") as f:
|
| 70 |
+
pp_toml = tomllib.loads(f.read())
|
| 71 |
+
build_system = pp_toml.get("build-system")
|
| 72 |
+
else:
|
| 73 |
+
build_system = None
|
| 74 |
+
|
| 75 |
+
# The following cases must use PEP 517
|
| 76 |
+
# We check for use_pep517 being non-None and falsy because that means
|
| 77 |
+
# the user explicitly requested --no-use-pep517. The value 0 as
|
| 78 |
+
# opposed to False can occur when the value is provided via an
|
| 79 |
+
# environment variable or config file option (due to the quirk of
|
| 80 |
+
# strtobool() returning an integer in pip's configuration code).
|
| 81 |
+
if has_pyproject and not has_setup:
|
| 82 |
+
if use_pep517 is not None and not use_pep517:
|
| 83 |
+
raise InstallationError(
|
| 84 |
+
"Disabling PEP 517 processing is invalid: "
|
| 85 |
+
"project does not have a setup.py"
|
| 86 |
+
)
|
| 87 |
+
use_pep517 = True
|
| 88 |
+
elif build_system and "build-backend" in build_system:
|
| 89 |
+
if use_pep517 is not None and not use_pep517:
|
| 90 |
+
raise InstallationError(
|
| 91 |
+
"Disabling PEP 517 processing is invalid: "
|
| 92 |
+
"project specifies a build backend of {} "
|
| 93 |
+
"in pyproject.toml".format(build_system["build-backend"])
|
| 94 |
+
)
|
| 95 |
+
use_pep517 = True
|
| 96 |
+
|
| 97 |
+
# If we haven't worked out whether to use PEP 517 yet,
|
| 98 |
+
# and the user hasn't explicitly stated a preference,
|
| 99 |
+
# we do so if the project has a pyproject.toml file
|
| 100 |
+
# or if we cannot import setuptools or wheels.
|
| 101 |
+
|
| 102 |
+
# We fallback to PEP 517 when without setuptools or without the wheel package,
|
| 103 |
+
# so setuptools can be installed as a default build backend.
|
| 104 |
+
# For more info see:
|
| 105 |
+
# https://discuss.python.org/t/pip-without-setuptools-could-the-experience-be-improved/11810/9
|
| 106 |
+
# https://github.com/pypa/pip/issues/8559
|
| 107 |
+
elif use_pep517 is None:
|
| 108 |
+
use_pep517 = (
|
| 109 |
+
has_pyproject
|
| 110 |
+
or not importlib.util.find_spec("setuptools")
|
| 111 |
+
or not importlib.util.find_spec("wheel")
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
# At this point, we know whether we're going to use PEP 517.
|
| 115 |
+
assert use_pep517 is not None
|
| 116 |
+
|
| 117 |
+
# If we're using the legacy code path, there is nothing further
|
| 118 |
+
# for us to do here.
|
| 119 |
+
if not use_pep517:
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
if build_system is None:
|
| 123 |
+
# Either the user has a pyproject.toml with no build-system
|
| 124 |
+
# section, or the user has no pyproject.toml, but has opted in
|
| 125 |
+
# explicitly via --use-pep517.
|
| 126 |
+
# In the absence of any explicit backend specification, we
|
| 127 |
+
# assume the setuptools backend that most closely emulates the
|
| 128 |
+
# traditional direct setup.py execution, and require wheel and
|
| 129 |
+
# a version of setuptools that supports that backend.
|
| 130 |
+
|
| 131 |
+
build_system = {
|
| 132 |
+
"requires": ["setuptools>=40.8.0"],
|
| 133 |
+
"build-backend": "setuptools.build_meta:__legacy__",
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
# If we're using PEP 517, we have build system information (either
|
| 137 |
+
# from pyproject.toml, or defaulted by the code above).
|
| 138 |
+
# Note that at this point, we do not know if the user has actually
|
| 139 |
+
# specified a backend, though.
|
| 140 |
+
assert build_system is not None
|
| 141 |
+
|
| 142 |
+
# Ensure that the build-system section in pyproject.toml conforms
|
| 143 |
+
# to PEP 518.
|
| 144 |
+
|
| 145 |
+
# Specifying the build-system table but not the requires key is invalid
|
| 146 |
+
if "requires" not in build_system:
|
| 147 |
+
raise MissingPyProjectBuildRequires(package=req_name)
|
| 148 |
+
|
| 149 |
+
# Error out if requires is not a list of strings
|
| 150 |
+
requires = build_system["requires"]
|
| 151 |
+
if not _is_list_of_str(requires):
|
| 152 |
+
raise InvalidPyProjectBuildRequires(
|
| 153 |
+
package=req_name,
|
| 154 |
+
reason="It is not a list of strings.",
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# Each requirement must be valid as per PEP 508
|
| 158 |
+
for requirement in requires:
|
| 159 |
+
try:
|
| 160 |
+
get_requirement(requirement)
|
| 161 |
+
except InvalidRequirement as error:
|
| 162 |
+
raise InvalidPyProjectBuildRequires(
|
| 163 |
+
package=req_name,
|
| 164 |
+
reason=f"It contains an invalid requirement: {requirement!r}",
|
| 165 |
+
) from error
|
| 166 |
+
|
| 167 |
+
backend = build_system.get("build-backend")
|
| 168 |
+
backend_path = build_system.get("backend-path", [])
|
| 169 |
+
check: List[str] = []
|
| 170 |
+
if backend is None:
|
| 171 |
+
# If the user didn't specify a backend, we assume they want to use
|
| 172 |
+
# the setuptools backend. But we can't be sure they have included
|
| 173 |
+
# a version of setuptools which supplies the backend. So we
|
| 174 |
+
# make a note to check that this requirement is present once
|
| 175 |
+
# we have set up the environment.
|
| 176 |
+
# This is quite a lot of work to check for a very specific case. But
|
| 177 |
+
# the problem is, that case is potentially quite common - projects that
|
| 178 |
+
# adopted PEP 518 early for the ability to specify requirements to
|
| 179 |
+
# execute setup.py, but never considered needing to mention the build
|
| 180 |
+
# tools themselves. The original PEP 518 code had a similar check (but
|
| 181 |
+
# implemented in a different way).
|
| 182 |
+
backend = "setuptools.build_meta:__legacy__"
|
| 183 |
+
check = ["setuptools>=40.8.0"]
|
| 184 |
+
|
| 185 |
+
return BuildSystemDetails(requires, backend, check, backend_path)
|
lib/python3.12/site-packages/pip/_internal/self_outdated_check.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import functools
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
import optparse
|
| 7 |
+
import os.path
|
| 8 |
+
import sys
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
from typing import Any, Callable, Dict, Optional
|
| 11 |
+
|
| 12 |
+
from pip._vendor.packaging.version import Version
|
| 13 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 14 |
+
from pip._vendor.rich.console import Group
|
| 15 |
+
from pip._vendor.rich.markup import escape
|
| 16 |
+
from pip._vendor.rich.text import Text
|
| 17 |
+
|
| 18 |
+
from pip._internal.index.collector import LinkCollector
|
| 19 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 20 |
+
from pip._internal.metadata import get_default_environment
|
| 21 |
+
from pip._internal.models.selection_prefs import SelectionPreferences
|
| 22 |
+
from pip._internal.network.session import PipSession
|
| 23 |
+
from pip._internal.utils.compat import WINDOWS
|
| 24 |
+
from pip._internal.utils.entrypoints import (
|
| 25 |
+
get_best_invocation_for_this_pip,
|
| 26 |
+
get_best_invocation_for_this_python,
|
| 27 |
+
)
|
| 28 |
+
from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace
|
| 29 |
+
from pip._internal.utils.misc import (
|
| 30 |
+
ExternallyManagedEnvironment,
|
| 31 |
+
check_externally_managed,
|
| 32 |
+
ensure_dir,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
_WEEK = datetime.timedelta(days=7)
|
| 36 |
+
|
| 37 |
+
logger = logging.getLogger(__name__)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _get_statefile_name(key: str) -> str:
|
| 41 |
+
key_bytes = key.encode()
|
| 42 |
+
name = hashlib.sha224(key_bytes).hexdigest()
|
| 43 |
+
return name
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _convert_date(isodate: str) -> datetime.datetime:
|
| 47 |
+
"""Convert an ISO format string to a date.
|
| 48 |
+
|
| 49 |
+
Handles the format 2020-01-22T14:24:01Z (trailing Z)
|
| 50 |
+
which is not supported by older versions of fromisoformat.
|
| 51 |
+
"""
|
| 52 |
+
return datetime.datetime.fromisoformat(isodate.replace("Z", "+00:00"))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class SelfCheckState:
|
| 56 |
+
def __init__(self, cache_dir: str) -> None:
|
| 57 |
+
self._state: Dict[str, Any] = {}
|
| 58 |
+
self._statefile_path = None
|
| 59 |
+
|
| 60 |
+
# Try to load the existing state
|
| 61 |
+
if cache_dir:
|
| 62 |
+
self._statefile_path = os.path.join(
|
| 63 |
+
cache_dir, "selfcheck", _get_statefile_name(self.key)
|
| 64 |
+
)
|
| 65 |
+
try:
|
| 66 |
+
with open(self._statefile_path, encoding="utf-8") as statefile:
|
| 67 |
+
self._state = json.load(statefile)
|
| 68 |
+
except (OSError, ValueError, KeyError):
|
| 69 |
+
# Explicitly suppressing exceptions, since we don't want to
|
| 70 |
+
# error out if the cache file is invalid.
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
@property
|
| 74 |
+
def key(self) -> str:
|
| 75 |
+
return sys.prefix
|
| 76 |
+
|
| 77 |
+
def get(self, current_time: datetime.datetime) -> Optional[str]:
|
| 78 |
+
"""Check if we have a not-outdated version loaded already."""
|
| 79 |
+
if not self._state:
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
if "last_check" not in self._state:
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
if "pypi_version" not in self._state:
|
| 86 |
+
return None
|
| 87 |
+
|
| 88 |
+
# Determine if we need to refresh the state
|
| 89 |
+
last_check = _convert_date(self._state["last_check"])
|
| 90 |
+
time_since_last_check = current_time - last_check
|
| 91 |
+
if time_since_last_check > _WEEK:
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
return self._state["pypi_version"]
|
| 95 |
+
|
| 96 |
+
def set(self, pypi_version: str, current_time: datetime.datetime) -> None:
|
| 97 |
+
# If we do not have a path to cache in, don't bother saving.
|
| 98 |
+
if not self._statefile_path:
|
| 99 |
+
return
|
| 100 |
+
|
| 101 |
+
# Check to make sure that we own the directory
|
| 102 |
+
if not check_path_owner(os.path.dirname(self._statefile_path)):
|
| 103 |
+
return
|
| 104 |
+
|
| 105 |
+
# Now that we've ensured the directory is owned by this user, we'll go
|
| 106 |
+
# ahead and make sure that all our directories are created.
|
| 107 |
+
ensure_dir(os.path.dirname(self._statefile_path))
|
| 108 |
+
|
| 109 |
+
state = {
|
| 110 |
+
# Include the key so it's easy to tell which pip wrote the
|
| 111 |
+
# file.
|
| 112 |
+
"key": self.key,
|
| 113 |
+
"last_check": current_time.isoformat(),
|
| 114 |
+
"pypi_version": pypi_version,
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
text = json.dumps(state, sort_keys=True, separators=(",", ":"))
|
| 118 |
+
|
| 119 |
+
with adjacent_tmp_file(self._statefile_path) as f:
|
| 120 |
+
f.write(text.encode())
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
# Since we have a prefix-specific state file, we can just
|
| 124 |
+
# overwrite whatever is there, no need to check.
|
| 125 |
+
replace(f.name, self._statefile_path)
|
| 126 |
+
except OSError:
|
| 127 |
+
# Best effort.
|
| 128 |
+
pass
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@dataclass
|
| 132 |
+
class UpgradePrompt:
|
| 133 |
+
old: str
|
| 134 |
+
new: str
|
| 135 |
+
|
| 136 |
+
def __rich__(self) -> Group:
|
| 137 |
+
if WINDOWS:
|
| 138 |
+
pip_cmd = f"{get_best_invocation_for_this_python()} -m pip"
|
| 139 |
+
else:
|
| 140 |
+
pip_cmd = get_best_invocation_for_this_pip()
|
| 141 |
+
|
| 142 |
+
notice = "[bold][[reset][blue]notice[reset][bold]][reset]"
|
| 143 |
+
return Group(
|
| 144 |
+
Text(),
|
| 145 |
+
Text.from_markup(
|
| 146 |
+
f"{notice} A new release of pip is available: "
|
| 147 |
+
f"[red]{self.old}[reset] -> [green]{self.new}[reset]"
|
| 148 |
+
),
|
| 149 |
+
Text.from_markup(
|
| 150 |
+
f"{notice} To update, run: "
|
| 151 |
+
f"[green]{escape(pip_cmd)} install --upgrade pip"
|
| 152 |
+
),
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def was_installed_by_pip(pkg: str) -> bool:
|
| 157 |
+
"""Checks whether pkg was installed by pip
|
| 158 |
+
|
| 159 |
+
This is used not to display the upgrade message when pip is in fact
|
| 160 |
+
installed by system package manager, such as dnf on Fedora.
|
| 161 |
+
"""
|
| 162 |
+
dist = get_default_environment().get_distribution(pkg)
|
| 163 |
+
return dist is not None and "pip" == dist.installer
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _get_current_remote_pip_version(
|
| 167 |
+
session: PipSession, options: optparse.Values
|
| 168 |
+
) -> Optional[str]:
|
| 169 |
+
# Lets use PackageFinder to see what the latest pip version is
|
| 170 |
+
link_collector = LinkCollector.create(
|
| 171 |
+
session,
|
| 172 |
+
options=options,
|
| 173 |
+
suppress_no_index=True,
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# Pass allow_yanked=False so we don't suggest upgrading to a
|
| 177 |
+
# yanked version.
|
| 178 |
+
selection_prefs = SelectionPreferences(
|
| 179 |
+
allow_yanked=False,
|
| 180 |
+
allow_all_prereleases=False, # Explicitly set to False
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
finder = PackageFinder.create(
|
| 184 |
+
link_collector=link_collector,
|
| 185 |
+
selection_prefs=selection_prefs,
|
| 186 |
+
)
|
| 187 |
+
best_candidate = finder.find_best_candidate("pip").best_candidate
|
| 188 |
+
if best_candidate is None:
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
return str(best_candidate.version)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def _self_version_check_logic(
|
| 195 |
+
*,
|
| 196 |
+
state: SelfCheckState,
|
| 197 |
+
current_time: datetime.datetime,
|
| 198 |
+
local_version: Version,
|
| 199 |
+
get_remote_version: Callable[[], Optional[str]],
|
| 200 |
+
) -> Optional[UpgradePrompt]:
|
| 201 |
+
remote_version_str = state.get(current_time)
|
| 202 |
+
if remote_version_str is None:
|
| 203 |
+
remote_version_str = get_remote_version()
|
| 204 |
+
if remote_version_str is None:
|
| 205 |
+
logger.debug("No remote pip version found")
|
| 206 |
+
return None
|
| 207 |
+
state.set(remote_version_str, current_time)
|
| 208 |
+
|
| 209 |
+
remote_version = parse_version(remote_version_str)
|
| 210 |
+
logger.debug("Remote version of pip: %s", remote_version)
|
| 211 |
+
logger.debug("Local version of pip: %s", local_version)
|
| 212 |
+
|
| 213 |
+
pip_installed_by_pip = was_installed_by_pip("pip")
|
| 214 |
+
logger.debug("Was pip installed by pip? %s", pip_installed_by_pip)
|
| 215 |
+
if not pip_installed_by_pip:
|
| 216 |
+
return None # Only suggest upgrade if pip is installed by pip.
|
| 217 |
+
|
| 218 |
+
local_version_is_older = (
|
| 219 |
+
local_version < remote_version
|
| 220 |
+
and local_version.base_version != remote_version.base_version
|
| 221 |
+
)
|
| 222 |
+
if local_version_is_older:
|
| 223 |
+
return UpgradePrompt(old=str(local_version), new=remote_version_str)
|
| 224 |
+
|
| 225 |
+
return None
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def pip_self_version_check(session: PipSession, options: optparse.Values) -> None:
|
| 229 |
+
"""Check for an update for pip.
|
| 230 |
+
|
| 231 |
+
Limit the frequency of checks to once per week. State is stored either in
|
| 232 |
+
the active virtualenv or in the user's USER_CACHE_DIR keyed off the prefix
|
| 233 |
+
of the pip script path.
|
| 234 |
+
"""
|
| 235 |
+
installed_dist = get_default_environment().get_distribution("pip")
|
| 236 |
+
if not installed_dist:
|
| 237 |
+
return
|
| 238 |
+
try:
|
| 239 |
+
check_externally_managed()
|
| 240 |
+
except ExternallyManagedEnvironment:
|
| 241 |
+
return
|
| 242 |
+
|
| 243 |
+
upgrade_prompt = _self_version_check_logic(
|
| 244 |
+
state=SelfCheckState(cache_dir=options.cache_dir),
|
| 245 |
+
current_time=datetime.datetime.now(datetime.timezone.utc),
|
| 246 |
+
local_version=installed_dist.version,
|
| 247 |
+
get_remote_version=functools.partial(
|
| 248 |
+
_get_current_remote_pip_version, session, options
|
| 249 |
+
),
|
| 250 |
+
)
|
| 251 |
+
if upgrade_prompt is not None:
|
| 252 |
+
logger.warning("%s", upgrade_prompt, extra={"rich": True})
|
lib/python3.12/site-packages/pip/_internal/wheel_builder.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Orchestrator for building wheels from InstallRequirements.
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
import os.path
|
| 6 |
+
import re
|
| 7 |
+
import shutil
|
| 8 |
+
from typing import Iterable, List, Optional, Tuple
|
| 9 |
+
|
| 10 |
+
from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
|
| 11 |
+
from pip._vendor.packaging.version import InvalidVersion, Version
|
| 12 |
+
|
| 13 |
+
from pip._internal.cache import WheelCache
|
| 14 |
+
from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel
|
| 15 |
+
from pip._internal.metadata import FilesystemWheel, get_wheel_distribution
|
| 16 |
+
from pip._internal.models.link import Link
|
| 17 |
+
from pip._internal.models.wheel import Wheel
|
| 18 |
+
from pip._internal.operations.build.wheel import build_wheel_pep517
|
| 19 |
+
from pip._internal.operations.build.wheel_editable import build_wheel_editable
|
| 20 |
+
from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
|
| 21 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 22 |
+
from pip._internal.utils.logging import indent_log
|
| 23 |
+
from pip._internal.utils.misc import ensure_dir, hash_file
|
| 24 |
+
from pip._internal.utils.setuptools_build import make_setuptools_clean_args
|
| 25 |
+
from pip._internal.utils.subprocess import call_subprocess
|
| 26 |
+
from pip._internal.utils.temp_dir import TempDirectory
|
| 27 |
+
from pip._internal.utils.urls import path_to_url
|
| 28 |
+
from pip._internal.vcs import vcs
|
| 29 |
+
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
_egg_info_re = re.compile(r"([a-z0-9_.]+)-([a-z0-9_.!+-]+)", re.IGNORECASE)
|
| 33 |
+
|
| 34 |
+
BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _contains_egg_info(s: str) -> bool:
|
| 38 |
+
"""Determine whether the string looks like an egg_info.
|
| 39 |
+
|
| 40 |
+
:param s: The string to parse. E.g. foo-2.1
|
| 41 |
+
"""
|
| 42 |
+
return bool(_egg_info_re.search(s))
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _should_build(
|
| 46 |
+
req: InstallRequirement,
|
| 47 |
+
need_wheel: bool,
|
| 48 |
+
) -> bool:
|
| 49 |
+
"""Return whether an InstallRequirement should be built into a wheel."""
|
| 50 |
+
if req.constraint:
|
| 51 |
+
# never build requirements that are merely constraints
|
| 52 |
+
return False
|
| 53 |
+
if req.is_wheel:
|
| 54 |
+
if need_wheel:
|
| 55 |
+
logger.info(
|
| 56 |
+
"Skipping %s, due to already being wheel.",
|
| 57 |
+
req.name,
|
| 58 |
+
)
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
if need_wheel:
|
| 62 |
+
# i.e. pip wheel, not pip install
|
| 63 |
+
return True
|
| 64 |
+
|
| 65 |
+
# From this point, this concerns the pip install command only
|
| 66 |
+
# (need_wheel=False).
|
| 67 |
+
|
| 68 |
+
if not req.source_dir:
|
| 69 |
+
return False
|
| 70 |
+
|
| 71 |
+
if req.editable:
|
| 72 |
+
# we only build PEP 660 editable requirements
|
| 73 |
+
return req.supports_pyproject_editable
|
| 74 |
+
|
| 75 |
+
return True
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def should_build_for_wheel_command(
|
| 79 |
+
req: InstallRequirement,
|
| 80 |
+
) -> bool:
|
| 81 |
+
return _should_build(req, need_wheel=True)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def should_build_for_install_command(
|
| 85 |
+
req: InstallRequirement,
|
| 86 |
+
) -> bool:
|
| 87 |
+
return _should_build(req, need_wheel=False)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _should_cache(
|
| 91 |
+
req: InstallRequirement,
|
| 92 |
+
) -> Optional[bool]:
|
| 93 |
+
"""
|
| 94 |
+
Return whether a built InstallRequirement can be stored in the persistent
|
| 95 |
+
wheel cache, assuming the wheel cache is available, and _should_build()
|
| 96 |
+
has determined a wheel needs to be built.
|
| 97 |
+
"""
|
| 98 |
+
if req.editable or not req.source_dir:
|
| 99 |
+
# never cache editable requirements
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
if req.link and req.link.is_vcs:
|
| 103 |
+
# VCS checkout. Do not cache
|
| 104 |
+
# unless it points to an immutable commit hash.
|
| 105 |
+
assert not req.editable
|
| 106 |
+
assert req.source_dir
|
| 107 |
+
vcs_backend = vcs.get_backend_for_scheme(req.link.scheme)
|
| 108 |
+
assert vcs_backend
|
| 109 |
+
if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir):
|
| 110 |
+
return True
|
| 111 |
+
return False
|
| 112 |
+
|
| 113 |
+
assert req.link
|
| 114 |
+
base, ext = req.link.splitext()
|
| 115 |
+
if _contains_egg_info(base):
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
# Otherwise, do not cache.
|
| 119 |
+
return False
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _get_cache_dir(
|
| 123 |
+
req: InstallRequirement,
|
| 124 |
+
wheel_cache: WheelCache,
|
| 125 |
+
) -> str:
|
| 126 |
+
"""Return the persistent or temporary cache directory where the built
|
| 127 |
+
wheel need to be stored.
|
| 128 |
+
"""
|
| 129 |
+
cache_available = bool(wheel_cache.cache_dir)
|
| 130 |
+
assert req.link
|
| 131 |
+
if cache_available and _should_cache(req):
|
| 132 |
+
cache_dir = wheel_cache.get_path_for_link(req.link)
|
| 133 |
+
else:
|
| 134 |
+
cache_dir = wheel_cache.get_ephem_path_for_link(req.link)
|
| 135 |
+
return cache_dir
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def _verify_one(req: InstallRequirement, wheel_path: str) -> None:
|
| 139 |
+
canonical_name = canonicalize_name(req.name or "")
|
| 140 |
+
w = Wheel(os.path.basename(wheel_path))
|
| 141 |
+
if canonicalize_name(w.name) != canonical_name:
|
| 142 |
+
raise InvalidWheelFilename(
|
| 143 |
+
f"Wheel has unexpected file name: expected {canonical_name!r}, "
|
| 144 |
+
f"got {w.name!r}",
|
| 145 |
+
)
|
| 146 |
+
dist = get_wheel_distribution(FilesystemWheel(wheel_path), canonical_name)
|
| 147 |
+
dist_verstr = str(dist.version)
|
| 148 |
+
if canonicalize_version(dist_verstr) != canonicalize_version(w.version):
|
| 149 |
+
raise InvalidWheelFilename(
|
| 150 |
+
f"Wheel has unexpected file name: expected {dist_verstr!r}, "
|
| 151 |
+
f"got {w.version!r}",
|
| 152 |
+
)
|
| 153 |
+
metadata_version_value = dist.metadata_version
|
| 154 |
+
if metadata_version_value is None:
|
| 155 |
+
raise UnsupportedWheel("Missing Metadata-Version")
|
| 156 |
+
try:
|
| 157 |
+
metadata_version = Version(metadata_version_value)
|
| 158 |
+
except InvalidVersion:
|
| 159 |
+
msg = f"Invalid Metadata-Version: {metadata_version_value}"
|
| 160 |
+
raise UnsupportedWheel(msg)
|
| 161 |
+
if metadata_version >= Version("1.2") and not isinstance(dist.version, Version):
|
| 162 |
+
raise UnsupportedWheel(
|
| 163 |
+
f"Metadata 1.2 mandates PEP 440 version, but {dist_verstr!r} is not"
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _build_one(
|
| 168 |
+
req: InstallRequirement,
|
| 169 |
+
output_dir: str,
|
| 170 |
+
verify: bool,
|
| 171 |
+
build_options: List[str],
|
| 172 |
+
global_options: List[str],
|
| 173 |
+
editable: bool,
|
| 174 |
+
) -> Optional[str]:
|
| 175 |
+
"""Build one wheel.
|
| 176 |
+
|
| 177 |
+
:return: The filename of the built wheel, or None if the build failed.
|
| 178 |
+
"""
|
| 179 |
+
artifact = "editable" if editable else "wheel"
|
| 180 |
+
try:
|
| 181 |
+
ensure_dir(output_dir)
|
| 182 |
+
except OSError as e:
|
| 183 |
+
logger.warning(
|
| 184 |
+
"Building %s for %s failed: %s",
|
| 185 |
+
artifact,
|
| 186 |
+
req.name,
|
| 187 |
+
e,
|
| 188 |
+
)
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
# Install build deps into temporary directory (PEP 518)
|
| 192 |
+
with req.build_env:
|
| 193 |
+
wheel_path = _build_one_inside_env(
|
| 194 |
+
req, output_dir, build_options, global_options, editable
|
| 195 |
+
)
|
| 196 |
+
if wheel_path and verify:
|
| 197 |
+
try:
|
| 198 |
+
_verify_one(req, wheel_path)
|
| 199 |
+
except (InvalidWheelFilename, UnsupportedWheel) as e:
|
| 200 |
+
logger.warning("Built %s for %s is invalid: %s", artifact, req.name, e)
|
| 201 |
+
return None
|
| 202 |
+
return wheel_path
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _build_one_inside_env(
|
| 206 |
+
req: InstallRequirement,
|
| 207 |
+
output_dir: str,
|
| 208 |
+
build_options: List[str],
|
| 209 |
+
global_options: List[str],
|
| 210 |
+
editable: bool,
|
| 211 |
+
) -> Optional[str]:
|
| 212 |
+
with TempDirectory(kind="wheel") as temp_dir:
|
| 213 |
+
assert req.name
|
| 214 |
+
if req.use_pep517:
|
| 215 |
+
assert req.metadata_directory
|
| 216 |
+
assert req.pep517_backend
|
| 217 |
+
if global_options:
|
| 218 |
+
logger.warning(
|
| 219 |
+
"Ignoring --global-option when building %s using PEP 517", req.name
|
| 220 |
+
)
|
| 221 |
+
if build_options:
|
| 222 |
+
logger.warning(
|
| 223 |
+
"Ignoring --build-option when building %s using PEP 517", req.name
|
| 224 |
+
)
|
| 225 |
+
if editable:
|
| 226 |
+
wheel_path = build_wheel_editable(
|
| 227 |
+
name=req.name,
|
| 228 |
+
backend=req.pep517_backend,
|
| 229 |
+
metadata_directory=req.metadata_directory,
|
| 230 |
+
tempd=temp_dir.path,
|
| 231 |
+
)
|
| 232 |
+
else:
|
| 233 |
+
wheel_path = build_wheel_pep517(
|
| 234 |
+
name=req.name,
|
| 235 |
+
backend=req.pep517_backend,
|
| 236 |
+
metadata_directory=req.metadata_directory,
|
| 237 |
+
tempd=temp_dir.path,
|
| 238 |
+
)
|
| 239 |
+
else:
|
| 240 |
+
wheel_path = build_wheel_legacy(
|
| 241 |
+
name=req.name,
|
| 242 |
+
setup_py_path=req.setup_py_path,
|
| 243 |
+
source_dir=req.unpacked_source_directory,
|
| 244 |
+
global_options=global_options,
|
| 245 |
+
build_options=build_options,
|
| 246 |
+
tempd=temp_dir.path,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if wheel_path is not None:
|
| 250 |
+
wheel_name = os.path.basename(wheel_path)
|
| 251 |
+
dest_path = os.path.join(output_dir, wheel_name)
|
| 252 |
+
try:
|
| 253 |
+
wheel_hash, length = hash_file(wheel_path)
|
| 254 |
+
shutil.move(wheel_path, dest_path)
|
| 255 |
+
logger.info(
|
| 256 |
+
"Created wheel for %s: filename=%s size=%d sha256=%s",
|
| 257 |
+
req.name,
|
| 258 |
+
wheel_name,
|
| 259 |
+
length,
|
| 260 |
+
wheel_hash.hexdigest(),
|
| 261 |
+
)
|
| 262 |
+
logger.info("Stored in directory: %s", output_dir)
|
| 263 |
+
return dest_path
|
| 264 |
+
except Exception as e:
|
| 265 |
+
logger.warning(
|
| 266 |
+
"Building wheel for %s failed: %s",
|
| 267 |
+
req.name,
|
| 268 |
+
e,
|
| 269 |
+
)
|
| 270 |
+
# Ignore return, we can't do anything else useful.
|
| 271 |
+
if not req.use_pep517:
|
| 272 |
+
_clean_one_legacy(req, global_options)
|
| 273 |
+
return None
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def _clean_one_legacy(req: InstallRequirement, global_options: List[str]) -> bool:
|
| 277 |
+
clean_args = make_setuptools_clean_args(
|
| 278 |
+
req.setup_py_path,
|
| 279 |
+
global_options=global_options,
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
logger.info("Running setup.py clean for %s", req.name)
|
| 283 |
+
try:
|
| 284 |
+
call_subprocess(
|
| 285 |
+
clean_args, command_desc="python setup.py clean", cwd=req.source_dir
|
| 286 |
+
)
|
| 287 |
+
return True
|
| 288 |
+
except Exception:
|
| 289 |
+
logger.error("Failed cleaning build dir for %s", req.name)
|
| 290 |
+
return False
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def build(
|
| 294 |
+
requirements: Iterable[InstallRequirement],
|
| 295 |
+
wheel_cache: WheelCache,
|
| 296 |
+
verify: bool,
|
| 297 |
+
build_options: List[str],
|
| 298 |
+
global_options: List[str],
|
| 299 |
+
) -> BuildResult:
|
| 300 |
+
"""Build wheels.
|
| 301 |
+
|
| 302 |
+
:return: The list of InstallRequirement that succeeded to build and
|
| 303 |
+
the list of InstallRequirement that failed to build.
|
| 304 |
+
"""
|
| 305 |
+
if not requirements:
|
| 306 |
+
return [], []
|
| 307 |
+
|
| 308 |
+
# Build the wheels.
|
| 309 |
+
logger.info(
|
| 310 |
+
"Building wheels for collected packages: %s",
|
| 311 |
+
", ".join(req.name for req in requirements), # type: ignore
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
with indent_log():
|
| 315 |
+
build_successes, build_failures = [], []
|
| 316 |
+
for req in requirements:
|
| 317 |
+
assert req.name
|
| 318 |
+
cache_dir = _get_cache_dir(req, wheel_cache)
|
| 319 |
+
wheel_file = _build_one(
|
| 320 |
+
req,
|
| 321 |
+
cache_dir,
|
| 322 |
+
verify,
|
| 323 |
+
build_options,
|
| 324 |
+
global_options,
|
| 325 |
+
req.editable and req.permit_editable_wheels,
|
| 326 |
+
)
|
| 327 |
+
if wheel_file:
|
| 328 |
+
# Record the download origin in the cache
|
| 329 |
+
if req.download_info is not None:
|
| 330 |
+
# download_info is guaranteed to be set because when we build an
|
| 331 |
+
# InstallRequirement it has been through the preparer before, but
|
| 332 |
+
# let's be cautious.
|
| 333 |
+
wheel_cache.record_download_origin(cache_dir, req.download_info)
|
| 334 |
+
# Update the link for this.
|
| 335 |
+
req.link = Link(path_to_url(wheel_file))
|
| 336 |
+
req.local_file_path = req.link.file_path
|
| 337 |
+
assert req.link.is_wheel
|
| 338 |
+
build_successes.append(req)
|
| 339 |
+
else:
|
| 340 |
+
build_failures.append(req)
|
| 341 |
+
|
| 342 |
+
# notify success/failure
|
| 343 |
+
if build_successes:
|
| 344 |
+
logger.info(
|
| 345 |
+
"Successfully built %s",
|
| 346 |
+
" ".join([req.name for req in build_successes]), # type: ignore
|
| 347 |
+
)
|
| 348 |
+
if build_failures:
|
| 349 |
+
logger.info(
|
| 350 |
+
"Failed to build %s",
|
| 351 |
+
" ".join([req.name for req in build_failures]), # type: ignore
|
| 352 |
+
)
|
| 353 |
+
# Return a list of requirements that failed to build
|
| 354 |
+
return build_successes, build_failures
|
lib/python3.12/site-packages/pip/py.typed
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pip is a command line program. While it is implemented in Python, and so is
|
| 2 |
+
available for import, you must not use pip's internal APIs in this way. Typing
|
| 3 |
+
information is provided as a convenience only and is not a guarantee. Expect
|
| 4 |
+
unannounced changes to the API and types in releases.
|
lib/python3.12/site-packages/pygments/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (3.46 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/__main__.cpython-312.pyc
ADDED
|
Binary file (779 Bytes). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/cmdline.cpython-312.pyc
ADDED
|
Binary file (26.5 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/console.cpython-312.pyc
ADDED
|
Binary file (2.62 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/filter.cpython-312.pyc
ADDED
|
Binary file (3.22 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/formatter.cpython-312.pyc
ADDED
|
Binary file (4.69 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/lexer.cpython-312.pyc
ADDED
|
Binary file (38.6 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/modeline.cpython-312.pyc
ADDED
|
Binary file (1.56 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/plugin.cpython-312.pyc
ADDED
|
Binary file (2.6 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/regexopt.cpython-312.pyc
ADDED
|
Binary file (4.07 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/scanner.cpython-312.pyc
ADDED
|
Binary file (4.75 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/sphinxext.cpython-312.pyc
ADDED
|
Binary file (12.1 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/style.cpython-312.pyc
ADDED
|
Binary file (6.68 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/token.cpython-312.pyc
ADDED
|
Binary file (8.19 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/unistring.cpython-312.pyc
ADDED
|
Binary file (33 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/__pycache__/util.cpython-312.pyc
ADDED
|
Binary file (14.1 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/formatters/__init__.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
pygments.formatters
|
| 3 |
+
~~~~~~~~~~~~~~~~~~~
|
| 4 |
+
|
| 5 |
+
Pygments formatters.
|
| 6 |
+
|
| 7 |
+
:copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
|
| 8 |
+
:license: BSD, see LICENSE for details.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
import sys
|
| 13 |
+
import types
|
| 14 |
+
import fnmatch
|
| 15 |
+
from os.path import basename
|
| 16 |
+
|
| 17 |
+
from pygments.formatters._mapping import FORMATTERS
|
| 18 |
+
from pygments.plugin import find_plugin_formatters
|
| 19 |
+
from pygments.util import ClassNotFound
|
| 20 |
+
|
| 21 |
+
__all__ = ['get_formatter_by_name', 'get_formatter_for_filename',
|
| 22 |
+
'get_all_formatters', 'load_formatter_from_file'] + list(FORMATTERS)
|
| 23 |
+
|
| 24 |
+
_formatter_cache = {} # classes by name
|
| 25 |
+
_pattern_cache = {}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _fn_matches(fn, glob):
|
| 29 |
+
"""Return whether the supplied file name fn matches pattern filename."""
|
| 30 |
+
if glob not in _pattern_cache:
|
| 31 |
+
pattern = _pattern_cache[glob] = re.compile(fnmatch.translate(glob))
|
| 32 |
+
return pattern.match(fn)
|
| 33 |
+
return _pattern_cache[glob].match(fn)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _load_formatters(module_name):
|
| 37 |
+
"""Load a formatter (and all others in the module too)."""
|
| 38 |
+
mod = __import__(module_name, None, None, ['__all__'])
|
| 39 |
+
for formatter_name in mod.__all__:
|
| 40 |
+
cls = getattr(mod, formatter_name)
|
| 41 |
+
_formatter_cache[cls.name] = cls
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def get_all_formatters():
|
| 45 |
+
"""Return a generator for all formatter classes."""
|
| 46 |
+
# NB: this returns formatter classes, not info like get_all_lexers().
|
| 47 |
+
for info in FORMATTERS.values():
|
| 48 |
+
if info[1] not in _formatter_cache:
|
| 49 |
+
_load_formatters(info[0])
|
| 50 |
+
yield _formatter_cache[info[1]]
|
| 51 |
+
for _, formatter in find_plugin_formatters():
|
| 52 |
+
yield formatter
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def find_formatter_class(alias):
|
| 56 |
+
"""Lookup a formatter by alias.
|
| 57 |
+
|
| 58 |
+
Returns None if not found.
|
| 59 |
+
"""
|
| 60 |
+
for module_name, name, aliases, _, _ in FORMATTERS.values():
|
| 61 |
+
if alias in aliases:
|
| 62 |
+
if name not in _formatter_cache:
|
| 63 |
+
_load_formatters(module_name)
|
| 64 |
+
return _formatter_cache[name]
|
| 65 |
+
for _, cls in find_plugin_formatters():
|
| 66 |
+
if alias in cls.aliases:
|
| 67 |
+
return cls
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def get_formatter_by_name(_alias, **options):
|
| 71 |
+
"""
|
| 72 |
+
Return an instance of a :class:`.Formatter` subclass that has `alias` in its
|
| 73 |
+
aliases list. The formatter is given the `options` at its instantiation.
|
| 74 |
+
|
| 75 |
+
Will raise :exc:`pygments.util.ClassNotFound` if no formatter with that
|
| 76 |
+
alias is found.
|
| 77 |
+
"""
|
| 78 |
+
cls = find_formatter_class(_alias)
|
| 79 |
+
if cls is None:
|
| 80 |
+
raise ClassNotFound(f"no formatter found for name {_alias!r}")
|
| 81 |
+
return cls(**options)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def load_formatter_from_file(filename, formattername="CustomFormatter", **options):
|
| 85 |
+
"""
|
| 86 |
+
Return a `Formatter` subclass instance loaded from the provided file, relative
|
| 87 |
+
to the current directory.
|
| 88 |
+
|
| 89 |
+
The file is expected to contain a Formatter class named ``formattername``
|
| 90 |
+
(by default, CustomFormatter). Users should be very careful with the input, because
|
| 91 |
+
this method is equivalent to running ``eval()`` on the input file. The formatter is
|
| 92 |
+
given the `options` at its instantiation.
|
| 93 |
+
|
| 94 |
+
:exc:`pygments.util.ClassNotFound` is raised if there are any errors loading
|
| 95 |
+
the formatter.
|
| 96 |
+
|
| 97 |
+
.. versionadded:: 2.2
|
| 98 |
+
"""
|
| 99 |
+
try:
|
| 100 |
+
# This empty dict will contain the namespace for the exec'd file
|
| 101 |
+
custom_namespace = {}
|
| 102 |
+
with open(filename, 'rb') as f:
|
| 103 |
+
exec(f.read(), custom_namespace)
|
| 104 |
+
# Retrieve the class `formattername` from that namespace
|
| 105 |
+
if formattername not in custom_namespace:
|
| 106 |
+
raise ClassNotFound(f'no valid {formattername} class found in {filename}')
|
| 107 |
+
formatter_class = custom_namespace[formattername]
|
| 108 |
+
# And finally instantiate it with the options
|
| 109 |
+
return formatter_class(**options)
|
| 110 |
+
except OSError as err:
|
| 111 |
+
raise ClassNotFound(f'cannot read {filename}: {err}')
|
| 112 |
+
except ClassNotFound:
|
| 113 |
+
raise
|
| 114 |
+
except Exception as err:
|
| 115 |
+
raise ClassNotFound(f'error when loading custom formatter: {err}')
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def get_formatter_for_filename(fn, **options):
|
| 119 |
+
"""
|
| 120 |
+
Return a :class:`.Formatter` subclass instance that has a filename pattern
|
| 121 |
+
matching `fn`. The formatter is given the `options` at its instantiation.
|
| 122 |
+
|
| 123 |
+
Will raise :exc:`pygments.util.ClassNotFound` if no formatter for that filename
|
| 124 |
+
is found.
|
| 125 |
+
"""
|
| 126 |
+
fn = basename(fn)
|
| 127 |
+
for modname, name, _, filenames, _ in FORMATTERS.values():
|
| 128 |
+
for filename in filenames:
|
| 129 |
+
if _fn_matches(fn, filename):
|
| 130 |
+
if name not in _formatter_cache:
|
| 131 |
+
_load_formatters(modname)
|
| 132 |
+
return _formatter_cache[name](**options)
|
| 133 |
+
for _name, cls in find_plugin_formatters():
|
| 134 |
+
for filename in cls.filenames:
|
| 135 |
+
if _fn_matches(fn, filename):
|
| 136 |
+
return cls(**options)
|
| 137 |
+
raise ClassNotFound(f"no formatter found for file name {fn!r}")
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class _automodule(types.ModuleType):
|
| 141 |
+
"""Automatically import formatters."""
|
| 142 |
+
|
| 143 |
+
def __getattr__(self, name):
|
| 144 |
+
info = FORMATTERS.get(name)
|
| 145 |
+
if info:
|
| 146 |
+
_load_formatters(info[0])
|
| 147 |
+
cls = _formatter_cache[info[1]]
|
| 148 |
+
setattr(self, name, cls)
|
| 149 |
+
return cls
|
| 150 |
+
raise AttributeError(name)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
oldmod = sys.modules[__name__]
|
| 154 |
+
newmod = _automodule(__name__)
|
| 155 |
+
newmod.__dict__.update(oldmod.__dict__)
|
| 156 |
+
sys.modules[__name__] = newmod
|
| 157 |
+
del newmod.newmod, newmod.oldmod, newmod.sys, newmod.types
|
lib/python3.12/site-packages/pygments/formatters/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (6.86 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/formatters/__pycache__/_mapping.cpython-312.pyc
ADDED
|
Binary file (4.21 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/formatters/__pycache__/bbcode.cpython-312.pyc
ADDED
|
Binary file (4.19 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/formatters/__pycache__/groff.cpython-312.pyc
ADDED
|
Binary file (7.27 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/formatters/__pycache__/html.cpython-312.pyc
ADDED
|
Binary file (41.3 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/formatters/__pycache__/img.cpython-312.pyc
ADDED
|
Binary file (28.6 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/formatters/__pycache__/irc.cpython-312.pyc
ADDED
|
Binary file (6.02 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/formatters/__pycache__/latex.cpython-312.pyc
ADDED
|
Binary file (20.1 kB). View file
|
|
|
lib/python3.12/site-packages/pygments/formatters/__pycache__/other.cpython-312.pyc
ADDED
|
Binary file (6.83 kB). View file
|
|
|