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(
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====")

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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