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

Support method plugin hooks on unions #6560

Merged
merged 17 commits into from
Jul 4, 2019

Conversation

ilevkivskyi
Copy link
Member

@ilevkivskyi ilevkivskyi commented Mar 18, 2019

Fixes #6117
Fixes #5930

Currently both our plugin method hooks don't work with unions. This PR fixes this with three things:

  • Moves a bit of logic from visit_call_expr_inner() (which is a long method already) to check_call_expr_with_callee_type() (which is a short method).
  • Special-cases unions in check_call_expr_with_callee_type() (normal method calls) and check_method_call_by_name() (dunder/operator method calls).
  • Adds some clarifying comments and a docstring.

The week point is interaction with binder, but IMO this is the best we can have for now. I left a comment mentioning that check for overlap should be consistent in two functions.

In general, I don't like special-casing, but I spent several days thinking of other solutions, and it looks like special-casing unions in couple more places is the only reasonable way to fix unions-vs-plugins interactions.

This PR may interfere with #6558 that fixes an "opposite" problem, hopefully they will work together unmodified, so that accessing union of literals on union of typed dicts works. Whatever PR lands second, should add a test for this.

Copy link
Collaborator

@Michael0x2a Michael0x2a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think looks good to me -- I couldn't really find anything I wanted to give feedback on.

I guess I am a little uncomfortable with how many different check_call_blah methods we're accumulating, but I don't think that's really relevant to this pull request.

I think we should merge this first -- I can add the extra tests to my PR.

Copy link
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this! Generally looks good, left some ideas about making the relevant code a bit easier to understand.

"""Type check call expression.

The given callee type overrides the type of the callee
expression.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above docstring part was a bit confusing. It looks like we aren't actually overriding the callee type but to reusing a previously calculated callee type. This looks like an optimization. Maybe update the docstring to mention that? Otherwise it's not clear whether recalculating the callee type is correct.

"""Type check call expression.

The given callee type overrides the type of the callee
expression.

The 'callable_name' and 'object_type' are used to call plugin hooks.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also 'member' is used for plugin hooks only? Maybe be more explicit and say that they are only used for this purpose.

in_literal_context=self.is_literal_context())
self.msg.enable_errors()
item = self.narrow_type_from_binder(e.callee, item)
# TODO: This check is not one-to-one with non-special-cased behavior.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know what issues this may cause? Currently it's not clear if this is a serious problem or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I found a cleaner solution, I will push an update soon.

callable_name, callee_type, e.args, e.arg_kinds, e, e.arg_names, object_type)
# Unions are special-cased to allow plugins to act on each item in the union.
elif member is not None and isinstance(object_type, UnionType):
res = [] # type: List[Type]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move the elif block to a separate method? Now the largest part of this function handles a rare special case, obscuring the main flows.

@@ -2021,6 +2058,22 @@ def check_method_call_by_name(self,
"""
local_errors = local_errors or self.msg
original_type = original_type or base_type
# Unions are special-cased to allow plugins to act on each element of the union.
if isinstance(base_type, UnionType):
res = [] # type: List[Type]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, it would be better to move this block to a separate function.

@ilevkivskyi
Copy link
Member Author

@Michael0x2a @JukkaL I addressed the comments from last round, do you have any more comments here?

Copy link
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, though I didn't look at all the recent checker updates in much detail. Just a few comments about tests.

if isinstance(x.meth, int):
reveal_type(x.meth) # N: Revealed type is 'builtins.int'
else:
reveal_type(x.meth(int())) # N: Revealed type is 'builtins.int'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the plugin switches types between int/str and the existing types are also int/str, it's a bit hard to see what is going on. What about using more than two separate types?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I now have different return types for two meth methods, and correspondingly different type conversions for the two hooks: str -> int, and float -> int, this way the call would fail if a wrong hook would act, and hopefully this is now easier to read.

class Foo:
def meth(self, x: str) -> str: ...
class Bar:
def meth(self, x: int) -> str: ...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If each meth variant would have a distinct return type, the test would perhaps catch more issues.

if isinstance(x.meth, int):
reveal_type(x.meth) # N: Revealed type is 'builtins.int'
else:
reveal_type(x.meth(int())) # N: Revealed type is 'builtins.int'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to above -- maybe use more than two separate types.

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

Successfully merging this pull request may close these issues.

Several plugin hooks don't work on unions Unions of TypedDicts don't work as expected
3 participants