Skip to content

Commit ac511d6

Browse files
authored
Clean-up and move operator access to checkmember.py (#19250)
Fixes #5136 Fixes #5491 This is a fifth "major" PR toward #7724. Although it would be impractical to move all the operator special-casing to `checkmember.py`, this does two things: * Removes known inconsistencies in operator handling * Adds a much more complete `has_operator()` helper that can be a starting point for future performance optimizations
1 parent 1778d66 commit ac511d6

File tree

4 files changed

+94
-69
lines changed

4 files changed

+94
-69
lines changed

mypy/checkexpr.py

Lines changed: 29 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from mypy import applytype, erasetype, join, message_registry, nodes, operators, types
1717
from mypy.argmap import ArgTypeExpander, map_actuals_to_formals, map_formals_to_actuals
1818
from mypy.checker_shared import ExpressionCheckerSharedApi
19-
from mypy.checkmember import analyze_member_access
19+
from mypy.checkmember import analyze_member_access, has_operator
2020
from mypy.checkstrformat import StringFormatterChecker
2121
from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars
2222
from mypy.errors import ErrorWatcher, report_internal_error
@@ -3834,13 +3834,16 @@ def check_method_call_by_name(
38343834
arg_kinds: list[ArgKind],
38353835
context: Context,
38363836
original_type: Type | None = None,
3837+
self_type: Type | None = None,
38373838
) -> tuple[Type, Type]:
38383839
"""Type check a call to a named method on an object.
38393840
38403841
Return tuple (result type, inferred method type). The 'original_type'
3841-
is used for error messages.
3842+
is used for error messages. The self_type is to bind self in methods
3843+
(see analyze_member_access for more details).
38423844
"""
38433845
original_type = original_type or base_type
3846+
self_type = self_type or base_type
38443847
# Unions are special-cased to allow plugins to act on each element of the union.
38453848
base_type = get_proper_type(base_type)
38463849
if isinstance(base_type, UnionType):
@@ -3856,7 +3859,7 @@ def check_method_call_by_name(
38563859
is_super=False,
38573860
is_operator=True,
38583861
original_type=original_type,
3859-
self_type=base_type,
3862+
self_type=self_type,
38603863
chk=self.chk,
38613864
in_literal_context=self.is_literal_context(),
38623865
)
@@ -3933,11 +3936,8 @@ def lookup_operator(op_name: str, base_type: Type) -> Type | None:
39333936
"""Looks up the given operator and returns the corresponding type,
39343937
if it exists."""
39353938

3936-
# This check is an important performance optimization,
3937-
# even though it is mostly a subset of
3938-
# analyze_member_access.
3939-
# TODO: Find a way to remove this call without performance implications.
3940-
if not self.has_member(base_type, op_name):
3939+
# This check is an important performance optimization.
3940+
if not has_operator(base_type, op_name, self.named_type):
39413941
return None
39423942

39433943
with self.msg.filter_errors() as w:
@@ -4097,14 +4097,8 @@ def lookup_definer(typ: Instance, attr_name: str) -> str | None:
40974097
errors.append(local_errors.filtered_errors())
40984098
results.append(result)
40994099
else:
4100-
# In theory, we should never enter this case, but it seems
4101-
# we sometimes do, when dealing with Type[...]? E.g. see
4102-
# check-classes.testTypeTypeComparisonWorks.
4103-
#
4104-
# This is probably related to the TODO in lookup_operator(...)
4105-
# up above.
4106-
#
4107-
# TODO: Remove this extra case
4100+
# Although we should not need this case anymore, we keep it just in case, as
4101+
# otherwise we will get a crash if we introduce inconsistency in checkmember.py
41084102
return result
41094103

41104104
self.msg.add_errors(errors[0])
@@ -4365,13 +4359,19 @@ def visit_index_expr_helper(self, e: IndexExpr) -> Type:
43654359
return self.visit_index_with_type(left_type, e)
43664360

43674361
def visit_index_with_type(
4368-
self, left_type: Type, e: IndexExpr, original_type: ProperType | None = None
4362+
self,
4363+
left_type: Type,
4364+
e: IndexExpr,
4365+
original_type: ProperType | None = None,
4366+
self_type: Type | None = None,
43694367
) -> Type:
43704368
"""Analyze type of an index expression for a given type of base expression.
43714369
4372-
The 'original_type' is used for error messages (currently used for union types).
4370+
The 'original_type' is used for error messages (currently used for union types). The
4371+
'self_type' is to bind self in methods (see analyze_member_access for more details).
43734372
"""
43744373
index = e.index
4374+
self_type = self_type or left_type
43754375
left_type = get_proper_type(left_type)
43764376

43774377
# Visit the index, just to make sure we have a type for it available
@@ -4426,16 +4426,22 @@ def visit_index_with_type(
44264426
):
44274427
return self.named_type("types.GenericAlias")
44284428

4429-
if isinstance(left_type, TypeVarType) and not self.has_member(
4430-
left_type.upper_bound, "__getitem__"
4431-
):
4432-
return self.visit_index_with_type(left_type.upper_bound, e, original_type)
4429+
if isinstance(left_type, TypeVarType):
4430+
return self.visit_index_with_type(
4431+
left_type.values_or_bound(), e, original_type, left_type
4432+
)
44334433
elif isinstance(left_type, Instance) and left_type.type.fullname == "typing._SpecialForm":
44344434
# Allow special forms to be indexed and used to create union types
44354435
return self.named_type("typing._SpecialForm")
44364436
else:
44374437
result, method_type = self.check_method_call_by_name(
4438-
"__getitem__", left_type, [e.index], [ARG_POS], e, original_type=original_type
4438+
"__getitem__",
4439+
left_type,
4440+
[e.index],
4441+
[ARG_POS],
4442+
e,
4443+
original_type=original_type,
4444+
self_type=self_type,
44394445
)
44404446
e.method_type = method_type
44414447
return result
@@ -5995,45 +6001,6 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool:
59956001
or isinstance(typ, ParamSpecType)
59966002
)
59976003

