diff --git a/mypy/copytype.py b/mypy/copytype.py index 3ec512193bece..12dda9d263975 100644 --- a/mypy/copytype.py +++ b/mypy/copytype.py @@ -107,7 +107,10 @@ def visit_tuple_type(self, t: TupleType) -> ProperType: def visit_typeddict_type(self, t: TypedDictType) -> ProperType: return self.copy_common( - t, TypedDictType(t.items, t.required_keys, t.readonly_keys, t.is_closed, t.fallback) + t, + TypedDictType( + t.items, t.required_keys, t.readonly_keys, t.fallback, is_closed=t.is_closed + ), ) def visit_literal_type(self, t: LiteralType) -> ProperType: diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 186429abd36a9..fd507216a6be9 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -350,7 +350,7 @@ def _possible_callable_kwargs(cls, repl: Parameters, dict_type: Instance) -> Pro return Instance(dict_type.type, [dict_type.args[0], extra_items]) # TODO: when PEP 728 `extra_items` is implemented, pass extra_items below. is_closed = extra_items is None - return TypedDictType(kwargs, required_names, set(), is_closed, fallback=dict_type) + return TypedDictType(kwargs, required_names, set(), dict_type, is_closed=is_closed) def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: # Sometimes solver may need to expand a type variable with (a copy of) itself diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 5fecf7b6fba9f..1c9323be056dd 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -277,7 +277,7 @@ def expr_to_unanalyzed_type( value, options, allow_new_syntax, expr, lookup_qualified=lookup_qualified ) result = TypedDictType( - items, set(), set(), False, Instance(MISSING_FALLBACK, ()), expr.line, expr.column + items, set(), set(), Instance(MISSING_FALLBACK, ()), expr.line, expr.column ) result.extra_items_from = extra_items_from return result diff --git a/mypy/fastparse.py b/mypy/fastparse.py index c2d2243d7555e..d9e2d5df8f4c1 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -2143,7 +2143,7 @@ def visit_Dict(self, n: ast3.Dict) -> Type: continue return self.invalid_type(n) items[item_name.value] = self.visit(value) - result = TypedDictType(items, set(), set(), False, _dummy_fallback, n.lineno, n.col_offset) + result = TypedDictType(items, set(), set(), _dummy_fallback, n.lineno, n.col_offset) result.extra_items_from = extra_items_from return result diff --git a/mypy/join.py b/mypy/join.py index b99ed190cc9da..c609b303bb087 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -663,7 +663,9 @@ def visit_typeddict_type(self, t: TypedDictType) -> ProperType: fallback = self.s.create_anonymous_fallback() is_closed = self.s.is_closed and t.is_closed - return TypedDictType(items, required_keys, readonly_keys, is_closed, fallback) + return TypedDictType( + items, required_keys, readonly_keys, fallback, is_closed=is_closed + ) elif isinstance(self.s, Instance): return join_types(self.s, t.fallback) else: diff --git a/mypy/meet.py b/mypy/meet.py index 57b79c51f8e11..bfc6d88a1e209 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -1186,7 +1186,9 @@ def visit_typeddict_type(self, t: TypedDictType) -> ProperType: fallback = self.s.create_anonymous_fallback() required_keys = self.s.required_keys | t.required_keys - return TypedDictType(items, required_keys, readonly_keys, is_closed, fallback) + return TypedDictType( + items, required_keys, readonly_keys, fallback, is_closed=is_closed + ) elif isinstance(self.s, Instance) and is_subtype(t, self.s): return t else: diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index f371746cab8b8..414426580fa73 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -926,7 +926,7 @@ def read_type(state: State, data: ReadBuffer) -> Type: extra_items_from.append(val) else: td_items[key] = val - typeddict_type = TypedDictType(td_items, set(), set(), False, _dummy_fallback) + typeddict_type = TypedDictType(td_items, set(), set(), _dummy_fallback) typeddict_type.extra_items_from = extra_items_from read_loc(data, typeddict_type) expect_end_tag(data) diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 5f152ba0e8988..b1ba9f6c3abec 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -785,7 +785,7 @@ def build_typeddict_typeinfo( assert fallback is not None info = existing_info or self.api.basic_new_typeinfo(name, fallback, line) typeddict_type = TypedDictType( - item_types, required_keys, readonly_keys, is_closed, fallback + item_types, required_keys, readonly_keys, fallback, is_closed=is_closed ) any_placeholder = has_placeholder(typeddict_type) if typeddict_data: diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index ec9af3e669344..ac6d24b1ef4c0 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -39,6 +39,7 @@ ProperType, TupleType, Type, + TypedDictType, TypeOfAny, TypeType, TypeVarId, @@ -223,6 +224,22 @@ def test_indirection_no_infinite_recursion(self) -> None: modules = visitor.modules assert modules == {"__main__", "builtins"} + def test_typeddict_type_constructor_signature(self) -> None: + typ = TypedDictType({"x": self.fx.o}, {"x"}, set(), self.fx.a, 10, 20) + + assert typ.fallback is self.fx.a + assert_equal(typ.line, 10) + assert_equal(typ.column, 20) + assert not typ.is_closed + + closed = TypedDictType({"x": self.fx.o}, {"x"}, set(), self.fx.a, is_closed=True) + assert closed.is_closed + + with self.assertRaises(TypeError): + TypedDictType( # type: ignore[misc] + {"x": self.fx.o}, {"x"}, set(), self.fx.a, 10, 20, True + ) + class TypeOpsSuite(Suite): def setUp(self) -> None: diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 7a486d6603a00..7052c80118710 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -284,11 +284,11 @@ def visit_typeddict_type(self, t: TypedDictType, /) -> Type: items, t.required_keys, t.readonly_keys, - t.is_closed, # TODO: This appears to be unsafe. cast(Any, t.fallback.accept(self)), t.line, t.column, + is_closed=t.is_closed, ) self.set_cached(t, result) return result diff --git a/mypy/typeanal.py b/mypy/typeanal.py index aa5d14bddc65a..ff3c8bd2816e1 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1416,7 +1416,7 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: fallback = t.fallback is_closed = t.is_closed return TypedDictType( - items, required_keys, readonly_keys, is_closed, fallback, t.line, t.column + items, required_keys, readonly_keys, fallback, t.line, t.column, is_closed=is_closed ) def visit_raw_expression_type(self, t: RawExpressionType) -> Type: diff --git a/mypy/types.py b/mypy/types.py index 324135df014d3..6e934f64315c0 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3048,10 +3048,11 @@ def __init__( items: dict[str, Type], required_keys: set[str], readonly_keys: set[str], - is_closed: bool, fallback: Instance, line: int = -1, column: int = -1, + *, + is_closed: bool = False, ) -> None: super().__init__(line, column) self.items = items @@ -3112,8 +3113,8 @@ def deserialize(cls, data: JsonDict) -> TypedDictType: {n: deserialize_type(t) for (n, t) in data["items"]}, set(data["required_keys"]), set(data["readonly_keys"]), - bool(data["is_closed"]), Instance.deserialize(data["fallback"]), + is_closed=bool(data["is_closed"]), ) def write(self, data: WriteBuffer) -> None: @@ -3133,8 +3134,8 @@ def read(cls, data: ReadBuffer) -> TypedDictType: read_type_map(data), set(read_str_list(data)), set(read_str_list(data)), - read_bool(data), fallback, + is_closed=read_bool(data), ) assert read_tag(data) == END_TAG return ret @@ -3178,7 +3179,13 @@ def copy_modified( items = {k: v for (k, v) in items.items() if k in item_names} required_keys &= set(item_names) return TypedDictType( - items, required_keys, readonly_keys, is_closed, fallback, self.line, self.column + items, + required_keys, + readonly_keys, + fallback, + self.line, + self.column, + is_closed=is_closed, ) def zip(self, right: TypedDictType) -> Iterable[tuple[str, Type, Type]]: