Skip to content

Commit

Permalink
feat!: log levels are string enum members
Browse files Browse the repository at this point in the history
  • Loading branch information
serhez committed Apr 24, 2024
1 parent 7f79684 commit 99d2ff6
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 63 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ Masks are used by the `MultiLogger` to filter loggers which are not supposed to

### Level filtering

Any logger is initialized with a `default_level` argument, which is set to `LogLevel.INFO` by default. `LogLevel` elements have an `importance` attribute, which defines a hierarchy of levels. When a logger is initialized with a given level, it will only log messages with a level of equal or higher importance. For example, if a logger is initialized with `LogLevel.WARN`, it will log messages with levels `WARN` and `ERROR`, but not `INFO` or `DEBUG`.
Any logger is initialized with a `default_priority` argument, which is set to `LogLevel.INFO` by default. `LogLevel` elements have an `importance` attribute, which defines a hierarchy of levels. When a logger is initialized with a given level, it will only log messages with a level of equal or higher importance. For example, if a logger is initialized with `LogLevel.WARN`, it will log messages with levels `WARN` and `ERROR`, but not `INFO` or `DEBUG`.

The importance values for the built-in levels are:

- `DEBUG`: -1
- `INFO`: 0
- `WARN`: 1
- `ERROR`: `np.inf` (as errors should always be logged)
- `ERROR`: `sys.maxsize` (a very large number, as errors should always be logged)

### Progress bars

Expand Down
38 changes: 26 additions & 12 deletions mloggers/_log_levels.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Any
import sys
from dataclasses import dataclass

from aenum import Enum, extend_enum
from numpy import inf


