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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).


## v8.2.0 - 2026-10-12

* Added `raise_from()` and `Buzz.raise_from()` for wrapping exceptions while preserving their causes
* Added tests, documentation, examples, and demos for `raise_from()`


## v8.1.2 - 2026-04-10

* Added `BaseException` widenign from 8.1.1 to base Buzz class as well.
Expand Down Expand Up @@ -175,6 +181,7 @@ reference the old callback signatures, you'll need to update them to use the new

* Added `enforce_defined()` to Buzz class.


## v3.2.0 - 2022-06-20

* Added `enforce_defined()` function with tests and docs.
Expand Down
42 changes: 39 additions & 3 deletions docs/source/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ with handle_errors("Something went wrong", do_finally=close_resource):
some_dangerous_function_that_uses_resource(resource)
```


#### `exc_builder`

Functions the same as `require_condition()`.
Expand Down Expand Up @@ -582,7 +583,8 @@ The `retry()` decorator accepts a required message parameter followed by several

#### `message`

The message for the exception raised after all retries are exhausted. This is a **required positional parameter**. Supports the `{attempts}` placeholder which will be replaced with the actual number of attempts.
The message for the exception raised after all retries are exhausted. This is a **required positional parameter**.
Supports the `{attempts}` placeholder which will be replaced with the actual number of attempts.


#### `max_attempts`
Expand Down Expand Up @@ -693,7 +695,7 @@ with check_expressions(main_message='there will be errors') as check:

If the above code was executed, an exception would be raised that looks like this:

```
```text
Exception: Checked expressions failed: there will be errors
2: 2nd expression failed
3: one is not two
Expand Down Expand Up @@ -735,6 +737,39 @@ Functions the same as `require_condition()`.

## Additional Features

### `raise_from`

Use `raise_from()` to wrap an exception when handling it yourself is more useful than using the `handle_errors()`
context manager. The new exception keeps the original exception as its cause, so Python's traceback shows both errors:

```python
from buzz import raise_from

try:
load_profile()
except OSError as err:
raise_from("Could not load profile", err, raise_exc_class=RuntimeError)
```

By default, the raised exception message includes the base message, the original exception type, and its message. Pass
`final_message` to supply a preformatted message, `trace` to attach a specific traceback, or `exc_builder` to construct
an exception with a nonstandard constructor. `raise_args` and `raise_kwargs` provide additional constructor arguments.

For exceptions derived from `Buzz`, use the class method to raise an instance of that exception class:

```python
from buzz import Buzz

class ProfileError(Buzz):
pass

try:
load_profile()
except OSError as err:
ProfileError.raise_from("Could not load profile", err)
```


### `reformat_exception`

This method is used internally by the `handle_errors()` context manager. However, it is
Expand All @@ -750,7 +785,7 @@ except Exception as err:

The above block would result in output like:

```
```text
> welp...that didn't work -- ValueError: I didn't like that
```

