mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-09-08 19:03:03 +00:00
Add default logging configuration when using Client.run
While it is possible to do this type of your set up yourself, it's better for beginners to have logging automatically set up for them. This has come up often in the help channel over the years. This also provides an escape hatch to disable it.
This commit is contained in:
@ -28,6 +28,7 @@ import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
from typing import (
|
||||
Any,
|
||||
@ -113,6 +114,61 @@ class _LoopSentinel:
|
||||
_loop: Any = _LoopSentinel()
|
||||
|
||||
|
||||
def stream_supports_colour(stream: Any) -> bool:
|
||||
is_a_tty = hasattr(stream, 'isatty') and stream.isatty()
|
||||
if sys.platform != 'win32':
|
||||
return is_a_tty
|
||||
|
||||
# ANSICON checks for things like ConEmu
|
||||
# WT_SESSION checks if this is Windows Terminal
|
||||
# VSCode built-in terminal supports colour too
|
||||
return is_a_tty and ('ANSICON' in os.environ or 'WT_SESSION' in os.environ or os.environ.get('TERM_PROGRAM') == 'vscode')
|
||||
|
||||
|
||||
class _ColourFormatter(logging.Formatter):
|
||||
|
||||
# ANSI codes are a bit weird to decipher if you're unfamiliar with them, so here's a refresher
|
||||
# It starts off with a format like \x1b[XXXm where XXX is a semicolon separated list of commands
|
||||
# The important ones here relate to colour.
|
||||
# 30-37 are black, red, green, yellow, blue, magenta, cyan and white in that order
|
||||
# 40-47 are the same except for the background
|
||||
# 90-97 are the same but "bright" foreground
|
||||
# 100-107 are the same as the bright ones but for the background.
|
||||
# 1 means bold, 2 means dim, 0 means reset, and 4 means underline.
|
||||
|
||||
LEVEL_COLOURS = [
|
||||
(logging.DEBUG, '\x1b[40;1m'),
|
||||
(logging.INFO, '\x1b[34;1m'),
|
||||
(logging.WARNING, '\x1b[33;1m'),
|
||||
(logging.ERROR, '\x1b[31m'),
|
||||
(logging.CRITICAL, '\x1b[41m'),
|
||||
]
|
||||
|
||||
FORMATS = {
|
||||
level: logging.Formatter(
|
||||
f'\x1b[30;1m%(asctime)s\x1b[0m {colour}%(levelname)-8s\x1b[0m \x1b[35m%(name)s\x1b[0m %(message)s',
|
||||
'%Y-%m-%d %H:%M:%S',
|
||||
)
|
||||
for level, colour in LEVEL_COLOURS
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
formatter = self.FORMATS.get(record.levelno)
|
||||
if formatter is None:
|
||||
formatter = self.FORMATS[logging.DEBUG]
|
||||
|
||||
# Override the traceback to always print in red
|
||||
if record.exc_info:
|
||||
text = formatter.formatException(record.exc_info)
|
||||
record.exc_text = f'\x1b[31m{text}\x1b[0m'
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
# Remove the cache layer
|
||||
record.exc_text = None
|
||||
return output
|
||||
|
||||
|
||||
class Client:
|
||||
r"""Represents a client connection that connects to Discord.
|
||||
This class is used to interact with the Discord WebSocket and API.
|
||||
@ -706,6 +762,17 @@ class Client:
|
||||
|
||||
A shorthand coroutine for :meth:`login` + :meth:`connect`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
token: :class:`str`
|
||||
The authentication token. Do not prefix this token with
|
||||
anything as the library will do it for you.
|
||||
reconnect: :class:`bool`
|
||||
If we should attempt reconnecting, either due to internet
|
||||
failure or a specific failure on Discord's part. Certain
|
||||
disconnects that lead to bad state will not be handled (such as
|
||||
invalid sharding payloads or bad tokens).
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
@ -714,7 +781,15 @@ class Client:
|
||||
await self.login(token)
|
||||
await self.connect(reconnect=reconnect)
|
||||
|
||||
def run(self, *args: Any, **kwargs: Any) -> None:
|
||||
def run(
|
||||
self,
|
||||
token: str,
|
||||
*,
|
||||
reconnect: bool = True,
|
||||
log_handler: Optional[logging.Handler] = MISSING,
|
||||
log_formatter: logging.Formatter = MISSING,
|
||||
log_level: int = MISSING,
|
||||
) -> None:
|
||||
"""A blocking call that abstracts away the event loop
|
||||
initialisation from you.
|
||||
|
||||
@ -722,23 +797,77 @@ class Client:
|
||||
function should not be used. Use :meth:`start` coroutine
|
||||
or :meth:`connect` + :meth:`login`.
|
||||
|
||||
Roughly Equivalent to: ::
|
||||
|
||||
try:
|
||||
asyncio.run(self.start(*args, **kwargs))
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
This function also sets up the logging library to make it easier
|
||||
for beginners to know what is going on with the library. For more
|
||||
advanced users, this can be disabled by passing ``None`` to
|
||||
the ``log_handler`` parameter.
|
||||
|
||||
.. warning::
|
||||
|
||||
This function must be the last function to call due to the fact that it
|
||||
is blocking. That means that registration of events or anything being
|
||||
called after this function call will not execute until it returns.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
token: :class:`str`
|
||||
The authentication token. Do not prefix this token with
|
||||
anything as the library will do it for you.
|
||||
reconnect: :class:`bool`
|
||||
If we should attempt reconnecting, either due to internet
|
||||
failure or a specific failure on Discord's part. Certain
|
||||
disconnects that lead to bad state will not be handled (such as
|
||||
invalid sharding payloads or bad tokens).
|
||||
log_handler: Optional[:class:`logging.Handler`]
|
||||
The log handler to use for the library's logger. If this is ``None``
|
||||
then the library will not set up anything logging related. Logging
|
||||
will still work if ``None`` is passed, though it is your responsibility
|
||||
to set it up.
|
||||
|
||||
The default log handler if not provided is :class:`logging.StreamHandler`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
log_formatter: :class:`logging.Formatter`
|
||||
The formatter to use with the given log handler. If not provided then it
|
||||
defaults to a colour based logging formatter (if available).
|
||||
|
||||
.. versionadded:: 2.0
|
||||
log_level: :class:`int`
|
||||
The default log level for the library's logger. This is only applied if the
|
||||
``log_handler`` parameter is not ``None``. Defaults to ``logging.INFO``.
|
||||
|
||||
Note that the *root* logger will always be set to ``logging.INFO`` and this
|
||||
only controls the library's log level. To control the root logger's level,
|
||||
you can use ``logging.getLogger().setLevel(level)``.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
async def runner():
|
||||
async with self:
|
||||
await self.start(*args, **kwargs)
|
||||
await self.start(token, reconnect=reconnect)
|
||||
|
||||
if log_level is MISSING:
|
||||
log_level = logging.INFO
|
||||
|
||||
if log_handler is MISSING:
|
||||
log_handler = logging.StreamHandler()
|
||||
|
||||
if log_formatter is MISSING:
|
||||
if isinstance(log_handler, logging.StreamHandler) and stream_supports_colour(log_handler.stream):
|
||||
log_formatter = _ColourFormatter()
|
||||
else:
|
||||
dt_fmt = '%Y-%m-%d %H:%M:%S'
|
||||
log_formatter = logging.Formatter('[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{')
|
||||
|
||||
logger = None
|
||||
if log_handler is not None:
|
||||
library, _, _ = __name__.partition('.')
|
||||
logger = logging.getLogger(library)
|
||||
|
||||
log_handler.setFormatter(log_formatter)
|
||||
logger.setLevel(log_level)
|
||||
logger.addHandler(log_handler)
|
||||
|
||||
try:
|
||||
asyncio.run(runner())
|
||||
@ -747,6 +876,9 @@ class Client:
|
||||
# `asyncio.run` handles the loop cleanup
|
||||
# and `self.start` closes all sockets and the HTTPClient instance.
|
||||
return
|
||||
finally:
|
||||
if log_handler is not None and logger is not None:
|
||||
logger.removeHandler(log_handler)
|
||||
|
||||
# properties
|
||||
|
||||
|
Reference in New Issue
Block a user