Skip to content

Commit

Permalink
Fix daemon crash on invalid type in TypedDict (#17495)
Browse files Browse the repository at this point in the history
Fixes #10007
Fixes #17477

This fixes the crash as proposed in
#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>
  • Loading branch information
2 people authored and JukkaL committed Jul 18, 2024
1 parent d8c67c3 commit 2563da0
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 15 deletions.
21 changes: 21 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions mypy/semanal_typeddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,18 +310,20 @@ 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",
)
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)
Expand Down
4 changes: 4 additions & 0 deletions mypy/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-flags.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
31 changes: 30 additions & 1 deletion test-data/unit/check-semanal-error.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
20 changes: 20 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
16 changes: 8 additions & 8 deletions test-data/unit/reports.test
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,19 @@ def foo(a: int) -> MyDict:
return {"a": a}
md: MyDict = MyDict(**foo(42))
[outfile build/cobertura.xml]
<coverage timestamp="$TIMESTAMP" version="$VERSION" line-rate="0.8333" branch-rate="0">
<coverage timestamp="$TIMESTAMP" version="$VERSION" line-rate="1.0000" branch-rate="0">
<sources>
<source>$PWD</source>
</sources>
<packages>
<package complexity="1.0" name="a" branch-rate="0" line-rate="0.8333">
<package complexity="1.0" name="a" branch-rate="0" line-rate="1.0000">
<classes>
<class complexity="1.0" filename="a.py" name="a.py" branch-rate="0" line-rate="0.8333">
<class complexity="1.0" filename="a.py" name="a.py" branch-rate="0" line-rate="1.0000">
<methods/>
<lines>
<line branch="false" hits="1" number="1" precision="precise"/>
<line branch="false" hits="1" number="3" precision="precise"/>
<line branch="false" hits="0" number="4" precision="any"/>
<line branch="false" hits="1" number="4" precision="precise"/>
<line branch="false" hits="1" number="6" precision="precise"/>
<line branch="false" hits="1" number="7" precision="precise"/>
<line branch="false" hits="1" number="8" precision="precise"/>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/semanal-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ MypyFile:1(
NameExpr(x)
TempNode:4(
Any)
str?)))
builtins.str)))

0 comments on commit 2563da0

Please sign in to comment.