Skip to content

Commit b29051c

Browse files
authored
Support for hasattr() checks (#13544)
Fixes #1424 Fixes mypyc/mypyc#939 Not that I really like `hasattr()` but this issue comes up surprisingly often. Also it looks like we can offer a simple solution that will cover 95% of use cases using `extra_attrs` for instances. Essentially the logic is like this: * In the if branch, keep types that already has a valid attribute as is, for other inject an attribute with `Any` type using fallbacks. * In the else branch, remove types that already have a valid attribute, while keeping the rest.
1 parent c2949e9 commit b29051c

File tree

12 files changed

+406
-41
lines changed

12 files changed

+406
-41
lines changed

mypy/checker.py

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
true_only,
168168
try_expanding_sum_type_to_union,
169169
try_getting_int_literals_from_type,
170+
try_getting_str_literals,
170171
try_getting_str_literals_from_type,
171172
tuple_fallback,
172173
)
@@ -4701,7 +4702,7 @@ def _make_fake_typeinfo_and_full_name(
47014702
return None
47024703

47034704
curr_module.names[full_name] = SymbolTableNode(GDEF, info)
4704-
return Instance(info, [])
4705+
return Instance(info, [], extra_attrs=instances[0].extra_attrs or instances[1].extra_attrs)
47054706

47064707
def intersect_instance_callable(self, typ: Instance, callable_type: CallableType) -> Instance:
47074708
"""Creates a fake type that represents the intersection of an Instance and a CallableType.
@@ -4728,7 +4729,7 @@ def intersect_instance_callable(self, typ: Instance, callable_type: CallableType
47284729

47294730
cur_module.names[gen_name] = SymbolTableNode(GDEF, info)
47304731

4731-
return Instance(info, [])
4732+
return Instance(info, [], extra_attrs=typ.extra_attrs)
47324733

47334734
def make_fake_callable(self, typ: Instance) -> Instance:
47344735
"""Produce a new type that makes type Callable with a generic callable type."""
@@ -5032,6 +5033,12 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM
50325033
if literal(expr) == LITERAL_TYPE:
50335034
vartype = self.lookup_type(expr)
50345035
return self.conditional_callable_type_map(expr, vartype)
5036+
elif refers_to_fullname(node.callee, "builtins.hasattr"):
5037+
if len(node.args) != 2: # the error will be reported elsewhere
5038+
return {}, {}
5039+
attr = try_getting_str_literals(node.args[1], self.lookup_type(node.args[1]))
5040+
if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1:
5041+
return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0])
50355042
elif isinstance(node.callee, RefExpr):
50365043
if node.callee.type_guard is not None:
50375044
# TODO: Follow keyword args or *args, **kwargs
@@ -6239,6 +6246,95 @@ class Foo(Enum):
62396246
and member_type.fallback.type == parent_type.type_object()
62406247
)
62416248

6249+
def add_any_attribute_to_type(self, typ: Type, name: str) -> Type:
6250+
"""Inject an extra attribute with Any type using fallbacks."""
6251+
orig_typ = typ
6252+
typ = get_proper_type(typ)
6253+
any_type = AnyType(TypeOfAny.unannotated)
6254+
if isinstance(typ, Instance):
6255+
result = typ.copy_with_extra_attr(name, any_type)
6256+
# For instances, we erase the possible module name, so that restrictions
6257+
# become anonymous types.ModuleType instances, allowing hasattr() to
6258+
# have effect on modules.
6259+
assert result.extra_attrs is not None
6260+
result.extra_attrs.mod_name = None
6261+
return result
6262+
if isinstance(typ, TupleType):
6263+
fallback = typ.partial_fallback.copy_with_extra_attr(name, any_type)
6264+
return typ.copy_modified(fallback=fallback)
6265+
if isinstance(typ, CallableType):
6266+
fallback = typ.fallback.copy_with_extra_attr(name, any_type)
6267+
return typ.copy_modified(fallback=fallback)
6268+
if isinstance(typ, TypeType) and isinstance(typ.item, Instance):
6269+
return TypeType.make_normalized(self.add_any_attribute_to_type(typ.item, name))
6270+
if isinstance(typ, TypeVarType):
6271+
return typ.copy_modified(
6272+
upper_bound=self.add_any_attribute_to_type(typ.upper_bound, name),
6273+
values=[self.add_any_attribute_to_type(v, name) for v in typ.values],
6274+
)
6275+
if isinstance(typ, UnionType):
6276+
with_attr, without_attr = self.partition_union_by_attr(typ, name)
6277+
return make_simplified_union(
6278+
with_attr + [self.add_any_attribute_to_type(typ, name) for typ in without_attr]
6279+
)
6280+
return orig_typ
6281+
6282+
def hasattr_type_maps(
6283+
self, expr: Expression, source_type: Type, name: str
6284+
) -> tuple[TypeMap, TypeMap]:
6285+
"""Simple support for hasattr() checks.
6286+
6287+
Essentially the logic is following:
6288+
* In the if branch, keep types that already has a valid attribute as is,
6289+
for other inject an attribute with `Any` type.
6290+
* In the else branch, remove types that already have a valid attribute,
6291+
while keeping the rest.
6292+
"""
6293+
if self.has_valid_attribute(source_type, name):
6294+
return {expr: source_type}, {}
6295+
6296+
source_type = get_proper_type(source_type)
6297+
if isinstance(source_type, UnionType):
6298+
_, without_attr = self.partition_union_by_attr(source_type, name)
6299+
yes_map = {expr: self.add_any_attribute_to_type(source_type, name)}
6300+
return yes_map, {expr: make_simplified_union(without_attr)}
6301+
6302+
type_with_attr = self.add_any_attribute_to_type(source_type, name)
6303+
if type_with_attr != source_type:
6304+
return {expr: type_with_attr}, {}
6305+
return {}, {}
6306+
6307+
def partition_union_by_attr(
6308+
self, source_type: UnionType, name: str
6309+
) -> tuple[list[Type], list[Type]]:
6310+
with_attr = []
6311+
without_attr = []
6312+
for item in source_type.items:
6313+
if self.has_valid_attribute(item, name):
6314+
with_attr.append(item)
6315+
else:
6316+
without_attr.append(item)
6317+
return with_attr, without_attr
6318+
6319+
def has_valid_attribute(self, typ: Type, name: str) -> bool:
6320+
if isinstance(get_proper_type(typ), AnyType):
6321+
return False
6322+
with self.msg.filter_errors() as watcher:
6323+
analyze_member_access(
6324+
name,
6325+
typ,
6326+
TempNode(AnyType(TypeOfAny.special_form)),
6327+
False,
6328+
False,
6329+
False,
6330+
self.msg,
6331+
original_type=typ,
6332+
chk=self,
6333+
# This is not a real attribute lookup so don't mess with deferring nodes.
6334+
no_deferral=True,
6335+
)
6336+
return not watcher.has_new_errors()
6337+
62426338

62436339
class CollectArgTypes(TypeTraverserVisitor):
62446340
"""Collects the non-nested argument types in a set."""

mypy/checkexpr.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,8 @@ def module_type(self, node: MypyFile) -> Instance:
380380
module_attrs = {}
381381
immutable = set()
382382
for name, n in node.names.items():
383+
if not n.module_public:
384+
continue
383385
if isinstance(n.node, Var) and n.node.is_final:
384386
immutable.add(name)
385387
typ = self.chk.determine_type_of_member(n)

mypy/checkmember.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def __init__(
9090
chk: mypy.checker.TypeChecker,
9191
self_type: Type | None,
9292
module_symbol_table: SymbolTable | None = None,
93+
no_deferral: bool = False,
9394
) -> None:
9495
self.is_lvalue = is_lvalue
9596
self.is_super = is_super
@@ -100,6 +101,7 @@ def __init__(
100101
self.msg = msg
101102
self.chk = chk
102103
self.module_symbol_table = module_symbol_table
104+
self.no_deferral = no_deferral
103105

104106
def named_type(self, name: str) -> Instance:
105107
return self.chk.named_type(name)
@@ -124,6 +126,7 @@ def copy_modified(
124126
self.chk,
125127
self.self_type,
126128
self.module_symbol_table,
129+
self.no_deferral,
127130
)
128131
if messages is not None:
129132
mx.msg = messages
@@ -149,6 +152,7 @@ def analyze_member_access(
149152
in_literal_context: bool = False,
150153
self_type: Type | None = None,
151154
module_symbol_table: SymbolTable | None = None,
155+
no_deferral: bool = False,
152156
) -> Type:
153157
"""Return the type of attribute 'name' of 'typ'.
154158
@@ -183,6 +187,7 @@ def analyze_member_access(
183187
chk=chk,
184188
self_type=self_type,
185189
module_symbol_table=module_symbol_table,
190+
no_deferral=no_deferral,
186191
)
187192
result = _analyze_member_access(name, typ, mx, override_info)
188193
possible_literal = get_proper_type(result)
@@ -540,6 +545,11 @@ def analyze_member_var_access(
540545
return AnyType(TypeOfAny.special_form)
541546

542547
# Could not find the member.
548+
if itype.extra_attrs and name in itype.extra_attrs.attrs:
549+
# For modules use direct symbol table lookup.
550+
if not itype.extra_attrs.mod_name:
551+
return itype.extra_attrs.attrs[name]
552+
543553
if mx.is_super:
544554
mx.msg.undefined_in_superclass(name, mx.context)
545555
return AnyType(TypeOfAny.from_error)
@@ -744,7 +754,7 @@ def analyze_var(
744754
else:
745755
result = expanded_signature
746756
else:
747-
if not var.is_ready:
757+
if not var.is_ready and not mx.no_deferral:
748758
mx.not_ready_callback(var.name, mx.context)
749759
# Implicit 'Any' type.
750760
result = AnyType(TypeOfAny.special_form)
@@ -858,6 +868,10 @@ def analyze_class_attribute_access(
858868

859869
node = info.get(name)
860870
if not node:
871+
if itype.extra_attrs and name in itype.extra_attrs.attrs:
872+
# For modules use direct symbol table lookup.
873+
if not itype.extra_attrs.mod_name:
874+
return itype.extra_attrs.attrs[name]
861875
if info.fallback_to_any:
862876
return apply_class_attr_hook(mx, hook, AnyType(TypeOfAny.special_form))
863877
return None

mypy/meet.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
from mypy.erasetype import erase_type
77
from mypy.maptype import map_instance_to_supertype
88
from mypy.state import state
9-
from mypy.subtypes import is_callable_compatible, is_equivalent, is_proper_subtype, is_subtype
9+
from mypy.subtypes import (
10+
is_callable_compatible,
11+
is_equivalent,
12+
is_proper_subtype,
13+
is_same_type,
14+
is_subtype,
15+
)
1016
from mypy.typeops import is_recursive_pair, make_simplified_union, tuple_fallback
1117
from mypy.types import (
1218
AnyType,
@@ -61,11 +67,25 @@ def meet_types(s: Type, t: Type) -> ProperType:
6167
"""Return the greatest lower bound of two types."""
6268
if is_recursive_pair(s, t):
6369
# This case can trigger an infinite recursion, general support for this will be
64-
# tricky so we use a trivial meet (like for protocols).
70+
# tricky, so we use a trivial meet (like for protocols).
6571
return trivial_meet(s, t)
6672
s = get_proper_type(s)
6773
t = get_proper_type(t)
6874

75+
if isinstance(s, Instance) and isinstance(t, Instance) and s.type == t.type:
76+
# Code in checker.py should merge any extra_items where possible, so we
77+
# should have only compatible extra_items here. We check this before
78+
# the below subtype check, so that extra_attrs will not get erased.
79+
if is_same_type(s, t) and (s.extra_attrs or t.extra_attrs):
80+
if s.extra_attrs and t.extra_attrs:
81+
if len(s.extra_attrs.attrs) > len(t.extra_attrs.attrs):
82+
# Return the one that has more precise information.
83+
return s
84+
return t
85+
if s.extra_attrs:
86+
return s
87+
return t
88+
6989
if not isinstance(s, UnboundType) and not isinstance(t, UnboundType):
7090
if is_proper_subtype(s, t, ignore_promotions=True):
7191
return s

mypy/server/objgraph.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ def get_edges(o: object) -> Iterator[tuple[object, object]]:
6464
# in closures and self pointers to other objects
6565

6666
if hasattr(e, "__closure__"):
67-
yield (s, "__closure__"), e.__closure__ # type: ignore[union-attr]
67+
yield (s, "__closure__"), e.__closure__
6868
if hasattr(e, "__self__"):
69-
se = e.__self__ # type: ignore[union-attr]
69+
se = e.__self__
7070
if se is not o and se is not type(o) and hasattr(s, "__self__"):
71-
yield s.__self__, se # type: ignore[attr-defined]
71+
yield s.__self__, se
7272
else:
7373
if not type(e) in TYPE_BLACKLIST:
7474
yield s, e

mypy/typeops.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
TupleType,
4646
Type,
4747
TypeAliasType,
48+
TypedDictType,
4849
TypeOfAny,
4950
TypeQuery,
5051
TypeType,
@@ -104,7 +105,7 @@ def tuple_fallback(typ: TupleType) -> Instance:
104105
raise NotImplementedError
105106
else:
106107
items.append(item)
107-
return Instance(info, [join_type_list(items)])
108+
return Instance(info, [join_type_list(items)], extra_attrs=typ.partial_fallback.extra_attrs)
108109

109110

110111
def get_self_type(func: CallableType, default_self: Instance | TupleType) -> Type | None:
@@ -462,7 +463,20 @@ def make_simplified_union(
462463
):
463464
simplified_set = try_contracting_literals_in_union(simplified_set)
464465

465-
return get_proper_type(UnionType.make_union(simplified_set, line, column))
466+
result = get_proper_type(UnionType.make_union(simplified_set, line, column))
467+
468+
# Step 4: At last, we erase any (inconsistent) extra attributes on instances.
469+
extra_attrs_set = set()
470+
for item in items:
471+
instance = try_getting_instance_fallback(item)
472+
if instance and instance.extra_attrs:
473+
extra_attrs_set.add(instance.extra_attrs)
474+
475+
fallback = try_getting_instance_fallback(result)
476+
if len(extra_attrs_set) > 1 and fallback:
477+
fallback.extra_attrs = None
478+
479+
return result
466480

467481

468482
def _remove_redundant_union_items(items: list[Type], keep_erased: bool) -> list[Type]:
@@ -984,3 +998,21 @@ def separate_union_literals(t: UnionType) -> tuple[Sequence[LiteralType], Sequen
984998
union_items.append(item)
985999

9861000
return literal_items, union_items
1001+
1002+
1003+
def try_getting_instance_fallback(typ: Type) -> Instance | None:
1004+
"""Returns the Instance fallback for this type if one exists or None."""
1005+
typ = get_proper_type(typ)
1006+
if isinstance(typ, Instance):
1007+
return typ
1008+
elif isinstance(typ, TupleType):
1009+
return typ.partial_fallback
1010+
elif isinstance(typ, TypedDictType):
1011+
return typ.fallback
1012+
elif isinstance(typ, FunctionLike):
1013+
return typ.fallback
1014+
elif isinstance(typ, LiteralType):
1015+
return typ.fallback
1016+
elif isinstance(typ, TypeVarType):
1017+
return try_getting_instance_fallback(typ.upper_bound)
1018+
return None

0 commit comments

Comments
 (0)