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

[Feature Request] PEP 362 (i.e., func.__signature__ + inspect.Signature) support #295

Open
patrick-kidger opened this issue Oct 22, 2023 · 4 comments

Comments

@patrick-kidger
Copy link
Contributor

import functools as ft
import inspect

import beartype

def f(x: int):
    pass

# ft.wraps doesn't raise:

@beartype.beartype
@ft.wraps(f)
def g(*args, **kwargs):
    pass

g("hi")  # no error!

# explicit assignment of __signature__ and __annotations__ also doesn't raise:

def h(*args, **kwargs):
    pass

h.__signature__ = inspect.signature(f)
h.__annotations__ = f.__annotations__.copy()
h("hi")  # still no error!

For what it's worth, the second is my actual use-case: beartyp'ing a dynamically-created function, with a manually-assigned signature and annotations. What's the best way to workaround this with current versions of beartype?

@leycec
Copy link
Member

leycec commented Oct 23, 2023

Ah, ha! It is everyone's favourite scuba-diving globe-trotting Google X ML specialist, Dr. Kidger!

Okay. So, you caught @beartype out red-pawed here. PEP 362 – Function Signature support is something we've been meaning (but failing) to implement for literally years. Since the road to bugginess is paved with good intentions, we did other stuff instead. This is why cats cry in the autumn. 😿

I will now quote my more idealistic younger self. Thankfully, he already figured out what we need to do here. That's good, because my more cynical older self was clueless. Here's what younger @leycec says about all this:

@beartype should be mildly refactored to obey the algorithm outlined by PEP 362:

  1. If the callable being decorated by @beartype defines the func.__signature__ attribute, @beartype should parse arguments from that signature. This should be mostly trivial. Rejoice! The inspect.Signature API is object-oriented, sane, and readable.
  2. Else, @beartype should parse arguments from the code object underlying the decorated callable. We already do this. Rejoice again!

Sadly, I have yet to finalize Python 3.12 compatibility. A firestorm of bugs erupted on the issue tracker, which @beartype 0.16.4 doused with kerosene. You'd think that would make things worse. Sometimes, you just gotta fight fire with fire.

Thankfully, I should finalize Python 3.12 compatibility in a week or two in a new @beartype 0.17.0 release. Assuming I am not lying to everyone yet again, I promise to then resolve this issue by adding PEP 362 support. And I dub thee... @beartype 0.18.0!

So much for our reasonable roadmap. Truly, this world is chaos.

@leycec leycec changed the title beartype.beartype + functools.wraps does not raise [Feature Request] PEP 362 (i.e., func.__signature__ + inspect.Signature) support Oct 23, 2023
@patrick-kidger
Copy link
Contributor Author

Haha! FWIW, I've worked around this by creating a string for the function and then eval'ing it.

@leycec
Copy link
Member

leycec commented Oct 24, 2023

I've worked around this by creating a string for the function and then eval'ing it.

So. eval. It comes to this. This is what @beartype has forced you to do. I... I'm so sorry.

Emoji cat: please weep for us as we implement clever but saddening workarounds. 😿

</ahem>. What I meant to say was...

Thanks Soooooo Much for...

...recommending @beartype for the Google Open Source Peer Bonus!!!! I'm so eternally grateful. Seriously. An Aspy cannot lie. (It's true, because you read it online.)

That is one of the nicest things anyone's ever done for me. It truly means alot. Social recognition isn't something I'm accustomed to. But it feels surprisingly great! Even if @beartype never wins anything beyond the sweet dopamine rush of crushing everyone's bugs, I can't thank you enough for publicly believing in @beartype – the undomesticated QA varmint.

Therefore, I have repaid you tonight by...

Doing Something That Doesn't Help You at All

Yes, it's true! Commit 973a00e now partially resolves this issue. @beartype now supports your first use case that you don't actually care about at all:

from beartype import beartype
from functools import wraps
import inspect

def muh_func(muh_arg: int):  # <-- wat!? no @beartype? how can this be?
    pass

@beartype  # <-- oh, okay. here's the @beartype. phew. that was close
@wraps(muh_func)  # <-- standard decorator idiom
def muh_wrapper(*args, **kwargs):  # <-- isomorphic non-closure wrapper
    return muh_func(*args, **kwargs)

muh_wrapper('This is an integer, I swear it!')  # <-- now raises an exception: BOOM!

Better than nuthin', @beartype. Better than nuthin'. 💪 🐻

Interestingly, @beartype has supported the standard idiom for defining decorators for a few years: e.g.,

from beartype import beartype
from collections.abc import Callable
from functools import wraps
import inspect

@beartype
def muh_decorator(func: Callable) -> Callable:
    @beartype  # <-- oh, okay. here's the @beartype. phew. that was close
    @wraps(func)  # <-- standard decorator idiom
    def muh_wrapper(*args, **kwargs):  # <-- isomorphic closure wrapper
        return func(*args, **kwargs)

    return muh_wrapper

@muh_decorator  # <-- wat!? no @beartype? how can this be?
def muh_func(muh_arg: int):
    pass

muh_func('This is an integer, I swear it!')  # <-- still raises an exception: BOOM!

I'd assumed that most user-defined decorators would look like that. Clearly, I was wrong. Never thought anyone would actually implement a decorator wrapper outside of a closure context... but they did.

@patrick-kidger: If he did what you expected, this world would be a really boring place.

leycec added a commit that referenced this issue Oct 24, 2023
This commit generalizes the `@beartype` decorator to support *all*
**isomorphic wrappers** (i.e., higher-level callables decorated by the
standard `@functools.wraps` decorator for wrapping lower-level callables
with additional functionality defined by even higher-level decorators
such that those wrappers isomorphically preserve both the number and
types of all passed parameters and returns by accepting only a variadic
positional argument and a variadic keyword argument), partially
resolving issue #295 kindly submitted by @patrick-kidger (Patrick
Kidger) – the Best Google X Researcher of All Time, Clearly. Previously,
@beartype only supported **isomorphic closure wrappers** (i.e.,
isomorphic wrappers defined as closures rather than non-closure
callables). Now, @beartype supports both isomorphic closure wrappers
*and* **isomorphic non-closure wrappers** (i.e., isomorphic wrappers
defined as non-closure callables rather than closures). Notably, this is
now fine:

```python
from beartype import beartype
from functools import wraps
import inspect

def muh_func(muh_arg: int):  # <-- wat!? no @beartype? how can this be?
    pass

@beartype  # <-- oh, okay. here's the @beartype. phew. that was close
@wraps(f)  # <-- standard decorator idiom
def muh_wrapper(*args, **kwargs):  # <-- isomorphic non-closure wrapper
    pass
```

(*Idiomatically immaterial idiocy in a cyclic automata, mate!*)
@patrick-kidger
Copy link
Contributor Author

...recommending https://github.com/beartype for the Google Open Source Peer Bonus!!!!

Haha, you're very welcome, I did it because I really appreciate all of the effort you've put into beartype!

Doing Something That Doesn't Help You at All

:D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants