From c9f1a45854a14acf76d6a6a98c2f3f3626a5b8c6 Mon Sep 17 00:00:00 2001 From: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Sat, 9 Apr 2022 19:34:12 +0200 Subject: [PATCH] Make `--debuginfo` more like `[p]debuginfo` (#5662) --- redbot/__main__.py | 59 ++++-------- redbot/core/_debuginfo.py | 170 +++++++++++++++++++++++++++++++++++ redbot/core/core_commands.py | 101 +-------------------- 3 files changed, 189 insertions(+), 141 deletions(-) create mode 100644 redbot/core/_debuginfo.py diff --git a/redbot/__main__.py b/redbot/__main__.py index 40e3208ad..67697766d 100644 --- a/redbot/__main__.py +++ b/redbot/__main__.py @@ -18,7 +18,7 @@ import sys from argparse import Namespace from copy import deepcopy from pathlib import Path -from typing import NoReturn +from typing import Any, Awaitable, Callable, NoReturn, Union import discord import rich @@ -29,6 +29,7 @@ from redbot.core.bot import Red, ExitCodes, _NoOwnerSet from redbot.core.cli import interactive_config, confirm, parse_cli_flags from redbot.setup import get_data_dir, get_name, save_config from redbot.core import data_manager, drivers +from redbot.core._debuginfo import DebugInfo from redbot.core._sharedlibdeprecation import SharedLibImportWarner @@ -62,42 +63,9 @@ def list_instances(): sys.exit(0) -def debug_info(): +async def debug_info(*args: Any) -> None: """Shows debug information useful for debugging.""" - if sys.platform == "linux": - import distro # pylint: disable=import-error - - IS_WINDOWS = os.name == "nt" - IS_MAC = sys.platform == "darwin" - IS_LINUX = sys.platform == "linux" - - pyver = sys.version - pipver = pip.__version__ - redver = __version__ - dpy_version = discord.__version__ - if IS_WINDOWS: - os_info = platform.uname() - osver = "{} {} (version {})".format(os_info.system, os_info.release, os_info.version) - elif IS_MAC: - os_info = platform.mac_ver() - osver = "Mac OSX {} {}".format(os_info[0], os_info[2]) - else: - osver = f"{distro.name()} {distro.version()}".strip() - user_who_ran = getpass.getuser() - info = ( - "Debug Info for Red\n\n" - + "Red version: {}\n".format(redver) - + "Python version: {}\n".format(pyver) - + "Python executable: {}\n".format(sys.executable) - + "Discord.py version: {}\n".format(dpy_version) - + "Pip version: {}\n".format(pipver) - + "OS version: {}\n".format(osver) - + "System arch: {}\n".format(platform.machine()) - + "User: {}\n".format(user_who_ran) - + "Metadata file: {}\n".format(data_manager.config_file) - ) - print(info) - sys.exit(0) + print(await DebugInfo().get_text()) async def edit_instance(red, cli_flags): @@ -291,18 +259,25 @@ def _copy_data(data): return True -def handle_edit(cli_flags: Namespace): +def early_exit_runner( + cli_flags: Namespace, + func: Union[Callable[[], Awaitable[Any]], Callable[[Red, Namespace], Awaitable[Any]]], +) -> None: """ This one exists to not log all the things like it's a full run of the bot. """ loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - data_manager.load_basic_configuration(cli_flags.instance_name) - red = Red(cli_flags=cli_flags, description="Red V3", dm_help=None) try: + if not cli_flags.instance_name: + loop.run_until_complete(func()) + return + + data_manager.load_basic_configuration(cli_flags.instance_name) + red = Red(cli_flags=cli_flags, description="Red V3", dm_help=None) driver_cls = drivers.get_driver_class() loop.run_until_complete(driver_cls.initialize(**data_manager.storage_details())) - loop.run_until_complete(edit_instance(red, cli_flags)) + loop.run_until_complete(func(red, cli_flags)) loop.run_until_complete(driver_cls.teardown()) except (KeyboardInterrupt, EOFError): print("Aborted!") @@ -430,7 +405,7 @@ def handle_early_exit_flags(cli_flags: Namespace): print("Current Version: {}".format(__version__)) sys.exit(0) elif cli_flags.debuginfo: - debug_info() + early_exit_runner(cli_flags, debug_info) elif not cli_flags.instance_name and (not cli_flags.no_instance or cli_flags.edit): print("Error: No instance name was provided!") sys.exit(1) @@ -502,7 +477,7 @@ def main(): cli_flags = parse_cli_flags(sys.argv[1:]) handle_early_exit_flags(cli_flags) if cli_flags.edit: - handle_edit(cli_flags) + early_exit_runner(cli_flags, edit_instance) return try: loop = asyncio.new_event_loop() diff --git a/redbot/core/_debuginfo.py b/redbot/core/_debuginfo.py new file mode 100644 index 000000000..7a1fdd4a1 --- /dev/null +++ b/redbot/core/_debuginfo.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import getpass +import os +import platform +import sys +from typing import Optional + +import discord +import pip +import psutil + +from redbot import __version__ +from redbot.core import data_manager +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import box + + +def noop_box(text: str, **kwargs) -> str: + return text + + +def _datasize(num: int): + for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]: + if abs(num) < 1024.0: + return "{0:.1f}{1}".format(num, unit) + num /= 1024.0 + return "{0:.1f}{1}".format(num, "YB") + + +class DebugInfoSection: + def __init__(self, section_name: str, *section_parts: str) -> None: + self.section_name = section_name + self.section_parts = section_parts + + def get_command_text(self) -> str: + parts = [box(f"## {self.section_name}:", lang="md")] + for part in self.section_parts: + parts.append(box(part)) + return "".join(parts) + + def get_cli_text(self) -> str: + parts = [f"\x1b[32m## {self.section_name}:\x1b[0m"] + for part in self.section_parts: + parts.append(part) + return "\n".join(parts) + + +class DebugInfo: + def __init__(self, bot: Optional[Red] = None) -> None: + self.bot = bot + + async def get_text(self) -> str: + if self.bot is None: + return await self.get_cli_text() + else: + return await self.get_command_text() + + async def get_cli_text(self) -> str: + parts = ["\x1b[31m# Debug Info for Red:\x1b[0m"] + for section in ( + self._get_system_metadata_section(), + self._get_os_variables_section(), + await self._get_red_vars_section(), + ): + parts.append("") + parts.append(section.get_cli_text()) + + return "\n".join(parts) + + async def get_command_text(self) -> str: + parts = [box("# Debug Info for Red:", lang="md")] + for section in ( + self._get_system_metadata_section(), + self._get_os_variables_section(), + await self._get_red_vars_section(), + ): + parts.append("\n") + parts.append(section.get_command_text()) + + return "".join(parts) + + def _get_system_metadata_section(self) -> DebugInfoSection: + memory_ram = psutil.virtual_memory() + ram_string = "{used}/{total} ({percent}%)".format( + used=_datasize(memory_ram.used), + total=_datasize(memory_ram.total), + percent=memory_ram.percent, + ) + return DebugInfoSection( + "System Metadata", + f"CPU Cores: {psutil.cpu_count()} ({platform.machine()})\nRAM: {ram_string}", + ) + + def _get_os_variables_section(self) -> DebugInfoSection: + IS_WINDOWS = os.name == "nt" + IS_MAC = sys.platform == "darwin" + IS_LINUX = sys.platform == "linux" + + python_version = ".".join(map(str, sys.version_info[:3])) + pyver = f"{python_version} ({platform.architecture()[0]})" + pipver = pip.__version__ + redver = __version__ + dpy_version = discord.__version__ + if IS_WINDOWS: + os_info = platform.uname() + osver = f"{os_info.system} {os_info.release} (version {os_info.version})" + elif IS_MAC: + os_info = platform.mac_ver() + osver = f"Mac OSX {os_info[0]} {os_info[2]}" + elif IS_LINUX: + import distro + + osver = f"{distro.name()} {distro.version()}".strip() + else: + osver = "Could not parse OS, report this on Github." + user_who_ran = getpass.getuser() + + resp_os = f"OS version: {osver}\nUser: {user_who_ran}\n" # Ran where off to?! + resp_py_metadata = ( + f"Python executable: {sys.executable}\n" + f"Python version: {pyver}\n" + f"Pip version: {pipver}\n" + ) + resp_red_metadata = f"Red version: {redver}\nDiscord.py version: {dpy_version}" + return DebugInfoSection( + "OS variables", + resp_os, + resp_py_metadata, + resp_red_metadata, + ) + + async def _get_red_vars_section(self) -> DebugInfoSection: + if data_manager.instance_name is None: + return DebugInfoSection( + "Red variables", + f"Metadata file: {data_manager.config_file}", + ) + + parts = [f"Instance name: {data_manager.instance_name}"] + if self.bot is not None: + owners = [] + for uid in self.bot.owner_ids: + try: + u = await self.bot.get_or_fetch_user(uid) + owners.append(f"{u.id} ({u})") + except discord.HTTPException: + owners.append(f"{uid} (Unresolvable)") + owners_string = ", ".join(owners) or "None" + parts.append(f"Owner(s): {', '.join(owners) or 'None'}") + + if self.bot is not None: + disabled_intents = ( + ", ".join( + intent_name.replace("_", " ").title() + for intent_name, enabled in self.bot.intents + if not enabled + ) + or "None" + ) + parts.append(f"Disabled intents: {disabled_intents}") + + parts.append(f"Storage type: {data_manager.storage_type()}") + parts.append(f"Data path: {data_manager.basic_config['DATA_PATH']}") + parts.append(f"Metadata file: {data_manager.config_file}") + + return DebugInfoSection( + "Red variables", + "\n".join(parts), + ) diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index 6c58d37e8..bee8a97a6 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -4091,106 +4091,9 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic): @checks.is_owner() async def debuginfo(self, ctx: commands.Context): """Shows debug information useful for debugging.""" + from redbot.core._debuginfo import DebugInfo - if sys.platform == "linux": - import distro # pylint: disable=import-error - - IS_WINDOWS = os.name == "nt" - IS_MAC = sys.platform == "darwin" - IS_LINUX = sys.platform == "linux" - - python_version = ".".join(map(str, sys.version_info[:3])) - pyver = f"{python_version} ({platform.architecture()[0]})" - pipver = pip.__version__ - redver = red_version_info - dpy_version = discord.__version__ - if IS_WINDOWS: - os_info = platform.uname() - osver = f"{os_info.system} {os_info.release} (version {os_info.version})" - elif IS_MAC: - os_info = platform.mac_ver() - osver = f"Mac OSX {os_info[0]} {os_info[2]}" - elif IS_LINUX: - osver = f"{distro.name()} {distro.version()}".strip() - else: - osver = "Could not parse OS, report this on Github." - user_who_ran = getpass.getuser() - driver = storage_type() - - from redbot.core.data_manager import basic_config, config_file - - data_path = Path(basic_config["DATA_PATH"]) - disabled_intents = ( - ", ".join( - intent_name.replace("_", " ").title() - for intent_name, enabled in self.bot.intents - if not enabled - ) - or "None" - ) - - def _datasize(num: int): - for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]: - if abs(num) < 1024.0: - return "{0:.1f}{1}".format(num, unit) - num /= 1024.0 - return "{0:.1f}{1}".format(num, "YB") - - memory_ram = psutil.virtual_memory() - ram_string = "{used}/{total} ({percent}%)".format( - used=_datasize(memory_ram.used), - total=_datasize(memory_ram.total), - percent=memory_ram.percent, - ) - - owners = [] - for uid in self.bot.owner_ids: - try: - u = await self.bot.get_or_fetch_user(uid) - owners.append(f"{u.id} ({u})") - except discord.HTTPException: - owners.append(f"{uid} (Unresolvable)") - owners_string = ", ".join(owners) or "None" - - resp_intro = "# Debug Info for Red:" - resp_system_intro = "## System Metadata:" - resp_system = ( - f"CPU Cores: {psutil.cpu_count()} ({platform.machine()})\nRAM: {ram_string}\n" - ) - resp_os_intro = "## OS Variables:" - resp_os = f"OS version: {osver}\nUser: {user_who_ran}\n" # Ran where off to?! - resp_py_metadata = ( - f"Python executable: {sys.executable}\n" - f"Python version: {pyver}\n" - f"Pip version: {pipver}\n" - ) - resp_red_metadata = f"Red version: {redver}\nDiscord.py version: {dpy_version}\n" - resp_red_vars_intro = "## Red variables:" - resp_red_vars = ( - f"Instance name: {data_manager.instance_name}\n" - f"Owner(s): {owners_string}\n" - f"Storage type: {driver}\n" - f"Disabled intents: {disabled_intents}\n" - f"Data path: {data_path}\n" - f"Metadata file: {config_file}" - ) - - response = ( - box(resp_intro, lang="md"), - "\n", - box(resp_system_intro, lang="md"), - box(resp_system), - "\n", - box(resp_os_intro, lang="md"), - box(resp_os), - box(resp_py_metadata), - box(resp_red_metadata), - "\n", - box(resp_red_vars_intro, lang="md"), - box(resp_red_vars), - ) - - await ctx.send("".join(response)) + await ctx.send(await DebugInfo(self.bot).get_text()) # You may ask why this command is owner-only, # cause after all it could be quite useful to guild owners!