Add redbot-update command for updating Red (#6734)

This commit is contained in:
Jakub Kuczys
2026-05-14 00:14:43 +02:00
committed by GitHub
parent 899f24ceca
commit 7e2a74b276
22 changed files with 3016 additions and 76 deletions
+10 -5
View File
@@ -721,12 +721,15 @@ async def update_cogs(
*,
cogs: Optional[List[InstalledModule]] = None,
repos: Optional[List[Repo]] = None,
update_repos: bool = True,
env: Environment = Environment.current(),
) -> CogUpdateResult:
if cogs is not None and repos is not None:
raise ValueError("You can specify cogs or repos argument, not both")
cogs_to_check, failed_repos = await _get_cogs_to_check(repos=repos, cogs=cogs)
cogs_to_check, failed_repos = await _get_cogs_to_check(
repos=repos, cogs=cogs, update_repos=update_repos
)
return await _update_cogs(cogs_to_check, failed_repos=failed_repos, env=env)
@@ -737,12 +740,14 @@ async def update_repo_cogs(
cogs: Optional[List[InstalledModule]] = None,
*,
rev: Optional[str] = None,
update_repo: bool = True,
env: Environment = Environment.current(),
) -> CogUpdateResult:
try:
await repo.update()
except errors.UpdateError:
return await _update_cogs(set(), failed_repos=(repo.name,))
if update_repo:
try:
await repo.update()
except errors.UpdateError:
return await _update_cogs(set(), failed_repos=(repo.name,))
# TODO: should this be set to `repo.branch` when `rev` is None?
commit = None
+45 -1
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union
@@ -21,6 +22,7 @@ class UseDefault:
# sentinel value
USE_DEFAULT = UseDefault()
RED_TAG_READY_PATTERN = re.compile(r"^red-(?:[3-9]|[1-9][0-9]+)\.(?:[1-9][0-9]*)-ready$")
def ensure_tuple_of_str(
@@ -203,6 +205,48 @@ def ensure_installable_type(
return installable.InstallableType.UNKNOWN
def ensure_tags(info_file: Path, key_name: str, value: Union[Any, UseDefault]) -> Tuple[str, ...]:
default: Tuple[str, ...] = ()
if value is USE_DEFAULT:
return default
if not isinstance(value, list):
log.warning(
"Invalid value of '%s' key (expected list, got %s)"
" in JSON information file at path: %s",
key_name,
type(value).__name__,
info_file,
)
return default
valid_tags = []
for item in value:
if not isinstance(item, str):
log.warning(
"Invalid item in '%s' list (expected str, got %s)"
" in JSON information file at path: %s",
key_name,
type(item).__name__,
info_file,
)
return default
# `red-` tags are reserved for informational metadata we only support a subset of tags
if not item.startswith("red-"):
valid_tags.append(item)
continue
if RED_TAG_READY_PATTERN.match(item):
valid_tags.append(item)
else:
log.warning(
"Invalid value in '%s' list (tag starts with the reserved 'red-' prefix"
" but does not use the only supported reserved tag format: 'red-X.Y-ready')"
" in JSON information file at path: %s",
key_name,
info_file,
)
return tuple(value)
EnsureCallable = Callable[[Path, str, Union[Any, UseDefault]], Any]
SchemaType = Dict[str, EnsureCallable]
@@ -224,7 +268,7 @@ INSTALLABLE_SCHEMA: SchemaType = {
"disabled": ensure_bool,
"required_cogs": ensure_required_cogs_mapping,
"requirements": ensure_tuple_of_str,
"tags": ensure_tuple_of_str,
"tags": ensure_tags,
"type": ensure_installable_type,
"end_user_data_statement": ensure_str,
}
+2 -1
View File
@@ -2,7 +2,7 @@ import enum
from typing import Optional, Type
from .. import data_manager
from .base import IdentifierData, BaseDriver, ConfigCategory
from .base import IdentifierData, BaseDriver, ConfigCategory, MissingExtraRequirements
from .json import JsonDriver
from .postgres import PostgresDriver
@@ -12,6 +12,7 @@ __all__ = [
"get_driver_class_include_old",
"ConfigCategory",
"IdentifierData",
"MissingExtraRequirements",
"BaseDriver",
"JsonDriver",
"PostgresDriver",
+10 -60
View File
@@ -1,6 +1,7 @@
import asyncio
import contextlib
import platform
import shlex
import sys
import logging
import traceback
@@ -9,8 +10,7 @@ from typing import Tuple
import aiohttp
import discord
import importlib.metadata
from packaging.requirements import Requirement
import redbot_update
from packaging.specifiers import SpecifierSet
from packaging.version import Version
from redbot.core import data_manager
@@ -53,7 +53,7 @@ ______ _ ______ _ _ ______ _
_ = Translator(__name__, __file__)
def get_outdated_red_messages(pypi_version: str, requires_python: SpecifierSet) -> Tuple[str, str]:
def get_outdated_red_messages(pypi_version: str) -> Tuple[str, str]:
outdated_red_message = _(
"Your Red instance is out of date! {} is the current version, however you are using {}!"
).format(pypi_version, red_version)
@@ -62,7 +62,6 @@ def get_outdated_red_messages(pypi_version: str, requires_python: SpecifierSet)
f"[red]!!![/red]Version [cyan]{pypi_version}[/] is available, "
f"but you're using [cyan]{red_version}[/][red]!!![/red]"
)
current_python = Version(platform.python_version())
extra_update = _(
"\n\nWhile the following command should work in most scenarios as it is "
"based on your current OS, environment, and Python version, "
@@ -71,64 +70,15 @@ def get_outdated_red_messages(pypi_version: str, requires_python: SpecifierSet)
"needs to be done during the update.**"
).format(docs="https://docs.discord.red/en/stable/update_red.html")
if current_python not in requires_python:
extra_update += _(
"\n\nYou have Python `{py_version}` and this update "
"requires `{req_py}`; you cannot simply run the update command.\n\n"
"You will need to follow the update instructions in our docs above, "
"if you still need help updating after following the docs go to our "
"#support channel in <https://discord.gg/red>"
).format(py_version=current_python, req_py=requires_python)
outdated_red_message += extra_update
return outdated_red_message, rich_outdated_message
red_dist = importlib.metadata.distribution("Red-DiscordBot")
installed_extras = red_dist.metadata.get_all("Provides-Extra")
installed_extras.remove("dev")
installed_extras.remove("all")
distributions = {}
for req_str in red_dist.requires:
req = Requirement(req_str)
if req.marker is None or req.marker.evaluate():
continue
for extra in reversed(installed_extras):
if not req.marker.evaluate({"extra": extra}):
continue
# Check that the requirement is met.
# This is a bit simplified for our purposes and does not check
# whether the requirements of our requirements are met as well.
# This could potentially be an issue if we'll ever depend on
# a dependency's extra in our extra when we already depend on that
# in our base dependencies. However, considering that right now, all
# our dependencies are also fully pinned, this should not ever matter.
if req.name in distributions:
dist = distributions[req.name]
else:
try:
dist = importlib.metadata.distribution(req.name)
except importlib.metadata.PackageNotFoundError:
dist = None
distributions[req.name] = dist
if dist is None or not req.specifier.contains(dist.version, prereleases=True):
installed_extras.remove(extra)
if installed_extras:
package_extras = f"[{','.join(installed_extras)}]"
else:
package_extras = ""
redbot_update_bin = redbot_update.find_redbot_update_bin()
is_windows = platform.system() == "Windows"
update_command = f'"{redbot_update_bin}"' if is_windows else shlex.quote(redbot_update_bin)
extra_update += _(
"\n\nTo update your bot, first shutdown your bot"
" then open a window of {console} (Not as admin) and run the following:"
"{command_1}\n"
"Once you've started up your bot again, we recommend that"
" you update any installed 3rd-party cogs with this command in Discord:"
"{command_2}"
" then open a window of {console} (Not as admin) and run the following: {command}"
).format(
console=_("Command Prompt") if platform.system() == "Windows" else _("Terminal"),
command_1=f'```"{sys.executable}" -m pip install -U "Red-DiscordBot{package_extras}"```',
command_2=f"```[p]cog update```",
console=_("Command Prompt") if is_windows else _("Terminal"),
command=f"```{update_command}```",
)
outdated_red_message += extra_update
return outdated_red_message, rich_outdated_message
@@ -224,7 +174,7 @@ def init_events(bot, cli_flags):
outdated = latest.version > Version(red_version)
if outdated:
outdated_red_message, rich_outdated_message = get_outdated_red_messages(
latest.version, latest.requires_python
latest.version
)
rich_console.print(rich_outdated_message)
await send_to_owners_with_prefix_replaced(bot, outdated_red_message)
+51
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import collections.abc
import contextlib
import importlib.metadata
import json
import logging
import os
@@ -40,6 +41,7 @@ import aiohttp
import discord
import yarl
from packaging.metadata import Metadata
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.utils import parse_sdist_filename
from packaging.version import Version
@@ -587,6 +589,43 @@ async def fetch_latest_red_version(
return available_versions[0]
def get_installed_extras() -> List[str]:
red_dist = importlib.metadata.distribution("Red-DiscordBot")
installed_extras = red_dist.metadata.get_all("Provides-Extra")
if installed_extras is None:
return []
installed_extras.remove("dev")
installed_extras.remove("all")
distributions: Dict[str, Optional[importlib.metadata.Distribution]] = {}
for req_str in red_dist.requires or []:
req = Requirement(req_str)
if req.marker is None or req.marker.evaluate():
continue
for extra in reversed(installed_extras):
if not req.marker.evaluate({"extra": extra}):
continue
# Check that the requirement is met.
# This is a bit simplified for our purposes and does not check
# whether the requirements of our requirements are met as well.
# This could potentially be an issue if we'll ever depend on
# a dependency's extra in our extra when we already depend on that
# in our base dependencies. However, considering that right now, all
# our dependencies are also fully pinned, this should not ever matter.
if req.name in distributions:
dist = distributions[req.name]
else:
try:
dist = importlib.metadata.distribution(req.name)
except importlib.metadata.PackageNotFoundError:
dist = None
distributions[req.name] = dist
if dist is None or not req.specifier.contains(dist.version, prereleases=True):
installed_extras.remove(extra)
return installed_extras
def deprecated_removed(
deprecation_target: str,
deprecation_version: str,
@@ -651,3 +690,15 @@ def cli_level_to_log_level(level: int) -> int:
else:
log_level = TRACE
return log_level
def log_level_to_cli_level(log_level: int) -> int:
if log_level == TRACE:
level = 3
elif log_level == VERBOSE:
level = 2
elif log_level == logging.DEBUG:
level = 1
else:
level = 0
return level