🎨 改进 logger

This commit is contained in:
Karako 2022-10-23 17:15:09 +08:00 committed by GitHub
parent 8a4147a4ff
commit 28a3c69892
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 283 additions and 116 deletions

View File

@ -32,6 +32,7 @@ ADMINS=[{ "username": "", "user_id": -1 }]
# VERIFY_GROUPS=[] # VERIFY_GROUPS=[]
# logger 配置 可选配置项 # logger 配置 可选配置项
LOGGER_NAME="TGPaimon"
# 打印时的宽度 # 打印时的宽度
LOGGER_WIDTH=180 LOGGER_WIDTH=180
# log 文件存放目录 # log 文件存放目录
@ -45,6 +46,9 @@ LOGGER_TRACEBACK_MAX_FRAMES=20
LOGGER_LOCALS_MAX_DEPTH=0 LOGGER_LOCALS_MAX_DEPTH=0
LOGGER_LOCALS_MAX_LENGTH=10 LOGGER_LOCALS_MAX_LENGTH=10
LOGGER_LOCALS_MAX_STRING=80 LOGGER_LOCALS_MAX_STRING=80
# 可被 logger 打印的 record 的名称(默认包含了 LOGGER_NAME
LOGGER_FILTERED_NAMES=["uvicorn"]
# 超时配置 可选配置项 # 超时配置 可选配置项
# TIMEOUT = 10 # TIMEOUT = 10

View File

@ -48,6 +48,7 @@ class BotConfig(BaseSettings):
verify_groups: List[Union[int, str]] = [] verify_groups: List[Union[int, str]] = []
join_groups: Optional[JoinGroups] = JoinGroups.NO_ALLOW join_groups: Optional[JoinGroups] = JoinGroups.NO_ALLOW
logger_name: str = "TGPaimon"
logger_width: int = 180 logger_width: int = 180
logger_log_path: str = "./logs" logger_log_path: str = "./logs"
logger_time_format: str = "[%Y-%m-%d %X]" logger_time_format: str = "[%Y-%m-%d %X]"
@ -56,6 +57,7 @@ class BotConfig(BaseSettings):
logger_locals_max_depth: Optional[int] = 0 logger_locals_max_depth: Optional[int] = 0
logger_locals_max_length: int = 10 logger_locals_max_length: int = 10
logger_locals_max_string: int = 80 logger_locals_max_string: int = 80
logger_filtered_names: List[str] = ["uvicorn"]
timeout: int = 10 timeout: int = 10
read_timeout: float = 2 read_timeout: float = 2
@ -104,6 +106,7 @@ class BotConfig(BaseSettings):
@property @property
def logger(self) -> "LoggerConfig": def logger(self) -> "LoggerConfig":
return LoggerConfig( return LoggerConfig(
name=self.logger_name,
width=self.logger_width, width=self.logger_width,
traceback_max_frames=self.logger_traceback_max_frames, traceback_max_frames=self.logger_traceback_max_frames,
path=PROJECT_ROOT.joinpath(self.logger_log_path).resolve(), path=PROJECT_ROOT.joinpath(self.logger_log_path).resolve(),
@ -112,6 +115,7 @@ class BotConfig(BaseSettings):
locals_max_length=self.logger_locals_max_length, locals_max_length=self.logger_locals_max_length,
locals_max_string=self.logger_locals_max_string, locals_max_string=self.logger_locals_max_string,
locals_max_depth=self.logger_locals_max_depth, locals_max_depth=self.logger_locals_max_depth,
filtered_names=self.logger_filtered_names,
) )
@property @property
@ -154,6 +158,7 @@ class RedisConfig(BaseModel):
class LoggerConfig(BaseModel): class LoggerConfig(BaseModel):
name: str = "TGPaimon"
width: int = 180 width: int = 180
time_format: str = "[%Y-%m-%d %X]" time_format: str = "[%Y-%m-%d %X]"
traceback_max_frames: int = 20 traceback_max_frames: int = 20
@ -162,6 +167,7 @@ class LoggerConfig(BaseModel):
locals_max_length: int = 10 locals_max_length: int = 10
locals_max_string: int = 80 locals_max_string: int = 80
locals_max_depth: Optional[int] = None locals_max_depth: Optional[int] = None
filtered_names: List[str] = ["uvicorn"]
@validator("locals_max_depth", pre=True, check_fields=False) @validator("locals_max_depth", pre=True, check_fields=False)
def locals_max_depth_validator(cls, value) -> Optional[int]: # pylint: disable=R0201 def locals_max_depth_validator(cls, value) -> Optional[int]: # pylint: disable=R0201

