Various improvements in usage of Rich in Red (#4726)

* Use Rich's default console object instead of making one

* Bump Rich to version 9.8.2

* Disable indent guides in tracebacks

* Skip empty lines between stack levels in traceback rendering

* Use full width of the terminal when printing tracebacks

* Disabling syntax highlighting on the log messages

* Make logger name bold

* Make logger level bold

* Make URLs in console bold

* Change `bright_blue` and `blue` colors in syntax theme (NEEDS CHANGING)

* Show only one line per stack level in tracebacks

* Shuffle things in `redbot.logging` a bit

* Change logging handler while redirecting stdout in Dev cog

* Revert last two commits...

This reverts commit cf563bd06a6ae398da12713ceef3db9cc903d902.
This reverts commit 6dddf300726ddf89b8924441eed59b67b58faca0.

* Change Rich console to always print to sys.stdout (therefore ignoring stdout redirects)

* Pass cli_flags to init_logging()

* Add a flag to set the amount of extra lines in rich tracebacks

* First take on the syntax theme colors

* Use the Windows trick

* ARE YOU SERIOUS!?

* Remove dead code

* Use Monokai when Terminal application supports truecolor

* Syntax theme update

* Change logger name color

* This is not needed

* Adjust logging level colors

* Add a flag for showing local variables in Rich tracebacks

* change imports a bit

* Remove usage of blue color fully

* Stop highlighting in Red-DiscordBot splash

* Fix unreadable paths in tracebacks

* Make CRITICAL logging level more readable

* Make time in logs more readable

* Fix the first row being bolded in tables

* Update rich to 9.9.0
This commit is contained in:
jack1142 2021-02-11 22:20:10 +01:00 committed by GitHub
parent 663876fba3
commit 7df1570d51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 136 additions and 71 deletions

View File

@ -337,7 +337,7 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
redbot.logging.init_logging( redbot.logging.init_logging(
level=cli_flags.logging_level, level=cli_flags.logging_level,
location=data_manager.core_data_path() / "logs", location=data_manager.core_data_path() / "logs",
force_rich_logging=cli_flags.rich_logging, cli_flags=cli_flags,
) )
log.debug("====Basic Config====") log.debug("====Basic Config====")

View File

@ -1,4 +1,3 @@
import colorama as _colorama
import discord as _discord import discord as _discord
from .. import __version__, version_info, VersionInfo 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"] __all__ = ["Config", "__version__", "version_info", "VersionInfo"]
_colorama.init()
# Prevent discord PyNaCl missing warning # Prevent discord PyNaCl missing warning
_discord.voice_client.VoiceClient.warn_nacl = False _discord.voice_client.VoiceClient.warn_nacl = False

View File

@ -29,7 +29,6 @@ from typing import (
overload, overload,
) )
from types import MappingProxyType from types import MappingProxyType
from rich.console import Console
import discord import discord
from discord.ext import commands as dpy_commands from discord.ext import commands as dpy_commands
@ -224,9 +223,6 @@ class RedBase(
self._deletion_requests: MutableMapping[int, asyncio.Lock] = weakref.WeakValueDictionary() 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): def set_help_formatter(self, formatter: commands.help.HelpFormatterABC):
""" """
Set's Red's help formatter. Set's Red's help formatter.

View File

@ -263,6 +263,20 @@ def parse_cli_flags(args):
default=None, default=None,
help="Forcefully disables the Rich logging handlers.", 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) args = parser.parse_args(args)

View File

@ -10,7 +10,6 @@ from datetime import datetime, timedelta
import aiohttp import aiohttp
import discord import discord
import pkg_resources import pkg_resources
from colorama import Fore, Style, init
from pkg_resources import DistributionNotFound from pkg_resources import DistributionNotFound
from redbot.core import data_manager 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 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.table import Table
from rich.columns import Columns from rich.columns import Columns
from rich.panel import Panel from rich.panel import Panel
from rich.text import Text from rich.text import Text
log = logging.getLogger("red") 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") red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
dpy_version = discord.__version__ 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("Prefixes", ", ".join(prefixes))
table_general_info.add_row("Language", lang) table_general_info.add_row("Language", lang)
table_general_info.add_row("Red version", red_version) table_general_info.add_row("Red version", red_version)
table_general_info.add_row("Discord.py version", dpy_version) table_general_info.add_row("Discord.py version", dpy_version)
table_general_info.add_row("Storage type", data_manager.storage_type()) 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 # String conversion is needed as Rich doesn't deal with ints
table_counts.add_row("Shards", str(bot.shard_count)) table_counts.add_row("Shards", str(bot.shard_count))
table_counts.add_row("Servers", str(guilds)) table_counts.add_row("Servers", str(guilds))
@ -114,8 +114,8 @@ def init_events(bot, cli_flags):
).format(pypi_version, red_version) ).format(pypi_version, red_version)
rich_outdated_message = ( rich_outdated_message = (
f"[red]Outdated version![/red]\n" f"[red]Outdated version![/red]\n"
f"[red]!!![/red]Version {pypi_version} is available, " f"[red]!!![/red]Version [cyan]{pypi_version}[/] is available, "
f"but you're using {red_version}[red]!!![/red]" f"but you're using [cyan]{red_version}[/][red]!!![/red]"
) )
current_python = platform.python_version() current_python = platform.python_version()
extra_update = _( extra_update = _(
@ -167,9 +167,10 @@ def init_events(bot, cli_flags):
).format(py_version=current_python, req_py=py_version_req) ).format(py_version=current_python, req_py=py_version_req)
outdated_red_message += extra_update 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: if guilds:
bot._rich_console.print( rich_console.print(
Columns( Columns(
[Panel(table_general_info, title=str(bot.user.name)), Panel(table_counts)], [Panel(table_general_info, title=str(bot.user.name)), Panel(table_counts)],
equal=True, equal=True,
@ -177,23 +178,21 @@ def init_events(bot, cli_flags):
) )
) )
else: 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)) "Loaded {} cogs with {} commands".format(len(bot.cogs), len(bot.commands))
) )
if invite_url: if invite_url:
bot._rich_console.print( rich_console.print(f"\nInvite URL: {Text(invite_url, style=f'link {invite_url}')}")
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. # We generally shouldn't care if the client supports it or not as Rich deals with it.
if not guilds: 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}')}" 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: if rich_outdated_message:
bot._rich_console.print(rich_outdated_message) rich_console.print(rich_outdated_message)
if not bot.owner_ids: if not bot.owner_ids:
# we could possibly exit here in future # we could possibly exit here in future
@ -404,37 +403,3 @@ def init_events(bot, cli_flags):
uuid = c.unique_identifier uuid = c.unique_identifier
group_data = c.custom_groups group_data = c.custom_groups
await bot._config.custom("CUSTOM_GROUPS", c.cog_name, uuid).set(group_data) 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

