Skip to content

Commit

Permalink
Merge pull request #6 from pomponchik/develop
Browse files Browse the repository at this point in the history
0.0.6
  • Loading branch information
pomponchik authored Jul 10, 2024
2 parents c7d9738 + ca67c77 commit 393dfa4
Show file tree
Hide file tree
Showing 17 changed files with 520 additions and 27 deletions.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This package ensures compatibility of any logger implementations with the built-
- [**Empty Logger**](#empty-logger)
- [**Memory Logger**](#memory-logger)
- [**Printing Logger**](#printing-logger)
- [**Summation of loggers**](#summation-of-loggers)


## Installing
Expand Down Expand Up @@ -169,3 +170,58 @@ logger.info('Hello, the colored world!')
#> 2024-07-09 11:20:03.693837 | INFO | Hello, the colored world!
# You can't see it here, but believe me, if you repeat the code at home, the output in the console will be green!
```


## Summation of loggers

All loggers represented in this library can be grouped together. To do this, just use the "+" operator:

```python
from emptylog import PrintingLogger, MemoryLogger

logger = PrintingLogger() + MemoryLogger()
print(logger)
#> LoggersGroup(PrintingLogger(), MemoryLogger())
```

The group object also implements the [logger protocol](#universal-logger-protocol). If you use it as a logger, it will alternately call the appropriate methods from the loggers nested in it:

```python
printing_logger = PrintingLogger()
memory_logger = MemoryLogger()

super_logger = printing_logger + memory_logger

super_logger.info('Together we are a force!')
#> 2024-07-10 16:49:21.247290 | INFO | Together we are a force!
print(memory_logger.data.info[0].message)
#> Together we are a force!
```

You can sum up more than 2 loggers. In this case, the number of nesting levels will not grow:

```python
print(MemoryLogger() + MemoryLogger() + MemoryLogger())
#> LoggersGroup(MemoryLogger(), MemoryLogger(), MemoryLogger())
```

You can also add any loggers from this library with loggers from other libraries, for example from the [standard library](https://docs.python.org/3/library/logging.html) or from [loguru](https://github.com/Delgan/loguru):

```python
import logging
from loguru import logger as loguru_logger

print(MemoryLogger() + loguru_logger + logging.getLogger(__name__))
#> LoggersGroup(MemoryLogger(), <loguru.logger handlers=[(id=0, level=10, sink=<stderr>)]>, <Logger __main__ (WARNING)>)
```

Finally, you can use a group as an iterable object, as well as find out the [number of nested loggers](https://docs.python.org/3/library/functions.html#len) in a standard way:

```python
group = PrintingLogger() + MemoryLogger()

print(len(group))
#> 2
print([x for x in group])
#> [PrintingLogger(), MemoryLogger()]
```
3 changes: 2 additions & 1 deletion emptylog/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from emptylog.empty_logger import EmptyLogger as EmptyLogger # noqa: F401
from emptylog.protocol import LoggerProtocol as LoggerProtocol # noqa: F401
from emptylog.protocols import LoggerProtocol as LoggerProtocol # noqa: F401
from emptylog.memory_logger import MemoryLogger as MemoryLogger # noqa: F401
from emptylog.printing_logger import PrintingLogger as PrintingLogger # noqa: F401
from emptylog.loggers_group import LoggersGroup as LoggersGroup # noqa: F401
30 changes: 30 additions & 0 deletions emptylog/abstract_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from abc import ABC

from emptylog.protocols import LoggerProtocol


class AbstractLogger(LoggerProtocol, ABC):
def __repr__(self) -> str:
return f'{type(self).__name__}()'

def __add__(self, other: LoggerProtocol) -> 'LoggersGroup': # type: ignore[name-defined] # noqa: F821
if not isinstance(other, LoggerProtocol):
raise NotImplementedError('The addition operation is defined only for loggers.')

from emptylog import LoggersGroup

local_loggers = self.loggers if isinstance(self, LoggersGroup) else [self]
other_loggers = other.loggers if isinstance(other, LoggersGroup) else [other]

return LoggersGroup(*local_loggers, *other_loggers)

def __radd__(self, other: LoggerProtocol) -> 'LoggersGroup': # type: ignore[name-defined] # noqa: F821
if not isinstance(other, LoggerProtocol):
raise NotImplementedError('The addition operation is defined only for loggers.')

from emptylog import LoggersGroup

local_loggers = self.loggers if isinstance(self, LoggersGroup) else [self]
other_loggers = other.loggers if isinstance(other, LoggersGroup) else [other]

return LoggersGroup(*other_loggers, *local_loggers)
4 changes: 2 additions & 2 deletions emptylog/empty_logger.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from emptylog.protocol import LoggerProtocol
from emptylog.abstract_logger import AbstractLogger


class EmptyLogger(LoggerProtocol):
class EmptyLogger(AbstractLogger):
pass
59 changes: 59 additions & 0 deletions emptylog/loggers_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import sys
from typing import Tuple, Callable, Any
from threading import Lock
from collections.abc import Iterator

from printo import descript_data_object

from emptylog.protocols import LoggerProtocol, LoggerMethodProtocol
from emptylog.abstract_logger import AbstractLogger


if sys.version_info < (3, 9):
GroupIterator = Iterator # pragma: no cover
else:
GroupIterator = Iterator[LoggerProtocol] # pragma: no cover

class LoggersGroup(AbstractLogger):
loggers: Tuple[LoggerProtocol, ...]

def __init__(self, *loggers: LoggerProtocol) -> None:
for logger in loggers:
if not isinstance(logger, LoggerProtocol):
raise TypeError(f'A logger group can only be created from loggers. You passed {repr(logger)} ({type(logger).__name__}).')

self.loggers = loggers
self.lock = Lock()

def __repr__(self) -> str:
return descript_data_object(type(self).__name__, self.loggers, {}, serializator=repr)

def __len__(self) -> int:
return len(self.loggers)

def __iter__(self) -> GroupIterator: # type: ignore[type-arg]
yield from self.loggers

def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
self.run_loggers(lambda x: x.debug, message, *args, **kwargs)

def info(self, message: str, *args: Any, **kwargs: Any) -> None:
self.run_loggers(lambda x: x.info, message, *args, **kwargs)

def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
self.run_loggers(lambda x: x.warning, message, *args, **kwargs)

def error(self, message: str, *args: Any, **kwargs: Any) -> None:
self.run_loggers(lambda x: x.error, message, *args, **kwargs)

def exception(self, message: str, *args: Any, **kwargs: Any) -> None:
self.run_loggers(lambda x: x.exception, message, *args, **kwargs)

def critical(self, message: str, *args: Any, **kwargs: Any) -> None:
self.run_loggers(lambda x: x.critical, message, *args, **kwargs)

def run_loggers(self, get_method: Callable[[LoggerProtocol], LoggerMethodProtocol], message: str, *args: Any, **kwargs: Any) -> None:
with self.lock:
for logger in self.loggers:
method = get_method(logger)
method(message, *args, **kwargs)
4 changes: 2 additions & 2 deletions emptylog/memory_logger.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import Any

from emptylog.protocol import LoggerProtocol
from emptylog.abstract_logger import AbstractLogger
from emptylog.call_data import LoggerCallData
from emptylog.accumulated_data import LoggerAccumulatedData


class MemoryLogger(LoggerProtocol):
class MemoryLogger(AbstractLogger):
def __init__(self) -> None:
self.data = LoggerAccumulatedData()

Expand Down
4 changes: 2 additions & 2 deletions emptylog/printing_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from datetime import datetime
from functools import partial

from emptylog.protocol import LoggerProtocol
from emptylog.abstract_logger import AbstractLogger


class PrintingLogger(LoggerProtocol):
class PrintingLogger(AbstractLogger):
def __init__(self, printing_callback: Callable[[Any], Any] = partial(print, end=''), separator: str = '|') -> None:
self.callback = printing_callback
self.separator = separator
Expand Down
4 changes: 4 additions & 0 deletions emptylog/protocol.py → emptylog/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ def warning(self, message: str, *args: Any, **kwargs: Any) -> None: return None
def error(self, message: str, *args: Any, **kwargs: Any) -> None: return None
def exception(self, message: str, *args: Any, **kwargs: Any) -> None: return None
def critical(self, message: str, *args: Any, **kwargs: Any) -> None: return None


class LoggerMethodProtocol(Protocol):
def __call__(self, message: str, *args: Any, **kwargs: Any) -> None: return None # pragma: no cover
16 changes: 11 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'

[project]
name = 'emptylog'
version = '0.0.5'
version = '0.0.6'
authors = [
{ name='Evgeniy Blinov', email='[email protected]' },
]
Expand All @@ -13,6 +13,7 @@ readme = 'README.md'
requires-python = '>=3.7'
dependencies = [
'typing_extensions ; python_version < "3.8"',
'printo>=0.0.2',
]
classifiers = [
'Operating System :: MacOS :: MacOS X',
Expand All @@ -26,13 +27,18 @@ classifiers = [
'Programming Language :: Python :: 3.12',
'License :: OSI Approved :: MIT License',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Interpreters',
'Topic :: Utilities',
'Topic :: System :: Archiving :: Packaging',
'Intended Audience :: System Administrators',
'Topic :: Software Development :: Testing',
'Topic :: Software Development :: Testing :: Mocking',
'Topic :: Software Development :: Testing :: Unit',
'Topic :: System :: Logging',
'Intended Audience :: Developers',
'Typing :: Typed',
]
keywords = [
'logging',
'protocols',
'loggers mocks',
]

[tool.setuptools.package-data]
"emptylog" = ["py.typed"]
Expand Down
2 changes: 2 additions & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ twine==4.0.2
mypy==1.4.1
ruff==0.0.290
mutmut==2.4.4
full_match==0.0.1
loguru==0.7.2
141 changes: 141 additions & 0 deletions tests/test_abstract_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import logging

import pytest
import full_match
from loguru import logger as loguru_logger

from emptylog.abstract_logger import AbstractLogger
from emptylog import EmptyLogger, LoggersGroup, MemoryLogger, PrintingLogger


@pytest.mark.parametrize(
['first_logger'],
(
(EmptyLogger(),),
(MemoryLogger(),),
(PrintingLogger(),),
),
)
@pytest.mark.parametrize(
['second_logger'],
(
(EmptyLogger(),),
(MemoryLogger(),),
(PrintingLogger(),),
(logging,),
(logging.getLogger('kek'),),
(loguru_logger,),
),
)
def test_sum_of_inner_loggers(first_logger, second_logger):
sum = first_logger + second_logger

assert isinstance(sum, LoggersGroup)

assert sum is not first_logger
assert sum is not second_logger

assert sum.loggers[0] is first_logger
assert sum.loggers[1] is second_logger


@pytest.mark.parametrize(
['first_logger'],
(
(logging,),
(logging.getLogger('kek'),),
(loguru_logger,),
),
)
@pytest.mark.parametrize(
['second_logger'],
(
(EmptyLogger(),),
(MemoryLogger(),),
(PrintingLogger(),),
),
)
def test_sum_with_another_loggers_as_first_operand(first_logger, second_logger):
sum = first_logger + second_logger

assert isinstance(sum, LoggersGroup)

assert sum is not first_logger
assert sum is not second_logger

assert sum.loggers[0] is first_logger
assert sum.loggers[1] is second_logger


@pytest.mark.parametrize(
['logger'],
(
(EmptyLogger(),),
(LoggersGroup(),),
(MemoryLogger(),),
(PrintingLogger(),),
),
)
def test_all_loggers_are_instances_of_abstract_logger(logger):
assert isinstance(logger, AbstractLogger)


@pytest.mark.parametrize(
['logger'],
(
(EmptyLogger(),),
(LoggersGroup(),),
(MemoryLogger(),),
(PrintingLogger(),),
),
)
@pytest.mark.parametrize(
['wrong_operand'],
(
(1,),
('kek',),
(None,),
),
)
def test_sum_with_wrong_first_operand(logger, wrong_operand):
with pytest.raises(NotImplementedError, match=full_match('The addition operation is defined only for loggers.')):
wrong_operand + logger


@pytest.mark.parametrize(
['logger'],
(
(EmptyLogger(),),
(LoggersGroup(),),
(MemoryLogger(),),
(PrintingLogger(),),
),
)
@pytest.mark.parametrize(
['wrong_operand'],
(
(1,),
('kek',),
(None,),
),
)
def test_sum_with_wrong_second_operand(logger, wrong_operand):
with pytest.raises(NotImplementedError, match=full_match('The addition operation is defined only for loggers.')):
logger + wrong_operand


def test_sum_of_three_loggers():
first_logger = EmptyLogger()
second_logger = MemoryLogger()
third_logger = PrintingLogger()

sum = first_logger + second_logger + third_logger

assert isinstance(sum, LoggersGroup)

assert len(sum) == 3
assert len(sum.loggers) == 3

assert sum.loggers[0] is first_logger
assert sum.loggers[1] is second_logger
assert sum.loggers[2] is third_logger
Loading

0 comments on commit 393dfa4

Please sign in to comment.