Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion docs/source/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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()`
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"}
Expand Down
34 changes: 34 additions & 0 deletions src/buzz/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from typing_extensions import Self, TypeForm, override

from buzz.tools import (
AsyncDoExceptParamsCallback,
AsyncNoArgCallback,
DoExceptParamsCallback,
ExcBuilderParams,
ExceptionCallback,
Expand All @@ -22,6 +24,7 @@
ensure_type,
get_traceback,
handle_errors,
handle_errors_async,
raise_from,
require_condition,
retry,
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading