diff --git a/mypy/checker.py b/mypy/checker.py index 073d33ee20a9..1eb9ab9e8626 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -167,6 +167,7 @@ true_only, try_expanding_sum_type_to_union, try_getting_int_literals_from_type, + try_getting_str_literals, try_getting_str_literals_from_type, tuple_fallback, ) @@ -4701,7 +4702,7 @@ def _make_fake_typeinfo_and_full_name( return None curr_module.names[full_name] = SymbolTableNode(GDEF, info) - return Instance(info, []) + return Instance(info, [], extra_attrs=instances[0].extra_attrs or instances[1].extra_attrs) def intersect_instance_callable(self, typ: Instance, callable_type: CallableType) -> Instance: """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 cur_module.names[gen_name] = SymbolTableNode(GDEF, info) - return Instance(info, []) + return Instance(info, [], extra_attrs=typ.extra_attrs) def make_fake_callable(self, typ: Instance) -> Instance: """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 if literal(expr) == LITERAL_TYPE: vartype = self.lookup_type(expr) return self.conditional_callable_type_map(expr, vartype) + elif refers_to_fullname(node.callee, "builtins.hasattr"): + if len(node.args) != 2: # the error will be reported elsewhere + return {}, {} + attr = try_getting_str_literals(node.args[1], self.lookup_type(node.args[1])) + if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1: + return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0]) elif isinstance(node.callee, RefExpr): if node.callee.type_guard is not None: # TODO: Follow keyword args or *args, **kwargs @@ -6239,6 +6246,95 @@ class Foo(Enum): and member_type.fallback.type == parent_type.type_object() ) + def add_any_attribute_to_type(self, typ: Type, name: str) -> Type: + """Inject an extra attribute with Any type using fallbacks.""" + orig_typ = typ + typ = get_proper_type(typ) + any_type = AnyType(TypeOfAny.unannotated) + if isinstance(typ, Instance): + result = typ.copy_with_extra_attr(name, any_type) + # For instances, we erase the possible module name, so that restrictions + # become anonymous types.ModuleType instances, allowing hasattr() to + # have effect on modules. + assert result.extra_attrs is not None + result.extra_attrs.mod_name = None + return result + if isinstance(typ, TupleType): + fallback = typ.partial_fallback.copy_with_extra_attr(name, any_type) + return typ.copy_modified(fallback=fallback) + if isinstance(typ, CallableType): + fallback = typ.fallback.copy_with_extra_attr(name, any_type) + return typ.copy_modified(fallback=fallback) + if isinstance(typ, TypeType) and isinstance(typ.item, Instance): + return TypeType.make_normalized(self.add_any_attribute_to_type(typ.item, name)) + if isinstance(typ, TypeVarType): + return typ.copy_modified( + upper_bound=self.add_any_attribute_to_type(typ.upper_bound, name), + values=[self.add_any_attribute_to_type(v, name) for v in typ.values], + ) + if isinstance(typ, UnionType): + with_attr, without_attr = self.partition_union_by_attr(typ, name) + return make_simplified_union( + with_attr + [self.add_any_attribute_to_type(typ, name) for typ in without_attr] + ) + return orig_typ + + def hasattr_type_maps( + self, expr: Expression, source_type: Type, name: str + ) -> tuple[TypeMap, TypeMap]: + """Simple support for hasattr() checks. + + Essentially the logic is following: + * In the if branch, keep types that already has a valid attribute as is, + for other inject an attribute with `Any` type. + * In the else branch, remove types that already have a valid attribute, + while keeping the rest. + """ + if self.has_valid_attribute(source_type, name): + return {expr: source_type}, {} + + source_type = get_proper_type(source_type) + if isinstance(source_type, UnionType): + _, without_attr = self.partition_union_by_attr(source_type, name) + yes_map = {expr: self.add_any_attribute_to_type(source_type, name)} + return yes_map, {expr: make_simplified_union(without_attr)} + + type_with_attr = self.add_any_attribute_to_type(source_type, name) + if type_with_attr != source_type: + return {expr: type_with_attr}, {} + return {}, {} + + def partition_union_by_attr( + self, source_type: UnionType, name: str + ) -> tuple[list[Type], list[Type]]: + with_attr = [] + without_attr = [] + for item in source_type.items: + if self.has_valid_attribute(item, name): + with_attr.append(item) + else: + without_attr.append(item) + return with_attr, without_attr + + def has_valid_attribute(self, typ: Type, name: str) -> bool: + if isinstance(get_proper_type(typ), AnyType): + return False + with self.msg.filter_errors() as watcher: + analyze_member_access( + name, + typ, + TempNode(AnyType(TypeOfAny.special_form)), + False, + False, + False, + self.msg, + original_type=typ, + chk=self, + # This is not a real attribute lookup so don't mess with deferring nodes. + no_deferral=True, + ) + return not watcher.has_new_errors() + class CollectArgTypes(TypeTraverserVisitor): """Collects the non-nested argument types in a set.""" diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 3c20ae872f71..fba6caec4072 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -380,6 +380,8 @@ def module_type(self, node: MypyFile) -> Instance: module_attrs = {} immutable = set() for name, n in node.names.items(): + if not n.module_public: + continue if isinstance(n.node, Var) and n.node.is_final: immutable.add(name) typ = self.chk.determine_type_of_member(n) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index ea2544442531..25f22df2cd45 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -90,6 +90,7 @@ def __init__( chk: mypy.checker.TypeChecker, self_type: Type | None, module_symbol_table: SymbolTable | None = None, + no_deferral: bool = False, ) -> None: self.is_lvalue = is_lvalue self.is_super = is_super @@ -100,6 +101,7 @@ def __init__( self.msg = msg self.chk = chk self.module_symbol_table = module_symbol_table + self.no_deferral = no_deferral def named_type(self, name: str) -> Instance: return self.chk.named_type(name) @@ -124,6 +126,7 @@ def copy_modified( self.chk, self.self_type, self.module_symbol_table, + self.no_deferral, ) if messages is not None: mx.msg = messages @@ -149,6 +152,7 @@ def analyze_member_access( in_literal_context: bool = False, self_type: Type | None = None, module_symbol_table: SymbolTable | None = None, + no_deferral: bool = False, ) -> Type: """Return the type of attribute 'name' of 'typ'. @@ -183,6 +187,7 @@ def analyze_member_access( chk=chk, self_type=self_type, module_symbol_table=module_symbol_table, + no_deferral=no_deferral, ) result = _analyze_member_access(name, typ, mx, override_info) possible_literal = get_proper_type(result) @@ -540,6 +545,11 @@ def analyze_member_var_access( return AnyType(TypeOfAny.special_form) # Could not find the member. + if itype.extra_attrs and name in itype.extra_attrs.attrs: + # For modules use direct symbol table lookup. + if not itype.extra_attrs.mod_name: + return itype.extra_attrs.attrs[name] + if mx.is_super: mx.msg.undefined_in_superclass(name, mx.context) return AnyType(TypeOfAny.from_error) @@ -744,7 +754,7 @@ def analyze_var( else: result = expanded_signature else: - if not var.is_ready: + if not var.is_ready and not mx.no_deferral: mx.not_ready_callback(var.name, mx.context) # Implicit 'Any' type. result = AnyType(TypeOfAny.special_form) @@ -858,6 +868,10 @@ def analyze_class_attribute_access( node = info.get(name) if not node: + if itype.extra_attrs and name in itype.extra_attrs.attrs: + # For modules use direct symbol table lookup. + if not itype.extra_attrs.mod_name: + return itype.extra_attrs.attrs[name] if info.fallback_to_any: return apply_class_attr_hook(mx, hook, AnyType(TypeOfAny.special_form)) return None diff --git a/mypy/meet.py b/mypy/meet.py index 1151b6ab460e..1da80741d70b 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -6,7 +6,13 @@ from mypy.erasetype import erase_type from mypy.maptype import map_instance_to_supertype from mypy.state import state -from mypy.subtypes import is_callable_compatible, is_equivalent, is_proper_subtype, is_subtype +from mypy.subtypes import ( + is_callable_compatible, + is_equivalent, + is_proper_subtype, + is_same_type, + is_subtype, +) from mypy.typeops import is_recursive_pair, make_simplified_union, tuple_fallback from mypy.types import ( AnyType, @@ -61,11 +67,25 @@ def meet_types(s: Type, t: Type) -> ProperType: """Return the greatest lower bound of two types.""" if is_recursive_pair(s, t): # This case can trigger an infinite recursion, general support for this will be - # tricky so we use a trivial meet (like for protocols). + # tricky, so we use a trivial meet (like for protocols). return trivial_meet(s, t) s = get_proper_type(s) t = get_proper_type(t) + if isinstance(s, Instance) and isinstance(t, Instance) and s.type == t.type: + # Code in checker.py should merge any extra_items where possible, so we + # should have only compatible extra_items here. We check this before + # the below subtype check, so that extra_attrs will not get erased. + if is_same_type(s, t) and (s.extra_attrs or t.extra_attrs): + if s.extra_attrs and t.extra_attrs: + if len(s.extra_attrs.attrs) > len(t.extra_attrs.attrs): + # Return the one that has more precise information. + return s + return t + if s.extra_attrs: + return s + return t + if not isinstance(s, UnboundType) and not isinstance(t, UnboundType): if is_proper_subtype(s, t, ignore_promotions=True): return s diff --git a/mypy/server/objgraph.py b/mypy/server/objgraph.py index ae5185092a13..89a086b8a0ab 100644 --- a/mypy/server/objgraph.py +++ b/mypy/server/objgraph.py @@ -64,11 +64,11 @@ def get_edges(o: object) -> Iterator[tuple[object, object]]: # in closures and self pointers to other objects if hasattr(e, "__closure__"): - yield (s, "__closure__"), e.__closure__ # type: ignore[union-attr] + yield (s, "__closure__"), e.__closure__ if hasattr(e, "__self__"): - se = e.__self__ # type: ignore[union-attr] + se = e.__self__ if se is not o and se is not type(o) and hasattr(s, "__self__"): - yield s.__self__, se # type: ignore[attr-defined] + yield s.__self__, se else: if not type(e) in TYPE_BLACKLIST: yield s, e diff --git a/mypy/typeops.py b/mypy/typeops.py index 8c49b6c870ed..3fc756ca4170 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -45,6 +45,7 @@ TupleType, Type, TypeAliasType, + TypedDictType, TypeOfAny, TypeQuery, TypeType, @@ -104,7 +105,7 @@ def tuple_fallback(typ: TupleType) -> Instance: raise NotImplementedError else: items.append(item) - return Instance(info, [join_type_list(items)]) + return Instance(info, [join_type_list(items)], extra_attrs=typ.partial_fallback.extra_attrs) def get_self_type(func: CallableType, default_self: Instance | TupleType) -> Type | None: @@ -462,7 +463,20 @@ def make_simplified_union( ): simplified_set = try_contracting_literals_in_union(simplified_set) - return get_proper_type(UnionType.make_union(simplified_set, line, column)) + result = get_proper_type(UnionType.make_union(simplified_set, line, column)) + + # Step 4: At last, we erase any (inconsistent) extra attributes on instances. + extra_attrs_set = set() + for item in items: + instance = try_getting_instance_fallback(item) + if instance and instance.extra_attrs: + extra_attrs_set.add(instance.extra_attrs) + + fallback = try_getting_instance_fallback(result) + if len(extra_attrs_set) > 1 and fallback: + fallback.extra_attrs = None + + return result 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 union_items.append(item) return literal_items, union_items + + +def try_getting_instance_fallback(typ: Type) -> Instance | None: + """Returns the Instance fallback for this type if one exists or None.""" + typ = get_proper_type(typ) + if isinstance(typ, Instance): + return typ + elif isinstance(typ, TupleType): + return typ.partial_fallback + elif isinstance(typ, TypedDictType): + return typ.fallback + elif isinstance(typ, FunctionLike): + return typ.fallback + elif isinstance(typ, LiteralType): + return typ.fallback + elif isinstance(typ, TypeVarType): + return try_getting_instance_fallback(typ.upper_bound) + return None diff --git a/mypy/types.py b/mypy/types.py index b1169234666f..fbbbb92a81d1 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -540,12 +540,12 @@ def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_type_var(self) def __hash__(self) -> int: - return hash(self.id) + return hash((self.id, self.upper_bound)) def __eq__(self, other: object) -> bool: if not isinstance(other, TypeVarType): return NotImplemented - return self.id == other.id + return self.id == other.id and self.upper_bound == other.upper_bound def serialize(self) -> JsonDict: assert not self.id.is_meta_var() @@ -1166,7 +1166,7 @@ class ExtraAttrs: """Summary of module attributes and types. This is used for instances of types.ModuleType, because they can have different - attributes per instance. + attributes per instance, and for type narrowing with hasattr() checks. """ def __init__( @@ -1189,36 +1189,18 @@ def __eq__(self, other: object) -> bool: return NotImplemented return self.attrs == other.attrs and self.immutable == other.immutable + def copy(self) -> ExtraAttrs: + return ExtraAttrs(self.attrs.copy(), self.immutable.copy(), self.mod_name) + class Instance(ProperType): """An instance type of form C[T1, ..., Tn]. The list of type variables may be empty. - Several types has fallbacks to `Instance`. Why? - Because, for example `TupleTuple` is related to `builtins.tuple` instance. - And `FunctionLike` has `builtins.function` fallback. - This allows us to use types defined - in typeshed for our "special" and more precise types. - - We used to have this helper function to get a fallback from different types. - Note, that it might be incomplete, since it is not used and not updated. - It just illustrates the concept: - - def try_getting_instance_fallback(typ: ProperType) -> Optional[Instance]: - '''Returns the Instance fallback for this type if one exists or None.''' - if isinstance(typ, Instance): - return typ - elif isinstance(typ, TupleType): - return tuple_fallback(typ) - elif isinstance(typ, TypedDictType): - return typ.fallback - elif isinstance(typ, FunctionLike): - return typ.fallback - elif isinstance(typ, LiteralType): - return typ.fallback - return None - + Several types have fallbacks to `Instance`, because in Python everything is an object + and this concept is impossible to express without intersection types. We therefore use + fallbacks for all "non-special" (like UninhabitedType, ErasedType etc) types. """ __slots__ = ("type", "args", "invalid", "type_ref", "last_known_value", "_hash", "extra_attrs") @@ -1231,6 +1213,7 @@ def __init__( column: int = -1, *, last_known_value: LiteralType | None = None, + extra_attrs: ExtraAttrs | None = None, ) -> None: super().__init__(line, column) self.type = typ @@ -1290,8 +1273,8 @@ def __init__( # Additional attributes defined per instance of this type. For example modules # have different attributes per instance of types.ModuleType. This is intended - # to be "short lived", we don't serialize it, and even don't store as variable type. - self.extra_attrs: ExtraAttrs | None = None + # to be "short-lived", we don't serialize it, and even don't store as variable type. + self.extra_attrs = extra_attrs def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_instance(self) @@ -1361,6 +1344,16 @@ def copy_modified( new.can_be_false = self.can_be_false return new + def copy_with_extra_attr(self, name: str, typ: Type) -> Instance: + if self.extra_attrs: + existing_attrs = self.extra_attrs.copy() + else: + existing_attrs = ExtraAttrs({}, set(), None) + existing_attrs.attrs[name] = typ + new = self.copy_modified() + new.extra_attrs = existing_attrs + return new + def has_readable_member(self, name: str) -> bool: return self.type.has_readable_member(name) @@ -1978,6 +1971,7 @@ def __hash__(self) -> int: tuple(self.arg_types), tuple(self.arg_names), tuple(self.arg_kinds), + self.fallback, ) ) @@ -1991,6 +1985,7 @@ def __eq__(self, other: object) -> bool: and self.name == other.name and self.is_type_obj() == other.is_type_obj() and self.is_ellipsis_args == other.is_ellipsis_args + and self.fallback == other.fallback ) else: return NotImplemented diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index cde12e2a0a75..59bf0c6a75d7 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -45,7 +45,16 @@ UnaryExpr, Var, ) -from mypy.types import Instance, TupleType, Type, UninhabitedType, get_proper_type +from mypy.types import ( + AnyType, + Instance, + ProperType, + TupleType, + Type, + TypeOfAny, + UninhabitedType, + get_proper_type, +) from mypy.util import split_target from mypy.visitor import ExpressionVisitor, StatementVisitor from mypyc.common import SELF_NAME, TEMP_ATTR_NAME @@ -867,7 +876,11 @@ def get_dict_item_type(self, expr: Expression) -> RType: def _analyze_iterable_item_type(self, expr: Expression) -> Type: """Return the item type given by 'expr' in an iterable context.""" # This logic is copied from mypy's TypeChecker.analyze_iterable_item_type. - iterable = get_proper_type(self.types[expr]) + if expr not in self.types: + # Mypy thinks this is unreachable. + iterable: ProperType = AnyType(TypeOfAny.from_error) + else: + iterable = get_proper_type(self.types[expr]) echk = self.graph[self.module_name].type_checker().expr_checker iterator = echk.check_method_call_by_name("__iter__", iterable, [], [], expr)[0] diff --git a/mypyc/test-data/run-generators.test b/mypyc/test-data/run-generators.test index db658eea6504..0f2cbe152fc0 100644 --- a/mypyc/test-data/run-generators.test +++ b/mypyc/test-data/run-generators.test @@ -650,3 +650,15 @@ from testutil import run_generator yields, val = run_generator(finally_yield()) assert yields == ('x',) assert val == 'test', val + +[case testUnreachableComprehensionNoCrash] +from typing import List + +def list_comp() -> List[int]: + if True: + return [5] + return [i for i in [5]] + +[file driver.py] +from native import list_comp +assert list_comp() == [5] diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 997b22e2eb28..c06802e69a69 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2729,3 +2729,182 @@ if type(x) is not C: reveal_type(x) # N: Revealed type is "__main__.D" else: reveal_type(x) # N: Revealed type is "__main__.C" + +[case testHasAttrExistingAttribute] +class C: + x: int +c: C +if hasattr(c, "x"): + reveal_type(c.x) # N: Revealed type is "builtins.int" +else: + # We don't mark this unreachable since people may check for deleted attributes + reveal_type(c.x) # N: Revealed type is "builtins.int" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeInstance] +class B: ... +b: B +if hasattr(b, "x"): + reveal_type(b.x) # N: Revealed type is "Any" +else: + b.x # E: "B" has no attribute "x" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeFunction] +def foo(x: int) -> None: ... +if hasattr(foo, "x"): + reveal_type(foo.x) # N: Revealed type is "Any" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeClassObject] +class C: ... +if hasattr(C, "x"): + reveal_type(C.x) # N: Revealed type is "Any" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeTypeType] +from typing import Type +class C: ... +c: Type[C] +if hasattr(c, "x"): + reveal_type(c.x) # N: Revealed type is "Any" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeTypeVar] +from typing import TypeVar + +T = TypeVar("T") +def foo(x: T) -> T: + if hasattr(x, "x"): + reveal_type(x.x) # N: Revealed type is "Any" + return x + else: + return x +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeChained] +class B: ... +b: B +if hasattr(b, "x"): + reveal_type(b.x) # N: Revealed type is "Any" +elif hasattr(b, "y"): + reveal_type(b.y) # N: Revealed type is "Any" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeNested] +class A: ... +class B: ... + +x: A +if hasattr(x, "x"): + if isinstance(x, B): + reveal_type(x.x) # N: Revealed type is "Any" + +if hasattr(x, "x") and hasattr(x, "y"): + reveal_type(x.x) # N: Revealed type is "Any" + reveal_type(x.y) # N: Revealed type is "Any" + +if hasattr(x, "x"): + if hasattr(x, "y"): + reveal_type(x.x) # N: Revealed type is "Any" + reveal_type(x.y) # N: Revealed type is "Any" + +if hasattr(x, "x") or hasattr(x, "y"): + x.x # E: "A" has no attribute "x" + x.y # E: "A" has no attribute "y" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrPreciseType] +class A: ... + +x: A +if hasattr(x, "a") and isinstance(x.a, int): + reveal_type(x.a) # N: Revealed type is "builtins.int" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeUnion] +from typing import Union + +class A: ... +class B: + x: int + +xu: Union[A, B] +if hasattr(xu, "x"): + reveal_type(xu) # N: Revealed type is "Union[__main__.A, __main__.B]" + reveal_type(xu.x) # N: Revealed type is "Union[Any, builtins.int]" +else: + reveal_type(xu) # N: Revealed type is "__main__.A" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeOuterUnion] +from typing import Union + +class A: ... +class B: ... +xu: Union[A, B] +if isinstance(xu, B): + if hasattr(xu, "x"): + reveal_type(xu.x) # N: Revealed type is "Any" + +if isinstance(xu, B) and hasattr(xu, "x"): + reveal_type(xu.x) # N: Revealed type is "Any" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrDoesntInterfereGetAttr] +class C: + def __getattr__(self, attr: str) -> str: ... + +c: C +if hasattr(c, "foo"): + reveal_type(c.foo) # N: Revealed type is "builtins.str" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrMissingAttributeLiteral] +from typing import Final +class B: ... +b: B +ATTR: Final = "x" +if hasattr(b, ATTR): + reveal_type(b.x) # N: Revealed type is "Any" +else: + b.x # E: "B" has no attribute "x" +[builtins fixtures/isinstance.pyi] + +[case testHasAttrDeferred] +def foo() -> str: ... + +class Test: + def stream(self) -> None: + if hasattr(self, "_body"): + reveal_type(self._body) # N: Revealed type is "builtins.str" + + def body(self) -> str: + if not hasattr(self, "_body"): + self._body = foo() + return self._body +[builtins fixtures/isinstance.pyi] + +[case testHasAttrModule] +import mod + +if hasattr(mod, "y"): + reveal_type(mod.y) # N: Revealed type is "Any" + reveal_type(mod.x) # N: Revealed type is "builtins.int" +else: + mod.y # E: Module has no attribute "y" + reveal_type(mod.x) # N: Revealed type is "builtins.int" + +[file mod.py] +x: int +[builtins fixtures/module.pyi] + +[case testHasAttrDoesntInterfereModuleGetAttr] +import mod + +if hasattr(mod, "y"): + reveal_type(mod.y) # N: Revealed type is "builtins.str" + +[file mod.py] +def __getattr__(attr: str) -> str: ... +[builtins fixtures/module.pyi] diff --git a/test-data/unit/fixtures/isinstance.pyi b/test-data/unit/fixtures/isinstance.pyi index 7f7cf501b5de..aa8bfce7fbe0 100644 --- a/test-data/unit/fixtures/isinstance.pyi +++ b/test-data/unit/fixtures/isinstance.pyi @@ -14,6 +14,7 @@ class function: pass def isinstance(x: object, t: Union[Type[object], Tuple[Type[object], ...]]) -> bool: pass def issubclass(x: object, t: Union[Type[object], Tuple[Type[object], ...]]) -> bool: pass +def hasattr(x: object, name: str) -> bool: pass class int: def __add__(self, other: 'int') -> 'int': pass diff --git a/test-data/unit/fixtures/module.pyi b/test-data/unit/fixtures/module.pyi index 98e989e59440..47408befd5ce 100644 --- a/test-data/unit/fixtures/module.pyi +++ b/test-data/unit/fixtures/module.pyi @@ -20,3 +20,4 @@ class ellipsis: pass classmethod = object() staticmethod = object() property = object() +def hasattr(x: object, name: str) -> bool: pass