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

Add decorator to make plain functions return a Result #33

Closed
noah04 opened this issue Sep 1, 2020 · 13 comments · Fixed by #71
Closed

Add decorator to make plain functions return a Result #33

noah04 opened this issue Sep 1, 2020 · 13 comments · Fixed by #71
Assignees

Comments

@noah04
Copy link

noah04 commented Sep 1, 2020

Idea

While using the library, I came up with the idea to implement a simple decorator that wraps any plain function into try-except and returns an Err if an exception occurred. This makes it very easy to wrap existing "unsafe" functions and thus prevent runtime errors.

Example:

@safe_wrap
def i_will_fail():
  raise Exception

@safe_wrap
def i_will_succeed():
  return 42

output = i_will_fail()
assert(output.is_err())

output = i_will_succeed()
assert(output.is_ok())

Existing implementation

Just to give you an idea, here is my naive first implementation (shortened):

@singledispatch
def safe_wrap(f: Callable[[Any], Any], O]) -> Callable[[Any, Any], Result[O, Exception]]:
    @wraps(f)
    def wrapper(*args: Any, **kwargs: Any) -> Result[O, Exception]:
        try:
            return Ok(f(args, kwargs))
        except Exception as err:
            return Err(err)
    return wrapper

# and now we register calls with no args and args only and kwargs only
@safe_wrap.register
def _(f: Callable[[Optional[Any]], O]) -> Callable[[Any, Any], Result[O, Exception]]:
[...]

Downsides

Typing arbitrary *args and **kwargs is not trivial, as described in python/mypy#5876. In my current implementation, I work around this with using Any and a generic dispatch. Nevertheless, this may not comply with the excessive typing that the rest of result-module aims for.

Question(s)

What do you think about the concept and do you want something like this in the library? If so, I could provide a PR with a more elaborated version of the code shown above.

@francium
Copy link
Member

francium commented Sep 5, 2020

I was looking at some Lua stuff (literally first time diving into some Lua stuff, so no prior experience) and found something quite relevant, https://riptutorial.com/lua/example/16000/using-pcall

In that case, it's not a decorator, not sure if Lua has decorators. But the idea is somewhat similar, wrap a exception throwing function in a protected 'call-scope' and read out the sucess/failure + exception/value.

@francium
Copy link
Member

francium commented Sep 5, 2020

@dbrgn I think it's a interesting idea. Not sure how you feel about the scope of this library and if this falls within it.

@dbrgn
Copy link
Member

dbrgn commented Sep 5, 2020

Yeah, it does sound interesting 🙂 I don't currently have the capacity to review and maintain this feature though, so @francium if you're interested just go ahead!

@noah04
Copy link
Author

noah04 commented Sep 5, 2020

@francium I also have no experience with Lua but had a quick look. In my opinion, pcall is exactly what I tried to mimic but without the decorator.

This would still leave the problem with the typing of args and kwargs (which I still found no good solution for). But it would make the usage non-api-breaking. In fact, you could use the pcall wherever you need, without touching old code.

But I don't see the advantage of importing a function where you probably have to read documentation for, over a two-line try-except.

@francium
Copy link
Member

francium commented Sep 6, 2020

I was having a quick play around with your sample code. You're correct about the generic args/kwargs typing, ran into same type checking issues without any obvious solution, except for reducing the typing here (1). But even then, I noticed some typing issues in the call site of the wrapped function (2)

(1)

O = TypeVar("O")

@singledispatch
def safe_wrap(f: Any) -> Callable[[Any], Result[O, Exception]]:
    @wraps(f)
    def wrapper(*args: Any, **kwargs: Any) -> Result[O, Exception]:
        try:
            return Ok(f(*args, **kwargs))
        except Exception as err:
            return Err(err)

    return wrapper

You also had some issues with how you passed through the args and kwargs to the wrapped f function, you missed the * and ** unpack operators in the f(...) call. I don't think you meant to omit those, just an oversight?

(2)

@safe_wrap
def throwing() -> None:
    raise TypeError()

x = throwing() # type: Result

But x = throwing() still complain about not have enough args passed into it. I'm assuming this is because of the same args/kwargs issue and no easy way of expressing any number of arguments

Anyway, it definitely works barring the type checking

Pros:

  • Can annotate existing legacy code with safe_wrap and it become completely runtime safe

Cons:

  • Type checking is a bit messy

But is there is a huge demand for this helper function? As you mentioned this is same as a few additional lines of try/except which should avoid all the type checking limitations.

I do this it's a cool approach to making things runtime safe, but I'm hesitant to push forward with this at this time. Perhaps in the future

  1. We'll have people expressing interest in this and show that it'll a really helpful thing to have in real world codebases
  2. Have better type checking support to avoid some of these limitations

