Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion mypy/binder.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,14 @@ def update_from_options(self, frames: list[Frame]) -> bool:
)
if simplified == self.declarations[key]:
type = simplified
if current_value is None or not is_same_type(type, current_value.type):
if (
current_value is None
or not is_same_type(type, current_value.type)
# Manually carry over any narrowing from hasattr() from inner frames. This is
# a bit ad-hoc, but our handling of hasattr() is on best effort basis anyway.
or isinstance(p_type := get_proper_type(type), Instance)
and p_type.extra_attrs
):
self._put(key, type, from_assignment=True)
if current_value is not None or extract_var_from_literal_hash(key) is None:
# We definitely learned something new
Expand Down
24 changes: 19 additions & 5 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,9 +629,23 @@ def make_simplified_union(
else:
extra_attrs_set.add(instance.extra_attrs)

if extra_attrs_set is not None and len(extra_attrs_set) > 1:
# Code below is awkward, because we don't want the extra checks to affect
# performance in the common case.
erase_extra = False
if extra_attrs_set is not None:
fallback = try_getting_instance_fallback(result)
if fallback:
if fallback is None:
return result
if len(extra_attrs_set) > 1: # This case is too tricky to handle.
erase_extra = True
else:
# Check that all relevant items have the extra attributes.
for item in items:
instance = try_getting_instance_fallback(item)
if instance and instance.type == fallback.type and not instance.extra_attrs:
erase_extra = True
break
if erase_extra:
fallback.extra_attrs = None

return result
Expand Down Expand Up @@ -1220,16 +1234,16 @@ def try_getting_instance_fallback(typ: Type) -> Instance | None:
return typ
elif isinstance(typ, LiteralType):
return typ.fallback
elif isinstance(typ, NoneType):
elif isinstance(typ, (NoneType, AnyType)):
return None # Fast path for None, which is common
elif isinstance(typ, FunctionLike):
return typ.fallback
elif isinstance(typ, TypeVarType):
return try_getting_instance_fallback(typ.upper_bound)
elif isinstance(typ, TupleType):
return typ.partial_fallback
elif isinstance(typ, TypedDictType):
return typ.fallback
elif isinstance(typ, TypeVarType):
return try_getting_instance_fallback(typ.upper_bound)
return None


Expand Down
33 changes: 33 additions & 0 deletions test-data/unit/check-redefine2.test
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,39 @@ def f1() -> None:
reveal_type(x) # N: Revealed type is "builtins.str"
reveal_type(x) # N: Revealed type is "builtins.str | None"

[case testNewRedefineHasAttr]
# flags: --allow-redefinition-new --local-partial-types
from typing import Union

def test(lst: list[object]) -> None:
for cls in lst:
if not hasattr(cls, "module"):
break
reveal_type(cls.module) # N: Revealed type is "Any"

other = object()
if not hasattr(other, "foo"):
return
reveal_type(other.foo) # N: Revealed type is "Any"

x = object()
if bool():
assert hasattr(x, "bar")
x.bar # OK
x.bar # E: "object" has no attribute "bar"

class C:
x: int

u: Union[C, object]
if hasattr(u, "x"):
# Ideally we should have Any | int here and below, but this is tricky
reveal_type(u.x) # N: Revealed type is "Any"

y = hasattr(u, "x") and u.x
reveal_type(y) # N: Revealed type is "Literal[False] | Any"
[builtins fixtures/isinstancelist.pyi]

[case testNewRedefineGlobalVariableSimple]
# flags: --allow-redefinition-new --local-partial-types
if int():
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/isinstancelist.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Ellipsis = ellipsis()

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

class int:
def __add__(self, x: int) -> int: pass
Expand Down