diff --git a/releasenotes/notes/add-configurable-retry-parameters-fe5068cf1395887d.yaml b/releasenotes/notes/add-configurable-retry-parameters-fe5068cf1395887d.yaml new file mode 100644 index 0000000..2e7d6ad --- /dev/null +++ b/releasenotes/notes/add-configurable-retry-parameters-fe5068cf1395887d.yaml @@ -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)" diff --git a/tenacity/__init__.py b/tenacity/__init__.py index 72eba04..b4054ec 100644 --- a/tenacity/__init__.py +++ b/tenacity/__init__.py @@ -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 @@ -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]) diff --git a/tenacity/config.py b/tenacity/config.py new file mode 100644 index 0000000..d010033 --- /dev/null +++ b/tenacity/config.py @@ -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"" + + +dict_config = DictConfig() diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index b76fec2..2d0baa0 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -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() @@ -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()