Repository: etianen/logot (language: python)

Issue:
Create a `unittest.TestCase` mixin that starts log capture on every test
This would make it much easier to get started with `unittest`!


Discussion / hints:

Fix patch:
diff --git a/docs/api/logot.logged.rst b/docs/api/logot.logged.rst
index 944b74c0..78c25466 100644
--- a/docs/api/logot.logged.rst
+++ b/docs/api/logot.logged.rst
@@ -7,14 +7,14 @@
 API reference
 -------------
 
-.. autofunction:: logot.logged.log
+.. autofunction:: log
 
-.. autofunction:: logot.logged.debug
+.. autofunction:: debug
 
-.. autofunction:: logot.logged.info
+.. autofunction:: info
 
-.. autofunction:: logot.logged.warning
+.. autofunction:: warning
 
-.. autofunction:: logot.logged.error
+.. autofunction:: error
 
-.. autofunction:: logot.logged.critical
+.. autofunction:: critical
diff --git a/docs/api/logot.rst b/docs/api/logot.rst
index 45b7ab0d..b6f2f247 100644
--- a/docs/api/logot.rst
+++ b/docs/api/logot.rst
@@ -7,10 +7,10 @@
 API reference
 -------------
 
-.. autoclass:: logot.Logot
+.. autoclass:: Logot
    :members:
 
-.. autoclass:: logot.Logged
+.. autoclass:: Logged
 
-.. autoclass:: logot.Captured
+.. autoclass:: Captured
    :members:
diff --git a/docs/conf.py b/docs/conf.py
index ce597b03..bf52ec02 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -22,7 +22,6 @@
     "sphinx.ext.intersphinx",
 ]
 
-autoclass_content = "both"
 autodoc_member_order = "bysource"
 autodoc_typehints = "both"
 
diff --git a/docs/installing.rst b/docs/installing.rst
index 10cff8f5..bdc291d3 100644
--- a/docs/installing.rst
+++ b/docs/installing.rst
@@ -24,7 +24,7 @@ Installing package ``extras``
 
    pip install 'logot[pytest]'
 
-Supported 3rd-party integrations:
+Package extras for supported 3rd-party integrations:
 
 - ``pytest`` - :doc:`usage-pytest`
 
diff --git a/docs/log-capturing.rst b/docs/log-capturing.rst
index 5e1a4ac9..2ec74c96 100644
--- a/docs/log-capturing.rst
+++ b/docs/log-capturing.rst
@@ -11,14 +11,18 @@ Log capturing
       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 </usage-pytest>` and skip manually configuring log capture. 💪
+Test framework integrations
+---------------------------
+
+Use a supported test framework integration for automatic log capturing in tests:
+
+- :doc:`/usage-pytest`
+- :doc:`/usage-unittest`
 
 
 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`:
@@ -40,7 +44,7 @@ careful to avoid capturing duplicate logs with overlapping calls to :meth:`Logot
 .. _log-capturing-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`:
diff --git a/pyproject.toml b/pyproject.toml
index b0defef2..43bc0103 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "logot"
-version = "0.3.1"
+version = "0.3.2"
 description = "Log-based testing"
 authors = ["Dave Hall <dave@etianen.com>"]
 license = "MIT"


Test patch:
diff --git a/docs/api/logot.unittest.rst b/docs/api/logot.unittest.rst
new file mode 100644
index 00000000..70e98250
--- /dev/null
+++ b/docs/api/logot.unittest.rst
@@ -0,0 +1,12 @@
+:mod:`logot.unittest`
+=====================
+
+.. automodule:: logot.unittest
+
+
+API reference
+-------------
+
+.. autoclass:: LogotTestCase
+   :members:
+   :exclude-members: run,debug
diff --git a/docs/usage-unittest.rst b/docs/usage-unittest.rst
index 7a2d81c6..60bbe859 100644
--- a/docs/usage-unittest.rst
+++ b/docs/usage-unittest.rst
@@ -3,20 +3,35 @@ Using with :mod:`unittest`
 
 .. currentmodule:: logot
 
-:mod:`logot` is compatible with any testing framework, including :mod:`unittest`:
+:mod:`logot` includes :class:`logot.unittest.LogotTestCase` for easy integration with :mod:`unittest`.
+
+The :attr:`logot <logot.unittest.LogotTestCase.logot>` attribute automatically :doc:`captures logs </log-capturing>`
+during tests and can be used to make log assertions:
 
 .. code:: python
 
