From e82b6fdcb82e2b5936d8a22740829d2eed8aa035 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 25 Aug 2022 15:38:43 +0100 Subject: [PATCH 1/9] Allow modules as protocol implementations --- mypy/checkexpr.py | 4 ++ mypy/constraints.py | 2 +- mypy/messages.py | 17 +++---- mypy/server/astmerge.py | 2 + mypy/subtypes.py | 11 ++-- mypy/types.py | 13 ++++- test-data/unit/check-incremental.test | 35 +++++++++++++ test-data/unit/check-protocols.test | 73 +++++++++++++++++++++++++++ test-data/unit/fine-grained.test | 35 +++++++++++++ 9 files changed, 176 insertions(+), 16 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b0a4ec5644cc..479ae8a624e7 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -339,6 +339,10 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: # Fall back to a dummy 'object' type instead to # avoid a crash. result = self.named_type("builtins.object") + result.extra_attrs = { + name: n.type if n.type else AnyType(TypeOfAny.special_form) + for name, n in node.names.items() + } elif isinstance(node, Decorator): result = self.analyze_var_ref(node.var, e) elif isinstance(node, TypeAlias): diff --git a/mypy/constraints.py b/mypy/constraints.py index 9e28ce503b6c..fda0bff38392 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -757,7 +757,7 @@ def infer_constraints_from_protocol_members( # The above is safe since at this point we know that 'instance' is a subtype # of (erased) 'template', therefore it defines all protocol members res.extend(infer_constraints(temp, inst, self.direction)) - if mypy.subtypes.IS_SETTABLE in mypy.subtypes.get_member_flags(member, protocol.type): + if mypy.subtypes.IS_SETTABLE in mypy.subtypes.get_member_flags(member, protocol): # Settable members are invariant, add opposite constraints res.extend(infer_constraints(temp, inst, neg_op(self.direction))) return res diff --git a/mypy/messages.py b/mypy/messages.py index 29fd1503e595..f9eb95bd9059 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1822,6 +1822,7 @@ def report_protocol_problems( return class_obj = False + is_module = isinstance(subtype, Instance) and subtype.extra_attrs if isinstance(subtype, TupleType): if not isinstance(subtype.partial_fallback, Instance): return @@ -1902,7 +1903,7 @@ def report_protocol_problems( self.note("Expected:", context, offset=OFFSET, code=code) if isinstance(exp, CallableType): self.note( - pretty_callable(exp, skip_self=class_obj), + pretty_callable(exp, skip_self=class_obj or is_module), context, offset=2 * OFFSET, code=code, @@ -1910,12 +1911,12 @@ def report_protocol_problems( else: assert isinstance(exp, Overloaded) self.pretty_overload( - exp, context, 2 * OFFSET, code=code, skip_self=class_obj + exp, context, 2 * OFFSET, code=code, skip_self=class_obj or is_module ) self.note("Got:", context, offset=OFFSET, code=code) if isinstance(got, CallableType): self.note( - pretty_callable(got, skip_self=class_obj), + pretty_callable(got, skip_self=class_obj or is_module), context, offset=2 * OFFSET, code=code, @@ -1923,7 +1924,7 @@ def report_protocol_problems( else: assert isinstance(got, Overloaded) self.pretty_overload( - got, context, 2 * OFFSET, code=code, skip_self=class_obj + got, context, 2 * OFFSET, code=code, skip_self=class_obj or is_module ) self.print_more(conflict_types, context, OFFSET, MAX_ITEMS, code=code) @@ -2564,7 +2565,7 @@ def get_conflict_protocol_types( if not subtype: continue is_compat = is_subtype(subtype, supertype, ignore_pos_arg_names=True) - if IS_SETTABLE in get_member_flags(member, right.type): + if IS_SETTABLE in get_member_flags(member, right): is_compat = is_compat and is_subtype(supertype, subtype) if not is_compat: conflicts.append((member, subtype, supertype)) @@ -2581,11 +2582,7 @@ def get_bad_protocol_flags( all_flags: list[tuple[str, set[int], set[int]]] = [] for member in right.type.protocol_members: if find_member(member, left, left): - item = ( - member, - get_member_flags(member, left.type), - get_member_flags(member, right.type), - ) + item = (member, get_member_flags(member, left), get_member_flags(member, right)) all_flags.append(item) bad_flags = [] for name, subflags, superflags in all_flags: diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index 7a6b247c84f8..6a121877951a 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -418,6 +418,8 @@ def visit_instance(self, typ: Instance) -> None: arg.accept(self) if typ.last_known_value: typ.last_known_value.accept(self) + if typ.extra_attrs: + typ.extra_attrs.accept(self) def visit_type_alias_type(self, typ: TypeAliasType) -> None: assert typ.alias is not None diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 3aefa315db9e..6eadf76aaf57 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1010,8 +1010,8 @@ def named_type(fullname: str) -> Instance: if isinstance(subtype, NoneType) and isinstance(supertype, CallableType): # We want __hash__ = None idiom to work even without --strict-optional return False - subflags = get_member_flags(member, left.type, class_obj=class_obj) - superflags = get_member_flags(member, right.type) + subflags = get_member_flags(member, left, class_obj=class_obj) + superflags = get_member_flags(member, right) if IS_SETTABLE in superflags: # Check opposite direction for settable attributes. if not is_subtype(supertype, subtype): @@ -1095,10 +1095,12 @@ def find_member( # PEP 544 doesn't specify anything about such use cases. So we just try # to do something meaningful (at least we should not crash). return TypeType(fill_typevars_with_any(v)) + if name in itype.extra_attrs: + return itype.extra_attrs[name] return None -def get_member_flags(name: str, info: TypeInfo, class_obj: bool = False) -> set[int]: +def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set[int]: """Detect whether a member 'name' is settable, whether it is an instance or class variable, and whether it is class or static method. @@ -1109,6 +1111,7 @@ def get_member_flags(name: str, info: TypeInfo, class_obj: bool = False) -> set[ * IS_CLASS_OR_STATIC: set for methods decorated with @classmethod or with @staticmethod. """ + info = itype.type method = info.get_method(name) setattr_meth = info.get_method("__setattr__") if method: @@ -1126,6 +1129,8 @@ def get_member_flags(name: str, info: TypeInfo, class_obj: bool = False) -> set[ if not node: if setattr_meth: return {IS_SETTABLE} + if name in itype.extra_attrs: + return {IS_SETTABLE} return set() v = node.node # just a variable diff --git a/mypy/types.py b/mypy/types.py index 7fc933ce38ba..c2b979b0b857 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1186,7 +1186,7 @@ def try_getting_instance_fallback(typ: ProperType) -> Optional[Instance]: """ - __slots__ = ("type", "args", "invalid", "type_ref", "last_known_value", "_hash") + __slots__ = ("type", "args", "invalid", "type_ref", "last_known_value", "_hash", "extra_attrs") def __init__( self, @@ -1253,12 +1253,19 @@ def __init__( # Cached hash value self._hash = -1 + # 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: dict[str, Type] = {} + def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_instance(self) def __hash__(self) -> int: if self._hash == -1: - self._hash = hash((self.type, self.args, self.last_known_value)) + self._hash = hash( + (self.type, self.args, self.last_known_value, tuple(self.extra_attrs.items())) + ) return self._hash def __eq__(self, other: object) -> bool: @@ -1268,6 +1275,7 @@ def __eq__(self, other: object) -> bool: self.type == other.type and self.args == other.args and self.last_known_value == other.last_known_value + and self.extra_attrs == other.extra_attrs ) def serialize(self) -> JsonDict | str: @@ -1315,6 +1323,7 @@ def copy_modified( if last_known_value is not _dummy else self.last_known_value, ) + # We intentionally don't copy the extra_attrs here. new.can_be_true = self.can_be_true new.can_be_false = self.can_be_false return new diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 599b00dabe3d..101be962bf03 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6012,3 +6012,38 @@ foo(name='Jennifer', age="38") [out] [out2] tmp/m.py:2: error: Argument "age" to "foo" has incompatible type "str"; expected "int" + +[case testModuleAsProtocolImplementationSerialize] +import m +[file m.py] +from typing import Protocol +from lib import C + +class Options(Protocol): + timeout: int + def update(self) -> bool: ... + +def setup(options: Options) -> None: ... +setup(C.config) + +[file lib.py] +import default_config + +class C: + config = default_config + +[file default_config.py] +timeout = 100 +def update() -> bool: ... + +[file default_config.py.2] +timeout = 100 +def update() -> str: ... +[out] +[out2] +tmp/m.py:9: error: Argument 1 to "setup" has incompatible type "object"; expected "Options" +tmp/m.py:9: note: Following member(s) of "object" have conflicts: +tmp/m.py:9: note: Expected: +tmp/m.py:9: note: def update() -> bool +tmp/m.py:9: note: Got: +tmp/m.py:9: note: def update() -> str diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 90276ebae972..59f652d023c7 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -3517,3 +3517,76 @@ test(c) # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" # N: def [T] foo(arg: T) -> T \ # N: Got: \ # N: def [T] foo(self: T) -> Union[T, int] + +[case testModuleAsProtocolImplementation] +import default_config +import bad_config_1 +import bad_config_2 +import bad_config_3 +from typing import Protocol + +class Options(Protocol): + timeout: int + one_flag: bool + other_flag: bool + def update(self) -> bool: ... + +def setup(options: Options) -> None: ... +setup(default_config) # OK +setup(bad_config_1) # E: Argument 1 to "setup" has incompatible type Module; expected "Options" \ + # N: "ModuleType" is missing following "Options" protocol member: \ + # N: timeout +setup(bad_config_2) # E: Argument 1 to "setup" has incompatible type Module; expected "Options" \ + # N: Following member(s) of Module have conflicts: \ + # N: one_flag: expected "bool", got "int" +setup(bad_config_3) # E: Argument 1 to "setup" has incompatible type Module; expected "Options" \ + # N: Following member(s) of Module have conflicts: \ + # N: Expected: \ + # N: def update() -> bool \ + # N: Got: \ + # N: def update(obj: Any) -> bool + +[file default_config.py] +timeout = 100 +one_flag = True +other_flag = False +def update() -> bool: ... + +[file bad_config_1.py] +one_flag = True +other_flag = False +def update() -> bool: ... + +[file bad_config_2.py] +timeout = 100 +one_flag = 42 +other_flag = False +def update() -> bool: ... + +[file bad_config_3.py] +timeout = 100 +one_flag = True +other_flag = False +def update(obj) -> bool: ... +[builtins fixtures/module.pyi] + +[case testModuleAsProtocolImplementationInference] +import default_config +from typing import Protocol, TypeVar + +T = TypeVar("T", covariant=True) +class Options(Protocol[T]): + timeout: int + one_flag: bool + other_flag: bool + def update(self) -> T: ... + +def setup(options: Options[T]) -> T: ... +reveal_type(setup(default_config)) # N: Revealed type is "builtins.str" + +[file default_config.py] +timeout = 100 +one_flag = True +other_flag = False +def update() -> str: ... +[builtins fixtures/module.pyi] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 3a054e8fcfe5..40cafb12708d 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -9933,3 +9933,38 @@ foo(name='Jennifer', age=38) [out] == m.py:2: error: Argument "age" to "foo" has incompatible type "int"; expected "str" + +[case testModuleAsProtocolImplementationFine] +import m +[file m.py] +from typing import Protocol +from lib import C + +class Options(Protocol): + timeout: int + def update(self) -> bool: ... + +def setup(options: Options) -> None: ... +setup(C.config) + +[file lib.py] +import default_config + +class C: + config = default_config + +[file default_config.py] +timeout = 100 +def update() -> bool: ... + +[file default_config.py.2] +timeout = 100 +def update() -> str: ... +[out] +== +tmp/m.py:9: error: Argument 1 to "setup" has incompatible type "object"; expected "Options" +tmp/m.py:9: note: Following member(s) of "object" have conflicts: +tmp/m.py:9: note: Expected: +tmp/m.py:9: note: def update() -> bool +tmp/m.py:9: note: Got: +tmp/m.py:9: note: def update() -> str From 978fa3264fb3f07557dad30f9dec275e413cc41e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 25 Aug 2022 17:10:20 +0100 Subject: [PATCH 2/9] Fix fine/incremental; better diagnostics --- mypy/checkexpr.py | 27 ++++++++++++++++----------- mypy/checkmember.py | 3 +++ mypy/messages.py | 5 ++++- mypy/server/deps.py | 2 ++ mypy/subtypes.py | 4 +++- mypy/types.py | 2 +- test-data/unit/check-incremental.test | 7 ++++--- test-data/unit/check-protocols.test | 10 +++++----- test-data/unit/fine-grained.test | 15 ++++++++------- 9 files changed, 46 insertions(+), 29 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 479ae8a624e7..7783b03b4157 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -332,17 +332,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: result = erasetype.erase_typevars(result) elif isinstance(node, MypyFile): # Reference to a module object. - try: - result = self.named_type("types.ModuleType") - except KeyError: - # In test cases might 'types' may not be available. - # Fall back to a dummy 'object' type instead to - # avoid a crash. - result = self.named_type("builtins.object") - result.extra_attrs = { - name: n.type if n.type else AnyType(TypeOfAny.special_form) - for name, n in node.names.items() - } + result = self.module_type(node) elif isinstance(node, Decorator): result = self.analyze_var_ref(node.var, e) elif isinstance(node, TypeAlias): @@ -378,6 +368,21 @@ def analyze_var_ref(self, var: Var, context: Context) -> Type: # Implicit 'Any' type. return AnyType(TypeOfAny.special_form) + def module_type(self, node: MypyFile) -> Instance: + try: + result = self.named_type("types.ModuleType") + except KeyError: + # In test cases might 'types' may not be available. + # Fall back to a dummy 'object' type instead to + # avoid a crash. + result = self.named_type("builtins.object") + result.extra_attrs = { + name: n.type if n.type else AnyType(TypeOfAny.special_form) + for name, n in node.names.items() + } + result.extra_attrs["@module"] = node.fullname + return result + def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: """Type check a call expression.""" if e.analyzed: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 3be961ee9fdc..f6b962e6fa1c 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -479,6 +479,9 @@ def analyze_member_var_access( return analyze_var(name, v, itype, info, mx, implicit=implicit) elif isinstance(v, FuncDef): assert False, "Did not expect a function" + elif isinstance(v, MypyFile): + mx.chk.module_refs.add(v.fullname) + return mx.chk.expr_checker.module_type(v) elif ( not v and name not in ["__getattr__", "__setattr__", "__getattribute__"] diff --git a/mypy/messages.py b/mypy/messages.py index f9eb95bd9059..bd62df9b8993 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2188,7 +2188,10 @@ def format_literal_value(typ: LiteralType) -> str: # Get the short name of the type. if itype.type.fullname in ("types.ModuleType", "_importlib_modulespec.ModuleType"): # Make some common error messages simpler and tidier. - return "Module" + base_str = "Module" + if "@module" in itype.extra_attrs: + return f"{base_str} {itype.extra_attrs['@module']}" + return base_str if verbosity >= 2 or (fullnames and itype.type.fullname in fullnames): base_str = itype.type.fullname else: diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 121386c4c73d..348760478bd8 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -969,6 +969,8 @@ def visit_instance(self, typ: Instance) -> list[str]: triggers.extend(self.get_type_triggers(arg)) if typ.last_known_value: triggers.extend(self.get_type_triggers(typ.last_known_value)) + if "@module" in typ.extra_attrs: + triggers.append(make_wildcard_trigger(typ.extra_attrs["@module"])) return triggers def visit_type_alias_type(self, typ: TypeAliasType) -> list[str]: diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 6eadf76aaf57..4841158fc17e 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1096,7 +1096,9 @@ def find_member( # to do something meaningful (at least we should not crash). return TypeType(fill_typevars_with_any(v)) if name in itype.extra_attrs: - return itype.extra_attrs[name] + typ = itype.extra_attrs[name] + assert isinstance(typ, Type) + return typ return None diff --git a/mypy/types.py b/mypy/types.py index c2b979b0b857..8623285d9ba5 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1256,7 +1256,7 @@ 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: dict[str, Type] = {} + self.extra_attrs: dict[str, Type | str] = {} def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_instance(self) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 101be962bf03..9e43075911b8 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6024,7 +6024,7 @@ class Options(Protocol): def update(self) -> bool: ... def setup(options: Options) -> None: ... -setup(C.config) +setup(C().config) [file lib.py] import default_config @@ -6039,10 +6039,11 @@ def update() -> bool: ... [file default_config.py.2] timeout = 100 def update() -> str: ... +[builtins fixtures/module.pyi] [out] [out2] -tmp/m.py:9: error: Argument 1 to "setup" has incompatible type "object"; expected "Options" -tmp/m.py:9: note: Following member(s) of "object" have conflicts: +tmp/m.py:9: error: Argument 1 to "setup" has incompatible type "Module default_config"; expected "Options" +tmp/m.py:9: note: Following member(s) of "Module default_config" have conflicts: tmp/m.py:9: note: Expected: tmp/m.py:9: note: def update() -> bool tmp/m.py:9: note: Got: diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 59f652d023c7..99d067defc46 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -3533,14 +3533,14 @@ class Options(Protocol): def setup(options: Options) -> None: ... setup(default_config) # OK -setup(bad_config_1) # E: Argument 1 to "setup" has incompatible type Module; expected "Options" \ +setup(bad_config_1) # E: Argument 1 to "setup" has incompatible type "Module bad_config_1"; expected "Options" \ # N: "ModuleType" is missing following "Options" protocol member: \ # N: timeout -setup(bad_config_2) # E: Argument 1 to "setup" has incompatible type Module; expected "Options" \ - # N: Following member(s) of Module have conflicts: \ +setup(bad_config_2) # E: Argument 1 to "setup" has incompatible type "Module bad_config_2"; expected "Options" \ + # N: Following member(s) of "Module bad_config_2" have conflicts: \ # N: one_flag: expected "bool", got "int" -setup(bad_config_3) # E: Argument 1 to "setup" has incompatible type Module; expected "Options" \ - # N: Following member(s) of Module have conflicts: \ +setup(bad_config_3) # E: Argument 1 to "setup" has incompatible type "Module bad_config_3"; expected "Options" \ + # N: Following member(s) of "Module bad_config_3" have conflicts: \ # N: Expected: \ # N: def update() -> bool \ # N: Got: \ diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 40cafb12708d..399ba506df9f 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -9945,7 +9945,7 @@ class Options(Protocol): def update(self) -> bool: ... def setup(options: Options) -> None: ... -setup(C.config) +setup(C().config) [file lib.py] import default_config @@ -9960,11 +9960,12 @@ def update() -> bool: ... [file default_config.py.2] timeout = 100 def update() -> str: ... +[builtins fixtures/module.pyi] [out] == -tmp/m.py:9: error: Argument 1 to "setup" has incompatible type "object"; expected "Options" -tmp/m.py:9: note: Following member(s) of "object" have conflicts: -tmp/m.py:9: note: Expected: -tmp/m.py:9: note: def update() -> bool -tmp/m.py:9: note: Got: -tmp/m.py:9: note: def update() -> str +m.py:9: error: Argument 1 to "setup" has incompatible type "Module default_config"; expected "Options" +m.py:9: note: Following member(s) of "Module default_config" have conflicts: +m.py:9: note: Expected: +m.py:9: note: def update() -> bool +m.py:9: note: Got: +m.py:9: note: def update() -> str From 3ce4a7370851681d98af6ceb9acb10ffc80073af Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 25 Aug 2022 17:17:28 +0100 Subject: [PATCH 3/9] Better module detection in messages --- mypy/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/messages.py b/mypy/messages.py index bd62df9b8993..8b784060bdbb 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1822,7 +1822,7 @@ def report_protocol_problems( return class_obj = False - is_module = isinstance(subtype, Instance) and subtype.extra_attrs + is_module = isinstance(subtype, Instance) and "@module" in subtype.extra_attrs if isinstance(subtype, TupleType): if not isinstance(subtype.partial_fallback, Instance): return From 16fa7dc2e1662e88b45195b44128497ec6957877 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 25 Aug 2022 17:45:55 +0100 Subject: [PATCH 4/9] Fix self-check --- misc/proper_plugin.py | 6 ++---- mypy/server/astmerge.py | 2 -- mypy/server/deps.py | 5 ++++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/misc/proper_plugin.py b/misc/proper_plugin.py index ed5fad36121e..4db3542705cd 100644 --- a/misc/proper_plugin.py +++ b/misc/proper_plugin.py @@ -50,10 +50,8 @@ def isinstance_proper_hook(ctx: FunctionContext) -> Type: right = get_proper_type(ctx.arg_types[1][0]) for arg in ctx.arg_types[0]: if ( - is_improper_type(arg) - or isinstance(get_proper_type(arg), AnyType) - and is_dangerous_target(right) - ): + is_improper_type(arg) or isinstance(get_proper_type(arg), AnyType) + ) and is_dangerous_target(right): if is_special_target(right): return ctx.default_return_type ctx.api.fail( diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index 6a121877951a..7a6b247c84f8 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -418,8 +418,6 @@ def visit_instance(self, typ: Instance) -> None: arg.accept(self) if typ.last_known_value: typ.last_known_value.accept(self) - if typ.extra_attrs: - typ.extra_attrs.accept(self) def visit_type_alias_type(self, typ: TypeAliasType) -> None: assert typ.alias is not None diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 348760478bd8..a0a23c046be9 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -970,7 +970,10 @@ def visit_instance(self, typ: Instance) -> list[str]: if typ.last_known_value: triggers.extend(self.get_type_triggers(typ.last_known_value)) if "@module" in typ.extra_attrs: - triggers.append(make_wildcard_trigger(typ.extra_attrs["@module"])) + mod_name = typ.extra_attrs["@module"] + assert isinstance(mod_name, str) + # Module as type effectively depends on all module attributes, use wildcard. + triggers.append(make_wildcard_trigger(mod_name)) return triggers def visit_type_alias_type(self, typ: TypeAliasType) -> list[str]: From 2e7ac2ed12711208f295c0a71d96d06edd832905 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 25 Aug 2022 18:03:46 +0100 Subject: [PATCH 5/9] Show module names only for protocols --- mypy/messages.py | 21 ++++++++++----------- test-data/unit/check-incremental.test | 2 +- test-data/unit/check-protocols.test | 6 +++--- test-data/unit/fine-grained.test | 2 +- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 8b784060bdbb..6681eed56b5c 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1882,11 +1882,8 @@ def report_protocol_problems( or not subtype.type.defn.type_vars or not supertype.type.defn.type_vars ): - self.note( - f"Following member(s) of {format_type(subtype)} have conflicts:", - context, - code=code, - ) + type_name = format_type(subtype, module_names=True) + self.note(f"Following member(s) of {type_name} have conflicts:", context, code=code) for name, got, exp in conflict_types[:MAX_ITEMS]: exp = get_proper_type(exp) got = get_proper_type(got) @@ -2148,7 +2145,9 @@ def format_callable_args( return ", ".join(arg_strings) -def format_type_inner(typ: Type, verbosity: int, fullnames: set[str] | None) -> str: +def format_type_inner( + typ: Type, verbosity: int, fullnames: set[str] | None, module_names: bool = False +) -> str: """ Convert a type to a relatively short string suitable for error messages. @@ -2189,7 +2188,7 @@ def format_literal_value(typ: LiteralType) -> str: if itype.type.fullname in ("types.ModuleType", "_importlib_modulespec.ModuleType"): # Make some common error messages simpler and tidier. base_str = "Module" - if "@module" in itype.extra_attrs: + if "@module" in itype.extra_attrs and module_names: return f"{base_str} {itype.extra_attrs['@module']}" return base_str if verbosity >= 2 or (fullnames and itype.type.fullname in fullnames): @@ -2365,7 +2364,7 @@ def find_type_overlaps(*types: Type) -> set[str]: return overlaps -def format_type(typ: Type, verbosity: int = 0) -> str: +def format_type(typ: Type, verbosity: int = 0, module_names: bool = False) -> str: """ Convert a type to a relatively short string suitable for error messages. @@ -2376,10 +2375,10 @@ def format_type(typ: Type, verbosity: int = 0) -> str: modification of the formatted string is required, callers should use format_type_bare. """ - return quote_type_string(format_type_bare(typ, verbosity)) + return quote_type_string(format_type_bare(typ, verbosity, module_names)) -def format_type_bare(typ: Type, verbosity: int = 0) -> str: +def format_type_bare(typ: Type, verbosity: int = 0, module_names: bool = False) -> str: """ Convert a type to a relatively short string suitable for error messages. @@ -2391,7 +2390,7 @@ def format_type_bare(typ: Type, verbosity: int = 0) -> str: instead. (The caller may want to use quote_type_string after processing has happened, to maintain consistent quoting in messages.) """ - return format_type_inner(typ, verbosity, find_type_overlaps(typ)) + return format_type_inner(typ, verbosity, find_type_overlaps(typ), module_names) def format_type_distinctly(*types: Type, bare: bool = False) -> tuple[str, ...]: diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 9e43075911b8..bf556543cc4f 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6042,7 +6042,7 @@ def update() -> str: ... [builtins fixtures/module.pyi] [out] [out2] -tmp/m.py:9: error: Argument 1 to "setup" has incompatible type "Module default_config"; expected "Options" +tmp/m.py:9: error: Argument 1 to "setup" has incompatible type Module; expected "Options" tmp/m.py:9: note: Following member(s) of "Module default_config" have conflicts: tmp/m.py:9: note: Expected: tmp/m.py:9: note: def update() -> bool diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 6f0772c78777..b47b2aa5fa1c 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -3562,13 +3562,13 @@ class Options(Protocol): def setup(options: Options) -> None: ... setup(default_config) # OK -setup(bad_config_1) # E: Argument 1 to "setup" has incompatible type "Module bad_config_1"; expected "Options" \ +setup(bad_config_1) # E: Argument 1 to "setup" has incompatible type Module; expected "Options" \ # N: "ModuleType" is missing following "Options" protocol member: \ # N: timeout -setup(bad_config_2) # E: Argument 1 to "setup" has incompatible type "Module bad_config_2"; expected "Options" \ +setup(bad_config_2) # E: Argument 1 to "setup" has incompatible type Module; expected "Options" \ # N: Following member(s) of "Module bad_config_2" have conflicts: \ # N: one_flag: expected "bool", got "int" -setup(bad_config_3) # E: Argument 1 to "setup" has incompatible type "Module bad_config_3"; expected "Options" \ +setup(bad_config_3) # E: Argument 1 to "setup" has incompatible type Module; expected "Options" \ # N: Following member(s) of "Module bad_config_3" have conflicts: \ # N: Expected: \ # N: def update() -> bool \ diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 399ba506df9f..0e443abc7237 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -9963,7 +9963,7 @@ def update() -> str: ... [builtins fixtures/module.pyi] [out] == -m.py:9: error: Argument 1 to "setup" has incompatible type "Module default_config"; expected "Options" +m.py:9: error: Argument 1 to "setup" has incompatible type Module; expected "Options" m.py:9: note: Following member(s) of "Module default_config" have conflicts: m.py:9: note: Expected: m.py:9: note: def update() -> bool From c9251a82a3a34c570ce855ae7c2fd6f543680c7f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 25 Aug 2022 18:52:48 +0100 Subject: [PATCH 6/9] Also allow classes and type aliases --- mypy/checker.py | 18 +++++--- mypy/checkexpr.py | 14 ++++-- test-data/unit/check-protocols.test | 66 +++++++++++++++++++++++++++++ test-data/unit/fixtures/module.pyi | 1 + 4 files changed, 90 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 3cd55cc25efd..1b51e87ebe3b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2293,18 +2293,26 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None: if name in base2.names and base2 not in base.mro: self.check_compatibility(name, base, base2, typ) - def determine_type_of_class_member(self, sym: SymbolTableNode) -> Type | None: + def determine_type_of_member(self, sym: SymbolTableNode) -> Type | None: if sym.type is not None: return sym.type if isinstance(sym.node, FuncBase): return self.function_type(sym.node) if isinstance(sym.node, TypeInfo): - # nested class - return type_object_type(sym.node, self.named_type) + if sym.node.typeddict_type: + # We special-case TypedDict, because they don't define any constructor. + return self.expr_checker.typeddict_callable(sym.node) + else: + return type_object_type(sym.node, self.named_type) if isinstance(sym.node, TypeVarExpr): # Use of TypeVars is rejected in an expression/runtime context, so # we don't need to check supertype compatibility for them. return AnyType(TypeOfAny.special_form) + if isinstance(sym.node, TypeAlias): + return self.expr_checker.alias_type_in_runtime_context( + sym.node, sym.node.no_args, sym.node + ) + # TODO: handle more node kinds here. return None def check_compatibility( @@ -2335,8 +2343,8 @@ class C(B, A[int]): ... # this is unsafe because... return first = base1.names[name] second = base2.names[name] - first_type = get_proper_type(self.determine_type_of_class_member(first)) - second_type = get_proper_type(self.determine_type_of_class_member(second)) + first_type = get_proper_type(self.determine_type_of_member(first)) + second_type = get_proper_type(self.determine_type_of_member(second)) if isinstance(first_type, FunctionLike) and isinstance(second_type, FunctionLike): if first_type.is_type_obj() and second_type.is_type_obj(): diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 7783b03b4157..1dbe82fbb78b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -376,10 +376,16 @@ def module_type(self, node: MypyFile) -> Instance: # Fall back to a dummy 'object' type instead to # avoid a crash. result = self.named_type("builtins.object") - result.extra_attrs = { - name: n.type if n.type else AnyType(TypeOfAny.special_form) - for name, n in node.names.items() - } + module_attrs: dict[str, Type | str] = {} + for name, n in node.names.items(): + typ = self.chk.determine_type_of_member(n) + if typ: + module_attrs[name] = typ + else: + # TODO: what to do about nested module references? + # They are non-trivial because there may be import cycles. + module_attrs[name] = AnyType(TypeOfAny.special_form) + result.extra_attrs = module_attrs result.extra_attrs["@module"] = node.fullname return result diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index b47b2aa5fa1c..5564a2dac47b 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -3619,3 +3619,69 @@ one_flag = True other_flag = False def update() -> str: ... [builtins fixtures/module.pyi] + +[case testModuleAsProtocolImplementationClassObject] +import runner +import bad_runner +from typing import Callable, Protocol + +class Runner(Protocol): + @property + def Run(self) -> Callable[[int], Result]: ... + +class Result(Protocol): + value: int + +def run(x: Runner) -> None: ... +run(runner) # OK +run(bad_runner) # E: Argument 1 to "run" has incompatible type Module; expected "Runner" \ + # N: Following member(s) of "Module bad_runner" have conflicts: \ + # N: Expected: \ + # N: def (int, /) -> Result \ + # N: Got: \ + # N: def __init__(arg: str) -> Run + +[file runner.py] +class Run: + value: int + def __init__(self, arg: int) -> None: ... + +[file bad_runner.py] +class Run: + value: int + def __init__(self, arg: str) -> None: ... +[builtins fixtures/module.pyi] + +[case testModuleAsProtocolImplementationTypeAlias] +import runner +import bad_runner +from typing import Callable, Protocol + +class Runner(Protocol): + @property + def run(self) -> Callable[[int], Result]: ... + +class Result(Protocol): + value: int + +def run(x: Runner) -> None: ... +run(runner) # OK +run(bad_runner) # E: Argument 1 to "run" has incompatible type Module; expected "Runner" \ + # N: Following member(s) of "Module bad_runner" have conflicts: \ + # N: Expected: \ + # N: def (int, /) -> Result \ + # N: Got: \ + # N: def __init__(arg: str) -> Run + +[file runner.py] +class Run: + value: int + def __init__(self, arg: int) -> None: ... +run = Run + +[file bad_runner.py] +class Run: + value: int + def __init__(self, arg: str) -> None: ... +run = Run +[builtins fixtures/module.pyi] diff --git a/test-data/unit/fixtures/module.pyi b/test-data/unit/fixtures/module.pyi index ac1d3688ed12..98e989e59440 100644 --- a/test-data/unit/fixtures/module.pyi +++ b/test-data/unit/fixtures/module.pyi @@ -19,3 +19,4 @@ class ellipsis: pass classmethod = object() staticmethod = object() +property = object() From 6ef1db2aa869f978828d3b10d31362728b4f8b6d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 25 Aug 2022 20:00:19 +0100 Subject: [PATCH 7/9] Try fixing mypy_primer --- mypy/checker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1b51e87ebe3b..6b52192d11d0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2309,9 +2309,12 @@ def determine_type_of_member(self, sym: SymbolTableNode) -> Type | None: # we don't need to check supertype compatibility for them. return AnyType(TypeOfAny.special_form) if isinstance(sym.node, TypeAlias): - return self.expr_checker.alias_type_in_runtime_context( - sym.node, sym.node.no_args, sym.node - ) + with self.chk.msg.filter_errors(): + # Suppress any errors, they will be given when analyzing the corresponding node. + # Here we may have incorrect options and location context. + return self.expr_checker.alias_type_in_runtime_context( + sym.node, sym.node.no_args, sym.node + ) # TODO: handle more node kinds here. return None From 4b2566adad08a89c2a985a9f06124373350f88d0 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 25 Aug 2022 20:02:35 +0100 Subject: [PATCH 8/9] Typo --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6b52192d11d0..513d2ef6fffa 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2309,7 +2309,7 @@ def determine_type_of_member(self, sym: SymbolTableNode) -> Type | None: # we don't need to check supertype compatibility for them. return AnyType(TypeOfAny.special_form) if isinstance(sym.node, TypeAlias): - with self.chk.msg.filter_errors(): + with self.msg.filter_errors(): # Suppress any errors, they will be given when analyzing the corresponding node. # Here we may have incorrect options and location context. return self.expr_checker.alias_type_in_runtime_context( From fb108beadf8f49bca6b62f6b671913ad6b41c9dd Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 26 Aug 2022 16:29:16 +0100 Subject: [PATCH 9/9] CR; also fix Final for instances --- mypy/checkexpr.py | 9 ++++-- mypy/messages.py | 8 +++-- mypy/server/deps.py | 6 ++-- mypy/subtypes.py | 17 ++++++---- mypy/types.py | 36 ++++++++++++++++++--- test-data/unit/check-protocols.test | 50 +++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 22 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1dbe82fbb78b..bc4b371d0abb 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -127,6 +127,7 @@ CallableType, DeletedType, ErasedType, + ExtraAttrs, FunctionLike, Instance, LiteralType, @@ -376,8 +377,11 @@ def module_type(self, node: MypyFile) -> Instance: # Fall back to a dummy 'object' type instead to # avoid a crash. result = self.named_type("builtins.object") - module_attrs: dict[str, Type | str] = {} + module_attrs = {} + immutable = set() for name, n in node.names.items(): + if isinstance(n.node, Var) and n.node.is_final: + immutable.add(name) typ = self.chk.determine_type_of_member(n) if typ: module_attrs[name] = typ @@ -385,8 +389,7 @@ def module_type(self, node: MypyFile) -> Instance: # TODO: what to do about nested module references? # They are non-trivial because there may be import cycles. module_attrs[name] = AnyType(TypeOfAny.special_form) - result.extra_attrs = module_attrs - result.extra_attrs["@module"] = node.fullname + result.extra_attrs = ExtraAttrs(module_attrs, immutable, node.fullname) return result def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: diff --git a/mypy/messages.py b/mypy/messages.py index 6681eed56b5c..fa9b4e398394 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1822,7 +1822,7 @@ def report_protocol_problems( return class_obj = False - is_module = isinstance(subtype, Instance) and "@module" in subtype.extra_attrs + is_module = False if isinstance(subtype, TupleType): if not isinstance(subtype.partial_fallback, Instance): return @@ -1846,6 +1846,8 @@ def report_protocol_problems( return class_obj = True subtype = ret_type + if subtype.extra_attrs and subtype.extra_attrs.mod_name: + is_module = True # Report missing members missing = get_missing_protocol_members(subtype, supertype) @@ -2188,8 +2190,8 @@ def format_literal_value(typ: LiteralType) -> str: if itype.type.fullname in ("types.ModuleType", "_importlib_modulespec.ModuleType"): # Make some common error messages simpler and tidier. base_str = "Module" - if "@module" in itype.extra_attrs and module_names: - return f"{base_str} {itype.extra_attrs['@module']}" + if itype.extra_attrs and itype.extra_attrs.mod_name and module_names: + return f"{base_str} {itype.extra_attrs.mod_name}" return base_str if verbosity >= 2 or (fullnames and itype.type.fullname in fullnames): base_str = itype.type.fullname diff --git a/mypy/server/deps.py b/mypy/server/deps.py index a0a23c046be9..45d7947641da 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -969,11 +969,9 @@ def visit_instance(self, typ: Instance) -> list[str]: triggers.extend(self.get_type_triggers(arg)) if typ.last_known_value: triggers.extend(self.get_type_triggers(typ.last_known_value)) - if "@module" in typ.extra_attrs: - mod_name = typ.extra_attrs["@module"] - assert isinstance(mod_name, str) + if typ.extra_attrs and typ.extra_attrs.mod_name: # Module as type effectively depends on all module attributes, use wildcard. - triggers.append(make_wildcard_trigger(mod_name)) + triggers.append(make_wildcard_trigger(typ.extra_attrs.mod_name)) return triggers def visit_type_alias_type(self, typ: TypeAliasType) -> list[str]: diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 4841158fc17e..1efdc7985e57 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1095,10 +1095,8 @@ def find_member( # PEP 544 doesn't specify anything about such use cases. So we just try # to do something meaningful (at least we should not crash). return TypeType(fill_typevars_with_any(v)) - if name in itype.extra_attrs: - typ = itype.extra_attrs[name] - assert isinstance(typ, Type) - return typ + if itype.extra_attrs and name in itype.extra_attrs.attrs: + return itype.extra_attrs.attrs[name] return None @@ -1131,13 +1129,18 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set if not node: if setattr_meth: return {IS_SETTABLE} - if name in itype.extra_attrs: - return {IS_SETTABLE} + if itype.extra_attrs and name in itype.extra_attrs.attrs: + flags = set() + if name not in itype.extra_attrs.immutable: + flags.add(IS_SETTABLE) + return flags return set() v = node.node # just a variable if isinstance(v, Var) and not v.is_property: - flags = {IS_SETTABLE} + flags = set() + if not v.is_final: + flags.add(IS_SETTABLE) if v.is_classvar: flags.add(IS_CLASSVAR) if class_obj and v.is_inferred: diff --git a/mypy/types.py b/mypy/types.py index 8623285d9ba5..f307bc937a55 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1155,6 +1155,34 @@ def deserialize(cls, data: JsonDict) -> DeletedType: NOT_READY: Final = mypy.nodes.FakeInfo("De-serialization failure: TypeInfo not fixed") +class ExtraAttrs: + """Summary of module attributes and types. + + This is used for instances of types.ModuleType, because they can have different + attributes per instance. + """ + + def __init__( + self, + attrs: dict[str, Type], + immutable: set[str] | None = None, + mod_name: str | None = None, + ) -> None: + self.attrs = attrs + if immutable is None: + immutable = set() + self.immutable = immutable + self.mod_name = mod_name + + def __hash__(self) -> int: + return hash((tuple(self.attrs.items()), tuple(sorted(self.immutable)))) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ExtraAttrs): + return NotImplemented + return self.attrs == other.attrs and self.immutable == other.immutable + + class Instance(ProperType): """An instance type of form C[T1, ..., Tn]. @@ -1256,16 +1284,14 @@ 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: dict[str, Type | str] = {} + self.extra_attrs: ExtraAttrs | None = None def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_instance(self) def __hash__(self) -> int: if self._hash == -1: - self._hash = hash( - (self.type, self.args, self.last_known_value, tuple(self.extra_attrs.items())) - ) + self._hash = hash((self.type, self.args, self.last_known_value, self.extra_attrs)) return self._hash def __eq__(self, other: object) -> bool: @@ -1323,7 +1349,7 @@ def copy_modified( if last_known_value is not _dummy else self.last_known_value, ) - # We intentionally don't copy the extra_attrs here. + # We intentionally don't copy the extra_attrs here, so they will be erased. new.can_be_true = self.can_be_true new.can_be_false = self.can_be_false return new diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 5564a2dac47b..78a7c6c0279a 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -1190,6 +1190,25 @@ z4 = y4 # E: Incompatible types in assignment (expression has type "PP", variabl # N: Protocol member PPS.attr expected settable variable, got read-only attribute [builtins fixtures/property.pyi] +[case testFinalAttributeProtocol] +from typing import Protocol, Final + +class P(Protocol): + x: int + +class C: + def __init__(self, x: int) -> None: + self.x = x +class CF: + def __init__(self, x: int) -> None: + self.x: Final = x + +x: P +y: P +x = C(42) +y = CF(42) # E: Incompatible types in assignment (expression has type "CF", variable has type "P") \ + # N: Protocol member P.x expected settable variable, got read-only attribute + [case testStaticAndClassMethodsInProtocols] from typing import Protocol, Type, TypeVar @@ -3685,3 +3704,34 @@ class Run: def __init__(self, arg: str) -> None: ... run = Run [builtins fixtures/module.pyi] + +[case testModuleAsProtocolImplementationClassVar] +from typing import ClassVar, Protocol +import mod + +class My(Protocol): + x: ClassVar[int] + +def test(mod: My) -> None: ... +test(mod=mod) # E: Argument "mod" to "test" has incompatible type Module; expected "My" \ + # N: Protocol member My.x expected class variable, got instance variable +[file mod.py] +x: int +[builtins fixtures/module.pyi] + +[case testModuleAsProtocolImplementationFinal] +from typing import Protocol +import some_module + +class My(Protocol): + a: int + +def func(arg: My) -> None: ... +func(some_module) # E: Argument 1 to "func" has incompatible type Module; expected "My" \ + # N: Protocol member My.a expected settable variable, got read-only attribute + +[file some_module.py] +from typing_extensions import Final + +a: Final = 1 +[builtins fixtures/module.pyi]