View File

@ -1 +1,44 @@
from utils.log._logger import * import re
from functools import lru_cache
from typing import TYPE_CHECKING
from core.config import config
from utils.log._config import LoggerConfig
from utils.log._logger import LogFilter, Logger
if TYPE_CHECKING:
from logging import LogRecord
__all__ = ["logger"]
logger = Logger(
LoggerConfig(
name=config.logger.name,
width=config.logger.width,
time_format=config.logger.time_format,
traceback_max_frames=config.logger.traceback_max_frames,
log_path=config.logger.path,
keywords=config.logger.render_keywords,
traceback_locals_max_depth=config.logger.locals_max_depth,
traceback_locals_max_length=config.logger.locals_max_length,
traceback_locals_max_string=config.logger_locals_max_string,
)
)
@lru_cache
def _name_filter(record_name: str) -> bool:
for name in config.logger.filtered_names + [config.logger.name]:
if re.match(rf"^{name}.*?$", record_name):
return True
return False
def name_filter(record: "LogRecord") -> bool:
"""默认的过滤器"""
return _name_filter(record.name)
log_filter = LogFilter()
log_filter.add_filter(name_filter)
logger.addFilter(log_filter)

44
utils/log/_config.py Normal file
View File

@ -0,0 +1,44 @@
from multiprocessing import RLock as Lock
from pathlib import Path
from typing import (
List,
Optional,
Union,
)
from pydantic import BaseSettings
from utils.const import PROJECT_ROOT
__all__ = ["LoggerConfig"]
class LoggerConfig(BaseSettings):
_lock = Lock()
_instance: Optional["LoggerConfig"] = None
def __new__(cls, *args, **kwargs) -> "LoggerConfig":
with cls._lock:
if cls._instance is None:
cls.update_forward_refs()
result = super(LoggerConfig, cls).__new__(cls)
result.__init__(*args, **kwargs)
cls._instance = result
return cls._instance
name: str = "logger"
level: Optional[Union[str, int]] = None
debug: bool = False
width: int = 180
keywords: List[str] = []
time_format: str = "[%Y-%m-%d %X]"
capture_warnings: bool = True
log_path: Union[str, Path] = "./logs"
project_root: Union[str, Path] = PROJECT_ROOT
traceback_max_frames: int = 20
traceback_locals_max_depth: Optional[int] = None
traceback_locals_max_length: int = 10
traceback_locals_max_string: int = 80

View File

@ -18,7 +18,7 @@ class FileIO(IO[str]):
today = date.today() today = date.today()
if self.file.exists(): if self.file.exists():
if not self.file.is_file(): if not self.file.is_file():
raise RuntimeError(f'日志文件冲突, 请删除文件夹 "{str(self.file.resolve())}"') raise FileExistsError(f'Log file conflict, please delete the folder "{str(self.file.resolve())}"')
if self.file_stream is None or self.file_stream.closed: if self.file_stream is None or self.file_stream.closed:
self.file_stream = self.file.open(mode="a+", encoding="utf-8") self.file_stream = self.file.open(mode="a+", encoding="utf-8")
modify_date = date.fromtimestamp(os.stat(self.file).st_mtime) modify_date = date.fromtimestamp(os.stat(self.file).st_mtime)

View File

