Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow set_testing to be used as a context manager #94

Merged
merged 2 commits into from
Feb 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
When `cap=True`, it is used as an upper cap; that means that if the original attempts number is lower, it's not changed.
[#80](https://github.com/hynek/stamina/pull/80)

- `stamina.set_testing()` can now be used as a context manager.
[#94](https://github.com/hynek/stamina/pull/94)


## [24.3.0](https://github.com/hynek/stamina/compare/24.2.0...24.3.0) - 2024-08-27

Expand Down
23 changes: 22 additions & 1 deletion src/stamina/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import annotations

from threading import Lock
from types import TracebackType
from typing import Callable

from .instrumentation import RetryHookFactory
Expand Down Expand Up @@ -154,9 +155,25 @@ def is_testing() -> bool:
return CONFIG.testing is not None


class _RestoreTestingCM:
def __init__(self, old: _Testing | None) -> None:
self.old = old

def __enter__(self) -> None:
pass

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
CONFIG.testing = self.old


def set_testing(
testing: bool, *, attempts: int = 1, cap: bool = False
) -> None:
) -> _RestoreTestingCM:
"""
Activate or deactivate test mode.

Expand All @@ -170,5 +187,9 @@ def set_testing(

.. versionadded:: 24.3.0
.. versionadded:: 25.1.0 *cap*
.. versionadded:: 25.1.0 Can be used as a context manager.
"""
old = CONFIG.testing
CONFIG.testing = _Testing(attempts, cap) if testing else None

return _RestoreTestingCM(old)
21 changes: 21 additions & 0 deletions tests/test_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,24 @@ def test_repr(self):
assert f"<BoundAsyncRetryingCaller(ValueError, {r})>" == repr(
arc.on(ValueError)
)


async def test_testing_mode_context():
"""
Testing mode context manager works with async code.
"""
assert not stamina.is_testing()

with stamina.set_testing(True, attempts=3):
assert stamina.is_testing()

with pytest.raises(ValueError): # noqa: PT012
async for attempt in stamina.retry_context(on=ValueError):
assert 0.0 == attempt.next_wait

with attempt:
raise ValueError

assert 3 == attempt.num

assert not stamina.is_testing()
51 changes: 49 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
#
# SPDX-License-Identifier: MIT

from contextlib import suppress
from threading import Lock

from stamina import is_active, set_active
from stamina._config import _Config, _Testing
from stamina import is_active, is_testing, set_active, set_testing
from stamina._config import CONFIG, _Config, _Testing


def test_activate_deactivate():
Expand Down Expand Up @@ -67,3 +68,49 @@ def test_cap_true_with_none(self):
t = _Testing(100, True)

assert 100 == t.get_attempts(None)

def test_context_manager(self):
"""
set_testing works as a context manager.
"""
assert not is_testing()

with set_testing(True, attempts=3):
assert is_testing()
assert 3 == CONFIG.testing.get_attempts(None)
assert not CONFIG.testing.cap

assert not is_testing()

def test_context_manager_nested(self):
"""
set_testing context managers can be nested.
"""
assert not is_testing()

with set_testing(True, attempts=3):
assert is_testing()
assert CONFIG.testing.attempts == 3

with set_testing(True, attempts=5, cap=True):
assert is_testing()
assert CONFIG.testing.attempts == 5
assert CONFIG.testing.cap

assert is_testing()
assert CONFIG.testing.attempts == 3
assert not CONFIG.testing.cap

assert not is_testing()

def test_context_manager_exception(self):
"""
set_testing context manager restores state even if an exception occurs.
"""
assert not is_testing()

with suppress(ValueError), set_testing(True, attempts=3):
assert is_testing()
raise ValueError("test")

assert not is_testing()
21 changes: 21 additions & 0 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,24 @@ def test_repr(self):
assert f"<BoundRetryingCaller(ValueError, {r})>" == repr(
rc.on(ValueError)
)


def test_testing_mode_context():
"""
Testing mode context manager works with sync code.
"""
assert not stamina.is_testing()

with stamina.set_testing(True, attempts=3):
assert stamina.is_testing()

with pytest.raises(ValueError): # noqa: PT012
for attempt in stamina.retry_context(on=ValueError):
assert 0.0 == attempt.next_wait

with attempt:
raise ValueError

assert 3 == attempt.num

assert not stamina.is_testing()