Skip to content

Support method plugin hooks on unions - #6560

Merged
ilevkivskyi merged 17 commits into
python:masterfrom
ilevkivskyi:union-plugin-other
Jul 4, 2019
Merged

ilevkivskyi merged 17 commits into
python:masterfrom
ilevkivskyi:union-plugin-other

Conversation

@ilevkivskyi

@ilevkivskyi ilevkivskyi commented Mar 18, 2019 •

Copy link
Copy Markdown
Member

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.

@Michael0x2a Michael0x2a left a comment

Copy link
Copy Markdown
Collaborator

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.

@JukkaL JukkaL left a comment

Copy link
Copy Markdown
Collaborator

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.

Comment thread mypy/checkexpr.py

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

Copy link
Copy Markdown
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.

Comment thread mypy/checkexpr.py Outdated
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
Copy Markdown
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.

Comment thread mypy/checkexpr.py Outdated
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
Copy Markdown
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
Copy Markdown
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.

Comment thread mypy/checkexpr.py Outdated
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
Copy Markdown
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.

Comment thread mypy/checkexpr.py Outdated
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
Copy Markdown
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
Copy Markdown
Member Author

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

@JukkaL JukkaL left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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.

Comment thread test-data/unit/check-custom-plugin.test Outdated
class Foo:
def meth(self, x: str) -> str: ...
class Bar:
def meth(self, x: int) -> str: ...

Copy link
Copy Markdown
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
Copy Markdown
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