I would be inclined to mark this as wontfix for the time being.

Let me know if you disagree otherwise I'll close this issue after waiting a bit for you to reply.


EDIT

# and now we register calls with no args and args only and kwargs only
@safe_wrap.register
def _(f: Callable[[Optional[Any]], O]) -> Callable[[Any, Any], Result[O, Exception]]:
[...]

Notice I missed this bit and that would've probably helped resolve (2).

@dbrgn
Copy link
Member

dbrgn commented Sep 6, 2020

We'll have people expressing interest in this and show that it'll a really helpful thing to have in real world codebases

Yeah, let's keep this open. If a few people express interest, we can still consider shipping an implementation.

@noah04
Copy link
Author

noah04 commented Sep 6, 2020

@francium great analysis, thanks for your time! 👍 And the edit you added is correct: This would solve the problem. I was stuck at the same point as you and added the generic dispatch to "solve" it.

I also had a deeper look and notices another "Con": The lack of typing for args and kwargs also causes decorated functions not to be linted correctly. Since we use Any, a caller gets no warning when passing an invalid type.

So all in all, this might be a cool feature but I absolutely agree with your concerns. Maybe generic argument typing will be added to Python in the future.

@wbolster
Copy link
Member

wbolster commented Oct 25, 2021

pep 612's ParamSpec could help here: https://www.python.org/dev/peps/pep-0612/

mypy support for this is tracked here: python/mypy#8645

@wbolster wbolster changed the title [Feature Request] Decorator to make plain functions return a Result Add decorator to make plain functions return a Result Oct 29, 2021
wbolster added a commit to wbolster/result that referenced this issue Oct 31, 2021
Add as_result() helper to make a decorator to turn a function into one
that returns a a ``Result``.

For type checking, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/)

This is currently not supported by Mypy; see
python/mypy#8645

Fixes rustedpy#33.
@wbolster
Copy link
Member

i've built a fully type-safe implementation using typing.ParamSpec to preserve type signatures. that means it depends on Python3.10+ and currently lacks Mypy support.

see #71 for details.

wbolster added a commit to wbolster/result that referenced this issue Oct 31, 2021
Add as_result() helper to make a decorator to turn a function into one
that returns a a ``Result``.

For type checking, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/)

This is currently not supported by Mypy; see
python/mypy#8645

Fixes rustedpy#33.
@wbolster
Copy link
Member

wbolster commented Nov 1, 2021

@noah04 out of curiosity, do you remember why you used functools.singledispatch() in your example code?

wbolster added a commit to wbolster/result that referenced this issue Nov 1, 2021
Add as_result() helper to make a decorator to turn a function into one
that returns a a ``Result``.

For type checking, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/)

This is currently not supported by Mypy; see
python/mypy#8645

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Nov 1, 2021
Add as_result() helper to make a decorator to turn a function into one
that returns a a ``Result``.

For type checking, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/)

This is currently not supported by Mypy; see
python/mypy#8645

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Nov 1, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Nov 1, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Nov 1, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
@noah04
Copy link
Author

noah04 commented Nov 1, 2021

@wbolster if I remember correctly, I did that to enable generic calls with args and/or kwargs using the same decorator. But don‘t rely on that piece of code - I‘m not that into Python generics/typing.

Mentioning that, I would really like you for investing time on this topic 👍🏼

@wbolster
Copy link
Member

wbolster commented Nov 2, 2021

i suggest you look at #71 🙃 which seems promising

wbolster added a commit to wbolster/result that referenced this issue Dec 13, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 23, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 23, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 23, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 23, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 23, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 23, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 23, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 23, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 24, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit to wbolster/result that referenced this issue Dec 24, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

This is currently not fully supported by Mypy; see
python/mypy#8645

Calling decorated functions works without errors from Mypy, but will
not be type-safe, i.e. it will behave as if it is calling a function
like f(*args: Any, **kwargs: Any)

Fixes rustedpy#33.
wbolster added a commit that referenced this issue Dec 24, 2021
Add a as_result() helper to make a decorator to turn a function into one
that returns a Result: Regular return values are turned into
Ok(return_value). Raised exceptions of the specified exception type(s)
are turned into Err(exc).

The decorator is signature-preserving, except for wrapping the return
type into a Result, of course.

For type annotations, this depends on typing.ParamSpec which requires
Python 3.10+ (or use typing_extensions); see
PEP612 (https://www.python.org/dev/peps/pep-0612/).

Fixes #33.
@wbolster
Copy link
Member

good news everybody, as_result() is now merged #71

see the readme for example usage!

@wbolster wbolster self-assigned this Dec 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants