Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Prism/LLaDA/LLaDA_Prism/.venv/lib/python3.12/site-packages/_multiprocess/__pycache__/__init__.cpython-312.pyc +0 -0
- Prism/LLaDA/LLaDA_Prism/.venv/lib/python3.12/site-packages/urllib3-2.0.7.dist-info/licenses/LICENSE.txt +21 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/__pycache__/__init__.cpython-312.pyc +0 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/__pycache__/__main__.cpython-312.pyc +0 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/__pycache__/__pip-runner__.cpython-312.pyc +0 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/__init__.py +18 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/build_env.py +322 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/cache.py +290 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/__init__.py +132 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/cache.py +228 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/check.py +67 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/completion.py +130 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/debug.py +201 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/freeze.py +109 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/hash.py +59 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/index.py +139 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/install.py +784 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/list.py +375 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/search.py +172 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/show.py +224 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/wheel.py +182 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/configuration.py +383 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/exceptions.py +809 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/index/__init__.py +2 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/index/collector.py +494 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/index/package_finder.py +1029 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/index/sources.py +284 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/locations/__init__.py +456 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/locations/_distutils.py +172 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/locations/_sysconfig.py +214 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/locations/base.py +81 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/main.py +12 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/metadata/__init__.py +128 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/metadata/_json.py +86 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/metadata/base.py +688 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/metadata/pkg_resources.py +301 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/__init__.py +2 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/candidate.py +25 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/direct_url.py +224 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/format_control.py +78 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/index.py +28 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/installation_report.py +56 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/link.py +604 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/scheme.py +25 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/search_scope.py +127 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/selection_prefs.py +53 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/target_python.py +121 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/wheel.py +118 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/pyproject.py +185 -0
- URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/resolution/__init__.py +0 -0
Prism/LLaDA/LLaDA_Prism/.venv/lib/python3.12/site-packages/_multiprocess/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (259 Bytes). View file
|
|
|
Prism/LLaDA/LLaDA_Prism/.venv/lib/python3.12/site-packages/urllib3-2.0.7.dist-info/licenses/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2008-2020 Andrey Petrov and contributors.
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (706 Bytes). View file
|
|
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/__pycache__/__main__.cpython-312.pyc
ADDED
|
Binary file (860 Bytes). View file
|
|
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/__pycache__/__pip-runner__.cpython-312.pyc
ADDED
|
Binary file (2.22 kB). View file
|
|
|
URSA/.venv_ursa/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)
|
URSA/.venv_ursa/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()
|
URSA/.venv_ursa/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")
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/__init__.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Package containing all pip commands
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import importlib
|
| 6 |
+
from collections import namedtuple
|
| 7 |
+
from typing import Any, Dict, Optional
|
| 8 |
+
|
| 9 |
+
from pip._internal.cli.base_command import Command
|
| 10 |
+
|
| 11 |
+
CommandInfo = namedtuple("CommandInfo", "module_path, class_name, summary")
|
| 12 |
+
|
| 13 |
+
# This dictionary does a bunch of heavy lifting for help output:
|
| 14 |
+
# - Enables avoiding additional (costly) imports for presenting `--help`.
|
| 15 |
+
# - The ordering matters for help display.
|
| 16 |
+
#
|
| 17 |
+
# Even though the module path starts with the same "pip._internal.commands"
|
| 18 |
+
# prefix, the full path makes testing easier (specifically when modifying
|
| 19 |
+
# `commands_dict` in test setup / teardown).
|
| 20 |
+
commands_dict: Dict[str, CommandInfo] = {
|
| 21 |
+
"install": CommandInfo(
|
| 22 |
+
"pip._internal.commands.install",
|
| 23 |
+
"InstallCommand",
|
| 24 |
+
"Install packages.",
|
| 25 |
+
),
|
| 26 |
+
"download": CommandInfo(
|
| 27 |
+
"pip._internal.commands.download",
|
| 28 |
+
"DownloadCommand",
|
| 29 |
+
"Download packages.",
|
| 30 |
+
),
|
| 31 |
+
"uninstall": CommandInfo(
|
| 32 |
+
"pip._internal.commands.uninstall",
|
| 33 |
+
"UninstallCommand",
|
| 34 |
+
"Uninstall packages.",
|
| 35 |
+
),
|
| 36 |
+
"freeze": CommandInfo(
|
| 37 |
+
"pip._internal.commands.freeze",
|
| 38 |
+
"FreezeCommand",
|
| 39 |
+
"Output installed packages in requirements format.",
|
| 40 |
+
),
|
| 41 |
+
"inspect": CommandInfo(
|
| 42 |
+
"pip._internal.commands.inspect",
|
| 43 |
+
"InspectCommand",
|
| 44 |
+
"Inspect the python environment.",
|
| 45 |
+
),
|
| 46 |
+
"list": CommandInfo(
|
| 47 |
+
"pip._internal.commands.list",
|
| 48 |
+
"ListCommand",
|
| 49 |
+
"List installed packages.",
|
| 50 |
+
),
|
| 51 |
+
"show": CommandInfo(
|
| 52 |
+
"pip._internal.commands.show",
|
| 53 |
+
"ShowCommand",
|
| 54 |
+
"Show information about installed packages.",
|
| 55 |
+
),
|
| 56 |
+
"check": CommandInfo(
|
| 57 |
+
"pip._internal.commands.check",
|
| 58 |
+
"CheckCommand",
|
| 59 |
+
"Verify installed packages have compatible dependencies.",
|
| 60 |
+
),
|
| 61 |
+
"config": CommandInfo(
|
| 62 |
+
"pip._internal.commands.configuration",
|
| 63 |
+
"ConfigurationCommand",
|
| 64 |
+
"Manage local and global configuration.",
|
| 65 |
+
),
|
| 66 |
+
"search": CommandInfo(
|
| 67 |
+
"pip._internal.commands.search",
|
| 68 |
+
"SearchCommand",
|
| 69 |
+
"Search PyPI for packages.",
|
| 70 |
+
),
|
| 71 |
+
"cache": CommandInfo(
|
| 72 |
+
"pip._internal.commands.cache",
|
| 73 |
+
"CacheCommand",
|
| 74 |
+
"Inspect and manage pip's wheel cache.",
|
| 75 |
+
),
|
| 76 |
+
"index": CommandInfo(
|
| 77 |
+
"pip._internal.commands.index",
|
| 78 |
+
"IndexCommand",
|
| 79 |
+
"Inspect information available from package indexes.",
|
| 80 |
+
),
|
| 81 |
+
"wheel": CommandInfo(
|
| 82 |
+
"pip._internal.commands.wheel",
|
| 83 |
+
"WheelCommand",
|
| 84 |
+
"Build wheels from your requirements.",
|
| 85 |
+
),
|
| 86 |
+
"hash": CommandInfo(
|
| 87 |
+
"pip._internal.commands.hash",
|
| 88 |
+
"HashCommand",
|
| 89 |
+
"Compute hashes of package archives.",
|
| 90 |
+
),
|
| 91 |
+
"completion": CommandInfo(
|
| 92 |
+
"pip._internal.commands.completion",
|
| 93 |
+
"CompletionCommand",
|
| 94 |
+
"A helper command used for command completion.",
|
| 95 |
+
),
|
| 96 |
+
"debug": CommandInfo(
|
| 97 |
+
"pip._internal.commands.debug",
|
| 98 |
+
"DebugCommand",
|
| 99 |
+
"Show information useful for debugging.",
|
| 100 |
+
),
|
| 101 |
+
"help": CommandInfo(
|
| 102 |
+
"pip._internal.commands.help",
|
| 103 |
+
"HelpCommand",
|
| 104 |
+
"Show help for commands.",
|
| 105 |
+
),
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def create_command(name: str, **kwargs: Any) -> Command:
|
| 110 |
+
"""
|
| 111 |
+
Create an instance of the Command class with the given name.
|
| 112 |
+
"""
|
| 113 |
+
module_path, class_name, summary = commands_dict[name]
|
| 114 |
+
module = importlib.import_module(module_path)
|
| 115 |
+
command_class = getattr(module, class_name)
|
| 116 |
+
command = command_class(name=name, summary=summary, **kwargs)
|
| 117 |
+
|
| 118 |
+
return command
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def get_similar_commands(name: str) -> Optional[str]:
|
| 122 |
+
"""Command name auto-correct."""
|
| 123 |
+
from difflib import get_close_matches
|
| 124 |
+
|
| 125 |
+
name = name.lower()
|
| 126 |
+
|
| 127 |
+
close_commands = get_close_matches(name, commands_dict.keys())
|
| 128 |
+
|
| 129 |
+
if close_commands:
|
| 130 |
+
return close_commands[0]
|
| 131 |
+
else:
|
| 132 |
+
return None
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/cache.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import textwrap
|
| 3 |
+
from optparse import Values
|
| 4 |
+
from typing import Any, List
|
| 5 |
+
|
| 6 |
+
from pip._internal.cli.base_command import Command
|
| 7 |
+
from pip._internal.cli.status_codes import ERROR, SUCCESS
|
| 8 |
+
from pip._internal.exceptions import CommandError, PipError
|
| 9 |
+
from pip._internal.utils import filesystem
|
| 10 |
+
from pip._internal.utils.logging import getLogger
|
| 11 |
+
from pip._internal.utils.misc import format_size
|
| 12 |
+
|
| 13 |
+
logger = getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class CacheCommand(Command):
|
| 17 |
+
"""
|
| 18 |
+
Inspect and manage pip's wheel cache.
|
| 19 |
+
|
| 20 |
+
Subcommands:
|
| 21 |
+
|
| 22 |
+
- dir: Show the cache directory.
|
| 23 |
+
- info: Show information about the cache.
|
| 24 |
+
- list: List filenames of packages stored in the cache.
|
| 25 |
+
- remove: Remove one or more package from the cache.
|
| 26 |
+
- purge: Remove all items from the cache.
|
| 27 |
+
|
| 28 |
+
``<pattern>`` can be a glob expression or a package name.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
ignore_require_venv = True
|
| 32 |
+
usage = """
|
| 33 |
+
%prog dir
|
| 34 |
+
%prog info
|
| 35 |
+
%prog list [<pattern>] [--format=[human, abspath]]
|
| 36 |
+
%prog remove <pattern>
|
| 37 |
+
%prog purge
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
def add_options(self) -> None:
|
| 41 |
+
self.cmd_opts.add_option(
|
| 42 |
+
"--format",
|
| 43 |
+
action="store",
|
| 44 |
+
dest="list_format",
|
| 45 |
+
default="human",
|
| 46 |
+
choices=("human", "abspath"),
|
| 47 |
+
help="Select the output format among: human (default) or abspath",
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 51 |
+
|
| 52 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 53 |
+
handlers = {
|
| 54 |
+
"dir": self.get_cache_dir,
|
| 55 |
+
"info": self.get_cache_info,
|
| 56 |
+
"list": self.list_cache_items,
|
| 57 |
+
"remove": self.remove_cache_items,
|
| 58 |
+
"purge": self.purge_cache,
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
if not options.cache_dir:
|
| 62 |
+
logger.error("pip cache commands can not function since cache is disabled.")
|
| 63 |
+
return ERROR
|
| 64 |
+
|
| 65 |
+
# Determine action
|
| 66 |
+
if not args or args[0] not in handlers:
|
| 67 |
+
logger.error(
|
| 68 |
+
"Need an action (%s) to perform.",
|
| 69 |
+
", ".join(sorted(handlers)),
|
| 70 |
+
)
|
| 71 |
+
return ERROR
|
| 72 |
+
|
| 73 |
+
action = args[0]
|
| 74 |
+
|
| 75 |
+
# Error handling happens here, not in the action-handlers.
|
| 76 |
+
try:
|
| 77 |
+
handlers[action](options, args[1:])
|
| 78 |
+
except PipError as e:
|
| 79 |
+
logger.error(e.args[0])
|
| 80 |
+
return ERROR
|
| 81 |
+
|
| 82 |
+
return SUCCESS
|
| 83 |
+
|
| 84 |
+
def get_cache_dir(self, options: Values, args: List[Any]) -> None:
|
| 85 |
+
if args:
|
| 86 |
+
raise CommandError("Too many arguments")
|
| 87 |
+
|
| 88 |
+
logger.info(options.cache_dir)
|
| 89 |
+
|
| 90 |
+
def get_cache_info(self, options: Values, args: List[Any]) -> None:
|
| 91 |
+
if args:
|
| 92 |
+
raise CommandError("Too many arguments")
|
| 93 |
+
|
| 94 |
+
num_http_files = len(self._find_http_files(options))
|
| 95 |
+
num_packages = len(self._find_wheels(options, "*"))
|
| 96 |
+
|
| 97 |
+
http_cache_location = self._cache_dir(options, "http-v2")
|
| 98 |
+
old_http_cache_location = self._cache_dir(options, "http")
|
| 99 |
+
wheels_cache_location = self._cache_dir(options, "wheels")
|
| 100 |
+
http_cache_size = filesystem.format_size(
|
| 101 |
+
filesystem.directory_size(http_cache_location)
|
| 102 |
+
+ filesystem.directory_size(old_http_cache_location)
|
| 103 |
+
)
|
| 104 |
+
wheels_cache_size = filesystem.format_directory_size(wheels_cache_location)
|
| 105 |
+
|
| 106 |
+
message = (
|
| 107 |
+
textwrap.dedent(
|
| 108 |
+
"""
|
| 109 |
+
Package index page cache location (pip v23.3+): {http_cache_location}
|
| 110 |
+
Package index page cache location (older pips): {old_http_cache_location}
|
| 111 |
+
Package index page cache size: {http_cache_size}
|
| 112 |
+
Number of HTTP files: {num_http_files}
|
| 113 |
+
Locally built wheels location: {wheels_cache_location}
|
| 114 |
+
Locally built wheels size: {wheels_cache_size}
|
| 115 |
+
Number of locally built wheels: {package_count}
|
| 116 |
+
""" # noqa: E501
|
| 117 |
+
)
|
| 118 |
+
.format(
|
| 119 |
+
http_cache_location=http_cache_location,
|
| 120 |
+
old_http_cache_location=old_http_cache_location,
|
| 121 |
+
http_cache_size=http_cache_size,
|
| 122 |
+
num_http_files=num_http_files,
|
| 123 |
+
wheels_cache_location=wheels_cache_location,
|
| 124 |
+
package_count=num_packages,
|
| 125 |
+
wheels_cache_size=wheels_cache_size,
|
| 126 |
+
)
|
| 127 |
+
.strip()
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
logger.info(message)
|
| 131 |
+
|
| 132 |
+
def list_cache_items(self, options: Values, args: List[Any]) -> None:
|
| 133 |
+
if len(args) > 1:
|
| 134 |
+
raise CommandError("Too many arguments")
|
| 135 |
+
|
| 136 |
+
if args:
|
| 137 |
+
pattern = args[0]
|
| 138 |
+
else:
|
| 139 |
+
pattern = "*"
|
| 140 |
+
|
| 141 |
+
files = self._find_wheels(options, pattern)
|
| 142 |
+
if options.list_format == "human":
|
| 143 |
+
self.format_for_human(files)
|
| 144 |
+
else:
|
| 145 |
+
self.format_for_abspath(files)
|
| 146 |
+
|
| 147 |
+
def format_for_human(self, files: List[str]) -> None:
|
| 148 |
+
if not files:
|
| 149 |
+
logger.info("No locally built wheels cached.")
|
| 150 |
+
return
|
| 151 |
+
|
| 152 |
+
results = []
|
| 153 |
+
for filename in files:
|
| 154 |
+
wheel = os.path.basename(filename)
|
| 155 |
+
size = filesystem.format_file_size(filename)
|
| 156 |
+
results.append(f" - {wheel} ({size})")
|
| 157 |
+
logger.info("Cache contents:\n")
|
| 158 |
+
logger.info("\n".join(sorted(results)))
|
| 159 |
+
|
| 160 |
+
def format_for_abspath(self, files: List[str]) -> None:
|
| 161 |
+
if files:
|
| 162 |
+
logger.info("\n".join(sorted(files)))
|
| 163 |
+
|
| 164 |
+
def remove_cache_items(self, options: Values, args: List[Any]) -> None:
|
| 165 |
+
if len(args) > 1:
|
| 166 |
+
raise CommandError("Too many arguments")
|
| 167 |
+
|
| 168 |
+
if not args:
|
| 169 |
+
raise CommandError("Please provide a pattern")
|
| 170 |
+
|
| 171 |
+
files = self._find_wheels(options, args[0])
|
| 172 |
+
|
| 173 |
+
no_matching_msg = "No matching packages"
|
| 174 |
+
if args[0] == "*":
|
| 175 |
+
# Only fetch http files if no specific pattern given
|
| 176 |
+
files += self._find_http_files(options)
|
| 177 |
+
else:
|
| 178 |
+
# Add the pattern to the log message
|
| 179 |
+
no_matching_msg += f' for pattern "{args[0]}"'
|
| 180 |
+
|
| 181 |
+
if not files:
|
| 182 |
+
logger.warning(no_matching_msg)
|
| 183 |
+
|
| 184 |
+
bytes_removed = 0
|
| 185 |
+
for filename in files:
|
| 186 |
+
bytes_removed += os.stat(filename).st_size
|
| 187 |
+
os.unlink(filename)
|
| 188 |
+
logger.verbose("Removed %s", filename)
|
| 189 |
+
logger.info("Files removed: %s (%s)", len(files), format_size(bytes_removed))
|
| 190 |
+
|
| 191 |
+
def purge_cache(self, options: Values, args: List[Any]) -> None:
|
| 192 |
+
if args:
|
| 193 |
+
raise CommandError("Too many arguments")
|
| 194 |
+
|
| 195 |
+
return self.remove_cache_items(options, ["*"])
|
| 196 |
+
|
| 197 |
+
def _cache_dir(self, options: Values, subdir: str) -> str:
|
| 198 |
+
return os.path.join(options.cache_dir, subdir)
|
| 199 |
+
|
| 200 |
+
def _find_http_files(self, options: Values) -> List[str]:
|
| 201 |
+
old_http_dir = self._cache_dir(options, "http")
|
| 202 |
+
new_http_dir = self._cache_dir(options, "http-v2")
|
| 203 |
+
return filesystem.find_files(old_http_dir, "*") + filesystem.find_files(
|
| 204 |
+
new_http_dir, "*"
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
def _find_wheels(self, options: Values, pattern: str) -> List[str]:
|
| 208 |
+
wheel_dir = self._cache_dir(options, "wheels")
|
| 209 |
+
|
| 210 |
+
# The wheel filename format, as specified in PEP 427, is:
|
| 211 |
+
# {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
|
| 212 |
+
#
|
| 213 |
+
# Additionally, non-alphanumeric values in the distribution are
|
| 214 |
+
# normalized to underscores (_), meaning hyphens can never occur
|
| 215 |
+
# before `-{version}`.
|
| 216 |
+
#
|
| 217 |
+
# Given that information:
|
| 218 |
+
# - If the pattern we're given contains a hyphen (-), the user is
|
| 219 |
+
# providing at least the version. Thus, we can just append `*.whl`
|
| 220 |
+
# to match the rest of it.
|
| 221 |
+
# - If the pattern we're given doesn't contain a hyphen (-), the
|
| 222 |
+
# user is only providing the name. Thus, we append `-*.whl` to
|
| 223 |
+
# match the hyphen before the version, followed by anything else.
|
| 224 |
+
#
|
| 225 |
+
# PEP 427: https://www.python.org/dev/peps/pep-0427/
|
| 226 |
+
pattern = pattern + ("*.whl" if "-" in pattern else "-*.whl")
|
| 227 |
+
|
| 228 |
+
return filesystem.find_files(wheel_dir, pattern)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/check.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from optparse import Values
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
from pip._internal.cli.base_command import Command
|
| 6 |
+
from pip._internal.cli.status_codes import ERROR, SUCCESS
|
| 7 |
+
from pip._internal.metadata import get_default_environment
|
| 8 |
+
from pip._internal.operations.check import (
|
| 9 |
+
check_package_set,
|
| 10 |
+
check_unsupported,
|
| 11 |
+
create_package_set_from_installed,
|
| 12 |
+
)
|
| 13 |
+
from pip._internal.utils.compatibility_tags import get_supported
|
| 14 |
+
from pip._internal.utils.misc import write_output
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class CheckCommand(Command):
|
| 20 |
+
"""Verify installed packages have compatible dependencies."""
|
| 21 |
+
|
| 22 |
+
ignore_require_venv = True
|
| 23 |
+
usage = """
|
| 24 |
+
%prog [options]"""
|
| 25 |
+
|
| 26 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 27 |
+
package_set, parsing_probs = create_package_set_from_installed()
|
| 28 |
+
missing, conflicting = check_package_set(package_set)
|
| 29 |
+
unsupported = list(
|
| 30 |
+
check_unsupported(
|
| 31 |
+
get_default_environment().iter_installed_distributions(),
|
| 32 |
+
get_supported(),
|
| 33 |
+
)
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
for project_name in missing:
|
| 37 |
+
version = package_set[project_name].version
|
| 38 |
+
for dependency in missing[project_name]:
|
| 39 |
+
write_output(
|
| 40 |
+
"%s %s requires %s, which is not installed.",
|
| 41 |
+
project_name,
|
| 42 |
+
version,
|
| 43 |
+
dependency[0],
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
for project_name in conflicting:
|
| 47 |
+
version = package_set[project_name].version
|
| 48 |
+
for dep_name, dep_version, req in conflicting[project_name]:
|
| 49 |
+
write_output(
|
| 50 |
+
"%s %s has requirement %s, but you have %s %s.",
|
| 51 |
+
project_name,
|
| 52 |
+
version,
|
| 53 |
+
req,
|
| 54 |
+
dep_name,
|
| 55 |
+
dep_version,
|
| 56 |
+
)
|
| 57 |
+
for package in unsupported:
|
| 58 |
+
write_output(
|
| 59 |
+
"%s %s is not supported on this platform",
|
| 60 |
+
package.raw_name,
|
| 61 |
+
package.version,
|
| 62 |
+
)
|
| 63 |
+
if missing or conflicting or parsing_probs or unsupported:
|
| 64 |
+
return ERROR
|
| 65 |
+
else:
|
| 66 |
+
write_output("No broken requirements found.")
|
| 67 |
+
return SUCCESS
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/completion.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import textwrap
|
| 3 |
+
from optparse import Values
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
from pip._internal.cli.base_command import Command
|
| 7 |
+
from pip._internal.cli.status_codes import SUCCESS
|
| 8 |
+
from pip._internal.utils.misc import get_prog
|
| 9 |
+
|
| 10 |
+
BASE_COMPLETION = """
|
| 11 |
+
# pip {shell} completion start{script}# pip {shell} completion end
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
COMPLETION_SCRIPTS = {
|
| 15 |
+
"bash": """
|
| 16 |
+
_pip_completion()
|
| 17 |
+
{{
|
| 18 |
+
COMPREPLY=( $( COMP_WORDS="${{COMP_WORDS[*]}}" \\
|
| 19 |
+
COMP_CWORD=$COMP_CWORD \\
|
| 20 |
+
PIP_AUTO_COMPLETE=1 $1 2>/dev/null ) )
|
| 21 |
+
}}
|
| 22 |
+
complete -o default -F _pip_completion {prog}
|
| 23 |
+
""",
|
| 24 |
+
"zsh": """
|
| 25 |
+
#compdef -P pip[0-9.]#
|
| 26 |
+
__pip() {{
|
| 27 |
+
compadd $( COMP_WORDS="$words[*]" \\
|
| 28 |
+
COMP_CWORD=$((CURRENT-1)) \\
|
| 29 |
+
PIP_AUTO_COMPLETE=1 $words[1] 2>/dev/null )
|
| 30 |
+
}}
|
| 31 |
+
if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
|
| 32 |
+
# autoload from fpath, call function directly
|
| 33 |
+
__pip "$@"
|
| 34 |
+
else
|
| 35 |
+
# eval/source/. command, register function for later
|
| 36 |
+
compdef __pip -P 'pip[0-9.]#'
|
| 37 |
+
fi
|
| 38 |
+
""",
|
| 39 |
+
"fish": """
|
| 40 |
+
function __fish_complete_pip
|
| 41 |
+
set -lx COMP_WORDS (commandline -o) ""
|
| 42 |
+
set -lx COMP_CWORD ( \\
|
| 43 |
+
math (contains -i -- (commandline -t) $COMP_WORDS)-1 \\
|
| 44 |
+
)
|
| 45 |
+
set -lx PIP_AUTO_COMPLETE 1
|
| 46 |
+
string split \\ -- (eval $COMP_WORDS[1])
|
| 47 |
+
end
|
| 48 |
+
complete -fa "(__fish_complete_pip)" -c {prog}
|
| 49 |
+
""",
|
| 50 |
+
"powershell": """
|
| 51 |
+
if ((Test-Path Function:\\TabExpansion) -and -not `
|
| 52 |
+
(Test-Path Function:\\_pip_completeBackup)) {{
|
| 53 |
+
Rename-Item Function:\\TabExpansion _pip_completeBackup
|
| 54 |
+
}}
|
| 55 |
+
function TabExpansion($line, $lastWord) {{
|
| 56 |
+
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
|
| 57 |
+
if ($lastBlock.StartsWith("{prog} ")) {{
|
| 58 |
+
$Env:COMP_WORDS=$lastBlock
|
| 59 |
+
$Env:COMP_CWORD=$lastBlock.Split().Length - 1
|
| 60 |
+
$Env:PIP_AUTO_COMPLETE=1
|
| 61 |
+
(& {prog}).Split()
|
| 62 |
+
Remove-Item Env:COMP_WORDS
|
| 63 |
+
Remove-Item Env:COMP_CWORD
|
| 64 |
+
Remove-Item Env:PIP_AUTO_COMPLETE
|
| 65 |
+
}}
|
| 66 |
+
elseif (Test-Path Function:\\_pip_completeBackup) {{
|
| 67 |
+
# Fall back on existing tab expansion
|
| 68 |
+
_pip_completeBackup $line $lastWord
|
| 69 |
+
}}
|
| 70 |
+
}}
|
| 71 |
+
""",
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class CompletionCommand(Command):
|
| 76 |
+
"""A helper command to be used for command completion."""
|
| 77 |
+
|
| 78 |
+
ignore_require_venv = True
|
| 79 |
+
|
| 80 |
+
def add_options(self) -> None:
|
| 81 |
+
self.cmd_opts.add_option(
|
| 82 |
+
"--bash",
|
| 83 |
+
"-b",
|
| 84 |
+
action="store_const",
|
| 85 |
+
const="bash",
|
| 86 |
+
dest="shell",
|
| 87 |
+
help="Emit completion code for bash",
|
| 88 |
+
)
|
| 89 |
+
self.cmd_opts.add_option(
|
| 90 |
+
"--zsh",
|
| 91 |
+
"-z",
|
| 92 |
+
action="store_const",
|
| 93 |
+
const="zsh",
|
| 94 |
+
dest="shell",
|
| 95 |
+
help="Emit completion code for zsh",
|
| 96 |
+
)
|
| 97 |
+
self.cmd_opts.add_option(
|
| 98 |
+
"--fish",
|
| 99 |
+
"-f",
|
| 100 |
+
action="store_const",
|
| 101 |
+
const="fish",
|
| 102 |
+
dest="shell",
|
| 103 |
+
help="Emit completion code for fish",
|
| 104 |
+
)
|
| 105 |
+
self.cmd_opts.add_option(
|
| 106 |
+
"--powershell",
|
| 107 |
+
"-p",
|
| 108 |
+
action="store_const",
|
| 109 |
+
const="powershell",
|
| 110 |
+
dest="shell",
|
| 111 |
+
help="Emit completion code for powershell",
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 115 |
+
|
| 116 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 117 |
+
"""Prints the completion code of the given shell"""
|
| 118 |
+
shells = COMPLETION_SCRIPTS.keys()
|
| 119 |
+
shell_options = ["--" + shell for shell in sorted(shells)]
|
| 120 |
+
if options.shell in shells:
|
| 121 |
+
script = textwrap.dedent(
|
| 122 |
+
COMPLETION_SCRIPTS.get(options.shell, "").format(prog=get_prog())
|
| 123 |
+
)
|
| 124 |
+
print(BASE_COMPLETION.format(script=script, shell=options.shell))
|
| 125 |
+
return SUCCESS
|
| 126 |
+
else:
|
| 127 |
+
sys.stderr.write(
|
| 128 |
+
"ERROR: You must pass {}\n".format(" or ".join(shell_options))
|
| 129 |
+
)
|
| 130 |
+
return SUCCESS
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/debug.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import locale
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
from optparse import Values
|
| 6 |
+
from types import ModuleType
|
| 7 |
+
from typing import Any, Dict, List, Optional
|
| 8 |
+
|
| 9 |
+
import pip._vendor
|
| 10 |
+
from pip._vendor.certifi import where
|
| 11 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 12 |
+
|
| 13 |
+
from pip._internal.cli import cmdoptions
|
| 14 |
+
from pip._internal.cli.base_command import Command
|
| 15 |
+
from pip._internal.cli.cmdoptions import make_target_python
|
| 16 |
+
from pip._internal.cli.status_codes import SUCCESS
|
| 17 |
+
from pip._internal.configuration import Configuration
|
| 18 |
+
from pip._internal.metadata import get_environment
|
| 19 |
+
from pip._internal.utils.compat import open_text_resource
|
| 20 |
+
from pip._internal.utils.logging import indent_log
|
| 21 |
+
from pip._internal.utils.misc import get_pip_version
|
| 22 |
+
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def show_value(name: str, value: Any) -> None:
|
| 27 |
+
logger.info("%s: %s", name, value)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def show_sys_implementation() -> None:
|
| 31 |
+
logger.info("sys.implementation:")
|
| 32 |
+
implementation_name = sys.implementation.name
|
| 33 |
+
with indent_log():
|
| 34 |
+
show_value("name", implementation_name)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def create_vendor_txt_map() -> Dict[str, str]:
|
| 38 |
+
with open_text_resource("pip._vendor", "vendor.txt") as f:
|
| 39 |
+
# Purge non version specifying lines.
|
| 40 |
+
# Also, remove any space prefix or suffixes (including comments).
|
| 41 |
+
lines = [
|
| 42 |
+
line.strip().split(" ", 1)[0] for line in f.readlines() if "==" in line
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
# Transform into "module" -> version dict.
|
| 46 |
+
return dict(line.split("==", 1) for line in lines)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_module_from_module_name(module_name: str) -> Optional[ModuleType]:
|
| 50 |
+
# Module name can be uppercase in vendor.txt for some reason...
|
| 51 |
+
module_name = module_name.lower().replace("-", "_")
|
| 52 |
+
# PATCH: setuptools is actually only pkg_resources.
|
| 53 |
+
if module_name == "setuptools":
|
| 54 |
+
module_name = "pkg_resources"
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
__import__(f"pip._vendor.{module_name}", globals(), locals(), level=0)
|
| 58 |
+
return getattr(pip._vendor, module_name)
|
| 59 |
+
except ImportError:
|
| 60 |
+
# We allow 'truststore' to fail to import due
|
| 61 |
+
# to being unavailable on Python 3.9 and earlier.
|
| 62 |
+
if module_name == "truststore" and sys.version_info < (3, 10):
|
| 63 |
+
return None
|
| 64 |
+
raise
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def get_vendor_version_from_module(module_name: str) -> Optional[str]:
|
| 68 |
+
module = get_module_from_module_name(module_name)
|
| 69 |
+
version = getattr(module, "__version__", None)
|
| 70 |
+
|
| 71 |
+
if module and not version:
|
| 72 |
+
# Try to find version in debundled module info.
|
| 73 |
+
assert module.__file__ is not None
|
| 74 |
+
env = get_environment([os.path.dirname(module.__file__)])
|
| 75 |
+
dist = env.get_distribution(module_name)
|
| 76 |
+
if dist:
|
| 77 |
+
version = str(dist.version)
|
| 78 |
+
|
| 79 |
+
return version
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def show_actual_vendor_versions(vendor_txt_versions: Dict[str, str]) -> None:
|
| 83 |
+
"""Log the actual version and print extra info if there is
|
| 84 |
+
a conflict or if the actual version could not be imported.
|
| 85 |
+
"""
|
| 86 |
+
for module_name, expected_version in vendor_txt_versions.items():
|
| 87 |
+
extra_message = ""
|
| 88 |
+
actual_version = get_vendor_version_from_module(module_name)
|
| 89 |
+
if not actual_version:
|
| 90 |
+
extra_message = (
|
| 91 |
+
" (Unable to locate actual module version, using"
|
| 92 |
+
" vendor.txt specified version)"
|
| 93 |
+
)
|
| 94 |
+
actual_version = expected_version
|
| 95 |
+
elif parse_version(actual_version) != parse_version(expected_version):
|
| 96 |
+
extra_message = (
|
| 97 |
+
" (CONFLICT: vendor.txt suggests version should"
|
| 98 |
+
f" be {expected_version})"
|
| 99 |
+
)
|
| 100 |
+
logger.info("%s==%s%s", module_name, actual_version, extra_message)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def show_vendor_versions() -> None:
|
| 104 |
+
logger.info("vendored library versions:")
|
| 105 |
+
|
| 106 |
+
vendor_txt_versions = create_vendor_txt_map()
|
| 107 |
+
with indent_log():
|
| 108 |
+
show_actual_vendor_versions(vendor_txt_versions)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def show_tags(options: Values) -> None:
|
| 112 |
+
tag_limit = 10
|
| 113 |
+
|
| 114 |
+
target_python = make_target_python(options)
|
| 115 |
+
tags = target_python.get_sorted_tags()
|
| 116 |
+
|
| 117 |
+
# Display the target options that were explicitly provided.
|
| 118 |
+
formatted_target = target_python.format_given()
|
| 119 |
+
suffix = ""
|
| 120 |
+
if formatted_target:
|
| 121 |
+
suffix = f" (target: {formatted_target})"
|
| 122 |
+
|
| 123 |
+
msg = f"Compatible tags: {len(tags)}{suffix}"
|
| 124 |
+
logger.info(msg)
|
| 125 |
+
|
| 126 |
+
if options.verbose < 1 and len(tags) > tag_limit:
|
| 127 |
+
tags_limited = True
|
| 128 |
+
tags = tags[:tag_limit]
|
| 129 |
+
else:
|
| 130 |
+
tags_limited = False
|
| 131 |
+
|
| 132 |
+
with indent_log():
|
| 133 |
+
for tag in tags:
|
| 134 |
+
logger.info(str(tag))
|
| 135 |
+
|
| 136 |
+
if tags_limited:
|
| 137 |
+
msg = f"...\n[First {tag_limit} tags shown. Pass --verbose to show all.]"
|
| 138 |
+
logger.info(msg)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def ca_bundle_info(config: Configuration) -> str:
|
| 142 |
+
levels = {key.split(".", 1)[0] for key, _ in config.items()}
|
| 143 |
+
if not levels:
|
| 144 |
+
return "Not specified"
|
| 145 |
+
|
| 146 |
+
levels_that_override_global = ["install", "wheel", "download"]
|
| 147 |
+
global_overriding_level = [
|
| 148 |
+
level for level in levels if level in levels_that_override_global
|
| 149 |
+
]
|
| 150 |
+
if not global_overriding_level:
|
| 151 |
+
return "global"
|
| 152 |
+
|
| 153 |
+
if "global" in levels:
|
| 154 |
+
levels.remove("global")
|
| 155 |
+
return ", ".join(levels)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class DebugCommand(Command):
|
| 159 |
+
"""
|
| 160 |
+
Display debug information.
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
usage = """
|
| 164 |
+
%prog <options>"""
|
| 165 |
+
ignore_require_venv = True
|
| 166 |
+
|
| 167 |
+
def add_options(self) -> None:
|
| 168 |
+
cmdoptions.add_target_python_options(self.cmd_opts)
|
| 169 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 170 |
+
self.parser.config.load()
|
| 171 |
+
|
| 172 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 173 |
+
logger.warning(
|
| 174 |
+
"This command is only meant for debugging. "
|
| 175 |
+
"Do not use this with automation for parsing and getting these "
|
| 176 |
+
"details, since the output and options of this command may "
|
| 177 |
+
"change without notice."
|
| 178 |
+
)
|
| 179 |
+
show_value("pip version", get_pip_version())
|
| 180 |
+
show_value("sys.version", sys.version)
|
| 181 |
+
show_value("sys.executable", sys.executable)
|
| 182 |
+
show_value("sys.getdefaultencoding", sys.getdefaultencoding())
|
| 183 |
+
show_value("sys.getfilesystemencoding", sys.getfilesystemencoding())
|
| 184 |
+
show_value(
|
| 185 |
+
"locale.getpreferredencoding",
|
| 186 |
+
locale.getpreferredencoding(),
|
| 187 |
+
)
|
| 188 |
+
show_value("sys.platform", sys.platform)
|
| 189 |
+
show_sys_implementation()
|
| 190 |
+
|
| 191 |
+
show_value("'cert' config value", ca_bundle_info(self.parser.config))
|
| 192 |
+
show_value("REQUESTS_CA_BUNDLE", os.environ.get("REQUESTS_CA_BUNDLE"))
|
| 193 |
+
show_value("CURL_CA_BUNDLE", os.environ.get("CURL_CA_BUNDLE"))
|
| 194 |
+
show_value("pip._vendor.certifi.where()", where())
|
| 195 |
+
show_value("pip._vendor.DEBUNDLED", pip._vendor.DEBUNDLED)
|
| 196 |
+
|
| 197 |
+
show_vendor_versions()
|
| 198 |
+
|
| 199 |
+
show_tags(options)
|
| 200 |
+
|
| 201 |
+
return SUCCESS
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/freeze.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from optparse import Values
|
| 3 |
+
from typing import AbstractSet, List
|
| 4 |
+
|
| 5 |
+
from pip._internal.cli import cmdoptions
|
| 6 |
+
from pip._internal.cli.base_command import Command
|
| 7 |
+
from pip._internal.cli.status_codes import SUCCESS
|
| 8 |
+
from pip._internal.operations.freeze import freeze
|
| 9 |
+
from pip._internal.utils.compat import stdlib_pkgs
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _should_suppress_build_backends() -> bool:
|
| 13 |
+
return sys.version_info < (3, 12)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _dev_pkgs() -> AbstractSet[str]:
|
| 17 |
+
pkgs = {"pip"}
|
| 18 |
+
|
| 19 |
+
if _should_suppress_build_backends():
|
| 20 |
+
pkgs |= {"setuptools", "distribute", "wheel"}
|
| 21 |
+
|
| 22 |
+
return pkgs
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class FreezeCommand(Command):
|
| 26 |
+
"""
|
| 27 |
+
Output installed packages in requirements format.
|
| 28 |
+
|
| 29 |
+
packages are listed in a case-insensitive sorted order.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
ignore_require_venv = True
|
| 33 |
+
usage = """
|
| 34 |
+
%prog [options]"""
|
| 35 |
+
log_streams = ("ext://sys.stderr", "ext://sys.stderr")
|
| 36 |
+
|
| 37 |
+
def add_options(self) -> None:
|
| 38 |
+
self.cmd_opts.add_option(
|
| 39 |
+
"-r",
|
| 40 |
+
"--requirement",
|
| 41 |
+
dest="requirements",
|
| 42 |
+
action="append",
|
| 43 |
+
default=[],
|
| 44 |
+
metavar="file",
|
| 45 |
+
help=(
|
| 46 |
+
"Use the order in the given requirements file and its "
|
| 47 |
+
"comments when generating output. This option can be "
|
| 48 |
+
"used multiple times."
|
| 49 |
+
),
|
| 50 |
+
)
|
| 51 |
+
self.cmd_opts.add_option(
|
| 52 |
+
"-l",
|
| 53 |
+
"--local",
|
| 54 |
+
dest="local",
|
| 55 |
+
action="store_true",
|
| 56 |
+
default=False,
|
| 57 |
+
help=(
|
| 58 |
+
"If in a virtualenv that has global access, do not output "
|
| 59 |
+
"globally-installed packages."
|
| 60 |
+
),
|
| 61 |
+
)
|
| 62 |
+
self.cmd_opts.add_option(
|
| 63 |
+
"--user",
|
| 64 |
+
dest="user",
|
| 65 |
+
action="store_true",
|
| 66 |
+
default=False,
|
| 67 |
+
help="Only output packages installed in user-site.",
|
| 68 |
+
)
|
| 69 |
+
self.cmd_opts.add_option(cmdoptions.list_path())
|
| 70 |
+
self.cmd_opts.add_option(
|
| 71 |
+
"--all",
|
| 72 |
+
dest="freeze_all",
|
| 73 |
+
action="store_true",
|
| 74 |
+
help=(
|
| 75 |
+
"Do not skip these packages in the output:"
|
| 76 |
+
" {}".format(", ".join(_dev_pkgs()))
|
| 77 |
+
),
|
| 78 |
+
)
|
| 79 |
+
self.cmd_opts.add_option(
|
| 80 |
+
"--exclude-editable",
|
| 81 |
+
dest="exclude_editable",
|
| 82 |
+
action="store_true",
|
| 83 |
+
help="Exclude editable package from output.",
|
| 84 |
+
)
|
| 85 |
+
self.cmd_opts.add_option(cmdoptions.list_exclude())
|
| 86 |
+
|
| 87 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 88 |
+
|
| 89 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 90 |
+
skip = set(stdlib_pkgs)
|
| 91 |
+
if not options.freeze_all:
|
| 92 |
+
skip.update(_dev_pkgs())
|
| 93 |
+
|
| 94 |
+
if options.excludes:
|
| 95 |
+
skip.update(options.excludes)
|
| 96 |
+
|
| 97 |
+
cmdoptions.check_list_path_option(options)
|
| 98 |
+
|
| 99 |
+
for line in freeze(
|
| 100 |
+
requirement=options.requirements,
|
| 101 |
+
local_only=options.local,
|
| 102 |
+
user_only=options.user,
|
| 103 |
+
paths=options.path,
|
| 104 |
+
isolated=options.isolated_mode,
|
| 105 |
+
skip=skip,
|
| 106 |
+
exclude_editable=options.exclude_editable,
|
| 107 |
+
):
|
| 108 |
+
sys.stdout.write(line + "\n")
|
| 109 |
+
return SUCCESS
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/hash.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import logging
|
| 3 |
+
import sys
|
| 4 |
+
from optparse import Values
|
| 5 |
+
from typing import List
|
| 6 |
+
|
| 7 |
+
from pip._internal.cli.base_command import Command
|
| 8 |
+
from pip._internal.cli.status_codes import ERROR, SUCCESS
|
| 9 |
+
from pip._internal.utils.hashes import FAVORITE_HASH, STRONG_HASHES
|
| 10 |
+
from pip._internal.utils.misc import read_chunks, write_output
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class HashCommand(Command):
|
| 16 |
+
"""
|
| 17 |
+
Compute a hash of a local package archive.
|
| 18 |
+
|
| 19 |
+
These can be used with --hash in a requirements file to do repeatable
|
| 20 |
+
installs.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
usage = "%prog [options] <file> ..."
|
| 24 |
+
ignore_require_venv = True
|
| 25 |
+
|
| 26 |
+
def add_options(self) -> None:
|
| 27 |
+
self.cmd_opts.add_option(
|
| 28 |
+
"-a",
|
| 29 |
+
"--algorithm",
|
| 30 |
+
dest="algorithm",
|
| 31 |
+
choices=STRONG_HASHES,
|
| 32 |
+
action="store",
|
| 33 |
+
default=FAVORITE_HASH,
|
| 34 |
+
help="The hash algorithm to use: one of {}".format(
|
| 35 |
+
", ".join(STRONG_HASHES)
|
| 36 |
+
),
|
| 37 |
+
)
|
| 38 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 39 |
+
|
| 40 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 41 |
+
if not args:
|
| 42 |
+
self.parser.print_usage(sys.stderr)
|
| 43 |
+
return ERROR
|
| 44 |
+
|
| 45 |
+
algorithm = options.algorithm
|
| 46 |
+
for path in args:
|
| 47 |
+
write_output(
|
| 48 |
+
"%s:\n--hash=%s:%s", path, algorithm, _hash_of_file(path, algorithm)
|
| 49 |
+
)
|
| 50 |
+
return SUCCESS
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _hash_of_file(path: str, algorithm: str) -> str:
|
| 54 |
+
"""Return the hash digest of a file."""
|
| 55 |
+
with open(path, "rb") as archive:
|
| 56 |
+
hash = hashlib.new(algorithm)
|
| 57 |
+
for chunk in read_chunks(archive):
|
| 58 |
+
hash.update(chunk)
|
| 59 |
+
return hash.hexdigest()
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/index.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from optparse import Values
|
| 3 |
+
from typing import Any, Iterable, List, Optional
|
| 4 |
+
|
| 5 |
+
from pip._vendor.packaging.version import Version
|
| 6 |
+
|
| 7 |
+
from pip._internal.cli import cmdoptions
|
| 8 |
+
from pip._internal.cli.req_command import IndexGroupCommand
|
| 9 |
+
from pip._internal.cli.status_codes import ERROR, SUCCESS
|
| 10 |
+
from pip._internal.commands.search import print_dist_installation_info
|
| 11 |
+
from pip._internal.exceptions import CommandError, DistributionNotFound, PipError
|
| 12 |
+
from pip._internal.index.collector import LinkCollector
|
| 13 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 14 |
+
from pip._internal.models.selection_prefs import SelectionPreferences
|
| 15 |
+
from pip._internal.models.target_python import TargetPython
|
| 16 |
+
from pip._internal.network.session import PipSession
|
| 17 |
+
from pip._internal.utils.misc import write_output
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class IndexCommand(IndexGroupCommand):
|
| 23 |
+
"""
|
| 24 |
+
Inspect information available from package indexes.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
ignore_require_venv = True
|
| 28 |
+
usage = """
|
| 29 |
+
%prog versions <package>
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def add_options(self) -> None:
|
| 33 |
+
cmdoptions.add_target_python_options(self.cmd_opts)
|
| 34 |
+
|
| 35 |
+
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
|
| 36 |
+
self.cmd_opts.add_option(cmdoptions.pre())
|
| 37 |
+
self.cmd_opts.add_option(cmdoptions.no_binary())
|
| 38 |
+
self.cmd_opts.add_option(cmdoptions.only_binary())
|
| 39 |
+
|
| 40 |
+
index_opts = cmdoptions.make_option_group(
|
| 41 |
+
cmdoptions.index_group,
|
| 42 |
+
self.parser,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
self.parser.insert_option_group(0, index_opts)
|
| 46 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 47 |
+
|
| 48 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 49 |
+
handlers = {
|
| 50 |
+
"versions": self.get_available_package_versions,
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
logger.warning(
|
| 54 |
+
"pip index is currently an experimental command. "
|
| 55 |
+
"It may be removed/changed in a future release "
|
| 56 |
+
"without prior warning."
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Determine action
|
| 60 |
+
if not args or args[0] not in handlers:
|
| 61 |
+
logger.error(
|
| 62 |
+
"Need an action (%s) to perform.",
|
| 63 |
+
", ".join(sorted(handlers)),
|
| 64 |
+
)
|
| 65 |
+
return ERROR
|
| 66 |
+
|
| 67 |
+
action = args[0]
|
| 68 |
+
|
| 69 |
+
# Error handling happens here, not in the action-handlers.
|
| 70 |
+
try:
|
| 71 |
+
handlers[action](options, args[1:])
|
| 72 |
+
except PipError as e:
|
| 73 |
+
logger.error(e.args[0])
|
| 74 |
+
return ERROR
|
| 75 |
+
|
| 76 |
+
return SUCCESS
|
| 77 |
+
|
| 78 |
+
def _build_package_finder(
|
| 79 |
+
self,
|
| 80 |
+
options: Values,
|
| 81 |
+
session: PipSession,
|
| 82 |
+
target_python: Optional[TargetPython] = None,
|
| 83 |
+
ignore_requires_python: Optional[bool] = None,
|
| 84 |
+
) -> PackageFinder:
|
| 85 |
+
"""
|
| 86 |
+
Create a package finder appropriate to the index command.
|
| 87 |
+
"""
|
| 88 |
+
link_collector = LinkCollector.create(session, options=options)
|
| 89 |
+
|
| 90 |
+
# Pass allow_yanked=False to ignore yanked versions.
|
| 91 |
+
selection_prefs = SelectionPreferences(
|
| 92 |
+
allow_yanked=False,
|
| 93 |
+
allow_all_prereleases=options.pre,
|
| 94 |
+
ignore_requires_python=ignore_requires_python,
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
return PackageFinder.create(
|
| 98 |
+
link_collector=link_collector,
|
| 99 |
+
selection_prefs=selection_prefs,
|
| 100 |
+
target_python=target_python,
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
def get_available_package_versions(self, options: Values, args: List[Any]) -> None:
|
| 104 |
+
if len(args) != 1:
|
| 105 |
+
raise CommandError("You need to specify exactly one argument")
|
| 106 |
+
|
| 107 |
+
target_python = cmdoptions.make_target_python(options)
|
| 108 |
+
query = args[0]
|
| 109 |
+
|
| 110 |
+
with self._build_session(options) as session:
|
| 111 |
+
finder = self._build_package_finder(
|
| 112 |
+
options=options,
|
| 113 |
+
session=session,
|
| 114 |
+
target_python=target_python,
|
| 115 |
+
ignore_requires_python=options.ignore_requires_python,
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
versions: Iterable[Version] = (
|
| 119 |
+
candidate.version for candidate in finder.find_all_candidates(query)
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
if not options.pre:
|
| 123 |
+
# Remove prereleases
|
| 124 |
+
versions = (
|
| 125 |
+
version for version in versions if not version.is_prerelease
|
| 126 |
+
)
|
| 127 |
+
versions = set(versions)
|
| 128 |
+
|
| 129 |
+
if not versions:
|
| 130 |
+
raise DistributionNotFound(
|
| 131 |
+
f"No matching distribution found for {query}"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
formatted_versions = [str(ver) for ver in sorted(versions, reverse=True)]
|
| 135 |
+
latest = formatted_versions[0]
|
| 136 |
+
|
| 137 |
+
write_output(f"{query} ({latest})")
|
| 138 |
+
write_output("Available versions: {}".format(", ".join(formatted_versions)))
|
| 139 |
+
print_dist_installation_info(query, latest)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/install.py
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import errno
|
| 2 |
+
import json
|
| 3 |
+
import operator
|
| 4 |
+
import os
|
| 5 |
+
import shutil
|
| 6 |
+
import site
|
| 7 |
+
from optparse import SUPPRESS_HELP, Values
|
| 8 |
+
from typing import List, Optional
|
| 9 |
+
|
| 10 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 11 |
+
from pip._vendor.rich import print_json
|
| 12 |
+
|
| 13 |
+
# Eagerly import self_outdated_check to avoid crashes. Otherwise,
|
| 14 |
+
# this module would be imported *after* pip was replaced, resulting
|
| 15 |
+
# in crashes if the new self_outdated_check module was incompatible
|
| 16 |
+
# with the rest of pip that's already imported, or allowing a
|
| 17 |
+
# wheel to execute arbitrary code on install by replacing
|
| 18 |
+
# self_outdated_check.
|
| 19 |
+
import pip._internal.self_outdated_check # noqa: F401
|
| 20 |
+
from pip._internal.cache import WheelCache
|
| 21 |
+
from pip._internal.cli import cmdoptions
|
| 22 |
+
from pip._internal.cli.cmdoptions import make_target_python
|
| 23 |
+
from pip._internal.cli.req_command import (
|
| 24 |
+
RequirementCommand,
|
| 25 |
+
with_cleanup,
|
| 26 |
+
)
|
| 27 |
+
from pip._internal.cli.status_codes import ERROR, SUCCESS
|
| 28 |
+
from pip._internal.exceptions import CommandError, InstallationError
|
| 29 |
+
from pip._internal.locations import get_scheme
|
| 30 |
+
from pip._internal.metadata import get_environment
|
| 31 |
+
from pip._internal.models.installation_report import InstallationReport
|
| 32 |
+
from pip._internal.operations.build.build_tracker import get_build_tracker
|
| 33 |
+
from pip._internal.operations.check import ConflictDetails, check_install_conflicts
|
| 34 |
+
from pip._internal.req import install_given_reqs
|
| 35 |
+
from pip._internal.req.req_install import (
|
| 36 |
+
InstallRequirement,
|
| 37 |
+
check_legacy_setup_py_options,
|
| 38 |
+
)
|
| 39 |
+
from pip._internal.utils.compat import WINDOWS
|
| 40 |
+
from pip._internal.utils.filesystem import test_writable_dir
|
| 41 |
+
from pip._internal.utils.logging import getLogger
|
| 42 |
+
from pip._internal.utils.misc import (
|
| 43 |
+
check_externally_managed,
|
| 44 |
+
ensure_dir,
|
| 45 |
+
get_pip_version,
|
| 46 |
+
protect_pip_from_modification_on_windows,
|
| 47 |
+
warn_if_run_as_root,
|
| 48 |
+
write_output,
|
| 49 |
+
)
|
| 50 |
+
from pip._internal.utils.temp_dir import TempDirectory
|
| 51 |
+
from pip._internal.utils.virtualenv import (
|
| 52 |
+
running_under_virtualenv,
|
| 53 |
+
virtualenv_no_global,
|
| 54 |
+
)
|
| 55 |
+
from pip._internal.wheel_builder import build, should_build_for_install_command
|
| 56 |
+
|
| 57 |
+
logger = getLogger(__name__)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class InstallCommand(RequirementCommand):
|
| 61 |
+
"""
|
| 62 |
+
Install packages from:
|
| 63 |
+
|
| 64 |
+
- PyPI (and other indexes) using requirement specifiers.
|
| 65 |
+
- VCS project urls.
|
| 66 |
+
- Local project directories.
|
| 67 |
+
- Local or remote source archives.
|
| 68 |
+
|
| 69 |
+
pip also supports installing from "requirements files", which provide
|
| 70 |
+
an easy way to specify a whole environment to be installed.
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
usage = """
|
| 74 |
+
%prog [options] <requirement specifier> [package-index-options] ...
|
| 75 |
+
%prog [options] -r <requirements file> [package-index-options] ...
|
| 76 |
+
%prog [options] [-e] <vcs project url> ...
|
| 77 |
+
%prog [options] [-e] <local project path> ...
|
| 78 |
+
%prog [options] <archive url/path> ..."""
|
| 79 |
+
|
| 80 |
+
def add_options(self) -> None:
|
| 81 |
+
self.cmd_opts.add_option(cmdoptions.requirements())
|
| 82 |
+
self.cmd_opts.add_option(cmdoptions.constraints())
|
| 83 |
+
self.cmd_opts.add_option(cmdoptions.no_deps())
|
| 84 |
+
self.cmd_opts.add_option(cmdoptions.pre())
|
| 85 |
+
|
| 86 |
+
self.cmd_opts.add_option(cmdoptions.editable())
|
| 87 |
+
self.cmd_opts.add_option(
|
| 88 |
+
"--dry-run",
|
| 89 |
+
action="store_true",
|
| 90 |
+
dest="dry_run",
|
| 91 |
+
default=False,
|
| 92 |
+
help=(
|
| 93 |
+
"Don't actually install anything, just print what would be. "
|
| 94 |
+
"Can be used in combination with --ignore-installed "
|
| 95 |
+
"to 'resolve' the requirements."
|
| 96 |
+
),
|
| 97 |
+
)
|
| 98 |
+
self.cmd_opts.add_option(
|
| 99 |
+
"-t",
|
| 100 |
+
"--target",
|
| 101 |
+
dest="target_dir",
|
| 102 |
+
metavar="dir",
|
| 103 |
+
default=None,
|
| 104 |
+
help=(
|
| 105 |
+
"Install packages into <dir>. "
|
| 106 |
+
"By default this will not replace existing files/folders in "
|
| 107 |
+
"<dir>. Use --upgrade to replace existing packages in <dir> "
|
| 108 |
+
"with new versions."
|
| 109 |
+
),
|
| 110 |
+
)
|
| 111 |
+
cmdoptions.add_target_python_options(self.cmd_opts)
|
| 112 |
+
|
| 113 |
+
self.cmd_opts.add_option(
|
| 114 |
+
"--user",
|
| 115 |
+
dest="use_user_site",
|
| 116 |
+
action="store_true",
|
| 117 |
+
help=(
|
| 118 |
+
"Install to the Python user install directory for your "
|
| 119 |
+
"platform. Typically ~/.local/, or %APPDATA%\\Python on "
|
| 120 |
+
"Windows. (See the Python documentation for site.USER_BASE "
|
| 121 |
+
"for full details.)"
|
| 122 |
+
),
|
| 123 |
+
)
|
| 124 |
+
self.cmd_opts.add_option(
|
| 125 |
+
"--no-user",
|
| 126 |
+
dest="use_user_site",
|
| 127 |
+
action="store_false",
|
| 128 |
+
help=SUPPRESS_HELP,
|
| 129 |
+
)
|
| 130 |
+
self.cmd_opts.add_option(
|
| 131 |
+
"--root",
|
| 132 |
+
dest="root_path",
|
| 133 |
+
metavar="dir",
|
| 134 |
+
default=None,
|
| 135 |
+
help="Install everything relative to this alternate root directory.",
|
| 136 |
+
)
|
| 137 |
+
self.cmd_opts.add_option(
|
| 138 |
+
"--prefix",
|
| 139 |
+
dest="prefix_path",
|
| 140 |
+
metavar="dir",
|
| 141 |
+
default=None,
|
| 142 |
+
help=(
|
| 143 |
+
"Installation prefix where lib, bin and other top-level "
|
| 144 |
+
"folders are placed. Note that the resulting installation may "
|
| 145 |
+
"contain scripts and other resources which reference the "
|
| 146 |
+
"Python interpreter of pip, and not that of ``--prefix``. "
|
| 147 |
+
"See also the ``--python`` option if the intention is to "
|
| 148 |
+
"install packages into another (possibly pip-free) "
|
| 149 |
+
"environment."
|
| 150 |
+
),
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
self.cmd_opts.add_option(cmdoptions.src())
|
| 154 |
+
|
| 155 |
+
self.cmd_opts.add_option(
|
| 156 |
+
"-U",
|
| 157 |
+
"--upgrade",
|
| 158 |
+
dest="upgrade",
|
| 159 |
+
action="store_true",
|
| 160 |
+
help=(
|
| 161 |
+
"Upgrade all specified packages to the newest available "
|
| 162 |
+
"version. The handling of dependencies depends on the "
|
| 163 |
+
"upgrade-strategy used."
|
| 164 |
+
),
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
self.cmd_opts.add_option(
|
| 168 |
+
"--upgrade-strategy",
|
| 169 |
+
dest="upgrade_strategy",
|
| 170 |
+
default="only-if-needed",
|
| 171 |
+
choices=["only-if-needed", "eager"],
|
| 172 |
+
help=(
|
| 173 |
+
"Determines how dependency upgrading should be handled "
|
| 174 |
+
"[default: %default]. "
|
| 175 |
+
'"eager" - dependencies are upgraded regardless of '
|
| 176 |
+
"whether the currently installed version satisfies the "
|
| 177 |
+
"requirements of the upgraded package(s). "
|
| 178 |
+
'"only-if-needed" - are upgraded only when they do not '
|
| 179 |
+
"satisfy the requirements of the upgraded package(s)."
|
| 180 |
+
),
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
self.cmd_opts.add_option(
|
| 184 |
+
"--force-reinstall",
|
| 185 |
+
dest="force_reinstall",
|
| 186 |
+
action="store_true",
|
| 187 |
+
help="Reinstall all packages even if they are already up-to-date.",
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
self.cmd_opts.add_option(
|
| 191 |
+
"-I",
|
| 192 |
+
"--ignore-installed",
|
| 193 |
+
dest="ignore_installed",
|
| 194 |
+
action="store_true",
|
| 195 |
+
help=(
|
| 196 |
+
"Ignore the installed packages, overwriting them. "
|
| 197 |
+
"This can break your system if the existing package "
|
| 198 |
+
"is of a different version or was installed "
|
| 199 |
+
"with a different package manager!"
|
| 200 |
+
),
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
|
| 204 |
+
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
|
| 205 |
+
self.cmd_opts.add_option(cmdoptions.use_pep517())
|
| 206 |
+
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
|
| 207 |
+
self.cmd_opts.add_option(cmdoptions.check_build_deps())
|
| 208 |
+
self.cmd_opts.add_option(cmdoptions.override_externally_managed())
|
| 209 |
+
|
| 210 |
+
self.cmd_opts.add_option(cmdoptions.config_settings())
|
| 211 |
+
self.cmd_opts.add_option(cmdoptions.global_options())
|
| 212 |
+
|
| 213 |
+
self.cmd_opts.add_option(
|
| 214 |
+
"--compile",
|
| 215 |
+
action="store_true",
|
| 216 |
+
dest="compile",
|
| 217 |
+
default=True,
|
| 218 |
+
help="Compile Python source files to bytecode",
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
self.cmd_opts.add_option(
|
| 222 |
+
"--no-compile",
|
| 223 |
+
action="store_false",
|
| 224 |
+
dest="compile",
|
| 225 |
+
help="Do not compile Python source files to bytecode",
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
self.cmd_opts.add_option(
|
| 229 |
+
"--no-warn-script-location",
|
| 230 |
+
action="store_false",
|
| 231 |
+
dest="warn_script_location",
|
| 232 |
+
default=True,
|
| 233 |
+
help="Do not warn when installing scripts outside PATH",
|
| 234 |
+
)
|
| 235 |
+
self.cmd_opts.add_option(
|
| 236 |
+
"--no-warn-conflicts",
|
| 237 |
+
action="store_false",
|
| 238 |
+
dest="warn_about_conflicts",
|
| 239 |
+
default=True,
|
| 240 |
+
help="Do not warn about broken dependencies",
|
| 241 |
+
)
|
| 242 |
+
self.cmd_opts.add_option(cmdoptions.no_binary())
|
| 243 |
+
self.cmd_opts.add_option(cmdoptions.only_binary())
|
| 244 |
+
self.cmd_opts.add_option(cmdoptions.prefer_binary())
|
| 245 |
+
self.cmd_opts.add_option(cmdoptions.require_hashes())
|
| 246 |
+
self.cmd_opts.add_option(cmdoptions.progress_bar())
|
| 247 |
+
self.cmd_opts.add_option(cmdoptions.root_user_action())
|
| 248 |
+
|
| 249 |
+
index_opts = cmdoptions.make_option_group(
|
| 250 |
+
cmdoptions.index_group,
|
| 251 |
+
self.parser,
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
self.parser.insert_option_group(0, index_opts)
|
| 255 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 256 |
+
|
| 257 |
+
self.cmd_opts.add_option(
|
| 258 |
+
"--report",
|
| 259 |
+
dest="json_report_file",
|
| 260 |
+
metavar="file",
|
| 261 |
+
default=None,
|
| 262 |
+
help=(
|
| 263 |
+
"Generate a JSON file describing what pip did to install "
|
| 264 |
+
"the provided requirements. "
|
| 265 |
+
"Can be used in combination with --dry-run and --ignore-installed "
|
| 266 |
+
"to 'resolve' the requirements. "
|
| 267 |
+
"When - is used as file name it writes to stdout. "
|
| 268 |
+
"When writing to stdout, please combine with the --quiet option "
|
| 269 |
+
"to avoid mixing pip logging output with JSON output."
|
| 270 |
+
),
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
@with_cleanup
|
| 274 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 275 |
+
if options.use_user_site and options.target_dir is not None:
|
| 276 |
+
raise CommandError("Can not combine '--user' and '--target'")
|
| 277 |
+
|
| 278 |
+
# Check whether the environment we're installing into is externally
|
| 279 |
+
# managed, as specified in PEP 668. Specifying --root, --target, or
|
| 280 |
+
# --prefix disables the check, since there's no reliable way to locate
|
| 281 |
+
# the EXTERNALLY-MANAGED file for those cases. An exception is also
|
| 282 |
+
# made specifically for "--dry-run --report" for convenience.
|
| 283 |
+
installing_into_current_environment = (
|
| 284 |
+
not (options.dry_run and options.json_report_file)
|
| 285 |
+
and options.root_path is None
|
| 286 |
+
and options.target_dir is None
|
| 287 |
+
and options.prefix_path is None
|
| 288 |
+
)
|
| 289 |
+
if (
|
| 290 |
+
installing_into_current_environment
|
| 291 |
+
and not options.override_externally_managed
|
| 292 |
+
):
|
| 293 |
+
check_externally_managed()
|
| 294 |
+
|
| 295 |
+
upgrade_strategy = "to-satisfy-only"
|
| 296 |
+
if options.upgrade:
|
| 297 |
+
upgrade_strategy = options.upgrade_strategy
|
| 298 |
+
|
| 299 |
+
cmdoptions.check_dist_restriction(options, check_target=True)
|
| 300 |
+
|
| 301 |
+
logger.verbose("Using %s", get_pip_version())
|
| 302 |
+
options.use_user_site = decide_user_install(
|
| 303 |
+
options.use_user_site,
|
| 304 |
+
prefix_path=options.prefix_path,
|
| 305 |
+
target_dir=options.target_dir,
|
| 306 |
+
root_path=options.root_path,
|
| 307 |
+
isolated_mode=options.isolated_mode,
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
target_temp_dir: Optional[TempDirectory] = None
|
| 311 |
+
target_temp_dir_path: Optional[str] = None
|
| 312 |
+
if options.target_dir:
|
| 313 |
+
options.ignore_installed = True
|
| 314 |
+
options.target_dir = os.path.abspath(options.target_dir)
|
| 315 |
+
if (
|
| 316 |
+
# fmt: off
|
| 317 |
+
os.path.exists(options.target_dir) and
|
| 318 |
+
not os.path.isdir(options.target_dir)
|
| 319 |
+
# fmt: on
|
| 320 |
+
):
|
| 321 |
+
raise CommandError(
|
| 322 |
+
"Target path exists but is not a directory, will not continue."
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
# Create a target directory for using with the target option
|
| 326 |
+
target_temp_dir = TempDirectory(kind="target")
|
| 327 |
+
target_temp_dir_path = target_temp_dir.path
|
| 328 |
+
self.enter_context(target_temp_dir)
|
| 329 |
+
|
| 330 |
+
global_options = options.global_options or []
|
| 331 |
+
|
| 332 |
+
session = self.get_default_session(options)
|
| 333 |
+
|
| 334 |
+
target_python = make_target_python(options)
|
| 335 |
+
finder = self._build_package_finder(
|
| 336 |
+
options=options,
|
| 337 |
+
session=session,
|
| 338 |
+
target_python=target_python,
|
| 339 |
+
ignore_requires_python=options.ignore_requires_python,
|
| 340 |
+
)
|
| 341 |
+
build_tracker = self.enter_context(get_build_tracker())
|
| 342 |
+
|
| 343 |
+
directory = TempDirectory(
|
| 344 |
+
delete=not options.no_clean,
|
| 345 |
+
kind="install",
|
| 346 |
+
globally_managed=True,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
try:
|
| 350 |
+
reqs = self.get_requirements(args, options, finder, session)
|
| 351 |
+
check_legacy_setup_py_options(options, reqs)
|
| 352 |
+
|
| 353 |
+
wheel_cache = WheelCache(options.cache_dir)
|
| 354 |
+
|
| 355 |
+
# Only when installing is it permitted to use PEP 660.
|
| 356 |
+
# In other circumstances (pip wheel, pip download) we generate
|
| 357 |
+
# regular (i.e. non editable) metadata and wheels.
|
| 358 |
+
for req in reqs:
|
| 359 |
+
req.permit_editable_wheels = True
|
| 360 |
+
|
| 361 |
+
preparer = self.make_requirement_preparer(
|
| 362 |
+
temp_build_dir=directory,
|
| 363 |
+
options=options,
|
| 364 |
+
build_tracker=build_tracker,
|
| 365 |
+
session=session,
|
| 366 |
+
finder=finder,
|
| 367 |
+
use_user_site=options.use_user_site,
|
| 368 |
+
verbosity=self.verbosity,
|
| 369 |
+
)
|
| 370 |
+
resolver = self.make_resolver(
|
| 371 |
+
preparer=preparer,
|
| 372 |
+
finder=finder,
|
| 373 |
+
options=options,
|
| 374 |
+
wheel_cache=wheel_cache,
|
| 375 |
+
use_user_site=options.use_user_site,
|
| 376 |
+
ignore_installed=options.ignore_installed,
|
| 377 |
+
ignore_requires_python=options.ignore_requires_python,
|
| 378 |
+
force_reinstall=options.force_reinstall,
|
| 379 |
+
upgrade_strategy=upgrade_strategy,
|
| 380 |
+
use_pep517=options.use_pep517,
|
| 381 |
+
py_version_info=options.python_version,
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
self.trace_basic_info(finder)
|
| 385 |
+
|
| 386 |
+
requirement_set = resolver.resolve(
|
| 387 |
+
reqs, check_supported_wheels=not options.target_dir
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
if options.json_report_file:
|
| 391 |
+
report = InstallationReport(requirement_set.requirements_to_install)
|
| 392 |
+
if options.json_report_file == "-":
|
| 393 |
+
print_json(data=report.to_dict())
|
| 394 |
+
else:
|
| 395 |
+
with open(options.json_report_file, "w", encoding="utf-8") as f:
|
| 396 |
+
json.dump(report.to_dict(), f, indent=2, ensure_ascii=False)
|
| 397 |
+
|
| 398 |
+
if options.dry_run:
|
| 399 |
+
would_install_items = sorted(
|
| 400 |
+
(r.metadata["name"], r.metadata["version"])
|
| 401 |
+
for r in requirement_set.requirements_to_install
|
| 402 |
+
)
|
| 403 |
+
if would_install_items:
|
| 404 |
+
write_output(
|
| 405 |
+
"Would install %s",
|
| 406 |
+
" ".join("-".join(item) for item in would_install_items),
|
| 407 |
+
)
|
| 408 |
+
return SUCCESS
|
| 409 |
+
|
| 410 |
+
try:
|
| 411 |
+
pip_req = requirement_set.get_requirement("pip")
|
| 412 |
+
except KeyError:
|
| 413 |
+
modifying_pip = False
|
| 414 |
+
else:
|
| 415 |
+
# If we're not replacing an already installed pip,
|
| 416 |
+
# we're not modifying it.
|
| 417 |
+
modifying_pip = pip_req.satisfied_by is None
|
| 418 |
+
protect_pip_from_modification_on_windows(modifying_pip=modifying_pip)
|
| 419 |
+
|
| 420 |
+
reqs_to_build = [
|
| 421 |
+
r
|
| 422 |
+
for r in requirement_set.requirements.values()
|
| 423 |
+
if should_build_for_install_command(r)
|
| 424 |
+
]
|
| 425 |
+
|
| 426 |
+
_, build_failures = build(
|
| 427 |
+
reqs_to_build,
|
| 428 |
+
wheel_cache=wheel_cache,
|
| 429 |
+
verify=True,
|
| 430 |
+
build_options=[],
|
| 431 |
+
global_options=global_options,
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
if build_failures:
|
| 435 |
+
raise InstallationError(
|
| 436 |
+
"Failed to build installable wheels for some "
|
| 437 |
+
"pyproject.toml based projects ({})".format(
|
| 438 |
+
", ".join(r.name for r in build_failures) # type: ignore
|
| 439 |
+
)
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
to_install = resolver.get_installation_order(requirement_set)
|
| 443 |
+
|
| 444 |
+
# Check for conflicts in the package set we're installing.
|
| 445 |
+
conflicts: Optional[ConflictDetails] = None
|
| 446 |
+
should_warn_about_conflicts = (
|
| 447 |
+
not options.ignore_dependencies and options.warn_about_conflicts
|
| 448 |
+
)
|
| 449 |
+
if should_warn_about_conflicts:
|
| 450 |
+
conflicts = self._determine_conflicts(to_install)
|
| 451 |
+
|
| 452 |
+
# Don't warn about script install locations if
|
| 453 |
+
# --target or --prefix has been specified
|
| 454 |
+
warn_script_location = options.warn_script_location
|
| 455 |
+
if options.target_dir or options.prefix_path:
|
| 456 |
+
warn_script_location = False
|
| 457 |
+
|
| 458 |
+
installed = install_given_reqs(
|
| 459 |
+
to_install,
|
| 460 |
+
global_options,
|
| 461 |
+
root=options.root_path,
|
| 462 |
+
home=target_temp_dir_path,
|
| 463 |
+
prefix=options.prefix_path,
|
| 464 |
+
warn_script_location=warn_script_location,
|
| 465 |
+
use_user_site=options.use_user_site,
|
| 466 |
+
pycompile=options.compile,
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
lib_locations = get_lib_location_guesses(
|
| 470 |
+
user=options.use_user_site,
|
| 471 |
+
home=target_temp_dir_path,
|
| 472 |
+
root=options.root_path,
|
| 473 |
+
prefix=options.prefix_path,
|
| 474 |
+
isolated=options.isolated_mode,
|
| 475 |
+
)
|
| 476 |
+
env = get_environment(lib_locations)
|
| 477 |
+
|
| 478 |
+
# Display a summary of installed packages, with extra care to
|
| 479 |
+
# display a package name as it was requested by the user.
|
| 480 |
+
installed.sort(key=operator.attrgetter("name"))
|
| 481 |
+
summary = []
|
| 482 |
+
installed_versions = {}
|
| 483 |
+
for distribution in env.iter_all_distributions():
|
| 484 |
+
installed_versions[distribution.canonical_name] = distribution.version
|
| 485 |
+
for package in installed:
|
| 486 |
+
display_name = package.name
|
| 487 |
+
version = installed_versions.get(canonicalize_name(display_name), None)
|
| 488 |
+
if version:
|
| 489 |
+
text = f"{display_name}-{version}"
|
| 490 |
+
else:
|
| 491 |
+
text = display_name
|
| 492 |
+
summary.append(text)
|
| 493 |
+
|
| 494 |
+
if conflicts is not None:
|
| 495 |
+
self._warn_about_conflicts(
|
| 496 |
+
conflicts,
|
| 497 |
+
resolver_variant=self.determine_resolver_variant(options),
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
installed_desc = " ".join(summary)
|
| 501 |
+
if installed_desc:
|
| 502 |
+
write_output(
|
| 503 |
+
"Successfully installed %s",
|
| 504 |
+
installed_desc,
|
| 505 |
+
)
|
| 506 |
+
except OSError as error:
|
| 507 |
+
show_traceback = self.verbosity >= 1
|
| 508 |
+
|
| 509 |
+
message = create_os_error_message(
|
| 510 |
+
error,
|
| 511 |
+
show_traceback,
|
| 512 |
+
options.use_user_site,
|
| 513 |
+
)
|
| 514 |
+
logger.error(message, exc_info=show_traceback)
|
| 515 |
+
|
| 516 |
+
return ERROR
|
| 517 |
+
|
| 518 |
+
if options.target_dir:
|
| 519 |
+
assert target_temp_dir
|
| 520 |
+
self._handle_target_dir(
|
| 521 |
+
options.target_dir, target_temp_dir, options.upgrade
|
| 522 |
+
)
|
| 523 |
+
if options.root_user_action == "warn":
|
| 524 |
+
warn_if_run_as_root()
|
| 525 |
+
return SUCCESS
|
| 526 |
+
|
| 527 |
+
def _handle_target_dir(
|
| 528 |
+
self, target_dir: str, target_temp_dir: TempDirectory, upgrade: bool
|
| 529 |
+
) -> None:
|
| 530 |
+
ensure_dir(target_dir)
|
| 531 |
+
|
| 532 |
+
# Checking both purelib and platlib directories for installed
|
| 533 |
+
# packages to be moved to target directory
|
| 534 |
+
lib_dir_list = []
|
| 535 |
+
|
| 536 |
+
# Checking both purelib and platlib directories for installed
|
| 537 |
+
# packages to be moved to target directory
|
| 538 |
+
scheme = get_scheme("", home=target_temp_dir.path)
|
| 539 |
+
purelib_dir = scheme.purelib
|
| 540 |
+
platlib_dir = scheme.platlib
|
| 541 |
+
data_dir = scheme.data
|
| 542 |
+
|
| 543 |
+
if os.path.exists(purelib_dir):
|
| 544 |
+
lib_dir_list.append(purelib_dir)
|
| 545 |
+
if os.path.exists(platlib_dir) and platlib_dir != purelib_dir:
|
| 546 |
+
lib_dir_list.append(platlib_dir)
|
| 547 |
+
if os.path.exists(data_dir):
|
| 548 |
+
lib_dir_list.append(data_dir)
|
| 549 |
+
|
| 550 |
+
for lib_dir in lib_dir_list:
|
| 551 |
+
for item in os.listdir(lib_dir):
|
| 552 |
+
if lib_dir == data_dir:
|
| 553 |
+
ddir = os.path.join(data_dir, item)
|
| 554 |
+
if any(s.startswith(ddir) for s in lib_dir_list[:-1]):
|
| 555 |
+
continue
|
| 556 |
+
target_item_dir = os.path.join(target_dir, item)
|
| 557 |
+
if os.path.exists(target_item_dir):
|
| 558 |
+
if not upgrade:
|
| 559 |
+
logger.warning(
|
| 560 |
+
"Target directory %s already exists. Specify "
|
| 561 |
+
"--upgrade to force replacement.",
|
| 562 |
+
target_item_dir,
|
| 563 |
+
)
|
| 564 |
+
continue
|
| 565 |
+
if os.path.islink(target_item_dir):
|
| 566 |
+
logger.warning(
|
| 567 |
+
"Target directory %s already exists and is "
|
| 568 |
+
"a link. pip will not automatically replace "
|
| 569 |
+
"links, please remove if replacement is "
|
| 570 |
+
"desired.",
|
| 571 |
+
target_item_dir,
|
| 572 |
+
)
|
| 573 |
+
continue
|
| 574 |
+
if os.path.isdir(target_item_dir):
|
| 575 |
+
shutil.rmtree(target_item_dir)
|
| 576 |
+
else:
|
| 577 |
+
os.remove(target_item_dir)
|
| 578 |
+
|
| 579 |
+
shutil.move(os.path.join(lib_dir, item), target_item_dir)
|
| 580 |
+
|
| 581 |
+
def _determine_conflicts(
|
| 582 |
+
self, to_install: List[InstallRequirement]
|
| 583 |
+
) -> Optional[ConflictDetails]:
|
| 584 |
+
try:
|
| 585 |
+
return check_install_conflicts(to_install)
|
| 586 |
+
except Exception:
|
| 587 |
+
logger.exception(
|
| 588 |
+
"Error while checking for conflicts. Please file an issue on "
|
| 589 |
+
"pip's issue tracker: https://github.com/pypa/pip/issues/new"
|
| 590 |
+
)
|
| 591 |
+
return None
|
| 592 |
+
|
| 593 |
+
def _warn_about_conflicts(
|
| 594 |
+
self, conflict_details: ConflictDetails, resolver_variant: str
|
| 595 |
+
) -> None:
|
| 596 |
+
package_set, (missing, conflicting) = conflict_details
|
| 597 |
+
if not missing and not conflicting:
|
| 598 |
+
return
|
| 599 |
+
|
| 600 |
+
parts: List[str] = []
|
| 601 |
+
if resolver_variant == "legacy":
|
| 602 |
+
parts.append(
|
| 603 |
+
"pip's legacy dependency resolver does not consider dependency "
|
| 604 |
+
"conflicts when selecting packages. This behaviour is the "
|
| 605 |
+
"source of the following dependency conflicts."
|
| 606 |
+
)
|
| 607 |
+
else:
|
| 608 |
+
assert resolver_variant == "resolvelib"
|
| 609 |
+
parts.append(
|
| 610 |
+
"pip's dependency resolver does not currently take into account "
|
| 611 |
+
"all the packages that are installed. This behaviour is the "
|
| 612 |
+
"source of the following dependency conflicts."
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
# NOTE: There is some duplication here, with commands/check.py
|
| 616 |
+
for project_name in missing:
|
| 617 |
+
version = package_set[project_name][0]
|
| 618 |
+
for dependency in missing[project_name]:
|
| 619 |
+
message = (
|
| 620 |
+
f"{project_name} {version} requires {dependency[1]}, "
|
| 621 |
+
"which is not installed."
|
| 622 |
+
)
|
| 623 |
+
parts.append(message)
|
| 624 |
+
|
| 625 |
+
for project_name in conflicting:
|
| 626 |
+
version = package_set[project_name][0]
|
| 627 |
+
for dep_name, dep_version, req in conflicting[project_name]:
|
| 628 |
+
message = (
|
| 629 |
+
"{name} {version} requires {requirement}, but {you} have "
|
| 630 |
+
"{dep_name} {dep_version} which is incompatible."
|
| 631 |
+
).format(
|
| 632 |
+
name=project_name,
|
| 633 |
+
version=version,
|
| 634 |
+
requirement=req,
|
| 635 |
+
dep_name=dep_name,
|
| 636 |
+
dep_version=dep_version,
|
| 637 |
+
you=("you" if resolver_variant == "resolvelib" else "you'll"),
|
| 638 |
+
)
|
| 639 |
+
parts.append(message)
|
| 640 |
+
|
| 641 |
+
logger.critical("\n".join(parts))
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
def get_lib_location_guesses(
|
| 645 |
+
user: bool = False,
|
| 646 |
+
home: Optional[str] = None,
|
| 647 |
+
root: Optional[str] = None,
|
| 648 |
+
isolated: bool = False,
|
| 649 |
+
prefix: Optional[str] = None,
|
| 650 |
+
) -> List[str]:
|
| 651 |
+
scheme = get_scheme(
|
| 652 |
+
"",
|
| 653 |
+
user=user,
|
| 654 |
+
home=home,
|
| 655 |
+
root=root,
|
| 656 |
+
isolated=isolated,
|
| 657 |
+
prefix=prefix,
|
| 658 |
+
)
|
| 659 |
+
return [scheme.purelib, scheme.platlib]
|
| 660 |
+
|
| 661 |
+
|
| 662 |
+
def site_packages_writable(root: Optional[str], isolated: bool) -> bool:
|
| 663 |
+
return all(
|
| 664 |
+
test_writable_dir(d)
|
| 665 |
+
for d in set(get_lib_location_guesses(root=root, isolated=isolated))
|
| 666 |
+
)
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
def decide_user_install(
|
| 670 |
+
use_user_site: Optional[bool],
|
| 671 |
+
prefix_path: Optional[str] = None,
|
| 672 |
+
target_dir: Optional[str] = None,
|
| 673 |
+
root_path: Optional[str] = None,
|
| 674 |
+
isolated_mode: bool = False,
|
| 675 |
+
) -> bool:
|
| 676 |
+
"""Determine whether to do a user install based on the input options.
|
| 677 |
+
|
| 678 |
+
If use_user_site is False, no additional checks are done.
|
| 679 |
+
If use_user_site is True, it is checked for compatibility with other
|
| 680 |
+
options.
|
| 681 |
+
If use_user_site is None, the default behaviour depends on the environment,
|
| 682 |
+
which is provided by the other arguments.
|
| 683 |
+
"""
|
| 684 |
+
# In some cases (config from tox), use_user_site can be set to an integer
|
| 685 |
+
# rather than a bool, which 'use_user_site is False' wouldn't catch.
|
| 686 |
+
if (use_user_site is not None) and (not use_user_site):
|
| 687 |
+
logger.debug("Non-user install by explicit request")
|
| 688 |
+
return False
|
| 689 |
+
|
| 690 |
+
if use_user_site:
|
| 691 |
+
if prefix_path:
|
| 692 |
+
raise CommandError(
|
| 693 |
+
"Can not combine '--user' and '--prefix' as they imply "
|
| 694 |
+
"different installation locations"
|
| 695 |
+
)
|
| 696 |
+
if virtualenv_no_global():
|
| 697 |
+
raise InstallationError(
|
| 698 |
+
"Can not perform a '--user' install. User site-packages "
|
| 699 |
+
"are not visible in this virtualenv."
|
| 700 |
+
)
|
| 701 |
+
logger.debug("User install by explicit request")
|
| 702 |
+
return True
|
| 703 |
+
|
| 704 |
+
# If we are here, user installs have not been explicitly requested/avoided
|
| 705 |
+
assert use_user_site is None
|
| 706 |
+
|
| 707 |
+
# user install incompatible with --prefix/--target
|
| 708 |
+
if prefix_path or target_dir:
|
| 709 |
+
logger.debug("Non-user install due to --prefix or --target option")
|
| 710 |
+
return False
|
| 711 |
+
|
| 712 |
+
# If user installs are not enabled, choose a non-user install
|
| 713 |
+
if not site.ENABLE_USER_SITE:
|
| 714 |
+
logger.debug("Non-user install because user site-packages disabled")
|
| 715 |
+
return False
|
| 716 |
+
|
| 717 |
+
# If we have permission for a non-user install, do that,
|
| 718 |
+
# otherwise do a user install.
|
| 719 |
+
if site_packages_writable(root=root_path, isolated=isolated_mode):
|
| 720 |
+
logger.debug("Non-user install because site-packages writeable")
|
| 721 |
+
return False
|
| 722 |
+
|
| 723 |
+
logger.info(
|
| 724 |
+
"Defaulting to user installation because normal site-packages "
|
| 725 |
+
"is not writeable"
|
| 726 |
+
)
|
| 727 |
+
return True
|
| 728 |
+
|
| 729 |
+
|
| 730 |
+
def create_os_error_message(
|
| 731 |
+
error: OSError, show_traceback: bool, using_user_site: bool
|
| 732 |
+
) -> str:
|
| 733 |
+
"""Format an error message for an OSError
|
| 734 |
+
|
| 735 |
+
It may occur anytime during the execution of the install command.
|
| 736 |
+
"""
|
| 737 |
+
parts = []
|
| 738 |
+
|
| 739 |
+
# Mention the error if we are not going to show a traceback
|
| 740 |
+
parts.append("Could not install packages due to an OSError")
|
| 741 |
+
if not show_traceback:
|
| 742 |
+
parts.append(": ")
|
| 743 |
+
parts.append(str(error))
|
| 744 |
+
else:
|
| 745 |
+
parts.append(".")
|
| 746 |
+
|
| 747 |
+
# Spilt the error indication from a helper message (if any)
|
| 748 |
+
parts[-1] += "\n"
|
| 749 |
+
|
| 750 |
+
# Suggest useful actions to the user:
|
| 751 |
+
# (1) using user site-packages or (2) verifying the permissions
|
| 752 |
+
if error.errno == errno.EACCES:
|
| 753 |
+
user_option_part = "Consider using the `--user` option"
|
| 754 |
+
permissions_part = "Check the permissions"
|
| 755 |
+
|
| 756 |
+
if not running_under_virtualenv() and not using_user_site:
|
| 757 |
+
parts.extend(
|
| 758 |
+
[
|
| 759 |
+
user_option_part,
|
| 760 |
+
" or ",
|
| 761 |
+
permissions_part.lower(),
|
| 762 |
+
]
|
| 763 |
+
)
|
| 764 |
+
else:
|
| 765 |
+
parts.append(permissions_part)
|
| 766 |
+
parts.append(".\n")
|
| 767 |
+
|
| 768 |
+
# Suggest the user to enable Long Paths if path length is
|
| 769 |
+
# more than 260
|
| 770 |
+
if (
|
| 771 |
+
WINDOWS
|
| 772 |
+
and error.errno == errno.ENOENT
|
| 773 |
+
and error.filename
|
| 774 |
+
and len(error.filename) > 260
|
| 775 |
+
):
|
| 776 |
+
parts.append(
|
| 777 |
+
"HINT: This error might have occurred since "
|
| 778 |
+
"this system does not have Windows Long Path "
|
| 779 |
+
"support enabled. You can find information on "
|
| 780 |
+
"how to enable this at "
|
| 781 |
+
"https://pip.pypa.io/warnings/enable-long-paths\n"
|
| 782 |
+
)
|
| 783 |
+
|
| 784 |
+
return "".join(parts).strip() + "\n"
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/list.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
from optparse import Values
|
| 4 |
+
from typing import TYPE_CHECKING, Generator, List, Optional, Sequence, Tuple, cast
|
| 5 |
+
|
| 6 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 7 |
+
from pip._vendor.packaging.version import Version
|
| 8 |
+
|
| 9 |
+
from pip._internal.cli import cmdoptions
|
| 10 |
+
from pip._internal.cli.index_command import IndexGroupCommand
|
| 11 |
+
from pip._internal.cli.status_codes import SUCCESS
|
| 12 |
+
from pip._internal.exceptions import CommandError
|
| 13 |
+
from pip._internal.metadata import BaseDistribution, get_environment
|
| 14 |
+
from pip._internal.models.selection_prefs import SelectionPreferences
|
| 15 |
+
from pip._internal.utils.compat import stdlib_pkgs
|
| 16 |
+
from pip._internal.utils.misc import tabulate, write_output
|
| 17 |
+
|
| 18 |
+
if TYPE_CHECKING:
|
| 19 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 20 |
+
from pip._internal.network.session import PipSession
|
| 21 |
+
|
| 22 |
+
class _DistWithLatestInfo(BaseDistribution):
|
| 23 |
+
"""Give the distribution object a couple of extra fields.
|
| 24 |
+
|
| 25 |
+
These will be populated during ``get_outdated()``. This is dirty but
|
| 26 |
+
makes the rest of the code much cleaner.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
latest_version: Version
|
| 30 |
+
latest_filetype: str
|
| 31 |
+
|
| 32 |
+
_ProcessedDists = Sequence[_DistWithLatestInfo]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
logger = logging.getLogger(__name__)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class ListCommand(IndexGroupCommand):
|
| 39 |
+
"""
|
| 40 |
+
List installed packages, including editables.
|
| 41 |
+
|
| 42 |
+
Packages are listed in a case-insensitive sorted order.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
ignore_require_venv = True
|
| 46 |
+
usage = """
|
| 47 |
+
%prog [options]"""
|
| 48 |
+
|
| 49 |
+
def add_options(self) -> None:
|
| 50 |
+
self.cmd_opts.add_option(
|
| 51 |
+
"-o",
|
| 52 |
+
"--outdated",
|
| 53 |
+
action="store_true",
|
| 54 |
+
default=False,
|
| 55 |
+
help="List outdated packages",
|
| 56 |
+
)
|
| 57 |
+
self.cmd_opts.add_option(
|
| 58 |
+
"-u",
|
| 59 |
+
"--uptodate",
|
| 60 |
+
action="store_true",
|
| 61 |
+
default=False,
|
| 62 |
+
help="List uptodate packages",
|
| 63 |
+
)
|
| 64 |
+
self.cmd_opts.add_option(
|
| 65 |
+
"-e",
|
| 66 |
+
"--editable",
|
| 67 |
+
action="store_true",
|
| 68 |
+
default=False,
|
| 69 |
+
help="List editable projects.",
|
| 70 |
+
)
|
| 71 |
+
self.cmd_opts.add_option(
|
| 72 |
+
"-l",
|
| 73 |
+
"--local",
|
| 74 |
+
action="store_true",
|
| 75 |
+
default=False,
|
| 76 |
+
help=(
|
| 77 |
+
"If in a virtualenv that has global access, do not list "
|
| 78 |
+
"globally-installed packages."
|
| 79 |
+
),
|
| 80 |
+
)
|
| 81 |
+
self.cmd_opts.add_option(
|
| 82 |
+
"--user",
|
| 83 |
+
dest="user",
|
| 84 |
+
action="store_true",
|
| 85 |
+
default=False,
|
| 86 |
+
help="Only output packages installed in user-site.",
|
| 87 |
+
)
|
| 88 |
+
self.cmd_opts.add_option(cmdoptions.list_path())
|
| 89 |
+
self.cmd_opts.add_option(
|
| 90 |
+
"--pre",
|
| 91 |
+
action="store_true",
|
| 92 |
+
default=False,
|
| 93 |
+
help=(
|
| 94 |
+
"Include pre-release and development versions. By default, "
|
| 95 |
+
"pip only finds stable versions."
|
| 96 |
+
),
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
self.cmd_opts.add_option(
|
| 100 |
+
"--format",
|
| 101 |
+
action="store",
|
| 102 |
+
dest="list_format",
|
| 103 |
+
default="columns",
|
| 104 |
+
choices=("columns", "freeze", "json"),
|
| 105 |
+
help=(
|
| 106 |
+
"Select the output format among: columns (default), freeze, or json. "
|
| 107 |
+
"The 'freeze' format cannot be used with the --outdated option."
|
| 108 |
+
),
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
self.cmd_opts.add_option(
|
| 112 |
+
"--not-required",
|
| 113 |
+
action="store_true",
|
| 114 |
+
dest="not_required",
|
| 115 |
+
help="List packages that are not dependencies of installed packages.",
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
self.cmd_opts.add_option(
|
| 119 |
+
"--exclude-editable",
|
| 120 |
+
action="store_false",
|
| 121 |
+
dest="include_editable",
|
| 122 |
+
help="Exclude editable package from output.",
|
| 123 |
+
)
|
| 124 |
+
self.cmd_opts.add_option(
|
| 125 |
+
"--include-editable",
|
| 126 |
+
action="store_true",
|
| 127 |
+
dest="include_editable",
|
| 128 |
+
help="Include editable package from output.",
|
| 129 |
+
default=True,
|
| 130 |
+
)
|
| 131 |
+
self.cmd_opts.add_option(cmdoptions.list_exclude())
|
| 132 |
+
index_opts = cmdoptions.make_option_group(cmdoptions.index_group, self.parser)
|
| 133 |
+
|
| 134 |
+
self.parser.insert_option_group(0, index_opts)
|
| 135 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 136 |
+
|
| 137 |
+
def handle_pip_version_check(self, options: Values) -> None:
|
| 138 |
+
if options.outdated or options.uptodate:
|
| 139 |
+
super().handle_pip_version_check(options)
|
| 140 |
+
|
| 141 |
+
def _build_package_finder(
|
| 142 |
+
self, options: Values, session: "PipSession"
|
| 143 |
+
) -> "PackageFinder":
|
| 144 |
+
"""
|
| 145 |
+
Create a package finder appropriate to this list command.
|
| 146 |
+
"""
|
| 147 |
+
# Lazy import the heavy index modules as most list invocations won't need 'em.
|
| 148 |
+
from pip._internal.index.collector import LinkCollector
|
| 149 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 150 |
+
|
| 151 |
+
link_collector = LinkCollector.create(session, options=options)
|
| 152 |
+
|
| 153 |
+
# Pass allow_yanked=False to ignore yanked versions.
|
| 154 |
+
selection_prefs = SelectionPreferences(
|
| 155 |
+
allow_yanked=False,
|
| 156 |
+
allow_all_prereleases=options.pre,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
return PackageFinder.create(
|
| 160 |
+
link_collector=link_collector,
|
| 161 |
+
selection_prefs=selection_prefs,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 165 |
+
if options.outdated and options.uptodate:
|
| 166 |
+
raise CommandError("Options --outdated and --uptodate cannot be combined.")
|
| 167 |
+
|
| 168 |
+
if options.outdated and options.list_format == "freeze":
|
| 169 |
+
raise CommandError(
|
| 170 |
+
"List format 'freeze' cannot be used with the --outdated option."
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
cmdoptions.check_list_path_option(options)
|
| 174 |
+
|
| 175 |
+
skip = set(stdlib_pkgs)
|
| 176 |
+
if options.excludes:
|
| 177 |
+
skip.update(canonicalize_name(n) for n in options.excludes)
|
| 178 |
+
|
| 179 |
+
packages: _ProcessedDists = [
|
| 180 |
+
cast("_DistWithLatestInfo", d)
|
| 181 |
+
for d in get_environment(options.path).iter_installed_distributions(
|
| 182 |
+
local_only=options.local,
|
| 183 |
+
user_only=options.user,
|
| 184 |
+
editables_only=options.editable,
|
| 185 |
+
include_editables=options.include_editable,
|
| 186 |
+
skip=skip,
|
| 187 |
+
)
|
| 188 |
+
]
|
| 189 |
+
|
| 190 |
+
# get_not_required must be called firstly in order to find and
|
| 191 |
+
# filter out all dependencies correctly. Otherwise a package
|
| 192 |
+
# can't be identified as requirement because some parent packages
|
| 193 |
+
# could be filtered out before.
|
| 194 |
+
if options.not_required:
|
| 195 |
+
packages = self.get_not_required(packages, options)
|
| 196 |
+
|
| 197 |
+
if options.outdated:
|
| 198 |
+
packages = self.get_outdated(packages, options)
|
| 199 |
+
elif options.uptodate:
|
| 200 |
+
packages = self.get_uptodate(packages, options)
|
| 201 |
+
|
| 202 |
+
self.output_package_listing(packages, options)
|
| 203 |
+
return SUCCESS
|
| 204 |
+
|
| 205 |
+
def get_outdated(
|
| 206 |
+
self, packages: "_ProcessedDists", options: Values
|
| 207 |
+
) -> "_ProcessedDists":
|
| 208 |
+
return [
|
| 209 |
+
dist
|
| 210 |
+
for dist in self.iter_packages_latest_infos(packages, options)
|
| 211 |
+
if dist.latest_version > dist.version
|
| 212 |
+
]
|
| 213 |
+
|
| 214 |
+
def get_uptodate(
|
| 215 |
+
self, packages: "_ProcessedDists", options: Values
|
| 216 |
+
) -> "_ProcessedDists":
|
| 217 |
+
return [
|
| 218 |
+
dist
|
| 219 |
+
for dist in self.iter_packages_latest_infos(packages, options)
|
| 220 |
+
if dist.latest_version == dist.version
|
| 221 |
+
]
|
| 222 |
+
|
| 223 |
+
def get_not_required(
|
| 224 |
+
self, packages: "_ProcessedDists", options: Values
|
| 225 |
+
) -> "_ProcessedDists":
|
| 226 |
+
dep_keys = {
|
| 227 |
+
canonicalize_name(dep.name)
|
| 228 |
+
for dist in packages
|
| 229 |
+
for dep in (dist.iter_dependencies() or ())
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
# Create a set to remove duplicate packages, and cast it to a list
|
| 233 |
+
# to keep the return type consistent with get_outdated and
|
| 234 |
+
# get_uptodate
|
| 235 |
+
return list({pkg for pkg in packages if pkg.canonical_name not in dep_keys})
|
| 236 |
+
|
| 237 |
+
def iter_packages_latest_infos(
|
| 238 |
+
self, packages: "_ProcessedDists", options: Values
|
| 239 |
+
) -> Generator["_DistWithLatestInfo", None, None]:
|
| 240 |
+
with self._build_session(options) as session:
|
| 241 |
+
finder = self._build_package_finder(options, session)
|
| 242 |
+
|
| 243 |
+
def latest_info(
|
| 244 |
+
dist: "_DistWithLatestInfo",
|
| 245 |
+
) -> Optional["_DistWithLatestInfo"]:
|
| 246 |
+
all_candidates = finder.find_all_candidates(dist.canonical_name)
|
| 247 |
+
if not options.pre:
|
| 248 |
+
# Remove prereleases
|
| 249 |
+
all_candidates = [
|
| 250 |
+
candidate
|
| 251 |
+
for candidate in all_candidates
|
| 252 |
+
if not candidate.version.is_prerelease
|
| 253 |
+
]
|
| 254 |
+
|
| 255 |
+
evaluator = finder.make_candidate_evaluator(
|
| 256 |
+
project_name=dist.canonical_name,
|
| 257 |
+
)
|
| 258 |
+
best_candidate = evaluator.sort_best_candidate(all_candidates)
|
| 259 |
+
if best_candidate is None:
|
| 260 |
+
return None
|
| 261 |
+
|
| 262 |
+
remote_version = best_candidate.version
|
| 263 |
+
if best_candidate.link.is_wheel:
|
| 264 |
+
typ = "wheel"
|
| 265 |
+
else:
|
| 266 |
+
typ = "sdist"
|
| 267 |
+
dist.latest_version = remote_version
|
| 268 |
+
dist.latest_filetype = typ
|
| 269 |
+
return dist
|
| 270 |
+
|
| 271 |
+
for dist in map(latest_info, packages):
|
| 272 |
+
if dist is not None:
|
| 273 |
+
yield dist
|
| 274 |
+
|
| 275 |
+
def output_package_listing(
|
| 276 |
+
self, packages: "_ProcessedDists", options: Values
|
| 277 |
+
) -> None:
|
| 278 |
+
packages = sorted(
|
| 279 |
+
packages,
|
| 280 |
+
key=lambda dist: dist.canonical_name,
|
| 281 |
+
)
|
| 282 |
+
if options.list_format == "columns" and packages:
|
| 283 |
+
data, header = format_for_columns(packages, options)
|
| 284 |
+
self.output_package_listing_columns(data, header)
|
| 285 |
+
elif options.list_format == "freeze":
|
| 286 |
+
for dist in packages:
|
| 287 |
+
if options.verbose >= 1:
|
| 288 |
+
write_output(
|
| 289 |
+
"%s==%s (%s)", dist.raw_name, dist.version, dist.location
|
| 290 |
+
)
|
| 291 |
+
else:
|
| 292 |
+
write_output("%s==%s", dist.raw_name, dist.version)
|
| 293 |
+
elif options.list_format == "json":
|
| 294 |
+
write_output(format_for_json(packages, options))
|
| 295 |
+
|
| 296 |
+
def output_package_listing_columns(
|
| 297 |
+
self, data: List[List[str]], header: List[str]
|
| 298 |
+
) -> None:
|
| 299 |
+
# insert the header first: we need to know the size of column names
|
| 300 |
+
if len(data) > 0:
|
| 301 |
+
data.insert(0, header)
|
| 302 |
+
|
| 303 |
+
pkg_strings, sizes = tabulate(data)
|
| 304 |
+
|
| 305 |
+
# Create and add a separator.
|
| 306 |
+
if len(data) > 0:
|
| 307 |
+
pkg_strings.insert(1, " ".join("-" * x for x in sizes))
|
| 308 |
+
|
| 309 |
+
for val in pkg_strings:
|
| 310 |
+
write_output(val)
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def format_for_columns(
|
| 314 |
+
pkgs: "_ProcessedDists", options: Values
|
| 315 |
+
) -> Tuple[List[List[str]], List[str]]:
|
| 316 |
+
"""
|
| 317 |
+
Convert the package data into something usable
|
| 318 |
+
by output_package_listing_columns.
|
| 319 |
+
"""
|
| 320 |
+
header = ["Package", "Version"]
|
| 321 |
+
|
| 322 |
+
running_outdated = options.outdated
|
| 323 |
+
if running_outdated:
|
| 324 |
+
header.extend(["Latest", "Type"])
|
| 325 |
+
|
| 326 |
+
has_editables = any(x.editable for x in pkgs)
|
| 327 |
+
if has_editables:
|
| 328 |
+
header.append("Editable project location")
|
| 329 |
+
|
| 330 |
+
if options.verbose >= 1:
|
| 331 |
+
header.append("Location")
|
| 332 |
+
if options.verbose >= 1:
|
| 333 |
+
header.append("Installer")
|
| 334 |
+
|
| 335 |
+
data = []
|
| 336 |
+
for proj in pkgs:
|
| 337 |
+
# if we're working on the 'outdated' list, separate out the
|
| 338 |
+
# latest_version and type
|
| 339 |
+
row = [proj.raw_name, proj.raw_version]
|
| 340 |
+
|
| 341 |
+
if running_outdated:
|
| 342 |
+
row.append(str(proj.latest_version))
|
| 343 |
+
row.append(proj.latest_filetype)
|
| 344 |
+
|
| 345 |
+
if has_editables:
|
| 346 |
+
row.append(proj.editable_project_location or "")
|
| 347 |
+
|
| 348 |
+
if options.verbose >= 1:
|
| 349 |
+
row.append(proj.location or "")
|
| 350 |
+
if options.verbose >= 1:
|
| 351 |
+
row.append(proj.installer)
|
| 352 |
+
|
| 353 |
+
data.append(row)
|
| 354 |
+
|
| 355 |
+
return data, header
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def format_for_json(packages: "_ProcessedDists", options: Values) -> str:
|
| 359 |
+
data = []
|
| 360 |
+
for dist in packages:
|
| 361 |
+
info = {
|
| 362 |
+
"name": dist.raw_name,
|
| 363 |
+
"version": str(dist.version),
|
| 364 |
+
}
|
| 365 |
+
if options.verbose >= 1:
|
| 366 |
+
info["location"] = dist.location or ""
|
| 367 |
+
info["installer"] = dist.installer
|
| 368 |
+
if options.outdated:
|
| 369 |
+
info["latest_version"] = str(dist.latest_version)
|
| 370 |
+
info["latest_filetype"] = dist.latest_filetype
|
| 371 |
+
editable_project_location = dist.editable_project_location
|
| 372 |
+
if editable_project_location:
|
| 373 |
+
info["editable_project_location"] = editable_project_location
|
| 374 |
+
data.append(info)
|
| 375 |
+
return json.dumps(data)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/search.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import shutil
|
| 3 |
+
import sys
|
| 4 |
+
import textwrap
|
| 5 |
+
import xmlrpc.client
|
| 6 |
+
from collections import OrderedDict
|
| 7 |
+
from optparse import Values
|
| 8 |
+
from typing import TYPE_CHECKING, Dict, List, Optional, TypedDict
|
| 9 |
+
|
| 10 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 11 |
+
|
| 12 |
+
from pip._internal.cli.base_command import Command
|
| 13 |
+
from pip._internal.cli.req_command import SessionCommandMixin
|
| 14 |
+
from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS
|
| 15 |
+
from pip._internal.exceptions import CommandError
|
| 16 |
+
from pip._internal.metadata import get_default_environment
|
| 17 |
+
from pip._internal.models.index import PyPI
|
| 18 |
+
from pip._internal.network.xmlrpc import PipXmlrpcTransport
|
| 19 |
+
from pip._internal.utils.logging import indent_log
|
| 20 |
+
from pip._internal.utils.misc import write_output
|
| 21 |
+
|
| 22 |
+
if TYPE_CHECKING:
|
| 23 |
+
|
| 24 |
+
class TransformedHit(TypedDict):
|
| 25 |
+
name: str
|
| 26 |
+
summary: str
|
| 27 |
+
versions: List[str]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class SearchCommand(Command, SessionCommandMixin):
|
| 34 |
+
"""Search for PyPI packages whose name or summary contains <query>."""
|
| 35 |
+
|
| 36 |
+
usage = """
|
| 37 |
+
%prog [options] <query>"""
|
| 38 |
+
ignore_require_venv = True
|
| 39 |
+
|
| 40 |
+
def add_options(self) -> None:
|
| 41 |
+
self.cmd_opts.add_option(
|
| 42 |
+
"-i",
|
| 43 |
+
"--index",
|
| 44 |
+
dest="index",
|
| 45 |
+
metavar="URL",
|
| 46 |
+
default=PyPI.pypi_url,
|
| 47 |
+
help="Base URL of Python Package Index (default %default)",
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 51 |
+
|
| 52 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 53 |
+
if not args:
|
| 54 |
+
raise CommandError("Missing required argument (search query).")
|
| 55 |
+
query = args
|
| 56 |
+
pypi_hits = self.search(query, options)
|
| 57 |
+
hits = transform_hits(pypi_hits)
|
| 58 |
+
|
| 59 |
+
terminal_width = None
|
| 60 |
+
if sys.stdout.isatty():
|
| 61 |
+
terminal_width = shutil.get_terminal_size()[0]
|
| 62 |
+
|
| 63 |
+
print_results(hits, terminal_width=terminal_width)
|
| 64 |
+
if pypi_hits:
|
| 65 |
+
return SUCCESS
|
| 66 |
+
return NO_MATCHES_FOUND
|
| 67 |
+
|
| 68 |
+
def search(self, query: List[str], options: Values) -> List[Dict[str, str]]:
|
| 69 |
+
index_url = options.index
|
| 70 |
+
|
| 71 |
+
session = self.get_default_session(options)
|
| 72 |
+
|
| 73 |
+
transport = PipXmlrpcTransport(index_url, session)
|
| 74 |
+
pypi = xmlrpc.client.ServerProxy(index_url, transport)
|
| 75 |
+
try:
|
| 76 |
+
hits = pypi.search({"name": query, "summary": query}, "or")
|
| 77 |
+
except xmlrpc.client.Fault as fault:
|
| 78 |
+
message = (
|
| 79 |
+
f"XMLRPC request failed [code: {fault.faultCode}]\n{fault.faultString}"
|
| 80 |
+
)
|
| 81 |
+
raise CommandError(message)
|
| 82 |
+
assert isinstance(hits, list)
|
| 83 |
+
return hits
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def transform_hits(hits: List[Dict[str, str]]) -> List["TransformedHit"]:
|
| 87 |
+
"""
|
| 88 |
+
The list from pypi is really a list of versions. We want a list of
|
| 89 |
+
packages with the list of versions stored inline. This converts the
|
| 90 |
+
list from pypi into one we can use.
|
| 91 |
+
"""
|
| 92 |
+
packages: Dict[str, TransformedHit] = OrderedDict()
|
| 93 |
+
for hit in hits:
|
| 94 |
+
name = hit["name"]
|
| 95 |
+
summary = hit["summary"]
|
| 96 |
+
version = hit["version"]
|
| 97 |
+
|
| 98 |
+
if name not in packages.keys():
|
| 99 |
+
packages[name] = {
|
| 100 |
+
"name": name,
|
| 101 |
+
"summary": summary,
|
| 102 |
+
"versions": [version],
|
| 103 |
+
}
|
| 104 |
+
else:
|
| 105 |
+
packages[name]["versions"].append(version)
|
| 106 |
+
|
| 107 |
+
# if this is the highest version, replace summary and score
|
| 108 |
+
if version == highest_version(packages[name]["versions"]):
|
| 109 |
+
packages[name]["summary"] = summary
|
| 110 |
+
|
| 111 |
+
return list(packages.values())
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def print_dist_installation_info(name: str, latest: str) -> None:
|
| 115 |
+
env = get_default_environment()
|
| 116 |
+
dist = env.get_distribution(name)
|
| 117 |
+
if dist is not None:
|
| 118 |
+
with indent_log():
|
| 119 |
+
if dist.version == latest:
|
| 120 |
+
write_output("INSTALLED: %s (latest)", dist.version)
|
| 121 |
+
else:
|
| 122 |
+
write_output("INSTALLED: %s", dist.version)
|
| 123 |
+
if parse_version(latest).pre:
|
| 124 |
+
write_output(
|
| 125 |
+
"LATEST: %s (pre-release; install"
|
| 126 |
+
" with `pip install --pre`)",
|
| 127 |
+
latest,
|
| 128 |
+
)
|
| 129 |
+
else:
|
| 130 |
+
write_output("LATEST: %s", latest)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def print_results(
|
| 134 |
+
hits: List["TransformedHit"],
|
| 135 |
+
name_column_width: Optional[int] = None,
|
| 136 |
+
terminal_width: Optional[int] = None,
|
| 137 |
+
) -> None:
|
| 138 |
+
if not hits:
|
| 139 |
+
return
|
| 140 |
+
if name_column_width is None:
|
| 141 |
+
name_column_width = (
|
| 142 |
+
max(
|
| 143 |
+
[
|
| 144 |
+
len(hit["name"]) + len(highest_version(hit.get("versions", ["-"])))
|
| 145 |
+
for hit in hits
|
| 146 |
+
]
|
| 147 |
+
)
|
| 148 |
+
+ 4
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
for hit in hits:
|
| 152 |
+
name = hit["name"]
|
| 153 |
+
summary = hit["summary"] or ""
|
| 154 |
+
latest = highest_version(hit.get("versions", ["-"]))
|
| 155 |
+
if terminal_width is not None:
|
| 156 |
+
target_width = terminal_width - name_column_width - 5
|
| 157 |
+
if target_width > 10:
|
| 158 |
+
# wrap and indent summary to fit terminal
|
| 159 |
+
summary_lines = textwrap.wrap(summary, target_width)
|
| 160 |
+
summary = ("\n" + " " * (name_column_width + 3)).join(summary_lines)
|
| 161 |
+
|
| 162 |
+
name_latest = f"{name} ({latest})"
|
| 163 |
+
line = f"{name_latest:{name_column_width}} - {summary}"
|
| 164 |
+
try:
|
| 165 |
+
write_output(line)
|
| 166 |
+
print_dist_installation_info(name, latest)
|
| 167 |
+
except UnicodeEncodeError:
|
| 168 |
+
pass
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def highest_version(versions: List[str]) -> str:
|
| 172 |
+
return max(versions, key=parse_version)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/show.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from optparse import Values
|
| 3 |
+
from typing import Generator, Iterable, Iterator, List, NamedTuple, Optional
|
| 4 |
+
|
| 5 |
+
from pip._vendor.packaging.requirements import InvalidRequirement
|
| 6 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 7 |
+
|
| 8 |
+
from pip._internal.cli.base_command import Command
|
| 9 |
+
from pip._internal.cli.status_codes import ERROR, SUCCESS
|
| 10 |
+
from pip._internal.metadata import BaseDistribution, get_default_environment
|
| 11 |
+
from pip._internal.utils.misc import write_output
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ShowCommand(Command):
|
| 17 |
+
"""
|
| 18 |
+
Show information about one or more installed packages.
|
| 19 |
+
|
| 20 |
+
The output is in RFC-compliant mail header format.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
usage = """
|
| 24 |
+
%prog [options] <package> ..."""
|
| 25 |
+
ignore_require_venv = True
|
| 26 |
+
|
| 27 |
+
def add_options(self) -> None:
|
| 28 |
+
self.cmd_opts.add_option(
|
| 29 |
+
"-f",
|
| 30 |
+
"--files",
|
| 31 |
+
dest="files",
|
| 32 |
+
action="store_true",
|
| 33 |
+
default=False,
|
| 34 |
+
help="Show the full list of installed files for each package.",
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 38 |
+
|
| 39 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 40 |
+
if not args:
|
| 41 |
+
logger.warning("ERROR: Please provide a package name or names.")
|
| 42 |
+
return ERROR
|
| 43 |
+
query = args
|
| 44 |
+
|
| 45 |
+
results = search_packages_info(query)
|
| 46 |
+
if not print_results(
|
| 47 |
+
results, list_files=options.files, verbose=options.verbose
|
| 48 |
+
):
|
| 49 |
+
return ERROR
|
| 50 |
+
return SUCCESS
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class _PackageInfo(NamedTuple):
|
| 54 |
+
name: str
|
| 55 |
+
version: str
|
| 56 |
+
location: str
|
| 57 |
+
editable_project_location: Optional[str]
|
| 58 |
+
requires: List[str]
|
| 59 |
+
required_by: List[str]
|
| 60 |
+
installer: str
|
| 61 |
+
metadata_version: str
|
| 62 |
+
classifiers: List[str]
|
| 63 |
+
summary: str
|
| 64 |
+
homepage: str
|
| 65 |
+
project_urls: List[str]
|
| 66 |
+
author: str
|
| 67 |
+
author_email: str
|
| 68 |
+
license: str
|
| 69 |
+
license_expression: str
|
| 70 |
+
entry_points: List[str]
|
| 71 |
+
files: Optional[List[str]]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def search_packages_info(query: List[str]) -> Generator[_PackageInfo, None, None]:
|
| 75 |
+
"""
|
| 76 |
+
Gather details from installed distributions. Print distribution name,
|
| 77 |
+
version, location, and installed files. Installed files requires a
|
| 78 |
+
pip generated 'installed-files.txt' in the distributions '.egg-info'
|
| 79 |
+
directory.
|
| 80 |
+
"""
|
| 81 |
+
env = get_default_environment()
|
| 82 |
+
|
| 83 |
+
installed = {dist.canonical_name: dist for dist in env.iter_all_distributions()}
|
| 84 |
+
query_names = [canonicalize_name(name) for name in query]
|
| 85 |
+
missing = sorted(
|
| 86 |
+
[name for name, pkg in zip(query, query_names) if pkg not in installed]
|
| 87 |
+
)
|
| 88 |
+
if missing:
|
| 89 |
+
logger.warning("Package(s) not found: %s", ", ".join(missing))
|
| 90 |
+
|
| 91 |
+
def _get_requiring_packages(current_dist: BaseDistribution) -> Iterator[str]:
|
| 92 |
+
return (
|
| 93 |
+
dist.metadata["Name"] or "UNKNOWN"
|
| 94 |
+
for dist in installed.values()
|
| 95 |
+
if current_dist.canonical_name
|
| 96 |
+
in {canonicalize_name(d.name) for d in dist.iter_dependencies()}
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
for query_name in query_names:
|
| 100 |
+
try:
|
| 101 |
+
dist = installed[query_name]
|
| 102 |
+
except KeyError:
|
| 103 |
+
continue
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
requires = sorted(
|
| 107 |
+
# Avoid duplicates in requirements (e.g. due to environment markers).
|
| 108 |
+
{req.name for req in dist.iter_dependencies()},
|
| 109 |
+
key=str.lower,
|
| 110 |
+
)
|
| 111 |
+
except InvalidRequirement:
|
| 112 |
+
requires = sorted(dist.iter_raw_dependencies(), key=str.lower)
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
required_by = sorted(_get_requiring_packages(dist), key=str.lower)
|
| 116 |
+
except InvalidRequirement:
|
| 117 |
+
required_by = ["#N/A"]
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
entry_points_text = dist.read_text("entry_points.txt")
|
| 121 |
+
entry_points = entry_points_text.splitlines(keepends=False)
|
| 122 |
+
except FileNotFoundError:
|
| 123 |
+
entry_points = []
|
| 124 |
+
|
| 125 |
+
files_iter = dist.iter_declared_entries()
|
| 126 |
+
if files_iter is None:
|
| 127 |
+
files: Optional[List[str]] = None
|
| 128 |
+
else:
|
| 129 |
+
files = sorted(files_iter)
|
| 130 |
+
|
| 131 |
+
metadata = dist.metadata
|
| 132 |
+
|
| 133 |
+
project_urls = metadata.get_all("Project-URL", [])
|
| 134 |
+
homepage = metadata.get("Home-page", "")
|
| 135 |
+
if not homepage:
|
| 136 |
+
# It's common that there is a "homepage" Project-URL, but Home-page
|
| 137 |
+
# remains unset (especially as PEP 621 doesn't surface the field).
|
| 138 |
+
#
|
| 139 |
+
# This logic was taken from PyPI's codebase.
|
| 140 |
+
for url in project_urls:
|
| 141 |
+
url_label, url = url.split(",", maxsplit=1)
|
| 142 |
+
normalized_label = (
|
| 143 |
+
url_label.casefold().replace("-", "").replace("_", "").strip()
|
| 144 |
+
)
|
| 145 |
+
if normalized_label == "homepage":
|
| 146 |
+
homepage = url.strip()
|
| 147 |
+
break
|
| 148 |
+
|
| 149 |
+
yield _PackageInfo(
|
| 150 |
+
name=dist.raw_name,
|
| 151 |
+
version=dist.raw_version,
|
| 152 |
+
location=dist.location or "",
|
| 153 |
+
editable_project_location=dist.editable_project_location,
|
| 154 |
+
requires=requires,
|
| 155 |
+
required_by=required_by,
|
| 156 |
+
installer=dist.installer,
|
| 157 |
+
metadata_version=dist.metadata_version or "",
|
| 158 |
+
classifiers=metadata.get_all("Classifier", []),
|
| 159 |
+
summary=metadata.get("Summary", ""),
|
| 160 |
+
homepage=homepage,
|
| 161 |
+
project_urls=project_urls,
|
| 162 |
+
author=metadata.get("Author", ""),
|
| 163 |
+
author_email=metadata.get("Author-email", ""),
|
| 164 |
+
license=metadata.get("License", ""),
|
| 165 |
+
license_expression=metadata.get("License-Expression", ""),
|
| 166 |
+
entry_points=entry_points,
|
| 167 |
+
files=files,
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def print_results(
|
| 172 |
+
distributions: Iterable[_PackageInfo],
|
| 173 |
+
list_files: bool,
|
| 174 |
+
verbose: bool,
|
| 175 |
+
) -> bool:
|
| 176 |
+
"""
|
| 177 |
+
Print the information from installed distributions found.
|
| 178 |
+
"""
|
| 179 |
+
results_printed = False
|
| 180 |
+
for i, dist in enumerate(distributions):
|
| 181 |
+
results_printed = True
|
| 182 |
+
if i > 0:
|
| 183 |
+
write_output("---")
|
| 184 |
+
|
| 185 |
+
metadata_version_tuple = tuple(map(int, dist.metadata_version.split(".")))
|
| 186 |
+
|
| 187 |
+
write_output("Name: %s", dist.name)
|
| 188 |
+
write_output("Version: %s", dist.version)
|
| 189 |
+
write_output("Summary: %s", dist.summary)
|
| 190 |
+
write_output("Home-page: %s", dist.homepage)
|
| 191 |
+
write_output("Author: %s", dist.author)
|
| 192 |
+
write_output("Author-email: %s", dist.author_email)
|
| 193 |
+
if metadata_version_tuple >= (2, 4) and dist.license_expression:
|
| 194 |
+
write_output("License-Expression: %s", dist.license_expression)
|
| 195 |
+
else:
|
| 196 |
+
write_output("License: %s", dist.license)
|
| 197 |
+
write_output("Location: %s", dist.location)
|
| 198 |
+
if dist.editable_project_location is not None:
|
| 199 |
+
write_output(
|
| 200 |
+
"Editable project location: %s", dist.editable_project_location
|
| 201 |
+
)
|
| 202 |
+
write_output("Requires: %s", ", ".join(dist.requires))
|
| 203 |
+
write_output("Required-by: %s", ", ".join(dist.required_by))
|
| 204 |
+
|
| 205 |
+
if verbose:
|
| 206 |
+
write_output("Metadata-Version: %s", dist.metadata_version)
|
| 207 |
+
write_output("Installer: %s", dist.installer)
|
| 208 |
+
write_output("Classifiers:")
|
| 209 |
+
for classifier in dist.classifiers:
|
| 210 |
+
write_output(" %s", classifier)
|
| 211 |
+
write_output("Entry-points:")
|
| 212 |
+
for entry in dist.entry_points:
|
| 213 |
+
write_output(" %s", entry.strip())
|
| 214 |
+
write_output("Project-URLs:")
|
| 215 |
+
for project_url in dist.project_urls:
|
| 216 |
+
write_output(" %s", project_url)
|
| 217 |
+
if list_files:
|
| 218 |
+
write_output("Files:")
|
| 219 |
+
if dist.files is None:
|
| 220 |
+
write_output("Cannot locate RECORD or installed-files.txt")
|
| 221 |
+
else:
|
| 222 |
+
for line in dist.files:
|
| 223 |
+
write_output(" %s", line.strip())
|
| 224 |
+
return results_printed
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/commands/wheel.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
import shutil
|
| 4 |
+
from optparse import Values
|
| 5 |
+
from typing import List
|
| 6 |
+
|
| 7 |
+
from pip._internal.cache import WheelCache
|
| 8 |
+
from pip._internal.cli import cmdoptions
|
| 9 |
+
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
|
| 10 |
+
from pip._internal.cli.status_codes import SUCCESS
|
| 11 |
+
from pip._internal.exceptions import CommandError
|
| 12 |
+
from pip._internal.operations.build.build_tracker import get_build_tracker
|
| 13 |
+
from pip._internal.req.req_install import (
|
| 14 |
+
InstallRequirement,
|
| 15 |
+
check_legacy_setup_py_options,
|
| 16 |
+
)
|
| 17 |
+
from pip._internal.utils.misc import ensure_dir, normalize_path
|
| 18 |
+
from pip._internal.utils.temp_dir import TempDirectory
|
| 19 |
+
from pip._internal.wheel_builder import build, should_build_for_wheel_command
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class WheelCommand(RequirementCommand):
|
| 25 |
+
"""
|
| 26 |
+
Build Wheel archives for your requirements and dependencies.
|
| 27 |
+
|
| 28 |
+
Wheel is a built-package format, and offers the advantage of not
|
| 29 |
+
recompiling your software during every install. For more details, see the
|
| 30 |
+
wheel docs: https://wheel.readthedocs.io/en/latest/
|
| 31 |
+
|
| 32 |
+
'pip wheel' uses the build system interface as described here:
|
| 33 |
+
https://pip.pypa.io/en/stable/reference/build-system/
|
| 34 |
+
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
usage = """
|
| 38 |
+
%prog [options] <requirement specifier> ...
|
| 39 |
+
%prog [options] -r <requirements file> ...
|
| 40 |
+
%prog [options] [-e] <vcs project url> ...
|
| 41 |
+
%prog [options] [-e] <local project path> ...
|
| 42 |
+
%prog [options] <archive url/path> ..."""
|
| 43 |
+
|
| 44 |
+
def add_options(self) -> None:
|
| 45 |
+
self.cmd_opts.add_option(
|
| 46 |
+
"-w",
|
| 47 |
+
"--wheel-dir",
|
| 48 |
+
dest="wheel_dir",
|
| 49 |
+
metavar="dir",
|
| 50 |
+
default=os.curdir,
|
| 51 |
+
help=(
|
| 52 |
+
"Build wheels into <dir>, where the default is the "
|
| 53 |
+
"current working directory."
|
| 54 |
+
),
|
| 55 |
+
)
|
| 56 |
+
self.cmd_opts.add_option(cmdoptions.no_binary())
|
| 57 |
+
self.cmd_opts.add_option(cmdoptions.only_binary())
|
| 58 |
+
self.cmd_opts.add_option(cmdoptions.prefer_binary())
|
| 59 |
+
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
|
| 60 |
+
self.cmd_opts.add_option(cmdoptions.use_pep517())
|
| 61 |
+
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
|
| 62 |
+
self.cmd_opts.add_option(cmdoptions.check_build_deps())
|
| 63 |
+
self.cmd_opts.add_option(cmdoptions.constraints())
|
| 64 |
+
self.cmd_opts.add_option(cmdoptions.editable())
|
| 65 |
+
self.cmd_opts.add_option(cmdoptions.requirements())
|
| 66 |
+
self.cmd_opts.add_option(cmdoptions.src())
|
| 67 |
+
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
|
| 68 |
+
self.cmd_opts.add_option(cmdoptions.no_deps())
|
| 69 |
+
self.cmd_opts.add_option(cmdoptions.progress_bar())
|
| 70 |
+
|
| 71 |
+
self.cmd_opts.add_option(
|
| 72 |
+
"--no-verify",
|
| 73 |
+
dest="no_verify",
|
| 74 |
+
action="store_true",
|
| 75 |
+
default=False,
|
| 76 |
+
help="Don't verify if built wheel is valid.",
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
self.cmd_opts.add_option(cmdoptions.config_settings())
|
| 80 |
+
self.cmd_opts.add_option(cmdoptions.build_options())
|
| 81 |
+
self.cmd_opts.add_option(cmdoptions.global_options())
|
| 82 |
+
|
| 83 |
+
self.cmd_opts.add_option(
|
| 84 |
+
"--pre",
|
| 85 |
+
action="store_true",
|
| 86 |
+
default=False,
|
| 87 |
+
help=(
|
| 88 |
+
"Include pre-release and development versions. By default, "
|
| 89 |
+
"pip only finds stable versions."
|
| 90 |
+
),
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
self.cmd_opts.add_option(cmdoptions.require_hashes())
|
| 94 |
+
|
| 95 |
+
index_opts = cmdoptions.make_option_group(
|
| 96 |
+
cmdoptions.index_group,
|
| 97 |
+
self.parser,
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
self.parser.insert_option_group(0, index_opts)
|
| 101 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 102 |
+
|
| 103 |
+
@with_cleanup
|
| 104 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 105 |
+
session = self.get_default_session(options)
|
| 106 |
+
|
| 107 |
+
finder = self._build_package_finder(options, session)
|
| 108 |
+
|
| 109 |
+
options.wheel_dir = normalize_path(options.wheel_dir)
|
| 110 |
+
ensure_dir(options.wheel_dir)
|
| 111 |
+
|
| 112 |
+
build_tracker = self.enter_context(get_build_tracker())
|
| 113 |
+
|
| 114 |
+
directory = TempDirectory(
|
| 115 |
+
delete=not options.no_clean,
|
| 116 |
+
kind="wheel",
|
| 117 |
+
globally_managed=True,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
reqs = self.get_requirements(args, options, finder, session)
|
| 121 |
+
check_legacy_setup_py_options(options, reqs)
|
| 122 |
+
|
| 123 |
+
wheel_cache = WheelCache(options.cache_dir)
|
| 124 |
+
|
| 125 |
+
preparer = self.make_requirement_preparer(
|
| 126 |
+
temp_build_dir=directory,
|
| 127 |
+
options=options,
|
| 128 |
+
build_tracker=build_tracker,
|
| 129 |
+
session=session,
|
| 130 |
+
finder=finder,
|
| 131 |
+
download_dir=options.wheel_dir,
|
| 132 |
+
use_user_site=False,
|
| 133 |
+
verbosity=self.verbosity,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
resolver = self.make_resolver(
|
| 137 |
+
preparer=preparer,
|
| 138 |
+
finder=finder,
|
| 139 |
+
options=options,
|
| 140 |
+
wheel_cache=wheel_cache,
|
| 141 |
+
ignore_requires_python=options.ignore_requires_python,
|
| 142 |
+
use_pep517=options.use_pep517,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
self.trace_basic_info(finder)
|
| 146 |
+
|
| 147 |
+
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
|
| 148 |
+
|
| 149 |
+
reqs_to_build: List[InstallRequirement] = []
|
| 150 |
+
for req in requirement_set.requirements.values():
|
| 151 |
+
if req.is_wheel:
|
| 152 |
+
preparer.save_linked_requirement(req)
|
| 153 |
+
elif should_build_for_wheel_command(req):
|
| 154 |
+
reqs_to_build.append(req)
|
| 155 |
+
|
| 156 |
+
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
|
| 157 |
+
|
| 158 |
+
# build wheels
|
| 159 |
+
build_successes, build_failures = build(
|
| 160 |
+
reqs_to_build,
|
| 161 |
+
wheel_cache=wheel_cache,
|
| 162 |
+
verify=(not options.no_verify),
|
| 163 |
+
build_options=options.build_options or [],
|
| 164 |
+
global_options=options.global_options or [],
|
| 165 |
+
)
|
| 166 |
+
for req in build_successes:
|
| 167 |
+
assert req.link and req.link.is_wheel
|
| 168 |
+
assert req.local_file_path
|
| 169 |
+
# copy from cache to target directory
|
| 170 |
+
try:
|
| 171 |
+
shutil.copy(req.local_file_path, options.wheel_dir)
|
| 172 |
+
except OSError as e:
|
| 173 |
+
logger.warning(
|
| 174 |
+
"Building wheel for %s failed: %s",
|
| 175 |
+
req.name,
|
| 176 |
+
e,
|
| 177 |
+
)
|
| 178 |
+
build_failures.append(req)
|
| 179 |
+
if len(build_failures) != 0:
|
| 180 |
+
raise CommandError("Failed to build one or more wheels")
|
| 181 |
+
|
| 182 |
+
return SUCCESS
|
URSA/.venv_ursa/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})"
|
URSA/.venv_ursa/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 |
+
)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/index/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Index interaction code
|
| 2 |
+
"""
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/index/collector.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
The main purpose of this module is to expose LinkCollector.collect_sources().
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import collections
|
| 6 |
+
import email.message
|
| 7 |
+
import functools
|
| 8 |
+
import itertools
|
| 9 |
+
import json
|
| 10 |
+
import logging
|
| 11 |
+
import os
|
| 12 |
+
import urllib.parse
|
| 13 |
+
import urllib.request
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from html.parser import HTMLParser
|
| 16 |
+
from optparse import Values
|
| 17 |
+
from typing import (
|
| 18 |
+
Callable,
|
| 19 |
+
Dict,
|
| 20 |
+
Iterable,
|
| 21 |
+
List,
|
| 22 |
+
MutableMapping,
|
| 23 |
+
NamedTuple,
|
| 24 |
+
Optional,
|
| 25 |
+
Protocol,
|
| 26 |
+
Sequence,
|
| 27 |
+
Tuple,
|
| 28 |
+
Union,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
from pip._vendor import requests
|
| 32 |
+
from pip._vendor.requests import Response
|
| 33 |
+
from pip._vendor.requests.exceptions import RetryError, SSLError
|
| 34 |
+
|
| 35 |
+
from pip._internal.exceptions import NetworkConnectionError
|
| 36 |
+
from pip._internal.models.link import Link
|
| 37 |
+
from pip._internal.models.search_scope import SearchScope
|
| 38 |
+
from pip._internal.network.session import PipSession
|
| 39 |
+
from pip._internal.network.utils import raise_for_status
|
| 40 |
+
from pip._internal.utils.filetypes import is_archive_file
|
| 41 |
+
from pip._internal.utils.misc import redact_auth_from_url
|
| 42 |
+
from pip._internal.vcs import vcs
|
| 43 |
+
|
| 44 |
+
from .sources import CandidatesFromPage, LinkSource, build_source
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
ResponseHeaders = MutableMapping[str, str]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _match_vcs_scheme(url: str) -> Optional[str]:
|
| 52 |
+
"""Look for VCS schemes in the URL.
|
| 53 |
+
|
| 54 |
+
Returns the matched VCS scheme, or None if there's no match.
|
| 55 |
+
"""
|
| 56 |
+
for scheme in vcs.schemes:
|
| 57 |
+
if url.lower().startswith(scheme) and url[len(scheme)] in "+:":
|
| 58 |
+
return scheme
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class _NotAPIContent(Exception):
|
| 63 |
+
def __init__(self, content_type: str, request_desc: str) -> None:
|
| 64 |
+
super().__init__(content_type, request_desc)
|
| 65 |
+
self.content_type = content_type
|
| 66 |
+
self.request_desc = request_desc
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _ensure_api_header(response: Response) -> None:
|
| 70 |
+
"""
|
| 71 |
+
Check the Content-Type header to ensure the response contains a Simple
|
| 72 |
+
API Response.
|
| 73 |
+
|
| 74 |
+
Raises `_NotAPIContent` if the content type is not a valid content-type.
|
| 75 |
+
"""
|
| 76 |
+
content_type = response.headers.get("Content-Type", "Unknown")
|
| 77 |
+
|
| 78 |
+
content_type_l = content_type.lower()
|
| 79 |
+
if content_type_l.startswith(
|
| 80 |
+
(
|
| 81 |
+
"text/html",
|
| 82 |
+
"application/vnd.pypi.simple.v1+html",
|
| 83 |
+
"application/vnd.pypi.simple.v1+json",
|
| 84 |
+
)
|
| 85 |
+
):
|
| 86 |
+
return
|
| 87 |
+
|
| 88 |
+
raise _NotAPIContent(content_type, response.request.method)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class _NotHTTP(Exception):
|
| 92 |
+
pass
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _ensure_api_response(url: str, session: PipSession) -> None:
|
| 96 |
+
"""
|
| 97 |
+
Send a HEAD request to the URL, and ensure the response contains a simple
|
| 98 |
+
API Response.
|
| 99 |
+
|
| 100 |
+
Raises `_NotHTTP` if the URL is not available for a HEAD request, or
|
| 101 |
+
`_NotAPIContent` if the content type is not a valid content type.
|
| 102 |
+
"""
|
| 103 |
+
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
|
| 104 |
+
if scheme not in {"http", "https"}:
|
| 105 |
+
raise _NotHTTP()
|
| 106 |
+
|
| 107 |
+
resp = session.head(url, allow_redirects=True)
|
| 108 |
+
raise_for_status(resp)
|
| 109 |
+
|
| 110 |
+
_ensure_api_header(resp)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _get_simple_response(url: str, session: PipSession) -> Response:
|
| 114 |
+
"""Access an Simple API response with GET, and return the response.
|
| 115 |
+
|
| 116 |
+
This consists of three parts:
|
| 117 |
+
|
| 118 |
+
1. If the URL looks suspiciously like an archive, send a HEAD first to
|
| 119 |
+
check the Content-Type is HTML or Simple API, to avoid downloading a
|
| 120 |
+
large file. Raise `_NotHTTP` if the content type cannot be determined, or
|
| 121 |
+
`_NotAPIContent` if it is not HTML or a Simple API.
|
| 122 |
+
2. Actually perform the request. Raise HTTP exceptions on network failures.
|
| 123 |
+
3. Check the Content-Type header to make sure we got a Simple API response,
|
| 124 |
+
and raise `_NotAPIContent` otherwise.
|
| 125 |
+
"""
|
| 126 |
+
if is_archive_file(Link(url).filename):
|
| 127 |
+
_ensure_api_response(url, session=session)
|
| 128 |
+
|
| 129 |
+
logger.debug("Getting page %s", redact_auth_from_url(url))
|
| 130 |
+
|
| 131 |
+
resp = session.get(
|
| 132 |
+
url,
|
| 133 |
+
headers={
|
| 134 |
+
"Accept": ", ".join(
|
| 135 |
+
[
|
| 136 |
+
"application/vnd.pypi.simple.v1+json",
|
| 137 |
+
"application/vnd.pypi.simple.v1+html; q=0.1",
|
| 138 |
+
"text/html; q=0.01",
|
| 139 |
+
]
|
| 140 |
+
),
|
| 141 |
+
# We don't want to blindly returned cached data for
|
| 142 |
+
# /simple/, because authors generally expecting that
|
| 143 |
+
# twine upload && pip install will function, but if
|
| 144 |
+
# they've done a pip install in the last ~10 minutes
|
| 145 |
+
# it won't. Thus by setting this to zero we will not
|
| 146 |
+
# blindly use any cached data, however the benefit of
|
| 147 |
+
# using max-age=0 instead of no-cache, is that we will
|
| 148 |
+
# still support conditional requests, so we will still
|
| 149 |
+
# minimize traffic sent in cases where the page hasn't
|
| 150 |
+
# changed at all, we will just always incur the round
|
| 151 |
+
# trip for the conditional GET now instead of only
|
| 152 |
+
# once per 10 minutes.
|
| 153 |
+
# For more information, please see pypa/pip#5670.
|
| 154 |
+
"Cache-Control": "max-age=0",
|
| 155 |
+
},
|
| 156 |
+
)
|
| 157 |
+
raise_for_status(resp)
|
| 158 |
+
|
| 159 |
+
# The check for archives above only works if the url ends with
|
| 160 |
+
# something that looks like an archive. However that is not a
|
| 161 |
+
# requirement of an url. Unless we issue a HEAD request on every
|
| 162 |
+
# url we cannot know ahead of time for sure if something is a
|
| 163 |
+
# Simple API response or not. However we can check after we've
|
| 164 |
+
# downloaded it.
|
| 165 |
+
_ensure_api_header(resp)
|
| 166 |
+
|
| 167 |
+
logger.debug(
|
| 168 |
+
"Fetched page %s as %s",
|
| 169 |
+
redact_auth_from_url(url),
|
| 170 |
+
resp.headers.get("Content-Type", "Unknown"),
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
return resp
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _get_encoding_from_headers(headers: ResponseHeaders) -> Optional[str]:
|
| 177 |
+
"""Determine if we have any encoding information in our headers."""
|
| 178 |
+
if headers and "Content-Type" in headers:
|
| 179 |
+
m = email.message.Message()
|
| 180 |
+
m["content-type"] = headers["Content-Type"]
|
| 181 |
+
charset = m.get_param("charset")
|
| 182 |
+
if charset:
|
| 183 |
+
return str(charset)
|
| 184 |
+
return None
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
class CacheablePageContent:
|
| 188 |
+
def __init__(self, page: "IndexContent") -> None:
|
| 189 |
+
assert page.cache_link_parsing
|
| 190 |
+
self.page = page
|
| 191 |
+
|
| 192 |
+
def __eq__(self, other: object) -> bool:
|
| 193 |
+
return isinstance(other, type(self)) and self.page.url == other.page.url
|
| 194 |
+
|
| 195 |
+
def __hash__(self) -> int:
|
| 196 |
+
return hash(self.page.url)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class ParseLinks(Protocol):
|
| 200 |
+
def __call__(self, page: "IndexContent") -> Iterable[Link]: ...
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def with_cached_index_content(fn: ParseLinks) -> ParseLinks:
|
| 204 |
+
"""
|
| 205 |
+
Given a function that parses an Iterable[Link] from an IndexContent, cache the
|
| 206 |
+
function's result (keyed by CacheablePageContent), unless the IndexContent
|
| 207 |
+
`page` has `page.cache_link_parsing == False`.
|
| 208 |
+
"""
|
| 209 |
+
|
| 210 |
+
@functools.lru_cache(maxsize=None)
|
| 211 |
+
def wrapper(cacheable_page: CacheablePageContent) -> List[Link]:
|
| 212 |
+
return list(fn(cacheable_page.page))
|
| 213 |
+
|
| 214 |
+
@functools.wraps(fn)
|
| 215 |
+
def wrapper_wrapper(page: "IndexContent") -> List[Link]:
|
| 216 |
+
if page.cache_link_parsing:
|
| 217 |
+
return wrapper(CacheablePageContent(page))
|
| 218 |
+
return list(fn(page))
|
| 219 |
+
|
| 220 |
+
return wrapper_wrapper
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@with_cached_index_content
|
| 224 |
+
def parse_links(page: "IndexContent") -> Iterable[Link]:
|
| 225 |
+
"""
|
| 226 |
+
Parse a Simple API's Index Content, and yield its anchor elements as Link objects.
|
| 227 |
+
"""
|
| 228 |
+
|
| 229 |
+
content_type_l = page.content_type.lower()
|
| 230 |
+
if content_type_l.startswith("application/vnd.pypi.simple.v1+json"):
|
| 231 |
+
data = json.loads(page.content)
|
| 232 |
+
for file in data.get("files", []):
|
| 233 |
+
link = Link.from_json(file, page.url)
|
| 234 |
+
if link is None:
|
| 235 |
+
continue
|
| 236 |
+
yield link
|
| 237 |
+
return
|
| 238 |
+
|
| 239 |
+
parser = HTMLLinkParser(page.url)
|
| 240 |
+
encoding = page.encoding or "utf-8"
|
| 241 |
+
parser.feed(page.content.decode(encoding))
|
| 242 |
+
|
| 243 |
+
url = page.url
|
| 244 |
+
base_url = parser.base_url or url
|
| 245 |
+
for anchor in parser.anchors:
|
| 246 |
+
link = Link.from_element(anchor, page_url=url, base_url=base_url)
|
| 247 |
+
if link is None:
|
| 248 |
+
continue
|
| 249 |
+
yield link
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
@dataclass(frozen=True)
|
| 253 |
+
class IndexContent:
|
| 254 |
+
"""Represents one response (or page), along with its URL.
|
| 255 |
+
|
| 256 |
+
:param encoding: the encoding to decode the given content.
|
| 257 |
+
:param url: the URL from which the HTML was downloaded.
|
| 258 |
+
:param cache_link_parsing: whether links parsed from this page's url
|
| 259 |
+
should be cached. PyPI index urls should
|
| 260 |
+
have this set to False, for example.
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
content: bytes
|
| 264 |
+
content_type: str
|
| 265 |
+
encoding: Optional[str]
|
| 266 |
+
url: str
|
| 267 |
+
cache_link_parsing: bool = True
|
| 268 |
+
|
| 269 |
+
def __str__(self) -> str:
|
| 270 |
+
return redact_auth_from_url(self.url)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
class HTMLLinkParser(HTMLParser):
|
| 274 |
+
"""
|
| 275 |
+
HTMLParser that keeps the first base HREF and a list of all anchor
|
| 276 |
+
elements' attributes.
|
| 277 |
+
"""
|
| 278 |
+
|
| 279 |
+
def __init__(self, url: str) -> None:
|
| 280 |
+
super().__init__(convert_charrefs=True)
|
| 281 |
+
|
| 282 |
+
self.url: str = url
|
| 283 |
+
self.base_url: Optional[str] = None
|
| 284 |
+
self.anchors: List[Dict[str, Optional[str]]] = []
|
| 285 |
+
|
| 286 |
+
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
|
| 287 |
+
if tag == "base" and self.base_url is None:
|
| 288 |
+
href = self.get_href(attrs)
|
| 289 |
+
if href is not None:
|
| 290 |
+
self.base_url = href
|
| 291 |
+
elif tag == "a":
|
| 292 |
+
self.anchors.append(dict(attrs))
|
| 293 |
+
|
| 294 |
+
def get_href(self, attrs: List[Tuple[str, Optional[str]]]) -> Optional[str]:
|
| 295 |
+
for name, value in attrs:
|
| 296 |
+
if name == "href":
|
| 297 |
+
return value
|
| 298 |
+
return None
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def _handle_get_simple_fail(
|
| 302 |
+
link: Link,
|
| 303 |
+
reason: Union[str, Exception],
|
| 304 |
+
meth: Optional[Callable[..., None]] = None,
|
| 305 |
+
) -> None:
|
| 306 |
+
if meth is None:
|
| 307 |
+
meth = logger.debug
|
| 308 |
+
meth("Could not fetch URL %s: %s - skipping", link, reason)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def _make_index_content(
|
| 312 |
+
response: Response, cache_link_parsing: bool = True
|
| 313 |
+
) -> IndexContent:
|
| 314 |
+
encoding = _get_encoding_from_headers(response.headers)
|
| 315 |
+
return IndexContent(
|
| 316 |
+
response.content,
|
| 317 |
+
response.headers["Content-Type"],
|
| 318 |
+
encoding=encoding,
|
| 319 |
+
url=response.url,
|
| 320 |
+
cache_link_parsing=cache_link_parsing,
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def _get_index_content(link: Link, *, session: PipSession) -> Optional["IndexContent"]:
|
| 325 |
+
url = link.url.split("#", 1)[0]
|
| 326 |
+
|
| 327 |
+
# Check for VCS schemes that do not support lookup as web pages.
|
| 328 |
+
vcs_scheme = _match_vcs_scheme(url)
|
| 329 |
+
if vcs_scheme:
|
| 330 |
+
logger.warning(
|
| 331 |
+
"Cannot look at %s URL %s because it does not support lookup as web pages.",
|
| 332 |
+
vcs_scheme,
|
| 333 |
+
link,
|
| 334 |
+
)
|
| 335 |
+
return None
|
| 336 |
+
|
| 337 |
+
# Tack index.html onto file:// URLs that point to directories
|
| 338 |
+
scheme, _, path, _, _, _ = urllib.parse.urlparse(url)
|
| 339 |
+
if scheme == "file" and os.path.isdir(urllib.request.url2pathname(path)):
|
| 340 |
+
# add trailing slash if not present so urljoin doesn't trim
|
| 341 |
+
# final segment
|
| 342 |
+
if not url.endswith("/"):
|
| 343 |
+
url += "/"
|
| 344 |
+
# TODO: In the future, it would be nice if pip supported PEP 691
|
| 345 |
+
# style responses in the file:// URLs, however there's no
|
| 346 |
+
# standard file extension for application/vnd.pypi.simple.v1+json
|
| 347 |
+
# so we'll need to come up with something on our own.
|
| 348 |
+
url = urllib.parse.urljoin(url, "index.html")
|
| 349 |
+
logger.debug(" file: URL is directory, getting %s", url)
|
| 350 |
+
|
| 351 |
+
try:
|
| 352 |
+
resp = _get_simple_response(url, session=session)
|
| 353 |
+
except _NotHTTP:
|
| 354 |
+
logger.warning(
|
| 355 |
+
"Skipping page %s because it looks like an archive, and cannot "
|
| 356 |
+
"be checked by a HTTP HEAD request.",
|
| 357 |
+
link,
|
| 358 |
+
)
|
| 359 |
+
except _NotAPIContent as exc:
|
| 360 |
+
logger.warning(
|
| 361 |
+
"Skipping page %s because the %s request got Content-Type: %s. "
|
| 362 |
+
"The only supported Content-Types are application/vnd.pypi.simple.v1+json, "
|
| 363 |
+
"application/vnd.pypi.simple.v1+html, and text/html",
|
| 364 |
+
link,
|
| 365 |
+
exc.request_desc,
|
| 366 |
+
exc.content_type,
|
| 367 |
+
)
|
| 368 |
+
except NetworkConnectionError as exc:
|
| 369 |
+
_handle_get_simple_fail(link, exc)
|
| 370 |
+
except RetryError as exc:
|
| 371 |
+
_handle_get_simple_fail(link, exc)
|
| 372 |
+
except SSLError as exc:
|
| 373 |
+
reason = "There was a problem confirming the ssl certificate: "
|
| 374 |
+
reason += str(exc)
|
| 375 |
+
_handle_get_simple_fail(link, reason, meth=logger.info)
|
| 376 |
+
except requests.ConnectionError as exc:
|
| 377 |
+
_handle_get_simple_fail(link, f"connection error: {exc}")
|
| 378 |
+
except requests.Timeout:
|
| 379 |
+
_handle_get_simple_fail(link, "timed out")
|
| 380 |
+
else:
|
| 381 |
+
return _make_index_content(resp, cache_link_parsing=link.cache_link_parsing)
|
| 382 |
+
return None
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
class CollectedSources(NamedTuple):
|
| 386 |
+
find_links: Sequence[Optional[LinkSource]]
|
| 387 |
+
index_urls: Sequence[Optional[LinkSource]]
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
class LinkCollector:
|
| 391 |
+
"""
|
| 392 |
+
Responsible for collecting Link objects from all configured locations,
|
| 393 |
+
making network requests as needed.
|
| 394 |
+
|
| 395 |
+
The class's main method is its collect_sources() method.
|
| 396 |
+
"""
|
| 397 |
+
|
| 398 |
+
def __init__(
|
| 399 |
+
self,
|
| 400 |
+
session: PipSession,
|
| 401 |
+
search_scope: SearchScope,
|
| 402 |
+
) -> None:
|
| 403 |
+
self.search_scope = search_scope
|
| 404 |
+
self.session = session
|
| 405 |
+
|
| 406 |
+
@classmethod
|
| 407 |
+
def create(
|
| 408 |
+
cls,
|
| 409 |
+
session: PipSession,
|
| 410 |
+
options: Values,
|
| 411 |
+
suppress_no_index: bool = False,
|
| 412 |
+
) -> "LinkCollector":
|
| 413 |
+
"""
|
| 414 |
+
:param session: The Session to use to make requests.
|
| 415 |
+
:param suppress_no_index: Whether to ignore the --no-index option
|
| 416 |
+
when constructing the SearchScope object.
|
| 417 |
+
"""
|
| 418 |
+
index_urls = [options.index_url] + options.extra_index_urls
|
| 419 |
+
if options.no_index and not suppress_no_index:
|
| 420 |
+
logger.debug(
|
| 421 |
+
"Ignoring indexes: %s",
|
| 422 |
+
",".join(redact_auth_from_url(url) for url in index_urls),
|
| 423 |
+
)
|
| 424 |
+
index_urls = []
|
| 425 |
+
|
| 426 |
+
# Make sure find_links is a list before passing to create().
|
| 427 |
+
find_links = options.find_links or []
|
| 428 |
+
|
| 429 |
+
search_scope = SearchScope.create(
|
| 430 |
+
find_links=find_links,
|
| 431 |
+
index_urls=index_urls,
|
| 432 |
+
no_index=options.no_index,
|
| 433 |
+
)
|
| 434 |
+
link_collector = LinkCollector(
|
| 435 |
+
session=session,
|
| 436 |
+
search_scope=search_scope,
|
| 437 |
+
)
|
| 438 |
+
return link_collector
|
| 439 |
+
|
| 440 |
+
@property
|
| 441 |
+
def find_links(self) -> List[str]:
|
| 442 |
+
return self.search_scope.find_links
|
| 443 |
+
|
| 444 |
+
def fetch_response(self, location: Link) -> Optional[IndexContent]:
|
| 445 |
+
"""
|
| 446 |
+
Fetch an HTML page containing package links.
|
| 447 |
+
"""
|
| 448 |
+
return _get_index_content(location, session=self.session)
|
| 449 |
+
|
| 450 |
+
def collect_sources(
|
| 451 |
+
self,
|
| 452 |
+
project_name: str,
|
| 453 |
+
candidates_from_page: CandidatesFromPage,
|
| 454 |
+
) -> CollectedSources:
|
| 455 |
+
# The OrderedDict calls deduplicate sources by URL.
|
| 456 |
+
index_url_sources = collections.OrderedDict(
|
| 457 |
+
build_source(
|
| 458 |
+
loc,
|
| 459 |
+
candidates_from_page=candidates_from_page,
|
| 460 |
+
page_validator=self.session.is_secure_origin,
|
| 461 |
+
expand_dir=False,
|
| 462 |
+
cache_link_parsing=False,
|
| 463 |
+
project_name=project_name,
|
| 464 |
+
)
|
| 465 |
+
for loc in self.search_scope.get_index_urls_locations(project_name)
|
| 466 |
+
).values()
|
| 467 |
+
find_links_sources = collections.OrderedDict(
|
| 468 |
+
build_source(
|
| 469 |
+
loc,
|
| 470 |
+
candidates_from_page=candidates_from_page,
|
| 471 |
+
page_validator=self.session.is_secure_origin,
|
| 472 |
+
expand_dir=True,
|
| 473 |
+
cache_link_parsing=True,
|
| 474 |
+
project_name=project_name,
|
| 475 |
+
)
|
| 476 |
+
for loc in self.find_links
|
| 477 |
+
).values()
|
| 478 |
+
|
| 479 |
+
if logger.isEnabledFor(logging.DEBUG):
|
| 480 |
+
lines = [
|
| 481 |
+
f"* {s.link}"
|
| 482 |
+
for s in itertools.chain(find_links_sources, index_url_sources)
|
| 483 |
+
if s is not None and s.link is not None
|
| 484 |
+
]
|
| 485 |
+
lines = [
|
| 486 |
+
f"{len(lines)} location(s) to search "
|
| 487 |
+
f"for versions of {project_name}:"
|
| 488 |
+
] + lines
|
| 489 |
+
logger.debug("\n".join(lines))
|
| 490 |
+
|
| 491 |
+
return CollectedSources(
|
| 492 |
+
find_links=list(find_links_sources),
|
| 493 |
+
index_urls=list(index_url_sources),
|
| 494 |
+
)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/index/package_finder.py
ADDED
|
@@ -0,0 +1,1029 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Routines related to PyPI, indexes"""
|
| 2 |
+
|
| 3 |
+
import enum
|
| 4 |
+
import functools
|
| 5 |
+
import itertools
|
| 6 |
+
import logging
|
| 7 |
+
import re
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from typing import TYPE_CHECKING, FrozenSet, Iterable, List, Optional, Set, Tuple, Union
|
| 10 |
+
|
| 11 |
+
from pip._vendor.packaging import specifiers
|
| 12 |
+
from pip._vendor.packaging.tags import Tag
|
| 13 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 14 |
+
from pip._vendor.packaging.version import InvalidVersion, _BaseVersion
|
| 15 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 16 |
+
|
| 17 |
+
from pip._internal.exceptions import (
|
| 18 |
+
BestVersionAlreadyInstalled,
|
| 19 |
+
DistributionNotFound,
|
| 20 |
+
InvalidWheelFilename,
|
| 21 |
+
UnsupportedWheel,
|
| 22 |
+
)
|
| 23 |
+
from pip._internal.index.collector import LinkCollector, parse_links
|
| 24 |
+
from pip._internal.models.candidate import InstallationCandidate
|
| 25 |
+
from pip._internal.models.format_control import FormatControl
|
| 26 |
+
from pip._internal.models.link import Link
|
| 27 |
+
from pip._internal.models.search_scope import SearchScope
|
| 28 |
+
from pip._internal.models.selection_prefs import SelectionPreferences
|
| 29 |
+
from pip._internal.models.target_python import TargetPython
|
| 30 |
+
from pip._internal.models.wheel import Wheel
|
| 31 |
+
from pip._internal.req import InstallRequirement
|
| 32 |
+
from pip._internal.utils._log import getLogger
|
| 33 |
+
from pip._internal.utils.filetypes import WHEEL_EXTENSION
|
| 34 |
+
from pip._internal.utils.hashes import Hashes
|
| 35 |
+
from pip._internal.utils.logging import indent_log
|
| 36 |
+
from pip._internal.utils.misc import build_netloc
|
| 37 |
+
from pip._internal.utils.packaging import check_requires_python
|
| 38 |
+
from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
|
| 39 |
+
|
| 40 |
+
if TYPE_CHECKING:
|
| 41 |
+
from pip._vendor.typing_extensions import TypeGuard
|
| 42 |
+
|
| 43 |
+
__all__ = ["FormatControl", "BestCandidateResult", "PackageFinder"]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
logger = getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
BuildTag = Union[Tuple[()], Tuple[int, str]]
|
| 49 |
+
CandidateSortingKey = Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _check_link_requires_python(
|
| 53 |
+
link: Link,
|
| 54 |
+
version_info: Tuple[int, int, int],
|
| 55 |
+
ignore_requires_python: bool = False,
|
| 56 |
+
) -> bool:
|
| 57 |
+
"""
|
| 58 |
+
Return whether the given Python version is compatible with a link's
|
| 59 |
+
"Requires-Python" value.
|
| 60 |
+
|
| 61 |
+
:param version_info: A 3-tuple of ints representing the Python
|
| 62 |
+
major-minor-micro version to check.
|
| 63 |
+
:param ignore_requires_python: Whether to ignore the "Requires-Python"
|
| 64 |
+
value if the given Python version isn't compatible.
|
| 65 |
+
"""
|
| 66 |
+
try:
|
| 67 |
+
is_compatible = check_requires_python(
|
| 68 |
+
link.requires_python,
|
| 69 |
+
version_info=version_info,
|
| 70 |
+
)
|
| 71 |
+
except specifiers.InvalidSpecifier:
|
| 72 |
+
logger.debug(
|
| 73 |
+
"Ignoring invalid Requires-Python (%r) for link: %s",
|
| 74 |
+
link.requires_python,
|
| 75 |
+
link,
|
| 76 |
+
)
|
| 77 |
+
else:
|
| 78 |
+
if not is_compatible:
|
| 79 |
+
version = ".".join(map(str, version_info))
|
| 80 |
+
if not ignore_requires_python:
|
| 81 |
+
logger.verbose(
|
| 82 |
+
"Link requires a different Python (%s not in: %r): %s",
|
| 83 |
+
version,
|
| 84 |
+
link.requires_python,
|
| 85 |
+
link,
|
| 86 |
+
)
|
| 87 |
+
return False
|
| 88 |
+
|
| 89 |
+
logger.debug(
|
| 90 |
+
"Ignoring failed Requires-Python check (%s not in: %r) for link: %s",
|
| 91 |
+
version,
|
| 92 |
+
link.requires_python,
|
| 93 |
+
link,
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
return True
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class LinkType(enum.Enum):
|
| 100 |
+
candidate = enum.auto()
|
| 101 |
+
different_project = enum.auto()
|
| 102 |
+
yanked = enum.auto()
|
| 103 |
+
format_unsupported = enum.auto()
|
| 104 |
+
format_invalid = enum.auto()
|
| 105 |
+
platform_mismatch = enum.auto()
|
| 106 |
+
requires_python_mismatch = enum.auto()
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class LinkEvaluator:
|
| 110 |
+
"""
|
| 111 |
+
Responsible for evaluating links for a particular project.
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
_py_version_re = re.compile(r"-py([123]\.?[0-9]?)$")
|
| 115 |
+
|
| 116 |
+
# Don't include an allow_yanked default value to make sure each call
|
| 117 |
+
# site considers whether yanked releases are allowed. This also causes
|
| 118 |
+
# that decision to be made explicit in the calling code, which helps
|
| 119 |
+
# people when reading the code.
|
| 120 |
+
def __init__(
|
| 121 |
+
self,
|
| 122 |
+
project_name: str,
|
| 123 |
+
canonical_name: str,
|
| 124 |
+
formats: FrozenSet[str],
|
| 125 |
+
target_python: TargetPython,
|
| 126 |
+
allow_yanked: bool,
|
| 127 |
+
ignore_requires_python: Optional[bool] = None,
|
| 128 |
+
) -> None:
|
| 129 |
+
"""
|
| 130 |
+
:param project_name: The user supplied package name.
|
| 131 |
+
:param canonical_name: The canonical package name.
|
| 132 |
+
:param formats: The formats allowed for this package. Should be a set
|
| 133 |
+
with 'binary' or 'source' or both in it.
|
| 134 |
+
:param target_python: The target Python interpreter to use when
|
| 135 |
+
evaluating link compatibility. This is used, for example, to
|
| 136 |
+
check wheel compatibility, as well as when checking the Python
|
| 137 |
+
version, e.g. the Python version embedded in a link filename
|
| 138 |
+
(or egg fragment) and against an HTML link's optional PEP 503
|
| 139 |
+
"data-requires-python" attribute.
|
| 140 |
+
:param allow_yanked: Whether files marked as yanked (in the sense
|
| 141 |
+
of PEP 592) are permitted to be candidates for install.
|
| 142 |
+
:param ignore_requires_python: Whether to ignore incompatible
|
| 143 |
+
PEP 503 "data-requires-python" values in HTML links. Defaults
|
| 144 |
+
to False.
|
| 145 |
+
"""
|
| 146 |
+
if ignore_requires_python is None:
|
| 147 |
+
ignore_requires_python = False
|
| 148 |
+
|
| 149 |
+
self._allow_yanked = allow_yanked
|
| 150 |
+
self._canonical_name = canonical_name
|
| 151 |
+
self._ignore_requires_python = ignore_requires_python
|
| 152 |
+
self._formats = formats
|
| 153 |
+
self._target_python = target_python
|
| 154 |
+
|
| 155 |
+
self.project_name = project_name
|
| 156 |
+
|
| 157 |
+
def evaluate_link(self, link: Link) -> Tuple[LinkType, str]:
|
| 158 |
+
"""
|
| 159 |
+
Determine whether a link is a candidate for installation.
|
| 160 |
+
|
| 161 |
+
:return: A tuple (result, detail), where *result* is an enum
|
| 162 |
+
representing whether the evaluation found a candidate, or the reason
|
| 163 |
+
why one is not found. If a candidate is found, *detail* will be the
|
| 164 |
+
candidate's version string; if one is not found, it contains the
|
| 165 |
+
reason the link fails to qualify.
|
| 166 |
+
"""
|
| 167 |
+
version = None
|
| 168 |
+
if link.is_yanked and not self._allow_yanked:
|
| 169 |
+
reason = link.yanked_reason or "<none given>"
|
| 170 |
+
return (LinkType.yanked, f"yanked for reason: {reason}")
|
| 171 |
+
|
| 172 |
+
if link.egg_fragment:
|
| 173 |
+
egg_info = link.egg_fragment
|
| 174 |
+
ext = link.ext
|
| 175 |
+
else:
|
| 176 |
+
egg_info, ext = link.splitext()
|
| 177 |
+
if not ext:
|
| 178 |
+
return (LinkType.format_unsupported, "not a file")
|
| 179 |
+
if ext not in SUPPORTED_EXTENSIONS:
|
| 180 |
+
return (
|
| 181 |
+
LinkType.format_unsupported,
|
| 182 |
+
f"unsupported archive format: {ext}",
|
| 183 |
+
)
|
| 184 |
+
if "binary" not in self._formats and ext == WHEEL_EXTENSION:
|
| 185 |
+
reason = f"No binaries permitted for {self.project_name}"
|
| 186 |
+
return (LinkType.format_unsupported, reason)
|
| 187 |
+
if "macosx10" in link.path and ext == ".zip":
|
| 188 |
+
return (LinkType.format_unsupported, "macosx10 one")
|
| 189 |
+
if ext == WHEEL_EXTENSION:
|
| 190 |
+
try:
|
| 191 |
+
wheel = Wheel(link.filename)
|
| 192 |
+
except InvalidWheelFilename:
|
| 193 |
+
return (
|
| 194 |
+
LinkType.format_invalid,
|
| 195 |
+
"invalid wheel filename",
|
| 196 |
+
)
|
| 197 |
+
if canonicalize_name(wheel.name) != self._canonical_name:
|
| 198 |
+
reason = f"wrong project name (not {self.project_name})"
|
| 199 |
+
return (LinkType.different_project, reason)
|
| 200 |
+
|
| 201 |
+
supported_tags = self._target_python.get_unsorted_tags()
|
| 202 |
+
if not wheel.supported(supported_tags):
|
| 203 |
+
# Include the wheel's tags in the reason string to
|
| 204 |
+
# simplify troubleshooting compatibility issues.
|
| 205 |
+
file_tags = ", ".join(wheel.get_formatted_file_tags())
|
| 206 |
+
reason = (
|
| 207 |
+
f"none of the wheel's tags ({file_tags}) are compatible "
|
| 208 |
+
f"(run pip debug --verbose to show compatible tags)"
|
| 209 |
+
)
|
| 210 |
+
return (LinkType.platform_mismatch, reason)
|
| 211 |
+
|
| 212 |
+
version = wheel.version
|
| 213 |
+
|
| 214 |
+
# This should be up by the self.ok_binary check, but see issue 2700.
|
| 215 |
+
if "source" not in self._formats and ext != WHEEL_EXTENSION:
|
| 216 |
+
reason = f"No sources permitted for {self.project_name}"
|
| 217 |
+
return (LinkType.format_unsupported, reason)
|
| 218 |
+
|
| 219 |
+
if not version:
|
| 220 |
+
version = _extract_version_from_fragment(
|
| 221 |
+
egg_info,
|
| 222 |
+
self._canonical_name,
|
| 223 |
+
)
|
| 224 |
+
if not version:
|
| 225 |
+
reason = f"Missing project version for {self.project_name}"
|
| 226 |
+
return (LinkType.format_invalid, reason)
|
| 227 |
+
|
| 228 |
+
match = self._py_version_re.search(version)
|
| 229 |
+
if match:
|
| 230 |
+
version = version[: match.start()]
|
| 231 |
+
py_version = match.group(1)
|
| 232 |
+
if py_version != self._target_python.py_version:
|
| 233 |
+
return (
|
| 234 |
+
LinkType.platform_mismatch,
|
| 235 |
+
"Python version is incorrect",
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
supports_python = _check_link_requires_python(
|
| 239 |
+
link,
|
| 240 |
+
version_info=self._target_python.py_version_info,
|
| 241 |
+
ignore_requires_python=self._ignore_requires_python,
|
| 242 |
+
)
|
| 243 |
+
if not supports_python:
|
| 244 |
+
reason = f"{version} Requires-Python {link.requires_python}"
|
| 245 |
+
return (LinkType.requires_python_mismatch, reason)
|
| 246 |
+
|
| 247 |
+
logger.debug("Found link %s, version: %s", link, version)
|
| 248 |
+
|
| 249 |
+
return (LinkType.candidate, version)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def filter_unallowed_hashes(
|
| 253 |
+
candidates: List[InstallationCandidate],
|
| 254 |
+
hashes: Optional[Hashes],
|
| 255 |
+
project_name: str,
|
| 256 |
+
) -> List[InstallationCandidate]:
|
| 257 |
+
"""
|
| 258 |
+
Filter out candidates whose hashes aren't allowed, and return a new
|
| 259 |
+
list of candidates.
|
| 260 |
+
|
| 261 |
+
If at least one candidate has an allowed hash, then all candidates with
|
| 262 |
+
either an allowed hash or no hash specified are returned. Otherwise,
|
| 263 |
+
the given candidates are returned.
|
| 264 |
+
|
| 265 |
+
Including the candidates with no hash specified when there is a match
|
| 266 |
+
allows a warning to be logged if there is a more preferred candidate
|
| 267 |
+
with no hash specified. Returning all candidates in the case of no
|
| 268 |
+
matches lets pip report the hash of the candidate that would otherwise
|
| 269 |
+
have been installed (e.g. permitting the user to more easily update
|
| 270 |
+
their requirements file with the desired hash).
|
| 271 |
+
"""
|
| 272 |
+
if not hashes:
|
| 273 |
+
logger.debug(
|
| 274 |
+
"Given no hashes to check %s links for project %r: "
|
| 275 |
+
"discarding no candidates",
|
| 276 |
+
len(candidates),
|
| 277 |
+
project_name,
|
| 278 |
+
)
|
| 279 |
+
# Make sure we're not returning back the given value.
|
| 280 |
+
return list(candidates)
|
| 281 |
+
|
| 282 |
+
matches_or_no_digest = []
|
| 283 |
+
# Collect the non-matches for logging purposes.
|
| 284 |
+
non_matches = []
|
| 285 |
+
match_count = 0
|
| 286 |
+
for candidate in candidates:
|
| 287 |
+
link = candidate.link
|
| 288 |
+
if not link.has_hash:
|
| 289 |
+
pass
|
| 290 |
+
elif link.is_hash_allowed(hashes=hashes):
|
| 291 |
+
match_count += 1
|
| 292 |
+
else:
|
| 293 |
+
non_matches.append(candidate)
|
| 294 |
+
continue
|
| 295 |
+
|
| 296 |
+
matches_or_no_digest.append(candidate)
|
| 297 |
+
|
| 298 |
+
if match_count:
|
| 299 |
+
filtered = matches_or_no_digest
|
| 300 |
+
else:
|
| 301 |
+
# Make sure we're not returning back the given value.
|
| 302 |
+
filtered = list(candidates)
|
| 303 |
+
|
| 304 |
+
if len(filtered) == len(candidates):
|
| 305 |
+
discard_message = "discarding no candidates"
|
| 306 |
+
else:
|
| 307 |
+
discard_message = "discarding {} non-matches:\n {}".format(
|
| 308 |
+
len(non_matches),
|
| 309 |
+
"\n ".join(str(candidate.link) for candidate in non_matches),
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
logger.debug(
|
| 313 |
+
"Checked %s links for project %r against %s hashes "
|
| 314 |
+
"(%s matches, %s no digest): %s",
|
| 315 |
+
len(candidates),
|
| 316 |
+
project_name,
|
| 317 |
+
hashes.digest_count,
|
| 318 |
+
match_count,
|
| 319 |
+
len(matches_or_no_digest) - match_count,
|
| 320 |
+
discard_message,
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
return filtered
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
@dataclass
|
| 327 |
+
class CandidatePreferences:
|
| 328 |
+
"""
|
| 329 |
+
Encapsulates some of the preferences for filtering and sorting
|
| 330 |
+
InstallationCandidate objects.
|
| 331 |
+
"""
|
| 332 |
+
|
| 333 |
+
prefer_binary: bool = False
|
| 334 |
+
allow_all_prereleases: bool = False
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
@dataclass(frozen=True)
|
| 338 |
+
class BestCandidateResult:
|
| 339 |
+
"""A collection of candidates, returned by `PackageFinder.find_best_candidate`.
|
| 340 |
+
|
| 341 |
+
This class is only intended to be instantiated by CandidateEvaluator's
|
| 342 |
+
`compute_best_candidate()` method.
|
| 343 |
+
|
| 344 |
+
:param all_candidates: A sequence of all available candidates found.
|
| 345 |
+
:param applicable_candidates: The applicable candidates.
|
| 346 |
+
:param best_candidate: The most preferred candidate found, or None
|
| 347 |
+
if no applicable candidates were found.
|
| 348 |
+
"""
|
| 349 |
+
|
| 350 |
+
all_candidates: List[InstallationCandidate]
|
| 351 |
+
applicable_candidates: List[InstallationCandidate]
|
| 352 |
+
best_candidate: Optional[InstallationCandidate]
|
| 353 |
+
|
| 354 |
+
def __post_init__(self) -> None:
|
| 355 |
+
assert set(self.applicable_candidates) <= set(self.all_candidates)
|
| 356 |
+
|
| 357 |
+
if self.best_candidate is None:
|
| 358 |
+
assert not self.applicable_candidates
|
| 359 |
+
else:
|
| 360 |
+
assert self.best_candidate in self.applicable_candidates
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
class CandidateEvaluator:
|
| 364 |
+
"""
|
| 365 |
+
Responsible for filtering and sorting candidates for installation based
|
| 366 |
+
on what tags are valid.
|
| 367 |
+
"""
|
| 368 |
+
|
| 369 |
+
@classmethod
|
| 370 |
+
def create(
|
| 371 |
+
cls,
|
| 372 |
+
project_name: str,
|
| 373 |
+
target_python: Optional[TargetPython] = None,
|
| 374 |
+
prefer_binary: bool = False,
|
| 375 |
+
allow_all_prereleases: bool = False,
|
| 376 |
+
specifier: Optional[specifiers.BaseSpecifier] = None,
|
| 377 |
+
hashes: Optional[Hashes] = None,
|
| 378 |
+
) -> "CandidateEvaluator":
|
| 379 |
+
"""Create a CandidateEvaluator object.
|
| 380 |
+
|
| 381 |
+
:param target_python: The target Python interpreter to use when
|
| 382 |
+
checking compatibility. If None (the default), a TargetPython
|
| 383 |
+
object will be constructed from the running Python.
|
| 384 |
+
:param specifier: An optional object implementing `filter`
|
| 385 |
+
(e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
|
| 386 |
+
versions.
|
| 387 |
+
:param hashes: An optional collection of allowed hashes.
|
| 388 |
+
"""
|
| 389 |
+
if target_python is None:
|
| 390 |
+
target_python = TargetPython()
|
| 391 |
+
if specifier is None:
|
| 392 |
+
specifier = specifiers.SpecifierSet()
|
| 393 |
+
|
| 394 |
+
supported_tags = target_python.get_sorted_tags()
|
| 395 |
+
|
| 396 |
+
return cls(
|
| 397 |
+
project_name=project_name,
|
| 398 |
+
supported_tags=supported_tags,
|
| 399 |
+
specifier=specifier,
|
| 400 |
+
prefer_binary=prefer_binary,
|
| 401 |
+
allow_all_prereleases=allow_all_prereleases,
|
| 402 |
+
hashes=hashes,
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
def __init__(
|
| 406 |
+
self,
|
| 407 |
+
project_name: str,
|
| 408 |
+
supported_tags: List[Tag],
|
| 409 |
+
specifier: specifiers.BaseSpecifier,
|
| 410 |
+
prefer_binary: bool = False,
|
| 411 |
+
allow_all_prereleases: bool = False,
|
| 412 |
+
hashes: Optional[Hashes] = None,
|
| 413 |
+
) -> None:
|
| 414 |
+
"""
|
| 415 |
+
:param supported_tags: The PEP 425 tags supported by the target
|
| 416 |
+
Python in order of preference (most preferred first).
|
| 417 |
+
"""
|
| 418 |
+
self._allow_all_prereleases = allow_all_prereleases
|
| 419 |
+
self._hashes = hashes
|
| 420 |
+
self._prefer_binary = prefer_binary
|
| 421 |
+
self._project_name = project_name
|
| 422 |
+
self._specifier = specifier
|
| 423 |
+
self._supported_tags = supported_tags
|
| 424 |
+
# Since the index of the tag in the _supported_tags list is used
|
| 425 |
+
# as a priority, precompute a map from tag to index/priority to be
|
| 426 |
+
# used in wheel.find_most_preferred_tag.
|
| 427 |
+
self._wheel_tag_preferences = {
|
| 428 |
+
tag: idx for idx, tag in enumerate(supported_tags)
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
def get_applicable_candidates(
|
| 432 |
+
self,
|
| 433 |
+
candidates: List[InstallationCandidate],
|
| 434 |
+
) -> List[InstallationCandidate]:
|
| 435 |
+
"""
|
| 436 |
+
Return the applicable candidates from a list of candidates.
|
| 437 |
+
"""
|
| 438 |
+
# Using None infers from the specifier instead.
|
| 439 |
+
allow_prereleases = self._allow_all_prereleases or None
|
| 440 |
+
specifier = self._specifier
|
| 441 |
+
|
| 442 |
+
# We turn the version object into a str here because otherwise
|
| 443 |
+
# when we're debundled but setuptools isn't, Python will see
|
| 444 |
+
# packaging.version.Version and
|
| 445 |
+
# pkg_resources._vendor.packaging.version.Version as different
|
| 446 |
+
# types. This way we'll use a str as a common data interchange
|
| 447 |
+
# format. If we stop using the pkg_resources provided specifier
|
| 448 |
+
# and start using our own, we can drop the cast to str().
|
| 449 |
+
candidates_and_versions = [(c, str(c.version)) for c in candidates]
|
| 450 |
+
versions = set(
|
| 451 |
+
specifier.filter(
|
| 452 |
+
(v for _, v in candidates_and_versions),
|
| 453 |
+
prereleases=allow_prereleases,
|
| 454 |
+
)
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
applicable_candidates = [c for c, v in candidates_and_versions if v in versions]
|
| 458 |
+
filtered_applicable_candidates = filter_unallowed_hashes(
|
| 459 |
+
candidates=applicable_candidates,
|
| 460 |
+
hashes=self._hashes,
|
| 461 |
+
project_name=self._project_name,
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
return sorted(filtered_applicable_candidates, key=self._sort_key)
|
| 465 |
+
|
| 466 |
+
def _sort_key(self, candidate: InstallationCandidate) -> CandidateSortingKey:
|
| 467 |
+
"""
|
| 468 |
+
Function to pass as the `key` argument to a call to sorted() to sort
|
| 469 |
+
InstallationCandidates by preference.
|
| 470 |
+
|
| 471 |
+
Returns a tuple such that tuples sorting as greater using Python's
|
| 472 |
+
default comparison operator are more preferred.
|
| 473 |
+
|
| 474 |
+
The preference is as follows:
|
| 475 |
+
|
| 476 |
+
First and foremost, candidates with allowed (matching) hashes are
|
| 477 |
+
always preferred over candidates without matching hashes. This is
|
| 478 |
+
because e.g. if the only candidate with an allowed hash is yanked,
|
| 479 |
+
we still want to use that candidate.
|
| 480 |
+
|
| 481 |
+
Second, excepting hash considerations, candidates that have been
|
| 482 |
+
yanked (in the sense of PEP 592) are always less preferred than
|
| 483 |
+
candidates that haven't been yanked. Then:
|
| 484 |
+
|
| 485 |
+
If not finding wheels, they are sorted by version only.
|
| 486 |
+
If finding wheels, then the sort order is by version, then:
|
| 487 |
+
1. existing installs
|
| 488 |
+
2. wheels ordered via Wheel.support_index_min(self._supported_tags)
|
| 489 |
+
3. source archives
|
| 490 |
+
If prefer_binary was set, then all wheels are sorted above sources.
|
| 491 |
+
|
| 492 |
+
Note: it was considered to embed this logic into the Link
|
| 493 |
+
comparison operators, but then different sdist links
|
| 494 |
+
with the same version, would have to be considered equal
|
| 495 |
+
"""
|
| 496 |
+
valid_tags = self._supported_tags
|
| 497 |
+
support_num = len(valid_tags)
|
| 498 |
+
build_tag: BuildTag = ()
|
| 499 |
+
binary_preference = 0
|
| 500 |
+
link = candidate.link
|
| 501 |
+
if link.is_wheel:
|
| 502 |
+
# can raise InvalidWheelFilename
|
| 503 |
+
wheel = Wheel(link.filename)
|
| 504 |
+
try:
|
| 505 |
+
pri = -(
|
| 506 |
+
wheel.find_most_preferred_tag(
|
| 507 |
+
valid_tags, self._wheel_tag_preferences
|
| 508 |
+
)
|
| 509 |
+
)
|
| 510 |
+
except ValueError:
|
| 511 |
+
raise UnsupportedWheel(
|
| 512 |
+
f"{wheel.filename} is not a supported wheel for this platform. It "
|
| 513 |
+
"can't be sorted."
|
| 514 |
+
)
|
| 515 |
+
if self._prefer_binary:
|
| 516 |
+
binary_preference = 1
|
| 517 |
+
if wheel.build_tag is not None:
|
| 518 |
+
match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
|
| 519 |
+
assert match is not None, "guaranteed by filename validation"
|
| 520 |
+
build_tag_groups = match.groups()
|
| 521 |
+
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
|
| 522 |
+
else: # sdist
|
| 523 |
+
pri = -(support_num)
|
| 524 |
+
has_allowed_hash = int(link.is_hash_allowed(self._hashes))
|
| 525 |
+
yank_value = -1 * int(link.is_yanked) # -1 for yanked.
|
| 526 |
+
return (
|
| 527 |
+
has_allowed_hash,
|
| 528 |
+
yank_value,
|
| 529 |
+
binary_preference,
|
| 530 |
+
candidate.version,
|
| 531 |
+
pri,
|
| 532 |
+
build_tag,
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
def sort_best_candidate(
|
| 536 |
+
self,
|
| 537 |
+
candidates: List[InstallationCandidate],
|
| 538 |
+
) -> Optional[InstallationCandidate]:
|
| 539 |
+
"""
|
| 540 |
+
Return the best candidate per the instance's sort order, or None if
|
| 541 |
+
no candidate is acceptable.
|
| 542 |
+
"""
|
| 543 |
+
if not candidates:
|
| 544 |
+
return None
|
| 545 |
+
best_candidate = max(candidates, key=self._sort_key)
|
| 546 |
+
return best_candidate
|
| 547 |
+
|
| 548 |
+
def compute_best_candidate(
|
| 549 |
+
self,
|
| 550 |
+
candidates: List[InstallationCandidate],
|
| 551 |
+
) -> BestCandidateResult:
|
| 552 |
+
"""
|
| 553 |
+
Compute and return a `BestCandidateResult` instance.
|
| 554 |
+
"""
|
| 555 |
+
applicable_candidates = self.get_applicable_candidates(candidates)
|
| 556 |
+
|
| 557 |
+
best_candidate = self.sort_best_candidate(applicable_candidates)
|
| 558 |
+
|
| 559 |
+
return BestCandidateResult(
|
| 560 |
+
candidates,
|
| 561 |
+
applicable_candidates=applicable_candidates,
|
| 562 |
+
best_candidate=best_candidate,
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
class PackageFinder:
|
| 567 |
+
"""This finds packages.
|
| 568 |
+
|
| 569 |
+
This is meant to match easy_install's technique for looking for
|
| 570 |
+
packages, by reading pages and looking for appropriate links.
|
| 571 |
+
"""
|
| 572 |
+
|
| 573 |
+
def __init__(
|
| 574 |
+
self,
|
| 575 |
+
link_collector: LinkCollector,
|
| 576 |
+
target_python: TargetPython,
|
| 577 |
+
allow_yanked: bool,
|
| 578 |
+
format_control: Optional[FormatControl] = None,
|
| 579 |
+
candidate_prefs: Optional[CandidatePreferences] = None,
|
| 580 |
+
ignore_requires_python: Optional[bool] = None,
|
| 581 |
+
) -> None:
|
| 582 |
+
"""
|
| 583 |
+
This constructor is primarily meant to be used by the create() class
|
| 584 |
+
method and from tests.
|
| 585 |
+
|
| 586 |
+
:param format_control: A FormatControl object, used to control
|
| 587 |
+
the selection of source packages / binary packages when consulting
|
| 588 |
+
the index and links.
|
| 589 |
+
:param candidate_prefs: Options to use when creating a
|
| 590 |
+
CandidateEvaluator object.
|
| 591 |
+
"""
|
| 592 |
+
if candidate_prefs is None:
|
| 593 |
+
candidate_prefs = CandidatePreferences()
|
| 594 |
+
|
| 595 |
+
format_control = format_control or FormatControl(set(), set())
|
| 596 |
+
|
| 597 |
+
self._allow_yanked = allow_yanked
|
| 598 |
+
self._candidate_prefs = candidate_prefs
|
| 599 |
+
self._ignore_requires_python = ignore_requires_python
|
| 600 |
+
self._link_collector = link_collector
|
| 601 |
+
self._target_python = target_python
|
| 602 |
+
|
| 603 |
+
self.format_control = format_control
|
| 604 |
+
|
| 605 |
+
# These are boring links that have already been logged somehow.
|
| 606 |
+
self._logged_links: Set[Tuple[Link, LinkType, str]] = set()
|
| 607 |
+
|
| 608 |
+
# Don't include an allow_yanked default value to make sure each call
|
| 609 |
+
# site considers whether yanked releases are allowed. This also causes
|
| 610 |
+
# that decision to be made explicit in the calling code, which helps
|
| 611 |
+
# people when reading the code.
|
| 612 |
+
@classmethod
|
| 613 |
+
def create(
|
| 614 |
+
cls,
|
| 615 |
+
link_collector: LinkCollector,
|
| 616 |
+
selection_prefs: SelectionPreferences,
|
| 617 |
+
target_python: Optional[TargetPython] = None,
|
| 618 |
+
) -> "PackageFinder":
|
| 619 |
+
"""Create a PackageFinder.
|
| 620 |
+
|
| 621 |
+
:param selection_prefs: The candidate selection preferences, as a
|
| 622 |
+
SelectionPreferences object.
|
| 623 |
+
:param target_python: The target Python interpreter to use when
|
| 624 |
+
checking compatibility. If None (the default), a TargetPython
|
| 625 |
+
object will be constructed from the running Python.
|
| 626 |
+
"""
|
| 627 |
+
if target_python is None:
|
| 628 |
+
target_python = TargetPython()
|
| 629 |
+
|
| 630 |
+
candidate_prefs = CandidatePreferences(
|
| 631 |
+
prefer_binary=selection_prefs.prefer_binary,
|
| 632 |
+
allow_all_prereleases=selection_prefs.allow_all_prereleases,
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
return cls(
|
| 636 |
+
candidate_prefs=candidate_prefs,
|
| 637 |
+
link_collector=link_collector,
|
| 638 |
+
target_python=target_python,
|
| 639 |
+
allow_yanked=selection_prefs.allow_yanked,
|
| 640 |
+
format_control=selection_prefs.format_control,
|
| 641 |
+
ignore_requires_python=selection_prefs.ignore_requires_python,
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
@property
|
| 645 |
+
def target_python(self) -> TargetPython:
|
| 646 |
+
return self._target_python
|
| 647 |
+
|
| 648 |
+
@property
|
| 649 |
+
def search_scope(self) -> SearchScope:
|
| 650 |
+
return self._link_collector.search_scope
|
| 651 |
+
|
| 652 |
+
@search_scope.setter
|
| 653 |
+
def search_scope(self, search_scope: SearchScope) -> None:
|
| 654 |
+
self._link_collector.search_scope = search_scope
|
| 655 |
+
|
| 656 |
+
@property
|
| 657 |
+
def find_links(self) -> List[str]:
|
| 658 |
+
return self._link_collector.find_links
|
| 659 |
+
|
| 660 |
+
@property
|
| 661 |
+
def index_urls(self) -> List[str]:
|
| 662 |
+
return self.search_scope.index_urls
|
| 663 |
+
|
| 664 |
+
@property
|
| 665 |
+
def proxy(self) -> Optional[str]:
|
| 666 |
+
return self._link_collector.session.pip_proxy
|
| 667 |
+
|
| 668 |
+
@property
|
| 669 |
+
def trusted_hosts(self) -> Iterable[str]:
|
| 670 |
+
for host_port in self._link_collector.session.pip_trusted_origins:
|
| 671 |
+
yield build_netloc(*host_port)
|
| 672 |
+
|
| 673 |
+
@property
|
| 674 |
+
def custom_cert(self) -> Optional[str]:
|
| 675 |
+
# session.verify is either a boolean (use default bundle/no SSL
|
| 676 |
+
# verification) or a string path to a custom CA bundle to use. We only
|
| 677 |
+
# care about the latter.
|
| 678 |
+
verify = self._link_collector.session.verify
|
| 679 |
+
return verify if isinstance(verify, str) else None
|
| 680 |
+
|
| 681 |
+
@property
|
| 682 |
+
def client_cert(self) -> Optional[str]:
|
| 683 |
+
cert = self._link_collector.session.cert
|
| 684 |
+
assert not isinstance(cert, tuple), "pip only supports PEM client certs"
|
| 685 |
+
return cert
|
| 686 |
+
|
| 687 |
+
@property
|
| 688 |
+
def allow_all_prereleases(self) -> bool:
|
| 689 |
+
return self._candidate_prefs.allow_all_prereleases
|
| 690 |
+
|
| 691 |
+
def set_allow_all_prereleases(self) -> None:
|
| 692 |
+
self._candidate_prefs.allow_all_prereleases = True
|
| 693 |
+
|
| 694 |
+
@property
|
| 695 |
+
def prefer_binary(self) -> bool:
|
| 696 |
+
return self._candidate_prefs.prefer_binary
|
| 697 |
+
|
| 698 |
+
def set_prefer_binary(self) -> None:
|
| 699 |
+
self._candidate_prefs.prefer_binary = True
|
| 700 |
+
|
| 701 |
+
def requires_python_skipped_reasons(self) -> List[str]:
|
| 702 |
+
reasons = {
|
| 703 |
+
detail
|
| 704 |
+
for _, result, detail in self._logged_links
|
| 705 |
+
if result == LinkType.requires_python_mismatch
|
| 706 |
+
}
|
| 707 |
+
return sorted(reasons)
|
| 708 |
+
|
| 709 |
+
def make_link_evaluator(self, project_name: str) -> LinkEvaluator:
|
| 710 |
+
canonical_name = canonicalize_name(project_name)
|
| 711 |
+
formats = self.format_control.get_allowed_formats(canonical_name)
|
| 712 |
+
|
| 713 |
+
return LinkEvaluator(
|
| 714 |
+
project_name=project_name,
|
| 715 |
+
canonical_name=canonical_name,
|
| 716 |
+
formats=formats,
|
| 717 |
+
target_python=self._target_python,
|
| 718 |
+
allow_yanked=self._allow_yanked,
|
| 719 |
+
ignore_requires_python=self._ignore_requires_python,
|
| 720 |
+
)
|
| 721 |
+
|
| 722 |
+
def _sort_links(self, links: Iterable[Link]) -> List[Link]:
|
| 723 |
+
"""
|
| 724 |
+
Returns elements of links in order, non-egg links first, egg links
|
| 725 |
+
second, while eliminating duplicates
|
| 726 |
+
"""
|
| 727 |
+
eggs, no_eggs = [], []
|
| 728 |
+
seen: Set[Link] = set()
|
| 729 |
+
for link in links:
|
| 730 |
+
if link not in seen:
|
| 731 |
+
seen.add(link)
|
| 732 |
+
if link.egg_fragment:
|
| 733 |
+
eggs.append(link)
|
| 734 |
+
else:
|
| 735 |
+
no_eggs.append(link)
|
| 736 |
+
return no_eggs + eggs
|
| 737 |
+
|
| 738 |
+
def _log_skipped_link(self, link: Link, result: LinkType, detail: str) -> None:
|
| 739 |
+
# This is a hot method so don't waste time hashing links unless we're
|
| 740 |
+
# actually going to log 'em.
|
| 741 |
+
if not logger.isEnabledFor(logging.DEBUG):
|
| 742 |
+
return
|
| 743 |
+
|
| 744 |
+
entry = (link, result, detail)
|
| 745 |
+
if entry not in self._logged_links:
|
| 746 |
+
# Put the link at the end so the reason is more visible and because
|
| 747 |
+
# the link string is usually very long.
|
| 748 |
+
logger.debug("Skipping link: %s: %s", detail, link)
|
| 749 |
+
self._logged_links.add(entry)
|
| 750 |
+
|
| 751 |
+
def get_install_candidate(
|
| 752 |
+
self, link_evaluator: LinkEvaluator, link: Link
|
| 753 |
+
) -> Optional[InstallationCandidate]:
|
| 754 |
+
"""
|
| 755 |
+
If the link is a candidate for install, convert it to an
|
| 756 |
+
InstallationCandidate and return it. Otherwise, return None.
|
| 757 |
+
"""
|
| 758 |
+
result, detail = link_evaluator.evaluate_link(link)
|
| 759 |
+
if result != LinkType.candidate:
|
| 760 |
+
self._log_skipped_link(link, result, detail)
|
| 761 |
+
return None
|
| 762 |
+
|
| 763 |
+
try:
|
| 764 |
+
return InstallationCandidate(
|
| 765 |
+
name=link_evaluator.project_name,
|
| 766 |
+
link=link,
|
| 767 |
+
version=detail,
|
| 768 |
+
)
|
| 769 |
+
except InvalidVersion:
|
| 770 |
+
return None
|
| 771 |
+
|
| 772 |
+
def evaluate_links(
|
| 773 |
+
self, link_evaluator: LinkEvaluator, links: Iterable[Link]
|
| 774 |
+
) -> List[InstallationCandidate]:
|
| 775 |
+
"""
|
| 776 |
+
Convert links that are candidates to InstallationCandidate objects.
|
| 777 |
+
"""
|
| 778 |
+
candidates = []
|
| 779 |
+
for link in self._sort_links(links):
|
| 780 |
+
candidate = self.get_install_candidate(link_evaluator, link)
|
| 781 |
+
if candidate is not None:
|
| 782 |
+
candidates.append(candidate)
|
| 783 |
+
|
| 784 |
+
return candidates
|
| 785 |
+
|
| 786 |
+
def process_project_url(
|
| 787 |
+
self, project_url: Link, link_evaluator: LinkEvaluator
|
| 788 |
+
) -> List[InstallationCandidate]:
|
| 789 |
+
logger.debug(
|
| 790 |
+
"Fetching project page and analyzing links: %s",
|
| 791 |
+
project_url,
|
| 792 |
+
)
|
| 793 |
+
index_response = self._link_collector.fetch_response(project_url)
|
| 794 |
+
if index_response is None:
|
| 795 |
+
return []
|
| 796 |
+
|
| 797 |
+
page_links = list(parse_links(index_response))
|
| 798 |
+
|
| 799 |
+
with indent_log():
|
| 800 |
+
package_links = self.evaluate_links(
|
| 801 |
+
link_evaluator,
|
| 802 |
+
links=page_links,
|
| 803 |
+
)
|
| 804 |
+
|
| 805 |
+
return package_links
|
| 806 |
+
|
| 807 |
+
@functools.lru_cache(maxsize=None)
|
| 808 |
+
def find_all_candidates(self, project_name: str) -> List[InstallationCandidate]:
|
| 809 |
+
"""Find all available InstallationCandidate for project_name
|
| 810 |
+
|
| 811 |
+
This checks index_urls and find_links.
|
| 812 |
+
All versions found are returned as an InstallationCandidate list.
|
| 813 |
+
|
| 814 |
+
See LinkEvaluator.evaluate_link() for details on which files
|
| 815 |
+
are accepted.
|
| 816 |
+
"""
|
| 817 |
+
link_evaluator = self.make_link_evaluator(project_name)
|
| 818 |
+
|
| 819 |
+
collected_sources = self._link_collector.collect_sources(
|
| 820 |
+
project_name=project_name,
|
| 821 |
+
candidates_from_page=functools.partial(
|
| 822 |
+
self.process_project_url,
|
| 823 |
+
link_evaluator=link_evaluator,
|
| 824 |
+
),
|
| 825 |
+
)
|
| 826 |
+
|
| 827 |
+
page_candidates_it = itertools.chain.from_iterable(
|
| 828 |
+
source.page_candidates()
|
| 829 |
+
for sources in collected_sources
|
| 830 |
+
for source in sources
|
| 831 |
+
if source is not None
|
| 832 |
+
)
|
| 833 |
+
page_candidates = list(page_candidates_it)
|
| 834 |
+
|
| 835 |
+
file_links_it = itertools.chain.from_iterable(
|
| 836 |
+
source.file_links()
|
| 837 |
+
for sources in collected_sources
|
| 838 |
+
for source in sources
|
| 839 |
+
if source is not None
|
| 840 |
+
)
|
| 841 |
+
file_candidates = self.evaluate_links(
|
| 842 |
+
link_evaluator,
|
| 843 |
+
sorted(file_links_it, reverse=True),
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
if logger.isEnabledFor(logging.DEBUG) and file_candidates:
|
| 847 |
+
paths = []
|
| 848 |
+
for candidate in file_candidates:
|
| 849 |
+
assert candidate.link.url # we need to have a URL
|
| 850 |
+
try:
|
| 851 |
+
paths.append(candidate.link.file_path)
|
| 852 |
+
except Exception:
|
| 853 |
+
paths.append(candidate.link.url) # it's not a local file
|
| 854 |
+
|
| 855 |
+
logger.debug("Local files found: %s", ", ".join(paths))
|
| 856 |
+
|
| 857 |
+
# This is an intentional priority ordering
|
| 858 |
+
return file_candidates + page_candidates
|
| 859 |
+
|
| 860 |
+
def make_candidate_evaluator(
|
| 861 |
+
self,
|
| 862 |
+
project_name: str,
|
| 863 |
+
specifier: Optional[specifiers.BaseSpecifier] = None,
|
| 864 |
+
hashes: Optional[Hashes] = None,
|
| 865 |
+
) -> CandidateEvaluator:
|
| 866 |
+
"""Create a CandidateEvaluator object to use."""
|
| 867 |
+
candidate_prefs = self._candidate_prefs
|
| 868 |
+
return CandidateEvaluator.create(
|
| 869 |
+
project_name=project_name,
|
| 870 |
+
target_python=self._target_python,
|
| 871 |
+
prefer_binary=candidate_prefs.prefer_binary,
|
| 872 |
+
allow_all_prereleases=candidate_prefs.allow_all_prereleases,
|
| 873 |
+
specifier=specifier,
|
| 874 |
+
hashes=hashes,
|
| 875 |
+
)
|
| 876 |
+
|
| 877 |
+
@functools.lru_cache(maxsize=None)
|
| 878 |
+
def find_best_candidate(
|
| 879 |
+
self,
|
| 880 |
+
project_name: str,
|
| 881 |
+
specifier: Optional[specifiers.BaseSpecifier] = None,
|
| 882 |
+
hashes: Optional[Hashes] = None,
|
| 883 |
+
) -> BestCandidateResult:
|
| 884 |
+
"""Find matches for the given project and specifier.
|
| 885 |
+
|
| 886 |
+
:param specifier: An optional object implementing `filter`
|
| 887 |
+
(e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
|
| 888 |
+
versions.
|
| 889 |
+
|
| 890 |
+
:return: A `BestCandidateResult` instance.
|
| 891 |
+
"""
|
| 892 |
+
candidates = self.find_all_candidates(project_name)
|
| 893 |
+
candidate_evaluator = self.make_candidate_evaluator(
|
| 894 |
+
project_name=project_name,
|
| 895 |
+
specifier=specifier,
|
| 896 |
+
hashes=hashes,
|
| 897 |
+
)
|
| 898 |
+
return candidate_evaluator.compute_best_candidate(candidates)
|
| 899 |
+
|
| 900 |
+
def find_requirement(
|
| 901 |
+
self, req: InstallRequirement, upgrade: bool
|
| 902 |
+
) -> Optional[InstallationCandidate]:
|
| 903 |
+
"""Try to find a Link matching req
|
| 904 |
+
|
| 905 |
+
Expects req, an InstallRequirement and upgrade, a boolean
|
| 906 |
+
Returns a InstallationCandidate if found,
|
| 907 |
+
Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise
|
| 908 |
+
"""
|
| 909 |
+
hashes = req.hashes(trust_internet=False)
|
| 910 |
+
best_candidate_result = self.find_best_candidate(
|
| 911 |
+
req.name,
|
| 912 |
+
specifier=req.specifier,
|
| 913 |
+
hashes=hashes,
|
| 914 |
+
)
|
| 915 |
+
best_candidate = best_candidate_result.best_candidate
|
| 916 |
+
|
| 917 |
+
installed_version: Optional[_BaseVersion] = None
|
| 918 |
+
if req.satisfied_by is not None:
|
| 919 |
+
installed_version = req.satisfied_by.version
|
| 920 |
+
|
| 921 |
+
def _format_versions(cand_iter: Iterable[InstallationCandidate]) -> str:
|
| 922 |
+
# This repeated parse_version and str() conversion is needed to
|
| 923 |
+
# handle different vendoring sources from pip and pkg_resources.
|
| 924 |
+
# If we stop using the pkg_resources provided specifier and start
|
| 925 |
+
# using our own, we can drop the cast to str().
|
| 926 |
+
return (
|
| 927 |
+
", ".join(
|
| 928 |
+
sorted(
|
| 929 |
+
{str(c.version) for c in cand_iter},
|
| 930 |
+
key=parse_version,
|
| 931 |
+
)
|
| 932 |
+
)
|
| 933 |
+
or "none"
|
| 934 |
+
)
|
| 935 |
+
|
| 936 |
+
if installed_version is None and best_candidate is None:
|
| 937 |
+
logger.critical(
|
| 938 |
+
"Could not find a version that satisfies the requirement %s "
|
| 939 |
+
"(from versions: %s)",
|
| 940 |
+
req,
|
| 941 |
+
_format_versions(best_candidate_result.all_candidates),
|
| 942 |
+
)
|
| 943 |
+
|
| 944 |
+
raise DistributionNotFound(f"No matching distribution found for {req}")
|
| 945 |
+
|
| 946 |
+
def _should_install_candidate(
|
| 947 |
+
candidate: Optional[InstallationCandidate],
|
| 948 |
+
) -> "TypeGuard[InstallationCandidate]":
|
| 949 |
+
if installed_version is None:
|
| 950 |
+
return True
|
| 951 |
+
if best_candidate is None:
|
| 952 |
+
return False
|
| 953 |
+
return best_candidate.version > installed_version
|
| 954 |
+
|
| 955 |
+
if not upgrade and installed_version is not None:
|
| 956 |
+
if _should_install_candidate(best_candidate):
|
| 957 |
+
logger.debug(
|
| 958 |
+
"Existing installed version (%s) satisfies requirement "
|
| 959 |
+
"(most up-to-date version is %s)",
|
| 960 |
+
installed_version,
|
| 961 |
+
best_candidate.version,
|
| 962 |
+
)
|
| 963 |
+
else:
|
| 964 |
+
logger.debug(
|
| 965 |
+
"Existing installed version (%s) is most up-to-date and "
|
| 966 |
+
"satisfies requirement",
|
| 967 |
+
installed_version,
|
| 968 |
+
)
|
| 969 |
+
return None
|
| 970 |
+
|
| 971 |
+
if _should_install_candidate(best_candidate):
|
| 972 |
+
logger.debug(
|
| 973 |
+
"Using version %s (newest of versions: %s)",
|
| 974 |
+
best_candidate.version,
|
| 975 |
+
_format_versions(best_candidate_result.applicable_candidates),
|
| 976 |
+
)
|
| 977 |
+
return best_candidate
|
| 978 |
+
|
| 979 |
+
# We have an existing version, and its the best version
|
| 980 |
+
logger.debug(
|
| 981 |
+
"Installed version (%s) is most up-to-date (past versions: %s)",
|
| 982 |
+
installed_version,
|
| 983 |
+
_format_versions(best_candidate_result.applicable_candidates),
|
| 984 |
+
)
|
| 985 |
+
raise BestVersionAlreadyInstalled
|
| 986 |
+
|
| 987 |
+
|
| 988 |
+
def _find_name_version_sep(fragment: str, canonical_name: str) -> int:
|
| 989 |
+
"""Find the separator's index based on the package's canonical name.
|
| 990 |
+
|
| 991 |
+
:param fragment: A <package>+<version> filename "fragment" (stem) or
|
| 992 |
+
egg fragment.
|
| 993 |
+
:param canonical_name: The package's canonical name.
|
| 994 |
+
|
| 995 |
+
This function is needed since the canonicalized name does not necessarily
|
| 996 |
+
have the same length as the egg info's name part. An example::
|
| 997 |
+
|
| 998 |
+
>>> fragment = 'foo__bar-1.0'
|
| 999 |
+
>>> canonical_name = 'foo-bar'
|
| 1000 |
+
>>> _find_name_version_sep(fragment, canonical_name)
|
| 1001 |
+
8
|
| 1002 |
+
"""
|
| 1003 |
+
# Project name and version must be separated by one single dash. Find all
|
| 1004 |
+
# occurrences of dashes; if the string in front of it matches the canonical
|
| 1005 |
+
# name, this is the one separating the name and version parts.
|
| 1006 |
+
for i, c in enumerate(fragment):
|
| 1007 |
+
if c != "-":
|
| 1008 |
+
continue
|
| 1009 |
+
if canonicalize_name(fragment[:i]) == canonical_name:
|
| 1010 |
+
return i
|
| 1011 |
+
raise ValueError(f"{fragment} does not match {canonical_name}")
|
| 1012 |
+
|
| 1013 |
+
|
| 1014 |
+
def _extract_version_from_fragment(fragment: str, canonical_name: str) -> Optional[str]:
|
| 1015 |
+
"""Parse the version string from a <package>+<version> filename
|
| 1016 |
+
"fragment" (stem) or egg fragment.
|
| 1017 |
+
|
| 1018 |
+
:param fragment: The string to parse. E.g. foo-2.1
|
| 1019 |
+
:param canonical_name: The canonicalized name of the package this
|
| 1020 |
+
belongs to.
|
| 1021 |
+
"""
|
| 1022 |
+
try:
|
| 1023 |
+
version_start = _find_name_version_sep(fragment, canonical_name) + 1
|
| 1024 |
+
except ValueError:
|
| 1025 |
+
return None
|
| 1026 |
+
version = fragment[version_start:]
|
| 1027 |
+
if not version:
|
| 1028 |
+
return None
|
| 1029 |
+
return version
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/index/sources.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import mimetypes
|
| 3 |
+
import os
|
| 4 |
+
from collections import defaultdict
|
| 5 |
+
from typing import Callable, Dict, Iterable, List, Optional, Tuple
|
| 6 |
+
|
| 7 |
+
from pip._vendor.packaging.utils import (
|
| 8 |
+
InvalidSdistFilename,
|
| 9 |
+
InvalidWheelFilename,
|
| 10 |
+
canonicalize_name,
|
| 11 |
+
parse_sdist_filename,
|
| 12 |
+
parse_wheel_filename,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
from pip._internal.models.candidate import InstallationCandidate
|
| 16 |
+
from pip._internal.models.link import Link
|
| 17 |
+
from pip._internal.utils.urls import path_to_url, url_to_path
|
| 18 |
+
from pip._internal.vcs import is_url
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
FoundCandidates = Iterable[InstallationCandidate]
|
| 23 |
+
FoundLinks = Iterable[Link]
|
| 24 |
+
CandidatesFromPage = Callable[[Link], Iterable[InstallationCandidate]]
|
| 25 |
+
PageValidator = Callable[[Link], bool]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class LinkSource:
|
| 29 |
+
@property
|
| 30 |
+
def link(self) -> Optional[Link]:
|
| 31 |
+
"""Returns the underlying link, if there's one."""
|
| 32 |
+
raise NotImplementedError()
|
| 33 |
+
|
| 34 |
+
def page_candidates(self) -> FoundCandidates:
|
| 35 |
+
"""Candidates found by parsing an archive listing HTML file."""
|
| 36 |
+
raise NotImplementedError()
|
| 37 |
+
|
| 38 |
+
def file_links(self) -> FoundLinks:
|
| 39 |
+
"""Links found by specifying archives directly."""
|
| 40 |
+
raise NotImplementedError()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _is_html_file(file_url: str) -> bool:
|
| 44 |
+
return mimetypes.guess_type(file_url, strict=False)[0] == "text/html"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class _FlatDirectoryToUrls:
|
| 48 |
+
"""Scans directory and caches results"""
|
| 49 |
+
|
| 50 |
+
def __init__(self, path: str) -> None:
|
| 51 |
+
self._path = path
|
| 52 |
+
self._page_candidates: List[str] = []
|
| 53 |
+
self._project_name_to_urls: Dict[str, List[str]] = defaultdict(list)
|
| 54 |
+
self._scanned_directory = False
|
| 55 |
+
|
| 56 |
+
def _scan_directory(self) -> None:
|
| 57 |
+
"""Scans directory once and populates both page_candidates
|
| 58 |
+
and project_name_to_urls at the same time
|
| 59 |
+
"""
|
| 60 |
+
for entry in os.scandir(self._path):
|
| 61 |
+
url = path_to_url(entry.path)
|
| 62 |
+
if _is_html_file(url):
|
| 63 |
+
self._page_candidates.append(url)
|
| 64 |
+
continue
|
| 65 |
+
|
| 66 |
+
# File must have a valid wheel or sdist name,
|
| 67 |
+
# otherwise not worth considering as a package
|
| 68 |
+
try:
|
| 69 |
+
project_filename = parse_wheel_filename(entry.name)[0]
|
| 70 |
+
except InvalidWheelFilename:
|
| 71 |
+
try:
|
| 72 |
+
project_filename = parse_sdist_filename(entry.name)[0]
|
| 73 |
+
except InvalidSdistFilename:
|
| 74 |
+
continue
|
| 75 |
+
|
| 76 |
+
self._project_name_to_urls[project_filename].append(url)
|
| 77 |
+
self._scanned_directory = True
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def page_candidates(self) -> List[str]:
|
| 81 |
+
if not self._scanned_directory:
|
| 82 |
+
self._scan_directory()
|
| 83 |
+
|
| 84 |
+
return self._page_candidates
|
| 85 |
+
|
| 86 |
+
@property
|
| 87 |
+
def project_name_to_urls(self) -> Dict[str, List[str]]:
|
| 88 |
+
if not self._scanned_directory:
|
| 89 |
+
self._scan_directory()
|
| 90 |
+
|
| 91 |
+
return self._project_name_to_urls
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class _FlatDirectorySource(LinkSource):
|
| 95 |
+
"""Link source specified by ``--find-links=<path-to-dir>``.
|
| 96 |
+
|
| 97 |
+
This looks the content of the directory, and returns:
|
| 98 |
+
|
| 99 |
+
* ``page_candidates``: Links listed on each HTML file in the directory.
|
| 100 |
+
* ``file_candidates``: Archives in the directory.
|
| 101 |
+
"""
|
| 102 |
+
|
| 103 |
+
_paths_to_urls: Dict[str, _FlatDirectoryToUrls] = {}
|
| 104 |
+
|
| 105 |
+
def __init__(
|
| 106 |
+
self,
|
| 107 |
+
candidates_from_page: CandidatesFromPage,
|
| 108 |
+
path: str,
|
| 109 |
+
project_name: str,
|
| 110 |
+
) -> None:
|
| 111 |
+
self._candidates_from_page = candidates_from_page
|
| 112 |
+
self._project_name = canonicalize_name(project_name)
|
| 113 |
+
|
| 114 |
+
# Get existing instance of _FlatDirectoryToUrls if it exists
|
| 115 |
+
if path in self._paths_to_urls:
|
| 116 |
+
self._path_to_urls = self._paths_to_urls[path]
|
| 117 |
+
else:
|
| 118 |
+
self._path_to_urls = _FlatDirectoryToUrls(path=path)
|
| 119 |
+
self._paths_to_urls[path] = self._path_to_urls
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def link(self) -> Optional[Link]:
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
def page_candidates(self) -> FoundCandidates:
|
| 126 |
+
for url in self._path_to_urls.page_candidates:
|
| 127 |
+
yield from self._candidates_from_page(Link(url))
|
| 128 |
+
|
| 129 |
+
def file_links(self) -> FoundLinks:
|
| 130 |
+
for url in self._path_to_urls.project_name_to_urls[self._project_name]:
|
| 131 |
+
yield Link(url)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class _LocalFileSource(LinkSource):
|
| 135 |
+
"""``--find-links=<path-or-url>`` or ``--[extra-]index-url=<path-or-url>``.
|
| 136 |
+
|
| 137 |
+
If a URL is supplied, it must be a ``file:`` URL. If a path is supplied to
|
| 138 |
+
the option, it is converted to a URL first. This returns:
|
| 139 |
+
|
| 140 |
+
* ``page_candidates``: Links listed on an HTML file.
|
| 141 |
+
* ``file_candidates``: The non-HTML file.
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
def __init__(
|
| 145 |
+
self,
|
| 146 |
+
candidates_from_page: CandidatesFromPage,
|
| 147 |
+
link: Link,
|
| 148 |
+
) -> None:
|
| 149 |
+
self._candidates_from_page = candidates_from_page
|
| 150 |
+
self._link = link
|
| 151 |
+
|
| 152 |
+
@property
|
| 153 |
+
def link(self) -> Optional[Link]:
|
| 154 |
+
return self._link
|
| 155 |
+
|
| 156 |
+
def page_candidates(self) -> FoundCandidates:
|
| 157 |
+
if not _is_html_file(self._link.url):
|
| 158 |
+
return
|
| 159 |
+
yield from self._candidates_from_page(self._link)
|
| 160 |
+
|
| 161 |
+
def file_links(self) -> FoundLinks:
|
| 162 |
+
if _is_html_file(self._link.url):
|
| 163 |
+
return
|
| 164 |
+
yield self._link
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
class _RemoteFileSource(LinkSource):
|
| 168 |
+
"""``--find-links=<url>`` or ``--[extra-]index-url=<url>``.
|
| 169 |
+
|
| 170 |
+
This returns:
|
| 171 |
+
|
| 172 |
+
* ``page_candidates``: Links listed on an HTML file.
|
| 173 |
+
* ``file_candidates``: The non-HTML file.
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
def __init__(
|
| 177 |
+
self,
|
| 178 |
+
candidates_from_page: CandidatesFromPage,
|
| 179 |
+
page_validator: PageValidator,
|
| 180 |
+
link: Link,
|
| 181 |
+
) -> None:
|
| 182 |
+
self._candidates_from_page = candidates_from_page
|
| 183 |
+
self._page_validator = page_validator
|
| 184 |
+
self._link = link
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def link(self) -> Optional[Link]:
|
| 188 |
+
return self._link
|
| 189 |
+
|
| 190 |
+
def page_candidates(self) -> FoundCandidates:
|
| 191 |
+
if not self._page_validator(self._link):
|
| 192 |
+
return
|
| 193 |
+
yield from self._candidates_from_page(self._link)
|
| 194 |
+
|
| 195 |
+
def file_links(self) -> FoundLinks:
|
| 196 |
+
yield self._link
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class _IndexDirectorySource(LinkSource):
|
| 200 |
+
"""``--[extra-]index-url=<path-to-directory>``.
|
| 201 |
+
|
| 202 |
+
This is treated like a remote URL; ``candidates_from_page`` contains logic
|
| 203 |
+
for this by appending ``index.html`` to the link.
|
| 204 |
+
"""
|
| 205 |
+
|
| 206 |
+
def __init__(
|
| 207 |
+
self,
|
| 208 |
+
candidates_from_page: CandidatesFromPage,
|
| 209 |
+
link: Link,
|
| 210 |
+
) -> None:
|
| 211 |
+
self._candidates_from_page = candidates_from_page
|
| 212 |
+
self._link = link
|
| 213 |
+
|
| 214 |
+
@property
|
| 215 |
+
def link(self) -> Optional[Link]:
|
| 216 |
+
return self._link
|
| 217 |
+
|
| 218 |
+
def page_candidates(self) -> FoundCandidates:
|
| 219 |
+
yield from self._candidates_from_page(self._link)
|
| 220 |
+
|
| 221 |
+
def file_links(self) -> FoundLinks:
|
| 222 |
+
return ()
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def build_source(
|
| 226 |
+
location: str,
|
| 227 |
+
*,
|
| 228 |
+
candidates_from_page: CandidatesFromPage,
|
| 229 |
+
page_validator: PageValidator,
|
| 230 |
+
expand_dir: bool,
|
| 231 |
+
cache_link_parsing: bool,
|
| 232 |
+
project_name: str,
|
| 233 |
+
) -> Tuple[Optional[str], Optional[LinkSource]]:
|
| 234 |
+
path: Optional[str] = None
|
| 235 |
+
url: Optional[str] = None
|
| 236 |
+
if os.path.exists(location): # Is a local path.
|
| 237 |
+
url = path_to_url(location)
|
| 238 |
+
path = location
|
| 239 |
+
elif location.startswith("file:"): # A file: URL.
|
| 240 |
+
url = location
|
| 241 |
+
path = url_to_path(location)
|
| 242 |
+
elif is_url(location):
|
| 243 |
+
url = location
|
| 244 |
+
|
| 245 |
+
if url is None:
|
| 246 |
+
msg = (
|
| 247 |
+
"Location '%s' is ignored: "
|
| 248 |
+
"it is either a non-existing path or lacks a specific scheme."
|
| 249 |
+
)
|
| 250 |
+
logger.warning(msg, location)
|
| 251 |
+
return (None, None)
|
| 252 |
+
|
| 253 |
+
if path is None:
|
| 254 |
+
source: LinkSource = _RemoteFileSource(
|
| 255 |
+
candidates_from_page=candidates_from_page,
|
| 256 |
+
page_validator=page_validator,
|
| 257 |
+
link=Link(url, cache_link_parsing=cache_link_parsing),
|
| 258 |
+
)
|
| 259 |
+
return (url, source)
|
| 260 |
+
|
| 261 |
+
if os.path.isdir(path):
|
| 262 |
+
if expand_dir:
|
| 263 |
+
source = _FlatDirectorySource(
|
| 264 |
+
candidates_from_page=candidates_from_page,
|
| 265 |
+
path=path,
|
| 266 |
+
project_name=project_name,
|
| 267 |
+
)
|
| 268 |
+
else:
|
| 269 |
+
source = _IndexDirectorySource(
|
| 270 |
+
candidates_from_page=candidates_from_page,
|
| 271 |
+
link=Link(url, cache_link_parsing=cache_link_parsing),
|
| 272 |
+
)
|
| 273 |
+
return (url, source)
|
| 274 |
+
elif os.path.isfile(path):
|
| 275 |
+
source = _LocalFileSource(
|
| 276 |
+
candidates_from_page=candidates_from_page,
|
| 277 |
+
link=Link(url, cache_link_parsing=cache_link_parsing),
|
| 278 |
+
)
|
| 279 |
+
return (url, source)
|
| 280 |
+
logger.warning(
|
| 281 |
+
"Location '%s' is ignored: it is neither a file nor a directory.",
|
| 282 |
+
location,
|
| 283 |
+
)
|
| 284 |
+
return (url, None)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/locations/__init__.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import pathlib
|
| 5 |
+
import sys
|
| 6 |
+
import sysconfig
|
| 7 |
+
from typing import Any, Dict, Generator, Optional, Tuple
|
| 8 |
+
|
| 9 |
+
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
|
| 10 |
+
from pip._internal.utils.compat import WINDOWS
|
| 11 |
+
from pip._internal.utils.deprecation import deprecated
|
| 12 |
+
from pip._internal.utils.virtualenv import running_under_virtualenv
|
| 13 |
+
|
| 14 |
+
from . import _sysconfig
|
| 15 |
+
from .base import (
|
| 16 |
+
USER_CACHE_DIR,
|
| 17 |
+
get_major_minor_version,
|
| 18 |
+
get_src_prefix,
|
| 19 |
+
is_osx_framework,
|
| 20 |
+
site_packages,
|
| 21 |
+
user_site,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
__all__ = [
|
| 25 |
+
"USER_CACHE_DIR",
|
| 26 |
+
"get_bin_prefix",
|
| 27 |
+
"get_bin_user",
|
| 28 |
+
"get_major_minor_version",
|
| 29 |
+
"get_platlib",
|
| 30 |
+
"get_purelib",
|
| 31 |
+
"get_scheme",
|
| 32 |
+
"get_src_prefix",
|
| 33 |
+
"site_packages",
|
| 34 |
+
"user_site",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
logger = logging.getLogger(__name__)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
_PLATLIBDIR: str = getattr(sys, "platlibdir", "lib")
|
| 42 |
+
|
| 43 |
+
_USE_SYSCONFIG_DEFAULT = sys.version_info >= (3, 10)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _should_use_sysconfig() -> bool:
|
| 47 |
+
"""This function determines the value of _USE_SYSCONFIG.
|
| 48 |
+
|
| 49 |
+
By default, pip uses sysconfig on Python 3.10+.
|
| 50 |
+
But Python distributors can override this decision by setting:
|
| 51 |
+
sysconfig._PIP_USE_SYSCONFIG = True / False
|
| 52 |
+
Rationale in https://github.com/pypa/pip/issues/10647
|
| 53 |
+
|
| 54 |
+
This is a function for testability, but should be constant during any one
|
| 55 |
+
run.
|
| 56 |
+
"""
|
| 57 |
+
return bool(getattr(sysconfig, "_PIP_USE_SYSCONFIG", _USE_SYSCONFIG_DEFAULT))
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
_USE_SYSCONFIG = _should_use_sysconfig()
|
| 61 |
+
|
| 62 |
+
if not _USE_SYSCONFIG:
|
| 63 |
+
# Import distutils lazily to avoid deprecation warnings,
|
| 64 |
+
# but import it soon enough that it is in memory and available during
|
| 65 |
+
# a pip reinstall.
|
| 66 |
+
from . import _distutils
|
| 67 |
+
|
| 68 |
+
# Be noisy about incompatibilities if this platforms "should" be using
|
| 69 |
+
# sysconfig, but is explicitly opting out and using distutils instead.
|
| 70 |
+
if _USE_SYSCONFIG_DEFAULT and not _USE_SYSCONFIG:
|
| 71 |
+
_MISMATCH_LEVEL = logging.WARNING
|
| 72 |
+
else:
|
| 73 |
+
_MISMATCH_LEVEL = logging.DEBUG
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _looks_like_bpo_44860() -> bool:
|
| 77 |
+
"""The resolution to bpo-44860 will change this incorrect platlib.
|
| 78 |
+
|
| 79 |
+
See <https://bugs.python.org/issue44860>.
|
| 80 |
+
"""
|
| 81 |
+
from distutils.command.install import INSTALL_SCHEMES
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
unix_user_platlib = INSTALL_SCHEMES["unix_user"]["platlib"]
|
| 85 |
+
except KeyError:
|
| 86 |
+
return False
|
| 87 |
+
return unix_user_platlib == "$usersite"
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _looks_like_red_hat_patched_platlib_purelib(scheme: Dict[str, str]) -> bool:
|
| 91 |
+
platlib = scheme["platlib"]
|
| 92 |
+
if "/$platlibdir/" in platlib:
|
| 93 |
+
platlib = platlib.replace("/$platlibdir/", f"/{_PLATLIBDIR}/")
|
| 94 |
+
if "/lib64/" not in platlib:
|
| 95 |
+
return False
|
| 96 |
+
unpatched = platlib.replace("/lib64/", "/lib/")
|
| 97 |
+
return unpatched.replace("$platbase/", "$base/") == scheme["purelib"]
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@functools.lru_cache(maxsize=None)
|
| 101 |
+
def _looks_like_red_hat_lib() -> bool:
|
| 102 |
+
"""Red Hat patches platlib in unix_prefix and unix_home, but not purelib.
|
| 103 |
+
|
| 104 |
+
This is the only way I can see to tell a Red Hat-patched Python.
|
| 105 |
+
"""
|
| 106 |
+
from distutils.command.install import INSTALL_SCHEMES
|
| 107 |
+
|
| 108 |
+
return all(
|
| 109 |
+
k in INSTALL_SCHEMES
|
| 110 |
+
and _looks_like_red_hat_patched_platlib_purelib(INSTALL_SCHEMES[k])
|
| 111 |
+
for k in ("unix_prefix", "unix_home")
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@functools.lru_cache(maxsize=None)
|
| 116 |
+
def _looks_like_debian_scheme() -> bool:
|
| 117 |
+
"""Debian adds two additional schemes."""
|
| 118 |
+
from distutils.command.install import INSTALL_SCHEMES
|
| 119 |
+
|
| 120 |
+
return "deb_system" in INSTALL_SCHEMES and "unix_local" in INSTALL_SCHEMES
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@functools.lru_cache(maxsize=None)
|
| 124 |
+
def _looks_like_red_hat_scheme() -> bool:
|
| 125 |
+
"""Red Hat patches ``sys.prefix`` and ``sys.exec_prefix``.
|
| 126 |
+
|
| 127 |
+
Red Hat's ``00251-change-user-install-location.patch`` changes the install
|
| 128 |
+
command's ``prefix`` and ``exec_prefix`` to append ``"/local"``. This is
|
| 129 |
+
(fortunately?) done quite unconditionally, so we create a default command
|
| 130 |
+
object without any configuration to detect this.
|
| 131 |
+
"""
|
| 132 |
+
from distutils.command.install import install
|
| 133 |
+
from distutils.dist import Distribution
|
| 134 |
+
|
| 135 |
+
cmd: Any = install(Distribution())
|
| 136 |
+
cmd.finalize_options()
|
| 137 |
+
return (
|
| 138 |
+
cmd.exec_prefix == f"{os.path.normpath(sys.exec_prefix)}/local"
|
| 139 |
+
and cmd.prefix == f"{os.path.normpath(sys.prefix)}/local"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@functools.lru_cache(maxsize=None)
|
| 144 |
+
def _looks_like_slackware_scheme() -> bool:
|
| 145 |
+
"""Slackware patches sysconfig but fails to patch distutils and site.
|
| 146 |
+
|
| 147 |
+
Slackware changes sysconfig's user scheme to use ``"lib64"`` for the lib
|
| 148 |
+
path, but does not do the same to the site module.
|
| 149 |
+
"""
|
| 150 |
+
if user_site is None: # User-site not available.
|
| 151 |
+
return False
|
| 152 |
+
try:
|
| 153 |
+
paths = sysconfig.get_paths(scheme="posix_user", expand=False)
|
| 154 |
+
except KeyError: # User-site not available.
|
| 155 |
+
return False
|
| 156 |
+
return "/lib64/" in paths["purelib"] and "/lib64/" not in user_site
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@functools.lru_cache(maxsize=None)
|
| 160 |
+
def _looks_like_msys2_mingw_scheme() -> bool:
|
| 161 |
+
"""MSYS2 patches distutils and sysconfig to use a UNIX-like scheme.
|
| 162 |
+
|
| 163 |
+
However, MSYS2 incorrectly patches sysconfig ``nt`` scheme. The fix is
|
| 164 |
+
likely going to be included in their 3.10 release, so we ignore the warning.
|
| 165 |
+
See msys2/MINGW-packages#9319.
|
| 166 |
+
|
| 167 |
+
MSYS2 MINGW's patch uses lowercase ``"lib"`` instead of the usual uppercase,
|
| 168 |
+
and is missing the final ``"site-packages"``.
|
| 169 |
+
"""
|
| 170 |
+
paths = sysconfig.get_paths("nt", expand=False)
|
| 171 |
+
return all(
|
| 172 |
+
"Lib" not in p and "lib" in p and not p.endswith("site-packages")
|
| 173 |
+
for p in (paths[key] for key in ("platlib", "purelib"))
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _fix_abiflags(parts: Tuple[str]) -> Generator[str, None, None]:
|
| 178 |
+
ldversion = sysconfig.get_config_var("LDVERSION")
|
| 179 |
+
abiflags = getattr(sys, "abiflags", None)
|
| 180 |
+
|
| 181 |
+
# LDVERSION does not end with sys.abiflags. Just return the path unchanged.
|
| 182 |
+
if not ldversion or not abiflags or not ldversion.endswith(abiflags):
|
| 183 |
+
yield from parts
|
| 184 |
+
return
|
| 185 |
+
|
| 186 |
+
# Strip sys.abiflags from LDVERSION-based path components.
|
| 187 |
+
for part in parts:
|
| 188 |
+
if part.endswith(ldversion):
|
| 189 |
+
part = part[: (0 - len(abiflags))]
|
| 190 |
+
yield part
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
@functools.lru_cache(maxsize=None)
|
| 194 |
+
def _warn_mismatched(old: pathlib.Path, new: pathlib.Path, *, key: str) -> None:
|
| 195 |
+
issue_url = "https://github.com/pypa/pip/issues/10151"
|
| 196 |
+
message = (
|
| 197 |
+
"Value for %s does not match. Please report this to <%s>"
|
| 198 |
+
"\ndistutils: %s"
|
| 199 |
+
"\nsysconfig: %s"
|
| 200 |
+
)
|
| 201 |
+
logger.log(_MISMATCH_LEVEL, message, key, issue_url, old, new)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def _warn_if_mismatch(old: pathlib.Path, new: pathlib.Path, *, key: str) -> bool:
|
| 205 |
+
if old == new:
|
| 206 |
+
return False
|
| 207 |
+
_warn_mismatched(old, new, key=key)
|
| 208 |
+
return True
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@functools.lru_cache(maxsize=None)
|
| 212 |
+
def _log_context(
|
| 213 |
+
*,
|
| 214 |
+
user: bool = False,
|
| 215 |
+
home: Optional[str] = None,
|
| 216 |
+
root: Optional[str] = None,
|
| 217 |
+
prefix: Optional[str] = None,
|
| 218 |
+
) -> None:
|
| 219 |
+
parts = [
|
| 220 |
+
"Additional context:",
|
| 221 |
+
"user = %r",
|
| 222 |
+
"home = %r",
|
| 223 |
+
"root = %r",
|
| 224 |
+
"prefix = %r",
|
| 225 |
+
]
|
| 226 |
+
|
| 227 |
+
logger.log(_MISMATCH_LEVEL, "\n".join(parts), user, home, root, prefix)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def get_scheme(
|
| 231 |
+
dist_name: str,
|
| 232 |
+
user: bool = False,
|
| 233 |
+
home: Optional[str] = None,
|
| 234 |
+
root: Optional[str] = None,
|
| 235 |
+
isolated: bool = False,
|
| 236 |
+
prefix: Optional[str] = None,
|
| 237 |
+
) -> Scheme:
|
| 238 |
+
new = _sysconfig.get_scheme(
|
| 239 |
+
dist_name,
|
| 240 |
+
user=user,
|
| 241 |
+
home=home,
|
| 242 |
+
root=root,
|
| 243 |
+
isolated=isolated,
|
| 244 |
+
prefix=prefix,
|
| 245 |
+
)
|
| 246 |
+
if _USE_SYSCONFIG:
|
| 247 |
+
return new
|
| 248 |
+
|
| 249 |
+
old = _distutils.get_scheme(
|
| 250 |
+
dist_name,
|
| 251 |
+
user=user,
|
| 252 |
+
home=home,
|
| 253 |
+
root=root,
|
| 254 |
+
isolated=isolated,
|
| 255 |
+
prefix=prefix,
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
warning_contexts = []
|
| 259 |
+
for k in SCHEME_KEYS:
|
| 260 |
+
old_v = pathlib.Path(getattr(old, k))
|
| 261 |
+
new_v = pathlib.Path(getattr(new, k))
|
| 262 |
+
|
| 263 |
+
if old_v == new_v:
|
| 264 |
+
continue
|
| 265 |
+
|
| 266 |
+
# distutils incorrectly put PyPy packages under ``site-packages/python``
|
| 267 |
+
# in the ``posix_home`` scheme, but PyPy devs said they expect the
|
| 268 |
+
# directory name to be ``pypy`` instead. So we treat this as a bug fix
|
| 269 |
+
# and not warn about it. See bpo-43307 and python/cpython#24628.
|
| 270 |
+
skip_pypy_special_case = (
|
| 271 |
+
sys.implementation.name == "pypy"
|
| 272 |
+
and home is not None
|
| 273 |
+
and k in ("platlib", "purelib")
|
| 274 |
+
and old_v.parent == new_v.parent
|
| 275 |
+
and old_v.name.startswith("python")
|
| 276 |
+
and new_v.name.startswith("pypy")
|
| 277 |
+
)
|
| 278 |
+
if skip_pypy_special_case:
|
| 279 |
+
continue
|
| 280 |
+
|
| 281 |
+
# sysconfig's ``osx_framework_user`` does not include ``pythonX.Y`` in
|
| 282 |
+
# the ``include`` value, but distutils's ``headers`` does. We'll let
|
| 283 |
+
# CPython decide whether this is a bug or feature. See bpo-43948.
|
| 284 |
+
skip_osx_framework_user_special_case = (
|
| 285 |
+
user
|
| 286 |
+
and is_osx_framework()
|
| 287 |
+
and k == "headers"
|
| 288 |
+
and old_v.parent.parent == new_v.parent
|
| 289 |
+
and old_v.parent.name.startswith("python")
|
| 290 |
+
)
|
| 291 |
+
if skip_osx_framework_user_special_case:
|
| 292 |
+
continue
|
| 293 |
+
|
| 294 |
+
# On Red Hat and derived Linux distributions, distutils is patched to
|
| 295 |
+
# use "lib64" instead of "lib" for platlib.
|
| 296 |
+
if k == "platlib" and _looks_like_red_hat_lib():
|
| 297 |
+
continue
|
| 298 |
+
|
| 299 |
+
# On Python 3.9+, sysconfig's posix_user scheme sets platlib against
|
| 300 |
+
# sys.platlibdir, but distutils's unix_user incorrectly coninutes
|
| 301 |
+
# using the same $usersite for both platlib and purelib. This creates a
|
| 302 |
+
# mismatch when sys.platlibdir is not "lib".
|
| 303 |
+
skip_bpo_44860 = (
|
| 304 |
+
user
|
| 305 |
+
and k == "platlib"
|
| 306 |
+
and not WINDOWS
|
| 307 |
+
and sys.version_info >= (3, 9)
|
| 308 |
+
and _PLATLIBDIR != "lib"
|
| 309 |
+
and _looks_like_bpo_44860()
|
| 310 |
+
)
|
| 311 |
+
if skip_bpo_44860:
|
| 312 |
+
continue
|
| 313 |
+
|
| 314 |
+
# Slackware incorrectly patches posix_user to use lib64 instead of lib,
|
| 315 |
+
# but not usersite to match the location.
|
| 316 |
+
skip_slackware_user_scheme = (
|
| 317 |
+
user
|
| 318 |
+
and k in ("platlib", "purelib")
|
| 319 |
+
and not WINDOWS
|
| 320 |
+
and _looks_like_slackware_scheme()
|
| 321 |
+
)
|
| 322 |
+
if skip_slackware_user_scheme:
|
| 323 |
+
continue
|
| 324 |
+
|
| 325 |
+
# Both Debian and Red Hat patch Python to place the system site under
|
| 326 |
+
# /usr/local instead of /usr. Debian also places lib in dist-packages
|
| 327 |
+
# instead of site-packages, but the /usr/local check should cover it.
|
| 328 |
+
skip_linux_system_special_case = (
|
| 329 |
+
not (user or home or prefix or running_under_virtualenv())
|
| 330 |
+
and old_v.parts[1:3] == ("usr", "local")
|
| 331 |
+
and len(new_v.parts) > 1
|
| 332 |
+
and new_v.parts[1] == "usr"
|
| 333 |
+
and (len(new_v.parts) < 3 or new_v.parts[2] != "local")
|
| 334 |
+
and (_looks_like_red_hat_scheme() or _looks_like_debian_scheme())
|
| 335 |
+
)
|
| 336 |
+
if skip_linux_system_special_case:
|
| 337 |
+
continue
|
| 338 |
+
|
| 339 |
+
# MSYS2 MINGW's sysconfig patch does not include the "site-packages"
|
| 340 |
+
# part of the path. This is incorrect and will be fixed in MSYS.
|
| 341 |
+
skip_msys2_mingw_bug = (
|
| 342 |
+
WINDOWS and k in ("platlib", "purelib") and _looks_like_msys2_mingw_scheme()
|
| 343 |
+
)
|
| 344 |
+
if skip_msys2_mingw_bug:
|
| 345 |
+
continue
|
| 346 |
+
|
| 347 |
+
# CPython's POSIX install script invokes pip (via ensurepip) against the
|
| 348 |
+
# interpreter located in the source tree, not the install site. This
|
| 349 |
+
# triggers special logic in sysconfig that's not present in distutils.
|
| 350 |
+
# https://github.com/python/cpython/blob/8c21941ddaf/Lib/sysconfig.py#L178-L194
|
| 351 |
+
skip_cpython_build = (
|
| 352 |
+
sysconfig.is_python_build(check_home=True)
|
| 353 |
+
and not WINDOWS
|
| 354 |
+
and k in ("headers", "include", "platinclude")
|
| 355 |
+
)
|
| 356 |
+
if skip_cpython_build:
|
| 357 |
+
continue
|
| 358 |
+
|
| 359 |
+
warning_contexts.append((old_v, new_v, f"scheme.{k}"))
|
| 360 |
+
|
| 361 |
+
if not warning_contexts:
|
| 362 |
+
return old
|
| 363 |
+
|
| 364 |
+
# Check if this path mismatch is caused by distutils config files. Those
|
| 365 |
+
# files will no longer work once we switch to sysconfig, so this raises a
|
| 366 |
+
# deprecation message for them.
|
| 367 |
+
default_old = _distutils.distutils_scheme(
|
| 368 |
+
dist_name,
|
| 369 |
+
user,
|
| 370 |
+
home,
|
| 371 |
+
root,
|
| 372 |
+
isolated,
|
| 373 |
+
prefix,
|
| 374 |
+
ignore_config_files=True,
|
| 375 |
+
)
|
| 376 |
+
if any(default_old[k] != getattr(old, k) for k in SCHEME_KEYS):
|
| 377 |
+
deprecated(
|
| 378 |
+
reason=(
|
| 379 |
+
"Configuring installation scheme with distutils config files "
|
| 380 |
+
"is deprecated and will no longer work in the near future. If you "
|
| 381 |
+
"are using a Homebrew or Linuxbrew Python, please see discussion "
|
| 382 |
+
"at https://github.com/Homebrew/homebrew-core/issues/76621"
|
| 383 |
+
),
|
| 384 |
+
replacement=None,
|
| 385 |
+
gone_in=None,
|
| 386 |
+
)
|
| 387 |
+
return old
|
| 388 |
+
|
| 389 |
+
# Post warnings about this mismatch so user can report them back.
|
| 390 |
+
for old_v, new_v, key in warning_contexts:
|
| 391 |
+
_warn_mismatched(old_v, new_v, key=key)
|
| 392 |
+
_log_context(user=user, home=home, root=root, prefix=prefix)
|
| 393 |
+
|
| 394 |
+
return old
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
def get_bin_prefix() -> str:
|
| 398 |
+
new = _sysconfig.get_bin_prefix()
|
| 399 |
+
if _USE_SYSCONFIG:
|
| 400 |
+
return new
|
| 401 |
+
|
| 402 |
+
old = _distutils.get_bin_prefix()
|
| 403 |
+
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"):
|
| 404 |
+
_log_context()
|
| 405 |
+
return old
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def get_bin_user() -> str:
|
| 409 |
+
return _sysconfig.get_scheme("", user=True).scripts
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def _looks_like_deb_system_dist_packages(value: str) -> bool:
|
| 413 |
+
"""Check if the value is Debian's APT-controlled dist-packages.
|
| 414 |
+
|
| 415 |
+
Debian's ``distutils.sysconfig.get_python_lib()`` implementation returns the
|
| 416 |
+
default package path controlled by APT, but does not patch ``sysconfig`` to
|
| 417 |
+
do the same. This is similar to the bug worked around in ``get_scheme()``,
|
| 418 |
+
but here the default is ``deb_system`` instead of ``unix_local``. Ultimately
|
| 419 |
+
we can't do anything about this Debian bug, and this detection allows us to
|
| 420 |
+
skip the warning when needed.
|
| 421 |
+
"""
|
| 422 |
+
if not _looks_like_debian_scheme():
|
| 423 |
+
return False
|
| 424 |
+
if value == "/usr/lib/python3/dist-packages":
|
| 425 |
+
return True
|
| 426 |
+
return False
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def get_purelib() -> str:
|
| 430 |
+
"""Return the default pure-Python lib location."""
|
| 431 |
+
new = _sysconfig.get_purelib()
|
| 432 |
+
if _USE_SYSCONFIG:
|
| 433 |
+
return new
|
| 434 |
+
|
| 435 |
+
old = _distutils.get_purelib()
|
| 436 |
+
if _looks_like_deb_system_dist_packages(old):
|
| 437 |
+
return old
|
| 438 |
+
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib"):
|
| 439 |
+
_log_context()
|
| 440 |
+
return old
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def get_platlib() -> str:
|
| 444 |
+
"""Return the default platform-shared lib location."""
|
| 445 |
+
new = _sysconfig.get_platlib()
|
| 446 |
+
if _USE_SYSCONFIG:
|
| 447 |
+
return new
|
| 448 |
+
|
| 449 |
+
from . import _distutils
|
| 450 |
+
|
| 451 |
+
old = _distutils.get_platlib()
|
| 452 |
+
if _looks_like_deb_system_dist_packages(old):
|
| 453 |
+
return old
|
| 454 |
+
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"):
|
| 455 |
+
_log_context()
|
| 456 |
+
return old
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/locations/_distutils.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Locations where we look for configs, install stuff, etc"""
|
| 2 |
+
|
| 3 |
+
# The following comment should be removed at some point in the future.
|
| 4 |
+
# mypy: strict-optional=False
|
| 5 |
+
|
| 6 |
+
# If pip's going to use distutils, it should not be using the copy that setuptools
|
| 7 |
+
# might have injected into the environment. This is done by removing the injected
|
| 8 |
+
# shim, if it's injected.
|
| 9 |
+
#
|
| 10 |
+
# See https://github.com/pypa/pip/issues/8761 for the original discussion and
|
| 11 |
+
# rationale for why this is done within pip.
|
| 12 |
+
try:
|
| 13 |
+
__import__("_distutils_hack").remove_shim()
|
| 14 |
+
except (ImportError, AttributeError):
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
import logging
|
| 18 |
+
import os
|
| 19 |
+
import sys
|
| 20 |
+
from distutils.cmd import Command as DistutilsCommand
|
| 21 |
+
from distutils.command.install import SCHEME_KEYS
|
| 22 |
+
from distutils.command.install import install as distutils_install_command
|
| 23 |
+
from distutils.sysconfig import get_python_lib
|
| 24 |
+
from typing import Dict, List, Optional, Union
|
| 25 |
+
|
| 26 |
+
from pip._internal.models.scheme import Scheme
|
| 27 |
+
from pip._internal.utils.compat import WINDOWS
|
| 28 |
+
from pip._internal.utils.virtualenv import running_under_virtualenv
|
| 29 |
+
|
| 30 |
+
from .base import get_major_minor_version
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def distutils_scheme(
|
| 36 |
+
dist_name: str,
|
| 37 |
+
user: bool = False,
|
| 38 |
+
home: Optional[str] = None,
|
| 39 |
+
root: Optional[str] = None,
|
| 40 |
+
isolated: bool = False,
|
| 41 |
+
prefix: Optional[str] = None,
|
| 42 |
+
*,
|
| 43 |
+
ignore_config_files: bool = False,
|
| 44 |
+
) -> Dict[str, str]:
|
| 45 |
+
"""
|
| 46 |
+
Return a distutils install scheme
|
| 47 |
+
"""
|
| 48 |
+
from distutils.dist import Distribution
|
| 49 |
+
|
| 50 |
+
dist_args: Dict[str, Union[str, List[str]]] = {"name": dist_name}
|
| 51 |
+
if isolated:
|
| 52 |
+
dist_args["script_args"] = ["--no-user-cfg"]
|
| 53 |
+
|
| 54 |
+
d = Distribution(dist_args)
|
| 55 |
+
if not ignore_config_files:
|
| 56 |
+
try:
|
| 57 |
+
d.parse_config_files()
|
| 58 |
+
except UnicodeDecodeError:
|
| 59 |
+
paths = d.find_config_files()
|
| 60 |
+
logger.warning(
|
| 61 |
+
"Ignore distutils configs in %s due to encoding errors.",
|
| 62 |
+
", ".join(os.path.basename(p) for p in paths),
|
| 63 |
+
)
|
| 64 |
+
obj: Optional[DistutilsCommand] = None
|
| 65 |
+
obj = d.get_command_obj("install", create=True)
|
| 66 |
+
assert obj is not None
|
| 67 |
+
i: distutils_install_command = obj
|
| 68 |
+
# NOTE: setting user or home has the side-effect of creating the home dir
|
| 69 |
+
# or user base for installations during finalize_options()
|
| 70 |
+
# ideally, we'd prefer a scheme class that has no side-effects.
|
| 71 |
+
assert not (user and prefix), f"user={user} prefix={prefix}"
|
| 72 |
+
assert not (home and prefix), f"home={home} prefix={prefix}"
|
| 73 |
+
i.user = user or i.user
|
| 74 |
+
if user or home:
|
| 75 |
+
i.prefix = ""
|
| 76 |
+
i.prefix = prefix or i.prefix
|
| 77 |
+
i.home = home or i.home
|
| 78 |
+
i.root = root or i.root
|
| 79 |
+
i.finalize_options()
|
| 80 |
+
|
| 81 |
+
scheme: Dict[str, str] = {}
|
| 82 |
+
for key in SCHEME_KEYS:
|
| 83 |
+
scheme[key] = getattr(i, "install_" + key)
|
| 84 |
+
|
| 85 |
+
# install_lib specified in setup.cfg should install *everything*
|
| 86 |
+
# into there (i.e. it takes precedence over both purelib and
|
| 87 |
+
# platlib). Note, i.install_lib is *always* set after
|
| 88 |
+
# finalize_options(); we only want to override here if the user
|
| 89 |
+
# has explicitly requested it hence going back to the config
|
| 90 |
+
if "install_lib" in d.get_option_dict("install"):
|
| 91 |
+
scheme.update({"purelib": i.install_lib, "platlib": i.install_lib})
|
| 92 |
+
|
| 93 |
+
if running_under_virtualenv():
|
| 94 |
+
if home:
|
| 95 |
+
prefix = home
|
| 96 |
+
elif user:
|
| 97 |
+
prefix = i.install_userbase
|
| 98 |
+
else:
|
| 99 |
+
prefix = i.prefix
|
| 100 |
+
scheme["headers"] = os.path.join(
|
| 101 |
+
prefix,
|
| 102 |
+
"include",
|
| 103 |
+
"site",
|
| 104 |
+
f"python{get_major_minor_version()}",
|
| 105 |
+
dist_name,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
if root is not None:
|
| 109 |
+
path_no_drive = os.path.splitdrive(os.path.abspath(scheme["headers"]))[1]
|
| 110 |
+
scheme["headers"] = os.path.join(root, path_no_drive[1:])
|
| 111 |
+
|
| 112 |
+
return scheme
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def get_scheme(
|
| 116 |
+
dist_name: str,
|
| 117 |
+
user: bool = False,
|
| 118 |
+
home: Optional[str] = None,
|
| 119 |
+
root: Optional[str] = None,
|
| 120 |
+
isolated: bool = False,
|
| 121 |
+
prefix: Optional[str] = None,
|
| 122 |
+
) -> Scheme:
|
| 123 |
+
"""
|
| 124 |
+
Get the "scheme" corresponding to the input parameters. The distutils
|
| 125 |
+
documentation provides the context for the available schemes:
|
| 126 |
+
https://docs.python.org/3/install/index.html#alternate-installation
|
| 127 |
+
|
| 128 |
+
:param dist_name: the name of the package to retrieve the scheme for, used
|
| 129 |
+
in the headers scheme path
|
| 130 |
+
:param user: indicates to use the "user" scheme
|
| 131 |
+
:param home: indicates to use the "home" scheme and provides the base
|
| 132 |
+
directory for the same
|
| 133 |
+
:param root: root under which other directories are re-based
|
| 134 |
+
:param isolated: equivalent to --no-user-cfg, i.e. do not consider
|
| 135 |
+
~/.pydistutils.cfg (posix) or ~/pydistutils.cfg (non-posix) for
|
| 136 |
+
scheme paths
|
| 137 |
+
:param prefix: indicates to use the "prefix" scheme and provides the
|
| 138 |
+
base directory for the same
|
| 139 |
+
"""
|
| 140 |
+
scheme = distutils_scheme(dist_name, user, home, root, isolated, prefix)
|
| 141 |
+
return Scheme(
|
| 142 |
+
platlib=scheme["platlib"],
|
| 143 |
+
purelib=scheme["purelib"],
|
| 144 |
+
headers=scheme["headers"],
|
| 145 |
+
scripts=scheme["scripts"],
|
| 146 |
+
data=scheme["data"],
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def get_bin_prefix() -> str:
|
| 151 |
+
# XXX: In old virtualenv versions, sys.prefix can contain '..' components,
|
| 152 |
+
# so we need to call normpath to eliminate them.
|
| 153 |
+
prefix = os.path.normpath(sys.prefix)
|
| 154 |
+
if WINDOWS:
|
| 155 |
+
bin_py = os.path.join(prefix, "Scripts")
|
| 156 |
+
# buildout uses 'bin' on Windows too?
|
| 157 |
+
if not os.path.exists(bin_py):
|
| 158 |
+
bin_py = os.path.join(prefix, "bin")
|
| 159 |
+
return bin_py
|
| 160 |
+
# Forcing to use /usr/local/bin for standard macOS framework installs
|
| 161 |
+
# Also log to ~/Library/Logs/ for use with the Console.app log viewer
|
| 162 |
+
if sys.platform[:6] == "darwin" and prefix[:16] == "/System/Library/":
|
| 163 |
+
return "/usr/local/bin"
|
| 164 |
+
return os.path.join(prefix, "bin")
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def get_purelib() -> str:
|
| 168 |
+
return get_python_lib(plat_specific=False)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def get_platlib() -> str:
|
| 172 |
+
return get_python_lib(plat_specific=True)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/locations/_sysconfig.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import sysconfig
|
| 5 |
+
import typing
|
| 6 |
+
|
| 7 |
+
from pip._internal.exceptions import InvalidSchemeCombination, UserInstallationInvalid
|
| 8 |
+
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
|
| 9 |
+
from pip._internal.utils.virtualenv import running_under_virtualenv
|
| 10 |
+
|
| 11 |
+
from .base import change_root, get_major_minor_version, is_osx_framework
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# Notes on _infer_* functions.
|
| 17 |
+
# Unfortunately ``get_default_scheme()`` didn't exist before 3.10, so there's no
|
| 18 |
+
# way to ask things like "what is the '_prefix' scheme on this platform". These
|
| 19 |
+
# functions try to answer that with some heuristics while accounting for ad-hoc
|
| 20 |
+
# platforms not covered by CPython's default sysconfig implementation. If the
|
| 21 |
+
# ad-hoc implementation does not fully implement sysconfig, we'll fall back to
|
| 22 |
+
# a POSIX scheme.
|
| 23 |
+
|
| 24 |
+
_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names())
|
| 25 |
+
|
| 26 |
+
_PREFERRED_SCHEME_API = getattr(sysconfig, "get_preferred_scheme", None)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _should_use_osx_framework_prefix() -> bool:
|
| 30 |
+
"""Check for Apple's ``osx_framework_library`` scheme.
|
| 31 |
+
|
| 32 |
+
Python distributed by Apple's Command Line Tools has this special scheme
|
| 33 |
+
that's used when:
|
| 34 |
+
|
| 35 |
+
* This is a framework build.
|
| 36 |
+
* We are installing into the system prefix.
|
| 37 |
+
|
| 38 |
+
This does not account for ``pip install --prefix`` (also means we're not
|
| 39 |
+
installing to the system prefix), which should use ``posix_prefix``, but
|
| 40 |
+
logic here means ``_infer_prefix()`` outputs ``osx_framework_library``. But
|
| 41 |
+
since ``prefix`` is not available for ``sysconfig.get_default_scheme()``,
|
| 42 |
+
which is the stdlib replacement for ``_infer_prefix()``, presumably Apple
|
| 43 |
+
wouldn't be able to magically switch between ``osx_framework_library`` and
|
| 44 |
+
``posix_prefix``. ``_infer_prefix()`` returning ``osx_framework_library``
|
| 45 |
+
means its behavior is consistent whether we use the stdlib implementation
|
| 46 |
+
or our own, and we deal with this special case in ``get_scheme()`` instead.
|
| 47 |
+
"""
|
| 48 |
+
return (
|
| 49 |
+
"osx_framework_library" in _AVAILABLE_SCHEMES
|
| 50 |
+
and not running_under_virtualenv()
|
| 51 |
+
and is_osx_framework()
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _infer_prefix() -> str:
|
| 56 |
+
"""Try to find a prefix scheme for the current platform.
|
| 57 |
+
|
| 58 |
+
This tries:
|
| 59 |
+
|
| 60 |
+
* A special ``osx_framework_library`` for Python distributed by Apple's
|
| 61 |
+
Command Line Tools, when not running in a virtual environment.
|
| 62 |
+
* Implementation + OS, used by PyPy on Windows (``pypy_nt``).
|
| 63 |
+
* Implementation without OS, used by PyPy on POSIX (``pypy``).
|
| 64 |
+
* OS + "prefix", used by CPython on POSIX (``posix_prefix``).
|
| 65 |
+
* Just the OS name, used by CPython on Windows (``nt``).
|
| 66 |
+
|
| 67 |
+
If none of the above works, fall back to ``posix_prefix``.
|
| 68 |
+
"""
|
| 69 |
+
if _PREFERRED_SCHEME_API:
|
| 70 |
+
return _PREFERRED_SCHEME_API("prefix")
|
| 71 |
+
if _should_use_osx_framework_prefix():
|
| 72 |
+
return "osx_framework_library"
|
| 73 |
+
implementation_suffixed = f"{sys.implementation.name}_{os.name}"
|
| 74 |
+
if implementation_suffixed in _AVAILABLE_SCHEMES:
|
| 75 |
+
return implementation_suffixed
|
| 76 |
+
if sys.implementation.name in _AVAILABLE_SCHEMES:
|
| 77 |
+
return sys.implementation.name
|
| 78 |
+
suffixed = f"{os.name}_prefix"
|
| 79 |
+
if suffixed in _AVAILABLE_SCHEMES:
|
| 80 |
+
return suffixed
|
| 81 |
+
if os.name in _AVAILABLE_SCHEMES: # On Windows, prefx is just called "nt".
|
| 82 |
+
return os.name
|
| 83 |
+
return "posix_prefix"
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _infer_user() -> str:
|
| 87 |
+
"""Try to find a user scheme for the current platform."""
|
| 88 |
+
if _PREFERRED_SCHEME_API:
|
| 89 |
+
return _PREFERRED_SCHEME_API("user")
|
| 90 |
+
if is_osx_framework() and not running_under_virtualenv():
|
| 91 |
+
suffixed = "osx_framework_user"
|
| 92 |
+
else:
|
| 93 |
+
suffixed = f"{os.name}_user"
|
| 94 |
+
if suffixed in _AVAILABLE_SCHEMES:
|
| 95 |
+
return suffixed
|
| 96 |
+
if "posix_user" not in _AVAILABLE_SCHEMES: # User scheme unavailable.
|
| 97 |
+
raise UserInstallationInvalid()
|
| 98 |
+
return "posix_user"
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _infer_home() -> str:
|
| 102 |
+
"""Try to find a home for the current platform."""
|
| 103 |
+
if _PREFERRED_SCHEME_API:
|
| 104 |
+
return _PREFERRED_SCHEME_API("home")
|
| 105 |
+
suffixed = f"{os.name}_home"
|
| 106 |
+
if suffixed in _AVAILABLE_SCHEMES:
|
| 107 |
+
return suffixed
|
| 108 |
+
return "posix_home"
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# Update these keys if the user sets a custom home.
|
| 112 |
+
_HOME_KEYS = [
|
| 113 |
+
"installed_base",
|
| 114 |
+
"base",
|
| 115 |
+
"installed_platbase",
|
| 116 |
+
"platbase",
|
| 117 |
+
"prefix",
|
| 118 |
+
"exec_prefix",
|
| 119 |
+
]
|
| 120 |
+
if sysconfig.get_config_var("userbase") is not None:
|
| 121 |
+
_HOME_KEYS.append("userbase")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def get_scheme(
|
| 125 |
+
dist_name: str,
|
| 126 |
+
user: bool = False,
|
| 127 |
+
home: typing.Optional[str] = None,
|
| 128 |
+
root: typing.Optional[str] = None,
|
| 129 |
+
isolated: bool = False,
|
| 130 |
+
prefix: typing.Optional[str] = None,
|
| 131 |
+
) -> Scheme:
|
| 132 |
+
"""
|
| 133 |
+
Get the "scheme" corresponding to the input parameters.
|
| 134 |
+
|
| 135 |
+
:param dist_name: the name of the package to retrieve the scheme for, used
|
| 136 |
+
in the headers scheme path
|
| 137 |
+
:param user: indicates to use the "user" scheme
|
| 138 |
+
:param home: indicates to use the "home" scheme
|
| 139 |
+
:param root: root under which other directories are re-based
|
| 140 |
+
:param isolated: ignored, but kept for distutils compatibility (where
|
| 141 |
+
this controls whether the user-site pydistutils.cfg is honored)
|
| 142 |
+
:param prefix: indicates to use the "prefix" scheme and provides the
|
| 143 |
+
base directory for the same
|
| 144 |
+
"""
|
| 145 |
+
if user and prefix:
|
| 146 |
+
raise InvalidSchemeCombination("--user", "--prefix")
|
| 147 |
+
if home and prefix:
|
| 148 |
+
raise InvalidSchemeCombination("--home", "--prefix")
|
| 149 |
+
|
| 150 |
+
if home is not None:
|
| 151 |
+
scheme_name = _infer_home()
|
| 152 |
+
elif user:
|
| 153 |
+
scheme_name = _infer_user()
|
| 154 |
+
else:
|
| 155 |
+
scheme_name = _infer_prefix()
|
| 156 |
+
|
| 157 |
+
# Special case: When installing into a custom prefix, use posix_prefix
|
| 158 |
+
# instead of osx_framework_library. See _should_use_osx_framework_prefix()
|
| 159 |
+
# docstring for details.
|
| 160 |
+
if prefix is not None and scheme_name == "osx_framework_library":
|
| 161 |
+
scheme_name = "posix_prefix"
|
| 162 |
+
|
| 163 |
+
if home is not None:
|
| 164 |
+
variables = {k: home for k in _HOME_KEYS}
|
| 165 |
+
elif prefix is not None:
|
| 166 |
+
variables = {k: prefix for k in _HOME_KEYS}
|
| 167 |
+
else:
|
| 168 |
+
variables = {}
|
| 169 |
+
|
| 170 |
+
paths = sysconfig.get_paths(scheme=scheme_name, vars=variables)
|
| 171 |
+
|
| 172 |
+
# Logic here is very arbitrary, we're doing it for compatibility, don't ask.
|
| 173 |
+
# 1. Pip historically uses a special header path in virtual environments.
|
| 174 |
+
# 2. If the distribution name is not known, distutils uses 'UNKNOWN'. We
|
| 175 |
+
# only do the same when not running in a virtual environment because
|
| 176 |
+
# pip's historical header path logic (see point 1) did not do this.
|
| 177 |
+
if running_under_virtualenv():
|
| 178 |
+
if user:
|
| 179 |
+
base = variables.get("userbase", sys.prefix)
|
| 180 |
+
else:
|
| 181 |
+
base = variables.get("base", sys.prefix)
|
| 182 |
+
python_xy = f"python{get_major_minor_version()}"
|
| 183 |
+
paths["include"] = os.path.join(base, "include", "site", python_xy)
|
| 184 |
+
elif not dist_name:
|
| 185 |
+
dist_name = "UNKNOWN"
|
| 186 |
+
|
| 187 |
+
scheme = Scheme(
|
| 188 |
+
platlib=paths["platlib"],
|
| 189 |
+
purelib=paths["purelib"],
|
| 190 |
+
headers=os.path.join(paths["include"], dist_name),
|
| 191 |
+
scripts=paths["scripts"],
|
| 192 |
+
data=paths["data"],
|
| 193 |
+
)
|
| 194 |
+
if root is not None:
|
| 195 |
+
converted_keys = {}
|
| 196 |
+
for key in SCHEME_KEYS:
|
| 197 |
+
converted_keys[key] = change_root(root, getattr(scheme, key))
|
| 198 |
+
scheme = Scheme(**converted_keys)
|
| 199 |
+
return scheme
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def get_bin_prefix() -> str:
|
| 203 |
+
# Forcing to use /usr/local/bin for standard macOS framework installs.
|
| 204 |
+
if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/":
|
| 205 |
+
return "/usr/local/bin"
|
| 206 |
+
return sysconfig.get_paths()["scripts"]
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def get_purelib() -> str:
|
| 210 |
+
return sysconfig.get_paths()["purelib"]
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def get_platlib() -> str:
|
| 214 |
+
return sysconfig.get_paths()["platlib"]
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/locations/base.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import os
|
| 3 |
+
import site
|
| 4 |
+
import sys
|
| 5 |
+
import sysconfig
|
| 6 |
+
import typing
|
| 7 |
+
|
| 8 |
+
from pip._internal.exceptions import InstallationError
|
| 9 |
+
from pip._internal.utils import appdirs
|
| 10 |
+
from pip._internal.utils.virtualenv import running_under_virtualenv
|
| 11 |
+
|
| 12 |
+
# Application Directories
|
| 13 |
+
USER_CACHE_DIR = appdirs.user_cache_dir("pip")
|
| 14 |
+
|
| 15 |
+
# FIXME doesn't account for venv linked to global site-packages
|
| 16 |
+
site_packages: str = sysconfig.get_path("purelib")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_major_minor_version() -> str:
|
| 20 |
+
"""
|
| 21 |
+
Return the major-minor version of the current Python as a string, e.g.
|
| 22 |
+
"3.7" or "3.10".
|
| 23 |
+
"""
|
| 24 |
+
return "{}.{}".format(*sys.version_info)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def change_root(new_root: str, pathname: str) -> str:
|
| 28 |
+
"""Return 'pathname' with 'new_root' prepended.
|
| 29 |
+
|
| 30 |
+
If 'pathname' is relative, this is equivalent to os.path.join(new_root, pathname).
|
| 31 |
+
Otherwise, it requires making 'pathname' relative and then joining the
|
| 32 |
+
two, which is tricky on DOS/Windows and Mac OS.
|
| 33 |
+
|
| 34 |
+
This is borrowed from Python's standard library's distutils module.
|
| 35 |
+
"""
|
| 36 |
+
if os.name == "posix":
|
| 37 |
+
if not os.path.isabs(pathname):
|
| 38 |
+
return os.path.join(new_root, pathname)
|
| 39 |
+
else:
|
| 40 |
+
return os.path.join(new_root, pathname[1:])
|
| 41 |
+
|
| 42 |
+
elif os.name == "nt":
|
| 43 |
+
(drive, path) = os.path.splitdrive(pathname)
|
| 44 |
+
if path[0] == "\\":
|
| 45 |
+
path = path[1:]
|
| 46 |
+
return os.path.join(new_root, path)
|
| 47 |
+
|
| 48 |
+
else:
|
| 49 |
+
raise InstallationError(
|
| 50 |
+
f"Unknown platform: {os.name}\n"
|
| 51 |
+
"Can not change root path prefix on unknown platform."
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def get_src_prefix() -> str:
|
| 56 |
+
if running_under_virtualenv():
|
| 57 |
+
src_prefix = os.path.join(sys.prefix, "src")
|
| 58 |
+
else:
|
| 59 |
+
# FIXME: keep src in cwd for now (it is not a temporary folder)
|
| 60 |
+
try:
|
| 61 |
+
src_prefix = os.path.join(os.getcwd(), "src")
|
| 62 |
+
except OSError:
|
| 63 |
+
# In case the current working directory has been renamed or deleted
|
| 64 |
+
sys.exit("The folder you are executing pip from can no longer be found.")
|
| 65 |
+
|
| 66 |
+
# under macOS + virtualenv sys.prefix is not properly resolved
|
| 67 |
+
# it is something like /path/to/python/bin/..
|
| 68 |
+
return os.path.abspath(src_prefix)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
# Use getusersitepackages if this is present, as it ensures that the
|
| 73 |
+
# value is initialised properly.
|
| 74 |
+
user_site: typing.Optional[str] = site.getusersitepackages()
|
| 75 |
+
except AttributeError:
|
| 76 |
+
user_site = site.USER_SITE
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@functools.lru_cache(maxsize=None)
|
| 80 |
+
def is_osx_framework() -> bool:
|
| 81 |
+
return bool(sysconfig.get_config_var("PYTHONFRAMEWORK"))
|
URSA/.venv_ursa/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)
|
URSA/.venv_ursa/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 |
+
)
|
URSA/.venv_ursa/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
|
URSA/.venv_ursa/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)
|
URSA/.venv_ursa/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)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A package that contains models that represent entities.
|
| 2 |
+
"""
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/candidate.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
|
| 3 |
+
from pip._vendor.packaging.version import Version
|
| 4 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 5 |
+
|
| 6 |
+
from pip._internal.models.link import Link
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass(frozen=True)
|
| 10 |
+
class InstallationCandidate:
|
| 11 |
+
"""Represents a potential "candidate" for installation."""
|
| 12 |
+
|
| 13 |
+
__slots__ = ["name", "version", "link"]
|
| 14 |
+
|
| 15 |
+
name: str
|
| 16 |
+
version: Version
|
| 17 |
+
link: Link
|
| 18 |
+
|
| 19 |
+
def __init__(self, name: str, version: str, link: Link) -> None:
|
| 20 |
+
object.__setattr__(self, "name", name)
|
| 21 |
+
object.__setattr__(self, "version", parse_version(version))
|
| 22 |
+
object.__setattr__(self, "link", link)
|
| 23 |
+
|
| 24 |
+
def __str__(self) -> str:
|
| 25 |
+
return f"{self.name!r} candidate (version {self.version} at {self.link})"
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/direct_url.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" PEP 610 """
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
import urllib.parse
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import Any, ClassVar, Dict, Iterable, Optional, Type, TypeVar, Union
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"DirectUrl",
|
| 11 |
+
"DirectUrlValidationError",
|
| 12 |
+
"DirInfo",
|
| 13 |
+
"ArchiveInfo",
|
| 14 |
+
"VcsInfo",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
T = TypeVar("T")
|
| 18 |
+
|
| 19 |
+
DIRECT_URL_METADATA_NAME = "direct_url.json"
|
| 20 |
+
ENV_VAR_RE = re.compile(r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class DirectUrlValidationError(Exception):
|
| 24 |
+
pass
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _get(
|
| 28 |
+
d: Dict[str, Any], expected_type: Type[T], key: str, default: Optional[T] = None
|
| 29 |
+
) -> Optional[T]:
|
| 30 |
+
"""Get value from dictionary and verify expected type."""
|
| 31 |
+
if key not in d:
|
| 32 |
+
return default
|
| 33 |
+
value = d[key]
|
| 34 |
+
if not isinstance(value, expected_type):
|
| 35 |
+
raise DirectUrlValidationError(
|
| 36 |
+
f"{value!r} has unexpected type for {key} (expected {expected_type})"
|
| 37 |
+
)
|
| 38 |
+
return value
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _get_required(
|
| 42 |
+
d: Dict[str, Any], expected_type: Type[T], key: str, default: Optional[T] = None
|
| 43 |
+
) -> T:
|
| 44 |
+
value = _get(d, expected_type, key, default)
|
| 45 |
+
if value is None:
|
| 46 |
+
raise DirectUrlValidationError(f"{key} must have a value")
|
| 47 |
+
return value
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _exactly_one_of(infos: Iterable[Optional["InfoType"]]) -> "InfoType":
|
| 51 |
+
infos = [info for info in infos if info is not None]
|
| 52 |
+
if not infos:
|
| 53 |
+
raise DirectUrlValidationError(
|
| 54 |
+
"missing one of archive_info, dir_info, vcs_info"
|
| 55 |
+
)
|
| 56 |
+
if len(infos) > 1:
|
| 57 |
+
raise DirectUrlValidationError(
|
| 58 |
+
"more than one of archive_info, dir_info, vcs_info"
|
| 59 |
+
)
|
| 60 |
+
assert infos[0] is not None
|
| 61 |
+
return infos[0]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _filter_none(**kwargs: Any) -> Dict[str, Any]:
|
| 65 |
+
"""Make dict excluding None values."""
|
| 66 |
+
return {k: v for k, v in kwargs.items() if v is not None}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@dataclass
|
| 70 |
+
class VcsInfo:
|
| 71 |
+
name: ClassVar = "vcs_info"
|
| 72 |
+
|
| 73 |
+
vcs: str
|
| 74 |
+
commit_id: str
|
| 75 |
+
requested_revision: Optional[str] = None
|
| 76 |
+
|
| 77 |
+
@classmethod
|
| 78 |
+
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["VcsInfo"]:
|
| 79 |
+
if d is None:
|
| 80 |
+
return None
|
| 81 |
+
return cls(
|
| 82 |
+
vcs=_get_required(d, str, "vcs"),
|
| 83 |
+
commit_id=_get_required(d, str, "commit_id"),
|
| 84 |
+
requested_revision=_get(d, str, "requested_revision"),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
def _to_dict(self) -> Dict[str, Any]:
|
| 88 |
+
return _filter_none(
|
| 89 |
+
vcs=self.vcs,
|
| 90 |
+
requested_revision=self.requested_revision,
|
| 91 |
+
commit_id=self.commit_id,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class ArchiveInfo:
|
| 96 |
+
name = "archive_info"
|
| 97 |
+
|
| 98 |
+
def __init__(
|
| 99 |
+
self,
|
| 100 |
+
hash: Optional[str] = None,
|
| 101 |
+
hashes: Optional[Dict[str, str]] = None,
|
| 102 |
+
) -> None:
|
| 103 |
+
# set hashes before hash, since the hash setter will further populate hashes
|
| 104 |
+
self.hashes = hashes
|
| 105 |
+
self.hash = hash
|
| 106 |
+
|
| 107 |
+
@property
|
| 108 |
+
def hash(self) -> Optional[str]:
|
| 109 |
+
return self._hash
|
| 110 |
+
|
| 111 |
+
@hash.setter
|
| 112 |
+
def hash(self, value: Optional[str]) -> None:
|
| 113 |
+
if value is not None:
|
| 114 |
+
# Auto-populate the hashes key to upgrade to the new format automatically.
|
| 115 |
+
# We don't back-populate the legacy hash key from hashes.
|
| 116 |
+
try:
|
| 117 |
+
hash_name, hash_value = value.split("=", 1)
|
| 118 |
+
except ValueError:
|
| 119 |
+
raise DirectUrlValidationError(
|
| 120 |
+
f"invalid archive_info.hash format: {value!r}"
|
| 121 |
+
)
|
| 122 |
+
if self.hashes is None:
|
| 123 |
+
self.hashes = {hash_name: hash_value}
|
| 124 |
+
elif hash_name not in self.hashes:
|
| 125 |
+
self.hashes = self.hashes.copy()
|
| 126 |
+
self.hashes[hash_name] = hash_value
|
| 127 |
+
self._hash = value
|
| 128 |
+
|
| 129 |
+
@classmethod
|
| 130 |
+
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["ArchiveInfo"]:
|
| 131 |
+
if d is None:
|
| 132 |
+
return None
|
| 133 |
+
return cls(hash=_get(d, str, "hash"), hashes=_get(d, dict, "hashes"))
|
| 134 |
+
|
| 135 |
+
def _to_dict(self) -> Dict[str, Any]:
|
| 136 |
+
return _filter_none(hash=self.hash, hashes=self.hashes)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@dataclass
|
| 140 |
+
class DirInfo:
|
| 141 |
+
name: ClassVar = "dir_info"
|
| 142 |
+
|
| 143 |
+
editable: bool = False
|
| 144 |
+
|
| 145 |
+
@classmethod
|
| 146 |
+
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["DirInfo"]:
|
| 147 |
+
if d is None:
|
| 148 |
+
return None
|
| 149 |
+
return cls(editable=_get_required(d, bool, "editable", default=False))
|
| 150 |
+
|
| 151 |
+
def _to_dict(self) -> Dict[str, Any]:
|
| 152 |
+
return _filter_none(editable=self.editable or None)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
InfoType = Union[ArchiveInfo, DirInfo, VcsInfo]
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
@dataclass
|
| 159 |
+
class DirectUrl:
|
| 160 |
+
url: str
|
| 161 |
+
info: InfoType
|
| 162 |
+
subdirectory: Optional[str] = None
|
| 163 |
+
|
| 164 |
+
def _remove_auth_from_netloc(self, netloc: str) -> str:
|
| 165 |
+
if "@" not in netloc:
|
| 166 |
+
return netloc
|
| 167 |
+
user_pass, netloc_no_user_pass = netloc.split("@", 1)
|
| 168 |
+
if (
|
| 169 |
+
isinstance(self.info, VcsInfo)
|
| 170 |
+
and self.info.vcs == "git"
|
| 171 |
+
and user_pass == "git"
|
| 172 |
+
):
|
| 173 |
+
return netloc
|
| 174 |
+
if ENV_VAR_RE.match(user_pass):
|
| 175 |
+
return netloc
|
| 176 |
+
return netloc_no_user_pass
|
| 177 |
+
|
| 178 |
+
@property
|
| 179 |
+
def redacted_url(self) -> str:
|
| 180 |
+
"""url with user:password part removed unless it is formed with
|
| 181 |
+
environment variables as specified in PEP 610, or it is ``git``
|
| 182 |
+
in the case of a git URL.
|
| 183 |
+
"""
|
| 184 |
+
purl = urllib.parse.urlsplit(self.url)
|
| 185 |
+
netloc = self._remove_auth_from_netloc(purl.netloc)
|
| 186 |
+
surl = urllib.parse.urlunsplit(
|
| 187 |
+
(purl.scheme, netloc, purl.path, purl.query, purl.fragment)
|
| 188 |
+
)
|
| 189 |
+
return surl
|
| 190 |
+
|
| 191 |
+
def validate(self) -> None:
|
| 192 |
+
self.from_dict(self.to_dict())
|
| 193 |
+
|
| 194 |
+
@classmethod
|
| 195 |
+
def from_dict(cls, d: Dict[str, Any]) -> "DirectUrl":
|
| 196 |
+
return DirectUrl(
|
| 197 |
+
url=_get_required(d, str, "url"),
|
| 198 |
+
subdirectory=_get(d, str, "subdirectory"),
|
| 199 |
+
info=_exactly_one_of(
|
| 200 |
+
[
|
| 201 |
+
ArchiveInfo._from_dict(_get(d, dict, "archive_info")),
|
| 202 |
+
DirInfo._from_dict(_get(d, dict, "dir_info")),
|
| 203 |
+
VcsInfo._from_dict(_get(d, dict, "vcs_info")),
|
| 204 |
+
]
|
| 205 |
+
),
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 209 |
+
res = _filter_none(
|
| 210 |
+
url=self.redacted_url,
|
| 211 |
+
subdirectory=self.subdirectory,
|
| 212 |
+
)
|
| 213 |
+
res[self.info.name] = self.info._to_dict()
|
| 214 |
+
return res
|
| 215 |
+
|
| 216 |
+
@classmethod
|
| 217 |
+
def from_json(cls, s: str) -> "DirectUrl":
|
| 218 |
+
return cls.from_dict(json.loads(s))
|
| 219 |
+
|
| 220 |
+
def to_json(self) -> str:
|
| 221 |
+
return json.dumps(self.to_dict(), sort_keys=True)
|
| 222 |
+
|
| 223 |
+
def is_local_editable(self) -> bool:
|
| 224 |
+
return isinstance(self.info, DirInfo) and self.info.editable
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/format_control.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import FrozenSet, Optional, Set
|
| 2 |
+
|
| 3 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 4 |
+
|
| 5 |
+
from pip._internal.exceptions import CommandError
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class FormatControl:
|
| 9 |
+
"""Helper for managing formats from which a package can be installed."""
|
| 10 |
+
|
| 11 |
+
__slots__ = ["no_binary", "only_binary"]
|
| 12 |
+
|
| 13 |
+
def __init__(
|
| 14 |
+
self,
|
| 15 |
+
no_binary: Optional[Set[str]] = None,
|
| 16 |
+
only_binary: Optional[Set[str]] = None,
|
| 17 |
+
) -> None:
|
| 18 |
+
if no_binary is None:
|
| 19 |
+
no_binary = set()
|
| 20 |
+
if only_binary is None:
|
| 21 |
+
only_binary = set()
|
| 22 |
+
|
| 23 |
+
self.no_binary = no_binary
|
| 24 |
+
self.only_binary = only_binary
|
| 25 |
+
|
| 26 |
+
def __eq__(self, other: object) -> bool:
|
| 27 |
+
if not isinstance(other, self.__class__):
|
| 28 |
+
return NotImplemented
|
| 29 |
+
|
| 30 |
+
if self.__slots__ != other.__slots__:
|
| 31 |
+
return False
|
| 32 |
+
|
| 33 |
+
return all(getattr(self, k) == getattr(other, k) for k in self.__slots__)
|
| 34 |
+
|
| 35 |
+
def __repr__(self) -> str:
|
| 36 |
+
return f"{self.__class__.__name__}({self.no_binary}, {self.only_binary})"
|
| 37 |
+
|
| 38 |
+
@staticmethod
|
| 39 |
+
def handle_mutual_excludes(value: str, target: Set[str], other: Set[str]) -> None:
|
| 40 |
+
if value.startswith("-"):
|
| 41 |
+
raise CommandError(
|
| 42 |
+
"--no-binary / --only-binary option requires 1 argument."
|
| 43 |
+
)
|
| 44 |
+
new = value.split(",")
|
| 45 |
+
while ":all:" in new:
|
| 46 |
+
other.clear()
|
| 47 |
+
target.clear()
|
| 48 |
+
target.add(":all:")
|
| 49 |
+
del new[: new.index(":all:") + 1]
|
| 50 |
+
# Without a none, we want to discard everything as :all: covers it
|
| 51 |
+
if ":none:" not in new:
|
| 52 |
+
return
|
| 53 |
+
for name in new:
|
| 54 |
+
if name == ":none:":
|
| 55 |
+
target.clear()
|
| 56 |
+
continue
|
| 57 |
+
name = canonicalize_name(name)
|
| 58 |
+
other.discard(name)
|
| 59 |
+
target.add(name)
|
| 60 |
+
|
| 61 |
+
def get_allowed_formats(self, canonical_name: str) -> FrozenSet[str]:
|
| 62 |
+
result = {"binary", "source"}
|
| 63 |
+
if canonical_name in self.only_binary:
|
| 64 |
+
result.discard("source")
|
| 65 |
+
elif canonical_name in self.no_binary:
|
| 66 |
+
result.discard("binary")
|
| 67 |
+
elif ":all:" in self.only_binary:
|
| 68 |
+
result.discard("source")
|
| 69 |
+
elif ":all:" in self.no_binary:
|
| 70 |
+
result.discard("binary")
|
| 71 |
+
return frozenset(result)
|
| 72 |
+
|
| 73 |
+
def disallow_binaries(self) -> None:
|
| 74 |
+
self.handle_mutual_excludes(
|
| 75 |
+
":all:",
|
| 76 |
+
self.no_binary,
|
| 77 |
+
self.only_binary,
|
| 78 |
+
)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/index.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import urllib.parse
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class PackageIndex:
|
| 5 |
+
"""Represents a Package Index and provides easier access to endpoints"""
|
| 6 |
+
|
| 7 |
+
__slots__ = ["url", "netloc", "simple_url", "pypi_url", "file_storage_domain"]
|
| 8 |
+
|
| 9 |
+
def __init__(self, url: str, file_storage_domain: str) -> None:
|
| 10 |
+
super().__init__()
|
| 11 |
+
self.url = url
|
| 12 |
+
self.netloc = urllib.parse.urlsplit(url).netloc
|
| 13 |
+
self.simple_url = self._url_for_path("simple")
|
| 14 |
+
self.pypi_url = self._url_for_path("pypi")
|
| 15 |
+
|
| 16 |
+
# This is part of a temporary hack used to block installs of PyPI
|
| 17 |
+
# packages which depend on external urls only necessary until PyPI can
|
| 18 |
+
# block such packages themselves
|
| 19 |
+
self.file_storage_domain = file_storage_domain
|
| 20 |
+
|
| 21 |
+
def _url_for_path(self, path: str) -> str:
|
| 22 |
+
return urllib.parse.urljoin(self.url, path)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
PyPI = PackageIndex("https://pypi.org/", file_storage_domain="files.pythonhosted.org")
|
| 26 |
+
TestPyPI = PackageIndex(
|
| 27 |
+
"https://test.pypi.org/", file_storage_domain="test-files.pythonhosted.org"
|
| 28 |
+
)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/installation_report.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, Sequence
|
| 2 |
+
|
| 3 |
+
from pip._vendor.packaging.markers import default_environment
|
| 4 |
+
|
| 5 |
+
from pip import __version__
|
| 6 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class InstallationReport:
|
| 10 |
+
def __init__(self, install_requirements: Sequence[InstallRequirement]):
|
| 11 |
+
self._install_requirements = install_requirements
|
| 12 |
+
|
| 13 |
+
@classmethod
|
| 14 |
+
def _install_req_to_dict(cls, ireq: InstallRequirement) -> Dict[str, Any]:
|
| 15 |
+
assert ireq.download_info, f"No download_info for {ireq}"
|
| 16 |
+
res = {
|
| 17 |
+
# PEP 610 json for the download URL. download_info.archive_info.hashes may
|
| 18 |
+
# be absent when the requirement was installed from the wheel cache
|
| 19 |
+
# and the cache entry was populated by an older pip version that did not
|
| 20 |
+
# record origin.json.
|
| 21 |
+
"download_info": ireq.download_info.to_dict(),
|
| 22 |
+
# is_direct is true if the requirement was a direct URL reference (which
|
| 23 |
+
# includes editable requirements), and false if the requirement was
|
| 24 |
+
# downloaded from a PEP 503 index or --find-links.
|
| 25 |
+
"is_direct": ireq.is_direct,
|
| 26 |
+
# is_yanked is true if the requirement was yanked from the index, but
|
| 27 |
+
# was still selected by pip to conform to PEP 592.
|
| 28 |
+
"is_yanked": ireq.link.is_yanked if ireq.link else False,
|
| 29 |
+
# requested is true if the requirement was specified by the user (aka
|
| 30 |
+
# top level requirement), and false if it was installed as a dependency of a
|
| 31 |
+
# requirement. https://peps.python.org/pep-0376/#requested
|
| 32 |
+
"requested": ireq.user_supplied,
|
| 33 |
+
# PEP 566 json encoding for metadata
|
| 34 |
+
# https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata
|
| 35 |
+
"metadata": ireq.get_dist().metadata_dict,
|
| 36 |
+
}
|
| 37 |
+
if ireq.user_supplied and ireq.extras:
|
| 38 |
+
# For top level requirements, the list of requested extras, if any.
|
| 39 |
+
res["requested_extras"] = sorted(ireq.extras)
|
| 40 |
+
return res
|
| 41 |
+
|
| 42 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 43 |
+
return {
|
| 44 |
+
"version": "1",
|
| 45 |
+
"pip_version": __version__,
|
| 46 |
+
"install": [
|
| 47 |
+
self._install_req_to_dict(ireq) for ireq in self._install_requirements
|
| 48 |
+
],
|
| 49 |
+
# https://peps.python.org/pep-0508/#environment-markers
|
| 50 |
+
# TODO: currently, the resolver uses the default environment to evaluate
|
| 51 |
+
# environment markers, so that is what we report here. In the future, it
|
| 52 |
+
# should also take into account options such as --python-version or
|
| 53 |
+
# --platform, perhaps under the form of an environment_override field?
|
| 54 |
+
# https://github.com/pypa/pip/issues/11198
|
| 55 |
+
"environment": default_environment(),
|
| 56 |
+
}
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/link.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import itertools
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import posixpath
|
| 6 |
+
import re
|
| 7 |
+
import urllib.parse
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from typing import (
|
| 10 |
+
TYPE_CHECKING,
|
| 11 |
+
Any,
|
| 12 |
+
Dict,
|
| 13 |
+
List,
|
| 14 |
+
Mapping,
|
| 15 |
+
NamedTuple,
|
| 16 |
+
Optional,
|
| 17 |
+
Tuple,
|
| 18 |
+
Union,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
from pip._internal.utils.deprecation import deprecated
|
| 22 |
+
from pip._internal.utils.filetypes import WHEEL_EXTENSION
|
| 23 |
+
from pip._internal.utils.hashes import Hashes
|
| 24 |
+
from pip._internal.utils.misc import (
|
| 25 |
+
pairwise,
|
| 26 |
+
redact_auth_from_url,
|
| 27 |
+
split_auth_from_netloc,
|
| 28 |
+
splitext,
|
| 29 |
+
)
|
| 30 |
+
from pip._internal.utils.urls import path_to_url, url_to_path
|
| 31 |
+
|
| 32 |
+
if TYPE_CHECKING:
|
| 33 |
+
from pip._internal.index.collector import IndexContent
|
| 34 |
+
|
| 35 |
+
logger = logging.getLogger(__name__)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# Order matters, earlier hashes have a precedence over later hashes for what
|
| 39 |
+
# we will pick to use.
|
| 40 |
+
_SUPPORTED_HASHES = ("sha512", "sha384", "sha256", "sha224", "sha1", "md5")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@dataclass(frozen=True)
|
| 44 |
+
class LinkHash:
|
| 45 |
+
"""Links to content may have embedded hash values. This class parses those.
|
| 46 |
+
|
| 47 |
+
`name` must be any member of `_SUPPORTED_HASHES`.
|
| 48 |
+
|
| 49 |
+
This class can be converted to and from `ArchiveInfo`. While ArchiveInfo intends to
|
| 50 |
+
be JSON-serializable to conform to PEP 610, this class contains the logic for
|
| 51 |
+
parsing a hash name and value for correctness, and then checking whether that hash
|
| 52 |
+
conforms to a schema with `.is_hash_allowed()`."""
|
| 53 |
+
|
| 54 |
+
name: str
|
| 55 |
+
value: str
|
| 56 |
+
|
| 57 |
+
_hash_url_fragment_re = re.compile(
|
| 58 |
+
# NB: we do not validate that the second group (.*) is a valid hex
|
| 59 |
+
# digest. Instead, we simply keep that string in this class, and then check it
|
| 60 |
+
# against Hashes when hash-checking is needed. This is easier to debug than
|
| 61 |
+
# proactively discarding an invalid hex digest, as we handle incorrect hashes
|
| 62 |
+
# and malformed hashes in the same place.
|
| 63 |
+
r"[#&]({choices})=([^&]*)".format(
|
| 64 |
+
choices="|".join(re.escape(hash_name) for hash_name in _SUPPORTED_HASHES)
|
| 65 |
+
),
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
def __post_init__(self) -> None:
|
| 69 |
+
assert self.name in _SUPPORTED_HASHES
|
| 70 |
+
|
| 71 |
+
@classmethod
|
| 72 |
+
@functools.lru_cache(maxsize=None)
|
| 73 |
+
def find_hash_url_fragment(cls, url: str) -> Optional["LinkHash"]:
|
| 74 |
+
"""Search a string for a checksum algorithm name and encoded output value."""
|
| 75 |
+
match = cls._hash_url_fragment_re.search(url)
|
| 76 |
+
if match is None:
|
| 77 |
+
return None
|
| 78 |
+
name, value = match.groups()
|
| 79 |
+
return cls(name=name, value=value)
|
| 80 |
+
|
| 81 |
+
def as_dict(self) -> Dict[str, str]:
|
| 82 |
+
return {self.name: self.value}
|
| 83 |
+
|
| 84 |
+
def as_hashes(self) -> Hashes:
|
| 85 |
+
"""Return a Hashes instance which checks only for the current hash."""
|
| 86 |
+
return Hashes({self.name: [self.value]})
|
| 87 |
+
|
| 88 |
+
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
|
| 89 |
+
"""
|
| 90 |
+
Return True if the current hash is allowed by `hashes`.
|
| 91 |
+
"""
|
| 92 |
+
if hashes is None:
|
| 93 |
+
return False
|
| 94 |
+
return hashes.is_hash_allowed(self.name, hex_digest=self.value)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@dataclass(frozen=True)
|
| 98 |
+
class MetadataFile:
|
| 99 |
+
"""Information about a core metadata file associated with a distribution."""
|
| 100 |
+
|
| 101 |
+
hashes: Optional[Dict[str, str]]
|
| 102 |
+
|
| 103 |
+
def __post_init__(self) -> None:
|
| 104 |
+
if self.hashes is not None:
|
| 105 |
+
assert all(name in _SUPPORTED_HASHES for name in self.hashes)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def supported_hashes(hashes: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
|
| 109 |
+
# Remove any unsupported hash types from the mapping. If this leaves no
|
| 110 |
+
# supported hashes, return None
|
| 111 |
+
if hashes is None:
|
| 112 |
+
return None
|
| 113 |
+
hashes = {n: v for n, v in hashes.items() if n in _SUPPORTED_HASHES}
|
| 114 |
+
if not hashes:
|
| 115 |
+
return None
|
| 116 |
+
return hashes
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _clean_url_path_part(part: str) -> str:
|
| 120 |
+
"""
|
| 121 |
+
Clean a "part" of a URL path (i.e. after splitting on "@" characters).
|
| 122 |
+
"""
|
| 123 |
+
# We unquote prior to quoting to make sure nothing is double quoted.
|
| 124 |
+
return urllib.parse.quote(urllib.parse.unquote(part))
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _clean_file_url_path(part: str) -> str:
|
| 128 |
+
"""
|
| 129 |
+
Clean the first part of a URL path that corresponds to a local
|
| 130 |
+
filesystem path (i.e. the first part after splitting on "@" characters).
|
| 131 |
+
"""
|
| 132 |
+
# We unquote prior to quoting to make sure nothing is double quoted.
|
| 133 |
+
# Also, on Windows the path part might contain a drive letter which
|
| 134 |
+
# should not be quoted. On Linux where drive letters do not
|
| 135 |
+
# exist, the colon should be quoted. We rely on urllib.request
|
| 136 |
+
# to do the right thing here.
|
| 137 |
+
return urllib.request.pathname2url(urllib.request.url2pathname(part))
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# percent-encoded: /
|
| 141 |
+
_reserved_chars_re = re.compile("(@|%2F)", re.IGNORECASE)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _clean_url_path(path: str, is_local_path: bool) -> str:
|
| 145 |
+
"""
|
| 146 |
+
Clean the path portion of a URL.
|
| 147 |
+
"""
|
| 148 |
+
if is_local_path:
|
| 149 |
+
clean_func = _clean_file_url_path
|
| 150 |
+
else:
|
| 151 |
+
clean_func = _clean_url_path_part
|
| 152 |
+
|
| 153 |
+
# Split on the reserved characters prior to cleaning so that
|
| 154 |
+
# revision strings in VCS URLs are properly preserved.
|
| 155 |
+
parts = _reserved_chars_re.split(path)
|
| 156 |
+
|
| 157 |
+
cleaned_parts = []
|
| 158 |
+
for to_clean, reserved in pairwise(itertools.chain(parts, [""])):
|
| 159 |
+
cleaned_parts.append(clean_func(to_clean))
|
| 160 |
+
# Normalize %xx escapes (e.g. %2f -> %2F)
|
| 161 |
+
cleaned_parts.append(reserved.upper())
|
| 162 |
+
|
| 163 |
+
return "".join(cleaned_parts)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _ensure_quoted_url(url: str) -> str:
|
| 167 |
+
"""
|
| 168 |
+
Make sure a link is fully quoted.
|
| 169 |
+
For example, if ' ' occurs in the URL, it will be replaced with "%20",
|
| 170 |
+
and without double-quoting other characters.
|
| 171 |
+
"""
|
| 172 |
+
# Split the URL into parts according to the general structure
|
| 173 |
+
# `scheme://netloc/path?query#fragment`.
|
| 174 |
+
result = urllib.parse.urlsplit(url)
|
| 175 |
+
# If the netloc is empty, then the URL refers to a local filesystem path.
|
| 176 |
+
is_local_path = not result.netloc
|
| 177 |
+
path = _clean_url_path(result.path, is_local_path=is_local_path)
|
| 178 |
+
return urllib.parse.urlunsplit(result._replace(path=path))
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _absolute_link_url(base_url: str, url: str) -> str:
|
| 182 |
+
"""
|
| 183 |
+
A faster implementation of urllib.parse.urljoin with a shortcut
|
| 184 |
+
for absolute http/https URLs.
|
| 185 |
+
"""
|
| 186 |
+
if url.startswith(("https://", "http://")):
|
| 187 |
+
return url
|
| 188 |
+
else:
|
| 189 |
+
return urllib.parse.urljoin(base_url, url)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
@functools.total_ordering
|
| 193 |
+
class Link:
|
| 194 |
+
"""Represents a parsed link from a Package Index's simple URL"""
|
| 195 |
+
|
| 196 |
+
__slots__ = [
|
| 197 |
+
"_parsed_url",
|
| 198 |
+
"_url",
|
| 199 |
+
"_path",
|
| 200 |
+
"_hashes",
|
| 201 |
+
"comes_from",
|
| 202 |
+
"requires_python",
|
| 203 |
+
"yanked_reason",
|
| 204 |
+
"metadata_file_data",
|
| 205 |
+
"cache_link_parsing",
|
| 206 |
+
"egg_fragment",
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
def __init__(
|
| 210 |
+
self,
|
| 211 |
+
url: str,
|
| 212 |
+
comes_from: Optional[Union[str, "IndexContent"]] = None,
|
| 213 |
+
requires_python: Optional[str] = None,
|
| 214 |
+
yanked_reason: Optional[str] = None,
|
| 215 |
+
metadata_file_data: Optional[MetadataFile] = None,
|
| 216 |
+
cache_link_parsing: bool = True,
|
| 217 |
+
hashes: Optional[Mapping[str, str]] = None,
|
| 218 |
+
) -> None:
|
| 219 |
+
"""
|
| 220 |
+
:param url: url of the resource pointed to (href of the link)
|
| 221 |
+
:param comes_from: instance of IndexContent where the link was found,
|
| 222 |
+
or string.
|
| 223 |
+
:param requires_python: String containing the `Requires-Python`
|
| 224 |
+
metadata field, specified in PEP 345. This may be specified by
|
| 225 |
+
a data-requires-python attribute in the HTML link tag, as
|
| 226 |
+
described in PEP 503.
|
| 227 |
+
:param yanked_reason: the reason the file has been yanked, if the
|
| 228 |
+
file has been yanked, or None if the file hasn't been yanked.
|
| 229 |
+
This is the value of the "data-yanked" attribute, if present, in
|
| 230 |
+
a simple repository HTML link. If the file has been yanked but
|
| 231 |
+
no reason was provided, this should be the empty string. See
|
| 232 |
+
PEP 592 for more information and the specification.
|
| 233 |
+
:param metadata_file_data: the metadata attached to the file, or None if
|
| 234 |
+
no such metadata is provided. This argument, if not None, indicates
|
| 235 |
+
that a separate metadata file exists, and also optionally supplies
|
| 236 |
+
hashes for that file.
|
| 237 |
+
:param cache_link_parsing: A flag that is used elsewhere to determine
|
| 238 |
+
whether resources retrieved from this link should be cached. PyPI
|
| 239 |
+
URLs should generally have this set to False, for example.
|
| 240 |
+
:param hashes: A mapping of hash names to digests to allow us to
|
| 241 |
+
determine the validity of a download.
|
| 242 |
+
"""
|
| 243 |
+
|
| 244 |
+
# The comes_from, requires_python, and metadata_file_data arguments are
|
| 245 |
+
# only used by classmethods of this class, and are not used in client
|
| 246 |
+
# code directly.
|
| 247 |
+
|
| 248 |
+
# url can be a UNC windows share
|
| 249 |
+
if url.startswith("\\\\"):
|
| 250 |
+
url = path_to_url(url)
|
| 251 |
+
|
| 252 |
+
self._parsed_url = urllib.parse.urlsplit(url)
|
| 253 |
+
# Store the url as a private attribute to prevent accidentally
|
| 254 |
+
# trying to set a new value.
|
| 255 |
+
self._url = url
|
| 256 |
+
# The .path property is hot, so calculate its value ahead of time.
|
| 257 |
+
self._path = urllib.parse.unquote(self._parsed_url.path)
|
| 258 |
+
|
| 259 |
+
link_hash = LinkHash.find_hash_url_fragment(url)
|
| 260 |
+
hashes_from_link = {} if link_hash is None else link_hash.as_dict()
|
| 261 |
+
if hashes is None:
|
| 262 |
+
self._hashes = hashes_from_link
|
| 263 |
+
else:
|
| 264 |
+
self._hashes = {**hashes, **hashes_from_link}
|
| 265 |
+
|
| 266 |
+
self.comes_from = comes_from
|
| 267 |
+
self.requires_python = requires_python if requires_python else None
|
| 268 |
+
self.yanked_reason = yanked_reason
|
| 269 |
+
self.metadata_file_data = metadata_file_data
|
| 270 |
+
|
| 271 |
+
self.cache_link_parsing = cache_link_parsing
|
| 272 |
+
self.egg_fragment = self._egg_fragment()
|
| 273 |
+
|
| 274 |
+
@classmethod
|
| 275 |
+
def from_json(
|
| 276 |
+
cls,
|
| 277 |
+
file_data: Dict[str, Any],
|
| 278 |
+
page_url: str,
|
| 279 |
+
) -> Optional["Link"]:
|
| 280 |
+
"""
|
| 281 |
+
Convert an pypi json document from a simple repository page into a Link.
|
| 282 |
+
"""
|
| 283 |
+
file_url = file_data.get("url")
|
| 284 |
+
if file_url is None:
|
| 285 |
+
return None
|
| 286 |
+
|
| 287 |
+
url = _ensure_quoted_url(_absolute_link_url(page_url, file_url))
|
| 288 |
+
pyrequire = file_data.get("requires-python")
|
| 289 |
+
yanked_reason = file_data.get("yanked")
|
| 290 |
+
hashes = file_data.get("hashes", {})
|
| 291 |
+
|
| 292 |
+
# PEP 714: Indexes must use the name core-metadata, but
|
| 293 |
+
# clients should support the old name as a fallback for compatibility.
|
| 294 |
+
metadata_info = file_data.get("core-metadata")
|
| 295 |
+
if metadata_info is None:
|
| 296 |
+
metadata_info = file_data.get("dist-info-metadata")
|
| 297 |
+
|
| 298 |
+
# The metadata info value may be a boolean, or a dict of hashes.
|
| 299 |
+
if isinstance(metadata_info, dict):
|
| 300 |
+
# The file exists, and hashes have been supplied
|
| 301 |
+
metadata_file_data = MetadataFile(supported_hashes(metadata_info))
|
| 302 |
+
elif metadata_info:
|
| 303 |
+
# The file exists, but there are no hashes
|
| 304 |
+
metadata_file_data = MetadataFile(None)
|
| 305 |
+
else:
|
| 306 |
+
# False or not present: the file does not exist
|
| 307 |
+
metadata_file_data = None
|
| 308 |
+
|
| 309 |
+
# The Link.yanked_reason expects an empty string instead of a boolean.
|
| 310 |
+
if yanked_reason and not isinstance(yanked_reason, str):
|
| 311 |
+
yanked_reason = ""
|
| 312 |
+
# The Link.yanked_reason expects None instead of False.
|
| 313 |
+
elif not yanked_reason:
|
| 314 |
+
yanked_reason = None
|
| 315 |
+
|
| 316 |
+
return cls(
|
| 317 |
+
url,
|
| 318 |
+
comes_from=page_url,
|
| 319 |
+
requires_python=pyrequire,
|
| 320 |
+
yanked_reason=yanked_reason,
|
| 321 |
+
hashes=hashes,
|
| 322 |
+
metadata_file_data=metadata_file_data,
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
@classmethod
|
| 326 |
+
def from_element(
|
| 327 |
+
cls,
|
| 328 |
+
anchor_attribs: Dict[str, Optional[str]],
|
| 329 |
+
page_url: str,
|
| 330 |
+
base_url: str,
|
| 331 |
+
) -> Optional["Link"]:
|
| 332 |
+
"""
|
| 333 |
+
Convert an anchor element's attributes in a simple repository page to a Link.
|
| 334 |
+
"""
|
| 335 |
+
href = anchor_attribs.get("href")
|
| 336 |
+
if not href:
|
| 337 |
+
return None
|
| 338 |
+
|
| 339 |
+
url = _ensure_quoted_url(_absolute_link_url(base_url, href))
|
| 340 |
+
pyrequire = anchor_attribs.get("data-requires-python")
|
| 341 |
+
yanked_reason = anchor_attribs.get("data-yanked")
|
| 342 |
+
|
| 343 |
+
# PEP 714: Indexes must use the name data-core-metadata, but
|
| 344 |
+
# clients should support the old name as a fallback for compatibility.
|
| 345 |
+
metadata_info = anchor_attribs.get("data-core-metadata")
|
| 346 |
+
if metadata_info is None:
|
| 347 |
+
metadata_info = anchor_attribs.get("data-dist-info-metadata")
|
| 348 |
+
# The metadata info value may be the string "true", or a string of
|
| 349 |
+
# the form "hashname=hashval"
|
| 350 |
+
if metadata_info == "true":
|
| 351 |
+
# The file exists, but there are no hashes
|
| 352 |
+
metadata_file_data = MetadataFile(None)
|
| 353 |
+
elif metadata_info is None:
|
| 354 |
+
# The file does not exist
|
| 355 |
+
metadata_file_data = None
|
| 356 |
+
else:
|
| 357 |
+
# The file exists, and hashes have been supplied
|
| 358 |
+
hashname, sep, hashval = metadata_info.partition("=")
|
| 359 |
+
if sep == "=":
|
| 360 |
+
metadata_file_data = MetadataFile(supported_hashes({hashname: hashval}))
|
| 361 |
+
else:
|
| 362 |
+
# Error - data is wrong. Treat as no hashes supplied.
|
| 363 |
+
logger.debug(
|
| 364 |
+
"Index returned invalid data-dist-info-metadata value: %s",
|
| 365 |
+
metadata_info,
|
| 366 |
+
)
|
| 367 |
+
metadata_file_data = MetadataFile(None)
|
| 368 |
+
|
| 369 |
+
return cls(
|
| 370 |
+
url,
|
| 371 |
+
comes_from=page_url,
|
| 372 |
+
requires_python=pyrequire,
|
| 373 |
+
yanked_reason=yanked_reason,
|
| 374 |
+
metadata_file_data=metadata_file_data,
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
def __str__(self) -> str:
|
| 378 |
+
if self.requires_python:
|
| 379 |
+
rp = f" (requires-python:{self.requires_python})"
|
| 380 |
+
else:
|
| 381 |
+
rp = ""
|
| 382 |
+
if self.comes_from:
|
| 383 |
+
return f"{redact_auth_from_url(self._url)} (from {self.comes_from}){rp}"
|
| 384 |
+
else:
|
| 385 |
+
return redact_auth_from_url(str(self._url))
|
| 386 |
+
|
| 387 |
+
def __repr__(self) -> str:
|
| 388 |
+
return f"<Link {self}>"
|
| 389 |
+
|
| 390 |
+
def __hash__(self) -> int:
|
| 391 |
+
return hash(self.url)
|
| 392 |
+
|
| 393 |
+
def __eq__(self, other: Any) -> bool:
|
| 394 |
+
if not isinstance(other, Link):
|
| 395 |
+
return NotImplemented
|
| 396 |
+
return self.url == other.url
|
| 397 |
+
|
| 398 |
+
def __lt__(self, other: Any) -> bool:
|
| 399 |
+
if not isinstance(other, Link):
|
| 400 |
+
return NotImplemented
|
| 401 |
+
return self.url < other.url
|
| 402 |
+
|
| 403 |
+
@property
|
| 404 |
+
def url(self) -> str:
|
| 405 |
+
return self._url
|
| 406 |
+
|
| 407 |
+
@property
|
| 408 |
+
def filename(self) -> str:
|
| 409 |
+
path = self.path.rstrip("/")
|
| 410 |
+
name = posixpath.basename(path)
|
| 411 |
+
if not name:
|
| 412 |
+
# Make sure we don't leak auth information if the netloc
|
| 413 |
+
# includes a username and password.
|
| 414 |
+
netloc, user_pass = split_auth_from_netloc(self.netloc)
|
| 415 |
+
return netloc
|
| 416 |
+
|
| 417 |
+
name = urllib.parse.unquote(name)
|
| 418 |
+
assert name, f"URL {self._url!r} produced no filename"
|
| 419 |
+
return name
|
| 420 |
+
|
| 421 |
+
@property
|
| 422 |
+
def file_path(self) -> str:
|
| 423 |
+
return url_to_path(self.url)
|
| 424 |
+
|
| 425 |
+
@property
|
| 426 |
+
def scheme(self) -> str:
|
| 427 |
+
return self._parsed_url.scheme
|
| 428 |
+
|
| 429 |
+
@property
|
| 430 |
+
def netloc(self) -> str:
|
| 431 |
+
"""
|
| 432 |
+
This can contain auth information.
|
| 433 |
+
"""
|
| 434 |
+
return self._parsed_url.netloc
|
| 435 |
+
|
| 436 |
+
@property
|
| 437 |
+
def path(self) -> str:
|
| 438 |
+
return self._path
|
| 439 |
+
|
| 440 |
+
def splitext(self) -> Tuple[str, str]:
|
| 441 |
+
return splitext(posixpath.basename(self.path.rstrip("/")))
|
| 442 |
+
|
| 443 |
+
@property
|
| 444 |
+
def ext(self) -> str:
|
| 445 |
+
return self.splitext()[1]
|
| 446 |
+
|
| 447 |
+
@property
|
| 448 |
+
def url_without_fragment(self) -> str:
|
| 449 |
+
scheme, netloc, path, query, fragment = self._parsed_url
|
| 450 |
+
return urllib.parse.urlunsplit((scheme, netloc, path, query, ""))
|
| 451 |
+
|
| 452 |
+
_egg_fragment_re = re.compile(r"[#&]egg=([^&]*)")
|
| 453 |
+
|
| 454 |
+
# Per PEP 508.
|
| 455 |
+
_project_name_re = re.compile(
|
| 456 |
+
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
def _egg_fragment(self) -> Optional[str]:
|
| 460 |
+
match = self._egg_fragment_re.search(self._url)
|
| 461 |
+
if not match:
|
| 462 |
+
return None
|
| 463 |
+
|
| 464 |
+
# An egg fragment looks like a PEP 508 project name, along with
|
| 465 |
+
# an optional extras specifier. Anything else is invalid.
|
| 466 |
+
project_name = match.group(1)
|
| 467 |
+
if not self._project_name_re.match(project_name):
|
| 468 |
+
deprecated(
|
| 469 |
+
reason=f"{self} contains an egg fragment with a non-PEP 508 name.",
|
| 470 |
+
replacement="to use the req @ url syntax, and remove the egg fragment",
|
| 471 |
+
gone_in="25.1",
|
| 472 |
+
issue=13157,
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
return project_name
|
| 476 |
+
|
| 477 |
+
_subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
|
| 478 |
+
|
| 479 |
+
@property
|
| 480 |
+
def subdirectory_fragment(self) -> Optional[str]:
|
| 481 |
+
match = self._subdirectory_fragment_re.search(self._url)
|
| 482 |
+
if not match:
|
| 483 |
+
return None
|
| 484 |
+
return match.group(1)
|
| 485 |
+
|
| 486 |
+
def metadata_link(self) -> Optional["Link"]:
|
| 487 |
+
"""Return a link to the associated core metadata file (if any)."""
|
| 488 |
+
if self.metadata_file_data is None:
|
| 489 |
+
return None
|
| 490 |
+
metadata_url = f"{self.url_without_fragment}.metadata"
|
| 491 |
+
if self.metadata_file_data.hashes is None:
|
| 492 |
+
return Link(metadata_url)
|
| 493 |
+
return Link(metadata_url, hashes=self.metadata_file_data.hashes)
|
| 494 |
+
|
| 495 |
+
def as_hashes(self) -> Hashes:
|
| 496 |
+
return Hashes({k: [v] for k, v in self._hashes.items()})
|
| 497 |
+
|
| 498 |
+
@property
|
| 499 |
+
def hash(self) -> Optional[str]:
|
| 500 |
+
return next(iter(self._hashes.values()), None)
|
| 501 |
+
|
| 502 |
+
@property
|
| 503 |
+
def hash_name(self) -> Optional[str]:
|
| 504 |
+
return next(iter(self._hashes), None)
|
| 505 |
+
|
| 506 |
+
@property
|
| 507 |
+
def show_url(self) -> str:
|
| 508 |
+
return posixpath.basename(self._url.split("#", 1)[0].split("?", 1)[0])
|
| 509 |
+
|
| 510 |
+
@property
|
| 511 |
+
def is_file(self) -> bool:
|
| 512 |
+
return self.scheme == "file"
|
| 513 |
+
|
| 514 |
+
def is_existing_dir(self) -> bool:
|
| 515 |
+
return self.is_file and os.path.isdir(self.file_path)
|
| 516 |
+
|
| 517 |
+
@property
|
| 518 |
+
def is_wheel(self) -> bool:
|
| 519 |
+
return self.ext == WHEEL_EXTENSION
|
| 520 |
+
|
| 521 |
+
@property
|
| 522 |
+
def is_vcs(self) -> bool:
|
| 523 |
+
from pip._internal.vcs import vcs
|
| 524 |
+
|
| 525 |
+
return self.scheme in vcs.all_schemes
|
| 526 |
+
|
| 527 |
+
@property
|
| 528 |
+
def is_yanked(self) -> bool:
|
| 529 |
+
return self.yanked_reason is not None
|
| 530 |
+
|
| 531 |
+
@property
|
| 532 |
+
def has_hash(self) -> bool:
|
| 533 |
+
return bool(self._hashes)
|
| 534 |
+
|
| 535 |
+
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
|
| 536 |
+
"""
|
| 537 |
+
Return True if the link has a hash and it is allowed by `hashes`.
|
| 538 |
+
"""
|
| 539 |
+
if hashes is None:
|
| 540 |
+
return False
|
| 541 |
+
return any(hashes.is_hash_allowed(k, v) for k, v in self._hashes.items())
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
class _CleanResult(NamedTuple):
|
| 545 |
+
"""Convert link for equivalency check.
|
| 546 |
+
|
| 547 |
+
This is used in the resolver to check whether two URL-specified requirements
|
| 548 |
+
likely point to the same distribution and can be considered equivalent. This
|
| 549 |
+
equivalency logic avoids comparing URLs literally, which can be too strict
|
| 550 |
+
(e.g. "a=1&b=2" vs "b=2&a=1") and produce conflicts unexpecting to users.
|
| 551 |
+
|
| 552 |
+
Currently this does three things:
|
| 553 |
+
|
| 554 |
+
1. Drop the basic auth part. This is technically wrong since a server can
|
| 555 |
+
serve different content based on auth, but if it does that, it is even
|
| 556 |
+
impossible to guarantee two URLs without auth are equivalent, since
|
| 557 |
+
the user can input different auth information when prompted. So the
|
| 558 |
+
practical solution is to assume the auth doesn't affect the response.
|
| 559 |
+
2. Parse the query to avoid the ordering issue. Note that ordering under the
|
| 560 |
+
same key in the query are NOT cleaned; i.e. "a=1&a=2" and "a=2&a=1" are
|
| 561 |
+
still considered different.
|
| 562 |
+
3. Explicitly drop most of the fragment part, except ``subdirectory=`` and
|
| 563 |
+
hash values, since it should have no impact the downloaded content. Note
|
| 564 |
+
that this drops the "egg=" part historically used to denote the requested
|
| 565 |
+
project (and extras), which is wrong in the strictest sense, but too many
|
| 566 |
+
people are supplying it inconsistently to cause superfluous resolution
|
| 567 |
+
conflicts, so we choose to also ignore them.
|
| 568 |
+
"""
|
| 569 |
+
|
| 570 |
+
parsed: urllib.parse.SplitResult
|
| 571 |
+
query: Dict[str, List[str]]
|
| 572 |
+
subdirectory: str
|
| 573 |
+
hashes: Dict[str, str]
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def _clean_link(link: Link) -> _CleanResult:
|
| 577 |
+
parsed = link._parsed_url
|
| 578 |
+
netloc = parsed.netloc.rsplit("@", 1)[-1]
|
| 579 |
+
# According to RFC 8089, an empty host in file: means localhost.
|
| 580 |
+
if parsed.scheme == "file" and not netloc:
|
| 581 |
+
netloc = "localhost"
|
| 582 |
+
fragment = urllib.parse.parse_qs(parsed.fragment)
|
| 583 |
+
if "egg" in fragment:
|
| 584 |
+
logger.debug("Ignoring egg= fragment in %s", link)
|
| 585 |
+
try:
|
| 586 |
+
# If there are multiple subdirectory values, use the first one.
|
| 587 |
+
# This matches the behavior of Link.subdirectory_fragment.
|
| 588 |
+
subdirectory = fragment["subdirectory"][0]
|
| 589 |
+
except (IndexError, KeyError):
|
| 590 |
+
subdirectory = ""
|
| 591 |
+
# If there are multiple hash values under the same algorithm, use the
|
| 592 |
+
# first one. This matches the behavior of Link.hash_value.
|
| 593 |
+
hashes = {k: fragment[k][0] for k in _SUPPORTED_HASHES if k in fragment}
|
| 594 |
+
return _CleanResult(
|
| 595 |
+
parsed=parsed._replace(netloc=netloc, query="", fragment=""),
|
| 596 |
+
query=urllib.parse.parse_qs(parsed.query),
|
| 597 |
+
subdirectory=subdirectory,
|
| 598 |
+
hashes=hashes,
|
| 599 |
+
)
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
@functools.lru_cache(maxsize=None)
|
| 603 |
+
def links_equivalent(link1: Link, link2: Link) -> bool:
|
| 604 |
+
return _clean_link(link1) == _clean_link(link2)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/scheme.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
For types associated with installation schemes.
|
| 3 |
+
|
| 4 |
+
For a general overview of available schemes and their context, see
|
| 5 |
+
https://docs.python.org/3/install/index.html#alternate-installation.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
|
| 10 |
+
SCHEME_KEYS = ["platlib", "purelib", "headers", "scripts", "data"]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass(frozen=True)
|
| 14 |
+
class Scheme:
|
| 15 |
+
"""A Scheme holds paths which are used as the base directories for
|
| 16 |
+
artifacts associated with a Python package.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
__slots__ = SCHEME_KEYS
|
| 20 |
+
|
| 21 |
+
platlib: str
|
| 22 |
+
purelib: str
|
| 23 |
+
headers: str
|
| 24 |
+
scripts: str
|
| 25 |
+
data: str
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/search_scope.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import itertools
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import posixpath
|
| 5 |
+
import urllib.parse
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import List
|
| 8 |
+
|
| 9 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 10 |
+
|
| 11 |
+
from pip._internal.models.index import PyPI
|
| 12 |
+
from pip._internal.utils.compat import has_tls
|
| 13 |
+
from pip._internal.utils.misc import normalize_path, redact_auth_from_url
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass(frozen=True)
|
| 19 |
+
class SearchScope:
|
| 20 |
+
"""
|
| 21 |
+
Encapsulates the locations that pip is configured to search.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
__slots__ = ["find_links", "index_urls", "no_index"]
|
| 25 |
+
|
| 26 |
+
find_links: List[str]
|
| 27 |
+
index_urls: List[str]
|
| 28 |
+
no_index: bool
|
| 29 |
+
|
| 30 |
+
@classmethod
|
| 31 |
+
def create(
|
| 32 |
+
cls,
|
| 33 |
+
find_links: List[str],
|
| 34 |
+
index_urls: List[str],
|
| 35 |
+
no_index: bool,
|
| 36 |
+
) -> "SearchScope":
|
| 37 |
+
"""
|
| 38 |
+
Create a SearchScope object after normalizing the `find_links`.
|
| 39 |
+
"""
|
| 40 |
+
# Build find_links. If an argument starts with ~, it may be
|
| 41 |
+
# a local file relative to a home directory. So try normalizing
|
| 42 |
+
# it and if it exists, use the normalized version.
|
| 43 |
+
# This is deliberately conservative - it might be fine just to
|
| 44 |
+
# blindly normalize anything starting with a ~...
|
| 45 |
+
built_find_links: List[str] = []
|
| 46 |
+
for link in find_links:
|
| 47 |
+
if link.startswith("~"):
|
| 48 |
+
new_link = normalize_path(link)
|
| 49 |
+
if os.path.exists(new_link):
|
| 50 |
+
link = new_link
|
| 51 |
+
built_find_links.append(link)
|
| 52 |
+
|
| 53 |
+
# If we don't have TLS enabled, then WARN if anyplace we're looking
|
| 54 |
+
# relies on TLS.
|
| 55 |
+
if not has_tls():
|
| 56 |
+
for link in itertools.chain(index_urls, built_find_links):
|
| 57 |
+
parsed = urllib.parse.urlparse(link)
|
| 58 |
+
if parsed.scheme == "https":
|
| 59 |
+
logger.warning(
|
| 60 |
+
"pip is configured with locations that require "
|
| 61 |
+
"TLS/SSL, however the ssl module in Python is not "
|
| 62 |
+
"available."
|
| 63 |
+
)
|
| 64 |
+
break
|
| 65 |
+
|
| 66 |
+
return cls(
|
| 67 |
+
find_links=built_find_links,
|
| 68 |
+
index_urls=index_urls,
|
| 69 |
+
no_index=no_index,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
def get_formatted_locations(self) -> str:
|
| 73 |
+
lines = []
|
| 74 |
+
redacted_index_urls = []
|
| 75 |
+
if self.index_urls and self.index_urls != [PyPI.simple_url]:
|
| 76 |
+
for url in self.index_urls:
|
| 77 |
+
redacted_index_url = redact_auth_from_url(url)
|
| 78 |
+
|
| 79 |
+
# Parse the URL
|
| 80 |
+
purl = urllib.parse.urlsplit(redacted_index_url)
|
| 81 |
+
|
| 82 |
+
# URL is generally invalid if scheme and netloc is missing
|
| 83 |
+
# there are issues with Python and URL parsing, so this test
|
| 84 |
+
# is a bit crude. See bpo-20271, bpo-23505. Python doesn't
|
| 85 |
+
# always parse invalid URLs correctly - it should raise
|
| 86 |
+
# exceptions for malformed URLs
|
| 87 |
+
if not purl.scheme and not purl.netloc:
|
| 88 |
+
logger.warning(
|
| 89 |
+
'The index url "%s" seems invalid, please provide a scheme.',
|
| 90 |
+
redacted_index_url,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
redacted_index_urls.append(redacted_index_url)
|
| 94 |
+
|
| 95 |
+
lines.append(
|
| 96 |
+
"Looking in indexes: {}".format(", ".join(redacted_index_urls))
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
if self.find_links:
|
| 100 |
+
lines.append(
|
| 101 |
+
"Looking in links: {}".format(
|
| 102 |
+
", ".join(redact_auth_from_url(url) for url in self.find_links)
|
| 103 |
+
)
|
| 104 |
+
)
|
| 105 |
+
return "\n".join(lines)
|
| 106 |
+
|
| 107 |
+
def get_index_urls_locations(self, project_name: str) -> List[str]:
|
| 108 |
+
"""Returns the locations found via self.index_urls
|
| 109 |
+
|
| 110 |
+
Checks the url_name on the main (first in the list) index and
|
| 111 |
+
use this url_name to produce all locations
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
def mkurl_pypi_url(url: str) -> str:
|
| 115 |
+
loc = posixpath.join(
|
| 116 |
+
url, urllib.parse.quote(canonicalize_name(project_name))
|
| 117 |
+
)
|
| 118 |
+
# For maximum compatibility with easy_install, ensure the path
|
| 119 |
+
# ends in a trailing slash. Although this isn't in the spec
|
| 120 |
+
# (and PyPI can handle it without the slash) some other index
|
| 121 |
+
# implementations might break if they relied on easy_install's
|
| 122 |
+
# behavior.
|
| 123 |
+
if not loc.endswith("/"):
|
| 124 |
+
loc = loc + "/"
|
| 125 |
+
return loc
|
| 126 |
+
|
| 127 |
+
return [mkurl_pypi_url(url) for url in self.index_urls]
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/selection_prefs.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
|
| 3 |
+
from pip._internal.models.format_control import FormatControl
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# TODO: This needs Python 3.10's improved slots support for dataclasses
|
| 7 |
+
# to be converted into a dataclass.
|
| 8 |
+
class SelectionPreferences:
|
| 9 |
+
"""
|
| 10 |
+
Encapsulates the candidate selection preferences for downloading
|
| 11 |
+
and installing files.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
__slots__ = [
|
| 15 |
+
"allow_yanked",
|
| 16 |
+
"allow_all_prereleases",
|
| 17 |
+
"format_control",
|
| 18 |
+
"prefer_binary",
|
| 19 |
+
"ignore_requires_python",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
# Don't include an allow_yanked default value to make sure each call
|
| 23 |
+
# site considers whether yanked releases are allowed. This also causes
|
| 24 |
+
# that decision to be made explicit in the calling code, which helps
|
| 25 |
+
# people when reading the code.
|
| 26 |
+
def __init__(
|
| 27 |
+
self,
|
| 28 |
+
allow_yanked: bool,
|
| 29 |
+
allow_all_prereleases: bool = False,
|
| 30 |
+
format_control: Optional[FormatControl] = None,
|
| 31 |
+
prefer_binary: bool = False,
|
| 32 |
+
ignore_requires_python: Optional[bool] = None,
|
| 33 |
+
) -> None:
|
| 34 |
+
"""Create a SelectionPreferences object.
|
| 35 |
+
|
| 36 |
+
:param allow_yanked: Whether files marked as yanked (in the sense
|
| 37 |
+
of PEP 592) are permitted to be candidates for install.
|
| 38 |
+
:param format_control: A FormatControl object or None. Used to control
|
| 39 |
+
the selection of source packages / binary packages when consulting
|
| 40 |
+
the index and links.
|
| 41 |
+
:param prefer_binary: Whether to prefer an old, but valid, binary
|
| 42 |
+
dist over a new source dist.
|
| 43 |
+
:param ignore_requires_python: Whether to ignore incompatible
|
| 44 |
+
"Requires-Python" values in links. Defaults to False.
|
| 45 |
+
"""
|
| 46 |
+
if ignore_requires_python is None:
|
| 47 |
+
ignore_requires_python = False
|
| 48 |
+
|
| 49 |
+
self.allow_yanked = allow_yanked
|
| 50 |
+
self.allow_all_prereleases = allow_all_prereleases
|
| 51 |
+
self.format_control = format_control
|
| 52 |
+
self.prefer_binary = prefer_binary
|
| 53 |
+
self.ignore_requires_python = ignore_requires_python
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/target_python.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from typing import List, Optional, Set, Tuple
|
| 3 |
+
|
| 4 |
+
from pip._vendor.packaging.tags import Tag
|
| 5 |
+
|
| 6 |
+
from pip._internal.utils.compatibility_tags import get_supported, version_info_to_nodot
|
| 7 |
+
from pip._internal.utils.misc import normalize_version_info
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TargetPython:
|
| 11 |
+
"""
|
| 12 |
+
Encapsulates the properties of a Python interpreter one is targeting
|
| 13 |
+
for a package install, download, etc.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
__slots__ = [
|
| 17 |
+
"_given_py_version_info",
|
| 18 |
+
"abis",
|
| 19 |
+
"implementation",
|
| 20 |
+
"platforms",
|
| 21 |
+
"py_version",
|
| 22 |
+
"py_version_info",
|
| 23 |
+
"_valid_tags",
|
| 24 |
+
"_valid_tags_set",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
def __init__(
|
| 28 |
+
self,
|
| 29 |
+
platforms: Optional[List[str]] = None,
|
| 30 |
+
py_version_info: Optional[Tuple[int, ...]] = None,
|
| 31 |
+
abis: Optional[List[str]] = None,
|
| 32 |
+
implementation: Optional[str] = None,
|
| 33 |
+
) -> None:
|
| 34 |
+
"""
|
| 35 |
+
:param platforms: A list of strings or None. If None, searches for
|
| 36 |
+
packages that are supported by the current system. Otherwise, will
|
| 37 |
+
find packages that can be built on the platforms passed in. These
|
| 38 |
+
packages will only be downloaded for distribution: they will
|
| 39 |
+
not be built locally.
|
| 40 |
+
:param py_version_info: An optional tuple of ints representing the
|
| 41 |
+
Python version information to use (e.g. `sys.version_info[:3]`).
|
| 42 |
+
This can have length 1, 2, or 3 when provided.
|
| 43 |
+
:param abis: A list of strings or None. This is passed to
|
| 44 |
+
compatibility_tags.py's get_supported() function as is.
|
| 45 |
+
:param implementation: A string or None. This is passed to
|
| 46 |
+
compatibility_tags.py's get_supported() function as is.
|
| 47 |
+
"""
|
| 48 |
+
# Store the given py_version_info for when we call get_supported().
|
| 49 |
+
self._given_py_version_info = py_version_info
|
| 50 |
+
|
| 51 |
+
if py_version_info is None:
|
| 52 |
+
py_version_info = sys.version_info[:3]
|
| 53 |
+
else:
|
| 54 |
+
py_version_info = normalize_version_info(py_version_info)
|
| 55 |
+
|
| 56 |
+
py_version = ".".join(map(str, py_version_info[:2]))
|
| 57 |
+
|
| 58 |
+
self.abis = abis
|
| 59 |
+
self.implementation = implementation
|
| 60 |
+
self.platforms = platforms
|
| 61 |
+
self.py_version = py_version
|
| 62 |
+
self.py_version_info = py_version_info
|
| 63 |
+
|
| 64 |
+
# This is used to cache the return value of get_(un)sorted_tags.
|
| 65 |
+
self._valid_tags: Optional[List[Tag]] = None
|
| 66 |
+
self._valid_tags_set: Optional[Set[Tag]] = None
|
| 67 |
+
|
| 68 |
+
def format_given(self) -> str:
|
| 69 |
+
"""
|
| 70 |
+
Format the given, non-None attributes for display.
|
| 71 |
+
"""
|
| 72 |
+
display_version = None
|
| 73 |
+
if self._given_py_version_info is not None:
|
| 74 |
+
display_version = ".".join(
|
| 75 |
+
str(part) for part in self._given_py_version_info
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
key_values = [
|
| 79 |
+
("platforms", self.platforms),
|
| 80 |
+
("version_info", display_version),
|
| 81 |
+
("abis", self.abis),
|
| 82 |
+
("implementation", self.implementation),
|
| 83 |
+
]
|
| 84 |
+
return " ".join(
|
| 85 |
+
f"{key}={value!r}" for key, value in key_values if value is not None
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
def get_sorted_tags(self) -> List[Tag]:
|
| 89 |
+
"""
|
| 90 |
+
Return the supported PEP 425 tags to check wheel candidates against.
|
| 91 |
+
|
| 92 |
+
The tags are returned in order of preference (most preferred first).
|
| 93 |
+
"""
|
| 94 |
+
if self._valid_tags is None:
|
| 95 |
+
# Pass versions=None if no py_version_info was given since
|
| 96 |
+
# versions=None uses special default logic.
|
| 97 |
+
py_version_info = self._given_py_version_info
|
| 98 |
+
if py_version_info is None:
|
| 99 |
+
version = None
|
| 100 |
+
else:
|
| 101 |
+
version = version_info_to_nodot(py_version_info)
|
| 102 |
+
|
| 103 |
+
tags = get_supported(
|
| 104 |
+
version=version,
|
| 105 |
+
platforms=self.platforms,
|
| 106 |
+
abis=self.abis,
|
| 107 |
+
impl=self.implementation,
|
| 108 |
+
)
|
| 109 |
+
self._valid_tags = tags
|
| 110 |
+
|
| 111 |
+
return self._valid_tags
|
| 112 |
+
|
| 113 |
+
def get_unsorted_tags(self) -> Set[Tag]:
|
| 114 |
+
"""Exactly the same as get_sorted_tags, but returns a set.
|
| 115 |
+
|
| 116 |
+
This is important for performance.
|
| 117 |
+
"""
|
| 118 |
+
if self._valid_tags_set is None:
|
| 119 |
+
self._valid_tags_set = set(self.get_sorted_tags())
|
| 120 |
+
|
| 121 |
+
return self._valid_tags_set
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/models/wheel.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Represents a wheel file and provides access to the various parts of the
|
| 2 |
+
name that have meaning.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
from typing import Dict, Iterable, List
|
| 7 |
+
|
| 8 |
+
from pip._vendor.packaging.tags import Tag
|
| 9 |
+
from pip._vendor.packaging.utils import (
|
| 10 |
+
InvalidWheelFilename as PackagingInvalidWheelName,
|
| 11 |
+
)
|
| 12 |
+
from pip._vendor.packaging.utils import parse_wheel_filename
|
| 13 |
+
|
| 14 |
+
from pip._internal.exceptions import InvalidWheelFilename
|
| 15 |
+
from pip._internal.utils.deprecation import deprecated
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class Wheel:
|
| 19 |
+
"""A wheel file"""
|
| 20 |
+
|
| 21 |
+
wheel_file_re = re.compile(
|
| 22 |
+
r"""^(?P<namever>(?P<name>[^\s-]+?)-(?P<ver>[^\s-]*?))
|
| 23 |
+
((-(?P<build>\d[^-]*?))?-(?P<pyver>[^\s-]+?)-(?P<abi>[^\s-]+?)-(?P<plat>[^\s-]+?)
|
| 24 |
+
\.whl|\.dist-info)$""",
|
| 25 |
+
re.VERBOSE,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
def __init__(self, filename: str) -> None:
|
| 29 |
+
"""
|
| 30 |
+
:raises InvalidWheelFilename: when the filename is invalid for a wheel
|
| 31 |
+
"""
|
| 32 |
+
wheel_info = self.wheel_file_re.match(filename)
|
| 33 |
+
if not wheel_info:
|
| 34 |
+
raise InvalidWheelFilename(f"{filename} is not a valid wheel filename.")
|
| 35 |
+
self.filename = filename
|
| 36 |
+
self.name = wheel_info.group("name").replace("_", "-")
|
| 37 |
+
_version = wheel_info.group("ver")
|
| 38 |
+
if "_" in _version:
|
| 39 |
+
try:
|
| 40 |
+
parse_wheel_filename(filename)
|
| 41 |
+
except PackagingInvalidWheelName as e:
|
| 42 |
+
deprecated(
|
| 43 |
+
reason=(
|
| 44 |
+
f"Wheel filename {filename!r} is not correctly normalised. "
|
| 45 |
+
"Future versions of pip will raise the following error:\n"
|
| 46 |
+
f"{e.args[0]}\n\n"
|
| 47 |
+
),
|
| 48 |
+
replacement=(
|
| 49 |
+
"to rename the wheel to use a correctly normalised "
|
| 50 |
+
"name (this may require updating the version in "
|
| 51 |
+
"the project metadata)"
|
| 52 |
+
),
|
| 53 |
+
gone_in="25.1",
|
| 54 |
+
issue=12938,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
_version = _version.replace("_", "-")
|
| 58 |
+
|
| 59 |
+
self.version = _version
|
| 60 |
+
self.build_tag = wheel_info.group("build")
|
| 61 |
+
self.pyversions = wheel_info.group("pyver").split(".")
|
| 62 |
+
self.abis = wheel_info.group("abi").split(".")
|
| 63 |
+
self.plats = wheel_info.group("plat").split(".")
|
| 64 |
+
|
| 65 |
+
# All the tag combinations from this file
|
| 66 |
+
self.file_tags = {
|
| 67 |
+
Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
def get_formatted_file_tags(self) -> List[str]:
|
| 71 |
+
"""Return the wheel's tags as a sorted list of strings."""
|
| 72 |
+
return sorted(str(tag) for tag in self.file_tags)
|
| 73 |
+
|
| 74 |
+
def support_index_min(self, tags: List[Tag]) -> int:
|
| 75 |
+
"""Return the lowest index that one of the wheel's file_tag combinations
|
| 76 |
+
achieves in the given list of supported tags.
|
| 77 |
+
|
| 78 |
+
For example, if there are 8 supported tags and one of the file tags
|
| 79 |
+
is first in the list, then return 0.
|
| 80 |
+
|
| 81 |
+
:param tags: the PEP 425 tags to check the wheel against, in order
|
| 82 |
+
with most preferred first.
|
| 83 |
+
|
| 84 |
+
:raises ValueError: If none of the wheel's file tags match one of
|
| 85 |
+
the supported tags.
|
| 86 |
+
"""
|
| 87 |
+
try:
|
| 88 |
+
return next(i for i, t in enumerate(tags) if t in self.file_tags)
|
| 89 |
+
except StopIteration:
|
| 90 |
+
raise ValueError()
|
| 91 |
+
|
| 92 |
+
def find_most_preferred_tag(
|
| 93 |
+
self, tags: List[Tag], tag_to_priority: Dict[Tag, int]
|
| 94 |
+
) -> int:
|
| 95 |
+
"""Return the priority of the most preferred tag that one of the wheel's file
|
| 96 |
+
tag combinations achieves in the given list of supported tags using the given
|
| 97 |
+
tag_to_priority mapping, where lower priorities are more-preferred.
|
| 98 |
+
|
| 99 |
+
This is used in place of support_index_min in some cases in order to avoid
|
| 100 |
+
an expensive linear scan of a large list of tags.
|
| 101 |
+
|
| 102 |
+
:param tags: the PEP 425 tags to check the wheel against.
|
| 103 |
+
:param tag_to_priority: a mapping from tag to priority of that tag, where
|
| 104 |
+
lower is more preferred.
|
| 105 |
+
|
| 106 |
+
:raises ValueError: If none of the wheel's file tags match one of
|
| 107 |
+
the supported tags.
|
| 108 |
+
"""
|
| 109 |
+
return min(
|
| 110 |
+
tag_to_priority[tag] for tag in self.file_tags if tag in tag_to_priority
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
def supported(self, tags: Iterable[Tag]) -> bool:
|
| 114 |
+
"""Return whether the wheel is compatible with one of the given tags.
|
| 115 |
+
|
| 116 |
+
:param tags: the PEP 425 tags to check the wheel against.
|
| 117 |
+
"""
|
| 118 |
+
return not self.file_tags.isdisjoint(tags)
|
URSA/.venv_ursa/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)
|
URSA/.venv_ursa/lib/python3.12/site-packages/pip/_internal/resolution/__init__.py
ADDED
|
File without changes
|