View File

@ -1,19 +1,37 @@
import argparse
import logging.handlers import logging.handlers
import pathlib import pathlib
import re import re
import sys import sys
from typing import List, Tuple, Optional, Union from typing import List, Tuple, Optional
from logging import LogRecord from logging import LogRecord
from datetime import datetime # This clearly never leads to confusion... from datetime import datetime # This clearly never leads to confusion...
from os import isatty 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.containers import Renderables
from rich.highlighter import NullHighlighter
from rich.logging import RichHandler from rich.logging import RichHandler
from rich.style import Style
from rich.syntax import ANSISyntaxTheme, PygmentsSyntaxTheme
from rich.table import Table from rich.table import Table
from rich.text import Text 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 MAX_OLD_LOGS = 8
@ -109,6 +127,37 @@ class RotatingFileHandler(logging.handlers.RotatingFileHandler):
self.stream = self._open() 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): class RedLogRender(LogRender):
def __call__( def __call__(
self, self,
@ -155,7 +204,7 @@ class RedLogRender(LogRender):
if logger_name: if logger_name:
logger_name_text = Text() 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) row.append(logger_name_text)
output.add_row(*row) output.add_row(*row)
@ -174,6 +223,19 @@ class RedRichHandler(RichHandler):
level_width=self._log_render.level_width, 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: def emit(self, record: LogRecord) -> None:
"""Invoked by logging.""" """Invoked by logging."""
path = pathlib.Path(record.pathname).name path = pathlib.Path(record.pathname).name
@ -187,7 +249,7 @@ class RedRichHandler(RichHandler):
exc_type, exc_value, exc_traceback = record.exc_info exc_type, exc_value, exc_traceback = record.exc_info
assert exc_type is not None assert exc_type is not None
assert exc_value is not None assert exc_value is not None
traceback = Traceback.from_exception( traceback = RedTraceback.from_exception(
exc_type, exc_type,
exc_value, exc_value,
exc_traceback, exc_traceback,
@ -198,6 +260,7 @@ class RedRichHandler(RichHandler):
show_locals=self.tracebacks_show_locals, show_locals=self.tracebacks_show_locals,
locals_max_length=self.locals_max_length, locals_max_length=self.locals_max_length,
locals_max_string=self.locals_max_string, locals_max_string=self.locals_max_string,
indent_guides=False,
) )
message = record.getMessage() message = record.getMessage()
@ -215,7 +278,7 @@ class RedRichHandler(RichHandler):
self.console.print( self.console.print(
self._log_render( self._log_render(
self.console, self.console,
[message_text] if not traceback else [message_text, traceback], [message_text],
log_time=log_time, log_time=log_time,
time_format=time_format, time_format=time_format,
level=level, level=level,
@ -225,11 +288,11 @@ class RedRichHandler(RichHandler):
logger_name=record.name, logger_name=record.name,
) )
) )
if traceback:
self.console.print(traceback)
def init_logging( def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespace) -> None:
level: int, location: pathlib.Path, force_rich_logging: Union[bool, None]
) -> None:
root_logger = logging.getLogger() root_logger = logging.getLogger()
base_logger = logging.getLogger("red") base_logger = logging.getLogger("red")
@ -239,12 +302,31 @@ def init_logging(
warnings_logger = logging.getLogger("py.warnings") warnings_logger = logging.getLogger("py.warnings")
warnings_logger.setLevel(logging.WARNING) 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 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. # Check if the bot thinks it has a active terminal.
enable_rich_logging = True enable_rich_logging = True
elif force_rich_logging is True: elif cli_flags.rich_logging is True:
enable_rich_logging = True enable_rich_logging = True
file_formatter = logging.Formatter( file_formatter = logging.Formatter(
@ -253,7 +335,18 @@ def init_logging(
if enable_rich_logging is True: if enable_rich_logging is True:
rich_formatter = logging.Formatter("{message}", datefmt="[%X]", style="{") 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) stdout_handler.setFormatter(rich_formatter)
else: else:
stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler = logging.StreamHandler(sys.stdout)

View File

@ -57,7 +57,7 @@ install_requires =
pytz==2021.1 pytz==2021.1
PyYAML==5.4.1 PyYAML==5.4.1
Red-Lavalink==0.7.2 Red-Lavalink==0.7.2
rich==9.5.1 rich==9.9.0
schema==0.7.4 schema==0.7.4
six==1.15.0 six==1.15.0
tqdm==4.56.2 tqdm==4.56.2