diff --git a/requirements.txt b/requirements.txt index 5df356853..c88c613b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -128,7 +128,7 @@ hyperlink==21.0.0 # via hatch hypothesis==6.112.1 # via dycw-utilities (pyproject.toml) -idna==3.9 +idna==3.10 # via # anyio # httpx @@ -186,7 +186,7 @@ multidict==6.1.0 # via # aiohttp # yarl -narwhals==1.8.0 +narwhals==1.8.1 # via altair nest-asyncio==1.6.0 # via dycw-utilities (pyproject.toml) diff --git a/requirements/altair.txt b/requirements/altair.txt index a98882572..e194c61b4 100644 --- a/requirements/altair.txt +++ b/requirements/altair.txt @@ -29,7 +29,7 @@ lxml==5.3.0 # via pikepdf markupsafe==2.1.5 # via jinja2 -narwhals==1.8.0 +narwhals==1.8.1 # via altair packaging==24.1 # via diff --git a/requirements/jupyter.txt b/requirements/jupyter.txt index e7554ba5b..cd111a4af 100644 --- a/requirements/jupyter.txt +++ b/requirements/jupyter.txt @@ -60,7 +60,7 @@ httpx==0.27.2 # via jupyterlab hypothesis==6.112.1 # via dycw-utilities (pyproject.toml) -idna==3.9 +idna==3.10 # via # anyio # httpx @@ -245,7 +245,7 @@ rpds-py==0.20.0 # referencing send2trash==1.8.3 # via jupyter-server -setuptools==74.1.2 +setuptools==75.0.0 # via jupyterlab six==1.16.0 # via diff --git a/requirements/slack-sdk.txt b/requirements/slack-sdk.txt index 5263f8e26..670110b40 100644 --- a/requirements/slack-sdk.txt +++ b/requirements/slack-sdk.txt @@ -18,7 +18,7 @@ frozenlist==1.4.1 # aiosignal hypothesis==6.112.1 # via dycw-utilities (pyproject.toml) -idna==3.9 +idna==3.10 # via yarl iniconfig==2.0.0 # via pytest diff --git a/requirements/streamlit.txt b/requirements/streamlit.txt index 518c63f8d..09b854037 100644 --- a/requirements/streamlit.txt +++ b/requirements/streamlit.txt @@ -25,7 +25,7 @@ gitpython==3.1.43 # via streamlit hypothesis==6.112.1 # via dycw-utilities (pyproject.toml) -idna==3.9 +idna==3.10 # via requests iniconfig==2.0.0 # via pytest @@ -43,7 +43,7 @@ markupsafe==2.1.5 # via jinja2 mdurl==0.1.2 # via markdown-it-py -narwhals==1.8.0 +narwhals==1.8.1 # via altair numpy==2.1.1 # via diff --git a/src/tests/functions.py b/src/tests/functions.py deleted file mode 100644 index 7c1e94dd9..000000000 --- a/src/tests/functions.py +++ /dev/null @@ -1,203 +0,0 @@ -from __future__ import annotations - -from asyncio import sleep -from functools import wraps -from typing import TYPE_CHECKING - -from loguru import logger -from tenacity import retry, wait_fixed - -from utilities.functions import is_not_none -from utilities.loguru import LogLevel, log -from utilities.tenacity import before_sleep_log - -if TYPE_CHECKING: - from collections.abc import Callable - - -# test entry sync - - -@log -def func_test_entry_sync_inc(x: int, /) -> int: - return x + 1 - - -@log -def func_test_entry_sync_dec(x: int, /) -> int: - return x - 1 - - -@log -def func_test_entry_sync_inc_and_dec(x: int, /) -> tuple[int, int]: - return func_test_entry_sync_inc(x), func_test_entry_sync_dec(x) - - -# test entry async - - -@log -async def func_test_entry_async_inc(x: int, /) -> int: - await sleep(0.01) - return x + 1 - - -@log -async def func_test_entry_async_dec(x: int, /) -> int: - await sleep(0.01) - return x - 1 - - -@log -async def func_test_entry_async_inc_and_dec(x: int, /) -> tuple[int, int]: - return (await func_test_entry_async_inc(x), await func_test_entry_async_dec(x)) - - -# test entry disabled - - -@log(entry=None) -def func_test_entry_disabled_sync(x: int, /) -> int: - return x + 1 - - -@log(entry=None) -async def func_test_entry_disabled_async(x: int, /) -> int: - await sleep(0.01) - return x + 1 - - -# test entry custom level - - -@log(entry=LogLevel.INFO) -def func_test_entry_custom_level(x: int, /) -> int: - return x + 1 - - -# test error - - -class Remainder1Error(Exception): ... - - -class Remainder2Error(Exception): ... - - -@log -def func_test_error_sync(x: int, /) -> int | None: - if x % 2 == 0: - return x + 1 - msg = f"Got an odd number {x}" - raise ValueError(msg) - - -@log -def func_test_error_chain_outer_sync(x: int, /) -> int | None: - try: - return func_test_error_chain_inner_sync(x) - except Remainder1Error: - return x + 1 - - -@log(error_expected=Remainder1Error) -def func_test_error_chain_inner_sync(x: int, /) -> int | None: - if x % 3 == 0: - return x + 1 - if x % 3 == 1: - msg = "Got a remainder of 1" - raise Remainder1Error(msg) - msg = "Got a remainder of 2" - raise Remainder2Error(msg) - - -@log -async def func_test_error_async(x: int, /) -> int | None: - await sleep(0.01) - if x % 2 == 0: - return x + 1 - msg = f"Got an odd number {x}" - raise ValueError(msg) - - -@log -async def func_test_error_chain_outer_async(x: int, /) -> int | None: - try: - return await func_test_error_chain_inner_async(x) - except Remainder1Error: - return x + 1 - - -@log(error_expected=Remainder1Error) -async def func_test_error_chain_inner_async(x: int, /) -> int | None: - if x % 3 == 0: - return x + 1 - if x % 3 == 1: - msg = "Got a remainder of 1" - raise Remainder1Error(msg) - msg = "Got a remainder of 2" - raise Remainder2Error(msg) - - -# test exit - - -@log(exit_=LogLevel.INFO) -def func_test_exit_sync(x: int, /) -> int: - logger.info("Starting") - return x + 1 - - -@log(exit_=LogLevel.INFO) -async def func_test_exit_async(x: int, /) -> int: - logger.info("Starting") - await sleep(0.01) - return x + 1 - - -@log(exit_=LogLevel.WARNING) -def func_test_exit_custom_level(x: int, /) -> int: - logger.info("Starting") - return x + 1 - - -@log(exit_=LogLevel.INFO, exit_predicate=is_not_none) -def func_test_exit_predicate(x: int, /) -> int | None: - logger.info("Starting") - return (x + 1) if x % 2 == 0 else None - - -# test decorated - - -def make_new(func: Callable[[int], int], /) -> Callable[[int], tuple[int, int]]: - @wraps(func) - @log - def wrapped(x: int, /) -> tuple[int, int]: - first = func(x) - second = func(x + 1) - return first, second - - return wrapped - - -@make_new -@log(depth=3) -def func_test_decorated(x: int, /) -> int: - logger.info(f"Starting x={x}") - return x + 1 - - -# test tenacity - - -_counter = 0 - - -@retry(wait=wait_fixed(0.01), before_sleep=before_sleep_log()) -def func_test_before_sleep_log() -> int: - global _counter # noqa: PLW0603 - _counter += 1 - if _counter >= 3: - return _counter - raise ValueError(_counter) diff --git a/src/tests/test_loguru.py b/src/tests/test_loguru.py index 6545304c5..346e7a131 100644 --- a/src/tests/test_loguru.py +++ b/src/tests/test_loguru.py @@ -7,26 +7,17 @@ from hypothesis import given from loguru import logger -from loguru._defaults import LOGURU_FORMAT from loguru._recattrs import RecordFile, RecordLevel, RecordProcess, RecordThread from pytest import CaptureFixture, mark, param, raises -from tests.functions import ( - Remainder2Error, - func_test_decorated, - func_test_entry_async_inc_and_dec, - func_test_entry_custom_level, - func_test_entry_disabled_async, - func_test_entry_disabled_sync, - func_test_entry_sync_inc_and_dec, - func_test_error_async, - func_test_error_chain_outer_async, - func_test_error_chain_outer_sync, - func_test_error_sync, - func_test_exit_async, - func_test_exit_custom_level, - func_test_exit_predicate, - func_test_exit_sync, +from tests.test_loguru_functions import ( + func_test_log_entry_disabled, + func_test_log_entry_inc_and_dec, + func_test_log_entry_non_default_level, + func_test_log_error, + func_test_log_error_expected, + func_test_log_exit_duration, + func_test_log_exit_explicit, ) from utilities.hypothesis import text_ascii from utilities.loguru import ( @@ -46,7 +37,7 @@ make_slack_sink, make_slack_sink_async, ) -from utilities.text import ensure_str, strip_and_dedent +from utilities.text import strip_and_dedent if TYPE_CHECKING: from collections.abc import Callable @@ -142,405 +133,105 @@ def test_main(self, *, capsys: CaptureFixture) -> None: class TestLog: datetime: ClassVar[str] = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} \| " - trace: ClassVar[str] = datetime + r"TRACE \| " - info: ClassVar[str] = datetime + r"INFO \| " - warning: ClassVar[str] = datetime + r"WARNING \| " - error: ClassVar[str] = datetime + r"ERROR \| " - - def test_entry_sync(self, *, capsys: CaptureFixture) -> None: - default_format = ensure_str(LOGURU_FORMAT) - handler: HandlerConfiguration = { - "sink": sys.stdout, - "level": LogLevel.TRACE, - "format": f"{default_format} | {{extra}}", - } - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert func_test_entry_sync_inc_and_dec(1) == (2, 0) - out = capsys.readouterr().out - line1, line2, line3 = out.splitlines() - expected1 = ( - self.trace - + r"tests\.test_loguru:test_entry_sync:\d+ - ⋯ \| {'𝑓': 'func_test_entry_sync_inc_and_dec'}$" # noqa: RUF001 - ) - assert search(expected1, line1), line1 - trace_and_func = ( - self.trace - + r"tests\.functions:func_test_entry_sync_inc_and_dec:\d+ - ⋯ \| " - ) - expected2 = trace_and_func + "{'𝑓': 'func_test_entry_sync_inc'}$" # noqa: RUF001 - assert search(expected2, line2), line2 - expected3 = trace_and_func + "{'𝑓': 'func_test_entry_sync_dec'}$" # noqa: RUF001 - assert search(expected3, line3), line3 - - async def test_entry_async(self, *, capsys: CaptureFixture) -> None: - default_format = ensure_str(LOGURU_FORMAT) - handler: HandlerConfiguration = { - "sink": sys.stdout, - "level": LogLevel.TRACE, - "format": f"{default_format} | {{extra}}", - } + loguru: ClassVar[str] = r"tests\.test_loguru_functions:" + trace: ClassVar[str] = datetime + r"TRACE \| " + loguru + debug: ClassVar[str] = datetime + r"DEBUG \| " + loguru + info: ClassVar[str] = datetime + r"INFO \| " + loguru + warning: ClassVar[str] = datetime + r"WARNING \| " + loguru + error: ClassVar[str] = datetime + r"ERROR \| " + loguru + + def test_entry(self, *, capsys: CaptureFixture) -> None: + handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - assert await func_test_entry_async_inc_and_dec(1) == (2, 0) + assert func_test_log_entry_inc_and_dec(1) == (2, 0) out = capsys.readouterr().out line1, line2, line3 = out.splitlines() - expected1 = ( - self.trace - + r"tests\.test_loguru:test_entry_async:\d+ - ⋯ \| {'𝑓': 'func_test_entry_async_inc_and_dec'}$" # noqa: RUF001 - ) + expected1 = self.trace + r"func_test_log_entry_inc_and_dec:\d+ - ➢$" assert search(expected1, line1), line1 - trace_and_func = ( - self.trace - + r"tests\.functions:func_test_entry_async_inc_and_dec:\d+ - ⋯ \| " - ) - expected2 = trace_and_func + "{'𝑓': 'func_test_entry_async_inc'}$" # noqa: RUF001 + expected2 = self.trace + r"_func_test_log_entry_inc:\d+ - ➢$" assert search(expected2, line2), line2 - expected3 = trace_and_func + "{'𝑓': 'func_test_entry_async_dec'}$" # noqa: RUF001 + expected3 = self.trace + r"_func_test_log_entry_dec:\d+ - ➢$" assert search(expected3, line3), line3 - def test_entry_disabled_sync(self, *, capsys: CaptureFixture) -> None: + def test_entry_disabled(self, *, capsys: CaptureFixture) -> None: handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - assert func_test_entry_disabled_sync(1) == 2 + assert func_test_log_entry_disabled(1) == 2 out = capsys.readouterr().out assert out == "" - async def test_entry_disabled_async(self, *, capsys: CaptureFixture) -> None: + def test_entry_non_default_level(self, *, capsys: CaptureFixture) -> None: handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - assert await func_test_entry_disabled_async(1) == 2 - out = capsys.readouterr().out - assert out == "" - - def test_entry_custom_level(self, *, capsys: CaptureFixture) -> None: - default_format = ensure_str(LOGURU_FORMAT) - handler: HandlerConfiguration = { - "sink": sys.stdout, - "level": LogLevel.TRACE, - "format": f"{default_format} | {{extra}}", - } - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert func_test_entry_custom_level(1) == 2 + assert func_test_log_entry_non_default_level(1) == 2 out = capsys.readouterr().out - expected = ( - self.info - + r"tests\.test_loguru:test_entry_custom_level:\d+ - ⋯ \| {'𝑓': 'func_test_entry_custom_level'}$" # noqa: RUF001 - ) + expected = self.debug + r"func_test_log_entry_non_default_level:\d+ - ➢$" assert search(expected, out), out - def test_error_no_effect_sync(self, *, capsys: CaptureFixture) -> None: + def test_error(self, *, capsys: CaptureFixture) -> None: handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - assert func_test_error_sync(0) == 1 - out = capsys.readouterr().out - (line,) = out.splitlines() - expected = self.trace + r"tests\.test_loguru:test_error_no_effect_sync:\d+ - ⋯$" - assert search(expected, line), line - - def test_error_catch_sync(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - with raises(ValueError, match="Got an odd number 1"): - assert func_test_error_sync(1) + with raises(ValueError, match="Got an odd number: 1"): + _ = func_test_log_error(1) out = capsys.readouterr().out line1, line2, line3, *_ = out.splitlines() - expected1 = self.trace + r"tests\.test_loguru:test_error_catch_sync:\d+ - ⋯$" + expected1 = self.trace + r"func_test_log_error:\d+ - ➢$" assert search(expected1, line1), line1 expected2 = ( self.error - + r"tests\.test_loguru:test_error_catch_sync:\d+ - ValueError\('Got an odd number 1'\)$" + + r"func_test_log_error:\d+ - ValueError\('Got an odd number: 1'\)$" ) assert search(expected2, line2), line2 assert line3 == "Traceback (most recent call last):" exp_last = strip_and_dedent( """ raise ValueError(msg) - └ 'Got an odd number 1' + └ 'Got an odd number: 1' - ValueError: Got an odd number 1 + ValueError: Got an odd number: 1 """ ) lines_last = "\n".join(out.splitlines()[-4:]) assert lines_last == exp_last - def test_error_chain_no_effect_sync(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert func_test_error_chain_outer_sync(0) == 1 - out = capsys.readouterr().out - line1, line2 = out.splitlines() - expected1 = ( - self.trace + r"tests\.test_loguru:test_error_chain_no_effect_sync:\d+ - ⋯$" - ) - assert search(expected1, line1), line1 - expected2 = ( - self.trace + r"tests\.functions:func_test_error_chain_outer_sync:\d+ - ⋯$" - ) - assert search(expected2, line2), line2 - - def test_error_chain_caught_sync(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert func_test_error_chain_outer_sync(1) == 2 - out = capsys.readouterr().out - line1, line2 = out.splitlines() - expected1 = ( - self.trace + r"tests\.test_loguru:test_error_chain_caught_sync:\d+ - ⋯$" - ) - assert search(expected1, line1), line1 - expected2 = ( - self.trace + r"tests\.functions:func_test_error_chain_outer_sync:\d+ - ⋯$" - ) - assert search(expected2, line2), line2 - - def test_error_chain_uncaught_sync(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - with raises(Remainder2Error): - assert func_test_error_chain_outer_sync(2) - out = capsys.readouterr().out - line1, line2, line3, line4, *_ = out.splitlines() - expected1 = ( - self.trace + r"tests\.test_loguru:test_error_chain_uncaught_sync:\d+ - ⋯$" - ) - assert search(expected1, line1), line1 - expected2 = ( - self.trace + r"tests\.functions:func_test_error_chain_outer_sync:\d+ - ⋯$" - ) - assert search(expected2, line2), line2 - expected3 = ( - self.error - + r"tests\.functions:func_test_error_chain_outer_sync:\d+ - Remainder2Error\('Got a remainder of 2'\)$" - ) - assert search(expected3, line3), line3 - assert line4 == "Traceback (most recent call last):" - exp_last = strip_and_dedent( - """ - raise Remainder2Error(msg) - │ └ 'Got a remainder of 2' - └ - - tests.functions.Remainder2Error: Got a remainder of 2 - """ - ) - lines_last = "\n".join(out.splitlines()[-5:]) - assert lines_last == exp_last - - async def test_error_no_effect_async(self, *, capsys: CaptureFixture) -> None: + def test_error_expected(self, *, capsys: CaptureFixture) -> None: handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - assert await func_test_error_async(0) == 1 + with raises(ValueError, match="Got an odd number: 1"): + _ = func_test_log_error_expected(1) out = capsys.readouterr().out (line,) = out.splitlines() - expected = ( - self.trace + r"tests\.test_loguru:test_error_no_effect_async:\d+ - ⋯$" - ) + expected = self.trace + r"func_test_log_error_expected:\d+ - ➢$" assert search(expected, line), line - async def test_error_catch_async(self, *, capsys: CaptureFixture) -> None: + def test_exit_explicit(self, *, capsys: CaptureFixture) -> None: handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - with raises(ValueError, match="Got an odd number 1"): - assert await func_test_error_async(1) - out = capsys.readouterr().out - line1, line2, line3, *_ = out.splitlines() - expected1 = self.trace + r"tests\.test_loguru:test_error_catch_async:\d+ - ⋯$" - assert search(expected1, line1), line1 - expected2 = ( - self.error - + r"tests\.test_loguru:test_error_catch_async:\d+ - ValueError\('Got an odd number 1'\)$" - ) - assert search(expected2, line2), line2 - assert line3 == "Traceback (most recent call last):" - exp_last = strip_and_dedent( - """ - raise ValueError(msg) - └ 'Got an odd number 1' - - ValueError: Got an odd number 1 - """ - ) - lines_last = "\n".join(out.splitlines()[-4:]) - assert lines_last == exp_last - - async def test_error_chain_no_effect_async(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert await func_test_error_chain_outer_async(0) == 1 + assert func_test_log_exit_explicit(1) == 2 out = capsys.readouterr().out line1, line2 = out.splitlines() - expected1 = ( - self.trace + r"tests\.test_loguru:test_error_chain_no_effect_async:\d+ - ⋯$" - ) + expected1 = self.trace + r"func_test_log_exit_explicit:\d+ - ➢$" assert search(expected1, line1), line1 - expected2 = ( - self.trace + r"tests\.functions:func_test_error_chain_outer_async:\d+ - ⋯$" - ) + expected2 = self.debug + r"func_test_log_exit_explicit:\d+ - ✔$" assert search(expected2, line2), line2 - async def test_error_chain_caught_async(self, *, capsys: CaptureFixture) -> None: + def test_exit_duration(self, *, capsys: CaptureFixture) -> None: handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - assert await func_test_error_chain_outer_async(1) == 2 + assert func_test_log_exit_duration(1) == 2 out = capsys.readouterr().out line1, line2 = out.splitlines() - expected1 = ( - self.trace + r"tests\.test_loguru:test_error_chain_caught_async:\d+ - ⋯$" - ) - assert search(expected1, line1), line1 - expected2 = ( - self.trace + r"tests\.functions:func_test_error_chain_outer_async:\d+ - ⋯$" - ) - assert search(expected2, line2), line2 - - async def test_error_chain_uncaught_async(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - with raises(Remainder2Error): - assert await func_test_error_chain_outer_async(2) - out = capsys.readouterr().out - line1, line2, line3, line4, *_ = out.splitlines() - expected1 = ( - self.trace + r"tests\.test_loguru:test_error_chain_uncaught_async:\d+ - ⋯$" - ) - assert search(expected1, line1), line1 - expected2 = ( - self.trace + r"tests\.functions:func_test_error_chain_outer_async:\d+ - ⋯$" - ) - assert search(expected2, line2), line2 - expected3 = ( - self.error - + r"tests\.functions:func_test_error_chain_outer_async:\d+ - Remainder2Error\('Got a remainder of 2'\)$" - ) - assert search(expected3, line3), line3 - assert line4 == "Traceback (most recent call last):" - exp_last = strip_and_dedent( - """ - raise Remainder2Error(msg) - │ └ 'Got a remainder of 2' - └ - - tests.functions.Remainder2Error: Got a remainder of 2 - """ - ) - lines_last = "\n".join(out.splitlines()[-5:]) - assert lines_last == exp_last - - def test_exit_sync(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert func_test_exit_sync(1) == 2 - out = capsys.readouterr().out - line1, line2, line3 = out.splitlines() - expected1 = self.trace + r"tests\.test_loguru:test_exit_sync:\d+ - ⋯$" + expected1 = self.trace + r"func_test_log_exit_duration:\d+ - ➢$" assert search(expected1, line1), line1 - expected2 = self.info + r"tests\.functions:func_test_exit_sync:\d+ - Starting$" + expected2 = self.trace + r"func_test_log_exit_duration:\d+ - ✔$" assert search(expected2, line2), line2 - expected3 = self.info + r"tests\.test_loguru:test_exit_sync:\d+ - ✔$" - assert search(expected3, line3), line3 - - async def test_exit_async(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert await func_test_exit_async(1) == 2 - out = capsys.readouterr().out - line1, line2, line3 = out.splitlines() - expected1 = self.trace + r"tests\.test_loguru:test_exit_async:\d+ - ⋯$" - assert search(expected1, line1), line1 - expected2 = self.info + r"tests\.functions:func_test_exit_async:\d+ - Starting$" - assert search(expected2, line2), line2 - expected3 = self.info + r"tests\.test_loguru:test_exit_async:\d+ - ✔$" - assert search(expected3, line3), line3 - - def test_exit_custom_level(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert func_test_exit_custom_level(1) == 2 - out = capsys.readouterr().out - (line1, line2, line3) = out.splitlines() - expected1 = self.trace + r"tests\.test_loguru:test_exit_custom_level:\d+ - ⋯$" - assert search(expected1, line1), line1 - expected2 = ( - self.info + r"tests\.functions:func_test_exit_custom_level:\d+ - Starting$" - ) - assert search(expected2, line2), line2 - expected3 = self.warning + r"tests\.test_loguru:test_exit_custom_level:\d+ - ✔$" - assert search(expected3, line3), line3 - - def test_exit_predicate_no_filter(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert func_test_exit_predicate(0) == 1 - out = capsys.readouterr().out - (line1, line2, line3) = out.splitlines() - expected1 = ( - self.trace + r"tests\.test_loguru:test_exit_predicate_no_filter:\d+ - ⋯$" - ) - assert search(expected1, line1), line1 - expected2 = ( - self.info + r"tests\.functions:func_test_exit_predicate:\d+ - Starting$" - ) - assert search(expected2, line2), line2 - expected3 = ( - self.info + r"tests\.test_loguru:test_exit_predicate_no_filter:\d+ - ✔$" - ) - assert search(expected3, line3), line3 - - def test_exit_predicate_filter(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert func_test_exit_predicate(1) is None - out = capsys.readouterr().out - (line1, line2) = out.splitlines() - expected1 = ( - self.trace + r"tests\.test_loguru:test_exit_predicate_filter:\d+ - ⋯$" - ) - assert search(expected1, line1), line1 - expected2 = ( - self.info + r"tests\.functions:func_test_exit_predicate:\d+ - Starting$" - ) - assert search(expected2, line2), line2 - - def test_decorated(self, *, capsys: CaptureFixture) -> None: - handler: HandlerConfiguration = {"sink": sys.stdout, "level": LogLevel.TRACE} - _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - - assert func_test_decorated(0) == (1, 2) - out = capsys.readouterr().out - (line1, line2, line3, line4, line5) = out.splitlines() - expected1 = self.trace + r"tests\.test_loguru:test_decorated:\d+ - ⋯$" - assert search(expected1, line1), line1 - expected2 = self.trace + r"tests\.test_loguru:test_decorated:\d+ - ⋯$" - assert search(expected2, line2), line2 - expected3 = ( - self.info + r"tests\.functions:func_test_decorated:\d+ - Starting x=0$" - ) - assert search(expected3, line3), line3 - expected4 = self.trace + r"tests\.test_loguru:test_decorated:\d+ - ⋯$" - assert search(expected4, line4), line4 - expected5 = ( - self.info + r"tests\.functions:func_test_decorated:\d+ - Starting x=1$" - ) - assert search(expected5, line5), line5 class TestLoggedSleep: diff --git a/src/tests/test_loguru_functions.py b/src/tests/test_loguru_functions.py new file mode 100644 index 000000000..978c2ee25 --- /dev/null +++ b/src/tests/test_loguru_functions.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from time import sleep + +from tenacity import retry, wait_fixed + +from utilities.loguru import LogLevel, log +from utilities.tenacity import before_sleep_log + + +def func_test_log_entry_inc_and_dec(x: int, /) -> tuple[int, int]: + with log(): + inc = _func_test_log_entry_inc(x) + dec = _func_test_log_entry_dec(x) + return inc, dec + + +def _func_test_log_entry_inc(x: int, /) -> int: + with log(): + return x + 1 + + +def _func_test_log_entry_dec(x: int, /) -> int: + with log(): + return x - 1 + + +def func_test_log_entry_disabled(x: int, /) -> int: + with log(entry_level=None): + return x + 1 + + +def func_test_log_entry_non_default_level(x: int, /) -> int: + with log(entry_level=LogLevel.DEBUG): + return x + 1 + + +def func_test_log_error(x: int, /) -> int | None: + with log(): + if x % 2 == 0: + return x + 1 + msg = f"Got an odd number: {x}" + raise ValueError(msg) + + +def func_test_log_error_expected(x: int, /) -> int | None: + with log(error_expected=ValueError): + if x % 2 == 0: + return x + 1 + msg = f"Got an odd number: {x}" + raise ValueError(msg) + + +def func_test_log_exit_explicit(x: int, /) -> int: + with log(exit_level=LogLevel.DEBUG): + return x + 1 + + +def func_test_log_exit_duration(x: int, /) -> int: + with log(exit_duration=0.01): + sleep(0.02) + return x + 1 + + +_counter = 0 + + +@retry(wait=wait_fixed(0.01), before_sleep=before_sleep_log()) +def func_test_tenacity_before_sleep_log() -> int: + global _counter # noqa: PLW0603 + _counter += 1 + if _counter >= 3: + return _counter + raise ValueError(_counter) diff --git a/src/tests/test_tenacity.py b/src/tests/test_tenacity.py index 83ae89bea..c5d44ff35 100644 --- a/src/tests/test_tenacity.py +++ b/src/tests/test_tenacity.py @@ -8,7 +8,7 @@ from hypothesis.strategies import floats from loguru import logger -from tests.functions import func_test_before_sleep_log +from tests.test_loguru_functions import func_test_tenacity_before_sleep_log from utilities.hypothesis import durations from utilities.tenacity import wait_exponential_jitter @@ -35,13 +35,13 @@ def test_main(self, *, capsys: CaptureFixture) -> None: handler: HandlerConfiguration = {"sink": sys.stdout} _ = logger.configure(handlers=[cast(dict[str, Any], handler)]) - assert func_test_before_sleep_log() == 3 + assert func_test_tenacity_before_sleep_log() == 3 out = capsys.readouterr().out lines = out.splitlines() assert len(lines) == 2 for i, line in enumerate(lines, start=1): expected = ( - r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} \| INFO \| utilities\.tenacity:log:\d+ - Retrying tests\.functions\.func_test_before_sleep_log in 0\.01 seconds as it raised ValueError: " + r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} \| INFO \| utilities\.tenacity:log:\d+ - Retrying tests\.test_loguru_functions\.func_test_tenacity_before_sleep_log in 0\.01 seconds as it raised ValueError: " + str(i) + r"\." ) diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py index cc958bf75..1cdad2321 100644 --- a/src/utilities/__init__.py +++ b/src/utilities/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "0.55.1" +__version__ = "0.55.2" diff --git a/src/utilities/loguru.py b/src/utilities/loguru.py index 43b7f9519..d8c629647 100644 --- a/src/utilities/loguru.py +++ b/src/utilities/loguru.py @@ -5,35 +5,25 @@ import sys import time from asyncio import AbstractEventLoop -from collections.abc import Callable, Hashable, Sequence +from collections.abc import Callable, Hashable, Iterator, Sequence +from contextlib import contextmanager from dataclasses import dataclass from enum import StrEnum, unique -from functools import partial, wraps -from inspect import iscoroutinefunction from logging import Handler, LogRecord from sys import __excepthook__, _getframe, stderr -from typing import ( - TYPE_CHECKING, - Any, - ParamSpec, - TextIO, - TypedDict, - TypeVar, - cast, - overload, -) +from typing import TYPE_CHECKING, Any, TextIO, TypedDict, assert_never, cast from loguru import logger from typing_extensions import override -from utilities.datetime import duration_to_timedelta -from utilities.functions import get_func_name +from utilities.datetime import SECOND, duration_to_timedelta from utilities.iterables import ( OneEmptyError, OneNonUniqueError, one, resolve_include_and_exclude, ) +from utilities.timer import Timer if TYPE_CHECKING: import datetime as dt @@ -58,10 +48,6 @@ from utilities.types import Duration, PathLike, StrMapping -_P = ParamSpec("_P") -_T = TypeVar("_T") - - _RECORD_EXCEPTION_VALUE = "{record[exception].value!r}" LEVEL_CONFIGS: Sequence[LevelConfig] = [ {"name": "TRACE", "color": ""}, @@ -199,130 +185,53 @@ def __str__(self) -> str: return f"Invalid logging level: {self.level!r}" -_MATHEMATICAL_ITALIC_SMALL_F = "𝑓" # noqa: RUF001 - - -@overload +@contextmanager def log( - func: Callable[_P, _T], - /, *, - depth: int = 1, - entry: LogLevel | None = ..., - entry_bind: StrMapping | None = ..., - entry_message: str = ..., - error_expected: type[Exception] | tuple[type[Exception], ...] | None = ..., - error_bind: StrMapping | None = ..., - error_message: str = ..., - exit_: LogLevel | None = ..., - exit_predicate: Callable[[_T], bool] | None = ..., - exit_bind: StrMapping | None = ..., - exit_message: str = ..., -) -> Callable[_P, _T]: ... -@overload -def log( - func: None = None, - /, - *, - depth: int = 1, - entry: LogLevel | None = ..., - entry_bind: StrMapping | None = ..., - entry_message: str = ..., - error_bind: StrMapping | None = ..., - error_expected: type[Exception] | tuple[type[Exception], ...] | None = ..., - error_message: str = ..., - exit_: LogLevel | None = ..., - exit_predicate: Callable[[Any], bool] | None = ..., - exit_bind: StrMapping | None = ..., - exit_message: str = ..., -) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: ... -def log( - func: Callable[_P, _T] | None = None, - /, - *, - depth: int = 1, - entry: LogLevel | None = LogLevel.TRACE, + depth: int = 2, + entry_level: LogLevel | None = LogLevel.TRACE, entry_bind: StrMapping | None = None, - entry_message: str = "⋯", + entry_message: str = "➢", error_expected: type[Exception] | tuple[type[Exception], ...] | None = None, error_bind: StrMapping | None = None, error_message: str = _RECORD_EXCEPTION_VALUE, - exit_: LogLevel | None = None, + exit_level: LogLevel | None = None, + exit_duration: Duration = SECOND, exit_bind: StrMapping | None = None, - exit_predicate: Callable[[_T], bool] | None = None, exit_message: str = "✔", -) -> Callable[_P, _T] | Callable[[Callable[_P, _T]], Callable[_P, _T]]: - """Log the function call.""" - if func is None: - return partial( - log, - depth=depth, - entry=entry, - entry_bind=entry_bind, - entry_message=entry_message, - error_expected=error_expected, - error_bind=error_bind, - error_message=error_message, - exit_=exit_, - exit_bind=exit_bind, - exit_predicate=exit_predicate, - exit_message=exit_message, - ) - - func_name = get_func_name(func) - if iscoroutinefunction(func): - - @wraps(func) - async def wrapped_async(*args: _P.args, **kwargs: _P.kwargs) -> _T: - if entry is not None: - logger_use = logger if entry_bind is None else logger.bind(**entry_bind) - logger_use.opt(depth=depth).log( - entry, entry_message, **{_MATHEMATICAL_ITALIC_SMALL_F: func_name} - ) - try: - result = await func(*args, **kwargs) - except Exception as error: - if (error_expected is None) or not isinstance(error, error_expected): - logger_use = ( - logger if error_bind is None else logger.bind(**error_bind) - ) - logger_use.opt(exception=True, record=True, depth=depth).error( - error_message - ) - raise - if ((exit_predicate is None) or (exit_predicate(result))) and ( - exit_ is not None - ): - logger_use = logger if exit_bind is None else logger.bind(**exit_bind) - logger_use.opt(depth=depth).log(exit_, exit_message) - return result - - return cast(Callable[_P, _T], wrapped_async) - - @wraps(func) - def wrapped_sync(*args: Any, **kwargs: Any) -> Any: - if entry is not None: - logger_use = logger if entry_bind is None else logger.bind(**entry_bind) - logger_use.opt(depth=depth).log( - entry, entry_message, **{_MATHEMATICAL_ITALIC_SMALL_F: func_name} - ) + **kwargs: Any, +) -> Iterator[None]: + """Log the function entry/error/exit/duration.""" + with logger.contextualize(**kwargs), Timer() as timer: + if entry_level is not None: + logger_entry = logger if entry_bind is None else logger.bind(**entry_bind) + logger_entry.opt(depth=depth).log(entry_level, entry_message) try: - result = func(*args, **kwargs) + yield except Exception as error: if (error_expected is None) or not isinstance(error, error_expected): - logger_use = logger if error_bind is None else logger.bind(**error_bind) - logger_use.opt(exception=True, record=True, depth=depth).error( + logger_error = ( + logger if error_bind is None else logger.bind(**error_bind) + ) + logger_error.opt(exception=True, record=True, depth=depth).error( error_message ) raise - if ((exit_predicate is None) or (exit_predicate(result))) and ( - exit_ is not None - ): - logger_use = logger if exit_bind is None else logger.bind(**exit_bind) - logger_use.opt(depth=depth).log(exit_, exit_message) - return result - - return cast(Callable[_P, _T], wrapped_sync) + finally: + if isinstance(exit_level, LogLevel) or (timer >= exit_duration): + match exit_level: + case LogLevel(): + exit_level_use = exit_level + case None: + exit_level_use = ( + LogLevel.TRACE if entry_level is None else entry_level + ) + case _ as never: # pyright: ignore[reportUnnecessaryComparison] + assert_never(never) + logger_exit = logger if exit_bind is None else logger.bind(**exit_bind) + logger_exit.opt(depth=depth).log( + exit_level_use, exit_message, timer=timer + ) def logged_sleep_sync(