5998-
def has_member(self, typ: Type, member: str) -> bool:
5999-
"""Does type have member with the given name?"""
6000-
# TODO: refactor this to use checkmember.analyze_member_access, otherwise
6001-
# these two should be carefully kept in sync.
6002-
# This is much faster than analyze_member_access, though, and so using
6003-
# it first as a filter is important for performance.
6004-
typ = get_proper_type(typ)
6005-
6006-
if isinstance(typ, TypeVarType):
6007-
typ = get_proper_type(typ.upper_bound)
6008-
if isinstance(typ, TupleType):
6009-
typ = tuple_fallback(typ)
6010-
if isinstance(typ, LiteralType):
6011-
typ = typ.fallback
6012-
if isinstance(typ, Instance):
6013-
return typ.type.has_readable_member(member)
6014-
if isinstance(typ, FunctionLike) and typ.is_type_obj():
6015-
return typ.fallback.type.has_readable_member(member)
6016-
elif isinstance(typ, AnyType):
6017-
return True
6018-
elif isinstance(typ, UnionType):
6019-
result = all(self.has_member(x, member) for x in typ.relevant_items())
6020-
return result
6021-
elif isinstance(typ, TypeType):
6022-
# Type[Union[X, ...]] is always normalized to Union[Type[X], ...],
6023-
# so we don't need to care about unions here.
6024-
item = typ.item
6025-
if isinstance(item, TypeVarType):
6026-
item = get_proper_type(item.upper_bound)
6027-
if isinstance(item, TupleType):
6028-
item = tuple_fallback(item)
6029-
if isinstance(item, Instance) and item.type.metaclass_type is not None:
6030-
return self.has_member(item.type.metaclass_type, member)
6031-
if isinstance(item, AnyType):
6032-
return True
6033-
return False
6034-
else:
6035-
return False
6036-
60376004
def not_ready_callback(self, name: str, context: Context) -> None:
60386005
"""Called when we can't infer the type of a variable because it's not ready yet.
60396006

mypy/checkmember.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,3 +1501,57 @@ def bind_self_fast(method: F, original_type: Type | None = None) -> F:
15011501
is_bound=True,
15021502
)
15031503
return cast(F, res)
1504+
1505+
1506+
def has_operator(typ: Type, op_method: str, named_type: Callable[[str], Instance]) -> bool:
1507+
"""Does type have operator with the given name?
1508+
1509+
Note: this follows the rules for operator access, in particular:
1510+
* __getattr__ is not considered
1511+
* for class objects we only look in metaclass
1512+
* instance level attributes (i.e. extra_attrs) are not considered
1513+
"""
1514+
# This is much faster than analyze_member_access, and so using
1515+
# it first as a filter is important for performance. This is mostly relevant
1516+
# in situations where we can't expect that method is likely present,
1517+
# e.g. for __OP__ vs __rOP__.
1518+
typ = get_proper_type(typ)
1519+
1520+
if isinstance(typ, TypeVarLikeType):
1521+
typ = typ.values_or_bound()
1522+
if isinstance(typ, AnyType):
1523+
return True
1524+
if isinstance(typ, UnionType):
1525+
return all(has_operator(x, op_method, named_type) for x in typ.relevant_items())
1526+
if isinstance(typ, FunctionLike) and typ.is_type_obj():
1527+
return typ.fallback.type.has_readable_member(op_method)
1528+
if isinstance(typ, TypeType):
1529+
# Type[Union[X, ...]] is always normalized to Union[Type[X], ...],
1530+
# so we don't need to care about unions here, but we need to care about
1531+
# Type[T], where upper bound of T is a union.
1532+
item = typ.item
1533+
if isinstance(item, TypeVarType):
1534+
item = item.values_or_bound()
1535+
if isinstance(item, UnionType):
1536+
return all(meta_has_operator(x, op_method, named_type) for x in item.relevant_items())
1537+
return meta_has_operator(item, op_method, named_type)
1538+
return instance_fallback(typ, named_type).type.has_readable_member(op_method)
1539+
1540+
1541+
def instance_fallback(typ: ProperType, named_type: Callable[[str], Instance]) -> Instance:
1542+
if isinstance(typ, Instance):
1543+
return typ
1544+
if isinstance(typ, TupleType):
1545+
return tuple_fallback(typ)
1546+
if isinstance(typ, (LiteralType, TypedDictType)):
1547+
return typ.fallback
1548+
return named_type("builtins.object")
1549+
1550+
1551+
def meta_has_operator(item: Type, op_method: str, named_type: Callable[[str], Instance]) -> bool:
1552+
item = get_proper_type(item)
1553+
if isinstance(item, AnyType):
1554+
return True
1555+
item = instance_fallback(item, named_type)
1556+
meta = item.type.metaclass_type or named_type("builtins.type")
1557+
return meta.type.has_readable_member(op_method)

mypy/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,11 @@ def has_default(self) -> bool:
615615
t = get_proper_type(self.default)
616616
return not (isinstance(t, AnyType) and t.type_of_any == TypeOfAny.from_omitted_generics)
617617

618+
def values_or_bound(self) -> ProperType:
619+
if isinstance(self, TypeVarType) and self.values:
620+
return UnionType(self.values)
621+
return get_proper_type(self.upper_bound)
622+
618623

619624
class TypeVarType(TypeVarLikeType):
620625
"""Type that refers to a type variable."""

test-data/unit/check-typeddict.test

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3358,16 +3358,13 @@ foo: Foo = {'key': 1}
33583358
foo | 1
33593359

33603360
class SubDict(dict): ...
3361-
foo | SubDict()
3361+
reveal_type(foo | SubDict())
33623362
[out]
33633363
main:7: error: No overload variant of "__or__" of "TypedDict" matches argument type "int"
33643364
main:7: note: Possible overload variants:
33653365
main:7: note: def __or__(self, TypedDict({'key'?: int}), /) -> Foo
33663366
main:7: note: def __or__(self, dict[str, Any], /) -> dict[str, object]
3367-
main:10: error: No overload variant of "__ror__" of "dict" matches argument type "Foo"
3368-
main:10: note: Possible overload variants:
3369-
main:10: note: def __ror__(self, dict[Any, Any], /) -> dict[Any, Any]
3370-
main:10: note: def [T, T2] __ror__(self, dict[T, T2], /) -> dict[Union[Any, T], Union[Any, T2]]
3367+
main:10: note: Revealed type is "builtins.dict[builtins.str, builtins.object]"
33713368
[builtins fixtures/dict-full.pyi]
33723369
[typing fixtures/typing-typeddict-iror.pyi]
33733370

@@ -3389,8 +3386,10 @@ d2: Dict[int, str]
33893386

33903387
reveal_type(d1 | foo) # N: Revealed type is "builtins.dict[builtins.str, builtins.object]"
33913388
d2 | foo # E: Unsupported operand types for | ("dict[int, str]" and "Foo")
3392-
1 | foo # E: Unsupported left operand type for | ("int")
3393-
3389+
1 | foo # E: No overload variant of "__ror__" of "TypedDict" matches argument type "int" \
3390+
# N: Possible overload variants: \
3391+
# N: def __ror__(self, TypedDict({'key'?: int}), /) -> Foo \
3392+
# N: def __ror__(self, dict[str, Any], /) -> dict[str, object]
33943393

33953394
class Bar(TypedDict):
33963395
key: int

0 commit comments

Comments
 (0)