@ -3,12 +3,18 @@ import os
import sys import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Iterable, List, Literal, Optional, TYPE_CHECKING, Union from typing import (
Any,
import ujson as json Callable,
from rich.console import ( Iterable,
Console, List,
Literal,
Optional,
TYPE_CHECKING,
Union,
) )
from rich.console import Console
from rich.logging import ( from rich.logging import (
LogRender as DefaultLogRender, LogRender as DefaultLogRender,
RichHandler as DefaultRichHandler, RichHandler as DefaultRichHandler,
@ -19,10 +25,7 @@ from rich.text import (
TextType, TextType,
) )
from rich.theme import Theme from rich.theme import Theme
from ujson import JSONDecodeError
from core.config import config
from utils.const import PROJECT_ROOT
from utils.log._file import FileIO from utils.log._file import FileIO
from utils.log._style import DEFAULT_STYLE from utils.log._style import DEFAULT_STYLE
from utils.log._traceback import Traceback from utils.log._traceback import Traceback
@ -45,24 +48,18 @@ if sys.platform == "win32":
color_system = "windows" color_system = "windows"
else: else:
color_system = "truecolor" color_system = "truecolor"
# noinspection SpellCheckingInspection
log_console = Console(color_system=color_system, theme=Theme(DEFAULT_STYLE), width=config.logger.width)
class LogRender(DefaultLogRender): class LogRender(DefaultLogRender):
@property @property
def last_time(self): def last_time(self):
"""上次打印的时间"""
return self._last_time return self._last_time
@last_time.setter @last_time.setter
def last_time(self, last_time): def last_time(self, last_time):
self._last_time = last_time self._last_time = last_time
def __init__(self, *args, **kwargs):
super(LogRender, self).__init__(*args, **kwargs)
self.show_level = True
self.time_format = config.logger.time_format
def __call__( def __call__(
self, self,
console: "Console", console: "Console",
@ -87,7 +84,7 @@ class LogRender(DefaultLogRender):
output.add_column(style="log.line_no", width=4) output.add_column(style="log.line_no", width=4)
row: List["RenderableType"] = [] row: List["RenderableType"] = []
if self.show_time: if self.show_time:
log_time = log_time or log_console.get_datetime() log_time = log_time or console.get_datetime()
time_format = time_format or self.time_format time_format = time_format or self.time_format
if callable(time_format): if callable(time_format):
log_time_display = time_format(log_time) log_time_display = time_format(log_time)
@ -108,7 +105,10 @@ class LogRender(DefaultLogRender):
row.append(path_text) row.append(path_text)
line_no_text = Text() line_no_text = Text()
line_no_text.append(str(line_no), style=f"link file://{link_path}#{line_no}" if link_path else "") line_no_text.append(
str(line_no),
style=f"link file://{link_path}#{line_no}" if link_path else "",
)
row.append(line_no_text) row.append(line_no_text)
output.add_row(*row) output.add_row(*row)
@ -119,16 +119,23 @@ class Handler(DefaultRichHandler):
def __init__( def __init__(
self, self,
*args, *args,
width: int = None,
rich_tracebacks: bool = True, rich_tracebacks: bool = True,
locals_max_depth: Optional[int] = config.logger.locals_max_depth, locals_max_depth: Optional[int] = None,
tracebacks_max_frames: int = 100,
keywords: Optional[List[str]] = None,
log_time_format: Union[str, FormatTimeCallable] = "[%x %X]",
project_root: Union[str, Path] = "./logs",
**kwargs, **kwargs,
) -> None: ) -> None:
super(Handler, self).__init__(*args, rich_tracebacks=rich_tracebacks, **kwargs) super(Handler, self).__init__(*args, rich_tracebacks=rich_tracebacks, **kwargs)
self._log_render = LogRender() self._log_render = LogRender(time_format=log_time_format, show_level=True)
self.console = log_console self.console = Console(color_system=color_system, theme=Theme(DEFAULT_STYLE), width=width)
self.tracebacks_show_locals = True self.tracebacks_show_locals = True
self.keywords = self.KEYWORDS + config.logger.render_keywords self.tracebacks_max_frames = tracebacks_max_frames
self.keywords = self.KEYWORDS + (keywords or [])
self.locals_max_depth = locals_max_depth self.locals_max_depth = locals_max_depth
self.project_root = project_root
def render( def render(
self, self,
@ -139,7 +146,7 @@ class Handler(DefaultRichHandler):
) -> "ConsoleRenderable": ) -> "ConsoleRenderable":
if record.pathname != "<input>": if record.pathname != "<input>":
try: try:
path = str(Path(record.pathname).relative_to(PROJECT_ROOT)) path = str(Path(record.pathname).relative_to(self.project_root))
path = path.split(".")[0].replace(os.sep, ".") path = path.split(".")[0].replace(os.sep, ".")
except ValueError: except ValueError:
import site import site
@ -178,7 +185,11 @@ class Handler(DefaultRichHandler):
) )
return log_renderable return log_renderable
def render_message(self, record: "LogRecord", message: Any) -> "ConsoleRenderable": def render_message(
self,
record: "LogRecord",
message: Any,
) -> "ConsoleRenderable":
use_markup = getattr(record, "markup", self.markup) use_markup = getattr(record, "markup", self.markup)
if isinstance(message, str): if isinstance(message, str):
message_text = Text.from_markup(message) if use_markup else Text(message) message_text = Text.from_markup(message) if use_markup else Text(message)
@ -216,15 +227,16 @@ class Handler(DefaultRichHandler):
width=self.tracebacks_width, width=self.tracebacks_width,
extra_lines=self.tracebacks_extra_lines, extra_lines=self.tracebacks_extra_lines,
word_wrap=self.tracebacks_word_wrap, word_wrap=self.tracebacks_word_wrap,
show_locals=getattr(record, "show_locals", None) or self.tracebacks_show_locals, show_locals=(getattr(record, "show_locals", None) or self.tracebacks_show_locals),
locals_max_length=getattr(record, "locals_max_length", None) or self.locals_max_length, locals_max_length=(getattr(record, "locals_max_length", None) or self.locals_max_length),
locals_max_string=getattr(record, "locals_max_string", None) or self.locals_max_string, locals_max_string=(getattr(record, "locals_max_string", None) or self.locals_max_string),
locals_max_depth=( locals_max_depth=(
getattr(record, "locals_max_depth") getattr(record, "locals_max_depth")
if hasattr(record, "locals_max_depth") if hasattr(record, "locals_max_depth")
else self.locals_max_depth else self.locals_max_depth
), ),
suppress=self.tracebacks_suppress, suppress=self.tracebacks_suppress,
max_frames=self.tracebacks_max_frames,
) )
message = record.getMessage() message = record.getMessage()
if self.formatter: if self.formatter:
@ -237,10 +249,7 @@ class Handler(DefaultRichHandler):
message = None message = None
if message is not None: if message is not None:
try: message_renderable = self.render_message(record, message)
message_renderable = self.render_message(record, json.loads(message))
except JSONDecodeError:
message_renderable = self.render_message(record, message)
else: else:
message_renderable = None message_renderable = None
log_renderable = self.render(record=record, traceback=_traceback, message_renderable=message_renderable) log_renderable = self.render(record=record, traceback=_traceback, message_renderable=message_renderable)
@ -252,7 +261,13 @@ class Handler(DefaultRichHandler):
class FileHandler(Handler): class FileHandler(Handler):
def __init__(self, *args, path: Path, **kwargs): def __init__(
self,
*args,
width: int = None,
path: Path,
**kwargs,
) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
path.parent.mkdir(exist_ok=True, parents=True) path.parent.mkdir(exist_ok=True, parents=True)
self.console = Console(width=180, file=FileIO(path), theme=Theme(DEFAULT_STYLE)) self.console = Console(width=width, file=FileIO(path), theme=Theme(DEFAULT_STYLE))

