From b48f701b24314ee2023b1ba5bb86b63f1852dcb9 Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 10:54:32 +0000 Subject: [PATCH 01/13] wip --- .github/workflows/build.yml | 2 + README.md | 8 +-- docs/api/logot.structlog.rst | 10 ++++ docs/conf.py | 4 +- docs/index.rst | 3 +- docs/integrations/structlog.rst | 90 +++++++++++++++++++++++++++++++++ docs/log-capturing.rst | 2 +- docs/using-pytest.rst | 3 +- docs/using-unittest.rst | 3 +- logot/_structlog.py | 41 +++++++++++++++ logot/structlog.py | 10 ++++ poetry.lock | 20 +++++++- pyproject.toml | 2 + tests/test_structlog.py | 58 +++++++++++++++++++++ 14 files changed, 243 insertions(+), 13 deletions(-) create mode 100644 docs/api/logot.structlog.rst create mode 100644 docs/integrations/structlog.rst create mode 100644 logot/_structlog.py create mode 100644 logot/structlog.py create mode 100644 tests/test_structlog.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e961f08..c4590e96 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,8 @@ jobs: - python-version: "3.10" - python-version: "3.11" - python-version: "3.12" + - lib-versions: "structlog~=23.1.0" + - lib-versions: "structlog~=24.1.0" - lib-versions: "loguru~=0.6.0" - lib-versions: "loguru~=0.7.0" - lib-versions: "pytest~=7.0" diff --git a/README.md b/README.md index 6a687c52..a01e78c0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ `logot` makes it easy to test whether your code is logging correctly: -``` python +```python from logot import Logot, logged def test_something(logot: Logot) -> None: @@ -17,24 +17,20 @@ def test_something(logot: Logot) -> None: logot.assert_logged(logged.info("Something was done")) ``` -`logot` integrates with popular testing (e.g. [`pytest`](https://logot.readthedocs.io/latest/using-pytest.html), [`unittest`](https://logot.readthedocs.io/latest/using-unittest.html)), asynchronous (e.g. [`asyncio`](https://logot.readthedocs.io/latest/index.html#index-testing-threaded), [`trio`](https://logot.readthedocs.io/latest/integrations/trio.html)) and logging frameworks (e.g. [`logging`](https://logot.readthedocs.io/latest/log-capturing.html), [`loguru`](https://logot.readthedocs.io/latest/integrations/loguru.html)). It can be extended to support many others. 💪 - +`logot` integrates with popular testing (e.g. [`pytest`](https://logot.readthedocs.io/latest/using-pytest.html), [`unittest`](https://logot.readthedocs.io/latest/using-unittest.html)), asynchronous (e.g. [`asyncio`](https://logot.readthedocs.io/latest/index.html#index-testing-threaded), [`trio`](https://logot.readthedocs.io/latest/integrations/trio.html)) and logging frameworks (e.g. [`logging`](https://logot.readthedocs.io/latest/log-capturing.html), [`loguru`](https://logot.readthedocs.io/latest/integrations/loguru.html), [`structlog`](https://logot.readthedocs.io/latest/integrations/structlog.html)). It can be extended to support many others. 💪 ## Documentation 📖 Full documentation is published on [Read the Docs](https://logot.readthedocs.io). - ## Bugs / feedback 🐛 Issue tracking is hosted on [GitHub](https://github.com/etianen/logot/issues). - ## Changelog 🏗️ Release notes are published on [GitHub](https://github.com/etianen/logot/releases). - ## License ⚖️ `logot` is published as open-source software under the [MIT license](https://github.com/etianen/logot/blob/main/LICENSE). diff --git a/docs/api/logot.structlog.rst b/docs/api/logot.structlog.rst new file mode 100644 index 00000000..0d365961 --- /dev/null +++ b/docs/api/logot.structlog.rst @@ -0,0 +1,10 @@ +:mod:`logot.structlog` +=================== + +.. automodule:: logot.structlog + + +API reference +------------- + +.. autoclass:: StructlogCapturer diff --git a/docs/conf.py b/docs/conf.py index 489d5e4d..ad97c45e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,10 +1,9 @@ from __future__ import annotations +import tomllib from datetime import date from pathlib import Path -import tomllib - _root = Path(__file__).parent.parent _poetry = tomllib.loads((_root / "pyproject.toml").read_text())["tool"]["poetry"] @@ -27,6 +26,7 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "loguru": ("https://loguru.readthedocs.io/en/latest/", None), + "structlog": ("https://www.structlog.org/en/stable/", None), "pytest": ("https://docs.pytest.org/en/latest/", None), "trio": ("https://trio.readthedocs.io/en/latest/", None), } diff --git a/docs/index.rst b/docs/index.rst index 3d210838..854ba79c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,7 +18,8 @@ Log-based testing 🪵 :mod:`logot` integrates with popular testing (e.g. :doc:`pytest `, :doc:`unittest `), asynchronous (e.g. :ref:`asyncio `, :doc:`trio `) and logging frameworks (e.g. :doc:`logging `, - :doc:`loguru `). It can be extended to support many others. 💪 + :doc:`loguru `, :doc:`structlog `). It can be extended + to support many others. 💪 Why test logging? 🤔 diff --git a/docs/integrations/structlog.rst b/docs/integrations/structlog.rst new file mode 100644 index 00000000..c3a8e4cf --- /dev/null +++ b/docs/integrations/structlog.rst @@ -0,0 +1,90 @@ +Using with :mod:`structlog` +======================== + +.. currentmodule:: logot + +:mod:`logot` makes it easy to capture logs from :mod:`structlog`: + +.. code:: python + + from logot.structlog import StructlogCapturer + + with Logot(capturer=StructlogCapturer).capturing() as logot: + do_something() + logot.assert_logged(logged.info("App started")) + + +Installing +---------- + +Ensure :mod:`logot` is installed alongside a compatible :mod:`structlog` version by adding the ``structlog`` extra: + +.. code:: bash + + pip install 'logot[structlog]' + +.. seealso:: + + See :ref:`installing-extras` usage guide. + + +Enabling for :mod:`pytest` +-------------------------- + +Enable :mod:`structlog` support in your :external+pytest:doc:`pytest configuration `: + +.. code:: ini + + # pytest.ini or .pytest.ini + [pytest] + logot_capturer = logot.structlog.StructlogCapturer + +.. code:: toml + + # pyproject.toml + [tool.pytest.ini_options] + logot_capturer = "logot.structlog.StructlogCapturer" + +.. seealso:: + + See :doc:`/using-pytest` usage guide. + + +Enabling for :mod:`unittest` +---------------------------- + +Enable :mod:`structlog` support in your :class:`logot.unittest.LogotTestCase`: + +.. code:: python + + from logot.structlog import StructlogCapturer + + class MyAppTest(LogotTestCase): + logot_capturer = StructlogCapturer + +.. seealso:: + + See :doc:`/using-unittest` usage guide. + + +Enabling manually +----------------- + +Enable :mod:`structlog` support for your :class:`Logot` instance: + +.. code:: python + + from logot.structlog import StructlogCapturer + + logot = Logot(capturer=StructlogCapturer) + +Enable :mod:`structlog` support for a single :meth:`Logot.capturing` call: + +.. code:: python + + with Logot().capturing(capturer=StructlogCapturer) as logot: + do_something() + +.. seealso:: + + See :class:`Logot` and :meth:`Logot.capturing` API reference. diff --git a/docs/log-capturing.rst b/docs/log-capturing.rst index 411539b5..e7e330e5 100644 --- a/docs/log-capturing.rst +++ b/docs/log-capturing.rst @@ -13,7 +13,7 @@ Log capturing .. seealso:: - See :ref:`integrations-logging` for other supported logging frameworks (e.g. :doc:`loguru `). + See :ref:`integrations-logging` for other supported logging frameworks (e.g. :doc:`loguru `, :doc:`structlog `). Test framework integrations diff --git a/docs/using-pytest.rst b/docs/using-pytest.rst index c0aeef1d..20bc6e46 100644 --- a/docs/using-pytest.rst +++ b/docs/using-pytest.rst @@ -37,7 +37,8 @@ using |caplog|_ as: - Support for :doc:`log message matching ` using ``%``-style placeholders. - Support for :doc:`log pattern matching ` using *log pattern operators*. - Support for testing :ref:`threaded ` and :ref:`async ` code. -- Support for :ref:`3rd-party logging frameworks ` (e.g. :doc:`loguru `). +- Support for :ref:`3rd-party logging frameworks ` (e.g. :doc:`loguru `, + :doc:`structlog `). - A cleaner, clearer syntax. diff --git a/docs/using-unittest.rst b/docs/using-unittest.rst index 8eebe5d0..8e349c02 100644 --- a/docs/using-unittest.rst +++ b/docs/using-unittest.rst @@ -43,7 +43,8 @@ testing. The above example can be rewritten using :meth:`assertLogs() ` using ``%``-style placeholders. - Support for :doc:`log pattern matching ` using *log pattern operators*. - Support for testing :ref:`threaded ` and :ref:`async ` code. -- Support for :ref:`3rd-party logging frameworks ` (e.g. :doc:`loguru `). +- Support for :ref:`3rd-party logging frameworks ` (e.g. :doc:`loguru `, + :doc:`structlog `). - A cleaner, clearer syntax. diff --git a/logot/_structlog.py b/logot/_structlog.py new file mode 100644 index 00000000..4717bd2d --- /dev/null +++ b/logot/_structlog.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from functools import partial + +from structlog import configure, get_config +from structlog.exceptions import DropEvent +from structlog.processors import NAME_TO_LEVEL +from structlog.typing import EventDict, WrappedLogger + +from logot._capture import Captured +from logot._logot import Capturer, Logot +from logot._typing import Level, Name + + +class StructlogCapturer(Capturer): + """ + A :class:`logot.Capturer` implementation for :mod:`structlog`. + """ + + __slots__ = ("_old_processors",) + + def start_capturing(self, logot: Logot, /, *, level: Level, name: Name) -> None: + processors = get_config()["processors"] + self._old_processors = processors.copy() + processors.clear() + processors.append(partial(_processor, logot=logot)) + configure(processors=processors) + + def stop_capturing(self) -> None: + processors = get_config()["processors"] + processors.clear() + processors.extend(self._old_processors) + configure(processors=processors) + + +def _processor(_: WrappedLogger, method_name: str, event_dict: EventDict, *, logot: Logot) -> None: + record = event_dict["event"] + level = method_name + logot.capture(Captured(level, record["message"], levelno=NAME_TO_LEVEL[level])) + + raise DropEvent diff --git a/logot/structlog.py b/logot/structlog.py new file mode 100644 index 00000000..0a1b460b --- /dev/null +++ b/logot/structlog.py @@ -0,0 +1,10 @@ +""" +Integration API for :mod:`structlog`. + +.. seealso:: + + See :doc:`/integrations/structlog` usage guide. +""" +from __future__ import annotations + +from logot._structlog import StructlogCapturer as StructlogCapturer diff --git a/poetry.lock b/poetry.lock index bf5baac1..3f4d6c9e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -982,6 +982,23 @@ lint = ["docutils-stubs", "flake8", "mypy"] standalone = ["Sphinx (>=5)"] test = ["pytest"] +[[package]] +name = "structlog" +version = "24.1.0" +description = "Structured Logging for Python" +optional = true +python-versions = ">=3.8" +files = [ + {file = "structlog-24.1.0-py3-none-any.whl", hash = "sha256:3f6efe7d25fab6e86f277713c218044669906537bb717c1807a09d46bca0714d"}, + {file = "structlog-24.1.0.tar.gz", hash = "sha256:41a09886e4d55df25bdcb9b5c9674bccfab723ff43e0a86a1b7b236be8e57b16"}, +] + +[package.extras] +dev = ["structlog[tests,typing]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] +tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy (>=1.4)", "rich", "twisted"] + [[package]] name = "tomli" version = "2.0.1" @@ -1115,9 +1132,10 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [extras] loguru = ["loguru"] pytest = ["pytest"] +structlog = ["structlog"] trio = ["trio"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "07cb8de1b3a73f101b1eaf92087c255c52163e6611d4def2da1830b41756c208" +content-hash = "0af88140fbc1712a8cb18b9cc125ccb07135a5dd2189fcfef6a15ddc2f8b0f0c" diff --git a/pyproject.toml b/pyproject.toml index 861e08f6..300f87f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,12 +25,14 @@ packages = [{ include = "logot" }] [tool.poetry.dependencies] python = "^3.8" loguru = { version = ">=0.6,<0.8", optional = true } +structlog = { version = ">=23,<25", optional = true } pytest = { version = ">=7,<9", optional = true } trio = { version = ">=0.22,<0.25", optional = true } typing-extensions = { version = ">=4.9", python = "<3.10" } [tool.poetry.extras] loguru = ["loguru"] +structlog = ["structlog"] pytest = ["pytest"] trio = ["trio"] diff --git a/tests/test_structlog.py b/tests/test_structlog.py new file mode 100644 index 00000000..71003ca4 --- /dev/null +++ b/tests/test_structlog.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import Callable + +import pytest +from structlog import logger + +from logot import Logot, logged +from logot.structlog import StructlogCapturer + + +@pytest.fixture(scope="session") +def logot_capturer() -> Callable[[], StructlogCapturer]: + return StructlogCapturer + + +def test_capturing() -> None: + with Logot(capturer=StructlogCapturer).capturing() as logot: + # Ensure log capturing is enabled. + logger.info("foo bar") + logot.assert_logged(logged.info("foo bar")) + # Ensure log capturing is disabled. + logger.info("foo bar") + logot.assert_not_logged(logged.info("foo bar")) + + +def test_capturing_level_pass() -> None: + with Logot(capturer=StructlogCapturer).capturing(level="INFO") as logot: + logger.info("foo bar") + logot.assert_logged(logged.info("foo bar")) + + +def test_capturing_level_fail() -> None: + with Logot(capturer=StructlogCapturer).capturing(level="INFO") as logot: + logger.debug("foo bar") + logot.assert_not_logged(logged.debug("foo bar")) + + +def test_capturing_name_pass() -> None: + with Logot(capturer=StructlogCapturer).capturing(name="tests") as logot: + logger.info("foo bar") + logot.assert_logged(logged.info("foo bar")) + + +def test_capturing_name_fail() -> None: + with Logot(capturer=StructlogCapturer).capturing(name="boom") as logot: + logger.info("foo bar") + logot.assert_not_logged(logged.info("foo bar")) + + +def test_capture(logot: Logot) -> None: + logger.info("foo bar") + logot.assert_logged(logged.info("foo bar")) + + +def test_capture_levelno(logot: Logot) -> None: + logger.log(20, "foo bar") + logot.assert_logged(logged.log(20, "foo bar")) From 617f35b2943386122fa501a3ece0c203125fc3d4 Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 22:17:37 +0000 Subject: [PATCH 02/13] wip --- logot/_structlog.py | 34 +++++++++++++++++++++++----------- tests/test_structlog.py | 20 ++++++++++++++++---- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/logot/_structlog.py b/logot/_structlog.py index 4717bd2d..10829017 100644 --- a/logot/_structlog.py +++ b/logot/_structlog.py @@ -2,7 +2,7 @@ from functools import partial -from structlog import configure, get_config +import structlog from structlog.exceptions import DropEvent from structlog.processors import NAME_TO_LEVEL from structlog.typing import EventDict, WrappedLogger @@ -17,25 +17,37 @@ class StructlogCapturer(Capturer): A :class:`logot.Capturer` implementation for :mod:`structlog`. """ - __slots__ = ("_old_processors",) + __slots__ = ("_old_processors", "_old_wrapper_class") def start_capturing(self, logot: Logot, /, *, level: Level, name: Name) -> None: - processors = get_config()["processors"] + config = structlog.get_config() + processors = config["processors"] + wrapper_class = config["wrapper_class"] self._old_processors = processors.copy() + self._old_wrapper_class = wrapper_class processors.clear() - processors.append(partial(_processor, logot=logot)) - configure(processors=processors) + processors.append(partial(_processor, logot=logot, name=name)) + + if level is not None: + if isinstance(level, str): + levelno = NAME_TO_LEVEL[level.lower()] + wrapper_class = structlog.make_filtering_bound_logger(levelno) + + structlog.configure(processors=processors, wrapper_class=wrapper_class) def stop_capturing(self) -> None: - processors = get_config()["processors"] + processors = structlog.get_config()["processors"] processors.clear() processors.extend(self._old_processors) - configure(processors=processors) + structlog.configure(processors=processors, wrapper_class=self._old_wrapper_class) + +def _processor(logger: WrappedLogger, method_name: str, event_dict: EventDict, *, logot: Logot, name: Name) -> None: + msg = event_dict["event"] + level = method_name.upper() + levelno = NAME_TO_LEVEL[method_name] -def _processor(_: WrappedLogger, method_name: str, event_dict: EventDict, *, logot: Logot) -> None: - record = event_dict["event"] - level = method_name - logot.capture(Captured(level, record["message"], levelno=NAME_TO_LEVEL[level])) + if name is None or logger.name == name: + logot.capture(Captured(level, msg, levelno=levelno)) raise DropEvent diff --git a/tests/test_structlog.py b/tests/test_structlog.py index 71003ca4..3947fc32 100644 --- a/tests/test_structlog.py +++ b/tests/test_structlog.py @@ -1,13 +1,23 @@ from __future__ import annotations -from typing import Callable +from typing import Callable, Iterator import pytest -from structlog import logger +from structlog import configure, get_logger, reset_defaults +from structlog.stdlib import LoggerFactory from logot import Logot, logged from logot.structlog import StructlogCapturer +logger = get_logger() + + +@pytest.fixture +def stdlib_logger() -> Iterator[None]: + configure(logger_factory=LoggerFactory()) + yield + reset_defaults() + @pytest.fixture(scope="session") def logot_capturer() -> Callable[[], StructlogCapturer]: @@ -36,13 +46,15 @@ def test_capturing_level_fail() -> None: logot.assert_not_logged(logged.debug("foo bar")) -def test_capturing_name_pass() -> None: +def test_capturing_name_pass(stdlib_logger) -> None: + logger = get_logger("tests") with Logot(capturer=StructlogCapturer).capturing(name="tests") as logot: logger.info("foo bar") logot.assert_logged(logged.info("foo bar")) -def test_capturing_name_fail() -> None: +def test_capturing_name_fail(stdlib_logger) -> None: + logger = get_logger("tests") with Logot(capturer=StructlogCapturer).capturing(name="boom") as logot: logger.info("foo bar") logot.assert_not_logged(logged.info("foo bar")) From 102c3da98d2afd0c51dafd8453af1343d6edd3b6 Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 22:28:06 +0000 Subject: [PATCH 03/13] wip --- docs/api/logot.structlog.rst | 2 +- docs/integrations/index.rst | 1 + docs/integrations/structlog.rst | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/api/logot.structlog.rst b/docs/api/logot.structlog.rst index 0d365961..e32d2210 100644 --- a/docs/api/logot.structlog.rst +++ b/docs/api/logot.structlog.rst @@ -1,5 +1,5 @@ :mod:`logot.structlog` -=================== +======================= .. automodule:: logot.structlog diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst index 2e27d61b..58b94d55 100644 --- a/docs/integrations/index.rst +++ b/docs/integrations/index.rst @@ -47,6 +47,7 @@ Supported frameworks: :maxdepth: 1 loguru + structlog .. seealso:: diff --git a/docs/integrations/structlog.rst b/docs/integrations/structlog.rst index c3a8e4cf..9989561b 100644 --- a/docs/integrations/structlog.rst +++ b/docs/integrations/structlog.rst @@ -1,5 +1,5 @@ Using with :mod:`structlog` -======================== +============================ .. currentmodule:: logot From 36a90a94292cb977a3f9add6ed3b3fbab67ffd7e Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 22:34:54 +0000 Subject: [PATCH 04/13] wip --- docs/conf.py | 3 ++- pyproject.toml | 2 ++ tests/test_structlog.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ad97c45e..e0cc52b9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,10 @@ from __future__ import annotations -import tomllib from datetime import date from pathlib import Path +import tomllib + _root = Path(__file__).parent.parent _poetry = tomllib.loads((_root / "pyproject.toml").read_text())["tool"]["poetry"] diff --git a/pyproject.toml b/pyproject.toml index 300f87f4..fee48853 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,8 @@ addopts = "--tb=native --import-mode=importlib" [tool.ruff] include = ["docs/**/*.py", "logot/**/*.py", "tests/**/*.py"] line-length = 120 + +[tool.ruff.lint] select = ["E", "F", "W", "I", "UP"] [build-system] diff --git a/tests/test_structlog.py b/tests/test_structlog.py index 3947fc32..7728bd78 100644 --- a/tests/test_structlog.py +++ b/tests/test_structlog.py @@ -46,14 +46,14 @@ def test_capturing_level_fail() -> None: logot.assert_not_logged(logged.debug("foo bar")) -def test_capturing_name_pass(stdlib_logger) -> None: +def test_capturing_name_pass(stdlib_logger: None) -> None: logger = get_logger("tests") with Logot(capturer=StructlogCapturer).capturing(name="tests") as logot: logger.info("foo bar") logot.assert_logged(logged.info("foo bar")) -def test_capturing_name_fail(stdlib_logger) -> None: +def test_capturing_name_fail(stdlib_logger: None) -> None: logger = get_logger("tests") with Logot(capturer=StructlogCapturer).capturing(name="boom") as logot: logger.info("foo bar") From 773f45a74c3d50e363ec6fef6d847c5b92bd3814 Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 22:41:46 +0000 Subject: [PATCH 05/13] wip --- .github/workflows/build.yml | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4590e96..23a7f3dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,9 @@ jobs: - python-version: "3.10" - python-version: "3.11" - python-version: "3.12" + - lib-versions: "structlog~=20.1.0" + - lib-versions: "structlog~=21.1.0" + - lib-versions: "structlog~=22.1.0" - lib-versions: "structlog~=23.1.0" - lib-versions: "structlog~=24.1.0" - lib-versions: "loguru~=0.6.0" diff --git a/pyproject.toml b/pyproject.toml index fee48853..4807539d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ packages = [{ include = "logot" }] [tool.poetry.dependencies] python = "^3.8" loguru = { version = ">=0.6,<0.8", optional = true } -structlog = { version = ">=23,<25", optional = true } +structlog = { version = ">=20,<25", optional = true } pytest = { version = ">=7,<9", optional = true } trio = { version = ">=0.22,<0.25", optional = true } typing-extensions = { version = ">=4.9", python = "<3.10" } From 49ea0322f8d4370f1c927efaf09a0f0f9c33d5b1 Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 22:47:20 +0000 Subject: [PATCH 06/13] lockfile --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 3f4d6c9e..91c4e0e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1138,4 +1138,4 @@ trio = ["trio"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "0af88140fbc1712a8cb18b9cc125ccb07135a5dd2189fcfef6a15ddc2f8b0f0c" +content-hash = "666c7653bfef885185a9b081e100779b7b507d2f8faed237f3a68161a39563ca" From 72dae5eb392e359d614cc97b020bdd8c35572e3e Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 23:14:07 +0000 Subject: [PATCH 07/13] wip --- .github/workflows/build.yml | 148 ++++++++++++++++++------------------ logot/_structlog.py | 27 ++++++- poetry.lock | 18 ++--- pyproject.toml | 2 +- tests/test_structlog.py | 13 ++-- 5 files changed, 116 insertions(+), 92 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23a7f3dd..4a01ed9f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - python-version: "3.10" - python-version: "3.11" - python-version: "3.12" - - lib-versions: "structlog~=20.1.0" + - lib-versions: "structlog~=20.2.0" - lib-versions: "structlog~=21.1.0" - lib-versions: "structlog~=22.1.0" - lib-versions: "structlog~=23.1.0" @@ -36,86 +36,86 @@ jobs: COVERAGE_FILE: .coverage.${{ matrix.python-version }}${{ matrix.lib-versions }} PYTHONDEVMODE: 1 steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - with: - python-version: ${{ matrix.python-version }} - # Install dependencies. - # This is done in two steps - first we install the `poetry` dev dependencies, then we install the project in - # editable mode with compatible `extra` dependencies from the test matrix. This is a compromise that allows us to - # largely rely on the `poetry` lockfile while still testing against multiple `extra` library versions. - - name: Install dependencies - run: poetry install --all-extras --no-root - - name: Install lib versions - run: pip install -e .[pytest,trio] ${{ matrix.lib-versions }} - # Run checks. - - name: Check (ruff) - run: ruff check - - name: Check (ruff format) - run: ruff format --check - - name: Check (mypy) - run: mypy - # Run tests. - - name: Test - run: coverage run -m pytest - # Upload coverage. - - name: Upload coverage - uses: actions/upload-artifact@v4 - with: - name: ${{ env.COVERAGE_FILE }} - path: ${{ env.COVERAGE_FILE }} + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + # Install dependencies. + # This is done in two steps - first we install the `poetry` dev dependencies, then we install the project in + # editable mode with compatible `extra` dependencies from the test matrix. This is a compromise that allows us to + # largely rely on the `poetry` lockfile while still testing against multiple `extra` library versions. + - name: Install dependencies + run: poetry install --all-extras --no-root + - name: Install lib versions + run: pip install -e .[pytest,trio] ${{ matrix.lib-versions }} + # Run checks. + - name: Check (ruff) + run: ruff check + - name: Check (ruff format) + run: ruff format --check + - name: Check (mypy) + run: mypy + # Run tests. + - name: Test + run: coverage run -m pytest + # Upload coverage. + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: ${{ env.COVERAGE_FILE }} + path: ${{ env.COVERAGE_FILE }} docs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - # Install dependencies. - - name: Install dependencies - run: poetry install --all-extras - # Build docs. - - name: Build docs - run: sphinx-build -W docs docs/_build + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + # Install dependencies. + - name: Install dependencies + run: poetry install --all-extras + # Build docs. + - name: Build docs + run: sphinx-build -W docs docs/_build report: runs-on: ubuntu-latest needs: - - test - - docs + - test + - docs if: always() steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - # Install dependencies. - - name: Install dependencies - run: poetry install --all-extras - # Report coverage. - - name: Download coverage - uses: actions/download-artifact@v4 - with: - pattern: .coverage.* - merge-multiple: true - - name: Combine coverage - run: coverage combine .coverage.* - - name: Report coverage - run: coverage report - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - # Fail if any `needs` job was not a success. - # Along with `if: always()`, this allows this job to act as a single required status check for the entire workflow. - - name: Fail on workflow error - run: exit 1 - if: >- - ${{ - contains(needs.*.result, 'failure') - || contains(needs.*.result, 'cancelled') - || contains(needs.*.result, 'skipped') - }} + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + # Install dependencies. + - name: Install dependencies + run: poetry install --all-extras + # Report coverage. + - name: Download coverage + uses: actions/download-artifact@v4 + with: + pattern: .coverage.* + merge-multiple: true + - name: Combine coverage + run: coverage combine .coverage.* + - name: Report coverage + run: coverage report + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # Fail if any `needs` job was not a success. + # Along with `if: always()`, this allows this job to act as a single required status check for the entire workflow. + - name: Fail on workflow error + run: exit 1 + if: >- + ${{ + contains(needs.*.result, 'failure') + || contains(needs.*.result, 'cancelled') + || contains(needs.*.result, 'skipped') + }} diff --git a/logot/_structlog.py b/logot/_structlog.py index 10829017..fd0ee472 100644 --- a/logot/_structlog.py +++ b/logot/_structlog.py @@ -1,16 +1,39 @@ from __future__ import annotations from functools import partial +from typing import Any, MutableMapping import structlog from structlog.exceptions import DropEvent -from structlog.processors import NAME_TO_LEVEL -from structlog.typing import EventDict, WrappedLogger from logot._capture import Captured from logot._logot import Capturer, Logot from logot._typing import Level, Name +EventDict = MutableMapping[str, Any] +WrappedLogger = Any + + +CRITICAL = 50 +FATAL = CRITICAL +ERROR = 40 +WARNING = 30 +WARN = WARNING +INFO = 20 +DEBUG = 10 +NOTSET = 0 + +NAME_TO_LEVEL = { + "critical": CRITICAL, + "exception": ERROR, + "error": ERROR, + "warn": WARNING, + "warning": WARNING, + "info": INFO, + "debug": DEBUG, + "notset": NOTSET, +} + class StructlogCapturer(Capturer): """ diff --git a/poetry.lock b/poetry.lock index 91c4e0e9..11c033a4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -984,20 +984,20 @@ test = ["pytest"] [[package]] name = "structlog" -version = "24.1.0" +version = "22.3.0" description = "Structured Logging for Python" optional = true -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "structlog-24.1.0-py3-none-any.whl", hash = "sha256:3f6efe7d25fab6e86f277713c218044669906537bb717c1807a09d46bca0714d"}, - {file = "structlog-24.1.0.tar.gz", hash = "sha256:41a09886e4d55df25bdcb9b5c9674bccfab723ff43e0a86a1b7b236be8e57b16"}, + {file = "structlog-22.3.0-py3-none-any.whl", hash = "sha256:b403f344f902b220648fa9f286a23c0cc5439a5844d271fec40562dbadbc70ad"}, + {file = "structlog-22.3.0.tar.gz", hash = "sha256:e7509391f215e4afb88b1b80fa3ea074be57a5a17d794bd436a5c949da023333"}, ] [package.extras] -dev = ["structlog[tests,typing]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] -tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] -typing = ["mypy (>=1.4)", "rich", "twisted"] +dev = ["structlog[docs,tests,typing]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] +tests = ["coverage[toml]", "freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy", "rich", "twisted"] [[package]] name = "tomli" @@ -1138,4 +1138,4 @@ trio = ["trio"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "666c7653bfef885185a9b081e100779b7b507d2f8faed237f3a68161a39563ca" +content-hash = "499e636f536573a2f8f5b2e99baafb2b60bf28ffff455b0b019698aa77d630d6" diff --git a/pyproject.toml b/pyproject.toml index 4807539d..f3eb8916 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ packages = [{ include = "logot" }] [tool.poetry.dependencies] python = "^3.8" loguru = { version = ">=0.6,<0.8", optional = true } -structlog = { version = ">=20,<25", optional = true } +structlog = { version = ">=20.2,<23", optional = true } pytest = { version = ">=7,<9", optional = true } trio = { version = ">=0.22,<0.25", optional = true } typing-extensions = { version = ">=4.9", python = "<3.10" } diff --git a/tests/test_structlog.py b/tests/test_structlog.py index 7728bd78..8a604066 100644 --- a/tests/test_structlog.py +++ b/tests/test_structlog.py @@ -3,20 +3,20 @@ from typing import Callable, Iterator import pytest -from structlog import configure, get_logger, reset_defaults +import structlog from structlog.stdlib import LoggerFactory from logot import Logot, logged from logot.structlog import StructlogCapturer -logger = get_logger() +logger = structlog.get_logger() @pytest.fixture def stdlib_logger() -> Iterator[None]: - configure(logger_factory=LoggerFactory()) + structlog.configure(logger_factory=LoggerFactory()) yield - reset_defaults() + structlog.reset_defaults() @pytest.fixture(scope="session") @@ -47,14 +47,14 @@ def test_capturing_level_fail() -> None: def test_capturing_name_pass(stdlib_logger: None) -> None: - logger = get_logger("tests") + logger = structlog.get_logger("tests") with Logot(capturer=StructlogCapturer).capturing(name="tests") as logot: logger.info("foo bar") logot.assert_logged(logged.info("foo bar")) def test_capturing_name_fail(stdlib_logger: None) -> None: - logger = get_logger("tests") + logger = structlog.get_logger("tests") with Logot(capturer=StructlogCapturer).capturing(name="boom") as logot: logger.info("foo bar") logot.assert_not_logged(logged.info("foo bar")) @@ -65,6 +65,7 @@ def test_capture(logot: Logot) -> None: logot.assert_logged(logged.info("foo bar")) +@pytest.mark.skipif(structlog.__version__ < "22", reason="requires structlog>=22") def test_capture_levelno(logot: Logot) -> None: logger.log(20, "foo bar") logot.assert_logged(logged.log(20, "foo bar")) From 0496431e07b4ab1fd75f03f52bc535a343b55c7c Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 23:14:23 +0000 Subject: [PATCH 08/13] wip --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f3eb8916..d217f758 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ packages = [{ include = "logot" }] [tool.poetry.dependencies] python = "^3.8" loguru = { version = ">=0.6,<0.8", optional = true } -structlog = { version = ">=20.2,<23", optional = true } +structlog = { version = ">=20.2,<25", optional = true } pytest = { version = ">=7,<9", optional = true } trio = { version = ">=0.22,<0.25", optional = true } typing-extensions = { version = ">=4.9", python = "<3.10" } From ad2301f40b3b9bad2cfd6183470b49c76e1f59a1 Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 23:15:12 +0000 Subject: [PATCH 09/13] wip --- poetry.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index 11c033a4..612feb9e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -984,20 +984,20 @@ test = ["pytest"] [[package]] name = "structlog" -version = "22.3.0" +version = "24.1.0" description = "Structured Logging for Python" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "structlog-22.3.0-py3-none-any.whl", hash = "sha256:b403f344f902b220648fa9f286a23c0cc5439a5844d271fec40562dbadbc70ad"}, - {file = "structlog-22.3.0.tar.gz", hash = "sha256:e7509391f215e4afb88b1b80fa3ea074be57a5a17d794bd436a5c949da023333"}, + {file = "structlog-24.1.0-py3-none-any.whl", hash = "sha256:3f6efe7d25fab6e86f277713c218044669906537bb717c1807a09d46bca0714d"}, + {file = "structlog-24.1.0.tar.gz", hash = "sha256:41a09886e4d55df25bdcb9b5c9674bccfab723ff43e0a86a1b7b236be8e57b16"}, ] [package.extras] -dev = ["structlog[docs,tests,typing]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] -tests = ["coverage[toml]", "freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] -typing = ["mypy", "rich", "twisted"] +dev = ["structlog[tests,typing]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] +tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy (>=1.4)", "rich", "twisted"] [[package]] name = "tomli" @@ -1138,4 +1138,4 @@ trio = ["trio"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "499e636f536573a2f8f5b2e99baafb2b60bf28ffff455b0b019698aa77d630d6" +content-hash = "5d2f8a9ef941278de179da239a750350415ea5a4991a2f547b33b6739abf2268" From 36335a6d2489359b4414b25e4caa30ce05d8bbb2 Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Wed, 14 Feb 2024 23:18:50 +0000 Subject: [PATCH 10/13] wip --- .github/workflows/build.yml | 146 ++++++++++++++++++------------------ 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a01ed9f..46b63411 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,86 +36,86 @@ jobs: COVERAGE_FILE: .coverage.${{ matrix.python-version }}${{ matrix.lib-versions }} PYTHONDEVMODE: 1 steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - with: - python-version: ${{ matrix.python-version }} - # Install dependencies. - # This is done in two steps - first we install the `poetry` dev dependencies, then we install the project in - # editable mode with compatible `extra` dependencies from the test matrix. This is a compromise that allows us to - # largely rely on the `poetry` lockfile while still testing against multiple `extra` library versions. - - name: Install dependencies - run: poetry install --all-extras --no-root - - name: Install lib versions - run: pip install -e .[pytest,trio] ${{ matrix.lib-versions }} - # Run checks. - - name: Check (ruff) - run: ruff check - - name: Check (ruff format) - run: ruff format --check - - name: Check (mypy) - run: mypy - # Run tests. - - name: Test - run: coverage run -m pytest - # Upload coverage. - - name: Upload coverage - uses: actions/upload-artifact@v4 - with: - name: ${{ env.COVERAGE_FILE }} - path: ${{ env.COVERAGE_FILE }} + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + # Install dependencies. + # This is done in two steps - first we install the `poetry` dev dependencies, then we install the project in + # editable mode with compatible `extra` dependencies from the test matrix. This is a compromise that allows us to + # largely rely on the `poetry` lockfile while still testing against multiple `extra` library versions. + - name: Install dependencies + run: poetry install --all-extras --no-root + - name: Install lib versions + run: pip install -e .[pytest,trio] ${{ matrix.lib-versions }} + # Run checks. + - name: Check (ruff) + run: ruff check + - name: Check (ruff format) + run: ruff format --check + - name: Check (mypy) + run: mypy + # Run tests. + - name: Test + run: coverage run -m pytest + # Upload coverage. + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: ${{ env.COVERAGE_FILE }} + path: ${{ env.COVERAGE_FILE }} docs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - # Install dependencies. - - name: Install dependencies - run: poetry install --all-extras - # Build docs. - - name: Build docs - run: sphinx-build -W docs docs/_build + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + # Install dependencies. + - name: Install dependencies + run: poetry install --all-extras + # Build docs. + - name: Build docs + run: sphinx-build -W docs docs/_build report: runs-on: ubuntu-latest needs: - - test - - docs + - test + - docs if: always() steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - # Install dependencies. - - name: Install dependencies - run: poetry install --all-extras - # Report coverage. - - name: Download coverage - uses: actions/download-artifact@v4 - with: - pattern: .coverage.* - merge-multiple: true - - name: Combine coverage - run: coverage combine .coverage.* - - name: Report coverage - run: coverage report - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - # Fail if any `needs` job was not a success. - # Along with `if: always()`, this allows this job to act as a single required status check for the entire workflow. - - name: Fail on workflow error - run: exit 1 - if: >- - ${{ - contains(needs.*.result, 'failure') - || contains(needs.*.result, 'cancelled') - || contains(needs.*.result, 'skipped') - }} + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + # Install dependencies. + - name: Install dependencies + run: poetry install --all-extras + # Report coverage. + - name: Download coverage + uses: actions/download-artifact@v4 + with: + pattern: .coverage.* + merge-multiple: true + - name: Combine coverage + run: coverage combine .coverage.* + - name: Report coverage + run: coverage report + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # Fail if any `needs` job was not a success. + # Along with `if: always()`, this allows this job to act as a single required status check for the entire workflow. + - name: Fail on workflow error + run: exit 1 + if: >- + ${{ + contains(needs.*.result, 'failure') + || contains(needs.*.result, 'cancelled') + || contains(needs.*.result, 'skipped') + }} From 4522587a5a1aeea93942e1300f587efbb1cacf93 Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Sat, 17 Feb 2024 11:50:57 +0000 Subject: [PATCH 11/13] review changes --- .github/workflows/build.yml | 151 ++++++++++++++++---------------- docs/integrations/structlog.rst | 5 ++ logot/_structlog.py | 69 +++++---------- poetry.lock | 12 +-- pyproject.toml | 2 +- tests/test_structlog.py | 13 +++ 6 files changed, 122 insertions(+), 130 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46b63411..051062de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,7 @@ jobs: - python-version: "3.10" - python-version: "3.11" - python-version: "3.12" - - lib-versions: "structlog~=20.2.0" - - lib-versions: "structlog~=21.1.0" - - lib-versions: "structlog~=22.1.0" - - lib-versions: "structlog~=23.1.0" + - lib-versions: "structlog~=23.3.0" - lib-versions: "structlog~=24.1.0" - lib-versions: "loguru~=0.6.0" - lib-versions: "loguru~=0.7.0" @@ -36,86 +33,86 @@ jobs: COVERAGE_FILE: .coverage.${{ matrix.python-version }}${{ matrix.lib-versions }} PYTHONDEVMODE: 1 steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - with: - python-version: ${{ matrix.python-version }} - # Install dependencies. - # This is done in two steps - first we install the `poetry` dev dependencies, then we install the project in - # editable mode with compatible `extra` dependencies from the test matrix. This is a compromise that allows us to - # largely rely on the `poetry` lockfile while still testing against multiple `extra` library versions. - - name: Install dependencies - run: poetry install --all-extras --no-root - - name: Install lib versions - run: pip install -e .[pytest,trio] ${{ matrix.lib-versions }} - # Run checks. - - name: Check (ruff) - run: ruff check - - name: Check (ruff format) - run: ruff format --check - - name: Check (mypy) - run: mypy - # Run tests. - - name: Test - run: coverage run -m pytest - # Upload coverage. - - name: Upload coverage - uses: actions/upload-artifact@v4 - with: - name: ${{ env.COVERAGE_FILE }} - path: ${{ env.COVERAGE_FILE }} + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + # Install dependencies. + # This is done in two steps - first we install the `poetry` dev dependencies, then we install the project in + # editable mode with compatible `extra` dependencies from the test matrix. This is a compromise that allows us to + # largely rely on the `poetry` lockfile while still testing against multiple `extra` library versions. + - name: Install dependencies + run: poetry install --all-extras --no-root + - name: Install lib versions + run: pip install -e .[pytest,trio] ${{ matrix.lib-versions }} + # Run checks. + - name: Check (ruff) + run: ruff check + - name: Check (ruff format) + run: ruff format --check + - name: Check (mypy) + run: mypy + # Run tests. + - name: Test + run: coverage run -m pytest + # Upload coverage. + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: ${{ env.COVERAGE_FILE }} + path: ${{ env.COVERAGE_FILE }} docs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - # Install dependencies. - - name: Install dependencies - run: poetry install --all-extras - # Build docs. - - name: Build docs - run: sphinx-build -W docs docs/_build + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + # Install dependencies. + - name: Install dependencies + run: poetry install --all-extras + # Build docs. + - name: Build docs + run: sphinx-build -W docs docs/_build report: runs-on: ubuntu-latest needs: - - test - - docs + - test + - docs if: always() steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - # Install dependencies. - - name: Install dependencies - run: poetry install --all-extras - # Report coverage. - - name: Download coverage - uses: actions/download-artifact@v4 - with: - pattern: .coverage.* - merge-multiple: true - - name: Combine coverage - run: coverage combine .coverage.* - - name: Report coverage - run: coverage report - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - # Fail if any `needs` job was not a success. - # Along with `if: always()`, this allows this job to act as a single required status check for the entire workflow. - - name: Fail on workflow error - run: exit 1 - if: >- - ${{ - contains(needs.*.result, 'failure') - || contains(needs.*.result, 'cancelled') - || contains(needs.*.result, 'skipped') - }} + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + # Install dependencies. + - name: Install dependencies + run: poetry install --all-extras + # Report coverage. + - name: Download coverage + uses: actions/download-artifact@v4 + with: + pattern: .coverage.* + merge-multiple: true + - name: Combine coverage + run: coverage combine .coverage.* + - name: Report coverage + run: coverage report + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # Fail if any `needs` job was not a success. + # Along with `if: always()`, this allows this job to act as a single required status check for the entire workflow. + - name: Fail on workflow error + run: exit 1 + if: >- + ${{ + contains(needs.*.result, 'failure') + || contains(needs.*.result, 'cancelled') + || contains(needs.*.result, 'skipped') + }} diff --git a/docs/integrations/structlog.rst b/docs/integrations/structlog.rst index 9989561b..4feb1d5c 100644 --- a/docs/integrations/structlog.rst +++ b/docs/integrations/structlog.rst @@ -13,6 +13,11 @@ Using with :mod:`structlog` do_something() logot.assert_logged(logged.info("App started")) +:mod:`logot` preserves the preconfigured :mod:`structlog` processor chain. Events are captured at the end of the chain, +but before the final processor, as it is responsible for emitting the log event to the underlying logging system. For +more information, see the +`structlog documentation `_. + Installing ---------- diff --git a/logot/_structlog.py b/logot/_structlog.py index fd0ee472..f1dd4415 100644 --- a/logot/_structlog.py +++ b/logot/_structlog.py @@ -1,76 +1,53 @@ from __future__ import annotations from functools import partial -from typing import Any, MutableMapping import structlog -from structlog.exceptions import DropEvent +from structlog.processors import NAME_TO_LEVEL +from structlog.types import EventDict, WrappedLogger from logot._capture import Captured from logot._logot import Capturer, Logot from logot._typing import Level, Name -EventDict = MutableMapping[str, Any] -WrappedLogger = Any - - -CRITICAL = 50 -FATAL = CRITICAL -ERROR = 40 -WARNING = 30 -WARN = WARNING -INFO = 20 -DEBUG = 10 -NOTSET = 0 - -NAME_TO_LEVEL = { - "critical": CRITICAL, - "exception": ERROR, - "error": ERROR, - "warn": WARNING, - "warning": WARNING, - "info": INFO, - "debug": DEBUG, - "notset": NOTSET, -} - class StructlogCapturer(Capturer): """ A :class:`logot.Capturer` implementation for :mod:`structlog`. """ - __slots__ = ("_old_processors", "_old_wrapper_class") + __slots__ = ("_old_processors",) def start_capturing(self, logot: Logot, /, *, level: Level, name: Name) -> None: config = structlog.get_config() processors = config["processors"] - wrapper_class = config["wrapper_class"] - self._old_processors = processors.copy() - self._old_wrapper_class = wrapper_class - processors.clear() - processors.append(partial(_processor, logot=logot, name=name)) + self._old_processors = processors - if level is not None: - if isinstance(level, str): - levelno = NAME_TO_LEVEL[level.lower()] - wrapper_class = structlog.make_filtering_bound_logger(levelno) + if isinstance(level, str): + levelno = NAME_TO_LEVEL[level.lower()] + else: + levelno = level - structlog.configure(processors=processors, wrapper_class=wrapper_class) + # We need to insert our processor before the last processor, as this is the processor that transforms the + # `event_dict` into the final log message. As this depends on the wrapped logger's formatting requirements, + # it can interfere with our capturing. + # See https://www.structlog.org/en/stable/processors.html#adapting-and-rendering + structlog.configure( + processors=[*processors[:-1], partial(_processor, logot=logot, name=name, levelno=levelno), processors[-1]] + ) def stop_capturing(self) -> None: - processors = structlog.get_config()["processors"] - processors.clear() - processors.extend(self._old_processors) - structlog.configure(processors=processors, wrapper_class=self._old_wrapper_class) + structlog.configure(processors=self._old_processors) -def _processor(logger: WrappedLogger, method_name: str, event_dict: EventDict, *, logot: Logot, name: Name) -> None: +def _processor( + logger: WrappedLogger, method_name: str, event_dict: EventDict, *, logot: Logot, name: Name, levelno: int +) -> EventDict: msg = event_dict["event"] level = method_name.upper() - levelno = NAME_TO_LEVEL[method_name] + event_levelno = NAME_TO_LEVEL[method_name] - if name is None or logger.name == name: - logot.capture(Captured(level, msg, levelno=levelno)) + if getattr(logger, "name", None) == name and event_levelno >= levelno: + logot.capture(Captured(level, msg, levelno=event_levelno)) - raise DropEvent + return event_dict diff --git a/poetry.lock b/poetry.lock index 612feb9e..38d098f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -369,13 +369,13 @@ sphinx-basic-ng = "*" [[package]] name = "hypothesis" -version = "6.98.4" +version = "6.98.5" description = "A library for property-based testing" optional = false python-versions = ">=3.8" files = [ - {file = "hypothesis-6.98.4-py3-none-any.whl", hash = "sha256:8417d1df13e7ba0eb6cba0917e0aa6c8b0b6b35a4e7fb78db6ab84dfbeb8c8fe"}, - {file = "hypothesis-6.98.4.tar.gz", hash = "sha256:785f47ddac183c7ffef9463b5ab7f2e4433ca9b2b1171e52eeb3f8c5b1f09fa2"}, + {file = "hypothesis-6.98.5-py3-none-any.whl", hash = "sha256:9449b9878116133269da4941b6a20e83003ef95503a2106365d4756ef3adc2b7"}, + {file = "hypothesis-6.98.5.tar.gz", hash = "sha256:cfe4c2320580f97dd0d11cd3ee954a347764aec42aa0c95b7a0285c2b02447ab"}, ] [package.dependencies] @@ -384,7 +384,7 @@ exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2023.4)"] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] dateutil = ["python-dateutil (>=1.4)"] @@ -397,7 +397,7 @@ pandas = ["pandas (>=1.1)"] pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] -zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2023.4)"] +zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.1)"] [[package]] name = "idna" @@ -1138,4 +1138,4 @@ trio = ["trio"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "5d2f8a9ef941278de179da239a750350415ea5a4991a2f547b33b6739abf2268" +content-hash = "63fcb3851814034bf59fd4e861abc8727b75025b58cc16d26c21b8bc9a90fd01" diff --git a/pyproject.toml b/pyproject.toml index d217f758..669e095e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ packages = [{ include = "logot" }] [tool.poetry.dependencies] python = "^3.8" loguru = { version = ">=0.6,<0.8", optional = true } -structlog = { version = ">=20.2,<25", optional = true } +structlog = { version = ">=23.3,<25", optional = true } pytest = { version = ">=7,<9", optional = true } trio = { version = ">=0.22,<0.25", optional = true } typing-extensions = { version = ">=4.9", python = "<3.10" } diff --git a/tests/test_structlog.py b/tests/test_structlog.py index 8a604066..b2d9d5c5 100644 --- a/tests/test_structlog.py +++ b/tests/test_structlog.py @@ -34,6 +34,19 @@ def test_capturing() -> None: logot.assert_not_logged(logged.info("foo bar")) +def test_multiple_capturing() -> None: + with Logot(capturer=StructlogCapturer).capturing() as logot_1: + with Logot(capturer=StructlogCapturer).capturing() as logot_2: + # Ensure log capturing is enabled. + logger.info("foo bar") + logot_1.assert_logged(logged.info("foo bar")) + logot_2.assert_logged(logged.info("foo bar")) + # Ensure log capturing is disabled. + logger.info("foo bar") + logot_1.assert_not_logged(logged.info("foo bar")) + logot_2.assert_not_logged(logged.info("foo bar")) + + def test_capturing_level_pass() -> None: with Logot(capturer=StructlogCapturer).capturing(level="INFO") as logot: logger.info("foo bar") From 1a54966d88b5d29dd860292451871832b8a33c8e Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Sat, 17 Feb 2024 12:18:33 +0000 Subject: [PATCH 12/13] full coverage --- tests/test_structlog.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_structlog.py b/tests/test_structlog.py index b2d9d5c5..bb98f0b0 100644 --- a/tests/test_structlog.py +++ b/tests/test_structlog.py @@ -59,6 +59,18 @@ def test_capturing_level_fail() -> None: logot.assert_not_logged(logged.debug("foo bar")) +def test_capturing_level_as_int_pass() -> None: + with Logot(capturer=StructlogCapturer).capturing(level=20) as logot: + logger.info("foo bar") + logot.assert_logged(logged.info("foo bar")) + + +def test_capturing_level_as_int_fail() -> None: + with Logot(capturer=StructlogCapturer).capturing(level=20) as logot: + logger.debug("foo bar") + logot.assert_not_logged(logged.debug("foo bar")) + + def test_capturing_name_pass(stdlib_logger: None) -> None: logger = structlog.get_logger("tests") with Logot(capturer=StructlogCapturer).capturing(name="tests") as logot: @@ -78,7 +90,6 @@ def test_capture(logot: Logot) -> None: logot.assert_logged(logged.info("foo bar")) -@pytest.mark.skipif(structlog.__version__ < "22", reason="requires structlog>=22") def test_capture_levelno(logot: Logot) -> None: logger.log(20, "foo bar") logot.assert_logged(logged.log(20, "foo bar")) From 3661e0ddeb13ccb61075a1b31401bbe70b6675ee Mon Sep 17 00:00:00 2001 From: Will Ockmore Date: Sat, 17 Feb 2024 12:22:33 +0000 Subject: [PATCH 13/13] revert spacing --- .github/workflows/build.yml | 146 ++++++++++++++++++------------------ 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 051062de..7e4b7b3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,86 +33,86 @@ jobs: COVERAGE_FILE: .coverage.${{ matrix.python-version }}${{ matrix.lib-versions }} PYTHONDEVMODE: 1 steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - with: - python-version: ${{ matrix.python-version }} - # Install dependencies. - # This is done in two steps - first we install the `poetry` dev dependencies, then we install the project in - # editable mode with compatible `extra` dependencies from the test matrix. This is a compromise that allows us to - # largely rely on the `poetry` lockfile while still testing against multiple `extra` library versions. - - name: Install dependencies - run: poetry install --all-extras --no-root - - name: Install lib versions - run: pip install -e .[pytest,trio] ${{ matrix.lib-versions }} - # Run checks. - - name: Check (ruff) - run: ruff check - - name: Check (ruff format) - run: ruff format --check - - name: Check (mypy) - run: mypy - # Run tests. - - name: Test - run: coverage run -m pytest - # Upload coverage. - - name: Upload coverage - uses: actions/upload-artifact@v4 - with: - name: ${{ env.COVERAGE_FILE }} - path: ${{ env.COVERAGE_FILE }} + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + # Install dependencies. + # This is done in two steps - first we install the `poetry` dev dependencies, then we install the project in + # editable mode with compatible `extra` dependencies from the test matrix. This is a compromise that allows us to + # largely rely on the `poetry` lockfile while still testing against multiple `extra` library versions. + - name: Install dependencies + run: poetry install --all-extras --no-root + - name: Install lib versions + run: pip install -e .[pytest,trio] ${{ matrix.lib-versions }} + # Run checks. + - name: Check (ruff) + run: ruff check + - name: Check (ruff format) + run: ruff format --check + - name: Check (mypy) + run: mypy + # Run tests. + - name: Test + run: coverage run -m pytest + # Upload coverage. + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: ${{ env.COVERAGE_FILE }} + path: ${{ env.COVERAGE_FILE }} docs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - # Install dependencies. - - name: Install dependencies - run: poetry install --all-extras - # Build docs. - - name: Build docs - run: sphinx-build -W docs docs/_build + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + # Install dependencies. + - name: Install dependencies + run: poetry install --all-extras + # Build docs. + - name: Build docs + run: sphinx-build -W docs docs/_build report: runs-on: ubuntu-latest needs: - - test - - docs + - test + - docs if: always() steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup - uses: ./.github/actions/setup - # Install dependencies. - - name: Install dependencies - run: poetry install --all-extras - # Report coverage. - - name: Download coverage - uses: actions/download-artifact@v4 - with: - pattern: .coverage.* - merge-multiple: true - - name: Combine coverage - run: coverage combine .coverage.* - - name: Report coverage - run: coverage report - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - # Fail if any `needs` job was not a success. - # Along with `if: always()`, this allows this job to act as a single required status check for the entire workflow. - - name: Fail on workflow error - run: exit 1 - if: >- - ${{ - contains(needs.*.result, 'failure') - || contains(needs.*.result, 'cancelled') - || contains(needs.*.result, 'skipped') - }} + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + uses: ./.github/actions/setup + # Install dependencies. + - name: Install dependencies + run: poetry install --all-extras + # Report coverage. + - name: Download coverage + uses: actions/download-artifact@v4 + with: + pattern: .coverage.* + merge-multiple: true + - name: Combine coverage + run: coverage combine .coverage.* + - name: Report coverage + run: coverage report + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # Fail if any `needs` job was not a success. + # Along with `if: always()`, this allows this job to act as a single required status check for the entire workflow. + - name: Fail on workflow error + run: exit 1 + if: >- + ${{ + contains(needs.*.result, 'failure') + || contains(needs.*.result, 'cancelled') + || contains(needs.*.result, 'skipped') + }}