Skip to content

Add the ability to add debugging notes to the result.safe decorator. #2110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ incremental in minor, bugfixes only are patches.
See [0Ver](https://0ver.org/).


### Features

- Add the ability to add debugging notes to the `safe` decorator.


## 0.25.0

### Features
Expand Down
46 changes: 46 additions & 0 deletions returns/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,16 +480,19 @@ def safe(
@overload
def safe(
exceptions: tuple[type[_ExceptionType], ...],
add_note_on_failure: bool | str = False,
) -> Callable[
[Callable[_FuncParams, _ValueType_co]],
Callable[_FuncParams, Result[_ValueType_co, _ExceptionType]],
]: ...


# add_note_on_failure is optional for backwards compatibility.
def safe( # noqa: WPS234
exceptions: (
Callable[_FuncParams, _ValueType_co] | tuple[type[_ExceptionType], ...]
),
add_note_on_failure: bool | str = False,
) -> (
Callable[_FuncParams, ResultE[_ValueType_co]]
| Callable[
Expand Down Expand Up @@ -534,6 +537,30 @@ def safe( # noqa: WPS234
In this case, only exceptions that are explicitly
listed are going to be caught.

In order to add a note to the exception, you can use the
``add_note_on_failure`` argument. It can be a string or a boolean value.
Either way, a generic note will be added to the exception that calls out
the file, line number, and function name where the error occured. If a
string is provided, it will be added as an additional note to the
exception.

This feature can help with logging and debugging.

Note that if you use this option, you must provide a tuple of exception
types as the first argument.

.. code:: python

>>> from returns.result import safe

>>> @safe((Exception,), add_note_on_failure=True)
... def error_throwing_function() -> None:
... raise ValueError("This is an error!")

>>> @safe((Exception,), add_note_on_failure="A custom message")
... def error_throwing_function() -> None:
... raise ValueError("This is an error!")

Similar to :func:`returns.io.impure_safe`
and :func:`returns.future.future_safe` decorators.
"""
Expand All @@ -550,6 +577,25 @@ def decorator(
try:
return Success(inner_function(*args, **kwargs))
except inner_exceptions as exc:
if add_note_on_failure:
# If the user provides a custom message, add it as a note
# to the exception. Otherwise just add a generic note.
if isinstance(add_note_on_failure, str):
exc.add_note(add_note_on_failure)

# Add the generic note.
exc_traceback = exc.__traceback__
if exc_traceback is not None:
filename = (
exc_traceback.tb_next.tb_frame.f_code.co_filename
)
line_number = exc_traceback.tb_next.tb_lineno
exc.add_note(
f'Exception occurred in {inner_function.__name__} '
f'of {filename} '
f'at line number {line_number}.'
)

return Failure(exc)

return decorator
Expand Down
43 changes: 43 additions & 0 deletions tests/test_result/test_add_note_safe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from returns.pipeline import is_successful
from returns.result import safe


@safe((Exception,), add_note_on_failure=True)
def error_throwing_function() -> None:
"""Raises an exception."""
raise ValueError('This is an error!')


@safe((Exception,), add_note_on_failure='A custom message')
def error_throwing_function_with_message() -> None:
"""Raises an exception."""
raise ValueError('This is an error!')


def test_add_note_safe() -> None:
"""Tests the add_note decorator with safe."""
result = error_throwing_function()

print(result)
print(result.failure().__notes__)
print(result.failure())
assert not is_successful(result)
assert (
'Exception occurred in error_throwing_function'
in result.failure().__notes__[0]
)


def test_add_note_safe_with_message() -> None:
"""Tests the add_note decorator with safe."""
result = error_throwing_function_with_message()

print(result)
print(result.failure().__notes__)
print(result.failure())
assert not is_successful(result)
assert 'A custom message' in result.failure().__notes__
assert (
'Exception occurred in error_throwing_function_with_message'
in result.failure().__notes__[1]
)
Loading