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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ incremental in minor, bugfixes only are patches.
See [0Ver](https://0ver.org/).


### Features

- Add the ability to add debugging notes to the `safe`, `impure_safe`, and
`future_safe` decorators.


## 0.25.0

### Features
Expand Down
42 changes: 38 additions & 4 deletions returns/future.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
from returns.interfaces.specific.future_result import FutureResultBased2
from returns.io import IO, IOResult
from returns.primitives.container import BaseContainer
from returns.primitives.exceptions import UnwrapFailedError
from returns.primitives.exceptions import (
UnwrapFailedError,
add_note_to_exception,
)
from returns.primitives.hkt import (
Kind1,
Kind2,
Expand Down Expand Up @@ -1342,7 +1345,7 @@ def from_io(
>>> anyio.run(main)

"""
return FutureResult.from_value(inner_value._inner_value) # noqa: SLF001
return FutureResult.from_value(inner_value._inner_value)

@classmethod
def from_failed_io(
Expand All @@ -1366,7 +1369,7 @@ def from_failed_io(
>>> anyio.run(main)

"""
return FutureResult.from_failure(inner_value._inner_value) # noqa: SLF001
return FutureResult.from_failure(inner_value._inner_value)

@classmethod
def from_ioresult(
Expand All @@ -1393,7 +1396,7 @@ def from_ioresult(
>>> anyio.run(main)

"""
return FutureResult(async_identity(inner_value._inner_value)) # noqa: SLF001
return FutureResult(async_identity(inner_value._inner_value))

@classmethod
def from_result(
Expand Down Expand Up @@ -1537,6 +1540,7 @@ def future_safe(
@overload
def future_safe(
exceptions: tuple[type[_ExceptionType], ...],
add_note_on_failure: bool | str = False,
) -> Callable[
[
Callable[
Expand All @@ -1548,6 +1552,7 @@ def future_safe(
]: ...


# add_note_on_failure is optional for backwards compatibility.
def future_safe( # noqa: WPS212, WPS234,
exceptions: (
Callable[
Expand All @@ -1556,6 +1561,7 @@ def future_safe( # noqa: WPS212, WPS234,
]
| tuple[type[_ExceptionType], ...]
),
add_note_on_failure: bool | str = False,
) -> (
Callable[_FuncParams, FutureResultE[_ValueType_co]]
| Callable[
Expand Down Expand Up @@ -1615,6 +1621,33 @@ def future_safe( # noqa: WPS212, 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.

Note that passing a blank string to ``add_note_on_failure`` will be treated
the same as passing False, and will not add a note.

.. code:: python

>>> from returns.future import future_safe

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

>>> @future_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.result.safe`
decorators, but works with ``async`` functions.

Expand All @@ -1634,6 +1667,7 @@ async def factory(
try:
return Success(await function(*args, **kwargs))
except inner_exceptions as exc:
exc = add_note_to_exception(exc, add_note_on_failure, function)
return Failure(exc)

@wraps(function)
Expand Down
40 changes: 39 additions & 1 deletion returns/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

from returns.interfaces.specific import io, ioresult
from returns.primitives.container import BaseContainer, container_equality
from returns.primitives.exceptions import UnwrapFailedError
from returns.primitives.exceptions import (
UnwrapFailedError,
add_note_to_exception,
)
from returns.primitives.hkt import (
Kind1,
Kind2,
Expand Down Expand Up @@ -905,16 +908,19 @@ def impure_safe(
@overload
def impure_safe(
exceptions: tuple[type[_ExceptionType], ...],
add_note_on_failure: bool | str = False,
) -> Callable[
[Callable[_FuncParams, _NewValueType]],
Callable[_FuncParams, IOResult[_NewValueType, _ExceptionType]],
]: ...


# add_note_on_failure is optional for backwards compatibility.
def impure_safe( # noqa: WPS234
exceptions: (
Callable[_FuncParams, _NewValueType] | tuple[type[_ExceptionType], ...]
),
add_note_on_failure: bool | str = False,
) -> (
Callable[_FuncParams, IOResultE[_NewValueType]]
| Callable[
Expand Down Expand Up @@ -960,6 +966,33 @@ def impure_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.

Note that passing a blank string to ``add_note_on_failure`` will be treated
the same as passing False, and will not add a note.

.. code:: python

>>> from returns.io import impure_safe

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

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

Similar to :func:`returns.future.future_safe`
and :func:`returns.result.safe` decorators.
"""
Expand All @@ -976,6 +1009,11 @@ def decorator(
try:
return IOSuccess(inner_function(*args, **kwargs))
except inner_exceptions as exc:
exc = add_note_to_exception(
exc,
add_note_on_failure,
inner_function,
)
return IOFailure(exc)

return decorator
Expand Down
47 changes: 46 additions & 1 deletion returns/primitives/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from collections.abc import Callable
from typing import TYPE_CHECKING, TypeVar

from typing_extensions import ParamSpec

if TYPE_CHECKING:
from returns.interfaces.unwrappable import Unwrappable # noqa: WPS433


_ValueType_co = TypeVar('_ValueType_co', covariant=True)
_FuncParams = ParamSpec('_FuncParams')
_ExceptionType = TypeVar('_ExceptionType', bound=Exception)


class UnwrapFailedError(Exception):
"""Raised when a container can not be unwrapped into a meaningful value."""

Expand Down Expand Up @@ -45,3 +53,40 @@ class ImmutableStateError(AttributeError):

See: https://github.com/dry-python/returns/issues/394
"""


def add_note_to_exception(
exception: _ExceptionType,
message: bool | str,
function: Callable[_FuncParams, _ValueType_co],
) -> _ExceptionType:
"""
A utility function to add a generic note with file name, line number, and
function name to the exception. If a custom message is provided, it will be
added as an additional note to the exception.
"""
if not message:
return exception

# If the user provides a custom message, add it as a note
# to the exception. Otherwise just add a generic note.
if isinstance(message, str):
exception.add_note(message)

# Add the generic note.
exc_traceback = exception.__traceback__
if exc_traceback is None:
return exception

if exc_traceback.tb_next is None:
return exception

filename = exc_traceback.tb_next.tb_frame.f_code.co_filename
line_number = exc_traceback.tb_next.tb_lineno
exception.add_note(
f'Exception occurred in {function.__name__} '
f'of {filename} '
f'at line number {line_number}.'
)

return exception
42 changes: 40 additions & 2 deletions returns/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

from returns.interfaces.specific import result
from returns.primitives.container import BaseContainer, container_equality
from returns.primitives.exceptions import UnwrapFailedError
from returns.primitives.exceptions import (
UnwrapFailedError,
add_note_to_exception,
)
from returns.primitives.hkt import Kind2, SupportsKind2

# Definitions:
Expand Down Expand Up @@ -466,7 +469,6 @@ def failure(self) -> Never:


# Decorators:

_ExceptionType = TypeVar('_ExceptionType', bound=Exception)


Expand All @@ -480,16 +482,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 +539,34 @@ 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.

Note that passing a blank string to ``add_note_on_failure`` will be treated
the same as passing False, and will not add a note.


.. 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 +583,11 @@ def decorator(
try:
return Success(inner_function(*args, **kwargs))
except inner_exceptions as exc:
exc = add_note_to_exception(
exc,
add_note_on_failure,
inner_function,
)
return Failure(exc)

return decorator
Expand Down
Loading
Loading