Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0177c0d

Browse files
authored
Improve our support for hasattr() (#20914)
Our support for `hasattr()` is quite fragile/limited. In particular, it didn't work if there was an existing narrowing in the current frame. For `--allow-redefinition-new` this was a critical hit, since it can do `binder.put()` on initial assignment, thus making our `hasattr()` support non-functional. This (somewhat ad-hoc) fix seems to restore parity for `hasattr()` support. Also it may even improve `hasattr()` support n regular mode in some edge cases
1 parent 0d20e52 commit 0177c0d

4 files changed

Lines changed: 61 additions & 6 deletions

File tree

mypy/binder.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,14 @@ def update_from_options(self, frames: list[Frame]) -> bool:
393393
)
394394
if simplified == self.declarations[key]:
395395
type = simplified
396-
if current_value is None or not is_same_type(type, current_value.type):
396+
if (
397+
current_value is None
398+
or not is_same_type(type, current_value.type)
399+
# Manually carry over any narrowing from hasattr() from inner frames. This is
400+
# a bit ad-hoc, but our handling of hasattr() is on best effort basis anyway.
401+
or isinstance(p_type := get_proper_type(type), Instance)
402+
and p_type.extra_attrs
403+
):
397404
self._put(key, type, from_assignment=True)
398405
if current_value is not None or extract_var_from_literal_hash(key) is None:
399406
# We definitely learned something new

mypy/typeops.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -629,9 +629,23 @@ def make_simplified_union(
629629
else:
630630
extra_attrs_set.add(instance.extra_attrs)
631631

632-
if extra_attrs_set is not None and len(extra_attrs_set) > 1:
632+
# Code below is awkward, because we don't want the extra checks to affect
633+
# performance in the common case.
634+
erase_extra = False
635+
if extra_attrs_set is not None:
633636
fallback = try_getting_instance_fallback(result)
634-
if fallback:
637+
if fallback is None:
638+
return result
639+
if len(extra_attrs_set) > 1: # This case is too tricky to handle.
640+
erase_extra = True
641+
else:
642+
# Check that all relevant items have the extra attributes.
643+
for item in items:
644+
instance = try_getting_instance_fallback(item)
645+
if instance and instance.type == fallback.type and not instance.extra_attrs:
646+
erase_extra = True
647+
break
648+
if erase_extra:
635649
fallback.extra_attrs = None
636650

637651
return result
@@ -1220,16 +1234,16 @@ def try_getting_instance_fallback(typ: Type) -> Instance | None:
12201234
return typ
12211235
elif isinstance(typ, LiteralType):
12221236
return typ.fallback
1223-
elif isinstance(typ, NoneType):
1237+
elif isinstance(typ, (NoneType, AnyType)):
12241238
return None # Fast path for None, which is common
12251239
elif isinstance(typ, FunctionLike):
12261240
return typ.fallback
1241+
elif isinstance(typ, TypeVarType):
1242+
return try_getting_instance_fallback(typ.upper_bound)
12271243
elif isinstance(typ, TupleType):
12281244
return typ.partial_fallback
12291245
elif isinstance(typ, TypedDictType):
12301246
return typ.fallback
1231-
elif isinstance(typ, TypeVarType):
1232-
return try_getting_instance_fallback(typ.upper_bound)
12331247
return None
12341248

12351249

test-data/unit/check-redefine2.test

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,39 @@ def f1() -> None:
114114
reveal_type(x) # N: Revealed type is "builtins.str"
115115
reveal_type(x) # N: Revealed type is "builtins.str | None"
116116

117+
[case testNewRedefineHasAttr]
118+
# flags: --allow-redefinition-new --local-partial-types
119+
from typing import Union
120+
121+
def test(lst: list[object]) -> None:
122+
for cls in lst:
123+
if not hasattr(cls, "module"):
124+
break
125+
reveal_type(cls.module) # N: Revealed type is "Any"
126+
127+
other = object()
128+
if not hasattr(other, "foo"):
129+
return
130+
reveal_type(other.foo) # N: Revealed type is "Any"
131+
132+
x = object()
133+
if bool():
134+
assert hasattr(x, "bar")
135+
x.bar # OK
136+
x.bar # E: "object" has no attribute "bar"
137+
138+
class C:
139+
x: int
140+
141+
u: Union[C, object]
142+
if hasattr(u, "x"):
143+
# Ideally we should have Any | int here and below, but this is tricky
144+
reveal_type(u.x) # N: Revealed type is "Any"
145+
146+
y = hasattr(u, "x") and u.x
147+
reveal_type(y) # N: Revealed type is "Literal[False] | Any"
148+
[builtins fixtures/isinstancelist.pyi]
149+
117150
[case testNewRedefineGlobalVariableSimple]
118151
# flags: --allow-redefinition-new --local-partial-types
119152
if int():

test-data/unit/fixtures/isinstancelist.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Ellipsis = ellipsis()
1818

1919
def isinstance(x: object, t: Union[type, Tuple]) -> bool: pass
2020
def issubclass(x: object, t: Union[type, Tuple]) -> bool: pass
21+
def hasattr(x: object, name: str) -> bool: pass
2122

2223
class int:
2324
def __add__(self, x: int) -> int: pass

0 commit comments

Comments
 (0)