-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Conditional type-check based on callable call #2627
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
else: | ||
return None, {} | ||
else: | ||
return {}, {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This could be factored with less indentation by reversing the order of conditional checks. Also, avoid backslashes. I know you based off conditional_type_map()
but given it's already copied and pasted, how about:
if not current_type:
return {}, {}
if isinstance(current_type, CallableType):
return {}, None
if isinstance(current_type, UnionType):
callables = [item for item in current_type.items
if isinstance(item, CallableType)] # type: List[Type]
non_callables = [item for item in current_type.items
if not isinstance(item, CallableType)] # type: List[Type]
return (
{expr: UnionType.make_union(callables)},
{expr: UnionType.make_union(non_callables)},
)
return None, {}
The two list comprehensions are a bit wasteful but I guess that's fine, we don't expect unions to be overly long.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reformatted. I also changed it to a for loop. I initially thought the comprehensions would be faster, but basic experiments show I'm wrong.
@@ -0,0 +1,24 @@ | |||
from typing import builtinclass, Tuple, TypeVar, Generic, Union |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@builtinclass is no longer a thing, see #599. Just remove this import, you're not using it anyway.
👍 |
I added more tests for chaining ands/ors with |
I have some doubts about this. Actually the issue is not with your code but the original feature request, but the issue only became clear when I read your implementation. In any case it seems better to comment here for now. The issue is that there's actually almost no situation where we can correctly conclude that a type is not callable. This even applies to the original program which involved There's actually a similar issue with normal |
That's completely true. class T(int):
def __call__(self) -> int:
return self
def f(i: int) -> int:
if callable(i):
print("Impossible!")
return i
f(T(5)) What's considered impossible while type-checking is very clearly possible at run-time. |
Well there are some bits that we can carve out here. For instance if we have a I feel like these examples involving subclassing |
I would defer to @JukkaL about this. That said, my own intuition is that the PR is valuable as-is (though I haven't reviewed it). While it's possible to subvert the types by e.g. subclassing |
But at a minimum I would expect mypy to treat |
Oh does the PR not implement that part? (Though `Any` should be treated as
"perhaps callable perhaps not" I suppose.)
|
Er yes, I changed what I was going to say by the time I got to the end of that sentence. :) I only read the code, I didn't read the tests or test it myself; but currently it appears to treat anything that isn't a Callable or a Union as not callable. |
Oh, that's definitely too narrow. Probably should look for some other piece
of code that already checks for callable and see what cases it
distinguishes. (Maybe the checking of a CallExpr?)
|
Yes, the PR as-is only supports Callable and Unions of Callables. I'll take a look at how CallExpr handles things and improve this. Edit: It's been improved and should now cover all cases. |
Status update: I discussed this with @JukkaL and he's in favor (though hasn't reviewed the PR in detail). I also ran tests on our internal codebases and found no problems. If I can wrap my head around the actual code I'll try to merge this ASAP so it will make it into the 0.4.7 release (planned for this Thursday). |
return all(is_callable_type(item) for item in type.items) | ||
|
||
if isinstance(type, TypeVarType): | ||
return not is_callable_type(type.erase_to_union_or_bound()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rereading through this, I have no idea why this is negated. I'll remove it and test to confirm this case is right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You seem to have no tests involving TypeVar. Also none for Type. So please add some!
return all(is_callable_type(item) for item in type.items) | ||
|
||
if isinstance(type, TypeVarType): | ||
return not is_callable_type(type.erase_to_union_or_bound()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You seem to have no tests involving TypeVar. Also none for Type. So please add some!
def conditional_callable_type_map(expr: Expression, | ||
current_type: Optional[Type], | ||
) -> Tuple[TypeMap, TypeMap]: | ||
"""Takes in an expression and the current type of the expression |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add period? Perhaps complete the sentence?
"""Takes in an expression and the current type of the expression | ||
|
||
Returns a 2-tuple: The first element is a map from the expression to | ||
the restricted type if it must be callable. The second element is a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"must be" feels odd. Perhaps make the phrasing more similar to the docstring of conditional_type_map()?
uncallable_map = {expr: UnionType.make_union(uncallables)} if len(uncallables) else None | ||
return callable_map, uncallable_map | ||
|
||
if is_callable_type(current_type): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also missing a case for TypeVar
. E.g. this example doesn't work right even if I remove the not
from line 2520 above:
T = TypeVar('T', bound=Union[int, Callable[[], int]])
def f(a: T) -> int:
if callable(a):
reveal_type(a)
return a()
else:
reveal_type(a)
return a
The output is
__tmp__.py:8: error: Revealed type is 'T`-1'
__tmp__.py:9: error: Incompatible return value type (got "T", expected "int")
uncallables.append(item) | ||
|
||
else: | ||
if is_callable_type(item): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a problem here: nested unions (e.g. Union[int, Union[str, float]]
) aren't necessarily flattened, so if the inner union contains a callable and a non-callable, that item is binned with the uncallables -- which is wrong. Try e.g.
U = Union[int, Union[str, Callable[[], int]]]
def f(a: U) -> int:
if callable(a):
reveal_type(a)
return a()
else:
reveal_type(a)
return a
vs.
U = Union[int, str, Callable[[], int]]
@gvanrossum I rewrote the underlying logic to handle the cases you brought up (and hopefully all other cases) and added various tests. |
W00t! Thanks! Merging now... |
Fixes #1973
This is my first PR to mypy, so let me know if this is out of scope, poor style, wrong, if it needs more tests, etc, etc. I'm happy to learn.
The naïve thing is to make use of
conditional_type_map
but the way that works, it basically casts theexpr
to theproposed_type
, so passing in a generic CallableType instance (ie AnyType *args, **kwargs, and return) results in a type map whereexpr
is just a generic CallableType. This new function instead preserves the signature ofexpr
and will make Unions as necessary (seetestUnionMultipleReturnTypes
, which would not raise an error with a generic CallableType).