From 4192f51b2198c96dd3af6bc0981308f889402e36 Mon Sep 17 00:00:00 2001 From: crimsonknave Date: Fri, 17 Oct 2025 14:21:39 -0400 Subject: [PATCH 1/7] Remove the logger of a decorated function when the function is done --- annotated_logger/__init__.py | 3 ++- example/calculator.py | 1 - pyproject.toml | 1 + requirements/requirements-dev.txt | 18 ++++++++++++++++-- test/test_memory.py | 26 ++++++++++++++++++++++++++ 5 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 test/test_memory.py diff --git a/annotated_logger/__init__.py b/annotated_logger/__init__.py index 4e0bfd4..417522c 100644 --- a/annotated_logger/__init__.py +++ b/annotated_logger/__init__.py @@ -33,7 +33,7 @@ # https://test.pypi.org/project/annotated-logger/ # The dev versions in testpypi can then be pulled in to whatever project needed # the new feature. -VERSION = "1.2.4" # pragma: no mutate +VERSION = "1.3.0" # pragma: no mutate T = TypeVar("T") P = ParamSpec("P") @@ -787,6 +787,7 @@ def wrap_function(*args: P.args, **kwargs: P.kwargs) -> R: if post_call and not post_call_attempted: _attempt_post_call(post_call, logger, *new_args, **new_kwargs) # pyright: ignore[reportCallIssue] raise + logging.root.manager.loggerDict.pop(logger.logger.name, None) return result return wrap_function diff --git a/example/calculator.py b/example/calculator.py index bcca52e..1f0aa2e 100644 --- a/example/calculator.py +++ b/example/calculator.py @@ -148,7 +148,6 @@ def power( @annotate_logs(success_info=False, _typing_requested=True) def add(self, annotated_logger: AnnotatedAdapter) -> Number: - # def add(self, *args, annotated_logger: AnnotatedAdapter) -> Number: """Add self.first and self.second.""" annotated_logger.annotate(first=self.first, second=self.second, foo="bar") diff --git a/pyproject.toml b/pyproject.toml index 1ace559..a876035 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ dependencies = [ "pytest-cov", "pytest-freezer", "pytest-github-actions-annotate-failures", + "pytest-memray", "pytest-mock", "pytest-randomly", "requests-mock", diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index c61e6e5..72a72fe 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -9,6 +9,7 @@ # - pytest-cov # - pytest-freezer # - pytest-github-actions-annotate-failures +# - pytest-memray # - pytest-mock # - pytest-randomly # - requests-mock @@ -44,6 +45,8 @@ idna==3.10 # via requests iniconfig==2.1.0 # via pytest +jinja2==3.1.6 + # via memray libcst==1.7.0 # via mutmut linkify-it-py==2.0.3 @@ -55,10 +58,14 @@ markdown-it-py==4.0.0 # mdit-py-plugins # rich # textual +markupsafe==3.0.3 + # via jinja2 mdit-py-plugins==0.5.0 # via markdown-it-py mdurl==0.1.2 # via markdown-it-py +memray==1.19.1 + # via pytest-memray mutmut==3.3.1 # via hatch.envs.dev nodeenv==1.9.1 @@ -93,6 +100,7 @@ pytest==8.4.2 # pytest-cov # pytest-freezer # pytest-github-actions-annotate-failures + # pytest-memray # pytest-mock # pytest-randomly pytest-cov==7.0.0 @@ -101,6 +109,8 @@ pytest-freezer==0.4.9 # via hatch.envs.dev pytest-github-actions-annotate-failures==0.3.0 # via hatch.envs.dev +pytest-memray==1.8.0 + # via hatch.envs.dev pytest-mock==3.15.1 # via hatch.envs.dev pytest-randomly==4.0.1 @@ -120,7 +130,9 @@ requests==2.32.5 requests-mock==1.12.1 # via hatch.envs.dev rich==14.2.0 - # via textual + # via + # memray + # textual ruff==0.14.0 # via hatch.envs.dev setproctitle==1.3.7 @@ -128,7 +140,9 @@ setproctitle==1.3.7 six==1.17.0 # via python-dateutil textual==6.2.1 - # via mutmut + # via + # memray + # mutmut typing-extensions==4.15.0 # via # hatch.envs.dev diff --git a/test/test_memory.py b/test/test_memory.py new file mode 100644 index 0000000..1cffe0c --- /dev/null +++ b/test/test_memory.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import logging + +import pytest + +import example.api +import example.calculator +import example.default + + +class TestMemory: + @pytest.mark.limit_memory("10 MB") + def test_repeated_calls_do_not_accumulate_memory(self): + calc = example.calculator.Calculator(1, 2) + for _ in range(10000): + calc.add() + + def test_repeated_calls_do_not_accumulate_loggers(self): + calc = example.calculator.Calculator(1, 2) + starting_loggers = len(logging.root.manager.loggerDict) + for _ in range(1000): + calc.add() + + ending_loggers = len(logging.root.manager.loggerDict) + assert starting_loggers == ending_loggers From 2a3cf8371cec0208f7511b97512a5dd92e0d0508 Mon Sep 17 00:00:00 2001 From: crimsonknave Date: Fri, 17 Oct 2025 14:32:09 -0400 Subject: [PATCH 2/7] Move log cleanup to finally --- annotated_logger/__init__.py | 5 +++-- test/test_memory.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/annotated_logger/__init__.py b/annotated_logger/__init__.py index 417522c..25f03d6 100644 --- a/annotated_logger/__init__.py +++ b/annotated_logger/__init__.py @@ -653,7 +653,7 @@ def annotate_logs( # Between the overloads and the two inner method definitions, # there's not much I can do to reduce the complexity more. # So, ignoring the complexity metric - def annotate_logs( # noqa: C901 + def annotate_logs( # noqa: C901 PLR0915 self, logger_name: str | None = None, *, @@ -787,7 +787,8 @@ def wrap_function(*args: P.args, **kwargs: P.kwargs) -> R: if post_call and not post_call_attempted: _attempt_post_call(post_call, logger, *new_args, **new_kwargs) # pyright: ignore[reportCallIssue] raise - logging.root.manager.loggerDict.pop(logger.logger.name, None) + finally: + logging.root.manager.loggerDict.pop(logger.logger.name, None) return result return wrap_function diff --git a/test/test_memory.py b/test/test_memory.py index 1cffe0c..d5debc0 100644 --- a/test/test_memory.py +++ b/test/test_memory.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import logging import pytest @@ -10,17 +11,21 @@ class TestMemory: + @pytest.mark.parametrize("denominator", [2, 0]) @pytest.mark.limit_memory("10 MB") - def test_repeated_calls_do_not_accumulate_memory(self): - calc = example.calculator.Calculator(1, 2) + def test_repeated_calls_do_not_accumulate_memory(self, denominator): + calc = example.calculator.Calculator(1, denominator) for _ in range(10000): - calc.add() + with contextlib.suppress(ZeroDivisionError): + calc.divide() - def test_repeated_calls_do_not_accumulate_loggers(self): - calc = example.calculator.Calculator(1, 2) + @pytest.mark.parametrize("denominator", [2, 0]) + def test_repeated_calls_do_not_accumulate_loggers(self, denominator): + calc = example.calculator.Calculator(1, denominator) starting_loggers = len(logging.root.manager.loggerDict) for _ in range(1000): - calc.add() + with contextlib.suppress(ZeroDivisionError): + calc.divide() ending_loggers = len(logging.root.manager.loggerDict) assert starting_loggers == ending_loggers From 2cfff70ccc1a36fd4fa50b195030484098d4e06d Mon Sep 17 00:00:00 2001 From: crimsonknave Date: Fri, 17 Oct 2025 14:44:27 -0400 Subject: [PATCH 3/7] Adding an note why we cleanup the logger --- annotated_logger/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/annotated_logger/__init__.py b/annotated_logger/__init__.py index 25f03d6..ecac8be 100644 --- a/annotated_logger/__init__.py +++ b/annotated_logger/__init__.py @@ -788,6 +788,8 @@ def wrap_function(*args: P.args, **kwargs: P.kwargs) -> R: _attempt_post_call(post_call, logger, *new_args, **new_kwargs) # pyright: ignore[reportCallIssue] raise finally: + # Remove the logger now that we are done with it, + # otherwise they build up and eat memory logging.root.manager.loggerDict.pop(logger.logger.name, None) return result From 9362e9e439364ccef2bcd5ed5c561dfee226f4f1 Mon Sep 17 00:00:00 2001 From: crimsonknave Date: Fri, 17 Oct 2025 14:57:25 -0400 Subject: [PATCH 4/7] Skip memray on windows --- test/test_memory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_memory.py b/test/test_memory.py index d5debc0..fc138d2 100644 --- a/test/test_memory.py +++ b/test/test_memory.py @@ -2,6 +2,7 @@ import contextlib import logging +import platform import pytest @@ -11,6 +12,7 @@ class TestMemory: + @pytest.mark.skipif(platform.system() == "Windows") @pytest.mark.parametrize("denominator", [2, 0]) @pytest.mark.limit_memory("10 MB") def test_repeated_calls_do_not_accumulate_memory(self, denominator): From 201fd5b0781ef7f337cef68714a3edc683a4fecb Mon Sep 17 00:00:00 2001 From: crimsonknave Date: Fri, 17 Oct 2025 14:59:26 -0400 Subject: [PATCH 5/7] Fix skipif --- test/test_memory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_memory.py b/test/test_memory.py index fc138d2..d89e878 100644 --- a/test/test_memory.py +++ b/test/test_memory.py @@ -12,7 +12,9 @@ class TestMemory: - @pytest.mark.skipif(platform.system() == "Windows") + @pytest.mark.skipif( + platform.system() == "Windows", reason="Memray doesn't work on Windows." + ) @pytest.mark.parametrize("denominator", [2, 0]) @pytest.mark.limit_memory("10 MB") def test_repeated_calls_do_not_accumulate_memory(self, denominator): From 6a3879c3b83158f0c652eaa0807223fe65965bc1 Mon Sep 17 00:00:00 2001 From: crimsonknave Date: Fri, 17 Oct 2025 15:15:41 -0400 Subject: [PATCH 6/7] Skip the memray in actions, it was being inconsistent --- test/test_memory.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_memory.py b/test/test_memory.py index d89e878..378aa56 100644 --- a/test/test_memory.py +++ b/test/test_memory.py @@ -2,6 +2,7 @@ import contextlib import logging +import os import platform import pytest @@ -15,6 +16,10 @@ class TestMemory: @pytest.mark.skipif( platform.system() == "Windows", reason="Memray doesn't work on Windows." ) + @pytest.mark.skipif( + os.environ.get("GITHUB_ACTIONS", "") == "true", + reason="Memory usage in actions was not being predictable.", + ) @pytest.mark.parametrize("denominator", [2, 0]) @pytest.mark.limit_memory("10 MB") def test_repeated_calls_do_not_accumulate_memory(self, denominator): From f540611c0106c3b0ba4e86a9b037c33c1279ccec Mon Sep 17 00:00:00 2001 From: crimsonknave Date: Fri, 17 Oct 2025 15:18:45 -0400 Subject: [PATCH 7/7] Remove the pytest-memray as it won't even install on windows --- pyproject.toml | 1 - test/test_memory.py | 19 ------------------- 2 files changed, 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a876035..1ace559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,6 @@ dependencies = [ "pytest-cov", "pytest-freezer", "pytest-github-actions-annotate-failures", - "pytest-memray", "pytest-mock", "pytest-randomly", "requests-mock", diff --git a/test/test_memory.py b/test/test_memory.py index 378aa56..9c09840 100644 --- a/test/test_memory.py +++ b/test/test_memory.py @@ -2,32 +2,13 @@ import contextlib import logging -import os -import platform import pytest -import example.api import example.calculator -import example.default class TestMemory: - @pytest.mark.skipif( - platform.system() == "Windows", reason="Memray doesn't work on Windows." - ) - @pytest.mark.skipif( - os.environ.get("GITHUB_ACTIONS", "") == "true", - reason="Memory usage in actions was not being predictable.", - ) - @pytest.mark.parametrize("denominator", [2, 0]) - @pytest.mark.limit_memory("10 MB") - def test_repeated_calls_do_not_accumulate_memory(self, denominator): - calc = example.calculator.Calculator(1, denominator) - for _ in range(10000): - with contextlib.suppress(ZeroDivisionError): - calc.divide() - @pytest.mark.parametrize("denominator", [2, 0]) def test_repeated_calls_do_not_accumulate_loggers(self, denominator): calc = example.calculator.Calculator(1, denominator)