Skip to content

Commit

Permalink
Added Captured and Logot.capture(), enabling custom log capture b…
Browse files Browse the repository at this point in the history
…ackends (#38)

- Added `Captured`, representing an abstract captured log.
- Added `_format` internal module, providing shared formatting logic for
`__str__` implementations.
- Bulk-renaming to represent decoupling from `logging` module. 😅 
- Removed log de-duplication, since it's non-portable and shouldn't be a
problem for users / is actually a feature.
- Added "Log capturing" docs page.

This is needed by #28.
  • Loading branch information
etianen authored Jan 28, 2024
1 parent b1c2662 commit 6b5e798
Show file tree
Hide file tree
Showing 14 changed files with 357 additions and 180 deletions.
8 changes: 8 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Import the :mod:`logot` API in your tests:

.. automethod:: capturing

.. automethod:: capture

.. automethod:: wait_for

.. automethod:: await_for
Expand All @@ -37,6 +39,12 @@ Import the :mod:`logot` API in your tests:

.. autoclass:: logot.Logged

.. autoclass:: logot.Captured

.. autoattribute:: levelno

.. autoattribute:: msg


:mod:`logot.logged`
-------------------
Expand Down
61 changes: 61 additions & 0 deletions docs/captured.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
Log capturing
=============

.. currentmodule:: logot

:mod:`logot` makes it easy to capture logs from the stdlib :mod:`logging` module:

.. code:: python
with Logot().capturing() as logot:
app.start()
logot.wait_for(logged.info("App started"))
.. note::

If using :mod:`pytest`, you can probably just use the pre-configured ``logot`` fixture included in the bundled
:doc:`pytest plugin <pytest>` and skip manually configuring log capture. 💪


Capturing :mod:`logging` logs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The :meth:`Logot.capturing` method defaults to capturing **all** records from the root logger. Customize this with the
``level`` and ``logger`` arguments to :meth:`Logot.capturing`:

.. code:: python
with Logot().capturing(level=logging.WARNING, logger="app") as logot:
app.start()
logot.wait_for(logged.info("App started"))
For advanced use-cases, multiple :meth:`Logot.capturing` calls on the same :class:`Logot` instance are supported. Be
careful to avoid capturing duplicate logs with overlapping calls to :meth:`Logot.capturing`!

.. seealso::

See :class:`Logot` and :meth:`Logot.capturing` API reference.


.. _captured-3rd-party:

Capturing 3rd-party logs
~~~~~~~~~~~~~~~~~~~~~~~~

Any 3rd-party logging library can be integrated with :mod:`logot` by sending :class:`Captured` logs to
:meth:`Logot.capture`:

.. code:: python
def on_foo_log(logot: Logot, record: FooRecord) -> None:
logot.capture(Captured(record.levelno, record.msg))
foo_logger.add_handler(on_foo_log)
.. note::

Using a context manager to set up and tear down log capture for every test run is *highly recommended*!

.. seealso::

See :class:`Captured` and :meth:`Logot.capture` API reference.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Learn more about :mod:`logot` with the following guides:

match
logged
captured
pytest
unittest
api
2 changes: 1 addition & 1 deletion docs/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Using with :mod:`unittest`
class MyAppTest(unittest.TestCase):
def test_my_app(self) -> None:
with Logot().capture() as logot:
with Logot().capturing() as logot:
app.start()
logot.wait_for(logged.info("App started"))
Expand Down
1 change: 1 addition & 0 deletions logot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations

from logot._captured import Captured as Captured
from logot._logged import Logged as Logged
from logot._logot import Logot as Logot
53 changes: 53 additions & 0 deletions logot/_captured.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

import logging
from typing import Final

from logot._format import format_log
from logot._validate import validate_levelno


class Captured:
"""
A captured log record.
Send :class:`Captured` logs to :meth:`Logot.capture` to integrate with
:ref:`3rd-party logging frameworks <captured-3rd-party>`
.. note::
This class is for integration with :ref:`3rd-party logging frameworks <captured-3rd-party>`. It is not generally
used when writing tests.
.. seealso::
See :ref:`captured-3rd-party` usage guide.
:param level: The log level (e.g. :data:`logging.DEBUG`) or string name (e.g. ``"DEBUG"``).
:param msg: The log message.
"""

__slots__ = ("levelno", "msg")

levelno: Final[int]
"""
The integer log level (e.g. :data:`logging.DEBUG`).
"""

msg: Final[str]
"""
The log message.
"""

def __init__(self, level: int | str, msg: str) -> None:
self.levelno = validate_levelno(level)
self.msg = msg

def __eq__(self, other: object) -> bool:
return isinstance(other, Captured) and other.levelno == self.levelno and other.msg == self.msg

def __repr__(self) -> str:
return f"Captured({logging.getLevelName(self.levelno)!r}, {self.msg!r})"

def __str__(self) -> str:
return format_log(self.levelno, self.msg)
7 changes: 7 additions & 0 deletions logot/_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from __future__ import annotations

import logging


def format_log(levelno: int, msg: str) -> str:
return f"[{logging.getLevelName(levelno)}] {msg}"
Loading

0 comments on commit 6b5e798

Please sign in to comment.