From 2563da0c721a89725bfd009da12dd6378554bfc6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 6 Jul 2024 22:04:07 +0100 Subject: [PATCH] Fix daemon crash on invalid type in TypedDict (#17495) Fixes https://github.com/python/mypy/issues/10007 Fixes https://github.com/python/mypy/issues/17477 This fixes the crash as proposed in https://github.com/python/mypy/pull/13732, but also fixes some inconsistencies in `Any` types exposed by the fix. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- mypy/semanal.py | 21 +++++++++++++++++ mypy/semanal_typeddict.py | 6 +++-- mypy/stats.py | 4 ++++ mypy/types.py | 5 +++- test-data/unit/check-flags.test | 4 ++-- test-data/unit/check-semanal-error.test | 31 ++++++++++++++++++++++++- test-data/unit/check-typeddict.test | 20 ++++++++++++++++ test-data/unit/reports.test | 16 ++++++------- test-data/unit/semanal-typeddict.test | 2 +- 9 files changed, 94 insertions(+), 15 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index f857c3e73381..f36149076fe6 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3935,6 +3935,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # When this type alias gets "inlined", the Any is not explicit anymore, # so we need to replace it with non-explicit Anys. res = make_any_non_explicit(res) + if self.options.disallow_any_unimported and has_any_from_unimported_type(res): + self.msg.unimported_type_becomes_any("Type alias target", res, s) + res = make_any_non_unimported(res) # Note: with the new (lazy) type alias representation we only need to set no_args to True # if the expected number of arguments is non-zero, so that aliases like `A = List` work # but not aliases like `A = TypeAliasType("A", List)` as these need explicit type params. @@ -5407,6 +5410,9 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: # When this type alias gets "inlined", the Any is not explicit anymore, # so we need to replace it with non-explicit Anys. res = make_any_non_explicit(res) + if self.options.disallow_any_unimported and has_any_from_unimported_type(res): + self.msg.unimported_type_becomes_any("Type alias target", res, s) + res = make_any_non_unimported(res) eager = self.is_func_scope() if isinstance(res, ProperType) and isinstance(res, Instance) and not res.args: fix_instance(res, self.fail, self.note, disallow_any=False, options=self.options) @@ -7433,6 +7439,21 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type: return t.copy_modified(args=[a.accept(self) for a in t.args]) +def make_any_non_unimported(t: Type) -> Type: + """Replace all Any types that come from unimported types with special form Any.""" + return t.accept(MakeAnyNonUnimported()) + + +class MakeAnyNonUnimported(TrivialSyntheticTypeTranslator): + def visit_any(self, t: AnyType) -> Type: + if t.type_of_any == TypeOfAny.from_unimported_type: + return t.copy_modified(TypeOfAny.special_form, missing_import_name=None) + return t + + def visit_type_alias_type(self, t: TypeAliasType) -> Type: + return t.copy_modified(args=[a.accept(self) for a in t.args]) + + def apply_semantic_analyzer_patches(patches: list[tuple[int, Callable[[], None]]]) -> None: """Call patch callbacks in the right order. diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index eee98d4d20fa..7b8d874337a2 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -310,11 +310,11 @@ def analyze_typeddict_classdef_fields( # Append stmt, name, and type in this case... fields.append(name) statements.append(stmt) - if stmt.type is None: + if stmt.unanalyzed_type is None: types.append(AnyType(TypeOfAny.unannotated)) else: analyzed = self.api.anal_type( - stmt.type, + stmt.unanalyzed_type, allow_required=True, allow_placeholder=not self.api.is_func_scope(), prohibit_self_type="TypedDict item type", @@ -322,6 +322,8 @@ def analyze_typeddict_classdef_fields( if analyzed is None: return None, [], [], set() # Need to defer types.append(analyzed) + if not has_placeholder(analyzed): + stmt.type = analyzed # ...despite possible minor failures that allow further analysis. if stmt.type is None or hasattr(stmt, "new_syntax") and not stmt.new_syntax: self.fail(TPDICT_CLASS_ERROR, stmt) diff --git a/mypy/stats.py b/mypy/stats.py index b167a41b0e34..9c69a245741b 100644 --- a/mypy/stats.py +++ b/mypy/stats.py @@ -203,7 +203,11 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None: # Type variable definition -- not a real assignment. return if o.type: + # If there is an explicit type, don't visit the l.h.s. as an expression + # to avoid double-counting and mishandling special forms. self.type(o.type) + o.rvalue.accept(self) + return elif self.inferred and not self.all_nodes: # if self.all_nodes is set, lvalues will be visited later for lvalue in o.lvalues: diff --git a/mypy/types.py b/mypy/types.py index 2e7cbfd4e733..89609e8d0546 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1120,15 +1120,18 @@ def copy_modified( # Mark with Bogus because _dummy is just an object (with type Any) type_of_any: int = _dummy_int, original_any: Bogus[AnyType | None] = _dummy, + missing_import_name: Bogus[str | None] = _dummy, ) -> AnyType: if type_of_any == _dummy_int: type_of_any = self.type_of_any if original_any is _dummy: original_any = self.source_any + if missing_import_name is _dummy: + missing_import_name = self.missing_import_name return AnyType( type_of_any=type_of_any, source_any=original_any, - missing_import_name=self.missing_import_name, + missing_import_name=missing_import_name, line=self.line, column=self.column, ) diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 62711d5f0071..4f327a2f0edc 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -924,9 +924,9 @@ class A(List[Unchecked]): # E: Base type becomes "List[Any]" due to an unfollowe from missing import Unchecked from typing import List -X = List[Unchecked] +X = List[Unchecked] # E: Type alias target becomes "List[Any]" due to an unfollowed import -def f(x: X) -> None: # E: Argument 1 to "f" becomes "List[Any]" due to an unfollowed import +def f(x: X) -> None: pass [builtins fixtures/list.pyi] diff --git a/test-data/unit/check-semanal-error.test b/test-data/unit/check-semanal-error.test index c6cf45d96691..d7ab272aed6c 100644 --- a/test-data/unit/check-semanal-error.test +++ b/test-data/unit/check-semanal-error.test @@ -151,4 +151,33 @@ class C: x: P[int] = C() [builtins fixtures/tuple.pyi] -[out] + +[case testSemanalDoesNotLeakSyntheticTypes] +# flags: --cache-fine-grained +from typing import Generic, NamedTuple, TypedDict, TypeVar +from dataclasses import dataclass + +T = TypeVar('T') +class Wrap(Generic[T]): pass + +invalid_1: 1 + 2 # E: Invalid type comment or annotation +invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation + +class A: + invalid_1: 1 + 2 # E: Invalid type comment or annotation + invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation + +class B(NamedTuple): + invalid_1: 1 + 2 # E: Invalid type comment or annotation + invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation + +class C(TypedDict): + invalid_1: 1 + 2 # E: Invalid type comment or annotation + invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation + +@dataclass +class D: + invalid_1: 1 + 2 # E: Invalid type comment or annotation + invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index fa77d98e4a34..d35ec8ddd80e 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -2362,6 +2362,26 @@ Foo = TypedDict('Foo', {'camelCaseKey': str}) value: Foo = {} # E: Missing key "camelCaseKey" for TypedDict "Foo" [builtins fixtures/dict.pyi] +[case testTypedDictWithDeferredFieldTypeEval] +from typing import Generic, TypeVar, TypedDict, NotRequired + +class Foo(TypedDict): + y: NotRequired[int] + x: Outer[Inner[ForceDeferredEval]] + +var: Foo +reveal_type(var) # N: Revealed type is "TypedDict('__main__.Foo', {'y'?: builtins.int, 'x': __main__.Outer[__main__.Inner[__main__.ForceDeferredEval]]})" + +T1 = TypeVar("T1") +class Outer(Generic[T1]): pass + +T2 = TypeVar("T2", bound="ForceDeferredEval") +class Inner(Generic[T2]): pass + +class ForceDeferredEval: pass +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + -- Required[] [case testDoesRecognizeRequiredInTypedDictWithClass] diff --git a/test-data/unit/reports.test b/test-data/unit/reports.test index 16061d9c32bf..81e24240af2d 100644 --- a/test-data/unit/reports.test +++ b/test-data/unit/reports.test @@ -81,19 +81,19 @@ def foo(a: int) -> MyDict: return {"a": a} md: MyDict = MyDict(**foo(42)) [outfile build/cobertura.xml] - + $PWD - + - + - + @@ -155,9 +155,9 @@ z: NestedGen[Any] [outfile report/types-of-anys.txt] Name Unannotated Explicit Unimported Omitted Generics Error Special Form Implementation Artifact ----------------------------------------------------------------------------------------------------------------- - n 0 4 0 8 0 0 0 + n 0 2 0 8 0 0 0 ----------------------------------------------------------------------------------------------------------------- -Total 0 4 0 8 0 0 0 +Total 0 2 0 8 0 0 0 [case testTypeVarTreatedAsEmptyLine] # cmd: mypy --html-report report n.py @@ -371,9 +371,9 @@ z = g.does_not_exist() # type: ignore # Error [outfile report/types-of-anys.txt] Name Unannotated Explicit Unimported Omitted Generics Error Special Form Implementation Artifact ----------------------------------------------------------------------------------------------------------------- - n 2 4 2 1 3 0 0 + n 2 3 1 1 3 0 0 ----------------------------------------------------------------------------------------------------------------- -Total 2 4 2 1 3 0 0 +Total 2 3 1 1 3 0 0 [case testAnyExpressionsReportUnqualifiedError] # cmd: mypy --any-exprs-report report n.py diff --git a/test-data/unit/semanal-typeddict.test b/test-data/unit/semanal-typeddict.test index b9eb6e0c2b13..9ce89155c308 100644 --- a/test-data/unit/semanal-typeddict.test +++ b/test-data/unit/semanal-typeddict.test @@ -42,4 +42,4 @@ MypyFile:1( NameExpr(x) TempNode:4( Any) - str?))) + builtins.str)))