-
-
Notifications
You must be signed in to change notification settings - Fork 437
Add a possible type hint to the @requires decorator. #1504
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 a possible type hint to the @requires decorator. #1504
Conversation
Codecov Report
@@ Coverage Diff @@
## main #1504 +/- ##
==========================================
- Coverage 90.00% 89.99% -0.02%
==========================================
Files 107 107
Lines 11528 11532 +4
==========================================
+ Hits 10376 10378 +2
- Misses 1152 1154 +2
Continue to review full report at Codecov.
|
Thanks for this! I followed the rabbithole of that issue for a while, and it seemed like there was particular interest (from Would it help to change @requires('posterior', 'prior')
def my_function(self):
... to a tower of @requires('posterior')
@requires('prior')
def my_function(self):
... |
We currently have both uses of multiple arguments (acts as a logical and IIRC) and uses of cascading (acts as a logical or IIRC, could be the other way around), however, the multiple argument cases could be modified to using a list of strings as a single argument. I think that option 2 is too restrictive though, there must be at least 1 positional argument in addition to self, otherwise the function can't know which attributes to check for. |
Ah sorry for the confusion, this doesn't need to change one way or the other. Having more than one parameter would be a problem on the decorated function, not in the decorator invocation. Here's an example of something that would not be supported: @requires('posterior')
def my_function(self, other_arg):
... This is because the decorator would be expecting a function that looks like The
@OriolAbril I'm assuming this was referring to the arguments to the Would you also find it overly restrictive to prevent @requires('posterior')
def my_function(self, other_arg):
... To be explicit, the issue here would be that |
Oh, yeah, I though the restriction was on what is passed to |
There is one function that takes multiple arguments and is currently using Line 211 in 25ce6c4
Otherwise, this refactor should be totally doable. This is what the essential portion of it would look like: def __call__(
self, func: Callable[[RequiresArgTypeT], RequiresReturnTypeT]
) -> Callable[[RequiresArgTypeT], Optional[RequiresReturnTypeT]]: # noqa: D202
"""Wrap the decorated function."""
def wrapped(arg: RequiresArgTypeT) -> Optional[RequiresReturnTypeT]:
"""Return None if not all props are available."""
for prop in self.props:
prop = [prop] if isinstance(prop, str) else prop
if all([getattr(arg, prop_i) is None for prop_i in prop]):
return None
return func(arg)
return wrapped I'm happy to do whatever you think is best here. We could easily inline the |
I just realized there's an alternative that, while a bit clunky, covers both the 1-argument and the 2-argument cases: explicit @overload # decorating 1-argument function
def __call__(
self, func: Callable[[RequiresArg1TypeT], RequiresReturnTypeT]
) -> Callable[[RequiresArg1TypeT], Optional[RequiresReturnTypeT]]:
...
@overload # decorating 2-argument function
def __call__(
self, func: Callable[[RequiresArg1TypeT, RequiresArg2TypeT], RequiresReturnTypeT]
) -> Callable[[RequiresArg1TypeT, RequiresArg2TypeT], Optional[RequiresReturnTypeT]]:
...
def __call__(self, func):
# actual implementation goes here
pass For more info, check out I just checked this locally and it seems to work as far as I can tell. It's a bit repetitive, and doesn't solve for cases that use 0 arguments or more than 2 arguments, but it does work for all current uses of the |
If this is the only function with arguments that uses requires I would simply remove the requires from this function and be done with it. It is called only from |
My thinking with the We might also not want people to have to contort their code in the future if they need If you still think that only handling only the 1-argument case is best, I'm happy to do that. Just wanted to put my 2 cents in :) |
I am leaning towards removing the requires in |
I've gotten side-tracked by a couple of other things so I haven't had a chance to update this PR; I hope to do so in the coming couple of weeks. I wanted to share a recent update on the topic of decorator type-hinting that might be of interest. Python 3.10 will ship with |
Sounds good! Thanks! |
847d68d
to
1005d2d
Compare
Description
This PR type-hints the
@requires
decorator, which seems to be used in over 100 places in the repository at the moment. Since decorators are a bit tricky to type-hint correctly, and still have some unsolved edge cases with regard to type-hinting (python/mypy#3157 comes to mind), I wanted to open this PR as an example of the available options and to provide a bit more context for #1498 where we were discussing whether to use@requires
or not. Also tangentially related to #1496.The gist of the still-open
mypy
issue is that there's no great way to type-hint decorators that work on functions with arbitrary type signatures but change the input function's type signature in some way. The@requires
decorator falls in this category because it works over functions with arbitrary inputs and outputs, and simply wraps the function's return type in anOptional
.This limitation leaves us with two options.
@requires
to work on arbitrary functions, but make the resulting decorated function's input argument signature appear to bef(*args, **kwargs)
.This is an excerpt of what that looks like:
The
Callable[..., Foo]
syntax means "this function allows any positional and keyword arguments". The equivalent way to define a function with such a signature in Python would be this:Pro:
@requires
can be used with arbitrary functionsCon: Functions decorated with
@requires
thus lose their type signatures, andmypy
is no longer able to statically check their call sites since they appear callable with any input arguments.@requires
functionality): only allow@requires
to be used on functions with one positional argument (i.e. ones that look likedef foo(self) -> Foo
), then preserve the function signature through the decoration process.I didn't want to implement this option without asking for approval first, since it does limit what
@requires
can do and I'm not sure if it is always used with only a single argument. However, if this option seems reasonable to all the maintainers, it is definitely preferable from amypy
perspective since it preserves the ability formypy
to type-check call sites of functions decorated with@requires
.This is what that would look like, as an excerpt:
The pro/con here are essentially the reverse of Option 1's ones above. You can see that the decorated function has a specific type signature including the function's argument types, and the generics (the
TypeVar
values) allow us to express the "same type as in the input function" type relationships between the function being decorated and the final produced function. This allowsmypy
to perform type-checking of calls to the decorated function, at the cost of only allowing a single argument to the function.In principle, similar variants of this decorator could be written for 2-argument or arbitrary k-argument versions of the same functionality. I admit that's not the most elegant option, but until the relevant portion of python/mypy#3157 is resolved, this is the best we can do.
I'd be happy to implement either of these options, based on whatever the maintainers decide is the best way forward for this project.
Checklist