View File

@ -5,25 +5,114 @@ import os
import traceback as traceback_ import traceback as traceback_
from multiprocessing import RLock as Lock from multiprocessing import RLock as Lock
from pathlib import Path from pathlib import Path
from typing import Any, Callable, List, Mapping, Optional, TYPE_CHECKING, Tuple from types import TracebackType
from typing import (
Any,
Callable,
List,
Mapping,
Optional,
TYPE_CHECKING,
Tuple,
Type,
Union,
)
from typing_extensions import Self from typing_extensions import Self
from core.config import config from utils.log._handler import (
from utils.const import NOT_SET FileHandler,
from utils.log._handler import FileHandler, Handler Handler,
from utils.typedefs import ExceptionInfoType )
from utils.typedefs import LogFilterType
if TYPE_CHECKING: if TYPE_CHECKING:
from utils.log._config import LoggerConfig # pylint: disable=unused-import
from logging import LogRecord # pylint: disable=unused-import from logging import LogRecord # pylint: disable=unused-import
__all__ = ["logger"] __all__ = ["Logger", "LogFilter"]
SysExcInfoType = Union[
Tuple[Type[BaseException], BaseException, Optional[TracebackType]],
Tuple[None, None, None],
]
ExceptionInfoType = Union[bool, SysExcInfoType, BaseException]
_lock = Lock() _lock = Lock()
__initialized__ = False NONE = object()
class Logger(logging.Logger): class Logger(logging.Logger):
_instance: Optional["Logger"] = None
def __new__(cls, *args, **kwargs) -> "Logger":
with _lock:
if cls._instance is None:
result = super(Logger, cls).__new__(cls)
cls._instance = result
return cls._instance
def __init__(self, config: "LoggerConfig" = None) -> None:
from utils.log._config import LoggerConfig
self.config = config or LoggerConfig()
level_ = 10 if self.config.debug else 20
super().__init__(
name=self.config.name,
level=level_ if self.config.level is None else self.config.level,
)
log_path = Path(self.config.project_root).joinpath(self.config.log_path)
handler, debug_handler, error_handler = (
# 控制台 log 配置
Handler(
width=self.config.width,
locals_max_length=self.config.traceback_locals_max_length,
locals_max_string=self.config.traceback_locals_max_string,
locals_max_depth=self.config.traceback_locals_max_depth,
project_root=self.config.project_root,
log_time_format=self.config.time_format,
),
# debug.log 配置
FileHandler(
width=self.config.width,
level=10,
path=log_path.joinpath("debug/debug.log"),
locals_max_depth=1,
locals_max_length=self.config.traceback_locals_max_length,
locals_max_string=self.config.traceback_locals_max_string,
project_root=self.config.project_root,
log_time_format=self.config.time_format,
),
# error.log 配置
FileHandler(
width=self.config.width,
level=40,
path=log_path.joinpath("error/error.log"),
locals_max_length=self.config.traceback_locals_max_length,
locals_max_string=self.config.traceback_locals_max_string,
locals_max_depth=self.config.traceback_locals_max_depth,
project_root=self.config.project_root,
log_time_format=self.config.time_format,
),
)
logging.basicConfig(
level=10 if self.config.debug else 20,
format="%(message)s",
datefmt=self.config.time_format,
handlers=[handler, debug_handler, error_handler],
)
if config.capture_warnings:
logging.captureWarnings(True)
warnings_logger = logging.getLogger("py.warnings")
warnings_logger.addHandler(handler)
warnings_logger.addHandler(debug_handler)
self.addHandler(handler)
self.addHandler(debug_handler)
self.addHandler(error_handler)
def success( def success(
self, self,
msg: Any, msg: Any,
@ -33,11 +122,19 @@ class Logger(logging.Logger):
stacklevel: int = 1, stacklevel: int = 1,
extra: Optional[Mapping[str, Any]] = None, extra: Optional[Mapping[str, Any]] = None,
) -> None: ) -> None:
return self.log(25, msg, *args, exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra) return self.log(
25,
msg,
*args,
exc_info=exc_info,
stack_info=stack_info,
stacklevel=stacklevel,
extra=extra,
)
def exception( def exception(
self, self,
msg: Any = NOT_SET, msg: Any = NONE,
*args: Any, *args: Any,
exc_info: Optional[ExceptionInfoType] = True, exc_info: Optional[ExceptionInfoType] = True,
stack_info: bool = False, stack_info: bool = False,
@ -46,7 +143,7 @@ class Logger(logging.Logger):
**kwargs, **kwargs,
) -> None: # pylint: disable=W1113 ) -> None: # pylint: disable=W1113
super(Logger, self).exception( super(Logger, self).exception(
"" if msg is NOT_SET else msg, "" if msg is NONE else msg,
*args, *args,
exc_info=exc_info, exc_info=exc_info,
stack_info=stack_info, stack_info=stack_info,
@ -87,6 +184,10 @@ class Logger(logging.Logger):
break break
return rv return rv
def addFilter(self, log_filter: LogFilterType) -> None: # pylint: disable=arguments-differ
for handler in self.handlers:
handler.addFilter(log_filter)
class LogFilter(logging.Filter): class LogFilter(logging.Filter):
_filter_list: List[Callable[["LogRecord"], bool]] = [] _filter_list: List[Callable[["LogRecord"], bool]] = []
@ -101,61 +202,3 @@ class LogFilter(logging.Filter):
def filter(self, record: "LogRecord") -> bool: def filter(self, record: "LogRecord") -> bool:
return all(map(lambda func: func(record), self._filter_list)) return all(map(lambda func: func(record), self._filter_list))
def default_filter(record: "LogRecord") -> bool:
"""默认的过滤器"""
return record.name.split(".")[0] in ["TGPaimon", "uvicorn"]
with _lock:
if not __initialized__:
if "PYCHARM_HOSTED" in os.environ:
print() # 针对 pycharm 的控制台 bug
logging.captureWarnings(True)
handler, debug_handler, error_handler = (
# 控制台 log 配置
Handler(
locals_max_length=config.logger.locals_max_length,
locals_max_string=config.logger.locals_max_string,
locals_max_depth=config.logger.locals_max_depth,
),
# debug.log 配置
FileHandler(
level=10,
path=config.logger.path.joinpath("debug/debug.log"),
locals_max_depth=1,
locals_max_length=config.logger.locals_max_length,
locals_max_string=config.logger.locals_max_string,
),
# error.log 配置
FileHandler(
level=40,
path=config.logger.path.joinpath("error/error.log"),
locals_max_length=config.logger.locals_max_length,
locals_max_string=config.logger.locals_max_string,
locals_max_depth=config.logger.locals_max_depth,
),
)
default_log_filter = LogFilter().add_filter(default_filter)
handler.addFilter(default_log_filter)
debug_handler.addFilter(default_log_filter)
level_ = 10 if config.debug else 20
logging.basicConfig(
level=10 if config.debug else 20,
format="%(message)s",
datefmt=config.logger.time_format,
handlers=[handler, debug_handler, error_handler],
)
warnings_logger = logging.getLogger("py.warnings")
warnings_logger.addHandler(handler)
warnings_logger.addHandler(debug_handler)
logger = Logger("TGPaimon", level_)
logger.addHandler(handler)
logger.addHandler(debug_handler)
logger.addHandler(error_handler)
__initialized__ = True

View File

@ -1,6 +1,9 @@
import os import os
import traceback as traceback_ import traceback as traceback_
from types import ModuleType, TracebackType from types import (
ModuleType,
TracebackType,
)
from typing import ( from typing import (
Any, Any,
Dict, Dict,
@ -40,10 +43,7 @@ from rich.traceback import (
Traceback as BaseTraceback, Traceback as BaseTraceback,
) )
from core.config import config from utils.log._style import MonokaiProStyle
from utils.log._style import (
MonokaiProStyle,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from rich.console import ConsoleRenderable # pylint: disable=W0611 from rich.console import ConsoleRenderable # pylint: disable=W0611
@ -114,7 +114,8 @@ class Traceback(BaseTraceback):
locals_max_depth: Optional[int] locals_max_depth: Optional[int]
def __init__(self, *args, locals_max_depth: Optional[int] = None, **kwargs): def __init__(self, *args, locals_max_depth: Optional[int] = None, **kwargs):
kwargs.update({"show_locals": True, "max_frames": config.logger.traceback_max_frames})
kwargs.update({"show_locals": True})
super(Traceback, self).__init__(*args, **kwargs) super(Traceback, self).__init__(*args, **kwargs)
self.locals_max_depth = locals_max_depth self.locals_max_depth = locals_max_depth
@ -128,11 +129,11 @@ class Traceback(BaseTraceback):
extra_lines: int = 3, extra_lines: int = 3,
theme: Optional[str] = None, theme: Optional[str] = None,
word_wrap: bool = False, word_wrap: bool = False,
show_locals: bool = False, show_locals: bool = True,
indent_guides: bool = True, indent_guides: bool = True,
locals_max_length: int = config.logger.locals_max_length, locals_max_length: int = 10,
locals_max_string: int = config.logger.locals_max_string, locals_max_string: int = 80,
locals_max_depth: Optional[int] = config.logger_locals_max_depth, locals_max_depth: Optional[int] = None,
suppress: Iterable[Union[str, ModuleType]] = (), suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100, max_frames: int = 100,
) -> "Traceback": ) -> "Traceback":
@ -249,8 +250,7 @@ class Traceback(BaseTraceback):
traceback = cause.__traceback__ traceback = cause.__traceback__
is_cause = False is_cause = False
continue continue
# No cover, code is reached but coverage doesn't recognize it. break
break # pragma: no cover
trace = Trace(stacks=stacks) trace = Trace(stacks=stacks)
return trace return trace

View File

@ -1,10 +1,20 @@
from logging import Filter, LogRecord
from pathlib import Path from pathlib import Path
from types import TracebackType from types import TracebackType
from typing import Any, Dict, Optional, Tuple, Type, Union from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
from httpx import URL from httpx import URL
__all__ = ["StrOrPath", "StrOrURL", "StrOrInt", "SysExcInfoType", "ExceptionInfoType", "JSONDict", "JSONType"] __all__ = [
"StrOrPath",
"StrOrURL",
"StrOrInt",
"SysExcInfoType",
"ExceptionInfoType",
"JSONDict",
"JSONType",
"LogFilterType",
]
StrOrPath = Union[str, Path] StrOrPath = Union[str, Path]
StrOrURL = Union[str, URL] StrOrURL = Union[str, URL]
@ -14,3 +24,5 @@ SysExcInfoType = Union[Tuple[Type[BaseException], BaseException, Optional[Traceb
ExceptionInfoType = Union[bool, SysExcInfoType, BaseException] ExceptionInfoType = Union[bool, SysExcInfoType, BaseException]
JSONDict = Dict[str, Any] JSONDict = Dict[str, Any]
JSONType = Union[JSONDict, list] JSONType = Union[JSONDict, list]
LogFilterType = Union[Filter, Callable[[LogRecord], int]]