Spaces:
Configuration error
Configuration error
Upload folder using huggingface_hub (part 17)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .venv/lib/python3.13/site-packages/pip/_internal/utils/logging.py +361 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/misc.py +773 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/packaging.py +43 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/retry.py +42 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/setuptools_build.py +147 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/subprocess.py +245 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/temp_dir.py +296 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/unpacking.py +335 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/urls.py +55 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/virtualenv.py +104 -0
- .venv/lib/python3.13/site-packages/pip/_internal/utils/wheel.py +133 -0
- .venv/lib/python3.13/site-packages/pip/_internal/vcs/__init__.py +15 -0
- .venv/lib/python3.13/site-packages/pip/_internal/vcs/bazaar.py +112 -0
- .venv/lib/python3.13/site-packages/pip/_internal/vcs/git.py +536 -0
- .venv/lib/python3.13/site-packages/pip/_internal/vcs/mercurial.py +163 -0
- .venv/lib/python3.13/site-packages/pip/_internal/vcs/subversion.py +324 -0
- .venv/lib/python3.13/site-packages/pip/_internal/vcs/versioncontrol.py +688 -0
- .venv/lib/python3.13/site-packages/pip/_internal/wheel_builder.py +332 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/__init__.py +117 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/__init__.py +29 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/_cmd.py +70 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/adapter.py +168 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/cache.py +75 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/__init__.py +8 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py +145 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py +48 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/controller.py +511 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/filewrapper.py +119 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/heuristics.py +157 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/py.typed +0 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/serialize.py +146 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/wrapper.py +43 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/certifi/__init__.py +4 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/certifi/__main__.py +12 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/certifi/cacert.pem +0 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/certifi/core.py +125 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/certifi/py.typed +0 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__init__.py +13 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__main__.py +65 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_implementation.py +209 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_lint_dependency_groups.py +59 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_pip_wrapper.py +62 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_toml_compat.py +9 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/py.typed +0 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/distlib/__init__.py +33 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/distlib/compat.py +1137 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/distlib/database.py +1329 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/distlib/index.py +508 -0
- .venv/lib/python3.13/site-packages/pip/_vendor/distlib/locators.py +1295 -0
- .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 & <-> & 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
|