-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
Implementing background infrastructure for recursive types: Part 2 #7366
Conversation
Conflicts: mypy/checkexpr.py
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 didn't check every change exhaustively, but I spot-checked a bunch and skimmed the rest. Overall, I think this all looks plausible. I did have some suggestions, but I think they're all low-priority ones and could probably be handled in a separate diff if you want (or not at all).
misc/proper_plugin.py
Outdated
'semanal.py', | ||
'typeanal.py' | ||
] | ||
FILE_WHITELIST = [] |
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 just delete this variable completely?
'mypy.types.TypeList', | ||
'mypy.types.CallableArgument', | ||
'mypy.types.PartialType', | ||
'mypy.types.ErasedType'): |
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.
Do we also want to include RawExpressionType and PlaceholderType to this list? I'm guessing "yes" for the former and "no" for the latter? (genuinely not sure).
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 agree.
@@ -999,6 +1012,8 @@ def is_reverse_op_method(self, method_name: str) -> bool: | |||
def check_for_missing_annotations(self, fdef: FuncItem) -> None: | |||
# Check for functions with unspecified/not fully specified types. | |||
def is_unannotated_any(t: Type) -> bool: | |||
if not isinstance(t, ProperType): | |||
return False |
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.
Should we maybe be trying to expand the type here via get_proper_type
instead of immediately returning False?
Sort of curious why this change is a bit different from what you're adding in other places in this file.
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.
Because we know that an alias can't target unannotated Any
.
# type (or any) | ||
if not isinstance(bound_type.ret_type, (AnyType, Instance, TupleType)): | ||
# type (or any). | ||
if not isinstance(get_proper_type(bound_type.ret_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.
Is it ok to be discarding the proper type like this? I suppose it doesn't matter too much since we're only using the proper type in an error message, but it might be nice to be consistent.
There were several other places below where we did the same thing. I spot-checked a bunch of them and they all largely looked fine, but it could maybe be worth double-checking. Perhaps this can come later as we convert our various helper functions to work with ProperType instead of Type? (Assuming we actually do want to do this.)
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.
Perhaps this can come later as we convert our various helper functions to work with ProperType instead of Type?
Yes, we can try to put more order here after get some experience, currently it is pure intuition.
@@ -1292,8 +1308,8 @@ def check_inplace_operator_method(self, defn: FuncBase) -> None: | |||
other_method = '__' + method[3:] | |||
if cls.has_readable_member(other_method): | |||
instance = fill_typevars(cls) | |||
typ2 = self.expr_checker.analyze_external_member_access( | |||
other_method, instance, defn) | |||
typ2 = get_proper_type(self.expr_checker.analyze_external_member_access( |
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.
Could we maybe have the analyze_external_member_access
method return a proper type directly? "Pushing down" the conversion could maybe help minimize the number of times we need to sprinkle around these get_proper_type
calls.
It's probably not important that we do this now though.
first_type = self.determine_type_of_class_member(first) | ||
second_type = self.determine_type_of_class_member(second) | ||
first_type = get_proper_type(self.determine_type_of_class_member(first)) | ||
second_type = get_proper_type(self.determine_type_of_class_member(second)) |
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.
Same comment as above regarding pushing down the proper type checks.
@@ -2999,7 +3031,7 @@ def visit_raise_stmt(self, s: RaiseStmt) -> None: | |||
|
|||
def type_check_raise(self, e: Expression, s: RaiseStmt, | |||
optional: bool = False) -> None: | |||
typ = self.expr_checker.accept(e) | |||
typ = get_proper_type(self.expr_checker.accept(e)) |
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.
And I guess same comment as before about pushing down this conversion. Though maybe making this change won't be feasible to do? I'm guessing making expr_checker.accept
return a ProperType would require substantially more work.
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.
Answering here for all three places. I was thinking about this, but I want to preserve actual type as more as possible to keep the possibility to mention original alias name in error messages. Also putting it in such hot places as accept()
may cause performance penalty.
return all(is_special_target(t) for t in get_proper_types(right.items)) | ||
return False | ||
|
||
|
||
def is_improper_type(typ: Type) -> bool: |
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.
Should we also consider 'Any' to be an improper type? (Could be overkill/out-of-scope for this PR)
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 this and below actually makes sense. I will try and if it is not hard, I will add to this PR.
# Special case: these are not valid targets for a type alias and thus safe. | ||
return ctx.default_return_type | ||
if is_special_target(right): | ||
return ctx.default_return_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.
Idea: we could also maybe add a check for when the left operand is Any and the right operand is some Type. Could be overkill/out-of-scope for this PR.
var_type = get_proper_type(var.type) | ||
if isinstance(var_type, Instance): | ||
if self.is_literal_context() and var_type.last_known_value is not None: | ||
return var_type.last_known_value | ||
if var.name() in {'True', 'False'}: | ||
return self.infer_literal_expr_type(var.name() == 'True', 'builtins.bool') | ||
return var.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.
Do things end up breaking if we return var_type
and make the return value be ProperType?
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.
No, but again I would like to keep the original type for error message (if we will decide this at some point, I think there is even an issue about this).
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 haven't done a super detailed review but this all looks basically reasonable.
(It would be a little cleaner in a language with pattern matching, because you could just throw it in at all the case statements (although nested matches would be hurt...), but it isn't as bad as I feared. Walrus operator would help in a lot of places, though.).
This is really important work that I think has the potential to unlock more than just recursive types. Any sort of unification based improvements to the type inference system would need something like this also.
@msullivan Thanks!
I am sorry but I am not sure what do you mean by "unification based improvements to the type inference system", do you mean something like |
…7885) This is the last part of plumbing for recursive types (previous #7366 and #7330). Here I implement visitors and related functions. I convinced myself that we need to only be more careful when a recursive type is checked against another recursive one, so I only special-case these. Logic is similar to how protocols behave, because very roughly type alias can be imagined as a protocol with single property: ```python A = Union[T, Tuple[A[T], ...]] class A(Protocol[T]): @Property def __base__(self) -> Union[T, Tuple[A[T], ...]]: ... ``` but where `TypeAliasType` plays role of `Instance` and `TypeAlias` plays role of `TypeInfo`. Next two pull requests will contain some non-trivial implementation logic.
This completes the addition of expansion points before every
isinstance()
checks on types.This essentially does the same as #7330. Couple comments:
AnyType
,NoneType
, andUninhabitedType
, but technically these are valid targets for aliases, so I still do expansions there.get_proper_type()
andget_proper_types()
calls, however sometimes when they trigger they add some additional spurious errors about non-matching overloads. I tried to fix this, but it looks like this requires callingget_function_hook()
once on a whole overload (now it is called for every item).Next part will contain implementation of the visitors.