Skip to content

Commit 2563da0

Browse files
ilevkivskyipre-commit-ci[bot]
authored andcommitted
Fix daemon crash on invalid type in TypedDict (#17495)
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>
1 parent d8c67c3 commit 2563da0

9 files changed

+94
-15
lines changed

mypy/semanal.py

+21
Original file line numberDiff line numberDiff line change
@@ -3935,6 +3935,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
39353935
# When this type alias gets "inlined", the Any is not explicit anymore,
39363936
# so we need to replace it with non-explicit Anys.
39373937
res = make_any_non_explicit(res)
3938+
if self.options.disallow_any_unimported and has_any_from_unimported_type(res):
3939+
self.msg.unimported_type_becomes_any("Type alias target", res, s)
3940+
res = make_any_non_unimported(res)
39383941
# Note: with the new (lazy) type alias representation we only need to set no_args to True
39393942
# if the expected number of arguments is non-zero, so that aliases like `A = List` work
39403943
# 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:
54075410
# When this type alias gets "inlined", the Any is not explicit anymore,
54085411
# so we need to replace it with non-explicit Anys.
54095412
res = make_any_non_explicit(res)
5413+
if self.options.disallow_any_unimported and has_any_from_unimported_type(res):
5414+
self.msg.unimported_type_becomes_any("Type alias target", res, s)
5415+
res = make_any_non_unimported(res)
54105416
eager = self.is_func_scope()
54115417
if isinstance(res, ProperType) and isinstance(res, Instance) and not res.args:
54125418
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:
74337439
return t.copy_modified(args=[a.accept(self) for a in t.args])
74347440

74357441

7442+
def make_any_non_unimported(t: Type) -> Type:
7443+
"""Replace all Any types that come from unimported types with special form Any."""
7444+
return t.accept(MakeAnyNonUnimported())
7445+
7446+
7447+
class MakeAnyNonUnimported(TrivialSyntheticTypeTranslator):
7448+
def visit_any(self, t: AnyType) -> Type:
7449+
if t.type_of_any == TypeOfAny.from_unimported_type:
7450+
return t.copy_modified(TypeOfAny.special_form, missing_import_name=None)
7451+
return t
7452+
7453+
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
7454+
return t.copy_modified(args=[a.accept(self) for a in t.args])
7455+
7456+
74367457
def apply_semantic_analyzer_patches(patches: list[tuple[int, Callable[[], None]]]) -> None:
74377458
"""Call patch callbacks in the right order.
74387459

mypy/semanal_typeddict.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -310,18 +310,20 @@ def analyze_typeddict_classdef_fields(
310310
# Append stmt, name, and type in this case...
311311
fields.append(name)
312312
statements.append(stmt)
313-
if stmt.type is None:
313+
if stmt.unanalyzed_type is None:
314314
types.append(AnyType(TypeOfAny.unannotated))
315315
else:
316316
analyzed = self.api.anal_type(
317-
stmt.type,
317+
stmt.unanalyzed_type,
318318
allow_required=True,
319319
allow_placeholder=not self.api.is_func_scope(),
320320
prohibit_self_type="TypedDict item type",
321321
)
322322
if analyzed is None:
323323
return None, [], [], set() # Need to defer
324324
types.append(analyzed)
325+
if not has_placeholder(analyzed):
326+
stmt.type = analyzed
325327
# ...despite possible minor failures that allow further analysis.
326328
if stmt.type is None or hasattr(stmt, "new_syntax") and not stmt.new_syntax:
327329
self.fail(TPDICT_CLASS_ERROR, stmt)

mypy/stats.py

+4
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,11 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None:
203203
# Type variable definition -- not a real assignment.
204204
return
205205
if o.type:
206+
# If there is an explicit type, don't visit the l.h.s. as an expression
207+
# to avoid double-counting and mishandling special forms.
206208
self.type(o.type)
209+
o.rvalue.accept(self)
210+
return
207211
elif self.inferred and not self.all_nodes:
208212
# if self.all_nodes is set, lvalues will be visited later
209213
for lvalue in o.lvalues:

mypy/types.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1120,15 +1120,18 @@ def copy_modified(
11201120
# Mark with Bogus because _dummy is just an object (with type Any)
11211121
type_of_any: int = _dummy_int,
11221122
original_any: Bogus[AnyType | None] = _dummy,
1123+
missing_import_name: Bogus[str | None] = _dummy,
11231124
) -> AnyType:
11241125
if type_of_any == _dummy_int:
11251126
type_of_any = self.type_of_any
11261127
if original_any is _dummy:
11271128
original_any = self.source_any
1129+
if missing_import_name is _dummy:
1130+
missing_import_name = self.missing_import_name
11281131
return AnyType(
11291132
type_of_any=type_of_any,
11301133
source_any=original_any,
1131-
missing_import_name=self.missing_import_name,
1134+
missing_import_name=missing_import_name,
11321135
line=self.line,
11331136
column=self.column,
11341137
)

test-data/unit/check-flags.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -924,9 +924,9 @@ class A(List[Unchecked]): # E: Base type becomes "List[Any]" due to an unfollowe
924924
from missing import Unchecked
925925
from typing import List
926926

927-
X = List[Unchecked]
927+
X = List[Unchecked] # E: Type alias target becomes "List[Any]" due to an unfollowed import
928928

929-
def f(x: X) -> None: # E: Argument 1 to "f" becomes "List[Any]" due to an unfollowed import
929+
def f(x: X) -> None:
930930
pass
931931
[builtins fixtures/list.pyi]
932932

test-data/unit/check-semanal-error.test

+30-1
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,33 @@ class C:
151151

152152
x: P[int] = C()
153153
[builtins fixtures/tuple.pyi]
154-
[out]
154+
155+
[case testSemanalDoesNotLeakSyntheticTypes]
156+
# flags: --cache-fine-grained
157+
from typing import Generic, NamedTuple, TypedDict, TypeVar
158+
from dataclasses import dataclass
159+
160+
T = TypeVar('T')
161+
class Wrap(Generic[T]): pass
162+
163+
invalid_1: 1 + 2 # E: Invalid type comment or annotation
164+
invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation
165+
166+
class A:
167+
invalid_1: 1 + 2 # E: Invalid type comment or annotation
168+
invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation
169+
170+
class B(NamedTuple):
171+
invalid_1: 1 + 2 # E: Invalid type comment or annotation
172+
invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation
173+
174+
class C(TypedDict):
175+
invalid_1: 1 + 2 # E: Invalid type comment or annotation
176+
invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation
177+
178+
@dataclass
179+
class D:
180+
invalid_1: 1 + 2 # E: Invalid type comment or annotation
181+
invalid_2: Wrap[1 + 2] # E: Invalid type comment or annotation
182+
[builtins fixtures/dict.pyi]
183+
[typing fixtures/typing-typeddict.pyi]

test-data/unit/check-typeddict.test

+20
Original file line numberDiff line numberDiff line change
@@ -2362,6 +2362,26 @@ Foo = TypedDict('Foo', {'camelCaseKey': str})
23622362
value: Foo = {} # E: Missing key "camelCaseKey" for TypedDict "Foo"
23632363
[builtins fixtures/dict.pyi]
23642364

2365+
[case testTypedDictWithDeferredFieldTypeEval]
2366+
from typing import Generic, TypeVar, TypedDict, NotRequired
2367+
2368+
class Foo(TypedDict):
2369+
y: NotRequired[int]
2370+
x: Outer[Inner[ForceDeferredEval]]
2371+
2372+
var: Foo
2373+
reveal_type(var) # N: Revealed type is "TypedDict('__main__.Foo', {'y'?: builtins.int, 'x': __main__.Outer[__main__.Inner[__main__.ForceDeferredEval]]})"
2374+
2375+
T1 = TypeVar("T1")
2376+
class Outer(Generic[T1]): pass
2377+
2378+
T2 = TypeVar("T2", bound="ForceDeferredEval")
2379+
class Inner(Generic[T2]): pass
2380+
2381+
class ForceDeferredEval: pass
2382+
[builtins fixtures/dict.pyi]
2383+
[typing fixtures/typing-typeddict.pyi]
2384+
23652385
-- Required[]
23662386

23672387
[case testDoesRecognizeRequiredInTypedDictWithClass]

test-data/unit/reports.test

+8-8
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,19 @@ def foo(a: int) -> MyDict:
8181
return {"a": a}
8282
md: MyDict = MyDict(**foo(42))
8383
[outfile build/cobertura.xml]
84-
<coverage timestamp="$TIMESTAMP" version="$VERSION" line-rate="0.8333" branch-rate="0">
84+
<coverage timestamp="$TIMESTAMP" version="$VERSION" line-rate="1.0000" branch-rate="0">
8585
<sources>
8686
<source>$PWD</source>
8787
</sources>
8888
<packages>
89-
<package complexity="1.0" name="a" branch-rate="0" line-rate="0.8333">
89+
<package complexity="1.0" name="a" branch-rate="0" line-rate="1.0000">
9090
<classes>
91-
<class complexity="1.0" filename="a.py" name="a.py" branch-rate="0" line-rate="0.8333">
91+
<class complexity="1.0" filename="a.py" name="a.py" branch-rate="0" line-rate="1.0000">
9292
<methods/>
9393
<lines>
9494
<line branch="false" hits="1" number="1" precision="precise"/>
9595
<line branch="false" hits="1" number="3" precision="precise"/>
96-
<line branch="false" hits="0" number="4" precision="any"/>
96+
<line branch="false" hits="1" number="4" precision="precise"/>
9797
<line branch="false" hits="1" number="6" precision="precise"/>
9898
<line branch="false" hits="1" number="7" precision="precise"/>
9999
<line branch="false" hits="1" number="8" precision="precise"/>
@@ -155,9 +155,9 @@ z: NestedGen[Any]
155155
[outfile report/types-of-anys.txt]
156156
Name Unannotated Explicit Unimported Omitted Generics Error Special Form Implementation Artifact
157157
-----------------------------------------------------------------------------------------------------------------
158-
n 0 4 0 8 0 0 0
158+
n 0 2 0 8 0 0 0
159159
-----------------------------------------------------------------------------------------------------------------
160-
Total 0 4 0 8 0 0 0
160+
Total 0 2 0 8 0 0 0
161161

162162
[case testTypeVarTreatedAsEmptyLine]
163163
# cmd: mypy --html-report report n.py
@@ -371,9 +371,9 @@ z = g.does_not_exist() # type: ignore # Error
371371
[outfile report/types-of-anys.txt]
372372
Name Unannotated Explicit Unimported Omitted Generics Error Special Form Implementation Artifact
373373
-----------------------------------------------------------------------------------------------------------------
374-
n 2 4 2 1 3 0 0
374+
n 2 3 1 1 3 0 0
375375
-----------------------------------------------------------------------------------------------------------------
376-
Total 2 4 2 1 3 0 0
376+
Total 2 3 1 1 3 0 0
377377

378378
[case testAnyExpressionsReportUnqualifiedError]
379379
# cmd: mypy --any-exprs-report report n.py

test-data/unit/semanal-typeddict.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ MypyFile:1(
4242
NameExpr(x)
4343
TempNode:4(
4444
Any)
45-
str?)))
45+
builtins.str)))

0 commit comments

Comments
 (0)