diff --git a/redbot/__main__.py b/redbot/__main__.py index ee0d171eb..a5666dca6 100644 --- a/redbot/__main__.py +++ b/redbot/__main__.py @@ -337,7 +337,7 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None: redbot.logging.init_logging( level=cli_flags.logging_level, location=data_manager.core_data_path() / "logs", - force_rich_logging=cli_flags.rich_logging, + cli_flags=cli_flags, ) log.debug("====Basic Config====") diff --git a/redbot/core/__init__.py b/redbot/core/__init__.py index 2fa66696e..ddebbea15 100644 --- a/redbot/core/__init__.py +++ b/redbot/core/__init__.py @@ -1,4 +1,3 @@ -import colorama as _colorama import discord as _discord from .. import __version__, version_info, VersionInfo @@ -7,7 +6,5 @@ from .utils.safety import warn_unsafe as _warn_unsafe __all__ = ["Config", "__version__", "version_info", "VersionInfo"] -_colorama.init() - # Prevent discord PyNaCl missing warning _discord.voice_client.VoiceClient.warn_nacl = False diff --git a/redbot/core/bot.py b/redbot/core/bot.py index 1bce5a9a1..bf9358075 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -29,7 +29,6 @@ from typing import ( overload, ) from types import MappingProxyType -from rich.console import Console import discord from discord.ext import commands as dpy_commands @@ -224,9 +223,6 @@ class RedBase( self._deletion_requests: MutableMapping[int, asyncio.Lock] = weakref.WeakValueDictionary() - # Although I see the use of keeping this public, lets rather make it private. - self._rich_console = Console() - def set_help_formatter(self, formatter: commands.help.HelpFormatterABC): """ Set's Red's help formatter. diff --git a/redbot/core/cli.py b/redbot/core/cli.py index 129bd7b86..bb845bda6 100644 --- a/redbot/core/cli.py +++ b/redbot/core/cli.py @@ -263,6 +263,20 @@ def parse_cli_flags(args): default=None, help="Forcefully disables the Rich logging handlers.", ) + parser.add_argument( + "--rich-traceback-extra-lines", + type=positive_int, + default=0, + help="Set the number of additional lines of code before and after the executed line" + " that should be shown in tracebacks generated by Rich.\n" + "Useful for development.", + ) + parser.add_argument( + "--rich-traceback-show-locals", + action="store_true", + help="Enable showing local variables in tracebacks generated by Rich.\n" + "Useful for development.", + ) args = parser.parse_args(args) diff --git a/redbot/core/events.py b/redbot/core/events.py index a465f48c4..00d6434f3 100644 --- a/redbot/core/events.py +++ b/redbot/core/events.py @@ -10,7 +10,6 @@ from datetime import datetime, timedelta import aiohttp import discord import pkg_resources -from colorama import Fore, Style, init from pkg_resources import DistributionNotFound from redbot.core import data_manager @@ -33,15 +32,16 @@ from .utils._internal_utils import ( ) from .utils.chat_formatting import inline, bordered, format_perms_list, humanize_timedelta +import rich +from rich import box from rich.table import Table from rich.columns import Columns from rich.panel import Panel from rich.text import Text log = logging.getLogger("red") -init() -INTRO = r"""[red] +INTRO = r""" ______ _ ______ _ _ ______ _ | ___ \ | | | _ (_) | | | ___ \ | | | |_/ /___ __| | ______ | | | |_ ___ ___ ___ _ __ __| | | |_/ / ___ | |_ @@ -88,14 +88,14 @@ def init_events(bot, cli_flags): red_pkg = pkg_resources.get_distribution("Red-DiscordBot") dpy_version = discord.__version__ - table_general_info = Table(show_edge=False, show_header=False) + table_general_info = Table(show_edge=False, show_header=False, box=box.MINIMAL) table_general_info.add_row("Prefixes", ", ".join(prefixes)) table_general_info.add_row("Language", lang) table_general_info.add_row("Red version", red_version) table_general_info.add_row("Discord.py version", dpy_version) table_general_info.add_row("Storage type", data_manager.storage_type()) - table_counts = Table(show_edge=False, show_header=False) + table_counts = Table(show_edge=False, show_header=False, box=box.MINIMAL) # String conversion is needed as Rich doesn't deal with ints table_counts.add_row("Shards", str(bot.shard_count)) table_counts.add_row("Servers", str(guilds)) @@ -114,8 +114,8 @@ def init_events(bot, cli_flags): ).format(pypi_version, red_version) rich_outdated_message = ( f"[red]Outdated version![/red]\n" - f"[red]!!![/red]Version {pypi_version} is available, " - f"but you're using {red_version}[red]!!![/red]" + f"[red]!!![/red]Version [cyan]{pypi_version}[/] is available, " + f"but you're using [cyan]{red_version}[/][red]!!![/red]" ) current_python = platform.python_version() extra_update = _( @@ -167,9 +167,10 @@ def init_events(bot, cli_flags): ).format(py_version=current_python, req_py=py_version_req) outdated_red_message += extra_update - bot._rich_console.print(INTRO) + rich_console = rich.get_console() + rich_console.print(INTRO, style="red", markup=False, highlight=False) if guilds: - bot._rich_console.print( + rich_console.print( Columns( [Panel(table_general_info, title=str(bot.user.name)), Panel(table_counts)], equal=True, @@ -177,23 +178,21 @@ def init_events(bot, cli_flags): ) ) else: - bot._rich_console.print(Columns([Panel(table_general_info, title=str(bot.user.name))])) + rich_console.print(Columns([Panel(table_general_info, title=str(bot.user.name))])) - bot._rich_console.print( + rich_console.print( "Loaded {} cogs with {} commands".format(len(bot.cogs), len(bot.commands)) ) if invite_url: - bot._rich_console.print( - f"\nInvite URL: {Text(invite_url, style=f'link {invite_url}')}" - ) + rich_console.print(f"\nInvite URL: {Text(invite_url, style=f'link {invite_url}')}") # We generally shouldn't care if the client supports it or not as Rich deals with it. if not guilds: - bot._rich_console.print( + rich_console.print( f"Looking for a quick guide on setting up Red? Checkout {Text('https://start.discord.red', style='link https://start.discord.red}')}" ) if rich_outdated_message: - bot._rich_console.print(rich_outdated_message) + rich_console.print(rich_outdated_message) if not bot.owner_ids: # we could possibly exit here in future @@ -404,37 +403,3 @@ def init_events(bot, cli_flags): uuid = c.unique_identifier group_data = c.custom_groups await bot._config.custom("CUSTOM_GROUPS", c.cog_name, uuid).set(group_data) - - -def _get_startup_screen_specs(): - """Get specs for displaying the startup screen on stdout. - - This is so we don't get encoding errors when trying to print unicode - emojis to stdout (particularly with Windows Command Prompt). - - Returns - ------- - `tuple` - Tuple in the form (`str`, `str`, `bool`) containing (in order) the - on symbol, off symbol and whether or not the border should be pure ascii. - - """ - encoder = codecs.getencoder(sys.stdout.encoding) - check_mark = "\N{SQUARE ROOT}" - try: - encoder(check_mark) - except UnicodeEncodeError: - on_symbol = "[X]" - off_symbol = "[ ]" - else: - on_symbol = check_mark - off_symbol = "X" - - try: - encoder("┌┐└┘─│") # border symbols - except UnicodeEncodeError: - ascii_border = True - else: - ascii_border = False - - return on_symbol, off_symbol, ascii_border diff --git a/redbot/logging.py b/redbot/logging.py index 694d91e4c..4bef0c385 100644 --- a/redbot/logging.py +++ b/redbot/logging.py @@ -1,19 +1,37 @@ +import argparse import logging.handlers import pathlib import re import sys -from typing import List, Tuple, Optional, Union +from typing import List, Tuple, Optional from logging import LogRecord from datetime import datetime # This clearly never leads to confusion... from os import isatty -from rich._log_render import LogRender +import rich +from pygments.styles.monokai import MonokaiStyle +from pygments.token import ( + Comment, + Error, + Keyword, + Name, + Number, + Operator, + String, + Token, +) +from rich._log_render import LogRender # DEP-WARN +from rich.console import render_group from rich.containers import Renderables +from rich.highlighter import NullHighlighter from rich.logging import RichHandler +from rich.style import Style +from rich.syntax import ANSISyntaxTheme, PygmentsSyntaxTheme from rich.table import Table from rich.text import Text -from rich.traceback import Traceback +from rich.theme import Theme +from rich.traceback import PathHighlighter, Traceback MAX_OLD_LOGS = 8 @@ -109,6 +127,37 @@ class RotatingFileHandler(logging.handlers.RotatingFileHandler): self.stream = self._open() +SYNTAX_THEME = { + Token: Style(), + Comment: Style(color="bright_black"), + Keyword: Style(color="cyan", bold=True), + Keyword.Constant: Style(color="bright_magenta"), + Keyword.Namespace: Style(color="bright_red"), + Operator: Style(bold=True), + Operator.Word: Style(color="cyan", bold=True), + Name.Builtin: Style(bold=True), + Name.Builtin.Pseudo: Style(color="bright_red"), + Name.Exception: Style(bold=True), + Name.Class: Style(color="bright_green"), + Name.Function: Style(color="bright_green"), + String: Style(color="yellow"), + Number: Style(color="cyan"), + Error: Style(bgcolor="red"), +} + + +class FixedMonokaiStyle(MonokaiStyle): + styles = {**MonokaiStyle.styles, Token: "#f8f8f2"} + + +class RedTraceback(Traceback): + @render_group() + def _render_stack(self, stack): + for obj in super()._render_stack.__wrapped__(self, stack): + if obj != "": + yield obj + + class RedLogRender(LogRender): def __call__( self, @@ -155,7 +204,7 @@ class RedLogRender(LogRender): if logger_name: logger_name_text = Text() - logger_name_text.append(f"[{logger_name}]") + logger_name_text.append(f"[{logger_name}]", style="bright_black") row.append(logger_name_text) output.add_row(*row) @@ -174,6 +223,19 @@ class RedRichHandler(RichHandler): level_width=self._log_render.level_width, ) + def get_level_text(self, record: LogRecord) -> Text: + """Get the level name from the record. + + Args: + record (LogRecord): LogRecord instance. + + Returns: + Text: A tuple of the style and level name. + """ + level_text = super().get_level_text(record) + level_text.stylize("bold") + return level_text + def emit(self, record: LogRecord) -> None: """Invoked by logging.""" path = pathlib.Path(record.pathname).name @@ -187,7 +249,7 @@ class RedRichHandler(RichHandler): exc_type, exc_value, exc_traceback = record.exc_info assert exc_type is not None assert exc_value is not None - traceback = Traceback.from_exception( + traceback = RedTraceback.from_exception( exc_type, exc_value, exc_traceback, @@ -198,6 +260,7 @@ class RedRichHandler(RichHandler): show_locals=self.tracebacks_show_locals, locals_max_length=self.locals_max_length, locals_max_string=self.locals_max_string, + indent_guides=False, ) message = record.getMessage() @@ -215,7 +278,7 @@ class RedRichHandler(RichHandler): self.console.print( self._log_render( self.console, - [message_text] if not traceback else [message_text, traceback], + [message_text], log_time=log_time, time_format=time_format, level=level, @@ -225,11 +288,11 @@ class RedRichHandler(RichHandler): logger_name=record.name, ) ) + if traceback: + self.console.print(traceback) -def init_logging( - level: int, location: pathlib.Path, force_rich_logging: Union[bool, None] -) -> None: +def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespace) -> None: root_logger = logging.getLogger() base_logger = logging.getLogger("red") @@ -239,12 +302,31 @@ def init_logging( warnings_logger = logging.getLogger("py.warnings") warnings_logger.setLevel(logging.WARNING) + rich_console = rich.get_console() + rich.reconfigure(tab_size=4) + rich_console.push_theme( + Theme( + { + "log.time": Style(dim=True), + "logging.level.warning": Style(color="yellow"), + "logging.level.critical": Style(color="white", bgcolor="red"), + "repr.number": Style(color="cyan"), + "repr.url": Style(underline=True, italic=True, bold=False, color="cyan"), + } + ) + ) + rich_console.file = sys.stdout + # This is terrible solution, but it's the best we can do if we want the paths in tracebacks + # to be visible. Rich uses `pygments.string` style which is fine, but it also uses + # this highlighter which dims most of the path and therefore makes it unreadable on Mac. + PathHighlighter.highlights = [] + enable_rich_logging = False - if isatty(0) and force_rich_logging is None: + if isatty(0) and cli_flags.rich_logging is None: # Check if the bot thinks it has a active terminal. enable_rich_logging = True - elif force_rich_logging is True: + elif cli_flags.rich_logging is True: enable_rich_logging = True file_formatter = logging.Formatter( @@ -253,7 +335,18 @@ def init_logging( if enable_rich_logging is True: rich_formatter = logging.Formatter("{message}", datefmt="[%X]", style="{") - stdout_handler = RedRichHandler(rich_tracebacks=True, show_path=False) + stdout_handler = RedRichHandler( + rich_tracebacks=True, + show_path=False, + highlighter=NullHighlighter(), + tracebacks_extra_lines=cli_flags.rich_traceback_extra_lines, + tracebacks_show_locals=cli_flags.rich_traceback_show_locals, + tracebacks_theme=( + PygmentsSyntaxTheme(FixedMonokaiStyle) + if rich_console.color_system == "truecolor" + else ANSISyntaxTheme(SYNTAX_THEME) + ), + ) stdout_handler.setFormatter(rich_formatter) else: stdout_handler = logging.StreamHandler(sys.stdout) diff --git a/setup.cfg b/setup.cfg index da75047e9..21648a140 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,7 +57,7 @@ install_requires = pytz==2021.1 PyYAML==5.4.1 Red-Lavalink==0.7.2 - rich==9.5.1 + rich==9.9.0 schema==0.7.4 six==1.15.0 tqdm==4.56.2