Skip to content
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

chore: improve error output when call unwrap_or_return on a function … #80

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
15 changes: 15 additions & 0 deletions docs/usage/result.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ Returns the encapsulated value if this instance is a success or return Result as
).unwrap_or_return(return_value_on_failure=MyError())
```

If you are using an async function, use `@async_early_return`:

```python
from meiga import Result, Error, async_early_return

@async_early_return
async def handling_result(key: str) -> Result:
user_info = {"first_name": "Rosalia", "last_name": "De Castro", "age": 60}
first_name = string_from_key(dictionary=user_info, key=key).unwrap_or_return()
# Do whatever with the name
name = first_name.lower()
return Result(success=name)
```


### unwrap_or

Returns the encapsulated value if this instance is a success or the selected failure_value if it is failure.
Expand Down
31 changes: 28 additions & 3 deletions meiga/failures.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,48 @@
class WaitingForEarlyReturn(Error):
result: "AnyResult"
called_from: Union[str, None]
called_from_coroutine: bool = False

def __init__(self, result: "AnyResult") -> None:
self.result = result
try:
stack = inspect.stack()[2]
filename = stack.filename.split("/")[-1]
self.called_from = f"{stack[3]} on {filename}"
stack = inspect.stack()
parent_frame = stack[3]
frame = stack[2]
filename = frame.filename.split("/")[-1]
self.called_from = f"{frame[3]} on {filename}"
func = frame.function
self.called_from_coroutine = inspect.iscoroutinefunction(parent_frame.frame.f_locals.get(func))
self.is_called_from_class_coroutine(frame, func)
# Create a descriptive string for where this was called from
if self.called_from_coroutine:
self.called_from = f"{func} (async) on {filename}"
else:
self.called_from = f"{func} on {filename}"
except: # noqa
self.called_from = None
Exception.__init__(self)

def is_called_from_class_coroutine(self, frame, func):
# Check if a class in the frame contains a member function with the same name as the frame function
args, _, _, value_dict = inspect.getargvalues(frame.frame)
# we check the first parameter for the frame function is
# named 'self'
if len(args) and args[0] == "self":
# in that case, 'self' will be referenced in value_dict
instance = value_dict.get("self", None)
if instance:
cls = getattr(instance, "__class__", None)
member = getattr(cls, func)
self.called_from_coroutine = inspect.iscoroutinefunction(member) or self.called_from_coroutine

def __str__(self) -> str:
function = f" ({self.called_from})" if self.called_from else ""
return (
f"This exception wraps the following result -> {self.result}"
f"\nIf you want to handle this error and return a Failure, please use early_return decorator on your function{function}."
f"\nMore info about how to use unwrap_or_return in combination with @early_return decorator on https://alice-biometrics.github.io/meiga/usage/result/#unwrap_or_return"
f"\nUse @async_early_return if your are calling from an async function."
)

def __repr__(self) -> str:
Expand Down
6 changes: 2 additions & 4 deletions meiga/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ def execute(self, result: Result[TS, TF]) -> None:
self.func(result.value)


class OnSuccessHandler(Handler):
...
class OnSuccessHandler(Handler): ...


class OnFailureHandler(Handler):
...
class OnFailureHandler(Handler): ...
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ include = [
profile = "black"

[tool.pytest.ini_options]
markers=["unit", "property"]
markers=["unit", "property", "asyncio"]
addopts=["tests",
"-v",
"--color=yes",
Expand Down
6 changes: 2 additions & 4 deletions tests/unit/doc/example_with_meiga.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
from meiga import Error, Failure, Result, Success


class NoSuchKey(Error):
...
class NoSuchKey(Error): ...
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this depends on some ruff configuration. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New versions of ruff handle things differently to older versions.



class TypeMismatch(Error):
...
class TypeMismatch(Error): ...


def string_from_key(dictionary: dict, key: str) -> Result[str, NoSuchKey | TypeMismatch]:
Expand Down
6 changes: 2 additions & 4 deletions tests/unit/doc/example_without_meiga.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
class NoSuchKey(Exception):
...
class NoSuchKey(Exception): ...


class TypeMismatch(Exception):
...
class TypeMismatch(Exception): ...


# This return value masks the behavior of the unhappy path (Exceptions). 🥲
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/test_waiting_for_early_return.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def expected_error(value: str, called_from: Union[str, None] = None, escape: boo
f"This exception wraps the following result -> Result[status: failure | value: {value}]"
f"\nIf you want to handle this error and return a Failure, please use early_return decorator on your function{called_from}."
f"\nMore info about how to use unwrap_or_return in combination with @early_return decorator on https://alice-biometrics.github.io/meiga/usage/result/#unwrap_or_return"
f"\nUse @async_early_return if your are calling from an async function."
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to guess if caller is a regular function or an async one with something like the following:

import inspect
from typing import Union

class WaitingForEarlyReturn(Exception):
    result: 'AnyResult'  # Replace with the actual class/type if AnyResult is not defined
    called_from: Union[str, None]
    called_from_coroutine: bool = False

    def __init__(self, result: 'AnyResult') -> None:
        self.result = result
        try:
            # Get the stack frame
            stack = inspect.stack()[2]
            frame = stack.frame
            function_name = frame.f_code.co_name  # Get the name of the function
            filename = stack.filename.split("/")[-1]

            # We can't retrieve the actual function object, so we check by name
            self.called_from_coroutine = frame.f_globals.get(function_name) and inspect.iscoroutinefunction(frame.f_globals[function_name])

            # Create a descriptive string for where this was called from
            if self.called_from_coroutine:
                self.called_from = f"{function_name} (async) on {filename}"
            else:
                self.called_from = f"{function_name} on {filename}"

        except Exception:  # noqa
            self.called_from = None

        # Call the base class initializer
        Exception.__init__(self)

But, I think is not possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why you say it doesn't work 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because frame.frame.f_globals.get(func) is alway None, so called_from_coroutine is always False.

This is because func is a string not an actual Callable.

If you modify the test, you'll notice the code never pass through called_from_coroutine positive if:

    @pytest.mark.asyncio
    async def should_log_hint_when_called_async_from_class_function_and_not_early_return(self):
        class MyClass:
            async def execute(self) -> AnyResult:
                result = Failure(Error())
                result.unwrap_or_return()
                return isSuccess

        with pytest.raises(
            WaitingForEarlyReturn,
            match=expected_error(
                "Error",
                called_from="execute (async) on test_waiting_for_early_return.py",  # <-------- THIS
                escape=True,
            ),
        ):
            await MyClass().execute()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I found a way (not very elegant). Check my last commit

)
if escape:
return re.escape(text) # necessary to match on pytest.raises contextmanager
Expand Down Expand Up @@ -84,3 +85,38 @@ def execute(self) -> AnyResult:
),
):
MyClass().execute()

@pytest.mark.asyncio
async def should_log_hint_when_called_async_from_class_function_and_not_early_return(self):
class MyClass:
async def execute(self) -> AnyResult:
result = Failure(Error())
result.unwrap_or_return()
return isSuccess

with pytest.raises(
WaitingForEarlyReturn,
match=expected_error(
"Error",
called_from="execute (async) on test_waiting_for_early_return.py",
escape=True,
),
):
await MyClass().execute()

@pytest.mark.asyncio
async def should_log_hint_when_called_async_from_function_and_not_early_return(self):
async def execute() -> AnyResult:
result = Failure(Error())
result.unwrap_or_return()
return isSuccess

with pytest.raises(
WaitingForEarlyReturn,
match=expected_error(
"Error",
called_from="execute (async) on test_waiting_for_early_return.py",
escape=True,
),
):
await execute()
Loading