ZhengyangZhang commited on
Commit
220151a
·
verified ·
1 Parent(s): bffc65d

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/pip/_internal/commands/__init__.py +132 -0
  2. lib/python3.12/site-packages/pip/_internal/commands/cache.py +228 -0
  3. lib/python3.12/site-packages/pip/_internal/commands/check.py +67 -0
  4. lib/python3.12/site-packages/pip/_internal/commands/completion.py +130 -0
  5. lib/python3.12/site-packages/pip/_internal/commands/debug.py +201 -0
  6. lib/python3.12/site-packages/pip/_internal/commands/download.py +146 -0
  7. lib/python3.12/site-packages/pip/_internal/commands/freeze.py +109 -0
  8. lib/python3.12/site-packages/pip/_internal/commands/hash.py +59 -0
  9. lib/python3.12/site-packages/pip/_internal/commands/help.py +41 -0
  10. lib/python3.12/site-packages/pip/_internal/commands/index.py +139 -0
  11. lib/python3.12/site-packages/pip/_internal/commands/install.py +784 -0
  12. lib/python3.12/site-packages/pip/_internal/commands/list.py +375 -0
  13. lib/python3.12/site-packages/pip/_internal/commands/search.py +172 -0
  14. lib/python3.12/site-packages/pip/_internal/commands/show.py +224 -0
  15. lib/python3.12/site-packages/pip/_internal/commands/wheel.py +182 -0
  16. lib/python3.12/site-packages/pip/_internal/index/__init__.py +2 -0
  17. lib/python3.12/site-packages/pip/_internal/index/__pycache__/__init__.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/pip/_internal/index/__pycache__/collector.cpython-312.pyc +0 -0
  19. lib/python3.12/site-packages/pip/_internal/index/__pycache__/package_finder.cpython-312.pyc +0 -0
  20. lib/python3.12/site-packages/pip/_internal/index/__pycache__/sources.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/pip/_internal/index/collector.py +494 -0
  22. lib/python3.12/site-packages/pip/_internal/index/package_finder.py +1029 -0
  23. lib/python3.12/site-packages/pip/_internal/index/sources.py +284 -0
  24. lib/python3.12/site-packages/pip/_internal/locations/__init__.py +456 -0
  25. lib/python3.12/site-packages/pip/_internal/locations/__pycache__/__init__.cpython-312.pyc +0 -0
  26. lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_distutils.cpython-312.pyc +0 -0
  27. lib/python3.12/site-packages/pip/_internal/locations/__pycache__/_sysconfig.cpython-312.pyc +0 -0
  28. lib/python3.12/site-packages/pip/_internal/locations/__pycache__/base.cpython-312.pyc +0 -0
  29. lib/python3.12/site-packages/pip/_internal/locations/_distutils.py +172 -0
  30. lib/python3.12/site-packages/pip/_internal/locations/_sysconfig.py +214 -0
  31. lib/python3.12/site-packages/pip/_internal/locations/base.py +81 -0
  32. lib/python3.12/site-packages/pip/_internal/models/__init__.py +2 -0
  33. lib/python3.12/site-packages/pip/_internal/models/__pycache__/__init__.cpython-312.pyc +0 -0
  34. lib/python3.12/site-packages/pip/_internal/models/__pycache__/candidate.cpython-312.pyc +0 -0
  35. lib/python3.12/site-packages/pip/_internal/models/__pycache__/direct_url.cpython-312.pyc +0 -0
  36. lib/python3.12/site-packages/pip/_internal/models/__pycache__/format_control.cpython-312.pyc +0 -0
  37. lib/python3.12/site-packages/pip/_internal/models/__pycache__/index.cpython-312.pyc +0 -0
  38. lib/python3.12/site-packages/pip/_internal/models/__pycache__/installation_report.cpython-312.pyc +0 -0
  39. lib/python3.12/site-packages/pip/_internal/models/__pycache__/link.cpython-312.pyc +0 -0
  40. lib/python3.12/site-packages/pip/_internal/models/__pycache__/scheme.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/pip/_internal/models/__pycache__/search_scope.cpython-312.pyc +0 -0
  42. lib/python3.12/site-packages/pip/_internal/models/__pycache__/selection_prefs.cpython-312.pyc +0 -0
  43. lib/python3.12/site-packages/pip/_internal/models/__pycache__/target_python.cpython-312.pyc +0 -0
  44. lib/python3.12/site-packages/pip/_internal/models/__pycache__/wheel.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/pip/_internal/models/candidate.py +25 -0
  46. lib/python3.12/site-packages/pip/_internal/models/direct_url.py +224 -0
  47. lib/python3.12/site-packages/pip/_internal/models/format_control.py +78 -0
  48. lib/python3.12/site-packages/pip/_internal/models/index.py +28 -0
  49. lib/python3.12/site-packages/pip/_internal/models/installation_report.py +56 -0
  50. 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)