From 622bea50f984e31d03ea137791e2beb3e4aa0bfb Mon Sep 17 00:00:00 2001 From: SalvatoreZagaria Date: Sat, 28 Sep 2024 12:17:24 +0100 Subject: [PATCH 1/7] Add configurable retry parameters with DictConfig Introduce a DictConfig class to manage default retry parameters in a singleton dictionary. Enhances flexibility by allowing default configurations to be overridden or extended when calling the retry decorator. --- tenacity/__init__.py | 6 +++ tenacity/config.py | 97 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 tenacity/config.py 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..ab10d97 --- /dev/null +++ b/tenacity/config.py @@ -0,0 +1,97 @@ +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 = None + _lock = Lock() # For thread safety + + def __new__(cls): + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not hasattr(self, '_config'): + self._config = {} + + 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): + self._config = {} + + 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() From 25efec031f7d9280a9bf1ff2705bcd7c31712b4b Mon Sep 17 00:00:00 2001 From: SalvatoreZagaria Date: Sat, 28 Sep 2024 12:57:30 +0100 Subject: [PATCH 2/7] Refactor and add new method for configuration handling Remove redundant attribute check in __init__ method and simplify its initialization. Add a new `get` method to access configuration values more explicitly and efficiently. --- tenacity/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tenacity/config.py b/tenacity/config.py index ab10d97..8c4cf3d 100644 --- a/tenacity/config.py +++ b/tenacity/config.py @@ -45,8 +45,7 @@ def __new__(cls): return cls._instance def __init__(self): - if not hasattr(self, '_config'): - self._config = {} + self._config = {} def set_config(self, **kwargs: t.Any) -> None: """Sets multiple configuration parameters.""" @@ -81,6 +80,9 @@ def get_config(self, override: t.Optional[t.Dict[str, t.Any]] = None) -> t.Dict[ def reset_config(self): 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) From 58b99932500803a59e721bf71dab8d6738398986 Mon Sep 17 00:00:00 2001 From: SalvatoreZagaria Date: Sat, 28 Sep 2024 12:57:41 +0100 Subject: [PATCH 3/7] Add unit tests for tenacity's default configurations This commit introduces a new test class `TestRetryDefaults` to verify `dict_config` functionalities within tenacity. Included tests cover setting, getting, overriding, and deleting configuration attributes, as well as testing retry behavior with default and overridden configurations. --- tests/test_tenacity.py | 53 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index b76fec2..0c9ba13 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,56 @@ 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() From 506a573b0c27b51b184e61173f137734a267083d Mon Sep 17 00:00:00 2001 From: SalvatoreZagaria Date: Sat, 28 Sep 2024 14:48:04 +0100 Subject: [PATCH 4/7] Refactor code formatting for better readability Reformatted function calls and comments across `tests/test_tenacity.py` and `tenacity/config.py` for consistent indentation and alignment. This improves the code's readability and maintainability. --- tenacity/config.py | 11 +++++++---- tests/test_tenacity.py | 23 +++++++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/tenacity/config.py b/tenacity/config.py index 8c4cf3d..e460e72 100644 --- a/tenacity/config.py +++ b/tenacity/config.py @@ -34,8 +34,9 @@ class DictConfig: - __contains__: Checks if a configuration attribute exists. - __repr__: Returns a string representation of the configuration object. """ + _instance = None - _lock = Lock() # For thread safety + _lock = Lock() # For thread safety def __new__(cls): if cls._instance is None: @@ -60,9 +61,11 @@ def delete_attribute(self, name: str) -> None: if name in self._config: del self._config[name] else: - raise KeyError(f'Attribute {name} not found in configuration.') + 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]: + def get_config( + self, override: t.Optional[t.Dict[str, t.Any]] = None + ) -> t.Dict[str, t.Any]: """ Retrieves the configuration dictionary. @@ -93,7 +96,7 @@ def __contains__(self, name: str) -> bool: return name in self._config def __repr__(self) -> str: - return f'' + return f"" dict_config = DictConfig() diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index 0c9ba13..2d0baa0 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -1800,16 +1800,22 @@ def setUp(self): 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)) + 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)) + 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)}) + 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) @@ -1824,17 +1830,22 @@ def test_delete_config(self): 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)) + 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 + 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)) + dict_config.set_config( + stop=tenacity.stop_after_attempt(2), wait=tenacity.wait_fixed(0.1) + ) @retry(reraise=True) def failing_func(): From 832973506c258d956e4d5244167a2921a56b7082 Mon Sep 17 00:00:00 2001 From: SalvatoreZagaria Date: Sat, 28 Sep 2024 14:51:15 +0100 Subject: [PATCH 5/7] Add configurable retry parameters and unit tests Introduced the `DictConfig` class to manage default retry parameters in a singleton dictionary. Refactored initialization methods, adding a new `get` method for better configuration access. Included a new test class `TestRetryDefaults` to verify `dict_config` functionalities. --- ...gurable-retry-parameters-fe5068cf1395887d.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 releasenotes/notes/add-configurable-retry-parameters-fe5068cf1395887d.yaml 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..096be78 --- /dev/null +++ b/releasenotes/notes/add-configurable-retry-parameters-fe5068cf1395887d.yaml @@ -0,0 +1,15 @@ +--- +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) From 93e542756f7748a7671a78292287669625765bf1 Mon Sep 17 00:00:00 2001 From: SalvatoreZagaria Date: Sat, 28 Sep 2024 15:08:22 +0100 Subject: [PATCH 6/7] Simplify release notes format for retry parameters. Converted multi-line entries in release notes to single-line with quotes. This improves readability and maintains consistency across the document. Reduced redundancy in formatting for a cleaner output. --- ...gurable-retry-parameters-fe5068cf1395887d.yaml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/releasenotes/notes/add-configurable-retry-parameters-fe5068cf1395887d.yaml b/releasenotes/notes/add-configurable-retry-parameters-fe5068cf1395887d.yaml index 096be78..2e7d6ad 100644 --- a/releasenotes/notes/add-configurable-retry-parameters-fe5068cf1395887d.yaml +++ b/releasenotes/notes/add-configurable-retry-parameters-fe5068cf1395887d.yaml @@ -1,15 +1,8 @@ --- -prelude: > - This release introduces configurable retry parameters with the - DictConfig class and adds comprehensive unit tests for default - configurations. +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) + - "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) + - "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) + - "Included a new test class `TestRetryDefaults` to verify `dict_config` functionalities within tenacity. (Commit 58b9993)" From cbd7d3f3c417c3c0b0886578050ae8df73ee093f Mon Sep 17 00:00:00 2001 From: SalvatoreZagaria Date: Sat, 28 Sep 2024 15:11:26 +0100 Subject: [PATCH 7/7] Refactor tenacity/config.py to add type annotations Updated the singleton implementation and added type annotations to various methods and attributes in the DictConfig class. This improves code readability and helps with static type checking. --- tenacity/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tenacity/config.py b/tenacity/config.py index e460e72..d010033 100644 --- a/tenacity/config.py +++ b/tenacity/config.py @@ -35,18 +35,18 @@ class DictConfig: - __repr__: Returns a string representation of the configuration object. """ - _instance = None + _instance: t.Optional["DictConfig"] = None _lock = Lock() # For thread safety - def __new__(cls): + 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): - self._config = {} + def __init__(self) -> None: + self._config: t.Dict[str, t.Any] = {} def set_config(self, **kwargs: t.Any) -> None: """Sets multiple configuration parameters.""" @@ -80,7 +80,7 @@ def get_config( config.update(override) return config - def reset_config(self): + def reset_config(self) -> None: self._config = {} def get(self, name: str) -> t.Any: