diff --git a/.releaserc.yml b/.releaserc.yml index 428dc2b..e9e03bc 100644 --- a/.releaserc.yml +++ b/.releaserc.yml @@ -74,7 +74,7 @@ presetConfig: - type: release hidden: true - type: test - section: hidden + hidden: true releaseRules: - type: chore scope: deps @@ -86,5 +86,9 @@ releaseRules: release: minor - type: fix release: patch + - scope: minor + release: minor + - scope: patch + release: patch - scope: no-release release: false diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cd117dc..0affbd4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,3 +1,16 @@ +## [1.3.0-rc.1](https://github.com/savannahghi/sghi-commons/compare/v1.2.0...v1.3.0-rc.1) (2024-03-31) + + +### Features + +* **checkers:** add a checker for callable objects ([#29](https://github.com/savannahghi/sghi-commons/issues/29)) ([0f8272d](https://github.com/savannahghi/sghi-commons/commit/0f8272de6f9ad13809599666ef8b78a34aab9853)) + + +### Refactors + +* **patch:** explicitly list `sghi.retry` module exports ([#28](https://github.com/savannahghi/sghi-commons/issues/28)) ([45cd87e](https://github.com/savannahghi/sghi-commons/commit/45cd87e7cf956d01a8ef12aef42dbd49dc3b8508)) +* refactor retry to use `ensure_callable` ([#30](https://github.com/savannahghi/sghi-commons/issues/30)) ([51511c8](https://github.com/savannahghi/sghi-commons/commit/51511c83eec7632bccc5f60e7eaf47f17dce9bf9)) + ## [1.2.0](https://github.com/savannahghi/sghi-commons/compare/v1.1.0...v1.2.0) (2024-03-30) diff --git a/package.json b/package.json index 07cc434..1ef6284 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sghi-commons", - "version": "1.2.0", + "version": "1.3.0-rc.1", "description": "Collection of utilities and reusable components used throughout our Python projects.", "directories": { "doc": "docs" diff --git a/src/sghi/retry/__init__.py b/src/sghi/retry/__init__.py index 27a44d5..8ffd247 100644 --- a/src/sghi/retry/__init__.py +++ b/src/sghi/retry/__init__.py @@ -27,9 +27,9 @@ from sghi.exceptions import SGHIError, SGHITransientError from sghi.utils import ( + ensure_callable, ensure_greater_or_equal, ensure_greater_than, - ensure_predicate, type_fqn, ) @@ -289,11 +289,10 @@ def __init__( timeout: float | None = _DEFAULT_TIMEOUT, multiplicative_factor: float = _DEFAULT_MULTIPLICATIVE_FACTOR, ) -> None: - ensure_predicate( - callable(predicate), + self._predicate: _RetryPredicate = ensure_callable( + value=predicate, message="'predicate' MUST be a callable.", ) - self._predicate: _RetryPredicate = predicate self._initial_delay: float = ensure_greater_than( value=initial_delay, base_value=0.0, @@ -436,3 +435,17 @@ def retry(self, f: Callable[_P, _RT]) -> Callable[_P, _RT]: exponential_backoff_retry = Retry.of_exponential_backoff noop_retry = Retry.of_noop + + +# ============================================================================= +# MODULE EXPORTS +# ============================================================================= + +__all__ = [ + "Retry", + "RetryError", + "exponential_backoff_retry", + "if_exception_type_factory", + "if_transient_exception", + "noop_retry", +] diff --git a/src/sghi/utils/__init__.py b/src/sghi/utils/__init__.py index 5a02226..9611ebe 100644 --- a/src/sghi/utils/__init__.py +++ b/src/sghi/utils/__init__.py @@ -1,6 +1,7 @@ """Common utilities used throughout SGHI projects.""" from .checkers import ( + ensure_callable, ensure_greater_or_equal, ensure_greater_than, ensure_instance_of, @@ -15,6 +16,7 @@ from .others import future_succeeded, type_fqn __all__ = [ + "ensure_callable", "ensure_greater_or_equal", "ensure_greater_than", "ensure_instance_of", diff --git a/src/sghi/utils/checkers.py b/src/sghi/utils/checkers.py index 500274a..601852e 100644 --- a/src/sghi/utils/checkers.py +++ b/src/sghi/utils/checkers.py @@ -19,6 +19,29 @@ # ============================================================================= +def ensure_callable(value: _T, message: str = "A callable is required.") -> _T: + """Check that the given value is a callable object (some kind of function). + + A callable should have the same semantics as those defined by the + ``builtin.callable`` function to qualify. That is, it should be function or + method, a class or an instance of a class with a ``__call__`` method. + + If ``value`` is NOT a callable, then a :exc:`ValueError` is raised; else + ``value`` is returned as is. + + :param value: The object to check if it is a callable. + :param message: An optional error message to be shown when ``value`` is NOT + a callable. + + :return: ``value`` if it is a callable. + + :raise ValueError: If the given ``value`` is NOT a callable. + """ + if not callable(value): + raise ValueError(message) + return value + + def ensure_greater_or_equal( value: _CT, base_value: Comparable, diff --git a/test/sghi/utils_tests/checkers_tests.py b/test/sghi/utils_tests/checkers_tests.py index 90df621..f19ece4 100644 --- a/test/sghi/utils_tests/checkers_tests.py +++ b/test/sghi/utils_tests/checkers_tests.py @@ -5,6 +5,7 @@ import sghi.app from sghi.config import Config, ConfigProxy from sghi.utils import ( + ensure_callable, ensure_greater_or_equal, ensure_greater_than, ensure_instance_of, @@ -23,6 +24,47 @@ from sghi.typing import Comparable +def test_ensure_callable_return_value_on_valid_input() -> None: + """ + :func:`ensure_callable` should return the input value if the given + ``value`` is a callable. + """ + + class _Callable: + def __call__(self, *args, **kwargs) -> None: ... + + a_callable = _Callable() + + assert ensure_callable(callable) is callable + assert ensure_callable(type_fqn) is type_fqn + assert ensure_callable(_Callable) is _Callable + assert ensure_callable(a_callable) is a_callable + + +def test_ensure_callable_fails_on_invalid_input() -> None: + """ + :func:`ensure_callable` should raise a ``ValueError`` when the given + ``value`` is not a callable. + """ + inputs: Iterable = ("", 45, None, []) + + # With default message + default_msg: str = "A callable is required." + for value in inputs: + with pytest.raises(ValueError, match=default_msg) as exp_info: + ensure_callable(value) + + assert exp_info.value.args[0] == default_msg + + # Test with a custom message + custom_msg: str = "Would you please provide a callable." + for value in inputs: + with pytest.raises(ValueError, match=custom_msg) as exp_info: + ensure_callable(value, message=custom_msg) + + assert exp_info.value.args[0] == custom_msg + + def test_ensure_greater_or_equal_return_value_on_valid_input() -> None: """ :func:`ensure_greater_or_equal` should return the input value if the given