diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e653f..749dc6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## v8.2.1 - 2026-10-12 + +* Added `handle_errors_async()` to Buzz + + ## v8.2.0 - 2026-10-12 * Added `raise_from()` and `Buzz.raise_from()` for wrapping exceptions while preserving their causes diff --git a/docs/source/features.md b/docs/source/features.md index 73095a7..d38ee7b 100644 --- a/docs/source/features.md +++ b/docs/source/features.md @@ -431,6 +431,18 @@ very complicated things while handling an exception, you should use a standard t catch block. However, there are some extra bells and whistles on `handle_errors()` that can be used by passing additional keyword arguments to the function. +For async code, use `Buzz.handle_errors_async()` as an async context manager. It wraps +handled exceptions as `Buzz` by default and supports async callbacks for `do_except`, +`do_else`, and `do_finally`: + +```python +from buzz import Buzz + +async def fetch_data(): + async with Buzz.handle_errors_async("Could not fetch data"): + return await fetch_from_service() +``` + #### `raise_exc_class` @@ -821,7 +833,7 @@ The `Buzz` base class provides the same sort of access for: - `enforce_defined()` - `ensure_type()` - `verify_literal()` -- `handle_errors()` +- `handle_errors()` and `handle_errors_async()` - `raise_from()` - `check_expressions()` - `retry()` and `retry_async()` diff --git a/pyproject.toml b/pyproject.toml index e903cf1..123affc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "py-buzz" -version = "8.2.0" +version = "8.2.1" description = "\"That's not flying, it's falling with style\": Exceptions with extras" authors = [ {name = "Tucker Beck", email = "tucker.beck@gmail.com"} diff --git a/src/buzz/base.py b/src/buzz/base.py index 2dff00d..ca4020e 100644 --- a/src/buzz/base.py +++ b/src/buzz/base.py @@ -12,6 +12,8 @@ from typing_extensions import Self, TypeForm, override from buzz.tools import ( + AsyncDoExceptParamsCallback, + AsyncNoArgCallback, DoExceptParamsCallback, ExcBuilderParams, ExceptionCallback, @@ -22,6 +24,7 @@ ensure_type, get_traceback, handle_errors, + handle_errors_async, raise_from, require_condition, retry, @@ -356,6 +359,37 @@ def handle_errors( exc_builder=cls.exc_builder, ) + @classmethod + def handle_errors_async( + cls, + base_message: str, + re_raise: bool = True, + raise_args: Iterable[Any] | None = None, + raise_kwargs: Mapping[str, Any] | None = None, + handle_exc_class: type[BaseException] | tuple[type[BaseException], ...] = Exception, + ignore_exc_class: type[BaseException] | tuple[type[BaseException], ...] | None = None, + do_finally: NoArgCallback | AsyncNoArgCallback | None = None, + do_except: DoExceptParamsCallback | AsyncDoExceptParamsCallback | None = None, + do_else: NoArgCallback | AsyncNoArgCallback | None = None, + ): + """ + Provide an async context manager that intercepts and repackages exceptions as this class. + + Async callbacks are supported for `do_finally`, `do_except`, and `do_else`. + """ + return handle_errors_async( + base_message, + raise_exc_class=cls if re_raise else None, + raise_args=raise_args, + raise_kwargs=raise_kwargs, + handle_exc_class=handle_exc_class, + ignore_exc_class=ignore_exc_class, + do_finally=do_finally, + do_except=do_except, + do_else=do_else, + exc_builder=cls.exc_builder, + ) + @classmethod def raise_from( cls, diff --git a/tests/test_base.py b/tests/test_base.py index 6c0078d..c1d8e07 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -45,6 +45,30 @@ def test_Buzz_handle_errors__false_re_raise_absorbs_errors(): pass +@pytest.mark.asyncio +async def test_Buzz_handle_errors_async__basic(): + handled_errors = [] + + async def record_error(params): + handled_errors.append(params.err) + + with pytest.raises(Buzz) as err_info: + async with Buzz.handle_errors_async("intercepted exception", do_except=record_error): + raise ValueError("there was a problem") + + assert "there was a problem" in str(err_info.value) + assert "intercepted exception" in str(err_info.value) + assert err_info.value.base_message == "intercepted exception" + assert len(handled_errors) == 1 + assert isinstance(handled_errors[0], ValueError) + + +@pytest.mark.asyncio +async def test_Buzz_handle_errors_async__false_re_raise_absorbs_errors(): + async with Buzz.handle_errors_async("intercepted exception", re_raise=False): + raise ValueError("there was a problem") + + def test_Buzz_check_expressions__basic(): with pytest.raises(Buzz) as err_info: with Buzz.check_expressions("there will be errors") as check: