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

Adding DictConfig #498

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
prelude: "This release introduces configurable retry parameters with the DictConfig class and adds comprehensive unit tests for default configurations."
features:
- "Added `DictConfig` class to manage default retry parameters in a singleton dictionary. (Commit 622bea5)"
improvements:
- "Refactored the initialization method by removing redundant attribute checks and adding a new `get` method for better configuration access. (Commit 25efec0)"
issues:
- "Included a new test class `TestRetryDefaults` to verify `dict_config` functionalities within tenacity. (Commit 58b9993)"
6 changes: 6 additions & 0 deletions tenacity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from concurrent import futures

from . import _utils
from .config import dict_config

# Import all built-in retry strategies for easier usage.
from .retry import retry_base # noqa
Expand Down Expand Up @@ -628,6 +629,11 @@ def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
:param dargs: positional arguments passed to Retrying object
:param dkw: keyword arguments passed to the Retrying object
"""

# getting default config values previously saved by the user
# and overriding with the new ones
dkw = dict_config.get_config(override=dkw)

# support both @retry and @retry() as valid syntax
if len(dargs) == 1 and callable(dargs[0]):
return retry()(dargs[0])
Expand Down
102 changes: 102 additions & 0 deletions tenacity/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import typing as t
from threading import Lock


class DictConfig:
"""
Class providing a singleton configuration dictionary.

Initialising the config with custom parameters is optional,
but if you happen to re-use the same parameters over and over again
in the `retry` function, this might save you some typing.

Usage Example:
```python
from tenacity import dict_config
dict_config.set_config(
wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)
)
```

When calling retry, you can override the default config parameters or add some
new ones:
```python
@retry(wait=wait_random_exponential(min=10, max=30), stop=stop_after_attempt(10), reraise=True)
```

Methods:
- set_config: Sets multiple configuration parameters.
- set_attribute: Sets a specific configuration attribute.
- delete_attribute: Deletes a specific configuration attribute.
- get_config: Retrieves the configuration dictionary.
- __getattr__: Retrieves the value of a configuration attribute.
- __getitem__: Retrieves the value of a configuration attribute using item access.
- __contains__: Checks if a configuration attribute exists.
- __repr__: Returns a string representation of the configuration object.
"""

_instance: t.Optional["DictConfig"] = None
_lock = Lock() # For thread safety

def __new__(cls) -> "DictConfig":
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self) -> None:
self._config: t.Dict[str, t.Any] = {}

def set_config(self, **kwargs: t.Any) -> None:
"""Sets multiple configuration parameters."""
self._config.update(kwargs)

def set_attribute(self, name: str, value: t.Any) -> None:
"""Sets a specific configuration attribute."""
self._config[name] = value

def delete_attribute(self, name: str) -> None:
"""Deletes a specific configuration attribute."""
if name in self._config:
del self._config[name]
else:
raise KeyError(f"Attribute {name} not found in configuration.")

def get_config(
self, override: t.Optional[t.Dict[str, t.Any]] = None
) -> t.Dict[str, t.Any]:
"""
Retrieves the configuration dictionary.

Parameters:
override: Optional dictionary to override current configuration.

Returns:
A copy of the configuration dictionary, possibly modified with the overrides.
"""
config = self._config.copy()
if override:
config.update(override)
return config

def reset_config(self) -> None:
self._config = {}

def get(self, name: str) -> t.Any:
return self._config.get(name)

def __getattr__(self, name: str) -> t.Any:
return self._config.get(name)

def __getitem__(self, name: str) -> t.Any:
return self._config[name]

def __contains__(self, name: str) -> bool:
return name in self._config

def __repr__(self) -> str:
return f"<DictConfig {self._config}>"


dict_config = DictConfig()
64 changes: 63 additions & 1 deletion tests/test_tenacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import pytest

import tenacity
from tenacity import RetryCallState, RetryError, Retrying, retry
from tenacity import RetryCallState, RetryError, Retrying, retry, dict_config

_unset = object()

Expand Down Expand Up @@ -1793,5 +1793,67 @@ def test_decorated_retry_with(self, mock_sleep):
assert mock_sleep.call_count == 1


class TestRetryDefaults(unittest.TestCase):
def setUp(self):
# Reset config before each test
dict_config.reset_config()

def test_set_and_get_config(self):
# Set new configuration attributes
dict_config.set_config(
stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_fixed(1)
)
self.assertIsInstance(dict_config.get("stop"), tenacity.stop_after_attempt)
self.assertIsInstance(dict_config.get("wait"), tenacity.wait_fixed)

def test_override_config(self):
# Set initial configuration
dict_config.set_config(
stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_fixed(1)
)

# Override specific attribute
custom_config = dict_config.get_config(
override={"wait": tenacity.wait_fixed(2)}
)
self.assertIsInstance(custom_config["wait"], tenacity.wait_fixed)
self.assertIsInstance(custom_config["stop"], tenacity.stop_after_attempt)

def test_delete_config(self):
# Set and then delete configuration attribute
dict_config.set_attribute("stop", tenacity.stop_after_attempt(3))
self.assertIn("stop", dict_config)
dict_config.delete_attribute("stop")
self.assertNotIn("stop", dict_config)
with self.assertRaises(KeyError):
dict_config.delete_attribute("stop")

def test_retry_with_default_config(self):
# Set default configuration
dict_config.set_config(
stop=tenacity.stop_after_attempt(2), wait=tenacity.wait_fixed(0.1)
)

@retry
def failing_func():
raise ValueError("This should trigger retries")

with self.assertRaises(tenacity.RetryError):
failing_func() # Should raise a RetryError

def test_retry_with_override(self):
# Set default configuration
dict_config.set_config(
stop=tenacity.stop_after_attempt(2), wait=tenacity.wait_fixed(0.1)
)

@retry(reraise=True)
def failing_func():
raise ValueError("This should trigger retries")

with self.assertRaises(ValueError):
failing_func() # Should raise a ValueError


if __name__ == "__main__":
unittest.main()