ZhengyangZhang commited on
Commit
ea1f43e
·
verified ·
1 Parent(s): 1e4a312

Add files using upload-large-folder tool

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