mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-05-25 00:08:45 -04:00
Add redbot-update command for updating Red (#6734)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user