diff --git a/.github/labeler.yml b/.github/labeler.yml index e9c3bcc1a..df0026978 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -204,6 +204,7 @@ - docs/cog_guides/core.rst "Category: Core - Command-line Interfaces": - redbot/__main__.py + - redbot/_update/**/* - redbot/logging.py - redbot/core/_cli.py - redbot/core/_debuginfo.py diff --git a/docs/update_red.rst b/docs/update_red.rst index ae82ea951..29a55b746 100644 --- a/docs/update_red.rst +++ b/docs/update_red.rst @@ -25,13 +25,57 @@ Updating differs depending on the version you currently have. Next sections will :depth: 1 -Red 3.5.0 or newer -****************** +Red 3.5.25 or newer +******************* Windows ------- -If you have Red 3.5.0 or newer, you can upgrade by following these steps: +If you have Red 3.5.25 or newer, you can upgrade by following these steps: + +#. Shut your bot down. +#. Activate your venv with the following command: + + .. prompt:: batch + + "%userprofile%\redenv\Scripts\activate.bat" +#. Update Red with this command: + + .. prompt:: batch + :prompts: (redenv) C:\\> + + redbot-update +#. Start your bot. + +Linux & Mac +----------- + +If you have Red 3.5.25 or newer, you can upgrade by following these steps: + +#. Shut your bot down. +#. Activate your virtual environment. + + If you used ``venv`` for your virtual environment, use: + + .. prompt:: bash + + source ~/redenv/bin/activate + +#. Update Red with this command: + + .. prompt:: bash + :prompts: (redenv) $ + + redbot-update +#. Start your bot. + +Red 3.5.0-3.5.24 +**************** + +Windows +------- + +If you have a Red version between 3.5.0 and 3.5.24, you can upgrade by following these steps: #. Shut your bot down. #. Activate your venv with the following command: @@ -55,7 +99,7 @@ If you have Red 3.5.0 or newer, you can upgrade by following these steps: Linux & Mac ----------- -If you have Red 3.5.0 or newer, you can upgrade by following these steps: +If you have a Red version between 3.5.0 and 3.5.24, you can upgrade by following these steps: #. Shut your bot down. #. Activate your virtual environment. diff --git a/redbot/_update/__init__.py b/redbot/_update/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/redbot/_update/__main__.py b/redbot/_update/__main__.py new file mode 100644 index 000000000..b2631fb9d --- /dev/null +++ b/redbot/_update/__main__.py @@ -0,0 +1,237 @@ +import asyncio +from pathlib import Path +from typing import Any, Final, Optional, Tuple + +import click +from packaging.version import Version +from python_discovery import PythonInfo + +from redbot.core._cli import asyncio_run + +from . import cmd, common, updater + + +_CHECK_OTHER_PYTHON_INSTALLS_CMD_ARG_NAME: Final = "--check-other-python-installs" + + +def _help_major_update_example() -> str: + version = common.get_current_red_version().__replace__(dev=None, local=None) + release = (version.major, version.minor + 1) + (0,) * (len(version.release) - 2) + next_major_version = version.__replace__(release=release) + return f"updating from Red {version} to Red {next_major_version}" + + +def _help_minor_update_example() -> str: + version = common.get_current_red_version().__replace__(dev=None, local=None) + release = (version.major, version.minor, version.micro + 1) + (0,) * (len(version.release) - 3) + next_minor_version = version.__replace__(release=release) + return f"updating from Red {version} to Red {next_minor_version}" + + +class _PythonInfoParamType(click.ParamType): + name = "Python interpreter" + + def convert( + self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context] + ) -> PythonInfo: + if isinstance(value, PythonInfo): + return value + + try: + return PythonInfo.from_exe(value) + except RuntimeError: + self.fail(f"{value!r} is not a valid Python executable.", param, ctx) + + +@click.group(invoke_without_command=True) +# command-specific options +@click.option( + "--include-instance", + "included_instances", + multiple=True, + type=click.Choice(common.INSTANCE_LIST), + help="The list of instances to backup and check cog compatibility for. If not specified," + " all instances that use the current virtual environment will be backed up and checked.", +) +@click.option( + "--exclude-instance", + "excluded_instances", + multiple=True, + type=click.Choice(common.INSTANCE_LIST), + help="Exclude an instance from the list of instances to backup" + " and check cog compatibility for.", +) +@click.option( + "--backup-dir", + default=None, + type=click.Path( + dir_okay=True, file_okay=False, resolve_path=True, writable=True, path_type=Path + ), + help="The directory to place the backups of the virtual environment and instances.", +) +@click.option( + "--no-backup", + help="Do not make backups of the virtual environment and instances before update.", + is_flag=True, +) +@click.option( + "--red-version", + "--version", + type=common.VersionParamType(), + default=None, + help="Version of Red to update to instead of the latest.", +) +@click.option( + "--no-major-updates", + help=f"Skip major updates. For example: {_help_major_update_example()} is a major update" + f" but {_help_minor_update_example()} isn't.", + is_flag=True, +) +@click.option( + "--no-full-changelog", + help='Skip showing full changelog in a terminal user interface. The "Read before updating"' + " sections will still be printed.", + is_flag=True, +) +@click.option( + "--no-cog-compatibility-check", + help="Skip performing cog compatibility check before the update.", + is_flag=True, +) +@click.option( + "--new-python-interpreter", + type=_PythonInfoParamType(), + help="The new Python interpreter that should be used when creating a virtual environment" + " for Red. This can either be a path to a Python executable or a name of a Python executable" + " on the PATH.", +) +@click.option( + "--update-cogs/--no-update-cogs", + default=None, + help="When this option is used, it determines whether the cogs should be updated after Red" + " is updated. By default, you'll be asked, if you want to update.\n" + "In non-interactive mode, cogs will be updated unless this option is used to override" + " the default behavior.", +) +@click.option( + # `pip install` having an option with the same name is coincidental, + # this does not call `pip install` with the `--force-reinstall` option. + # Not that there would be any point in doing so - we create a fresh virtual environment. + "--force-reinstall", + type=bool, + is_flag=True, + help="Force the update process to proceed even, if there is no new version detected." + " This will essentially reinstall latest Red version into a fresh virtual environment. You can" + " combine it with the --new-python-interpreter option to change Red's Python interpreter.", +) +@click.option( + "--no-prompt", + "interactive", + type=bool, + is_flag=True, + default=True, + help="Don't ask for user input during the process (non-interactive mode).\n" + "NOTE: If you want to use this to automate Red updates, consider specifying --no-major-update" + " to avoid performing major updates without making an explicit decision to.\n" + "When performing a major update where the current Python interpreter is no longer compatible," + " the --new-python-interpreter option has to be specified or the command will fail.", +) +# global options +@click.option( + cmd.arg_names.DEBUG, + "--verbose", + "-v", + "logging_level", + count=True, + help=( + "Increase the verbosity of the logs, each usage of this flag increases the verbosity" + " level by 1." + ), +) +@click.option( + "--check-other-venvs", + _CHECK_OTHER_PYTHON_INSTALLS_CMD_ARG_NAME, + "ignore_prefix", + help="Check the compatibility of cogs for instances that are normally ran with" + " a different Python installation and/or virtual environment than the current one.", + is_flag=True, +) +@click.pass_context +def cli( + ctx: click.Context, + included_instances: Tuple[str, ...], + excluded_instances: Tuple[str, ...], + backup_dir: Optional[Path], + no_backup: bool, + red_version: Optional[Version], + no_major_updates: bool, + no_full_changelog: bool, + no_cog_compatibility_check: bool, + new_python_interpreter: Optional[PythonInfo], + update_cogs: Optional[bool], + force_reinstall: bool, + interactive: bool, + logging_level: int, + ignore_prefix: bool, +) -> None: + common.ensure_supported_env() + common.configure_logging(logging_level) + + ctx.ensure_object(dict) + ctx.obj["IGNORE_PREFIX"] = ignore_prefix + + if ctx.invoked_subcommand is None: + if included_instances: + # de-duplicate with order intact + instances = list(dict.fromkeys(included_instances)) + else: + instances = list(common.INSTANCE_LIST) + options = updater.UpdaterOptions( + instances=instances, + excluded_instances=set(excluded_instances), + ignore_prefix=ignore_prefix, + backup_dir=backup_dir, + no_backup=no_backup, + red_version=red_version, + no_major_updates=no_major_updates, + no_full_changelog=no_full_changelog, + no_cog_compatibility_check=no_cog_compatibility_check, + new_python_interpreter=new_python_interpreter, + update_cogs=update_cogs, + force_reinstall=force_reinstall, + interactive=interactive, + ) + app = updater.Updater(options) + asyncio_run(app.run()) + # these should not be available to subcommands + elif included_instances: + raise click.NoSuchOption("--include-instance", ctx=ctx) + elif excluded_instances: + raise click.NoSuchOption("--exclude-instance", ctx=ctx) + elif backup_dir is not None: + raise click.NoSuchOption("--backup-dir", ctx=ctx) + elif no_backup: + raise click.NoSuchOption("--no-backup", ctx=ctx) + elif red_version: + raise click.NoSuchOption("--red-version", ctx=ctx) + elif no_major_updates: + raise click.NoSuchOption("--no-major-updates", ctx=ctx) + elif no_cog_compatibility_check: + raise click.NoSuchOption("--no-cog-compatibility-check", ctx=ctx) + elif new_python_interpreter: + raise click.NoSuchOption("--new-python-interpreter", ctx=ctx) + elif update_cogs is True: + raise click.NoSuchOption("--update-cogs", ctx=ctx) + elif update_cogs is False: + raise click.NoSuchOption("--no-update-cogs", ctx=ctx) + elif not interactive: + raise click.NoSuchOption("--no-prompt", ctx=ctx) + elif force_reinstall: + raise click.NoSuchOption("--force-reinstall", ctx=ctx) + + +cli.add_command(cmd.cog_compatibility.check_cog_compatibility) + + +if __name__ == "__main__": + cli() diff --git a/redbot/_update/changelog.py b/redbot/_update/changelog.py new file mode 100644 index 000000000..9fe5dc9e5 --- /dev/null +++ b/redbot/_update/changelog.py @@ -0,0 +1,144 @@ +import dataclasses +import datetime +import functools +import os +import re +from typing import Any, Dict, List + +import aiohttp +import yarl +from packaging.version import Version +from typing_extensions import Self + + +_CHANGELOG_PATTERN = re.compile( + r"\n