mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-05-15 12:31:46 -04:00
222 lines
7.3 KiB
Python
222 lines
7.3 KiB
Python
import enum
|
|
import logging
|
|
import os
|
|
import sys
|
|
from operator import itemgetter
|
|
from typing import Any, Final, Iterable, List, Literal, Optional, Tuple, Union
|
|
|
|
import click
|
|
import rich
|
|
from packaging.specifiers import SpecifierSet
|
|
from packaging.version import Version
|
|
from python_discovery import PythonInfo, get_interpreter
|
|
from rich.console import Console, RenderableType
|
|
from rich.logging import RichHandler
|
|
from rich.table import Table
|
|
from rich.text import Text
|
|
|
|
from redbot import __version__
|
|
from redbot.core.utils._internal_utils import (
|
|
cli_level_to_log_level,
|
|
get_installed_extras,
|
|
log_level_to_cli_level,
|
|
)
|
|
from redbot.core import data_manager
|
|
|
|
_instance_data = data_manager.load_existing_config()
|
|
INSTANCE_LIST: Final = () if _instance_data is None else tuple(_instance_data.keys())
|
|
|
|
|
|
ICON_SUCCESS = "[green]:white_heavy_check_mark-emoji:[/]"
|
|
ICON_INFO = "[blue]:information-emoji:[/]"
|
|
ICON_WARN = "[yellow]:warning-emoji:[/]"
|
|
ICON_ERROR = "[red]:cross_mark-emoji:[/]"
|
|
|
|
INTERNAL_LEGACY_WINDOWS_ENV_VAR = "_RED_UPDATE_INTERNAL_LEGACY_WINDOWS"
|
|
INTERNAL_UPDATER_METADATA_ENV_VAR = "_RED_UPDATE_INTERNAL_UPDATER_METADATA"
|
|
_STDERR_CONSOLE: Optional[Console] = None
|
|
|
|
RUNNER_DIR_ENV_VAR: Final = "REDBOT_UPDATE_RUNNER_DIR"
|
|
RUNNER_WRAPPER_EXE_ENV_VAR: Final = "REDBOT_UPDATE_RUNNER_WRAPPER_EXE"
|
|
|
|
OLD_VENV_BACKUP_DIR_NAME: Final = "redbot-update-old-venv-backup"
|
|
|
|
|
|
def get_red_dependency_specifier(version: Version, extras: Iterable[str]) -> str:
|
|
specifier_template = (
|
|
os.getenv("_RED_UPDATE_PRETEND_SPECIFIER_TEMPLATE")
|
|
or "Red-DiscordBot {extras} {versionspec}"
|
|
)
|
|
joined_extras = ",".join(extras)
|
|
return specifier_template.format(
|
|
extras=f"[{joined_extras}]" if joined_extras else "",
|
|
versionspec=f"=={version}",
|
|
)
|
|
|
|
|
|
def get_current_red_version() -> Version:
|
|
return Version(os.getenv("_RED_UPDATE_PRETEND_VERSION") or __version__)
|
|
|
|
|
|
def get_current_python_version() -> Version:
|
|
return Version(".".join(map(str, sys.version_info[:3])))
|
|
|
|
|
|
def prefix_column(prefix: RenderableType, *parts: Union[str, Text]) -> Table:
|
|
output = Table.grid(padding=(0, 2))
|
|
output.add_column()
|
|
output.add_column()
|
|
text = Text()
|
|
for renderable in parts:
|
|
if isinstance(renderable, str):
|
|
text.append_text(Text.from_markup(renderable))
|
|
else:
|
|
text.append_text(renderable)
|
|
output.add_row(prefix, text)
|
|
return output
|
|
|
|
|
|
def print_with_prefix_column(
|
|
prefix: RenderableType, *parts: Union[str, Text], console: Optional[Console] = None
|
|
) -> None:
|
|
if console is None:
|
|
console = rich.get_console()
|
|
console.print(prefix_column(prefix, *parts))
|
|
|
|
|
|
def _apply_legacy_windows_workaround() -> None:
|
|
# Rich does not properly support printing to stderr, when stdout is redirected...
|
|
# This monkeypatch should be enough to workaround this for our purposes.
|
|
# https://github.com/Textualize/rich/issues/4071
|
|
if sys.platform == "win32" and not sys.stdout.isatty():
|
|
import rich._win32_console
|
|
|
|
rich._win32_console.STDOUT = -12
|
|
|
|
|
|
def configure_rich() -> None:
|
|
_apply_legacy_windows_workaround()
|
|
value = os.getenv(INTERNAL_LEGACY_WINDOWS_ENV_VAR, "")
|
|
legacy_windows = int(value) if value else None
|
|
rich.reconfigure(highlight=False, legacy_windows=legacy_windows)
|
|
global _STDERR_CONSOLE
|
|
_STDERR_CONSOLE = Console(highlight=False, stderr=True, legacy_windows=legacy_windows)
|
|
|
|
|
|
def get_console(stderr: bool = False) -> Console:
|
|
global _STDERR_CONSOLE
|
|
if _STDERR_CONSOLE is None:
|
|
raise RuntimeError("_STDERR_CONSOLE is not set")
|
|
return _STDERR_CONSOLE if stderr else rich.get_console()
|
|
|
|
|
|
def configure_logging(logging_level: int) -> None:
|
|
configure_rich()
|
|
level = cli_level_to_log_level(logging_level)
|
|
base_logger = logging.getLogger("red")
|
|
base_logger.setLevel(level)
|
|
base_logger.addHandler(RichHandler(console=get_console(stderr=True), show_path=False))
|
|
|
|
|
|
def get_logging_level() -> int:
|
|
return logging.getLogger("red").level
|
|
|
|
|
|
def get_log_cli_level() -> int:
|
|
return log_level_to_cli_level(logging.getLogger("red").level)
|
|
|
|
|
|
def ensure_supported_env() -> None:
|
|
if sys.prefix == sys.base_prefix:
|
|
print("redbot-update cannot be used when Red is installed outside a virtual environment.")
|
|
raise SystemExit(1)
|
|
if not (
|
|
os.environ.get(RUNNER_DIR_ENV_VAR, "") and os.environ.get(RUNNER_WRAPPER_EXE_ENV_VAR, "")
|
|
):
|
|
print("redbot-update was called incorrectly.")
|
|
raise SystemExit(1)
|
|
|
|
|
|
def _get_system_interpreters(
|
|
requires_python: SpecifierSet,
|
|
) -> List[Tuple[str, Version, PythonInfo]]:
|
|
interpreters = {}
|
|
|
|
def _append_interpreter(info: PythonInfo) -> Literal[False]:
|
|
version = Version(info.version_str)
|
|
if version in requires_python:
|
|
# realpath call is needed because get_interpreter lists
|
|
# /usr/bin and /bin as separate even though they're the same path
|
|
interpreters[os.path.realpath(info.executable)] = (version, info)
|
|
return False
|
|
|
|
get_interpreter("cpython", predicate=_append_interpreter)
|
|
|
|
ret = [(key, *value) for key, value in interpreters.items()]
|
|
ret.sort(key=itemgetter(1), reverse=True)
|
|
return ret
|
|
|
|
|
|
def search_for_interpreters(
|
|
requires_python: SpecifierSet,
|
|
) -> List[Tuple[str, Version, PythonInfo]]:
|
|
console = get_console()
|
|
with console.status("Searching for compatible Python interpreters on your system..."):
|
|
interpreters = _get_system_interpreters(requires_python)
|
|
|
|
if not interpreters:
|
|
url = "https://docs.discord.red/en/stable/install_guides/"
|
|
console.print(
|
|
f"{ICON_ERROR} Could not find a compatible Python interpreter!\n"
|
|
'Please follow the steps from the "Installing the pre-requirements" section'
|
|
" of the install guide for your system:"
|
|
)
|
|
console.print(Text(url, style=f"link {url}"))
|
|
console.print("Once you finish installing the pre-requirements, run this command again.")
|
|
raise SystemExit(1)
|
|
|
|
return interpreters
|
|
|
|
|
|
class OrderedEnum(enum.Enum):
|
|
def __ge__(self, other: Any) -> bool:
|
|
if self.__class__ is other.__class__:
|
|
return self.value >= other.value
|
|
return NotImplemented
|
|
|
|
def __gt__(self, other: Any) -> bool:
|
|
if self.__class__ is other.__class__:
|
|
return self.value > other.value
|
|
return NotImplemented
|
|
|
|
def __le__(self, other: Any) -> bool:
|
|
if self.__class__ is other.__class__:
|
|
return self.value <= other.value
|
|
return NotImplemented
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if self.__class__ is other.__class__:
|
|
return self.value < other.value
|
|
return NotImplemented
|
|
|
|
|
|
class VersionParamType(click.ParamType):
|
|
name = "version"
|
|
|
|
def convert(
|
|
self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
|
|
) -> Version:
|
|
if isinstance(value, Version):
|
|
if len(value.release) < 2:
|
|
self.fail(
|
|
f"{value!r} needs to have at least 2 release components (major and minor).",
|
|
param,
|
|
ctx,
|
|
)
|
|
return value
|
|
|
|
try:
|
|
return self.convert(Version(value), param, ctx)
|
|
except ValueError:
|
|
self.fail(f"{value!r} is not a valid version number", param, ctx)
|