class LogLevel(Enum): # type:ignore[reportGeneralTypeIssues]
Expand All @@ -12,13 +12,29 @@ class LogLevel(Enum): # type:ignore[reportGeneralTypeIssues]
To register a new level use `mloggers.register_level`.
"""

WARN = {"color": "yellow", "level": 1}
ERROR = {"color": "red", "level": inf}
DEBUG = {"color": "magenta", "level": -1}
INFO = {"color": "cyan", "level": 0}
@dataclass
class Properties:
color: str
"""The color to use when printing the log."""

priority: int
"""The importance level of the log."""

def register_level(level: str, level_info: dict[str, Any]):
ERROR = "error"
WARN = "warn"
INFO = "info"
DEBUG = "debug"


_log_level_properties: dict[str, LogLevel.Properties] = {
LogLevel.ERROR: LogLevel.Properties(color="red", priority=sys.maxsize),
LogLevel.WARN: LogLevel.Properties(color="yellow", priority=1),
LogLevel.INFO: LogLevel.Properties(color="cyan", priority=0),
LogLevel.DEBUG: LogLevel.Properties(color="magenta", priority=-1),
}


def register_level(name: str, properties: LogLevel.Properties):
"""
Register a customized logger level, which will then be available as a member of `LogLevel`,
where its `name` is the argument `level` and its `value` is the argument `level_info`.
Expand All @@ -27,10 +43,8 @@ def register_level(level: str, level_info: dict[str, Any]):
### Parameters
----------
`level`: the level name to register.
`level_info`: a dictionary with the following
- `color`: the color to use when printing the log. (It must be a valid color name from the `termcolor` package.)
- `level`: the importance level of the log. (It must be a number or `np.inf`.)
`properties`: the properties of the level.
"""

level = level.upper()
extend_enum(LogLevel, level, level_info)
extend_enum(LogLevel, name.upper(), name)
_log_level_properties[name] = properties
9 changes: 4 additions & 5 deletions mloggers/console_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

from termcolor import colored

from mloggers._log_levels import LogLevel
from mloggers._log_levels import LogLevel, _log_level_properties
from mloggers.logger import Logger


class ConsoleLogger(Logger):
"""Logs to the console (i.e., standard I/O)."""

def __init__(self, default_level: LogLevel = LogLevel.INFO): # type:ignore[reportArgumentType]
super().__init__(default_level)
def __init__(self, default_priority: LogLevel = LogLevel.INFO): # type:ignore[reportArgumentType]
super().__init__(default_priority)

def log(
self,
Expand All @@ -37,7 +37,7 @@ def log(

# Color the level string
if isinstance(level, LogLevel):
level_clr = colored(level_str, level.value["color"])
level_clr = colored(level_str, _log_level_properties[level].color) # type:ignore[reportArgumentType]
else:
level_clr = colored(level_str, "green")

Expand Down Expand Up @@ -86,4 +86,3 @@ def log(

elif hasattr(messages, "__str__") and callable(getattr(messages, "__str__")):
print(f"{level_clr}{time} {str(messages)}")

7 changes: 3 additions & 4 deletions mloggers/file_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,18 @@
class FileLogger(Logger):
"""Logs to a file."""

def __init__(self, file_path: str, default_level: LogLevel | int = LogLevel.INFO): # type:ignore[reportArgumentType]
def __init__(self, file_path: str, default_priority: LogLevel | int = LogLevel.INFO): # type:ignore[reportArgumentType]
"""
Initializes a file logger.
### Parameters
----------
`file_path`: the path to the file to log to.
- The file will be created if it does not exist. If it does, the logs will be appended to it.
`default_level`: the default log level to use.
`default_priority`: The default log level priority to use.
"""

super().__init__(default_level)
super().__init__(default_priority)

# Create the file if it does not exist
if not os.path.exists(file_path):
Expand Down
42 changes: 24 additions & 18 deletions mloggers/logger.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
from abc import ABC, abstractmethod
from typing import Any, Callable

from mloggers._log_levels import LogLevel
from mloggers._log_levels import LogLevel, _log_level_properties

# This constant is used to assign an importance level to anything not using the LogLevel enum.
# It was chosen to be the same as LogLevel.INFO, but it can be changed to any other value.
DEFAULT_IMPORTANCE = LogLevel.INFO.value["level"] # type:ignore[reportAttributeAccessIssue]
# It was chosen to be the same as `LogLevel.INFO`, but it can be changed to any other value.
DEFAULT_IMPORTANCE = _log_level_properties[LogLevel.INFO].priority # type:ignore[reportAttributeAccessIssue]


class Logger(ABC):
"""The abstract class for a logger."""

def __init__(self, default_level: LogLevel | int = LogLevel.INFO): # type:ignore[reportArgumentType]
def __init__(self, default_priority: LogLevel | int = LogLevel.INFO): # type:ignore[reportArgumentType]
"""
Initialize the logger.
### Parameters
----------
`log_level`: the default log level to use.
`log_level`: The default log level priority to use.
- This parameter filters out messages with a lower importance level than the one provided. It can be either a `LogLevel` object or an integer.
- When calling the logger with a level not from the `LogLevel` enum, the importance level will be set to 0 (same as `LogLevel.INFO`).
- For example, if the log level is set to `LogLevel.INFO`, only messages with a level of `LogLevel.INFO` or higher will be printed (which excludes `LogLevel.DEBUG`).
"""

self._log_level = (
default_level.value["level"]
if isinstance(default_level, LogLevel)
else default_level
self._min_priority = (
_log_level_properties[default_priority].priority
if isinstance(default_priority, LogLevel)
else default_priority
)

@abstractmethod
Expand Down Expand Up @@ -77,25 +77,31 @@ def log(
"Expected all messages to be either strings or dictionaries, but got a mix of both."
)

# Filter out messages with a lower importance level than the current log level.
if isinstance(level, LogLevel) and level.value["level"] < self._log_level:
# Filter out messages with a lower importance level than the current priority.
if (
isinstance(level, LogLevel)
and _log_level_properties[level].priority < self._min_priority
):
return False
elif isinstance(level, str) and DEFAULT_IMPORTANCE < self._log_level:
elif isinstance(level, str) and DEFAULT_IMPORTANCE < self._min_priority:
return False
return True

def set_level(self, level: LogLevel | int):
def set_min_priority(self, value: LogLevel | int):
"""
Set the log level.
Set the minimum log level priority.
### Parameters
----------
`level`: the log level to set.
- If a string, it must be a valid log level (e.g., INFO, WARN, ERROR, DEBUG, etc.).
- If a `LogLevel` object, it will be used as-is.
`value`: the log level priority to set.
- If a `LogLevel`, the priority of that log level will be considered.
- If an `int`, such number will be considered.
"""

self._log_level = level.value["level"] if isinstance(level, LogLevel) else level
if isinstance(value, int):
self._min_priority = value
else:
self._min_priority = _log_level_properties[value].priority

def _call_impl(self, *args, **kwargs):
return self.log(*args, **kwargs)
Expand Down
30 changes: 11 additions & 19 deletions mloggers/multi_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def __init__(
self,
loggers: list[Logger],
default_mask: list[type[Logger]] = [],
default_level: LogLevel | int = LogLevel.INFO, # type:ignore[reportArgumentType]
default_priority: LogLevel | int = LogLevel.INFO, # type:ignore[reportArgumentType]
):
"""
Initializes a multi-logger.
Expand All @@ -20,30 +20,22 @@ def __init__(
----------
`loggers`: a list of the initialized loggers to use.
`default_mask`: the default mask to use when logging.
`default_level`: the default log level to use.
`default_priority`: The default log level priority to use.
"""

super().__init__(default_level)
super().__init__(default_priority)

self._loggers = loggers
self._default_mask = default_mask

for logger in self._loggers:
logger.set_level(self._log_level)
logger.set_min_priority(self._min_priority)

def set_level(self, level: LogLevel | int):
"""
Sets the log level of the multi-logger.
### Parameters
----------
`level`: the level to set.
"""

super(MultiLogger, self).set_level(level)
def set_min_priority(self, level: LogLevel | int):
super(MultiLogger, self).set_min_priority(level)

for logger in self._loggers:
logger.set_level(self._log_level)
logger.set_min_priority(level)

def log(
self,
Expand Down Expand Up @@ -94,7 +86,7 @@ def info(
**kwargs: Any,
):
"""
Wrapper for calling `log` with level=LogLevel.INFO.
Wrapper for calling `log` with `level=LogLevel.INFO`.
### Parameters
----------
Expand All @@ -117,7 +109,7 @@ def warn(
**kwargs: Any,
):
"""
Wrapper for calling `log` with level=LogLevel.WARN.
Wrapper for calling `log` with `level=LogLevel.WARN`.
### Parameters
----------
Expand All @@ -143,7 +135,7 @@ def error(
**kwargs: Any,
):
"""
Wrapper for calling `log` with level=LogLevel.ERROR.
Wrapper for calling `log` with `level=LogLevel.ERROR`.
### Parameters
----------
Expand All @@ -166,7 +158,7 @@ def debug(
**kwargs: Any,
):
"""
Wrapper for calling `log` with level=LogLevel.DEBUG.
Wrapper for calling `log` with `level=LogLevel.DEBUG`.
### Parameters
----------
Expand Down
6 changes: 3 additions & 3 deletions mloggers/wandb_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(
project: str,
group: str,
experiment: str,
default_level: LogLevel | int = LogLevel.INFO, # type:ignore[reportArgumentType]
default_priority: LogLevel | int = LogLevel.INFO, # type:ignore[reportArgumentType]
config: DictConfig | None = None,
):
"""
Expand All @@ -27,11 +27,11 @@ def __init__(
`project`: the name of the project to log to.
`group`: the name of the group to log to.
`experiment`: the name of the experiment to log to.
`default_level`: the default log level to use.
`default_priority`: the default log level priority to use.
[optional] `config`: the configuration of the experiment.
"""

super().__init__(default_level)
super().__init__(default_priority)

if config is not None:
config = vars(config)
Expand Down

0 comments on commit 99d2ff6

Please sign in to comment.