tharaka-axonect commited on
Commit
cfa1a6f
·
verified ·
1 Parent(s): 22f3c37

Upload folder using huggingface_hub (part 5)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env/lib/python3.13/site-packages/pip/_internal/operations/install/editable_legacy.py +46 -0
  2. .env/lib/python3.13/site-packages/pip/_internal/operations/install/wheel.py +738 -0
  3. .env/lib/python3.13/site-packages/pip/_internal/operations/prepare.py +737 -0
  4. .env/lib/python3.13/site-packages/pip/_internal/pyproject.py +185 -0
  5. .env/lib/python3.13/site-packages/pip/_internal/req/__init__.py +103 -0
  6. .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/__init__.cpython-313.pyc +0 -0
  7. .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/constructors.cpython-313.pyc +0 -0
  8. .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_dependency_group.cpython-313.pyc +0 -0
  9. .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_file.cpython-313.pyc +0 -0
  10. .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_install.cpython-313.pyc +0 -0
  11. .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_set.cpython-313.pyc +0 -0
  12. .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_uninstall.cpython-313.pyc +0 -0
  13. .env/lib/python3.13/site-packages/pip/_internal/req/constructors.py +560 -0
  14. .env/lib/python3.13/site-packages/pip/_internal/req/req_dependency_group.py +79 -0
  15. .env/lib/python3.13/site-packages/pip/_internal/req/req_file.py +623 -0
  16. .env/lib/python3.13/site-packages/pip/_internal/req/req_install.py +934 -0
  17. .env/lib/python3.13/site-packages/pip/_internal/req/req_set.py +82 -0
  18. .env/lib/python3.13/site-packages/pip/_internal/req/req_uninstall.py +636 -0
  19. .env/lib/python3.13/site-packages/pip/_internal/resolution/__init__.py +0 -0
  20. .env/lib/python3.13/site-packages/pip/_internal/resolution/__pycache__/__init__.cpython-313.pyc +0 -0
  21. .env/lib/python3.13/site-packages/pip/_internal/resolution/__pycache__/base.cpython-313.pyc +0 -0
  22. .env/lib/python3.13/site-packages/pip/_internal/resolution/base.py +20 -0
  23. .env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__init__.py +0 -0
  24. .env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__pycache__/__init__.cpython-313.pyc +0 -0
  25. .env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__pycache__/resolver.cpython-313.pyc +0 -0
  26. .env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/resolver.py +597 -0
  27. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__init__.py +0 -0
  28. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/__init__.cpython-313.pyc +0 -0
  29. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/base.cpython-313.pyc +0 -0
  30. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/candidates.cpython-313.pyc +0 -0
  31. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/factory.cpython-313.pyc +0 -0
  32. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/found_candidates.cpython-313.pyc +0 -0
  33. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/provider.cpython-313.pyc +0 -0
  34. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/reporter.cpython-313.pyc +0 -0
  35. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/requirements.cpython-313.pyc +0 -0
  36. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/resolver.cpython-313.pyc +0 -0
  37. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/base.py +139 -0
  38. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/candidates.py +579 -0
  39. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/factory.py +823 -0
  40. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py +164 -0
  41. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/provider.py +281 -0
  42. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/reporter.py +83 -0
  43. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/requirements.py +245 -0
  44. .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/resolver.py +320 -0
  45. .env/lib/python3.13/site-packages/pip/_internal/self_outdated_check.py +252 -0
  46. .env/lib/python3.13/site-packages/pip/_internal/utils/__init__.py +0 -0
  47. .env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  48. .env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/_jaraco_text.cpython-313.pyc +0 -0
  49. .env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/_log.cpython-313.pyc +0 -0
  50. .env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-313.pyc +0 -0
