mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-23 15:33:26 +08:00
119 lines
3.9 KiB
Python
119 lines
3.9 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""System for managing loggers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING, Annotated
|
|
from dataclasses import dataclass, field
|
|
|
|
from efro.dataclassio import ioprepped, IOAttrs
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Self
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class LoggerControlConfig:
|
|
"""A logging level configuration that applies to all loggers.
|
|
|
|
Any loggers not explicitly contained in the configuration will be
|
|
set to NOTSET.
|
|
"""
|
|
|
|
# Logger names mapped to log-level values (from system logging module).
|
|
levels: Annotated[dict[str, int], IOAttrs('l', store_default=False)] = (
|
|
field(default_factory=dict)
|
|
)
|
|
|
|
def apply(self) -> None:
|
|
"""Apply the config to all Python loggers."""
|
|
existinglognames = (
|
|
set(['root']) | logging.root.manager.loggerDict.keys()
|
|
)
|
|
|
|
# First, update levels for all existing loggers.
|
|
for logname in existinglognames:
|
|
logger = logging.getLogger(logname)
|
|
level = self.levels.get(logname)
|
|
if level is None:
|
|
level = logging.NOTSET
|
|
logger.setLevel(level)
|
|
|
|
# Next, assign levels to any loggers that don't exist.
|
|
for logname, level in self.levels.items():
|
|
if logname not in existinglognames:
|
|
logging.getLogger(logname).setLevel(level)
|
|
|
|
def would_make_changes(self) -> bool:
|
|
"""Return whether calling apply would change anything."""
|
|
|
|
existinglognames = (
|
|
set(['root']) | logging.root.manager.loggerDict.keys()
|
|
)
|
|
|
|
# Return True if we contain any nonexistent loggers. Even if
|
|
# we wouldn't change their level, the fact that we'd create
|
|
# them still counts as a difference.
|
|
if any(
|
|
logname not in existinglognames for logname in self.levels.keys()
|
|
):
|
|
return True
|
|
|
|
# Now go through all existing loggers and return True if we
|
|
# would change their level.
|
|
for logname in existinglognames:
|
|
logger = logging.getLogger(logname)
|
|
level = self.levels.get(logname)
|
|
if level is None:
|
|
level = logging.NOTSET
|
|
if logger.level != level:
|
|
return True
|
|
|
|
return False
|
|
|
|
def diff(self, baseconfig: LoggerControlConfig) -> LoggerControlConfig:
|
|
"""Return a config containing only changes compared to a base config.
|
|
|
|
Note that this omits all NOTSET values that resolve to NOTSET in
|
|
the base config.
|
|
|
|
This diffed config can later be used with apply_diff() against the
|
|
base config to recreate the state represented by self.
|
|
"""
|
|
cls = type(self)
|
|
config = cls()
|
|
for loggername, level in self.levels.items():
|
|
baselevel = baseconfig.levels.get(loggername, logging.NOTSET)
|
|
if level != baselevel:
|
|
config.levels[loggername] = level
|
|
return config
|
|
|
|
def apply_diff(
|
|
self, diffconfig: LoggerControlConfig
|
|
) -> LoggerControlConfig:
|
|
"""Apply a diff config to ourself."""
|
|
cls = type(self)
|
|
|
|
# Create a new config (with an indepenent levels dict copy).
|
|
config = cls(levels=dict(self.levels))
|
|
|
|
# Overlay the diff levels dict onto our new one.
|
|
config.levels.update(diffconfig.levels)
|
|
|
|
# Note: we do NOT prune NOTSET values here. This is so all
|
|
# loggers mentioned in the base config get created if we are
|
|
# applied, even if they are assigned a default level.
|
|
return config
|
|
|
|
@classmethod
|
|
def from_current_loggers(cls) -> Self:
|
|
"""Build a config from the current set of loggers."""
|
|
lognames = ['root'] + sorted(logging.root.manager.loggerDict)
|
|
config = cls()
|
|
for logname in lognames:
|
|
config.levels[logname] = logging.getLogger(logname).level
|
|
return config
|