-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Rework how we refine information from callable() #4343
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
test-data/unit/check-callable.test
Outdated
|
||
if not callable(b): | ||
5 + 'test' | ||
|
||
if callable(c): | ||
reveal_type(c) # E: Revealed type is '__main__.B' | ||
reveal_type(c) # E: Revealed type is 'Union[<callable subtype of A>, __main__.B]' |
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.
Just a random comment: we need to be sure that this type <callable subtype of A>
will never appear in error messages (apart from actually reveal_type
), since this can be confusing. I think it can be formatted just as A
in other cases
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.
Does there exist mechanism for having different internal and external names for instances?
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.
Yes, see messages.py
. There is a method MessageBuilder.format
that controls most of the formatting. But, see other comments for this code that propose more radical solutions.
I remember I also wanted this few times. |
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 a reasonable first try at this! I didn't do a detailed review yet but left some comments.
test-data/unit/check-callable.test
Outdated
|
||
if not callable(b): | ||
5 + 'test' | ||
|
||
if callable(c): | ||
reveal_type(c) # E: Revealed type is '__main__.B' | ||
reveal_type(c) # E: Revealed type is 'Union[<callable subtype of A>, __main__.B]' |
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.
Hmm this might be pretty surprising for users. I wonder if we should special case unions and only consider items with a known __call__
here, so that the inferred type would continue to be just __main__.B
here. Yes, it would be ugly, but I'd say that pragmatism beats purity here, since the whole callable
check is a legacy thing.
Random though: One way of thinking about this would be to consider this as similar to hasattr(c, '__call__')
. We could perhaps eventually extend the support for callable
to handle hasattr
, and for hasattr
I feel that__main__.B
would be the correct type to infer here.
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.
I think nearly all users will be surprised by this. And I think in actual code it is very rare to find a situation where c (defined as Union[A, B]) was an instance of a callable subclass of A. We may see the occasional non-callable class that has a callable subclass, but it's unlikely to figure in type discriminations like this. The realistic scenarios probably involve either a union of callable and non-callable classes, where there is no concern that one of non-callable classes has a callable subclass; or a non-callable class that has a callable subclass (but then the discrimination is just between the base class and the callable subclass).
test-data/unit/check-callable.test
Outdated
if callable(o): | ||
o() | ||
1 + 'boom' # E: Unsupported operand types for + ("int" and "str") | ||
o() |
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.
Maybe test calling with some randon arguments? Also consider doing reveal_type(o)
.
test-data/unit/check-callable.test
Outdated
def g(o: Foo) -> None: | ||
o.bar() | ||
if callable(o): | ||
o.bar() |
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.
Check that o.foo()
is rejected here.
test-data/unit/check-callable.test
Outdated
o.bar() | ||
if callable(o): | ||
o.bar() | ||
o() |
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.
Again, also test call with arguments.
mypy/checker.py
Outdated
return Instance(info, []) | ||
|
||
|
||
def make_fake_callable(type: Instance, typechecker: TypeChecker) -> 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.
Passing TypeChecker
instances around is against the prevailing style in the codebase. Instead, it looks like you could just pass a callable corresponding to typechecker.named_type
. However, other changes may introduce additional dependencies to TypeChecker
and it may be better to turn this and other functions into type checker methods.
test-data/unit/check-callable.test
Outdated
@@ -227,7 +227,7 @@ T = Union[Union[int, Callable[[], int]], Union[str, Callable[[], str]]] | |||
|
|||
def f(t: T) -> None: | |||
if callable(t): | |||
reveal_type(t()) # E: Revealed type is 'Union[builtins.int, builtins.str]' | |||
reveal_type(t()) # E: Revealed type is 'Union[Any, builtins.int, builtins.str]' |
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.
Similar to above, if we special cased unions this would remain as previously, which arguably matches the likely intent of the check.
test-data/unit/check-callable.test
Outdated
@@ -253,7 +253,7 @@ T = TypeVar('T', int, Callable[[], int], Union[str, Callable[[], str]]) | |||
|
|||
def f(t: T) -> None: | |||
if callable(t): | |||
reveal_type(t()) # E: Revealed type is 'builtins.int' # E: Revealed type is 'builtins.str' | |||
reveal_type(t()) # E: Revealed type is 'Any' # E: Revealed type is 'builtins.int' # E: Revealed type is 'Union[Any, builtins.str]' |
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 change seems okay, but can you split the long line using \
?
mypy/checker.py
Outdated
@@ -2883,7 +2883,45 @@ def conditional_type_map(expr: Expression, | |||
return {}, {} | |||
|
|||
|
|||
def partition_by_callable(type: Type) -> Tuple[List[Type], List[Type]]: | |||
def intersect_instance_callable(type: Instance, callable_type: CallableType) -> 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.
Style nit: we generally prefer typ
instead of type
since type
is a builtin :-( This doesn't apply to attribute access, since it's clear that foo.type
doesn't refer to the builtin.
mypy/checker.py
Outdated
|
||
# Build the fake ClassDef and TypeInfo together. | ||
# The ClassDef is full of lies and doesn't actually contain a body. | ||
cdef = ClassDef("<callable subtype of {}>".format(type.type.name()), Block([])) |
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 the trickiest thing in this PR. However, there generally seems to be no better way.
There are several things that should be handled here, such as:
- The original class may be generic.
- The original class may inherit from
Tuple[...]
and in that case we should returnTupleType
, notInstance
. - If the original class inherits from
Any
, the new class should also do that.
Basically you should check that every TypeInfo
attribute is initialized correctly.
You may be able to share code with basic_new_typeinfo
in mypy.semanal
, but you shouldn't add a dependency from type checker to the semantic analyzer. You could move basic_new_typeinfo
to some third module, perhaps.
Additionally, it's possible to leak these types through type inference to class and module top level scopes. This means that the types can be serialized in incremental mode, and you should test that it works. It may be necessary to store the created class in a symbol table (with a generated unique dummy name) for serialization to work, but I'm not sure about this.
test-data/unit/check-callable.test
Outdated
@@ -208,13 +208,13 @@ b = B() # type: B | |||
c = A() # type: Union[A, B] | |||
|
|||
if callable(a): | |||
5 + 'test' | |||
5 + 'test' # E: Unsupported operand types for + ("int" and "str") |
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 of nits: our coding standard has two spaces before #
.
No, but you can special-case the way types are formatted with various
methods/functions in messages.py.
|
On second thought, creating a new subclass might be bit of an overkill considering that this is not a very common use case. Here's an idea about a simpler "good enough" fix:
|
Oops, I wish I had noticed your last comment this morning. Pushed changes to address most of the comments. I still need to move the functions into the TypeChecker class, but I've put that off to keep the diffs easy to read. |
Another way to structure this would be to return just 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.
Thanks for the updates! This is looking pretty good (once you refactor the TypeChecker
arguments away).
The following TypeInfo
attributes might still need processing, though some of these are kind of unlikely to have an effect so just adding a TODO comment may suffice:
declared_metaclass
metaclass_type
is_abstract
(say, if one doestype(x)()
)is_protocol
(not sure about this)runtime_protocol
(if one doesisinstance(y, type(x))
which would be pretty perverse)protocol_members
(not sure)is_enum
(if it's an enum)_promote
(you can test by doingif callable(x)
whenx: int
and checking that the narrowed type is compatible withfloat
)tuple_type
(discussed previously)
mypy/checker.py
Outdated
|
||
def intersect_instance_callable(type: Instance, callable_type: CallableType, | ||
typechecker: TypeChecker) -> Type: | ||
"""Creates a fake type that represents the intersection of an |
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.
Style nit: Format multi-line docstrings so that there is an empty lines after the first one and the final """"
is on a line by its own. Example:
def foo(x):
"""Do stuff (a 1-line summary).
More elaborate description of stuff. This
can span multiple lines.
"""
test-data/unit/check-callable.test
Outdated
o.bar() | ||
|
||
[builtins fixtures/callable.pyi] | ||
[case testCallableObjectAny] |
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.
Style nit: Add empty line before each test case.
I think the |
There are certain checks for protocols. For example, |
Right, but the concern here was |
876087a
to
f04740d
Compare
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! Just one minor nit. Feel to merge once you've fixed the typo and tested this against internal Dropbox codebases.
mypy/checker.py
Outdated
# Build the fake ClassDef and TypeInfo together. | ||
# The ClassDef is full of lies and doesn't actually contain a body. | ||
# Use format_bare to generate a nice name for error messages. | ||
# We skip filling fully filling out a handful of TypeInfo fields because they |
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.
Typo: 'filling fully filling out'.
Don't ever assume that something is *not* callable; basically anything could be, and getting it wrong leaves code to not be typechecked. When operating on an Instance that is not clearly callable, construct a callable version of it by generating a fake subclass with a __call__ method to be used. Fixes #3605
f04740d
to
dd881a1
Compare
🥇 |
Don't ever assume that something is not callable; basically anything
could be, and getting it wrong leaves code to not be typechecked.
When operating on an Instance that is not clearly callable, construct
a callable version of it by generating a fake subclass with a
__call__
method to be used.
Fixes #3605
This is being posted to solicit feedback, I expect it might need some large changes.
The hack perpetrated here is somewhat grevious, and I suspect I missed something, so I am interested to find out what :).
Also, to reduce the change size during development I threaded a
TypeChecker
through a bunch of functions; the right thing might be to move all of those functions to beTypeChecker
methods.