.env/lib/python3.13/site-packages/pip/_internal/operations/install/editable_legacy.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Legacy editable installation process, i.e. `setup.py develop`."""
2
+
3
+ import logging
4
+ from typing import Optional, Sequence
5
+
6
+ from pip._internal.build_env import BuildEnvironment
7
+ from pip._internal.utils.logging import indent_log
8
+ from pip._internal.utils.setuptools_build import make_setuptools_develop_args
9
+ from pip._internal.utils.subprocess import call_subprocess
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def install_editable(
15
+ *,
16
+ global_options: Sequence[str],
17
+ prefix: Optional[str],
18
+ home: Optional[str],
19
+ use_user_site: bool,
20
+ name: str,
21
+ setup_py_path: str,
22
+ isolated: bool,
23
+ build_env: BuildEnvironment,
24
+ unpacked_source_directory: str,
25
+ ) -> None:
26
+ """Install a package in editable mode. Most arguments are pass-through
27
+ to setuptools.
28
+ """
29
+ logger.info("Running setup.py develop for %s", name)
30
+
31
+ args = make_setuptools_develop_args(
32
+ setup_py_path,
33
+ global_options=global_options,
34
+ no_user_config=isolated,
35
+ prefix=prefix,
36
+ home=home,
37
+ use_user_site=use_user_site,
38
+ )
39
+
40
+ with indent_log():
41
+ with build_env:
42
+ call_subprocess(
43
+ args,
44
+ command_desc="python setup.py develop",
45
+ cwd=unpacked_source_directory,
46
+ )
.env/lib/python3.13/site-packages/pip/_internal/operations/install/wheel.py ADDED
@@ -0,0 +1,738 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Support for installing and building the "wheel" binary package format."""
2
+
3
+ import collections
4
+ import compileall
5
+ import contextlib
6
+ import csv
7
+ import importlib
8
+ import logging
9
+ import os.path
10
+ import re
11
+ import shutil
12
+ import sys
13
+ import warnings
14
+ from base64 import urlsafe_b64encode
15
+ from email.message import Message
16
+ from itertools import chain, filterfalse, starmap
17
+ from typing import (
18
+ IO,
19
+ Any,
20
+ BinaryIO,
21
+ Callable,
22
+ Dict,
23
+ Generator,
24
+ Iterable,
25
+ Iterator,
26
+ List,
27
+ NewType,
28
+ Optional,
29
+ Protocol,
30
+ Sequence,
31
+ Set,
32
+ Tuple,
33
+ Union,
34
+ cast,
35
+ )
36
+ from zipfile import ZipFile, ZipInfo
37
+
38
+ from pip._vendor.distlib.scripts import ScriptMaker
39
+ from pip._vendor.distlib.util import get_export_entry
40
+ from pip._vendor.packaging.utils import canonicalize_name
41
+
42
+ from pip._internal.exceptions import InstallationError
43
+ from pip._internal.locations import get_major_minor_version
44
+ from pip._internal.metadata import (
45
+ BaseDistribution,
46
+ FilesystemWheel,
47
+ get_wheel_distribution,
48
+ )
49
+ from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
50
+ from pip._internal.models.scheme import SCHEME_KEYS, Scheme
51
+ from pip._internal.utils.filesystem import adjacent_tmp_file, replace
52
+ from pip._internal.utils.misc import StreamWrapper, ensure_dir, hash_file, partition
53
+ from pip._internal.utils.unpacking import (
54
+ current_umask,
55
+ is_within_directory,
56
+ set_extracted_file_to_default_mode_plus_executable,
57
+ zip_item_is_executable,
58
+ )
59
+ from pip._internal.utils.wheel import parse_wheel
60
+
61
+
62
+ class File(Protocol):
63
+ src_record_path: "RecordPath"
64
+ dest_path: str
65
+ changed: bool
66
+
67
+ def save(self) -> None:
68
+ pass
69
+
70
+
71
+ logger = logging.getLogger(__name__)
72
+
73
+ RecordPath = NewType("RecordPath", str)
74
+ InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]]
75
+
76
+
77
+ def rehash(path: str, blocksize: int = 1 << 20) -> Tuple[str, str]:
78
+ """Return (encoded_digest, length) for path using hashlib.sha256()"""
79
+ h, length = hash_file(path, blocksize)
80
+ digest = "sha256=" + urlsafe_b64encode(h.digest()).decode("latin1").rstrip("=")
81
+ return (digest, str(length))
82
+
83
+
84
+ def csv_io_kwargs(mode: str) -> Dict[str, Any]:
85
+ """Return keyword arguments to properly open a CSV file
86
+ in the given mode.
87
+ """
88
+ return {"mode": mode, "newline": "", "encoding": "utf-8"}
89
+
90
+
91
+ def fix_script(path: str) -> bool:
92
+ """Replace #!python with #!/path/to/python
93
+ Return True if file was changed.
94
+ """
95
+ # XXX RECORD hashes will need to be updated
96
+ assert os.path.isfile(path)
97
+
98
+ with open(path, "rb") as script:
99
+ firstline = script.readline()
100
+ if not firstline.startswith(b"#!python"):
101
+ return False
102
+ exename = sys.executable.encode(sys.getfilesystemencoding())
103
+ firstline = b"#!" + exename + os.linesep.encode("ascii")
104
+ rest = script.read()
105
+ with open(path, "wb") as script:
106
+ script.write(firstline)
107
+ script.write(rest)
108
+ return True
109
+
110
+
111
+ def wheel_root_is_purelib(metadata: Message) -> bool:
112
+ return metadata.get("Root-Is-Purelib", "").lower() == "true"
113
+
114
+
115
+ def get_entrypoints(dist: BaseDistribution) -> Tuple[Dict[str, str], Dict[str, str]]:
116
+ console_scripts = {}
117
+ gui_scripts = {}
118
+ for entry_point in dist.iter_entry_points():
119
+ if entry_point.group == "console_scripts":
120
+ console_scripts[entry_point.name] = entry_point.value
121
+ elif entry_point.group == "gui_scripts":
122
+ gui_scripts[entry_point.name] = entry_point.value
123
+ return console_scripts, gui_scripts
124
+
125
+
126
+ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
127
+ """Determine if any scripts are not on PATH and format a warning.
128
+ Returns a warning message if one or more scripts are not on PATH,
129
+ otherwise None.
130
+ """
131
+ if not scripts:
132
+ return None
133
+
134
+ # Group scripts by the path they were installed in
135
+ grouped_by_dir: Dict[str, Set[str]] = collections.defaultdict(set)
136
+ for destfile in scripts:
137
+ parent_dir = os.path.dirname(destfile)
138
+ script_name = os.path.basename(destfile)
139
+ grouped_by_dir[parent_dir].add(script_name)
140
+
141
+ # We don't want to warn for directories that are on PATH.
142
+ not_warn_dirs = [
143
+ os.path.normcase(os.path.normpath(i)).rstrip(os.sep)
144
+ for i in os.environ.get("PATH", "").split(os.pathsep)
145
+ ]
146
+ # If an executable sits with sys.executable, we don't warn for it.
147
+ # This covers the case of venv invocations without activating the venv.
148
+ not_warn_dirs.append(
149
+ os.path.normcase(os.path.normpath(os.path.dirname(sys.executable)))
150
+ )
151
+ warn_for: Dict[str, Set[str]] = {
152
+ parent_dir: scripts
153
+ for parent_dir, scripts in grouped_by_dir.items()
154
+ if os.path.normcase(os.path.normpath(parent_dir)) not in not_warn_dirs
155
+ }
156
+ if not warn_for:
157
+ return None
158
+
159
+ # Format a message
160
+ msg_lines = []
161
+ for parent_dir, dir_scripts in warn_for.items():
162
+ sorted_scripts: List[str] = sorted(dir_scripts)
163
+ if len(sorted_scripts) == 1:
164
+ start_text = f"script {sorted_scripts[0]} is"
165
+ else:
166
+ start_text = "scripts {} are".format(
167
+ ", ".join(sorted_scripts[:-1]) + " and " + sorted_scripts[-1]
168
+ )
169
+
170
+ msg_lines.append(
171
+ f"The {start_text} installed in '{parent_dir}' which is not on PATH."
172
+ )
173
+
174
+ last_line_fmt = (
175
+ "Consider adding {} to PATH or, if you prefer "
176
+ "to suppress this warning, use --no-warn-script-location."
177
+ )
178
+ if len(msg_lines) == 1:
179
+ msg_lines.append(last_line_fmt.format("this directory"))
180
+ else:
181
+ msg_lines.append(last_line_fmt.format("these directories"))
182
+
183
+ # Add a note if any directory starts with ~
184
+ warn_for_tilde = any(
185
+ i[0] == "~" for i in os.environ.get("PATH", "").split(os.pathsep) if i
186
+ )
187
+ if warn_for_tilde:
188
+ tilde_warning_msg = (
189
+ "NOTE: The current PATH contains path(s) starting with `~`, "
190
+ "which may not be expanded by all applications."
191
+ )
192
+ msg_lines.append(tilde_warning_msg)
193
+
194
+ # Returns the formatted multiline message
195
+ return "\n".join(msg_lines)
196
+
197
+
198
+ def _normalized_outrows(
199
+ outrows: Iterable[InstalledCSVRow],
200
+ ) -> List[Tuple[str, str, str]]:
201
+ """Normalize the given rows of a RECORD file.
202
+
203
+ Items in each row are converted into str. Rows are then sorted to make
204
+ the value more predictable for tests.
205
+
206
+ Each row is a 3-tuple (path, hash, size) and corresponds to a record of
207
+ a RECORD file (see PEP 376 and PEP 427 for details). For the rows
208
+ passed to this function, the size can be an integer as an int or string,
209
+ or the empty string.
210
+ """
211
+ # Normally, there should only be one row per path, in which case the
212
+ # second and third elements don't come into play when sorting.
213
+ # However, in cases in the wild where a path might happen to occur twice,
214
+ # we don't want the sort operation to trigger an error (but still want
215
+ # determinism). Since the third element can be an int or string, we
216
+ # coerce each element to a string to avoid a TypeError in this case.
217
+ # For additional background, see--
218
+ # https://github.com/pypa/pip/issues/5868
219
+ return sorted(
220
+ (record_path, hash_, str(size)) for record_path, hash_, size in outrows
221
+ )
222
+
223
+
224
+ def _record_to_fs_path(record_path: RecordPath, lib_dir: str) -> str:
225
+ return os.path.join(lib_dir, record_path)
226
+
227
+
228
+ def _fs_to_record_path(path: str, lib_dir: str) -> RecordPath:
229
+ # On Windows, do not handle relative paths if they belong to different
230
+ # logical disks
231
+ if os.path.splitdrive(path)[0].lower() == os.path.splitdrive(lib_dir)[0].lower():
232
+ path = os.path.relpath(path, lib_dir)
233
+
234
+ path = path.replace(os.path.sep, "/")
235
+ return cast("RecordPath", path)
236
+
237
+
238
+ def get_csv_rows_for_installed(
239
+ old_csv_rows: List[List[str]],
240
+ installed: Dict[RecordPath, RecordPath],
241
+ changed: Set[RecordPath],
242
+ generated: List[str],
243
+ lib_dir: str,
244
+ ) -> List[InstalledCSVRow]:
245
+ """
246
+ :param installed: A map from archive RECORD path to installation RECORD
247
+ path.
248
+ """
249
+ installed_rows: List[InstalledCSVRow] = []
250
+ for row in old_csv_rows:
251
+ if len(row) > 3:
252
+ logger.warning("RECORD line has more than three elements: %s", row)
253
+ old_record_path = cast("RecordPath", row[0])
254
+ new_record_path = installed.pop(old_record_path, old_record_path)
255
+ if new_record_path in changed:
256
+ digest, length = rehash(_record_to_fs_path(new_record_path, lib_dir))
257
+ else:
258
+ digest = row[1] if len(row) > 1 else ""
259
+ length = row[2] if len(row) > 2 else ""
260
+ installed_rows.append((new_record_path, digest, length))
261
+ for f in generated:
262
+ path = _fs_to_record_path(f, lib_dir)
263
+ digest, length = rehash(f)
264
+ installed_rows.append((path, digest, length))
265
+ return installed_rows + [
266
+ (installed_record_path, "", "") for installed_record_path in installed.values()
267
+ ]
268
+
269
+
270
+ def get_console_script_specs(console: Dict[str, str]) -> List[str]:
271
+ """
272
+ Given the mapping from entrypoint name to callable, return the relevant
273
+ console script specs.
274
+ """
275
+ # Don't mutate caller's version
276
+ console = console.copy()
277
+
278
+ scripts_to_generate = []
279
+
280
+ # Special case pip and setuptools to generate versioned wrappers
281
+ #
282
+ # The issue is that some projects (specifically, pip and setuptools) use
283
+ # code in setup.py to create "versioned" entry points - pip2.7 on Python
284
+ # 2.7, pip3.3 on Python 3.3, etc. But these entry points are baked into
285
+ # the wheel metadata at build time, and so if the wheel is installed with
286
+ # a *different* version of Python the entry points will be wrong. The
287
+ # correct fix for this is to enhance the metadata to be able to describe
288
+ # such versioned entry points.
289
+ # Currently, projects using versioned entry points will either have
290
+ # incorrect versioned entry points, or they will not be able to distribute
291
+ # "universal" wheels (i.e., they will need a wheel per Python version).
292
+ #
293
+ # Because setuptools and pip are bundled with _ensurepip and virtualenv,
294
+ # we need to use universal wheels. As a workaround, we
295
+ # override the versioned entry points in the wheel and generate the
296
+ # correct ones.
297
+ #
298
+ # To add the level of hack in this section of code, in order to support
299
+ # ensurepip this code will look for an ``ENSUREPIP_OPTIONS`` environment
300
+ # variable which will control which version scripts get installed.
301
+ #
302
+ # ENSUREPIP_OPTIONS=altinstall
303
+ # - Only pipX.Y and easy_install-X.Y will be generated and installed
304
+ # ENSUREPIP_OPTIONS=install
305
+ # - pipX.Y, pipX, easy_install-X.Y will be generated and installed. Note
306
+ # that this option is technically if ENSUREPIP_OPTIONS is set and is
307
+ # not altinstall
308
+ # DEFAULT
309
+ # - The default behavior is to install pip, pipX, pipX.Y, easy_install
310
+ # and easy_install-X.Y.
311
+ pip_script = console.pop("pip", None)
312
+ if pip_script:
313
+ if "ENSUREPIP_OPTIONS" not in os.environ:
314
+ scripts_to_generate.append("pip = " + pip_script)
315
+
316
+ if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall":
317
+ scripts_to_generate.append(f"pip{sys.version_info[0]} = {pip_script}")
318
+
319
+ scripts_to_generate.append(f"pip{get_major_minor_version()} = {pip_script}")
320
+ # Delete any other versioned pip entry points
321
+ pip_ep = [k for k in console if re.match(r"pip(\d+(\.\d+)?)?$", k)]
322
+ for k in pip_ep:
323
+ del console[k]
324
+ easy_install_script = console.pop("easy_install", None)
325
+ if easy_install_script:
326
+ if "ENSUREPIP_OPTIONS" not in os.environ:
327
+ scripts_to_generate.append("easy_install = " + easy_install_script)
328
+
329
+ scripts_to_generate.append(
330
+ f"easy_install-{get_major_minor_version()} = {easy_install_script}"
331
+ )
332
+ # Delete any other versioned easy_install entry points
333
+ easy_install_ep = [
334
+ k for k in console if re.match(r"easy_install(-\d+\.\d+)?$", k)
335
+ ]
336
+ for k in easy_install_ep:
337
+ del console[k]
338
+
339
+ # Generate the console entry points specified in the wheel
340
+ scripts_to_generate.extend(starmap("{} = {}".format, console.items()))
341
+
342
+ return scripts_to_generate
343
+
344
+
345
+ class ZipBackedFile:
346
+ def __init__(
347
+ self, src_record_path: RecordPath, dest_path: str, zip_file: ZipFile
348
+ ) -> None:
349
+ self.src_record_path = src_record_path
350
+ self.dest_path = dest_path
351
+ self._zip_file = zip_file
352
+ self.changed = False
353
+
354
+ def _getinfo(self) -> ZipInfo:
355
+ return self._zip_file.getinfo(self.src_record_path)
356
+
357
+ def save(self) -> None:
358
+ # When we open the output file below, any existing file is truncated
359
+ # before we start writing the new contents. This is fine in most
360
+ # cases, but can cause a segfault if pip has loaded a shared
361
+ # object (e.g. from pyopenssl through its vendored urllib3)
362
+ # Since the shared object is mmap'd an attempt to call a
363
+ # symbol in it will then cause a segfault. Unlinking the file
364
+ # allows writing of new contents while allowing the process to
365
+ # continue to use the old copy.
366
+ if os.path.exists(self.dest_path):
367
+ os.unlink(self.dest_path)
368
+
369
+ zipinfo = self._getinfo()
370
+
371
+ # optimization: the file is created by open(),
372
+ # skip the decompression when there is 0 bytes to decompress.
373
+ with open(self.dest_path, "wb") as dest:
374
+ if zipinfo.file_size > 0:
375
+ with self._zip_file.open(zipinfo) as f:
376
+ blocksize = min(zipinfo.file_size, 1024 * 1024)
377
+ shutil.copyfileobj(f, dest, blocksize)
378
+
379
+ if zip_item_is_executable(zipinfo):
380
+ set_extracted_file_to_default_mode_plus_executable(self.dest_path)
381
+
382
+
383
+ class ScriptFile:
384
+ def __init__(self, file: "File") -> None:
385
+ self._file = file
386
+ self.src_record_path = self._file.src_record_path
387
+ self.dest_path = self._file.dest_path
388
+ self.changed = False
389
+
390
+ def save(self) -> None:
391
+ self._file.save()
392
+ self.changed = fix_script(self.dest_path)
393
+
394
+
395
+ class MissingCallableSuffix(InstallationError):
396
+ def __init__(self, entry_point: str) -> None:
397
+ super().__init__(
398
+ f"Invalid script entry point: {entry_point} - A callable "
399
+ "suffix is required. Cf https://packaging.python.org/"
400
+ "specifications/entry-points/#use-for-scripts for more "
401
+ "information."
402
+ )
403
+
404
+
405
+ def _raise_for_invalid_entrypoint(specification: str) -> None:
406
+ entry = get_export_entry(specification)
407
+ if entry is not None and entry.suffix is None:
408
+ raise MissingCallableSuffix(str(entry))
409
+
410
+
411
+ class PipScriptMaker(ScriptMaker):
412
+ def make(
413
+ self, specification: str, options: Optional[Dict[str, Any]] = None
414
+ ) -> List[str]:
415
+ _raise_for_invalid_entrypoint(specification)
416
+ return super().make(specification, options)
417
+
418
+
419
+ def _install_wheel( # noqa: C901, PLR0915 function is too long
420
+ name: str,
421
+ wheel_zip: ZipFile,
422
+ wheel_path: str,
423
+ scheme: Scheme,
424
+ pycompile: bool = True,
425
+ warn_script_location: bool = True,
426
+ direct_url: Optional[DirectUrl] = None,
427
+ requested: bool = False,
428
+ ) -> None:
429
+ """Install a wheel.
430
+
431
+ :param name: Name of the project to install
432
+ :param wheel_zip: open ZipFile for wheel being installed
433
+ :param scheme: Distutils scheme dictating the install directories
434
+ :param req_description: String used in place of the requirement, for
435
+ logging
436
+ :param pycompile: Whether to byte-compile installed Python files
437
+ :param warn_script_location: Whether to check that scripts are installed
438
+ into a directory on PATH
439
+ :raises UnsupportedWheel:
440
+ * when the directory holds an unpacked wheel with incompatible
441
+ Wheel-Version
442
+ * when the .dist-info dir does not match the wheel
443
+ """
444
+ info_dir, metadata = parse_wheel(wheel_zip, name)
445
+
446
+ if wheel_root_is_purelib(metadata):
447
+ lib_dir = scheme.purelib
448
+ else:
449
+ lib_dir = scheme.platlib
450
+
451
+ # Record details of the files moved
452
+ # installed = files copied from the wheel to the destination
453
+ # changed = files changed while installing (scripts #! line typically)
454
+ # generated = files newly generated during the install (script wrappers)
455
+ installed: Dict[RecordPath, RecordPath] = {}
456
+ changed: Set[RecordPath] = set()
457
+ generated: List[str] = []
458
+
459
+ def record_installed(
460
+ srcfile: RecordPath, destfile: str, modified: bool = False
461
+ ) -> None:
462
+ """Map archive RECORD paths to installation RECORD paths."""
463
+ newpath = _fs_to_record_path(destfile, lib_dir)
464
+ installed[srcfile] = newpath
465
+ if modified:
466
+ changed.add(newpath)
467
+
468
+ def is_dir_path(path: RecordPath) -> bool:
469
+ return path.endswith("/")
470
+
471
+ def assert_no_path_traversal(dest_dir_path: str, target_path: str) -> None:
472
+ if not is_within_directory(dest_dir_path, target_path):
473
+ message = (
474
+ "The wheel {!r} has a file {!r} trying to install"
475
+ " outside the target directory {!r}"
476
+ )
477
+ raise InstallationError(
478
+ message.format(wheel_path, target_path, dest_dir_path)
479
+ )
480
+
481
+ def root_scheme_file_maker(
482
+ zip_file: ZipFile, dest: str
483
+ ) -> Callable[[RecordPath], "File"]:
484
+ def make_root_scheme_file(record_path: RecordPath) -> "File":
485
+ normed_path = os.path.normpath(record_path)
486
+ dest_path = os.path.join(dest, normed_path)
487
+ assert_no_path_traversal(dest, dest_path)
488
+ return ZipBackedFile(record_path, dest_path, zip_file)
489
+
490
+ return make_root_scheme_file
491
+
492
+ def data_scheme_file_maker(
493
+ zip_file: ZipFile, scheme: Scheme
494
+ ) -> Callable[[RecordPath], "File"]:
495
+ scheme_paths = {key: getattr(scheme, key) for key in SCHEME_KEYS}
496
+
497
+ def make_data_scheme_file(record_path: RecordPath) -> "File":
498
+ normed_path = os.path.normpath(record_path)
499
+ try:
500
+ _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2)
501
+ except ValueError:
502
+ message = (
503
+ f"Unexpected file in {wheel_path}: {record_path!r}. .data directory"
504
+ " contents should be named like: '<scheme key>/<path>'."
505
+ )
506
+ raise InstallationError(message)
507
+
508
+ try:
509
+ scheme_path = scheme_paths[scheme_key]
510
+ except KeyError:
511
+ valid_scheme_keys = ", ".join(sorted(scheme_paths))
512
+ message = (
513
+ f"Unknown scheme key used in {wheel_path}: {scheme_key} "
514
+ f"(for file {record_path!r}). .data directory contents "
515
+ f"should be in subdirectories named with a valid scheme "
516
+ f"key ({valid_scheme_keys})"
517
+ )
518
+ raise InstallationError(message)
519
+
520
+ dest_path = os.path.join(scheme_path, dest_subpath)
521
+ assert_no_path_traversal(scheme_path, dest_path)
522
+ return ZipBackedFile(record_path, dest_path, zip_file)
523
+
524
+ return make_data_scheme_file
525
+
526
+ def is_data_scheme_path(path: RecordPath) -> bool:
527
+ return path.split("/", 1)[0].endswith(".data")
528
+
529
+ paths = cast(List[RecordPath], wheel_zip.namelist())
530
+ file_paths = filterfalse(is_dir_path, paths)
531
+ root_scheme_paths, data_scheme_paths = partition(is_data_scheme_path, file_paths)
532
+
533
+ make_root_scheme_file = root_scheme_file_maker(wheel_zip, lib_dir)
534
+ files: Iterator[File] = map(make_root_scheme_file, root_scheme_paths)
535
+
536
+ def is_script_scheme_path(path: RecordPath) -> bool:
537
+ parts = path.split("/", 2)
538
+ return len(parts) > 2 and parts[0].endswith(".data") and parts[1] == "scripts"
539
+
540
+ other_scheme_paths, script_scheme_paths = partition(
541
+ is_script_scheme_path, data_scheme_paths
542
+ )
543
+
544
+ make_data_scheme_file = data_scheme_file_maker(wheel_zip, scheme)
545
+ other_scheme_files = map(make_data_scheme_file, other_scheme_paths)
546
+ files = chain(files, other_scheme_files)
547
+
548
+ # Get the defined entry points
549
+ distribution = get_wheel_distribution(
550
+ FilesystemWheel(wheel_path),
551
+ canonicalize_name(name),
552
+ )
553
+ console, gui = get_entrypoints(distribution)
554
+
555
+ def is_entrypoint_wrapper(file: "File") -> bool:
556
+ # EP, EP.exe and EP-script.py are scripts generated for
557
+ # entry point EP by setuptools
558
+ path = file.dest_path
559
+ name = os.path.basename(path)
560
+ if name.lower().endswith(".exe"):
561
+ matchname = name[:-4]
562
+ elif name.lower().endswith("-script.py"):
563
+ matchname = name[:-10]
564
+ elif name.lower().endswith(".pya"):
565
+ matchname = name[:-4]
566
+ else:
567
+ matchname = name
568
+ # Ignore setuptools-generated scripts
569
+ return matchname in console or matchname in gui
570
+
571
+ script_scheme_files: Iterator[File] = map(
572
+ make_data_scheme_file, script_scheme_paths
573
+ )
574
+ script_scheme_files = filterfalse(is_entrypoint_wrapper, script_scheme_files)
575
+ script_scheme_files = map(ScriptFile, script_scheme_files)
576
+ files = chain(files, script_scheme_files)
577
+
578
+ existing_parents = set()
579
+ for file in files:
580
+ # directory creation is lazy and after file filtering
581
+ # to ensure we don't install empty dirs; empty dirs can't be
582
+ # uninstalled.
583
+ parent_dir = os.path.dirname(file.dest_path)
584
+ if parent_dir not in existing_parents:
585
+ ensure_dir(parent_dir)
586
+ existing_parents.add(parent_dir)
587
+ file.save()
588
+ record_installed(file.src_record_path, file.dest_path, file.changed)
589
+
590
+ def pyc_source_file_paths() -> Generator[str, None, None]:
591
+ # We de-duplicate installation paths, since there can be overlap (e.g.
592
+ # file in .data maps to same location as file in wheel root).
593
+ # Sorting installation paths makes it easier to reproduce and debug
594
+ # issues related to permissions on existing files.
595
+ for installed_path in sorted(set(installed.values())):
596
+ full_installed_path = os.path.join(lib_dir, installed_path)
597
+ if not os.path.isfile(full_installed_path):
598
+ continue
599
+ if not full_installed_path.endswith(".py"):
600
+ continue
601
+ yield full_installed_path
602
+
603
+ def pyc_output_path(path: str) -> str:
604
+ """Return the path the pyc file would have been written to."""
605
+ return importlib.util.cache_from_source(path)
606
+
607
+ # Compile all of the pyc files for the installed files
608
+ if pycompile:
609
+ with contextlib.redirect_stdout(
610
+ StreamWrapper.from_stream(sys.stdout)
611
+ ) as stdout:
612
+ with warnings.catch_warnings():
613
+ warnings.filterwarnings("ignore")
614
+ for path in pyc_source_file_paths():
615
+ success = compileall.compile_file(path, force=True, quiet=True)
616
+ if success:
617
+ pyc_path = pyc_output_path(path)
618
+ assert os.path.exists(pyc_path)
619
+ pyc_record_path = cast(
620
+ "RecordPath", pyc_path.replace(os.path.sep, "/")
621
+ )
622
+ record_installed(pyc_record_path, pyc_path)
623
+ logger.debug(stdout.getvalue())
624
+
625
+ maker = PipScriptMaker(None, scheme.scripts)
626
+
627
+ # Ensure old scripts are overwritten.
628
+ # See https://github.com/pypa/pip/issues/1800
629
+ maker.clobber = True
630
+
631
+ # Ensure we don't generate any variants for scripts because this is almost
632
+ # never what somebody wants.
633
+ # See https://bitbucket.org/pypa/distlib/issue/35/
634
+ maker.variants = {""}
635
+
636
+ # This is required because otherwise distlib creates scripts that are not
637
+ # executable.
638
+ # See https://bitbucket.org/pypa/distlib/issue/32/
639
+ maker.set_mode = True
640
+
641
+ # Generate the console and GUI entry points specified in the wheel
642
+ scripts_to_generate = get_console_script_specs(console)
643
+
644
+ gui_scripts_to_generate = list(starmap("{} = {}".format, gui.items()))
645
+
646
+ generated_console_scripts = maker.make_multiple(scripts_to_generate)
647
+ generated.extend(generated_console_scripts)
648
+
649
+ generated.extend(maker.make_multiple(gui_scripts_to_generate, {"gui": True}))
650
+
651
+ if warn_script_location:
652
+ msg = message_about_scripts_not_on_PATH(generated_console_scripts)
653
+ if msg is not None:
654
+ logger.warning(msg)
655
+
656
+ generated_file_mode = 0o666 & ~current_umask()
657
+
658
+ @contextlib.contextmanager
659
+ def _generate_file(path: str, **kwargs: Any) -> Generator[BinaryIO, None, None]:
660
+ with adjacent_tmp_file(path, **kwargs) as f:
661
+ yield f
662
+ os.chmod(f.name, generated_file_mode)
663
+ replace(f.name, path)
664
+
665
+ dest_info_dir = os.path.join(lib_dir, info_dir)
666
+
667
+ # Record pip as the installer
668
+ installer_path = os.path.join(dest_info_dir, "INSTALLER")
669
+ with _generate_file(installer_path) as installer_file:
670
+ installer_file.write(b"pip\n")
671
+ generated.append(installer_path)
672
+
673
+ # Record the PEP 610 direct URL reference
674
+ if direct_url is not None:
675
+ direct_url_path = os.path.join(dest_info_dir, DIRECT_URL_METADATA_NAME)
676
+ with _generate_file(direct_url_path) as direct_url_file:
677
+ direct_url_file.write(direct_url.to_json().encode("utf-8"))
678
+ generated.append(direct_url_path)
679
+
680
+ # Record the REQUESTED file
681
+ if requested:
682
+ requested_path = os.path.join(dest_info_dir, "REQUESTED")
683
+ with open(requested_path, "wb"):
684
+ pass
685
+ generated.append(requested_path)
686
+
687
+ record_text = distribution.read_text("RECORD")
688
+ record_rows = list(csv.reader(record_text.splitlines()))
689
+
690
+ rows = get_csv_rows_for_installed(
691
+ record_rows,
692
+ installed=installed,
693
+ changed=changed,
694
+ generated=generated,
695
+ lib_dir=lib_dir,
696
+ )
697
+
698
+ # Record details of all files installed
699
+ record_path = os.path.join(dest_info_dir, "RECORD")
700
+
701
+ with _generate_file(record_path, **csv_io_kwargs("w")) as record_file:
702
+ # Explicitly cast to typing.IO[str] as a workaround for the mypy error:
703
+ # "writer" has incompatible type "BinaryIO"; expected "_Writer"
704
+ writer = csv.writer(cast("IO[str]", record_file))
705
+ writer.writerows(_normalized_outrows(rows))
706
+
707
+
708
+ @contextlib.contextmanager
709
+ def req_error_context(req_description: str) -> Generator[None, None, None]:
710
+ try:
711
+ yield
712
+ except InstallationError as e:
713
+ message = f"For req: {req_description}. {e.args[0]}"
714
+ raise InstallationError(message) from e
715
+
716
+
717
+ def install_wheel(
718
+ name: str,
719
+ wheel_path: str,
720
+ scheme: Scheme,
721
+ req_description: str,
722
+ pycompile: bool = True,
723
+ warn_script_location: bool = True,
724
+ direct_url: Optional[DirectUrl] = None,
725
+ requested: bool = False,
726
+ ) -> None:
727
+ with ZipFile(wheel_path, allowZip64=True) as z:
728
+ with req_error_context(req_description):
729
+ _install_wheel(
730
+ name=name,
731
+ wheel_zip=z,
732
+ wheel_path=wheel_path,
733
+ scheme=scheme,
734
+ pycompile=pycompile,
735
+ warn_script_location=warn_script_location,
736
+ direct_url=direct_url,
737
+ requested=requested,
738
+ )
.env/lib/python3.13/site-packages/pip/_internal/operations/prepare.py ADDED
@@ -0,0 +1,737 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prepares a distribution for installation"""
2
+
3
+ # The following comment should be removed at some point in the future.
4
+ # mypy: strict-optional=False
5
+
6
+ import mimetypes
7
+ import os
8
+ import shutil
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Dict, Iterable, List, Optional
12
+
13
+ from pip._vendor.packaging.utils import canonicalize_name
14
+
15
+ from pip._internal.distributions import make_distribution_for_install_requirement
16
+ from pip._internal.distributions.installed import InstalledDistribution
17
+ from pip._internal.exceptions import (
18
+ DirectoryUrlHashUnsupported,
19
+ HashMismatch,
20
+ HashUnpinned,
21
+ InstallationError,
22
+ MetadataInconsistent,
23
+ NetworkConnectionError,
24
+ VcsHashUnsupported,
25
+ )
26
+ from pip._internal.index.package_finder import PackageFinder
27
+ from pip._internal.metadata import BaseDistribution, get_metadata_distribution
28
+ from pip._internal.models.direct_url import ArchiveInfo
29
+ from pip._internal.models.link import Link
30
+ from pip._internal.models.wheel import Wheel
31
+ from pip._internal.network.download import BatchDownloader, Downloader
32
+ from pip._internal.network.lazy_wheel import (
33
+ HTTPRangeRequestUnsupported,
34
+ dist_from_wheel_url,
35
+ )
36
+ from pip._internal.network.session import PipSession
37
+ from pip._internal.operations.build.build_tracker import BuildTracker
38
+ from pip._internal.req.req_install import InstallRequirement
39
+ from pip._internal.utils._log import getLogger
40
+ from pip._internal.utils.direct_url_helpers import (
41
+ direct_url_for_editable,
42
+ direct_url_from_link,
43
+ )
44
+ from pip._internal.utils.hashes import Hashes, MissingHashes
45
+ from pip._internal.utils.logging import indent_log
46
+ from pip._internal.utils.misc import (
47
+ display_path,
48
+ hash_file,
49
+ hide_url,
50
+ redact_auth_from_requirement,
51
+ )
52
+ from pip._internal.utils.temp_dir import TempDirectory
53
+ from pip._internal.utils.unpacking import unpack_file
54
+ from pip._internal.vcs import vcs
55
+
56
+ logger = getLogger(__name__)
57
+
58
+
59
+ def _get_prepared_distribution(
60
+ req: InstallRequirement,
61
+ build_tracker: BuildTracker,
62
+ finder: PackageFinder,
63
+ build_isolation: bool,
64
+ check_build_deps: bool,
65
+ ) -> BaseDistribution:
66
+ """Prepare a distribution for installation."""
67
+ abstract_dist = make_distribution_for_install_requirement(req)
68
+ tracker_id = abstract_dist.build_tracker_id
69
+ if tracker_id is not None:
70
+ with build_tracker.track(req, tracker_id):
71
+ abstract_dist.prepare_distribution_metadata(
72
+ finder, build_isolation, check_build_deps
73
+ )
74
+ return abstract_dist.get_metadata_distribution()
75
+
76
+
77
+ def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None:
78
+ vcs_backend = vcs.get_backend_for_scheme(link.scheme)
79
+ assert vcs_backend is not None
80
+ vcs_backend.unpack(location, url=hide_url(link.url), verbosity=verbosity)
81
+
82
+
83
+ @dataclass
84
+ class File:
85
+ path: str
86
+ content_type: Optional[str] = None
87
+
88
+ def __post_init__(self) -> None:
89
+ if self.content_type is None:
90
+ # Try to guess the file's MIME type. If the system MIME tables
91
+ # can't be loaded, give up.
92
+ try:
93
+ self.content_type = mimetypes.guess_type(self.path)[0]
94
+ except OSError:
95
+ pass
96
+
97
+
98
+ def get_http_url(
99
+ link: Link,
100
+ download: Downloader,
101
+ download_dir: Optional[str] = None,
102
+ hashes: Optional[Hashes] = None,
103
+ ) -> File:
104
+ temp_dir = TempDirectory(kind="unpack", globally_managed=True)
105
+ # If a download dir is specified, is the file already downloaded there?
106
+ already_downloaded_path = None
107
+ if download_dir:
108
+ already_downloaded_path = _check_download_dir(link, download_dir, hashes)
109
+
110
+ if already_downloaded_path:
111
+ from_path = already_downloaded_path
112
+ content_type = None
113
+ else:
114
+ # let's download to a tmp dir
115
+ from_path, content_type = download(link, temp_dir.path)
116
+ if hashes:
117
+ hashes.check_against_path(from_path)
118
+
119
+ return File(from_path, content_type)
120
+
121
+
122
+ def get_file_url(
123
+ link: Link, download_dir: Optional[str] = None, hashes: Optional[Hashes] = None
124
+ ) -> File:
125
+ """Get file and optionally check its hash."""
126
+ # If a download dir is specified, is the file already there and valid?
127
+ already_downloaded_path = None
128
+ if download_dir:
129
+ already_downloaded_path = _check_download_dir(link, download_dir, hashes)
130
+
131
+ if already_downloaded_path:
132
+ from_path = already_downloaded_path
133
+ else:
134
+ from_path = link.file_path
135
+
136
+ # If --require-hashes is off, `hashes` is either empty, the
137
+ # link's embedded hash, or MissingHashes; it is required to
138
+ # match. If --require-hashes is on, we are satisfied by any
139
+ # hash in `hashes` matching: a URL-based or an option-based
140
+ # one; no internet-sourced hash will be in `hashes`.
141
+ if hashes:
142
+ hashes.check_against_path(from_path)
143
+ return File(from_path, None)
144
+
145
+
146
+ def unpack_url(
147
+ link: Link,
148
+ location: str,
149
+ download: Downloader,
150
+ verbosity: int,
151
+ download_dir: Optional[str] = None,
152
+ hashes: Optional[Hashes] = None,
153
+ ) -> Optional[File]:
154
+ """Unpack link into location, downloading if required.
155
+
156
+ :param hashes: A Hashes object, one of whose embedded hashes must match,
157
+ or HashMismatch will be raised. If the Hashes is empty, no matches are
158
+ required, and unhashable types of requirements (like VCS ones, which
159
+ would ordinarily raise HashUnsupported) are allowed.
160
+ """
161
+ # non-editable vcs urls
162
+ if link.is_vcs:
163
+ unpack_vcs_link(link, location, verbosity=verbosity)
164
+ return None
165
+
166
+ assert not link.is_existing_dir()
167
+
168
+ # file urls
169
+ if link.is_file:
170
+ file = get_file_url(link, download_dir, hashes=hashes)
171
+
172
+ # http urls
173
+ else:
174
+ file = get_http_url(
175
+ link,
176
+ download,
177
+ download_dir,
178
+ hashes=hashes,
179
+ )
180
+
181
+ # unpack the archive to the build dir location. even when only downloading
182
+ # archives, they have to be unpacked to parse dependencies, except wheels
183
+ if not link.is_wheel:
184
+ unpack_file(file.path, location, file.content_type)
185
+
186
+ return file
187
+
188
+
189
+ def _check_download_dir(
190
+ link: Link,
191
+ download_dir: str,
192
+ hashes: Optional[Hashes],
193
+ warn_on_hash_mismatch: bool = True,
194
+ ) -> Optional[str]:
195
+ """Check download_dir for previously downloaded file with correct hash
196
+ If a correct file is found return its path else None
197
+ """
198
+ download_path = os.path.join(download_dir, link.filename)
199
+
200
+ if not os.path.exists(download_path):
201
+ return None
202
+
203
+ # If already downloaded, does its hash match?
204
+ logger.info("File was already downloaded %s", download_path)
205
+ if hashes:
206
+ try:
207
+ hashes.check_against_path(download_path)
208
+ except HashMismatch:
209
+ if warn_on_hash_mismatch:
210
+ logger.warning(
211
+ "Previously-downloaded file %s has bad hash. Re-downloading.",
212
+ download_path,
213
+ )
214
+ os.unlink(download_path)
215
+ return None
216
+ return download_path
217
+
218
+
219
+ class RequirementPreparer:
220
+ """Prepares a Requirement"""
221
+
222
+ def __init__(
223
+ self,
224
+ build_dir: str,
225
+ download_dir: Optional[str],
226
+ src_dir: str,
227
+ build_isolation: bool,
228
+ check_build_deps: bool,
229
+ build_tracker: BuildTracker,
230
+ session: PipSession,
231
+ progress_bar: str,
232
+ finder: PackageFinder,
233
+ require_hashes: bool,
234
+ use_user_site: bool,
235
+ lazy_wheel: bool,
236
+ verbosity: int,
237
+ legacy_resolver: bool,
238
+ resume_retries: int,
239
+ ) -> None:
240
+ super().__init__()
241
+
242
+ self.src_dir = src_dir
243
+ self.build_dir = build_dir
244
+ self.build_tracker = build_tracker
245
+ self._session = session
246
+ self._download = Downloader(session, progress_bar, resume_retries)
247
+ self._batch_download = BatchDownloader(session, progress_bar, resume_retries)
248
+ self.finder = finder
249
+
250
+ # Where still-packed archives should be written to. If None, they are
251
+ # not saved, and are deleted immediately after unpacking.
252
+ self.download_dir = download_dir
253
+
254
+ # Is build isolation allowed?
255
+ self.build_isolation = build_isolation
256
+
257
+ # Should check build dependencies?
258
+ self.check_build_deps = check_build_deps
259
+
260
+ # Should hash-checking be required?
261
+ self.require_hashes = require_hashes
262
+
263
+ # Should install in user site-packages?
264
+ self.use_user_site = use_user_site
265
+
266
+ # Should wheels be downloaded lazily?
267
+ self.use_lazy_wheel = lazy_wheel
268
+
269
+ # How verbose should underlying tooling be?
270
+ self.verbosity = verbosity
271
+
272
+ # Are we using the legacy resolver?
273
+ self.legacy_resolver = legacy_resolver
274
+
275
+ # Memoized downloaded files, as mapping of url: path.
276
+ self._downloaded: Dict[str, str] = {}
277
+
278
+ # Previous "header" printed for a link-based InstallRequirement
279
+ self._previous_requirement_header = ("", "")
280
+
281
+ def _log_preparing_link(self, req: InstallRequirement) -> None:
282
+ """Provide context for the requirement being prepared."""
283
+ if req.link.is_file and not req.is_wheel_from_cache:
284
+ message = "Processing %s"
285
+ information = str(display_path(req.link.file_path))
286
+ else:
287
+ message = "Collecting %s"
288
+ information = redact_auth_from_requirement(req.req) if req.req else str(req)
289
+
290
+ # If we used req.req, inject requirement source if available (this
291
+ # would already be included if we used req directly)
292
+ if req.req and req.comes_from:
293
+ if isinstance(req.comes_from, str):
294
+ comes_from: Optional[str] = req.comes_from
295
+ else:
296
+ comes_from = req.comes_from.from_path()
297
+ if comes_from:
298
+ information += f" (from {comes_from})"
299
+
300
+ if (message, information) != self._previous_requirement_header:
301
+ self._previous_requirement_header = (message, information)
302
+ logger.info(message, information)
303
+
304
+ if req.is_wheel_from_cache:
305
+ with indent_log():
306
+ logger.info("Using cached %s", req.link.filename)
307
+
308
+ def _ensure_link_req_src_dir(
309
+ self, req: InstallRequirement, parallel_builds: bool
310
+ ) -> None:
311
+ """Ensure source_dir of a linked InstallRequirement."""
312
+ # Since source_dir is only set for editable requirements.
313
+ if req.link.is_wheel:
314
+ # We don't need to unpack wheels, so no need for a source
315
+ # directory.
316
+ return
317
+ assert req.source_dir is None
318
+ if req.link.is_existing_dir():
319
+ # build local directories in-tree
320
+ req.source_dir = req.link.file_path
321
+ return
322
+
323
+ # We always delete unpacked sdists after pip runs.
324
+ req.ensure_has_source_dir(
325
+ self.build_dir,
326
+ autodelete=True,
327
+ parallel_builds=parallel_builds,
328
+ )
329
+ req.ensure_pristine_source_checkout()
330
+
331
+ def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:
332
+ # By the time this is called, the requirement's link should have
333
+ # been checked so we can tell what kind of requirements req is
334
+ # and raise some more informative errors than otherwise.
335
+ # (For example, we can raise VcsHashUnsupported for a VCS URL
336
+ # rather than HashMissing.)
337
+ if not self.require_hashes:
338
+ return req.hashes(trust_internet=True)
339
+
340
+ # We could check these first 2 conditions inside unpack_url
341
+ # and save repetition of conditions, but then we would
342
+ # report less-useful error messages for unhashable
343
+ # requirements, complaining that there's no hash provided.
344
+ if req.link.is_vcs:
345
+ raise VcsHashUnsupported()
346
+ if req.link.is_existing_dir():
347
+ raise DirectoryUrlHashUnsupported()
348
+
349
+ # Unpinned packages are asking for trouble when a new version
350
+ # is uploaded. This isn't a security check, but it saves users
351
+ # a surprising hash mismatch in the future.
352
+ # file:/// URLs aren't pinnable, so don't complain about them
353
+ # not being pinned.
354
+ if not req.is_direct and not req.is_pinned:
355
+ raise HashUnpinned()
356
+
357
+ # If known-good hashes are missing for this requirement,
358
+ # shim it with a facade object that will provoke hash
359
+ # computation and then raise a HashMissing exception
360
+ # showing the user what the hash should be.
361
+ return req.hashes(trust_internet=False) or MissingHashes()
362
+
363
+ def _fetch_metadata_only(
364
+ self,
365
+ req: InstallRequirement,
366
+ ) -> Optional[BaseDistribution]:
367
+ if self.legacy_resolver:
368
+ logger.debug(
369
+ "Metadata-only fetching is not used in the legacy resolver",
370
+ )
371
+ return None
372
+ if self.require_hashes:
373
+ logger.debug(
374
+ "Metadata-only fetching is not used as hash checking is required",
375
+ )
376
+ return None
377
+ # Try PEP 658 metadata first, then fall back to lazy wheel if unavailable.
378
+ return self._fetch_metadata_using_link_data_attr(
379
+ req
380
+ ) or self._fetch_metadata_using_lazy_wheel(req.link)
381
+
382
+ def _fetch_metadata_using_link_data_attr(
383
+ self,
384
+ req: InstallRequirement,
385
+ ) -> Optional[BaseDistribution]:
386
+ """Fetch metadata from the data-dist-info-metadata attribute, if possible."""
387
+ # (1) Get the link to the metadata file, if provided by the backend.
388
+ metadata_link = req.link.metadata_link()
389
+ if metadata_link is None:
390
+ return None
391
+ assert req.req is not None
392
+ logger.verbose(
393
+ "Obtaining dependency information for %s from %s",
394
+ req.req,
395
+ metadata_link,
396
+ )
397
+ # (2) Download the contents of the METADATA file, separate from the dist itself.
398
+ metadata_file = get_http_url(
399
+ metadata_link,
400
+ self._download,
401
+ hashes=metadata_link.as_hashes(),
402
+ )
403
+ with open(metadata_file.path, "rb") as f:
404
+ metadata_contents = f.read()
405
+ # (3) Generate a dist just from those file contents.
406
+ metadata_dist = get_metadata_distribution(
407
+ metadata_contents,
408
+ req.link.filename,
409
+ req.req.name,
410
+ )
411
+ # (4) Ensure the Name: field from the METADATA file matches the name from the
412
+ # install requirement.
413
+ #
414
+ # NB: raw_name will fall back to the name from the install requirement if
415
+ # the Name: field is not present, but it's noted in the raw_name docstring
416
+ # that that should NEVER happen anyway.
417
+ if canonicalize_name(metadata_dist.raw_name) != canonicalize_name(req.req.name):
418
+ raise MetadataInconsistent(
419
+ req, "Name", req.req.name, metadata_dist.raw_name
420
+ )
421
+ return metadata_dist
422
+
423
+ def _fetch_metadata_using_lazy_wheel(
424
+ self,
425
+ link: Link,
426
+ ) -> Optional[BaseDistribution]:
427
+ """Fetch metadata using lazy wheel, if possible."""
428
+ # --use-feature=fast-deps must be provided.
429
+ if not self.use_lazy_wheel:
430
+ return None
431
+ if link.is_file or not link.is_wheel:
432
+ logger.debug(
433
+ "Lazy wheel is not used as %r does not point to a remote wheel",
434
+ link,
435
+ )
436
+ return None
437
+
438
+ wheel = Wheel(link.filename)
439
+ name = canonicalize_name(wheel.name)
440
+ logger.info(
441
+ "Obtaining dependency information from %s %s",
442
+ name,
443
+ wheel.version,
444
+ )
445
+ url = link.url.split("#", 1)[0]
446
+ try:
447
+ return dist_from_wheel_url(name, url, self._session)
448
+ except HTTPRangeRequestUnsupported:
449
+ logger.debug("%s does not support range requests", url)
450
+ return None
451
+
452
+ def _complete_partial_requirements(
453
+ self,
454
+ partially_downloaded_reqs: Iterable[InstallRequirement],
455
+ parallel_builds: bool = False,
456
+ ) -> None:
457
+ """Download any requirements which were only fetched by metadata."""
458
+ # Download to a temporary directory. These will be copied over as
459
+ # needed for downstream 'download', 'wheel', and 'install' commands.
460
+ temp_dir = TempDirectory(kind="unpack", globally_managed=True).path
461
+
462
+ # Map each link to the requirement that owns it. This allows us to set
463
+ # `req.local_file_path` on the appropriate requirement after passing
464
+ # all the links at once into BatchDownloader.
465
+ links_to_fully_download: Dict[Link, InstallRequirement] = {}
466
+ for req in partially_downloaded_reqs:
467
+ assert req.link
468
+ links_to_fully_download[req.link] = req
469
+
470
+ batch_download = self._batch_download(
471
+ links_to_fully_download.keys(),
472
+ temp_dir,
473
+ )
474
+ for link, (filepath, _) in batch_download:
475
+ logger.debug("Downloading link %s to %s", link, filepath)
476
+ req = links_to_fully_download[link]
477
+ # Record the downloaded file path so wheel reqs can extract a Distribution
478
+ # in .get_dist().
479
+ req.local_file_path = filepath
480
+ # Record that the file is downloaded so we don't do it again in
481
+ # _prepare_linked_requirement().
482
+ self._downloaded[req.link.url] = filepath
483
+
484
+ # If this is an sdist, we need to unpack it after downloading, but the
485
+ # .source_dir won't be set up until we are in _prepare_linked_requirement().
486
+ # Add the downloaded archive to the install requirement to unpack after
487
+ # preparing the source dir.
488
+ if not req.is_wheel:
489
+ req.needs_unpacked_archive(Path(filepath))
490
+
491
+ # This step is necessary to ensure all lazy wheels are processed
492
+ # successfully by the 'download', 'wheel', and 'install' commands.
493
+ for req in partially_downloaded_reqs:
494
+ self._prepare_linked_requirement(req, parallel_builds)
495
+
496
+ def prepare_linked_requirement(
497
+ self, req: InstallRequirement, parallel_builds: bool = False
498
+ ) -> BaseDistribution:
499
+ """Prepare a requirement to be obtained from req.link."""
500
+ assert req.link
501
+ self._log_preparing_link(req)
502
+ with indent_log():
503
+ # Check if the relevant file is already available
504
+ # in the download directory
505
+ file_path = None
506
+ if self.download_dir is not None and req.link.is_wheel:
507
+ hashes = self._get_linked_req_hashes(req)
508
+ file_path = _check_download_dir(
509
+ req.link,
510
+ self.download_dir,
511
+ hashes,
512
+ # When a locally built wheel has been found in cache, we don't warn
513
+ # about re-downloading when the already downloaded wheel hash does
514
+ # not match. This is because the hash must be checked against the
515
+ # original link, not the cached link. It that case the already
516
+ # downloaded file will be removed and re-fetched from cache (which
517
+ # implies a hash check against the cache entry's origin.json).
518
+ warn_on_hash_mismatch=not req.is_wheel_from_cache,
519
+ )
520
+
521
+ if file_path is not None:
522
+ # The file is already available, so mark it as downloaded
523
+ self._downloaded[req.link.url] = file_path
524
+ else:
525
+ # The file is not available, attempt to fetch only metadata
526
+ metadata_dist = self._fetch_metadata_only(req)
527
+ if metadata_dist is not None:
528
+ req.needs_more_preparation = True
529
+ return metadata_dist
530
+
531
+ # None of the optimizations worked, fully prepare the requirement
532
+ return self._prepare_linked_requirement(req, parallel_builds)
533
+
534
+ def prepare_linked_requirements_more(
535
+ self, reqs: Iterable[InstallRequirement], parallel_builds: bool = False
536
+ ) -> None:
537
+ """Prepare linked requirements more, if needed."""
538
+ reqs = [req for req in reqs if req.needs_more_preparation]
539
+ for req in reqs:
540
+ # Determine if any of these requirements were already downloaded.
541
+ if self.download_dir is not None and req.link.is_wheel:
542
+ hashes = self._get_linked_req_hashes(req)
543
+ file_path = _check_download_dir(req.link, self.download_dir, hashes)
544
+ if file_path is not None:
545
+ self._downloaded[req.link.url] = file_path
546
+ req.needs_more_preparation = False
547
+
548
+ # Prepare requirements we found were already downloaded for some
549
+ # reason. The other downloads will be completed separately.
550
+ partially_downloaded_reqs: List[InstallRequirement] = []
551
+ for req in reqs:
552
+ if req.needs_more_preparation:
553
+ partially_downloaded_reqs.append(req)
554
+ else:
555
+ self._prepare_linked_requirement(req, parallel_builds)
556
+
557
+ # TODO: separate this part out from RequirementPreparer when the v1
558
+ # resolver can be removed!
559
+ self._complete_partial_requirements(
560
+ partially_downloaded_reqs,
561
+ parallel_builds=parallel_builds,
562
+ )
563
+
564
+ def _prepare_linked_requirement(
565
+ self, req: InstallRequirement, parallel_builds: bool
566
+ ) -> BaseDistribution:
567
+ assert req.link
568
+ link = req.link
569
+
570
+ hashes = self._get_linked_req_hashes(req)
571
+
572
+ if hashes and req.is_wheel_from_cache:
573
+ assert req.download_info is not None
574
+ assert link.is_wheel
575
+ assert link.is_file
576
+ # We need to verify hashes, and we have found the requirement in the cache
577
+ # of locally built wheels.
578
+ if (
579
+ isinstance(req.download_info.info, ArchiveInfo)
580
+ and req.download_info.info.hashes
581
+ and hashes.has_one_of(req.download_info.info.hashes)
582
+ ):
583
+ # At this point we know the requirement was built from a hashable source
584
+ # artifact, and we verified that the cache entry's hash of the original
585
+ # artifact matches one of the hashes we expect. We don't verify hashes
586
+ # against the cached wheel, because the wheel is not the original.
587
+ hashes = None
588
+ else:
589
+ logger.warning(
590
+ "The hashes of the source archive found in cache entry "
591
+ "don't match, ignoring cached built wheel "
592
+ "and re-downloading source."
593
+ )
594
+ req.link = req.cached_wheel_source_link
595
+ link = req.link
596
+
597
+ self._ensure_link_req_src_dir(req, parallel_builds)
598
+
599
+ if link.is_existing_dir():
600
+ local_file = None
601
+ elif link.url not in self._downloaded:
602
+ try:
603
+ local_file = unpack_url(
604
+ link,
605
+ req.source_dir,
606
+ self._download,
607
+ self.verbosity,
608
+ self.download_dir,
609
+ hashes,
610
+ )
611
+ except NetworkConnectionError as exc:
612
+ raise InstallationError(
613
+ f"Could not install requirement {req} because of HTTP "
614
+ f"error {exc} for URL {link}"
615
+ )
616
+ else:
617
+ file_path = self._downloaded[link.url]
618
+ if hashes:
619
+ hashes.check_against_path(file_path)
620
+ local_file = File(file_path, content_type=None)
621
+
622
+ # If download_info is set, we got it from the wheel cache.
623
+ if req.download_info is None:
624
+ # Editables don't go through this function (see
625
+ # prepare_editable_requirement).
626
+ assert not req.editable
627
+ req.download_info = direct_url_from_link(link, req.source_dir)
628
+ # Make sure we have a hash in download_info. If we got it as part of the
629
+ # URL, it will have been verified and we can rely on it. Otherwise we
630
+ # compute it from the downloaded file.
631
+ # FIXME: https://github.com/pypa/pip/issues/11943
632
+ if (
633
+ isinstance(req.download_info.info, ArchiveInfo)
634
+ and not req.download_info.info.hashes
635
+ and local_file
636
+ ):
637
+ hash = hash_file(local_file.path)[0].hexdigest()
638
+ # We populate info.hash for backward compatibility.
639
+ # This will automatically populate info.hashes.
640
+ req.download_info.info.hash = f"sha256={hash}"
641
+
642
+ # For use in later processing,
643
+ # preserve the file path on the requirement.
644
+ if local_file:
645
+ req.local_file_path = local_file.path
646
+
647
+ dist = _get_prepared_distribution(
648
+ req,
649
+ self.build_tracker,
650
+ self.finder,
651
+ self.build_isolation,
652
+ self.check_build_deps,
653
+ )
654
+ return dist
655
+
656
+ def save_linked_requirement(self, req: InstallRequirement) -> None:
657
+ assert self.download_dir is not None
658
+ assert req.link is not None
659
+ link = req.link
660
+ if link.is_vcs or (link.is_existing_dir() and req.editable):
661
+ # Make a .zip of the source_dir we already created.
662
+ req.archive(self.download_dir)
663
+ return
664
+
665
+ if link.is_existing_dir():
666
+ logger.debug(
667
+ "Not copying link to destination directory "
668
+ "since it is a directory: %s",
669
+ link,
670
+ )
671
+ return
672
+ if req.local_file_path is None:
673
+ # No distribution was downloaded for this requirement.
674
+ return
675
+
676
+ download_location = os.path.join(self.download_dir, link.filename)
677
+ if not os.path.exists(download_location):
678
+ shutil.copy(req.local_file_path, download_location)
679
+ download_path = display_path(download_location)
680
+ logger.info("Saved %s", download_path)
681
+
682
+ def prepare_editable_requirement(
683
+ self,
684
+ req: InstallRequirement,
685
+ ) -> BaseDistribution:
686
+ """Prepare an editable requirement."""
687
+ assert req.editable, "cannot prepare a non-editable req as editable"
688
+
689
+ logger.info("Obtaining %s", req)
690
+
691
+ with indent_log():
692
+ if self.require_hashes:
693
+ raise InstallationError(
694
+ f"The editable requirement {req} cannot be installed when "
695
+ "requiring hashes, because there is no single file to "
696
+ "hash."
697
+ )
698
+ req.ensure_has_source_dir(self.src_dir)
699
+ req.update_editable()
700
+ assert req.source_dir
701
+ req.download_info = direct_url_for_editable(req.unpacked_source_directory)
702
+
703
+ dist = _get_prepared_distribution(
704
+ req,
705
+ self.build_tracker,
706
+ self.finder,
707
+ self.build_isolation,
708
+ self.check_build_deps,
709
+ )
710
+
711
+ req.check_if_exists(self.use_user_site)
712
+
713
+ return dist
714
+
715
+ def prepare_installed_requirement(
716
+ self,
717
+ req: InstallRequirement,
718
+ skip_reason: str,
719
+ ) -> BaseDistribution:
720
+ """Prepare an already-installed requirement."""
721
+ assert req.satisfied_by, "req should have been satisfied but isn't"
722
+ assert skip_reason is not None, (
723
+ "did not get skip reason skipped but req.satisfied_by "
724
+ f"is set to {req.satisfied_by}"
725
+ )
726
+ logger.info(
727
+ "Requirement %s: %s (%s)", skip_reason, req, req.satisfied_by.version
728
+ )
729
+ with indent_log():
730
+ if self.require_hashes:
731
+ logger.debug(
732
+ "Since it is already installed, we are trusting this "
733
+ "package without checking its hash. To ensure a "
734
+ "completely repeatable environment, install into an "
735
+ "empty virtualenv."
736
+ )
737
+ return InstalledDistribution(req).get_metadata_distribution()
.env/lib/python3.13/site-packages/pip/_internal/pyproject.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib.util
2
+ import os
3
+ import sys
4
+ from collections import namedtuple
5
+ from typing import Any, List, Optional
6
+
7
+ if sys.version_info >= (3, 11):
8
+ import tomllib
9
+ else:
10
+ from pip._vendor import tomli as tomllib
11
+
12
+ from pip._vendor.packaging.requirements import InvalidRequirement
13
+
14
+ from pip._internal.exceptions import (
15
+ InstallationError,
16
+ InvalidPyProjectBuildRequires,
17
+ MissingPyProjectBuildRequires,
18
+ )
19
+ from pip._internal.utils.packaging import get_requirement
20
+
21
+
22
+ def _is_list_of_str(obj: Any) -> bool:
23
+ return isinstance(obj, list) and all(isinstance(item, str) for item in obj)
24
+
25
+
26
+ def make_pyproject_path(unpacked_source_directory: str) -> str:
27
+ return os.path.join(unpacked_source_directory, "pyproject.toml")
28
+
29
+
30
+ BuildSystemDetails = namedtuple(
31
+ "BuildSystemDetails", ["requires", "backend", "check", "backend_path"]
32
+ )
33
+
34
+
35
+ def load_pyproject_toml(
36
+ use_pep517: Optional[bool], pyproject_toml: str, setup_py: str, req_name: str
37
+ ) -> Optional[BuildSystemDetails]:
38
+ """Load the pyproject.toml file.
39
+
40
+ Parameters:
41
+ use_pep517 - Has the user requested PEP 517 processing? None
42
+ means the user hasn't explicitly specified.
43
+ pyproject_toml - Location of the project's pyproject.toml file
44
+ setup_py - Location of the project's setup.py file
45
+ req_name - The name of the requirement we're processing (for
46
+ error reporting)
47
+
48
+ Returns:
49
+ None if we should use the legacy code path, otherwise a tuple
50
+ (
51
+ requirements from pyproject.toml,
52
+ name of PEP 517 backend,
53
+ requirements we should check are installed after setting
54
+ up the build environment
55
+ directory paths to import the backend from (backend-path),
56
+ relative to the project root.
57
+ )
58
+ """
59
+ has_pyproject = os.path.isfile(pyproject_toml)
60
+ has_setup = os.path.isfile(setup_py)
61
+
62
+ if not has_pyproject and not has_setup:
63
+ raise InstallationError(
64
+ f"{req_name} does not appear to be a Python project: "
65
+ f"neither 'setup.py' nor 'pyproject.toml' found."
66
+ )
67
+
68
+ if has_pyproject:
69
+ with open(pyproject_toml, encoding="utf-8") as f:
70
+ pp_toml = tomllib.loads(f.read())
71
+ build_system = pp_toml.get("build-system")
72
+ else:
73
+ build_system = None
74
+
75
+ # The following cases must use PEP 517
76
+ # We check for use_pep517 being non-None and falsy because that means
77
+ # the user explicitly requested --no-use-pep517. The value 0 as
78
+ # opposed to False can occur when the value is provided via an
79
+ # environment variable or config file option (due to the quirk of
80
+ # strtobool() returning an integer in pip's configuration code).
81
+ if has_pyproject and not has_setup:
82
+ if use_pep517 is not None and not use_pep517:
83
+ raise InstallationError(
84
+ "Disabling PEP 517 processing is invalid: "
85
+ "project does not have a setup.py"
86
+ )
87
+ use_pep517 = True
88
+ elif build_system and "build-backend" in build_system:
89
+ if use_pep517 is not None and not use_pep517:
90
+ raise InstallationError(
91
+ "Disabling PEP 517 processing is invalid: "
92
+ "project specifies a build backend of {} "
93
+ "in pyproject.toml".format(build_system["build-backend"])
94
+ )
95
+ use_pep517 = True
96
+
97
+ # If we haven't worked out whether to use PEP 517 yet,
98
+ # and the user hasn't explicitly stated a preference,
99
+ # we do so if the project has a pyproject.toml file
100
+ # or if we cannot import setuptools or wheels.
101
+
102
+ # We fallback to PEP 517 when without setuptools or without the wheel package,
103
+ # so setuptools can be installed as a default build backend.
104
+ # For more info see:
105
+ # https://discuss.python.org/t/pip-without-setuptools-could-the-experience-be-improved/11810/9
106
+ # https://github.com/pypa/pip/issues/8559
107
+ elif use_pep517 is None:
108
+ use_pep517 = (
109
+ has_pyproject
110
+ or not importlib.util.find_spec("setuptools")
111
+ or not importlib.util.find_spec("wheel")
112
+ )
113
+
114
+ # At this point, we know whether we're going to use PEP 517.
115
+ assert use_pep517 is not None
116
+
117
+ # If we're using the legacy code path, there is nothing further
118
+ # for us to do here.
119
+ if not use_pep517:
120
+ return None
121
+
122
+ if build_system is None:
123
+ # Either the user has a pyproject.toml with no build-system
124
+ # section, or the user has no pyproject.toml, but has opted in
125
+ # explicitly via --use-pep517.
126
+ # In the absence of any explicit backend specification, we
127
+ # assume the setuptools backend that most closely emulates the
128
+ # traditional direct setup.py execution, and require wheel and
129
+ # a version of setuptools that supports that backend.
130
+
131
+ build_system = {
132
+ "requires": ["setuptools>=40.8.0"],
133
+ "build-backend": "setuptools.build_meta:__legacy__",
134
+ }
135
+
136
+ # If we're using PEP 517, we have build system information (either
137
+ # from pyproject.toml, or defaulted by the code above).
138
+ # Note that at this point, we do not know if the user has actually
139
+ # specified a backend, though.
140
+ assert build_system is not None
141
+
142
+ # Ensure that the build-system section in pyproject.toml conforms
143
+ # to PEP 518.
144
+
145
+ # Specifying the build-system table but not the requires key is invalid
146
+ if "requires" not in build_system:
147
+ raise MissingPyProjectBuildRequires(package=req_name)
148
+
149
+ # Error out if requires is not a list of strings
150
+ requires = build_system["requires"]
151
+ if not _is_list_of_str(requires):
152
+ raise InvalidPyProjectBuildRequires(
153
+ package=req_name,
154
+ reason="It is not a list of strings.",
155
+ )
156
+
157
+ # Each requirement must be valid as per PEP 508
158
+ for requirement in requires:
159
+ try:
160
+ get_requirement(requirement)
161
+ except InvalidRequirement as error:
162
+ raise InvalidPyProjectBuildRequires(
163
+ package=req_name,
164
+ reason=f"It contains an invalid requirement: {requirement!r}",
165
+ ) from error
166
+
167
+ backend = build_system.get("build-backend")
168
+ backend_path = build_system.get("backend-path", [])
169
+ check: List[str] = []
170
+ if backend is None:
171
+ # If the user didn't specify a backend, we assume they want to use
172
+ # the setuptools backend. But we can't be sure they have included
173
+ # a version of setuptools which supplies the backend. So we
174
+ # make a note to check that this requirement is present once
175
+ # we have set up the environment.
176
+ # This is quite a lot of work to check for a very specific case. But
177
+ # the problem is, that case is potentially quite common - projects that
178
+ # adopted PEP 518 early for the ability to specify requirements to
179
+ # execute setup.py, but never considered needing to mention the build
180
+ # tools themselves. The original PEP 518 code had a similar check (but
181
+ # implemented in a different way).
182
+ backend = "setuptools.build_meta:__legacy__"
183
+ check = ["setuptools>=40.8.0"]
184
+
185
+ return BuildSystemDetails(requires, backend, check, backend_path)
.env/lib/python3.13/site-packages/pip/_internal/req/__init__.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import collections
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from typing import Generator, List, Optional, Sequence, Tuple
5
+
6
+ from pip._internal.cli.progress_bars import get_install_progress_renderer
7
+ from pip._internal.utils.logging import indent_log
8
+
9
+ from .req_file import parse_requirements
10
+ from .req_install import InstallRequirement
11
+ from .req_set import RequirementSet
12
+
13
+ __all__ = [
14
+ "RequirementSet",
15
+ "InstallRequirement",
16
+ "parse_requirements",
17
+ "install_given_reqs",
18
+ ]
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class InstallationResult:
25
+ name: str
26
+
27
+
28
+ def _validate_requirements(
29
+ requirements: List[InstallRequirement],
30
+ ) -> Generator[Tuple[str, InstallRequirement], None, None]:
31
+ for req in requirements:
32
+ assert req.name, f"invalid to-be-installed requirement: {req}"
33
+ yield req.name, req
34
+
35
+
36
+ def install_given_reqs(
37
+ requirements: List[InstallRequirement],
38
+ global_options: Sequence[str],
39
+ root: Optional[str],
40
+ home: Optional[str],
41
+ prefix: Optional[str],
42
+ warn_script_location: bool,
43
+ use_user_site: bool,
44
+ pycompile: bool,
45
+ progress_bar: str,
46
+ ) -> List[InstallationResult]:
47
+ """
48
+ Install everything in the given list.
49
+
50
+ (to be called after having downloaded and unpacked the packages)
51
+ """
52
+ to_install = collections.OrderedDict(_validate_requirements(requirements))
53
+
54
+ if to_install:
55
+ logger.info(
56
+ "Installing collected packages: %s",
57
+ ", ".join(to_install.keys()),
58
+ )
59
+
60
+ installed = []
61
+
62
+ show_progress = logger.isEnabledFor(logging.INFO) and len(to_install) > 1
63
+
64
+ items = iter(to_install.values())
65
+ if show_progress:
66
+ renderer = get_install_progress_renderer(
67
+ bar_type=progress_bar, total=len(to_install)
68
+ )
69
+ items = renderer(items)
70
+
71
+ with indent_log():
72
+ for requirement in items:
73
+ req_name = requirement.name
74
+ assert req_name is not None
75
+ if requirement.should_reinstall:
76
+ logger.info("Attempting uninstall: %s", req_name)
77
+ with indent_log():
78
+ uninstalled_pathset = requirement.uninstall(auto_confirm=True)
79
+ else:
80
+ uninstalled_pathset = None
81
+
82
+ try:
83
+ requirement.install(
84
+ global_options,
85
+ root=root,
86
+ home=home,
87
+ prefix=prefix,
88
+ warn_script_location=warn_script_location,
89
+ use_user_site=use_user_site,
90
+ pycompile=pycompile,
91
+ )
92
+ except Exception:
93
+ # if install did not succeed, rollback previous uninstall
94
+ if uninstalled_pathset and not requirement.install_succeeded:
95
+ uninstalled_pathset.rollback()
96
+ raise
97
+ else:
98
+ if uninstalled_pathset and requirement.install_succeeded:
99
+ uninstalled_pathset.commit()
100
+
101
+ installed.append(InstallationResult(req_name))
102
+
103
+ return installed
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (4.14 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/constructors.cpython-313.pyc ADDED
Binary file (21.6 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_dependency_group.cpython-313.pyc ADDED
Binary file (4.11 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_file.cpython-313.pyc ADDED
Binary file (24.6 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_install.cpython-313.pyc ADDED
Binary file (39.4 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_set.cpython-313.pyc ADDED
Binary file (5.63 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_uninstall.cpython-313.pyc ADDED
Binary file (32.9 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/req/constructors.py ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Backing implementation for InstallRequirement's various constructors
2
+
3
+ The idea here is that these formed a major chunk of InstallRequirement's size
4
+ so, moving them and support code dedicated to them outside of that class
5
+ helps creates for better understandability for the rest of the code.
6
+
7
+ These are meant to be used elsewhere within pip to create instances of
8
+ InstallRequirement.
9
+ """
10
+
11
+ import copy
12
+ import logging
13
+ import os
14
+ import re
15
+ from dataclasses import dataclass
16
+ from typing import Collection, Dict, List, Optional, Set, Tuple, Union
17
+
18
+ from pip._vendor.packaging.markers import Marker
19
+ from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
20
+ from pip._vendor.packaging.specifiers import Specifier
21
+
22
+ from pip._internal.exceptions import InstallationError
23
+ from pip._internal.models.index import PyPI, TestPyPI
24
+ from pip._internal.models.link import Link
25
+ from pip._internal.models.wheel import Wheel
26
+ from pip._internal.req.req_file import ParsedRequirement
27
+ from pip._internal.req.req_install import InstallRequirement
28
+ from pip._internal.utils.filetypes import is_archive_file
29
+ from pip._internal.utils.misc import is_installable_dir
30
+ from pip._internal.utils.packaging import get_requirement
31
+ from pip._internal.utils.urls import path_to_url
32
+ from pip._internal.vcs import is_url, vcs
33
+
34
+ __all__ = [
35
+ "install_req_from_editable",
36
+ "install_req_from_line",
37
+ "parse_editable",
38
+ ]
39
+
40
+ logger = logging.getLogger(__name__)
41
+ operators = Specifier._operators.keys()
42
+
43
+
44
+ def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
45
+ m = re.match(r"^(.+)(\[[^\]]+\])$", path)
46
+ extras = None
47
+ if m:
48
+ path_no_extras = m.group(1)
49
+ extras = m.group(2)
50
+ else:
51
+ path_no_extras = path
52
+
53
+ return path_no_extras, extras
54
+
55
+
56
+ def convert_extras(extras: Optional[str]) -> Set[str]:
57
+ if not extras:
58
+ return set()
59
+ return get_requirement("placeholder" + extras.lower()).extras
60
+
61
+
62
+ def _set_requirement_extras(req: Requirement, new_extras: Set[str]) -> Requirement:
63
+ """
64
+ Returns a new requirement based on the given one, with the supplied extras. If the
65
+ given requirement already has extras those are replaced (or dropped if no new extras
66
+ are given).
67
+ """
68
+ match: Optional[re.Match[str]] = re.fullmatch(
69
+ # see https://peps.python.org/pep-0508/#complete-grammar
70
+ r"([\w\t .-]+)(\[[^\]]*\])?(.*)",
71
+ str(req),
72
+ flags=re.ASCII,
73
+ )
74
+ # ireq.req is a valid requirement so the regex should always match
75
+ assert (
76
+ match is not None
77
+ ), f"regex match on requirement {req} failed, this should never happen"
78
+ pre: Optional[str] = match.group(1)
79
+ post: Optional[str] = match.group(3)
80
+ assert (
81
+ pre is not None and post is not None
82
+ ), f"regex group selection for requirement {req} failed, this should never happen"
83
+ extras: str = "[{}]".format(",".join(sorted(new_extras)) if new_extras else "")
84
+ return get_requirement(f"{pre}{extras}{post}")
85
+
86
+
87
+ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
88
+ """Parses an editable requirement into:
89
+ - a requirement name
90
+ - an URL
91
+ - extras
92
+ - editable options
93
+ Accepted requirements:
94
+ svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
95
+ .[some_extra]
96
+ """
97
+
98
+ url = editable_req
99
+
100
+ # If a file path is specified with extras, strip off the extras.
101
+ url_no_extras, extras = _strip_extras(url)
102
+
103
+ if os.path.isdir(url_no_extras):
104
+ # Treating it as code that has already been checked out
105
+ url_no_extras = path_to_url(url_no_extras)
106
+
107
+ if url_no_extras.lower().startswith("file:"):
108
+ package_name = Link(url_no_extras).egg_fragment
109
+ if extras:
110
+ return (
111
+ package_name,
112
+ url_no_extras,
113
+ get_requirement("placeholder" + extras.lower()).extras,
114
+ )
115
+ else:
116
+ return package_name, url_no_extras, set()
117
+
118
+ for version_control in vcs:
119
+ if url.lower().startswith(f"{version_control}:"):
120
+ url = f"{version_control}+{url}"
121
+ break
122
+
123
+ link = Link(url)
124
+
125
+ if not link.is_vcs:
126
+ backends = ", ".join(vcs.all_schemes)
127
+ raise InstallationError(
128
+ f"{editable_req} is not a valid editable requirement. "
129
+ f"It should either be a path to a local project or a VCS URL "
130
+ f"(beginning with {backends})."
131
+ )
132
+
133
+ package_name = link.egg_fragment
134
+ if not package_name:
135
+ raise InstallationError(
136
+ f"Could not detect requirement name for '{editable_req}', "
137
+ "please specify one with #egg=your_package_name"
138
+ )
139
+ return package_name, url, set()
140
+
141
+
142
+ def check_first_requirement_in_file(filename: str) -> None:
143
+ """Check if file is parsable as a requirements file.
144
+
145
+ This is heavily based on ``pkg_resources.parse_requirements``, but
146
+ simplified to just check the first meaningful line.
147
+
148
+ :raises InvalidRequirement: If the first meaningful line cannot be parsed
149
+ as an requirement.
150
+ """
151
+ with open(filename, encoding="utf-8", errors="ignore") as f:
152
+ # Create a steppable iterator, so we can handle \-continuations.
153
+ lines = (
154
+ line
155
+ for line in (line.strip() for line in f)
156
+ if line and not line.startswith("#") # Skip blank lines/comments.
157
+ )
158
+
159
+ for line in lines:
160
+ # Drop comments -- a hash without a space may be in a URL.
161
+ if " #" in line:
162
+ line = line[: line.find(" #")]
163
+ # If there is a line continuation, drop it, and append the next line.
164
+ if line.endswith("\\"):
165
+ line = line[:-2].strip() + next(lines, "")
166
+ get_requirement(line)
167
+ return
168
+
169
+
170
+ def deduce_helpful_msg(req: str) -> str:
171
+ """Returns helpful msg in case requirements file does not exist,
172
+ or cannot be parsed.
173
+
174
+ :params req: Requirements file path
175
+ """
176
+ if not os.path.exists(req):
177
+ return f" File '{req}' does not exist."
178
+ msg = " The path does exist. "
179
+ # Try to parse and check if it is a requirements file.
180
+ try:
181
+ check_first_requirement_in_file(req)
182
+ except InvalidRequirement:
183
+ logger.debug("Cannot parse '%s' as requirements file", req)
184
+ else:
185
+ msg += (
186
+ f"The argument you provided "
187
+ f"({req}) appears to be a"
188
+ f" requirements file. If that is the"
189
+ f" case, use the '-r' flag to install"
190
+ f" the packages specified within it."
191
+ )
192
+ return msg
193
+
194
+
195
+ @dataclass(frozen=True)
196
+ class RequirementParts:
197
+ requirement: Optional[Requirement]
198
+ link: Optional[Link]
199
+ markers: Optional[Marker]
200
+ extras: Set[str]
201
+
202
+
203
+ def parse_req_from_editable(editable_req: str) -> RequirementParts:
204
+ name, url, extras_override = parse_editable(editable_req)
205
+
206
+ if name is not None:
207
+ try:
208
+ req: Optional[Requirement] = get_requirement(name)
209
+ except InvalidRequirement as exc:
210
+ raise InstallationError(f"Invalid requirement: {name!r}: {exc}")
211
+ else:
212
+ req = None
213
+
214
+ link = Link(url)
215
+
216
+ return RequirementParts(req, link, None, extras_override)
217
+
218
+
219
+ # ---- The actual constructors follow ----
220
+
221
+
222
+ def install_req_from_editable(
223
+ editable_req: str,
224
+ comes_from: Optional[Union[InstallRequirement, str]] = None,
225
+ *,
226
+ use_pep517: Optional[bool] = None,
227
+ isolated: bool = False,
228
+ global_options: Optional[List[str]] = None,
229
+ hash_options: Optional[Dict[str, List[str]]] = None,
230
+ constraint: bool = False,
231
+ user_supplied: bool = False,
232
+ permit_editable_wheels: bool = False,
233
+ config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
234
+ ) -> InstallRequirement:
235
+ parts = parse_req_from_editable(editable_req)
236
+
237
+ return InstallRequirement(
238
+ parts.requirement,
239
+ comes_from=comes_from,
240
+ user_supplied=user_supplied,
241
+ editable=True,
242
+ permit_editable_wheels=permit_editable_wheels,
243
+ link=parts.link,
244
+ constraint=constraint,
245
+ use_pep517=use_pep517,
246
+ isolated=isolated,
247
+ global_options=global_options,
248
+ hash_options=hash_options,
249
+ config_settings=config_settings,
250
+ extras=parts.extras,
251
+ )
252
+
253
+
254
+ def _looks_like_path(name: str) -> bool:
255
+ """Checks whether the string "looks like" a path on the filesystem.
256
+
257
+ This does not check whether the target actually exists, only judge from the
258
+ appearance.
259
+
260
+ Returns true if any of the following conditions is true:
261
+ * a path separator is found (either os.path.sep or os.path.altsep);
262
+ * a dot is found (which represents the current directory).
263
+ """
264
+ if os.path.sep in name:
265
+ return True
266
+ if os.path.altsep is not None and os.path.altsep in name:
267
+ return True
268
+ if name.startswith("."):
269
+ return True
270
+ return False
271
+
272
+
273
+ def _get_url_from_path(path: str, name: str) -> Optional[str]:
274
+ """
275
+ First, it checks whether a provided path is an installable directory. If it
276
+ is, returns the path.
277
+
278
+ If false, check if the path is an archive file (such as a .whl).
279
+ The function checks if the path is a file. If false, if the path has
280
+ an @, it will treat it as a PEP 440 URL requirement and return the path.
281
+ """
282
+ if _looks_like_path(name) and os.path.isdir(path):
283
+ if is_installable_dir(path):
284
+ return path_to_url(path)
285
+ # TODO: The is_installable_dir test here might not be necessary
286
+ # now that it is done in load_pyproject_toml too.
287
+ raise InstallationError(
288
+ f"Directory {name!r} is not installable. Neither 'setup.py' "
289
+ "nor 'pyproject.toml' found."
290
+ )
291
+ if not is_archive_file(path):
292
+ return None
293
+ if os.path.isfile(path):
294
+ return path_to_url(path)
295
+ urlreq_parts = name.split("@", 1)
296
+ if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]):
297
+ # If the path contains '@' and the part before it does not look
298
+ # like a path, try to treat it as a PEP 440 URL req instead.
299
+ return None
300
+ logger.warning(
301
+ "Requirement %r looks like a filename, but the file does not exist",
302
+ name,
303
+ )
304
+ return path_to_url(path)
305
+
306
+
307
+ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementParts:
308
+ if is_url(name):
309
+ marker_sep = "; "
310
+ else:
311
+ marker_sep = ";"
312
+ if marker_sep in name:
313
+ name, markers_as_string = name.split(marker_sep, 1)
314
+ markers_as_string = markers_as_string.strip()
315
+ if not markers_as_string:
316
+ markers = None
317
+ else:
318
+ markers = Marker(markers_as_string)
319
+ else:
320
+ markers = None
321
+ name = name.strip()
322
+ req_as_string = None
323
+ path = os.path.normpath(os.path.abspath(name))
324
+ link = None
325
+ extras_as_string = None
326
+
327
+ if is_url(name):
328
+ link = Link(name)
329
+ else:
330
+ p, extras_as_string = _strip_extras(path)
331
+ url = _get_url_from_path(p, name)
332
+ if url is not None:
333
+ link = Link(url)
334
+
335
+ # it's a local file, dir, or url
336
+ if link:
337
+ # Handle relative file URLs
338
+ if link.scheme == "file" and re.search(r"\.\./", link.url):
339
+ link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path))))
340
+ # wheel file
341
+ if link.is_wheel:
342
+ wheel = Wheel(link.filename) # can raise InvalidWheelFilename
343
+ req_as_string = f"{wheel.name}=={wheel.version}"
344
+ else:
345
+ # set the req to the egg fragment. when it's not there, this
346
+ # will become an 'unnamed' requirement
347
+ req_as_string = link.egg_fragment
348
+
349
+ # a requirement specifier
350
+ else:
351
+ req_as_string = name
352
+
353
+ extras = convert_extras(extras_as_string)
354
+
355
+ def with_source(text: str) -> str:
356
+ if not line_source:
357
+ return text
358
+ return f"{text} (from {line_source})"
359
+
360
+ def _parse_req_string(req_as_string: str) -> Requirement:
361
+ try:
362
+ return get_requirement(req_as_string)
363
+ except InvalidRequirement as exc:
364
+ if os.path.sep in req_as_string:
365
+ add_msg = "It looks like a path."
366
+ add_msg += deduce_helpful_msg(req_as_string)
367
+ elif "=" in req_as_string and not any(
368
+ op in req_as_string for op in operators
369
+ ):
370
+ add_msg = "= is not a valid operator. Did you mean == ?"
371
+ else:
372
+ add_msg = ""
373
+ msg = with_source(f"Invalid requirement: {req_as_string!r}: {exc}")
374
+ if add_msg:
375
+ msg += f"\nHint: {add_msg}"
376
+ raise InstallationError(msg)
377
+
378
+ if req_as_string is not None:
379
+ req: Optional[Requirement] = _parse_req_string(req_as_string)
380
+ else:
381
+ req = None
382
+
383
+ return RequirementParts(req, link, markers, extras)
384
+
385
+
386
+ def install_req_from_line(
387
+ name: str,
388
+ comes_from: Optional[Union[str, InstallRequirement]] = None,
389
+ *,
390
+ use_pep517: Optional[bool] = None,
391
+ isolated: bool = False,
392
+ global_options: Optional[List[str]] = None,
393
+ hash_options: Optional[Dict[str, List[str]]] = None,
394
+ constraint: bool = False,
395
+ line_source: Optional[str] = None,
396
+ user_supplied: bool = False,
397
+ config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
398
+ ) -> InstallRequirement:
399
+ """Creates an InstallRequirement from a name, which might be a
400
+ requirement, directory containing 'setup.py', filename, or URL.
401
+
402
+ :param line_source: An optional string describing where the line is from,
403
+ for logging purposes in case of an error.
404
+ """
405
+ parts = parse_req_from_line(name, line_source)
406
+
407
+ return InstallRequirement(
408
+ parts.requirement,
409
+ comes_from,
410
+ link=parts.link,
411
+ markers=parts.markers,
412
+ use_pep517=use_pep517,
413
+ isolated=isolated,
414
+ global_options=global_options,
415
+ hash_options=hash_options,
416
+ config_settings=config_settings,
417
+ constraint=constraint,
418
+ extras=parts.extras,
419
+ user_supplied=user_supplied,
420
+ )
421
+
422
+
423
+ def install_req_from_req_string(
424
+ req_string: str,
425
+ comes_from: Optional[InstallRequirement] = None,
426
+ isolated: bool = False,
427
+ use_pep517: Optional[bool] = None,
428
+ user_supplied: bool = False,
429
+ ) -> InstallRequirement:
430
+ try:
431
+ req = get_requirement(req_string)
432
+ except InvalidRequirement as exc:
433
+ raise InstallationError(f"Invalid requirement: {req_string!r}: {exc}")
434
+
435
+ domains_not_allowed = [
436
+ PyPI.file_storage_domain,
437
+ TestPyPI.file_storage_domain,
438
+ ]
439
+ if (
440
+ req.url
441
+ and comes_from
442
+ and comes_from.link
443
+ and comes_from.link.netloc in domains_not_allowed
444
+ ):
445
+ # Explicitly disallow pypi packages that depend on external urls
446
+ raise InstallationError(
447
+ "Packages installed from PyPI cannot depend on packages "
448
+ "which are not also hosted on PyPI.\n"
449
+ f"{comes_from.name} depends on {req} "
450
+ )
451
+
452
+ return InstallRequirement(
453
+ req,
454
+ comes_from,
455
+ isolated=isolated,
456
+ use_pep517=use_pep517,
457
+ user_supplied=user_supplied,
458
+ )
459
+
460
+
461
+ def install_req_from_parsed_requirement(
462
+ parsed_req: ParsedRequirement,
463
+ isolated: bool = False,
464
+ use_pep517: Optional[bool] = None,
465
+ user_supplied: bool = False,
466
+ config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
467
+ ) -> InstallRequirement:
468
+ if parsed_req.is_editable:
469
+ req = install_req_from_editable(
470
+ parsed_req.requirement,
471
+ comes_from=parsed_req.comes_from,
472
+ use_pep517=use_pep517,
473
+ constraint=parsed_req.constraint,
474
+ isolated=isolated,
475
+ user_supplied=user_supplied,
476
+ config_settings=config_settings,
477
+ )
478
+
479
+ else:
480
+ req = install_req_from_line(
481
+ parsed_req.requirement,
482
+ comes_from=parsed_req.comes_from,
483
+ use_pep517=use_pep517,
484
+ isolated=isolated,
485
+ global_options=(
486
+ parsed_req.options.get("global_options", [])
487
+ if parsed_req.options
488
+ else []
489
+ ),
490
+ hash_options=(
491
+ parsed_req.options.get("hashes", {}) if parsed_req.options else {}
492
+ ),
493
+ constraint=parsed_req.constraint,
494
+ line_source=parsed_req.line_source,
495
+ user_supplied=user_supplied,
496
+ config_settings=config_settings,
497
+ )
498
+ return req
499
+
500
+
501
+ def install_req_from_link_and_ireq(
502
+ link: Link, ireq: InstallRequirement
503
+ ) -> InstallRequirement:
504
+ return InstallRequirement(
505
+ req=ireq.req,
506
+ comes_from=ireq.comes_from,
507
+ editable=ireq.editable,
508
+ link=link,
509
+ markers=ireq.markers,
510
+ use_pep517=ireq.use_pep517,
511
+ isolated=ireq.isolated,
512
+ global_options=ireq.global_options,
513
+ hash_options=ireq.hash_options,
514
+ config_settings=ireq.config_settings,
515
+ user_supplied=ireq.user_supplied,
516
+ )
517
+
518
+
519
+ def install_req_drop_extras(ireq: InstallRequirement) -> InstallRequirement:
520
+ """
521
+ Creates a new InstallationRequirement using the given template but without
522
+ any extras. Sets the original requirement as the new one's parent
523
+ (comes_from).
524
+ """
525
+ return InstallRequirement(
526
+ req=(
527
+ _set_requirement_extras(ireq.req, set()) if ireq.req is not None else None
528
+ ),
529
+ comes_from=ireq,
530
+ editable=ireq.editable,
531
+ link=ireq.link,
532
+ markers=ireq.markers,
533
+ use_pep517=ireq.use_pep517,
534
+ isolated=ireq.isolated,
535
+ global_options=ireq.global_options,
536
+ hash_options=ireq.hash_options,
537
+ constraint=ireq.constraint,
538
+ extras=[],
539
+ config_settings=ireq.config_settings,
540
+ user_supplied=ireq.user_supplied,
541
+ permit_editable_wheels=ireq.permit_editable_wheels,
542
+ )
543
+
544
+
545
+ def install_req_extend_extras(
546
+ ireq: InstallRequirement,
547
+ extras: Collection[str],
548
+ ) -> InstallRequirement:
549
+ """
550
+ Returns a copy of an installation requirement with some additional extras.
551
+ Makes a shallow copy of the ireq object.
552
+ """
553
+ result = copy.copy(ireq)
554
+ result.extras = {*ireq.extras, *extras}
555
+ result.req = (
556
+ _set_requirement_extras(ireq.req, result.extras)
557
+ if ireq.req is not None
558
+ else None
559
+ )
560
+ return result
.env/lib/python3.13/site-packages/pip/_internal/req/req_dependency_group.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from typing import Any, Dict, Iterable, Iterator, List, Tuple
3
+
4
+ if sys.version_info >= (3, 11):
5
+ import tomllib
6
+ else:
7
+ from pip._vendor import tomli as tomllib
8
+
9
+ from pip._vendor.dependency_groups import DependencyGroupResolver
10
+
11
+ from pip._internal.exceptions import InstallationError
12
+
13
+
14
+ def parse_dependency_groups(groups: List[Tuple[str, str]]) -> List[str]:
15
+ """
16
+ Parse dependency groups data as provided via the CLI, in a `[path:]group` syntax.
17
+
18
+ Raises InstallationErrors if anything goes wrong.
19
+ """
20
+ resolvers = _build_resolvers(path for (path, _) in groups)
21
+ return list(_resolve_all_groups(resolvers, groups))
22
+
23
+
24
+ def _resolve_all_groups(
25
+ resolvers: Dict[str, DependencyGroupResolver], groups: List[Tuple[str, str]]
26
+ ) -> Iterator[str]:
27
+ """
28
+ Run all resolution, converting any error from `DependencyGroupResolver` into
29
+ an InstallationError.
30
+ """
31
+ for path, groupname in groups:
32
+ resolver = resolvers[path]
33
+ try:
34
+ yield from (str(req) for req in resolver.resolve(groupname))
35
+ except (ValueError, TypeError, LookupError) as e:
36
+ raise InstallationError(
37
+ f"[dependency-groups] resolution failed for '{groupname}' "
38
+ f"from '{path}': {e}"
39
+ ) from e
40
+
41
+
42
+ def _build_resolvers(paths: Iterable[str]) -> Dict[str, Any]:
43
+ resolvers = {}
44
+ for path in paths:
45
+ if path in resolvers:
46
+ continue
47
+
48
+ pyproject = _load_pyproject(path)
49
+ if "dependency-groups" not in pyproject:
50
+ raise InstallationError(
51
+ f"[dependency-groups] table was missing from '{path}'. "
52
+ "Cannot resolve '--group' option."
53
+ )
54
+ raw_dependency_groups = pyproject["dependency-groups"]
55
+ if not isinstance(raw_dependency_groups, dict):
56
+ raise InstallationError(
57
+ f"[dependency-groups] table was malformed in {path}. "
58
+ "Cannot resolve '--group' option."
59
+ )
60
+
61
+ resolvers[path] = DependencyGroupResolver(raw_dependency_groups)
62
+ return resolvers
63
+
64
+
65
+ def _load_pyproject(path: str) -> Dict[str, Any]:
66
+ """
67
+ This helper loads a pyproject.toml as TOML.
68
+
69
+ It raises an InstallationError if the operation fails.
70
+ """
71
+ try:
72
+ with open(path, "rb") as fp:
73
+ return tomllib.load(fp)
74
+ except FileNotFoundError:
75
+ raise InstallationError(f"{path} not found. Cannot resolve '--group' option.")
76
+ except tomllib.TOMLDecodeError as e:
77
+ raise InstallationError(f"Error parsing {path}: {e}") from e
78
+ except OSError as e:
79
+ raise InstallationError(f"Error reading {path}: {e}") from e
.env/lib/python3.13/site-packages/pip/_internal/req/req_file.py ADDED
@@ -0,0 +1,623 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Requirements file parsing
3
+ """
4
+
5
+ import codecs
6
+ import locale
7
+ import logging
8
+ import optparse
9
+ import os
10
+ import re
11
+ import shlex
12
+ import sys
13
+ import urllib.parse
14
+ from dataclasses import dataclass
15
+ from optparse import Values
16
+ from typing import (
17
+ TYPE_CHECKING,
18
+ Any,
19
+ Callable,
20
+ Dict,
21
+ Generator,
22
+ Iterable,
23
+ List,
24
+ NoReturn,
25
+ Optional,
26
+ Tuple,
27
+ )
28
+
29
+ from pip._internal.cli import cmdoptions
30
+ from pip._internal.exceptions import InstallationError, RequirementsFileParseError
31
+ from pip._internal.models.search_scope import SearchScope
32
+
33
+ if TYPE_CHECKING:
34
+ from pip._internal.index.package_finder import PackageFinder
35
+ from pip._internal.network.session import PipSession
36
+
37
+ __all__ = ["parse_requirements"]
38
+
39
+ ReqFileLines = Iterable[Tuple[int, str]]
40
+
41
+ LineParser = Callable[[str], Tuple[str, Values]]
42
+
43
+ SCHEME_RE = re.compile(r"^(http|https|file):", re.I)
44
+ COMMENT_RE = re.compile(r"(^|\s+)#.*$")
45
+
46
+ # Matches environment variable-style values in '${MY_VARIABLE_1}' with the
47
+ # variable name consisting of only uppercase letters, digits or the '_'
48
+ # (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
49
+ # 2013 Edition.
50
+ ENV_VAR_RE = re.compile(r"(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})")
51
+
52
+ SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
53
+ cmdoptions.index_url,
54
+ cmdoptions.extra_index_url,
55
+ cmdoptions.no_index,
56
+ cmdoptions.constraints,
57
+ cmdoptions.requirements,
58
+ cmdoptions.editable,
59
+ cmdoptions.find_links,
60
+ cmdoptions.no_binary,
61
+ cmdoptions.only_binary,
62
+ cmdoptions.prefer_binary,
63
+ cmdoptions.require_hashes,
64
+ cmdoptions.pre,
65
+ cmdoptions.trusted_host,
66
+ cmdoptions.use_new_feature,
67
+ ]
68
+
69
+ # options to be passed to requirements
70
+ SUPPORTED_OPTIONS_REQ: List[Callable[..., optparse.Option]] = [
71
+ cmdoptions.global_options,
72
+ cmdoptions.hash,
73
+ cmdoptions.config_settings,
74
+ ]
75
+
76
+ SUPPORTED_OPTIONS_EDITABLE_REQ: List[Callable[..., optparse.Option]] = [
77
+ cmdoptions.config_settings,
78
+ ]
79
+
80
+
81
+ # the 'dest' string values
82
+ SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ]
83
+ SUPPORTED_OPTIONS_EDITABLE_REQ_DEST = [
84
+ str(o().dest) for o in SUPPORTED_OPTIONS_EDITABLE_REQ
85
+ ]
86
+
87
+ # order of BOMS is important: codecs.BOM_UTF16_LE is a prefix of codecs.BOM_UTF32_LE
88
+ # so data.startswith(BOM_UTF16_LE) would be true for UTF32_LE data
89
+ BOMS: List[Tuple[bytes, str]] = [
90
+ (codecs.BOM_UTF8, "utf-8"),
91
+ (codecs.BOM_UTF32, "utf-32"),
92
+ (codecs.BOM_UTF32_BE, "utf-32-be"),
93
+ (codecs.BOM_UTF32_LE, "utf-32-le"),
94
+ (codecs.BOM_UTF16, "utf-16"),
95
+ (codecs.BOM_UTF16_BE, "utf-16-be"),
96
+ (codecs.BOM_UTF16_LE, "utf-16-le"),
97
+ ]
98
+
99
+ PEP263_ENCODING_RE = re.compile(rb"coding[:=]\s*([-\w.]+)")
100
+ DEFAULT_ENCODING = "utf-8"
101
+
102
+ logger = logging.getLogger(__name__)
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class ParsedRequirement:
107
+ # TODO: replace this with slots=True when dropping Python 3.9 support.
108
+ __slots__ = (
109
+ "requirement",
110
+ "is_editable",
111
+ "comes_from",
112
+ "constraint",
113
+ "options",
114
+ "line_source",
115
+ )
116
+
117
+ requirement: str
118
+ is_editable: bool
119
+ comes_from: str
120
+ constraint: bool
121
+ options: Optional[Dict[str, Any]]
122
+ line_source: Optional[str]
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class ParsedLine:
127
+ __slots__ = ("filename", "lineno", "args", "opts", "constraint")
128
+
129
+ filename: str
130
+ lineno: int
131
+ args: str
132
+ opts: Values
133
+ constraint: bool
134
+
135
+ @property
136
+ def is_editable(self) -> bool:
137
+ return bool(self.opts.editables)
138
+
139
+ @property
140
+ def requirement(self) -> Optional[str]:
141
+ if self.args:
142
+ return self.args
143
+ elif self.is_editable:
144
+ # We don't support multiple -e on one line
145
+ return self.opts.editables[0]
146
+ return None
147
+
148
+
149
+ def parse_requirements(
150
+ filename: str,
151
+ session: "PipSession",
152
+ finder: Optional["PackageFinder"] = None,
153
+ options: Optional[optparse.Values] = None,
154
+ constraint: bool = False,
155
+ ) -> Generator[ParsedRequirement, None, None]:
156
+ """Parse a requirements file and yield ParsedRequirement instances.
157
+
158
+ :param filename: Path or url of requirements file.
159
+ :param session: PipSession instance.
160
+ :param finder: Instance of pip.index.PackageFinder.
161
+ :param options: cli options.
162
+ :param constraint: If true, parsing a constraint file rather than
163
+ requirements file.
164
+ """
165
+ line_parser = get_line_parser(finder)
166
+ parser = RequirementsFileParser(session, line_parser)
167
+
168
+ for parsed_line in parser.parse(filename, constraint):
169
+ parsed_req = handle_line(
170
+ parsed_line, options=options, finder=finder, session=session
171
+ )
172
+ if parsed_req is not None:
173
+ yield parsed_req
174
+
175
+
176
+ def preprocess(content: str) -> ReqFileLines:
177
+ """Split, filter, and join lines, and return a line iterator
178
+
179
+ :param content: the content of the requirements file
180
+ """
181
+ lines_enum: ReqFileLines = enumerate(content.splitlines(), start=1)
182
+ lines_enum = join_lines(lines_enum)
183
+ lines_enum = ignore_comments(lines_enum)
184
+ lines_enum = expand_env_variables(lines_enum)
185
+ return lines_enum
186
+
187
+
188
+ def handle_requirement_line(
189
+ line: ParsedLine,
190
+ options: Optional[optparse.Values] = None,
191
+ ) -> ParsedRequirement:
192
+ # preserve for the nested code path
193
+ line_comes_from = "{} {} (line {})".format(
194
+ "-c" if line.constraint else "-r",
195
+ line.filename,
196
+ line.lineno,
197
+ )
198
+
199
+ assert line.requirement is not None
200
+
201
+ # get the options that apply to requirements
202
+ if line.is_editable:
203
+ supported_dest = SUPPORTED_OPTIONS_EDITABLE_REQ_DEST
204
+ else:
205
+ supported_dest = SUPPORTED_OPTIONS_REQ_DEST
206
+ req_options = {}
207
+ for dest in supported_dest:
208
+ if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
209
+ req_options[dest] = line.opts.__dict__[dest]
210
+
211
+ line_source = f"line {line.lineno} of {line.filename}"
212
+ return ParsedRequirement(
213
+ requirement=line.requirement,
214
+ is_editable=line.is_editable,
215
+ comes_from=line_comes_from,
216
+ constraint=line.constraint,
217
+ options=req_options,
218
+ line_source=line_source,
219
+ )
220
+
221
+
222
+ def handle_option_line(
223
+ opts: Values,
224
+ filename: str,
225
+ lineno: int,
226
+ finder: Optional["PackageFinder"] = None,
227
+ options: Optional[optparse.Values] = None,
228
+ session: Optional["PipSession"] = None,
229
+ ) -> None:
230
+ if opts.hashes:
231
+ logger.warning(
232
+ "%s line %s has --hash but no requirement, and will be ignored.",
233
+ filename,
234
+ lineno,
235
+ )
236
+
237
+ if options:
238
+ # percolate options upward
239
+ if opts.require_hashes:
240
+ options.require_hashes = opts.require_hashes
241
+ if opts.features_enabled:
242
+ options.features_enabled.extend(
243
+ f for f in opts.features_enabled if f not in options.features_enabled
244
+ )
245
+
246
+ # set finder options
247
+ if finder:
248
+ find_links = finder.find_links
249
+ index_urls = finder.index_urls
250
+ no_index = finder.search_scope.no_index
251
+ if opts.no_index is True:
252
+ no_index = True
253
+ index_urls = []
254
+ if opts.index_url and not no_index:
255
+ index_urls = [opts.index_url]
256
+ if opts.extra_index_urls and not no_index:
257
+ index_urls.extend(opts.extra_index_urls)
258
+ if opts.find_links:
259
+ # FIXME: it would be nice to keep track of the source
260
+ # of the find_links: support a find-links local path
261
+ # relative to a requirements file.
262
+ value = opts.find_links[0]
263
+ req_dir = os.path.dirname(os.path.abspath(filename))
264
+ relative_to_reqs_file = os.path.join(req_dir, value)
265
+ if os.path.exists(relative_to_reqs_file):
266
+ value = relative_to_reqs_file
267
+ find_links.append(value)
268
+
269
+ if session:
270
+ # We need to update the auth urls in session
271
+ session.update_index_urls(index_urls)
272
+
273
+ search_scope = SearchScope(
274
+ find_links=find_links,
275
+ index_urls=index_urls,
276
+ no_index=no_index,
277
+ )
278
+ finder.search_scope = search_scope
279
+
280
+ if opts.pre:
281
+ finder.set_allow_all_prereleases()
282
+
283
+ if opts.prefer_binary:
284
+ finder.set_prefer_binary()
285
+
286
+ if session:
287
+ for host in opts.trusted_hosts or []:
288
+ source = f"line {lineno} of {filename}"
289
+ session.add_trusted_host(host, source=source)
290
+
291
+
292
+ def handle_line(
293
+ line: ParsedLine,
294
+ options: Optional[optparse.Values] = None,
295
+ finder: Optional["PackageFinder"] = None,
296
+ session: Optional["PipSession"] = None,
297
+ ) -> Optional[ParsedRequirement]:
298
+ """Handle a single parsed requirements line; This can result in
299
+ creating/yielding requirements, or updating the finder.
300
+
301
+ :param line: The parsed line to be processed.
302
+ :param options: CLI options.
303
+ :param finder: The finder - updated by non-requirement lines.
304
+ :param session: The session - updated by non-requirement lines.
305
+
306
+ Returns a ParsedRequirement object if the line is a requirement line,
307
+ otherwise returns None.
308
+
309
+ For lines that contain requirements, the only options that have an effect
310
+ are from SUPPORTED_OPTIONS_REQ, and they are scoped to the
311
+ requirement. Other options from SUPPORTED_OPTIONS may be present, but are
312
+ ignored.
313
+
314
+ For lines that do not contain requirements, the only options that have an
315
+ effect are from SUPPORTED_OPTIONS. Options from SUPPORTED_OPTIONS_REQ may
316
+ be present, but are ignored. These lines may contain multiple options
317
+ (although our docs imply only one is supported), and all our parsed and
318
+ affect the finder.
319
+ """
320
+
321
+ if line.requirement is not None:
322
+ parsed_req = handle_requirement_line(line, options)
323
+ return parsed_req
324
+ else:
325
+ handle_option_line(
326
+ line.opts,
327
+ line.filename,
328
+ line.lineno,
329
+ finder,
330
+ options,
331
+ session,
332
+ )
333
+ return None
334
+
335
+
336
+ class RequirementsFileParser:
337
+ def __init__(
338
+ self,
339
+ session: "PipSession",
340
+ line_parser: LineParser,
341
+ ) -> None:
342
+ self._session = session
343
+ self._line_parser = line_parser
344
+
345
+ def parse(
346
+ self, filename: str, constraint: bool
347
+ ) -> Generator[ParsedLine, None, None]:
348
+ """Parse a given file, yielding parsed lines."""
349
+ yield from self._parse_and_recurse(
350
+ filename, constraint, [{os.path.abspath(filename): None}]
351
+ )
352
+
353
+ def _parse_and_recurse(
354
+ self,
355
+ filename: str,
356
+ constraint: bool,
357
+ parsed_files_stack: List[Dict[str, Optional[str]]],
358
+ ) -> Generator[ParsedLine, None, None]:
359
+ for line in self._parse_file(filename, constraint):
360
+ if line.requirement is None and (
361
+ line.opts.requirements or line.opts.constraints
362
+ ):
363
+ # parse a nested requirements file
364
+ if line.opts.requirements:
365
+ req_path = line.opts.requirements[0]
366
+ nested_constraint = False
367
+ else:
368
+ req_path = line.opts.constraints[0]
369
+ nested_constraint = True
370
+
371
+ # original file is over http
372
+ if SCHEME_RE.search(filename):
373
+ # do a url join so relative paths work
374
+ req_path = urllib.parse.urljoin(filename, req_path)
375
+ # original file and nested file are paths
376
+ elif not SCHEME_RE.search(req_path):
377
+ # do a join so relative paths work
378
+ # and then abspath so that we can identify recursive references
379
+ req_path = os.path.abspath(
380
+ os.path.join(
381
+ os.path.dirname(filename),
382
+ req_path,
383
+ )
384
+ )
385
+ parsed_files = parsed_files_stack[0]
386
+ if req_path in parsed_files:
387
+ initial_file = parsed_files[req_path]
388
+ tail = (
389
+ f" and again in {initial_file}"
390
+ if initial_file is not None
391
+ else ""
392
+ )
393
+ raise RequirementsFileParseError(
394
+ f"{req_path} recursively references itself in {filename}{tail}"
395
+ )
396
+ # Keeping a track where was each file first included in
397
+ new_parsed_files = parsed_files.copy()
398
+ new_parsed_files[req_path] = filename
399
+ yield from self._parse_and_recurse(
400
+ req_path, nested_constraint, [new_parsed_files, *parsed_files_stack]
401
+ )
402
+ else:
403
+ yield line
404
+
405
+ def _parse_file(
406
+ self, filename: str, constraint: bool
407
+ ) -> Generator[ParsedLine, None, None]:
408
+ _, content = get_file_content(filename, self._session)
409
+
410
+ lines_enum = preprocess(content)
411
+
412
+ for line_number, line in lines_enum:
413
+ try:
414
+ args_str, opts = self._line_parser(line)
415
+ except OptionParsingError as e:
416
+ # add offending line
417
+ msg = f"Invalid requirement: {line}\n{e.msg}"
418
+ raise RequirementsFileParseError(msg)
419
+
420
+ yield ParsedLine(
421
+ filename,
422
+ line_number,
423
+ args_str,
424
+ opts,
425
+ constraint,
426
+ )
427
+
428
+
429
+ def get_line_parser(finder: Optional["PackageFinder"]) -> LineParser:
430
+ def parse_line(line: str) -> Tuple[str, Values]:
431
+ # Build new parser for each line since it accumulates appendable
432
+ # options.
433
+ parser = build_parser()
434
+ defaults = parser.get_default_values()
435
+ defaults.index_url = None
436
+ if finder:
437
+ defaults.format_control = finder.format_control
438
+
439
+ args_str, options_str = break_args_options(line)
440
+
441
+ try:
442
+ options = shlex.split(options_str)
443
+ except ValueError as e:
444
+ raise OptionParsingError(f"Could not split options: {options_str}") from e
445
+
446
+ opts, _ = parser.parse_args(options, defaults)
447
+
448
+ return args_str, opts
449
+
450
+ return parse_line
451
+
452
+
453
+ def break_args_options(line: str) -> Tuple[str, str]:
454
+ """Break up the line into an args and options string. We only want to shlex
455
+ (and then optparse) the options, not the args. args can contain markers
456
+ which are corrupted by shlex.
457
+ """
458
+ tokens = line.split(" ")
459
+ args = []
460
+ options = tokens[:]
461
+ for token in tokens:
462
+ if token.startswith("-") or token.startswith("--"):
463
+ break
464
+ else:
465
+ args.append(token)
466
+ options.pop(0)
467
+ return " ".join(args), " ".join(options)
468
+
469
+
470
+ class OptionParsingError(Exception):
471
+ def __init__(self, msg: str) -> None:
472
+ self.msg = msg
473
+
474
+
475
+ def build_parser() -> optparse.OptionParser:
476
+ """
477
+ Return a parser for parsing requirement lines
478
+ """
479
+ parser = optparse.OptionParser(add_help_option=False)
480
+
481
+ option_factories = SUPPORTED_OPTIONS + SUPPORTED_OPTIONS_REQ
482
+ for option_factory in option_factories:
483
+ option = option_factory()
484
+ parser.add_option(option)
485
+
486
+ # By default optparse sys.exits on parsing errors. We want to wrap
487
+ # that in our own exception.
488
+ def parser_exit(self: Any, msg: str) -> "NoReturn":
489
+ raise OptionParsingError(msg)
490
+
491
+ # NOTE: mypy disallows assigning to a method
492
+ # https://github.com/python/mypy/issues/2427
493
+ parser.exit = parser_exit # type: ignore
494
+
495
+ return parser
496
+
497
+
498
+ def join_lines(lines_enum: ReqFileLines) -> ReqFileLines:
499
+ """Joins a line ending in '\' with the previous line (except when following
500
+ comments). The joined line takes on the index of the first line.
501
+ """
502
+ primary_line_number = None
503
+ new_line: List[str] = []
504
+ for line_number, line in lines_enum:
505
+ if not line.endswith("\\") or COMMENT_RE.match(line):
506
+ if COMMENT_RE.match(line):
507
+ # this ensures comments are always matched later
508
+ line = " " + line
509
+ if new_line:
510
+ new_line.append(line)
511
+ assert primary_line_number is not None
512
+ yield primary_line_number, "".join(new_line)
513
+ new_line = []
514
+ else:
515
+ yield line_number, line
516
+ else:
517
+ if not new_line:
518
+ primary_line_number = line_number
519
+ new_line.append(line.strip("\\"))
520
+
521
+ # last line contains \
522
+ if new_line:
523
+ assert primary_line_number is not None
524
+ yield primary_line_number, "".join(new_line)
525
+
526
+ # TODO: handle space after '\'.
527
+
528
+
529
+ def ignore_comments(lines_enum: ReqFileLines) -> ReqFileLines:
530
+ """
531
+ Strips comments and filter empty lines.
532
+ """
533
+ for line_number, line in lines_enum:
534
+ line = COMMENT_RE.sub("", line)
535
+ line = line.strip()
536
+ if line:
537
+ yield line_number, line
538
+
539
+
540
+ def expand_env_variables(lines_enum: ReqFileLines) -> ReqFileLines:
541
+ """Replace all environment variables that can be retrieved via `os.getenv`.
542
+
543
+ The only allowed format for environment variables defined in the
544
+ requirement file is `${MY_VARIABLE_1}` to ensure two things:
545
+
546
+ 1. Strings that contain a `$` aren't accidentally (partially) expanded.
547
+ 2. Ensure consistency across platforms for requirement files.
548
+
549
+ These points are the result of a discussion on the `github pull
550
+ request #3514 <https://github.com/pypa/pip/pull/3514>`_.
551
+
552
+ Valid characters in variable names follow the `POSIX standard
553
+ <http://pubs.opengroup.org/onlinepubs/9699919799/>`_ and are limited
554
+ to uppercase letter, digits and the `_` (underscore).
555
+ """
556
+ for line_number, line in lines_enum:
557
+ for env_var, var_name in ENV_VAR_RE.findall(line):
558
+ value = os.getenv(var_name)
559
+ if not value:
560
+ continue
561
+
562
+ line = line.replace(env_var, value)
563
+
564
+ yield line_number, line
565
+
566
+
567
+ def get_file_content(url: str, session: "PipSession") -> Tuple[str, str]:
568
+ """Gets the content of a file; it may be a filename, file: URL, or
569
+ http: URL. Returns (location, content). Content is unicode.
570
+ Respects # -*- coding: declarations on the retrieved files.
571
+
572
+ :param url: File path or url.
573
+ :param session: PipSession instance.
574
+ """
575
+ scheme = urllib.parse.urlsplit(url).scheme
576
+ # Pip has special support for file:// URLs (LocalFSAdapter).
577
+ if scheme in ["http", "https", "file"]:
578
+ # Delay importing heavy network modules until absolutely necessary.
579
+ from pip._internal.network.utils import raise_for_status
580
+
581
+ resp = session.get(url)
582
+ raise_for_status(resp)
583
+ return resp.url, resp.text
584
+
585
+ # Assume this is a bare path.
586
+ try:
587
+ with open(url, "rb") as f:
588
+ raw_content = f.read()
589
+ except OSError as exc:
590
+ raise InstallationError(f"Could not open requirements file: {exc}")
591
+
592
+ content = _decode_req_file(raw_content, url)
593
+
594
+ return url, content
595
+
596
+
597
+ def _decode_req_file(data: bytes, url: str) -> str:
598
+ for bom, encoding in BOMS:
599
+ if data.startswith(bom):
600
+ return data[len(bom) :].decode(encoding)
601
+
602
+ for line in data.split(b"\n")[:2]:
603
+ if line[0:1] == b"#":
604
+ result = PEP263_ENCODING_RE.search(line)
605
+ if result is not None:
606
+ encoding = result.groups()[0].decode("ascii")
607
+ return data.decode(encoding)
608
+
609
+ try:
610
+ return data.decode(DEFAULT_ENCODING)
611
+ except UnicodeDecodeError:
612
+ locale_encoding = locale.getpreferredencoding(False) or sys.getdefaultencoding()
613
+ logging.warning(
614
+ "unable to decode data from %s with default encoding %s, "
615
+ "falling back to encoding from locale: %s. "
616
+ "If this is intentional you should specify the encoding with a "
617
+ "PEP-263 style comment, e.g. '# -*- coding: %s -*-'",
618
+ url,
619
+ DEFAULT_ENCODING,
620
+ locale_encoding,
621
+ locale_encoding,
622
+ )
623
+ return data.decode(locale_encoding)
.env/lib/python3.13/site-packages/pip/_internal/req/req_install.py ADDED
@@ -0,0 +1,934 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import sys
6
+ import uuid
7
+ import zipfile
8
+ from optparse import Values
9
+ from pathlib import Path
10
+ from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
11
+
12
+ from pip._vendor.packaging.markers import Marker
13
+ from pip._vendor.packaging.requirements import Requirement
14
+ from pip._vendor.packaging.specifiers import SpecifierSet
15
+ from pip._vendor.packaging.utils import canonicalize_name
16
+ from pip._vendor.packaging.version import Version
17
+ from pip._vendor.packaging.version import parse as parse_version
18
+ from pip._vendor.pyproject_hooks import BuildBackendHookCaller
19
+
20
+ from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
21
+ from pip._internal.exceptions import InstallationError, PreviousBuildDirError
22
+ from pip._internal.locations import get_scheme
23
+ from pip._internal.metadata import (
24
+ BaseDistribution,
25
+ get_default_environment,
26
+ get_directory_distribution,
27
+ get_wheel_distribution,
28
+ )
29
+ from pip._internal.metadata.base import FilesystemWheel
30
+ from pip._internal.models.direct_url import DirectUrl
31
+ from pip._internal.models.link import Link
32
+ from pip._internal.operations.build.metadata import generate_metadata
33
+ from pip._internal.operations.build.metadata_editable import generate_editable_metadata
34
+ from pip._internal.operations.build.metadata_legacy import (
35
+ generate_metadata as generate_metadata_legacy,
36
+ )
37
+ from pip._internal.operations.install.editable_legacy import (
38
+ install_editable as install_editable_legacy,
39
+ )
40
+ from pip._internal.operations.install.wheel import install_wheel
41
+ from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
42
+ from pip._internal.req.req_uninstall import UninstallPathSet
43
+ from pip._internal.utils.deprecation import deprecated
44
+ from pip._internal.utils.hashes import Hashes
45
+ from pip._internal.utils.misc import (
46
+ ConfiguredBuildBackendHookCaller,
47
+ ask_path_exists,
48
+ backup_dir,
49
+ display_path,
50
+ hide_url,
51
+ is_installable_dir,
52
+ redact_auth_from_requirement,
53
+ redact_auth_from_url,
54
+ )
55
+ from pip._internal.utils.packaging import get_requirement
56
+ from pip._internal.utils.subprocess import runner_with_spinner_message
57
+ from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
58
+ from pip._internal.utils.unpacking import unpack_file
59
+ from pip._internal.utils.virtualenv import running_under_virtualenv
60
+ from pip._internal.vcs import vcs
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+
65
+ class InstallRequirement:
66
+ """
67
+ Represents something that may be installed later on, may have information
68
+ about where to fetch the relevant requirement and also contains logic for
69
+ installing the said requirement.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ req: Optional[Requirement],
75
+ comes_from: Optional[Union[str, "InstallRequirement"]],
76
+ editable: bool = False,
77
+ link: Optional[Link] = None,
78
+ markers: Optional[Marker] = None,
79
+ use_pep517: Optional[bool] = None,
80
+ isolated: bool = False,
81
+ *,
82
+ global_options: Optional[List[str]] = None,
83
+ hash_options: Optional[Dict[str, List[str]]] = None,
84
+ config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
85
+ constraint: bool = False,
86
+ extras: Collection[str] = (),
87
+ user_supplied: bool = False,
88
+ permit_editable_wheels: bool = False,
89
+ ) -> None:
90
+ assert req is None or isinstance(req, Requirement), req
91
+ self.req = req
92
+ self.comes_from = comes_from
93
+ self.constraint = constraint
94
+ self.editable = editable
95
+ self.permit_editable_wheels = permit_editable_wheels
96
+
97
+ # source_dir is the local directory where the linked requirement is
98
+ # located, or unpacked. In case unpacking is needed, creating and
99
+ # populating source_dir is done by the RequirementPreparer. Note this
100
+ # is not necessarily the directory where pyproject.toml or setup.py is
101
+ # located - that one is obtained via unpacked_source_directory.
102
+ self.source_dir: Optional[str] = None
103
+ if self.editable:
104
+ assert link
105
+ if link.is_file:
106
+ self.source_dir = os.path.normpath(os.path.abspath(link.file_path))
107
+
108
+ # original_link is the direct URL that was provided by the user for the
109
+ # requirement, either directly or via a constraints file.
110
+ if link is None and req and req.url:
111
+ # PEP 508 URL requirement
112
+ link = Link(req.url)
113
+ self.link = self.original_link = link
114
+
115
+ # When this InstallRequirement is a wheel obtained from the cache of locally
116
+ # built wheels, this is the source link corresponding to the cache entry, which
117
+ # was used to download and build the cached wheel.
118
+ self.cached_wheel_source_link: Optional[Link] = None
119
+
120
+ # Information about the location of the artifact that was downloaded . This
121
+ # property is guaranteed to be set in resolver results.
122
+ self.download_info: Optional[DirectUrl] = None
123
+
124
+ # Path to any downloaded or already-existing package.
125
+ self.local_file_path: Optional[str] = None
126
+ if self.link and self.link.is_file:
127
+ self.local_file_path = self.link.file_path
128
+
129
+ if extras:
130
+ self.extras = extras
131
+ elif req:
132
+ self.extras = req.extras
133
+ else:
134
+ self.extras = set()
135
+ if markers is None and req:
136
+ markers = req.marker
137
+ self.markers = markers
138
+
139
+ # This holds the Distribution object if this requirement is already installed.
140
+ self.satisfied_by: Optional[BaseDistribution] = None
141
+ # Whether the installation process should try to uninstall an existing
142
+ # distribution before installing this requirement.
143
+ self.should_reinstall = False
144
+ # Temporary build location
145
+ self._temp_build_dir: Optional[TempDirectory] = None
146
+ # Set to True after successful installation
147
+ self.install_succeeded: Optional[bool] = None
148
+ # Supplied options
149
+ self.global_options = global_options if global_options else []
150
+ self.hash_options = hash_options if hash_options else {}
151
+ self.config_settings = config_settings
152
+ # Set to True after successful preparation of this requirement
153
+ self.prepared = False
154
+ # User supplied requirement are explicitly requested for installation
155
+ # by the user via CLI arguments or requirements files, as opposed to,
156
+ # e.g. dependencies, extras or constraints.
157
+ self.user_supplied = user_supplied
158
+
159
+ self.isolated = isolated
160
+ self.build_env: BuildEnvironment = NoOpBuildEnvironment()
161
+
162
+ # For PEP 517, the directory where we request the project metadata
163
+ # gets stored. We need this to pass to build_wheel, so the backend
164
+ # can ensure that the wheel matches the metadata (see the PEP for
165
+ # details).
166
+ self.metadata_directory: Optional[str] = None
167
+
168
+ # The static build requirements (from pyproject.toml)
169
+ self.pyproject_requires: Optional[List[str]] = None
170
+
171
+ # Build requirements that we will check are available
172
+ self.requirements_to_check: List[str] = []
173
+
174
+ # The PEP 517 backend we should use to build the project
175
+ self.pep517_backend: Optional[BuildBackendHookCaller] = None
176
+
177
+ # Are we using PEP 517 for this requirement?
178
+ # After pyproject.toml has been loaded, the only valid values are True
179
+ # and False. Before loading, None is valid (meaning "use the default").
180
+ # Setting an explicit value before loading pyproject.toml is supported,
181
+ # but after loading this flag should be treated as read only.
182
+ self.use_pep517 = use_pep517
183
+
184
+ # If config settings are provided, enforce PEP 517.
185
+ if self.config_settings:
186
+ if self.use_pep517 is False:
187
+ logger.warning(
188
+ "--no-use-pep517 ignored for %s "
189
+ "because --config-settings are specified.",
190
+ self,
191
+ )
192
+ self.use_pep517 = True
193
+
194
+ # This requirement needs more preparation before it can be built
195
+ self.needs_more_preparation = False
196
+
197
+ # This requirement needs to be unpacked before it can be installed.
198
+ self._archive_source: Optional[Path] = None
199
+
200
+ def __str__(self) -> str:
201
+ if self.req:
202
+ s = redact_auth_from_requirement(self.req)
203
+ if self.link:
204
+ s += f" from {redact_auth_from_url(self.link.url)}"
205
+ elif self.link:
206
+ s = redact_auth_from_url(self.link.url)
207
+ else:
208
+ s = "<InstallRequirement>"
209
+ if self.satisfied_by is not None:
210
+ if self.satisfied_by.location is not None:
211
+ location = display_path(self.satisfied_by.location)
212
+ else:
213
+ location = "<memory>"
214
+ s += f" in {location}"
215
+ if self.comes_from:
216
+ if isinstance(self.comes_from, str):
217
+ comes_from: Optional[str] = self.comes_from
218
+ else:
219
+ comes_from = self.comes_from.from_path()
220
+ if comes_from:
221
+ s += f" (from {comes_from})"
222
+ return s
223
+
224
+ def __repr__(self) -> str:
225
+ return (
226
+ f"<{self.__class__.__name__} object: "
227
+ f"{str(self)} editable={self.editable!r}>"
228
+ )
229
+
230
+ def format_debug(self) -> str:
231
+ """An un-tested helper for getting state, for debugging."""
232
+ attributes = vars(self)
233
+ names = sorted(attributes)
234
+
235
+ state = (f"{attr}={attributes[attr]!r}" for attr in sorted(names))
236
+ return "<{name} object: {{{state}}}>".format(
237
+ name=self.__class__.__name__,
238
+ state=", ".join(state),
239
+ )
240
+
241
+ # Things that are valid for all kinds of requirements?
242
+ @property
243
+ def name(self) -> Optional[str]:
244
+ if self.req is None:
245
+ return None
246
+ return self.req.name
247
+
248
+ @functools.cached_property
249
+ def supports_pyproject_editable(self) -> bool:
250
+ if not self.use_pep517:
251
+ return False
252
+ assert self.pep517_backend
253
+ with self.build_env:
254
+ runner = runner_with_spinner_message(
255
+ "Checking if build backend supports build_editable"
256
+ )
257
+ with self.pep517_backend.subprocess_runner(runner):
258
+ return "build_editable" in self.pep517_backend._supported_features()
259
+
260
+ @property
261
+ def specifier(self) -> SpecifierSet:
262
+ assert self.req is not None
263
+ return self.req.specifier
264
+
265
+ @property
266
+ def is_direct(self) -> bool:
267
+ """Whether this requirement was specified as a direct URL."""
268
+ return self.original_link is not None
269
+
270
+ @property
271
+ def is_pinned(self) -> bool:
272
+ """Return whether I am pinned to an exact version.
273
+
274
+ For example, some-package==1.2 is pinned; some-package>1.2 is not.
275
+ """
276
+ assert self.req is not None
277
+ specifiers = self.req.specifier
278
+ return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
279
+
280
+ def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool:
281
+ if not extras_requested:
282
+ # Provide an extra to safely evaluate the markers
283
+ # without matching any extra
284
+ extras_requested = ("",)
285
+ if self.markers is not None:
286
+ return any(
287
+ self.markers.evaluate({"extra": extra}) for extra in extras_requested
288
+ )
289
+ else:
290
+ return True
291
+
292
+ @property
293
+ def has_hash_options(self) -> bool:
294
+ """Return whether any known-good hashes are specified as options.
295
+
296
+ These activate --require-hashes mode; hashes specified as part of a
297
+ URL do not.
298
+
299
+ """
300
+ return bool(self.hash_options)
301
+
302
+ def hashes(self, trust_internet: bool = True) -> Hashes:
303
+ """Return a hash-comparer that considers my option- and URL-based
304
+ hashes to be known-good.
305
+
306
+ Hashes in URLs--ones embedded in the requirements file, not ones
307
+ downloaded from an index server--are almost peers with ones from
308
+ flags. They satisfy --require-hashes (whether it was implicitly or
309
+ explicitly activated) but do not activate it. md5 and sha224 are not
310
+ allowed in flags, which should nudge people toward good algos. We
311
+ always OR all hashes together, even ones from URLs.
312
+
313
+ :param trust_internet: Whether to trust URL-based (#md5=...) hashes
314
+ downloaded from the internet, as by populate_link()
315
+
316
+ """
317
+ good_hashes = self.hash_options.copy()
318
+ if trust_internet:
319
+ link = self.link
320
+ elif self.is_direct and self.user_supplied:
321
+ link = self.original_link
322
+ else:
323
+ link = None
324
+ if link and link.hash:
325
+ assert link.hash_name is not None
326
+ good_hashes.setdefault(link.hash_name, []).append(link.hash)
327
+ return Hashes(good_hashes)
328
+
329
+ def from_path(self) -> Optional[str]:
330
+ """Format a nice indicator to show where this "comes from" """
331
+ if self.req is None:
332
+ return None
333
+ s = str(self.req)
334
+ if self.comes_from:
335
+ comes_from: Optional[str]
336
+ if isinstance(self.comes_from, str):
337
+ comes_from = self.comes_from
338
+ else:
339
+ comes_from = self.comes_from.from_path()
340
+ if comes_from:
341
+ s += "->" + comes_from
342
+ return s
343
+
344
+ def ensure_build_location(
345
+ self, build_dir: str, autodelete: bool, parallel_builds: bool
346
+ ) -> str:
347
+ assert build_dir is not None
348
+ if self._temp_build_dir is not None:
349
+ assert self._temp_build_dir.path
350
+ return self._temp_build_dir.path
351
+ if self.req is None:
352
+ # Some systems have /tmp as a symlink which confuses custom
353
+ # builds (such as numpy). Thus, we ensure that the real path
354
+ # is returned.
355
+ self._temp_build_dir = TempDirectory(
356
+ kind=tempdir_kinds.REQ_BUILD, globally_managed=True
357
+ )
358
+
359
+ return self._temp_build_dir.path
360
+
361
+ # This is the only remaining place where we manually determine the path
362
+ # for the temporary directory. It is only needed for editables where
363
+ # it is the value of the --src option.
364
+
365
+ # When parallel builds are enabled, add a UUID to the build directory
366
+ # name so multiple builds do not interfere with each other.
367
+ dir_name: str = canonicalize_name(self.req.name)
368
+ if parallel_builds:
369
+ dir_name = f"{dir_name}_{uuid.uuid4().hex}"
370
+
371
+ # FIXME: Is there a better place to create the build_dir? (hg and bzr
372
+ # need this)
373
+ if not os.path.exists(build_dir):
374
+ logger.debug("Creating directory %s", build_dir)
375
+ os.makedirs(build_dir)
376
+ actual_build_dir = os.path.join(build_dir, dir_name)
377
+ # `None` indicates that we respect the globally-configured deletion
378
+ # settings, which is what we actually want when auto-deleting.
379
+ delete_arg = None if autodelete else False
380
+ return TempDirectory(
381
+ path=actual_build_dir,
382
+ delete=delete_arg,
383
+ kind=tempdir_kinds.REQ_BUILD,
384
+ globally_managed=True,
385
+ ).path
386
+
387
+ def _set_requirement(self) -> None:
388
+ """Set requirement after generating metadata."""
389
+ assert self.req is None
390
+ assert self.metadata is not None
391
+ assert self.source_dir is not None
392
+
393
+ # Construct a Requirement object from the generated metadata
394
+ if isinstance(parse_version(self.metadata["Version"]), Version):
395
+ op = "=="
396
+ else:
397
+ op = "==="
398
+
399
+ self.req = get_requirement(
400
+ "".join(
401
+ [
402
+ self.metadata["Name"],
403
+ op,
404
+ self.metadata["Version"],
405
+ ]
406
+ )
407
+ )
408
+
409
+ def warn_on_mismatching_name(self) -> None:
410
+ assert self.req is not None
411
+ metadata_name = canonicalize_name(self.metadata["Name"])
412
+ if canonicalize_name(self.req.name) == metadata_name:
413
+ # Everything is fine.
414
+ return
415
+
416
+ # If we're here, there's a mismatch. Log a warning about it.
417
+ logger.warning(
418
+ "Generating metadata for package %s "
419
+ "produced metadata for project name %s. Fix your "
420
+ "#egg=%s fragments.",
421
+ self.name,
422
+ metadata_name,
423
+ self.name,
424
+ )
425
+ self.req = get_requirement(metadata_name)
426
+
427
+ def check_if_exists(self, use_user_site: bool) -> None:
428
+ """Find an installed distribution that satisfies or conflicts
429
+ with this requirement, and set self.satisfied_by or
430
+ self.should_reinstall appropriately.
431
+ """
432
+ if self.req is None:
433
+ return
434
+ existing_dist = get_default_environment().get_distribution(self.req.name)
435
+ if not existing_dist:
436
+ return
437
+
438
+ version_compatible = self.req.specifier.contains(
439
+ existing_dist.version,
440
+ prereleases=True,
441
+ )
442
+ if not version_compatible:
443
+ self.satisfied_by = None
444
+ if use_user_site:
445
+ if existing_dist.in_usersite:
446
+ self.should_reinstall = True
447
+ elif running_under_virtualenv() and existing_dist.in_site_packages:
448
+ raise InstallationError(
449
+ f"Will not install to the user site because it will "
450
+ f"lack sys.path precedence to {existing_dist.raw_name} "
451
+ f"in {existing_dist.location}"
452
+ )
453
+ else:
454
+ self.should_reinstall = True
455
+ else:
456
+ if self.editable:
457
+ self.should_reinstall = True
458
+ # when installing editables, nothing pre-existing should ever
459
+ # satisfy
460
+ self.satisfied_by = None
461
+ else:
462
+ self.satisfied_by = existing_dist
463
+
464
+ # Things valid for wheels
465
+ @property
466
+ def is_wheel(self) -> bool:
467
+ if not self.link:
468
+ return False
469
+ return self.link.is_wheel
470
+
471
+ @property
472
+ def is_wheel_from_cache(self) -> bool:
473
+ # When True, it means that this InstallRequirement is a local wheel file in the
474
+ # cache of locally built wheels.
475
+ return self.cached_wheel_source_link is not None
476
+
477
+ # Things valid for sdists
478
+ @property
479
+ def unpacked_source_directory(self) -> str:
480
+ assert self.source_dir, f"No source dir for {self}"
481
+ return os.path.join(
482
+ self.source_dir, self.link and self.link.subdirectory_fragment or ""
483
+ )
484
+
485
+ @property
486
+ def setup_py_path(self) -> str:
487
+ assert self.source_dir, f"No source dir for {self}"
488
+ setup_py = os.path.join(self.unpacked_source_directory, "setup.py")
489
+
490
+ return setup_py
491
+
492
+ @property
493
+ def setup_cfg_path(self) -> str:
494
+ assert self.source_dir, f"No source dir for {self}"
495
+ setup_cfg = os.path.join(self.unpacked_source_directory, "setup.cfg")
496
+
497
+ return setup_cfg
498
+
499
+ @property
500
+ def pyproject_toml_path(self) -> str:
501
+ assert self.source_dir, f"No source dir for {self}"
502
+ return make_pyproject_path(self.unpacked_source_directory)
503
+
504
+ def load_pyproject_toml(self) -> None:
505
+ """Load the pyproject.toml file.
506
+
507
+ After calling this routine, all of the attributes related to PEP 517
508
+ processing for this requirement have been set. In particular, the
509
+ use_pep517 attribute can be used to determine whether we should
510
+ follow the PEP 517 or legacy (setup.py) code path.
511
+ """
512
+ pyproject_toml_data = load_pyproject_toml(
513
+ self.use_pep517, self.pyproject_toml_path, self.setup_py_path, str(self)
514
+ )
515
+
516
+ if pyproject_toml_data is None:
517
+ assert not self.config_settings
518
+ self.use_pep517 = False
519
+ return
520
+
521
+ self.use_pep517 = True
522
+ requires, backend, check, backend_path = pyproject_toml_data
523
+ self.requirements_to_check = check
524
+ self.pyproject_requires = requires
525
+ self.pep517_backend = ConfiguredBuildBackendHookCaller(
526
+ self,
527
+ self.unpacked_source_directory,
528
+ backend,
529
+ backend_path=backend_path,
530
+ )
531
+
532
+ def isolated_editable_sanity_check(self) -> None:
533
+ """Check that an editable requirement if valid for use with PEP 517/518.
534
+
535
+ This verifies that an editable that has a pyproject.toml either supports PEP 660
536
+ or as a setup.py or a setup.cfg
537
+ """
538
+ if (
539
+ self.editable
540
+ and self.use_pep517
541
+ and not self.supports_pyproject_editable
542
+ and not os.path.isfile(self.setup_py_path)
543
+ and not os.path.isfile(self.setup_cfg_path)
544
+ ):
545
+ raise InstallationError(
546
+ f"Project {self} has a 'pyproject.toml' and its build "
547
+ f"backend is missing the 'build_editable' hook. Since it does not "
548
+ f"have a 'setup.py' nor a 'setup.cfg', "
549
+ f"it cannot be installed in editable mode. "
550
+ f"Consider using a build backend that supports PEP 660."
551
+ )
552
+
553
+ def prepare_metadata(self) -> None:
554
+ """Ensure that project metadata is available.
555
+
556
+ Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
557
+ Under legacy processing, call setup.py egg-info.
558
+ """
559
+ assert self.source_dir, f"No source dir for {self}"
560
+ details = self.name or f"from {self.link}"
561
+
562
+ if self.use_pep517:
563
+ assert self.pep517_backend is not None
564
+ if (
565
+ self.editable
566
+ and self.permit_editable_wheels
567
+ and self.supports_pyproject_editable
568
+ ):
569
+ self.metadata_directory = generate_editable_metadata(
570
+ build_env=self.build_env,
571
+ backend=self.pep517_backend,
572
+ details=details,
573
+ )
574
+ else:
575
+ self.metadata_directory = generate_metadata(
576
+ build_env=self.build_env,
577
+ backend=self.pep517_backend,
578
+ details=details,
579
+ )
580
+ else:
581
+ self.metadata_directory = generate_metadata_legacy(
582
+ build_env=self.build_env,
583
+ setup_py_path=self.setup_py_path,
584
+ source_dir=self.unpacked_source_directory,
585
+ isolated=self.isolated,
586
+ details=details,
587
+ )
588
+
589
+ # Act on the newly generated metadata, based on the name and version.
590
+ if not self.name:
591
+ self._set_requirement()
592
+ else:
593
+ self.warn_on_mismatching_name()
594
+
595
+ self.assert_source_matches_version()
596
+
597
+ @property
598
+ def metadata(self) -> Any:
599
+ if not hasattr(self, "_metadata"):
600
+ self._metadata = self.get_dist().metadata
601
+
602
+ return self._metadata
603
+
604
+ def get_dist(self) -> BaseDistribution:
605
+ if self.metadata_directory:
606
+ return get_directory_distribution(self.metadata_directory)
607
+ elif self.local_file_path and self.is_wheel:
608
+ assert self.req is not None
609
+ return get_wheel_distribution(
610
+ FilesystemWheel(self.local_file_path),
611
+ canonicalize_name(self.req.name),
612
+ )
613
+ raise AssertionError(
614
+ f"InstallRequirement {self} has no metadata directory and no wheel: "
615
+ f"can't make a distribution."
616
+ )
617
+
618
+ def assert_source_matches_version(self) -> None:
619
+ assert self.source_dir, f"No source dir for {self}"
620
+ version = self.metadata["version"]
621
+ if self.req and self.req.specifier and version not in self.req.specifier:
622
+ logger.warning(
623
+ "Requested %s, but installing version %s",
624
+ self,
625
+ version,
626
+ )
627
+ else:
628
+ logger.debug(
629
+ "Source in %s has version %s, which satisfies requirement %s",
630
+ display_path(self.source_dir),
631
+ version,
632
+ self,
633
+ )
634
+
635
+ # For both source distributions and editables
636
+ def ensure_has_source_dir(
637
+ self,
638
+ parent_dir: str,
639
+ autodelete: bool = False,
640
+ parallel_builds: bool = False,
641
+ ) -> None:
642
+ """Ensure that a source_dir is set.
643
+
644
+ This will create a temporary build dir if the name of the requirement
645
+ isn't known yet.
646
+
647
+ :param parent_dir: The ideal pip parent_dir for the source_dir.
648
+ Generally src_dir for editables and build_dir for sdists.
649
+ :return: self.source_dir
650
+ """
651
+ if self.source_dir is None:
652
+ self.source_dir = self.ensure_build_location(
653
+ parent_dir,
654
+ autodelete=autodelete,
655
+ parallel_builds=parallel_builds,
656
+ )
657
+
658
+ def needs_unpacked_archive(self, archive_source: Path) -> None:
659
+ assert self._archive_source is None
660
+ self._archive_source = archive_source
661
+
662
+ def ensure_pristine_source_checkout(self) -> None:
663
+ """Ensure the source directory has not yet been built in."""
664
+ assert self.source_dir is not None
665
+ if self._archive_source is not None:
666
+ unpack_file(str(self._archive_source), self.source_dir)
667
+ elif is_installable_dir(self.source_dir):
668
+ # If a checkout exists, it's unwise to keep going.
669
+ # version inconsistencies are logged later, but do not fail
670
+ # the installation.
671
+ raise PreviousBuildDirError(
672
+ f"pip can't proceed with requirements '{self}' due to a "
673
+ f"pre-existing build directory ({self.source_dir}). This is likely "
674
+ "due to a previous installation that failed . pip is "
675
+ "being responsible and not assuming it can delete this. "
676
+ "Please delete it and try again."
677
+ )
678
+
679
+ # For editable installations
680
+ def update_editable(self) -> None:
681
+ if not self.link:
682
+ logger.debug(
683
+ "Cannot update repository at %s; repository location is unknown",
684
+ self.source_dir,
685
+ )
686
+ return
687
+ assert self.editable
688
+ assert self.source_dir
689
+ if self.link.scheme == "file":
690
+ # Static paths don't get updated
691
+ return
692
+ vcs_backend = vcs.get_backend_for_scheme(self.link.scheme)
693
+ # Editable requirements are validated in Requirement constructors.
694
+ # So here, if it's neither a path nor a valid VCS URL, it's a bug.
695
+ assert vcs_backend, f"Unsupported VCS URL {self.link.url}"
696
+ hidden_url = hide_url(self.link.url)
697
+ vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
698
+
699
+ # Top-level Actions
700
+ def uninstall(
701
+ self, auto_confirm: bool = False, verbose: bool = False
702
+ ) -> Optional[UninstallPathSet]:
703
+ """
704
+ Uninstall the distribution currently satisfying this requirement.
705
+
706
+ Prompts before removing or modifying files unless
707
+ ``auto_confirm`` is True.
708
+
709
+ Refuses to delete or modify files outside of ``sys.prefix`` -
710
+ thus uninstallation within a virtual environment can only
711
+ modify that virtual environment, even if the virtualenv is
712
+ linked to global site-packages.
713
+
714
+ """
715
+ assert self.req
716
+ dist = get_default_environment().get_distribution(self.req.name)
717
+ if not dist:
718
+ logger.warning("Skipping %s as it is not installed.", self.name)
719
+ return None
720
+ logger.info("Found existing installation: %s", dist)
721
+
722
+ uninstalled_pathset = UninstallPathSet.from_dist(dist)
723
+ uninstalled_pathset.remove(auto_confirm, verbose)
724
+ return uninstalled_pathset
725
+
726
+ def _get_archive_name(self, path: str, parentdir: str, rootdir: str) -> str:
727
+ def _clean_zip_name(name: str, prefix: str) -> str:
728
+ assert name.startswith(
729
+ prefix + os.path.sep
730
+ ), f"name {name!r} doesn't start with prefix {prefix!r}"
731
+ name = name[len(prefix) + 1 :]
732
+ name = name.replace(os.path.sep, "/")
733
+ return name
734
+
735
+ assert self.req is not None
736
+ path = os.path.join(parentdir, path)
737
+ name = _clean_zip_name(path, rootdir)
738
+ return self.req.name + "/" + name
739
+
740
+ def archive(self, build_dir: Optional[str]) -> None:
741
+ """Saves archive to provided build_dir.
742
+
743
+ Used for saving downloaded VCS requirements as part of `pip download`.
744
+ """
745
+ assert self.source_dir
746
+ if build_dir is None:
747
+ return
748
+
749
+ create_archive = True
750
+ archive_name = "{}-{}.zip".format(self.name, self.metadata["version"])
751
+ archive_path = os.path.join(build_dir, archive_name)
752
+
753
+ if os.path.exists(archive_path):
754
+ response = ask_path_exists(
755
+ f"The file {display_path(archive_path)} exists. (i)gnore, (w)ipe, "
756
+ "(b)ackup, (a)bort ",
757
+ ("i", "w", "b", "a"),
758
+ )
759
+ if response == "i":
760
+ create_archive = False
761
+ elif response == "w":
762
+ logger.warning("Deleting %s", display_path(archive_path))
763
+ os.remove(archive_path)
764
+ elif response == "b":
765
+ dest_file = backup_dir(archive_path)
766
+ logger.warning(
767
+ "Backing up %s to %s",
768
+ display_path(archive_path),
769
+ display_path(dest_file),
770
+ )
771
+ shutil.move(archive_path, dest_file)
772
+ elif response == "a":
773
+ sys.exit(-1)
774
+
775
+ if not create_archive:
776
+ return
777
+
778
+ zip_output = zipfile.ZipFile(
779
+ archive_path,
780
+ "w",
781
+ zipfile.ZIP_DEFLATED,
782
+ allowZip64=True,
783
+ )
784
+ with zip_output:
785
+ dir = os.path.normcase(os.path.abspath(self.unpacked_source_directory))
786
+ for dirpath, dirnames, filenames in os.walk(dir):
787
+ for dirname in dirnames:
788
+ dir_arcname = self._get_archive_name(
789
+ dirname,
790
+ parentdir=dirpath,
791
+ rootdir=dir,
792
+ )
793
+ zipdir = zipfile.ZipInfo(dir_arcname + "/")
794
+ zipdir.external_attr = 0x1ED << 16 # 0o755
795
+ zip_output.writestr(zipdir, "")
796
+ for filename in filenames:
797
+ file_arcname = self._get_archive_name(
798
+ filename,
799
+ parentdir=dirpath,
800
+ rootdir=dir,
801
+ )
802
+ filename = os.path.join(dirpath, filename)
803
+ zip_output.write(filename, file_arcname)
804
+
805
+ logger.info("Saved %s", display_path(archive_path))
806
+
807
+ def install(
808
+ self,
809
+ global_options: Optional[Sequence[str]] = None,
810
+ root: Optional[str] = None,
811
+ home: Optional[str] = None,
812
+ prefix: Optional[str] = None,
813
+ warn_script_location: bool = True,
814
+ use_user_site: bool = False,
815
+ pycompile: bool = True,
816
+ ) -> None:
817
+ assert self.req is not None
818
+ scheme = get_scheme(
819
+ self.req.name,
820
+ user=use_user_site,
821
+ home=home,
822
+ root=root,
823
+ isolated=self.isolated,
824
+ prefix=prefix,
825
+ )
826
+
827
+ if self.editable and not self.is_wheel:
828
+ deprecated(
829
+ reason=(
830
+ f"Legacy editable install of {self} (setup.py develop) "
831
+ "is deprecated."
832
+ ),
833
+ replacement=(
834
+ "to add a pyproject.toml or enable --use-pep517, "
835
+ "and use setuptools >= 64. "
836
+ "If the resulting installation is not behaving as expected, "
837
+ "try using --config-settings editable_mode=compat. "
838
+ "Please consult the setuptools documentation for more information"
839
+ ),
840
+ gone_in="25.3",
841
+ issue=11457,
842
+ )
843
+ if self.config_settings:
844
+ logger.warning(
845
+ "--config-settings ignored for legacy editable install of %s. "
846
+ "Consider upgrading to a version of setuptools "
847
+ "that supports PEP 660 (>= 64).",
848
+ self,
849
+ )
850
+ install_editable_legacy(
851
+ global_options=global_options if global_options is not None else [],
852
+ prefix=prefix,
853
+ home=home,
854
+ use_user_site=use_user_site,
855
+ name=self.req.name,
856
+ setup_py_path=self.setup_py_path,
857
+ isolated=self.isolated,
858
+ build_env=self.build_env,
859
+ unpacked_source_directory=self.unpacked_source_directory,
860
+ )
861
+ self.install_succeeded = True
862
+ return
863
+
864
+ assert self.is_wheel
865
+ assert self.local_file_path
866
+
867
+ install_wheel(
868
+ self.req.name,
869
+ self.local_file_path,
870
+ scheme=scheme,
871
+ req_description=str(self.req),
872
+ pycompile=pycompile,
873
+ warn_script_location=warn_script_location,
874
+ direct_url=self.download_info if self.is_direct else None,
875
+ requested=self.user_supplied,
876
+ )
877
+ self.install_succeeded = True
878
+
879
+
880
+ def check_invalid_constraint_type(req: InstallRequirement) -> str:
881
+ # Check for unsupported forms
882
+ problem = ""
883
+ if not req.name:
884
+ problem = "Unnamed requirements are not allowed as constraints"
885
+ elif req.editable:
886
+ problem = "Editable requirements are not allowed as constraints"
887
+ elif req.extras:
888
+ problem = "Constraints cannot have extras"
889
+
890
+ if problem:
891
+ deprecated(
892
+ reason=(
893
+ "Constraints are only allowed to take the form of a package "
894
+ "name and a version specifier. Other forms were originally "
895
+ "permitted as an accident of the implementation, but were "
896
+ "undocumented. The new implementation of the resolver no "
897
+ "longer supports these forms."
898
+ ),
899
+ replacement="replacing the constraint with a requirement",
900
+ # No plan yet for when the new resolver becomes default
901
+ gone_in=None,
902
+ issue=8210,
903
+ )
904
+
905
+ return problem
906
+
907
+
908
+ def _has_option(options: Values, reqs: List[InstallRequirement], option: str) -> bool:
909
+ if getattr(options, option, None):
910
+ return True
911
+ for req in reqs:
912
+ if getattr(req, option, None):
913
+ return True
914
+ return False
915
+
916
+
917
+ def check_legacy_setup_py_options(
918
+ options: Values,
919
+ reqs: List[InstallRequirement],
920
+ ) -> None:
921
+ has_build_options = _has_option(options, reqs, "build_options")
922
+ has_global_options = _has_option(options, reqs, "global_options")
923
+ if has_build_options or has_global_options:
924
+ deprecated(
925
+ reason="--build-option and --global-option are deprecated.",
926
+ issue=11859,
927
+ replacement="to use --config-settings",
928
+ gone_in="25.3",
929
+ )
930
+ logger.warning(
931
+ "Implying --no-binary=:all: due to the presence of "
932
+ "--build-option / --global-option. "
933
+ )
934
+ options.format_control.disallow_binaries()
.env/lib/python3.13/site-packages/pip/_internal/req/req_set.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from collections import OrderedDict
3
+ from typing import Dict, List
4
+
5
+ from pip._vendor.packaging.utils import canonicalize_name
6
+
7
+ from pip._internal.req.req_install import InstallRequirement
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class RequirementSet:
13
+ def __init__(self, check_supported_wheels: bool = True) -> None:
14
+ """Create a RequirementSet."""
15
+
16
+ self.requirements: Dict[str, InstallRequirement] = OrderedDict()
17
+ self.check_supported_wheels = check_supported_wheels
18
+
19
+ self.unnamed_requirements: List[InstallRequirement] = []
20
+
21
+ def __str__(self) -> str:
22
+ requirements = sorted(
23
+ (req for req in self.requirements.values() if not req.comes_from),
24
+ key=lambda req: canonicalize_name(req.name or ""),
25
+ )
26
+ return " ".join(str(req.req) for req in requirements)
27
+
28
+ def __repr__(self) -> str:
29
+ requirements = sorted(
30
+ self.requirements.values(),
31
+ key=lambda req: canonicalize_name(req.name or ""),
32
+ )
33
+
34
+ format_string = "<{classname} object; {count} requirement(s): {reqs}>"
35
+ return format_string.format(
36
+ classname=self.__class__.__name__,
37
+ count=len(requirements),
38
+ reqs=", ".join(str(req.req) for req in requirements),
39
+ )
40
+
41
+ def add_unnamed_requirement(self, install_req: InstallRequirement) -> None:
42
+ assert not install_req.name
43
+ self.unnamed_requirements.append(install_req)
44
+
45
+ def add_named_requirement(self, install_req: InstallRequirement) -> None:
46
+ assert install_req.name
47
+
48
+ project_name = canonicalize_name(install_req.name)
49
+ self.requirements[project_name] = install_req
50
+
51
+ def has_requirement(self, name: str) -> bool:
52
+ project_name = canonicalize_name(name)
53
+
54
+ return (
55
+ project_name in self.requirements
56
+ and not self.requirements[project_name].constraint
57
+ )
58
+
59
+ def get_requirement(self, name: str) -> InstallRequirement:
60
+ project_name = canonicalize_name(name)
61
+
62
+ if project_name in self.requirements:
63
+ return self.requirements[project_name]
64
+
65
+ raise KeyError(f"No project with the name {name!r}")
66
+
67
+ @property
68
+ def all_requirements(self) -> List[InstallRequirement]:
69
+ return self.unnamed_requirements + list(self.requirements.values())
70
+
71
+ @property
72
+ def requirements_to_install(self) -> List[InstallRequirement]:
73
+ """Return the list of requirements that need to be installed.
74
+
75
+ TODO remove this property together with the legacy resolver, since the new
76
+ resolver only returns requirements that need to be installed.
77
+ """
78
+ return [
79
+ install_req
80
+ for install_req in self.all_requirements
81
+ if not install_req.constraint and not install_req.satisfied_by
82
+ ]
.env/lib/python3.13/site-packages/pip/_internal/req/req_uninstall.py ADDED
@@ -0,0 +1,636 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import os
3
+ import sys
4
+ import sysconfig
5
+ from importlib.util import cache_from_source
6
+ from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Set, Tuple
7
+
8
+ from pip._internal.exceptions import LegacyDistutilsInstall, UninstallMissingRecord
9
+ from pip._internal.locations import get_bin_prefix, get_bin_user
10
+ from pip._internal.metadata import BaseDistribution
11
+ from pip._internal.utils.compat import WINDOWS
12
+ from pip._internal.utils.egg_link import egg_link_path_from_location
13
+ from pip._internal.utils.logging import getLogger, indent_log
14
+ from pip._internal.utils.misc import ask, normalize_path, renames, rmtree
15
+ from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
16
+ from pip._internal.utils.virtualenv import running_under_virtualenv
17
+
18
+ logger = getLogger(__name__)
19
+
20
+
21
+ def _script_names(
22
+ bin_dir: str, script_name: str, is_gui: bool
23
+ ) -> Generator[str, None, None]:
24
+ """Create the fully qualified name of the files created by
25
+ {console,gui}_scripts for the given ``dist``.
26
+ Returns the list of file names
27
+ """
28
+ exe_name = os.path.join(bin_dir, script_name)
29
+ yield exe_name
30
+ if not WINDOWS:
31
+ return
32
+ yield f"{exe_name}.exe"
33
+ yield f"{exe_name}.exe.manifest"
34
+ if is_gui:
35
+ yield f"{exe_name}-script.pyw"
36
+ else:
37
+ yield f"{exe_name}-script.py"
38
+
39
+
40
+ def _unique(
41
+ fn: Callable[..., Generator[Any, None, None]],
42
+ ) -> Callable[..., Generator[Any, None, None]]:
43
+ @functools.wraps(fn)
44
+ def unique(*args: Any, **kw: Any) -> Generator[Any, None, None]:
45
+ seen: Set[Any] = set()
46
+ for item in fn(*args, **kw):
47
+ if item not in seen:
48
+ seen.add(item)
49
+ yield item
50
+
51
+ return unique
52
+
53
+
54
+ @_unique
55
+ def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
56
+ """
57
+ Yield all the uninstallation paths for dist based on RECORD-without-.py[co]
58
+
59
+ Yield paths to all the files in RECORD. For each .py file in RECORD, add
60
+ the .pyc and .pyo in the same directory.
61
+
62
+ UninstallPathSet.add() takes care of the __pycache__ .py[co].
63
+
64
+ If RECORD is not found, raises an error,
65
+ with possible information from the INSTALLER file.
66
+
67
+ https://packaging.python.org/specifications/recording-installed-packages/
68
+ """
69
+ location = dist.location
70
+ assert location is not None, "not installed"
71
+
72
+ entries = dist.iter_declared_entries()
73
+ if entries is None:
74
+ raise UninstallMissingRecord(distribution=dist)
75
+
76
+ for entry in entries:
77
+ path = os.path.join(location, entry)
78
+ yield path
79
+ if path.endswith(".py"):
80
+ dn, fn = os.path.split(path)
81
+ base = fn[:-3]
82
+ path = os.path.join(dn, base + ".pyc")
83
+ yield path
84
+ path = os.path.join(dn, base + ".pyo")
85
+ yield path
86
+
87
+
88
+ def compact(paths: Iterable[str]) -> Set[str]:
89
+ """Compact a path set to contain the minimal number of paths
90
+ necessary to contain all paths in the set. If /a/path/ and
91
+ /a/path/to/a/file.txt are both in the set, leave only the
92
+ shorter path."""
93
+
94
+ sep = os.path.sep
95
+ short_paths: Set[str] = set()
96
+ for path in sorted(paths, key=len):
97
+ should_skip = any(
98
+ path.startswith(shortpath.rstrip("*"))
99
+ and path[len(shortpath.rstrip("*").rstrip(sep))] == sep
100
+ for shortpath in short_paths
101
+ )
102
+ if not should_skip:
103
+ short_paths.add(path)
104
+ return short_paths
105
+
106
+
107
+ def compress_for_rename(paths: Iterable[str]) -> Set[str]:
108
+ """Returns a set containing the paths that need to be renamed.
109
+
110
+ This set may include directories when the original sequence of paths
111
+ included every file on disk.
112
+ """
113
+ case_map = {os.path.normcase(p): p for p in paths}
114
+ remaining = set(case_map)
115
+ unchecked = sorted({os.path.split(p)[0] for p in case_map.values()}, key=len)
116
+ wildcards: Set[str] = set()
117
+
118
+ def norm_join(*a: str) -> str:
119
+ return os.path.normcase(os.path.join(*a))
120
+
121
+ for root in unchecked:
122
+ if any(os.path.normcase(root).startswith(w) for w in wildcards):
123
+ # This directory has already been handled.
124
+ continue
125
+
126
+ all_files: Set[str] = set()
127
+ all_subdirs: Set[str] = set()
128
+ for dirname, subdirs, files in os.walk(root):
129
+ all_subdirs.update(norm_join(root, dirname, d) for d in subdirs)
130
+ all_files.update(norm_join(root, dirname, f) for f in files)
131
+ # If all the files we found are in our remaining set of files to
132
+ # remove, then remove them from the latter set and add a wildcard
133
+ # for the directory.
134
+ if not (all_files - remaining):
135
+ remaining.difference_update(all_files)
136
+ wildcards.add(root + os.sep)
137
+
138
+ return set(map(case_map.__getitem__, remaining)) | wildcards
139
+
140
+
141
+ def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str]]:
142
+ """Returns a tuple of 2 sets of which paths to display to user
143
+
144
+ The first set contains paths that would be deleted. Files of a package
145
+ are not added and the top-level directory of the package has a '*' added
146
+ at the end - to signify that all it's contents are removed.
147
+
148
+ The second set contains files that would have been skipped in the above
149
+ folders.
150
+ """
151
+
152
+ will_remove = set(paths)
153
+ will_skip = set()
154
+
155
+ # Determine folders and files
156
+ folders = set()
157
+ files = set()
158
+ for path in will_remove:
159
+ if path.endswith(".pyc"):
160
+ continue
161
+ if path.endswith("__init__.py") or ".dist-info" in path:
162
+ folders.add(os.path.dirname(path))
163
+ files.add(path)
164
+
165
+ _normcased_files = set(map(os.path.normcase, files))
166
+
167
+ folders = compact(folders)
168
+
169
+ # This walks the tree using os.walk to not miss extra folders
170
+ # that might get added.
171
+ for folder in folders:
172
+ for dirpath, _, dirfiles in os.walk(folder):
173
+ for fname in dirfiles:
174
+ if fname.endswith(".pyc"):
175
+ continue
176
+
177
+ file_ = os.path.join(dirpath, fname)
178
+ if (
179
+ os.path.isfile(file_)
180
+ and os.path.normcase(file_) not in _normcased_files
181
+ ):
182
+ # We are skipping this file. Add it to the set.
183
+ will_skip.add(file_)
184
+
185
+ will_remove = files | {os.path.join(folder, "*") for folder in folders}
186
+
187
+ return will_remove, will_skip
188
+
189
+
190
+ class StashedUninstallPathSet:
191
+ """A set of file rename operations to stash files while
192
+ tentatively uninstalling them."""
193
+
194
+ def __init__(self) -> None:
195
+ # Mapping from source file root to [Adjacent]TempDirectory
196
+ # for files under that directory.
197
+ self._save_dirs: Dict[str, TempDirectory] = {}
198
+ # (old path, new path) tuples for each move that may need
199
+ # to be undone.
200
+ self._moves: List[Tuple[str, str]] = []
201
+
202
+ def _get_directory_stash(self, path: str) -> str:
203
+ """Stashes a directory.
204
+
205
+ Directories are stashed adjacent to their original location if
206
+ possible, or else moved/copied into the user's temp dir."""
207
+
208
+ try:
209
+ save_dir: TempDirectory = AdjacentTempDirectory(path)
210
+ except OSError:
211
+ save_dir = TempDirectory(kind="uninstall")
212
+ self._save_dirs[os.path.normcase(path)] = save_dir
213
+
214
+ return save_dir.path
215
+
216
+ def _get_file_stash(self, path: str) -> str:
217
+ """Stashes a file.
218
+
219
+ If no root has been provided, one will be created for the directory
220
+ in the user's temp directory."""
221
+ path = os.path.normcase(path)
222
+ head, old_head = os.path.dirname(path), None
223
+ save_dir = None
224
+
225
+ while head != old_head:
226
+ try:
227
+ save_dir = self._save_dirs[head]
228
+ break
229
+ except KeyError:
230
+ pass
231
+ head, old_head = os.path.dirname(head), head
232
+ else:
233
+ # Did not find any suitable root
234
+ head = os.path.dirname(path)
235
+ save_dir = TempDirectory(kind="uninstall")
236
+ self._save_dirs[head] = save_dir
237
+
238
+ relpath = os.path.relpath(path, head)
239
+ if relpath and relpath != os.path.curdir:
240
+ return os.path.join(save_dir.path, relpath)
241
+ return save_dir.path
242
+
243
+ def stash(self, path: str) -> str:
244
+ """Stashes the directory or file and returns its new location.
245
+ Handle symlinks as files to avoid modifying the symlink targets.
246
+ """
247
+ path_is_dir = os.path.isdir(path) and not os.path.islink(path)
248
+ if path_is_dir:
249
+ new_path = self._get_directory_stash(path)
250
+ else:
251
+ new_path = self._get_file_stash(path)
252
+
253
+ self._moves.append((path, new_path))
254
+ if path_is_dir and os.path.isdir(new_path):
255
+ # If we're moving a directory, we need to
256
+ # remove the destination first or else it will be
257
+ # moved to inside the existing directory.
258
+ # We just created new_path ourselves, so it will
259
+ # be removable.
260
+ os.rmdir(new_path)
261
+ renames(path, new_path)
262
+ return new_path
263
+
264
+ def commit(self) -> None:
265
+ """Commits the uninstall by removing stashed files."""
266
+ for save_dir in self._save_dirs.values():
267
+ save_dir.cleanup()
268
+ self._moves = []
269
+ self._save_dirs = {}
270
+
271
+ def rollback(self) -> None:
272
+ """Undoes the uninstall by moving stashed files back."""
273
+ for p in self._moves:
274
+ logger.info("Moving to %s\n from %s", *p)
275
+
276
+ for new_path, path in self._moves:
277
+ try:
278
+ logger.debug("Replacing %s from %s", new_path, path)
279
+ if os.path.isfile(new_path) or os.path.islink(new_path):
280
+ os.unlink(new_path)
281
+ elif os.path.isdir(new_path):
282
+ rmtree(new_path)
283
+ renames(path, new_path)
284
+ except OSError as ex:
285
+ logger.error("Failed to restore %s", new_path)
286
+ logger.debug("Exception: %s", ex)
287
+
288
+ self.commit()
289
+
290
+ @property
291
+ def can_rollback(self) -> bool:
292
+ return bool(self._moves)
293
+
294
+
295
+ class UninstallPathSet:
296
+ """A set of file paths to be removed in the uninstallation of a
297
+ requirement."""
298
+
299
+ def __init__(self, dist: BaseDistribution) -> None:
300
+ self._paths: Set[str] = set()
301
+ self._refuse: Set[str] = set()
302
+ self._pth: Dict[str, UninstallPthEntries] = {}
303
+ self._dist = dist
304
+ self._moved_paths = StashedUninstallPathSet()
305
+ # Create local cache of normalize_path results. Creating an UninstallPathSet
306
+ # can result in hundreds/thousands of redundant calls to normalize_path with
307
+ # the same args, which hurts performance.
308
+ self._normalize_path_cached = functools.lru_cache(normalize_path)
309
+
310
+ def _permitted(self, path: str) -> bool:
311
+ """
312
+ Return True if the given path is one we are permitted to
313
+ remove/modify, False otherwise.
314
+
315
+ """
316
+ # aka is_local, but caching normalized sys.prefix
317
+ if not running_under_virtualenv():
318
+ return True
319
+ return path.startswith(self._normalize_path_cached(sys.prefix))
320
+
321
+ def add(self, path: str) -> None:
322
+ head, tail = os.path.split(path)
323
+
324
+ # we normalize the head to resolve parent directory symlinks, but not
325
+ # the tail, since we only want to uninstall symlinks, not their targets
326
+ path = os.path.join(self._normalize_path_cached(head), os.path.normcase(tail))
327
+
328
+ if not os.path.exists(path):
329
+ return
330
+ if self._permitted(path):
331
+ self._paths.add(path)
332
+ else:
333
+ self._refuse.add(path)
334
+
335
+ # __pycache__ files can show up after 'installed-files.txt' is created,
336
+ # due to imports
337
+ if os.path.splitext(path)[1] == ".py":
338
+ self.add(cache_from_source(path))
339
+
340
+ def add_pth(self, pth_file: str, entry: str) -> None:
341
+ pth_file = self._normalize_path_cached(pth_file)
342
+ if self._permitted(pth_file):
343
+ if pth_file not in self._pth:
344
+ self._pth[pth_file] = UninstallPthEntries(pth_file)
345
+ self._pth[pth_file].add(entry)
346
+ else:
347
+ self._refuse.add(pth_file)
348
+
349
+ def remove(self, auto_confirm: bool = False, verbose: bool = False) -> None:
350
+ """Remove paths in ``self._paths`` with confirmation (unless
351
+ ``auto_confirm`` is True)."""
352
+
353
+ if not self._paths:
354
+ logger.info(
355
+ "Can't uninstall '%s'. No files were found to uninstall.",
356
+ self._dist.raw_name,
357
+ )
358
+ return
359
+
360
+ dist_name_version = f"{self._dist.raw_name}-{self._dist.raw_version}"
361
+ logger.info("Uninstalling %s:", dist_name_version)
362
+
363
+ with indent_log():
364
+ if auto_confirm or self._allowed_to_proceed(verbose):
365
+ moved = self._moved_paths
366
+
367
+ for_rename = compress_for_rename(self._paths)
368
+
369
+ for path in sorted(compact(for_rename)):
370
+ moved.stash(path)
371
+ logger.verbose("Removing file or directory %s", path)
372
+
373
+ for pth in self._pth.values():
374
+ pth.remove()
375
+
376
+ logger.info("Successfully uninstalled %s", dist_name_version)
377
+
378
+ def _allowed_to_proceed(self, verbose: bool) -> bool:
379
+ """Display which files would be deleted and prompt for confirmation"""
380
+
381
+ def _display(msg: str, paths: Iterable[str]) -> None:
382
+ if not paths:
383
+ return
384
+
385
+ logger.info(msg)
386
+ with indent_log():
387
+ for path in sorted(compact(paths)):
388
+ logger.info(path)
389
+
390
+ if not verbose:
391
+ will_remove, will_skip = compress_for_output_listing(self._paths)
392
+ else:
393
+ # In verbose mode, display all the files that are going to be
394
+ # deleted.
395
+ will_remove = set(self._paths)
396
+ will_skip = set()
397
+
398
+ _display("Would remove:", will_remove)
399
+ _display("Would not remove (might be manually added):", will_skip)
400
+ _display("Would not remove (outside of prefix):", self._refuse)
401
+ if verbose:
402
+ _display("Will actually move:", compress_for_rename(self._paths))
403
+
404
+ return ask("Proceed (Y/n)? ", ("y", "n", "")) != "n"
405
+
406
+ def rollback(self) -> None:
407
+ """Rollback the changes previously made by remove()."""
408
+ if not self._moved_paths.can_rollback:
409
+ logger.error(
410
+ "Can't roll back %s; was not uninstalled",
411
+ self._dist.raw_name,
412
+ )
413
+ return
414
+ logger.info("Rolling back uninstall of %s", self._dist.raw_name)
415
+ self._moved_paths.rollback()
416
+ for pth in self._pth.values():
417
+ pth.rollback()
418
+
419
+ def commit(self) -> None:
420
+ """Remove temporary save dir: rollback will no longer be possible."""
421
+ self._moved_paths.commit()
422
+
423
+ @classmethod
424
+ def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet":
425
+ dist_location = dist.location
426
+ info_location = dist.info_location
427
+ if dist_location is None:
428
+ logger.info(
429
+ "Not uninstalling %s since it is not installed",
430
+ dist.canonical_name,
431
+ )
432
+ return cls(dist)
433
+
434
+ normalized_dist_location = normalize_path(dist_location)
435
+ if not dist.local:
436
+ logger.info(
437
+ "Not uninstalling %s at %s, outside environment %s",
438
+ dist.canonical_name,
439
+ normalized_dist_location,
440
+ sys.prefix,
441
+ )
442
+ return cls(dist)
443
+
444
+ if normalized_dist_location in {
445
+ p
446
+ for p in {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")}
447
+ if p
448
+ }:
449
+ logger.info(
450
+ "Not uninstalling %s at %s, as it is in the standard library.",
451
+ dist.canonical_name,
452
+ normalized_dist_location,
453
+ )
454
+ return cls(dist)
455
+
456
+ paths_to_remove = cls(dist)
457
+ develop_egg_link = egg_link_path_from_location(dist.raw_name)
458
+
459
+ # Distribution is installed with metadata in a "flat" .egg-info
460
+ # directory. This means it is not a modern .dist-info installation, an
461
+ # egg, or legacy editable.
462
+ setuptools_flat_installation = (
463
+ dist.installed_with_setuptools_egg_info
464
+ and info_location is not None
465
+ and os.path.exists(info_location)
466
+ # If dist is editable and the location points to a ``.egg-info``,
467
+ # we are in fact in the legacy editable case.
468
+ and not info_location.endswith(f"{dist.setuptools_filename}.egg-info")
469
+ )
470
+
471
+ # Uninstall cases order do matter as in the case of 2 installs of the
472
+ # same package, pip needs to uninstall the currently detected version
473
+ if setuptools_flat_installation:
474
+ if info_location is not None:
475
+ paths_to_remove.add(info_location)
476
+ installed_files = dist.iter_declared_entries()
477
+ if installed_files is not None:
478
+ for installed_file in installed_files:
479
+ paths_to_remove.add(os.path.join(dist_location, installed_file))
480
+ # FIXME: need a test for this elif block
481
+ # occurs with --single-version-externally-managed/--record outside
482
+ # of pip
483
+ elif dist.is_file("top_level.txt"):
484
+ try:
485
+ namespace_packages = dist.read_text("namespace_packages.txt")
486
+ except FileNotFoundError:
487
+ namespaces = []
488
+ else:
489
+ namespaces = namespace_packages.splitlines(keepends=False)
490
+ for top_level_pkg in [
491
+ p
492
+ for p in dist.read_text("top_level.txt").splitlines()
493
+ if p and p not in namespaces
494
+ ]:
495
+ path = os.path.join(dist_location, top_level_pkg)
496
+ paths_to_remove.add(path)
497
+ paths_to_remove.add(f"{path}.py")
498
+ paths_to_remove.add(f"{path}.pyc")
499
+ paths_to_remove.add(f"{path}.pyo")
500
+
501
+ elif dist.installed_by_distutils:
502
+ raise LegacyDistutilsInstall(distribution=dist)
503
+
504
+ elif dist.installed_as_egg:
505
+ # package installed by easy_install
506
+ # We cannot match on dist.egg_name because it can slightly vary
507
+ # i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
508
+ # XXX We use normalized_dist_location because dist_location my contain
509
+ # a trailing / if the distribution is a zipped egg
510
+ # (which is not a directory).
511
+ paths_to_remove.add(normalized_dist_location)
512
+ easy_install_egg = os.path.split(normalized_dist_location)[1]
513
+ easy_install_pth = os.path.join(
514
+ os.path.dirname(normalized_dist_location),
515
+ "easy-install.pth",
516
+ )
517
+ paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)
518
+
519
+ elif dist.installed_with_dist_info:
520
+ for path in uninstallation_paths(dist):
521
+ paths_to_remove.add(path)
522
+
523
+ elif develop_egg_link:
524
+ # PEP 660 modern editable is handled in the ``.dist-info`` case
525
+ # above, so this only covers the setuptools-style editable.
526
+ with open(develop_egg_link) as fh:
527
+ link_pointer = os.path.normcase(fh.readline().strip())
528
+ normalized_link_pointer = paths_to_remove._normalize_path_cached(
529
+ link_pointer
530
+ )
531
+ assert os.path.samefile(
532
+ normalized_link_pointer, normalized_dist_location
533
+ ), (
534
+ f"Egg-link {develop_egg_link} (to {link_pointer}) does not match "
535
+ f"installed location of {dist.raw_name} (at {dist_location})"
536
+ )
537
+ paths_to_remove.add(develop_egg_link)
538
+ easy_install_pth = os.path.join(
539
+ os.path.dirname(develop_egg_link), "easy-install.pth"
540
+ )
541
+ paths_to_remove.add_pth(easy_install_pth, dist_location)
542
+
543
+ else:
544
+ logger.debug(
545
+ "Not sure how to uninstall: %s - Check: %s",
546
+ dist,
547
+ dist_location,
548
+ )
549
+
550
+ if dist.in_usersite:
551
+ bin_dir = get_bin_user()
552
+ else:
553
+ bin_dir = get_bin_prefix()
554
+
555
+ # find distutils scripts= scripts
556
+ try:
557
+ for script in dist.iter_distutils_script_names():
558
+ paths_to_remove.add(os.path.join(bin_dir, script))
559
+ if WINDOWS:
560
+ paths_to_remove.add(os.path.join(bin_dir, f"{script}.bat"))
561
+ except (FileNotFoundError, NotADirectoryError):
562
+ pass
563
+
564
+ # find console_scripts and gui_scripts
565
+ def iter_scripts_to_remove(
566
+ dist: BaseDistribution,
567
+ bin_dir: str,
568
+ ) -> Generator[str, None, None]:
569
+ for entry_point in dist.iter_entry_points():
570
+ if entry_point.group == "console_scripts":
571
+ yield from _script_names(bin_dir, entry_point.name, False)
572
+ elif entry_point.group == "gui_scripts":
573
+ yield from _script_names(bin_dir, entry_point.name, True)
574
+
575
+ for s in iter_scripts_to_remove(dist, bin_dir):
576
+ paths_to_remove.add(s)
577
+
578
+ return paths_to_remove
579
+
580
+
581
+ class UninstallPthEntries:
582
+ def __init__(self, pth_file: str) -> None:
583
+ self.file = pth_file
584
+ self.entries: Set[str] = set()
585
+ self._saved_lines: Optional[List[bytes]] = None
586
+
587
+ def add(self, entry: str) -> None:
588
+ entry = os.path.normcase(entry)
589
+ # On Windows, os.path.normcase converts the entry to use
590
+ # backslashes. This is correct for entries that describe absolute
591
+ # paths outside of site-packages, but all the others use forward
592
+ # slashes.
593
+ # os.path.splitdrive is used instead of os.path.isabs because isabs
594
+ # treats non-absolute paths with drive letter markings like c:foo\bar
595
+ # as absolute paths. It also does not recognize UNC paths if they don't
596
+ # have more than "\\sever\share". Valid examples: "\\server\share\" or
597
+ # "\\server\share\folder".
598
+ if WINDOWS and not os.path.splitdrive(entry)[0]:
599
+ entry = entry.replace("\\", "/")
600
+ self.entries.add(entry)
601
+
602
+ def remove(self) -> None:
603
+ logger.verbose("Removing pth entries from %s:", self.file)
604
+
605
+ # If the file doesn't exist, log a warning and return
606
+ if not os.path.isfile(self.file):
607
+ logger.warning("Cannot remove entries from nonexistent file %s", self.file)
608
+ return
609
+ with open(self.file, "rb") as fh:
610
+ # windows uses '\r\n' with py3k, but uses '\n' with py2.x
611
+ lines = fh.readlines()
612
+ self._saved_lines = lines
613
+ if any(b"\r\n" in line for line in lines):
614
+ endline = "\r\n"
615
+ else:
616
+ endline = "\n"
617
+ # handle missing trailing newline
618
+ if lines and not lines[-1].endswith(endline.encode("utf-8")):
619
+ lines[-1] = lines[-1] + endline.encode("utf-8")
620
+ for entry in self.entries:
621
+ try:
622
+ logger.verbose("Removing entry: %s", entry)
623
+ lines.remove((entry + endline).encode("utf-8"))
624
+ except ValueError:
625
+ pass
626
+ with open(self.file, "wb") as fh:
627
+ fh.writelines(lines)
628
+
629
+ def rollback(self) -> bool:
630
+ if self._saved_lines is None:
631
+ logger.error("Cannot roll back changes to %s, none were made", self.file)
632
+ return False
633
+ logger.debug("Rolling %s back to previous state", self.file)
634
+ with open(self.file, "wb") as fh:
635
+ fh.writelines(self._saved_lines)
636
+ return True
.env/lib/python3.13/site-packages/pip/_internal/resolution/__init__.py ADDED
File without changes
.env/lib/python3.13/site-packages/pip/_internal/resolution/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (201 Bytes). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/__pycache__/base.cpython-313.pyc ADDED
Binary file (1.25 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/base.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable, List, Optional
2
+
3
+ from pip._internal.req.req_install import InstallRequirement
4
+ from pip._internal.req.req_set import RequirementSet
5
+
6
+ InstallRequirementProvider = Callable[
7
+ [str, Optional[InstallRequirement]], InstallRequirement
8
+ ]
9
+
10
+
11
+ class BaseResolver:
12
+ def resolve(
13
+ self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
14
+ ) -> RequirementSet:
15
+ raise NotImplementedError()
16
+
17
+ def get_installation_order(
18
+ self, req_set: RequirementSet
19
+ ) -> List[InstallRequirement]:
20
+ raise NotImplementedError()
.env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__init__.py ADDED
File without changes
.env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (208 Bytes). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__pycache__/resolver.cpython-313.pyc ADDED
Binary file (22.9 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/resolver.py ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dependency Resolution
2
+
3
+ The dependency resolution in pip is performed as follows:
4
+
5
+ for top-level requirements:
6
+ a. only one spec allowed per project, regardless of conflicts or not.
7
+ otherwise a "double requirement" exception is raised
8
+ b. they override sub-dependency requirements.
9
+ for sub-dependencies
10
+ a. "first found, wins" (where the order is breadth first)
11
+ """
12
+
13
+ import logging
14
+ import sys
15
+ from collections import defaultdict
16
+ from itertools import chain
17
+ from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
18
+
19
+ from pip._vendor.packaging import specifiers
20
+ from pip._vendor.packaging.requirements import Requirement
21
+
22
+ from pip._internal.cache import WheelCache
23
+ from pip._internal.exceptions import (
24
+ BestVersionAlreadyInstalled,
25
+ DistributionNotFound,
26
+ HashError,
27
+ HashErrors,
28
+ InstallationError,
29
+ NoneMetadataError,
30
+ UnsupportedPythonVersion,
31
+ )
32
+ from pip._internal.index.package_finder import PackageFinder
33
+ from pip._internal.metadata import BaseDistribution
34
+ from pip._internal.models.link import Link
35
+ from pip._internal.models.wheel import Wheel
36
+ from pip._internal.operations.prepare import RequirementPreparer
37
+ from pip._internal.req.req_install import (
38
+ InstallRequirement,
39
+ check_invalid_constraint_type,
40
+ )
41
+ from pip._internal.req.req_set import RequirementSet
42
+ from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
43
+ from pip._internal.utils import compatibility_tags
44
+ from pip._internal.utils.compatibility_tags import get_supported
45
+ from pip._internal.utils.direct_url_helpers import direct_url_from_link
46
+ from pip._internal.utils.logging import indent_log
47
+ from pip._internal.utils.misc import normalize_version_info
48
+ from pip._internal.utils.packaging import check_requires_python
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+ DiscoveredDependencies = DefaultDict[Optional[str], List[InstallRequirement]]
53
+
54
+
55
+ def _check_dist_requires_python(
56
+ dist: BaseDistribution,
57
+ version_info: Tuple[int, int, int],
58
+ ignore_requires_python: bool = False,
59
+ ) -> None:
60
+ """
61
+ Check whether the given Python version is compatible with a distribution's
62
+ "Requires-Python" value.
63
+
64
+ :param version_info: A 3-tuple of ints representing the Python
65
+ major-minor-micro version to check.
66
+ :param ignore_requires_python: Whether to ignore the "Requires-Python"
67
+ value if the given Python version isn't compatible.
68
+
69
+ :raises UnsupportedPythonVersion: When the given Python version isn't
70
+ compatible.
71
+ """
72
+ # This idiosyncratically converts the SpecifierSet to str and let
73
+ # check_requires_python then parse it again into SpecifierSet. But this
74
+ # is the legacy resolver so I'm just not going to bother refactoring.
75
+ try:
76
+ requires_python = str(dist.requires_python)
77
+ except FileNotFoundError as e:
78
+ raise NoneMetadataError(dist, str(e))
79
+ try:
80
+ is_compatible = check_requires_python(
81
+ requires_python,
82
+ version_info=version_info,
83
+ )
84
+ except specifiers.InvalidSpecifier as exc:
85
+ logger.warning(
86
+ "Package %r has an invalid Requires-Python: %s", dist.raw_name, exc
87
+ )
88
+ return
89
+
90
+ if is_compatible:
91
+ return
92
+
93
+ version = ".".join(map(str, version_info))
94
+ if ignore_requires_python:
95
+ logger.debug(
96
+ "Ignoring failed Requires-Python check for package %r: %s not in %r",
97
+ dist.raw_name,
98
+ version,
99
+ requires_python,
100
+ )
101
+ return
102
+
103
+ raise UnsupportedPythonVersion(
104
+ f"Package {dist.raw_name!r} requires a different Python: "
105
+ f"{version} not in {requires_python!r}"
106
+ )
107
+
108
+
109
+ class Resolver(BaseResolver):
110
+ """Resolves which packages need to be installed/uninstalled to perform \
111
+ the requested operation without breaking the requirements of any package.
112
+ """
113
+
114
+ _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
115
+
116
+ def __init__(
117
+ self,
118
+ preparer: RequirementPreparer,
119
+ finder: PackageFinder,
120
+ wheel_cache: Optional[WheelCache],
121
+ make_install_req: InstallRequirementProvider,
122
+ use_user_site: bool,
123
+ ignore_dependencies: bool,
124
+ ignore_installed: bool,
125
+ ignore_requires_python: bool,
126
+ force_reinstall: bool,
127
+ upgrade_strategy: str,
128
+ py_version_info: Optional[Tuple[int, ...]] = None,
129
+ ) -> None:
130
+ super().__init__()
131
+ assert upgrade_strategy in self._allowed_strategies
132
+
133
+ if py_version_info is None:
134
+ py_version_info = sys.version_info[:3]
135
+ else:
136
+ py_version_info = normalize_version_info(py_version_info)
137
+
138
+ self._py_version_info = py_version_info
139
+
140
+ self.preparer = preparer
141
+ self.finder = finder
142
+ self.wheel_cache = wheel_cache
143
+
144
+ self.upgrade_strategy = upgrade_strategy
145
+ self.force_reinstall = force_reinstall
146
+ self.ignore_dependencies = ignore_dependencies
147
+ self.ignore_installed = ignore_installed
148
+ self.ignore_requires_python = ignore_requires_python
149
+ self.use_user_site = use_user_site
150
+ self._make_install_req = make_install_req
151
+
152
+ self._discovered_dependencies: DiscoveredDependencies = defaultdict(list)
153
+
154
+ def resolve(
155
+ self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
156
+ ) -> RequirementSet:
157
+ """Resolve what operations need to be done
158
+
159
+ As a side-effect of this method, the packages (and their dependencies)
160
+ are downloaded, unpacked and prepared for installation. This
161
+ preparation is done by ``pip.operations.prepare``.
162
+
163
+ Once PyPI has static dependency metadata available, it would be
164
+ possible to move the preparation to become a step separated from
165
+ dependency resolution.
166
+ """
167
+ requirement_set = RequirementSet(check_supported_wheels=check_supported_wheels)
168
+ for req in root_reqs:
169
+ if req.constraint:
170
+ check_invalid_constraint_type(req)
171
+ self._add_requirement_to_set(requirement_set, req)
172
+
173
+ # Actually prepare the files, and collect any exceptions. Most hash
174
+ # exceptions cannot be checked ahead of time, because
175
+ # _populate_link() needs to be called before we can make decisions
176
+ # based on link type.
177
+ discovered_reqs: List[InstallRequirement] = []
178
+ hash_errors = HashErrors()
179
+ for req in chain(requirement_set.all_requirements, discovered_reqs):
180
+ try:
181
+ discovered_reqs.extend(self._resolve_one(requirement_set, req))
182
+ except HashError as exc:
183
+ exc.req = req
184
+ hash_errors.append(exc)
185
+
186
+ if hash_errors:
187
+ raise hash_errors
188
+
189
+ return requirement_set
190
+
191
+ def _add_requirement_to_set(
192
+ self,
193
+ requirement_set: RequirementSet,
194
+ install_req: InstallRequirement,
195
+ parent_req_name: Optional[str] = None,
196
+ extras_requested: Optional[Iterable[str]] = None,
197
+ ) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
198
+ """Add install_req as a requirement to install.
199
+
200
+ :param parent_req_name: The name of the requirement that needed this
201
+ added. The name is used because when multiple unnamed requirements
202
+ resolve to the same name, we could otherwise end up with dependency
203
+ links that point outside the Requirements set. parent_req must
204
+ already be added. Note that None implies that this is a user
205
+ supplied requirement, vs an inferred one.
206
+ :param extras_requested: an iterable of extras used to evaluate the
207
+ environment markers.
208
+ :return: Additional requirements to scan. That is either [] if
209
+ the requirement is not applicable, or [install_req] if the
210
+ requirement is applicable and has just been added.
211
+ """
212
+ # If the markers do not match, ignore this requirement.
213
+ if not install_req.match_markers(extras_requested):
214
+ logger.info(
215
+ "Ignoring %s: markers '%s' don't match your environment",
216
+ install_req.name,
217
+ install_req.markers,
218
+ )
219
+ return [], None
220
+
221
+ # If the wheel is not supported, raise an error.
222
+ # Should check this after filtering out based on environment markers to
223
+ # allow specifying different wheels based on the environment/OS, in a
224
+ # single requirements file.
225
+ if install_req.link and install_req.link.is_wheel:
226
+ wheel = Wheel(install_req.link.filename)
227
+ tags = compatibility_tags.get_supported()
228
+ if requirement_set.check_supported_wheels and not wheel.supported(tags):
229
+ raise InstallationError(
230
+ f"{wheel.filename} is not a supported wheel on this platform."
231
+ )
232
+
233
+ # This next bit is really a sanity check.
234
+ assert (
235
+ not install_req.user_supplied or parent_req_name is None
236
+ ), "a user supplied req shouldn't have a parent"
237
+
238
+ # Unnamed requirements are scanned again and the requirement won't be
239
+ # added as a dependency until after scanning.
240
+ if not install_req.name:
241
+ requirement_set.add_unnamed_requirement(install_req)
242
+ return [install_req], None
243
+
244
+ try:
245
+ existing_req: Optional[InstallRequirement] = (
246
+ requirement_set.get_requirement(install_req.name)
247
+ )
248
+ except KeyError:
249
+ existing_req = None
250
+
251
+ has_conflicting_requirement = (
252
+ parent_req_name is None
253
+ and existing_req
254
+ and not existing_req.constraint
255
+ and existing_req.extras == install_req.extras
256
+ and existing_req.req
257
+ and install_req.req
258
+ and existing_req.req.specifier != install_req.req.specifier
259
+ )
260
+ if has_conflicting_requirement:
261
+ raise InstallationError(
262
+ f"Double requirement given: {install_req} "
263
+ f"(already in {existing_req}, name={install_req.name!r})"
264
+ )
265
+
266
+ # When no existing requirement exists, add the requirement as a
267
+ # dependency and it will be scanned again after.
268
+ if not existing_req:
269
+ requirement_set.add_named_requirement(install_req)
270
+ # We'd want to rescan this requirement later
271
+ return [install_req], install_req
272
+
273
+ # Assume there's no need to scan, and that we've already
274
+ # encountered this for scanning.
275
+ if install_req.constraint or not existing_req.constraint:
276
+ return [], existing_req
277
+
278
+ does_not_satisfy_constraint = install_req.link and not (
279
+ existing_req.link and install_req.link.path == existing_req.link.path
280
+ )
281
+ if does_not_satisfy_constraint:
282
+ raise InstallationError(
283
+ f"Could not satisfy constraints for '{install_req.name}': "
284
+ "installation from path or url cannot be "
285
+ "constrained to a version"
286
+ )
287
+ # If we're now installing a constraint, mark the existing
288
+ # object for real installation.
289
+ existing_req.constraint = False
290
+ # If we're now installing a user supplied requirement,
291
+ # mark the existing object as such.
292
+ if install_req.user_supplied:
293
+ existing_req.user_supplied = True
294
+ existing_req.extras = tuple(
295
+ sorted(set(existing_req.extras) | set(install_req.extras))
296
+ )
297
+ logger.debug(
298
+ "Setting %s extras to: %s",
299
+ existing_req,
300
+ existing_req.extras,
301
+ )
302
+ # Return the existing requirement for addition to the parent and
303
+ # scanning again.
304
+ return [existing_req], existing_req
305
+
306
+ def _is_upgrade_allowed(self, req: InstallRequirement) -> bool:
307
+ if self.upgrade_strategy == "to-satisfy-only":
308
+ return False
309
+ elif self.upgrade_strategy == "eager":
310
+ return True
311
+ else:
312
+ assert self.upgrade_strategy == "only-if-needed"
313
+ return req.user_supplied or req.constraint
314
+
315
+ def _set_req_to_reinstall(self, req: InstallRequirement) -> None:
316
+ """
317
+ Set a requirement to be installed.
318
+ """
319
+ # Don't uninstall the conflict if doing a user install and the
320
+ # conflict is not a user install.
321
+ assert req.satisfied_by is not None
322
+ if not self.use_user_site or req.satisfied_by.in_usersite:
323
+ req.should_reinstall = True
324
+ req.satisfied_by = None
325
+
326
+ def _check_skip_installed(
327
+ self, req_to_install: InstallRequirement
328
+ ) -> Optional[str]:
329
+ """Check if req_to_install should be skipped.
330
+
331
+ This will check if the req is installed, and whether we should upgrade
332
+ or reinstall it, taking into account all the relevant user options.
333
+
334
+ After calling this req_to_install will only have satisfied_by set to
335
+ None if the req_to_install is to be upgraded/reinstalled etc. Any
336
+ other value will be a dist recording the current thing installed that
337
+ satisfies the requirement.
338
+
339
+ Note that for vcs urls and the like we can't assess skipping in this
340
+ routine - we simply identify that we need to pull the thing down,
341
+ then later on it is pulled down and introspected to assess upgrade/
342
+ reinstalls etc.
343
+
344
+ :return: A text reason for why it was skipped, or None.
345
+ """
346
+ if self.ignore_installed:
347
+ return None
348
+
349
+ req_to_install.check_if_exists(self.use_user_site)
350
+ if not req_to_install.satisfied_by:
351
+ return None
352
+
353
+ if self.force_reinstall:
354
+ self._set_req_to_reinstall(req_to_install)
355
+ return None
356
+
357
+ if not self._is_upgrade_allowed(req_to_install):
358
+ if self.upgrade_strategy == "only-if-needed":
359
+ return "already satisfied, skipping upgrade"
360
+ return "already satisfied"
361
+
362
+ # Check for the possibility of an upgrade. For link-based
363
+ # requirements we have to pull the tree down and inspect to assess
364
+ # the version #, so it's handled way down.
365
+ if not req_to_install.link:
366
+ try:
367
+ self.finder.find_requirement(req_to_install, upgrade=True)
368
+ except BestVersionAlreadyInstalled:
369
+ # Then the best version is installed.
370
+ return "already up-to-date"
371
+ except DistributionNotFound:
372
+ # No distribution found, so we squash the error. It will
373
+ # be raised later when we re-try later to do the install.
374
+ # Why don't we just raise here?
375
+ pass
376
+
377
+ self._set_req_to_reinstall(req_to_install)
378
+ return None
379
+
380
+ def _find_requirement_link(self, req: InstallRequirement) -> Optional[Link]:
381
+ upgrade = self._is_upgrade_allowed(req)
382
+ best_candidate = self.finder.find_requirement(req, upgrade)
383
+ if not best_candidate:
384
+ return None
385
+
386
+ # Log a warning per PEP 592 if necessary before returning.
387
+ link = best_candidate.link
388
+ if link.is_yanked:
389
+ reason = link.yanked_reason or "<none given>"
390
+ msg = (
391
+ # Mark this as a unicode string to prevent
392
+ # "UnicodeEncodeError: 'ascii' codec can't encode character"
393
+ # in Python 2 when the reason contains non-ascii characters.
394
+ "The candidate selected for download or install is a "
395
+ f"yanked version: {best_candidate}\n"
396
+ f"Reason for being yanked: {reason}"
397
+ )
398
+ logger.warning(msg)
399
+
400
+ return link
401
+
402
+ def _populate_link(self, req: InstallRequirement) -> None:
403
+ """Ensure that if a link can be found for this, that it is found.
404
+
405
+ Note that req.link may still be None - if the requirement is already
406
+ installed and not needed to be upgraded based on the return value of
407
+ _is_upgrade_allowed().
408
+
409
+ If preparer.require_hashes is True, don't use the wheel cache, because
410
+ cached wheels, always built locally, have different hashes than the
411
+ files downloaded from the index server and thus throw false hash
412
+ mismatches. Furthermore, cached wheels at present have undeterministic
413
+ contents due to file modification times.
414
+ """
415
+ if req.link is None:
416
+ req.link = self._find_requirement_link(req)
417
+
418
+ if self.wheel_cache is None or self.preparer.require_hashes:
419
+ return
420
+
421
+ assert req.link is not None, "_find_requirement_link unexpectedly returned None"
422
+ cache_entry = self.wheel_cache.get_cache_entry(
423
+ link=req.link,
424
+ package_name=req.name,
425
+ supported_tags=get_supported(),
426
+ )
427
+ if cache_entry is not None:
428
+ logger.debug("Using cached wheel link: %s", cache_entry.link)
429
+ if req.link is req.original_link and cache_entry.persistent:
430
+ req.cached_wheel_source_link = req.link
431
+ if cache_entry.origin is not None:
432
+ req.download_info = cache_entry.origin
433
+ else:
434
+ # Legacy cache entry that does not have origin.json.
435
+ # download_info may miss the archive_info.hashes field.
436
+ req.download_info = direct_url_from_link(
437
+ req.link, link_is_in_wheel_cache=cache_entry.persistent
438
+ )
439
+ req.link = cache_entry.link
440
+
441
+ def _get_dist_for(self, req: InstallRequirement) -> BaseDistribution:
442
+ """Takes a InstallRequirement and returns a single AbstractDist \
443
+ representing a prepared variant of the same.
444
+ """
445
+ if req.editable:
446
+ return self.preparer.prepare_editable_requirement(req)
447
+
448
+ # satisfied_by is only evaluated by calling _check_skip_installed,
449
+ # so it must be None here.
450
+ assert req.satisfied_by is None
451
+ skip_reason = self._check_skip_installed(req)
452
+
453
+ if req.satisfied_by:
454
+ return self.preparer.prepare_installed_requirement(req, skip_reason)
455
+
456
+ # We eagerly populate the link, since that's our "legacy" behavior.
457
+ self._populate_link(req)
458
+ dist = self.preparer.prepare_linked_requirement(req)
459
+
460
+ # NOTE
461
+ # The following portion is for determining if a certain package is
462
+ # going to be re-installed/upgraded or not and reporting to the user.
463
+ # This should probably get cleaned up in a future refactor.
464
+
465
+ # req.req is only avail after unpack for URL
466
+ # pkgs repeat check_if_exists to uninstall-on-upgrade
467
+ # (#14)
468
+ if not self.ignore_installed:
469
+ req.check_if_exists(self.use_user_site)
470
+
471
+ if req.satisfied_by:
472
+ should_modify = (
473
+ self.upgrade_strategy != "to-satisfy-only"
474
+ or self.force_reinstall
475
+ or self.ignore_installed
476
+ or req.link.scheme == "file"
477
+ )
478
+ if should_modify:
479
+ self._set_req_to_reinstall(req)
480
+ else:
481
+ logger.info(
482
+ "Requirement already satisfied (use --upgrade to upgrade): %s",
483
+ req,
484
+ )
485
+ return dist
486
+
487
+ def _resolve_one(
488
+ self,
489
+ requirement_set: RequirementSet,
490
+ req_to_install: InstallRequirement,
491
+ ) -> List[InstallRequirement]:
492
+ """Prepare a single requirements file.
493
+
494
+ :return: A list of additional InstallRequirements to also install.
495
+ """
496
+ # Tell user what we are doing for this requirement:
497
+ # obtain (editable), skipping, processing (local url), collecting
498
+ # (remote url or package name)
499
+ if req_to_install.constraint or req_to_install.prepared:
500
+ return []
501
+
502
+ req_to_install.prepared = True
503
+
504
+ # Parse and return dependencies
505
+ dist = self._get_dist_for(req_to_install)
506
+ # This will raise UnsupportedPythonVersion if the given Python
507
+ # version isn't compatible with the distribution's Requires-Python.
508
+ _check_dist_requires_python(
509
+ dist,
510
+ version_info=self._py_version_info,
511
+ ignore_requires_python=self.ignore_requires_python,
512
+ )
513
+
514
+ more_reqs: List[InstallRequirement] = []
515
+
516
+ def add_req(subreq: Requirement, extras_requested: Iterable[str]) -> None:
517
+ # This idiosyncratically converts the Requirement to str and let
518
+ # make_install_req then parse it again into Requirement. But this is
519
+ # the legacy resolver so I'm just not going to bother refactoring.
520
+ sub_install_req = self._make_install_req(str(subreq), req_to_install)
521
+ parent_req_name = req_to_install.name
522
+ to_scan_again, add_to_parent = self._add_requirement_to_set(
523
+ requirement_set,
524
+ sub_install_req,
525
+ parent_req_name=parent_req_name,
526
+ extras_requested=extras_requested,
527
+ )
528
+ if parent_req_name and add_to_parent:
529
+ self._discovered_dependencies[parent_req_name].append(add_to_parent)
530
+ more_reqs.extend(to_scan_again)
531
+
532
+ with indent_log():
533
+ # We add req_to_install before its dependencies, so that we
534
+ # can refer to it when adding dependencies.
535
+ assert req_to_install.name is not None
536
+ if not requirement_set.has_requirement(req_to_install.name):
537
+ # 'unnamed' requirements will get added here
538
+ # 'unnamed' requirements can only come from being directly
539
+ # provided by the user.
540
+ assert req_to_install.user_supplied
541
+ self._add_requirement_to_set(
542
+ requirement_set, req_to_install, parent_req_name=None
543
+ )
544
+
545
+ if not self.ignore_dependencies:
546
+ if req_to_install.extras:
547
+ logger.debug(
548
+ "Installing extra requirements: %r",
549
+ ",".join(req_to_install.extras),
550
+ )
551
+ missing_requested = sorted(
552
+ set(req_to_install.extras) - set(dist.iter_provided_extras())
553
+ )
554
+ for missing in missing_requested:
555
+ logger.warning(
556
+ "%s %s does not provide the extra '%s'",
557
+ dist.raw_name,
558
+ dist.version,
559
+ missing,
560
+ )
561
+
562
+ available_requested = sorted(
563
+ set(dist.iter_provided_extras()) & set(req_to_install.extras)
564
+ )
565
+ for subreq in dist.iter_dependencies(available_requested):
566
+ add_req(subreq, extras_requested=available_requested)
567
+
568
+ return more_reqs
569
+
570
+ def get_installation_order(
571
+ self, req_set: RequirementSet
572
+ ) -> List[InstallRequirement]:
573
+ """Create the installation order.
574
+
575
+ The installation order is topological - requirements are installed
576
+ before the requiring thing. We break cycles at an arbitrary point,
577
+ and make no other guarantees.
578
+ """
579
+ # The current implementation, which we may change at any point
580
+ # installs the user specified things in the order given, except when
581
+ # dependencies must come earlier to achieve topological order.
582
+ order = []
583
+ ordered_reqs: Set[InstallRequirement] = set()
584
+
585
+ def schedule(req: InstallRequirement) -> None:
586
+ if req.satisfied_by or req in ordered_reqs:
587
+ return
588
+ if req.constraint:
589
+ return
590
+ ordered_reqs.add(req)
591
+ for dep in self._discovered_dependencies[req.name]:
592
+ schedule(dep)
593
+ order.append(req)
594
+
595
+ for install_req in req_set.requirements.values():
596
+ schedule(install_req)
597
+ return order
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__init__.py ADDED
File without changes
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (212 Bytes). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/base.cpython-313.pyc ADDED
Binary file (8.27 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/candidates.cpython-313.pyc ADDED
Binary file (30 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/factory.cpython-313.pyc ADDED
Binary file (33.2 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/found_candidates.cpython-313.pyc ADDED
Binary file (6.84 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/provider.cpython-313.pyc ADDED
Binary file (11 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/reporter.cpython-313.pyc ADDED
Binary file (5.23 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/requirements.cpython-313.pyc ADDED
Binary file (15.8 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/resolver.cpython-313.pyc ADDED
Binary file (12.6 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/base.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import FrozenSet, Iterable, Optional, Tuple
3
+
4
+ from pip._vendor.packaging.specifiers import SpecifierSet
5
+ from pip._vendor.packaging.utils import NormalizedName
6
+ from pip._vendor.packaging.version import Version
7
+
8
+ from pip._internal.models.link import Link, links_equivalent
9
+ from pip._internal.req.req_install import InstallRequirement
10
+ from pip._internal.utils.hashes import Hashes
11
+
12
+ CandidateLookup = Tuple[Optional["Candidate"], Optional[InstallRequirement]]
13
+
14
+
15
+ def format_name(project: NormalizedName, extras: FrozenSet[NormalizedName]) -> str:
16
+ if not extras:
17
+ return project
18
+ extras_expr = ",".join(sorted(extras))
19
+ return f"{project}[{extras_expr}]"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Constraint:
24
+ specifier: SpecifierSet
25
+ hashes: Hashes
26
+ links: FrozenSet[Link]
27
+
28
+ @classmethod
29
+ def empty(cls) -> "Constraint":
30
+ return Constraint(SpecifierSet(), Hashes(), frozenset())
31
+
32
+ @classmethod
33
+ def from_ireq(cls, ireq: InstallRequirement) -> "Constraint":
34
+ links = frozenset([ireq.link]) if ireq.link else frozenset()
35
+ return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links)
36
+
37
+ def __bool__(self) -> bool:
38
+ return bool(self.specifier) or bool(self.hashes) or bool(self.links)
39
+
40
+ def __and__(self, other: InstallRequirement) -> "Constraint":
41
+ if not isinstance(other, InstallRequirement):
42
+ return NotImplemented
43
+ specifier = self.specifier & other.specifier
44
+ hashes = self.hashes & other.hashes(trust_internet=False)
45
+ links = self.links
46
+ if other.link:
47
+ links = links.union([other.link])
48
+ return Constraint(specifier, hashes, links)
49
+
50
+ def is_satisfied_by(self, candidate: "Candidate") -> bool:
51
+ # Reject if there are any mismatched URL constraints on this package.
52
+ if self.links and not all(_match_link(link, candidate) for link in self.links):
53
+ return False
54
+ # We can safely always allow prereleases here since PackageFinder
55
+ # already implements the prerelease logic, and would have filtered out
56
+ # prerelease candidates if the user does not expect them.
57
+ return self.specifier.contains(candidate.version, prereleases=True)
58
+
59
+
60
+ class Requirement:
61
+ @property
62
+ def project_name(self) -> NormalizedName:
63
+ """The "project name" of a requirement.
64
+
65
+ This is different from ``name`` if this requirement contains extras,
66
+ in which case ``name`` would contain the ``[...]`` part, while this
67
+ refers to the name of the project.
68
+ """
69
+ raise NotImplementedError("Subclass should override")
70
+
71
+ @property
72
+ def name(self) -> str:
73
+ """The name identifying this requirement in the resolver.
74
+
75
+ This is different from ``project_name`` if this requirement contains
76
+ extras, where ``project_name`` would not contain the ``[...]`` part.
77
+ """
78
+ raise NotImplementedError("Subclass should override")
79
+
80
+ def is_satisfied_by(self, candidate: "Candidate") -> bool:
81
+ return False
82
+
83
+ def get_candidate_lookup(self) -> CandidateLookup:
84
+ raise NotImplementedError("Subclass should override")
85
+
86
+ def format_for_error(self) -> str:
87
+ raise NotImplementedError("Subclass should override")
88
+
89
+
90
+ def _match_link(link: Link, candidate: "Candidate") -> bool:
91
+ if candidate.source_link:
92
+ return links_equivalent(link, candidate.source_link)
93
+ return False
94
+
95
+
96
+ class Candidate:
97
+ @property
98
+ def project_name(self) -> NormalizedName:
99
+ """The "project name" of the candidate.
100
+
101
+ This is different from ``name`` if this candidate contains extras,
102
+ in which case ``name`` would contain the ``[...]`` part, while this
103
+ refers to the name of the project.
104
+ """
105
+ raise NotImplementedError("Override in subclass")
106
+
107
+ @property
108
+ def name(self) -> str:
109
+ """The name identifying this candidate in the resolver.
110
+
111
+ This is different from ``project_name`` if this candidate contains
112
+ extras, where ``project_name`` would not contain the ``[...]`` part.
113
+ """
114
+ raise NotImplementedError("Override in subclass")
115
+
116
+ @property
117
+ def version(self) -> Version:
118
+ raise NotImplementedError("Override in subclass")
119
+
120
+ @property
121
+ def is_installed(self) -> bool:
122
+ raise NotImplementedError("Override in subclass")
123
+
124
+ @property
125
+ def is_editable(self) -> bool:
126
+ raise NotImplementedError("Override in subclass")
127
+
128
+ @property
129
+ def source_link(self) -> Optional[Link]:
130
+ raise NotImplementedError("Override in subclass")
131
+
132
+ def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
133
+ raise NotImplementedError("Override in subclass")
134
+
135
+ def get_install_requirement(self) -> Optional[InstallRequirement]:
136
+ raise NotImplementedError("Override in subclass")
137
+
138
+ def format_for_error(self) -> str:
139
+ raise NotImplementedError("Subclass should override")
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/candidates.py ADDED
@@ -0,0 +1,579 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+ from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast
4
+
5
+ from pip._vendor.packaging.requirements import InvalidRequirement
6
+ from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
7
+ from pip._vendor.packaging.version import Version
8
+
9
+ from pip._internal.exceptions import (
10
+ HashError,
11
+ InstallationSubprocessError,
12
+ InvalidInstalledPackage,
13
+ MetadataInconsistent,
14
+ MetadataInvalid,
15
+ )
16
+ from pip._internal.metadata import BaseDistribution
17
+ from pip._internal.models.link import Link, links_equivalent
18
+ from pip._internal.models.wheel import Wheel
19
+ from pip._internal.req.constructors import (
20
+ install_req_from_editable,
21
+ install_req_from_line,
22
+ )
23
+ from pip._internal.req.req_install import InstallRequirement
24
+ from pip._internal.utils.direct_url_helpers import direct_url_from_link
25
+ from pip._internal.utils.misc import normalize_version_info
26
+
27
+ from .base import Candidate, Requirement, format_name
28
+
29
+ if TYPE_CHECKING:
30
+ from .factory import Factory
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ BaseCandidate = Union[
35
+ "AlreadyInstalledCandidate",
36
+ "EditableCandidate",
37
+ "LinkCandidate",
38
+ ]
39
+
40
+ # Avoid conflicting with the PyPI package "Python".
41
+ REQUIRES_PYTHON_IDENTIFIER = cast(NormalizedName, "<Python from Requires-Python>")
42
+
43
+
44
+ def as_base_candidate(candidate: Candidate) -> Optional[BaseCandidate]:
45
+ """The runtime version of BaseCandidate."""
46
+ base_candidate_classes = (
47
+ AlreadyInstalledCandidate,
48
+ EditableCandidate,
49
+ LinkCandidate,
50
+ )
51
+ if isinstance(candidate, base_candidate_classes):
52
+ return candidate
53
+ return None
54
+
55
+
56
+ def make_install_req_from_link(
57
+ link: Link, template: InstallRequirement
58
+ ) -> InstallRequirement:
59
+ assert not template.editable, "template is editable"
60
+ if template.req:
61
+ line = str(template.req)
62
+ else:
63
+ line = link.url
64
+ ireq = install_req_from_line(
65
+ line,
66
+ user_supplied=template.user_supplied,
67
+ comes_from=template.comes_from,
68
+ use_pep517=template.use_pep517,
69
+ isolated=template.isolated,
70
+ constraint=template.constraint,
71
+ global_options=template.global_options,
72
+ hash_options=template.hash_options,
73
+ config_settings=template.config_settings,
74
+ )
75
+ ireq.original_link = template.original_link
76
+ ireq.link = link
77
+ ireq.extras = template.extras
78
+ return ireq
79
+
80
+
81
+ def make_install_req_from_editable(
82
+ link: Link, template: InstallRequirement
83
+ ) -> InstallRequirement:
84
+ assert template.editable, "template not editable"
85
+ ireq = install_req_from_editable(
86
+ link.url,
87
+ user_supplied=template.user_supplied,
88
+ comes_from=template.comes_from,
89
+ use_pep517=template.use_pep517,
90
+ isolated=template.isolated,
91
+ constraint=template.constraint,
92
+ permit_editable_wheels=template.permit_editable_wheels,
93
+ global_options=template.global_options,
94
+ hash_options=template.hash_options,
95
+ config_settings=template.config_settings,
96
+ )
97
+ ireq.extras = template.extras
98
+ return ireq
99
+
100
+
101
+ def _make_install_req_from_dist(
102
+ dist: BaseDistribution, template: InstallRequirement
103
+ ) -> InstallRequirement:
104
+ if template.req:
105
+ line = str(template.req)
106
+ elif template.link:
107
+ line = f"{dist.canonical_name} @ {template.link.url}"
108
+ else:
109
+ line = f"{dist.canonical_name}=={dist.version}"
110
+ ireq = install_req_from_line(
111
+ line,
112
+ user_supplied=template.user_supplied,
113
+ comes_from=template.comes_from,
114
+ use_pep517=template.use_pep517,
115
+ isolated=template.isolated,
116
+ constraint=template.constraint,
117
+ global_options=template.global_options,
118
+ hash_options=template.hash_options,
119
+ config_settings=template.config_settings,
120
+ )
121
+ ireq.satisfied_by = dist
122
+ return ireq
123
+
124
+
125
+ class _InstallRequirementBackedCandidate(Candidate):
126
+ """A candidate backed by an ``InstallRequirement``.
127
+
128
+ This represents a package request with the target not being already
129
+ in the environment, and needs to be fetched and installed. The backing
130
+ ``InstallRequirement`` is responsible for most of the leg work; this
131
+ class exposes appropriate information to the resolver.
132
+
133
+ :param link: The link passed to the ``InstallRequirement``. The backing
134
+ ``InstallRequirement`` will use this link to fetch the distribution.
135
+ :param source_link: The link this candidate "originates" from. This is
136
+ different from ``link`` when the link is found in the wheel cache.
137
+ ``link`` would point to the wheel cache, while this points to the
138
+ found remote link (e.g. from pypi.org).
139
+ """
140
+
141
+ dist: BaseDistribution
142
+ is_installed = False
143
+
144
+ def __init__(
145
+ self,
146
+ link: Link,
147
+ source_link: Link,
148
+ ireq: InstallRequirement,
149
+ factory: "Factory",
150
+ name: Optional[NormalizedName] = None,
151
+ version: Optional[Version] = None,
152
+ ) -> None:
153
+ self._link = link
154
+ self._source_link = source_link
155
+ self._factory = factory
156
+ self._ireq = ireq
157
+ self._name = name
158
+ self._version = version
159
+ self.dist = self._prepare()
160
+ self._hash: Optional[int] = None
161
+
162
+ def __str__(self) -> str:
163
+ return f"{self.name} {self.version}"
164
+
165
+ def __repr__(self) -> str:
166
+ return f"{self.__class__.__name__}({str(self._link)!r})"
167
+
168
+ def __hash__(self) -> int:
169
+ if self._hash is not None:
170
+ return self._hash
171
+
172
+ self._hash = hash((self.__class__, self._link))
173
+ return self._hash
174
+
175
+ def __eq__(self, other: Any) -> bool:
176
+ if isinstance(other, self.__class__):
177
+ return links_equivalent(self._link, other._link)
178
+ return False
179
+
180
+ @property
181
+ def source_link(self) -> Optional[Link]:
182
+ return self._source_link
183
+
184
+ @property
185
+ def project_name(self) -> NormalizedName:
186
+ """The normalised name of the project the candidate refers to"""
187
+ if self._name is None:
188
+ self._name = self.dist.canonical_name
189
+ return self._name
190
+
191
+ @property
192
+ def name(self) -> str:
193
+ return self.project_name
194
+
195
+ @property
196
+ def version(self) -> Version:
197
+ if self._version is None:
198
+ self._version = self.dist.version
199
+ return self._version
200
+
201
+ def format_for_error(self) -> str:
202
+ return (
203
+ f"{self.name} {self.version} "
204
+ f"(from {self._link.file_path if self._link.is_file else self._link})"
205
+ )
206
+
207
+ def _prepare_distribution(self) -> BaseDistribution:
208
+ raise NotImplementedError("Override in subclass")
209
+
210
+ def _check_metadata_consistency(self, dist: BaseDistribution) -> None:
211
+ """Check for consistency of project name and version of dist."""
212
+ if self._name is not None and self._name != dist.canonical_name:
213
+ raise MetadataInconsistent(
214
+ self._ireq,
215
+ "name",
216
+ self._name,
217
+ dist.canonical_name,
218
+ )
219
+ if self._version is not None and self._version != dist.version:
220
+ raise MetadataInconsistent(
221
+ self._ireq,
222
+ "version",
223
+ str(self._version),
224
+ str(dist.version),
225
+ )
226
+ # check dependencies are valid
227
+ # TODO performance: this means we iterate the dependencies at least twice,
228
+ # we may want to cache parsed Requires-Dist
229
+ try:
230
+ list(dist.iter_dependencies(list(dist.iter_provided_extras())))
231
+ except InvalidRequirement as e:
232
+ raise MetadataInvalid(self._ireq, str(e))
233
+
234
+ def _prepare(self) -> BaseDistribution:
235
+ try:
236
+ dist = self._prepare_distribution()
237
+ except HashError as e:
238
+ # Provide HashError the underlying ireq that caused it. This
239
+ # provides context for the resulting error message to show the
240
+ # offending line to the user.
241
+ e.req = self._ireq
242
+ raise
243
+ except InstallationSubprocessError as exc:
244
+ # The output has been presented already, so don't duplicate it.
245
+ exc.context = "See above for output."
246
+ raise
247
+
248
+ self._check_metadata_consistency(dist)
249
+ return dist
250
+
251
+ def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
252
+ # Emit the Requires-Python requirement first to fail fast on
253
+ # unsupported candidates and avoid pointless downloads/preparation.
254
+ yield self._factory.make_requires_python_requirement(self.dist.requires_python)
255
+ requires = self.dist.iter_dependencies() if with_requires else ()
256
+ for r in requires:
257
+ yield from self._factory.make_requirements_from_spec(str(r), self._ireq)
258
+
259
+ def get_install_requirement(self) -> Optional[InstallRequirement]:
260
+ return self._ireq
261
+
262
+
263
+ class LinkCandidate(_InstallRequirementBackedCandidate):
264
+ is_editable = False
265
+
266
+ def __init__(
267
+ self,
268
+ link: Link,
269
+ template: InstallRequirement,
270
+ factory: "Factory",
271
+ name: Optional[NormalizedName] = None,
272
+ version: Optional[Version] = None,
273
+ ) -> None:
274
+ source_link = link
275
+ cache_entry = factory.get_wheel_cache_entry(source_link, name)
276
+ if cache_entry is not None:
277
+ logger.debug("Using cached wheel link: %s", cache_entry.link)
278
+ link = cache_entry.link
279
+ ireq = make_install_req_from_link(link, template)
280
+ assert ireq.link == link
281
+ if ireq.link.is_wheel and not ireq.link.is_file:
282
+ wheel = Wheel(ireq.link.filename)
283
+ wheel_name = canonicalize_name(wheel.name)
284
+ assert name == wheel_name, f"{name!r} != {wheel_name!r} for wheel"
285
+ # Version may not be present for PEP 508 direct URLs
286
+ if version is not None:
287
+ wheel_version = Version(wheel.version)
288
+ assert (
289
+ version == wheel_version
290
+ ), f"{version!r} != {wheel_version!r} for wheel {name}"
291
+
292
+ if cache_entry is not None:
293
+ assert ireq.link.is_wheel
294
+ assert ireq.link.is_file
295
+ if cache_entry.persistent and template.link is template.original_link:
296
+ ireq.cached_wheel_source_link = source_link
297
+ if cache_entry.origin is not None:
298
+ ireq.download_info = cache_entry.origin
299
+ else:
300
+ # Legacy cache entry that does not have origin.json.
301
+ # download_info may miss the archive_info.hashes field.
302
+ ireq.download_info = direct_url_from_link(
303
+ source_link, link_is_in_wheel_cache=cache_entry.persistent
304
+ )
305
+
306
+ super().__init__(
307
+ link=link,
308
+ source_link=source_link,
309
+ ireq=ireq,
310
+ factory=factory,
311
+ name=name,
312
+ version=version,
313
+ )
314
+
315
+ def _prepare_distribution(self) -> BaseDistribution:
316
+ preparer = self._factory.preparer
317
+ return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
318
+
319
+
320
+ class EditableCandidate(_InstallRequirementBackedCandidate):
321
+ is_editable = True
322
+
323
+ def __init__(
324
+ self,
325
+ link: Link,
326
+ template: InstallRequirement,
327
+ factory: "Factory",
328
+ name: Optional[NormalizedName] = None,
329
+ version: Optional[Version] = None,
330
+ ) -> None:
331
+ super().__init__(
332
+ link=link,
333
+ source_link=link,
334
+ ireq=make_install_req_from_editable(link, template),
335
+ factory=factory,
336
+ name=name,
337
+ version=version,
338
+ )
339
+
340
+ def _prepare_distribution(self) -> BaseDistribution:
341
+ return self._factory.preparer.prepare_editable_requirement(self._ireq)
342
+
343
+
344
+ class AlreadyInstalledCandidate(Candidate):
345
+ is_installed = True
346
+ source_link = None
347
+
348
+ def __init__(
349
+ self,
350
+ dist: BaseDistribution,
351
+ template: InstallRequirement,
352
+ factory: "Factory",
353
+ ) -> None:
354
+ self.dist = dist
355
+ self._ireq = _make_install_req_from_dist(dist, template)
356
+ self._factory = factory
357
+ self._version = None
358
+
359
+ # This is just logging some messages, so we can do it eagerly.
360
+ # The returned dist would be exactly the same as self.dist because we
361
+ # set satisfied_by in _make_install_req_from_dist.
362
+ # TODO: Supply reason based on force_reinstall and upgrade_strategy.
363
+ skip_reason = "already satisfied"
364
+ factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
365
+
366
+ def __str__(self) -> str:
367
+ return str(self.dist)
368
+
369
+ def __repr__(self) -> str:
370
+ return f"{self.__class__.__name__}({self.dist!r})"
371
+
372
+ def __eq__(self, other: object) -> bool:
373
+ if not isinstance(other, AlreadyInstalledCandidate):
374
+ return NotImplemented
375
+ return self.name == other.name and self.version == other.version
376
+
377
+ def __hash__(self) -> int:
378
+ return hash((self.name, self.version))
379
+
380
+ @property
381
+ def project_name(self) -> NormalizedName:
382
+ return self.dist.canonical_name
383
+
384
+ @property
385
+ def name(self) -> str:
386
+ return self.project_name
387
+
388
+ @property
389
+ def version(self) -> Version:
390
+ if self._version is None:
391
+ self._version = self.dist.version
392
+ return self._version
393
+
394
+ @property
395
+ def is_editable(self) -> bool:
396
+ return self.dist.editable
397
+
398
+ def format_for_error(self) -> str:
399
+ return f"{self.name} {self.version} (Installed)"
400
+
401
+ def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
402
+ if not with_requires:
403
+ return
404
+
405
+ try:
406
+ for r in self.dist.iter_dependencies():
407
+ yield from self._factory.make_requirements_from_spec(str(r), self._ireq)
408
+ except InvalidRequirement as exc:
409
+ raise InvalidInstalledPackage(dist=self.dist, invalid_exc=exc) from None
410
+
411
+ def get_install_requirement(self) -> Optional[InstallRequirement]:
412
+ return None
413
+
414
+
415
+ class ExtrasCandidate(Candidate):
416
+ """A candidate that has 'extras', indicating additional dependencies.
417
+
418
+ Requirements can be for a project with dependencies, something like
419
+ foo[extra]. The extras don't affect the project/version being installed
420
+ directly, but indicate that we need additional dependencies. We model that
421
+ by having an artificial ExtrasCandidate that wraps the "base" candidate.
422
+
423
+ The ExtrasCandidate differs from the base in the following ways:
424
+
425
+ 1. It has a unique name, of the form foo[extra]. This causes the resolver
426
+ to treat it as a separate node in the dependency graph.
427
+ 2. When we're getting the candidate's dependencies,
428
+ a) We specify that we want the extra dependencies as well.
429
+ b) We add a dependency on the base candidate.
430
+ See below for why this is needed.
431
+ 3. We return None for the underlying InstallRequirement, as the base
432
+ candidate will provide it, and we don't want to end up with duplicates.
433
+
434
+ The dependency on the base candidate is needed so that the resolver can't
435
+ decide that it should recommend foo[extra1] version 1.0 and foo[extra2]
436
+ version 2.0. Having those candidates depend on foo=1.0 and foo=2.0
437
+ respectively forces the resolver to recognise that this is a conflict.
438
+ """
439
+
440
+ def __init__(
441
+ self,
442
+ base: BaseCandidate,
443
+ extras: FrozenSet[str],
444
+ *,
445
+ comes_from: Optional[InstallRequirement] = None,
446
+ ) -> None:
447
+ """
448
+ :param comes_from: the InstallRequirement that led to this candidate if it
449
+ differs from the base's InstallRequirement. This will often be the
450
+ case in the sense that this candidate's requirement has the extras
451
+ while the base's does not. Unlike the InstallRequirement backed
452
+ candidates, this requirement is used solely for reporting purposes,
453
+ it does not do any leg work.
454
+ """
455
+ self.base = base
456
+ self.extras = frozenset(canonicalize_name(e) for e in extras)
457
+ self._comes_from = comes_from if comes_from is not None else self.base._ireq
458
+
459
+ def __str__(self) -> str:
460
+ name, rest = str(self.base).split(" ", 1)
461
+ return "{}[{}] {}".format(name, ",".join(self.extras), rest)
462
+
463
+ def __repr__(self) -> str:
464
+ return f"{self.__class__.__name__}(base={self.base!r}, extras={self.extras!r})"
465
+
466
+ def __hash__(self) -> int:
467
+ return hash((self.base, self.extras))
468
+
469
+ def __eq__(self, other: Any) -> bool:
470
+ if isinstance(other, self.__class__):
471
+ return self.base == other.base and self.extras == other.extras
472
+ return False
473
+
474
+ @property
475
+ def project_name(self) -> NormalizedName:
476
+ return self.base.project_name
477
+
478
+ @property
479
+ def name(self) -> str:
480
+ """The normalised name of the project the candidate refers to"""
481
+ return format_name(self.base.project_name, self.extras)
482
+
483
+ @property
484
+ def version(self) -> Version:
485
+ return self.base.version
486
+
487
+ def format_for_error(self) -> str:
488
+ return "{} [{}]".format(
489
+ self.base.format_for_error(), ", ".join(sorted(self.extras))
490
+ )
491
+
492
+ @property
493
+ def is_installed(self) -> bool:
494
+ return self.base.is_installed
495
+
496
+ @property
497
+ def is_editable(self) -> bool:
498
+ return self.base.is_editable
499
+
500
+ @property
501
+ def source_link(self) -> Optional[Link]:
502
+ return self.base.source_link
503
+
504
+ def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
505
+ factory = self.base._factory
506
+
507
+ # Add a dependency on the exact base
508
+ # (See note 2b in the class docstring)
509
+ yield factory.make_requirement_from_candidate(self.base)
510
+ if not with_requires:
511
+ return
512
+
513
+ # The user may have specified extras that the candidate doesn't
514
+ # support. We ignore any unsupported extras here.
515
+ valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
516
+ invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
517
+ for extra in sorted(invalid_extras):
518
+ logger.warning(
519
+ "%s %s does not provide the extra '%s'",
520
+ self.base.name,
521
+ self.version,
522
+ extra,
523
+ )
524
+
525
+ for r in self.base.dist.iter_dependencies(valid_extras):
526
+ yield from factory.make_requirements_from_spec(
527
+ str(r),
528
+ self._comes_from,
529
+ valid_extras,
530
+ )
531
+
532
+ def get_install_requirement(self) -> Optional[InstallRequirement]:
533
+ # We don't return anything here, because we always
534
+ # depend on the base candidate, and we'll get the
535
+ # install requirement from that.
536
+ return None
537
+
538
+
539
+ class RequiresPythonCandidate(Candidate):
540
+ is_installed = False
541
+ source_link = None
542
+
543
+ def __init__(self, py_version_info: Optional[Tuple[int, ...]]) -> None:
544
+ if py_version_info is not None:
545
+ version_info = normalize_version_info(py_version_info)
546
+ else:
547
+ version_info = sys.version_info[:3]
548
+ self._version = Version(".".join(str(c) for c in version_info))
549
+
550
+ # We don't need to implement __eq__() and __ne__() since there is always
551
+ # only one RequiresPythonCandidate in a resolution, i.e. the host Python.
552
+ # The built-in object.__eq__() and object.__ne__() do exactly what we want.
553
+
554
+ def __str__(self) -> str:
555
+ return f"Python {self._version}"
556
+
557
+ def __repr__(self) -> str:
558
+ return f"{self.__class__.__name__}({self._version!r})"
559
+
560
+ @property
561
+ def project_name(self) -> NormalizedName:
562
+ return REQUIRES_PYTHON_IDENTIFIER
563
+
564
+ @property
565
+ def name(self) -> str:
566
+ return REQUIRES_PYTHON_IDENTIFIER
567
+
568
+ @property
569
+ def version(self) -> Version:
570
+ return self._version
571
+
572
+ def format_for_error(self) -> str:
573
+ return f"Python {self.version}"
574
+
575
+ def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
576
+ return ()
577
+
578
+ def get_install_requirement(self) -> Optional[InstallRequirement]:
579
+ return None
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/factory.py ADDED
@@ -0,0 +1,823 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+ import functools
3
+ import logging
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Callable,
7
+ Dict,
8
+ FrozenSet,
9
+ Iterable,
10
+ Iterator,
11
+ List,
12
+ Mapping,
13
+ NamedTuple,
14
+ Optional,
15
+ Protocol,
16
+ Sequence,
17
+ Set,
18
+ Tuple,
19
+ TypeVar,
20
+ cast,
21
+ )
22
+
23
+ from pip._vendor.packaging.requirements import InvalidRequirement
24
+ from pip._vendor.packaging.specifiers import SpecifierSet
25
+ from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
26
+ from pip._vendor.packaging.version import InvalidVersion, Version
27
+ from pip._vendor.resolvelib import ResolutionImpossible
28
+
29
+ from pip._internal.cache import CacheEntry, WheelCache
30
+ from pip._internal.exceptions import (
31
+ DistributionNotFound,
32
+ InstallationError,
33
+ InvalidInstalledPackage,
34
+ MetadataInconsistent,
35
+ MetadataInvalid,
36
+ UnsupportedPythonVersion,
37
+ UnsupportedWheel,
38
+ )
39
+ from pip._internal.index.package_finder import PackageFinder
40
+ from pip._internal.metadata import BaseDistribution, get_default_environment
41
+ from pip._internal.models.link import Link
42
+ from pip._internal.models.wheel import Wheel
43
+ from pip._internal.operations.prepare import RequirementPreparer
44
+ from pip._internal.req.constructors import (
45
+ install_req_drop_extras,
46
+ install_req_from_link_and_ireq,
47
+ )
48
+ from pip._internal.req.req_install import (
49
+ InstallRequirement,
50
+ check_invalid_constraint_type,
51
+ )
52
+ from pip._internal.resolution.base import InstallRequirementProvider
53
+ from pip._internal.utils.compatibility_tags import get_supported
54
+ from pip._internal.utils.hashes import Hashes
55
+ from pip._internal.utils.packaging import get_requirement
56
+ from pip._internal.utils.virtualenv import running_under_virtualenv
57
+
58
+ from .base import Candidate, Constraint, Requirement
59
+ from .candidates import (
60
+ AlreadyInstalledCandidate,
61
+ BaseCandidate,
62
+ EditableCandidate,
63
+ ExtrasCandidate,
64
+ LinkCandidate,
65
+ RequiresPythonCandidate,
66
+ as_base_candidate,
67
+ )
68
+ from .found_candidates import FoundCandidates, IndexCandidateInfo
69
+ from .requirements import (
70
+ ExplicitRequirement,
71
+ RequiresPythonRequirement,
72
+ SpecifierRequirement,
73
+ SpecifierWithoutExtrasRequirement,
74
+ UnsatisfiableRequirement,
75
+ )
76
+
77
+ if TYPE_CHECKING:
78
+
79
+ class ConflictCause(Protocol):
80
+ requirement: RequiresPythonRequirement
81
+ parent: Candidate
82
+
83
+
84
+ logger = logging.getLogger(__name__)
85
+
86
+ C = TypeVar("C")
87
+ Cache = Dict[Link, C]
88
+
89
+
90
+ class CollectedRootRequirements(NamedTuple):
91
+ requirements: List[Requirement]
92
+ constraints: Dict[str, Constraint]
93
+ user_requested: Dict[str, int]
94
+
95
+
96
+ class Factory:
97
+ def __init__(
98
+ self,
99
+ finder: PackageFinder,
100
+ preparer: RequirementPreparer,
101
+ make_install_req: InstallRequirementProvider,
102
+ wheel_cache: Optional[WheelCache],
103
+ use_user_site: bool,
104
+ force_reinstall: bool,
105
+ ignore_installed: bool,
106
+ ignore_requires_python: bool,
107
+ py_version_info: Optional[Tuple[int, ...]] = None,
108
+ ) -> None:
109
+ self._finder = finder
110
+ self.preparer = preparer
111
+ self._wheel_cache = wheel_cache
112
+ self._python_candidate = RequiresPythonCandidate(py_version_info)
113
+ self._make_install_req_from_spec = make_install_req
114
+ self._use_user_site = use_user_site
115
+ self._force_reinstall = force_reinstall
116
+ self._ignore_requires_python = ignore_requires_python
117
+
118
+ self._build_failures: Cache[InstallationError] = {}
119
+ self._link_candidate_cache: Cache[LinkCandidate] = {}
120
+ self._editable_candidate_cache: Cache[EditableCandidate] = {}
121
+ self._installed_candidate_cache: Dict[str, AlreadyInstalledCandidate] = {}
122
+ self._extras_candidate_cache: Dict[
123
+ Tuple[int, FrozenSet[NormalizedName]], ExtrasCandidate
124
+ ] = {}
125
+ self._supported_tags_cache = get_supported()
126
+
127
+ if not ignore_installed:
128
+ env = get_default_environment()
129
+ self._installed_dists = {
130
+ dist.canonical_name: dist
131
+ for dist in env.iter_installed_distributions(local_only=False)
132
+ }
133
+ else:
134
+ self._installed_dists = {}
135
+
136
+ @property
137
+ def force_reinstall(self) -> bool:
138
+ return self._force_reinstall
139
+
140
+ def _fail_if_link_is_unsupported_wheel(self, link: Link) -> None:
141
+ if not link.is_wheel:
142
+ return
143
+ wheel = Wheel(link.filename)
144
+ if wheel.supported(self._finder.target_python.get_unsorted_tags()):
145
+ return
146
+ msg = f"{link.filename} is not a supported wheel on this platform."
147
+ raise UnsupportedWheel(msg)
148
+
149
+ def _make_extras_candidate(
150
+ self,
151
+ base: BaseCandidate,
152
+ extras: FrozenSet[str],
153
+ *,
154
+ comes_from: Optional[InstallRequirement] = None,
155
+ ) -> ExtrasCandidate:
156
+ cache_key = (id(base), frozenset(canonicalize_name(e) for e in extras))
157
+ try:
158
+ candidate = self._extras_candidate_cache[cache_key]
159
+ except KeyError:
160
+ candidate = ExtrasCandidate(base, extras, comes_from=comes_from)
161
+ self._extras_candidate_cache[cache_key] = candidate
162
+ return candidate
163
+
164
+ def _make_candidate_from_dist(
165
+ self,
166
+ dist: BaseDistribution,
167
+ extras: FrozenSet[str],
168
+ template: InstallRequirement,
169
+ ) -> Candidate:
170
+ try:
171
+ base = self._installed_candidate_cache[dist.canonical_name]
172
+ except KeyError:
173
+ base = AlreadyInstalledCandidate(dist, template, factory=self)
174
+ self._installed_candidate_cache[dist.canonical_name] = base
175
+ if not extras:
176
+ return base
177
+ return self._make_extras_candidate(base, extras, comes_from=template)
178
+
179
+ def _make_candidate_from_link(
180
+ self,
181
+ link: Link,
182
+ extras: FrozenSet[str],
183
+ template: InstallRequirement,
184
+ name: Optional[NormalizedName],
185
+ version: Optional[Version],
186
+ ) -> Optional[Candidate]:
187
+ base: Optional[BaseCandidate] = self._make_base_candidate_from_link(
188
+ link, template, name, version
189
+ )
190
+ if not extras or base is None:
191
+ return base
192
+ return self._make_extras_candidate(base, extras, comes_from=template)
193
+
194
+ def _make_base_candidate_from_link(
195
+ self,
196
+ link: Link,
197
+ template: InstallRequirement,
198
+ name: Optional[NormalizedName],
199
+ version: Optional[Version],
200
+ ) -> Optional[BaseCandidate]:
201
+ # TODO: Check already installed candidate, and use it if the link and
202
+ # editable flag match.
203
+
204
+ if link in self._build_failures:
205
+ # We already tried this candidate before, and it does not build.
206
+ # Don't bother trying again.
207
+ return None
208
+
209
+ if template.editable:
210
+ if link not in self._editable_candidate_cache:
211
+ try:
212
+ self._editable_candidate_cache[link] = EditableCandidate(
213
+ link,
214
+ template,
215
+ factory=self,
216
+ name=name,
217
+ version=version,
218
+ )
219
+ except (MetadataInconsistent, MetadataInvalid) as e:
220
+ logger.info(
221
+ "Discarding [blue underline]%s[/]: [yellow]%s[reset]",
222
+ link,
223
+ e,
224
+ extra={"markup": True},
225
+ )
226
+ self._build_failures[link] = e
227
+ return None
228
+
229
+ return self._editable_candidate_cache[link]
230
+ else:
231
+ if link not in self._link_candidate_cache:
232
+ try:
233
+ self._link_candidate_cache[link] = LinkCandidate(
234
+ link,
235
+ template,
236
+ factory=self,
237
+ name=name,
238
+ version=version,
239
+ )
240
+ except MetadataInconsistent as e:
241
+ logger.info(
242
+ "Discarding [blue underline]%s[/]: [yellow]%s[reset]",
243
+ link,
244
+ e,
245
+ extra={"markup": True},
246
+ )
247
+ self._build_failures[link] = e
248
+ return None
249
+ return self._link_candidate_cache[link]
250
+
251
+ def _iter_found_candidates(
252
+ self,
253
+ ireqs: Sequence[InstallRequirement],
254
+ specifier: SpecifierSet,
255
+ hashes: Hashes,
256
+ prefers_installed: bool,
257
+ incompatible_ids: Set[int],
258
+ ) -> Iterable[Candidate]:
259
+ if not ireqs:
260
+ return ()
261
+
262
+ # The InstallRequirement implementation requires us to give it a
263
+ # "template". Here we just choose the first requirement to represent
264
+ # all of them.
265
+ # Hopefully the Project model can correct this mismatch in the future.
266
+ template = ireqs[0]
267
+ assert template.req, "Candidates found on index must be PEP 508"
268
+ name = canonicalize_name(template.req.name)
269
+
270
+ extras: FrozenSet[str] = frozenset()
271
+ for ireq in ireqs:
272
+ assert ireq.req, "Candidates found on index must be PEP 508"
273
+ specifier &= ireq.req.specifier
274
+ hashes &= ireq.hashes(trust_internet=False)
275
+ extras |= frozenset(ireq.extras)
276
+
277
+ def _get_installed_candidate() -> Optional[Candidate]:
278
+ """Get the candidate for the currently-installed version."""
279
+ # If --force-reinstall is set, we want the version from the index
280
+ # instead, so we "pretend" there is nothing installed.
281
+ if self._force_reinstall:
282
+ return None
283
+ try:
284
+ installed_dist = self._installed_dists[name]
285
+ except KeyError:
286
+ return None
287
+
288
+ try:
289
+ # Don't use the installed distribution if its version
290
+ # does not fit the current dependency graph.
291
+ if not specifier.contains(installed_dist.version, prereleases=True):
292
+ return None
293
+ except InvalidVersion as e:
294
+ raise InvalidInstalledPackage(dist=installed_dist, invalid_exc=e)
295
+
296
+ candidate = self._make_candidate_from_dist(
297
+ dist=installed_dist,
298
+ extras=extras,
299
+ template=template,
300
+ )
301
+ # The candidate is a known incompatibility. Don't use it.
302
+ if id(candidate) in incompatible_ids:
303
+ return None
304
+ return candidate
305
+
306
+ def iter_index_candidate_infos() -> Iterator[IndexCandidateInfo]:
307
+ result = self._finder.find_best_candidate(
308
+ project_name=name,
309
+ specifier=specifier,
310
+ hashes=hashes,
311
+ )
312
+ icans = result.applicable_candidates
313
+
314
+ # PEP 592: Yanked releases are ignored unless the specifier
315
+ # explicitly pins a version (via '==' or '===') that can be
316
+ # solely satisfied by a yanked release.
317
+ all_yanked = all(ican.link.is_yanked for ican in icans)
318
+
319
+ def is_pinned(specifier: SpecifierSet) -> bool:
320
+ for sp in specifier:
321
+ if sp.operator == "===":
322
+ return True
323
+ if sp.operator != "==":
324
+ continue
325
+ if sp.version.endswith(".*"):
326
+ continue
327
+ return True
328
+ return False
329
+
330
+ pinned = is_pinned(specifier)
331
+
332
+ # PackageFinder returns earlier versions first, so we reverse.
333
+ for ican in reversed(icans):
334
+ if not (all_yanked and pinned) and ican.link.is_yanked:
335
+ continue
336
+ func = functools.partial(
337
+ self._make_candidate_from_link,
338
+ link=ican.link,
339
+ extras=extras,
340
+ template=template,
341
+ name=name,
342
+ version=ican.version,
343
+ )
344
+ yield ican.version, func
345
+
346
+ return FoundCandidates(
347
+ iter_index_candidate_infos,
348
+ _get_installed_candidate(),
349
+ prefers_installed,
350
+ incompatible_ids,
351
+ )
352
+
353
+ def _iter_explicit_candidates_from_base(
354
+ self,
355
+ base_requirements: Iterable[Requirement],
356
+ extras: FrozenSet[str],
357
+ ) -> Iterator[Candidate]:
358
+ """Produce explicit candidates from the base given an extra-ed package.
359
+
360
+ :param base_requirements: Requirements known to the resolver. The
361
+ requirements are guaranteed to not have extras.
362
+ :param extras: The extras to inject into the explicit requirements'
363
+ candidates.
364
+ """
365
+ for req in base_requirements:
366
+ lookup_cand, _ = req.get_candidate_lookup()
367
+ if lookup_cand is None: # Not explicit.
368
+ continue
369
+ # We've stripped extras from the identifier, and should always
370
+ # get a BaseCandidate here, unless there's a bug elsewhere.
371
+ base_cand = as_base_candidate(lookup_cand)
372
+ assert base_cand is not None, "no extras here"
373
+ yield self._make_extras_candidate(base_cand, extras)
374
+
375
+ def _iter_candidates_from_constraints(
376
+ self,
377
+ identifier: str,
378
+ constraint: Constraint,
379
+ template: InstallRequirement,
380
+ ) -> Iterator[Candidate]:
381
+ """Produce explicit candidates from constraints.
382
+
383
+ This creates "fake" InstallRequirement objects that are basically clones
384
+ of what "should" be the template, but with original_link set to link.
385
+ """
386
+ for link in constraint.links:
387
+ self._fail_if_link_is_unsupported_wheel(link)
388
+ candidate = self._make_base_candidate_from_link(
389
+ link,
390
+ template=install_req_from_link_and_ireq(link, template),
391
+ name=canonicalize_name(identifier),
392
+ version=None,
393
+ )
394
+ if candidate:
395
+ yield candidate
396
+
397
+ def find_candidates(
398
+ self,
399
+ identifier: str,
400
+ requirements: Mapping[str, Iterable[Requirement]],
401
+ incompatibilities: Mapping[str, Iterator[Candidate]],
402
+ constraint: Constraint,
403
+ prefers_installed: bool,
404
+ is_satisfied_by: Callable[[Requirement, Candidate], bool],
405
+ ) -> Iterable[Candidate]:
406
+ # Collect basic lookup information from the requirements.
407
+ explicit_candidates: Set[Candidate] = set()
408
+ ireqs: List[InstallRequirement] = []
409
+ for req in requirements[identifier]:
410
+ cand, ireq = req.get_candidate_lookup()
411
+ if cand is not None:
412
+ explicit_candidates.add(cand)
413
+ if ireq is not None:
414
+ ireqs.append(ireq)
415
+
416
+ # If the current identifier contains extras, add requires and explicit
417
+ # candidates from entries from extra-less identifier.
418
+ with contextlib.suppress(InvalidRequirement):
419
+ parsed_requirement = get_requirement(identifier)
420
+ if parsed_requirement.name != identifier:
421
+ explicit_candidates.update(
422
+ self._iter_explicit_candidates_from_base(
423
+ requirements.get(parsed_requirement.name, ()),
424
+ frozenset(parsed_requirement.extras),
425
+ ),
426
+ )
427
+ for req in requirements.get(parsed_requirement.name, []):
428
+ _, ireq = req.get_candidate_lookup()
429
+ if ireq is not None:
430
+ ireqs.append(ireq)
431
+
432
+ # Add explicit candidates from constraints. We only do this if there are
433
+ # known ireqs, which represent requirements not already explicit. If
434
+ # there are no ireqs, we're constraining already-explicit requirements,
435
+ # which is handled later when we return the explicit candidates.
436
+ if ireqs:
437
+ try:
438
+ explicit_candidates.update(
439
+ self._iter_candidates_from_constraints(
440
+ identifier,
441
+ constraint,
442
+ template=ireqs[0],
443
+ ),
444
+ )
445
+ except UnsupportedWheel:
446
+ # If we're constrained to install a wheel incompatible with the
447
+ # target architecture, no candidates will ever be valid.
448
+ return ()
449
+
450
+ # Since we cache all the candidates, incompatibility identification
451
+ # can be made quicker by comparing only the id() values.
452
+ incompat_ids = {id(c) for c in incompatibilities.get(identifier, ())}
453
+
454
+ # If none of the requirements want an explicit candidate, we can ask
455
+ # the finder for candidates.
456
+ if not explicit_candidates:
457
+ return self._iter_found_candidates(
458
+ ireqs,
459
+ constraint.specifier,
460
+ constraint.hashes,
461
+ prefers_installed,
462
+ incompat_ids,
463
+ )
464
+
465
+ return (
466
+ c
467
+ for c in explicit_candidates
468
+ if id(c) not in incompat_ids
469
+ and constraint.is_satisfied_by(c)
470
+ and all(is_satisfied_by(req, c) for req in requirements[identifier])
471
+ )
472
+
473
+ def _make_requirements_from_install_req(
474
+ self, ireq: InstallRequirement, requested_extras: Iterable[str]
475
+ ) -> Iterator[Requirement]:
476
+ """
477
+ Returns requirement objects associated with the given InstallRequirement. In
478
+ most cases this will be a single object but the following special cases exist:
479
+ - the InstallRequirement has markers that do not apply -> result is empty
480
+ - the InstallRequirement has both a constraint (or link) and extras
481
+ -> result is split in two requirement objects: one with the constraint
482
+ (or link) and one with the extra. This allows centralized constraint
483
+ handling for the base, resulting in fewer candidate rejections.
484
+ """
485
+ if not ireq.match_markers(requested_extras):
486
+ logger.info(
487
+ "Ignoring %s: markers '%s' don't match your environment",
488
+ ireq.name,
489
+ ireq.markers,
490
+ )
491
+ elif not ireq.link:
492
+ if ireq.extras and ireq.req is not None and ireq.req.specifier:
493
+ yield SpecifierWithoutExtrasRequirement(ireq)
494
+ yield SpecifierRequirement(ireq)
495
+ else:
496
+ self._fail_if_link_is_unsupported_wheel(ireq.link)
497
+ # Always make the link candidate for the base requirement to make it
498
+ # available to `find_candidates` for explicit candidate lookup for any
499
+ # set of extras.
500
+ # The extras are required separately via a second requirement.
501
+ cand = self._make_base_candidate_from_link(
502
+ ireq.link,
503
+ template=install_req_drop_extras(ireq) if ireq.extras else ireq,
504
+ name=canonicalize_name(ireq.name) if ireq.name else None,
505
+ version=None,
506
+ )
507
+ if cand is None:
508
+ # There's no way we can satisfy a URL requirement if the underlying
509
+ # candidate fails to build. An unnamed URL must be user-supplied, so
510
+ # we fail eagerly. If the URL is named, an unsatisfiable requirement
511
+ # can make the resolver do the right thing, either backtrack (and
512
+ # maybe find some other requirement that's buildable) or raise a
513
+ # ResolutionImpossible eventually.
514
+ if not ireq.name:
515
+ raise self._build_failures[ireq.link]
516
+ yield UnsatisfiableRequirement(canonicalize_name(ireq.name))
517
+ else:
518
+ # require the base from the link
519
+ yield self.make_requirement_from_candidate(cand)
520
+ if ireq.extras:
521
+ # require the extras on top of the base candidate
522
+ yield self.make_requirement_from_candidate(
523
+ self._make_extras_candidate(cand, frozenset(ireq.extras))
524
+ )
525
+
526
+ def collect_root_requirements(
527
+ self, root_ireqs: List[InstallRequirement]
528
+ ) -> CollectedRootRequirements:
529
+ collected = CollectedRootRequirements([], {}, {})
530
+ for i, ireq in enumerate(root_ireqs):
531
+ if ireq.constraint:
532
+ # Ensure we only accept valid constraints
533
+ problem = check_invalid_constraint_type(ireq)
534
+ if problem:
535
+ raise InstallationError(problem)
536
+ if not ireq.match_markers():
537
+ continue
538
+ assert ireq.name, "Constraint must be named"
539
+ name = canonicalize_name(ireq.name)
540
+ if name in collected.constraints:
541
+ collected.constraints[name] &= ireq
542
+ else:
543
+ collected.constraints[name] = Constraint.from_ireq(ireq)
544
+ else:
545
+ reqs = list(
546
+ self._make_requirements_from_install_req(
547
+ ireq,
548
+ requested_extras=(),
549
+ )
550
+ )
551
+ if not reqs:
552
+ continue
553
+ template = reqs[0]
554
+ if ireq.user_supplied and template.name not in collected.user_requested:
555
+ collected.user_requested[template.name] = i
556
+ collected.requirements.extend(reqs)
557
+ # Put requirements with extras at the end of the root requires. This does not
558
+ # affect resolvelib's picking preference but it does affect its initial criteria
559
+ # population: by putting extras at the end we enable the candidate finder to
560
+ # present resolvelib with a smaller set of candidates to resolvelib, already
561
+ # taking into account any non-transient constraints on the associated base. This
562
+ # means resolvelib will have fewer candidates to visit and reject.
563
+ # Python's list sort is stable, meaning relative order is kept for objects with
564
+ # the same key.
565
+ collected.requirements.sort(key=lambda r: r.name != r.project_name)
566
+ return collected
567
+
568
+ def make_requirement_from_candidate(
569
+ self, candidate: Candidate
570
+ ) -> ExplicitRequirement:
571
+ return ExplicitRequirement(candidate)
572
+
573
+ def make_requirements_from_spec(
574
+ self,
575
+ specifier: str,
576
+ comes_from: Optional[InstallRequirement],
577
+ requested_extras: Iterable[str] = (),
578
+ ) -> Iterator[Requirement]:
579
+ """
580
+ Returns requirement objects associated with the given specifier. In most cases
581
+ this will be a single object but the following special cases exist:
582
+ - the specifier has markers that do not apply -> result is empty
583
+ - the specifier has both a constraint and extras -> result is split
584
+ in two requirement objects: one with the constraint and one with the
585
+ extra. This allows centralized constraint handling for the base,
586
+ resulting in fewer candidate rejections.
587
+ """
588
+ ireq = self._make_install_req_from_spec(specifier, comes_from)
589
+ return self._make_requirements_from_install_req(ireq, requested_extras)
590
+
591
+ def make_requires_python_requirement(
592
+ self,
593
+ specifier: SpecifierSet,
594
+ ) -> Optional[Requirement]:
595
+ if self._ignore_requires_python:
596
+ return None
597
+ # Don't bother creating a dependency for an empty Requires-Python.
598
+ if not str(specifier):
599
+ return None
600
+ return RequiresPythonRequirement(specifier, self._python_candidate)
601
+
602
+ def get_wheel_cache_entry(
603
+ self, link: Link, name: Optional[str]
604
+ ) -> Optional[CacheEntry]:
605
+ """Look up the link in the wheel cache.
606
+
607
+ If ``preparer.require_hashes`` is True, don't use the wheel cache,
608
+ because cached wheels, always built locally, have different hashes
609
+ than the files downloaded from the index server and thus throw false
610
+ hash mismatches. Furthermore, cached wheels at present have
611
+ nondeterministic contents due to file modification times.
612
+ """
613
+ if self._wheel_cache is None:
614
+ return None
615
+ return self._wheel_cache.get_cache_entry(
616
+ link=link,
617
+ package_name=name,
618
+ supported_tags=self._supported_tags_cache,
619
+ )
620
+
621
+ def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[BaseDistribution]:
622
+ # TODO: Are there more cases this needs to return True? Editable?
623
+ dist = self._installed_dists.get(candidate.project_name)
624
+ if dist is None: # Not installed, no uninstallation required.
625
+ return None
626
+
627
+ # We're installing into global site. The current installation must
628
+ # be uninstalled, no matter it's in global or user site, because the
629
+ # user site installation has precedence over global.
630
+ if not self._use_user_site:
631
+ return dist
632
+
633
+ # We're installing into user site. Remove the user site installation.
634
+ if dist.in_usersite:
635
+ return dist
636
+
637
+ # We're installing into user site, but the installed incompatible
638
+ # package is in global site. We can't uninstall that, and would let
639
+ # the new user installation to "shadow" it. But shadowing won't work
640
+ # in virtual environments, so we error out.
641
+ if running_under_virtualenv() and dist.in_site_packages:
642
+ message = (
643
+ f"Will not install to the user site because it will lack "
644
+ f"sys.path precedence to {dist.raw_name} in {dist.location}"
645
+ )
646
+ raise InstallationError(message)
647
+ return None
648
+
649
+ def _report_requires_python_error(
650
+ self, causes: Sequence["ConflictCause"]
651
+ ) -> UnsupportedPythonVersion:
652
+ assert causes, "Requires-Python error reported with no cause"
653
+
654
+ version = self._python_candidate.version
655
+
656
+ if len(causes) == 1:
657
+ specifier = str(causes[0].requirement.specifier)
658
+ message = (
659
+ f"Package {causes[0].parent.name!r} requires a different "
660
+ f"Python: {version} not in {specifier!r}"
661
+ )
662
+ return UnsupportedPythonVersion(message)
663
+
664
+ message = f"Packages require a different Python. {version} not in:"
665
+ for cause in causes:
666
+ package = cause.parent.format_for_error()
667
+ specifier = str(cause.requirement.specifier)
668
+ message += f"\n{specifier!r} (required by {package})"
669
+ return UnsupportedPythonVersion(message)
670
+
671
+ def _report_single_requirement_conflict(
672
+ self, req: Requirement, parent: Optional[Candidate]
673
+ ) -> DistributionNotFound:
674
+ if parent is None:
675
+ req_disp = str(req)
676
+ else:
677
+ req_disp = f"{req} (from {parent.name})"
678
+
679
+ cands = self._finder.find_all_candidates(req.project_name)
680
+ skipped_by_requires_python = self._finder.requires_python_skipped_reasons()
681
+
682
+ versions_set: Set[Version] = set()
683
+ yanked_versions_set: Set[Version] = set()
684
+ for c in cands:
685
+ is_yanked = c.link.is_yanked if c.link else False
686
+ if is_yanked:
687
+ yanked_versions_set.add(c.version)
688
+ else:
689
+ versions_set.add(c.version)
690
+
691
+ versions = [str(v) for v in sorted(versions_set)]
692
+ yanked_versions = [str(v) for v in sorted(yanked_versions_set)]
693
+
694
+ if yanked_versions:
695
+ # Saying "version X is yanked" isn't entirely accurate.
696
+ # https://github.com/pypa/pip/issues/11745#issuecomment-1402805842
697
+ logger.critical(
698
+ "Ignored the following yanked versions: %s",
699
+ ", ".join(yanked_versions) or "none",
700
+ )
701
+ if skipped_by_requires_python:
702
+ logger.critical(
703
+ "Ignored the following versions that require a different python "
704
+ "version: %s",
705
+ "; ".join(skipped_by_requires_python) or "none",
706
+ )
707
+ logger.critical(
708
+ "Could not find a version that satisfies the requirement %s "
709
+ "(from versions: %s)",
710
+ req_disp,
711
+ ", ".join(versions) or "none",
712
+ )
713
+ if str(req) == "requirements.txt":
714
+ logger.info(
715
+ "HINT: You are attempting to install a package literally "
716
+ 'named "requirements.txt" (which cannot exist). Consider '
717
+ "using the '-r' flag to install the packages listed in "
718
+ "requirements.txt"
719
+ )
720
+
721
+ return DistributionNotFound(f"No matching distribution found for {req}")
722
+
723
+ def get_installation_error(
724
+ self,
725
+ e: "ResolutionImpossible[Requirement, Candidate]",
726
+ constraints: Dict[str, Constraint],
727
+ ) -> InstallationError:
728
+ assert e.causes, "Installation error reported with no cause"
729
+
730
+ # If one of the things we can't solve is "we need Python X.Y",
731
+ # that is what we report.
732
+ requires_python_causes = [
733
+ cause
734
+ for cause in e.causes
735
+ if isinstance(cause.requirement, RequiresPythonRequirement)
736
+ and not cause.requirement.is_satisfied_by(self._python_candidate)
737
+ ]
738
+ if requires_python_causes:
739
+ # The comprehension above makes sure all Requirement instances are
740
+ # RequiresPythonRequirement, so let's cast for convenience.
741
+ return self._report_requires_python_error(
742
+ cast("Sequence[ConflictCause]", requires_python_causes),
743
+ )
744
+
745
+ # Otherwise, we have a set of causes which can't all be satisfied
746
+ # at once.
747
+
748
+ # The simplest case is when we have *one* cause that can't be
749
+ # satisfied. We just report that case.
750
+ if len(e.causes) == 1:
751
+ req, parent = next(iter(e.causes))
752
+ if req.name not in constraints:
753
+ return self._report_single_requirement_conflict(req, parent)
754
+
755
+ # OK, we now have a list of requirements that can't all be
756
+ # satisfied at once.
757
+
758
+ # A couple of formatting helpers
759
+ def text_join(parts: List[str]) -> str:
760
+ if len(parts) == 1:
761
+ return parts[0]
762
+
763
+ return ", ".join(parts[:-1]) + " and " + parts[-1]
764
+
765
+ def describe_trigger(parent: Candidate) -> str:
766
+ ireq = parent.get_install_requirement()
767
+ if not ireq or not ireq.comes_from:
768
+ return f"{parent.name}=={parent.version}"
769
+ if isinstance(ireq.comes_from, InstallRequirement):
770
+ return str(ireq.comes_from.name)
771
+ return str(ireq.comes_from)
772
+
773
+ triggers = set()
774
+ for req, parent in e.causes:
775
+ if parent is None:
776
+ # This is a root requirement, so we can report it directly
777
+ trigger = req.format_for_error()
778
+ else:
779
+ trigger = describe_trigger(parent)
780
+ triggers.add(trigger)
781
+
782
+ if triggers:
783
+ info = text_join(sorted(triggers))
784
+ else:
785
+ info = "the requested packages"
786
+
787
+ msg = (
788
+ f"Cannot install {info} because these package versions "
789
+ "have conflicting dependencies."
790
+ )
791
+ logger.critical(msg)
792
+ msg = "\nThe conflict is caused by:"
793
+
794
+ relevant_constraints = set()
795
+ for req, parent in e.causes:
796
+ if req.name in constraints:
797
+ relevant_constraints.add(req.name)
798
+ msg = msg + "\n "
799
+ if parent:
800
+ msg = msg + f"{parent.name} {parent.version} depends on "
801
+ else:
802
+ msg = msg + "The user requested "
803
+ msg = msg + req.format_for_error()
804
+ for key in relevant_constraints:
805
+ spec = constraints[key].specifier
806
+ msg += f"\n The user requested (constraint) {key}{spec}"
807
+
808
+ msg = (
809
+ msg
810
+ + "\n\n"
811
+ + "To fix this you could try to:\n"
812
+ + "1. loosen the range of package versions you've specified\n"
813
+ + "2. remove package versions to allow pip to attempt to solve "
814
+ + "the dependency conflict\n"
815
+ )
816
+
817
+ logger.info(msg)
818
+
819
+ return DistributionNotFound(
820
+ "ResolutionImpossible: for help visit "
821
+ "https://pip.pypa.io/en/latest/topics/dependency-resolution/"
822
+ "#dealing-with-dependency-conflicts"
823
+ )
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utilities to lazily create and visit candidates found.
2
+
3
+ Creating and visiting a candidate is a *very* costly operation. It involves
4
+ fetching, extracting, potentially building modules from source, and verifying
5
+ distribution metadata. It is therefore crucial for performance to keep
6
+ everything here lazy all the way down, so we only touch candidates that we
7
+ absolutely need, and not "download the world" when we only need one version of
8
+ something.
9
+ """
10
+
11
+ import logging
12
+ from collections.abc import Sequence
13
+ from typing import Any, Callable, Iterator, Optional, Set, Tuple
14
+
15
+ from pip._vendor.packaging.version import _BaseVersion
16
+
17
+ from pip._internal.exceptions import MetadataInvalid
18
+
19
+ from .base import Candidate
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ IndexCandidateInfo = Tuple[_BaseVersion, Callable[[], Optional[Candidate]]]
24
+
25
+
26
+ def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
27
+ """Iterator for ``FoundCandidates``.
28
+
29
+ This iterator is used when the package is not already installed. Candidates
30
+ from index come later in their normal ordering.
31
+ """
32
+ versions_found: Set[_BaseVersion] = set()
33
+ for version, func in infos:
34
+ if version in versions_found:
35
+ continue
36
+ try:
37
+ candidate = func()
38
+ except MetadataInvalid as e:
39
+ logger.warning(
40
+ "Ignoring version %s of %s since it has invalid metadata:\n"
41
+ "%s\n"
42
+ "Please use pip<24.1 if you need to use this version.",
43
+ version,
44
+ e.ireq.name,
45
+ e,
46
+ )
47
+ # Mark version as found to avoid trying other candidates with the same
48
+ # version, since they most likely have invalid metadata as well.
49
+ versions_found.add(version)
50
+ else:
51
+ if candidate is None:
52
+ continue
53
+ yield candidate
54
+ versions_found.add(version)
55
+
56
+
57
+ def _iter_built_with_prepended(
58
+ installed: Candidate, infos: Iterator[IndexCandidateInfo]
59
+ ) -> Iterator[Candidate]:
60
+ """Iterator for ``FoundCandidates``.
61
+
62
+ This iterator is used when the resolver prefers the already-installed
63
+ candidate and NOT to upgrade. The installed candidate is therefore
64
+ always yielded first, and candidates from index come later in their
65
+ normal ordering, except skipped when the version is already installed.
66
+ """
67
+ yield installed
68
+ versions_found: Set[_BaseVersion] = {installed.version}
69
+ for version, func in infos:
70
+ if version in versions_found:
71
+ continue
72
+ candidate = func()
73
+ if candidate is None:
74
+ continue
75
+ yield candidate
76
+ versions_found.add(version)
77
+
78
+
79
+ def _iter_built_with_inserted(
80
+ installed: Candidate, infos: Iterator[IndexCandidateInfo]
81
+ ) -> Iterator[Candidate]:
82
+ """Iterator for ``FoundCandidates``.
83
+
84
+ This iterator is used when the resolver prefers to upgrade an
85
+ already-installed package. Candidates from index are returned in their
86
+ normal ordering, except replaced when the version is already installed.
87
+
88
+ The implementation iterates through and yields other candidates, inserting
89
+ the installed candidate exactly once before we start yielding older or
90
+ equivalent candidates, or after all other candidates if they are all newer.
91
+ """
92
+ versions_found: Set[_BaseVersion] = set()
93
+ for version, func in infos:
94
+ if version in versions_found:
95
+ continue
96
+ # If the installed candidate is better, yield it first.
97
+ if installed.version >= version:
98
+ yield installed
99
+ versions_found.add(installed.version)
100
+ candidate = func()
101
+ if candidate is None:
102
+ continue
103
+ yield candidate
104
+ versions_found.add(version)
105
+
106
+ # If the installed candidate is older than all other candidates.
107
+ if installed.version not in versions_found:
108
+ yield installed
109
+
110
+
111
+ class FoundCandidates(Sequence[Candidate]):
112
+ """A lazy sequence to provide candidates to the resolver.
113
+
114
+ The intended usage is to return this from `find_matches()` so the resolver
115
+ can iterate through the sequence multiple times, but only access the index
116
+ page when remote packages are actually needed. This improve performances
117
+ when suitable candidates are already installed on disk.
118
+ """
119
+
120
+ def __init__(
121
+ self,
122
+ get_infos: Callable[[], Iterator[IndexCandidateInfo]],
123
+ installed: Optional[Candidate],
124
+ prefers_installed: bool,
125
+ incompatible_ids: Set[int],
126
+ ):
127
+ self._get_infos = get_infos
128
+ self._installed = installed
129
+ self._prefers_installed = prefers_installed
130
+ self._incompatible_ids = incompatible_ids
131
+ self._bool: Optional[bool] = None
132
+
133
+ def __getitem__(self, index: Any) -> Any:
134
+ # Implemented to satisfy the ABC check. This is not needed by the
135
+ # resolver, and should not be used by the provider either (for
136
+ # performance reasons).
137
+ raise NotImplementedError("don't do this")
138
+
139
+ def __iter__(self) -> Iterator[Candidate]:
140
+ infos = self._get_infos()
141
+ if not self._installed:
142
+ iterator = _iter_built(infos)
143
+ elif self._prefers_installed:
144
+ iterator = _iter_built_with_prepended(self._installed, infos)
145
+ else:
146
+ iterator = _iter_built_with_inserted(self._installed, infos)
147
+ return (c for c in iterator if id(c) not in self._incompatible_ids)
148
+
149
+ def __len__(self) -> int:
150
+ # Implemented to satisfy the ABC check. This is not needed by the
151
+ # resolver, and should not be used by the provider either (for
152
+ # performance reasons).
153
+ raise NotImplementedError("don't do this")
154
+
155
+ def __bool__(self) -> bool:
156
+ if self._bool is not None:
157
+ return self._bool
158
+
159
+ if self._prefers_installed and self._installed:
160
+ self._bool = True
161
+ return True
162
+
163
+ self._bool = any(self)
164
+ return self._bool
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/provider.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from functools import lru_cache
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Dict,
6
+ Iterable,
7
+ Iterator,
8
+ Mapping,
9
+ Optional,
10
+ Sequence,
11
+ Tuple,
12
+ TypeVar,
13
+ Union,
14
+ )
15
+
16
+ from pip._vendor.resolvelib.providers import AbstractProvider
17
+
18
+ from pip._internal.req.req_install import InstallRequirement
19
+
20
+ from .base import Candidate, Constraint, Requirement
21
+ from .candidates import REQUIRES_PYTHON_IDENTIFIER
22
+ from .factory import Factory
23
+ from .requirements import ExplicitRequirement
24
+
25
+ if TYPE_CHECKING:
26
+ from pip._vendor.resolvelib.providers import Preference
27
+ from pip._vendor.resolvelib.resolvers import RequirementInformation
28
+
29
+ PreferenceInformation = RequirementInformation[Requirement, Candidate]
30
+
31
+ _ProviderBase = AbstractProvider[Requirement, Candidate, str]
32
+ else:
33
+ _ProviderBase = AbstractProvider
34
+
35
+ # Notes on the relationship between the provider, the factory, and the
36
+ # candidate and requirement classes.
37
+ #
38
+ # The provider is a direct implementation of the resolvelib class. Its role
39
+ # is to deliver the API that resolvelib expects.
40
+ #
41
+ # Rather than work with completely abstract "requirement" and "candidate"
42
+ # concepts as resolvelib does, pip has concrete classes implementing these two
43
+ # ideas. The API of Requirement and Candidate objects are defined in the base
44
+ # classes, but essentially map fairly directly to the equivalent provider
45
+ # methods. In particular, `find_matches` and `is_satisfied_by` are
46
+ # requirement methods, and `get_dependencies` is a candidate method.
47
+ #
48
+ # The factory is the interface to pip's internal mechanisms. It is stateless,
49
+ # and is created by the resolver and held as a property of the provider. It is
50
+ # responsible for creating Requirement and Candidate objects, and provides
51
+ # services to those objects (access to pip's finder and preparer).
52
+
53
+
54
+ D = TypeVar("D")
55
+ V = TypeVar("V")
56
+
57
+
58
+ def _get_with_identifier(
59
+ mapping: Mapping[str, V],
60
+ identifier: str,
61
+ default: D,
62
+ ) -> Union[D, V]:
63
+ """Get item from a package name lookup mapping with a resolver identifier.
64
+
65
+ This extra logic is needed when the target mapping is keyed by package
66
+ name, which cannot be directly looked up with an identifier (which may
67
+ contain requested extras). Additional logic is added to also look up a value
68
+ by "cleaning up" the extras from the identifier.
69
+ """
70
+ if identifier in mapping:
71
+ return mapping[identifier]
72
+ # HACK: Theoretically we should check whether this identifier is a valid
73
+ # "NAME[EXTRAS]" format, and parse out the name part with packaging or
74
+ # some regular expression. But since pip's resolver only spits out three
75
+ # kinds of identifiers: normalized PEP 503 names, normalized names plus
76
+ # extras, and Requires-Python, we can cheat a bit here.
77
+ name, open_bracket, _ = identifier.partition("[")
78
+ if open_bracket and name in mapping:
79
+ return mapping[name]
80
+ return default
81
+
82
+
83
+ class PipProvider(_ProviderBase):
84
+ """Pip's provider implementation for resolvelib.
85
+
86
+ :params constraints: A mapping of constraints specified by the user. Keys
87
+ are canonicalized project names.
88
+ :params ignore_dependencies: Whether the user specified ``--no-deps``.
89
+ :params upgrade_strategy: The user-specified upgrade strategy.
90
+ :params user_requested: A set of canonicalized package names that the user
91
+ supplied for pip to install/upgrade.
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ factory: Factory,
97
+ constraints: Dict[str, Constraint],
98
+ ignore_dependencies: bool,
99
+ upgrade_strategy: str,
100
+ user_requested: Dict[str, int],
101
+ ) -> None:
102
+ self._factory = factory
103
+ self._constraints = constraints
104
+ self._ignore_dependencies = ignore_dependencies
105
+ self._upgrade_strategy = upgrade_strategy
106
+ self._user_requested = user_requested
107
+
108
+ def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str:
109
+ return requirement_or_candidate.name
110
+
111
+ def narrow_requirement_selection(
112
+ self,
113
+ identifiers: Iterable[str],
114
+ resolutions: Mapping[str, Candidate],
115
+ candidates: Mapping[str, Iterator[Candidate]],
116
+ information: Mapping[str, Iterator["PreferenceInformation"]],
117
+ backtrack_causes: Sequence["PreferenceInformation"],
118
+ ) -> Iterable[str]:
119
+ """Produce a subset of identifiers that should be considered before others.
120
+
121
+ Currently pip narrows the following selection:
122
+ * Requires-Python, if present is always returned by itself
123
+ * Backtrack causes are considered next because they can be identified
124
+ in linear time here, whereas because get_preference() is called
125
+ for each identifier, it would be quadratic to check for them there.
126
+ Further, the current backtrack causes likely need to be resolved
127
+ before other requirements as a resolution can't be found while
128
+ there is a conflict.
129
+ """
130
+ backtrack_identifiers = set()
131
+ for info in backtrack_causes:
132
+ backtrack_identifiers.add(info.requirement.name)
133
+ if info.parent is not None:
134
+ backtrack_identifiers.add(info.parent.name)
135
+
136
+ current_backtrack_causes = []
137
+ for identifier in identifiers:
138
+ # Requires-Python has only one candidate and the check is basically
139
+ # free, so we always do it first to avoid needless work if it fails.
140
+ # This skips calling get_preference() for all other identifiers.
141
+ if identifier == REQUIRES_PYTHON_IDENTIFIER:
142
+ return [identifier]
143
+
144
+ # Check if this identifier is a backtrack cause
145
+ if identifier in backtrack_identifiers:
146
+ current_backtrack_causes.append(identifier)
147
+ continue
148
+
149
+ if current_backtrack_causes:
150
+ return current_backtrack_causes
151
+
152
+ return identifiers
153
+
154
+ def get_preference(
155
+ self,
156
+ identifier: str,
157
+ resolutions: Mapping[str, Candidate],
158
+ candidates: Mapping[str, Iterator[Candidate]],
159
+ information: Mapping[str, Iterable["PreferenceInformation"]],
160
+ backtrack_causes: Sequence["PreferenceInformation"],
161
+ ) -> "Preference":
162
+ """Produce a sort key for given requirement based on preference.
163
+
164
+ The lower the return value is, the more preferred this group of
165
+ arguments is.
166
+
167
+ Currently pip considers the following in order:
168
+
169
+ * Any requirement that is "direct", e.g., points to an explicit URL.
170
+ * Any requirement that is "pinned", i.e., contains the operator ``===``
171
+ or ``==`` without a wildcard.
172
+ * Any requirement that imposes an upper version limit, i.e., contains the
173
+ operator ``<``, ``<=``, ``~=``, or ``==`` with a wildcard. Because
174
+ pip prioritizes the latest version, preferring explicit upper bounds
175
+ can rule out infeasible candidates sooner. This does not imply that
176
+ upper bounds are good practice; they can make dependency management
177
+ and resolution harder.
178
+ * Order user-specified requirements as they are specified, placing
179
+ other requirements afterward.
180
+ * Any "non-free" requirement, i.e., one that contains at least one
181
+ operator, such as ``>=`` or ``!=``.
182
+ * Alphabetical order for consistency (aids debuggability).
183
+ """
184
+ try:
185
+ next(iter(information[identifier]))
186
+ except StopIteration:
187
+ # There is no information for this identifier, so there's no known
188
+ # candidates.
189
+ has_information = False
190
+ else:
191
+ has_information = True
192
+
193
+ if not has_information:
194
+ direct = False
195
+ ireqs: Tuple[Optional[InstallRequirement], ...] = ()
196
+ else:
197
+ # Go through the information and for each requirement,
198
+ # check if it's explicit (e.g., a direct link) and get the
199
+ # InstallRequirement (the second element) from get_candidate_lookup()
200
+ directs, ireqs = zip(
201
+ *(
202
+ (isinstance(r, ExplicitRequirement), r.get_candidate_lookup()[1])
203
+ for r, _ in information[identifier]
204
+ )
205
+ )
206
+ direct = any(directs)
207
+
208
+ operators: list[tuple[str, str]] = [
209
+ (specifier.operator, specifier.version)
210
+ for specifier_set in (ireq.specifier for ireq in ireqs if ireq)
211
+ for specifier in specifier_set
212
+ ]
213
+
214
+ pinned = any(((op[:2] == "==") and ("*" not in ver)) for op, ver in operators)
215
+ upper_bounded = any(
216
+ ((op in ("<", "<=", "~=")) or (op == "==" and "*" in ver))
217
+ for op, ver in operators
218
+ )
219
+ unfree = bool(operators)
220
+ requested_order = self._user_requested.get(identifier, math.inf)
221
+
222
+ return (
223
+ not direct,
224
+ not pinned,
225
+ not upper_bounded,
226
+ requested_order,
227
+ not unfree,
228
+ identifier,
229
+ )
230
+
231
+ def find_matches(
232
+ self,
233
+ identifier: str,
234
+ requirements: Mapping[str, Iterator[Requirement]],
235
+ incompatibilities: Mapping[str, Iterator[Candidate]],
236
+ ) -> Iterable[Candidate]:
237
+ def _eligible_for_upgrade(identifier: str) -> bool:
238
+ """Are upgrades allowed for this project?
239
+
240
+ This checks the upgrade strategy, and whether the project was one
241
+ that the user specified in the command line, in order to decide
242
+ whether we should upgrade if there's a newer version available.
243
+
244
+ (Note that we don't need access to the `--upgrade` flag, because
245
+ an upgrade strategy of "to-satisfy-only" means that `--upgrade`
246
+ was not specified).
247
+ """
248
+ if self._upgrade_strategy == "eager":
249
+ return True
250
+ elif self._upgrade_strategy == "only-if-needed":
251
+ user_order = _get_with_identifier(
252
+ self._user_requested,
253
+ identifier,
254
+ default=None,
255
+ )
256
+ return user_order is not None
257
+ return False
258
+
259
+ constraint = _get_with_identifier(
260
+ self._constraints,
261
+ identifier,
262
+ default=Constraint.empty(),
263
+ )
264
+ return self._factory.find_candidates(
265
+ identifier=identifier,
266
+ requirements=requirements,
267
+ constraint=constraint,
268
+ prefers_installed=(not _eligible_for_upgrade(identifier)),
269
+ incompatibilities=incompatibilities,
270
+ is_satisfied_by=self.is_satisfied_by,
271
+ )
272
+
273
+ @staticmethod
274
+ @lru_cache(maxsize=None)
275
+ def is_satisfied_by(requirement: Requirement, candidate: Candidate) -> bool:
276
+ return requirement.is_satisfied_by(candidate)
277
+
278
+ def get_dependencies(self, candidate: Candidate) -> Iterable[Requirement]:
279
+ with_requires = not self._ignore_dependencies
280
+ # iter_dependencies() can perform nontrivial work so delay until needed.
281
+ return (r for r in candidate.iter_dependencies(with_requires) if r is not None)
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/reporter.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict
2
+ from logging import getLogger
3
+ from typing import Any, DefaultDict, Optional
4
+
5
+ from pip._vendor.resolvelib.reporters import BaseReporter
6
+
7
+ from .base import Candidate, Requirement
8
+
9
+ logger = getLogger(__name__)
10
+
11
+
12
+ class PipReporter(BaseReporter[Requirement, Candidate, str]):
13
+ def __init__(self) -> None:
14
+ self.reject_count_by_package: DefaultDict[str, int] = defaultdict(int)
15
+
16
+ self._messages_at_reject_count = {
17
+ 1: (
18
+ "pip is looking at multiple versions of {package_name} to "
19
+ "determine which version is compatible with other "
20
+ "requirements. This could take a while."
21
+ ),
22
+ 8: (
23
+ "pip is still looking at multiple versions of {package_name} to "
24
+ "determine which version is compatible with other "
25
+ "requirements. This could take a while."
26
+ ),
27
+ 13: (
28
+ "This is taking longer than usual. You might need to provide "
29
+ "the dependency resolver with stricter constraints to reduce "
30
+ "runtime. See https://pip.pypa.io/warnings/backtracking for "
31
+ "guidance. If you want to abort this run, press Ctrl + C."
32
+ ),
33
+ }
34
+
35
+ def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
36
+ self.reject_count_by_package[candidate.name] += 1
37
+
38
+ count = self.reject_count_by_package[candidate.name]
39
+ if count not in self._messages_at_reject_count:
40
+ return
41
+
42
+ message = self._messages_at_reject_count[count]
43
+ logger.info("INFO: %s", message.format(package_name=candidate.name))
44
+
45
+ msg = "Will try a different candidate, due to conflict:"
46
+ for req_info in criterion.information:
47
+ req, parent = req_info.requirement, req_info.parent
48
+ # Inspired by Factory.get_installation_error
49
+ msg += "\n "
50
+ if parent:
51
+ msg += f"{parent.name} {parent.version} depends on "
52
+ else:
53
+ msg += "The user requested "
54
+ msg += req.format_for_error()
55
+ logger.debug(msg)
56
+
57
+
58
+ class PipDebuggingReporter(BaseReporter[Requirement, Candidate, str]):
59
+ """A reporter that does an info log for every event it sees."""
60
+
61
+ def starting(self) -> None:
62
+ logger.info("Reporter.starting()")
63
+
64
+ def starting_round(self, index: int) -> None:
65
+ logger.info("Reporter.starting_round(%r)", index)
66
+
67
+ def ending_round(self, index: int, state: Any) -> None:
68
+ logger.info("Reporter.ending_round(%r, state)", index)
69
+ logger.debug("Reporter.ending_round(%r, %r)", index, state)
70
+
71
+ def ending(self, state: Any) -> None:
72
+ logger.info("Reporter.ending(%r)", state)
73
+
74
+ def adding_requirement(
75
+ self, requirement: Requirement, parent: Optional[Candidate]
76
+ ) -> None:
77
+ logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent)
78
+
79
+ def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
80
+ logger.info("Reporter.rejecting_candidate(%r, %r)", criterion, candidate)
81
+
82
+ def pinning(self, candidate: Candidate) -> None:
83
+ logger.info("Reporter.pinning(%r)", candidate)
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/requirements.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Optional
2
+
3
+ from pip._vendor.packaging.specifiers import SpecifierSet
4
+ from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
5
+
6
+ from pip._internal.req.constructors import install_req_drop_extras
7
+ from pip._internal.req.req_install import InstallRequirement
8
+
9
+ from .base import Candidate, CandidateLookup, Requirement, format_name
10
+
11
+
12
+ class ExplicitRequirement(Requirement):
13
+ def __init__(self, candidate: Candidate) -> None:
14
+ self.candidate = candidate
15
+
16
+ def __str__(self) -> str:
17
+ return str(self.candidate)
18
+
19
+ def __repr__(self) -> str:
20
+ return f"{self.__class__.__name__}({self.candidate!r})"
21
+
22
+ def __hash__(self) -> int:
23
+ return hash(self.candidate)
24
+
25
+ def __eq__(self, other: Any) -> bool:
26
+ if not isinstance(other, ExplicitRequirement):
27
+ return False
28
+ return self.candidate == other.candidate
29
+
30
+ @property
31
+ def project_name(self) -> NormalizedName:
32
+ # No need to canonicalize - the candidate did this
33
+ return self.candidate.project_name
34
+
35
+ @property
36
+ def name(self) -> str:
37
+ # No need to canonicalize - the candidate did this
38
+ return self.candidate.name
39
+
40
+ def format_for_error(self) -> str:
41
+ return self.candidate.format_for_error()
42
+
43
+ def get_candidate_lookup(self) -> CandidateLookup:
44
+ return self.candidate, None
45
+
46
+ def is_satisfied_by(self, candidate: Candidate) -> bool:
47
+ return candidate == self.candidate
48
+
49
+
50
+ class SpecifierRequirement(Requirement):
51
+ def __init__(self, ireq: InstallRequirement) -> None:
52
+ assert ireq.link is None, "This is a link, not a specifier"
53
+ self._ireq = ireq
54
+ self._equal_cache: Optional[str] = None
55
+ self._hash: Optional[int] = None
56
+ self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
57
+
58
+ @property
59
+ def _equal(self) -> str:
60
+ if self._equal_cache is not None:
61
+ return self._equal_cache
62
+
63
+ self._equal_cache = str(self._ireq)
64
+ return self._equal_cache
65
+
66
+ def __str__(self) -> str:
67
+ return str(self._ireq.req)
68
+
69
+ def __repr__(self) -> str:
70
+ return f"{self.__class__.__name__}({str(self._ireq.req)!r})"
71
+
72
+ def __eq__(self, other: object) -> bool:
73
+ if not isinstance(other, SpecifierRequirement):
74
+ return NotImplemented
75
+ return self._equal == other._equal
76
+
77
+ def __hash__(self) -> int:
78
+ if self._hash is not None:
79
+ return self._hash
80
+
81
+ self._hash = hash(self._equal)
82
+ return self._hash
83
+
84
+ @property
85
+ def project_name(self) -> NormalizedName:
86
+ assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
87
+ return canonicalize_name(self._ireq.req.name)
88
+
89
+ @property
90
+ def name(self) -> str:
91
+ return format_name(self.project_name, self._extras)
92
+
93
+ def format_for_error(self) -> str:
94
+ # Convert comma-separated specifiers into "A, B, ..., F and G"
95
+ # This makes the specifier a bit more "human readable", without
96
+ # risking a change in meaning. (Hopefully! Not all edge cases have
97
+ # been checked)
98
+ parts = [s.strip() for s in str(self).split(",")]
99
+ if len(parts) == 0:
100
+ return ""
101
+ elif len(parts) == 1:
102
+ return parts[0]
103
+
104
+ return ", ".join(parts[:-1]) + " and " + parts[-1]
105
+
106
+ def get_candidate_lookup(self) -> CandidateLookup:
107
+ return None, self._ireq
108
+
109
+ def is_satisfied_by(self, candidate: Candidate) -> bool:
110
+ assert candidate.name == self.name, (
111
+ f"Internal issue: Candidate is not for this requirement "
112
+ f"{candidate.name} vs {self.name}"
113
+ )
114
+ # We can safely always allow prereleases here since PackageFinder
115
+ # already implements the prerelease logic, and would have filtered out
116
+ # prerelease candidates if the user does not expect them.
117
+ assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
118
+ spec = self._ireq.req.specifier
119
+ return spec.contains(candidate.version, prereleases=True)
120
+
121
+
122
+ class SpecifierWithoutExtrasRequirement(SpecifierRequirement):
123
+ """
124
+ Requirement backed by an install requirement on a base package.
125
+ Trims extras from its install requirement if there are any.
126
+ """
127
+
128
+ def __init__(self, ireq: InstallRequirement) -> None:
129
+ assert ireq.link is None, "This is a link, not a specifier"
130
+ self._ireq = install_req_drop_extras(ireq)
131
+ self._equal_cache: Optional[str] = None
132
+ self._hash: Optional[int] = None
133
+ self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
134
+
135
+ @property
136
+ def _equal(self) -> str:
137
+ if self._equal_cache is not None:
138
+ return self._equal_cache
139
+
140
+ self._equal_cache = str(self._ireq)
141
+ return self._equal_cache
142
+
143
+ def __eq__(self, other: object) -> bool:
144
+ if not isinstance(other, SpecifierWithoutExtrasRequirement):
145
+ return NotImplemented
146
+ return self._equal == other._equal
147
+
148
+ def __hash__(self) -> int:
149
+ if self._hash is not None:
150
+ return self._hash
151
+
152
+ self._hash = hash(self._equal)
153
+ return self._hash
154
+
155
+
156
+ class RequiresPythonRequirement(Requirement):
157
+ """A requirement representing Requires-Python metadata."""
158
+
159
+ def __init__(self, specifier: SpecifierSet, match: Candidate) -> None:
160
+ self.specifier = specifier
161
+ self._specifier_string = str(specifier) # for faster __eq__
162
+ self._hash: Optional[int] = None
163
+ self._candidate = match
164
+
165
+ def __str__(self) -> str:
166
+ return f"Python {self.specifier}"
167
+
168
+ def __repr__(self) -> str:
169
+ return f"{self.__class__.__name__}({str(self.specifier)!r})"
170
+
171
+ def __hash__(self) -> int:
172
+ if self._hash is not None:
173
+ return self._hash
174
+
175
+ self._hash = hash((self._specifier_string, self._candidate))
176
+ return self._hash
177
+
178
+ def __eq__(self, other: Any) -> bool:
179
+ if not isinstance(other, RequiresPythonRequirement):
180
+ return False
181
+ return (
182
+ self._specifier_string == other._specifier_string
183
+ and self._candidate == other._candidate
184
+ )
185
+
186
+ @property
187
+ def project_name(self) -> NormalizedName:
188
+ return self._candidate.project_name
189
+
190
+ @property
191
+ def name(self) -> str:
192
+ return self._candidate.name
193
+
194
+ def format_for_error(self) -> str:
195
+ return str(self)
196
+
197
+ def get_candidate_lookup(self) -> CandidateLookup:
198
+ if self.specifier.contains(self._candidate.version, prereleases=True):
199
+ return self._candidate, None
200
+ return None, None
201
+
202
+ def is_satisfied_by(self, candidate: Candidate) -> bool:
203
+ assert candidate.name == self._candidate.name, "Not Python candidate"
204
+ # We can safely always allow prereleases here since PackageFinder
205
+ # already implements the prerelease logic, and would have filtered out
206
+ # prerelease candidates if the user does not expect them.
207
+ return self.specifier.contains(candidate.version, prereleases=True)
208
+
209
+
210
+ class UnsatisfiableRequirement(Requirement):
211
+ """A requirement that cannot be satisfied."""
212
+
213
+ def __init__(self, name: NormalizedName) -> None:
214
+ self._name = name
215
+
216
+ def __str__(self) -> str:
217
+ return f"{self._name} (unavailable)"
218
+
219
+ def __repr__(self) -> str:
220
+ return f"{self.__class__.__name__}({str(self._name)!r})"
221
+
222
+ def __eq__(self, other: object) -> bool:
223
+ if not isinstance(other, UnsatisfiableRequirement):
224
+ return NotImplemented
225
+ return self._name == other._name
226
+
227
+ def __hash__(self) -> int:
228
+ return hash(self._name)
229
+
230
+ @property
231
+ def project_name(self) -> NormalizedName:
232
+ return self._name
233
+
234
+ @property
235
+ def name(self) -> str:
236
+ return self._name
237
+
238
+ def format_for_error(self) -> str:
239
+ return str(self)
240
+
241
+ def get_candidate_lookup(self) -> CandidateLookup:
242
+ return None, None
243
+
244
+ def is_satisfied_by(self, candidate: Candidate) -> bool:
245
+ return False
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/resolver.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+ import functools
3
+ import logging
4
+ import os
5
+ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, cast
6
+
7
+ from pip._vendor.packaging.utils import canonicalize_name
8
+ from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible, ResolutionTooDeep
9
+ from pip._vendor.resolvelib import Resolver as RLResolver
10
+ from pip._vendor.resolvelib.structs import DirectedGraph
11
+
12
+ from pip._internal.cache import WheelCache
13
+ from pip._internal.exceptions import ResolutionTooDeepError
14
+ from pip._internal.index.package_finder import PackageFinder
15
+ from pip._internal.operations.prepare import RequirementPreparer
16
+ from pip._internal.req.constructors import install_req_extend_extras
17
+ from pip._internal.req.req_install import InstallRequirement
18
+ from pip._internal.req.req_set import RequirementSet
19
+ from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
20
+ from pip._internal.resolution.resolvelib.provider import PipProvider
21
+ from pip._internal.resolution.resolvelib.reporter import (
22
+ PipDebuggingReporter,
23
+ PipReporter,
24
+ )
25
+ from pip._internal.utils.packaging import get_requirement
26
+
27
+ from .base import Candidate, Requirement
28
+ from .factory import Factory
29
+
30
+ if TYPE_CHECKING:
31
+ from pip._vendor.resolvelib.resolvers import Result as RLResult
32
+
33
+ Result = RLResult[Requirement, Candidate, str]
34
+
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class Resolver(BaseResolver):
40
+ _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
41
+
42
+ def __init__(
43
+ self,
44
+ preparer: RequirementPreparer,
45
+ finder: PackageFinder,
46
+ wheel_cache: Optional[WheelCache],
47
+ make_install_req: InstallRequirementProvider,
48
+ use_user_site: bool,
49
+ ignore_dependencies: bool,
50
+ ignore_installed: bool,
51
+ ignore_requires_python: bool,
52
+ force_reinstall: bool,
53
+ upgrade_strategy: str,
54
+ py_version_info: Optional[Tuple[int, ...]] = None,
55
+ ):
56
+ super().__init__()
57
+ assert upgrade_strategy in self._allowed_strategies
58
+
59
+ self.factory = Factory(
60
+ finder=finder,
61
+ preparer=preparer,
62
+ make_install_req=make_install_req,
63
+ wheel_cache=wheel_cache,
64
+ use_user_site=use_user_site,
65
+ force_reinstall=force_reinstall,
66
+ ignore_installed=ignore_installed,
67
+ ignore_requires_python=ignore_requires_python,
68
+ py_version_info=py_version_info,
69
+ )
70
+ self.ignore_dependencies = ignore_dependencies
71
+ self.upgrade_strategy = upgrade_strategy
72
+ self._result: Optional[Result] = None
73
+
74
+ def resolve(
75
+ self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
76
+ ) -> RequirementSet:
77
+ collected = self.factory.collect_root_requirements(root_reqs)
78
+ provider = PipProvider(
79
+ factory=self.factory,
80
+ constraints=collected.constraints,
81
+ ignore_dependencies=self.ignore_dependencies,
82
+ upgrade_strategy=self.upgrade_strategy,
83
+ user_requested=collected.user_requested,
84
+ )
85
+ if "PIP_RESOLVER_DEBUG" in os.environ:
86
+ reporter: BaseReporter[Requirement, Candidate, str] = PipDebuggingReporter()
87
+ else:
88
+ reporter = PipReporter()
89
+ resolver: RLResolver[Requirement, Candidate, str] = RLResolver(
90
+ provider,
91
+ reporter,
92
+ )
93
+
94
+ try:
95
+ limit_how_complex_resolution_can_be = 200000
96
+ result = self._result = resolver.resolve(
97
+ collected.requirements, max_rounds=limit_how_complex_resolution_can_be
98
+ )
99
+
100
+ except ResolutionImpossible as e:
101
+ error = self.factory.get_installation_error(
102
+ cast("ResolutionImpossible[Requirement, Candidate]", e),
103
+ collected.constraints,
104
+ )
105
+ raise error from e
106
+ except ResolutionTooDeep:
107
+ raise ResolutionTooDeepError from None
108
+
109
+ req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
110
+ # process candidates with extras last to ensure their base equivalent is
111
+ # already in the req_set if appropriate.
112
+ # Python's sort is stable so using a binary key function keeps relative order
113
+ # within both subsets.
114
+ for candidate in sorted(
115
+ result.mapping.values(), key=lambda c: c.name != c.project_name
116
+ ):
117
+ ireq = candidate.get_install_requirement()
118
+ if ireq is None:
119
+ if candidate.name != candidate.project_name:
120
+ # extend existing req's extras
121
+ with contextlib.suppress(KeyError):
122
+ req = req_set.get_requirement(candidate.project_name)
123
+ req_set.add_named_requirement(
124
+ install_req_extend_extras(
125
+ req, get_requirement(candidate.name).extras
126
+ )
127
+ )
128
+ continue
129
+
130
+ # Check if there is already an installation under the same name,
131
+ # and set a flag for later stages to uninstall it, if needed.
132
+ installed_dist = self.factory.get_dist_to_uninstall(candidate)
133
+ if installed_dist is None:
134
+ # There is no existing installation -- nothing to uninstall.
135
+ ireq.should_reinstall = False
136
+ elif self.factory.force_reinstall:
137
+ # The --force-reinstall flag is set -- reinstall.
138
+ ireq.should_reinstall = True
139
+ elif installed_dist.version != candidate.version:
140
+ # The installation is different in version -- reinstall.
141
+ ireq.should_reinstall = True
142
+ elif candidate.is_editable or installed_dist.editable:
143
+ # The incoming distribution is editable, or different in
144
+ # editable-ness to installation -- reinstall.
145
+ ireq.should_reinstall = True
146
+ elif candidate.source_link and candidate.source_link.is_file:
147
+ # The incoming distribution is under file://
148
+ if candidate.source_link.is_wheel:
149
+ # is a local wheel -- do nothing.
150
+ logger.info(
151
+ "%s is already installed with the same version as the "
152
+ "provided wheel. Use --force-reinstall to force an "
153
+ "installation of the wheel.",
154
+ ireq.name,
155
+ )
156
+ continue
157
+
158
+ # is a local sdist or path -- reinstall
159
+ ireq.should_reinstall = True
160
+ else:
161
+ continue
162
+
163
+ link = candidate.source_link
164
+ if link and link.is_yanked:
165
+ # The reason can contain non-ASCII characters, Unicode
166
+ # is required for Python 2.
167
+ msg = (
168
+ "The candidate selected for download or install is a "
169
+ "yanked version: {name!r} candidate (version {version} "
170
+ "at {link})\nReason for being yanked: {reason}"
171
+ ).format(
172
+ name=candidate.name,
173
+ version=candidate.version,
174
+ link=link,
175
+ reason=link.yanked_reason or "<none given>",
176
+ )
177
+ logger.warning(msg)
178
+
179
+ req_set.add_named_requirement(ireq)
180
+
181
+ reqs = req_set.all_requirements
182
+ self.factory.preparer.prepare_linked_requirements_more(reqs)
183
+ for req in reqs:
184
+ req.prepared = True
185
+ req.needs_more_preparation = False
186
+ return req_set
187
+
188
+ def get_installation_order(
189
+ self, req_set: RequirementSet
190
+ ) -> List[InstallRequirement]:
191
+ """Get order for installation of requirements in RequirementSet.
192
+
193
+ The returned list contains a requirement before another that depends on
194
+ it. This helps ensure that the environment is kept consistent as they
195
+ get installed one-by-one.
196
+
197
+ The current implementation creates a topological ordering of the
198
+ dependency graph, giving more weight to packages with less
199
+ or no dependencies, while breaking any cycles in the graph at
200
+ arbitrary points. We make no guarantees about where the cycle
201
+ would be broken, other than it *would* be broken.
202
+ """
203
+ assert self._result is not None, "must call resolve() first"
204
+
205
+ if not req_set.requirements:
206
+ # Nothing is left to install, so we do not need an order.
207
+ return []
208
+
209
+ graph = self._result.graph
210
+ weights = get_topological_weights(graph, set(req_set.requirements.keys()))
211
+
212
+ sorted_items = sorted(
213
+ req_set.requirements.items(),
214
+ key=functools.partial(_req_set_item_sorter, weights=weights),
215
+ reverse=True,
216
+ )
217
+ return [ireq for _, ireq in sorted_items]
218
+
219
+
220
+ def get_topological_weights(
221
+ graph: "DirectedGraph[Optional[str]]", requirement_keys: Set[str]
222
+ ) -> Dict[Optional[str], int]:
223
+ """Assign weights to each node based on how "deep" they are.
224
+
225
+ This implementation may change at any point in the future without prior
226
+ notice.
227
+
228
+ We first simplify the dependency graph by pruning any leaves and giving them
229
+ the highest weight: a package without any dependencies should be installed
230
+ first. This is done again and again in the same way, giving ever less weight
231
+ to the newly found leaves. The loop stops when no leaves are left: all
232
+ remaining packages have at least one dependency left in the graph.
233
+
234
+ Then we continue with the remaining graph, by taking the length for the
235
+ longest path to any node from root, ignoring any paths that contain a single
236
+ node twice (i.e. cycles). This is done through a depth-first search through
237
+ the graph, while keeping track of the path to the node.
238
+
239
+ Cycles in the graph result would result in node being revisited while also
240
+ being on its own path. In this case, take no action. This helps ensure we
241
+ don't get stuck in a cycle.
242
+
243
+ When assigning weight, the longer path (i.e. larger length) is preferred.
244
+
245
+ We are only interested in the weights of packages that are in the
246
+ requirement_keys.
247
+ """
248
+ path: Set[Optional[str]] = set()
249
+ weights: Dict[Optional[str], int] = {}
250
+
251
+ def visit(node: Optional[str]) -> None:
252
+ if node in path:
253
+ # We hit a cycle, so we'll break it here.
254
+ return
255
+
256
+ # Time to visit the children!
257
+ path.add(node)
258
+ for child in graph.iter_children(node):
259
+ visit(child)
260
+ path.remove(node)
261
+
262
+ if node not in requirement_keys:
263
+ return
264
+
265
+ last_known_parent_count = weights.get(node, 0)
266
+ weights[node] = max(last_known_parent_count, len(path))
267
+
268
+ # Simplify the graph, pruning leaves that have no dependencies.
269
+ # This is needed for large graphs (say over 200 packages) because the
270
+ # `visit` function is exponentially slower then, taking minutes.
271
+ # See https://github.com/pypa/pip/issues/10557
272
+ # We will loop until we explicitly break the loop.
273
+ while True:
274
+ leaves = set()
275
+ for key in graph:
276
+ if key is None:
277
+ continue
278
+ for _child in graph.iter_children(key):
279
+ # This means we have at least one child
280
+ break
281
+ else:
282
+ # No child.
283
+ leaves.add(key)
284
+ if not leaves:
285
+ # We are done simplifying.
286
+ break
287
+ # Calculate the weight for the leaves.
288
+ weight = len(graph) - 1
289
+ for leaf in leaves:
290
+ if leaf not in requirement_keys:
291
+ continue
292
+ weights[leaf] = weight
293
+ # Remove the leaves from the graph, making it simpler.
294
+ for leaf in leaves:
295
+ graph.remove(leaf)
296
+
297
+ # Visit the remaining graph.
298
+ # `None` is guaranteed to be the root node by resolvelib.
299
+ visit(None)
300
+
301
+ # Sanity check: all requirement keys should be in the weights,
302
+ # and no other keys should be in the weights.
303
+ difference = set(weights.keys()).difference(requirement_keys)
304
+ assert not difference, difference
305
+
306
+ return weights
307
+
308
+
309
+ def _req_set_item_sorter(
310
+ item: Tuple[str, InstallRequirement],
311
+ weights: Dict[Optional[str], int],
312
+ ) -> Tuple[int, str]:
313
+ """Key function used to sort install requirements for installation.
314
+
315
+ Based on the "weight" mapping calculated in ``get_installation_order()``.
316
+ The canonical package name is returned as the second member as a tie-
317
+ breaker to ensure the result is predictable, which is useful in tests.
318
+ """
319
+ name = canonicalize_name(item[0])
320
+ return weights[name], name
.env/lib/python3.13/site-packages/pip/_internal/self_outdated_check.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import functools
3
+ import hashlib
4
+ import json
5
+ import logging
6
+ import optparse
7
+ import os.path
8
+ import sys
9
+ from dataclasses import dataclass
10
+ from typing import Any, Callable, Dict, Optional
11
+
12
+ from pip._vendor.packaging.version import Version
13
+ from pip._vendor.packaging.version import parse as parse_version
14
+ from pip._vendor.rich.console import Group
15
+ from pip._vendor.rich.markup import escape
16
+ from pip._vendor.rich.text import Text
17
+
18
+ from pip._internal.index.collector import LinkCollector
19
+ from pip._internal.index.package_finder import PackageFinder
20
+ from pip._internal.metadata import get_default_environment
21
+ from pip._internal.models.selection_prefs import SelectionPreferences
22
+ from pip._internal.network.session import PipSession
23
+ from pip._internal.utils.compat import WINDOWS
24
+ from pip._internal.utils.entrypoints import (
25
+ get_best_invocation_for_this_pip,
26
+ get_best_invocation_for_this_python,
27
+ )
28
+ from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace
29
+ from pip._internal.utils.misc import (
30
+ ExternallyManagedEnvironment,
31
+ check_externally_managed,
32
+ ensure_dir,
33
+ )
34
+
35
+ _WEEK = datetime.timedelta(days=7)
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def _get_statefile_name(key: str) -> str:
41
+ key_bytes = key.encode()
42
+ name = hashlib.sha224(key_bytes).hexdigest()
43
+ return name
44
+
45
+
46
+ def _convert_date(isodate: str) -> datetime.datetime:
47
+ """Convert an ISO format string to a date.
48
+
49
+ Handles the format 2020-01-22T14:24:01Z (trailing Z)
50
+ which is not supported by older versions of fromisoformat.
51
+ """
52
+ return datetime.datetime.fromisoformat(isodate.replace("Z", "+00:00"))
53
+
54
+
55
+ class SelfCheckState:
56
+ def __init__(self, cache_dir: str) -> None:
57
+ self._state: Dict[str, Any] = {}
58
+ self._statefile_path = None
59
+
60
+ # Try to load the existing state
61
+ if cache_dir:
62
+ self._statefile_path = os.path.join(
63
+ cache_dir, "selfcheck", _get_statefile_name(self.key)
64
+ )
65
+ try:
66
+ with open(self._statefile_path, encoding="utf-8") as statefile:
67
+ self._state = json.load(statefile)
68
+ except (OSError, ValueError, KeyError):
69
+ # Explicitly suppressing exceptions, since we don't want to
70
+ # error out if the cache file is invalid.
71
+ pass
72
+
73
+ @property
74
+ def key(self) -> str:
75
+ return sys.prefix
76
+
77
+ def get(self, current_time: datetime.datetime) -> Optional[str]:
78
+ """Check if we have a not-outdated version loaded already."""
79
+ if not self._state:
80
+ return None
81
+
82
+ if "last_check" not in self._state:
83
+ return None
84
+
85
+ if "pypi_version" not in self._state:
86
+ return None
87
+
88
+ # Determine if we need to refresh the state
89
+ last_check = _convert_date(self._state["last_check"])
90
+ time_since_last_check = current_time - last_check
91
+ if time_since_last_check > _WEEK:
92
+ return None
93
+
94
+ return self._state["pypi_version"]
95
+
96
+ def set(self, pypi_version: str, current_time: datetime.datetime) -> None:
97
+ # If we do not have a path to cache in, don't bother saving.
98
+ if not self._statefile_path:
99
+ return
100
+
101
+ # Check to make sure that we own the directory
102
+ if not check_path_owner(os.path.dirname(self._statefile_path)):
103
+ return
104
+
105
+ # Now that we've ensured the directory is owned by this user, we'll go
106
+ # ahead and make sure that all our directories are created.
107
+ ensure_dir(os.path.dirname(self._statefile_path))
108
+
109
+ state = {
110
+ # Include the key so it's easy to tell which pip wrote the
111
+ # file.
112
+ "key": self.key,
113
+ "last_check": current_time.isoformat(),
114
+ "pypi_version": pypi_version,
115
+ }
116
+
117
+ text = json.dumps(state, sort_keys=True, separators=(",", ":"))
118
+
119
+ with adjacent_tmp_file(self._statefile_path) as f:
120
+ f.write(text.encode())
121
+
122
+ try:
123
+ # Since we have a prefix-specific state file, we can just
124
+ # overwrite whatever is there, no need to check.
125
+ replace(f.name, self._statefile_path)
126
+ except OSError:
127
+ # Best effort.
128
+ pass
129
+
130
+
131
+ @dataclass
132
+ class UpgradePrompt:
133
+ old: str
134
+ new: str
135
+
136
+ def __rich__(self) -> Group:
137
+ if WINDOWS:
138
+ pip_cmd = f"{get_best_invocation_for_this_python()} -m pip"
139
+ else:
140
+ pip_cmd = get_best_invocation_for_this_pip()
141
+
142
+ notice = "[bold][[reset][blue]notice[reset][bold]][reset]"
143
+ return Group(
144
+ Text(),
145
+ Text.from_markup(
146
+ f"{notice} A new release of pip is available: "
147
+ f"[red]{self.old}[reset] -> [green]{self.new}[reset]"
148
+ ),
149
+ Text.from_markup(
150
+ f"{notice} To update, run: "
151
+ f"[green]{escape(pip_cmd)} install --upgrade pip"
152
+ ),
153
+ )
154
+
155
+
156
+ def was_installed_by_pip(pkg: str) -> bool:
157
+ """Checks whether pkg was installed by pip
158
+
159
+ This is used not to display the upgrade message when pip is in fact
160
+ installed by system package manager, such as dnf on Fedora.
161
+ """
162
+ dist = get_default_environment().get_distribution(pkg)
163
+ return dist is not None and "pip" == dist.installer
164
+
165
+
166
+ def _get_current_remote_pip_version(
167
+ session: PipSession, options: optparse.Values
168
+ ) -> Optional[str]:
169
+ # Lets use PackageFinder to see what the latest pip version is
170
+ link_collector = LinkCollector.create(
171
+ session,
172
+ options=options,
173
+ suppress_no_index=True,
174
+ )
175
+
176
+ # Pass allow_yanked=False so we don't suggest upgrading to a
177
+ # yanked version.
178
+ selection_prefs = SelectionPreferences(
179
+ allow_yanked=False,
180
+ allow_all_prereleases=False, # Explicitly set to False
181
+ )
182
+
183
+ finder = PackageFinder.create(
184
+ link_collector=link_collector,
185
+ selection_prefs=selection_prefs,
186
+ )
187
+ best_candidate = finder.find_best_candidate("pip").best_candidate
188
+ if best_candidate is None:
189
+ return None
190
+
191
+ return str(best_candidate.version)
192
+
193
+
194
+ def _self_version_check_logic(
195
+ *,
196
+ state: SelfCheckState,
197
+ current_time: datetime.datetime,
198
+ local_version: Version,
199
+ get_remote_version: Callable[[], Optional[str]],
200
+ ) -> Optional[UpgradePrompt]:
201
+ remote_version_str = state.get(current_time)
202
+ if remote_version_str is None:
203
+ remote_version_str = get_remote_version()
204
+ if remote_version_str is None:
205
+ logger.debug("No remote pip version found")
206
+ return None
207
+ state.set(remote_version_str, current_time)
208
+
209
+ remote_version = parse_version(remote_version_str)
210
+ logger.debug("Remote version of pip: %s", remote_version)
211
+ logger.debug("Local version of pip: %s", local_version)
212
+
213
+ pip_installed_by_pip = was_installed_by_pip("pip")
214
+ logger.debug("Was pip installed by pip? %s", pip_installed_by_pip)
215
+ if not pip_installed_by_pip:
216
+ return None # Only suggest upgrade if pip is installed by pip.
217
+
218
+ local_version_is_older = (
219
+ local_version < remote_version
220
+ and local_version.base_version != remote_version.base_version
221
+ )
222
+ if local_version_is_older:
223
+ return UpgradePrompt(old=str(local_version), new=remote_version_str)
224
+
225
+ return None
226
+
227
+
228
+ def pip_self_version_check(session: PipSession, options: optparse.Values) -> None:
229
+ """Check for an update for pip.
230
+
231
+ Limit the frequency of checks to once per week. State is stored either in
232
+ the active virtualenv or in the user's USER_CACHE_DIR keyed off the prefix
233
+ of the pip script path.
234
+ """
235
+ installed_dist = get_default_environment().get_distribution("pip")
236
+ if not installed_dist:
237
+ return
238
+ try:
239
+ check_externally_managed()
240
+ except ExternallyManagedEnvironment:
241
+ return
242
+
243
+ upgrade_prompt = _self_version_check_logic(
244
+ state=SelfCheckState(cache_dir=options.cache_dir),
245
+ current_time=datetime.datetime.now(datetime.timezone.utc),
246
+ local_version=installed_dist.version,
247
+ get_remote_version=functools.partial(
248
+ _get_current_remote_pip_version, session, options
249
+ ),
250
+ )
251
+ if upgrade_prompt is not None:
252
+ logger.warning("%s", upgrade_prompt, extra={"rich": True})
.env/lib/python3.13/site-packages/pip/_internal/utils/__init__.py ADDED
File without changes
.env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (196 Bytes). View file
 
.env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/_jaraco_text.cpython-313.pyc ADDED
Binary file (4.44 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/_log.cpython-313.pyc ADDED
Binary file (1.92 kB). View file
 
.env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-313.pyc ADDED
Binary file (2.5 kB). View file