Brettapps commited on
Commit
d153a82
·
verified ·
1 Parent(s): c3a790d

Upload folder using huggingface_hub (part 17)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .venv/lib/python3.13/site-packages/pip/_internal/utils/logging.py +361 -0
  2. .venv/lib/python3.13/site-packages/pip/_internal/utils/misc.py +773 -0
  3. .venv/lib/python3.13/site-packages/pip/_internal/utils/packaging.py +43 -0
  4. .venv/lib/python3.13/site-packages/pip/_internal/utils/retry.py +42 -0
  5. .venv/lib/python3.13/site-packages/pip/_internal/utils/setuptools_build.py +147 -0
  6. .venv/lib/python3.13/site-packages/pip/_internal/utils/subprocess.py +245 -0
  7. .venv/lib/python3.13/site-packages/pip/_internal/utils/temp_dir.py +296 -0
  8. .venv/lib/python3.13/site-packages/pip/_internal/utils/unpacking.py +335 -0
  9. .venv/lib/python3.13/site-packages/pip/_internal/utils/urls.py +55 -0
  10. .venv/lib/python3.13/site-packages/pip/_internal/utils/virtualenv.py +104 -0
  11. .venv/lib/python3.13/site-packages/pip/_internal/utils/wheel.py +133 -0
  12. .venv/lib/python3.13/site-packages/pip/_internal/vcs/__init__.py +15 -0
  13. .venv/lib/python3.13/site-packages/pip/_internal/vcs/bazaar.py +112 -0
  14. .venv/lib/python3.13/site-packages/pip/_internal/vcs/git.py +536 -0
  15. .venv/lib/python3.13/site-packages/pip/_internal/vcs/mercurial.py +163 -0
  16. .venv/lib/python3.13/site-packages/pip/_internal/vcs/subversion.py +324 -0
  17. .venv/lib/python3.13/site-packages/pip/_internal/vcs/versioncontrol.py +688 -0
  18. .venv/lib/python3.13/site-packages/pip/_internal/wheel_builder.py +332 -0
  19. .venv/lib/python3.13/site-packages/pip/_vendor/__init__.py +117 -0
  20. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/__init__.py +29 -0
  21. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/_cmd.py +70 -0
  22. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/adapter.py +168 -0
  23. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/cache.py +75 -0
  24. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/__init__.py +8 -0
  25. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py +145 -0
  26. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py +48 -0
  27. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/controller.py +511 -0
  28. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/filewrapper.py +119 -0
  29. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/heuristics.py +157 -0
  30. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/py.typed +0 -0
  31. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/serialize.py +146 -0
  32. .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/wrapper.py +43 -0
  33. .venv/lib/python3.13/site-packages/pip/_vendor/certifi/__init__.py +4 -0
  34. .venv/lib/python3.13/site-packages/pip/_vendor/certifi/__main__.py +12 -0
  35. .venv/lib/python3.13/site-packages/pip/_vendor/certifi/cacert.pem +0 -0
  36. .venv/lib/python3.13/site-packages/pip/_vendor/certifi/core.py +125 -0
  37. .venv/lib/python3.13/site-packages/pip/_vendor/certifi/py.typed +0 -0
  38. .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__init__.py +13 -0
  39. .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__main__.py +65 -0
  40. .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_implementation.py +209 -0
  41. .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_lint_dependency_groups.py +59 -0
  42. .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_pip_wrapper.py +62 -0
  43. .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_toml_compat.py +9 -0
  44. .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/py.typed +0 -0
  45. .venv/lib/python3.13/site-packages/pip/_vendor/distlib/__init__.py +33 -0
  46. .venv/lib/python3.13/site-packages/pip/_vendor/distlib/compat.py +1137 -0
  47. .venv/lib/python3.13/site-packages/pip/_vendor/distlib/database.py +1329 -0
  48. .venv/lib/python3.13/site-packages/pip/_vendor/distlib/index.py +508 -0
  49. .venv/lib/python3.13/site-packages/pip/_vendor/distlib/locators.py +1295 -0
  50. .venv/lib/python3.13/site-packages/pip/_vendor/distlib/manifest.py +384 -0
.venv/lib/python3.13/site-packages/pip/_internal/utils/logging.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+ import errno
3
+ import logging
4
+ import logging.handlers
5
+ import os
6
+ import sys
7
+ import threading
8
+ from dataclasses import dataclass
9
+ from io import TextIOWrapper
10
+ from logging import Filter
11
+ from typing import Any, ClassVar, Generator, List, Optional, Type
12
+
13
+ from pip._vendor.rich.console import (
14
+ Console,
15
+ ConsoleOptions,
16
+ ConsoleRenderable,
17
+ RenderableType,
18
+ RenderResult,
19
+ RichCast,
20
+ )
21
+ from pip._vendor.rich.highlighter import NullHighlighter
22
+ from pip._vendor.rich.logging import RichHandler
23
+ from pip._vendor.rich.segment import Segment
24
+ from pip._vendor.rich.style import Style
25
+
26
+ from pip._internal.utils._log import VERBOSE, getLogger
27
+ from pip._internal.utils.compat import WINDOWS
28
+ from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX
29
+ from pip._internal.utils.misc import ensure_dir
30
+
31
+ _log_state = threading.local()
32
+ _stdout_console = None
33
+ _stderr_console = None
34
+ subprocess_logger = getLogger("pip.subprocessor")
35
+
36
+
37
+ class BrokenStdoutLoggingError(Exception):
38
+ """
39
+ Raised if BrokenPipeError occurs for the stdout stream while logging.
40
+ """
41
+
42
+
43
+ def _is_broken_pipe_error(exc_class: Type[BaseException], exc: BaseException) -> bool:
44
+ if exc_class is BrokenPipeError:
45
+ return True
46
+
47
+ # On Windows, a broken pipe can show up as EINVAL rather than EPIPE:
48
+ # https://bugs.python.org/issue19612
49
+ # https://bugs.python.org/issue30418
50
+ if not WINDOWS:
51
+ return False
52
+
53
+ return isinstance(exc, OSError) and exc.errno in (errno.EINVAL, errno.EPIPE)
54
+
55
+
56
+ @contextlib.contextmanager
57
+ def indent_log(num: int = 2) -> Generator[None, None, None]:
58
+ """
59
+ A context manager which will cause the log output to be indented for any
60
+ log messages emitted inside it.
61
+ """
62
+ # For thread-safety
63
+ _log_state.indentation = get_indentation()
64
+ _log_state.indentation += num
65
+ try:
66
+ yield
67
+ finally:
68
+ _log_state.indentation -= num
69
+
70
+
71
+ def get_indentation() -> int:
72
+ return getattr(_log_state, "indentation", 0)
73
+
74
+
75
+ class IndentingFormatter(logging.Formatter):
76
+ default_time_format = "%Y-%m-%dT%H:%M:%S"
77
+
78
+ def __init__(
79
+ self,
80
+ *args: Any,
81
+ add_timestamp: bool = False,
82
+ **kwargs: Any,
83
+ ) -> None:
84
+ """
85
+ A logging.Formatter that obeys the indent_log() context manager.
86
+
87
+ :param add_timestamp: A bool indicating output lines should be prefixed
88
+ with their record's timestamp.
89
+ """
90
+ self.add_timestamp = add_timestamp
91
+ super().__init__(*args, **kwargs)
92
+
93
+ def get_message_start(self, formatted: str, levelno: int) -> str:
94
+ """
95
+ Return the start of the formatted log message (not counting the
96
+ prefix to add to each line).
97
+ """
98
+ if levelno < logging.WARNING:
99
+ return ""
100
+ if formatted.startswith(DEPRECATION_MSG_PREFIX):
101
+ # Then the message already has a prefix. We don't want it to
102
+ # look like "WARNING: DEPRECATION: ...."
103
+ return ""
104
+ if levelno < logging.ERROR:
105
+ return "WARNING: "
106
+
107
+ return "ERROR: "
108
+
109
+ def format(self, record: logging.LogRecord) -> str:
110
+ """
111
+ Calls the standard formatter, but will indent all of the log message
112
+ lines by our current indentation level.
113
+ """
114
+ formatted = super().format(record)
115
+ message_start = self.get_message_start(formatted, record.levelno)
116
+ formatted = message_start + formatted
117
+
118
+ prefix = ""
119
+ if self.add_timestamp:
120
+ prefix = f"{self.formatTime(record)} "
121
+ prefix += " " * get_indentation()
122
+ formatted = "".join([prefix + line for line in formatted.splitlines(True)])
123
+ return formatted
124
+
125
+
126
+ @dataclass
127
+ class IndentedRenderable:
128
+ renderable: RenderableType
129
+ indent: int
130
+
131
+ def __rich_console__(
132
+ self, console: Console, options: ConsoleOptions
133
+ ) -> RenderResult:
134
+ segments = console.render(self.renderable, options)
135
+ lines = Segment.split_lines(segments)
136
+ for line in lines:
137
+ yield Segment(" " * self.indent)
138
+ yield from line
139
+ yield Segment("\n")
140
+
141
+
142
+ class PipConsole(Console):
143
+ def on_broken_pipe(self) -> None:
144
+ # Reraise the original exception, rich 13.8.0+ exits by default
145
+ # instead, preventing our handler from firing.
146
+ raise BrokenPipeError() from None
147
+
148
+
149
+ def get_console(*, stderr: bool = False) -> Console:
150
+ if stderr:
151
+ assert _stderr_console is not None, "stderr rich console is missing!"
152
+ return _stderr_console
153
+ else:
154
+ assert _stdout_console is not None, "stdout rich console is missing!"
155
+ return _stdout_console
156
+
157
+
158
+ class RichPipStreamHandler(RichHandler):
159
+ KEYWORDS: ClassVar[Optional[List[str]]] = []
160
+
161
+ def __init__(self, console: Console) -> None:
162
+ super().__init__(
163
+ console=console,
164
+ show_time=False,
165
+ show_level=False,
166
+ show_path=False,
167
+ highlighter=NullHighlighter(),
168
+ )
169
+
170
+ # Our custom override on Rich's logger, to make things work as we need them to.
171
+ def emit(self, record: logging.LogRecord) -> None:
172
+ style: Optional[Style] = None
173
+
174
+ # If we are given a diagnostic error to present, present it with indentation.
175
+ if getattr(record, "rich", False):
176
+ assert isinstance(record.args, tuple)
177
+ (rich_renderable,) = record.args
178
+ assert isinstance(
179
+ rich_renderable, (ConsoleRenderable, RichCast, str)
180
+ ), f"{rich_renderable} is not rich-console-renderable"
181
+
182
+ renderable: RenderableType = IndentedRenderable(
183
+ rich_renderable, indent=get_indentation()
184
+ )
185
+ else:
186
+ message = self.format(record)
187
+ renderable = self.render_message(record, message)
188
+ if record.levelno is not None:
189
+ if record.levelno >= logging.ERROR:
190
+ style = Style(color="red")
191
+ elif record.levelno >= logging.WARNING:
192
+ style = Style(color="yellow")
193
+
194
+ try:
195
+ self.console.print(renderable, overflow="ignore", crop=False, style=style)
196
+ except Exception:
197
+ self.handleError(record)
198
+
199
+ def handleError(self, record: logging.LogRecord) -> None:
200
+ """Called when logging is unable to log some output."""
201
+
202
+ exc_class, exc = sys.exc_info()[:2]
203
+ # If a broken pipe occurred while calling write() or flush() on the
204
+ # stdout stream in logging's Handler.emit(), then raise our special
205
+ # exception so we can handle it in main() instead of logging the
206
+ # broken pipe error and continuing.
207
+ if (
208
+ exc_class
209
+ and exc
210
+ and self.console.file is sys.stdout
211
+ and _is_broken_pipe_error(exc_class, exc)
212
+ ):
213
+ raise BrokenStdoutLoggingError()
214
+
215
+ return super().handleError(record)
216
+
217
+
218
+ class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler):
219
+ def _open(self) -> TextIOWrapper:
220
+ ensure_dir(os.path.dirname(self.baseFilename))
221
+ return super()._open()
222
+
223
+
224
+ class MaxLevelFilter(Filter):
225
+ def __init__(self, level: int) -> None:
226
+ self.level = level
227
+
228
+ def filter(self, record: logging.LogRecord) -> bool:
229
+ return record.levelno < self.level
230
+
231
+
232
+ class ExcludeLoggerFilter(Filter):
233
+ """
234
+ A logging Filter that excludes records from a logger (or its children).
235
+ """
236
+
237
+ def filter(self, record: logging.LogRecord) -> bool:
238
+ # The base Filter class allows only records from a logger (or its
239
+ # children).
240
+ return not super().filter(record)
241
+
242
+
243
+ def setup_logging(verbosity: int, no_color: bool, user_log_file: Optional[str]) -> int:
244
+ """Configures and sets up all of the logging
245
+
246
+ Returns the requested logging level, as its integer value.
247
+ """
248
+
249
+ # Determine the level to be logging at.
250
+ if verbosity >= 2:
251
+ level_number = logging.DEBUG
252
+ elif verbosity == 1:
253
+ level_number = VERBOSE
254
+ elif verbosity == -1:
255
+ level_number = logging.WARNING
256
+ elif verbosity == -2:
257
+ level_number = logging.ERROR
258
+ elif verbosity <= -3:
259
+ level_number = logging.CRITICAL
260
+ else:
261
+ level_number = logging.INFO
262
+
263
+ level = logging.getLevelName(level_number)
264
+
265
+ # The "root" logger should match the "console" level *unless* we also need
266
+ # to log to a user log file.
267
+ include_user_log = user_log_file is not None
268
+ if include_user_log:
269
+ additional_log_file = user_log_file
270
+ root_level = "DEBUG"
271
+ else:
272
+ additional_log_file = "/dev/null"
273
+ root_level = level
274
+
275
+ # Disable any logging besides WARNING unless we have DEBUG level logging
276
+ # enabled for vendored libraries.
277
+ vendored_log_level = "WARNING" if level in ["INFO", "ERROR"] else "DEBUG"
278
+
279
+ # Shorthands for clarity
280
+ handler_classes = {
281
+ "stream": "pip._internal.utils.logging.RichPipStreamHandler",
282
+ "file": "pip._internal.utils.logging.BetterRotatingFileHandler",
283
+ }
284
+ handlers = ["console", "console_errors", "console_subprocess"] + (
285
+ ["user_log"] if include_user_log else []
286
+ )
287
+ global _stdout_console, stderr_console
288
+ _stdout_console = PipConsole(file=sys.stdout, no_color=no_color, soft_wrap=True)
289
+ _stderr_console = PipConsole(file=sys.stderr, no_color=no_color, soft_wrap=True)
290
+
291
+ logging.config.dictConfig(
292
+ {
293
+ "version": 1,
294
+ "disable_existing_loggers": False,
295
+ "filters": {
296
+ "exclude_warnings": {
297
+ "()": "pip._internal.utils.logging.MaxLevelFilter",
298
+ "level": logging.WARNING,
299
+ },
300
+ "restrict_to_subprocess": {
301
+ "()": "logging.Filter",
302
+ "name": subprocess_logger.name,
303
+ },
304
+ "exclude_subprocess": {
305
+ "()": "pip._internal.utils.logging.ExcludeLoggerFilter",
306
+ "name": subprocess_logger.name,
307
+ },
308
+ },
309
+ "formatters": {
310
+ "indent": {
311
+ "()": IndentingFormatter,
312
+ "format": "%(message)s",
313
+ },
314
+ "indent_with_timestamp": {
315
+ "()": IndentingFormatter,
316
+ "format": "%(message)s",
317
+ "add_timestamp": True,
318
+ },
319
+ },
320
+ "handlers": {
321
+ "console": {
322
+ "level": level,
323
+ "class": handler_classes["stream"],
324
+ "console": _stdout_console,
325
+ "filters": ["exclude_subprocess", "exclude_warnings"],
326
+ "formatter": "indent",
327
+ },
328
+ "console_errors": {
329
+ "level": "WARNING",
330
+ "class": handler_classes["stream"],
331
+ "console": _stderr_console,
332
+ "filters": ["exclude_subprocess"],
333
+ "formatter": "indent",
334
+ },
335
+ # A handler responsible for logging to the console messages
336
+ # from the "subprocessor" logger.
337
+ "console_subprocess": {
338
+ "level": level,
339
+ "class": handler_classes["stream"],
340
+ "console": _stderr_console,
341
+ "filters": ["restrict_to_subprocess"],
342
+ "formatter": "indent",
343
+ },
344
+ "user_log": {
345
+ "level": "DEBUG",
346
+ "class": handler_classes["file"],
347
+ "filename": additional_log_file,
348
+ "encoding": "utf-8",
349
+ "delay": True,
350
+ "formatter": "indent_with_timestamp",
351
+ },
352
+ },
353
+ "root": {
354
+ "level": root_level,
355
+ "handlers": handlers,
356
+ },
357
+ "loggers": {"pip._vendor": {"level": vendored_log_level}},
358
+ }
359
+ )
360
+
361
+ return level_number
.venv/lib/python3.13/site-packages/pip/_internal/utils/misc.py ADDED
@@ -0,0 +1,773 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import errno
2
+ import getpass
3
+ import hashlib
4
+ import logging
5
+ import os
6
+ import posixpath
7
+ import shutil
8
+ import stat
9
+ import sys
10
+ import sysconfig
11
+ import urllib.parse
12
+ from dataclasses import dataclass
13
+ from functools import partial
14
+ from io import StringIO
15
+ from itertools import filterfalse, tee, zip_longest
16
+ from pathlib import Path
17
+ from types import FunctionType, TracebackType
18
+ from typing import (
19
+ Any,
20
+ BinaryIO,
21
+ Callable,
22
+ Generator,
23
+ Iterable,
24
+ Iterator,
25
+ List,
26
+ Mapping,
27
+ Optional,
28
+ Sequence,
29
+ TextIO,
30
+ Tuple,
31
+ Type,
32
+ TypeVar,
33
+ Union,
34
+ cast,
35
+ )
36
+
37
+ from pip._vendor.packaging.requirements import Requirement
38
+ from pip._vendor.pyproject_hooks import BuildBackendHookCaller
39
+
40
+ from pip import __version__
41
+ from pip._internal.exceptions import CommandError, ExternallyManagedEnvironment
42
+ from pip._internal.locations import get_major_minor_version
43
+ from pip._internal.utils.compat import WINDOWS
44
+ from pip._internal.utils.retry import retry
45
+ from pip._internal.utils.virtualenv import running_under_virtualenv
46
+
47
+ __all__ = [
48
+ "rmtree",
49
+ "display_path",
50
+ "backup_dir",
51
+ "ask",
52
+ "splitext",
53
+ "format_size",
54
+ "is_installable_dir",
55
+ "normalize_path",
56
+ "renames",
57
+ "get_prog",
58
+ "ensure_dir",
59
+ "remove_auth_from_url",
60
+ "check_externally_managed",
61
+ "ConfiguredBuildBackendHookCaller",
62
+ ]
63
+
64
+ logger = logging.getLogger(__name__)
65
+
66
+ T = TypeVar("T")
67
+ ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType]
68
+ VersionInfo = Tuple[int, int, int]
69
+ NetlocTuple = Tuple[str, Tuple[Optional[str], Optional[str]]]
70
+ OnExc = Callable[[FunctionType, Path, BaseException], Any]
71
+ OnErr = Callable[[FunctionType, Path, ExcInfo], Any]
72
+
73
+ FILE_CHUNK_SIZE = 1024 * 1024
74
+
75
+
76
+ def get_pip_version() -> str:
77
+ pip_pkg_dir = os.path.join(os.path.dirname(__file__), "..", "..")
78
+ pip_pkg_dir = os.path.abspath(pip_pkg_dir)
79
+
80
+ return f"pip {__version__} from {pip_pkg_dir} (python {get_major_minor_version()})"
81
+
82
+
83
+ def normalize_version_info(py_version_info: Tuple[int, ...]) -> Tuple[int, int, int]:
84
+ """
85
+ Convert a tuple of ints representing a Python version to one of length
86
+ three.
87
+
88
+ :param py_version_info: a tuple of ints representing a Python version,
89
+ or None to specify no version. The tuple can have any length.
90
+
91
+ :return: a tuple of length three if `py_version_info` is non-None.
92
+ Otherwise, return `py_version_info` unchanged (i.e. None).
93
+ """
94
+ if len(py_version_info) < 3:
95
+ py_version_info += (3 - len(py_version_info)) * (0,)
96
+ elif len(py_version_info) > 3:
97
+ py_version_info = py_version_info[:3]
98
+
99
+ return cast("VersionInfo", py_version_info)
100
+
101
+
102
+ def ensure_dir(path: str) -> None:
103
+ """os.path.makedirs without EEXIST."""
104
+ try:
105
+ os.makedirs(path)
106
+ except OSError as e:
107
+ # Windows can raise spurious ENOTEMPTY errors. See #6426.
108
+ if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
109
+ raise
110
+
111
+
112
+ def get_prog() -> str:
113
+ try:
114
+ prog = os.path.basename(sys.argv[0])
115
+ if prog in ("__main__.py", "-c"):
116
+ return f"{sys.executable} -m pip"
117
+ else:
118
+ return prog
119
+ except (AttributeError, TypeError, IndexError):
120
+ pass
121
+ return "pip"
122
+
123
+
124
+ # Retry every half second for up to 3 seconds
125
+ @retry(stop_after_delay=3, wait=0.5)
126
+ def rmtree(
127
+ dir: str, ignore_errors: bool = False, onexc: Optional[OnExc] = None
128
+ ) -> None:
129
+ if ignore_errors:
130
+ onexc = _onerror_ignore
131
+ if onexc is None:
132
+ onexc = _onerror_reraise
133
+ handler: OnErr = partial(rmtree_errorhandler, onexc=onexc)
134
+ if sys.version_info >= (3, 12):
135
+ # See https://docs.python.org/3.12/whatsnew/3.12.html#shutil.
136
+ shutil.rmtree(dir, onexc=handler) # type: ignore
137
+ else:
138
+ shutil.rmtree(dir, onerror=handler) # type: ignore
139
+
140
+
141
+ def _onerror_ignore(*_args: Any) -> None:
142
+ pass
143
+
144
+
145
+ def _onerror_reraise(*_args: Any) -> None:
146
+ raise # noqa: PLE0704 - Bare exception used to reraise existing exception
147
+
148
+
149
+ def rmtree_errorhandler(
150
+ func: FunctionType,
151
+ path: Path,
152
+ exc_info: Union[ExcInfo, BaseException],
153
+ *,
154
+ onexc: OnExc = _onerror_reraise,
155
+ ) -> None:
156
+ """
157
+ `rmtree` error handler to 'force' a file remove (i.e. like `rm -f`).
158
+
159
+ * If a file is readonly then it's write flag is set and operation is
160
+ retried.
161
+
162
+ * `onerror` is the original callback from `rmtree(... onerror=onerror)`
163
+ that is chained at the end if the "rm -f" still fails.
164
+ """
165
+ try:
166
+ st_mode = os.stat(path).st_mode
167
+ except OSError:
168
+ # it's equivalent to os.path.exists
169
+ return
170
+
171
+ if not st_mode & stat.S_IWRITE:
172
+ # convert to read/write
173
+ try:
174
+ os.chmod(path, st_mode | stat.S_IWRITE)
175
+ except OSError:
176
+ pass
177
+ else:
178
+ # use the original function to repeat the operation
179
+ try:
180
+ func(path)
181
+ return
182
+ except OSError:
183
+ pass
184
+
185
+ if not isinstance(exc_info, BaseException):
186
+ _, exc_info, _ = exc_info
187
+ onexc(func, path, exc_info)
188
+
189
+
190
+ def display_path(path: str) -> str:
191
+ """Gives the display value for a given path, making it relative to cwd
192
+ if possible."""
193
+ path = os.path.normcase(os.path.abspath(path))
194
+ if path.startswith(os.getcwd() + os.path.sep):
195
+ path = "." + path[len(os.getcwd()) :]
196
+ return path
197
+
198
+
199
+ def backup_dir(dir: str, ext: str = ".bak") -> str:
200
+ """Figure out the name of a directory to back up the given dir to
201
+ (adding .bak, .bak2, etc)"""
202
+ n = 1
203
+ extension = ext
204
+ while os.path.exists(dir + extension):
205
+ n += 1
206
+ extension = ext + str(n)
207
+ return dir + extension
208
+
209
+
210
+ def ask_path_exists(message: str, options: Iterable[str]) -> str:
211
+ for action in os.environ.get("PIP_EXISTS_ACTION", "").split():
212
+ if action in options:
213
+ return action
214
+ return ask(message, options)
215
+
216
+
217
+ def _check_no_input(message: str) -> None:
218
+ """Raise an error if no input is allowed."""
219
+ if os.environ.get("PIP_NO_INPUT"):
220
+ raise Exception(
221
+ f"No input was expected ($PIP_NO_INPUT set); question: {message}"
222
+ )
223
+
224
+
225
+ def ask(message: str, options: Iterable[str]) -> str:
226
+ """Ask the message interactively, with the given possible responses"""
227
+ while 1:
228
+ _check_no_input(message)
229
+ response = input(message)
230
+ response = response.strip().lower()
231
+ if response not in options:
232
+ print(
233
+ "Your response ({!r}) was not one of the expected responses: "
234
+ "{}".format(response, ", ".join(options))
235
+ )
236
+ else:
237
+ return response
238
+
239
+
240
+ def ask_input(message: str) -> str:
241
+ """Ask for input interactively."""
242
+ _check_no_input(message)
243
+ return input(message)
244
+
245
+
246
+ def ask_password(message: str) -> str:
247
+ """Ask for a password interactively."""
248
+ _check_no_input(message)
249
+ return getpass.getpass(message)
250
+
251
+
252
+ def strtobool(val: str) -> int:
253
+ """Convert a string representation of truth to true (1) or false (0).
254
+
255
+ True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
256
+ are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
257
+ 'val' is anything else.
258
+ """
259
+ val = val.lower()
260
+ if val in ("y", "yes", "t", "true", "on", "1"):
261
+ return 1
262
+ elif val in ("n", "no", "f", "false", "off", "0"):
263
+ return 0
264
+ else:
265
+ raise ValueError(f"invalid truth value {val!r}")
266
+
267
+
268
+ def format_size(bytes: float) -> str:
269
+ if bytes > 1000 * 1000:
270
+ return f"{bytes / 1000.0 / 1000:.1f} MB"
271
+ elif bytes > 10 * 1000:
272
+ return f"{int(bytes / 1000)} kB"
273
+ elif bytes > 1000:
274
+ return f"{bytes / 1000.0:.1f} kB"
275
+ else:
276
+ return f"{int(bytes)} bytes"
277
+
278
+
279
+ def tabulate(rows: Iterable[Iterable[Any]]) -> Tuple[List[str], List[int]]:
280
+ """Return a list of formatted rows and a list of column sizes.
281
+
282
+ For example::
283
+
284
+ >>> tabulate([['foobar', 2000], [0xdeadbeef]])
285
+ (['foobar 2000', '3735928559'], [10, 4])
286
+ """
287
+ rows = [tuple(map(str, row)) for row in rows]
288
+ sizes = [max(map(len, col)) for col in zip_longest(*rows, fillvalue="")]
289
+ table = [" ".join(map(str.ljust, row, sizes)).rstrip() for row in rows]
290
+ return table, sizes
291
+
292
+
293
+ def is_installable_dir(path: str) -> bool:
294
+ """Is path is a directory containing pyproject.toml or setup.py?
295
+
296
+ If pyproject.toml exists, this is a PEP 517 project. Otherwise we look for
297
+ a legacy setuptools layout by identifying setup.py. We don't check for the
298
+ setup.cfg because using it without setup.py is only available for PEP 517
299
+ projects, which are already covered by the pyproject.toml check.
300
+ """
301
+ if not os.path.isdir(path):
302
+ return False
303
+ if os.path.isfile(os.path.join(path, "pyproject.toml")):
304
+ return True
305
+ if os.path.isfile(os.path.join(path, "setup.py")):
306
+ return True
307
+ return False
308
+
309
+
310
+ def read_chunks(
311
+ file: BinaryIO, size: int = FILE_CHUNK_SIZE
312
+ ) -> Generator[bytes, None, None]:
313
+ """Yield pieces of data from a file-like object until EOF."""
314
+ while True:
315
+ chunk = file.read(size)
316
+ if not chunk:
317
+ break
318
+ yield chunk
319
+
320
+
321
+ def normalize_path(path: str, resolve_symlinks: bool = True) -> str:
322
+ """
323
+ Convert a path to its canonical, case-normalized, absolute version.
324
+
325
+ """
326
+ path = os.path.expanduser(path)
327
+ if resolve_symlinks:
328
+ path = os.path.realpath(path)
329
+ else:
330
+ path = os.path.abspath(path)
331
+ return os.path.normcase(path)
332
+
333
+
334
+ def splitext(path: str) -> Tuple[str, str]:
335
+ """Like os.path.splitext, but take off .tar too"""
336
+ base, ext = posixpath.splitext(path)
337
+ if base.lower().endswith(".tar"):
338
+ ext = base[-4:] + ext
339
+ base = base[:-4]
340
+ return base, ext
341
+
342
+
343
+ def renames(old: str, new: str) -> None:
344
+ """Like os.renames(), but handles renaming across devices."""
345
+ # Implementation borrowed from os.renames().
346
+ head, tail = os.path.split(new)
347
+ if head and tail and not os.path.exists(head):
348
+ os.makedirs(head)
349
+
350
+ shutil.move(old, new)
351
+
352
+ head, tail = os.path.split(old)
353
+ if head and tail:
354
+ try:
355
+ os.removedirs(head)
356
+ except OSError:
357
+ pass
358
+
359
+
360
+ def is_local(path: str) -> bool:
361
+ """
362
+ Return True if path is within sys.prefix, if we're running in a virtualenv.
363
+
364
+ If we're not in a virtualenv, all paths are considered "local."
365
+
366
+ Caution: this function assumes the head of path has been normalized
367
+ with normalize_path.
368
+ """
369
+ if not running_under_virtualenv():
370
+ return True
371
+ return path.startswith(normalize_path(sys.prefix))
372
+
373
+
374
+ def write_output(msg: Any, *args: Any) -> None:
375
+ logger.info(msg, *args)
376
+
377
+
378
+ class StreamWrapper(StringIO):
379
+ orig_stream: TextIO
380
+
381
+ @classmethod
382
+ def from_stream(cls, orig_stream: TextIO) -> "StreamWrapper":
383
+ ret = cls()
384
+ ret.orig_stream = orig_stream
385
+ return ret
386
+
387
+ # compileall.compile_dir() needs stdout.encoding to print to stdout
388
+ # type ignore is because TextIOBase.encoding is writeable
389
+ @property
390
+ def encoding(self) -> str: # type: ignore
391
+ return self.orig_stream.encoding
392
+
393
+
394
+ # Simulates an enum
395
+ def enum(*sequential: Any, **named: Any) -> Type[Any]:
396
+ enums = dict(zip(sequential, range(len(sequential))), **named)
397
+ reverse = {value: key for key, value in enums.items()}
398
+ enums["reverse_mapping"] = reverse
399
+ return type("Enum", (), enums)
400
+
401
+
402
+ def build_netloc(host: str, port: Optional[int]) -> str:
403
+ """
404
+ Build a netloc from a host-port pair
405
+ """
406
+ if port is None:
407
+ return host
408
+ if ":" in host:
409
+ # Only wrap host with square brackets when it is IPv6
410
+ host = f"[{host}]"
411
+ return f"{host}:{port}"
412
+
413
+
414
+ def build_url_from_netloc(netloc: str, scheme: str = "https") -> str:
415
+ """
416
+ Build a full URL from a netloc.
417
+ """
418
+ if netloc.count(":") >= 2 and "@" not in netloc and "[" not in netloc:
419
+ # It must be a bare IPv6 address, so wrap it with brackets.
420
+ netloc = f"[{netloc}]"
421
+ return f"{scheme}://{netloc}"
422
+
423
+
424
+ def parse_netloc(netloc: str) -> Tuple[Optional[str], Optional[int]]:
425
+ """
426
+ Return the host-port pair from a netloc.
427
+ """
428
+ url = build_url_from_netloc(netloc)
429
+ parsed = urllib.parse.urlparse(url)
430
+ return parsed.hostname, parsed.port
431
+
432
+
433
+ def split_auth_from_netloc(netloc: str) -> NetlocTuple:
434
+ """
435
+ Parse out and remove the auth information from a netloc.
436
+
437
+ Returns: (netloc, (username, password)).
438
+ """
439
+ if "@" not in netloc:
440
+ return netloc, (None, None)
441
+
442
+ # Split from the right because that's how urllib.parse.urlsplit()
443
+ # behaves if more than one @ is present (which can be checked using
444
+ # the password attribute of urlsplit()'s return value).
445
+ auth, netloc = netloc.rsplit("@", 1)
446
+ pw: Optional[str] = None
447
+ if ":" in auth:
448
+ # Split from the left because that's how urllib.parse.urlsplit()
449
+ # behaves if more than one : is present (which again can be checked
450
+ # using the password attribute of the return value)
451
+ user, pw = auth.split(":", 1)
452
+ else:
453
+ user, pw = auth, None
454
+
455
+ user = urllib.parse.unquote(user)
456
+ if pw is not None:
457
+ pw = urllib.parse.unquote(pw)
458
+
459
+ return netloc, (user, pw)
460
+
461
+
462
+ def redact_netloc(netloc: str) -> str:
463
+ """
464
+ Replace the sensitive data in a netloc with "****", if it exists.
465
+
466
+ For example:
467
+ - "user:pass@example.com" returns "user:****@example.com"
468
+ - "accesstoken@example.com" returns "****@example.com"
469
+ """
470
+ netloc, (user, password) = split_auth_from_netloc(netloc)
471
+ if user is None:
472
+ return netloc
473
+ if password is None:
474
+ user = "****"
475
+ password = ""
476
+ else:
477
+ user = urllib.parse.quote(user)
478
+ password = ":****"
479
+ return f"{user}{password}@{netloc}"
480
+
481
+
482
+ def _transform_url(
483
+ url: str, transform_netloc: Callable[[str], Tuple[Any, ...]]
484
+ ) -> Tuple[str, NetlocTuple]:
485
+ """Transform and replace netloc in a url.
486
+
487
+ transform_netloc is a function taking the netloc and returning a
488
+ tuple. The first element of this tuple is the new netloc. The
489
+ entire tuple is returned.
490
+
491
+ Returns a tuple containing the transformed url as item 0 and the
492
+ original tuple returned by transform_netloc as item 1.
493
+ """
494
+ purl = urllib.parse.urlsplit(url)
495
+ netloc_tuple = transform_netloc(purl.netloc)
496
+ # stripped url
497
+ url_pieces = (purl.scheme, netloc_tuple[0], purl.path, purl.query, purl.fragment)
498
+ surl = urllib.parse.urlunsplit(url_pieces)
499
+ return surl, cast("NetlocTuple", netloc_tuple)
500
+
501
+
502
+ def _get_netloc(netloc: str) -> NetlocTuple:
503
+ return split_auth_from_netloc(netloc)
504
+
505
+
506
+ def _redact_netloc(netloc: str) -> Tuple[str]:
507
+ return (redact_netloc(netloc),)
508
+
509
+
510
+ def split_auth_netloc_from_url(
511
+ url: str,
512
+ ) -> Tuple[str, str, Tuple[Optional[str], Optional[str]]]:
513
+ """
514
+ Parse a url into separate netloc, auth, and url with no auth.
515
+
516
+ Returns: (url_without_auth, netloc, (username, password))
517
+ """
518
+ url_without_auth, (netloc, auth) = _transform_url(url, _get_netloc)
519
+ return url_without_auth, netloc, auth
520
+
521
+
522
+ def remove_auth_from_url(url: str) -> str:
523
+ """Return a copy of url with 'username:password@' removed."""
524
+ # username/pass params are passed to subversion through flags
525
+ # and are not recognized in the url.
526
+ return _transform_url(url, _get_netloc)[0]
527
+
528
+
529
+ def redact_auth_from_url(url: str) -> str:
530
+ """Replace the password in a given url with ****."""
531
+ return _transform_url(url, _redact_netloc)[0]
532
+
533
+
534
+ def redact_auth_from_requirement(req: Requirement) -> str:
535
+ """Replace the password in a given requirement url with ****."""
536
+ if not req.url:
537
+ return str(req)
538
+ return str(req).replace(req.url, redact_auth_from_url(req.url))
539
+
540
+
541
+ @dataclass(frozen=True)
542
+ class HiddenText:
543
+ secret: str
544
+ redacted: str
545
+
546
+ def __repr__(self) -> str:
547
+ return f"<HiddenText {str(self)!r}>"
548
+
549
+ def __str__(self) -> str:
550
+ return self.redacted
551
+
552
+ # This is useful for testing.
553
+ def __eq__(self, other: Any) -> bool:
554
+ if type(self) is not type(other):
555
+ return False
556
+
557
+ # The string being used for redaction doesn't also have to match,
558
+ # just the raw, original string.
559
+ return self.secret == other.secret
560
+
561
+
562
+ def hide_value(value: str) -> HiddenText:
563
+ return HiddenText(value, redacted="****")
564
+
565
+
566
+ def hide_url(url: str) -> HiddenText:
567
+ redacted = redact_auth_from_url(url)
568
+ return HiddenText(url, redacted=redacted)
569
+
570
+
571
+ def protect_pip_from_modification_on_windows(modifying_pip: bool) -> None:
572
+ """Protection of pip.exe from modification on Windows
573
+
574
+ On Windows, any operation modifying pip should be run as:
575
+ python -m pip ...
576
+ """
577
+ pip_names = [
578
+ "pip",
579
+ f"pip{sys.version_info.major}",
580
+ f"pip{sys.version_info.major}.{sys.version_info.minor}",
581
+ ]
582
+
583
+ # See https://github.com/pypa/pip/issues/1299 for more discussion
584
+ should_show_use_python_msg = (
585
+ modifying_pip and WINDOWS and os.path.basename(sys.argv[0]) in pip_names
586
+ )
587
+
588
+ if should_show_use_python_msg:
589
+ new_command = [sys.executable, "-m", "pip"] + sys.argv[1:]
590
+ raise CommandError(
591
+ "To modify pip, please run the following command:\n{}".format(
592
+ " ".join(new_command)
593
+ )
594
+ )
595
+
596
+
597
+ def check_externally_managed() -> None:
598
+ """Check whether the current environment is externally managed.
599
+
600
+ If the ``EXTERNALLY-MANAGED`` config file is found, the current environment
601
+ is considered externally managed, and an ExternallyManagedEnvironment is
602
+ raised.
603
+ """
604
+ if running_under_virtualenv():
605
+ return
606
+ marker = os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED")
607
+ if not os.path.isfile(marker):
608
+ return
609
+ raise ExternallyManagedEnvironment.from_config(marker)
610
+
611
+
612
+ def is_console_interactive() -> bool:
613
+ """Is this console interactive?"""
614
+ return sys.stdin is not None and sys.stdin.isatty()
615
+
616
+
617
+ def hash_file(path: str, blocksize: int = 1 << 20) -> Tuple[Any, int]:
618
+ """Return (hash, length) for path using hashlib.sha256()"""
619
+
620
+ h = hashlib.sha256()
621
+ length = 0
622
+ with open(path, "rb") as f:
623
+ for block in read_chunks(f, size=blocksize):
624
+ length += len(block)
625
+ h.update(block)
626
+ return h, length
627
+
628
+
629
+ def pairwise(iterable: Iterable[Any]) -> Iterator[Tuple[Any, Any]]:
630
+ """
631
+ Return paired elements.
632
+
633
+ For example:
634
+ s -> (s0, s1), (s2, s3), (s4, s5), ...
635
+ """
636
+ iterable = iter(iterable)
637
+ return zip_longest(iterable, iterable)
638
+
639
+
640
+ def partition(
641
+ pred: Callable[[T], bool], iterable: Iterable[T]
642
+ ) -> Tuple[Iterable[T], Iterable[T]]:
643
+ """
644
+ Use a predicate to partition entries into false entries and true entries,
645
+ like
646
+
647
+ partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9
648
+ """
649
+ t1, t2 = tee(iterable)
650
+ return filterfalse(pred, t1), filter(pred, t2)
651
+
652
+
653
+ class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller):
654
+ def __init__(
655
+ self,
656
+ config_holder: Any,
657
+ source_dir: str,
658
+ build_backend: str,
659
+ backend_path: Optional[str] = None,
660
+ runner: Optional[Callable[..., None]] = None,
661
+ python_executable: Optional[str] = None,
662
+ ):
663
+ super().__init__(
664
+ source_dir, build_backend, backend_path, runner, python_executable
665
+ )
666
+ self.config_holder = config_holder
667
+
668
+ def build_wheel(
669
+ self,
670
+ wheel_directory: str,
671
+ config_settings: Optional[Mapping[str, Any]] = None,
672
+ metadata_directory: Optional[str] = None,
673
+ ) -> str:
674
+ cs = self.config_holder.config_settings
675
+ return super().build_wheel(
676
+ wheel_directory, config_settings=cs, metadata_directory=metadata_directory
677
+ )
678
+
679
+ def build_sdist(
680
+ self,
681
+ sdist_directory: str,
682
+ config_settings: Optional[Mapping[str, Any]] = None,
683
+ ) -> str:
684
+ cs = self.config_holder.config_settings
685
+ return super().build_sdist(sdist_directory, config_settings=cs)
686
+
687
+ def build_editable(
688
+ self,
689
+ wheel_directory: str,
690
+ config_settings: Optional[Mapping[str, Any]] = None,
691
+ metadata_directory: Optional[str] = None,
692
+ ) -> str:
693
+ cs = self.config_holder.config_settings
694
+ return super().build_editable(
695
+ wheel_directory, config_settings=cs, metadata_directory=metadata_directory
696
+ )
697
+
698
+ def get_requires_for_build_wheel(
699
+ self, config_settings: Optional[Mapping[str, Any]] = None
700
+ ) -> Sequence[str]:
701
+ cs = self.config_holder.config_settings
702
+ return super().get_requires_for_build_wheel(config_settings=cs)
703
+
704
+ def get_requires_for_build_sdist(
705
+ self, config_settings: Optional[Mapping[str, Any]] = None
706
+ ) -> Sequence[str]:
707
+ cs = self.config_holder.config_settings
708
+ return super().get_requires_for_build_sdist(config_settings=cs)
709
+
710
+ def get_requires_for_build_editable(
711
+ self, config_settings: Optional[Mapping[str, Any]] = None
712
+ ) -> Sequence[str]:
713
+ cs = self.config_holder.config_settings
714
+ return super().get_requires_for_build_editable(config_settings=cs)
715
+
716
+ def prepare_metadata_for_build_wheel(
717
+ self,
718
+ metadata_directory: str,
719
+ config_settings: Optional[Mapping[str, Any]] = None,
720
+ _allow_fallback: bool = True,
721
+ ) -> str:
722
+ cs = self.config_holder.config_settings
723
+ return super().prepare_metadata_for_build_wheel(
724
+ metadata_directory=metadata_directory,
725
+ config_settings=cs,
726
+ _allow_fallback=_allow_fallback,
727
+ )
728
+
729
+ def prepare_metadata_for_build_editable(
730
+ self,
731
+ metadata_directory: str,
732
+ config_settings: Optional[Mapping[str, Any]] = None,
733
+ _allow_fallback: bool = True,
734
+ ) -> Optional[str]:
735
+ cs = self.config_holder.config_settings
736
+ return super().prepare_metadata_for_build_editable(
737
+ metadata_directory=metadata_directory,
738
+ config_settings=cs,
739
+ _allow_fallback=_allow_fallback,
740
+ )
741
+
742
+
743
+ def warn_if_run_as_root() -> None:
744
+ """Output a warning for sudo users on Unix.
745
+
746
+ In a virtual environment, sudo pip still writes to virtualenv.
747
+ On Windows, users may run pip as Administrator without issues.
748
+ This warning only applies to Unix root users outside of virtualenv.
749
+ """
750
+ if running_under_virtualenv():
751
+ return
752
+ if not hasattr(os, "getuid"):
753
+ return
754
+ # On Windows, there are no "system managed" Python packages. Installing as
755
+ # Administrator via pip is the correct way of updating system environments.
756
+ #
757
+ # We choose sys.platform over utils.compat.WINDOWS here to enable Mypy platform
758
+ # checks: https://mypy.readthedocs.io/en/stable/common_issues.html
759
+ if sys.platform == "win32" or sys.platform == "cygwin":
760
+ return
761
+
762
+ if os.getuid() != 0:
763
+ return
764
+
765
+ logger.warning(
766
+ "Running pip as the 'root' user can result in broken permissions and "
767
+ "conflicting behaviour with the system package manager, possibly "
768
+ "rendering your system unusable. "
769
+ "It is recommended to use a virtual environment instead: "
770
+ "https://pip.pypa.io/warnings/venv. "
771
+ "Use the --root-user-action option if you know what you are doing and "
772
+ "want to suppress this warning."
773
+ )
.venv/lib/python3.13/site-packages/pip/_internal/utils/packaging.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import logging
3
+ from typing import Optional, Tuple
4
+
5
+ from pip._vendor.packaging import specifiers, version
6
+ from pip._vendor.packaging.requirements import Requirement
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ @functools.lru_cache(maxsize=32)
12
+ def check_requires_python(
13
+ requires_python: Optional[str], version_info: Tuple[int, ...]
14
+ ) -> bool:
15
+ """
16
+ Check if the given Python version matches a "Requires-Python" specifier.
17
+
18
+ :param version_info: A 3-tuple of ints representing a Python
19
+ major-minor-micro version to check (e.g. `sys.version_info[:3]`).
20
+
21
+ :return: `True` if the given Python version satisfies the requirement.
22
+ Otherwise, return `False`.
23
+
24
+ :raises InvalidSpecifier: If `requires_python` has an invalid format.
25
+ """
26
+ if requires_python is None:
27
+ # The package provides no information
28
+ return True
29
+ requires_python_specifier = specifiers.SpecifierSet(requires_python)
30
+
31
+ python_version = version.parse(".".join(map(str, version_info)))
32
+ return python_version in requires_python_specifier
33
+
34
+
35
+ @functools.lru_cache(maxsize=10000)
36
+ def get_requirement(req_string: str) -> Requirement:
37
+ """Construct a packaging.Requirement object with caching"""
38
+ # Parsing requirement strings is expensive, and is also expected to happen
39
+ # with a low diversity of different arguments (at least relative the number
40
+ # constructed). This method adds a cache to requirement object creation to
41
+ # minimize repeated parsing of the same string to construct equivalent
42
+ # Requirement objects.
43
+ return Requirement(req_string)
.venv/lib/python3.13/site-packages/pip/_internal/utils/retry.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ from time import perf_counter, sleep
3
+ from typing import Callable, TypeVar
4
+
5
+ from pip._vendor.typing_extensions import ParamSpec
6
+
7
+ T = TypeVar("T")
8
+ P = ParamSpec("P")
9
+
10
+
11
+ def retry(
12
+ wait: float, stop_after_delay: float
13
+ ) -> Callable[[Callable[P, T]], Callable[P, T]]:
14
+ """Decorator to automatically retry a function on error.
15
+
16
+ If the function raises, the function is recalled with the same arguments
17
+ until it returns or the time limit is reached. When the time limit is
18
+ surpassed, the last exception raised is reraised.
19
+
20
+ :param wait: The time to wait after an error before retrying, in seconds.
21
+ :param stop_after_delay: The time limit after which retries will cease,
22
+ in seconds.
23
+ """
24
+
25
+ def wrapper(func: Callable[P, T]) -> Callable[P, T]:
26
+
27
+ @functools.wraps(func)
28
+ def retry_wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
29
+ # The performance counter is monotonic on all platforms we care
30
+ # about and has much better resolution than time.monotonic().
31
+ start_time = perf_counter()
32
+ while True:
33
+ try:
34
+ return func(*args, **kwargs)
35
+ except Exception:
36
+ if perf_counter() - start_time > stop_after_delay:
37
+ raise
38
+ sleep(wait)
39
+
40
+ return retry_wrapped
41
+
42
+ return wrapper
.venv/lib/python3.13/site-packages/pip/_internal/utils/setuptools_build.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import textwrap
3
+ from typing import List, Optional, Sequence
4
+
5
+ # Shim to wrap setup.py invocation with setuptools
6
+ # Note that __file__ is handled via two {!r} *and* %r, to ensure that paths on
7
+ # Windows are correctly handled (it should be "C:\\Users" not "C:\Users").
8
+ _SETUPTOOLS_SHIM = textwrap.dedent(
9
+ """
10
+ exec(compile('''
11
+ # This is <pip-setuptools-caller> -- a caller that pip uses to run setup.py
12
+ #
13
+ # - It imports setuptools before invoking setup.py, to enable projects that directly
14
+ # import from `distutils.core` to work with newer packaging standards.
15
+ # - It provides a clear error message when setuptools is not installed.
16
+ # - It sets `sys.argv[0]` to the underlying `setup.py`, when invoking `setup.py` so
17
+ # setuptools doesn't think the script is `-c`. This avoids the following warning:
18
+ # manifest_maker: standard file '-c' not found".
19
+ # - It generates a shim setup.py, for handling setup.cfg-only projects.
20
+ import os, sys, tokenize, traceback
21
+
22
+ try:
23
+ import setuptools
24
+ except ImportError:
25
+ print(
26
+ "ERROR: Can not execute `setup.py` since setuptools failed to import in "
27
+ "the build environment with exception:",
28
+ file=sys.stderr,
29
+ )
30
+ traceback.print_exc()
31
+ sys.exit(1)
32
+
33
+ __file__ = %r
34
+ sys.argv[0] = __file__
35
+
36
+ if os.path.exists(__file__):
37
+ filename = __file__
38
+ with tokenize.open(__file__) as f:
39
+ setup_py_code = f.read()
40
+ else:
41
+ filename = "<auto-generated setuptools caller>"
42
+ setup_py_code = "from setuptools import setup; setup()"
43
+
44
+ exec(compile(setup_py_code, filename, "exec"))
45
+ ''' % ({!r},), "<pip-setuptools-caller>", "exec"))
46
+ """
47
+ ).rstrip()
48
+
49
+
50
+ def make_setuptools_shim_args(
51
+ setup_py_path: str,
52
+ global_options: Optional[Sequence[str]] = None,
53
+ no_user_config: bool = False,
54
+ unbuffered_output: bool = False,
55
+ ) -> List[str]:
56
+ """
57
+ Get setuptools command arguments with shim wrapped setup file invocation.
58
+
59
+ :param setup_py_path: The path to setup.py to be wrapped.
60
+ :param global_options: Additional global options.
61
+ :param no_user_config: If True, disables personal user configuration.
62
+ :param unbuffered_output: If True, adds the unbuffered switch to the
63
+ argument list.
64
+ """
65
+ args = [sys.executable]
66
+ if unbuffered_output:
67
+ args += ["-u"]
68
+ args += ["-c", _SETUPTOOLS_SHIM.format(setup_py_path)]
69
+ if global_options:
70
+ args += global_options
71
+ if no_user_config:
72
+ args += ["--no-user-cfg"]
73
+ return args
74
+
75
+
76
+ def make_setuptools_bdist_wheel_args(
77
+ setup_py_path: str,
78
+ global_options: Sequence[str],
79
+ build_options: Sequence[str],
80
+ destination_dir: str,
81
+ ) -> List[str]:
82
+ # NOTE: Eventually, we'd want to also -S to the flags here, when we're
83
+ # isolating. Currently, it breaks Python in virtualenvs, because it
84
+ # relies on site.py to find parts of the standard library outside the
85
+ # virtualenv.
86
+ args = make_setuptools_shim_args(
87
+ setup_py_path, global_options=global_options, unbuffered_output=True
88
+ )
89
+ args += ["bdist_wheel", "-d", destination_dir]
90
+ args += build_options
91
+ return args
92
+
93
+
94
+ def make_setuptools_clean_args(
95
+ setup_py_path: str,
96
+ global_options: Sequence[str],
97
+ ) -> List[str]:
98
+ args = make_setuptools_shim_args(
99
+ setup_py_path, global_options=global_options, unbuffered_output=True
100
+ )
101
+ args += ["clean", "--all"]
102
+ return args
103
+
104
+
105
+ def make_setuptools_develop_args(
106
+ setup_py_path: str,
107
+ *,
108
+ global_options: Sequence[str],
109
+ no_user_config: bool,
110
+ prefix: Optional[str],
111
+ home: Optional[str],
112
+ use_user_site: bool,
113
+ ) -> List[str]:
114
+ assert not (use_user_site and prefix)
115
+
116
+ args = make_setuptools_shim_args(
117
+ setup_py_path,
118
+ global_options=global_options,
119
+ no_user_config=no_user_config,
120
+ )
121
+
122
+ args += ["develop", "--no-deps"]
123
+
124
+ if prefix:
125
+ args += ["--prefix", prefix]
126
+ if home is not None:
127
+ args += ["--install-dir", home]
128
+
129
+ if use_user_site:
130
+ args += ["--user", "--prefix="]
131
+
132
+ return args
133
+
134
+
135
+ def make_setuptools_egg_info_args(
136
+ setup_py_path: str,
137
+ egg_info_dir: Optional[str],
138
+ no_user_config: bool,
139
+ ) -> List[str]:
140
+ args = make_setuptools_shim_args(setup_py_path, no_user_config=no_user_config)
141
+
142
+ args += ["egg_info"]
143
+
144
+ if egg_info_dir:
145
+ args += ["--egg-base", egg_info_dir]
146
+
147
+ return args
.venv/lib/python3.13/site-packages/pip/_internal/utils/subprocess.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import shlex
4
+ import subprocess
5
+ from typing import Any, Callable, Iterable, List, Literal, Mapping, Optional, Union
6
+
7
+ from pip._vendor.rich.markup import escape
8
+
9
+ from pip._internal.cli.spinners import SpinnerInterface, open_spinner
10
+ from pip._internal.exceptions import InstallationSubprocessError
11
+ from pip._internal.utils.logging import VERBOSE, subprocess_logger
12
+ from pip._internal.utils.misc import HiddenText
13
+
14
+ CommandArgs = List[Union[str, HiddenText]]
15
+
16
+
17
+ def make_command(*args: Union[str, HiddenText, CommandArgs]) -> CommandArgs:
18
+ """
19
+ Create a CommandArgs object.
20
+ """
21
+ command_args: CommandArgs = []
22
+ for arg in args:
23
+ # Check for list instead of CommandArgs since CommandArgs is
24
+ # only known during type-checking.
25
+ if isinstance(arg, list):
26
+ command_args.extend(arg)
27
+ else:
28
+ # Otherwise, arg is str or HiddenText.
29
+ command_args.append(arg)
30
+
31
+ return command_args
32
+
33
+
34
+ def format_command_args(args: Union[List[str], CommandArgs]) -> str:
35
+ """
36
+ Format command arguments for display.
37
+ """
38
+ # For HiddenText arguments, display the redacted form by calling str().
39
+ # Also, we don't apply str() to arguments that aren't HiddenText since
40
+ # this can trigger a UnicodeDecodeError in Python 2 if the argument
41
+ # has type unicode and includes a non-ascii character. (The type
42
+ # checker doesn't ensure the annotations are correct in all cases.)
43
+ return " ".join(
44
+ shlex.quote(str(arg)) if isinstance(arg, HiddenText) else shlex.quote(arg)
45
+ for arg in args
46
+ )
47
+
48
+
49
+ def reveal_command_args(args: Union[List[str], CommandArgs]) -> List[str]:
50
+ """
51
+ Return the arguments in their raw, unredacted form.
52
+ """
53
+ return [arg.secret if isinstance(arg, HiddenText) else arg for arg in args]
54
+
55
+
56
+ def call_subprocess(
57
+ cmd: Union[List[str], CommandArgs],
58
+ show_stdout: bool = False,
59
+ cwd: Optional[str] = None,
60
+ on_returncode: 'Literal["raise", "warn", "ignore"]' = "raise",
61
+ extra_ok_returncodes: Optional[Iterable[int]] = None,
62
+ extra_environ: Optional[Mapping[str, Any]] = None,
63
+ unset_environ: Optional[Iterable[str]] = None,
64
+ spinner: Optional[SpinnerInterface] = None,
65
+ log_failed_cmd: Optional[bool] = True,
66
+ stdout_only: Optional[bool] = False,
67
+ *,
68
+ command_desc: str,
69
+ ) -> str:
70
+ """
71
+ Args:
72
+ show_stdout: if true, use INFO to log the subprocess's stderr and
73
+ stdout streams. Otherwise, use DEBUG. Defaults to False.
74
+ extra_ok_returncodes: an iterable of integer return codes that are
75
+ acceptable, in addition to 0. Defaults to None, which means [].
76
+ unset_environ: an iterable of environment variable names to unset
77
+ prior to calling subprocess.Popen().
78
+ log_failed_cmd: if false, failed commands are not logged, only raised.
79
+ stdout_only: if true, return only stdout, else return both. When true,
80
+ logging of both stdout and stderr occurs when the subprocess has
81
+ terminated, else logging occurs as subprocess output is produced.
82
+ """
83
+ if extra_ok_returncodes is None:
84
+ extra_ok_returncodes = []
85
+ if unset_environ is None:
86
+ unset_environ = []
87
+ # Most places in pip use show_stdout=False. What this means is--
88
+ #
89
+ # - We connect the child's output (combined stderr and stdout) to a
90
+ # single pipe, which we read.
91
+ # - We log this output to stderr at DEBUG level as it is received.
92
+ # - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't
93
+ # requested), then we show a spinner so the user can still see the
94
+ # subprocess is in progress.
95
+ # - If the subprocess exits with an error, we log the output to stderr
96
+ # at ERROR level if it hasn't already been displayed to the console
97
+ # (e.g. if --verbose logging wasn't enabled). This way we don't log
98
+ # the output to the console twice.
99
+ #
100
+ # If show_stdout=True, then the above is still done, but with DEBUG
101
+ # replaced by INFO.
102
+ if show_stdout:
103
+ # Then log the subprocess output at INFO level.
104
+ log_subprocess: Callable[..., None] = subprocess_logger.info
105
+ used_level = logging.INFO
106
+ else:
107
+ # Then log the subprocess output using VERBOSE. This also ensures
108
+ # it will be logged to the log file (aka user_log), if enabled.
109
+ log_subprocess = subprocess_logger.verbose
110
+ used_level = VERBOSE
111
+
112
+ # Whether the subprocess will be visible in the console.
113
+ showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level
114
+
115
+ # Only use the spinner if we're not showing the subprocess output
116
+ # and we have a spinner.
117
+ use_spinner = not showing_subprocess and spinner is not None
118
+
119
+ log_subprocess("Running command %s", command_desc)
120
+ env = os.environ.copy()
121
+ if extra_environ:
122
+ env.update(extra_environ)
123
+ for name in unset_environ:
124
+ env.pop(name, None)
125
+ try:
126
+ proc = subprocess.Popen(
127
+ # Convert HiddenText objects to the underlying str.
128
+ reveal_command_args(cmd),
129
+ stdin=subprocess.PIPE,
130
+ stdout=subprocess.PIPE,
131
+ stderr=subprocess.STDOUT if not stdout_only else subprocess.PIPE,
132
+ cwd=cwd,
133
+ env=env,
134
+ errors="backslashreplace",
135
+ )
136
+ except Exception as exc:
137
+ if log_failed_cmd:
138
+ subprocess_logger.critical(
139
+ "Error %s while executing command %s",
140
+ exc,
141
+ command_desc,
142
+ )
143
+ raise
144
+ all_output = []
145
+ if not stdout_only:
146
+ assert proc.stdout
147
+ assert proc.stdin
148
+ proc.stdin.close()
149
+ # In this mode, stdout and stderr are in the same pipe.
150
+ while True:
151
+ line: str = proc.stdout.readline()
152
+ if not line:
153
+ break
154
+ line = line.rstrip()
155
+ all_output.append(line + "\n")
156
+
157
+ # Show the line immediately.
158
+ log_subprocess(line)
159
+ # Update the spinner.
160
+ if use_spinner:
161
+ assert spinner
162
+ spinner.spin()
163
+ try:
164
+ proc.wait()
165
+ finally:
166
+ if proc.stdout:
167
+ proc.stdout.close()
168
+ output = "".join(all_output)
169
+ else:
170
+ # In this mode, stdout and stderr are in different pipes.
171
+ # We must use communicate() which is the only safe way to read both.
172
+ out, err = proc.communicate()
173
+ # log line by line to preserve pip log indenting
174
+ for out_line in out.splitlines():
175
+ log_subprocess(out_line)
176
+ all_output.append(out)
177
+ for err_line in err.splitlines():
178
+ log_subprocess(err_line)
179
+ all_output.append(err)
180
+ output = out
181
+
182
+ proc_had_error = proc.returncode and proc.returncode not in extra_ok_returncodes
183
+ if use_spinner:
184
+ assert spinner
185
+ if proc_had_error:
186
+ spinner.finish("error")
187
+ else:
188
+ spinner.finish("done")
189
+ if proc_had_error:
190
+ if on_returncode == "raise":
191
+ error = InstallationSubprocessError(
192
+ command_description=command_desc,
193
+ exit_code=proc.returncode,
194
+ output_lines=all_output if not showing_subprocess else None,
195
+ )
196
+ if log_failed_cmd:
197
+ subprocess_logger.error("%s", error, extra={"rich": True})
198
+ subprocess_logger.verbose(
199
+ "[bold magenta]full command[/]: [blue]%s[/]",
200
+ escape(format_command_args(cmd)),
201
+ extra={"markup": True},
202
+ )
203
+ subprocess_logger.verbose(
204
+ "[bold magenta]cwd[/]: %s",
205
+ escape(cwd or "[inherit]"),
206
+ extra={"markup": True},
207
+ )
208
+
209
+ raise error
210
+ elif on_returncode == "warn":
211
+ subprocess_logger.warning(
212
+ 'Command "%s" had error code %s in %s',
213
+ command_desc,
214
+ proc.returncode,
215
+ cwd,
216
+ )
217
+ elif on_returncode == "ignore":
218
+ pass
219
+ else:
220
+ raise ValueError(f"Invalid value: on_returncode={on_returncode!r}")
221
+ return output
222
+
223
+
224
+ def runner_with_spinner_message(message: str) -> Callable[..., None]:
225
+ """Provide a subprocess_runner that shows a spinner message.
226
+
227
+ Intended for use with for BuildBackendHookCaller. Thus, the runner has
228
+ an API that matches what's expected by BuildBackendHookCaller.subprocess_runner.
229
+ """
230
+
231
+ def runner(
232
+ cmd: List[str],
233
+ cwd: Optional[str] = None,
234
+ extra_environ: Optional[Mapping[str, Any]] = None,
235
+ ) -> None:
236
+ with open_spinner(message) as spinner:
237
+ call_subprocess(
238
+ cmd,
239
+ command_desc=message,
240
+ cwd=cwd,
241
+ extra_environ=extra_environ,
242
+ spinner=spinner,
243
+ )
244
+
245
+ return runner
.venv/lib/python3.13/site-packages/pip/_internal/utils/temp_dir.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import errno
2
+ import itertools
3
+ import logging
4
+ import os.path
5
+ import tempfile
6
+ import traceback
7
+ from contextlib import ExitStack, contextmanager
8
+ from pathlib import Path
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Dict,
13
+ Generator,
14
+ List,
15
+ Optional,
16
+ TypeVar,
17
+ Union,
18
+ )
19
+
20
+ from pip._internal.utils.misc import enum, rmtree
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _T = TypeVar("_T", bound="TempDirectory")
25
+
26
+
27
+ # Kinds of temporary directories. Only needed for ones that are
28
+ # globally-managed.
29
+ tempdir_kinds = enum(
30
+ BUILD_ENV="build-env",
31
+ EPHEM_WHEEL_CACHE="ephem-wheel-cache",
32
+ REQ_BUILD="req-build",
33
+ )
34
+
35
+
36
+ _tempdir_manager: Optional[ExitStack] = None
37
+
38
+
39
+ @contextmanager
40
+ def global_tempdir_manager() -> Generator[None, None, None]:
41
+ global _tempdir_manager
42
+ with ExitStack() as stack:
43
+ old_tempdir_manager, _tempdir_manager = _tempdir_manager, stack
44
+ try:
45
+ yield
46
+ finally:
47
+ _tempdir_manager = old_tempdir_manager
48
+
49
+
50
+ class TempDirectoryTypeRegistry:
51
+ """Manages temp directory behavior"""
52
+
53
+ def __init__(self) -> None:
54
+ self._should_delete: Dict[str, bool] = {}
55
+
56
+ def set_delete(self, kind: str, value: bool) -> None:
57
+ """Indicate whether a TempDirectory of the given kind should be
58
+ auto-deleted.
59
+ """
60
+ self._should_delete[kind] = value
61
+
62
+ def get_delete(self, kind: str) -> bool:
63
+ """Get configured auto-delete flag for a given TempDirectory type,
64
+ default True.
65
+ """
66
+ return self._should_delete.get(kind, True)
67
+
68
+
69
+ _tempdir_registry: Optional[TempDirectoryTypeRegistry] = None
70
+
71
+
72
+ @contextmanager
73
+ def tempdir_registry() -> Generator[TempDirectoryTypeRegistry, None, None]:
74
+ """Provides a scoped global tempdir registry that can be used to dictate
75
+ whether directories should be deleted.
76
+ """
77
+ global _tempdir_registry
78
+ old_tempdir_registry = _tempdir_registry
79
+ _tempdir_registry = TempDirectoryTypeRegistry()
80
+ try:
81
+ yield _tempdir_registry
82
+ finally:
83
+ _tempdir_registry = old_tempdir_registry
84
+
85
+
86
+ class _Default:
87
+ pass
88
+
89
+
90
+ _default = _Default()
91
+
92
+
93
+ class TempDirectory:
94
+ """Helper class that owns and cleans up a temporary directory.
95
+
96
+ This class can be used as a context manager or as an OO representation of a
97
+ temporary directory.
98
+
99
+ Attributes:
100
+ path
101
+ Location to the created temporary directory
102
+ delete
103
+ Whether the directory should be deleted when exiting
104
+ (when used as a contextmanager)
105
+
106
+ Methods:
107
+ cleanup()
108
+ Deletes the temporary directory
109
+
110
+ When used as a context manager, if the delete attribute is True, on
111
+ exiting the context the temporary directory is deleted.
112
+ """
113
+
114
+ def __init__(
115
+ self,
116
+ path: Optional[str] = None,
117
+ delete: Union[bool, None, _Default] = _default,
118
+ kind: str = "temp",
119
+ globally_managed: bool = False,
120
+ ignore_cleanup_errors: bool = True,
121
+ ):
122
+ super().__init__()
123
+
124
+ if delete is _default:
125
+ if path is not None:
126
+ # If we were given an explicit directory, resolve delete option
127
+ # now.
128
+ delete = False
129
+ else:
130
+ # Otherwise, we wait until cleanup and see what
131
+ # tempdir_registry says.
132
+ delete = None
133
+
134
+ # The only time we specify path is in for editables where it
135
+ # is the value of the --src option.
136
+ if path is None:
137
+ path = self._create(kind)
138
+
139
+ self._path = path
140
+ self._deleted = False
141
+ self.delete = delete
142
+ self.kind = kind
143
+ self.ignore_cleanup_errors = ignore_cleanup_errors
144
+
145
+ if globally_managed:
146
+ assert _tempdir_manager is not None
147
+ _tempdir_manager.enter_context(self)
148
+
149
+ @property
150
+ def path(self) -> str:
151
+ assert not self._deleted, f"Attempted to access deleted path: {self._path}"
152
+ return self._path
153
+
154
+ def __repr__(self) -> str:
155
+ return f"<{self.__class__.__name__} {self.path!r}>"
156
+
157
+ def __enter__(self: _T) -> _T:
158
+ return self
159
+
160
+ def __exit__(self, exc: Any, value: Any, tb: Any) -> None:
161
+ if self.delete is not None:
162
+ delete = self.delete
163
+ elif _tempdir_registry:
164
+ delete = _tempdir_registry.get_delete(self.kind)
165
+ else:
166
+ delete = True
167
+
168
+ if delete:
169
+ self.cleanup()
170
+
171
+ def _create(self, kind: str) -> str:
172
+ """Create a temporary directory and store its path in self.path"""
173
+ # We realpath here because some systems have their default tmpdir
174
+ # symlinked to another directory. This tends to confuse build
175
+ # scripts, so we canonicalize the path by traversing potential
176
+ # symlinks here.
177
+ path = os.path.realpath(tempfile.mkdtemp(prefix=f"pip-{kind}-"))
178
+ logger.debug("Created temporary directory: %s", path)
179
+ return path
180
+
181
+ def cleanup(self) -> None:
182
+ """Remove the temporary directory created and reset state"""
183
+ self._deleted = True
184
+ if not os.path.exists(self._path):
185
+ return
186
+
187
+ errors: List[BaseException] = []
188
+
189
+ def onerror(
190
+ func: Callable[..., Any],
191
+ path: Path,
192
+ exc_val: BaseException,
193
+ ) -> None:
194
+ """Log a warning for a `rmtree` error and continue"""
195
+ formatted_exc = "\n".join(
196
+ traceback.format_exception_only(type(exc_val), exc_val)
197
+ )
198
+ formatted_exc = formatted_exc.rstrip() # remove trailing new line
199
+ if func in (os.unlink, os.remove, os.rmdir):
200
+ logger.debug(
201
+ "Failed to remove a temporary file '%s' due to %s.\n",
202
+ path,
203
+ formatted_exc,
204
+ )
205
+ else:
206
+ logger.debug("%s failed with %s.", func.__qualname__, formatted_exc)
207
+ errors.append(exc_val)
208
+
209
+ if self.ignore_cleanup_errors:
210
+ try:
211
+ # first try with @retry; retrying to handle ephemeral errors
212
+ rmtree(self._path, ignore_errors=False)
213
+ except OSError:
214
+ # last pass ignore/log all errors
215
+ rmtree(self._path, onexc=onerror)
216
+ if errors:
217
+ logger.warning(
218
+ "Failed to remove contents in a temporary directory '%s'.\n"
219
+ "You can safely remove it manually.",
220
+ self._path,
221
+ )
222
+ else:
223
+ rmtree(self._path)
224
+
225
+
226
+ class AdjacentTempDirectory(TempDirectory):
227
+ """Helper class that creates a temporary directory adjacent to a real one.
228
+
229
+ Attributes:
230
+ original
231
+ The original directory to create a temp directory for.
232
+ path
233
+ After calling create() or entering, contains the full
234
+ path to the temporary directory.
235
+ delete
236
+ Whether the directory should be deleted when exiting
237
+ (when used as a contextmanager)
238
+
239
+ """
240
+
241
+ # The characters that may be used to name the temp directory
242
+ # We always prepend a ~ and then rotate through these until
243
+ # a usable name is found.
244
+ # pkg_resources raises a different error for .dist-info folder
245
+ # with leading '-' and invalid metadata
246
+ LEADING_CHARS = "-~.=%0123456789"
247
+
248
+ def __init__(self, original: str, delete: Optional[bool] = None) -> None:
249
+ self.original = original.rstrip("/\\")
250
+ super().__init__(delete=delete)
251
+
252
+ @classmethod
253
+ def _generate_names(cls, name: str) -> Generator[str, None, None]:
254
+ """Generates a series of temporary names.
255
+
256
+ The algorithm replaces the leading characters in the name
257
+ with ones that are valid filesystem characters, but are not
258
+ valid package names (for both Python and pip definitions of
259
+ package).
260
+ """
261
+ for i in range(1, len(name)):
262
+ for candidate in itertools.combinations_with_replacement(
263
+ cls.LEADING_CHARS, i - 1
264
+ ):
265
+ new_name = "~" + "".join(candidate) + name[i:]
266
+ if new_name != name:
267
+ yield new_name
268
+
269
+ # If we make it this far, we will have to make a longer name
270
+ for i in range(len(cls.LEADING_CHARS)):
271
+ for candidate in itertools.combinations_with_replacement(
272
+ cls.LEADING_CHARS, i
273
+ ):
274
+ new_name = "~" + "".join(candidate) + name
275
+ if new_name != name:
276
+ yield new_name
277
+
278
+ def _create(self, kind: str) -> str:
279
+ root, name = os.path.split(self.original)
280
+ for candidate in self._generate_names(name):
281
+ path = os.path.join(root, candidate)
282
+ try:
283
+ os.mkdir(path)
284
+ except OSError as ex:
285
+ # Continue if the name exists already
286
+ if ex.errno != errno.EEXIST:
287
+ raise
288
+ else:
289
+ path = os.path.realpath(path)
290
+ break
291
+ else:
292
+ # Final fallback on the default behavior.
293
+ path = os.path.realpath(tempfile.mkdtemp(prefix=f"pip-{kind}-"))
294
+
295
+ logger.debug("Created temporary directory: %s", path)
296
+ return path
.venv/lib/python3.13/site-packages/pip/_internal/utils/unpacking.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utilities related archives."""
2
+
3
+ import logging
4
+ import os
5
+ import shutil
6
+ import stat
7
+ import sys
8
+ import tarfile
9
+ import zipfile
10
+ from typing import Iterable, List, Optional
11
+ from zipfile import ZipInfo
12
+
13
+ from pip._internal.exceptions import InstallationError
14
+ from pip._internal.utils.filetypes import (
15
+ BZ2_EXTENSIONS,
16
+ TAR_EXTENSIONS,
17
+ XZ_EXTENSIONS,
18
+ ZIP_EXTENSIONS,
19
+ )
20
+ from pip._internal.utils.misc import ensure_dir
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ SUPPORTED_EXTENSIONS = ZIP_EXTENSIONS + TAR_EXTENSIONS
26
+
27
+ try:
28
+ import bz2 # noqa
29
+
30
+ SUPPORTED_EXTENSIONS += BZ2_EXTENSIONS
31
+ except ImportError:
32
+ logger.debug("bz2 module is not available")
33
+
34
+ try:
35
+ # Only for Python 3.3+
36
+ import lzma # noqa
37
+
38
+ SUPPORTED_EXTENSIONS += XZ_EXTENSIONS
39
+ except ImportError:
40
+ logger.debug("lzma module is not available")
41
+
42
+
43
+ def current_umask() -> int:
44
+ """Get the current umask which involves having to set it temporarily."""
45
+ mask = os.umask(0)
46
+ os.umask(mask)
47
+ return mask
48
+
49
+
50
+ def split_leading_dir(path: str) -> List[str]:
51
+ path = path.lstrip("/").lstrip("\\")
52
+ if "/" in path and (
53
+ ("\\" in path and path.find("/") < path.find("\\")) or "\\" not in path
54
+ ):
55
+ return path.split("/", 1)
56
+ elif "\\" in path:
57
+ return path.split("\\", 1)
58
+ else:
59
+ return [path, ""]
60
+
61
+
62
+ def has_leading_dir(paths: Iterable[str]) -> bool:
63
+ """Returns true if all the paths have the same leading path name
64
+ (i.e., everything is in one subdirectory in an archive)"""
65
+ common_prefix = None
66
+ for path in paths:
67
+ prefix, rest = split_leading_dir(path)
68
+ if not prefix:
69
+ return False
70
+ elif common_prefix is None:
71
+ common_prefix = prefix
72
+ elif prefix != common_prefix:
73
+ return False
74
+ return True
75
+
76
+
77
+ def is_within_directory(directory: str, target: str) -> bool:
78
+ """
79
+ Return true if the absolute path of target is within the directory
80
+ """
81
+ abs_directory = os.path.abspath(directory)
82
+ abs_target = os.path.abspath(target)
83
+
84
+ prefix = os.path.commonprefix([abs_directory, abs_target])
85
+ return prefix == abs_directory
86
+
87
+
88
+ def _get_default_mode_plus_executable() -> int:
89
+ return 0o777 & ~current_umask() | 0o111
90
+
91
+
92
+ def set_extracted_file_to_default_mode_plus_executable(path: str) -> None:
93
+ """
94
+ Make file present at path have execute for user/group/world
95
+ (chmod +x) is no-op on windows per python docs
96
+ """
97
+ os.chmod(path, _get_default_mode_plus_executable())
98
+
99
+
100
+ def zip_item_is_executable(info: ZipInfo) -> bool:
101
+ mode = info.external_attr >> 16
102
+ # if mode and regular file and any execute permissions for
103
+ # user/group/world?
104
+ return bool(mode and stat.S_ISREG(mode) and mode & 0o111)
105
+
106
+
107
+ def unzip_file(filename: str, location: str, flatten: bool = True) -> None:
108
+ """
109
+ Unzip the file (with path `filename`) to the destination `location`. All
110
+ files are written based on system defaults and umask (i.e. permissions are
111
+ not preserved), except that regular file members with any execute
112
+ permissions (user, group, or world) have "chmod +x" applied after being
113
+ written. Note that for windows, any execute changes using os.chmod are
114
+ no-ops per the python docs.
115
+ """
116
+ ensure_dir(location)
117
+ zipfp = open(filename, "rb")
118
+ try:
119
+ zip = zipfile.ZipFile(zipfp, allowZip64=True)
120
+ leading = has_leading_dir(zip.namelist()) and flatten
121
+ for info in zip.infolist():
122
+ name = info.filename
123
+ fn = name
124
+ if leading:
125
+ fn = split_leading_dir(name)[1]
126
+ fn = os.path.join(location, fn)
127
+ dir = os.path.dirname(fn)
128
+ if not is_within_directory(location, fn):
129
+ message = (
130
+ "The zip file ({}) has a file ({}) trying to install "
131
+ "outside target directory ({})"
132
+ )
133
+ raise InstallationError(message.format(filename, fn, location))
134
+ if fn.endswith("/") or fn.endswith("\\"):
135
+ # A directory
136
+ ensure_dir(fn)
137
+ else:
138
+ ensure_dir(dir)
139
+ # Don't use read() to avoid allocating an arbitrarily large
140
+ # chunk of memory for the file's content
141
+ fp = zip.open(name)
142
+ try:
143
+ with open(fn, "wb") as destfp:
144
+ shutil.copyfileobj(fp, destfp)
145
+ finally:
146
+ fp.close()
147
+ if zip_item_is_executable(info):
148
+ set_extracted_file_to_default_mode_plus_executable(fn)
149
+ finally:
150
+ zipfp.close()
151
+
152
+
153
+ def untar_file(filename: str, location: str) -> None:
154
+ """
155
+ Untar the file (with path `filename`) to the destination `location`.
156
+ All files are written based on system defaults and umask (i.e. permissions
157
+ are not preserved), except that regular file members with any execute
158
+ permissions (user, group, or world) have "chmod +x" applied on top of the
159
+ default. Note that for windows, any execute changes using os.chmod are
160
+ no-ops per the python docs.
161
+ """
162
+ ensure_dir(location)
163
+ if filename.lower().endswith(".gz") or filename.lower().endswith(".tgz"):
164
+ mode = "r:gz"
165
+ elif filename.lower().endswith(BZ2_EXTENSIONS):
166
+ mode = "r:bz2"
167
+ elif filename.lower().endswith(XZ_EXTENSIONS):
168
+ mode = "r:xz"
169
+ elif filename.lower().endswith(".tar"):
170
+ mode = "r"
171
+ else:
172
+ logger.warning(
173
+ "Cannot determine compression type for file %s",
174
+ filename,
175
+ )
176
+ mode = "r:*"
177
+
178
+ tar = tarfile.open(filename, mode, encoding="utf-8") # type: ignore
179
+ try:
180
+ leading = has_leading_dir([member.name for member in tar.getmembers()])
181
+
182
+ # PEP 706 added `tarfile.data_filter`, and made some other changes to
183
+ # Python's tarfile module (see below). The features were backported to
184
+ # security releases.
185
+ try:
186
+ data_filter = tarfile.data_filter
187
+ except AttributeError:
188
+ _untar_without_filter(filename, location, tar, leading)
189
+ else:
190
+ default_mode_plus_executable = _get_default_mode_plus_executable()
191
+
192
+ if leading:
193
+ # Strip the leading directory from all files in the archive,
194
+ # including hardlink targets (which are relative to the
195
+ # unpack location).
196
+ for member in tar.getmembers():
197
+ name_lead, name_rest = split_leading_dir(member.name)
198
+ member.name = name_rest
199
+ if member.islnk():
200
+ lnk_lead, lnk_rest = split_leading_dir(member.linkname)
201
+ if lnk_lead == name_lead:
202
+ member.linkname = lnk_rest
203
+
204
+ def pip_filter(member: tarfile.TarInfo, path: str) -> tarfile.TarInfo:
205
+ orig_mode = member.mode
206
+ try:
207
+ try:
208
+ member = data_filter(member, location)
209
+ except tarfile.LinkOutsideDestinationError:
210
+ if sys.version_info[:3] in {
211
+ (3, 9, 17),
212
+ (3, 10, 12),
213
+ (3, 11, 4),
214
+ }:
215
+ # The tarfile filter in specific Python versions
216
+ # raises LinkOutsideDestinationError on valid input
217
+ # (https://github.com/python/cpython/issues/107845)
218
+ # Ignore the error there, but do use the
219
+ # more lax `tar_filter`
220
+ member = tarfile.tar_filter(member, location)
221
+ else:
222
+ raise
223
+ except tarfile.TarError as exc:
224
+ message = "Invalid member in the tar file {}: {}"
225
+ # Filter error messages mention the member name.
226
+ # No need to add it here.
227
+ raise InstallationError(
228
+ message.format(
229
+ filename,
230
+ exc,
231
+ )
232
+ )
233
+ if member.isfile() and orig_mode & 0o111:
234
+ member.mode = default_mode_plus_executable
235
+ else:
236
+ # See PEP 706 note above.
237
+ # The PEP changed this from `int` to `Optional[int]`,
238
+ # where None means "use the default". Mypy doesn't
239
+ # know this yet.
240
+ member.mode = None # type: ignore [assignment]
241
+ return member
242
+
243
+ tar.extractall(location, filter=pip_filter)
244
+
245
+ finally:
246
+ tar.close()
247
+
248
+
249
+ def _untar_without_filter(
250
+ filename: str,
251
+ location: str,
252
+ tar: tarfile.TarFile,
253
+ leading: bool,
254
+ ) -> None:
255
+ """Fallback for Python without tarfile.data_filter"""
256
+ for member in tar.getmembers():
257
+ fn = member.name
258
+ if leading:
259
+ fn = split_leading_dir(fn)[1]
260
+ path = os.path.join(location, fn)
261
+ if not is_within_directory(location, path):
262
+ message = (
263
+ "The tar file ({}) has a file ({}) trying to install "
264
+ "outside target directory ({})"
265
+ )
266
+ raise InstallationError(message.format(filename, path, location))
267
+ if member.isdir():
268
+ ensure_dir(path)
269
+ elif member.issym():
270
+ try:
271
+ tar._extract_member(member, path)
272
+ except Exception as exc:
273
+ # Some corrupt tar files seem to produce this
274
+ # (specifically bad symlinks)
275
+ logger.warning(
276
+ "In the tar file %s the member %s is invalid: %s",
277
+ filename,
278
+ member.name,
279
+ exc,
280
+ )
281
+ continue
282
+ else:
283
+ try:
284
+ fp = tar.extractfile(member)
285
+ except (KeyError, AttributeError) as exc:
286
+ # Some corrupt tar files seem to produce this
287
+ # (specifically bad symlinks)
288
+ logger.warning(
289
+ "In the tar file %s the member %s is invalid: %s",
290
+ filename,
291
+ member.name,
292
+ exc,
293
+ )
294
+ continue
295
+ ensure_dir(os.path.dirname(path))
296
+ assert fp is not None
297
+ with open(path, "wb") as destfp:
298
+ shutil.copyfileobj(fp, destfp)
299
+ fp.close()
300
+ # Update the timestamp (useful for cython compiled files)
301
+ tar.utime(member, path)
302
+ # member have any execute permissions for user/group/world?
303
+ if member.mode & 0o111:
304
+ set_extracted_file_to_default_mode_plus_executable(path)
305
+
306
+
307
+ def unpack_file(
308
+ filename: str,
309
+ location: str,
310
+ content_type: Optional[str] = None,
311
+ ) -> None:
312
+ filename = os.path.realpath(filename)
313
+ if (
314
+ content_type == "application/zip"
315
+ or filename.lower().endswith(ZIP_EXTENSIONS)
316
+ or zipfile.is_zipfile(filename)
317
+ ):
318
+ unzip_file(filename, location, flatten=not filename.endswith(".whl"))
319
+ elif (
320
+ content_type == "application/x-gzip"
321
+ or tarfile.is_tarfile(filename)
322
+ or filename.lower().endswith(TAR_EXTENSIONS + BZ2_EXTENSIONS + XZ_EXTENSIONS)
323
+ ):
324
+ untar_file(filename, location)
325
+ else:
326
+ # FIXME: handle?
327
+ # FIXME: magic signatures?
328
+ logger.critical(
329
+ "Cannot unpack file %s (downloaded from %s, content-type: %s); "
330
+ "cannot detect archive format",
331
+ filename,
332
+ location,
333
+ content_type,
334
+ )
335
+ raise InstallationError(f"Cannot determine archive format of {location}")
.venv/lib/python3.13/site-packages/pip/_internal/utils/urls.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import string
3
+ import urllib.parse
4
+ import urllib.request
5
+
6
+ from .compat import WINDOWS
7
+
8
+
9
+ def path_to_url(path: str) -> str:
10
+ """
11
+ Convert a path to a file: URL. The path will be made absolute and have
12
+ quoted path parts.
13
+ """
14
+ path = os.path.normpath(os.path.abspath(path))
15
+ url = urllib.parse.urljoin("file:", urllib.request.pathname2url(path))
16
+ return url
17
+
18
+
19
+ def url_to_path(url: str) -> str:
20
+ """
21
+ Convert a file: URL to a path.
22
+ """
23
+ assert url.startswith(
24
+ "file:"
25
+ ), f"You can only turn file: urls into filenames (not {url!r})"
26
+
27
+ _, netloc, path, _, _ = urllib.parse.urlsplit(url)
28
+
29
+ if not netloc or netloc == "localhost":
30
+ # According to RFC 8089, same as empty authority.
31
+ netloc = ""
32
+ elif WINDOWS:
33
+ # If we have a UNC path, prepend UNC share notation.
34
+ netloc = "\\\\" + netloc
35
+ else:
36
+ raise ValueError(
37
+ f"non-local file URIs are not supported on this platform: {url!r}"
38
+ )
39
+
40
+ path = urllib.request.url2pathname(netloc + path)
41
+
42
+ # On Windows, urlsplit parses the path as something like "/C:/Users/foo".
43
+ # This creates issues for path-related functions like io.open(), so we try
44
+ # to detect and strip the leading slash.
45
+ if (
46
+ WINDOWS
47
+ and not netloc # Not UNC.
48
+ and len(path) >= 3
49
+ and path[0] == "/" # Leading slash to strip.
50
+ and path[1] in string.ascii_letters # Drive letter.
51
+ and path[2:4] in (":", ":/") # Colon + end of string, or colon + absolute path.
52
+ ):
53
+ path = path[1:]
54
+
55
+ return path
.venv/lib/python3.13/site-packages/pip/_internal/utils/virtualenv.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import re
4
+ import site
5
+ import sys
6
+ from typing import List, Optional
7
+
8
+ logger = logging.getLogger(__name__)
9
+ _INCLUDE_SYSTEM_SITE_PACKAGES_REGEX = re.compile(
10
+ r"include-system-site-packages\s*=\s*(?P<value>true|false)"
11
+ )
12
+
13
+
14
+ def _running_under_venv() -> bool:
15
+ """Checks if sys.base_prefix and sys.prefix match.
16
+
17
+ This handles PEP 405 compliant virtual environments.
18
+ """
19
+ return sys.prefix != getattr(sys, "base_prefix", sys.prefix)
20
+
21
+
22
+ def _running_under_legacy_virtualenv() -> bool:
23
+ """Checks if sys.real_prefix is set.
24
+
25
+ This handles virtual environments created with pypa's virtualenv.
26
+ """
27
+ # pypa/virtualenv case
28
+ return hasattr(sys, "real_prefix")
29
+
30
+
31
+ def running_under_virtualenv() -> bool:
32
+ """True if we're running inside a virtual environment, False otherwise."""
33
+ return _running_under_venv() or _running_under_legacy_virtualenv()
34
+
35
+
36
+ def _get_pyvenv_cfg_lines() -> Optional[List[str]]:
37
+ """Reads {sys.prefix}/pyvenv.cfg and returns its contents as list of lines
38
+
39
+ Returns None, if it could not read/access the file.
40
+ """
41
+ pyvenv_cfg_file = os.path.join(sys.prefix, "pyvenv.cfg")
42
+ try:
43
+ # Although PEP 405 does not specify, the built-in venv module always
44
+ # writes with UTF-8. (pypa/pip#8717)
45
+ with open(pyvenv_cfg_file, encoding="utf-8") as f:
46
+ return f.read().splitlines() # avoids trailing newlines
47
+ except OSError:
48
+ return None
49
+
50
+
51
+ def _no_global_under_venv() -> bool:
52
+ """Check `{sys.prefix}/pyvenv.cfg` for system site-packages inclusion
53
+
54
+ PEP 405 specifies that when system site-packages are not supposed to be
55
+ visible from a virtual environment, `pyvenv.cfg` must contain the following
56
+ line:
57
+
58
+ include-system-site-packages = false
59
+
60
+ Additionally, log a warning if accessing the file fails.
61
+ """
62
+ cfg_lines = _get_pyvenv_cfg_lines()
63
+ if cfg_lines is None:
64
+ # We're not in a "sane" venv, so assume there is no system
65
+ # site-packages access (since that's PEP 405's default state).
66
+ logger.warning(
67
+ "Could not access 'pyvenv.cfg' despite a virtual environment "
68
+ "being active. Assuming global site-packages is not accessible "
69
+ "in this environment."
70
+ )
71
+ return True
72
+
73
+ for line in cfg_lines:
74
+ match = _INCLUDE_SYSTEM_SITE_PACKAGES_REGEX.match(line)
75
+ if match is not None and match.group("value") == "false":
76
+ return True
77
+ return False
78
+
79
+
80
+ def _no_global_under_legacy_virtualenv() -> bool:
81
+ """Check if "no-global-site-packages.txt" exists beside site.py
82
+
83
+ This mirrors logic in pypa/virtualenv for determining whether system
84
+ site-packages are visible in the virtual environment.
85
+ """
86
+ site_mod_dir = os.path.dirname(os.path.abspath(site.__file__))
87
+ no_global_site_packages_file = os.path.join(
88
+ site_mod_dir,
89
+ "no-global-site-packages.txt",
90
+ )
91
+ return os.path.exists(no_global_site_packages_file)
92
+
93
+
94
+ def virtualenv_no_global() -> bool:
95
+ """Returns a boolean, whether running in venv with no system site-packages."""
96
+ # PEP 405 compliance needs to be checked first since virtualenv >=20 would
97
+ # return True for both checks, but is only able to use the PEP 405 config.
98
+ if _running_under_venv():
99
+ return _no_global_under_venv()
100
+
101
+ if _running_under_legacy_virtualenv():
102
+ return _no_global_under_legacy_virtualenv()
103
+
104
+ return False
.venv/lib/python3.13/site-packages/pip/_internal/utils/wheel.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Support functions for working with wheel files."""
2
+
3
+ import logging
4
+ from email.message import Message
5
+ from email.parser import Parser
6
+ from typing import Tuple
7
+ from zipfile import BadZipFile, ZipFile
8
+
9
+ from pip._vendor.packaging.utils import canonicalize_name
10
+
11
+ from pip._internal.exceptions import UnsupportedWheel
12
+
13
+ VERSION_COMPATIBLE = (1, 0)
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def parse_wheel(wheel_zip: ZipFile, name: str) -> Tuple[str, Message]:
20
+ """Extract information from the provided wheel, ensuring it meets basic
21
+ standards.
22
+
23
+ Returns the name of the .dist-info directory and the parsed WHEEL metadata.
24
+ """
25
+ try:
26
+ info_dir = wheel_dist_info_dir(wheel_zip, name)
27
+ metadata = wheel_metadata(wheel_zip, info_dir)
28
+ version = wheel_version(metadata)
29
+ except UnsupportedWheel as e:
30
+ raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
31
+
32
+ check_compatibility(version, name)
33
+
34
+ return info_dir, metadata
35
+
36
+
37
+ def wheel_dist_info_dir(source: ZipFile, name: str) -> str:
38
+ """Returns the name of the contained .dist-info directory.
39
+
40
+ Raises AssertionError or UnsupportedWheel if not found, >1 found, or
41
+ it doesn't match the provided name.
42
+ """
43
+ # Zip file path separators must be /
44
+ subdirs = {p.split("/", 1)[0] for p in source.namelist()}
45
+
46
+ info_dirs = [s for s in subdirs if s.endswith(".dist-info")]
47
+
48
+ if not info_dirs:
49
+ raise UnsupportedWheel(".dist-info directory not found")
50
+
51
+ if len(info_dirs) > 1:
52
+ raise UnsupportedWheel(
53
+ "multiple .dist-info directories found: {}".format(", ".join(info_dirs))
54
+ )
55
+
56
+ info_dir = info_dirs[0]
57
+
58
+ info_dir_name = canonicalize_name(info_dir)
59
+ canonical_name = canonicalize_name(name)
60
+ if not info_dir_name.startswith(canonical_name):
61
+ raise UnsupportedWheel(
62
+ f".dist-info directory {info_dir!r} does not start with {canonical_name!r}"
63
+ )
64
+
65
+ return info_dir
66
+
67
+
68
+ def read_wheel_metadata_file(source: ZipFile, path: str) -> bytes:
69
+ try:
70
+ return source.read(path)
71
+ # BadZipFile for general corruption, KeyError for missing entry,
72
+ # and RuntimeError for password-protected files
73
+ except (BadZipFile, KeyError, RuntimeError) as e:
74
+ raise UnsupportedWheel(f"could not read {path!r} file: {e!r}")
75
+
76
+
77
+ def wheel_metadata(source: ZipFile, dist_info_dir: str) -> Message:
78
+ """Return the WHEEL metadata of an extracted wheel, if possible.
79
+ Otherwise, raise UnsupportedWheel.
80
+ """
81
+ path = f"{dist_info_dir}/WHEEL"
82
+ # Zip file path separators must be /
83
+ wheel_contents = read_wheel_metadata_file(source, path)
84
+
85
+ try:
86
+ wheel_text = wheel_contents.decode()
87
+ except UnicodeDecodeError as e:
88
+ raise UnsupportedWheel(f"error decoding {path!r}: {e!r}")
89
+
90
+ # FeedParser (used by Parser) does not raise any exceptions. The returned
91
+ # message may have .defects populated, but for backwards-compatibility we
92
+ # currently ignore them.
93
+ return Parser().parsestr(wheel_text)
94
+
95
+
96
+ def wheel_version(wheel_data: Message) -> Tuple[int, ...]:
97
+ """Given WHEEL metadata, return the parsed Wheel-Version.
98
+ Otherwise, raise UnsupportedWheel.
99
+ """
100
+ version_text = wheel_data["Wheel-Version"]
101
+ if version_text is None:
102
+ raise UnsupportedWheel("WHEEL is missing Wheel-Version")
103
+
104
+ version = version_text.strip()
105
+
106
+ try:
107
+ return tuple(map(int, version.split(".")))
108
+ except ValueError:
109
+ raise UnsupportedWheel(f"invalid Wheel-Version: {version!r}")
110
+
111
+
112
+ def check_compatibility(version: Tuple[int, ...], name: str) -> None:
113
+ """Raises errors or warns if called with an incompatible Wheel-Version.
114
+
115
+ pip should refuse to install a Wheel-Version that's a major series
116
+ ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when
117
+ installing a version only minor version ahead (e.g 1.2 > 1.1).
118
+
119
+ version: a 2-tuple representing a Wheel-Version (Major, Minor)
120
+ name: name of wheel or package to raise exception about
121
+
122
+ :raises UnsupportedWheel: when an incompatible Wheel-Version is given
123
+ """
124
+ if version[0] > VERSION_COMPATIBLE[0]:
125
+ raise UnsupportedWheel(
126
+ "{}'s Wheel-Version ({}) is not compatible with this version "
127
+ "of pip".format(name, ".".join(map(str, version)))
128
+ )
129
+ elif version > VERSION_COMPATIBLE:
130
+ logger.warning(
131
+ "Installing from a newer Wheel-Version (%s)",
132
+ ".".join(map(str, version)),
133
+ )
.venv/lib/python3.13/site-packages/pip/_internal/vcs/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Expose a limited set of classes and functions so callers outside of
2
+ # the vcs package don't need to import deeper than `pip._internal.vcs`.
3
+ # (The test directory may still need to import from a vcs sub-package.)
4
+ # Import all vcs modules to register each VCS in the VcsSupport object.
5
+ import pip._internal.vcs.bazaar
6
+ import pip._internal.vcs.git
7
+ import pip._internal.vcs.mercurial
8
+ import pip._internal.vcs.subversion # noqa: F401
9
+ from pip._internal.vcs.versioncontrol import ( # noqa: F401
10
+ RemoteNotFoundError,
11
+ RemoteNotValidError,
12
+ is_url,
13
+ make_vcs_requirement_url,
14
+ vcs,
15
+ )
.venv/lib/python3.13/site-packages/pip/_internal/vcs/bazaar.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import List, Optional, Tuple
3
+
4
+ from pip._internal.utils.misc import HiddenText, display_path
5
+ from pip._internal.utils.subprocess import make_command
6
+ from pip._internal.utils.urls import path_to_url
7
+ from pip._internal.vcs.versioncontrol import (
8
+ AuthInfo,
9
+ RemoteNotFoundError,
10
+ RevOptions,
11
+ VersionControl,
12
+ vcs,
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Bazaar(VersionControl):
19
+ name = "bzr"
20
+ dirname = ".bzr"
21
+ repo_name = "branch"
22
+ schemes = (
23
+ "bzr+http",
24
+ "bzr+https",
25
+ "bzr+ssh",
26
+ "bzr+sftp",
27
+ "bzr+ftp",
28
+ "bzr+lp",
29
+ "bzr+file",
30
+ )
31
+
32
+ @staticmethod
33
+ def get_base_rev_args(rev: str) -> List[str]:
34
+ return ["-r", rev]
35
+
36
+ def fetch_new(
37
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
38
+ ) -> None:
39
+ rev_display = rev_options.to_display()
40
+ logger.info(
41
+ "Checking out %s%s to %s",
42
+ url,
43
+ rev_display,
44
+ display_path(dest),
45
+ )
46
+ if verbosity <= 0:
47
+ flags = ["--quiet"]
48
+ elif verbosity == 1:
49
+ flags = []
50
+ else:
51
+ flags = [f"-{'v'*verbosity}"]
52
+ cmd_args = make_command(
53
+ "checkout", "--lightweight", *flags, rev_options.to_args(), url, dest
54
+ )
55
+ self.run_command(cmd_args)
56
+
57
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
58
+ self.run_command(make_command("switch", url), cwd=dest)
59
+
60
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
61
+ output = self.run_command(
62
+ make_command("info"), show_stdout=False, stdout_only=True, cwd=dest
63
+ )
64
+ if output.startswith("Standalone "):
65
+ # Older versions of pip used to create standalone branches.
66
+ # Convert the standalone branch to a checkout by calling "bzr bind".
67
+ cmd_args = make_command("bind", "-q", url)
68
+ self.run_command(cmd_args, cwd=dest)
69
+
70
+ cmd_args = make_command("update", "-q", rev_options.to_args())
71
+ self.run_command(cmd_args, cwd=dest)
72
+
73
+ @classmethod
74
+ def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:
75
+ # hotfix the URL scheme after removing bzr+ from bzr+ssh:// re-add it
76
+ url, rev, user_pass = super().get_url_rev_and_auth(url)
77
+ if url.startswith("ssh://"):
78
+ url = "bzr+" + url
79
+ return url, rev, user_pass
80
+
81
+ @classmethod
82
+ def get_remote_url(cls, location: str) -> str:
83
+ urls = cls.run_command(
84
+ ["info"], show_stdout=False, stdout_only=True, cwd=location
85
+ )
86
+ for line in urls.splitlines():
87
+ line = line.strip()
88
+ for x in ("checkout of branch: ", "parent branch: "):
89
+ if line.startswith(x):
90
+ repo = line.split(x)[1]
91
+ if cls._is_local_repository(repo):
92
+ return path_to_url(repo)
93
+ return repo
94
+ raise RemoteNotFoundError
95
+
96
+ @classmethod
97
+ def get_revision(cls, location: str) -> str:
98
+ revision = cls.run_command(
99
+ ["revno"],
100
+ show_stdout=False,
101
+ stdout_only=True,
102
+ cwd=location,
103
+ )
104
+ return revision.splitlines()[-1]
105
+
106
+ @classmethod
107
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
108
+ """Always assume the versions don't match"""
109
+ return False
110
+
111
+
112
+ vcs.register(Bazaar)
.venv/lib/python3.13/site-packages/pip/_internal/vcs/git.py ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os.path
3
+ import pathlib
4
+ import re
5
+ import urllib.parse
6
+ import urllib.request
7
+ from dataclasses import replace
8
+ from typing import Any, List, Optional, Tuple
9
+
10
+ from pip._internal.exceptions import BadCommand, InstallationError
11
+ from pip._internal.utils.misc import HiddenText, display_path, hide_url
12
+ from pip._internal.utils.subprocess import make_command
13
+ from pip._internal.vcs.versioncontrol import (
14
+ AuthInfo,
15
+ RemoteNotFoundError,
16
+ RemoteNotValidError,
17
+ RevOptions,
18
+ VersionControl,
19
+ find_path_to_project_root_from_repo_root,
20
+ vcs,
21
+ )
22
+
23
+ urlsplit = urllib.parse.urlsplit
24
+ urlunsplit = urllib.parse.urlunsplit
25
+
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ GIT_VERSION_REGEX = re.compile(
31
+ r"^git version " # Prefix.
32
+ r"(\d+)" # Major.
33
+ r"\.(\d+)" # Dot, minor.
34
+ r"(?:\.(\d+))?" # Optional dot, patch.
35
+ r".*$" # Suffix, including any pre- and post-release segments we don't care about.
36
+ )
37
+
38
+ HASH_REGEX = re.compile("^[a-fA-F0-9]{40}$")
39
+
40
+ # SCP (Secure copy protocol) shorthand. e.g. 'git@example.com:foo/bar.git'
41
+ SCP_REGEX = re.compile(
42
+ r"""^
43
+ # Optional user, e.g. 'git@'
44
+ (\w+@)?
45
+ # Server, e.g. 'github.com'.
46
+ ([^/:]+):
47
+ # The server-side path. e.g. 'user/project.git'. Must start with an
48
+ # alphanumeric character so as not to be confusable with a Windows paths
49
+ # like 'C:/foo/bar' or 'C:\foo\bar'.
50
+ (\w[^:]*)
51
+ $""",
52
+ re.VERBOSE,
53
+ )
54
+
55
+
56
+ def looks_like_hash(sha: str) -> bool:
57
+ return bool(HASH_REGEX.match(sha))
58
+
59
+
60
+ class Git(VersionControl):
61
+ name = "git"
62
+ dirname = ".git"
63
+ repo_name = "clone"
64
+ schemes = (
65
+ "git+http",
66
+ "git+https",
67
+ "git+ssh",
68
+ "git+git",
69
+ "git+file",
70
+ )
71
+ # Prevent the user's environment variables from interfering with pip:
72
+ # https://github.com/pypa/pip/issues/1130
73
+ unset_environ = ("GIT_DIR", "GIT_WORK_TREE")
74
+ default_arg_rev = "HEAD"
75
+
76
+ @staticmethod
77
+ def get_base_rev_args(rev: str) -> List[str]:
78
+ return [rev]
79
+
80
+ @classmethod
81
+ def run_command(cls, *args: Any, **kwargs: Any) -> str:
82
+ if os.environ.get("PIP_NO_INPUT"):
83
+ extra_environ = kwargs.get("extra_environ", {})
84
+ extra_environ["GIT_TERMINAL_PROMPT"] = "0"
85
+ extra_environ["GIT_SSH_COMMAND"] = "ssh -oBatchMode=yes"
86
+ kwargs["extra_environ"] = extra_environ
87
+ return super().run_command(*args, **kwargs)
88
+
89
+ def is_immutable_rev_checkout(self, url: str, dest: str) -> bool:
90
+ _, rev_options = self.get_url_rev_options(hide_url(url))
91
+ if not rev_options.rev:
92
+ return False
93
+ if not self.is_commit_id_equal(dest, rev_options.rev):
94
+ # the current commit is different from rev,
95
+ # which means rev was something else than a commit hash
96
+ return False
97
+ # return False in the rare case rev is both a commit hash
98
+ # and a tag or a branch; we don't want to cache in that case
99
+ # because that branch/tag could point to something else in the future
100
+ is_tag_or_branch = bool(self.get_revision_sha(dest, rev_options.rev)[0])
101
+ return not is_tag_or_branch
102
+
103
+ def get_git_version(self) -> Tuple[int, ...]:
104
+ version = self.run_command(
105
+ ["version"],
106
+ command_desc="git version",
107
+ show_stdout=False,
108
+ stdout_only=True,
109
+ )
110
+ match = GIT_VERSION_REGEX.match(version)
111
+ if not match:
112
+ logger.warning("Can't parse git version: %s", version)
113
+ return ()
114
+ return (int(match.group(1)), int(match.group(2)))
115
+
116
+ @classmethod
117
+ def get_current_branch(cls, location: str) -> Optional[str]:
118
+ """
119
+ Return the current branch, or None if HEAD isn't at a branch
120
+ (e.g. detached HEAD).
121
+ """
122
+ # git-symbolic-ref exits with empty stdout if "HEAD" is a detached
123
+ # HEAD rather than a symbolic ref. In addition, the -q causes the
124
+ # command to exit with status code 1 instead of 128 in this case
125
+ # and to suppress the message to stderr.
126
+ args = ["symbolic-ref", "-q", "HEAD"]
127
+ output = cls.run_command(
128
+ args,
129
+ extra_ok_returncodes=(1,),
130
+ show_stdout=False,
131
+ stdout_only=True,
132
+ cwd=location,
133
+ )
134
+ ref = output.strip()
135
+
136
+ if ref.startswith("refs/heads/"):
137
+ return ref[len("refs/heads/") :]
138
+
139
+ return None
140
+
141
+ @classmethod
142
+ def get_revision_sha(cls, dest: str, rev: str) -> Tuple[Optional[str], bool]:
143
+ """
144
+ Return (sha_or_none, is_branch), where sha_or_none is a commit hash
145
+ if the revision names a remote branch or tag, otherwise None.
146
+
147
+ Args:
148
+ dest: the repository directory.
149
+ rev: the revision name.
150
+ """
151
+ # Pass rev to pre-filter the list.
152
+ output = cls.run_command(
153
+ ["show-ref", rev],
154
+ cwd=dest,
155
+ show_stdout=False,
156
+ stdout_only=True,
157
+ on_returncode="ignore",
158
+ )
159
+ refs = {}
160
+ # NOTE: We do not use splitlines here since that would split on other
161
+ # unicode separators, which can be maliciously used to install a
162
+ # different revision.
163
+ for line in output.strip().split("\n"):
164
+ line = line.rstrip("\r")
165
+ if not line:
166
+ continue
167
+ try:
168
+ ref_sha, ref_name = line.split(" ", maxsplit=2)
169
+ except ValueError:
170
+ # Include the offending line to simplify troubleshooting if
171
+ # this error ever occurs.
172
+ raise ValueError(f"unexpected show-ref line: {line!r}")
173
+
174
+ refs[ref_name] = ref_sha
175
+
176
+ branch_ref = f"refs/remotes/origin/{rev}"
177
+ tag_ref = f"refs/tags/{rev}"
178
+
179
+ sha = refs.get(branch_ref)
180
+ if sha is not None:
181
+ return (sha, True)
182
+
183
+ sha = refs.get(tag_ref)
184
+
185
+ return (sha, False)
186
+
187
+ @classmethod
188
+ def _should_fetch(cls, dest: str, rev: str) -> bool:
189
+ """
190
+ Return true if rev is a ref or is a commit that we don't have locally.
191
+
192
+ Branches and tags are not considered in this method because they are
193
+ assumed to be always available locally (which is a normal outcome of
194
+ ``git clone`` and ``git fetch --tags``).
195
+ """
196
+ if rev.startswith("refs/"):
197
+ # Always fetch remote refs.
198
+ return True
199
+
200
+ if not looks_like_hash(rev):
201
+ # Git fetch would fail with abbreviated commits.
202
+ return False
203
+
204
+ if cls.has_commit(dest, rev):
205
+ # Don't fetch if we have the commit locally.
206
+ return False
207
+
208
+ return True
209
+
210
+ @classmethod
211
+ def resolve_revision(
212
+ cls, dest: str, url: HiddenText, rev_options: RevOptions
213
+ ) -> RevOptions:
214
+ """
215
+ Resolve a revision to a new RevOptions object with the SHA1 of the
216
+ branch, tag, or ref if found.
217
+
218
+ Args:
219
+ rev_options: a RevOptions object.
220
+ """
221
+ rev = rev_options.arg_rev
222
+ # The arg_rev property's implementation for Git ensures that the
223
+ # rev return value is always non-None.
224
+ assert rev is not None
225
+
226
+ sha, is_branch = cls.get_revision_sha(dest, rev)
227
+
228
+ if sha is not None:
229
+ rev_options = rev_options.make_new(sha)
230
+ rev_options = replace(rev_options, branch_name=(rev if is_branch else None))
231
+
232
+ return rev_options
233
+
234
+ # Do not show a warning for the common case of something that has
235
+ # the form of a Git commit hash.
236
+ if not looks_like_hash(rev):
237
+ logger.warning(
238
+ "Did not find branch or tag '%s', assuming revision or ref.",
239
+ rev,
240
+ )
241
+
242
+ if not cls._should_fetch(dest, rev):
243
+ return rev_options
244
+
245
+ # fetch the requested revision
246
+ cls.run_command(
247
+ make_command("fetch", "-q", url, rev_options.to_args()),
248
+ cwd=dest,
249
+ )
250
+ # Change the revision to the SHA of the ref we fetched
251
+ sha = cls.get_revision(dest, rev="FETCH_HEAD")
252
+ rev_options = rev_options.make_new(sha)
253
+
254
+ return rev_options
255
+
256
+ @classmethod
257
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
258
+ """
259
+ Return whether the current commit hash equals the given name.
260
+
261
+ Args:
262
+ dest: the repository directory.
263
+ name: a string name.
264
+ """
265
+ if not name:
266
+ # Then avoid an unnecessary subprocess call.
267
+ return False
268
+
269
+ return cls.get_revision(dest) == name
270
+
271
+ def fetch_new(
272
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
273
+ ) -> None:
274
+ rev_display = rev_options.to_display()
275
+ logger.info("Cloning %s%s to %s", url, rev_display, display_path(dest))
276
+ if verbosity <= 0:
277
+ flags: Tuple[str, ...] = ("--quiet",)
278
+ elif verbosity == 1:
279
+ flags = ()
280
+ else:
281
+ flags = ("--verbose", "--progress")
282
+ if self.get_git_version() >= (2, 17):
283
+ # Git added support for partial clone in 2.17
284
+ # https://git-scm.com/docs/partial-clone
285
+ # Speeds up cloning by functioning without a complete copy of repository
286
+ self.run_command(
287
+ make_command(
288
+ "clone",
289
+ "--filter=blob:none",
290
+ *flags,
291
+ url,
292
+ dest,
293
+ )
294
+ )
295
+ else:
296
+ self.run_command(make_command("clone", *flags, url, dest))
297
+
298
+ if rev_options.rev:
299
+ # Then a specific revision was requested.
300
+ rev_options = self.resolve_revision(dest, url, rev_options)
301
+ branch_name = getattr(rev_options, "branch_name", None)
302
+ logger.debug("Rev options %s, branch_name %s", rev_options, branch_name)
303
+ if branch_name is None:
304
+ # Only do a checkout if the current commit id doesn't match
305
+ # the requested revision.
306
+ if not self.is_commit_id_equal(dest, rev_options.rev):
307
+ cmd_args = make_command(
308
+ "checkout",
309
+ "-q",
310
+ rev_options.to_args(),
311
+ )
312
+ self.run_command(cmd_args, cwd=dest)
313
+ elif self.get_current_branch(dest) != branch_name:
314
+ # Then a specific branch was requested, and that branch
315
+ # is not yet checked out.
316
+ track_branch = f"origin/{branch_name}"
317
+ cmd_args = [
318
+ "checkout",
319
+ "-b",
320
+ branch_name,
321
+ "--track",
322
+ track_branch,
323
+ ]
324
+ self.run_command(cmd_args, cwd=dest)
325
+ else:
326
+ sha = self.get_revision(dest)
327
+ rev_options = rev_options.make_new(sha)
328
+
329
+ logger.info("Resolved %s to commit %s", url, rev_options.rev)
330
+
331
+ #: repo may contain submodules
332
+ self.update_submodules(dest)
333
+
334
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
335
+ self.run_command(
336
+ make_command("config", "remote.origin.url", url),
337
+ cwd=dest,
338
+ )
339
+ cmd_args = make_command("checkout", "-q", rev_options.to_args())
340
+ self.run_command(cmd_args, cwd=dest)
341
+
342
+ self.update_submodules(dest)
343
+
344
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
345
+ # First fetch changes from the default remote
346
+ if self.get_git_version() >= (1, 9):
347
+ # fetch tags in addition to everything else
348
+ self.run_command(["fetch", "-q", "--tags"], cwd=dest)
349
+ else:
350
+ self.run_command(["fetch", "-q"], cwd=dest)
351
+ # Then reset to wanted revision (maybe even origin/master)
352
+ rev_options = self.resolve_revision(dest, url, rev_options)
353
+ cmd_args = make_command("reset", "--hard", "-q", rev_options.to_args())
354
+ self.run_command(cmd_args, cwd=dest)
355
+ #: update submodules
356
+ self.update_submodules(dest)
357
+
358
+ @classmethod
359
+ def get_remote_url(cls, location: str) -> str:
360
+ """
361
+ Return URL of the first remote encountered.
362
+
363
+ Raises RemoteNotFoundError if the repository does not have a remote
364
+ url configured.
365
+ """
366
+ # We need to pass 1 for extra_ok_returncodes since the command
367
+ # exits with return code 1 if there are no matching lines.
368
+ stdout = cls.run_command(
369
+ ["config", "--get-regexp", r"remote\..*\.url"],
370
+ extra_ok_returncodes=(1,),
371
+ show_stdout=False,
372
+ stdout_only=True,
373
+ cwd=location,
374
+ )
375
+ remotes = stdout.splitlines()
376
+ try:
377
+ found_remote = remotes[0]
378
+ except IndexError:
379
+ raise RemoteNotFoundError
380
+
381
+ for remote in remotes:
382
+ if remote.startswith("remote.origin.url "):
383
+ found_remote = remote
384
+ break
385
+ url = found_remote.split(" ")[1]
386
+ return cls._git_remote_to_pip_url(url.strip())
387
+
388
+ @staticmethod
389
+ def _git_remote_to_pip_url(url: str) -> str:
390
+ """
391
+ Convert a remote url from what git uses to what pip accepts.
392
+
393
+ There are 3 legal forms **url** may take:
394
+
395
+ 1. A fully qualified url: ssh://git@example.com/foo/bar.git
396
+ 2. A local project.git folder: /path/to/bare/repository.git
397
+ 3. SCP shorthand for form 1: git@example.com:foo/bar.git
398
+
399
+ Form 1 is output as-is. Form 2 must be converted to URI and form 3 must
400
+ be converted to form 1.
401
+
402
+ See the corresponding test test_git_remote_url_to_pip() for examples of
403
+ sample inputs/outputs.
404
+ """
405
+ if re.match(r"\w+://", url):
406
+ # This is already valid. Pass it though as-is.
407
+ return url
408
+ if os.path.exists(url):
409
+ # A local bare remote (git clone --mirror).
410
+ # Needs a file:// prefix.
411
+ return pathlib.PurePath(url).as_uri()
412
+ scp_match = SCP_REGEX.match(url)
413
+ if scp_match:
414
+ # Add an ssh:// prefix and replace the ':' with a '/'.
415
+ return scp_match.expand(r"ssh://\1\2/\3")
416
+ # Otherwise, bail out.
417
+ raise RemoteNotValidError(url)
418
+
419
+ @classmethod
420
+ def has_commit(cls, location: str, rev: str) -> bool:
421
+ """
422
+ Check if rev is a commit that is available in the local repository.
423
+ """
424
+ try:
425
+ cls.run_command(
426
+ ["rev-parse", "-q", "--verify", "sha^" + rev],
427
+ cwd=location,
428
+ log_failed_cmd=False,
429
+ )
430
+ except InstallationError:
431
+ return False
432
+ else:
433
+ return True
434
+
435
+ @classmethod
436
+ def get_revision(cls, location: str, rev: Optional[str] = None) -> str:
437
+ if rev is None:
438
+ rev = "HEAD"
439
+ current_rev = cls.run_command(
440
+ ["rev-parse", rev],
441
+ show_stdout=False,
442
+ stdout_only=True,
443
+ cwd=location,
444
+ )
445
+ return current_rev.strip()
446
+
447
+ @classmethod
448
+ def get_subdirectory(cls, location: str) -> Optional[str]:
449
+ """
450
+ Return the path to Python project root, relative to the repo root.
451
+ Return None if the project root is in the repo root.
452
+ """
453
+ # find the repo root
454
+ git_dir = cls.run_command(
455
+ ["rev-parse", "--git-dir"],
456
+ show_stdout=False,
457
+ stdout_only=True,
458
+ cwd=location,
459
+ ).strip()
460
+ if not os.path.isabs(git_dir):
461
+ git_dir = os.path.join(location, git_dir)
462
+ repo_root = os.path.abspath(os.path.join(git_dir, ".."))
463
+ return find_path_to_project_root_from_repo_root(location, repo_root)
464
+
465
+ @classmethod
466
+ def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:
467
+ """
468
+ Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'.
469
+ That's required because although they use SSH they sometimes don't
470
+ work with a ssh:// scheme (e.g. GitHub). But we need a scheme for
471
+ parsing. Hence we remove it again afterwards and return it as a stub.
472
+ """
473
+ # Works around an apparent Git bug
474
+ # (see https://article.gmane.org/gmane.comp.version-control.git/146500)
475
+ scheme, netloc, path, query, fragment = urlsplit(url)
476
+ if scheme.endswith("file"):
477
+ initial_slashes = path[: -len(path.lstrip("/"))]
478
+ newpath = initial_slashes + urllib.request.url2pathname(path).replace(
479
+ "\\", "/"
480
+ ).lstrip("/")
481
+ after_plus = scheme.find("+") + 1
482
+ url = scheme[:after_plus] + urlunsplit(
483
+ (scheme[after_plus:], netloc, newpath, query, fragment),
484
+ )
485
+
486
+ if "://" not in url:
487
+ assert "file:" not in url
488
+ url = url.replace("git+", "git+ssh://")
489
+ url, rev, user_pass = super().get_url_rev_and_auth(url)
490
+ url = url.replace("ssh://", "")
491
+ else:
492
+ url, rev, user_pass = super().get_url_rev_and_auth(url)
493
+
494
+ return url, rev, user_pass
495
+
496
+ @classmethod
497
+ def update_submodules(cls, location: str) -> None:
498
+ if not os.path.exists(os.path.join(location, ".gitmodules")):
499
+ return
500
+ cls.run_command(
501
+ ["submodule", "update", "--init", "--recursive", "-q"],
502
+ cwd=location,
503
+ )
504
+
505
+ @classmethod
506
+ def get_repository_root(cls, location: str) -> Optional[str]:
507
+ loc = super().get_repository_root(location)
508
+ if loc:
509
+ return loc
510
+ try:
511
+ r = cls.run_command(
512
+ ["rev-parse", "--show-toplevel"],
513
+ cwd=location,
514
+ show_stdout=False,
515
+ stdout_only=True,
516
+ on_returncode="raise",
517
+ log_failed_cmd=False,
518
+ )
519
+ except BadCommand:
520
+ logger.debug(
521
+ "could not determine if %s is under git control "
522
+ "because git is not available",
523
+ location,
524
+ )
525
+ return None
526
+ except InstallationError:
527
+ return None
528
+ return os.path.normpath(r.rstrip("\r\n"))
529
+
530
+ @staticmethod
531
+ def should_add_vcs_url_prefix(repo_url: str) -> bool:
532
+ """In either https or ssh form, requirements must be prefixed with git+."""
533
+ return True
534
+
535
+
536
+ vcs.register(Git)
.venv/lib/python3.13/site-packages/pip/_internal/vcs/mercurial.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import configparser
2
+ import logging
3
+ import os
4
+ from typing import List, Optional, Tuple
5
+
6
+ from pip._internal.exceptions import BadCommand, InstallationError
7
+ from pip._internal.utils.misc import HiddenText, display_path
8
+ from pip._internal.utils.subprocess import make_command
9
+ from pip._internal.utils.urls import path_to_url
10
+ from pip._internal.vcs.versioncontrol import (
11
+ RevOptions,
12
+ VersionControl,
13
+ find_path_to_project_root_from_repo_root,
14
+ vcs,
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class Mercurial(VersionControl):
21
+ name = "hg"
22
+ dirname = ".hg"
23
+ repo_name = "clone"
24
+ schemes = (
25
+ "hg+file",
26
+ "hg+http",
27
+ "hg+https",
28
+ "hg+ssh",
29
+ "hg+static-http",
30
+ )
31
+
32
+ @staticmethod
33
+ def get_base_rev_args(rev: str) -> List[str]:
34
+ return [f"--rev={rev}"]
35
+
36
+ def fetch_new(
37
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
38
+ ) -> None:
39
+ rev_display = rev_options.to_display()
40
+ logger.info(
41
+ "Cloning hg %s%s to %s",
42
+ url,
43
+ rev_display,
44
+ display_path(dest),
45
+ )
46
+ if verbosity <= 0:
47
+ flags: Tuple[str, ...] = ("--quiet",)
48
+ elif verbosity == 1:
49
+ flags = ()
50
+ elif verbosity == 2:
51
+ flags = ("--verbose",)
52
+ else:
53
+ flags = ("--verbose", "--debug")
54
+ self.run_command(make_command("clone", "--noupdate", *flags, url, dest))
55
+ self.run_command(
56
+ make_command("update", *flags, rev_options.to_args()),
57
+ cwd=dest,
58
+ )
59
+
60
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
61
+ repo_config = os.path.join(dest, self.dirname, "hgrc")
62
+ config = configparser.RawConfigParser()
63
+ try:
64
+ config.read(repo_config)
65
+ config.set("paths", "default", url.secret)
66
+ with open(repo_config, "w") as config_file:
67
+ config.write(config_file)
68
+ except (OSError, configparser.NoSectionError) as exc:
69
+ logger.warning("Could not switch Mercurial repository to %s: %s", url, exc)
70
+ else:
71
+ cmd_args = make_command("update", "-q", rev_options.to_args())
72
+ self.run_command(cmd_args, cwd=dest)
73
+
74
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
75
+ self.run_command(["pull", "-q"], cwd=dest)
76
+ cmd_args = make_command("update", "-q", rev_options.to_args())
77
+ self.run_command(cmd_args, cwd=dest)
78
+
79
+ @classmethod
80
+ def get_remote_url(cls, location: str) -> str:
81
+ url = cls.run_command(
82
+ ["showconfig", "paths.default"],
83
+ show_stdout=False,
84
+ stdout_only=True,
85
+ cwd=location,
86
+ ).strip()
87
+ if cls._is_local_repository(url):
88
+ url = path_to_url(url)
89
+ return url.strip()
90
+
91
+ @classmethod
92
+ def get_revision(cls, location: str) -> str:
93
+ """
94
+ Return the repository-local changeset revision number, as an integer.
95
+ """
96
+ current_revision = cls.run_command(
97
+ ["parents", "--template={rev}"],
98
+ show_stdout=False,
99
+ stdout_only=True,
100
+ cwd=location,
101
+ ).strip()
102
+ return current_revision
103
+
104
+ @classmethod
105
+ def get_requirement_revision(cls, location: str) -> str:
106
+ """
107
+ Return the changeset identification hash, as a 40-character
108
+ hexadecimal string
109
+ """
110
+ current_rev_hash = cls.run_command(
111
+ ["parents", "--template={node}"],
112
+ show_stdout=False,
113
+ stdout_only=True,
114
+ cwd=location,
115
+ ).strip()
116
+ return current_rev_hash
117
+
118
+ @classmethod
119
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
120
+ """Always assume the versions don't match"""
121
+ return False
122
+
123
+ @classmethod
124
+ def get_subdirectory(cls, location: str) -> Optional[str]:
125
+ """
126
+ Return the path to Python project root, relative to the repo root.
127
+ Return None if the project root is in the repo root.
128
+ """
129
+ # find the repo root
130
+ repo_root = cls.run_command(
131
+ ["root"], show_stdout=False, stdout_only=True, cwd=location
132
+ ).strip()
133
+ if not os.path.isabs(repo_root):
134
+ repo_root = os.path.abspath(os.path.join(location, repo_root))
135
+ return find_path_to_project_root_from_repo_root(location, repo_root)
136
+
137
+ @classmethod
138
+ def get_repository_root(cls, location: str) -> Optional[str]:
139
+ loc = super().get_repository_root(location)
140
+ if loc:
141
+ return loc
142
+ try:
143
+ r = cls.run_command(
144
+ ["root"],
145
+ cwd=location,
146
+ show_stdout=False,
147
+ stdout_only=True,
148
+ on_returncode="raise",
149
+ log_failed_cmd=False,
150
+ )
151
+ except BadCommand:
152
+ logger.debug(
153
+ "could not determine if %s is under hg control "
154
+ "because hg is not available",
155
+ location,
156
+ )
157
+ return None
158
+ except InstallationError:
159
+ return None
160
+ return os.path.normpath(r.rstrip("\r\n"))
161
+
162
+
163
+ vcs.register(Mercurial)
.venv/lib/python3.13/site-packages/pip/_internal/vcs/subversion.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import re
4
+ from typing import List, Optional, Tuple
5
+
6
+ from pip._internal.utils.misc import (
7
+ HiddenText,
8
+ display_path,
9
+ is_console_interactive,
10
+ is_installable_dir,
11
+ split_auth_from_netloc,
12
+ )
13
+ from pip._internal.utils.subprocess import CommandArgs, make_command
14
+ from pip._internal.vcs.versioncontrol import (
15
+ AuthInfo,
16
+ RemoteNotFoundError,
17
+ RevOptions,
18
+ VersionControl,
19
+ vcs,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _svn_xml_url_re = re.compile('url="([^"]+)"')
25
+ _svn_rev_re = re.compile(r'committed-rev="(\d+)"')
26
+ _svn_info_xml_rev_re = re.compile(r'\s*revision="(\d+)"')
27
+ _svn_info_xml_url_re = re.compile(r"<url>(.*)</url>")
28
+
29
+
30
+ class Subversion(VersionControl):
31
+ name = "svn"
32
+ dirname = ".svn"
33
+ repo_name = "checkout"
34
+ schemes = ("svn+ssh", "svn+http", "svn+https", "svn+svn", "svn+file")
35
+
36
+ @classmethod
37
+ def should_add_vcs_url_prefix(cls, remote_url: str) -> bool:
38
+ return True
39
+
40
+ @staticmethod
41
+ def get_base_rev_args(rev: str) -> List[str]:
42
+ return ["-r", rev]
43
+
44
+ @classmethod
45
+ def get_revision(cls, location: str) -> str:
46
+ """
47
+ Return the maximum revision for all files under a given location
48
+ """
49
+ # Note: taken from setuptools.command.egg_info
50
+ revision = 0
51
+
52
+ for base, dirs, _ in os.walk(location):
53
+ if cls.dirname not in dirs:
54
+ dirs[:] = []
55
+ continue # no sense walking uncontrolled subdirs
56
+ dirs.remove(cls.dirname)
57
+ entries_fn = os.path.join(base, cls.dirname, "entries")
58
+ if not os.path.exists(entries_fn):
59
+ # FIXME: should we warn?
60
+ continue
61
+
62
+ dirurl, localrev = cls._get_svn_url_rev(base)
63
+
64
+ if base == location:
65
+ assert dirurl is not None
66
+ base = dirurl + "/" # save the root url
67
+ elif not dirurl or not dirurl.startswith(base):
68
+ dirs[:] = []
69
+ continue # not part of the same svn tree, skip it
70
+ revision = max(revision, localrev)
71
+ return str(revision)
72
+
73
+ @classmethod
74
+ def get_netloc_and_auth(
75
+ cls, netloc: str, scheme: str
76
+ ) -> Tuple[str, Tuple[Optional[str], Optional[str]]]:
77
+ """
78
+ This override allows the auth information to be passed to svn via the
79
+ --username and --password options instead of via the URL.
80
+ """
81
+ if scheme == "ssh":
82
+ # The --username and --password options can't be used for
83
+ # svn+ssh URLs, so keep the auth information in the URL.
84
+ return super().get_netloc_and_auth(netloc, scheme)
85
+
86
+ return split_auth_from_netloc(netloc)
87
+
88
+ @classmethod
89
+ def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:
90
+ # hotfix the URL scheme after removing svn+ from svn+ssh:// re-add it
91
+ url, rev, user_pass = super().get_url_rev_and_auth(url)
92
+ if url.startswith("ssh://"):
93
+ url = "svn+" + url
94
+ return url, rev, user_pass
95
+
96
+ @staticmethod
97
+ def make_rev_args(
98
+ username: Optional[str], password: Optional[HiddenText]
99
+ ) -> CommandArgs:
100
+ extra_args: CommandArgs = []
101
+ if username:
102
+ extra_args += ["--username", username]
103
+ if password:
104
+ extra_args += ["--password", password]
105
+
106
+ return extra_args
107
+
108
+ @classmethod
109
+ def get_remote_url(cls, location: str) -> str:
110
+ # In cases where the source is in a subdirectory, we have to look up in
111
+ # the location until we find a valid project root.
112
+ orig_location = location
113
+ while not is_installable_dir(location):
114
+ last_location = location
115
+ location = os.path.dirname(location)
116
+ if location == last_location:
117
+ # We've traversed up to the root of the filesystem without
118
+ # finding a Python project.
119
+ logger.warning(
120
+ "Could not find Python project for directory %s (tried all "
121
+ "parent directories)",
122
+ orig_location,
123
+ )
124
+ raise RemoteNotFoundError
125
+
126
+ url, _rev = cls._get_svn_url_rev(location)
127
+ if url is None:
128
+ raise RemoteNotFoundError
129
+
130
+ return url
131
+
132
+ @classmethod
133
+ def _get_svn_url_rev(cls, location: str) -> Tuple[Optional[str], int]:
134
+ from pip._internal.exceptions import InstallationError
135
+
136
+ entries_path = os.path.join(location, cls.dirname, "entries")
137
+ if os.path.exists(entries_path):
138
+ with open(entries_path) as f:
139
+ data = f.read()
140
+ else: # subversion >= 1.7 does not have the 'entries' file
141
+ data = ""
142
+
143
+ url = None
144
+ if data.startswith("8") or data.startswith("9") or data.startswith("10"):
145
+ entries = list(map(str.splitlines, data.split("\n\x0c\n")))
146
+ del entries[0][0] # get rid of the '8'
147
+ url = entries[0][3]
148
+ revs = [int(d[9]) for d in entries if len(d) > 9 and d[9]] + [0]
149
+ elif data.startswith("<?xml"):
150
+ match = _svn_xml_url_re.search(data)
151
+ if not match:
152
+ raise ValueError(f"Badly formatted data: {data!r}")
153
+ url = match.group(1) # get repository URL
154
+ revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)] + [0]
155
+ else:
156
+ try:
157
+ # subversion >= 1.7
158
+ # Note that using get_remote_call_options is not necessary here
159
+ # because `svn info` is being run against a local directory.
160
+ # We don't need to worry about making sure interactive mode
161
+ # is being used to prompt for passwords, because passwords
162
+ # are only potentially needed for remote server requests.
163
+ xml = cls.run_command(
164
+ ["info", "--xml", location],
165
+ show_stdout=False,
166
+ stdout_only=True,
167
+ )
168
+ match = _svn_info_xml_url_re.search(xml)
169
+ assert match is not None
170
+ url = match.group(1)
171
+ revs = [int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml)]
172
+ except InstallationError:
173
+ url, revs = None, []
174
+
175
+ if revs:
176
+ rev = max(revs)
177
+ else:
178
+ rev = 0
179
+
180
+ return url, rev
181
+
182
+ @classmethod
183
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
184
+ """Always assume the versions don't match"""
185
+ return False
186
+
187
+ def __init__(self, use_interactive: Optional[bool] = None) -> None:
188
+ if use_interactive is None:
189
+ use_interactive = is_console_interactive()
190
+ self.use_interactive = use_interactive
191
+
192
+ # This member is used to cache the fetched version of the current
193
+ # ``svn`` client.
194
+ # Special value definitions:
195
+ # None: Not evaluated yet.
196
+ # Empty tuple: Could not parse version.
197
+ self._vcs_version: Optional[Tuple[int, ...]] = None
198
+
199
+ super().__init__()
200
+
201
+ def call_vcs_version(self) -> Tuple[int, ...]:
202
+ """Query the version of the currently installed Subversion client.
203
+
204
+ :return: A tuple containing the parts of the version information or
205
+ ``()`` if the version returned from ``svn`` could not be parsed.
206
+ :raises: BadCommand: If ``svn`` is not installed.
207
+ """
208
+ # Example versions:
209
+ # svn, version 1.10.3 (r1842928)
210
+ # compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0
211
+ # svn, version 1.7.14 (r1542130)
212
+ # compiled Mar 28 2018, 08:49:13 on x86_64-pc-linux-gnu
213
+ # svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0)
214
+ # compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2
215
+ version_prefix = "svn, version "
216
+ version = self.run_command(["--version"], show_stdout=False, stdout_only=True)
217
+ if not version.startswith(version_prefix):
218
+ return ()
219
+
220
+ version = version[len(version_prefix) :].split()[0]
221
+ version_list = version.partition("-")[0].split(".")
222
+ try:
223
+ parsed_version = tuple(map(int, version_list))
224
+ except ValueError:
225
+ return ()
226
+
227
+ return parsed_version
228
+
229
+ def get_vcs_version(self) -> Tuple[int, ...]:
230
+ """Return the version of the currently installed Subversion client.
231
+
232
+ If the version of the Subversion client has already been queried,
233
+ a cached value will be used.
234
+
235
+ :return: A tuple containing the parts of the version information or
236
+ ``()`` if the version returned from ``svn`` could not be parsed.
237
+ :raises: BadCommand: If ``svn`` is not installed.
238
+ """
239
+ if self._vcs_version is not None:
240
+ # Use cached version, if available.
241
+ # If parsing the version failed previously (empty tuple),
242
+ # do not attempt to parse it again.
243
+ return self._vcs_version
244
+
245
+ vcs_version = self.call_vcs_version()
246
+ self._vcs_version = vcs_version
247
+ return vcs_version
248
+
249
+ def get_remote_call_options(self) -> CommandArgs:
250
+ """Return options to be used on calls to Subversion that contact the server.
251
+
252
+ These options are applicable for the following ``svn`` subcommands used
253
+ in this class.
254
+
255
+ - checkout
256
+ - switch
257
+ - update
258
+
259
+ :return: A list of command line arguments to pass to ``svn``.
260
+ """
261
+ if not self.use_interactive:
262
+ # --non-interactive switch is available since Subversion 0.14.4.
263
+ # Subversion < 1.8 runs in interactive mode by default.
264
+ return ["--non-interactive"]
265
+
266
+ svn_version = self.get_vcs_version()
267
+ # By default, Subversion >= 1.8 runs in non-interactive mode if
268
+ # stdin is not a TTY. Since that is how pip invokes SVN, in
269
+ # call_subprocess(), pip must pass --force-interactive to ensure
270
+ # the user can be prompted for a password, if required.
271
+ # SVN added the --force-interactive option in SVN 1.8. Since
272
+ # e.g. RHEL/CentOS 7, which is supported until 2024, ships with
273
+ # SVN 1.7, pip should continue to support SVN 1.7. Therefore, pip
274
+ # can't safely add the option if the SVN version is < 1.8 (or unknown).
275
+ if svn_version >= (1, 8):
276
+ return ["--force-interactive"]
277
+
278
+ return []
279
+
280
+ def fetch_new(
281
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
282
+ ) -> None:
283
+ rev_display = rev_options.to_display()
284
+ logger.info(
285
+ "Checking out %s%s to %s",
286
+ url,
287
+ rev_display,
288
+ display_path(dest),
289
+ )
290
+ if verbosity <= 0:
291
+ flags = ["--quiet"]
292
+ else:
293
+ flags = []
294
+ cmd_args = make_command(
295
+ "checkout",
296
+ *flags,
297
+ self.get_remote_call_options(),
298
+ rev_options.to_args(),
299
+ url,
300
+ dest,
301
+ )
302
+ self.run_command(cmd_args)
303
+
304
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
305
+ cmd_args = make_command(
306
+ "switch",
307
+ self.get_remote_call_options(),
308
+ rev_options.to_args(),
309
+ url,
310
+ dest,
311
+ )
312
+ self.run_command(cmd_args)
313
+
314
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
315
+ cmd_args = make_command(
316
+ "update",
317
+ self.get_remote_call_options(),
318
+ rev_options.to_args(),
319
+ dest,
320
+ )
321
+ self.run_command(cmd_args)
322
+
323
+
324
+ vcs.register(Subversion)
.venv/lib/python3.13/site-packages/pip/_internal/vcs/versioncontrol.py ADDED
@@ -0,0 +1,688 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Handles all VCS (version control) support"""
2
+
3
+ import logging
4
+ import os
5
+ import shutil
6
+ import sys
7
+ import urllib.parse
8
+ from dataclasses import dataclass, field
9
+ from typing import (
10
+ Any,
11
+ Dict,
12
+ Iterable,
13
+ Iterator,
14
+ List,
15
+ Literal,
16
+ Mapping,
17
+ Optional,
18
+ Tuple,
19
+ Type,
20
+ Union,
21
+ )
22
+
23
+ from pip._internal.cli.spinners import SpinnerInterface
24
+ from pip._internal.exceptions import BadCommand, InstallationError
25
+ from pip._internal.utils.misc import (
26
+ HiddenText,
27
+ ask_path_exists,
28
+ backup_dir,
29
+ display_path,
30
+ hide_url,
31
+ hide_value,
32
+ is_installable_dir,
33
+ rmtree,
34
+ )
35
+ from pip._internal.utils.subprocess import (
36
+ CommandArgs,
37
+ call_subprocess,
38
+ format_command_args,
39
+ make_command,
40
+ )
41
+
42
+ __all__ = ["vcs"]
43
+
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ AuthInfo = Tuple[Optional[str], Optional[str]]
48
+
49
+
50
+ def is_url(name: str) -> bool:
51
+ """
52
+ Return true if the name looks like a URL.
53
+ """
54
+ scheme = urllib.parse.urlsplit(name).scheme
55
+ if not scheme:
56
+ return False
57
+ return scheme in ["http", "https", "file", "ftp"] + vcs.all_schemes
58
+
59
+
60
+ def make_vcs_requirement_url(
61
+ repo_url: str, rev: str, project_name: str, subdir: Optional[str] = None
62
+ ) -> str:
63
+ """
64
+ Return the URL for a VCS requirement.
65
+
66
+ Args:
67
+ repo_url: the remote VCS url, with any needed VCS prefix (e.g. "git+").
68
+ project_name: the (unescaped) project name.
69
+ """
70
+ egg_project_name = project_name.replace("-", "_")
71
+ req = f"{repo_url}@{rev}#egg={egg_project_name}"
72
+ if subdir:
73
+ req += f"&subdirectory={subdir}"
74
+
75
+ return req
76
+
77
+
78
+ def find_path_to_project_root_from_repo_root(
79
+ location: str, repo_root: str
80
+ ) -> Optional[str]:
81
+ """
82
+ Find the the Python project's root by searching up the filesystem from
83
+ `location`. Return the path to project root relative to `repo_root`.
84
+ Return None if the project root is `repo_root`, or cannot be found.
85
+ """
86
+ # find project root.
87
+ orig_location = location
88
+ while not is_installable_dir(location):
89
+ last_location = location
90
+ location = os.path.dirname(location)
91
+ if location == last_location:
92
+ # We've traversed up to the root of the filesystem without
93
+ # finding a Python project.
94
+ logger.warning(
95
+ "Could not find a Python project for directory %s (tried all "
96
+ "parent directories)",
97
+ orig_location,
98
+ )
99
+ return None
100
+
101
+ if os.path.samefile(repo_root, location):
102
+ return None
103
+
104
+ return os.path.relpath(location, repo_root)
105
+
106
+
107
+ class RemoteNotFoundError(Exception):
108
+ pass
109
+
110
+
111
+ class RemoteNotValidError(Exception):
112
+ def __init__(self, url: str):
113
+ super().__init__(url)
114
+ self.url = url
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class RevOptions:
119
+ """
120
+ Encapsulates a VCS-specific revision to install, along with any VCS
121
+ install options.
122
+
123
+ Args:
124
+ vc_class: a VersionControl subclass.
125
+ rev: the name of the revision to install.
126
+ extra_args: a list of extra options.
127
+ """
128
+
129
+ vc_class: Type["VersionControl"]
130
+ rev: Optional[str] = None
131
+ extra_args: CommandArgs = field(default_factory=list)
132
+ branch_name: Optional[str] = None
133
+
134
+ def __repr__(self) -> str:
135
+ return f"<RevOptions {self.vc_class.name}: rev={self.rev!r}>"
136
+
137
+ @property
138
+ def arg_rev(self) -> Optional[str]:
139
+ if self.rev is None:
140
+ return self.vc_class.default_arg_rev
141
+
142
+ return self.rev
143
+
144
+ def to_args(self) -> CommandArgs:
145
+ """
146
+ Return the VCS-specific command arguments.
147
+ """
148
+ args: CommandArgs = []
149
+ rev = self.arg_rev
150
+ if rev is not None:
151
+ args += self.vc_class.get_base_rev_args(rev)
152
+ args += self.extra_args
153
+
154
+ return args
155
+
156
+ def to_display(self) -> str:
157
+ if not self.rev:
158
+ return ""
159
+
160
+ return f" (to revision {self.rev})"
161
+
162
+ def make_new(self, rev: str) -> "RevOptions":
163
+ """
164
+ Make a copy of the current instance, but with a new rev.
165
+
166
+ Args:
167
+ rev: the name of the revision for the new object.
168
+ """
169
+ return self.vc_class.make_rev_options(rev, extra_args=self.extra_args)
170
+
171
+
172
+ class VcsSupport:
173
+ _registry: Dict[str, "VersionControl"] = {}
174
+ schemes = ["ssh", "git", "hg", "bzr", "sftp", "svn"]
175
+
176
+ def __init__(self) -> None:
177
+ # Register more schemes with urlparse for various version control
178
+ # systems
179
+ urllib.parse.uses_netloc.extend(self.schemes)
180
+ super().__init__()
181
+
182
+ def __iter__(self) -> Iterator[str]:
183
+ return self._registry.__iter__()
184
+
185
+ @property
186
+ def backends(self) -> List["VersionControl"]:
187
+ return list(self._registry.values())
188
+
189
+ @property
190
+ def dirnames(self) -> List[str]:
191
+ return [backend.dirname for backend in self.backends]
192
+
193
+ @property
194
+ def all_schemes(self) -> List[str]:
195
+ schemes: List[str] = []
196
+ for backend in self.backends:
197
+ schemes.extend(backend.schemes)
198
+ return schemes
199
+
200
+ def register(self, cls: Type["VersionControl"]) -> None:
201
+ if not hasattr(cls, "name"):
202
+ logger.warning("Cannot register VCS %s", cls.__name__)
203
+ return
204
+ if cls.name not in self._registry:
205
+ self._registry[cls.name] = cls()
206
+ logger.debug("Registered VCS backend: %s", cls.name)
207
+
208
+ def unregister(self, name: str) -> None:
209
+ if name in self._registry:
210
+ del self._registry[name]
211
+
212
+ def get_backend_for_dir(self, location: str) -> Optional["VersionControl"]:
213
+ """
214
+ Return a VersionControl object if a repository of that type is found
215
+ at the given directory.
216
+ """
217
+ vcs_backends = {}
218
+ for vcs_backend in self._registry.values():
219
+ repo_path = vcs_backend.get_repository_root(location)
220
+ if not repo_path:
221
+ continue
222
+ logger.debug("Determine that %s uses VCS: %s", location, vcs_backend.name)
223
+ vcs_backends[repo_path] = vcs_backend
224
+
225
+ if not vcs_backends:
226
+ return None
227
+
228
+ # Choose the VCS in the inner-most directory. Since all repository
229
+ # roots found here would be either `location` or one of its
230
+ # parents, the longest path should have the most path components,
231
+ # i.e. the backend representing the inner-most repository.
232
+ inner_most_repo_path = max(vcs_backends, key=len)
233
+ return vcs_backends[inner_most_repo_path]
234
+
235
+ def get_backend_for_scheme(self, scheme: str) -> Optional["VersionControl"]:
236
+ """
237
+ Return a VersionControl object or None.
238
+ """
239
+ for vcs_backend in self._registry.values():
240
+ if scheme in vcs_backend.schemes:
241
+ return vcs_backend
242
+ return None
243
+
244
+ def get_backend(self, name: str) -> Optional["VersionControl"]:
245
+ """
246
+ Return a VersionControl object or None.
247
+ """
248
+ name = name.lower()
249
+ return self._registry.get(name)
250
+
251
+
252
+ vcs = VcsSupport()
253
+
254
+
255
+ class VersionControl:
256
+ name = ""
257
+ dirname = ""
258
+ repo_name = ""
259
+ # List of supported schemes for this Version Control
260
+ schemes: Tuple[str, ...] = ()
261
+ # Iterable of environment variable names to pass to call_subprocess().
262
+ unset_environ: Tuple[str, ...] = ()
263
+ default_arg_rev: Optional[str] = None
264
+
265
+ @classmethod
266
+ def should_add_vcs_url_prefix(cls, remote_url: str) -> bool:
267
+ """
268
+ Return whether the vcs prefix (e.g. "git+") should be added to a
269
+ repository's remote url when used in a requirement.
270
+ """
271
+ return not remote_url.lower().startswith(f"{cls.name}:")
272
+
273
+ @classmethod
274
+ def get_subdirectory(cls, location: str) -> Optional[str]:
275
+ """
276
+ Return the path to Python project root, relative to the repo root.
277
+ Return None if the project root is in the repo root.
278
+ """
279
+ return None
280
+
281
+ @classmethod
282
+ def get_requirement_revision(cls, repo_dir: str) -> str:
283
+ """
284
+ Return the revision string that should be used in a requirement.
285
+ """
286
+ return cls.get_revision(repo_dir)
287
+
288
+ @classmethod
289
+ def get_src_requirement(cls, repo_dir: str, project_name: str) -> str:
290
+ """
291
+ Return the requirement string to use to redownload the files
292
+ currently at the given repository directory.
293
+
294
+ Args:
295
+ project_name: the (unescaped) project name.
296
+
297
+ The return value has a form similar to the following:
298
+
299
+ {repository_url}@{revision}#egg={project_name}
300
+ """
301
+ repo_url = cls.get_remote_url(repo_dir)
302
+
303
+ if cls.should_add_vcs_url_prefix(repo_url):
304
+ repo_url = f"{cls.name}+{repo_url}"
305
+
306
+ revision = cls.get_requirement_revision(repo_dir)
307
+ subdir = cls.get_subdirectory(repo_dir)
308
+ req = make_vcs_requirement_url(repo_url, revision, project_name, subdir=subdir)
309
+
310
+ return req
311
+
312
+ @staticmethod
313
+ def get_base_rev_args(rev: str) -> List[str]:
314
+ """
315
+ Return the base revision arguments for a vcs command.
316
+
317
+ Args:
318
+ rev: the name of a revision to install. Cannot be None.
319
+ """
320
+ raise NotImplementedError
321
+
322
+ def is_immutable_rev_checkout(self, url: str, dest: str) -> bool:
323
+ """
324
+ Return true if the commit hash checked out at dest matches
325
+ the revision in url.
326
+
327
+ Always return False, if the VCS does not support immutable commit
328
+ hashes.
329
+
330
+ This method does not check if there are local uncommitted changes
331
+ in dest after checkout, as pip currently has no use case for that.
332
+ """
333
+ return False
334
+
335
+ @classmethod
336
+ def make_rev_options(
337
+ cls, rev: Optional[str] = None, extra_args: Optional[CommandArgs] = None
338
+ ) -> RevOptions:
339
+ """
340
+ Return a RevOptions object.
341
+
342
+ Args:
343
+ rev: the name of a revision to install.
344
+ extra_args: a list of extra options.
345
+ """
346
+ return RevOptions(cls, rev, extra_args=extra_args or [])
347
+
348
+ @classmethod
349
+ def _is_local_repository(cls, repo: str) -> bool:
350
+ """
351
+ posix absolute paths start with os.path.sep,
352
+ win32 ones start with drive (like c:\\folder)
353
+ """
354
+ drive, tail = os.path.splitdrive(repo)
355
+ return repo.startswith(os.path.sep) or bool(drive)
356
+
357
+ @classmethod
358
+ def get_netloc_and_auth(
359
+ cls, netloc: str, scheme: str
360
+ ) -> Tuple[str, Tuple[Optional[str], Optional[str]]]:
361
+ """
362
+ Parse the repository URL's netloc, and return the new netloc to use
363
+ along with auth information.
364
+
365
+ Args:
366
+ netloc: the original repository URL netloc.
367
+ scheme: the repository URL's scheme without the vcs prefix.
368
+
369
+ This is mainly for the Subversion class to override, so that auth
370
+ information can be provided via the --username and --password options
371
+ instead of through the URL. For other subclasses like Git without
372
+ such an option, auth information must stay in the URL.
373
+
374
+ Returns: (netloc, (username, password)).
375
+ """
376
+ return netloc, (None, None)
377
+
378
+ @classmethod
379
+ def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:
380
+ """
381
+ Parse the repository URL to use, and return the URL, revision,
382
+ and auth info to use.
383
+
384
+ Returns: (url, rev, (username, password)).
385
+ """
386
+ scheme, netloc, path, query, frag = urllib.parse.urlsplit(url)
387
+ if "+" not in scheme:
388
+ raise ValueError(
389
+ f"Sorry, {url!r} is a malformed VCS url. "
390
+ "The format is <vcs>+<protocol>://<url>, "
391
+ "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp"
392
+ )
393
+ # Remove the vcs prefix.
394
+ scheme = scheme.split("+", 1)[1]
395
+ netloc, user_pass = cls.get_netloc_and_auth(netloc, scheme)
396
+ rev = None
397
+ if "@" in path:
398
+ path, rev = path.rsplit("@", 1)
399
+ if not rev:
400
+ raise InstallationError(
401
+ f"The URL {url!r} has an empty revision (after @) "
402
+ "which is not supported. Include a revision after @ "
403
+ "or remove @ from the URL."
404
+ )
405
+ url = urllib.parse.urlunsplit((scheme, netloc, path, query, ""))
406
+ return url, rev, user_pass
407
+
408
+ @staticmethod
409
+ def make_rev_args(
410
+ username: Optional[str], password: Optional[HiddenText]
411
+ ) -> CommandArgs:
412
+ """
413
+ Return the RevOptions "extra arguments" to use in obtain().
414
+ """
415
+ return []
416
+
417
+ def get_url_rev_options(self, url: HiddenText) -> Tuple[HiddenText, RevOptions]:
418
+ """
419
+ Return the URL and RevOptions object to use in obtain(),
420
+ as a tuple (url, rev_options).
421
+ """
422
+ secret_url, rev, user_pass = self.get_url_rev_and_auth(url.secret)
423
+ username, secret_password = user_pass
424
+ password: Optional[HiddenText] = None
425
+ if secret_password is not None:
426
+ password = hide_value(secret_password)
427
+ extra_args = self.make_rev_args(username, password)
428
+ rev_options = self.make_rev_options(rev, extra_args=extra_args)
429
+
430
+ return hide_url(secret_url), rev_options
431
+
432
+ @staticmethod
433
+ def normalize_url(url: str) -> str:
434
+ """
435
+ Normalize a URL for comparison by unquoting it and removing any
436
+ trailing slash.
437
+ """
438
+ return urllib.parse.unquote(url).rstrip("/")
439
+
440
+ @classmethod
441
+ def compare_urls(cls, url1: str, url2: str) -> bool:
442
+ """
443
+ Compare two repo URLs for identity, ignoring incidental differences.
444
+ """
445
+ return cls.normalize_url(url1) == cls.normalize_url(url2)
446
+
447
+ def fetch_new(
448
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
449
+ ) -> None:
450
+ """
451
+ Fetch a revision from a repository, in the case that this is the
452
+ first fetch from the repository.
453
+
454
+ Args:
455
+ dest: the directory to fetch the repository to.
456
+ rev_options: a RevOptions object.
457
+ verbosity: verbosity level.
458
+ """
459
+ raise NotImplementedError
460
+
461
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
462
+ """
463
+ Switch the repo at ``dest`` to point to ``URL``.
464
+
465
+ Args:
466
+ rev_options: a RevOptions object.
467
+ """
468
+ raise NotImplementedError
469
+
470
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
471
+ """
472
+ Update an already-existing repo to the given ``rev_options``.
473
+
474
+ Args:
475
+ rev_options: a RevOptions object.
476
+ """
477
+ raise NotImplementedError
478
+
479
+ @classmethod
480
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
481
+ """
482
+ Return whether the id of the current commit equals the given name.
483
+
484
+ Args:
485
+ dest: the repository directory.
486
+ name: a string name.
487
+ """
488
+ raise NotImplementedError
489
+
490
+ def obtain(self, dest: str, url: HiddenText, verbosity: int) -> None:
491
+ """
492
+ Install or update in editable mode the package represented by this
493
+ VersionControl object.
494
+
495
+ :param dest: the repository directory in which to install or update.
496
+ :param url: the repository URL starting with a vcs prefix.
497
+ :param verbosity: verbosity level.
498
+ """
499
+ url, rev_options = self.get_url_rev_options(url)
500
+
501
+ if not os.path.exists(dest):
502
+ self.fetch_new(dest, url, rev_options, verbosity=verbosity)
503
+ return
504
+
505
+ rev_display = rev_options.to_display()
506
+ if self.is_repository_directory(dest):
507
+ existing_url = self.get_remote_url(dest)
508
+ if self.compare_urls(existing_url, url.secret):
509
+ logger.debug(
510
+ "%s in %s exists, and has correct URL (%s)",
511
+ self.repo_name.title(),
512
+ display_path(dest),
513
+ url,
514
+ )
515
+ if not self.is_commit_id_equal(dest, rev_options.rev):
516
+ logger.info(
517
+ "Updating %s %s%s",
518
+ display_path(dest),
519
+ self.repo_name,
520
+ rev_display,
521
+ )
522
+ self.update(dest, url, rev_options)
523
+ else:
524
+ logger.info("Skipping because already up-to-date.")
525
+ return
526
+
527
+ logger.warning(
528
+ "%s %s in %s exists with URL %s",
529
+ self.name,
530
+ self.repo_name,
531
+ display_path(dest),
532
+ existing_url,
533
+ )
534
+ prompt = ("(s)witch, (i)gnore, (w)ipe, (b)ackup ", ("s", "i", "w", "b"))
535
+ else:
536
+ logger.warning(
537
+ "Directory %s already exists, and is not a %s %s.",
538
+ dest,
539
+ self.name,
540
+ self.repo_name,
541
+ )
542
+ # https://github.com/python/mypy/issues/1174
543
+ prompt = ("(i)gnore, (w)ipe, (b)ackup ", ("i", "w", "b")) # type: ignore
544
+
545
+ logger.warning(
546
+ "The plan is to install the %s repository %s",
547
+ self.name,
548
+ url,
549
+ )
550
+ response = ask_path_exists(f"What to do? {prompt[0]}", prompt[1])
551
+
552
+ if response == "a":
553
+ sys.exit(-1)
554
+
555
+ if response == "w":
556
+ logger.warning("Deleting %s", display_path(dest))
557
+ rmtree(dest)
558
+ self.fetch_new(dest, url, rev_options, verbosity=verbosity)
559
+ return
560
+
561
+ if response == "b":
562
+ dest_dir = backup_dir(dest)
563
+ logger.warning("Backing up %s to %s", display_path(dest), dest_dir)
564
+ shutil.move(dest, dest_dir)
565
+ self.fetch_new(dest, url, rev_options, verbosity=verbosity)
566
+ return
567
+
568
+ # Do nothing if the response is "i".
569
+ if response == "s":
570
+ logger.info(
571
+ "Switching %s %s to %s%s",
572
+ self.repo_name,
573
+ display_path(dest),
574
+ url,
575
+ rev_display,
576
+ )
577
+ self.switch(dest, url, rev_options)
578
+
579
+ def unpack(self, location: str, url: HiddenText, verbosity: int) -> None:
580
+ """
581
+ Clean up current location and download the url repository
582
+ (and vcs infos) into location
583
+
584
+ :param url: the repository URL starting with a vcs prefix.
585
+ :param verbosity: verbosity level.
586
+ """
587
+ if os.path.exists(location):
588
+ rmtree(location)
589
+ self.obtain(location, url=url, verbosity=verbosity)
590
+
591
+ @classmethod
592
+ def get_remote_url(cls, location: str) -> str:
593
+ """
594
+ Return the url used at location
595
+
596
+ Raises RemoteNotFoundError if the repository does not have a remote
597
+ url configured.
598
+ """
599
+ raise NotImplementedError
600
+
601
+ @classmethod
602
+ def get_revision(cls, location: str) -> str:
603
+ """
604
+ Return the current commit id of the files at the given location.
605
+ """
606
+ raise NotImplementedError
607
+
608
+ @classmethod
609
+ def run_command(
610
+ cls,
611
+ cmd: Union[List[str], CommandArgs],
612
+ show_stdout: bool = True,
613
+ cwd: Optional[str] = None,
614
+ on_returncode: 'Literal["raise", "warn", "ignore"]' = "raise",
615
+ extra_ok_returncodes: Optional[Iterable[int]] = None,
616
+ command_desc: Optional[str] = None,
617
+ extra_environ: Optional[Mapping[str, Any]] = None,
618
+ spinner: Optional[SpinnerInterface] = None,
619
+ log_failed_cmd: bool = True,
620
+ stdout_only: bool = False,
621
+ ) -> str:
622
+ """
623
+ Run a VCS subcommand
624
+ This is simply a wrapper around call_subprocess that adds the VCS
625
+ command name, and checks that the VCS is available
626
+ """
627
+ cmd = make_command(cls.name, *cmd)
628
+ if command_desc is None:
629
+ command_desc = format_command_args(cmd)
630
+ try:
631
+ return call_subprocess(
632
+ cmd,
633
+ show_stdout,
634
+ cwd,
635
+ on_returncode=on_returncode,
636
+ extra_ok_returncodes=extra_ok_returncodes,
637
+ command_desc=command_desc,
638
+ extra_environ=extra_environ,
639
+ unset_environ=cls.unset_environ,
640
+ spinner=spinner,
641
+ log_failed_cmd=log_failed_cmd,
642
+ stdout_only=stdout_only,
643
+ )
644
+ except NotADirectoryError:
645
+ raise BadCommand(f"Cannot find command {cls.name!r} - invalid PATH")
646
+ except FileNotFoundError:
647
+ # errno.ENOENT = no such file or directory
648
+ # In other words, the VCS executable isn't available
649
+ raise BadCommand(
650
+ f"Cannot find command {cls.name!r} - do you have "
651
+ f"{cls.name!r} installed and in your PATH?"
652
+ )
653
+ except PermissionError:
654
+ # errno.EACCES = Permission denied
655
+ # This error occurs, for instance, when the command is installed
656
+ # only for another user. So, the current user don't have
657
+ # permission to call the other user command.
658
+ raise BadCommand(
659
+ f"No permission to execute {cls.name!r} - install it "
660
+ f"locally, globally (ask admin), or check your PATH. "
661
+ f"See possible solutions at "
662
+ f"https://pip.pypa.io/en/latest/reference/pip_freeze/"
663
+ f"#fixing-permission-denied."
664
+ )
665
+
666
+ @classmethod
667
+ def is_repository_directory(cls, path: str) -> bool:
668
+ """
669
+ Return whether a directory path is a repository directory.
670
+ """
671
+ logger.debug("Checking in %s for %s (%s)...", path, cls.dirname, cls.name)
672
+ return os.path.exists(os.path.join(path, cls.dirname))
673
+
674
+ @classmethod
675
+ def get_repository_root(cls, location: str) -> Optional[str]:
676
+ """
677
+ Return the "root" (top-level) directory controlled by the vcs,
678
+ or `None` if the directory is not in any.
679
+
680
+ It is meant to be overridden to implement smarter detection
681
+ mechanisms for specific vcs.
682
+
683
+ This can do more than is_repository_directory() alone. For
684
+ example, the Git override checks that Git is actually available.
685
+ """
686
+ if cls.is_repository_directory(location):
687
+ return location
688
+ return None
.venv/lib/python3.13/site-packages/pip/_internal/wheel_builder.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Orchestrator for building wheels from InstallRequirements."""
2
+
3
+ import logging
4
+ import os.path
5
+ import re
6
+ import shutil
7
+ from typing import Iterable, List, Optional, Tuple
8
+
9
+ from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
10
+ from pip._vendor.packaging.version import InvalidVersion, Version
11
+
12
+ from pip._internal.cache import WheelCache
13
+ from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel
14
+ from pip._internal.metadata import FilesystemWheel, get_wheel_distribution
15
+ from pip._internal.models.link import Link
16
+ from pip._internal.models.wheel import Wheel
17
+ from pip._internal.operations.build.wheel import build_wheel_pep517
18
+ from pip._internal.operations.build.wheel_editable import build_wheel_editable
19
+ from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
20
+ from pip._internal.req.req_install import InstallRequirement
21
+ from pip._internal.utils.logging import indent_log
22
+ from pip._internal.utils.misc import ensure_dir, hash_file
23
+ from pip._internal.utils.setuptools_build import make_setuptools_clean_args
24
+ from pip._internal.utils.subprocess import call_subprocess
25
+ from pip._internal.utils.temp_dir import TempDirectory
26
+ from pip._internal.utils.urls import path_to_url
27
+ from pip._internal.vcs import vcs
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ _egg_info_re = re.compile(r"([a-z0-9_.]+)-([a-z0-9_.!+-]+)", re.IGNORECASE)
32
+
33
+ BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]]
34
+
35
+
36
+ def _contains_egg_info(s: str) -> bool:
37
+ """Determine whether the string looks like an egg_info.
38
+
39
+ :param s: The string to parse. E.g. foo-2.1
40
+ """
41
+ return bool(_egg_info_re.search(s))
42
+
43
+
44
+ def _should_build(
45
+ req: InstallRequirement,
46
+ ) -> bool:
47
+ """Return whether an InstallRequirement should be built into a wheel."""
48
+ assert not req.constraint
49
+
50
+ if req.is_wheel:
51
+ return False
52
+
53
+ assert req.source_dir
54
+
55
+ if req.editable:
56
+ # we only build PEP 660 editable requirements
57
+ return req.supports_pyproject_editable
58
+
59
+ return True
60
+
61
+
62
+ def should_build_for_install_command(
63
+ req: InstallRequirement,
64
+ ) -> bool:
65
+ return _should_build(req)
66
+
67
+
68
+ def _should_cache(
69
+ req: InstallRequirement,
70
+ ) -> Optional[bool]:
71
+ """
72
+ Return whether a built InstallRequirement can be stored in the persistent
73
+ wheel cache, assuming the wheel cache is available, and _should_build()
74
+ has determined a wheel needs to be built.
75
+ """
76
+ if req.editable or not req.source_dir:
77
+ # never cache editable requirements
78
+ return False
79
+
80
+ if req.link and req.link.is_vcs:
81
+ # VCS checkout. Do not cache
82
+ # unless it points to an immutable commit hash.
83
+ assert not req.editable
84
+ assert req.source_dir
85
+ vcs_backend = vcs.get_backend_for_scheme(req.link.scheme)
86
+ assert vcs_backend
87
+ if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir):
88
+ return True
89
+ return False
90
+
91
+ assert req.link
92
+ base, ext = req.link.splitext()
93
+ if _contains_egg_info(base):
94
+ return True
95
+
96
+ # Otherwise, do not cache.
97
+ return False
98
+
99
+
100
+ def _get_cache_dir(
101
+ req: InstallRequirement,
102
+ wheel_cache: WheelCache,
103
+ ) -> str:
104
+ """Return the persistent or temporary cache directory where the built
105
+ wheel need to be stored.
106
+ """
107
+ cache_available = bool(wheel_cache.cache_dir)
108
+ assert req.link
109
+ if cache_available and _should_cache(req):
110
+ cache_dir = wheel_cache.get_path_for_link(req.link)
111
+ else:
112
+ cache_dir = wheel_cache.get_ephem_path_for_link(req.link)
113
+ return cache_dir
114
+
115
+
116
+ def _verify_one(req: InstallRequirement, wheel_path: str) -> None:
117
+ canonical_name = canonicalize_name(req.name or "")
118
+ w = Wheel(os.path.basename(wheel_path))
119
+ if canonicalize_name(w.name) != canonical_name:
120
+ raise InvalidWheelFilename(
121
+ f"Wheel has unexpected file name: expected {canonical_name!r}, "
122
+ f"got {w.name!r}",
123
+ )
124
+ dist = get_wheel_distribution(FilesystemWheel(wheel_path), canonical_name)
125
+ dist_verstr = str(dist.version)
126
+ if canonicalize_version(dist_verstr) != canonicalize_version(w.version):
127
+ raise InvalidWheelFilename(
128
+ f"Wheel has unexpected file name: expected {dist_verstr!r}, "
129
+ f"got {w.version!r}",
130
+ )
131
+ metadata_version_value = dist.metadata_version
132
+ if metadata_version_value is None:
133
+ raise UnsupportedWheel("Missing Metadata-Version")
134
+ try:
135
+ metadata_version = Version(metadata_version_value)
136
+ except InvalidVersion:
137
+ msg = f"Invalid Metadata-Version: {metadata_version_value}"
138
+ raise UnsupportedWheel(msg)
139
+ if metadata_version >= Version("1.2") and not isinstance(dist.version, Version):
140
+ raise UnsupportedWheel(
141
+ f"Metadata 1.2 mandates PEP 440 version, but {dist_verstr!r} is not"
142
+ )
143
+
144
+
145
+ def _build_one(
146
+ req: InstallRequirement,
147
+ output_dir: str,
148
+ verify: bool,
149
+ build_options: List[str],
150
+ global_options: List[str],
151
+ editable: bool,
152
+ ) -> Optional[str]:
153
+ """Build one wheel.
154
+
155
+ :return: The filename of the built wheel, or None if the build failed.
156
+ """
157
+ artifact = "editable" if editable else "wheel"
158
+ try:
159
+ ensure_dir(output_dir)
160
+ except OSError as e:
161
+ logger.warning(
162
+ "Building %s for %s failed: %s",
163
+ artifact,
164
+ req.name,
165
+ e,
166
+ )
167
+ return None
168
+
169
+ # Install build deps into temporary directory (PEP 518)
170
+ with req.build_env:
171
+ wheel_path = _build_one_inside_env(
172
+ req, output_dir, build_options, global_options, editable
173
+ )
174
+ if wheel_path and verify:
175
+ try:
176
+ _verify_one(req, wheel_path)
177
+ except (InvalidWheelFilename, UnsupportedWheel) as e:
178
+ logger.warning("Built %s for %s is invalid: %s", artifact, req.name, e)
179
+ return None
180
+ return wheel_path
181
+
182
+
183
+ def _build_one_inside_env(
184
+ req: InstallRequirement,
185
+ output_dir: str,
186
+ build_options: List[str],
187
+ global_options: List[str],
188
+ editable: bool,
189
+ ) -> Optional[str]:
190
+ with TempDirectory(kind="wheel") as temp_dir:
191
+ assert req.name
192
+ if req.use_pep517:
193
+ assert req.metadata_directory
194
+ assert req.pep517_backend
195
+ if global_options:
196
+ logger.warning(
197
+ "Ignoring --global-option when building %s using PEP 517", req.name
198
+ )
199
+ if build_options:
200
+ logger.warning(
201
+ "Ignoring --build-option when building %s using PEP 517", req.name
202
+ )
203
+ if editable:
204
+ wheel_path = build_wheel_editable(
205
+ name=req.name,
206
+ backend=req.pep517_backend,
207
+ metadata_directory=req.metadata_directory,
208
+ tempd=temp_dir.path,
209
+ )
210
+ else:
211
+ wheel_path = build_wheel_pep517(
212
+ name=req.name,
213
+ backend=req.pep517_backend,
214
+ metadata_directory=req.metadata_directory,
215
+ tempd=temp_dir.path,
216
+ )
217
+ else:
218
+ wheel_path = build_wheel_legacy(
219
+ name=req.name,
220
+ setup_py_path=req.setup_py_path,
221
+ source_dir=req.unpacked_source_directory,
222
+ global_options=global_options,
223
+ build_options=build_options,
224
+ tempd=temp_dir.path,
225
+ )
226
+
227
+ if wheel_path is not None:
228
+ wheel_name = os.path.basename(wheel_path)
229
+ dest_path = os.path.join(output_dir, wheel_name)
230
+ try:
231
+ wheel_hash, length = hash_file(wheel_path)
232
+ shutil.move(wheel_path, dest_path)
233
+ logger.info(
234
+ "Created wheel for %s: filename=%s size=%d sha256=%s",
235
+ req.name,
236
+ wheel_name,
237
+ length,
238
+ wheel_hash.hexdigest(),
239
+ )
240
+ logger.info("Stored in directory: %s", output_dir)
241
+ return dest_path
242
+ except Exception as e:
243
+ logger.warning(
244
+ "Building wheel for %s failed: %s",
245
+ req.name,
246
+ e,
247
+ )
248
+ # Ignore return, we can't do anything else useful.
249
+ if not req.use_pep517:
250
+ _clean_one_legacy(req, global_options)
251
+ return None
252
+
253
+
254
+ def _clean_one_legacy(req: InstallRequirement, global_options: List[str]) -> bool:
255
+ clean_args = make_setuptools_clean_args(
256
+ req.setup_py_path,
257
+ global_options=global_options,
258
+ )
259
+
260
+ logger.info("Running setup.py clean for %s", req.name)
261
+ try:
262
+ call_subprocess(
263
+ clean_args, command_desc="python setup.py clean", cwd=req.source_dir
264
+ )
265
+ return True
266
+ except Exception:
267
+ logger.error("Failed cleaning build dir for %s", req.name)
268
+ return False
269
+
270
+
271
+ def build(
272
+ requirements: Iterable[InstallRequirement],
273
+ wheel_cache: WheelCache,
274
+ verify: bool,
275
+ build_options: List[str],
276
+ global_options: List[str],
277
+ ) -> BuildResult:
278
+ """Build wheels.
279
+
280
+ :return: The list of InstallRequirement that succeeded to build and
281
+ the list of InstallRequirement that failed to build.
282
+ """
283
+ if not requirements:
284
+ return [], []
285
+
286
+ # Build the wheels.
287
+ logger.info(
288
+ "Building wheels for collected packages: %s",
289
+ ", ".join(req.name for req in requirements), # type: ignore
290
+ )
291
+
292
+ with indent_log():
293
+ build_successes, build_failures = [], []
294
+ for req in requirements:
295
+ assert req.name
296
+ cache_dir = _get_cache_dir(req, wheel_cache)
297
+ wheel_file = _build_one(
298
+ req,
299
+ cache_dir,
300
+ verify,
301
+ build_options,
302
+ global_options,
303
+ req.editable and req.permit_editable_wheels,
304
+ )
305
+ if wheel_file:
306
+ # Record the download origin in the cache
307
+ if req.download_info is not None:
308
+ # download_info is guaranteed to be set because when we build an
309
+ # InstallRequirement it has been through the preparer before, but
310
+ # let's be cautious.
311
+ wheel_cache.record_download_origin(cache_dir, req.download_info)
312
+ # Update the link for this.
313
+ req.link = Link(path_to_url(wheel_file))
314
+ req.local_file_path = req.link.file_path
315
+ assert req.link.is_wheel
316
+ build_successes.append(req)
317
+ else:
318
+ build_failures.append(req)
319
+
320
+ # notify success/failure
321
+ if build_successes:
322
+ logger.info(
323
+ "Successfully built %s",
324
+ " ".join([req.name for req in build_successes]), # type: ignore
325
+ )
326
+ if build_failures:
327
+ logger.info(
328
+ "Failed to build %s",
329
+ " ".join([req.name for req in build_failures]), # type: ignore
330
+ )
331
+ # Return a list of requirements that failed to build
332
+ return build_successes, build_failures
.venv/lib/python3.13/site-packages/pip/_vendor/__init__.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ pip._vendor is for vendoring dependencies of pip to prevent needing pip to
3
+ depend on something external.
4
+
5
+ Files inside of pip._vendor should be considered immutable and should only be
6
+ updated to versions from upstream.
7
+ """
8
+ from __future__ import absolute_import
9
+
10
+ import glob
11
+ import os.path
12
+ import sys
13
+
14
+ # Downstream redistributors which have debundled our dependencies should also
15
+ # patch this value to be true. This will trigger the additional patching
16
+ # to cause things like "six" to be available as pip.
17
+ DEBUNDLED = False
18
+
19
+ # By default, look in this directory for a bunch of .whl files which we will
20
+ # add to the beginning of sys.path before attempting to import anything. This
21
+ # is done to support downstream re-distributors like Debian and Fedora who
22
+ # wish to create their own Wheels for our dependencies to aid in debundling.
23
+ WHEEL_DIR = os.path.abspath(os.path.dirname(__file__))
24
+
25
+
26
+ # Define a small helper function to alias our vendored modules to the real ones
27
+ # if the vendored ones do not exist. This idea of this was taken from
28
+ # https://github.com/kennethreitz/requests/pull/2567.
29
+ def vendored(modulename):
30
+ vendored_name = "{0}.{1}".format(__name__, modulename)
31
+
32
+ try:
33
+ __import__(modulename, globals(), locals(), level=0)
34
+ except ImportError:
35
+ # We can just silently allow import failures to pass here. If we
36
+ # got to this point it means that ``import pip._vendor.whatever``
37
+ # failed and so did ``import whatever``. Since we're importing this
38
+ # upfront in an attempt to alias imports, not erroring here will
39
+ # just mean we get a regular import error whenever pip *actually*
40
+ # tries to import one of these modules to use it, which actually
41
+ # gives us a better error message than we would have otherwise
42
+ # gotten.
43
+ pass
44
+ else:
45
+ sys.modules[vendored_name] = sys.modules[modulename]
46
+ base, head = vendored_name.rsplit(".", 1)
47
+ setattr(sys.modules[base], head, sys.modules[modulename])
48
+
49
+
50
+ # If we're operating in a debundled setup, then we want to go ahead and trigger
51
+ # the aliasing of our vendored libraries as well as looking for wheels to add
52
+ # to our sys.path. This will cause all of this code to be a no-op typically
53
+ # however downstream redistributors can enable it in a consistent way across
54
+ # all platforms.
55
+ if DEBUNDLED:
56
+ # Actually look inside of WHEEL_DIR to find .whl files and add them to the
57
+ # front of our sys.path.
58
+ sys.path[:] = glob.glob(os.path.join(WHEEL_DIR, "*.whl")) + sys.path
59
+
60
+ # Actually alias all of our vendored dependencies.
61
+ vendored("cachecontrol")
62
+ vendored("certifi")
63
+ vendored("dependency-groups")
64
+ vendored("distlib")
65
+ vendored("distro")
66
+ vendored("packaging")
67
+ vendored("packaging.version")
68
+ vendored("packaging.specifiers")
69
+ vendored("pkg_resources")
70
+ vendored("platformdirs")
71
+ vendored("progress")
72
+ vendored("pyproject_hooks")
73
+ vendored("requests")
74
+ vendored("requests.exceptions")
75
+ vendored("requests.packages")
76
+ vendored("requests.packages.urllib3")
77
+ vendored("requests.packages.urllib3._collections")
78
+ vendored("requests.packages.urllib3.connection")
79
+ vendored("requests.packages.urllib3.connectionpool")
80
+ vendored("requests.packages.urllib3.contrib")
81
+ vendored("requests.packages.urllib3.contrib.ntlmpool")
82
+ vendored("requests.packages.urllib3.contrib.pyopenssl")
83
+ vendored("requests.packages.urllib3.exceptions")
84
+ vendored("requests.packages.urllib3.fields")
85
+ vendored("requests.packages.urllib3.filepost")
86
+ vendored("requests.packages.urllib3.packages")
87
+ vendored("requests.packages.urllib3.packages.ordered_dict")
88
+ vendored("requests.packages.urllib3.packages.six")
89
+ vendored("requests.packages.urllib3.packages.ssl_match_hostname")
90
+ vendored("requests.packages.urllib3.packages.ssl_match_hostname."
91
+ "_implementation")
92
+ vendored("requests.packages.urllib3.poolmanager")
93
+ vendored("requests.packages.urllib3.request")
94
+ vendored("requests.packages.urllib3.response")
95
+ vendored("requests.packages.urllib3.util")
96
+ vendored("requests.packages.urllib3.util.connection")
97
+ vendored("requests.packages.urllib3.util.request")
98
+ vendored("requests.packages.urllib3.util.response")
99
+ vendored("requests.packages.urllib3.util.retry")
100
+ vendored("requests.packages.urllib3.util.ssl_")
101
+ vendored("requests.packages.urllib3.util.timeout")
102
+ vendored("requests.packages.urllib3.util.url")
103
+ vendored("resolvelib")
104
+ vendored("rich")
105
+ vendored("rich.console")
106
+ vendored("rich.highlighter")
107
+ vendored("rich.logging")
108
+ vendored("rich.markup")
109
+ vendored("rich.progress")
110
+ vendored("rich.segment")
111
+ vendored("rich.style")
112
+ vendored("rich.text")
113
+ vendored("rich.traceback")
114
+ if sys.version_info < (3, 11):
115
+ vendored("tomli")
116
+ vendored("truststore")
117
+ vendored("urllib3")
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/__init__.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """CacheControl import Interface.
6
+
7
+ Make it easy to import from cachecontrol without long namespaces.
8
+ """
9
+
10
+ __author__ = "Eric Larson"
11
+ __email__ = "eric@ionrock.org"
12
+ __version__ = "0.14.2"
13
+
14
+ from pip._vendor.cachecontrol.adapter import CacheControlAdapter
15
+ from pip._vendor.cachecontrol.controller import CacheController
16
+ from pip._vendor.cachecontrol.wrapper import CacheControl
17
+
18
+ __all__ = [
19
+ "__author__",
20
+ "__email__",
21
+ "__version__",
22
+ "CacheControlAdapter",
23
+ "CacheController",
24
+ "CacheControl",
25
+ ]
26
+
27
+ import logging
28
+
29
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/_cmd.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ from argparse import ArgumentParser
8
+ from typing import TYPE_CHECKING
9
+
10
+ from pip._vendor import requests
11
+
12
+ from pip._vendor.cachecontrol.adapter import CacheControlAdapter
13
+ from pip._vendor.cachecontrol.cache import DictCache
14
+ from pip._vendor.cachecontrol.controller import logger
15
+
16
+ if TYPE_CHECKING:
17
+ from argparse import Namespace
18
+
19
+ from pip._vendor.cachecontrol.controller import CacheController
20
+
21
+
22
+ def setup_logging() -> None:
23
+ logger.setLevel(logging.DEBUG)
24
+ handler = logging.StreamHandler()
25
+ logger.addHandler(handler)
26
+
27
+
28
+ def get_session() -> requests.Session:
29
+ adapter = CacheControlAdapter(
30
+ DictCache(), cache_etags=True, serializer=None, heuristic=None
31
+ )
32
+ sess = requests.Session()
33
+ sess.mount("http://", adapter)
34
+ sess.mount("https://", adapter)
35
+
36
+ sess.cache_controller = adapter.controller # type: ignore[attr-defined]
37
+ return sess
38
+
39
+
40
+ def get_args() -> Namespace:
41
+ parser = ArgumentParser()
42
+ parser.add_argument("url", help="The URL to try and cache")
43
+ return parser.parse_args()
44
+
45
+
46
+ def main() -> None:
47
+ args = get_args()
48
+ sess = get_session()
49
+
50
+ # Make a request to get a response
51
+ resp = sess.get(args.url)
52
+
53
+ # Turn on logging
54
+ setup_logging()
55
+
56
+ # try setting the cache
57
+ cache_controller: CacheController = (
58
+ sess.cache_controller # type: ignore[attr-defined]
59
+ )
60
+ cache_controller.cache_response(resp.request, resp.raw)
61
+
62
+ # Now try to get it
63
+ if cache_controller.cached_request(resp.request):
64
+ print("Cached!")
65
+ else:
66
+ print("Not cached :(")
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/adapter.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from __future__ import annotations
5
+
6
+ import functools
7
+ import types
8
+ import weakref
9
+ import zlib
10
+ from typing import TYPE_CHECKING, Any, Collection, Mapping
11
+
12
+ from pip._vendor.requests.adapters import HTTPAdapter
13
+
14
+ from pip._vendor.cachecontrol.cache import DictCache
15
+ from pip._vendor.cachecontrol.controller import PERMANENT_REDIRECT_STATUSES, CacheController
16
+ from pip._vendor.cachecontrol.filewrapper import CallbackFileWrapper
17
+
18
+ if TYPE_CHECKING:
19
+ from pip._vendor.requests import PreparedRequest, Response
20
+ from pip._vendor.urllib3 import HTTPResponse
21
+
22
+ from pip._vendor.cachecontrol.cache import BaseCache
23
+ from pip._vendor.cachecontrol.heuristics import BaseHeuristic
24
+ from pip._vendor.cachecontrol.serialize import Serializer
25
+
26
+
27
+ class CacheControlAdapter(HTTPAdapter):
28
+ invalidating_methods = {"PUT", "PATCH", "DELETE"}
29
+
30
+ def __init__(
31
+ self,
32
+ cache: BaseCache | None = None,
33
+ cache_etags: bool = True,
34
+ controller_class: type[CacheController] | None = None,
35
+ serializer: Serializer | None = None,
36
+ heuristic: BaseHeuristic | None = None,
37
+ cacheable_methods: Collection[str] | None = None,
38
+ *args: Any,
39
+ **kw: Any,
40
+ ) -> None:
41
+ super().__init__(*args, **kw)
42
+ self.cache = DictCache() if cache is None else cache
43
+ self.heuristic = heuristic
44
+ self.cacheable_methods = cacheable_methods or ("GET",)
45
+
46
+ controller_factory = controller_class or CacheController
47
+ self.controller = controller_factory(
48
+ self.cache, cache_etags=cache_etags, serializer=serializer
49
+ )
50
+
51
+ def send(
52
+ self,
53
+ request: PreparedRequest,
54
+ stream: bool = False,
55
+ timeout: None | float | tuple[float, float] | tuple[float, None] = None,
56
+ verify: bool | str = True,
57
+ cert: (None | bytes | str | tuple[bytes | str, bytes | str]) = None,
58
+ proxies: Mapping[str, str] | None = None,
59
+ cacheable_methods: Collection[str] | None = None,
60
+ ) -> Response:
61
+ """
62
+ Send a request. Use the request information to see if it
63
+ exists in the cache and cache the response if we need to and can.
64
+ """
65
+ cacheable = cacheable_methods or self.cacheable_methods
66
+ if request.method in cacheable:
67
+ try:
68
+ cached_response = self.controller.cached_request(request)
69
+ except zlib.error:
70
+ cached_response = None
71
+ if cached_response:
72
+ return self.build_response(request, cached_response, from_cache=True)
73
+
74
+ # check for etags and add headers if appropriate
75
+ request.headers.update(self.controller.conditional_headers(request))
76
+
77
+ resp = super().send(request, stream, timeout, verify, cert, proxies)
78
+
79
+ return resp
80
+
81
+ def build_response( # type: ignore[override]
82
+ self,
83
+ request: PreparedRequest,
84
+ response: HTTPResponse,
85
+ from_cache: bool = False,
86
+ cacheable_methods: Collection[str] | None = None,
87
+ ) -> Response:
88
+ """
89
+ Build a response by making a request or using the cache.
90
+
91
+ This will end up calling send and returning a potentially
92
+ cached response
93
+ """
94
+ cacheable = cacheable_methods or self.cacheable_methods
95
+ if not from_cache and request.method in cacheable:
96
+ # Check for any heuristics that might update headers
97
+ # before trying to cache.
98
+ if self.heuristic:
99
+ response = self.heuristic.apply(response)
100
+
101
+ # apply any expiration heuristics
102
+ if response.status == 304:
103
+ # We must have sent an ETag request. This could mean
104
+ # that we've been expired already or that we simply
105
+ # have an etag. In either case, we want to try and
106
+ # update the cache if that is the case.
107
+ cached_response = self.controller.update_cached_response(
108
+ request, response
109
+ )
110
+
111
+ if cached_response is not response:
112
+ from_cache = True
113
+
114
+ # We are done with the server response, read a
115
+ # possible response body (compliant servers will
116
+ # not return one, but we cannot be 100% sure) and
117
+ # release the connection back to the pool.
118
+ response.read(decode_content=False)
119
+ response.release_conn()
120
+
121
+ response = cached_response
122
+
123
+ # We always cache the 301 responses
124
+ elif int(response.status) in PERMANENT_REDIRECT_STATUSES:
125
+ self.controller.cache_response(request, response)
126
+ else:
127
+ # Wrap the response file with a wrapper that will cache the
128
+ # response when the stream has been consumed.
129
+ response._fp = CallbackFileWrapper( # type: ignore[assignment]
130
+ response._fp, # type: ignore[arg-type]
131
+ functools.partial(
132
+ self.controller.cache_response, request, weakref.ref(response)
133
+ ),
134
+ )
135
+ if response.chunked:
136
+ super_update_chunk_length = response.__class__._update_chunk_length
137
+
138
+ def _update_chunk_length(
139
+ weak_self: weakref.ReferenceType[HTTPResponse],
140
+ ) -> None:
141
+ self = weak_self()
142
+ if self is None:
143
+ return
144
+
145
+ super_update_chunk_length(self)
146
+ if self.chunk_left == 0:
147
+ self._fp._close() # type: ignore[union-attr]
148
+
149
+ response._update_chunk_length = functools.partial( # type: ignore[method-assign]
150
+ _update_chunk_length, weakref.ref(response)
151
+ )
152
+
153
+ resp: Response = super().build_response(request, response)
154
+
155
+ # See if we should invalidate the cache.
156
+ if request.method in self.invalidating_methods and resp.ok:
157
+ assert request.url is not None
158
+ cache_url = self.controller.cache_url(request.url)
159
+ self.cache.delete(cache_url)
160
+
161
+ # Give the request a from_cache attr to let people use it
162
+ resp.from_cache = from_cache # type: ignore[attr-defined]
163
+
164
+ return resp
165
+
166
+ def close(self) -> None:
167
+ self.cache.close()
168
+ super().close() # type: ignore[no-untyped-call]
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/cache.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """
6
+ The cache object API for implementing caches. The default is a thread
7
+ safe in-memory dictionary.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from threading import Lock
13
+ from typing import IO, TYPE_CHECKING, MutableMapping
14
+
15
+ if TYPE_CHECKING:
16
+ from datetime import datetime
17
+
18
+
19
+ class BaseCache:
20
+ def get(self, key: str) -> bytes | None:
21
+ raise NotImplementedError()
22
+
23
+ def set(
24
+ self, key: str, value: bytes, expires: int | datetime | None = None
25
+ ) -> None:
26
+ raise NotImplementedError()
27
+
28
+ def delete(self, key: str) -> None:
29
+ raise NotImplementedError()
30
+
31
+ def close(self) -> None:
32
+ pass
33
+
34
+
35
+ class DictCache(BaseCache):
36
+ def __init__(self, init_dict: MutableMapping[str, bytes] | None = None) -> None:
37
+ self.lock = Lock()
38
+ self.data = init_dict or {}
39
+
40
+ def get(self, key: str) -> bytes | None:
41
+ return self.data.get(key, None)
42
+
43
+ def set(
44
+ self, key: str, value: bytes, expires: int | datetime | None = None
45
+ ) -> None:
46
+ with self.lock:
47
+ self.data.update({key: value})
48
+
49
+ def delete(self, key: str) -> None:
50
+ with self.lock:
51
+ if key in self.data:
52
+ self.data.pop(key)
53
+
54
+
55
+ class SeparateBodyBaseCache(BaseCache):
56
+ """
57
+ In this variant, the body is not stored mixed in with the metadata, but is
58
+ passed in (as a bytes-like object) in a separate call to ``set_body()``.
59
+
60
+ That is, the expected interaction pattern is::
61
+
62
+ cache.set(key, serialized_metadata)
63
+ cache.set_body(key)
64
+
65
+ Similarly, the body should be loaded separately via ``get_body()``.
66
+ """
67
+
68
+ def set_body(self, key: str, body: bytes) -> None:
69
+ raise NotImplementedError()
70
+
71
+ def get_body(self, key: str) -> IO[bytes] | None:
72
+ """
73
+ Return the body as file-like object.
74
+ """
75
+ raise NotImplementedError()
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from pip._vendor.cachecontrol.caches.file_cache import FileCache, SeparateBodyFileCache
6
+ from pip._vendor.cachecontrol.caches.redis_cache import RedisCache
7
+
8
+ __all__ = ["FileCache", "SeparateBodyFileCache", "RedisCache"]
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import os
8
+ import tempfile
9
+ from textwrap import dedent
10
+ from typing import IO, TYPE_CHECKING
11
+ from pathlib import Path
12
+
13
+ from pip._vendor.cachecontrol.cache import BaseCache, SeparateBodyBaseCache
14
+ from pip._vendor.cachecontrol.controller import CacheController
15
+
16
+ if TYPE_CHECKING:
17
+ from datetime import datetime
18
+
19
+ from filelock import BaseFileLock
20
+
21
+
22
+ class _FileCacheMixin:
23
+ """Shared implementation for both FileCache variants."""
24
+
25
+ def __init__(
26
+ self,
27
+ directory: str | Path,
28
+ forever: bool = False,
29
+ filemode: int = 0o0600,
30
+ dirmode: int = 0o0700,
31
+ lock_class: type[BaseFileLock] | None = None,
32
+ ) -> None:
33
+ try:
34
+ if lock_class is None:
35
+ from filelock import FileLock
36
+
37
+ lock_class = FileLock
38
+ except ImportError:
39
+ notice = dedent(
40
+ """
41
+ NOTE: In order to use the FileCache you must have
42
+ filelock installed. You can install it via pip:
43
+ pip install cachecontrol[filecache]
44
+ """
45
+ )
46
+ raise ImportError(notice)
47
+
48
+ self.directory = directory
49
+ self.forever = forever
50
+ self.filemode = filemode
51
+ self.dirmode = dirmode
52
+ self.lock_class = lock_class
53
+
54
+ @staticmethod
55
+ def encode(x: str) -> str:
56
+ return hashlib.sha224(x.encode()).hexdigest()
57
+
58
+ def _fn(self, name: str) -> str:
59
+ # NOTE: This method should not change as some may depend on it.
60
+ # See: https://github.com/ionrock/cachecontrol/issues/63
61
+ hashed = self.encode(name)
62
+ parts = list(hashed[:5]) + [hashed]
63
+ return os.path.join(self.directory, *parts)
64
+
65
+ def get(self, key: str) -> bytes | None:
66
+ name = self._fn(key)
67
+ try:
68
+ with open(name, "rb") as fh:
69
+ return fh.read()
70
+
71
+ except FileNotFoundError:
72
+ return None
73
+
74
+ def set(
75
+ self, key: str, value: bytes, expires: int | datetime | None = None
76
+ ) -> None:
77
+ name = self._fn(key)
78
+ self._write(name, value)
79
+
80
+ def _write(self, path: str, data: bytes) -> None:
81
+ """
82
+ Safely write the data to the given path.
83
+ """
84
+ # Make sure the directory exists
85
+ dirname = os.path.dirname(path)
86
+ os.makedirs(dirname, self.dirmode, exist_ok=True)
87
+
88
+ with self.lock_class(path + ".lock"):
89
+ # Write our actual file
90
+ (fd, name) = tempfile.mkstemp(dir=dirname)
91
+ try:
92
+ os.write(fd, data)
93
+ finally:
94
+ os.close(fd)
95
+ os.chmod(name, self.filemode)
96
+ os.replace(name, path)
97
+
98
+ def _delete(self, key: str, suffix: str) -> None:
99
+ name = self._fn(key) + suffix
100
+ if not self.forever:
101
+ try:
102
+ os.remove(name)
103
+ except FileNotFoundError:
104
+ pass
105
+
106
+
107
+ class FileCache(_FileCacheMixin, BaseCache):
108
+ """
109
+ Traditional FileCache: body is stored in memory, so not suitable for large
110
+ downloads.
111
+ """
112
+
113
+ def delete(self, key: str) -> None:
114
+ self._delete(key, "")
115
+
116
+
117
+ class SeparateBodyFileCache(_FileCacheMixin, SeparateBodyBaseCache):
118
+ """
119
+ Memory-efficient FileCache: body is stored in a separate file, reducing
120
+ peak memory usage.
121
+ """
122
+
123
+ def get_body(self, key: str) -> IO[bytes] | None:
124
+ name = self._fn(key) + ".body"
125
+ try:
126
+ return open(name, "rb")
127
+ except FileNotFoundError:
128
+ return None
129
+
130
+ def set_body(self, key: str, body: bytes) -> None:
131
+ name = self._fn(key) + ".body"
132
+ self._write(name, body)
133
+
134
+ def delete(self, key: str) -> None:
135
+ self._delete(key, "")
136
+ self._delete(key, ".body")
137
+
138
+
139
+ def url_to_file_path(url: str, filecache: FileCache) -> str:
140
+ """Return the file cache path based on the URL.
141
+
142
+ This does not ensure the file exists!
143
+ """
144
+ key = CacheController.cache_url(url)
145
+ return filecache._fn(key)
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from __future__ import annotations
5
+
6
+
7
+ from datetime import datetime, timezone
8
+ from typing import TYPE_CHECKING
9
+
10
+ from pip._vendor.cachecontrol.cache import BaseCache
11
+
12
+ if TYPE_CHECKING:
13
+ from redis import Redis
14
+
15
+
16
+ class RedisCache(BaseCache):
17
+ def __init__(self, conn: Redis[bytes]) -> None:
18
+ self.conn = conn
19
+
20
+ def get(self, key: str) -> bytes | None:
21
+ return self.conn.get(key)
22
+
23
+ def set(
24
+ self, key: str, value: bytes, expires: int | datetime | None = None
25
+ ) -> None:
26
+ if not expires:
27
+ self.conn.set(key, value)
28
+ elif isinstance(expires, datetime):
29
+ now_utc = datetime.now(timezone.utc)
30
+ if expires.tzinfo is None:
31
+ now_utc = now_utc.replace(tzinfo=None)
32
+ delta = expires - now_utc
33
+ self.conn.setex(key, int(delta.total_seconds()), value)
34
+ else:
35
+ self.conn.setex(key, expires, value)
36
+
37
+ def delete(self, key: str) -> None:
38
+ self.conn.delete(key)
39
+
40
+ def clear(self) -> None:
41
+ """Helper for clearing all the keys in a database. Use with
42
+ caution!"""
43
+ for key in self.conn.keys():
44
+ self.conn.delete(key)
45
+
46
+ def close(self) -> None:
47
+ """Redis uses connection pooling, no need to close the connection."""
48
+ pass
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/controller.py ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """
6
+ The httplib2 algorithms ported for use with requests.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import calendar
12
+ import logging
13
+ import re
14
+ import time
15
+ import weakref
16
+ from email.utils import parsedate_tz
17
+ from typing import TYPE_CHECKING, Collection, Mapping
18
+
19
+ from pip._vendor.requests.structures import CaseInsensitiveDict
20
+
21
+ from pip._vendor.cachecontrol.cache import DictCache, SeparateBodyBaseCache
22
+ from pip._vendor.cachecontrol.serialize import Serializer
23
+
24
+ if TYPE_CHECKING:
25
+ from typing import Literal
26
+
27
+ from pip._vendor.requests import PreparedRequest
28
+ from pip._vendor.urllib3 import HTTPResponse
29
+
30
+ from pip._vendor.cachecontrol.cache import BaseCache
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")
35
+
36
+ PERMANENT_REDIRECT_STATUSES = (301, 308)
37
+
38
+
39
+ def parse_uri(uri: str) -> tuple[str, str, str, str, str]:
40
+ """Parses a URI using the regex given in Appendix B of RFC 3986.
41
+
42
+ (scheme, authority, path, query, fragment) = parse_uri(uri)
43
+ """
44
+ match = URI.match(uri)
45
+ assert match is not None
46
+ groups = match.groups()
47
+ return (groups[1], groups[3], groups[4], groups[6], groups[8])
48
+
49
+
50
+ class CacheController:
51
+ """An interface to see if request should cached or not."""
52
+
53
+ def __init__(
54
+ self,
55
+ cache: BaseCache | None = None,
56
+ cache_etags: bool = True,
57
+ serializer: Serializer | None = None,
58
+ status_codes: Collection[int] | None = None,
59
+ ):
60
+ self.cache = DictCache() if cache is None else cache
61
+ self.cache_etags = cache_etags
62
+ self.serializer = serializer or Serializer()
63
+ self.cacheable_status_codes = status_codes or (200, 203, 300, 301, 308)
64
+
65
+ @classmethod
66
+ def _urlnorm(cls, uri: str) -> str:
67
+ """Normalize the URL to create a safe key for the cache"""
68
+ (scheme, authority, path, query, fragment) = parse_uri(uri)
69
+ if not scheme or not authority:
70
+ raise Exception("Only absolute URIs are allowed. uri = %s" % uri)
71
+
72
+ scheme = scheme.lower()
73
+ authority = authority.lower()
74
+
75
+ if not path:
76
+ path = "/"
77
+
78
+ # Could do syntax based normalization of the URI before
79
+ # computing the digest. See Section 6.2.2 of Std 66.
80
+ request_uri = query and "?".join([path, query]) or path
81
+ defrag_uri = scheme + "://" + authority + request_uri
82
+
83
+ return defrag_uri
84
+
85
+ @classmethod
86
+ def cache_url(cls, uri: str) -> str:
87
+ return cls._urlnorm(uri)
88
+
89
+ def parse_cache_control(self, headers: Mapping[str, str]) -> dict[str, int | None]:
90
+ known_directives = {
91
+ # https://tools.ietf.org/html/rfc7234#section-5.2
92
+ "max-age": (int, True),
93
+ "max-stale": (int, False),
94
+ "min-fresh": (int, True),
95
+ "no-cache": (None, False),
96
+ "no-store": (None, False),
97
+ "no-transform": (None, False),
98
+ "only-if-cached": (None, False),
99
+ "must-revalidate": (None, False),
100
+ "public": (None, False),
101
+ "private": (None, False),
102
+ "proxy-revalidate": (None, False),
103
+ "s-maxage": (int, True),
104
+ }
105
+
106
+ cc_headers = headers.get("cache-control", headers.get("Cache-Control", ""))
107
+
108
+ retval: dict[str, int | None] = {}
109
+
110
+ for cc_directive in cc_headers.split(","):
111
+ if not cc_directive.strip():
112
+ continue
113
+
114
+ parts = cc_directive.split("=", 1)
115
+ directive = parts[0].strip()
116
+
117
+ try:
118
+ typ, required = known_directives[directive]
119
+ except KeyError:
120
+ logger.debug("Ignoring unknown cache-control directive: %s", directive)
121
+ continue
122
+
123
+ if not typ or not required:
124
+ retval[directive] = None
125
+ if typ:
126
+ try:
127
+ retval[directive] = typ(parts[1].strip())
128
+ except IndexError:
129
+ if required:
130
+ logger.debug(
131
+ "Missing value for cache-control " "directive: %s",
132
+ directive,
133
+ )
134
+ except ValueError:
135
+ logger.debug(
136
+ "Invalid value for cache-control directive " "%s, must be %s",
137
+ directive,
138
+ typ.__name__,
139
+ )
140
+
141
+ return retval
142
+
143
+ def _load_from_cache(self, request: PreparedRequest) -> HTTPResponse | None:
144
+ """
145
+ Load a cached response, or return None if it's not available.
146
+ """
147
+ # We do not support caching of partial content: so if the request contains a
148
+ # Range header then we don't want to load anything from the cache.
149
+ if "Range" in request.headers:
150
+ return None
151
+
152
+ cache_url = request.url
153
+ assert cache_url is not None
154
+ cache_data = self.cache.get(cache_url)
155
+ if cache_data is None:
156
+ logger.debug("No cache entry available")
157
+ return None
158
+
159
+ if isinstance(self.cache, SeparateBodyBaseCache):
160
+ body_file = self.cache.get_body(cache_url)
161
+ else:
162
+ body_file = None
163
+
164
+ result = self.serializer.loads(request, cache_data, body_file)
165
+ if result is None:
166
+ logger.warning("Cache entry deserialization failed, entry ignored")
167
+ return result
168
+
169
+ def cached_request(self, request: PreparedRequest) -> HTTPResponse | Literal[False]:
170
+ """
171
+ Return a cached response if it exists in the cache, otherwise
172
+ return False.
173
+ """
174
+ assert request.url is not None
175
+ cache_url = self.cache_url(request.url)
176
+ logger.debug('Looking up "%s" in the cache', cache_url)
177
+ cc = self.parse_cache_control(request.headers)
178
+
179
+ # Bail out if the request insists on fresh data
180
+ if "no-cache" in cc:
181
+ logger.debug('Request header has "no-cache", cache bypassed')
182
+ return False
183
+
184
+ if "max-age" in cc and cc["max-age"] == 0:
185
+ logger.debug('Request header has "max_age" as 0, cache bypassed')
186
+ return False
187
+
188
+ # Check whether we can load the response from the cache:
189
+ resp = self._load_from_cache(request)
190
+ if not resp:
191
+ return False
192
+
193
+ # If we have a cached permanent redirect, return it immediately. We
194
+ # don't need to test our response for other headers b/c it is
195
+ # intrinsically "cacheable" as it is Permanent.
196
+ #
197
+ # See:
198
+ # https://tools.ietf.org/html/rfc7231#section-6.4.2
199
+ #
200
+ # Client can try to refresh the value by repeating the request
201
+ # with cache busting headers as usual (ie no-cache).
202
+ if int(resp.status) in PERMANENT_REDIRECT_STATUSES:
203
+ msg = (
204
+ "Returning cached permanent redirect response "
205
+ "(ignoring date and etag information)"
206
+ )
207
+ logger.debug(msg)
208
+ return resp
209
+
210
+ headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(resp.headers)
211
+ if not headers or "date" not in headers:
212
+ if "etag" not in headers:
213
+ # Without date or etag, the cached response can never be used
214
+ # and should be deleted.
215
+ logger.debug("Purging cached response: no date or etag")
216
+ self.cache.delete(cache_url)
217
+ logger.debug("Ignoring cached response: no date")
218
+ return False
219
+
220
+ now = time.time()
221
+ time_tuple = parsedate_tz(headers["date"])
222
+ assert time_tuple is not None
223
+ date = calendar.timegm(time_tuple[:6])
224
+ current_age = max(0, now - date)
225
+ logger.debug("Current age based on date: %i", current_age)
226
+
227
+ # TODO: There is an assumption that the result will be a
228
+ # urllib3 response object. This may not be best since we
229
+ # could probably avoid instantiating or constructing the
230
+ # response until we know we need it.
231
+ resp_cc = self.parse_cache_control(headers)
232
+
233
+ # determine freshness
234
+ freshness_lifetime = 0
235
+
236
+ # Check the max-age pragma in the cache control header
237
+ max_age = resp_cc.get("max-age")
238
+ if max_age is not None:
239
+ freshness_lifetime = max_age
240
+ logger.debug("Freshness lifetime from max-age: %i", freshness_lifetime)
241
+
242
+ # If there isn't a max-age, check for an expires header
243
+ elif "expires" in headers:
244
+ expires = parsedate_tz(headers["expires"])
245
+ if expires is not None:
246
+ expire_time = calendar.timegm(expires[:6]) - date
247
+ freshness_lifetime = max(0, expire_time)
248
+ logger.debug("Freshness lifetime from expires: %i", freshness_lifetime)
249
+
250
+ # Determine if we are setting freshness limit in the
251
+ # request. Note, this overrides what was in the response.
252
+ max_age = cc.get("max-age")
253
+ if max_age is not None:
254
+ freshness_lifetime = max_age
255
+ logger.debug(
256
+ "Freshness lifetime from request max-age: %i", freshness_lifetime
257
+ )
258
+
259
+ min_fresh = cc.get("min-fresh")
260
+ if min_fresh is not None:
261
+ # adjust our current age by our min fresh
262
+ current_age += min_fresh
263
+ logger.debug("Adjusted current age from min-fresh: %i", current_age)
264
+
265
+ # Return entry if it is fresh enough
266
+ if freshness_lifetime > current_age:
267
+ logger.debug('The response is "fresh", returning cached response')
268
+ logger.debug("%i > %i", freshness_lifetime, current_age)
269
+ return resp
270
+
271
+ # we're not fresh. If we don't have an Etag, clear it out
272
+ if "etag" not in headers:
273
+ logger.debug('The cached response is "stale" with no etag, purging')
274
+ self.cache.delete(cache_url)
275
+
276
+ # return the original handler
277
+ return False
278
+
279
+ def conditional_headers(self, request: PreparedRequest) -> dict[str, str]:
280
+ resp = self._load_from_cache(request)
281
+ new_headers = {}
282
+
283
+ if resp:
284
+ headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(resp.headers)
285
+
286
+ if "etag" in headers:
287
+ new_headers["If-None-Match"] = headers["ETag"]
288
+
289
+ if "last-modified" in headers:
290
+ new_headers["If-Modified-Since"] = headers["Last-Modified"]
291
+
292
+ return new_headers
293
+
294
+ def _cache_set(
295
+ self,
296
+ cache_url: str,
297
+ request: PreparedRequest,
298
+ response: HTTPResponse,
299
+ body: bytes | None = None,
300
+ expires_time: int | None = None,
301
+ ) -> None:
302
+ """
303
+ Store the data in the cache.
304
+ """
305
+ if isinstance(self.cache, SeparateBodyBaseCache):
306
+ # We pass in the body separately; just put a placeholder empty
307
+ # string in the metadata.
308
+ self.cache.set(
309
+ cache_url,
310
+ self.serializer.dumps(request, response, b""),
311
+ expires=expires_time,
312
+ )
313
+ # body is None can happen when, for example, we're only updating
314
+ # headers, as is the case in update_cached_response().
315
+ if body is not None:
316
+ self.cache.set_body(cache_url, body)
317
+ else:
318
+ self.cache.set(
319
+ cache_url,
320
+ self.serializer.dumps(request, response, body),
321
+ expires=expires_time,
322
+ )
323
+
324
+ def cache_response(
325
+ self,
326
+ request: PreparedRequest,
327
+ response_or_ref: HTTPResponse | weakref.ReferenceType[HTTPResponse],
328
+ body: bytes | None = None,
329
+ status_codes: Collection[int] | None = None,
330
+ ) -> None:
331
+ """
332
+ Algorithm for caching requests.
333
+
334
+ This assumes a requests Response object.
335
+ """
336
+ if isinstance(response_or_ref, weakref.ReferenceType):
337
+ response = response_or_ref()
338
+ if response is None:
339
+ # The weakref can be None only in case the user used streamed request
340
+ # and did not consume or close it, and holds no reference to requests.Response.
341
+ # In such case, we don't want to cache the response.
342
+ return
343
+ else:
344
+ response = response_or_ref
345
+
346
+ # From httplib2: Don't cache 206's since we aren't going to
347
+ # handle byte range requests
348
+ cacheable_status_codes = status_codes or self.cacheable_status_codes
349
+ if response.status not in cacheable_status_codes:
350
+ logger.debug(
351
+ "Status code %s not in %s", response.status, cacheable_status_codes
352
+ )
353
+ return
354
+
355
+ response_headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(
356
+ response.headers
357
+ )
358
+
359
+ if "date" in response_headers:
360
+ time_tuple = parsedate_tz(response_headers["date"])
361
+ assert time_tuple is not None
362
+ date = calendar.timegm(time_tuple[:6])
363
+ else:
364
+ date = 0
365
+
366
+ # If we've been given a body, our response has a Content-Length, that
367
+ # Content-Length is valid then we can check to see if the body we've
368
+ # been given matches the expected size, and if it doesn't we'll just
369
+ # skip trying to cache it.
370
+ if (
371
+ body is not None
372
+ and "content-length" in response_headers
373
+ and response_headers["content-length"].isdigit()
374
+ and int(response_headers["content-length"]) != len(body)
375
+ ):
376
+ return
377
+
378
+ cc_req = self.parse_cache_control(request.headers)
379
+ cc = self.parse_cache_control(response_headers)
380
+
381
+ assert request.url is not None
382
+ cache_url = self.cache_url(request.url)
383
+ logger.debug('Updating cache with response from "%s"', cache_url)
384
+
385
+ # Delete it from the cache if we happen to have it stored there
386
+ no_store = False
387
+ if "no-store" in cc:
388
+ no_store = True
389
+ logger.debug('Response header has "no-store"')
390
+ if "no-store" in cc_req:
391
+ no_store = True
392
+ logger.debug('Request header has "no-store"')
393
+ if no_store and self.cache.get(cache_url):
394
+ logger.debug('Purging existing cache entry to honor "no-store"')
395
+ self.cache.delete(cache_url)
396
+ if no_store:
397
+ return
398
+
399
+ # https://tools.ietf.org/html/rfc7234#section-4.1:
400
+ # A Vary header field-value of "*" always fails to match.
401
+ # Storing such a response leads to a deserialization warning
402
+ # during cache lookup and is not allowed to ever be served,
403
+ # so storing it can be avoided.
404
+ if "*" in response_headers.get("vary", ""):
405
+ logger.debug('Response header has "Vary: *"')
406
+ return
407
+
408
+ # If we've been given an etag, then keep the response
409
+ if self.cache_etags and "etag" in response_headers:
410
+ expires_time = 0
411
+ if response_headers.get("expires"):
412
+ expires = parsedate_tz(response_headers["expires"])
413
+ if expires is not None:
414
+ expires_time = calendar.timegm(expires[:6]) - date
415
+
416
+ expires_time = max(expires_time, 14 * 86400)
417
+
418
+ logger.debug(f"etag object cached for {expires_time} seconds")
419
+ logger.debug("Caching due to etag")
420
+ self._cache_set(cache_url, request, response, body, expires_time)
421
+
422
+ # Add to the cache any permanent redirects. We do this before looking
423
+ # that the Date headers.
424
+ elif int(response.status) in PERMANENT_REDIRECT_STATUSES:
425
+ logger.debug("Caching permanent redirect")
426
+ self._cache_set(cache_url, request, response, b"")
427
+
428
+ # Add to the cache if the response headers demand it. If there
429
+ # is no date header then we can't do anything about expiring
430
+ # the cache.
431
+ elif "date" in response_headers:
432
+ time_tuple = parsedate_tz(response_headers["date"])
433
+ assert time_tuple is not None
434
+ date = calendar.timegm(time_tuple[:6])
435
+ # cache when there is a max-age > 0
436
+ max_age = cc.get("max-age")
437
+ if max_age is not None and max_age > 0:
438
+ logger.debug("Caching b/c date exists and max-age > 0")
439
+ expires_time = max_age
440
+ self._cache_set(
441
+ cache_url,
442
+ request,
443
+ response,
444
+ body,
445
+ expires_time,
446
+ )
447
+
448
+ # If the request can expire, it means we should cache it
449
+ # in the meantime.
450
+ elif "expires" in response_headers:
451
+ if response_headers["expires"]:
452
+ expires = parsedate_tz(response_headers["expires"])
453
+ if expires is not None:
454
+ expires_time = calendar.timegm(expires[:6]) - date
455
+ else:
456
+ expires_time = None
457
+
458
+ logger.debug(
459
+ "Caching b/c of expires header. expires in {} seconds".format(
460
+ expires_time
461
+ )
462
+ )
463
+ self._cache_set(
464
+ cache_url,
465
+ request,
466
+ response,
467
+ body,
468
+ expires_time,
469
+ )
470
+
471
+ def update_cached_response(
472
+ self, request: PreparedRequest, response: HTTPResponse
473
+ ) -> HTTPResponse:
474
+ """On a 304 we will get a new set of headers that we want to
475
+ update our cached value with, assuming we have one.
476
+
477
+ This should only ever be called when we've sent an ETag and
478
+ gotten a 304 as the response.
479
+ """
480
+ assert request.url is not None
481
+ cache_url = self.cache_url(request.url)
482
+ cached_response = self._load_from_cache(request)
483
+
484
+ if not cached_response:
485
+ # we didn't have a cached response
486
+ return response
487
+
488
+ # Lets update our headers with the headers from the new request:
489
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-26#section-4.1
490
+ #
491
+ # The server isn't supposed to send headers that would make
492
+ # the cached body invalid. But... just in case, we'll be sure
493
+ # to strip out ones we know that might be problmatic due to
494
+ # typical assumptions.
495
+ excluded_headers = ["content-length"]
496
+
497
+ cached_response.headers.update(
498
+ {
499
+ k: v
500
+ for k, v in response.headers.items()
501
+ if k.lower() not in excluded_headers
502
+ }
503
+ )
504
+
505
+ # we want a 200 b/c we have content via the cache
506
+ cached_response.status = 200
507
+
508
+ # update our cache
509
+ self._cache_set(cache_url, request, cached_response)
510
+
511
+ return cached_response
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/filewrapper.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from __future__ import annotations
5
+
6
+ import mmap
7
+ from tempfile import NamedTemporaryFile
8
+ from typing import TYPE_CHECKING, Any, Callable
9
+
10
+ if TYPE_CHECKING:
11
+ from http.client import HTTPResponse
12
+
13
+
14
+ class CallbackFileWrapper:
15
+ """
16
+ Small wrapper around a fp object which will tee everything read into a
17
+ buffer, and when that file is closed it will execute a callback with the
18
+ contents of that buffer.
19
+
20
+ All attributes are proxied to the underlying file object.
21
+
22
+ This class uses members with a double underscore (__) leading prefix so as
23
+ not to accidentally shadow an attribute.
24
+
25
+ The data is stored in a temporary file until it is all available. As long
26
+ as the temporary files directory is disk-based (sometimes it's a
27
+ memory-backed-``tmpfs`` on Linux), data will be unloaded to disk if memory
28
+ pressure is high. For small files the disk usually won't be used at all,
29
+ it'll all be in the filesystem memory cache, so there should be no
30
+ performance impact.
31
+ """
32
+
33
+ def __init__(
34
+ self, fp: HTTPResponse, callback: Callable[[bytes], None] | None
35
+ ) -> None:
36
+ self.__buf = NamedTemporaryFile("rb+", delete=True)
37
+ self.__fp = fp
38
+ self.__callback = callback
39
+
40
+ def __getattr__(self, name: str) -> Any:
41
+ # The vagaries of garbage collection means that self.__fp is
42
+ # not always set. By using __getattribute__ and the private
43
+ # name[0] allows looking up the attribute value and raising an
44
+ # AttributeError when it doesn't exist. This stop things from
45
+ # infinitely recursing calls to getattr in the case where
46
+ # self.__fp hasn't been set.
47
+ #
48
+ # [0] https://docs.python.org/2/reference/expressions.html#atom-identifiers
49
+ fp = self.__getattribute__("_CallbackFileWrapper__fp")
50
+ return getattr(fp, name)
51
+
52
+ def __is_fp_closed(self) -> bool:
53
+ try:
54
+ return self.__fp.fp is None
55
+
56
+ except AttributeError:
57
+ pass
58
+
59
+ try:
60
+ closed: bool = self.__fp.closed
61
+ return closed
62
+
63
+ except AttributeError:
64
+ pass
65
+
66
+ # We just don't cache it then.
67
+ # TODO: Add some logging here...
68
+ return False
69
+
70
+ def _close(self) -> None:
71
+ if self.__callback:
72
+ if self.__buf.tell() == 0:
73
+ # Empty file:
74
+ result = b""
75
+ else:
76
+ # Return the data without actually loading it into memory,
77
+ # relying on Python's buffer API and mmap(). mmap() just gives
78
+ # a view directly into the filesystem's memory cache, so it
79
+ # doesn't result in duplicate memory use.
80
+ self.__buf.seek(0, 0)
81
+ result = memoryview(
82
+ mmap.mmap(self.__buf.fileno(), 0, access=mmap.ACCESS_READ)
83
+ )
84
+ self.__callback(result)
85
+
86
+ # We assign this to None here, because otherwise we can get into
87
+ # really tricky problems where the CPython interpreter dead locks
88
+ # because the callback is holding a reference to something which
89
+ # has a __del__ method. Setting this to None breaks the cycle
90
+ # and allows the garbage collector to do it's thing normally.
91
+ self.__callback = None
92
+
93
+ # Closing the temporary file releases memory and frees disk space.
94
+ # Important when caching big files.
95
+ self.__buf.close()
96
+
97
+ def read(self, amt: int | None = None) -> bytes:
98
+ data: bytes = self.__fp.read(amt)
99
+ if data:
100
+ # We may be dealing with b'', a sign that things are over:
101
+ # it's passed e.g. after we've already closed self.__buf.
102
+ self.__buf.write(data)
103
+ if self.__is_fp_closed():
104
+ self._close()
105
+
106
+ return data
107
+
108
+ def _safe_read(self, amt: int) -> bytes:
109
+ data: bytes = self.__fp._safe_read(amt) # type: ignore[attr-defined]
110
+ if amt == 2 and data == b"\r\n":
111
+ # urllib executes this read to toss the CRLF at the end
112
+ # of the chunk.
113
+ return data
114
+
115
+ self.__buf.write(data)
116
+ if self.__is_fp_closed():
117
+ self._close()
118
+
119
+ return data
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/heuristics.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from __future__ import annotations
5
+
6
+ import calendar
7
+ import time
8
+ from datetime import datetime, timedelta, timezone
9
+ from email.utils import formatdate, parsedate, parsedate_tz
10
+ from typing import TYPE_CHECKING, Any, Mapping
11
+
12
+ if TYPE_CHECKING:
13
+ from pip._vendor.urllib3 import HTTPResponse
14
+
15
+ TIME_FMT = "%a, %d %b %Y %H:%M:%S GMT"
16
+
17
+
18
+ def expire_after(delta: timedelta, date: datetime | None = None) -> datetime:
19
+ date = date or datetime.now(timezone.utc)
20
+ return date + delta
21
+
22
+
23
+ def datetime_to_header(dt: datetime) -> str:
24
+ return formatdate(calendar.timegm(dt.timetuple()))
25
+
26
+
27
+ class BaseHeuristic:
28
+ def warning(self, response: HTTPResponse) -> str | None:
29
+ """
30
+ Return a valid 1xx warning header value describing the cache
31
+ adjustments.
32
+
33
+ The response is provided too allow warnings like 113
34
+ http://tools.ietf.org/html/rfc7234#section-5.5.4 where we need
35
+ to explicitly say response is over 24 hours old.
36
+ """
37
+ return '110 - "Response is Stale"'
38
+
39
+ def update_headers(self, response: HTTPResponse) -> dict[str, str]:
40
+ """Update the response headers with any new headers.
41
+
42
+ NOTE: This SHOULD always include some Warning header to
43
+ signify that the response was cached by the client, not
44
+ by way of the provided headers.
45
+ """
46
+ return {}
47
+
48
+ def apply(self, response: HTTPResponse) -> HTTPResponse:
49
+ updated_headers = self.update_headers(response)
50
+
51
+ if updated_headers:
52
+ response.headers.update(updated_headers)
53
+ warning_header_value = self.warning(response)
54
+ if warning_header_value is not None:
55
+ response.headers.update({"Warning": warning_header_value})
56
+
57
+ return response
58
+
59
+
60
+ class OneDayCache(BaseHeuristic):
61
+ """
62
+ Cache the response by providing an expires 1 day in the
63
+ future.
64
+ """
65
+
66
+ def update_headers(self, response: HTTPResponse) -> dict[str, str]:
67
+ headers = {}
68
+
69
+ if "expires" not in response.headers:
70
+ date = parsedate(response.headers["date"])
71
+ expires = expire_after(
72
+ timedelta(days=1),
73
+ date=datetime(*date[:6], tzinfo=timezone.utc), # type: ignore[index,misc]
74
+ )
75
+ headers["expires"] = datetime_to_header(expires)
76
+ headers["cache-control"] = "public"
77
+ return headers
78
+
79
+
80
+ class ExpiresAfter(BaseHeuristic):
81
+ """
82
+ Cache **all** requests for a defined time period.
83
+ """
84
+
85
+ def __init__(self, **kw: Any) -> None:
86
+ self.delta = timedelta(**kw)
87
+
88
+ def update_headers(self, response: HTTPResponse) -> dict[str, str]:
89
+ expires = expire_after(self.delta)
90
+ return {"expires": datetime_to_header(expires), "cache-control": "public"}
91
+
92
+ def warning(self, response: HTTPResponse) -> str | None:
93
+ tmpl = "110 - Automatically cached for %s. Response might be stale"
94
+ return tmpl % self.delta
95
+
96
+
97
+ class LastModified(BaseHeuristic):
98
+ """
99
+ If there is no Expires header already, fall back on Last-Modified
100
+ using the heuristic from
101
+ http://tools.ietf.org/html/rfc7234#section-4.2.2
102
+ to calculate a reasonable value.
103
+
104
+ Firefox also does something like this per
105
+ https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching_FAQ
106
+ http://lxr.mozilla.org/mozilla-release/source/netwerk/protocol/http/nsHttpResponseHead.cpp#397
107
+ Unlike mozilla we limit this to 24-hr.
108
+ """
109
+
110
+ cacheable_by_default_statuses = {
111
+ 200,
112
+ 203,
113
+ 204,
114
+ 206,
115
+ 300,
116
+ 301,
117
+ 404,
118
+ 405,
119
+ 410,
120
+ 414,
121
+ 501,
122
+ }
123
+
124
+ def update_headers(self, resp: HTTPResponse) -> dict[str, str]:
125
+ headers: Mapping[str, str] = resp.headers
126
+
127
+ if "expires" in headers:
128
+ return {}
129
+
130
+ if "cache-control" in headers and headers["cache-control"] != "public":
131
+ return {}
132
+
133
+ if resp.status not in self.cacheable_by_default_statuses:
134
+ return {}
135
+
136
+ if "date" not in headers or "last-modified" not in headers:
137
+ return {}
138
+
139
+ time_tuple = parsedate_tz(headers["date"])
140
+ assert time_tuple is not None
141
+ date = calendar.timegm(time_tuple[:6])
142
+ last_modified = parsedate(headers["last-modified"])
143
+ if last_modified is None:
144
+ return {}
145
+
146
+ now = time.time()
147
+ current_age = max(0, now - date)
148
+ delta = date - calendar.timegm(last_modified)
149
+ freshness_lifetime = max(0, min(delta / 10, 24 * 3600))
150
+ if freshness_lifetime <= current_age:
151
+ return {}
152
+
153
+ expires = date + freshness_lifetime
154
+ return {"expires": time.strftime(TIME_FMT, time.gmtime(expires))}
155
+
156
+ def warning(self, resp: HTTPResponse) -> str | None:
157
+ return None
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/py.typed ADDED
File without changes
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/serialize.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from __future__ import annotations
5
+
6
+ import io
7
+ from typing import IO, TYPE_CHECKING, Any, Mapping, cast
8
+
9
+ from pip._vendor import msgpack
10
+ from pip._vendor.requests.structures import CaseInsensitiveDict
11
+ from pip._vendor.urllib3 import HTTPResponse
12
+
13
+ if TYPE_CHECKING:
14
+ from pip._vendor.requests import PreparedRequest
15
+
16
+
17
+ class Serializer:
18
+ serde_version = "4"
19
+
20
+ def dumps(
21
+ self,
22
+ request: PreparedRequest,
23
+ response: HTTPResponse,
24
+ body: bytes | None = None,
25
+ ) -> bytes:
26
+ response_headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(
27
+ response.headers
28
+ )
29
+
30
+ if body is None:
31
+ # When a body isn't passed in, we'll read the response. We
32
+ # also update the response with a new file handler to be
33
+ # sure it acts as though it was never read.
34
+ body = response.read(decode_content=False)
35
+ response._fp = io.BytesIO(body) # type: ignore[assignment]
36
+ response.length_remaining = len(body)
37
+
38
+ data = {
39
+ "response": {
40
+ "body": body, # Empty bytestring if body is stored separately
41
+ "headers": {str(k): str(v) for k, v in response.headers.items()},
42
+ "status": response.status,
43
+ "version": response.version,
44
+ "reason": str(response.reason),
45
+ "decode_content": response.decode_content,
46
+ }
47
+ }
48
+
49
+ # Construct our vary headers
50
+ data["vary"] = {}
51
+ if "vary" in response_headers:
52
+ varied_headers = response_headers["vary"].split(",")
53
+ for header in varied_headers:
54
+ header = str(header).strip()
55
+ header_value = request.headers.get(header, None)
56
+ if header_value is not None:
57
+ header_value = str(header_value)
58
+ data["vary"][header] = header_value
59
+
60
+ return b",".join([f"cc={self.serde_version}".encode(), self.serialize(data)])
61
+
62
+ def serialize(self, data: dict[str, Any]) -> bytes:
63
+ return cast(bytes, msgpack.dumps(data, use_bin_type=True))
64
+
65
+ def loads(
66
+ self,
67
+ request: PreparedRequest,
68
+ data: bytes,
69
+ body_file: IO[bytes] | None = None,
70
+ ) -> HTTPResponse | None:
71
+ # Short circuit if we've been given an empty set of data
72
+ if not data:
73
+ return None
74
+
75
+ # Previous versions of this library supported other serialization
76
+ # formats, but these have all been removed.
77
+ if not data.startswith(f"cc={self.serde_version},".encode()):
78
+ return None
79
+
80
+ data = data[5:]
81
+ return self._loads_v4(request, data, body_file)
82
+
83
+ def prepare_response(
84
+ self,
85
+ request: PreparedRequest,
86
+ cached: Mapping[str, Any],
87
+ body_file: IO[bytes] | None = None,
88
+ ) -> HTTPResponse | None:
89
+ """Verify our vary headers match and construct a real urllib3
90
+ HTTPResponse object.
91
+ """
92
+ # Special case the '*' Vary value as it means we cannot actually
93
+ # determine if the cached response is suitable for this request.
94
+ # This case is also handled in the controller code when creating
95
+ # a cache entry, but is left here for backwards compatibility.
96
+ if "*" in cached.get("vary", {}):
97
+ return None
98
+
99
+ # Ensure that the Vary headers for the cached response match our
100
+ # request
101
+ for header, value in cached.get("vary", {}).items():
102
+ if request.headers.get(header, None) != value:
103
+ return None
104
+
105
+ body_raw = cached["response"].pop("body")
106
+
107
+ headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(
108
+ data=cached["response"]["headers"]
109
+ )
110
+ if headers.get("transfer-encoding", "") == "chunked":
111
+ headers.pop("transfer-encoding")
112
+
113
+ cached["response"]["headers"] = headers
114
+
115
+ try:
116
+ body: IO[bytes]
117
+ if body_file is None:
118
+ body = io.BytesIO(body_raw)
119
+ else:
120
+ body = body_file
121
+ except TypeError:
122
+ # This can happen if cachecontrol serialized to v1 format (pickle)
123
+ # using Python 2. A Python 2 str(byte string) will be unpickled as
124
+ # a Python 3 str (unicode string), which will cause the above to
125
+ # fail with:
126
+ #
127
+ # TypeError: 'str' does not support the buffer interface
128
+ body = io.BytesIO(body_raw.encode("utf8"))
129
+
130
+ # Discard any `strict` parameter serialized by older version of cachecontrol.
131
+ cached["response"].pop("strict", None)
132
+
133
+ return HTTPResponse(body=body, preload_content=False, **cached["response"])
134
+
135
+ def _loads_v4(
136
+ self,
137
+ request: PreparedRequest,
138
+ data: bytes,
139
+ body_file: IO[bytes] | None = None,
140
+ ) -> HTTPResponse | None:
141
+ try:
142
+ cached = msgpack.loads(data, raw=False)
143
+ except ValueError:
144
+ return None
145
+
146
+ return self.prepare_response(request, cached, body_file)
.venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/wrapper.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: 2015 Eric Larson
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from __future__ import annotations
5
+
6
+ from typing import TYPE_CHECKING, Collection
7
+
8
+ from pip._vendor.cachecontrol.adapter import CacheControlAdapter
9
+ from pip._vendor.cachecontrol.cache import DictCache
10
+
11
+ if TYPE_CHECKING:
12
+ from pip._vendor import requests
13
+
14
+ from pip._vendor.cachecontrol.cache import BaseCache
15
+ from pip._vendor.cachecontrol.controller import CacheController
16
+ from pip._vendor.cachecontrol.heuristics import BaseHeuristic
17
+ from pip._vendor.cachecontrol.serialize import Serializer
18
+
19
+
20
+ def CacheControl(
21
+ sess: requests.Session,
22
+ cache: BaseCache | None = None,
23
+ cache_etags: bool = True,
24
+ serializer: Serializer | None = None,
25
+ heuristic: BaseHeuristic | None = None,
26
+ controller_class: type[CacheController] | None = None,
27
+ adapter_class: type[CacheControlAdapter] | None = None,
28
+ cacheable_methods: Collection[str] | None = None,
29
+ ) -> requests.Session:
30
+ cache = DictCache() if cache is None else cache
31
+ adapter_class = adapter_class or CacheControlAdapter
32
+ adapter = adapter_class(
33
+ cache,
34
+ cache_etags=cache_etags,
35
+ serializer=serializer,
36
+ heuristic=heuristic,
37
+ controller_class=controller_class,
38
+ cacheable_methods=cacheable_methods,
39
+ )
40
+ sess.mount("http://", adapter)
41
+ sess.mount("https://", adapter)
42
+
43
+ return sess
.venv/lib/python3.13/site-packages/pip/_vendor/certifi/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .core import contents, where
2
+
3
+ __all__ = ["contents", "where"]
4
+ __version__ = "2025.01.31"
.venv/lib/python3.13/site-packages/pip/_vendor/certifi/__main__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+
3
+ from pip._vendor.certifi import contents, where
4
+
5
+ parser = argparse.ArgumentParser()
6
+ parser.add_argument("-c", "--contents", action="store_true")
7
+ args = parser.parse_args()
8
+
9
+ if args.contents:
10
+ print(contents())
11
+ else:
12
+ print(where())
.venv/lib/python3.13/site-packages/pip/_vendor/certifi/cacert.pem ADDED
The diff for this file is too large to render. See raw diff
 
.venv/lib/python3.13/site-packages/pip/_vendor/certifi/core.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ certifi.py
3
+ ~~~~~~~~~~
4
+
5
+ This module returns the installation location of cacert.pem or its contents.
6
+ """
7
+ import sys
8
+ import atexit
9
+
10
+ def exit_cacert_ctx() -> None:
11
+ _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr]
12
+
13
+ DEBIAN_CA_CERTS_PATH = '/etc/ssl/certs/ca-certificates.crt'
14
+
15
+ if sys.version_info >= (3, 11):
16
+
17
+ from importlib.resources import as_file, files
18
+
19
+ _CACERT_CTX = None
20
+ _CACERT_PATH = None
21
+
22
+ def where() -> str:
23
+ # This is slightly terrible, but we want to delay extracting the file
24
+ # in cases where we're inside of a zipimport situation until someone
25
+ # actually calls where(), but we don't want to re-extract the file
26
+ # on every call of where(), so we'll do it once then store it in a
27
+ # global variable.
28
+ global _CACERT_CTX
29
+ global _CACERT_PATH
30
+ if _CACERT_PATH is None:
31
+ # This is slightly janky, the importlib.resources API wants you to
32
+ # manage the cleanup of this file, so it doesn't actually return a
33
+ # path, it returns a context manager that will give you the path
34
+ # when you enter it and will do any cleanup when you leave it. In
35
+ # the common case of not needing a temporary file, it will just
36
+ # return the file system location and the __exit__() is a no-op.
37
+ #
38
+ # We also have to hold onto the actual context manager, because
39
+ # it will do the cleanup whenever it gets garbage collected, so
40
+ # we will also store that at the global level as well.
41
+ _CACERT_CTX = as_file(files("pip._vendor.certifi").joinpath("cacert.pem"))
42
+ _CACERT_PATH = str(_CACERT_CTX.__enter__())
43
+ atexit.register(exit_cacert_ctx)
44
+
45
+ return _CACERT_PATH
46
+
47
+ def contents() -> str:
48
+ return files("pip._vendor.certifi").joinpath("cacert.pem").read_text(encoding="ascii")
49
+
50
+ elif sys.version_info >= (3, 7):
51
+
52
+ from importlib.resources import path as get_path, read_text
53
+
54
+ _CACERT_CTX = None
55
+ _CACERT_PATH = None
56
+
57
+ def where() -> str:
58
+ # This is slightly terrible, but we want to delay extracting the
59
+ # file in cases where we're inside of a zipimport situation until
60
+ # someone actually calls where(), but we don't want to re-extract
61
+ # the file on every call of where(), so we'll do it once then store
62
+ # it in a global variable.
63
+ global _CACERT_CTX
64
+ global _CACERT_PATH
65
+ if _CACERT_PATH is None:
66
+ # This is slightly janky, the importlib.resources API wants you
67
+ # to manage the cleanup of this file, so it doesn't actually
68
+ # return a path, it returns a context manager that will give
69
+ # you the path when you enter it and will do any cleanup when
70
+ # you leave it. In the common case of not needing a temporary
71
+ # file, it will just return the file system location and the
72
+ # __exit__() is a no-op.
73
+ #
74
+ # We also have to hold onto the actual context manager, because
75
+ # it will do the cleanup whenever it gets garbage collected, so
76
+ # we will also store that at the global level as well.
77
+ _CACERT_CTX = get_path("pip._vendor.certifi", "cacert.pem")
78
+ _CACERT_PATH = str(_CACERT_CTX.__enter__())
79
+ atexit.register(exit_cacert_ctx)
80
+
81
+ return _CACERT_PATH
82
+
83
+ def contents() -> str:
84
+ return read_text("pip._vendor.certifi", "cacert.pem", encoding="ascii")
85
+
86
+ else:
87
+ import os
88
+ import types
89
+ from typing import Union
90
+
91
+ Package = Union[types.ModuleType, str]
92
+ Resource = Union[str, "os.PathLike"]
93
+
94
+ # This fallback will work for Python versions prior to 3.7 that lack the
95
+ # importlib.resources module but relies on the existing `where` function
96
+ # so won't address issues with environments like PyOxidizer that don't set
97
+ # __file__ on modules.
98
+ def read_text(
99
+ package: Package,
100
+ resource: Resource,
101
+ encoding: str = 'utf-8',
102
+ errors: str = 'strict'
103
+ ) -> str:
104
+ with open(where(), encoding=encoding) as data:
105
+ return data.read()
106
+
107
+ # If we don't have importlib.resources, then we will just do the old logic
108
+ # of assuming we're on the filesystem and munge the path directly.
109
+ def where() -> str:
110
+ f = os.path.dirname(__file__)
111
+
112
+ return os.path.join(f, "cacert.pem")
113
+
114
+ def contents() -> str:
115
+ return read_text("pip._vendor.certifi", "cacert.pem", encoding="ascii")
116
+
117
+
118
+ # Debian: Use system CA certs:
119
+ def where() -> str:
120
+ return DEBIAN_CA_CERTS_PATH
121
+
122
+
123
+ def contents() -> str:
124
+ with open(where(), "r", encoding="ascii") as data:
125
+ return data.read()
.venv/lib/python3.13/site-packages/pip/_vendor/certifi/py.typed ADDED
File without changes
.venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ._implementation import (
2
+ CyclicDependencyError,
3
+ DependencyGroupInclude,
4
+ DependencyGroupResolver,
5
+ resolve,
6
+ )
7
+
8
+ __all__ = (
9
+ "CyclicDependencyError",
10
+ "DependencyGroupInclude",
11
+ "DependencyGroupResolver",
12
+ "resolve",
13
+ )
.venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__main__.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import sys
3
+
4
+ from ._implementation import resolve
5
+ from ._toml_compat import tomllib
6
+
7
+
8
+ def main() -> None:
9
+ if tomllib is None:
10
+ print(
11
+ "Usage error: dependency-groups CLI requires tomli or Python 3.11+",
12
+ file=sys.stderr,
13
+ )
14
+ raise SystemExit(2)
15
+
16
+ parser = argparse.ArgumentParser(
17
+ description=(
18
+ "A dependency-groups CLI. Prints out a resolved group, newline-delimited."
19
+ )
20
+ )
21
+ parser.add_argument(
22
+ "GROUP_NAME", nargs="*", help="The dependency group(s) to resolve."
23
+ )
24
+ parser.add_argument(
25
+ "-f",
26
+ "--pyproject-file",
27
+ default="pyproject.toml",
28
+ help="The pyproject.toml file. Defaults to trying in the current directory.",
29
+ )
30
+ parser.add_argument(
31
+ "-o",
32
+ "--output",
33
+ help="An output file. Defaults to stdout.",
34
+ )
35
+ parser.add_argument(
36
+ "-l",
37
+ "--list",
38
+ action="store_true",
39
+ help="List the available dependency groups",
40
+ )
41
+ args = parser.parse_args()
42
+
43
+ with open(args.pyproject_file, "rb") as fp:
44
+ pyproject = tomllib.load(fp)
45
+
46
+ dependency_groups_raw = pyproject.get("dependency-groups", {})
47
+
48
+ if args.list:
49
+ print(*dependency_groups_raw.keys())
50
+ return
51
+ if not args.GROUP_NAME:
52
+ print("A GROUP_NAME is required", file=sys.stderr)
53
+ raise SystemExit(3)
54
+
55
+ content = "\n".join(resolve(dependency_groups_raw, *args.GROUP_NAME))
56
+
57
+ if args.output is None or args.output == "-":
58
+ print(content)
59
+ else:
60
+ with open(args.output, "w", encoding="utf-8") as fp:
61
+ print(content, file=fp)
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
.venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_implementation.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import re
5
+ from collections.abc import Mapping
6
+
7
+ from pip._vendor.packaging.requirements import Requirement
8
+
9
+
10
+ def _normalize_name(name: str) -> str:
11
+ return re.sub(r"[-_.]+", "-", name).lower()
12
+
13
+
14
+ def _normalize_group_names(
15
+ dependency_groups: Mapping[str, str | Mapping[str, str]],
16
+ ) -> Mapping[str, str | Mapping[str, str]]:
17
+ original_names: dict[str, list[str]] = {}
18
+ normalized_groups = {}
19
+
20
+ for group_name, value in dependency_groups.items():
21
+ normed_group_name = _normalize_name(group_name)
22
+ original_names.setdefault(normed_group_name, []).append(group_name)
23
+ normalized_groups[normed_group_name] = value
24
+
25
+ errors = []
26
+ for normed_name, names in original_names.items():
27
+ if len(names) > 1:
28
+ errors.append(f"{normed_name} ({', '.join(names)})")
29
+ if errors:
30
+ raise ValueError(f"Duplicate dependency group names: {', '.join(errors)}")
31
+
32
+ return normalized_groups
33
+
34
+
35
+ @dataclasses.dataclass
36
+ class DependencyGroupInclude:
37
+ include_group: str
38
+
39
+
40
+ class CyclicDependencyError(ValueError):
41
+ """
42
+ An error representing the detection of a cycle.
43
+ """
44
+
45
+ def __init__(self, requested_group: str, group: str, include_group: str) -> None:
46
+ self.requested_group = requested_group
47
+ self.group = group
48
+ self.include_group = include_group
49
+
50
+ if include_group == group:
51
+ reason = f"{group} includes itself"
52
+ else:
53
+ reason = f"{include_group} -> {group}, {group} -> {include_group}"
54
+ super().__init__(
55
+ "Cyclic dependency group include while resolving "
56
+ f"{requested_group}: {reason}"
57
+ )
58
+
59
+
60
+ class DependencyGroupResolver:
61
+ """
62
+ A resolver for Dependency Group data.
63
+
64
+ This class handles caching, name normalization, cycle detection, and other
65
+ parsing requirements. There are only two public methods for exploring the data:
66
+ ``lookup()`` and ``resolve()``.
67
+
68
+ :param dependency_groups: A mapping, as provided via pyproject
69
+ ``[dependency-groups]``.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ dependency_groups: Mapping[str, str | Mapping[str, str]],
75
+ ) -> None:
76
+ if not isinstance(dependency_groups, Mapping):
77
+ raise TypeError("Dependency Groups table is not a mapping")
78
+ self.dependency_groups = _normalize_group_names(dependency_groups)
79
+ # a map of group names to parsed data
80
+ self._parsed_groups: dict[
81
+ str, tuple[Requirement | DependencyGroupInclude, ...]
82
+ ] = {}
83
+ # a map of group names to their ancestors, used for cycle detection
84
+ self._include_graph_ancestors: dict[str, tuple[str, ...]] = {}
85
+ # a cache of completed resolutions to Requirement lists
86
+ self._resolve_cache: dict[str, tuple[Requirement, ...]] = {}
87
+
88
+ def lookup(self, group: str) -> tuple[Requirement | DependencyGroupInclude, ...]:
89
+ """
90
+ Lookup a group name, returning the parsed dependency data for that group.
91
+ This will not resolve includes.
92
+
93
+ :param group: the name of the group to lookup
94
+
95
+ :raises ValueError: if the data does not appear to be valid dependency group
96
+ data
97
+ :raises TypeError: if the data is not a string
98
+ :raises LookupError: if group name is absent
99
+ :raises packaging.requirements.InvalidRequirement: if a specifier is not valid
100
+ """
101
+ if not isinstance(group, str):
102
+ raise TypeError("Dependency group name is not a str")
103
+ group = _normalize_name(group)
104
+ return self._parse_group(group)
105
+
106
+ def resolve(self, group: str) -> tuple[Requirement, ...]:
107
+ """
108
+ Resolve a dependency group to a list of requirements.
109
+
110
+ :param group: the name of the group to resolve
111
+
112
+ :raises TypeError: if the inputs appear to be the wrong types
113
+ :raises ValueError: if the data does not appear to be valid dependency group
114
+ data
115
+ :raises LookupError: if group name is absent
116
+ :raises packaging.requirements.InvalidRequirement: if a specifier is not valid
117
+ """
118
+ if not isinstance(group, str):
119
+ raise TypeError("Dependency group name is not a str")
120
+ group = _normalize_name(group)
121
+ return self._resolve(group, group)
122
+
123
+ def _parse_group(
124
+ self, group: str
125
+ ) -> tuple[Requirement | DependencyGroupInclude, ...]:
126
+ # short circuit -- never do the work twice
127
+ if group in self._parsed_groups:
128
+ return self._parsed_groups[group]
129
+
130
+ if group not in self.dependency_groups:
131
+ raise LookupError(f"Dependency group '{group}' not found")
132
+
133
+ raw_group = self.dependency_groups[group]
134
+ if not isinstance(raw_group, list):
135
+ raise TypeError(f"Dependency group '{group}' is not a list")
136
+
137
+ elements: list[Requirement | DependencyGroupInclude] = []
138
+ for item in raw_group:
139
+ if isinstance(item, str):
140
+ # packaging.requirements.Requirement parsing ensures that this is a
141
+ # valid PEP 508 Dependency Specifier
142
+ # raises InvalidRequirement on failure
143
+ elements.append(Requirement(item))
144
+ elif isinstance(item, dict):
145
+ if tuple(item.keys()) != ("include-group",):
146
+ raise ValueError(f"Invalid dependency group item: {item}")
147
+
148
+ include_group = next(iter(item.values()))
149
+ elements.append(DependencyGroupInclude(include_group=include_group))
150
+ else:
151
+ raise ValueError(f"Invalid dependency group item: {item}")
152
+
153
+ self._parsed_groups[group] = tuple(elements)
154
+ return self._parsed_groups[group]
155
+
156
+ def _resolve(self, group: str, requested_group: str) -> tuple[Requirement, ...]:
157
+ """
158
+ This is a helper for cached resolution to strings.
159
+
160
+ :param group: The name of the group to resolve.
161
+ :param requested_group: The group which was used in the original, user-facing
162
+ request.
163
+ """
164
+ if group in self._resolve_cache:
165
+ return self._resolve_cache[group]
166
+
167
+ parsed = self._parse_group(group)
168
+
169
+ resolved_group = []
170
+ for item in parsed:
171
+ if isinstance(item, Requirement):
172
+ resolved_group.append(item)
173
+ elif isinstance(item, DependencyGroupInclude):
174
+ include_group = _normalize_name(item.include_group)
175
+ if include_group in self._include_graph_ancestors.get(group, ()):
176
+ raise CyclicDependencyError(
177
+ requested_group, group, item.include_group
178
+ )
179
+ self._include_graph_ancestors[include_group] = (
180
+ *self._include_graph_ancestors.get(group, ()),
181
+ group,
182
+ )
183
+ resolved_group.extend(self._resolve(include_group, requested_group))
184
+ else: # unreachable
185
+ raise NotImplementedError(
186
+ f"Invalid dependency group item after parse: {item}"
187
+ )
188
+
189
+ self._resolve_cache[group] = tuple(resolved_group)
190
+ return self._resolve_cache[group]
191
+
192
+
193
+ def resolve(
194
+ dependency_groups: Mapping[str, str | Mapping[str, str]], /, *groups: str
195
+ ) -> tuple[str, ...]:
196
+ """
197
+ Resolve a dependency group to a tuple of requirements, as strings.
198
+
199
+ :param dependency_groups: the parsed contents of the ``[dependency-groups]`` table
200
+ from ``pyproject.toml``
201
+ :param groups: the name of the group(s) to resolve
202
+
203
+ :raises TypeError: if the inputs appear to be the wrong types
204
+ :raises ValueError: if the data does not appear to be valid dependency group data
205
+ :raises LookupError: if group name is absent
206
+ :raises packaging.requirements.InvalidRequirement: if a specifier is not valid
207
+ """
208
+ resolver = DependencyGroupResolver(dependency_groups)
209
+ return tuple(str(r) for group in groups for r in resolver.resolve(group))
.venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_lint_dependency_groups.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from ._implementation import DependencyGroupResolver
7
+ from ._toml_compat import tomllib
8
+
9
+
10
+ def main(*, argv: list[str] | None = None) -> None:
11
+ if tomllib is None:
12
+ print(
13
+ "Usage error: dependency-groups CLI requires tomli or Python 3.11+",
14
+ file=sys.stderr,
15
+ )
16
+ raise SystemExit(2)
17
+
18
+ parser = argparse.ArgumentParser(
19
+ description=(
20
+ "Lint Dependency Groups for validity. "
21
+ "This will eagerly load and check all of your Dependency Groups."
22
+ )
23
+ )
24
+ parser.add_argument(
25
+ "-f",
26
+ "--pyproject-file",
27
+ default="pyproject.toml",
28
+ help="The pyproject.toml file. Defaults to trying in the current directory.",
29
+ )
30
+ args = parser.parse_args(argv if argv is not None else sys.argv[1:])
31
+
32
+ with open(args.pyproject_file, "rb") as fp:
33
+ pyproject = tomllib.load(fp)
34
+ dependency_groups_raw = pyproject.get("dependency-groups", {})
35
+
36
+ errors: list[str] = []
37
+ try:
38
+ resolver = DependencyGroupResolver(dependency_groups_raw)
39
+ except (ValueError, TypeError) as e:
40
+ errors.append(f"{type(e).__name__}: {e}")
41
+ else:
42
+ for groupname in resolver.dependency_groups:
43
+ try:
44
+ resolver.resolve(groupname)
45
+ except (LookupError, ValueError, TypeError) as e:
46
+ errors.append(f"{type(e).__name__}: {e}")
47
+
48
+ if errors:
49
+ print("errors encountered while examining dependency groups:")
50
+ for msg in errors:
51
+ print(f" {msg}")
52
+ sys.exit(1)
53
+ else:
54
+ print("ok")
55
+ sys.exit(0)
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
.venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_pip_wrapper.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import subprocess
5
+ import sys
6
+
7
+ from ._implementation import DependencyGroupResolver
8
+ from ._toml_compat import tomllib
9
+
10
+
11
+ def _invoke_pip(deps: list[str]) -> None:
12
+ subprocess.check_call([sys.executable, "-m", "pip", "install", *deps])
13
+
14
+
15
+ def main(*, argv: list[str] | None = None) -> None:
16
+ if tomllib is None:
17
+ print(
18
+ "Usage error: dependency-groups CLI requires tomli or Python 3.11+",
19
+ file=sys.stderr,
20
+ )
21
+ raise SystemExit(2)
22
+
23
+ parser = argparse.ArgumentParser(description="Install Dependency Groups.")
24
+ parser.add_argument(
25
+ "DEPENDENCY_GROUP", nargs="+", help="The dependency groups to install."
26
+ )
27
+ parser.add_argument(
28
+ "-f",
29
+ "--pyproject-file",
30
+ default="pyproject.toml",
31
+ help="The pyproject.toml file. Defaults to trying in the current directory.",
32
+ )
33
+ args = parser.parse_args(argv if argv is not None else sys.argv[1:])
34
+
35
+ with open(args.pyproject_file, "rb") as fp:
36
+ pyproject = tomllib.load(fp)
37
+ dependency_groups_raw = pyproject.get("dependency-groups", {})
38
+
39
+ errors: list[str] = []
40
+ resolved: list[str] = []
41
+ try:
42
+ resolver = DependencyGroupResolver(dependency_groups_raw)
43
+ except (ValueError, TypeError) as e:
44
+ errors.append(f"{type(e).__name__}: {e}")
45
+ else:
46
+ for groupname in args.DEPENDENCY_GROUP:
47
+ try:
48
+ resolved.extend(str(r) for r in resolver.resolve(groupname))
49
+ except (LookupError, ValueError, TypeError) as e:
50
+ errors.append(f"{type(e).__name__}: {e}")
51
+
52
+ if errors:
53
+ print("errors encountered while examining dependency groups:")
54
+ for msg in errors:
55
+ print(f" {msg}")
56
+ sys.exit(1)
57
+
58
+ _invoke_pip(resolved)
59
+
60
+
61
+ if __name__ == "__main__":
62
+ main()
.venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_toml_compat.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ try:
2
+ import tomllib
3
+ except ImportError:
4
+ try:
5
+ from pip._vendor import tomli as tomllib # type: ignore[no-redef, unused-ignore]
6
+ except ModuleNotFoundError: # pragma: no cover
7
+ tomllib = None # type: ignore[assignment, unused-ignore]
8
+
9
+ __all__ = ("tomllib",)
.venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/py.typed ADDED
File without changes
.venv/lib/python3.13/site-packages/pip/_vendor/distlib/__init__.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2023 Vinay Sajip.
4
+ # Licensed to the Python Software Foundation under a contributor agreement.
5
+ # See LICENSE.txt and CONTRIBUTORS.txt.
6
+ #
7
+ import logging
8
+
9
+ __version__ = '0.3.9'
10
+
11
+
12
+ class DistlibException(Exception):
13
+ pass
14
+
15
+
16
+ try:
17
+ from logging import NullHandler
18
+ except ImportError: # pragma: no cover
19
+
20
+ class NullHandler(logging.Handler):
21
+
22
+ def handle(self, record):
23
+ pass
24
+
25
+ def emit(self, record):
26
+ pass
27
+
28
+ def createLock(self):
29
+ self.lock = None
30
+
31
+
32
+ logger = logging.getLogger(__name__)
33
+ logger.addHandler(NullHandler())
.venv/lib/python3.13/site-packages/pip/_vendor/distlib/compat.py ADDED
@@ -0,0 +1,1137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2013-2017 Vinay Sajip.
4
+ # Licensed to the Python Software Foundation under a contributor agreement.
5
+ # See LICENSE.txt and CONTRIBUTORS.txt.
6
+ #
7
+ from __future__ import absolute_import
8
+
9
+ import os
10
+ import re
11
+ import shutil
12
+ import sys
13
+
14
+ try:
15
+ import ssl
16
+ except ImportError: # pragma: no cover
17
+ ssl = None
18
+
19
+ if sys.version_info[0] < 3: # pragma: no cover
20
+ from StringIO import StringIO
21
+ string_types = basestring,
22
+ text_type = unicode
23
+ from types import FileType as file_type
24
+ import __builtin__ as builtins
25
+ import ConfigParser as configparser
26
+ from urlparse import urlparse, urlunparse, urljoin, urlsplit, urlunsplit
27
+ from urllib import (urlretrieve, quote as _quote, unquote, url2pathname,
28
+ pathname2url, ContentTooShortError, splittype)
29
+
30
+ def quote(s):
31
+ if isinstance(s, unicode):
32
+ s = s.encode('utf-8')
33
+ return _quote(s)
34
+
35
+ import urllib2
36
+ from urllib2 import (Request, urlopen, URLError, HTTPError,
37
+ HTTPBasicAuthHandler, HTTPPasswordMgr, HTTPHandler,
38
+ HTTPRedirectHandler, build_opener)
39
+ if ssl:
40
+ from urllib2 import HTTPSHandler
41
+ import httplib
42
+ import xmlrpclib
43
+ import Queue as queue
44
+ from HTMLParser import HTMLParser
45
+ import htmlentitydefs
46
+ raw_input = raw_input
47
+ from itertools import ifilter as filter
48
+ from itertools import ifilterfalse as filterfalse
49
+
50
+ # Leaving this around for now, in case it needs resurrecting in some way
51
+ # _userprog = None
52
+ # def splituser(host):
53
+ # """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'."""
54
+ # global _userprog
55
+ # if _userprog is None:
56
+ # import re
57
+ # _userprog = re.compile('^(.*)@(.*)$')
58
+
59
+ # match = _userprog.match(host)
60
+ # if match: return match.group(1, 2)
61
+ # return None, host
62
+
63
+ else: # pragma: no cover
64
+ from io import StringIO
65
+ string_types = str,
66
+ text_type = str
67
+ from io import TextIOWrapper as file_type
68
+ import builtins
69
+ import configparser
70
+ from urllib.parse import (urlparse, urlunparse, urljoin, quote, unquote,
71
+ urlsplit, urlunsplit, splittype)
72
+ from urllib.request import (urlopen, urlretrieve, Request, url2pathname,
73
+ pathname2url, HTTPBasicAuthHandler,
74
+ HTTPPasswordMgr, HTTPHandler,
75
+ HTTPRedirectHandler, build_opener)
76
+ if ssl:
77
+ from urllib.request import HTTPSHandler
78
+ from urllib.error import HTTPError, URLError, ContentTooShortError
79
+ import http.client as httplib
80
+ import urllib.request as urllib2
81
+ import xmlrpc.client as xmlrpclib
82
+ import queue
83
+ from html.parser import HTMLParser
84
+ import html.entities as htmlentitydefs
85
+ raw_input = input
86
+ from itertools import filterfalse
87
+ filter = filter
88
+
89
+ try:
90
+ from ssl import match_hostname, CertificateError
91
+ except ImportError: # pragma: no cover
92
+
93
+ class CertificateError(ValueError):
94
+ pass
95
+
96
+ def _dnsname_match(dn, hostname, max_wildcards=1):
97
+ """Matching according to RFC 6125, section 6.4.3
98
+
99
+ http://tools.ietf.org/html/rfc6125#section-6.4.3
100
+ """
101
+ pats = []
102
+ if not dn:
103
+ return False
104
+
105
+ parts = dn.split('.')
106
+ leftmost, remainder = parts[0], parts[1:]
107
+
108
+ wildcards = leftmost.count('*')
109
+ if wildcards > max_wildcards:
110
+ # Issue #17980: avoid denials of service by refusing more
111
+ # than one wildcard per fragment. A survey of established
112
+ # policy among SSL implementations showed it to be a
113
+ # reasonable choice.
114
+ raise CertificateError(
115
+ "too many wildcards in certificate DNS name: " + repr(dn))
116
+
117
+ # speed up common case w/o wildcards
118
+ if not wildcards:
119
+ return dn.lower() == hostname.lower()
120
+
121
+ # RFC 6125, section 6.4.3, subitem 1.
122
+ # The client SHOULD NOT attempt to match a presented identifier in which
123
+ # the wildcard character comprises a label other than the left-most label.
124
+ if leftmost == '*':
125
+ # When '*' is a fragment by itself, it matches a non-empty dotless
126
+ # fragment.
127
+ pats.append('[^.]+')
128
+ elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
129
+ # RFC 6125, section 6.4.3, subitem 3.
130
+ # The client SHOULD NOT attempt to match a presented identifier
131
+ # where the wildcard character is embedded within an A-label or
132
+ # U-label of an internationalized domain name.
133
+ pats.append(re.escape(leftmost))
134
+ else:
135
+ # Otherwise, '*' matches any dotless string, e.g. www*
136
+ pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
137
+
138
+ # add the remaining fragments, ignore any wildcards
139
+ for frag in remainder:
140
+ pats.append(re.escape(frag))
141
+
142
+ pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
143
+ return pat.match(hostname)
144
+
145
+ def match_hostname(cert, hostname):
146
+ """Verify that *cert* (in decoded format as returned by
147
+ SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
148
+ rules are followed, but IP addresses are not accepted for *hostname*.
149
+
150
+ CertificateError is raised on failure. On success, the function
151
+ returns nothing.
152
+ """
153
+ if not cert:
154
+ raise ValueError("empty or no certificate, match_hostname needs a "
155
+ "SSL socket or SSL context with either "
156
+ "CERT_OPTIONAL or CERT_REQUIRED")
157
+ dnsnames = []
158
+ san = cert.get('subjectAltName', ())
159
+ for key, value in san:
160
+ if key == 'DNS':
161
+ if _dnsname_match(value, hostname):
162
+ return
163
+ dnsnames.append(value)
164
+ if not dnsnames:
165
+ # The subject is only checked when there is no dNSName entry
166
+ # in subjectAltName
167
+ for sub in cert.get('subject', ()):
168
+ for key, value in sub:
169
+ # XXX according to RFC 2818, the most specific Common Name
170
+ # must be used.
171
+ if key == 'commonName':
172
+ if _dnsname_match(value, hostname):
173
+ return
174
+ dnsnames.append(value)
175
+ if len(dnsnames) > 1:
176
+ raise CertificateError("hostname %r "
177
+ "doesn't match either of %s" %
178
+ (hostname, ', '.join(map(repr, dnsnames))))
179
+ elif len(dnsnames) == 1:
180
+ raise CertificateError("hostname %r "
181
+ "doesn't match %r" %
182
+ (hostname, dnsnames[0]))
183
+ else:
184
+ raise CertificateError("no appropriate commonName or "
185
+ "subjectAltName fields were found")
186
+
187
+
188
+ try:
189
+ from types import SimpleNamespace as Container
190
+ except ImportError: # pragma: no cover
191
+
192
+ class Container(object):
193
+ """
194
+ A generic container for when multiple values need to be returned
195
+ """
196
+
197
+ def __init__(self, **kwargs):
198
+ self.__dict__.update(kwargs)
199
+
200
+
201
+ try:
202
+ from shutil import which
203
+ except ImportError: # pragma: no cover
204
+ # Implementation from Python 3.3
205
+ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
206
+ """Given a command, mode, and a PATH string, return the path which
207
+ conforms to the given mode on the PATH, or None if there is no such
208
+ file.
209
+
210
+ `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
211
+ of os.environ.get("PATH"), or can be overridden with a custom search
212
+ path.
213
+
214
+ """
215
+
216
+ # Check that a given file can be accessed with the correct mode.
217
+ # Additionally check that `file` is not a directory, as on Windows
218
+ # directories pass the os.access check.
219
+ def _access_check(fn, mode):
220
+ return (os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn))
221
+
222
+ # If we're given a path with a directory part, look it up directly rather
223
+ # than referring to PATH directories. This includes checking relative to the
224
+ # current directory, e.g. ./script
225
+ if os.path.dirname(cmd):
226
+ if _access_check(cmd, mode):
227
+ return cmd
228
+ return None
229
+
230
+ if path is None:
231
+ path = os.environ.get("PATH", os.defpath)
232
+ if not path:
233
+ return None
234
+ path = path.split(os.pathsep)
235
+
236
+ if sys.platform == "win32":
237
+ # The current directory takes precedence on Windows.
238
+ if os.curdir not in path:
239
+ path.insert(0, os.curdir)
240
+
241
+ # PATHEXT is necessary to check on Windows.
242
+ pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
243
+ # See if the given file matches any of the expected path extensions.
244
+ # This will allow us to short circuit when given "python.exe".
245
+ # If it does match, only test that one, otherwise we have to try
246
+ # others.
247
+ if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
248
+ files = [cmd]
249
+ else:
250
+ files = [cmd + ext for ext in pathext]
251
+ else:
252
+ # On other platforms you don't have things like PATHEXT to tell you
253
+ # what file suffixes are executable, so just pass on cmd as-is.
254
+ files = [cmd]
255
+
256
+ seen = set()
257
+ for dir in path:
258
+ normdir = os.path.normcase(dir)
259
+ if normdir not in seen:
260
+ seen.add(normdir)
261
+ for thefile in files:
262
+ name = os.path.join(dir, thefile)
263
+ if _access_check(name, mode):
264
+ return name
265
+ return None
266
+
267
+
268
+ # ZipFile is a context manager in 2.7, but not in 2.6
269
+
270
+ from zipfile import ZipFile as BaseZipFile
271
+
272
+ if hasattr(BaseZipFile, '__enter__'): # pragma: no cover
273
+ ZipFile = BaseZipFile
274
+ else: # pragma: no cover
275
+ from zipfile import ZipExtFile as BaseZipExtFile
276
+
277
+ class ZipExtFile(BaseZipExtFile):
278
+
279
+ def __init__(self, base):
280
+ self.__dict__.update(base.__dict__)
281
+
282
+ def __enter__(self):
283
+ return self
284
+
285
+ def __exit__(self, *exc_info):
286
+ self.close()
287
+ # return None, so if an exception occurred, it will propagate
288
+
289
+ class ZipFile(BaseZipFile):
290
+
291
+ def __enter__(self):
292
+ return self
293
+
294
+ def __exit__(self, *exc_info):
295
+ self.close()
296
+ # return None, so if an exception occurred, it will propagate
297
+
298
+ def open(self, *args, **kwargs):
299
+ base = BaseZipFile.open(self, *args, **kwargs)
300
+ return ZipExtFile(base)
301
+
302
+
303
+ try:
304
+ from platform import python_implementation
305
+ except ImportError: # pragma: no cover
306
+
307
+ def python_implementation():
308
+ """Return a string identifying the Python implementation."""
309
+ if 'PyPy' in sys.version:
310
+ return 'PyPy'
311
+ if os.name == 'java':
312
+ return 'Jython'
313
+ if sys.version.startswith('IronPython'):
314
+ return 'IronPython'
315
+ return 'CPython'
316
+
317
+
318
+ import sysconfig
319
+
320
+ try:
321
+ callable = callable
322
+ except NameError: # pragma: no cover
323
+ from collections.abc import Callable
324
+
325
+ def callable(obj):
326
+ return isinstance(obj, Callable)
327
+
328
+
329
+ try:
330
+ fsencode = os.fsencode
331
+ fsdecode = os.fsdecode
332
+ except AttributeError: # pragma: no cover
333
+ # Issue #99: on some systems (e.g. containerised),
334
+ # sys.getfilesystemencoding() returns None, and we need a real value,
335
+ # so fall back to utf-8. From the CPython 2.7 docs relating to Unix and
336
+ # sys.getfilesystemencoding(): the return value is "the user’s preference
337
+ # according to the result of nl_langinfo(CODESET), or None if the
338
+ # nl_langinfo(CODESET) failed."
339
+ _fsencoding = sys.getfilesystemencoding() or 'utf-8'
340
+ if _fsencoding == 'mbcs':
341
+ _fserrors = 'strict'
342
+ else:
343
+ _fserrors = 'surrogateescape'
344
+
345
+ def fsencode(filename):
346
+ if isinstance(filename, bytes):
347
+ return filename
348
+ elif isinstance(filename, text_type):
349
+ return filename.encode(_fsencoding, _fserrors)
350
+ else:
351
+ raise TypeError("expect bytes or str, not %s" %
352
+ type(filename).__name__)
353
+
354
+ def fsdecode(filename):
355
+ if isinstance(filename, text_type):
356
+ return filename
357
+ elif isinstance(filename, bytes):
358
+ return filename.decode(_fsencoding, _fserrors)
359
+ else:
360
+ raise TypeError("expect bytes or str, not %s" %
361
+ type(filename).__name__)
362
+
363
+
364
+ try:
365
+ from tokenize import detect_encoding
366
+ except ImportError: # pragma: no cover
367
+ from codecs import BOM_UTF8, lookup
368
+
369
+ cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)")
370
+
371
+ def _get_normal_name(orig_enc):
372
+ """Imitates get_normal_name in tokenizer.c."""
373
+ # Only care about the first 12 characters.
374
+ enc = orig_enc[:12].lower().replace("_", "-")
375
+ if enc == "utf-8" or enc.startswith("utf-8-"):
376
+ return "utf-8"
377
+ if enc in ("latin-1", "iso-8859-1", "iso-latin-1") or \
378
+ enc.startswith(("latin-1-", "iso-8859-1-", "iso-latin-1-")):
379
+ return "iso-8859-1"
380
+ return orig_enc
381
+
382
+ def detect_encoding(readline):
383
+ """
384
+ The detect_encoding() function is used to detect the encoding that should
385
+ be used to decode a Python source file. It requires one argument, readline,
386
+ in the same way as the tokenize() generator.
387
+
388
+ It will call readline a maximum of twice, and return the encoding used
389
+ (as a string) and a list of any lines (left as bytes) it has read in.
390
+
391
+ It detects the encoding from the presence of a utf-8 bom or an encoding
392
+ cookie as specified in pep-0263. If both a bom and a cookie are present,
393
+ but disagree, a SyntaxError will be raised. If the encoding cookie is an
394
+ invalid charset, raise a SyntaxError. Note that if a utf-8 bom is found,
395
+ 'utf-8-sig' is returned.
396
+
397
+ If no encoding is specified, then the default of 'utf-8' will be returned.
398
+ """
399
+ try:
400
+ filename = readline.__self__.name
401
+ except AttributeError:
402
+ filename = None
403
+ bom_found = False
404
+ encoding = None
405
+ default = 'utf-8'
406
+
407
+ def read_or_stop():
408
+ try:
409
+ return readline()
410
+ except StopIteration:
411
+ return b''
412
+
413
+ def find_cookie(line):
414
+ try:
415
+ # Decode as UTF-8. Either the line is an encoding declaration,
416
+ # in which case it should be pure ASCII, or it must be UTF-8
417
+ # per default encoding.
418
+ line_string = line.decode('utf-8')
419
+ except UnicodeDecodeError:
420
+ msg = "invalid or missing encoding declaration"
421
+ if filename is not None:
422
+ msg = '{} for {!r}'.format(msg, filename)
423
+ raise SyntaxError(msg)
424
+
425
+ matches = cookie_re.findall(line_string)
426
+ if not matches:
427
+ return None
428
+ encoding = _get_normal_name(matches[0])
429
+ try:
430
+ codec = lookup(encoding)
431
+ except LookupError:
432
+ # This behaviour mimics the Python interpreter
433
+ if filename is None:
434
+ msg = "unknown encoding: " + encoding
435
+ else:
436
+ msg = "unknown encoding for {!r}: {}".format(
437
+ filename, encoding)
438
+ raise SyntaxError(msg)
439
+
440
+ if bom_found:
441
+ if codec.name != 'utf-8':
442
+ # This behaviour mimics the Python interpreter
443
+ if filename is None:
444
+ msg = 'encoding problem: utf-8'
445
+ else:
446
+ msg = 'encoding problem for {!r}: utf-8'.format(
447
+ filename)
448
+ raise SyntaxError(msg)
449
+ encoding += '-sig'
450
+ return encoding
451
+
452
+ first = read_or_stop()
453
+ if first.startswith(BOM_UTF8):
454
+ bom_found = True
455
+ first = first[3:]
456
+ default = 'utf-8-sig'
457
+ if not first:
458
+ return default, []
459
+
460
+ encoding = find_cookie(first)
461
+ if encoding:
462
+ return encoding, [first]
463
+
464
+ second = read_or_stop()
465
+ if not second:
466
+ return default, [first]
467
+
468
+ encoding = find_cookie(second)
469
+ if encoding:
470
+ return encoding, [first, second]
471
+
472
+ return default, [first, second]
473
+
474
+
475
+ # For converting & <-> &amp; etc.
476
+ try:
477
+ from html import escape
478
+ except ImportError:
479
+ from cgi import escape
480
+ if sys.version_info[:2] < (3, 4):
481
+ unescape = HTMLParser().unescape
482
+ else:
483
+ from html import unescape
484
+
485
+ try:
486
+ from collections import ChainMap
487
+ except ImportError: # pragma: no cover
488
+ from collections import MutableMapping
489
+
490
+ try:
491
+ from reprlib import recursive_repr as _recursive_repr
492
+ except ImportError:
493
+
494
+ def _recursive_repr(fillvalue='...'):
495
+ '''
496
+ Decorator to make a repr function return fillvalue for a recursive
497
+ call
498
+ '''
499
+
500
+ def decorating_function(user_function):
501
+ repr_running = set()
502
+
503
+ def wrapper(self):
504
+ key = id(self), get_ident()
505
+ if key in repr_running:
506
+ return fillvalue
507
+ repr_running.add(key)
508
+ try:
509
+ result = user_function(self)
510
+ finally:
511
+ repr_running.discard(key)
512
+ return result
513
+
514
+ # Can't use functools.wraps() here because of bootstrap issues
515
+ wrapper.__module__ = getattr(user_function, '__module__')
516
+ wrapper.__doc__ = getattr(user_function, '__doc__')
517
+ wrapper.__name__ = getattr(user_function, '__name__')
518
+ wrapper.__annotations__ = getattr(user_function,
519
+ '__annotations__', {})
520
+ return wrapper
521
+
522
+ return decorating_function
523
+
524
+ class ChainMap(MutableMapping):
525
+ '''
526
+ A ChainMap groups multiple dicts (or other mappings) together
527
+ to create a single, updateable view.
528
+
529
+ The underlying mappings are stored in a list. That list is public and can
530
+ accessed or updated using the *maps* attribute. There is no other state.
531
+
532
+ Lookups search the underlying mappings successively until a key is found.
533
+ In contrast, writes, updates, and deletions only operate on the first
534
+ mapping.
535
+ '''
536
+
537
+ def __init__(self, *maps):
538
+ '''Initialize a ChainMap by setting *maps* to the given mappings.
539
+ If no mappings are provided, a single empty dictionary is used.
540
+
541
+ '''
542
+ self.maps = list(maps) or [{}] # always at least one map
543
+
544
+ def __missing__(self, key):
545
+ raise KeyError(key)
546
+
547
+ def __getitem__(self, key):
548
+ for mapping in self.maps:
549
+ try:
550
+ return mapping[
551
+ key] # can't use 'key in mapping' with defaultdict
552
+ except KeyError:
553
+ pass
554
+ return self.__missing__(
555
+ key) # support subclasses that define __missing__
556
+
557
+ def get(self, key, default=None):
558
+ return self[key] if key in self else default
559
+
560
+ def __len__(self):
561
+ return len(set().union(
562
+ *self.maps)) # reuses stored hash values if possible
563
+
564
+ def __iter__(self):
565
+ return iter(set().union(*self.maps))
566
+
567
+ def __contains__(self, key):
568
+ return any(key in m for m in self.maps)
569
+
570
+ def __bool__(self):
571
+ return any(self.maps)
572
+
573
+ @_recursive_repr()
574
+ def __repr__(self):
575
+ return '{0.__class__.__name__}({1})'.format(
576
+ self, ', '.join(map(repr, self.maps)))
577
+
578
+ @classmethod
579
+ def fromkeys(cls, iterable, *args):
580
+ 'Create a ChainMap with a single dict created from the iterable.'
581
+ return cls(dict.fromkeys(iterable, *args))
582
+
583
+ def copy(self):
584
+ 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]'
585
+ return self.__class__(self.maps[0].copy(), *self.maps[1:])
586
+
587
+ __copy__ = copy
588
+
589
+ def new_child(self): # like Django's Context.push()
590
+ 'New ChainMap with a new dict followed by all previous maps.'
591
+ return self.__class__({}, *self.maps)
592
+
593
+ @property
594
+ def parents(self): # like Django's Context.pop()
595
+ 'New ChainMap from maps[1:].'
596
+ return self.__class__(*self.maps[1:])
597
+
598
+ def __setitem__(self, key, value):
599
+ self.maps[0][key] = value
600
+
601
+ def __delitem__(self, key):
602
+ try:
603
+ del self.maps[0][key]
604
+ except KeyError:
605
+ raise KeyError(
606
+ 'Key not found in the first mapping: {!r}'.format(key))
607
+
608
+ def popitem(self):
609
+ 'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.'
610
+ try:
611
+ return self.maps[0].popitem()
612
+ except KeyError:
613
+ raise KeyError('No keys found in the first mapping.')
614
+
615
+ def pop(self, key, *args):
616
+ 'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].'
617
+ try:
618
+ return self.maps[0].pop(key, *args)
619
+ except KeyError:
620
+ raise KeyError(
621
+ 'Key not found in the first mapping: {!r}'.format(key))
622
+
623
+ def clear(self):
624
+ 'Clear maps[0], leaving maps[1:] intact.'
625
+ self.maps[0].clear()
626
+
627
+
628
+ try:
629
+ from importlib.util import cache_from_source # Python >= 3.4
630
+ except ImportError: # pragma: no cover
631
+
632
+ def cache_from_source(path, debug_override=None):
633
+ assert path.endswith('.py')
634
+ if debug_override is None:
635
+ debug_override = __debug__
636
+ if debug_override:
637
+ suffix = 'c'
638
+ else:
639
+ suffix = 'o'
640
+ return path + suffix
641
+
642
+
643
+ try:
644
+ from collections import OrderedDict
645
+ except ImportError: # pragma: no cover
646
+ # {{{ http://code.activestate.com/recipes/576693/ (r9)
647
+ # Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
648
+ # Passes Python2.7's test suite and incorporates all the latest updates.
649
+ try:
650
+ from thread import get_ident as _get_ident
651
+ except ImportError:
652
+ from dummy_thread import get_ident as _get_ident
653
+
654
+ try:
655
+ from _abcoll import KeysView, ValuesView, ItemsView
656
+ except ImportError:
657
+ pass
658
+
659
+ class OrderedDict(dict):
660
+ 'Dictionary that remembers insertion order'
661
+
662
+ # An inherited dict maps keys to values.
663
+ # The inherited dict provides __getitem__, __len__, __contains__, and get.
664
+ # The remaining methods are order-aware.
665
+ # Big-O running times for all methods are the same as for regular dictionaries.
666
+
667
+ # The internal self.__map dictionary maps keys to links in a doubly linked list.
668
+ # The circular doubly linked list starts and ends with a sentinel element.
669
+ # The sentinel element never gets deleted (this simplifies the algorithm).
670
+ # Each link is stored as a list of length three: [PREV, NEXT, KEY].
671
+
672
+ def __init__(self, *args, **kwds):
673
+ '''Initialize an ordered dictionary. Signature is the same as for
674
+ regular dictionaries, but keyword arguments are not recommended
675
+ because their insertion order is arbitrary.
676
+
677
+ '''
678
+ if len(args) > 1:
679
+ raise TypeError('expected at most 1 arguments, got %d' %
680
+ len(args))
681
+ try:
682
+ self.__root
683
+ except AttributeError:
684
+ self.__root = root = [] # sentinel node
685
+ root[:] = [root, root, None]
686
+ self.__map = {}
687
+ self.__update(*args, **kwds)
688
+
689
+ def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
690
+ 'od.__setitem__(i, y) <==> od[i]=y'
691
+ # Setting a new item creates a new link which goes at the end of the linked
692
+ # list, and the inherited dictionary is updated with the new key/value pair.
693
+ if key not in self:
694
+ root = self.__root
695
+ last = root[0]
696
+ last[1] = root[0] = self.__map[key] = [last, root, key]
697
+ dict_setitem(self, key, value)
698
+
699
+ def __delitem__(self, key, dict_delitem=dict.__delitem__):
700
+ 'od.__delitem__(y) <==> del od[y]'
701
+ # Deleting an existing item uses self.__map to find the link which is
702
+ # then removed by updating the links in the predecessor and successor nodes.
703
+ dict_delitem(self, key)
704
+ link_prev, link_next, key = self.__map.pop(key)
705
+ link_prev[1] = link_next
706
+ link_next[0] = link_prev
707
+
708
+ def __iter__(self):
709
+ 'od.__iter__() <==> iter(od)'
710
+ root = self.__root
711
+ curr = root[1]
712
+ while curr is not root:
713
+ yield curr[2]
714
+ curr = curr[1]
715
+
716
+ def __reversed__(self):
717
+ 'od.__reversed__() <==> reversed(od)'
718
+ root = self.__root
719
+ curr = root[0]
720
+ while curr is not root:
721
+ yield curr[2]
722
+ curr = curr[0]
723
+
724
+ def clear(self):
725
+ 'od.clear() -> None. Remove all items from od.'
726
+ try:
727
+ for node in self.__map.itervalues():
728
+ del node[:]
729
+ root = self.__root
730
+ root[:] = [root, root, None]
731
+ self.__map.clear()
732
+ except AttributeError:
733
+ pass
734
+ dict.clear(self)
735
+
736
+ def popitem(self, last=True):
737
+ '''od.popitem() -> (k, v), return and remove a (key, value) pair.
738
+ Pairs are returned in LIFO order if last is true or FIFO order if false.
739
+
740
+ '''
741
+ if not self:
742
+ raise KeyError('dictionary is empty')
743
+ root = self.__root
744
+ if last:
745
+ link = root[0]
746
+ link_prev = link[0]
747
+ link_prev[1] = root
748
+ root[0] = link_prev
749
+ else:
750
+ link = root[1]
751
+ link_next = link[1]
752
+ root[1] = link_next
753
+ link_next[0] = root
754
+ key = link[2]
755
+ del self.__map[key]
756
+ value = dict.pop(self, key)
757
+ return key, value
758
+
759
+ # -- the following methods do not depend on the internal structure --
760
+
761
+ def keys(self):
762
+ 'od.keys() -> list of keys in od'
763
+ return list(self)
764
+
765
+ def values(self):
766
+ 'od.values() -> list of values in od'
767
+ return [self[key] for key in self]
768
+
769
+ def items(self):
770
+ 'od.items() -> list of (key, value) pairs in od'
771
+ return [(key, self[key]) for key in self]
772
+
773
+ def iterkeys(self):
774
+ 'od.iterkeys() -> an iterator over the keys in od'
775
+ return iter(self)
776
+
777
+ def itervalues(self):
778
+ 'od.itervalues -> an iterator over the values in od'
779
+ for k in self:
780
+ yield self[k]
781
+
782
+ def iteritems(self):
783
+ 'od.iteritems -> an iterator over the (key, value) items in od'
784
+ for k in self:
785
+ yield (k, self[k])
786
+
787
+ def update(*args, **kwds):
788
+ '''od.update(E, **F) -> None. Update od from dict/iterable E and F.
789
+
790
+ If E is a dict instance, does: for k in E: od[k] = E[k]
791
+ If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
792
+ Or if E is an iterable of items, does: for k, v in E: od[k] = v
793
+ In either case, this is followed by: for k, v in F.items(): od[k] = v
794
+
795
+ '''
796
+ if len(args) > 2:
797
+ raise TypeError('update() takes at most 2 positional '
798
+ 'arguments (%d given)' % (len(args), ))
799
+ elif not args:
800
+ raise TypeError('update() takes at least 1 argument (0 given)')
801
+ self = args[0]
802
+ # Make progressively weaker assumptions about "other"
803
+ other = ()
804
+ if len(args) == 2:
805
+ other = args[1]
806
+ if isinstance(other, dict):
807
+ for key in other:
808
+ self[key] = other[key]
809
+ elif hasattr(other, 'keys'):
810
+ for key in other.keys():
811
+ self[key] = other[key]
812
+ else:
813
+ for key, value in other:
814
+ self[key] = value
815
+ for key, value in kwds.items():
816
+ self[key] = value
817
+
818
+ __update = update # let subclasses override update without breaking __init__
819
+
820
+ __marker = object()
821
+
822
+ def pop(self, key, default=__marker):
823
+ '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
824
+ If key is not found, d is returned if given, otherwise KeyError is raised.
825
+
826
+ '''
827
+ if key in self:
828
+ result = self[key]
829
+ del self[key]
830
+ return result
831
+ if default is self.__marker:
832
+ raise KeyError(key)
833
+ return default
834
+
835
+ def setdefault(self, key, default=None):
836
+ 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
837
+ if key in self:
838
+ return self[key]
839
+ self[key] = default
840
+ return default
841
+
842
+ def __repr__(self, _repr_running=None):
843
+ 'od.__repr__() <==> repr(od)'
844
+ if not _repr_running:
845
+ _repr_running = {}
846
+ call_key = id(self), _get_ident()
847
+ if call_key in _repr_running:
848
+ return '...'
849
+ _repr_running[call_key] = 1
850
+ try:
851
+ if not self:
852
+ return '%s()' % (self.__class__.__name__, )
853
+ return '%s(%r)' % (self.__class__.__name__, self.items())
854
+ finally:
855
+ del _repr_running[call_key]
856
+
857
+ def __reduce__(self):
858
+ 'Return state information for pickling'
859
+ items = [[k, self[k]] for k in self]
860
+ inst_dict = vars(self).copy()
861
+ for k in vars(OrderedDict()):
862
+ inst_dict.pop(k, None)
863
+ if inst_dict:
864
+ return (self.__class__, (items, ), inst_dict)
865
+ return self.__class__, (items, )
866
+
867
+ def copy(self):
868
+ 'od.copy() -> a shallow copy of od'
869
+ return self.__class__(self)
870
+
871
+ @classmethod
872
+ def fromkeys(cls, iterable, value=None):
873
+ '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
874
+ and values equal to v (which defaults to None).
875
+
876
+ '''
877
+ d = cls()
878
+ for key in iterable:
879
+ d[key] = value
880
+ return d
881
+
882
+ def __eq__(self, other):
883
+ '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
884
+ while comparison to a regular mapping is order-insensitive.
885
+
886
+ '''
887
+ if isinstance(other, OrderedDict):
888
+ return len(self) == len(
889
+ other) and self.items() == other.items()
890
+ return dict.__eq__(self, other)
891
+
892
+ def __ne__(self, other):
893
+ return not self == other
894
+
895
+ # -- the following methods are only used in Python 2.7 --
896
+
897
+ def viewkeys(self):
898
+ "od.viewkeys() -> a set-like object providing a view on od's keys"
899
+ return KeysView(self)
900
+
901
+ def viewvalues(self):
902
+ "od.viewvalues() -> an object providing a view on od's values"
903
+ return ValuesView(self)
904
+
905
+ def viewitems(self):
906
+ "od.viewitems() -> a set-like object providing a view on od's items"
907
+ return ItemsView(self)
908
+
909
+
910
+ try:
911
+ from logging.config import BaseConfigurator, valid_ident
912
+ except ImportError: # pragma: no cover
913
+ IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
914
+
915
+ def valid_ident(s):
916
+ m = IDENTIFIER.match(s)
917
+ if not m:
918
+ raise ValueError('Not a valid Python identifier: %r' % s)
919
+ return True
920
+
921
+ # The ConvertingXXX classes are wrappers around standard Python containers,
922
+ # and they serve to convert any suitable values in the container. The
923
+ # conversion converts base dicts, lists and tuples to their wrapped
924
+ # equivalents, whereas strings which match a conversion format are converted
925
+ # appropriately.
926
+ #
927
+ # Each wrapper should have a configurator attribute holding the actual
928
+ # configurator to use for conversion.
929
+
930
+ class ConvertingDict(dict):
931
+ """A converting dictionary wrapper."""
932
+
933
+ def __getitem__(self, key):
934
+ value = dict.__getitem__(self, key)
935
+ result = self.configurator.convert(value)
936
+ # If the converted value is different, save for next time
937
+ if value is not result:
938
+ self[key] = result
939
+ if type(result) in (ConvertingDict, ConvertingList,
940
+ ConvertingTuple):
941
+ result.parent = self
942
+ result.key = key
943
+ return result
944
+
945
+ def get(self, key, default=None):
946
+ value = dict.get(self, key, default)
947
+ result = self.configurator.convert(value)
948
+ # If the converted value is different, save for next time
949
+ if value is not result:
950
+ self[key] = result
951
+ if type(result) in (ConvertingDict, ConvertingList,
952
+ ConvertingTuple):
953
+ result.parent = self
954
+ result.key = key
955
+ return result
956
+
957
+ def pop(self, key, default=None):
958
+ value = dict.pop(self, key, default)
959
+ result = self.configurator.convert(value)
960
+ if value is not result:
961
+ if type(result) in (ConvertingDict, ConvertingList,
962
+ ConvertingTuple):
963
+ result.parent = self
964
+ result.key = key
965
+ return result
966
+
967
+ class ConvertingList(list):
968
+ """A converting list wrapper."""
969
+
970
+ def __getitem__(self, key):
971
+ value = list.__getitem__(self, key)
972
+ result = self.configurator.convert(value)
973
+ # If the converted value is different, save for next time
974
+ if value is not result:
975
+ self[key] = result
976
+ if type(result) in (ConvertingDict, ConvertingList,
977
+ ConvertingTuple):
978
+ result.parent = self
979
+ result.key = key
980
+ return result
981
+
982
+ def pop(self, idx=-1):
983
+ value = list.pop(self, idx)
984
+ result = self.configurator.convert(value)
985
+ if value is not result:
986
+ if type(result) in (ConvertingDict, ConvertingList,
987
+ ConvertingTuple):
988
+ result.parent = self
989
+ return result
990
+
991
+ class ConvertingTuple(tuple):
992
+ """A converting tuple wrapper."""
993
+
994
+ def __getitem__(self, key):
995
+ value = tuple.__getitem__(self, key)
996
+ result = self.configurator.convert(value)
997
+ if value is not result:
998
+ if type(result) in (ConvertingDict, ConvertingList,
999
+ ConvertingTuple):
1000
+ result.parent = self
1001
+ result.key = key
1002
+ return result
1003
+
1004
+ class BaseConfigurator(object):
1005
+ """
1006
+ The configurator base class which defines some useful defaults.
1007
+ """
1008
+
1009
+ CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
1010
+
1011
+ WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
1012
+ DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
1013
+ INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
1014
+ DIGIT_PATTERN = re.compile(r'^\d+$')
1015
+
1016
+ value_converters = {
1017
+ 'ext': 'ext_convert',
1018
+ 'cfg': 'cfg_convert',
1019
+ }
1020
+
1021
+ # We might want to use a different one, e.g. importlib
1022
+ importer = staticmethod(__import__)
1023
+
1024
+ def __init__(self, config):
1025
+ self.config = ConvertingDict(config)
1026
+ self.config.configurator = self
1027
+
1028
+ def resolve(self, s):
1029
+ """
1030
+ Resolve strings to objects using standard import and attribute
1031
+ syntax.
1032
+ """
1033
+ name = s.split('.')
1034
+ used = name.pop(0)
1035
+ try:
1036
+ found = self.importer(used)
1037
+ for frag in name:
1038
+ used += '.' + frag
1039
+ try:
1040
+ found = getattr(found, frag)
1041
+ except AttributeError:
1042
+ self.importer(used)
1043
+ found = getattr(found, frag)
1044
+ return found
1045
+ except ImportError:
1046
+ e, tb = sys.exc_info()[1:]
1047
+ v = ValueError('Cannot resolve %r: %s' % (s, e))
1048
+ v.__cause__, v.__traceback__ = e, tb
1049
+ raise v
1050
+
1051
+ def ext_convert(self, value):
1052
+ """Default converter for the ext:// protocol."""
1053
+ return self.resolve(value)
1054
+
1055
+ def cfg_convert(self, value):
1056
+ """Default converter for the cfg:// protocol."""
1057
+ rest = value
1058
+ m = self.WORD_PATTERN.match(rest)
1059
+ if m is None:
1060
+ raise ValueError("Unable to convert %r" % value)
1061
+ else:
1062
+ rest = rest[m.end():]
1063
+ d = self.config[m.groups()[0]]
1064
+ while rest:
1065
+ m = self.DOT_PATTERN.match(rest)
1066
+ if m:
1067
+ d = d[m.groups()[0]]
1068
+ else:
1069
+ m = self.INDEX_PATTERN.match(rest)
1070
+ if m:
1071
+ idx = m.groups()[0]
1072
+ if not self.DIGIT_PATTERN.match(idx):
1073
+ d = d[idx]
1074
+ else:
1075
+ try:
1076
+ n = int(
1077
+ idx
1078
+ ) # try as number first (most likely)
1079
+ d = d[n]
1080
+ except TypeError:
1081
+ d = d[idx]
1082
+ if m:
1083
+ rest = rest[m.end():]
1084
+ else:
1085
+ raise ValueError('Unable to convert '
1086
+ '%r at %r' % (value, rest))
1087
+ # rest should be empty
1088
+ return d
1089
+
1090
+ def convert(self, value):
1091
+ """
1092
+ Convert values to an appropriate type. dicts, lists and tuples are
1093
+ replaced by their converting alternatives. Strings are checked to
1094
+ see if they have a conversion format and are converted if they do.
1095
+ """
1096
+ if not isinstance(value, ConvertingDict) and isinstance(
1097
+ value, dict):
1098
+ value = ConvertingDict(value)
1099
+ value.configurator = self
1100
+ elif not isinstance(value, ConvertingList) and isinstance(
1101
+ value, list):
1102
+ value = ConvertingList(value)
1103
+ value.configurator = self
1104
+ elif not isinstance(value, ConvertingTuple) and isinstance(value, tuple):
1105
+ value = ConvertingTuple(value)
1106
+ value.configurator = self
1107
+ elif isinstance(value, string_types):
1108
+ m = self.CONVERT_PATTERN.match(value)
1109
+ if m:
1110
+ d = m.groupdict()
1111
+ prefix = d['prefix']
1112
+ converter = self.value_converters.get(prefix, None)
1113
+ if converter:
1114
+ suffix = d['suffix']
1115
+ converter = getattr(self, converter)
1116
+ value = converter(suffix)
1117
+ return value
1118
+
1119
+ def configure_custom(self, config):
1120
+ """Configure an object with a user-supplied factory."""
1121
+ c = config.pop('()')
1122
+ if not callable(c):
1123
+ c = self.resolve(c)
1124
+ props = config.pop('.', None)
1125
+ # Check for valid identifiers
1126
+ kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
1127
+ result = c(**kwargs)
1128
+ if props:
1129
+ for name, value in props.items():
1130
+ setattr(result, name, value)
1131
+ return result
1132
+
1133
+ def as_tuple(self, value):
1134
+ """Utility function which converts lists to tuples."""
1135
+ if isinstance(value, list):
1136
+ value = tuple(value)
1137
+ return value
.venv/lib/python3.13/site-packages/pip/_vendor/distlib/database.py ADDED
@@ -0,0 +1,1329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2023 The Python Software Foundation.
4
+ # See LICENSE.txt and CONTRIBUTORS.txt.
5
+ #
6
+ """PEP 376 implementation."""
7
+
8
+ from __future__ import unicode_literals
9
+
10
+ import base64
11
+ import codecs
12
+ import contextlib
13
+ import hashlib
14
+ import logging
15
+ import os
16
+ import posixpath
17
+ import sys
18
+ import zipimport
19
+
20
+ from . import DistlibException, resources
21
+ from .compat import StringIO
22
+ from .version import get_scheme, UnsupportedVersionError
23
+ from .metadata import (Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME)
24
+ from .util import (parse_requirement, cached_property, parse_name_and_version, read_exports, write_exports, CSVReader,
25
+ CSVWriter)
26
+
27
+ __all__ = [
28
+ 'Distribution', 'BaseInstalledDistribution', 'InstalledDistribution', 'EggInfoDistribution', 'DistributionPath'
29
+ ]
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ EXPORTS_FILENAME = 'pydist-exports.json'
34
+ COMMANDS_FILENAME = 'pydist-commands.json'
35
+
36
+ DIST_FILES = ('INSTALLER', METADATA_FILENAME, 'RECORD', 'REQUESTED', 'RESOURCES', EXPORTS_FILENAME, 'SHARED')
37
+
38
+ DISTINFO_EXT = '.dist-info'
39
+
40
+
41
+ class _Cache(object):
42
+ """
43
+ A simple cache mapping names and .dist-info paths to distributions
44
+ """
45
+
46
+ def __init__(self):
47
+ """
48
+ Initialise an instance. There is normally one for each DistributionPath.
49
+ """
50
+ self.name = {}
51
+ self.path = {}
52
+ self.generated = False
53
+
54
+ def clear(self):
55
+ """
56
+ Clear the cache, setting it to its initial state.
57
+ """
58
+ self.name.clear()
59
+ self.path.clear()
60
+ self.generated = False
61
+
62
+ def add(self, dist):
63
+ """
64
+ Add a distribution to the cache.
65
+ :param dist: The distribution to add.
66
+ """
67
+ if dist.path not in self.path:
68
+ self.path[dist.path] = dist
69
+ self.name.setdefault(dist.key, []).append(dist)
70
+
71
+
72
+ class DistributionPath(object):
73
+ """
74
+ Represents a set of distributions installed on a path (typically sys.path).
75
+ """
76
+
77
+ def __init__(self, path=None, include_egg=False):
78
+ """
79
+ Create an instance from a path, optionally including legacy (distutils/
80
+ setuptools/distribute) distributions.
81
+ :param path: The path to use, as a list of directories. If not specified,
82
+ sys.path is used.
83
+ :param include_egg: If True, this instance will look for and return legacy
84
+ distributions as well as those based on PEP 376.
85
+ """
86
+ if path is None:
87
+ path = sys.path
88
+ self.path = path
89
+ self._include_dist = True
90
+ self._include_egg = include_egg
91
+
92
+ self._cache = _Cache()
93
+ self._cache_egg = _Cache()
94
+ self._cache_enabled = True
95
+ self._scheme = get_scheme('default')
96
+
97
+ def _get_cache_enabled(self):
98
+ return self._cache_enabled
99
+
100
+ def _set_cache_enabled(self, value):
101
+ self._cache_enabled = value
102
+
103
+ cache_enabled = property(_get_cache_enabled, _set_cache_enabled)
104
+
105
+ def clear_cache(self):
106
+ """
107
+ Clears the internal cache.
108
+ """
109
+ self._cache.clear()
110
+ self._cache_egg.clear()
111
+
112
+ def _yield_distributions(self):
113
+ """
114
+ Yield .dist-info and/or .egg(-info) distributions.
115
+ """
116
+ # We need to check if we've seen some resources already, because on
117
+ # some Linux systems (e.g. some Debian/Ubuntu variants) there are
118
+ # symlinks which alias other files in the environment.
119
+ seen = set()
120
+ for path in self.path:
121
+ finder = resources.finder_for_path(path)
122
+ if finder is None:
123
+ continue
124
+ r = finder.find('')
125
+ if not r or not r.is_container:
126
+ continue
127
+ rset = sorted(r.resources)
128
+ for entry in rset:
129
+ r = finder.find(entry)
130
+ if not r or r.path in seen:
131
+ continue
132
+ try:
133
+ if self._include_dist and entry.endswith(DISTINFO_EXT):
134
+ possible_filenames = [METADATA_FILENAME, WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME]
135
+ for metadata_filename in possible_filenames:
136
+ metadata_path = posixpath.join(entry, metadata_filename)
137
+ pydist = finder.find(metadata_path)
138
+ if pydist:
139
+ break
140
+ else:
141
+ continue
142
+
143
+ with contextlib.closing(pydist.as_stream()) as stream:
144
+ metadata = Metadata(fileobj=stream, scheme='legacy')
145
+ logger.debug('Found %s', r.path)
146
+ seen.add(r.path)
147
+ yield new_dist_class(r.path, metadata=metadata, env=self)
148
+ elif self._include_egg and entry.endswith(('.egg-info', '.egg')):
149
+ logger.debug('Found %s', r.path)
150
+ seen.add(r.path)
151
+ yield old_dist_class(r.path, self)
152
+ except Exception as e:
153
+ msg = 'Unable to read distribution at %s, perhaps due to bad metadata: %s'
154
+ logger.warning(msg, r.path, e)
155
+ import warnings
156
+ warnings.warn(msg % (r.path, e), stacklevel=2)
157
+
158
+ def _generate_cache(self):
159
+ """
160
+ Scan the path for distributions and populate the cache with
161
+ those that are found.
162
+ """
163
+ gen_dist = not self._cache.generated
164
+ gen_egg = self._include_egg and not self._cache_egg.generated
165
+ if gen_dist or gen_egg:
166
+ for dist in self._yield_distributions():
167
+ if isinstance(dist, InstalledDistribution):
168
+ self._cache.add(dist)
169
+ else:
170
+ self._cache_egg.add(dist)
171
+
172
+ if gen_dist:
173
+ self._cache.generated = True
174
+ if gen_egg:
175
+ self._cache_egg.generated = True
176
+
177
+ @classmethod
178
+ def distinfo_dirname(cls, name, version):
179
+ """
180
+ The *name* and *version* parameters are converted into their
181
+ filename-escaped form, i.e. any ``'-'`` characters are replaced
182
+ with ``'_'`` other than the one in ``'dist-info'`` and the one
183
+ separating the name from the version number.
184
+
185
+ :parameter name: is converted to a standard distribution name by replacing
186
+ any runs of non- alphanumeric characters with a single
187
+ ``'-'``.
188
+ :type name: string
189
+ :parameter version: is converted to a standard version string. Spaces
190
+ become dots, and all other non-alphanumeric characters
191
+ (except dots) become dashes, with runs of multiple
192
+ dashes condensed to a single dash.
193
+ :type version: string
194
+ :returns: directory name
195
+ :rtype: string"""
196
+ name = name.replace('-', '_')
197
+ return '-'.join([name, version]) + DISTINFO_EXT
198
+
199
+ def get_distributions(self):
200
+ """
201
+ Provides an iterator that looks for distributions and returns
202
+ :class:`InstalledDistribution` or
203
+ :class:`EggInfoDistribution` instances for each one of them.
204
+
205
+ :rtype: iterator of :class:`InstalledDistribution` and
206
+ :class:`EggInfoDistribution` instances
207
+ """
208
+ if not self._cache_enabled:
209
+ for dist in self._yield_distributions():
210
+ yield dist
211
+ else:
212
+ self._generate_cache()
213
+
214
+ for dist in self._cache.path.values():
215
+ yield dist
216
+
217
+ if self._include_egg:
218
+ for dist in self._cache_egg.path.values():
219
+ yield dist
220
+
221
+ def get_distribution(self, name):
222
+ """
223
+ Looks for a named distribution on the path.
224
+
225
+ This function only returns the first result found, as no more than one
226
+ value is expected. If nothing is found, ``None`` is returned.
227
+
228
+ :rtype: :class:`InstalledDistribution`, :class:`EggInfoDistribution`
229
+ or ``None``
230
+ """
231
+ result = None
232
+ name = name.lower()
233
+ if not self._cache_enabled:
234
+ for dist in self._yield_distributions():
235
+ if dist.key == name:
236
+ result = dist
237
+ break
238
+ else:
239
+ self._generate_cache()
240
+
241
+ if name in self._cache.name:
242
+ result = self._cache.name[name][0]
243
+ elif self._include_egg and name in self._cache_egg.name:
244
+ result = self._cache_egg.name[name][0]
245
+ return result
246
+
247
+ def provides_distribution(self, name, version=None):
248
+ """
249
+ Iterates over all distributions to find which distributions provide *name*.
250
+ If a *version* is provided, it will be used to filter the results.
251
+
252
+ This function only returns the first result found, since no more than
253
+ one values are expected. If the directory is not found, returns ``None``.
254
+
255
+ :parameter version: a version specifier that indicates the version
256
+ required, conforming to the format in ``PEP-345``
257
+
258
+ :type name: string
259
+ :type version: string
260
+ """
261
+ matcher = None
262
+ if version is not None:
263
+ try:
264
+ matcher = self._scheme.matcher('%s (%s)' % (name, version))
265
+ except ValueError:
266
+ raise DistlibException('invalid name or version: %r, %r' % (name, version))
267
+
268
+ for dist in self.get_distributions():
269
+ # We hit a problem on Travis where enum34 was installed and doesn't
270
+ # have a provides attribute ...
271
+ if not hasattr(dist, 'provides'):
272
+ logger.debug('No "provides": %s', dist)
273
+ else:
274
+ provided = dist.provides
275
+
276
+ for p in provided:
277
+ p_name, p_ver = parse_name_and_version(p)
278
+ if matcher is None:
279
+ if p_name == name:
280
+ yield dist
281
+ break
282
+ else:
283
+ if p_name == name and matcher.match(p_ver):
284
+ yield dist
285
+ break
286
+
287
+ def get_file_path(self, name, relative_path):
288
+ """
289
+ Return the path to a resource file.
290
+ """
291
+ dist = self.get_distribution(name)
292
+ if dist is None:
293
+ raise LookupError('no distribution named %r found' % name)
294
+ return dist.get_resource_path(relative_path)
295
+
296
+ def get_exported_entries(self, category, name=None):
297
+ """
298
+ Return all of the exported entries in a particular category.
299
+
300
+ :param category: The category to search for entries.
301
+ :param name: If specified, only entries with that name are returned.
302
+ """
303
+ for dist in self.get_distributions():
304
+ r = dist.exports
305
+ if category in r:
306
+ d = r[category]
307
+ if name is not None:
308
+ if name in d:
309
+ yield d[name]
310
+ else:
311
+ for v in d.values():
312
+ yield v
313
+
314
+
315
+ class Distribution(object):
316
+ """
317
+ A base class for distributions, whether installed or from indexes.
318
+ Either way, it must have some metadata, so that's all that's needed
319
+ for construction.
320
+ """
321
+
322
+ build_time_dependency = False
323
+ """
324
+ Set to True if it's known to be only a build-time dependency (i.e.
325
+ not needed after installation).
326
+ """
327
+
328
+ requested = False
329
+ """A boolean that indicates whether the ``REQUESTED`` metadata file is
330
+ present (in other words, whether the package was installed by user
331
+ request or it was installed as a dependency)."""
332
+
333
+ def __init__(self, metadata):
334
+ """
335
+ Initialise an instance.
336
+ :param metadata: The instance of :class:`Metadata` describing this
337
+ distribution.
338
+ """
339
+ self.metadata = metadata
340
+ self.name = metadata.name
341
+ self.key = self.name.lower() # for case-insensitive comparisons
342
+ self.version = metadata.version
343
+ self.locator = None
344
+ self.digest = None
345
+ self.extras = None # additional features requested
346
+ self.context = None # environment marker overrides
347
+ self.download_urls = set()
348
+ self.digests = {}
349
+
350
+ @property
351
+ def source_url(self):
352
+ """
353
+ The source archive download URL for this distribution.
354
+ """
355
+ return self.metadata.source_url
356
+
357
+ download_url = source_url # Backward compatibility
358
+
359
+ @property
360
+ def name_and_version(self):
361
+ """
362
+ A utility property which displays the name and version in parentheses.
363
+ """
364
+ return '%s (%s)' % (self.name, self.version)
365
+
366
+ @property
367
+ def provides(self):
368
+ """
369
+ A set of distribution names and versions provided by this distribution.
370
+ :return: A set of "name (version)" strings.
371
+ """
372
+ plist = self.metadata.provides
373
+ s = '%s (%s)' % (self.name, self.version)
374
+ if s not in plist:
375
+ plist.append(s)
376
+ return plist
377
+
378
+ def _get_requirements(self, req_attr):
379
+ md = self.metadata
380
+ reqts = getattr(md, req_attr)
381
+ logger.debug('%s: got requirements %r from metadata: %r', self.name, req_attr, reqts)
382
+ return set(md.get_requirements(reqts, extras=self.extras, env=self.context))
383
+
384
+ @property
385
+ def run_requires(self):
386
+ return self._get_requirements('run_requires')
387
+
388
+ @property
389
+ def meta_requires(self):
390
+ return self._get_requirements('meta_requires')
391
+
392
+ @property
393
+ def build_requires(self):
394
+ return self._get_requirements('build_requires')
395
+
396
+ @property
397
+ def test_requires(self):
398
+ return self._get_requirements('test_requires')
399
+
400
+ @property
401
+ def dev_requires(self):
402
+ return self._get_requirements('dev_requires')
403
+
404
+ def matches_requirement(self, req):
405
+ """
406
+ Say if this instance matches (fulfills) a requirement.
407
+ :param req: The requirement to match.
408
+ :rtype req: str
409
+ :return: True if it matches, else False.
410
+ """
411
+ # Requirement may contain extras - parse to lose those
412
+ # from what's passed to the matcher
413
+ r = parse_requirement(req)
414
+ scheme = get_scheme(self.metadata.scheme)
415
+ try:
416
+ matcher = scheme.matcher(r.requirement)
417
+ except UnsupportedVersionError:
418
+ # XXX compat-mode if cannot read the version
419
+ logger.warning('could not read version %r - using name only', req)
420
+ name = req.split()[0]
421
+ matcher = scheme.matcher(name)
422
+
423
+ name = matcher.key # case-insensitive
424
+
425
+ result = False
426
+ for p in self.provides:
427
+ p_name, p_ver = parse_name_and_version(p)
428
+ if p_name != name:
429
+ continue
430
+ try:
431
+ result = matcher.match(p_ver)
432
+ break
433
+ except UnsupportedVersionError:
434
+ pass
435
+ return result
436
+
437
+ def __repr__(self):
438
+ """
439
+ Return a textual representation of this instance,
440
+ """
441
+ if self.source_url:
442
+ suffix = ' [%s]' % self.source_url
443
+ else:
444
+ suffix = ''
445
+ return '<Distribution %s (%s)%s>' % (self.name, self.version, suffix)
446
+
447
+ def __eq__(self, other):
448
+ """
449
+ See if this distribution is the same as another.
450
+ :param other: The distribution to compare with. To be equal to one
451
+ another. distributions must have the same type, name,
452
+ version and source_url.
453
+ :return: True if it is the same, else False.
454
+ """
455
+ if type(other) is not type(self):
456
+ result = False
457
+ else:
458
+ result = (self.name == other.name and self.version == other.version and self.source_url == other.source_url)
459
+ return result
460
+
461
+ def __hash__(self):
462
+ """
463
+ Compute hash in a way which matches the equality test.
464
+ """
465
+ return hash(self.name) + hash(self.version) + hash(self.source_url)
466
+
467
+
468
+ class BaseInstalledDistribution(Distribution):
469
+ """
470
+ This is the base class for installed distributions (whether PEP 376 or
471
+ legacy).
472
+ """
473
+
474
+ hasher = None
475
+
476
+ def __init__(self, metadata, path, env=None):
477
+ """
478
+ Initialise an instance.
479
+ :param metadata: An instance of :class:`Metadata` which describes the
480
+ distribution. This will normally have been initialised
481
+ from a metadata file in the ``path``.
482
+ :param path: The path of the ``.dist-info`` or ``.egg-info``
483
+ directory for the distribution.
484
+ :param env: This is normally the :class:`DistributionPath`
485
+ instance where this distribution was found.
486
+ """
487
+ super(BaseInstalledDistribution, self).__init__(metadata)
488
+ self.path = path
489
+ self.dist_path = env
490
+
491
+ def get_hash(self, data, hasher=None):
492
+ """
493
+ Get the hash of some data, using a particular hash algorithm, if
494
+ specified.
495
+
496
+ :param data: The data to be hashed.
497
+ :type data: bytes
498
+ :param hasher: The name of a hash implementation, supported by hashlib,
499
+ or ``None``. Examples of valid values are ``'sha1'``,
500
+ ``'sha224'``, ``'sha384'``, '``sha256'``, ``'md5'`` and
501
+ ``'sha512'``. If no hasher is specified, the ``hasher``
502
+ attribute of the :class:`InstalledDistribution` instance
503
+ is used. If the hasher is determined to be ``None``, MD5
504
+ is used as the hashing algorithm.
505
+ :returns: The hash of the data. If a hasher was explicitly specified,
506
+ the returned hash will be prefixed with the specified hasher
507
+ followed by '='.
508
+ :rtype: str
509
+ """
510
+ if hasher is None:
511
+ hasher = self.hasher
512
+ if hasher is None:
513
+ hasher = hashlib.md5
514
+ prefix = ''
515
+ else:
516
+ hasher = getattr(hashlib, hasher)
517
+ prefix = '%s=' % self.hasher
518
+ digest = hasher(data).digest()
519
+ digest = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
520
+ return '%s%s' % (prefix, digest)
521
+
522
+
523
+ class InstalledDistribution(BaseInstalledDistribution):
524
+ """
525
+ Created with the *path* of the ``.dist-info`` directory provided to the
526
+ constructor. It reads the metadata contained in ``pydist.json`` when it is
527
+ instantiated., or uses a passed in Metadata instance (useful for when
528
+ dry-run mode is being used).
529
+ """
530
+
531
+ hasher = 'sha256'
532
+
533
+ def __init__(self, path, metadata=None, env=None):
534
+ self.modules = []
535
+ self.finder = finder = resources.finder_for_path(path)
536
+ if finder is None:
537
+ raise ValueError('finder unavailable for %s' % path)
538
+ if env and env._cache_enabled and path in env._cache.path:
539
+ metadata = env._cache.path[path].metadata
540
+ elif metadata is None:
541
+ r = finder.find(METADATA_FILENAME)
542
+ # Temporary - for Wheel 0.23 support
543
+ if r is None:
544
+ r = finder.find(WHEEL_METADATA_FILENAME)
545
+ # Temporary - for legacy support
546
+ if r is None:
547
+ r = finder.find(LEGACY_METADATA_FILENAME)
548
+ if r is None:
549
+ raise ValueError('no %s found in %s' % (METADATA_FILENAME, path))
550
+ with contextlib.closing(r.as_stream()) as stream:
551
+ metadata = Metadata(fileobj=stream, scheme='legacy')
552
+
553
+ super(InstalledDistribution, self).__init__(metadata, path, env)
554
+
555
+ if env and env._cache_enabled:
556
+ env._cache.add(self)
557
+
558
+ r = finder.find('REQUESTED')
559
+ self.requested = r is not None
560
+ p = os.path.join(path, 'top_level.txt')
561
+ if os.path.exists(p):
562
+ with open(p, 'rb') as f:
563
+ data = f.read().decode('utf-8')
564
+ self.modules = data.splitlines()
565
+
566
+ def __repr__(self):
567
+ return '<InstalledDistribution %r %s at %r>' % (self.name, self.version, self.path)
568
+
569
+ def __str__(self):
570
+ return "%s %s" % (self.name, self.version)
571
+
572
+ def _get_records(self):
573
+ """
574
+ Get the list of installed files for the distribution
575
+ :return: A list of tuples of path, hash and size. Note that hash and
576
+ size might be ``None`` for some entries. The path is exactly
577
+ as stored in the file (which is as in PEP 376).
578
+ """
579
+ results = []
580
+ r = self.get_distinfo_resource('RECORD')
581
+ with contextlib.closing(r.as_stream()) as stream:
582
+ with CSVReader(stream=stream) as record_reader:
583
+ # Base location is parent dir of .dist-info dir
584
+ # base_location = os.path.dirname(self.path)
585
+ # base_location = os.path.abspath(base_location)
586
+ for row in record_reader:
587
+ missing = [None for i in range(len(row), 3)]
588
+ path, checksum, size = row + missing
589
+ # if not os.path.isabs(path):
590
+ # path = path.replace('/', os.sep)
591
+ # path = os.path.join(base_location, path)
592
+ results.append((path, checksum, size))
593
+ return results
594
+
595
+ @cached_property
596
+ def exports(self):
597
+ """
598
+ Return the information exported by this distribution.
599
+ :return: A dictionary of exports, mapping an export category to a dict
600
+ of :class:`ExportEntry` instances describing the individual
601
+ export entries, and keyed by name.
602
+ """
603
+ result = {}
604
+ r = self.get_distinfo_resource(EXPORTS_FILENAME)
605
+ if r:
606
+ result = self.read_exports()
607
+ return result
608
+
609
+ def read_exports(self):
610
+ """
611
+ Read exports data from a file in .ini format.
612
+
613
+ :return: A dictionary of exports, mapping an export category to a list
614
+ of :class:`ExportEntry` instances describing the individual
615
+ export entries.
616
+ """
617
+ result = {}
618
+ r = self.get_distinfo_resource(EXPORTS_FILENAME)
619
+ if r:
620
+ with contextlib.closing(r.as_stream()) as stream:
621
+ result = read_exports(stream)
622
+ return result
623
+
624
+ def write_exports(self, exports):
625
+ """
626
+ Write a dictionary of exports to a file in .ini format.
627
+ :param exports: A dictionary of exports, mapping an export category to
628
+ a list of :class:`ExportEntry` instances describing the
629
+ individual export entries.
630
+ """
631
+ rf = self.get_distinfo_file(EXPORTS_FILENAME)
632
+ with open(rf, 'w') as f:
633
+ write_exports(exports, f)
634
+
635
+ def get_resource_path(self, relative_path):
636
+ """
637
+ NOTE: This API may change in the future.
638
+
639
+ Return the absolute path to a resource file with the given relative
640
+ path.
641
+
642
+ :param relative_path: The path, relative to .dist-info, of the resource
643
+ of interest.
644
+ :return: The absolute path where the resource is to be found.
645
+ """
646
+ r = self.get_distinfo_resource('RESOURCES')
647
+ with contextlib.closing(r.as_stream()) as stream:
648
+ with CSVReader(stream=stream) as resources_reader:
649
+ for relative, destination in resources_reader:
650
+ if relative == relative_path:
651
+ return destination
652
+ raise KeyError('no resource file with relative path %r '
653
+ 'is installed' % relative_path)
654
+
655
+ def list_installed_files(self):
656
+ """
657
+ Iterates over the ``RECORD`` entries and returns a tuple
658
+ ``(path, hash, size)`` for each line.
659
+
660
+ :returns: iterator of (path, hash, size)
661
+ """
662
+ for result in self._get_records():
663
+ yield result
664
+
665
+ def write_installed_files(self, paths, prefix, dry_run=False):
666
+ """
667
+ Writes the ``RECORD`` file, using the ``paths`` iterable passed in. Any
668
+ existing ``RECORD`` file is silently overwritten.
669
+
670
+ prefix is used to determine when to write absolute paths.
671
+ """
672
+ prefix = os.path.join(prefix, '')
673
+ base = os.path.dirname(self.path)
674
+ base_under_prefix = base.startswith(prefix)
675
+ base = os.path.join(base, '')
676
+ record_path = self.get_distinfo_file('RECORD')
677
+ logger.info('creating %s', record_path)
678
+ if dry_run:
679
+ return None
680
+ with CSVWriter(record_path) as writer:
681
+ for path in paths:
682
+ if os.path.isdir(path) or path.endswith(('.pyc', '.pyo')):
683
+ # do not put size and hash, as in PEP-376
684
+ hash_value = size = ''
685
+ else:
686
+ size = '%d' % os.path.getsize(path)
687
+ with open(path, 'rb') as fp:
688
+ hash_value = self.get_hash(fp.read())
689
+ if path.startswith(base) or (base_under_prefix and path.startswith(prefix)):
690
+ path = os.path.relpath(path, base)
691
+ writer.writerow((path, hash_value, size))
692
+
693
+ # add the RECORD file itself
694
+ if record_path.startswith(base):
695
+ record_path = os.path.relpath(record_path, base)
696
+ writer.writerow((record_path, '', ''))
697
+ return record_path
698
+
699
+ def check_installed_files(self):
700
+ """
701
+ Checks that the hashes and sizes of the files in ``RECORD`` are
702
+ matched by the files themselves. Returns a (possibly empty) list of
703
+ mismatches. Each entry in the mismatch list will be a tuple consisting
704
+ of the path, 'exists', 'size' or 'hash' according to what didn't match
705
+ (existence is checked first, then size, then hash), the expected
706
+ value and the actual value.
707
+ """
708
+ mismatches = []
709
+ base = os.path.dirname(self.path)
710
+ record_path = self.get_distinfo_file('RECORD')
711
+ for path, hash_value, size in self.list_installed_files():
712
+ if not os.path.isabs(path):
713
+ path = os.path.join(base, path)
714
+ if path == record_path:
715
+ continue
716
+ if not os.path.exists(path):
717
+ mismatches.append((path, 'exists', True, False))
718
+ elif os.path.isfile(path):
719
+ actual_size = str(os.path.getsize(path))
720
+ if size and actual_size != size:
721
+ mismatches.append((path, 'size', size, actual_size))
722
+ elif hash_value:
723
+ if '=' in hash_value:
724
+ hasher = hash_value.split('=', 1)[0]
725
+ else:
726
+ hasher = None
727
+
728
+ with open(path, 'rb') as f:
729
+ actual_hash = self.get_hash(f.read(), hasher)
730
+ if actual_hash != hash_value:
731
+ mismatches.append((path, 'hash', hash_value, actual_hash))
732
+ return mismatches
733
+
734
+ @cached_property
735
+ def shared_locations(self):
736
+ """
737
+ A dictionary of shared locations whose keys are in the set 'prefix',
738
+ 'purelib', 'platlib', 'scripts', 'headers', 'data' and 'namespace'.
739
+ The corresponding value is the absolute path of that category for
740
+ this distribution, and takes into account any paths selected by the
741
+ user at installation time (e.g. via command-line arguments). In the
742
+ case of the 'namespace' key, this would be a list of absolute paths
743
+ for the roots of namespace packages in this distribution.
744
+
745
+ The first time this property is accessed, the relevant information is
746
+ read from the SHARED file in the .dist-info directory.
747
+ """
748
+ result = {}
749
+ shared_path = os.path.join(self.path, 'SHARED')
750
+ if os.path.isfile(shared_path):
751
+ with codecs.open(shared_path, 'r', encoding='utf-8') as f:
752
+ lines = f.read().splitlines()
753
+ for line in lines:
754
+ key, value = line.split('=', 1)
755
+ if key == 'namespace':
756
+ result.setdefault(key, []).append(value)
757
+ else:
758
+ result[key] = value
759
+ return result
760
+
761
+ def write_shared_locations(self, paths, dry_run=False):
762
+ """
763
+ Write shared location information to the SHARED file in .dist-info.
764
+ :param paths: A dictionary as described in the documentation for
765
+ :meth:`shared_locations`.
766
+ :param dry_run: If True, the action is logged but no file is actually
767
+ written.
768
+ :return: The path of the file written to.
769
+ """
770
+ shared_path = os.path.join(self.path, 'SHARED')
771
+ logger.info('creating %s', shared_path)
772
+ if dry_run:
773
+ return None
774
+ lines = []
775
+ for key in ('prefix', 'lib', 'headers', 'scripts', 'data'):
776
+ path = paths[key]
777
+ if os.path.isdir(paths[key]):
778
+ lines.append('%s=%s' % (key, path))
779
+ for ns in paths.get('namespace', ()):
780
+ lines.append('namespace=%s' % ns)
781
+
782
+ with codecs.open(shared_path, 'w', encoding='utf-8') as f:
783
+ f.write('\n'.join(lines))
784
+ return shared_path
785
+
786
+ def get_distinfo_resource(self, path):
787
+ if path not in DIST_FILES:
788
+ raise DistlibException('invalid path for a dist-info file: '
789
+ '%r at %r' % (path, self.path))
790
+ finder = resources.finder_for_path(self.path)
791
+ if finder is None:
792
+ raise DistlibException('Unable to get a finder for %s' % self.path)
793
+ return finder.find(path)
794
+
795
+ def get_distinfo_file(self, path):
796
+ """
797
+ Returns a path located under the ``.dist-info`` directory. Returns a
798
+ string representing the path.
799
+
800
+ :parameter path: a ``'/'``-separated path relative to the
801
+ ``.dist-info`` directory or an absolute path;
802
+ If *path* is an absolute path and doesn't start
803
+ with the ``.dist-info`` directory path,
804
+ a :class:`DistlibException` is raised
805
+ :type path: str
806
+ :rtype: str
807
+ """
808
+ # Check if it is an absolute path # XXX use relpath, add tests
809
+ if path.find(os.sep) >= 0:
810
+ # it's an absolute path?
811
+ distinfo_dirname, path = path.split(os.sep)[-2:]
812
+ if distinfo_dirname != self.path.split(os.sep)[-1]:
813
+ raise DistlibException('dist-info file %r does not belong to the %r %s '
814
+ 'distribution' % (path, self.name, self.version))
815
+
816
+ # The file must be relative
817
+ if path not in DIST_FILES:
818
+ raise DistlibException('invalid path for a dist-info file: '
819
+ '%r at %r' % (path, self.path))
820
+
821
+ return os.path.join(self.path, path)
822
+
823
+ def list_distinfo_files(self):
824
+ """
825
+ Iterates over the ``RECORD`` entries and returns paths for each line if
826
+ the path is pointing to a file located in the ``.dist-info`` directory
827
+ or one of its subdirectories.
828
+
829
+ :returns: iterator of paths
830
+ """
831
+ base = os.path.dirname(self.path)
832
+ for path, checksum, size in self._get_records():
833
+ # XXX add separator or use real relpath algo
834
+ if not os.path.isabs(path):
835
+ path = os.path.join(base, path)
836
+ if path.startswith(self.path):
837
+ yield path
838
+
839
+ def __eq__(self, other):
840
+ return (isinstance(other, InstalledDistribution) and self.path == other.path)
841
+
842
+ # See http://docs.python.org/reference/datamodel#object.__hash__
843
+ __hash__ = object.__hash__
844
+
845
+
846
+ class EggInfoDistribution(BaseInstalledDistribution):
847
+ """Created with the *path* of the ``.egg-info`` directory or file provided
848
+ to the constructor. It reads the metadata contained in the file itself, or
849
+ if the given path happens to be a directory, the metadata is read from the
850
+ file ``PKG-INFO`` under that directory."""
851
+
852
+ requested = True # as we have no way of knowing, assume it was
853
+ shared_locations = {}
854
+
855
+ def __init__(self, path, env=None):
856
+
857
+ def set_name_and_version(s, n, v):
858
+ s.name = n
859
+ s.key = n.lower() # for case-insensitive comparisons
860
+ s.version = v
861
+
862
+ self.path = path
863
+ self.dist_path = env
864
+ if env and env._cache_enabled and path in env._cache_egg.path:
865
+ metadata = env._cache_egg.path[path].metadata
866
+ set_name_and_version(self, metadata.name, metadata.version)
867
+ else:
868
+ metadata = self._get_metadata(path)
869
+
870
+ # Need to be set before caching
871
+ set_name_and_version(self, metadata.name, metadata.version)
872
+
873
+ if env and env._cache_enabled:
874
+ env._cache_egg.add(self)
875
+ super(EggInfoDistribution, self).__init__(metadata, path, env)
876
+
877
+ def _get_metadata(self, path):
878
+ requires = None
879
+
880
+ def parse_requires_data(data):
881
+ """Create a list of dependencies from a requires.txt file.
882
+
883
+ *data*: the contents of a setuptools-produced requires.txt file.
884
+ """
885
+ reqs = []
886
+ lines = data.splitlines()
887
+ for line in lines:
888
+ line = line.strip()
889
+ # sectioned files have bare newlines (separating sections)
890
+ if not line: # pragma: no cover
891
+ continue
892
+ if line.startswith('['): # pragma: no cover
893
+ logger.warning('Unexpected line: quitting requirement scan: %r', line)
894
+ break
895
+ r = parse_requirement(line)
896
+ if not r: # pragma: no cover
897
+ logger.warning('Not recognised as a requirement: %r', line)
898
+ continue
899
+ if r.extras: # pragma: no cover
900
+ logger.warning('extra requirements in requires.txt are '
901
+ 'not supported')
902
+ if not r.constraints:
903
+ reqs.append(r.name)
904
+ else:
905
+ cons = ', '.join('%s%s' % c for c in r.constraints)
906
+ reqs.append('%s (%s)' % (r.name, cons))
907
+ return reqs
908
+
909
+ def parse_requires_path(req_path):
910
+ """Create a list of dependencies from a requires.txt file.
911
+
912
+ *req_path*: the path to a setuptools-produced requires.txt file.
913
+ """
914
+
915
+ reqs = []
916
+ try:
917
+ with codecs.open(req_path, 'r', 'utf-8') as fp:
918
+ reqs = parse_requires_data(fp.read())
919
+ except IOError:
920
+ pass
921
+ return reqs
922
+
923
+ tl_path = tl_data = None
924
+ if path.endswith('.egg'):
925
+ if os.path.isdir(path):
926
+ p = os.path.join(path, 'EGG-INFO')
927
+ meta_path = os.path.join(p, 'PKG-INFO')
928
+ metadata = Metadata(path=meta_path, scheme='legacy')
929
+ req_path = os.path.join(p, 'requires.txt')
930
+ tl_path = os.path.join(p, 'top_level.txt')
931
+ requires = parse_requires_path(req_path)
932
+ else:
933
+ # FIXME handle the case where zipfile is not available
934
+ zipf = zipimport.zipimporter(path)
935
+ fileobj = StringIO(zipf.get_data('EGG-INFO/PKG-INFO').decode('utf8'))
936
+ metadata = Metadata(fileobj=fileobj, scheme='legacy')
937
+ try:
938
+ data = zipf.get_data('EGG-INFO/requires.txt')
939
+ tl_data = zipf.get_data('EGG-INFO/top_level.txt').decode('utf-8')
940
+ requires = parse_requires_data(data.decode('utf-8'))
941
+ except IOError:
942
+ requires = None
943
+ elif path.endswith('.egg-info'):
944
+ if os.path.isdir(path):
945
+ req_path = os.path.join(path, 'requires.txt')
946
+ requires = parse_requires_path(req_path)
947
+ path = os.path.join(path, 'PKG-INFO')
948
+ tl_path = os.path.join(path, 'top_level.txt')
949
+ metadata = Metadata(path=path, scheme='legacy')
950
+ else:
951
+ raise DistlibException('path must end with .egg-info or .egg, '
952
+ 'got %r' % path)
953
+
954
+ if requires:
955
+ metadata.add_requirements(requires)
956
+ # look for top-level modules in top_level.txt, if present
957
+ if tl_data is None:
958
+ if tl_path is not None and os.path.exists(tl_path):
959
+ with open(tl_path, 'rb') as f:
960
+ tl_data = f.read().decode('utf-8')
961
+ if not tl_data:
962
+ tl_data = []
963
+ else:
964
+ tl_data = tl_data.splitlines()
965
+ self.modules = tl_data
966
+ return metadata
967
+
968
+ def __repr__(self):
969
+ return '<EggInfoDistribution %r %s at %r>' % (self.name, self.version, self.path)
970
+
971
+ def __str__(self):
972
+ return "%s %s" % (self.name, self.version)
973
+
974
+ def check_installed_files(self):
975
+ """
976
+ Checks that the hashes and sizes of the files in ``RECORD`` are
977
+ matched by the files themselves. Returns a (possibly empty) list of
978
+ mismatches. Each entry in the mismatch list will be a tuple consisting
979
+ of the path, 'exists', 'size' or 'hash' according to what didn't match
980
+ (existence is checked first, then size, then hash), the expected
981
+ value and the actual value.
982
+ """
983
+ mismatches = []
984
+ record_path = os.path.join(self.path, 'installed-files.txt')
985
+ if os.path.exists(record_path):
986
+ for path, _, _ in self.list_installed_files():
987
+ if path == record_path:
988
+ continue
989
+ if not os.path.exists(path):
990
+ mismatches.append((path, 'exists', True, False))
991
+ return mismatches
992
+
993
+ def list_installed_files(self):
994
+ """
995
+ Iterates over the ``installed-files.txt`` entries and returns a tuple
996
+ ``(path, hash, size)`` for each line.
997
+
998
+ :returns: a list of (path, hash, size)
999
+ """
1000
+
1001
+ def _md5(path):
1002
+ f = open(path, 'rb')
1003
+ try:
1004
+ content = f.read()
1005
+ finally:
1006
+ f.close()
1007
+ return hashlib.md5(content).hexdigest()
1008
+
1009
+ def _size(path):
1010
+ return os.stat(path).st_size
1011
+
1012
+ record_path = os.path.join(self.path, 'installed-files.txt')
1013
+ result = []
1014
+ if os.path.exists(record_path):
1015
+ with codecs.open(record_path, 'r', encoding='utf-8') as f:
1016
+ for line in f:
1017
+ line = line.strip()
1018
+ p = os.path.normpath(os.path.join(self.path, line))
1019
+ # "./" is present as a marker between installed files
1020
+ # and installation metadata files
1021
+ if not os.path.exists(p):
1022
+ logger.warning('Non-existent file: %s', p)
1023
+ if p.endswith(('.pyc', '.pyo')):
1024
+ continue
1025
+ # otherwise fall through and fail
1026
+ if not os.path.isdir(p):
1027
+ result.append((p, _md5(p), _size(p)))
1028
+ result.append((record_path, None, None))
1029
+ return result
1030
+
1031
+ def list_distinfo_files(self, absolute=False):
1032
+ """
1033
+ Iterates over the ``installed-files.txt`` entries and returns paths for
1034
+ each line if the path is pointing to a file located in the
1035
+ ``.egg-info`` directory or one of its subdirectories.
1036
+
1037
+ :parameter absolute: If *absolute* is ``True``, each returned path is
1038
+ transformed into a local absolute path. Otherwise the
1039
+ raw value from ``installed-files.txt`` is returned.
1040
+ :type absolute: boolean
1041
+ :returns: iterator of paths
1042
+ """
1043
+ record_path = os.path.join(self.path, 'installed-files.txt')
1044
+ if os.path.exists(record_path):
1045
+ skip = True
1046
+ with codecs.open(record_path, 'r', encoding='utf-8') as f:
1047
+ for line in f:
1048
+ line = line.strip()
1049
+ if line == './':
1050
+ skip = False
1051
+ continue
1052
+ if not skip:
1053
+ p = os.path.normpath(os.path.join(self.path, line))
1054
+ if p.startswith(self.path):
1055
+ if absolute:
1056
+ yield p
1057
+ else:
1058
+ yield line
1059
+
1060
+ def __eq__(self, other):
1061
+ return (isinstance(other, EggInfoDistribution) and self.path == other.path)
1062
+
1063
+ # See http://docs.python.org/reference/datamodel#object.__hash__
1064
+ __hash__ = object.__hash__
1065
+
1066
+
1067
+ new_dist_class = InstalledDistribution
1068
+ old_dist_class = EggInfoDistribution
1069
+
1070
+
1071
+ class DependencyGraph(object):
1072
+ """
1073
+ Represents a dependency graph between distributions.
1074
+
1075
+ The dependency relationships are stored in an ``adjacency_list`` that maps
1076
+ distributions to a list of ``(other, label)`` tuples where ``other``
1077
+ is a distribution and the edge is labeled with ``label`` (i.e. the version
1078
+ specifier, if such was provided). Also, for more efficient traversal, for
1079
+ every distribution ``x``, a list of predecessors is kept in
1080
+ ``reverse_list[x]``. An edge from distribution ``a`` to
1081
+ distribution ``b`` means that ``a`` depends on ``b``. If any missing
1082
+ dependencies are found, they are stored in ``missing``, which is a
1083
+ dictionary that maps distributions to a list of requirements that were not
1084
+ provided by any other distributions.
1085
+ """
1086
+
1087
+ def __init__(self):
1088
+ self.adjacency_list = {}
1089
+ self.reverse_list = {}
1090
+ self.missing = {}
1091
+
1092
+ def add_distribution(self, distribution):
1093
+ """Add the *distribution* to the graph.
1094
+
1095
+ :type distribution: :class:`distutils2.database.InstalledDistribution`
1096
+ or :class:`distutils2.database.EggInfoDistribution`
1097
+ """
1098
+ self.adjacency_list[distribution] = []
1099
+ self.reverse_list[distribution] = []
1100
+ # self.missing[distribution] = []
1101
+
1102
+ def add_edge(self, x, y, label=None):
1103
+ """Add an edge from distribution *x* to distribution *y* with the given
1104
+ *label*.
1105
+
1106
+ :type x: :class:`distutils2.database.InstalledDistribution` or
1107
+ :class:`distutils2.database.EggInfoDistribution`
1108
+ :type y: :class:`distutils2.database.InstalledDistribution` or
1109
+ :class:`distutils2.database.EggInfoDistribution`
1110
+ :type label: ``str`` or ``None``
1111
+ """
1112
+ self.adjacency_list[x].append((y, label))
1113
+ # multiple edges are allowed, so be careful
1114
+ if x not in self.reverse_list[y]:
1115
+ self.reverse_list[y].append(x)
1116
+
1117
+ def add_missing(self, distribution, requirement):
1118
+ """
1119
+ Add a missing *requirement* for the given *distribution*.
1120
+
1121
+ :type distribution: :class:`distutils2.database.InstalledDistribution`
1122
+ or :class:`distutils2.database.EggInfoDistribution`
1123
+ :type requirement: ``str``
1124
+ """
1125
+ logger.debug('%s missing %r', distribution, requirement)
1126
+ self.missing.setdefault(distribution, []).append(requirement)
1127
+
1128
+ def _repr_dist(self, dist):
1129
+ return '%s %s' % (dist.name, dist.version)
1130
+
1131
+ def repr_node(self, dist, level=1):
1132
+ """Prints only a subgraph"""
1133
+ output = [self._repr_dist(dist)]
1134
+ for other, label in self.adjacency_list[dist]:
1135
+ dist = self._repr_dist(other)
1136
+ if label is not None:
1137
+ dist = '%s [%s]' % (dist, label)
1138
+ output.append(' ' * level + str(dist))
1139
+ suboutput = self.repr_node(other, level + 1)
1140
+ subs = suboutput.split('\n')
1141
+ output.extend(subs[1:])
1142
+ return '\n'.join(output)
1143
+
1144
+ def to_dot(self, f, skip_disconnected=True):
1145
+ """Writes a DOT output for the graph to the provided file *f*.
1146
+
1147
+ If *skip_disconnected* is set to ``True``, then all distributions
1148
+ that are not dependent on any other distribution are skipped.
1149
+
1150
+ :type f: has to support ``file``-like operations
1151
+ :type skip_disconnected: ``bool``
1152
+ """
1153
+ disconnected = []
1154
+
1155
+ f.write("digraph dependencies {\n")
1156
+ for dist, adjs in self.adjacency_list.items():
1157
+ if len(adjs) == 0 and not skip_disconnected:
1158
+ disconnected.append(dist)
1159
+ for other, label in adjs:
1160
+ if label is not None:
1161
+ f.write('"%s" -> "%s" [label="%s"]\n' % (dist.name, other.name, label))
1162
+ else:
1163
+ f.write('"%s" -> "%s"\n' % (dist.name, other.name))
1164
+ if not skip_disconnected and len(disconnected) > 0:
1165
+ f.write('subgraph disconnected {\n')
1166
+ f.write('label = "Disconnected"\n')
1167
+ f.write('bgcolor = red\n')
1168
+
1169
+ for dist in disconnected:
1170
+ f.write('"%s"' % dist.name)
1171
+ f.write('\n')
1172
+ f.write('}\n')
1173
+ f.write('}\n')
1174
+
1175
+ def topological_sort(self):
1176
+ """
1177
+ Perform a topological sort of the graph.
1178
+ :return: A tuple, the first element of which is a topologically sorted
1179
+ list of distributions, and the second element of which is a
1180
+ list of distributions that cannot be sorted because they have
1181
+ circular dependencies and so form a cycle.
1182
+ """
1183
+ result = []
1184
+ # Make a shallow copy of the adjacency list
1185
+ alist = {}
1186
+ for k, v in self.adjacency_list.items():
1187
+ alist[k] = v[:]
1188
+ while True:
1189
+ # See what we can remove in this run
1190
+ to_remove = []
1191
+ for k, v in list(alist.items())[:]:
1192
+ if not v:
1193
+ to_remove.append(k)
1194
+ del alist[k]
1195
+ if not to_remove:
1196
+ # What's left in alist (if anything) is a cycle.
1197
+ break
1198
+ # Remove from the adjacency list of others
1199
+ for k, v in alist.items():
1200
+ alist[k] = [(d, r) for d, r in v if d not in to_remove]
1201
+ logger.debug('Moving to result: %s', ['%s (%s)' % (d.name, d.version) for d in to_remove])
1202
+ result.extend(to_remove)
1203
+ return result, list(alist.keys())
1204
+
1205
+ def __repr__(self):
1206
+ """Representation of the graph"""
1207
+ output = []
1208
+ for dist, adjs in self.adjacency_list.items():
1209
+ output.append(self.repr_node(dist))
1210
+ return '\n'.join(output)
1211
+
1212
+
1213
+ def make_graph(dists, scheme='default'):
1214
+ """Makes a dependency graph from the given distributions.
1215
+
1216
+ :parameter dists: a list of distributions
1217
+ :type dists: list of :class:`distutils2.database.InstalledDistribution` and
1218
+ :class:`distutils2.database.EggInfoDistribution` instances
1219
+ :rtype: a :class:`DependencyGraph` instance
1220
+ """
1221
+ scheme = get_scheme(scheme)
1222
+ graph = DependencyGraph()
1223
+ provided = {} # maps names to lists of (version, dist) tuples
1224
+
1225
+ # first, build the graph and find out what's provided
1226
+ for dist in dists:
1227
+ graph.add_distribution(dist)
1228
+
1229
+ for p in dist.provides:
1230
+ name, version = parse_name_and_version(p)
1231
+ logger.debug('Add to provided: %s, %s, %s', name, version, dist)
1232
+ provided.setdefault(name, []).append((version, dist))
1233
+
1234
+ # now make the edges
1235
+ for dist in dists:
1236
+ requires = (dist.run_requires | dist.meta_requires | dist.build_requires | dist.dev_requires)
1237
+ for req in requires:
1238
+ try:
1239
+ matcher = scheme.matcher(req)
1240
+ except UnsupportedVersionError:
1241
+ # XXX compat-mode if cannot read the version
1242
+ logger.warning('could not read version %r - using name only', req)
1243
+ name = req.split()[0]
1244
+ matcher = scheme.matcher(name)
1245
+
1246
+ name = matcher.key # case-insensitive
1247
+
1248
+ matched = False
1249
+ if name in provided:
1250
+ for version, provider in provided[name]:
1251
+ try:
1252
+ match = matcher.match(version)
1253
+ except UnsupportedVersionError:
1254
+ match = False
1255
+
1256
+ if match:
1257
+ graph.add_edge(dist, provider, req)
1258
+ matched = True
1259
+ break
1260
+ if not matched:
1261
+ graph.add_missing(dist, req)
1262
+ return graph
1263
+
1264
+
1265
+ def get_dependent_dists(dists, dist):
1266
+ """Recursively generate a list of distributions from *dists* that are
1267
+ dependent on *dist*.
1268
+
1269
+ :param dists: a list of distributions
1270
+ :param dist: a distribution, member of *dists* for which we are interested
1271
+ """
1272
+ if dist not in dists:
1273
+ raise DistlibException('given distribution %r is not a member '
1274
+ 'of the list' % dist.name)
1275
+ graph = make_graph(dists)
1276
+
1277
+ dep = [dist] # dependent distributions
1278
+ todo = graph.reverse_list[dist] # list of nodes we should inspect
1279
+
1280
+ while todo:
1281
+ d = todo.pop()
1282
+ dep.append(d)
1283
+ for succ in graph.reverse_list[d]:
1284
+ if succ not in dep:
1285
+ todo.append(succ)
1286
+
1287
+ dep.pop(0) # remove dist from dep, was there to prevent infinite loops
1288
+ return dep
1289
+
1290
+
1291
+ def get_required_dists(dists, dist):
1292
+ """Recursively generate a list of distributions from *dists* that are
1293
+ required by *dist*.
1294
+
1295
+ :param dists: a list of distributions
1296
+ :param dist: a distribution, member of *dists* for which we are interested
1297
+ in finding the dependencies.
1298
+ """
1299
+ if dist not in dists:
1300
+ raise DistlibException('given distribution %r is not a member '
1301
+ 'of the list' % dist.name)
1302
+ graph = make_graph(dists)
1303
+
1304
+ req = set() # required distributions
1305
+ todo = graph.adjacency_list[dist] # list of nodes we should inspect
1306
+ seen = set(t[0] for t in todo) # already added to todo
1307
+
1308
+ while todo:
1309
+ d = todo.pop()[0]
1310
+ req.add(d)
1311
+ pred_list = graph.adjacency_list[d]
1312
+ for pred in pred_list:
1313
+ d = pred[0]
1314
+ if d not in req and d not in seen:
1315
+ seen.add(d)
1316
+ todo.append(pred)
1317
+ return req
1318
+
1319
+
1320
+ def make_dist(name, version, **kwargs):
1321
+ """
1322
+ A convenience method for making a dist given just a name and version.
1323
+ """
1324
+ summary = kwargs.pop('summary', 'Placeholder for summary')
1325
+ md = Metadata(**kwargs)
1326
+ md.name = name
1327
+ md.version = version
1328
+ md.summary = summary or 'Placeholder for summary'
1329
+ return Distribution(md)
.venv/lib/python3.13/site-packages/pip/_vendor/distlib/index.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2013-2023 Vinay Sajip.
4
+ # Licensed to the Python Software Foundation under a contributor agreement.
5
+ # See LICENSE.txt and CONTRIBUTORS.txt.
6
+ #
7
+ import hashlib
8
+ import logging
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+ import tempfile
13
+ try:
14
+ from threading import Thread
15
+ except ImportError: # pragma: no cover
16
+ from dummy_threading import Thread
17
+
18
+ from . import DistlibException
19
+ from .compat import (HTTPBasicAuthHandler, Request, HTTPPasswordMgr,
20
+ urlparse, build_opener, string_types)
21
+ from .util import zip_dir, ServerProxy
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ DEFAULT_INDEX = 'https://pypi.org/pypi'
26
+ DEFAULT_REALM = 'pypi'
27
+
28
+
29
+ class PackageIndex(object):
30
+ """
31
+ This class represents a package index compatible with PyPI, the Python
32
+ Package Index.
33
+ """
34
+
35
+ boundary = b'----------ThIs_Is_tHe_distlib_index_bouNdaRY_$'
36
+
37
+ def __init__(self, url=None):
38
+ """
39
+ Initialise an instance.
40
+
41
+ :param url: The URL of the index. If not specified, the URL for PyPI is
42
+ used.
43
+ """
44
+ self.url = url or DEFAULT_INDEX
45
+ self.read_configuration()
46
+ scheme, netloc, path, params, query, frag = urlparse(self.url)
47
+ if params or query or frag or scheme not in ('http', 'https'):
48
+ raise DistlibException('invalid repository: %s' % self.url)
49
+ self.password_handler = None
50
+ self.ssl_verifier = None
51
+ self.gpg = None
52
+ self.gpg_home = None
53
+ with open(os.devnull, 'w') as sink:
54
+ # Use gpg by default rather than gpg2, as gpg2 insists on
55
+ # prompting for passwords
56
+ for s in ('gpg', 'gpg2'):
57
+ try:
58
+ rc = subprocess.check_call([s, '--version'], stdout=sink,
59
+ stderr=sink)
60
+ if rc == 0:
61
+ self.gpg = s
62
+ break
63
+ except OSError:
64
+ pass
65
+
66
+ def _get_pypirc_command(self):
67
+ """
68
+ Get the distutils command for interacting with PyPI configurations.
69
+ :return: the command.
70
+ """
71
+ from .util import _get_pypirc_command as cmd
72
+ return cmd()
73
+
74
+ def read_configuration(self):
75
+ """
76
+ Read the PyPI access configuration as supported by distutils. This populates
77
+ ``username``, ``password``, ``realm`` and ``url`` attributes from the
78
+ configuration.
79
+ """
80
+ from .util import _load_pypirc
81
+ cfg = _load_pypirc(self)
82
+ self.username = cfg.get('username')
83
+ self.password = cfg.get('password')
84
+ self.realm = cfg.get('realm', 'pypi')
85
+ self.url = cfg.get('repository', self.url)
86
+
87
+ def save_configuration(self):
88
+ """
89
+ Save the PyPI access configuration. You must have set ``username`` and
90
+ ``password`` attributes before calling this method.
91
+ """
92
+ self.check_credentials()
93
+ from .util import _store_pypirc
94
+ _store_pypirc(self)
95
+
96
+ def check_credentials(self):
97
+ """
98
+ Check that ``username`` and ``password`` have been set, and raise an
99
+ exception if not.
100
+ """
101
+ if self.username is None or self.password is None:
102
+ raise DistlibException('username and password must be set')
103
+ pm = HTTPPasswordMgr()
104
+ _, netloc, _, _, _, _ = urlparse(self.url)
105
+ pm.add_password(self.realm, netloc, self.username, self.password)
106
+ self.password_handler = HTTPBasicAuthHandler(pm)
107
+
108
+ def register(self, metadata): # pragma: no cover
109
+ """
110
+ Register a distribution on PyPI, using the provided metadata.
111
+
112
+ :param metadata: A :class:`Metadata` instance defining at least a name
113
+ and version number for the distribution to be
114
+ registered.
115
+ :return: The HTTP response received from PyPI upon submission of the
116
+ request.
117
+ """
118
+ self.check_credentials()
119
+ metadata.validate()
120
+ d = metadata.todict()
121
+ d[':action'] = 'verify'
122
+ request = self.encode_request(d.items(), [])
123
+ self.send_request(request)
124
+ d[':action'] = 'submit'
125
+ request = self.encode_request(d.items(), [])
126
+ return self.send_request(request)
127
+
128
+ def _reader(self, name, stream, outbuf):
129
+ """
130
+ Thread runner for reading lines of from a subprocess into a buffer.
131
+
132
+ :param name: The logical name of the stream (used for logging only).
133
+ :param stream: The stream to read from. This will typically a pipe
134
+ connected to the output stream of a subprocess.
135
+ :param outbuf: The list to append the read lines to.
136
+ """
137
+ while True:
138
+ s = stream.readline()
139
+ if not s:
140
+ break
141
+ s = s.decode('utf-8').rstrip()
142
+ outbuf.append(s)
143
+ logger.debug('%s: %s' % (name, s))
144
+ stream.close()
145
+
146
+ def get_sign_command(self, filename, signer, sign_password, keystore=None): # pragma: no cover
147
+ """
148
+ Return a suitable command for signing a file.
149
+
150
+ :param filename: The pathname to the file to be signed.
151
+ :param signer: The identifier of the signer of the file.
152
+ :param sign_password: The passphrase for the signer's
153
+ private key used for signing.
154
+ :param keystore: The path to a directory which contains the keys
155
+ used in verification. If not specified, the
156
+ instance's ``gpg_home`` attribute is used instead.
157
+ :return: The signing command as a list suitable to be
158
+ passed to :class:`subprocess.Popen`.
159
+ """
160
+ cmd = [self.gpg, '--status-fd', '2', '--no-tty']
161
+ if keystore is None:
162
+ keystore = self.gpg_home
163
+ if keystore:
164
+ cmd.extend(['--homedir', keystore])
165
+ if sign_password is not None:
166
+ cmd.extend(['--batch', '--passphrase-fd', '0'])
167
+ td = tempfile.mkdtemp()
168
+ sf = os.path.join(td, os.path.basename(filename) + '.asc')
169
+ cmd.extend(['--detach-sign', '--armor', '--local-user',
170
+ signer, '--output', sf, filename])
171
+ logger.debug('invoking: %s', ' '.join(cmd))
172
+ return cmd, sf
173
+
174
+ def run_command(self, cmd, input_data=None):
175
+ """
176
+ Run a command in a child process , passing it any input data specified.
177
+
178
+ :param cmd: The command to run.
179
+ :param input_data: If specified, this must be a byte string containing
180
+ data to be sent to the child process.
181
+ :return: A tuple consisting of the subprocess' exit code, a list of
182
+ lines read from the subprocess' ``stdout``, and a list of
183
+ lines read from the subprocess' ``stderr``.
184
+ """
185
+ kwargs = {
186
+ 'stdout': subprocess.PIPE,
187
+ 'stderr': subprocess.PIPE,
188
+ }
189
+ if input_data is not None:
190
+ kwargs['stdin'] = subprocess.PIPE
191
+ stdout = []
192
+ stderr = []
193
+ p = subprocess.Popen(cmd, **kwargs)
194
+ # We don't use communicate() here because we may need to
195
+ # get clever with interacting with the command
196
+ t1 = Thread(target=self._reader, args=('stdout', p.stdout, stdout))
197
+ t1.start()
198
+ t2 = Thread(target=self._reader, args=('stderr', p.stderr, stderr))
199
+ t2.start()
200
+ if input_data is not None:
201
+ p.stdin.write(input_data)
202
+ p.stdin.close()
203
+
204
+ p.wait()
205
+ t1.join()
206
+ t2.join()
207
+ return p.returncode, stdout, stderr
208
+
209
+ def sign_file(self, filename, signer, sign_password, keystore=None): # pragma: no cover
210
+ """
211
+ Sign a file.
212
+
213
+ :param filename: The pathname to the file to be signed.
214
+ :param signer: The identifier of the signer of the file.
215
+ :param sign_password: The passphrase for the signer's
216
+ private key used for signing.
217
+ :param keystore: The path to a directory which contains the keys
218
+ used in signing. If not specified, the instance's
219
+ ``gpg_home`` attribute is used instead.
220
+ :return: The absolute pathname of the file where the signature is
221
+ stored.
222
+ """
223
+ cmd, sig_file = self.get_sign_command(filename, signer, sign_password,
224
+ keystore)
225
+ rc, stdout, stderr = self.run_command(cmd,
226
+ sign_password.encode('utf-8'))
227
+ if rc != 0:
228
+ raise DistlibException('sign command failed with error '
229
+ 'code %s' % rc)
230
+ return sig_file
231
+
232
+ def upload_file(self, metadata, filename, signer=None, sign_password=None,
233
+ filetype='sdist', pyversion='source', keystore=None):
234
+ """
235
+ Upload a release file to the index.
236
+
237
+ :param metadata: A :class:`Metadata` instance defining at least a name
238
+ and version number for the file to be uploaded.
239
+ :param filename: The pathname of the file to be uploaded.
240
+ :param signer: The identifier of the signer of the file.
241
+ :param sign_password: The passphrase for the signer's
242
+ private key used for signing.
243
+ :param filetype: The type of the file being uploaded. This is the
244
+ distutils command which produced that file, e.g.
245
+ ``sdist`` or ``bdist_wheel``.
246
+ :param pyversion: The version of Python which the release relates
247
+ to. For code compatible with any Python, this would
248
+ be ``source``, otherwise it would be e.g. ``3.2``.
249
+ :param keystore: The path to a directory which contains the keys
250
+ used in signing. If not specified, the instance's
251
+ ``gpg_home`` attribute is used instead.
252
+ :return: The HTTP response received from PyPI upon submission of the
253
+ request.
254
+ """
255
+ self.check_credentials()
256
+ if not os.path.exists(filename):
257
+ raise DistlibException('not found: %s' % filename)
258
+ metadata.validate()
259
+ d = metadata.todict()
260
+ sig_file = None
261
+ if signer:
262
+ if not self.gpg:
263
+ logger.warning('no signing program available - not signed')
264
+ else:
265
+ sig_file = self.sign_file(filename, signer, sign_password,
266
+ keystore)
267
+ with open(filename, 'rb') as f:
268
+ file_data = f.read()
269
+ md5_digest = hashlib.md5(file_data).hexdigest()
270
+ sha256_digest = hashlib.sha256(file_data).hexdigest()
271
+ d.update({
272
+ ':action': 'file_upload',
273
+ 'protocol_version': '1',
274
+ 'filetype': filetype,
275
+ 'pyversion': pyversion,
276
+ 'md5_digest': md5_digest,
277
+ 'sha256_digest': sha256_digest,
278
+ })
279
+ files = [('content', os.path.basename(filename), file_data)]
280
+ if sig_file:
281
+ with open(sig_file, 'rb') as f:
282
+ sig_data = f.read()
283
+ files.append(('gpg_signature', os.path.basename(sig_file),
284
+ sig_data))
285
+ shutil.rmtree(os.path.dirname(sig_file))
286
+ request = self.encode_request(d.items(), files)
287
+ return self.send_request(request)
288
+
289
+ def upload_documentation(self, metadata, doc_dir): # pragma: no cover
290
+ """
291
+ Upload documentation to the index.
292
+
293
+ :param metadata: A :class:`Metadata` instance defining at least a name
294
+ and version number for the documentation to be
295
+ uploaded.
296
+ :param doc_dir: The pathname of the directory which contains the
297
+ documentation. This should be the directory that
298
+ contains the ``index.html`` for the documentation.
299
+ :return: The HTTP response received from PyPI upon submission of the
300
+ request.
301
+ """
302
+ self.check_credentials()
303
+ if not os.path.isdir(doc_dir):
304
+ raise DistlibException('not a directory: %r' % doc_dir)
305
+ fn = os.path.join(doc_dir, 'index.html')
306
+ if not os.path.exists(fn):
307
+ raise DistlibException('not found: %r' % fn)
308
+ metadata.validate()
309
+ name, version = metadata.name, metadata.version
310
+ zip_data = zip_dir(doc_dir).getvalue()
311
+ fields = [(':action', 'doc_upload'),
312
+ ('name', name), ('version', version)]
313
+ files = [('content', name, zip_data)]
314
+ request = self.encode_request(fields, files)
315
+ return self.send_request(request)
316
+
317
+ def get_verify_command(self, signature_filename, data_filename,
318
+ keystore=None):
319
+ """
320
+ Return a suitable command for verifying a file.
321
+
322
+ :param signature_filename: The pathname to the file containing the
323
+ signature.
324
+ :param data_filename: The pathname to the file containing the
325
+ signed data.
326
+ :param keystore: The path to a directory which contains the keys
327
+ used in verification. If not specified, the
328
+ instance's ``gpg_home`` attribute is used instead.
329
+ :return: The verifying command as a list suitable to be
330
+ passed to :class:`subprocess.Popen`.
331
+ """
332
+ cmd = [self.gpg, '--status-fd', '2', '--no-tty']
333
+ if keystore is None:
334
+ keystore = self.gpg_home
335
+ if keystore:
336
+ cmd.extend(['--homedir', keystore])
337
+ cmd.extend(['--verify', signature_filename, data_filename])
338
+ logger.debug('invoking: %s', ' '.join(cmd))
339
+ return cmd
340
+
341
+ def verify_signature(self, signature_filename, data_filename,
342
+ keystore=None):
343
+ """
344
+ Verify a signature for a file.
345
+
346
+ :param signature_filename: The pathname to the file containing the
347
+ signature.
348
+ :param data_filename: The pathname to the file containing the
349
+ signed data.
350
+ :param keystore: The path to a directory which contains the keys
351
+ used in verification. If not specified, the
352
+ instance's ``gpg_home`` attribute is used instead.
353
+ :return: True if the signature was verified, else False.
354
+ """
355
+ if not self.gpg:
356
+ raise DistlibException('verification unavailable because gpg '
357
+ 'unavailable')
358
+ cmd = self.get_verify_command(signature_filename, data_filename,
359
+ keystore)
360
+ rc, stdout, stderr = self.run_command(cmd)
361
+ if rc not in (0, 1):
362
+ raise DistlibException('verify command failed with error code %s' % rc)
363
+ return rc == 0
364
+
365
+ def download_file(self, url, destfile, digest=None, reporthook=None):
366
+ """
367
+ This is a convenience method for downloading a file from an URL.
368
+ Normally, this will be a file from the index, though currently
369
+ no check is made for this (i.e. a file can be downloaded from
370
+ anywhere).
371
+
372
+ The method is just like the :func:`urlretrieve` function in the
373
+ standard library, except that it allows digest computation to be
374
+ done during download and checking that the downloaded data
375
+ matched any expected value.
376
+
377
+ :param url: The URL of the file to be downloaded (assumed to be
378
+ available via an HTTP GET request).
379
+ :param destfile: The pathname where the downloaded file is to be
380
+ saved.
381
+ :param digest: If specified, this must be a (hasher, value)
382
+ tuple, where hasher is the algorithm used (e.g.
383
+ ``'md5'``) and ``value`` is the expected value.
384
+ :param reporthook: The same as for :func:`urlretrieve` in the
385
+ standard library.
386
+ """
387
+ if digest is None:
388
+ digester = None
389
+ logger.debug('No digest specified')
390
+ else:
391
+ if isinstance(digest, (list, tuple)):
392
+ hasher, digest = digest
393
+ else:
394
+ hasher = 'md5'
395
+ digester = getattr(hashlib, hasher)()
396
+ logger.debug('Digest specified: %s' % digest)
397
+ # The following code is equivalent to urlretrieve.
398
+ # We need to do it this way so that we can compute the
399
+ # digest of the file as we go.
400
+ with open(destfile, 'wb') as dfp:
401
+ # addinfourl is not a context manager on 2.x
402
+ # so we have to use try/finally
403
+ sfp = self.send_request(Request(url))
404
+ try:
405
+ headers = sfp.info()
406
+ blocksize = 8192
407
+ size = -1
408
+ read = 0
409
+ blocknum = 0
410
+ if "content-length" in headers:
411
+ size = int(headers["Content-Length"])
412
+ if reporthook:
413
+ reporthook(blocknum, blocksize, size)
414
+ while True:
415
+ block = sfp.read(blocksize)
416
+ if not block:
417
+ break
418
+ read += len(block)
419
+ dfp.write(block)
420
+ if digester:
421
+ digester.update(block)
422
+ blocknum += 1
423
+ if reporthook:
424
+ reporthook(blocknum, blocksize, size)
425
+ finally:
426
+ sfp.close()
427
+
428
+ # check that we got the whole file, if we can
429
+ if size >= 0 and read < size:
430
+ raise DistlibException(
431
+ 'retrieval incomplete: got only %d out of %d bytes'
432
+ % (read, size))
433
+ # if we have a digest, it must match.
434
+ if digester:
435
+ actual = digester.hexdigest()
436
+ if digest != actual:
437
+ raise DistlibException('%s digest mismatch for %s: expected '
438
+ '%s, got %s' % (hasher, destfile,
439
+ digest, actual))
440
+ logger.debug('Digest verified: %s', digest)
441
+
442
+ def send_request(self, req):
443
+ """
444
+ Send a standard library :class:`Request` to PyPI and return its
445
+ response.
446
+
447
+ :param req: The request to send.
448
+ :return: The HTTP response from PyPI (a standard library HTTPResponse).
449
+ """
450
+ handlers = []
451
+ if self.password_handler:
452
+ handlers.append(self.password_handler)
453
+ if self.ssl_verifier:
454
+ handlers.append(self.ssl_verifier)
455
+ opener = build_opener(*handlers)
456
+ return opener.open(req)
457
+
458
+ def encode_request(self, fields, files):
459
+ """
460
+ Encode fields and files for posting to an HTTP server.
461
+
462
+ :param fields: The fields to send as a list of (fieldname, value)
463
+ tuples.
464
+ :param files: The files to send as a list of (fieldname, filename,
465
+ file_bytes) tuple.
466
+ """
467
+ # Adapted from packaging, which in turn was adapted from
468
+ # http://code.activestate.com/recipes/146306
469
+
470
+ parts = []
471
+ boundary = self.boundary
472
+ for k, values in fields:
473
+ if not isinstance(values, (list, tuple)):
474
+ values = [values]
475
+
476
+ for v in values:
477
+ parts.extend((
478
+ b'--' + boundary,
479
+ ('Content-Disposition: form-data; name="%s"' %
480
+ k).encode('utf-8'),
481
+ b'',
482
+ v.encode('utf-8')))
483
+ for key, filename, value in files:
484
+ parts.extend((
485
+ b'--' + boundary,
486
+ ('Content-Disposition: form-data; name="%s"; filename="%s"' %
487
+ (key, filename)).encode('utf-8'),
488
+ b'',
489
+ value))
490
+
491
+ parts.extend((b'--' + boundary + b'--', b''))
492
+
493
+ body = b'\r\n'.join(parts)
494
+ ct = b'multipart/form-data; boundary=' + boundary
495
+ headers = {
496
+ 'Content-type': ct,
497
+ 'Content-length': str(len(body))
498
+ }
499
+ return Request(self.url, body, headers)
500
+
501
+ def search(self, terms, operator=None): # pragma: no cover
502
+ if isinstance(terms, string_types):
503
+ terms = {'name': terms}
504
+ rpc_proxy = ServerProxy(self.url, timeout=3.0)
505
+ try:
506
+ return rpc_proxy.search(terms, operator or 'and')
507
+ finally:
508
+ rpc_proxy('close')()
.venv/lib/python3.13/site-packages/pip/_vendor/distlib/locators.py ADDED
@@ -0,0 +1,1295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2023 Vinay Sajip.
4
+ # Licensed to the Python Software Foundation under a contributor agreement.
5
+ # See LICENSE.txt and CONTRIBUTORS.txt.
6
+ #
7
+
8
+ import gzip
9
+ from io import BytesIO
10
+ import json
11
+ import logging
12
+ import os
13
+ import posixpath
14
+ import re
15
+ try:
16
+ import threading
17
+ except ImportError: # pragma: no cover
18
+ import dummy_threading as threading
19
+ import zlib
20
+
21
+ from . import DistlibException
22
+ from .compat import (urljoin, urlparse, urlunparse, url2pathname, pathname2url, queue, quote, unescape, build_opener,
23
+ HTTPRedirectHandler as BaseRedirectHandler, text_type, Request, HTTPError, URLError)
24
+ from .database import Distribution, DistributionPath, make_dist
25
+ from .metadata import Metadata, MetadataInvalidError
26
+ from .util import (cached_property, ensure_slash, split_filename, get_project_data, parse_requirement,
27
+ parse_name_and_version, ServerProxy, normalize_name)
28
+ from .version import get_scheme, UnsupportedVersionError
29
+ from .wheel import Wheel, is_compatible
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ HASHER_HASH = re.compile(r'^(\w+)=([a-f0-9]+)')
34
+ CHARSET = re.compile(r';\s*charset\s*=\s*(.*)\s*$', re.I)
35
+ HTML_CONTENT_TYPE = re.compile('text/html|application/x(ht)?ml')
36
+ DEFAULT_INDEX = 'https://pypi.org/pypi'
37
+
38
+
39
+ def get_all_distribution_names(url=None):
40
+ """
41
+ Return all distribution names known by an index.
42
+ :param url: The URL of the index.
43
+ :return: A list of all known distribution names.
44
+ """
45
+ if url is None:
46
+ url = DEFAULT_INDEX
47
+ client = ServerProxy(url, timeout=3.0)
48
+ try:
49
+ return client.list_packages()
50
+ finally:
51
+ client('close')()
52
+
53
+
54
+ class RedirectHandler(BaseRedirectHandler):
55
+ """
56
+ A class to work around a bug in some Python 3.2.x releases.
57
+ """
58
+
59
+ # There's a bug in the base version for some 3.2.x
60
+ # (e.g. 3.2.2 on Ubuntu Oneiric). If a Location header
61
+ # returns e.g. /abc, it bails because it says the scheme ''
62
+ # is bogus, when actually it should use the request's
63
+ # URL for the scheme. See Python issue #13696.
64
+ def http_error_302(self, req, fp, code, msg, headers):
65
+ # Some servers (incorrectly) return multiple Location headers
66
+ # (so probably same goes for URI). Use first header.
67
+ newurl = None
68
+ for key in ('location', 'uri'):
69
+ if key in headers:
70
+ newurl = headers[key]
71
+ break
72
+ if newurl is None: # pragma: no cover
73
+ return
74
+ urlparts = urlparse(newurl)
75
+ if urlparts.scheme == '':
76
+ newurl = urljoin(req.get_full_url(), newurl)
77
+ if hasattr(headers, 'replace_header'):
78
+ headers.replace_header(key, newurl)
79
+ else:
80
+ headers[key] = newurl
81
+ return BaseRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
82
+
83
+ http_error_301 = http_error_303 = http_error_307 = http_error_302
84
+
85
+
86
+ class Locator(object):
87
+ """
88
+ A base class for locators - things that locate distributions.
89
+ """
90
+ source_extensions = ('.tar.gz', '.tar.bz2', '.tar', '.zip', '.tgz', '.tbz')
91
+ binary_extensions = ('.egg', '.exe', '.whl')
92
+ excluded_extensions = ('.pdf', )
93
+
94
+ # A list of tags indicating which wheels you want to match. The default
95
+ # value of None matches against the tags compatible with the running
96
+ # Python. If you want to match other values, set wheel_tags on a locator
97
+ # instance to a list of tuples (pyver, abi, arch) which you want to match.
98
+ wheel_tags = None
99
+
100
+ downloadable_extensions = source_extensions + ('.whl', )
101
+
102
+ def __init__(self, scheme='default'):
103
+ """
104
+ Initialise an instance.
105
+ :param scheme: Because locators look for most recent versions, they
106
+ need to know the version scheme to use. This specifies
107
+ the current PEP-recommended scheme - use ``'legacy'``
108
+ if you need to support existing distributions on PyPI.
109
+ """
110
+ self._cache = {}
111
+ self.scheme = scheme
112
+ # Because of bugs in some of the handlers on some of the platforms,
113
+ # we use our own opener rather than just using urlopen.
114
+ self.opener = build_opener(RedirectHandler())
115
+ # If get_project() is called from locate(), the matcher instance
116
+ # is set from the requirement passed to locate(). See issue #18 for
117
+ # why this can be useful to know.
118
+ self.matcher = None
119
+ self.errors = queue.Queue()
120
+
121
+ def get_errors(self):
122
+ """
123
+ Return any errors which have occurred.
124
+ """
125
+ result = []
126
+ while not self.errors.empty(): # pragma: no cover
127
+ try:
128
+ e = self.errors.get(False)
129
+ result.append(e)
130
+ except self.errors.Empty:
131
+ continue
132
+ self.errors.task_done()
133
+ return result
134
+
135
+ def clear_errors(self):
136
+ """
137
+ Clear any errors which may have been logged.
138
+ """
139
+ # Just get the errors and throw them away
140
+ self.get_errors()
141
+
142
+ def clear_cache(self):
143
+ self._cache.clear()
144
+
145
+ def _get_scheme(self):
146
+ return self._scheme
147
+
148
+ def _set_scheme(self, value):
149
+ self._scheme = value
150
+
151
+ scheme = property(_get_scheme, _set_scheme)
152
+
153
+ def _get_project(self, name):
154
+ """
155
+ For a given project, get a dictionary mapping available versions to Distribution
156
+ instances.
157
+
158
+ This should be implemented in subclasses.
159
+
160
+ If called from a locate() request, self.matcher will be set to a
161
+ matcher for the requirement to satisfy, otherwise it will be None.
162
+ """
163
+ raise NotImplementedError('Please implement in the subclass')
164
+
165
+ def get_distribution_names(self):
166
+ """
167
+ Return all the distribution names known to this locator.
168
+ """
169
+ raise NotImplementedError('Please implement in the subclass')
170
+
171
+ def get_project(self, name):
172
+ """
173
+ For a given project, get a dictionary mapping available versions to Distribution
174
+ instances.
175
+
176
+ This calls _get_project to do all the work, and just implements a caching layer on top.
177
+ """
178
+ if self._cache is None: # pragma: no cover
179
+ result = self._get_project(name)
180
+ elif name in self._cache:
181
+ result = self._cache[name]
182
+ else:
183
+ self.clear_errors()
184
+ result = self._get_project(name)
185
+ self._cache[name] = result
186
+ return result
187
+
188
+ def score_url(self, url):
189
+ """
190
+ Give an url a score which can be used to choose preferred URLs
191
+ for a given project release.
192
+ """
193
+ t = urlparse(url)
194
+ basename = posixpath.basename(t.path)
195
+ compatible = True
196
+ is_wheel = basename.endswith('.whl')
197
+ is_downloadable = basename.endswith(self.downloadable_extensions)
198
+ if is_wheel:
199
+ compatible = is_compatible(Wheel(basename), self.wheel_tags)
200
+ return (t.scheme == 'https', 'pypi.org' in t.netloc, is_downloadable, is_wheel, compatible, basename)
201
+
202
+ def prefer_url(self, url1, url2):
203
+ """
204
+ Choose one of two URLs where both are candidates for distribution
205
+ archives for the same version of a distribution (for example,
206
+ .tar.gz vs. zip).
207
+
208
+ The current implementation favours https:// URLs over http://, archives
209
+ from PyPI over those from other locations, wheel compatibility (if a
210
+ wheel) and then the archive name.
211
+ """
212
+ result = url2
213
+ if url1:
214
+ s1 = self.score_url(url1)
215
+ s2 = self.score_url(url2)
216
+ if s1 > s2:
217
+ result = url1
218
+ if result != url2:
219
+ logger.debug('Not replacing %r with %r', url1, url2)
220
+ else:
221
+ logger.debug('Replacing %r with %r', url1, url2)
222
+ return result
223
+
224
+ def split_filename(self, filename, project_name):
225
+ """
226
+ Attempt to split a filename in project name, version and Python version.
227
+ """
228
+ return split_filename(filename, project_name)
229
+
230
+ def convert_url_to_download_info(self, url, project_name):
231
+ """
232
+ See if a URL is a candidate for a download URL for a project (the URL
233
+ has typically been scraped from an HTML page).
234
+
235
+ If it is, a dictionary is returned with keys "name", "version",
236
+ "filename" and "url"; otherwise, None is returned.
237
+ """
238
+
239
+ def same_project(name1, name2):
240
+ return normalize_name(name1) == normalize_name(name2)
241
+
242
+ result = None
243
+ scheme, netloc, path, params, query, frag = urlparse(url)
244
+ if frag.lower().startswith('egg='): # pragma: no cover
245
+ logger.debug('%s: version hint in fragment: %r', project_name, frag)
246
+ m = HASHER_HASH.match(frag)
247
+ if m:
248
+ algo, digest = m.groups()
249
+ else:
250
+ algo, digest = None, None
251
+ origpath = path
252
+ if path and path[-1] == '/': # pragma: no cover
253
+ path = path[:-1]
254
+ if path.endswith('.whl'):
255
+ try:
256
+ wheel = Wheel(path)
257
+ if not is_compatible(wheel, self.wheel_tags):
258
+ logger.debug('Wheel not compatible: %s', path)
259
+ else:
260
+ if project_name is None:
261
+ include = True
262
+ else:
263
+ include = same_project(wheel.name, project_name)
264
+ if include:
265
+ result = {
266
+ 'name': wheel.name,
267
+ 'version': wheel.version,
268
+ 'filename': wheel.filename,
269
+ 'url': urlunparse((scheme, netloc, origpath, params, query, '')),
270
+ 'python-version': ', '.join(['.'.join(list(v[2:])) for v in wheel.pyver]),
271
+ }
272
+ except Exception: # pragma: no cover
273
+ logger.warning('invalid path for wheel: %s', path)
274
+ elif not path.endswith(self.downloadable_extensions): # pragma: no cover
275
+ logger.debug('Not downloadable: %s', path)
276
+ else: # downloadable extension
277
+ path = filename = posixpath.basename(path)
278
+ for ext in self.downloadable_extensions:
279
+ if path.endswith(ext):
280
+ path = path[:-len(ext)]
281
+ t = self.split_filename(path, project_name)
282
+ if not t: # pragma: no cover
283
+ logger.debug('No match for project/version: %s', path)
284
+ else:
285
+ name, version, pyver = t
286
+ if not project_name or same_project(project_name, name):
287
+ result = {
288
+ 'name': name,
289
+ 'version': version,
290
+ 'filename': filename,
291
+ 'url': urlunparse((scheme, netloc, origpath, params, query, '')),
292
+ }
293
+ if pyver: # pragma: no cover
294
+ result['python-version'] = pyver
295
+ break
296
+ if result and algo:
297
+ result['%s_digest' % algo] = digest
298
+ return result
299
+
300
+ def _get_digest(self, info):
301
+ """
302
+ Get a digest from a dictionary by looking at a "digests" dictionary
303
+ or keys of the form 'algo_digest'.
304
+
305
+ Returns a 2-tuple (algo, digest) if found, else None. Currently
306
+ looks only for SHA256, then MD5.
307
+ """
308
+ result = None
309
+ if 'digests' in info:
310
+ digests = info['digests']
311
+ for algo in ('sha256', 'md5'):
312
+ if algo in digests:
313
+ result = (algo, digests[algo])
314
+ break
315
+ if not result:
316
+ for algo in ('sha256', 'md5'):
317
+ key = '%s_digest' % algo
318
+ if key in info:
319
+ result = (algo, info[key])
320
+ break
321
+ return result
322
+
323
+ def _update_version_data(self, result, info):
324
+ """
325
+ Update a result dictionary (the final result from _get_project) with a
326
+ dictionary for a specific version, which typically holds information
327
+ gleaned from a filename or URL for an archive for the distribution.
328
+ """
329
+ name = info.pop('name')
330
+ version = info.pop('version')
331
+ if version in result:
332
+ dist = result[version]
333
+ md = dist.metadata
334
+ else:
335
+ dist = make_dist(name, version, scheme=self.scheme)
336
+ md = dist.metadata
337
+ dist.digest = digest = self._get_digest(info)
338
+ url = info['url']
339
+ result['digests'][url] = digest
340
+ if md.source_url != info['url']:
341
+ md.source_url = self.prefer_url(md.source_url, url)
342
+ result['urls'].setdefault(version, set()).add(url)
343
+ dist.locator = self
344
+ result[version] = dist
345
+
346
+ def locate(self, requirement, prereleases=False):
347
+ """
348
+ Find the most recent distribution which matches the given
349
+ requirement.
350
+
351
+ :param requirement: A requirement of the form 'foo (1.0)' or perhaps
352
+ 'foo (>= 1.0, < 2.0, != 1.3)'
353
+ :param prereleases: If ``True``, allow pre-release versions
354
+ to be located. Otherwise, pre-release versions
355
+ are not returned.
356
+ :return: A :class:`Distribution` instance, or ``None`` if no such
357
+ distribution could be located.
358
+ """
359
+ result = None
360
+ r = parse_requirement(requirement)
361
+ if r is None: # pragma: no cover
362
+ raise DistlibException('Not a valid requirement: %r' % requirement)
363
+ scheme = get_scheme(self.scheme)
364
+ self.matcher = matcher = scheme.matcher(r.requirement)
365
+ logger.debug('matcher: %s (%s)', matcher, type(matcher).__name__)
366
+ versions = self.get_project(r.name)
367
+ if len(versions) > 2: # urls and digests keys are present
368
+ # sometimes, versions are invalid
369
+ slist = []
370
+ vcls = matcher.version_class
371
+ for k in versions:
372
+ if k in ('urls', 'digests'):
373
+ continue
374
+ try:
375
+ if not matcher.match(k):
376
+ pass # logger.debug('%s did not match %r', matcher, k)
377
+ else:
378
+ if prereleases or not vcls(k).is_prerelease:
379
+ slist.append(k)
380
+ except Exception: # pragma: no cover
381
+ logger.warning('error matching %s with %r', matcher, k)
382
+ pass # slist.append(k)
383
+ if len(slist) > 1:
384
+ slist = sorted(slist, key=scheme.key)
385
+ if slist:
386
+ logger.debug('sorted list: %s', slist)
387
+ version = slist[-1]
388
+ result = versions[version]
389
+ if result:
390
+ if r.extras:
391
+ result.extras = r.extras
392
+ result.download_urls = versions.get('urls', {}).get(version, set())
393
+ d = {}
394
+ sd = versions.get('digests', {})
395
+ for url in result.download_urls:
396
+ if url in sd: # pragma: no cover
397
+ d[url] = sd[url]
398
+ result.digests = d
399
+ self.matcher = None
400
+ return result
401
+
402
+
403
+ class PyPIRPCLocator(Locator):
404
+ """
405
+ This locator uses XML-RPC to locate distributions. It therefore
406
+ cannot be used with simple mirrors (that only mirror file content).
407
+ """
408
+
409
+ def __init__(self, url, **kwargs):
410
+ """
411
+ Initialise an instance.
412
+
413
+ :param url: The URL to use for XML-RPC.
414
+ :param kwargs: Passed to the superclass constructor.
415
+ """
416
+ super(PyPIRPCLocator, self).__init__(**kwargs)
417
+ self.base_url = url
418
+ self.client = ServerProxy(url, timeout=3.0)
419
+
420
+ def get_distribution_names(self):
421
+ """
422
+ Return all the distribution names known to this locator.
423
+ """
424
+ return set(self.client.list_packages())
425
+
426
+ def _get_project(self, name):
427
+ result = {'urls': {}, 'digests': {}}
428
+ versions = self.client.package_releases(name, True)
429
+ for v in versions:
430
+ urls = self.client.release_urls(name, v)
431
+ data = self.client.release_data(name, v)
432
+ metadata = Metadata(scheme=self.scheme)
433
+ metadata.name = data['name']
434
+ metadata.version = data['version']
435
+ metadata.license = data.get('license')
436
+ metadata.keywords = data.get('keywords', [])
437
+ metadata.summary = data.get('summary')
438
+ dist = Distribution(metadata)
439
+ if urls:
440
+ info = urls[0]
441
+ metadata.source_url = info['url']
442
+ dist.digest = self._get_digest(info)
443
+ dist.locator = self
444
+ result[v] = dist
445
+ for info in urls:
446
+ url = info['url']
447
+ digest = self._get_digest(info)
448
+ result['urls'].setdefault(v, set()).add(url)
449
+ result['digests'][url] = digest
450
+ return result
451
+
452
+
453
+ class PyPIJSONLocator(Locator):
454
+ """
455
+ This locator uses PyPI's JSON interface. It's very limited in functionality
456
+ and probably not worth using.
457
+ """
458
+
459
+ def __init__(self, url, **kwargs):
460
+ super(PyPIJSONLocator, self).__init__(**kwargs)
461
+ self.base_url = ensure_slash(url)
462
+
463
+ def get_distribution_names(self):
464
+ """
465
+ Return all the distribution names known to this locator.
466
+ """
467
+ raise NotImplementedError('Not available from this locator')
468
+
469
+ def _get_project(self, name):
470
+ result = {'urls': {}, 'digests': {}}
471
+ url = urljoin(self.base_url, '%s/json' % quote(name))
472
+ try:
473
+ resp = self.opener.open(url)
474
+ data = resp.read().decode() # for now
475
+ d = json.loads(data)
476
+ md = Metadata(scheme=self.scheme)
477
+ data = d['info']
478
+ md.name = data['name']
479
+ md.version = data['version']
480
+ md.license = data.get('license')
481
+ md.keywords = data.get('keywords', [])
482
+ md.summary = data.get('summary')
483
+ dist = Distribution(md)
484
+ dist.locator = self
485
+ # urls = d['urls']
486
+ result[md.version] = dist
487
+ for info in d['urls']:
488
+ url = info['url']
489
+ dist.download_urls.add(url)
490
+ dist.digests[url] = self._get_digest(info)
491
+ result['urls'].setdefault(md.version, set()).add(url)
492
+ result['digests'][url] = self._get_digest(info)
493
+ # Now get other releases
494
+ for version, infos in d['releases'].items():
495
+ if version == md.version:
496
+ continue # already done
497
+ omd = Metadata(scheme=self.scheme)
498
+ omd.name = md.name
499
+ omd.version = version
500
+ odist = Distribution(omd)
501
+ odist.locator = self
502
+ result[version] = odist
503
+ for info in infos:
504
+ url = info['url']
505
+ odist.download_urls.add(url)
506
+ odist.digests[url] = self._get_digest(info)
507
+ result['urls'].setdefault(version, set()).add(url)
508
+ result['digests'][url] = self._get_digest(info)
509
+
510
+
511
+ # for info in urls:
512
+ # md.source_url = info['url']
513
+ # dist.digest = self._get_digest(info)
514
+ # dist.locator = self
515
+ # for info in urls:
516
+ # url = info['url']
517
+ # result['urls'].setdefault(md.version, set()).add(url)
518
+ # result['digests'][url] = self._get_digest(info)
519
+ except Exception as e:
520
+ self.errors.put(text_type(e))
521
+ logger.exception('JSON fetch failed: %s', e)
522
+ return result
523
+
524
+
525
+ class Page(object):
526
+ """
527
+ This class represents a scraped HTML page.
528
+ """
529
+ # The following slightly hairy-looking regex just looks for the contents of
530
+ # an anchor link, which has an attribute "href" either immediately preceded
531
+ # or immediately followed by a "rel" attribute. The attribute values can be
532
+ # declared with double quotes, single quotes or no quotes - which leads to
533
+ # the length of the expression.
534
+ _href = re.compile(
535
+ """
536
+ (rel\\s*=\\s*(?:"(?P<rel1>[^"]*)"|'(?P<rel2>[^']*)'|(?P<rel3>[^>\\s\n]*))\\s+)?
537
+ href\\s*=\\s*(?:"(?P<url1>[^"]*)"|'(?P<url2>[^']*)'|(?P<url3>[^>\\s\n]*))
538
+ (\\s+rel\\s*=\\s*(?:"(?P<rel4>[^"]*)"|'(?P<rel5>[^']*)'|(?P<rel6>[^>\\s\n]*)))?
539
+ """, re.I | re.S | re.X)
540
+ _base = re.compile(r"""<base\s+href\s*=\s*['"]?([^'">]+)""", re.I | re.S)
541
+
542
+ def __init__(self, data, url):
543
+ """
544
+ Initialise an instance with the Unicode page contents and the URL they
545
+ came from.
546
+ """
547
+ self.data = data
548
+ self.base_url = self.url = url
549
+ m = self._base.search(self.data)
550
+ if m:
551
+ self.base_url = m.group(1)
552
+
553
+ _clean_re = re.compile(r'[^a-z0-9$&+,/:;=?@.#%_\\|-]', re.I)
554
+
555
+ @cached_property
556
+ def links(self):
557
+ """
558
+ Return the URLs of all the links on a page together with information
559
+ about their "rel" attribute, for determining which ones to treat as
560
+ downloads and which ones to queue for further scraping.
561
+ """
562
+
563
+ def clean(url):
564
+ "Tidy up an URL."
565
+ scheme, netloc, path, params, query, frag = urlparse(url)
566
+ return urlunparse((scheme, netloc, quote(path), params, query, frag))
567
+
568
+ result = set()
569
+ for match in self._href.finditer(self.data):
570
+ d = match.groupdict('')
571
+ rel = (d['rel1'] or d['rel2'] or d['rel3'] or d['rel4'] or d['rel5'] or d['rel6'])
572
+ url = d['url1'] or d['url2'] or d['url3']
573
+ url = urljoin(self.base_url, url)
574
+ url = unescape(url)
575
+ url = self._clean_re.sub(lambda m: '%%%2x' % ord(m.group(0)), url)
576
+ result.add((url, rel))
577
+ # We sort the result, hoping to bring the most recent versions
578
+ # to the front
579
+ result = sorted(result, key=lambda t: t[0], reverse=True)
580
+ return result
581
+
582
+
583
+ class SimpleScrapingLocator(Locator):
584
+ """
585
+ A locator which scrapes HTML pages to locate downloads for a distribution.
586
+ This runs multiple threads to do the I/O; performance is at least as good
587
+ as pip's PackageFinder, which works in an analogous fashion.
588
+ """
589
+
590
+ # These are used to deal with various Content-Encoding schemes.
591
+ decoders = {
592
+ 'deflate': zlib.decompress,
593
+ 'gzip': lambda b: gzip.GzipFile(fileobj=BytesIO(b)).read(),
594
+ 'none': lambda b: b,
595
+ }
596
+
597
+ def __init__(self, url, timeout=None, num_workers=10, **kwargs):
598
+ """
599
+ Initialise an instance.
600
+ :param url: The root URL to use for scraping.
601
+ :param timeout: The timeout, in seconds, to be applied to requests.
602
+ This defaults to ``None`` (no timeout specified).
603
+ :param num_workers: The number of worker threads you want to do I/O,
604
+ This defaults to 10.
605
+ :param kwargs: Passed to the superclass.
606
+ """
607
+ super(SimpleScrapingLocator, self).__init__(**kwargs)
608
+ self.base_url = ensure_slash(url)
609
+ self.timeout = timeout
610
+ self._page_cache = {}
611
+ self._seen = set()
612
+ self._to_fetch = queue.Queue()
613
+ self._bad_hosts = set()
614
+ self.skip_externals = False
615
+ self.num_workers = num_workers
616
+ self._lock = threading.RLock()
617
+ # See issue #45: we need to be resilient when the locator is used
618
+ # in a thread, e.g. with concurrent.futures. We can't use self._lock
619
+ # as it is for coordinating our internal threads - the ones created
620
+ # in _prepare_threads.
621
+ self._gplock = threading.RLock()
622
+ self.platform_check = False # See issue #112
623
+
624
+ def _prepare_threads(self):
625
+ """
626
+ Threads are created only when get_project is called, and terminate
627
+ before it returns. They are there primarily to parallelise I/O (i.e.
628
+ fetching web pages).
629
+ """
630
+ self._threads = []
631
+ for i in range(self.num_workers):
632
+ t = threading.Thread(target=self._fetch)
633
+ t.daemon = True
634
+ t.start()
635
+ self._threads.append(t)
636
+
637
+ def _wait_threads(self):
638
+ """
639
+ Tell all the threads to terminate (by sending a sentinel value) and
640
+ wait for them to do so.
641
+ """
642
+ # Note that you need two loops, since you can't say which
643
+ # thread will get each sentinel
644
+ for t in self._threads:
645
+ self._to_fetch.put(None) # sentinel
646
+ for t in self._threads:
647
+ t.join()
648
+ self._threads = []
649
+
650
+ def _get_project(self, name):
651
+ result = {'urls': {}, 'digests': {}}
652
+ with self._gplock:
653
+ self.result = result
654
+ self.project_name = name
655
+ url = urljoin(self.base_url, '%s/' % quote(name))
656
+ self._seen.clear()
657
+ self._page_cache.clear()
658
+ self._prepare_threads()
659
+ try:
660
+ logger.debug('Queueing %s', url)
661
+ self._to_fetch.put(url)
662
+ self._to_fetch.join()
663
+ finally:
664
+ self._wait_threads()
665
+ del self.result
666
+ return result
667
+
668
+ platform_dependent = re.compile(r'\b(linux_(i\d86|x86_64|arm\w+)|'
669
+ r'win(32|_amd64)|macosx_?\d+)\b', re.I)
670
+
671
+ def _is_platform_dependent(self, url):
672
+ """
673
+ Does an URL refer to a platform-specific download?
674
+ """
675
+ return self.platform_dependent.search(url)
676
+
677
+ def _process_download(self, url):
678
+ """
679
+ See if an URL is a suitable download for a project.
680
+
681
+ If it is, register information in the result dictionary (for
682
+ _get_project) about the specific version it's for.
683
+
684
+ Note that the return value isn't actually used other than as a boolean
685
+ value.
686
+ """
687
+ if self.platform_check and self._is_platform_dependent(url):
688
+ info = None
689
+ else:
690
+ info = self.convert_url_to_download_info(url, self.project_name)
691
+ logger.debug('process_download: %s -> %s', url, info)
692
+ if info:
693
+ with self._lock: # needed because self.result is shared
694
+ self._update_version_data(self.result, info)
695
+ return info
696
+
697
+ def _should_queue(self, link, referrer, rel):
698
+ """
699
+ Determine whether a link URL from a referring page and with a
700
+ particular "rel" attribute should be queued for scraping.
701
+ """
702
+ scheme, netloc, path, _, _, _ = urlparse(link)
703
+ if path.endswith(self.source_extensions + self.binary_extensions + self.excluded_extensions):
704
+ result = False
705
+ elif self.skip_externals and not link.startswith(self.base_url):
706
+ result = False
707
+ elif not referrer.startswith(self.base_url):
708
+ result = False
709
+ elif rel not in ('homepage', 'download'):
710
+ result = False
711
+ elif scheme not in ('http', 'https', 'ftp'):
712
+ result = False
713
+ elif self._is_platform_dependent(link):
714
+ result = False
715
+ else:
716
+ host = netloc.split(':', 1)[0]
717
+ if host.lower() == 'localhost':
718
+ result = False
719
+ else:
720
+ result = True
721
+ logger.debug('should_queue: %s (%s) from %s -> %s', link, rel, referrer, result)
722
+ return result
723
+
724
+ def _fetch(self):
725
+ """
726
+ Get a URL to fetch from the work queue, get the HTML page, examine its
727
+ links for download candidates and candidates for further scraping.
728
+
729
+ This is a handy method to run in a thread.
730
+ """
731
+ while True:
732
+ url = self._to_fetch.get()
733
+ try:
734
+ if url:
735
+ page = self.get_page(url)
736
+ if page is None: # e.g. after an error
737
+ continue
738
+ for link, rel in page.links:
739
+ if link not in self._seen:
740
+ try:
741
+ self._seen.add(link)
742
+ if (not self._process_download(link) and self._should_queue(link, url, rel)):
743
+ logger.debug('Queueing %s from %s', link, url)
744
+ self._to_fetch.put(link)
745
+ except MetadataInvalidError: # e.g. invalid versions
746
+ pass
747
+ except Exception as e: # pragma: no cover
748
+ self.errors.put(text_type(e))
749
+ finally:
750
+ # always do this, to avoid hangs :-)
751
+ self._to_fetch.task_done()
752
+ if not url:
753
+ # logger.debug('Sentinel seen, quitting.')
754
+ break
755
+
756
+ def get_page(self, url):
757
+ """
758
+ Get the HTML for an URL, possibly from an in-memory cache.
759
+
760
+ XXX TODO Note: this cache is never actually cleared. It's assumed that
761
+ the data won't get stale over the lifetime of a locator instance (not
762
+ necessarily true for the default_locator).
763
+ """
764
+ # http://peak.telecommunity.com/DevCenter/EasyInstall#package-index-api
765
+ scheme, netloc, path, _, _, _ = urlparse(url)
766
+ if scheme == 'file' and os.path.isdir(url2pathname(path)):
767
+ url = urljoin(ensure_slash(url), 'index.html')
768
+
769
+ if url in self._page_cache:
770
+ result = self._page_cache[url]
771
+ logger.debug('Returning %s from cache: %s', url, result)
772
+ else:
773
+ host = netloc.split(':', 1)[0]
774
+ result = None
775
+ if host in self._bad_hosts:
776
+ logger.debug('Skipping %s due to bad host %s', url, host)
777
+ else:
778
+ req = Request(url, headers={'Accept-encoding': 'identity'})
779
+ try:
780
+ logger.debug('Fetching %s', url)
781
+ resp = self.opener.open(req, timeout=self.timeout)
782
+ logger.debug('Fetched %s', url)
783
+ headers = resp.info()
784
+ content_type = headers.get('Content-Type', '')
785
+ if HTML_CONTENT_TYPE.match(content_type):
786
+ final_url = resp.geturl()
787
+ data = resp.read()
788
+ encoding = headers.get('Content-Encoding')
789
+ if encoding:
790
+ decoder = self.decoders[encoding] # fail if not found
791
+ data = decoder(data)
792
+ encoding = 'utf-8'
793
+ m = CHARSET.search(content_type)
794
+ if m:
795
+ encoding = m.group(1)
796
+ try:
797
+ data = data.decode(encoding)
798
+ except UnicodeError: # pragma: no cover
799
+ data = data.decode('latin-1') # fallback
800
+ result = Page(data, final_url)
801
+ self._page_cache[final_url] = result
802
+ except HTTPError as e:
803
+ if e.code != 404:
804
+ logger.exception('Fetch failed: %s: %s', url, e)
805
+ except URLError as e: # pragma: no cover
806
+ logger.exception('Fetch failed: %s: %s', url, e)
807
+ with self._lock:
808
+ self._bad_hosts.add(host)
809
+ except Exception as e: # pragma: no cover
810
+ logger.exception('Fetch failed: %s: %s', url, e)
811
+ finally:
812
+ self._page_cache[url] = result # even if None (failure)
813
+ return result
814
+
815
+ _distname_re = re.compile('<a href=[^>]*>([^<]+)<')
816
+
817
+ def get_distribution_names(self):
818
+ """
819
+ Return all the distribution names known to this locator.
820
+ """
821
+ result = set()
822
+ page = self.get_page(self.base_url)
823
+ if not page:
824
+ raise DistlibException('Unable to get %s' % self.base_url)
825
+ for match in self._distname_re.finditer(page.data):
826
+ result.add(match.group(1))
827
+ return result
828
+
829
+
830
+ class DirectoryLocator(Locator):
831
+ """
832
+ This class locates distributions in a directory tree.
833
+ """
834
+
835
+ def __init__(self, path, **kwargs):
836
+ """
837
+ Initialise an instance.
838
+ :param path: The root of the directory tree to search.
839
+ :param kwargs: Passed to the superclass constructor,
840
+ except for:
841
+ * recursive - if True (the default), subdirectories are
842
+ recursed into. If False, only the top-level directory
843
+ is searched,
844
+ """
845
+ self.recursive = kwargs.pop('recursive', True)
846
+ super(DirectoryLocator, self).__init__(**kwargs)
847
+ path = os.path.abspath(path)
848
+ if not os.path.isdir(path): # pragma: no cover
849
+ raise DistlibException('Not a directory: %r' % path)
850
+ self.base_dir = path
851
+
852
+ def should_include(self, filename, parent):
853
+ """
854
+ Should a filename be considered as a candidate for a distribution
855
+ archive? As well as the filename, the directory which contains it
856
+ is provided, though not used by the current implementation.
857
+ """
858
+ return filename.endswith(self.downloadable_extensions)
859
+
860
+ def _get_project(self, name):
861
+ result = {'urls': {}, 'digests': {}}
862
+ for root, dirs, files in os.walk(self.base_dir):
863
+ for fn in files:
864
+ if self.should_include(fn, root):
865
+ fn = os.path.join(root, fn)
866
+ url = urlunparse(('file', '', pathname2url(os.path.abspath(fn)), '', '', ''))
867
+ info = self.convert_url_to_download_info(url, name)
868
+ if info:
869
+ self._update_version_data(result, info)
870
+ if not self.recursive:
871
+ break
872
+ return result
873
+
874
+ def get_distribution_names(self):
875
+ """
876
+ Return all the distribution names known to this locator.
877
+ """
878
+ result = set()
879
+ for root, dirs, files in os.walk(self.base_dir):
880
+ for fn in files:
881
+ if self.should_include(fn, root):
882
+ fn = os.path.join(root, fn)
883
+ url = urlunparse(('file', '', pathname2url(os.path.abspath(fn)), '', '', ''))
884
+ info = self.convert_url_to_download_info(url, None)
885
+ if info:
886
+ result.add(info['name'])
887
+ if not self.recursive:
888
+ break
889
+ return result
890
+
891
+
892
+ class JSONLocator(Locator):
893
+ """
894
+ This locator uses special extended metadata (not available on PyPI) and is
895
+ the basis of performant dependency resolution in distlib. Other locators
896
+ require archive downloads before dependencies can be determined! As you
897
+ might imagine, that can be slow.
898
+ """
899
+
900
+ def get_distribution_names(self):
901
+ """
902
+ Return all the distribution names known to this locator.
903
+ """
904
+ raise NotImplementedError('Not available from this locator')
905
+
906
+ def _get_project(self, name):
907
+ result = {'urls': {}, 'digests': {}}
908
+ data = get_project_data(name)
909
+ if data:
910
+ for info in data.get('files', []):
911
+ if info['ptype'] != 'sdist' or info['pyversion'] != 'source':
912
+ continue
913
+ # We don't store summary in project metadata as it makes
914
+ # the data bigger for no benefit during dependency
915
+ # resolution
916
+ dist = make_dist(data['name'],
917
+ info['version'],
918
+ summary=data.get('summary', 'Placeholder for summary'),
919
+ scheme=self.scheme)
920
+ md = dist.metadata
921
+ md.source_url = info['url']
922
+ # TODO SHA256 digest
923
+ if 'digest' in info and info['digest']:
924
+ dist.digest = ('md5', info['digest'])
925
+ md.dependencies = info.get('requirements', {})
926
+ dist.exports = info.get('exports', {})
927
+ result[dist.version] = dist
928
+ result['urls'].setdefault(dist.version, set()).add(info['url'])
929
+ return result
930
+
931
+
932
+ class DistPathLocator(Locator):
933
+ """
934
+ This locator finds installed distributions in a path. It can be useful for
935
+ adding to an :class:`AggregatingLocator`.
936
+ """
937
+
938
+ def __init__(self, distpath, **kwargs):
939
+ """
940
+ Initialise an instance.
941
+
942
+ :param distpath: A :class:`DistributionPath` instance to search.
943
+ """
944
+ super(DistPathLocator, self).__init__(**kwargs)
945
+ assert isinstance(distpath, DistributionPath)
946
+ self.distpath = distpath
947
+
948
+ def _get_project(self, name):
949
+ dist = self.distpath.get_distribution(name)
950
+ if dist is None:
951
+ result = {'urls': {}, 'digests': {}}
952
+ else:
953
+ result = {
954
+ dist.version: dist,
955
+ 'urls': {
956
+ dist.version: set([dist.source_url])
957
+ },
958
+ 'digests': {
959
+ dist.version: set([None])
960
+ }
961
+ }
962
+ return result
963
+
964
+
965
+ class AggregatingLocator(Locator):
966
+ """
967
+ This class allows you to chain and/or merge a list of locators.
968
+ """
969
+
970
+ def __init__(self, *locators, **kwargs):
971
+ """
972
+ Initialise an instance.
973
+
974
+ :param locators: The list of locators to search.
975
+ :param kwargs: Passed to the superclass constructor,
976
+ except for:
977
+ * merge - if False (the default), the first successful
978
+ search from any of the locators is returned. If True,
979
+ the results from all locators are merged (this can be
980
+ slow).
981
+ """
982
+ self.merge = kwargs.pop('merge', False)
983
+ self.locators = locators
984
+ super(AggregatingLocator, self).__init__(**kwargs)
985
+
986
+ def clear_cache(self):
987
+ super(AggregatingLocator, self).clear_cache()
988
+ for locator in self.locators:
989
+ locator.clear_cache()
990
+
991
+ def _set_scheme(self, value):
992
+ self._scheme = value
993
+ for locator in self.locators:
994
+ locator.scheme = value
995
+
996
+ scheme = property(Locator.scheme.fget, _set_scheme)
997
+
998
+ def _get_project(self, name):
999
+ result = {}
1000
+ for locator in self.locators:
1001
+ d = locator.get_project(name)
1002
+ if d:
1003
+ if self.merge:
1004
+ files = result.get('urls', {})
1005
+ digests = result.get('digests', {})
1006
+ # next line could overwrite result['urls'], result['digests']
1007
+ result.update(d)
1008
+ df = result.get('urls')
1009
+ if files and df:
1010
+ for k, v in files.items():
1011
+ if k in df:
1012
+ df[k] |= v
1013
+ else:
1014
+ df[k] = v
1015
+ dd = result.get('digests')
1016
+ if digests and dd:
1017
+ dd.update(digests)
1018
+ else:
1019
+ # See issue #18. If any dists are found and we're looking
1020
+ # for specific constraints, we only return something if
1021
+ # a match is found. For example, if a DirectoryLocator
1022
+ # returns just foo (1.0) while we're looking for
1023
+ # foo (>= 2.0), we'll pretend there was nothing there so
1024
+ # that subsequent locators can be queried. Otherwise we
1025
+ # would just return foo (1.0) which would then lead to a
1026
+ # failure to find foo (>= 2.0), because other locators
1027
+ # weren't searched. Note that this only matters when
1028
+ # merge=False.
1029
+ if self.matcher is None:
1030
+ found = True
1031
+ else:
1032
+ found = False
1033
+ for k in d:
1034
+ if self.matcher.match(k):
1035
+ found = True
1036
+ break
1037
+ if found:
1038
+ result = d
1039
+ break
1040
+ return result
1041
+
1042
+ def get_distribution_names(self):
1043
+ """
1044
+ Return all the distribution names known to this locator.
1045
+ """
1046
+ result = set()
1047
+ for locator in self.locators:
1048
+ try:
1049
+ result |= locator.get_distribution_names()
1050
+ except NotImplementedError:
1051
+ pass
1052
+ return result
1053
+
1054
+
1055
+ # We use a legacy scheme simply because most of the dists on PyPI use legacy
1056
+ # versions which don't conform to PEP 440.
1057
+ default_locator = AggregatingLocator(
1058
+ # JSONLocator(), # don't use as PEP 426 is withdrawn
1059
+ SimpleScrapingLocator('https://pypi.org/simple/', timeout=3.0),
1060
+ scheme='legacy')
1061
+
1062
+ locate = default_locator.locate
1063
+
1064
+
1065
+ class DependencyFinder(object):
1066
+ """
1067
+ Locate dependencies for distributions.
1068
+ """
1069
+
1070
+ def __init__(self, locator=None):
1071
+ """
1072
+ Initialise an instance, using the specified locator
1073
+ to locate distributions.
1074
+ """
1075
+ self.locator = locator or default_locator
1076
+ self.scheme = get_scheme(self.locator.scheme)
1077
+
1078
+ def add_distribution(self, dist):
1079
+ """
1080
+ Add a distribution to the finder. This will update internal information
1081
+ about who provides what.
1082
+ :param dist: The distribution to add.
1083
+ """
1084
+ logger.debug('adding distribution %s', dist)
1085
+ name = dist.key
1086
+ self.dists_by_name[name] = dist
1087
+ self.dists[(name, dist.version)] = dist
1088
+ for p in dist.provides:
1089
+ name, version = parse_name_and_version(p)
1090
+ logger.debug('Add to provided: %s, %s, %s', name, version, dist)
1091
+ self.provided.setdefault(name, set()).add((version, dist))
1092
+
1093
+ def remove_distribution(self, dist):
1094
+ """
1095
+ Remove a distribution from the finder. This will update internal
1096
+ information about who provides what.
1097
+ :param dist: The distribution to remove.
1098
+ """
1099
+ logger.debug('removing distribution %s', dist)
1100
+ name = dist.key
1101
+ del self.dists_by_name[name]
1102
+ del self.dists[(name, dist.version)]
1103
+ for p in dist.provides:
1104
+ name, version = parse_name_and_version(p)
1105
+ logger.debug('Remove from provided: %s, %s, %s', name, version, dist)
1106
+ s = self.provided[name]
1107
+ s.remove((version, dist))
1108
+ if not s:
1109
+ del self.provided[name]
1110
+
1111
+ def get_matcher(self, reqt):
1112
+ """
1113
+ Get a version matcher for a requirement.
1114
+ :param reqt: The requirement
1115
+ :type reqt: str
1116
+ :return: A version matcher (an instance of
1117
+ :class:`distlib.version.Matcher`).
1118
+ """
1119
+ try:
1120
+ matcher = self.scheme.matcher(reqt)
1121
+ except UnsupportedVersionError: # pragma: no cover
1122
+ # XXX compat-mode if cannot read the version
1123
+ name = reqt.split()[0]
1124
+ matcher = self.scheme.matcher(name)
1125
+ return matcher
1126
+
1127
+ def find_providers(self, reqt):
1128
+ """
1129
+ Find the distributions which can fulfill a requirement.
1130
+
1131
+ :param reqt: The requirement.
1132
+ :type reqt: str
1133
+ :return: A set of distribution which can fulfill the requirement.
1134
+ """
1135
+ matcher = self.get_matcher(reqt)
1136
+ name = matcher.key # case-insensitive
1137
+ result = set()
1138
+ provided = self.provided
1139
+ if name in provided:
1140
+ for version, provider in provided[name]:
1141
+ try:
1142
+ match = matcher.match(version)
1143
+ except UnsupportedVersionError:
1144
+ match = False
1145
+
1146
+ if match:
1147
+ result.add(provider)
1148
+ break
1149
+ return result
1150
+
1151
+ def try_to_replace(self, provider, other, problems):
1152
+ """
1153
+ Attempt to replace one provider with another. This is typically used
1154
+ when resolving dependencies from multiple sources, e.g. A requires
1155
+ (B >= 1.0) while C requires (B >= 1.1).
1156
+
1157
+ For successful replacement, ``provider`` must meet all the requirements
1158
+ which ``other`` fulfills.
1159
+
1160
+ :param provider: The provider we are trying to replace with.
1161
+ :param other: The provider we're trying to replace.
1162
+ :param problems: If False is returned, this will contain what
1163
+ problems prevented replacement. This is currently
1164
+ a tuple of the literal string 'cantreplace',
1165
+ ``provider``, ``other`` and the set of requirements
1166
+ that ``provider`` couldn't fulfill.
1167
+ :return: True if we can replace ``other`` with ``provider``, else
1168
+ False.
1169
+ """
1170
+ rlist = self.reqts[other]
1171
+ unmatched = set()
1172
+ for s in rlist:
1173
+ matcher = self.get_matcher(s)
1174
+ if not matcher.match(provider.version):
1175
+ unmatched.add(s)
1176
+ if unmatched:
1177
+ # can't replace other with provider
1178
+ problems.add(('cantreplace', provider, other, frozenset(unmatched)))
1179
+ result = False
1180
+ else:
1181
+ # can replace other with provider
1182
+ self.remove_distribution(other)
1183
+ del self.reqts[other]
1184
+ for s in rlist:
1185
+ self.reqts.setdefault(provider, set()).add(s)
1186
+ self.add_distribution(provider)
1187
+ result = True
1188
+ return result
1189
+
1190
+ def find(self, requirement, meta_extras=None, prereleases=False):
1191
+ """
1192
+ Find a distribution and all distributions it depends on.
1193
+
1194
+ :param requirement: The requirement specifying the distribution to
1195
+ find, or a Distribution instance.
1196
+ :param meta_extras: A list of meta extras such as :test:, :build: and
1197
+ so on.
1198
+ :param prereleases: If ``True``, allow pre-release versions to be
1199
+ returned - otherwise, don't return prereleases
1200
+ unless they're all that's available.
1201
+
1202
+ Return a set of :class:`Distribution` instances and a set of
1203
+ problems.
1204
+
1205
+ The distributions returned should be such that they have the
1206
+ :attr:`required` attribute set to ``True`` if they were
1207
+ from the ``requirement`` passed to ``find()``, and they have the
1208
+ :attr:`build_time_dependency` attribute set to ``True`` unless they
1209
+ are post-installation dependencies of the ``requirement``.
1210
+
1211
+ The problems should be a tuple consisting of the string
1212
+ ``'unsatisfied'`` and the requirement which couldn't be satisfied
1213
+ by any distribution known to the locator.
1214
+ """
1215
+
1216
+ self.provided = {}
1217
+ self.dists = {}
1218
+ self.dists_by_name = {}
1219
+ self.reqts = {}
1220
+
1221
+ meta_extras = set(meta_extras or [])
1222
+ if ':*:' in meta_extras:
1223
+ meta_extras.remove(':*:')
1224
+ # :meta: and :run: are implicitly included
1225
+ meta_extras |= set([':test:', ':build:', ':dev:'])
1226
+
1227
+ if isinstance(requirement, Distribution):
1228
+ dist = odist = requirement
1229
+ logger.debug('passed %s as requirement', odist)
1230
+ else:
1231
+ dist = odist = self.locator.locate(requirement, prereleases=prereleases)
1232
+ if dist is None:
1233
+ raise DistlibException('Unable to locate %r' % requirement)
1234
+ logger.debug('located %s', odist)
1235
+ dist.requested = True
1236
+ problems = set()
1237
+ todo = set([dist])
1238
+ install_dists = set([odist])
1239
+ while todo:
1240
+ dist = todo.pop()
1241
+ name = dist.key # case-insensitive
1242
+ if name not in self.dists_by_name:
1243
+ self.add_distribution(dist)
1244
+ else:
1245
+ # import pdb; pdb.set_trace()
1246
+ other = self.dists_by_name[name]
1247
+ if other != dist:
1248
+ self.try_to_replace(dist, other, problems)
1249
+
1250
+ ireqts = dist.run_requires | dist.meta_requires
1251
+ sreqts = dist.build_requires
1252
+ ereqts = set()
1253
+ if meta_extras and dist in install_dists:
1254
+ for key in ('test', 'build', 'dev'):
1255
+ e = ':%s:' % key
1256
+ if e in meta_extras:
1257
+ ereqts |= getattr(dist, '%s_requires' % key)
1258
+ all_reqts = ireqts | sreqts | ereqts
1259
+ for r in all_reqts:
1260
+ providers = self.find_providers(r)
1261
+ if not providers:
1262
+ logger.debug('No providers found for %r', r)
1263
+ provider = self.locator.locate(r, prereleases=prereleases)
1264
+ # If no provider is found and we didn't consider
1265
+ # prereleases, consider them now.
1266
+ if provider is None and not prereleases:
1267
+ provider = self.locator.locate(r, prereleases=True)
1268
+ if provider is None:
1269
+ logger.debug('Cannot satisfy %r', r)
1270
+ problems.add(('unsatisfied', r))
1271
+ else:
1272
+ n, v = provider.key, provider.version
1273
+ if (n, v) not in self.dists:
1274
+ todo.add(provider)
1275
+ providers.add(provider)
1276
+ if r in ireqts and dist in install_dists:
1277
+ install_dists.add(provider)
1278
+ logger.debug('Adding %s to install_dists', provider.name_and_version)
1279
+ for p in providers:
1280
+ name = p.key
1281
+ if name not in self.dists_by_name:
1282
+ self.reqts.setdefault(p, set()).add(r)
1283
+ else:
1284
+ other = self.dists_by_name[name]
1285
+ if other != p:
1286
+ # see if other can be replaced by p
1287
+ self.try_to_replace(p, other, problems)
1288
+
1289
+ dists = set(self.dists.values())
1290
+ for dist in dists:
1291
+ dist.build_time_dependency = dist not in install_dists
1292
+ if dist.build_time_dependency:
1293
+ logger.debug('%s is a build-time dependency only.', dist.name_and_version)
1294
+ logger.debug('find done for %s', odist)
1295
+ return dists, problems
.venv/lib/python3.13/site-packages/pip/_vendor/distlib/manifest.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2023 Python Software Foundation.
4
+ # See LICENSE.txt and CONTRIBUTORS.txt.
5
+ #
6
+ """
7
+ Class representing the list of files in a distribution.
8
+
9
+ Equivalent to distutils.filelist, but fixes some problems.
10
+ """
11
+ import fnmatch
12
+ import logging
13
+ import os
14
+ import re
15
+ import sys
16
+
17
+ from . import DistlibException
18
+ from .compat import fsdecode
19
+ from .util import convert_path
20
+
21
+
22
+ __all__ = ['Manifest']
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # a \ followed by some spaces + EOL
27
+ _COLLAPSE_PATTERN = re.compile('\\\\w*\n', re.M)
28
+ _COMMENTED_LINE = re.compile('#.*?(?=\n)|\n(?=$)', re.M | re.S)
29
+
30
+ #
31
+ # Due to the different results returned by fnmatch.translate, we need
32
+ # to do slightly different processing for Python 2.7 and 3.2 ... this needed
33
+ # to be brought in for Python 3.6 onwards.
34
+ #
35
+ _PYTHON_VERSION = sys.version_info[:2]
36
+
37
+
38
+ class Manifest(object):
39
+ """
40
+ A list of files built by exploring the filesystem and filtered by applying various
41
+ patterns to what we find there.
42
+ """
43
+
44
+ def __init__(self, base=None):
45
+ """
46
+ Initialise an instance.
47
+
48
+ :param base: The base directory to explore under.
49
+ """
50
+ self.base = os.path.abspath(os.path.normpath(base or os.getcwd()))
51
+ self.prefix = self.base + os.sep
52
+ self.allfiles = None
53
+ self.files = set()
54
+
55
+ #
56
+ # Public API
57
+ #
58
+
59
+ def findall(self):
60
+ """Find all files under the base and set ``allfiles`` to the absolute
61
+ pathnames of files found.
62
+ """
63
+ from stat import S_ISREG, S_ISDIR, S_ISLNK
64
+
65
+ self.allfiles = allfiles = []
66
+ root = self.base
67
+ stack = [root]
68
+ pop = stack.pop
69
+ push = stack.append
70
+
71
+ while stack:
72
+ root = pop()
73
+ names = os.listdir(root)
74
+
75
+ for name in names:
76
+ fullname = os.path.join(root, name)
77
+
78
+ # Avoid excess stat calls -- just one will do, thank you!
79
+ stat = os.stat(fullname)
80
+ mode = stat.st_mode
81
+ if S_ISREG(mode):
82
+ allfiles.append(fsdecode(fullname))
83
+ elif S_ISDIR(mode) and not S_ISLNK(mode):
84
+ push(fullname)
85
+
86
+ def add(self, item):
87
+ """
88
+ Add a file to the manifest.
89
+
90
+ :param item: The pathname to add. This can be relative to the base.
91
+ """
92
+ if not item.startswith(self.prefix):
93
+ item = os.path.join(self.base, item)
94
+ self.files.add(os.path.normpath(item))
95
+
96
+ def add_many(self, items):
97
+ """
98
+ Add a list of files to the manifest.
99
+
100
+ :param items: The pathnames to add. These can be relative to the base.
101
+ """
102
+ for item in items:
103
+ self.add(item)
104
+
105
+ def sorted(self, wantdirs=False):
106
+ """
107
+ Return sorted files in directory order
108
+ """
109
+
110
+ def add_dir(dirs, d):
111
+ dirs.add(d)
112
+ logger.debug('add_dir added %s', d)
113
+ if d != self.base:
114
+ parent, _ = os.path.split(d)
115
+ assert parent not in ('', '/')
116
+ add_dir(dirs, parent)
117
+
118
+ result = set(self.files) # make a copy!
119
+ if wantdirs:
120
+ dirs = set()
121
+ for f in result:
122
+ add_dir(dirs, os.path.dirname(f))
123
+ result |= dirs
124
+ return [os.path.join(*path_tuple) for path_tuple in
125
+ sorted(os.path.split(path) for path in result)]
126
+
127
+ def clear(self):
128
+ """Clear all collected files."""
129
+ self.files = set()
130
+ self.allfiles = []
131
+
132
+ def process_directive(self, directive):
133
+ """
134
+ Process a directive which either adds some files from ``allfiles`` to
135
+ ``files``, or removes some files from ``files``.
136
+
137
+ :param directive: The directive to process. This should be in a format
138
+ compatible with distutils ``MANIFEST.in`` files:
139
+
140
+ http://docs.python.org/distutils/sourcedist.html#commands
141
+ """
142
+ # Parse the line: split it up, make sure the right number of words
143
+ # is there, and return the relevant words. 'action' is always
144
+ # defined: it's the first word of the line. Which of the other
145
+ # three are defined depends on the action; it'll be either
146
+ # patterns, (dir and patterns), or (dirpattern).
147
+ action, patterns, thedir, dirpattern = self._parse_directive(directive)
148
+
149
+ # OK, now we know that the action is valid and we have the
150
+ # right number of words on the line for that action -- so we
151
+ # can proceed with minimal error-checking.
152
+ if action == 'include':
153
+ for pattern in patterns:
154
+ if not self._include_pattern(pattern, anchor=True):
155
+ logger.warning('no files found matching %r', pattern)
156
+
157
+ elif action == 'exclude':
158
+ for pattern in patterns:
159
+ self._exclude_pattern(pattern, anchor=True)
160
+
161
+ elif action == 'global-include':
162
+ for pattern in patterns:
163
+ if not self._include_pattern(pattern, anchor=False):
164
+ logger.warning('no files found matching %r '
165
+ 'anywhere in distribution', pattern)
166
+
167
+ elif action == 'global-exclude':
168
+ for pattern in patterns:
169
+ self._exclude_pattern(pattern, anchor=False)
170
+
171
+ elif action == 'recursive-include':
172
+ for pattern in patterns:
173
+ if not self._include_pattern(pattern, prefix=thedir):
174
+ logger.warning('no files found matching %r '
175
+ 'under directory %r', pattern, thedir)
176
+
177
+ elif action == 'recursive-exclude':
178
+ for pattern in patterns:
179
+ self._exclude_pattern(pattern, prefix=thedir)
180
+
181
+ elif action == 'graft':
182
+ if not self._include_pattern(None, prefix=dirpattern):
183
+ logger.warning('no directories found matching %r',
184
+ dirpattern)
185
+
186
+ elif action == 'prune':
187
+ if not self._exclude_pattern(None, prefix=dirpattern):
188
+ logger.warning('no previously-included directories found '
189
+ 'matching %r', dirpattern)
190
+ else: # pragma: no cover
191
+ # This should never happen, as it should be caught in
192
+ # _parse_template_line
193
+ raise DistlibException(
194
+ 'invalid action %r' % action)
195
+
196
+ #
197
+ # Private API
198
+ #
199
+
200
+ def _parse_directive(self, directive):
201
+ """
202
+ Validate a directive.
203
+ :param directive: The directive to validate.
204
+ :return: A tuple of action, patterns, thedir, dir_patterns
205
+ """
206
+ words = directive.split()
207
+ if len(words) == 1 and words[0] not in ('include', 'exclude',
208
+ 'global-include',
209
+ 'global-exclude',
210
+ 'recursive-include',
211
+ 'recursive-exclude',
212
+ 'graft', 'prune'):
213
+ # no action given, let's use the default 'include'
214
+ words.insert(0, 'include')
215
+
216
+ action = words[0]
217
+ patterns = thedir = dir_pattern = None
218
+
219
+ if action in ('include', 'exclude',
220
+ 'global-include', 'global-exclude'):
221
+ if len(words) < 2:
222
+ raise DistlibException(
223
+ '%r expects <pattern1> <pattern2> ...' % action)
224
+
225
+ patterns = [convert_path(word) for word in words[1:]]
226
+
227
+ elif action in ('recursive-include', 'recursive-exclude'):
228
+ if len(words) < 3:
229
+ raise DistlibException(
230
+ '%r expects <dir> <pattern1> <pattern2> ...' % action)
231
+
232
+ thedir = convert_path(words[1])
233
+ patterns = [convert_path(word) for word in words[2:]]
234
+
235
+ elif action in ('graft', 'prune'):
236
+ if len(words) != 2:
237
+ raise DistlibException(
238
+ '%r expects a single <dir_pattern>' % action)
239
+
240
+ dir_pattern = convert_path(words[1])
241
+
242
+ else:
243
+ raise DistlibException('unknown action %r' % action)
244
+
245
+ return action, patterns, thedir, dir_pattern
246
+
247
+ def _include_pattern(self, pattern, anchor=True, prefix=None,
248
+ is_regex=False):
249
+ """Select strings (presumably filenames) from 'self.files' that
250
+ match 'pattern', a Unix-style wildcard (glob) pattern.
251
+
252
+ Patterns are not quite the same as implemented by the 'fnmatch'
253
+ module: '*' and '?' match non-special characters, where "special"
254
+ is platform-dependent: slash on Unix; colon, slash, and backslash on
255
+ DOS/Windows; and colon on Mac OS.
256
+
257
+ If 'anchor' is true (the default), then the pattern match is more
258
+ stringent: "*.py" will match "foo.py" but not "foo/bar.py". If
259
+ 'anchor' is false, both of these will match.
260
+
261
+ If 'prefix' is supplied, then only filenames starting with 'prefix'
262
+ (itself a pattern) and ending with 'pattern', with anything in between
263
+ them, will match. 'anchor' is ignored in this case.
264
+
265
+ If 'is_regex' is true, 'anchor' and 'prefix' are ignored, and
266
+ 'pattern' is assumed to be either a string containing a regex or a
267
+ regex object -- no translation is done, the regex is just compiled
268
+ and used as-is.
269
+
270
+ Selected strings will be added to self.files.
271
+
272
+ Return True if files are found.
273
+ """
274
+ # XXX docstring lying about what the special chars are?
275
+ found = False
276
+ pattern_re = self._translate_pattern(pattern, anchor, prefix, is_regex)
277
+
278
+ # delayed loading of allfiles list
279
+ if self.allfiles is None:
280
+ self.findall()
281
+
282
+ for name in self.allfiles:
283
+ if pattern_re.search(name):
284
+ self.files.add(name)
285
+ found = True
286
+ return found
287
+
288
+ def _exclude_pattern(self, pattern, anchor=True, prefix=None,
289
+ is_regex=False):
290
+ """Remove strings (presumably filenames) from 'files' that match
291
+ 'pattern'.
292
+
293
+ Other parameters are the same as for 'include_pattern()', above.
294
+ The list 'self.files' is modified in place. Return True if files are
295
+ found.
296
+
297
+ This API is public to allow e.g. exclusion of SCM subdirs, e.g. when
298
+ packaging source distributions
299
+ """
300
+ found = False
301
+ pattern_re = self._translate_pattern(pattern, anchor, prefix, is_regex)
302
+ for f in list(self.files):
303
+ if pattern_re.search(f):
304
+ self.files.remove(f)
305
+ found = True
306
+ return found
307
+
308
+ def _translate_pattern(self, pattern, anchor=True, prefix=None,
309
+ is_regex=False):
310
+ """Translate a shell-like wildcard pattern to a compiled regular
311
+ expression.
312
+
313
+ Return the compiled regex. If 'is_regex' true,
314
+ then 'pattern' is directly compiled to a regex (if it's a string)
315
+ or just returned as-is (assumes it's a regex object).
316
+ """
317
+ if is_regex:
318
+ if isinstance(pattern, str):
319
+ return re.compile(pattern)
320
+ else:
321
+ return pattern
322
+
323
+ if _PYTHON_VERSION > (3, 2):
324
+ # ditch start and end characters
325
+ start, _, end = self._glob_to_re('_').partition('_')
326
+
327
+ if pattern:
328
+ pattern_re = self._glob_to_re(pattern)
329
+ if _PYTHON_VERSION > (3, 2):
330
+ assert pattern_re.startswith(start) and pattern_re.endswith(end)
331
+ else:
332
+ pattern_re = ''
333
+
334
+ base = re.escape(os.path.join(self.base, ''))
335
+ if prefix is not None:
336
+ # ditch end of pattern character
337
+ if _PYTHON_VERSION <= (3, 2):
338
+ empty_pattern = self._glob_to_re('')
339
+ prefix_re = self._glob_to_re(prefix)[:-len(empty_pattern)]
340
+ else:
341
+ prefix_re = self._glob_to_re(prefix)
342
+ assert prefix_re.startswith(start) and prefix_re.endswith(end)
343
+ prefix_re = prefix_re[len(start): len(prefix_re) - len(end)]
344
+ sep = os.sep
345
+ if os.sep == '\\':
346
+ sep = r'\\'
347
+ if _PYTHON_VERSION <= (3, 2):
348
+ pattern_re = '^' + base + sep.join((prefix_re,
349
+ '.*' + pattern_re))
350
+ else:
351
+ pattern_re = pattern_re[len(start): len(pattern_re) - len(end)]
352
+ pattern_re = r'%s%s%s%s.*%s%s' % (start, base, prefix_re, sep,
353
+ pattern_re, end)
354
+ else: # no prefix -- respect anchor flag
355
+ if anchor:
356
+ if _PYTHON_VERSION <= (3, 2):
357
+ pattern_re = '^' + base + pattern_re
358
+ else:
359
+ pattern_re = r'%s%s%s' % (start, base, pattern_re[len(start):])
360
+
361
+ return re.compile(pattern_re)
362
+
363
+ def _glob_to_re(self, pattern):
364
+ """Translate a shell-like glob pattern to a regular expression.
365
+
366
+ Return a string containing the regex. Differs from
367
+ 'fnmatch.translate()' in that '*' does not match "special characters"
368
+ (which are platform-specific).
369
+ """
370
+ pattern_re = fnmatch.translate(pattern)
371
+
372
+ # '?' and '*' in the glob pattern become '.' and '.*' in the RE, which
373
+ # IMHO is wrong -- '?' and '*' aren't supposed to match slash in Unix,
374
+ # and by extension they shouldn't match such "special characters" under
375
+ # any OS. So change all non-escaped dots in the RE to match any
376
+ # character except the special characters (currently: just os.sep).
377
+ sep = os.sep
378
+ if os.sep == '\\':
379
+ # we're using a regex to manipulate a regex, so we need
380
+ # to escape the backslash twice
381
+ sep = r'\\\\'
382
+ escaped = r'\1[^%s]' % sep
383
+ pattern_re = re.sub(r'((?<!\\)(\\\\)*)\.', escaped, pattern_re)
384
+ return pattern_re