Skip to content

Commit

Permalink
Merge pull request #7 from alice-biometrics/feature/result_id_by_refe…
Browse files Browse the repository at this point in the history
…rence_on_handle

Feature/result id by reference on handle
  • Loading branch information
acostapazo authored Mar 11, 2020
2 parents 4fd3861 + 399b7d1 commit a83e062
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 32 deletions.
117 changes: 89 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
meiga 🧙 [![version](https://img.shields.io/github/release/alice-biometrics/meiga/all.svg)](https://github.com/alice-biometrics/meiga/releases) [![ci](https://github.com/alice-biometrics/meiga/workflows/ci/badge.svg)](https://github.com/alice-biometrics/meiga/actions) [![pypi](https://img.shields.io/pypi/dm/meiga)](https://pypi.org/project/meiga/)
=====
# meiga 🧙 [![version](https://img.shields.io/github/release/alice-biometrics/meiga/all.svg)](https://github.com/alice-biometrics/meiga/releases) [![ci](https://github.com/alice-biometrics/meiga/workflows/ci/badge.svg)](https://github.com/alice-biometrics/meiga/actions) [![pypi](https://img.shields.io/pypi/dm/meiga)](https://pypi.org/project/meiga/)

<img src="https://github.com/alice-biometrics/custom-emojis/blob/master/images/alice_header.png" width=auto>

A simple, typed and monad-based Result type for Python.

**meiga 🧙** give us a simpler and clearer way of handling errors in Python. Use it whenever a class method or a function has the possibility of failure.
## Table of Contents
- [Installation :computer:](#installation-computer)
- [Getting Started :chart_with_upwards_trend:](#getting-started-chart_with_upwards_trend)
* [Example](#example)
* [Features](#features)
- [Result](#result)
- [Functions](#functions)
- [Properties](#properties)
- [Alias](#alias)
- [Advance Usage :rocket:](#advance-usage-rocket)
* [Unwrap Result](#unwrap-result)
* [Handle Result](#handle-result)
* [Test Assertions](#test-assertions)
- [Contact :mailbox_with_mail:](#contact-mailbox_with_mail)

This package provides a new type for your Python applications, the **Result[Type, Type]**.
This Result type allows to simplify a wide range of problems, like handling potential undefined values, or reduce complexity handling exceptions. Additionally, code can be simplified following a semantic pipeline reducing the visual noise of checking data types, controlling runtime flow and side-effects.

This package is based in another solutions from another modern languages as the swift-based [Result](https://github.com/antitypical/Result) implementation.

## Installation :computer:

Expand All @@ -18,9 +28,16 @@ pip install meiga

## Getting Started :chart_with_upwards_trend:

The best way to illustrate how **meiga 🧙** can help you is with an example.
**Meiga** give us a simpler and clearer way of handling errors in Python. Use it whenever a class method or a function has the possibility of failure.

This package provides a new type for your Python applications, the **Result[Type, Type]**.
This Result type allows to simplify a wide range of problems, like handling potential undefined values, or reduce complexity handling exceptions. Additionally, code can be simplified following a semantic pipeline reducing the visual noise of checking data types, controlling runtime flow and side-effects.

This package is based in another solutions from another modern languages as the swift-based [Result](https://github.com/antitypical/Result) implementation.

### Example

The best way to illustrate how **meiga 🧙** can help you is with an example.

Consider the following example of a function that tries to extract a String (str) for a given key from a Dict.

Expand Down Expand Up @@ -50,11 +67,11 @@ def string_from_key(dictionary: dict, key: str) -> Result[str, Error]:
Result meiga type provides a robust wrapper around the functions.
Rather than throw an exception, it returns a Result that either contains the String value for the given key, or an ErrorClass detailing what went wrong.

## Features
### Features

#### Result[T, Error]
#### Result

A discriminated union that encapsulates successful outcome with a value of type T or a failure with an arbitrary Error exception.
`Result[T, Error]` 👉 A discriminated union that encapsulates successful outcome with a value of type T or a failure with an arbitrary Error exception.

#### Functions

Expand Down Expand Up @@ -131,7 +148,7 @@ True
TypeMismatch() // Error
```

### Alias
#### Alias

Use meiga aliases to improve the semantics of your code.

Expand Down Expand Up @@ -216,6 +233,8 @@ class AuthService:
return NotImplementedMethodError
```

## Advance Usage :rocket:

### Unwrap Result

If you *wrap* a Result object, its will return a valid value if it is success. Otherwise, it will return None.
Expand All @@ -231,9 +250,32 @@ value = result.unwrap()
assert value is None
```

* Check [Functions](#functions) to know more about *unwraping* methods.
* Check [tests/unit/test_result_unwrap.py](https://github.com/alice-biometrics/meiga/blob/master/tests/unit/test_result_unwrap.py) to see examples of usage.


You can use `unwrap_or_return`in combination with `@meiga` decorator. If something wrong happens unwraping your `Result`, the `unwrap_or_return` function will raise an Exception (ReturnErrorOnFailure). `@meiga` decorator allows to handle the exception in case of error and unwrap the value in case of success. The following example illustrate this:

```python
from meiga import Result, Error
from meiga.decorators import meiga

@meiga
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).handle()
# Do whatever with the name
name = first_name.lower()
return Result(success=name)
```

If key is valid success value would be returned. Otherwise, an Error would be returned.


### Handle Result

This framework also allows a method for handling Result type
This framework also allows a method for handling Result type. `handle` method returns itself and execute the `on_success` function when the instance represemts success and the `on_failure` function when it is failure.


When the operations is executed with its happy path, handle function returns the success value, as with result.value.

Expand All @@ -259,6 +301,8 @@ result = string_from_key(dictionary=user_info, key="first_name")
result.handle(on_success=success_handler, on_failure=failure_handler)
```

##### Additional parameters

If you need to add some arguments as a parameters, use **success_args** and **failure_args**:

```python
Expand All @@ -278,29 +322,46 @@ result.handle(on_success=success_handler,
failure_args=(1, 2))
```

##### Additional parameters in combination with the Result itself

On the other hand, if something wrong happens handle function will raise an Exception (ReturnErrorOnFailure).
Meiga has available a decorator to allow to handle the exception in case of error and unwrap the value in case of success.

Sometimes a handle function will need information about external parameters and also about the result itself. Now, is possible this combination thanks to `Result.__id__` identifier.

```python
from meiga import Result, Error
from meiga.decorators import meiga

@meiga
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).handle()
# Do whatever with the name
name = first_name.lower()
return Result(success=name)
parameters = (1, Result.__id__, 2)

def on_success(param_1: int, result: Result, param_2: int):
assert param_1 == 1
assert isinstance(result, Result)
assert result.value is True
assert param_2 == 2

def on_failure(param_1: int, result: Result, param_2: int):
assert param_1 == 1
assert isinstance(result, Result)
assert result.value == Error()
assert param_2 == 2

@meiga
def run():
result.handle(
on_success=on_success,
on_failure=on_failure,
success_args=parameters,
failure_args=parameters,
)

run()
```

If key is valid success value would be returned. Otherwise, an Error would be returned.


### Assertions

### Test Assertions

To help us on testing functions that returns Result, meiga provide us two functions: **assert_success** and **access_failure**.

Check the following pytest-based test for more information: [tests/unit/test_result_assertions.py](https://github.com/alice-biometrics/meiga/blob/master/tests/unit/test_result_assertions.py)

## Contact :mailbox_with_mail:

[email protected]
14 changes: 12 additions & 2 deletions meiga/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@


class Result(Generic[TS, TF]):
__id__ = "__meiga_result_identifier__"

_is_success: bool = False

def __init__(self, success: TS = NoGivenValue, failure: TF = NoGivenValue) -> None:
Expand Down Expand Up @@ -101,7 +103,11 @@ def unwrap_or_else(
if not self._is_success:
if on_failure:
if failure_args:
on_failure(*failure_args)
failure_args = list(failure_args)
if Result.__id__ in failure_args:
index_meiga_result = failure_args.index(Result.__id__)
failure_args[index_meiga_result] = self
on_failure(*tuple(failure_args))
else:
if on_failure.__code__.co_argcount == 0:
on_failure()
Expand All @@ -115,7 +121,11 @@ def unwrap_and(self, on_success: Callable, success_args=None):
if self._is_success:
if on_success:
if success_args:
on_success(*success_args)
success_args = list(success_args)
if Result.__id__ in success_args:
index_meiga_result = success_args.index(Result.__id__)
success_args[index_meiga_result] = self
on_success(*tuple(success_args))
else:
if on_success.__code__.co_argcount == 0:
on_success()
Expand Down
89 changes: 87 additions & 2 deletions tests/unit/test_result_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def on_failure():

@pytest.mark.unit
@pytest.mark.parametrize("result", [isSuccess, isFailure])
def test_should_execute_handler_with_additional_and_non_required_parameters(result):
def test_should_execute_handler_with_additional_parameters(result):
given_first_parameter = (1,)

def on_success(param_1: int):
Expand All @@ -112,7 +112,6 @@ def run():

@pytest.mark.unit
def test_should_execute_success_handler_without_any_argument():

global called_on_success
called_on_success = False

Expand All @@ -134,3 +133,89 @@ def on_failure():

assert called_on_success is True
assert called_on_failure is False


@pytest.mark.parametrize("result", [isSuccess, isFailure])
def test_should_execute_handler_with_additional_and_non_required_parameters_result_first(
result
):
given_first_parameter = (Result.__id__, 1)

def on_success(result: Result, param_1: int):
assert isinstance(result, Result)
assert result.value is True
assert param_1 == 1

def on_failure(result: Result, param_1: int):
assert isinstance(result, Result)
assert result.value == Error()
assert param_1 == 1

@meiga
def run():
result.handle(
on_success=on_success,
on_failure=on_failure,
success_args=given_first_parameter,
failure_args=given_first_parameter,
)

run()


@pytest.mark.parametrize("result", [isSuccess, isFailure])
def test_should_execute_handler_with_additional_and_non_required_parameters_result_last(
result
):
given_first_parameter = (1, Result.__id__)

def on_success(param_1: int, result: Result):
assert param_1 == 1
assert isinstance(result, Result)
assert result.value is True

def on_failure(param_1: int, result: Result):
assert param_1 == 1
assert isinstance(result, Result)
assert result.value == Error()

@meiga
def run():
result.handle(
on_success=on_success,
on_failure=on_failure,
success_args=given_first_parameter,
failure_args=given_first_parameter,
)

run()


@pytest.mark.parametrize("result", [isSuccess, isFailure])
def test_should_execute_handler_with_additional_and_non_required_parameters_result_middle(
result
):
given_first_parameter = (1, Result.__id__, 2)

def on_success(param_1: int, result: Result, param_2: int):
assert param_1 == 1
assert isinstance(result, Result)
assert result.value is True
assert param_2 == 2

def on_failure(param_1: int, result: Result, param_2: int):
assert param_1 == 1
assert isinstance(result, Result)
assert result.value == Error()
assert param_2 == 2

@meiga
def run():
result.handle(
on_success=on_success,
on_failure=on_failure,
success_args=given_first_parameter,
failure_args=given_first_parameter,
)

run()

0 comments on commit a83e062

Please sign in to comment.