Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/pip/_internal/commands/__init__.py +132 -0
- lib/python3.12/site-packages/pip/_internal/commands/cache.py +228 -0
- lib/python3.12/site-packages/pip/_internal/commands/check.py +67 -0
- lib/python3.12/site-packages/pip/_internal/commands/completion.py +130 -0
- lib/python3.12/site-packages/pip/_internal/commands/debug.py +201 -0
- lib/python3.12/site-packages/pip/_internal/commands/download.py +146 -0
- lib/python3.12/site-packages/pip/_internal/commands/freeze.py +109 -0
- lib/python3.12/site-packages/pip/_internal/commands/hash.py +59 -0
- lib/python3.12/site-packages/pip/_internal/commands/help.py +41 -0
- lib/python3.12/site-packages/pip/_internal/commands/index.py +139 -0
- lib/python3.12/site-packages/pip/_internal/commands/install.py +784 -0
- lib/python3.12/site-packages/pip/_internal/commands/list.py +375 -0
- lib/python3.12/site-packages/pip/_internal/commands/search.py +172 -0
- lib/python3.12/site-packages/pip/_internal/commands/show.py +224 -0
- lib/python3.12/site-packages/pip/_internal/commands/wheel.py +182 -0
- lib/python3.12/site-packages/pip/_internal/index/__init__.py +2 -0
- lib/python3.12/site-packages/pip/_internal/index/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/index/__pycache__/collector.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/index/__pycache__/package_finder.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/index/__pycache__/sources.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/index/collector.py +494 -0
- lib/python3.12/site-packages/pip/_internal/index/package_finder.py +1029 -0
- lib/python3.12/site-packages/pip/_internal/index/sources.py +284 -0
- lib/python3.12/site-packages/pip/_internal/locations/__init__.py +456 -0
- lib/python3.12/site-packages/pip/_internal/locations/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_distutils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_sysconfig.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/locations/__pycache__/base.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/locations/_distutils.py +172 -0
- lib/python3.12/site-packages/pip/_internal/locations/_sysconfig.py +214 -0
- lib/python3.12/site-packages/pip/_internal/locations/base.py +81 -0
- lib/python3.12/site-packages/pip/_internal/models/__init__.py +2 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/candidate.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/direct_url.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/format_control.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/index.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/installation_report.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/link.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/scheme.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/search_scope.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/selection_prefs.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/target_python.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/__pycache__/wheel.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/pip/_internal/models/candidate.py +25 -0
- lib/python3.12/site-packages/pip/_internal/models/direct_url.py +224 -0
- lib/python3.12/site-packages/pip/_internal/models/format_control.py +78 -0
- lib/python3.12/site-packages/pip/_internal/models/index.py +28 -0
- lib/python3.12/site-packages/pip/_internal/models/installation_report.py +56 -0
- lib/python3.12/site-packages/pip/_internal/models/link.py +604 -0
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
|
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)
|
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
|
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
|
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
|
lib/python3.12/site-packages/pip/_internal/commands/download.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
from optparse import Values
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
from pip._internal.cli import cmdoptions
|
| 7 |
+
from pip._internal.cli.cmdoptions import make_target_python
|
| 8 |
+
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
|
| 9 |
+
from pip._internal.cli.status_codes import SUCCESS
|
| 10 |
+
from pip._internal.operations.build.build_tracker import get_build_tracker
|
| 11 |
+
from pip._internal.req.req_install import check_legacy_setup_py_options
|
| 12 |
+
from pip._internal.utils.misc import ensure_dir, normalize_path, write_output
|
| 13 |
+
from pip._internal.utils.temp_dir import TempDirectory
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class DownloadCommand(RequirementCommand):
|
| 19 |
+
"""
|
| 20 |
+
Download packages from:
|
| 21 |
+
|
| 22 |
+
- PyPI (and other indexes) using requirement specifiers.
|
| 23 |
+
- VCS project urls.
|
| 24 |
+
- Local project directories.
|
| 25 |
+
- Local or remote source archives.
|
| 26 |
+
|
| 27 |
+
pip also supports downloading from "requirements files", which provide
|
| 28 |
+
an easy way to specify a whole environment to be downloaded.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
usage = """
|
| 32 |
+
%prog [options] <requirement specifier> [package-index-options] ...
|
| 33 |
+
%prog [options] -r <requirements file> [package-index-options] ...
|
| 34 |
+
%prog [options] <vcs project url> ...
|
| 35 |
+
%prog [options] <local project path> ...
|
| 36 |
+
%prog [options] <archive url/path> ..."""
|
| 37 |
+
|
| 38 |
+
def add_options(self) -> None:
|
| 39 |
+
self.cmd_opts.add_option(cmdoptions.constraints())
|
| 40 |
+
self.cmd_opts.add_option(cmdoptions.requirements())
|
| 41 |
+
self.cmd_opts.add_option(cmdoptions.no_deps())
|
| 42 |
+
self.cmd_opts.add_option(cmdoptions.global_options())
|
| 43 |
+
self.cmd_opts.add_option(cmdoptions.no_binary())
|
| 44 |
+
self.cmd_opts.add_option(cmdoptions.only_binary())
|
| 45 |
+
self.cmd_opts.add_option(cmdoptions.prefer_binary())
|
| 46 |
+
self.cmd_opts.add_option(cmdoptions.src())
|
| 47 |
+
self.cmd_opts.add_option(cmdoptions.pre())
|
| 48 |
+
self.cmd_opts.add_option(cmdoptions.require_hashes())
|
| 49 |
+
self.cmd_opts.add_option(cmdoptions.progress_bar())
|
| 50 |
+
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
|
| 51 |
+
self.cmd_opts.add_option(cmdoptions.use_pep517())
|
| 52 |
+
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
|
| 53 |
+
self.cmd_opts.add_option(cmdoptions.check_build_deps())
|
| 54 |
+
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
|
| 55 |
+
|
| 56 |
+
self.cmd_opts.add_option(
|
| 57 |
+
"-d",
|
| 58 |
+
"--dest",
|
| 59 |
+
"--destination-dir",
|
| 60 |
+
"--destination-directory",
|
| 61 |
+
dest="download_dir",
|
| 62 |
+
metavar="dir",
|
| 63 |
+
default=os.curdir,
|
| 64 |
+
help="Download packages into <dir>.",
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
cmdoptions.add_target_python_options(self.cmd_opts)
|
| 68 |
+
|
| 69 |
+
index_opts = cmdoptions.make_option_group(
|
| 70 |
+
cmdoptions.index_group,
|
| 71 |
+
self.parser,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
self.parser.insert_option_group(0, index_opts)
|
| 75 |
+
self.parser.insert_option_group(0, self.cmd_opts)
|
| 76 |
+
|
| 77 |
+
@with_cleanup
|
| 78 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 79 |
+
options.ignore_installed = True
|
| 80 |
+
# editable doesn't really make sense for `pip download`, but the bowels
|
| 81 |
+
# of the RequirementSet code require that property.
|
| 82 |
+
options.editables = []
|
| 83 |
+
|
| 84 |
+
cmdoptions.check_dist_restriction(options)
|
| 85 |
+
|
| 86 |
+
options.download_dir = normalize_path(options.download_dir)
|
| 87 |
+
ensure_dir(options.download_dir)
|
| 88 |
+
|
| 89 |
+
session = self.get_default_session(options)
|
| 90 |
+
|
| 91 |
+
target_python = make_target_python(options)
|
| 92 |
+
finder = self._build_package_finder(
|
| 93 |
+
options=options,
|
| 94 |
+
session=session,
|
| 95 |
+
target_python=target_python,
|
| 96 |
+
ignore_requires_python=options.ignore_requires_python,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
build_tracker = self.enter_context(get_build_tracker())
|
| 100 |
+
|
| 101 |
+
directory = TempDirectory(
|
| 102 |
+
delete=not options.no_clean,
|
| 103 |
+
kind="download",
|
| 104 |
+
globally_managed=True,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
reqs = self.get_requirements(args, options, finder, session)
|
| 108 |
+
check_legacy_setup_py_options(options, reqs)
|
| 109 |
+
|
| 110 |
+
preparer = self.make_requirement_preparer(
|
| 111 |
+
temp_build_dir=directory,
|
| 112 |
+
options=options,
|
| 113 |
+
build_tracker=build_tracker,
|
| 114 |
+
session=session,
|
| 115 |
+
finder=finder,
|
| 116 |
+
download_dir=options.download_dir,
|
| 117 |
+
use_user_site=False,
|
| 118 |
+
verbosity=self.verbosity,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
resolver = self.make_resolver(
|
| 122 |
+
preparer=preparer,
|
| 123 |
+
finder=finder,
|
| 124 |
+
options=options,
|
| 125 |
+
ignore_requires_python=options.ignore_requires_python,
|
| 126 |
+
use_pep517=options.use_pep517,
|
| 127 |
+
py_version_info=options.python_version,
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
self.trace_basic_info(finder)
|
| 131 |
+
|
| 132 |
+
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
|
| 133 |
+
|
| 134 |
+
downloaded: List[str] = []
|
| 135 |
+
for req in requirement_set.requirements.values():
|
| 136 |
+
if req.satisfied_by is None:
|
| 137 |
+
assert req.name is not None
|
| 138 |
+
preparer.save_linked_requirement(req)
|
| 139 |
+
downloaded.append(req.name)
|
| 140 |
+
|
| 141 |
+
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
|
| 142 |
+
|
| 143 |
+
if downloaded:
|
| 144 |
+
write_output("Successfully downloaded %s", " ".join(downloaded))
|
| 145 |
+
|
| 146 |
+
return SUCCESS
|
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
|
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()
|
lib/python3.12/site-packages/pip/_internal/commands/help.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from optparse import Values
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
from pip._internal.cli.base_command import Command
|
| 5 |
+
from pip._internal.cli.status_codes import SUCCESS
|
| 6 |
+
from pip._internal.exceptions import CommandError
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class HelpCommand(Command):
|
| 10 |
+
"""Show help for commands"""
|
| 11 |
+
|
| 12 |
+
usage = """
|
| 13 |
+
%prog <command>"""
|
| 14 |
+
ignore_require_venv = True
|
| 15 |
+
|
| 16 |
+
def run(self, options: Values, args: List[str]) -> int:
|
| 17 |
+
from pip._internal.commands import (
|
| 18 |
+
commands_dict,
|
| 19 |
+
create_command,
|
| 20 |
+
get_similar_commands,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
# 'pip help' with no args is handled by pip.__init__.parseopt()
|
| 25 |
+
cmd_name = args[0] # the command we need help for
|
| 26 |
+
except IndexError:
|
| 27 |
+
return SUCCESS
|
| 28 |
+
|
| 29 |
+
if cmd_name not in commands_dict:
|
| 30 |
+
guess = get_similar_commands(cmd_name)
|
| 31 |
+
|
| 32 |
+
msg = [f'unknown command "{cmd_name}"']
|
| 33 |
+
if guess:
|
| 34 |
+
msg.append(f'maybe you meant "{guess}"')
|
| 35 |
+
|
| 36 |
+
raise CommandError(" - ".join(msg))
|
| 37 |
+
|
| 38 |
+
command = create_command(cmd_name)
|
| 39 |
+
command.parser.print_help()
|
| 40 |
+
|
| 41 |
+
return SUCCESS
|
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)
|
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"
|
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)
|
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)
|
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
|
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
|
lib/python3.12/site-packages/pip/_internal/index/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Index interaction code
|
| 2 |
+
"""
|
lib/python3.12/site-packages/pip/_internal/index/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (238 Bytes). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/index/__pycache__/collector.cpython-312.pyc
ADDED
|
Binary file (21.6 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/index/__pycache__/package_finder.cpython-312.pyc
ADDED
|
Binary file (41.4 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/index/__pycache__/sources.cpython-312.pyc
ADDED
|
Binary file (12.5 kB). View file
|
|
|
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 |
+
)
|
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
|
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)
|
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
|
lib/python3.12/site-packages/pip/_internal/locations/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (16.4 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_distutils.cpython-312.pyc
ADDED
|
Binary file (6.8 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_sysconfig.cpython-312.pyc
ADDED
|
Binary file (8.03 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/locations/__pycache__/base.cpython-312.pyc
ADDED
|
Binary file (3.79 kB). View file
|
|
|
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)
|
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"]
|
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"))
|
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 |
+
"""
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (272 Bytes). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/candidate.cpython-312.pyc
ADDED
|
Binary file (1.61 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/direct_url.cpython-312.pyc
ADDED
|
Binary file (10.9 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/format_control.cpython-312.pyc
ADDED
|
Binary file (4.23 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/index.cpython-312.pyc
ADDED
|
Binary file (1.7 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/installation_report.cpython-312.pyc
ADDED
|
Binary file (2.28 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/link.cpython-312.pyc
ADDED
|
Binary file (27 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/scheme.cpython-312.pyc
ADDED
|
Binary file (1.03 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/search_scope.cpython-312.pyc
ADDED
|
Binary file (4.99 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/selection_prefs.cpython-312.pyc
ADDED
|
Binary file (1.86 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/target_python.cpython-312.pyc
ADDED
|
Binary file (4.96 kB). View file
|
|
|
lib/python3.12/site-packages/pip/_internal/models/__pycache__/wheel.cpython-312.pyc
ADDED
|
Binary file (6.56 kB). View file
|
|
|
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})"
|
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
|
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 |
+
)
|
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 |
+
)
|
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 |
+
}
|
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)
|