diff --git a/mypy/binder.py b/mypy/binder.py index 8e02f7b0848f..adcc812badc9 100644 --- a/mypy/binder.py +++ b/mypy/binder.py @@ -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 diff --git a/mypy/typeops.py b/mypy/typeops.py index e3657c2fc4d5..839c6454ca28 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -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 @@ -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 diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index ee6386d6d5fc..b1a30784b99c 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -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(): diff --git a/test-data/unit/fixtures/isinstancelist.pyi b/test-data/unit/fixtures/isinstancelist.pyi index 2a43606f361a..54d6e9e38b49 100644 --- a/test-data/unit/fixtures/isinstancelist.pyi +++ b/test-data/unit/fixtures/isinstancelist.pyi @@ -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