diff --git a/cfig/config.py b/cfig/config.py index 30bb756..ad0ef0e 100644 --- a/cfig/config.py +++ b/cfig/config.py @@ -31,17 +31,39 @@ class Configuration: An extended :class:`dict` with methods to perform some actions on the contained proxies. """ - def resolve(self): + def resolve(self) -> None: """ Resolve all values of the proxies inside this dictionary. + + :raises .errors.BatchResolutionFailure: If it was not possible to resolve at least one value. """ - log.debug("Resolving and caching all values...") - for item in self.values(): - log.debug(f"Resolving: {item!r}") - _ = item.__wrapped__ + errors_dict = {} - def unresolve(self): + log.debug("Resolving and caching all proxied values...") + for key, proxy in self.items(): + log.debug(f"Resolving: {proxy!r}") + try: + _ = proxy.__wrapped__ + except Exception as e: + errors_dict[key] = e + + if errors_dict: + raise errors.BatchResolutionFailure(errors=errors_dict) + + def resolve_failfast(self) -> None: + """ + Resolve all values of the proxies inside this dictionary, failing immediately if an error occurs during a resolution, and raising the error itself. + + :raises Exception: The error occurred during the resolution. + """ + + log.debug("Resolving and caching all proxied values in failfast mode...") + for key, proxy in self.items(): + log.debug(f"Resolving: {proxy!r}") + _ = proxy.__wrapped__ + + def unresolve(self) -> None: """ Unresolve all values of the proxies inside this dictionary. """ @@ -98,7 +120,7 @@ class Configuration: if not key: log.debug("Determining key...") - key: str = self._find_resolver_key(configurable) + key = self._find_resolver_key(configurable) log.debug(f"Key is: {key!r}") log.debug("Creating required item...") @@ -135,7 +157,7 @@ class Configuration: if not key: log.debug("Determining key...") - key: str = self._find_resolver_key(configurable) + key = self._find_resolver_key(configurable) log.debug(f"Key is: {key!r}") log.debug("Creating optional item...") diff --git a/cfig/errors.py b/cfig/errors.py index e47d0d3..fae59d7 100644 --- a/cfig/errors.py +++ b/cfig/errors.py @@ -1,6 +1,14 @@ -class DefinitionError(Exception): +class CfigError(Exception): + """ + Base class for all :mod:`cfig` errors. + """ + + +class DefinitionError(CfigError): """ An error is present in the definition of a :class:`cfig.Configuration`. + + This is a developer-side error: the user has no way to solve it. """ @@ -24,9 +32,11 @@ class DuplicateProxyNameError(ProxyRegistrationError): """ -class ConfigurationError(Exception): +class ConfigurationError(CfigError): """ An error is present in the configuration specified by the user. + + This is a user-side error: the developer of the application has no way to solve it. """ @@ -36,6 +46,18 @@ class MissingValueError(ConfigurationError): """ +class BatchResolutionFailure(BaseException): + """ + A cumulative error which sums the errors occurred while resolving proxied configuration values. + """ + + def __init__(self, errors: dict[str, Exception]): + self.errors: dict[str, Exception] = errors + + def __repr__(self): + return f"<{self.__class__.__qualname__}: {len(self.errors)} errors>" + + __all__ = ( "DefinitionError", "UnknownResolverNameError", @@ -43,4 +65,5 @@ __all__ = ( "DuplicateProxyNameError", "ConfigurationError", "MissingValueError", + "BatchResolutionFailure", ) diff --git a/cfig/tests/test_all.py b/cfig/tests/test_all.py index 4280851..dd44d54 100644 --- a/cfig/tests/test_all.py +++ b/cfig/tests/test_all.py @@ -60,8 +60,9 @@ class TestConfig: assert not os.environ.get("FIRST_NUMBER") assert not os.environ.get("SECOND_NUMBER") - with pytest.raises(cfig.MissingValueError): + with pytest.raises(cfig.BatchResolutionFailure) as ei: numbers_config.proxies.resolve() + assert isinstance(ei.value.errors["FIRST_NUMBER"], cfig.MissingValueError) def test_resolve_required(self, numbers_config, monkeypatch): monkeypatch.setenv("FIRST_NUMBER", "1")