diff --git a/mypy/checker.py b/mypy/checker.py index 33705c98e10c..4da07541137c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1332,6 +1332,11 @@ def visit_func_def(self, defn: FuncDef) -> None: self.visit_func_def_impl(defn) def visit_func_def_impl(self, defn: FuncDef) -> None: + if defn.is_no_type_check: + # @typing.no_type_check: the body must be skipped. The decorator + # dispatch (visit_decorator) handles this at the top level, but a + # fine-grained reprocess can re-check the bare FuncDef target. + return with self.tscope.function_scope(defn), self.set_recurse_into_functions(): self.check_func_item(defn, name=defn.name) if not self.can_skip_diagnostics: diff --git a/mypy/nodes.py b/mypy/nodes.py index f837185b858a..1ed7849ab759 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1083,6 +1083,7 @@ def is_dynamic(self) -> bool: "is_trivial_body", "is_trivial_self", "is_mypy_only", + "is_no_type_check", ] # Abstract status of a function @@ -1109,6 +1110,7 @@ class FuncDef(FuncItem, SymbolNode, Statement): "is_trivial_self", "is_invalid_redefinition", "is_mypy_only", + "is_no_type_check", # Present only when a function is decorated with @typing.dataclass_transform or similar "dataclass_transform_spec", "docstring", @@ -1139,6 +1141,10 @@ def __init__( self.original_def: None | FuncDef | Var | Decorator = None # Definitions that appear in if TYPE_CHECKING are marked with this flag. self.is_mypy_only = False + # Decorated with @typing.no_type_check: the body must never be type-checked. + # Stored as a durable flag so it survives fine-grained reprocessing of the + # FuncDef target (which bypasses the decorator dispatch in the checker). + self.is_no_type_check = False self.dataclass_transform_spec: DataclassTransformSpec | None = None self.docstring: str | None = None self.deprecated: str | None = None diff --git a/mypy/semanal.py b/mypy/semanal.py index e010273b0781..3a82f9f4080a 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1810,6 +1810,7 @@ def visit_decorator(self, dec: Decorator) -> None: if (not dec.is_overload or dec.var.is_property) and self.type: dec.var.info = self.type dec.var.is_initialized_in_class = True + dec.func.is_no_type_check = no_type_check if no_type_check: erase_func_annotations(dec.func) if not no_type_check and (self.recurse_into_functions or dec.func.def_or_infer_vars): diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index bc02f49e5d83..b12238fe6a9b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -11839,3 +11839,25 @@ def bar() -> str: return "a" main:2: note: Revealed type is "None | builtins.int" == main:2: note: Revealed type is "None | builtins.str" + +[case testNoTypeCheckReprocessedViaDependency] +# A @no_type_check function reprocessed via a dependency change (without editing +# its own module) must still have its body skipped. Regression test: the daemon +# used to re-check the bare FuncDef target, bypassing the @no_type_check guard. +# flags: --check-untyped-defs +import a +[file a.py] +from typing import no_type_check +from b import SCALE + +@no_type_check +def f() -> None: + factor: int = SCALE + bad: int = "not an int" +[file b.py] +SCALE: int = 3 +[file b.py.2] +SCALE: str = "" +[typing fixtures/typing-medium.pyi] +[out] +==