-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Use checkmember.py to check protocol subtyping #18943
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
base: master
Are you sure you want to change the base?
Changes from all commits
3a3cdf8
9c3d303
738a1d7
4b08554
5b3190a
fac2854
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from __future__ import annotations | ||
|
||
from collections.abc import Iterator | ||
from contextlib import contextmanager | ||
from typing import Final | ||
|
||
from mypy.checker_shared import TypeCheckerSharedApi | ||
|
||
# This is global mutable state. Don't add anything here unless there's a very | ||
# good reason. | ||
|
||
|
||
class TypeCheckerState: | ||
# Wrap this in a class since it's faster that using a module-level attribute. | ||
|
||
def __init__(self, type_checker: TypeCheckerSharedApi | None) -> None: | ||
# Value varies by file being processed | ||
self.type_checker = type_checker | ||
|
||
@contextmanager | ||
def set(self, value: TypeCheckerSharedApi) -> Iterator[None]: | ||
saved = self.type_checker | ||
self.type_checker = value | ||
try: | ||
yield | ||
finally: | ||
self.type_checker = saved | ||
|
||
|
||
checker_state: Final = TypeCheckerState(type_checker=None) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
import mypy.applytype | ||
import mypy.constraints | ||
import mypy.typeops | ||
from mypy.checker_state import checker_state | ||
from mypy.erasetype import erase_type | ||
from mypy.expandtype import ( | ||
expand_self_type, | ||
|
@@ -26,6 +27,7 @@ | |
COVARIANT, | ||
INVARIANT, | ||
VARIANCE_NOT_READY, | ||
Context, | ||
Decorator, | ||
FuncBase, | ||
OverloadedFuncDef, | ||
|
@@ -717,8 +719,7 @@ def visit_callable_type(self, left: CallableType) -> bool: | |
elif isinstance(right, Instance): | ||
if right.type.is_protocol and "__call__" in right.type.protocol_members: | ||
# OK, a callable can implement a protocol with a `__call__` member. | ||
# TODO: we should probably explicitly exclude self-types in this case. | ||
call = find_member("__call__", right, left, is_operator=True) | ||
call = find_member("__call__", right, right, is_operator=True) | ||
assert call is not None | ||
if self._is_subtype(left, call): | ||
if len(right.type.protocol_members) == 1: | ||
|
@@ -954,7 +955,7 @@ def visit_overloaded(self, left: Overloaded) -> bool: | |
if isinstance(right, Instance): | ||
if right.type.is_protocol and "__call__" in right.type.protocol_members: | ||
# same as for CallableType | ||
call = find_member("__call__", right, left, is_operator=True) | ||
call = find_member("__call__", right, right, is_operator=True) | ||
assert call is not None | ||
if self._is_subtype(left, call): | ||
if len(right.type.protocol_members) == 1: | ||
|
@@ -1266,14 +1267,87 @@ def find_member( | |
is_operator: bool = False, | ||
class_obj: bool = False, | ||
is_lvalue: bool = False, | ||
) -> Type | None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of the remaining performance regression comes from That fast path might cover access to normal attribute/method via instance when there are no self types or properties, for example. Maybe we can avoid creating There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may be harder to tune, so although I agree we should do this, i would leave this optimization for later. |
||
type_checker = checker_state.type_checker | ||
if type_checker is None: | ||
# Unfortunately, there are many scenarios where someone calls is_subtype() before | ||
# type checking phase. In this case we fallback to old (incomplete) logic. | ||
# TODO: reduce number of such cases (e.g. semanal_typeargs, post-semanal plugins). | ||
return find_member_simple( | ||
name, itype, subtype, is_operator=is_operator, class_obj=class_obj, is_lvalue=is_lvalue | ||
) | ||
|
||
# We don't use ATTR_DEFINED error code below (since missing attributes can cause various | ||
# other error codes), instead we perform quick node lookup with all the fallbacks. | ||
info = itype.type | ||
sym = info.get(name) | ||
node = sym.node if sym else None | ||
if not node: | ||
name_not_found = True | ||
if ( | ||
name not in ["__getattr__", "__setattr__", "__getattribute__"] | ||
and not is_operator | ||
and not class_obj | ||
and itype.extra_attrs is None # skip ModuleType.__getattr__ | ||
): | ||
for method_name in ("__getattribute__", "__getattr__"): | ||
method = info.get_method(method_name) | ||
if method and method.info.fullname != "builtins.object": | ||
name_not_found = False | ||
break | ||
if name_not_found: | ||
if info.fallback_to_any or class_obj and info.meta_fallback_to_any: | ||
return AnyType(TypeOfAny.special_form) | ||
if itype.extra_attrs and name in itype.extra_attrs.attrs: | ||
return itype.extra_attrs.attrs[name] | ||
return None | ||
|
||
from mypy.checkmember import ( | ||
MemberContext, | ||
analyze_class_attribute_access, | ||
analyze_instance_member_access, | ||
) | ||
|
||
mx = MemberContext( | ||
is_lvalue=is_lvalue, | ||
is_super=False, | ||
is_operator=is_operator, | ||
original_type=itype, | ||
self_type=subtype, | ||
context=Context(), # all errors are filtered, but this is a required argument | ||
chk=type_checker, | ||
suppress_errors=True, | ||
# This is needed to avoid infinite recursion in situations involving protocols like | ||
# class P(Protocol[T]): | ||
# def combine(self, other: P[S]) -> P[Tuple[T, S]]: ... | ||
# Normally we call freshen_all_functions_type_vars() during attribute access, | ||
# to avoid type variable id collisions, but for protocols this means we can't | ||
# use the assumption stack, that will grow indefinitely. | ||
# TODO: find a cleaner solution that doesn't involve massive perf impact. | ||
preserve_type_var_ids=True, | ||
) | ||
with type_checker.msg.filter_errors(filter_deprecated=True): | ||
if class_obj: | ||
fallback = itype.type.metaclass_type or mx.named_type("builtins.type") | ||
return analyze_class_attribute_access(itype, name, mx, mcs_fallback=fallback) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't tell why this PR doesn't fix #17567, because this should work? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, this is because the fallback to metaclass is handled by the caller of |
||
else: | ||
return analyze_instance_member_access(name, itype, mx, info) | ||
|
||
|
||
def find_member_simple( | ||
name: str, | ||
itype: Instance, | ||
subtype: Type, | ||
*, | ||
is_operator: bool = False, | ||
class_obj: bool = False, | ||
is_lvalue: bool = False, | ||
) -> Type | None: | ||
"""Find the type of member by 'name' in 'itype's TypeInfo. | ||
|
||
Find the member type after applying type arguments from 'itype', and binding | ||
'self' to 'subtype'. Return None if member was not found. | ||
""" | ||
# TODO: this code shares some logic with checkmember.analyze_member_access, | ||
# consider refactoring. | ||
info = itype.type | ||
method = info.get_method(name) | ||
if method: | ||
|
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 function does not appear to be a performance bottleneck (at least in self check).
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.
@JukkaL If you will have time, could you please check if there is any slowness because of
bind_self()
andcheck_self_arg()
? Although they are not modified, they may be called much more often now.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_self_arg
could be more expensive -- it appears to consume an extra ~0.5% of runtime in this PR. We are now spending maybe 2-3% of CPU in it, so it's quite hot, but it already was pretty hot before this PR. This could be noise though.I didn't see any major change in
bind_self
when doing self check, though it's pretty hot both before and after, though less hot thancheck_self_arg
.