-   import unittest
-   from logot import Logot, logged
+   from logot import logged
+   from logot.unittest import LogotTestCase
 
-   class MyAppTest(unittest.TestCase):
+   class MyAppTest(LogotTestCase):
 
       def test_my_app(self) -> None:
-         with Logot().capturing() as logot:
-            app.start()
-            logot.wait_for(logged.info("App started"))
+         app.start()
+         self.logot.wait_for(logged.info("App started"))
 
 .. seealso::
 
-   See :class:`Logot` and :meth:`Logot.capturing` API reference.
+   See :mod:`logot.unittest` API reference.
+
+
+Customizing log capturing
+-------------------------
+
+Override :mod:`logot`-prefixed attributes in your :class:`logot.unittest.LogotTestCase` subclass to customize automatic
+:doc:`log capturing </log-capturing>`:
+
+.. code:: python
+
+   class MyAppTest(LogotTestCase):
+      logot_level = logging.WARNING
+      logot_logger = "app"
diff --git a/logot/_unittest.py b/logot/_unittest.py
new file mode 100644
index 00000000..18caa862
--- /dev/null
+++ b/logot/_unittest.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+import logging
+from typing import ClassVar
+from unittest import TestCase, TestResult
+
+from logot._logot import Logot
+
+
+class LogotTestCase(TestCase):
+    """
+    A :class:`unittest.TestCase` subclass with automatic :doc:`log capturing </log-capturing>`.
+    """
+
+    logot: Logot
+    """
+    An initialized :class:`logot.Logot` instance with :doc:`log capturing </log-capturing>` enabled.
+
+    Use this to make log assertions in your tests.
+    """
+
+    logot_level: ClassVar[str | int] = Logot.DEFAULT_LEVEL
+    """
+    The ``level`` used by automatic :doc:`log capturing </log-capturing>`.
+
+    Defaults to :attr:`logot.Logot.DEFAULT_LEVEL`.
+
+    .. note::
+
+        Override this in subclasses to configure automatic :doc:`log capturing </log-capturing>`.
+    """
+
+    logot_logger: ClassVar[logging.Logger | str | None] = Logot.DEFAULT_LOGGER
+    """
+    The ``logger`` used by automatic :doc:`log capturing </log-capturing>`.
+
+    Defaults to :attr:`logot.Logot.DEFAULT_LOGGER`.
+
+    .. note::
+
+        Override this in subclasses to configure automatic :doc:`log capturing </log-capturing>`.
+    """
+
+    logot_timeout: ClassVar[float] = Logot.DEFAULT_TIMEOUT
+    """
+    The ``timeout`` (in seconds) for :attr:`LogotTestCase.logot`.
+
+    Defaults to :attr:`logot.Logot.DEFAULT_TIMEOUT`.
+
+    .. note::
+
+        Override this in subclasses to configure automatic :doc:`log capturing </log-capturing>`.
+    """
+
+    def _logot_setup(self) -> None:
+        self.logot = Logot(timeout=self.logot_timeout)
+        ctx = self.logot.capturing(level=self.logot_level, logger=self.logot_logger)
+        ctx.__enter__()
+        self.addCleanup(ctx.__exit__, None, None, None)
+
+    def run(self, result: TestResult | None = None) -> TestResult | None:
+        self._logot_setup()
+        return super().run(result)
+
+    def debug(self) -> None:
+        self._logot_setup()
+        return super().debug()
diff --git a/logot/unittest.py b/logot/unittest.py
new file mode 100644
index 00000000..1d94e9da
--- /dev/null
+++ b/logot/unittest.py
@@ -0,0 +1,10 @@
+"""
+Integration API for :mod:`unittest`.
+
+.. seealso::
+
+    See :doc:`/usage-unittest` usage guide.
+"""
+from __future__ import annotations
+
+from logot._unittest import LogotTestCase as LogotTestCase
diff --git a/tests/test_unittest.py b/tests/test_unittest.py
new file mode 100644
index 00000000..a2858eb5
--- /dev/null
+++ b/tests/test_unittest.py
@@ -0,0 +1,14 @@
+from __future__ import annotations
+
+from logot import logged
+from logot.unittest import LogotTestCase
+from tests import logger
+
+
+class TestUnitTest(LogotTestCase):
+    def test_capture(self) -> None:
+        logger.info("foo bar")
+        self.logot.assert_logged(logged.info("foo bar"))
+
+    def test_debug(self) -> None:
+        TestUnitTest("test_capture").debug()