From 934976d26d316adf26cb072f88afc73dd4147f2e Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 7 Sep 2025 04:40:53 +0200 Subject: [PATCH 1/3] Defer when constructor type is not yet available --- mypy/checkexpr.py | 5 +++++ mypy/typeops.py | 15 +++++++++++++++ test-data/unit/check-classes.test | 31 +++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 835eeb725394..6914450e327c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -138,6 +138,7 @@ function_type, get_all_type_vars, get_type_vars, + has_deferred_constructor, is_literal_type_like, make_simplified_union, simple_literal_type, @@ -400,6 +401,10 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: result = node.type elif isinstance(node, (FuncDef, TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)): result = self.analyze_static_reference(node, e, e.is_alias_rvalue or lvalue) + if isinstance(node, TypeInfo) and has_deferred_constructor(node): + # When __init__ or __new__ is wrapped in a custom decorator, we need to defer. + # analyze_static_reference guarantees that it never defers, so play along. + self.chk.handle_cannot_determine_type(node.name, e) else: if isinstance(node, PlaceholderNode): assert False, f"PlaceholderNode {node.fullname!r} leaked to checker" diff --git a/mypy/typeops.py b/mypy/typeops.py index 87a4d8cefd13..90dd32c9351b 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -237,6 +237,21 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P return result +def has_deferred_constructor(info: TypeInfo) -> bool: + init_method = info.get("__init__") + new_method = info.get("__new__") or init_method + return ( + init_method is not None + and _is_deferred_decorator(init_method.node) + or new_method is not None + and _is_deferred_decorator(new_method.node) + ) + + +def _is_deferred_decorator(n: SymbolNode | None) -> bool: + return isinstance(n, Decorator) and n.type is None + + def is_valid_constructor(n: SymbolNode | None) -> bool: """Does this node represents a valid constructor method? diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 498a2c12b6e8..08f3366613d1 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -9289,3 +9289,34 @@ from typ import NT def f() -> NT: return NT(x='') [builtins fixtures/tuple.pyi] + +[case testDecoratedConstructorDeferral] +from typing import Any, Callable, TypeVar + +Tc = TypeVar('Tc', bound=Callable[..., Any]) + +def any_decorator_factory() -> Callable[[Tc], Tc]: + def inner(func: Tc) -> Tc: + return func + return inner + + +def function_pre() -> None: + reveal_type(GoodClass()) # N: Revealed type is "__main__.GoodClass" + reveal_type(BadClass()) # N: Revealed type is "Any" + + +class GoodClass: + @any_decorator_factory() + def __init__(self): + pass + +class BadClass: + @unknown() # E: Name "unknown" is not defined + def __init__(self): + pass + + +def function_post() -> None: + reveal_type(GoodClass()) # N: Revealed type is "__main__.GoodClass" + reveal_type(BadClass()) # N: Revealed type is "Any" From 131eef2c591177430d2d745d1dac31ab3ae136c0 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 7 Sep 2025 04:46:33 +0200 Subject: [PATCH 2/3] Only check if we received Any? --- mypy/checkexpr.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 6914450e327c..bfcf0f43134e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -401,7 +401,12 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: result = node.type elif isinstance(node, (FuncDef, TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)): result = self.analyze_static_reference(node, e, e.is_alias_rvalue or lvalue) - if isinstance(node, TypeInfo) and has_deferred_constructor(node): + if ( + isinstance(node, TypeInfo) + and isinstance(result, ProperType) + and isinstance(result, AnyType) + and has_deferred_constructor(node) + ): # When __init__ or __new__ is wrapped in a custom decorator, we need to defer. # analyze_static_reference guarantees that it never defers, so play along. self.chk.handle_cannot_determine_type(node.name, e) From b77452a9f7fb753937275d35a77fee7b01f7a2b6 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 7 Sep 2025 05:24:38 +0200 Subject: [PATCH 3/3] Do not do that indefinitely --- mypy/typeops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index 90dd32c9351b..60104c5e97ba 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -249,7 +249,7 @@ def has_deferred_constructor(info: TypeInfo) -> bool: def _is_deferred_decorator(n: SymbolNode | None) -> bool: - return isinstance(n, Decorator) and n.type is None + return isinstance(n, Decorator) and n.type is None and not n.var.is_ready def is_valid_constructor(n: SymbolNode | None) -> bool: