diff --git a/docs/usage/result.md b/docs/usage/result.md index 3bc5a31..d5bbe31 100644 --- a/docs/usage/result.md +++ b/docs/usage/result.md @@ -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. diff --git a/meiga/failures.py b/meiga/failures.py index 3f3954c..c3f02b6 100644 --- a/meiga/failures.py +++ b/meiga/failures.py @@ -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: diff --git a/meiga/handlers.py b/meiga/handlers.py index 0fd5b18..aca9c1e 100644 --- a/meiga/handlers.py +++ b/meiga/handlers.py @@ -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): ... diff --git a/pyproject.toml b/pyproject.toml index b41748e..4290722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ include = [ profile = "black" [tool.pytest.ini_options] -markers=["unit", "property"] +markers=["unit", "property", "asyncio"] addopts=["tests", "-v", "--color=yes", diff --git a/tests/unit/doc/example_with_meiga.py b/tests/unit/doc/example_with_meiga.py index 2450a22..4d20922 100644 --- a/tests/unit/doc/example_with_meiga.py +++ b/tests/unit/doc/example_with_meiga.py @@ -3,12 +3,10 @@ from meiga import Error, Failure, Result, Success -class NoSuchKey(Error): - ... +class NoSuchKey(Error): ... -class TypeMismatch(Error): - ... +class TypeMismatch(Error): ... def string_from_key(dictionary: dict, key: str) -> Result[str, NoSuchKey | TypeMismatch]: diff --git a/tests/unit/doc/example_without_meiga.py b/tests/unit/doc/example_without_meiga.py index c337b8b..d090837 100644 --- a/tests/unit/doc/example_without_meiga.py +++ b/tests/unit/doc/example_without_meiga.py @@ -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). 🥲 diff --git a/tests/unit/test_waiting_for_early_return.py b/tests/unit/test_waiting_for_early_return.py index c941daa..ba7c95e 100644 --- a/tests/unit/test_waiting_for_early_return.py +++ b/tests/unit/test_waiting_for_early_return.py @@ -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." ) if escape: return re.escape(text) # necessary to match on pytest.raises contextmanager @@ -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()