Expand Down Expand Up @@ -787,6 +822,7 @@ The `Buzz` base class provides the same sort of access for:
- `ensure_type()`
- `verify_literal()`
- `handle_errors()`
- `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.1.2"
version = "8.2.0"
description = "\"That's not flying, it's falling with style\": Exceptions with extras"
authors = [
{name = "Tucker Beck", email = "tucker.beck@gmail.com"}
Expand Down
2 changes: 2 additions & 0 deletions src/buzz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
get_traceback,
handle_errors,
handle_errors_async,
raise_from,
reformat_exception,
require_condition,
verify_literal,
Expand All @@ -22,6 +23,7 @@
"handle_errors",
"handle_errors_async",
"reformat_exception",
"raise_from",
"require_condition",
"verify_literal",
]
36 changes: 36 additions & 0 deletions src/buzz/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import textwrap
from collections.abc import Iterable, Mapping
from types import TracebackType
from typing import Any, TypeVar

from typing_extensions import Self, TypeForm, override
Expand All @@ -21,6 +22,7 @@
ensure_type,
get_traceback,
handle_errors,
raise_from,
require_condition,
retry,
retry_async,
Expand Down Expand Up @@ -354,6 +356,40 @@ def handle_errors(
exc_builder=cls.exc_builder,
)

@classmethod
def raise_from(
cls,
base_message: str,
err: BaseException,
raise_args: Iterable[Any] | None = None,
raise_kwargs: Mapping[str, Any] | None = None,
final_message: str | None = None,
trace: TracebackType | None = None,
) -> None:
"""
Raise an instance of this Buzz exception from an existing exception.

Args:
base_message: The base message to include in the raised exception.
err: The original exception to raise from.
raise_args: Additional positional arguments to pass to the raised exception.
raise_kwargs: Keyword arguments to pass to the raised exception.
final_message: The message to use for the raised exception. If omitted, format it from `base_message`
and `err`.
trace: The traceback to attach to the raised exception. If omitted, use the active exception's
traceback.
"""
raise_from(
base_message,
err,
raise_exc_class=cls,
raise_args=raise_args,
raise_kwargs=raise_kwargs,
exc_builder=cls.exc_builder,
final_message=final_message,
trace=trace,
)

@classmethod
def get_traceback(cls, *args: Any, **kwargs: Any):
"""
Expand Down
61 changes: 52 additions & 9 deletions src/buzz/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from asyncio import iscoroutinefunction
from collections.abc import AsyncIterator, Coroutine, Iterable, Iterator, Mapping
from typing import Any, Callable, Literal, Protocol, TypeVar, cast, get_args, get_origin

from typing_extensions import TypeForm

TNonNull = TypeVar("TNonNull")
Expand Down Expand Up @@ -465,6 +466,47 @@ def get_traceback() -> types.TracebackType | None:
return sys.exc_info()[2]


def raise_from(
base_message: str,
err: BaseException,
raise_exc_class: type[Exception] = Exception,
raise_args: Iterable[Any] | None = None,
raise_kwargs: Mapping[str, Any] | None = None,
exc_builder: Callable[[ExcBuilderParams], Exception] = default_exc_builder,
final_message: str | None = None,
trace: types.TracebackType | None = None,
) -> None:
"""
Raise a new exception from an existing exception.

Build the raised exception using `exc_builder`, preserving `err` as its `__cause__`. By default, format the
message from `base_message` and `err`, and use the active exception's traceback.

Args:
base_message: The base message to include in `ExcBuilderParams` passed to `exc_builder`.
err: The original exception to raise from.
raise_exc_class: The type of exception to raise.
raise_args: Additional positional arguments to pass to the raised exception.
raise_kwargs: Keyword arguments to pass to the raised exception.
exc_builder: A function that constructs the raised exception, useful for exception classes with
nonstandard constructors.
final_message: The message to use for the raised exception. If omitted, format it from `base_message`
and `err` with `reformat_exception()`.
trace: The traceback to attach to the raised exception. If omitted, use `get_traceback()`.
"""
final_message = final_message or reformat_exception(base_message, err)
trace = trace or get_traceback()
raise exc_builder(
ExcBuilderParams(
raise_exc_class=raise_exc_class,
message=final_message,
raise_args=raise_args or [],
raise_kwargs=raise_kwargs or {},
base_message=base_message,
)
).with_traceback(trace) from err


@contextlib.contextmanager
def handle_errors(
base_message: str,
Expand Down Expand Up @@ -556,15 +598,16 @@ class _DefaultIgnoreException(Exception):
)
)
if raise_exc_class is not None:
raise exc_builder(
ExcBuilderParams(
raise_exc_class=raise_exc_class,
message=final_message,
raise_args=raise_args or [],
raise_kwargs=raise_kwargs or {},
base_message=base_message,
)
).with_traceback(trace) from err
raise_from(
base_message,
err,
raise_exc_class=raise_exc_class,
raise_args=raise_args,
raise_kwargs=raise_kwargs,
exc_builder=exc_builder,
final_message=final_message,
trace=trace,
)
else:
if do_else:
do_else()
Expand Down
1 change: 1 addition & 0 deletions src/buzz_demo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Feature(AutoNameEnum):
ensure_type = auto()
verify_literal = auto()
require_condition = auto()
raise_from = auto()
with_buzz_class = auto()
using_exc_builder = auto()

Expand Down
35 changes: 35 additions & 0 deletions src/buzz_demo/raise_from.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
This demo shows how to raise a new exception from an existing exception with `raise_from()`.
"""

from __future__ import annotations

from buzz import Buzz, raise_from


def demo_1__preserve_cause():
"""
This function demonstrates how the `raise_from()` function raises a new exception of the
provided type from an existing exception while reformatting the final message to wrap the
original exception message.
"""
try:
raise OSError("connection refused")
except OSError as err:
raise_from("Could not load the profile", err, raise_exc_class=RuntimeError)


def demo_2__buzz_class():
"""
This function demonstrates how the `raise_from()` method on a Buzz-derived class raises a
new exception of the provided type from an existing exception while reformatting the final
message to wrap the original exception message.
"""

class ProfileError(Buzz):
pass

try:
raise OSError("connection refused")
except OSError as err:
ProfileError.raise_from("Could not load the profile", err)
13 changes: 13 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ class OtherBuzz(Buzz):
Buzz.exc_builder(params)


def test_Buzz_raise_from__basic():
original_error = ValueError("original problem")

with pytest.raises(Buzz, match="request failed -- ValueError: original problem") as err_info:
try:
raise original_error
except ValueError as err:
Buzz.raise_from("request failed", err)

assert err_info.value.__cause__ is original_error
assert err_info.value.base_message == "request failed"


def test_Buzz_get_traceback():
"""Verify get_traceback can be called via Buzz class."""
try:
Expand Down
19 changes: 19 additions & 0 deletions tests/test_derived.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ def test_derived_check_expressions():
assert err_info.value.extra_kwarg == "extra kwarg"


def test_derived_raise_from():
original_error = ValueError("original problem")

with pytest.raises(MiraNova, match="request failed -- ValueError: original problem") as err_info:
try:
raise original_error
except ValueError as err:
MiraNova.raise_from(
"request failed",
err,
raise_args=["extra arg"],
raise_kwargs={"extra_kwarg": "extra kwarg"},
)

assert err_info.value.__cause__ is original_error
assert err_info.value.extra_arg == "extra arg"
assert err_info.value.extra_kwarg == "extra kwarg"


def test_derived_handle_errors():
with pytest.raises(MiraNova) as err_info:
with MiraNova.handle_errors(
Expand Down
Loading
Loading