From 839afb5ed662e20870d34641a74bac48a7009c96 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 5 Apr 2025 15:31:21 -0400 Subject: [PATCH 1/7] Add experimental support for PEP-764 inline TypedDicts --- docs/source/command_line.rst | 10 ++- docs/source/typed_dict.rst | 38 +++++---- mypy/fastparse.py | 2 - mypy/options.py | 5 +- mypy/typeanal.py | 43 ++++++++++- mypy/types.py | 2 + test-data/unit/check-literal.test | 3 +- test-data/unit/check-python312.test | 11 +++ test-data/unit/check-typeddict.test | 115 +++++++++++++++++++++++++++- 9 files changed, 205 insertions(+), 24 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 2a54c1144171..f7c4463db854 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -1085,7 +1085,7 @@ format into the specified directory. Enabling incomplete/experimental features ***************************************** -.. option:: --enable-incomplete-feature {PreciseTupleTypes, InlineTypedDict} +.. option:: --enable-incomplete-feature {PreciseTupleTypes, NewInlineTypedDict, InlineTypedDict} Some features may require several mypy releases to implement, for example due to their complexity, potential for backwards incompatibility, or @@ -1132,6 +1132,14 @@ List of currently incomplete/experimental features: # Without PreciseTupleTypes: tuple[int, ...] # With PreciseTupleTypes: tuple[()] | tuple[int] | tuple[int, int] +* ``NewInlineTypedDict``: this feature enables :pep:`764` syntax for inline + :ref:`TypedDicts `, for example: + + .. code-block:: python + + def test_values() -> TypedDict[{"int": int, "str": str}]: + return {"int": 42, "str": "test"} + * ``InlineTypedDict``: this feature enables non-standard syntax for inline :ref:`TypedDicts `, for example: diff --git a/docs/source/typed_dict.rst b/docs/source/typed_dict.rst index bbb10a12abe8..990140f1d3f8 100644 --- a/docs/source/typed_dict.rst +++ b/docs/source/typed_dict.rst @@ -294,8 +294,8 @@ Inline TypedDict types .. note:: - This is an experimental (non-standard) feature. Use - ``--enable-incomplete-feature=InlineTypedDict`` to enable. + This is an experimental feature proposed by :pep:`764`. Use + ``--enable-incomplete-feature=NewInlineTypedDict`` to enable. Sometimes you may want to define a complex nested JSON schema, or annotate a one-off function that returns a TypedDict. In such cases it may be convenient @@ -303,26 +303,36 @@ to use inline TypedDict syntax. For example: .. code-block:: python - def test_values() -> {"int": int, "str": str}: + def test_values() -> TypedDict[{"int": int, "str": str}]: return {"int": 42, "str": "test"} class Response(TypedDict): status: int msg: str # Using inline syntax here avoids defining two additional TypedDicts. - content: {"items": list[{"key": str, "value": str}]} + content: TypedDict[{"items": list[TypedDict[{"key": str, "value": str}]]}] -Inline TypedDicts can also by used as targets of type aliases, but due to -ambiguity with a regular variables it is only allowed for (newer) explicit -type alias forms: +.. note:: -.. code-block:: python + Mypy also supports a legacy syntax for inline TypedDicts that pre-dates :pep:`764`: + + .. code-block:: python + + def test_values() -> {"int": int, "str": str}: + return {"int": 42, "str": "test"} + + This legacy syntax can be enabled using ``--enable-incomplete-feature=InlineTypedDict``. + Due to ambiguity with a regular variables, the legacy syntax may only be used in + type aliases when using (newer) explicit type alias forms: + + .. code-block:: python - from typing import TypeAlias + from typing import TypeAlias - X = {"a": int, "b": int} # creates a variable with type dict[str, type[int]] - Y: TypeAlias = {"a": int, "b": int} # creates a type alias - type Z = {"a": int, "b": int} # same as above (Python 3.12+ only) + X = {"a": int, "b": int} # creates a variable with type dict[str, type[int]] + Y: TypeAlias = {"a": int, "b": int} # creates a type alias + type Z = {"a": int, "b": int} # same as above (Python 3.12+ only) -Also, due to incompatibility with runtime type-checking it is strongly recommended -to *not* use inline syntax in union types. + This restriction does not apply to the :pep:`764` syntax. + Also, due to incompatibility with runtime type-checking it is strongly recommended + to *not* use legacy inline syntax in union types. diff --git a/mypy/fastparse.py b/mypy/fastparse.py index b9a55613ec16..982eb7058e4e 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -2077,8 +2077,6 @@ def visit_Tuple(self, n: ast3.Tuple) -> Type: ) def visit_Dict(self, n: ast3.Dict) -> Type: - if not n.keys: - return self.invalid_type(n) items: dict[str, Type] = {} extra_items_from = [] for item_name, value in zip(n.keys, n.values): diff --git a/mypy/options.py b/mypy/options.py index 17fea6b0bf29..c968b8884ba4 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -80,7 +80,10 @@ class BuildType: PRECISE_TUPLE_TYPES: Final = "PreciseTupleTypes" NEW_GENERIC_SYNTAX: Final = "NewGenericSyntax" INLINE_TYPEDDICT: Final = "InlineTypedDict" -INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT)) +NEW_INLINE_TYPEDDICT: Final = "NewInlineTypedDict" +INCOMPLETE_FEATURES: Final = frozenset( + (PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT, NEW_INLINE_TYPEDDICT) +) COMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK, NEW_GENERIC_SYNTAX)) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 7bf21709b863..d97ea662c4dd 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -49,7 +49,7 @@ check_arg_names, get_nongen_builtins, ) -from mypy.options import INLINE_TYPEDDICT, Options +from mypy.options import INLINE_TYPEDDICT, NEW_INLINE_TYPEDDICT, Options from mypy.plugin import AnalyzeTypeContext, Plugin, TypeAnalyzerPluginInterface from mypy.semanal_shared import ( SemanticAnalyzerCoreInterface, @@ -66,6 +66,7 @@ FINAL_TYPE_NAMES, LITERAL_TYPE_NAMES, NEVER_NAMES, + TPDICT_NAMES, TYPE_ALIAS_NAMES, UNPACK_TYPE_NAMES, AnyType, @@ -125,6 +126,7 @@ "typing.Union", *LITERAL_TYPE_NAMES, *ANNOTATED_TYPE_NAMES, + *TPDICT_NAMES, } ARG_KINDS_BY_CONSTRUCTOR: Final = { @@ -810,6 +812,22 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ # TODO: verify this is unreachable and replace with an assert? self.fail("Unexpected Self type", t) return AnyType(TypeOfAny.from_error) + elif fullname in TPDICT_NAMES: + if len(t.args) != 1: + self.fail( + "TypedDict[] must have exactly one type argument", t, code=codes.VALID_TYPE + ) + return AnyType(TypeOfAny.from_error) + item = t.args[0] + if not isinstance(item, TypedDictType): + self.fail( + "Argument to TypedDict[] must be a literal dictionary mapping item names to types", + t, + code=codes.VALID_TYPE, + ) + return AnyType(TypeOfAny.from_error) + item.is_pep764 = True + return self.anal_type(item, allow_typed_dict_special_forms=True) return None def get_omitted_any(self, typ: Type, fullname: str | None = None) -> AnyType: @@ -1336,14 +1354,26 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: analyzed = analyzed.item items[item_name] = analyzed if t.fallback.type is MISSING_FALLBACK: # anonymous/inline TypedDict - if INLINE_TYPEDDICT not in self.options.enable_incomplete_feature: + if not t.is_pep764 and INLINE_TYPEDDICT not in self.options.enable_incomplete_feature: self.fail( - "Inline TypedDict is experimental," - " must be enabled with --enable-incomplete-feature=InlineTypedDict", + "Legacy inline TypedDict is experimental," + f" must be enabled with --enable-incomplete-feature={INLINE_TYPEDDICT}", + t, + ) + self.note("Did you mean TypedDict[...]?", t) + if t.is_pep764 and NEW_INLINE_TYPEDDICT not in self.options.enable_incomplete_feature: + self.fail( + "PEP-764 inline TypedDict is experimental," + f" must be enabled with --enable-incomplete-feature={NEW_INLINE_TYPEDDICT}", t, ) required_keys = req_keys fallback = self.named_type("typing._TypedDict") + if t.is_pep764 and t.extra_items_from: + self.fail( + "PEP-764 inline TypedDict does not support merge-in", t, code=codes.VALID_TYPE + ) + return AnyType(TypeOfAny.from_error) for typ in t.extra_items_from: analyzed = self.analyze_type(typ) p_analyzed = get_proper_type(analyzed) @@ -1363,6 +1393,11 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: else: required_keys = t.required_keys fallback = t.fallback + if not t.is_pep764 and not t.items: + self.fail( + "Legacy inline TypedDict must have at least one item", t, code=codes.VALID_TYPE + ) + return AnyType(TypeOfAny.from_error) return TypedDictType(items, required_keys, readonly_keys, fallback, t.line, t.column) def visit_raw_expression_type(self, t: RawExpressionType) -> Type: diff --git a/mypy/types.py b/mypy/types.py index 41a958ae93cc..f6c44883f782 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2575,6 +2575,7 @@ class TypedDictType(ProperType): "fallback", "extra_items_from", "to_be_mutated", + "is_pep764", ) items: dict[str, Type] # item_name -> item_type @@ -2603,6 +2604,7 @@ def __init__( self.can_be_false = len(self.required_keys) == 0 self.extra_items_from = [] self.to_be_mutated = False + self.is_pep764 = False def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_typeddict_type(self) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 88c02f70488c..5c15002ec7c7 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -615,7 +615,8 @@ e: Literal[dummy()] # E: Invalid type: Literal[...] cannot contain a from typing import Literal a: Literal[{"a": 1, "b": 2}] # E: Parameter 1 of Literal[...] is invalid b: Literal[{1, 2, 3}] # E: Invalid type: Literal[...] cannot contain arbitrary expressions -c: {"a": 1, "b": 2} # E: Inline TypedDict is experimental, must be enabled with --enable-incomplete-feature=InlineTypedDict \ +c: {"a": 1, "b": 2} # E: Legacy inline TypedDict is experimental, must be enabled with --enable-incomplete-feature=InlineTypedDict \ + # N: Did you mean TypedDict[...]? \ # E: Invalid type: try using Literal[1] instead? \ # E: Invalid type: try using Literal[2] instead? d: {1, 2, 3} # E: Invalid type comment or annotation diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 2f3d5e08dab3..864001ee2215 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1733,6 +1733,17 @@ type Y[T] = {"item": T, **Y[T]} # E: Overwriting TypedDict field "item" while m [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] +[case testTypedDictInlinePEP764YesNewStyleAlias] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypedDict +type X[T] = TypedDict[{"item": T, "other": X[T] | None}] +x: X[str] +reveal_type(x) # N: Revealed type is "TypedDict({'item': builtins.str, 'other': Union[..., None]})" +if x["other"] is not None: + reveal_type(x["other"]["item"]) # N: Revealed type is "builtins.str" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + [case testPEP695UsingIncorrectExpressionsInTypeVariableBound] type X[T: (yield 1)] = Any # E: Yield expression cannot be used as a type variable bound type Y[T: (yield from [])] = Any # E: Yield expression cannot be used as a type variable bound diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 47c8a71ba0e3..b45dd3d0de45 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -3659,7 +3659,7 @@ reveal_type(x) # N: # N: Revealed type is "TypedDict({'int': builtins.int, 'st [case testTypedDictInlineNoEmpty] # flags: --enable-incomplete-feature=InlineTypedDict -x: {} # E: Invalid type comment or annotation +x: {} # E: Legacy inline TypedDict must have at least one item reveal_type(x) # N: Revealed type is "Any" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] @@ -3707,6 +3707,119 @@ reveal_type(x) # N: Revealed type is "TypedDict({'a': builtins.int, 'b': builti [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] +[case testTypedDictInlinePEP764OldStyleAlias] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypedDict +X = TypedDict[{"int": int, "str": str}] +x: X +reveal_type(x) # N: Revealed type is "TypedDict({'int': builtins.int, 'str': builtins.str})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictInlinePEP764MidStyleAlias] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypedDict +from typing_extensions import TypeAlias +X: TypeAlias = TypedDict[{"int": int, "str": str}] +x: X +reveal_type(x) # N: Revealed type is "TypedDict({'int': builtins.int, 'str': builtins.str})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictInlinePEP764OYesEmpty] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypedDict +x: TypedDict[{}] +reveal_type(x) # N: Revealed type is "TypedDict({})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + + +[case testTypedDictInlinePEP764NotRequired] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import NotRequired, TypedDict + +x: TypedDict[{"one": int, "other": NotRequired[int]}] +x = {"one": 1} # OK +y: TypedDict[{"one": int, "other": int}] +y = {"one": 1} # E: Expected TypedDict keys ("one", "other") but found only key "one" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictInlinePEP764ReadOnly] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import ReadOnly, TypedDict + +x: TypedDict[{"one": int, "other": ReadOnly[int]}] +x["one"] = 1 # ok +x["other"] = 1 # E: ReadOnly TypedDict key "other" TypedDict is mutated +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictInlinePEP764NestedSchema] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypedDict +def nested() -> TypedDict[{"one": str, "other": TypedDict[{"a": int, "b": int}]}]: + if bool(): + return {"one": "yes", "other": {"a": 1, "b": 2}} # OK + else: + return {"one": "no", "other": {"a": 1, "b": "2"}} # E: Incompatible types (expression has type "str", TypedDict item "b" has type "int") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictInlinePEP764MergeAnother] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypeVar, TypedDict +from typing_extensions import TypeAlias + +T = TypeVar("T") +X: TypeAlias = TypedDict[{"item": T}] +x: TypedDict[{"a": int, **X[str], "b": int}] # E: PEP-764 inline TypedDict does not support merge-in +reveal_type(x) # N: Revealed type is "Any" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypedDictInlinePEP764BadArg] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypedDict +x: TypedDict[int] # E: Argument to TypedDict[] must be a literal dictionary mapping item names to types +reveal_type(x) # N: Revealed type is "Any" +y: TypedDict[{1: str}] # E: Argument to TypedDict[] must be a literal dictionary mapping item names to types +reveal_type(y) # N: Revealed type is "Any" +z: TypedDict[{"a": 1}] # E: Invalid type: try using Literal[1] instead? +reveal_type(z) # N: Revealed type is "TypedDict({'a': Any})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictInlinePEP764TooManyArgs] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypedDict +x: TypedDict[{"a": int}, "foo"] # E: TypedDict[] must have exactly one type argument +reveal_type(x) # N: Revealed type is "Any" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictInlinePEP764TypeVarValid] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import Generic, TypeVar, TypedDict +T = TypeVar("T") +class X(Generic[T]): + attr: TypedDict[{'name': T}] +def f(arg: T) -> TypedDict[{'name': T}]: ... +Y = TypedDict[{'name': T}] +reveal_type(X[int]().attr['name']) # N: Revealed type is "builtins.int" +reveal_type(f('a')['name']) # N: Revealed type is "builtins.str" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictInlinePEP764TypeVarInvalid] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypeVar, TypedDict +T = TypeVar('T') +def f(): + X = TypedDict[{'name': T}] # TODO: emit error - this is invalid per PEP-764 +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] # ReadOnly # See: https://peps.python.org/pep-0705 From e31a00cd4da9d1c310030d420c6e280dd9dfa9af Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 5 Apr 2025 16:18:20 -0400 Subject: [PATCH 2/7] Add get_proper_type --- mypy/typeanal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index d97ea662c4dd..97866238d961 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -819,7 +819,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ ) return AnyType(TypeOfAny.from_error) item = t.args[0] - if not isinstance(item, TypedDictType): + if not isinstance(get_proper_type(item), TypedDictType): self.fail( "Argument to TypedDict[] must be a literal dictionary mapping item names to types", t, From 84edf6da6c30b05140f04fb26ab4315e24d32750 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 5 Apr 2025 16:33:28 -0400 Subject: [PATCH 3/7] Typo --- mypy/typeanal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 97866238d961..afc8daa44990 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -826,7 +826,8 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ code=codes.VALID_TYPE, ) return AnyType(TypeOfAny.from_error) - item.is_pep764 = True + else: + item.is_pep764 = True return self.anal_type(item, allow_typed_dict_special_forms=True) return None From caafa1724f50479730862c9c0817235265461cc0 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 5 Apr 2025 16:41:19 -0400 Subject: [PATCH 4/7] Add hint, satisfy self type-checking --- mypy/typeanal.py | 5 ++--- mypy/types.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index afc8daa44990..7721cd422daa 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -819,15 +819,14 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ ) return AnyType(TypeOfAny.from_error) item = t.args[0] - if not isinstance(get_proper_type(item), TypedDictType): + if not isinstance(item, TypedDictType): # type: ignore[misc] self.fail( "Argument to TypedDict[] must be a literal dictionary mapping item names to types", t, code=codes.VALID_TYPE, ) return AnyType(TypeOfAny.from_error) - else: - item.is_pep764 = True + item.is_pep764 = True return self.anal_type(item, allow_typed_dict_special_forms=True) return None diff --git a/mypy/types.py b/mypy/types.py index f6c44883f782..f17c4f87de5f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2585,6 +2585,7 @@ class TypedDictType(ProperType): extra_items_from: list[ProperType] # only used during semantic analysis to_be_mutated: bool # only used in a plugin for `.update`, `|=`, etc + is_pep764: bool def __init__( self, From 9ff1cf9ca955f6b9bbbb178f55a2ee9e67457574 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 7 Apr 2025 10:19:27 -0400 Subject: [PATCH 5/7] Address misc review comments --- docs/source/typed_dict.rst | 2 +- test-data/unit/check-typeddict.test | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/source/typed_dict.rst b/docs/source/typed_dict.rst index 990140f1d3f8..93959ad9db70 100644 --- a/docs/source/typed_dict.rst +++ b/docs/source/typed_dict.rst @@ -334,5 +334,5 @@ to use inline TypedDict syntax. For example: type Z = {"a": int, "b": int} # same as above (Python 3.12+ only) This restriction does not apply to the :pep:`764` syntax. - Also, due to incompatibility with runtime type-checking it is strongly recommended + Also, due to incompatibility with runtime type-checking, it is strongly recommended to *not* use legacy inline syntax in union types. diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index b45dd3d0de45..c3af57a31d2b 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -3821,6 +3821,15 @@ def f(): [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] +[case testTypedDictInlinePEP764UsesExtensionSyntax] +# flags: --enable-incomplete-feature=NewInlineTypedDict +from typing import TypedDict +x: TypedDict[{'name': str, 'production': {'location': str}}] # E: Legacy inline TypedDict is experimental, must be enabled with --enable-incomplete-feature=InlineTypedDict \ + # N: Did you mean TypedDict[...]? +reveal_type(x) # N: Revealed type is "TypedDict({'name': builtins.str, 'production': TypedDict({'location': builtins.str})})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + # ReadOnly # See: https://peps.python.org/pep-0705 From 440ca8275ba25c6105f7ffce88ab8339c9474c3a Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Thu, 17 Apr 2025 09:30:03 -0400 Subject: [PATCH 6/7] Tweak error message: "PEP-764" -> "PEP 764" --- mypy/typeanal.py | 4 ++-- test-data/unit/check-typeddict.test | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 7721cd422daa..3d745099753a 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1363,7 +1363,7 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: self.note("Did you mean TypedDict[...]?", t) if t.is_pep764 and NEW_INLINE_TYPEDDICT not in self.options.enable_incomplete_feature: self.fail( - "PEP-764 inline TypedDict is experimental," + "PEP 764 inline TypedDict is experimental," f" must be enabled with --enable-incomplete-feature={NEW_INLINE_TYPEDDICT}", t, ) @@ -1371,7 +1371,7 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: fallback = self.named_type("typing._TypedDict") if t.is_pep764 and t.extra_items_from: self.fail( - "PEP-764 inline TypedDict does not support merge-in", t, code=codes.VALID_TYPE + "PEP 764 inline TypedDict does not support merge-in", t, code=codes.VALID_TYPE ) return AnyType(TypeOfAny.from_error) for typ in t.extra_items_from: diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index c3af57a31d2b..6ff7dd4b6c51 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -3774,7 +3774,7 @@ from typing_extensions import TypeAlias T = TypeVar("T") X: TypeAlias = TypedDict[{"item": T}] -x: TypedDict[{"a": int, **X[str], "b": int}] # E: PEP-764 inline TypedDict does not support merge-in +x: TypedDict[{"a": int, **X[str], "b": int}] # E: PEP 764 inline TypedDict does not support merge-in reveal_type(x) # N: Revealed type is "Any" [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] From e45ef52f96fbcca1ee74ca964cc5151bc9837a77 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Thu, 17 Apr 2025 09:36:02 -0400 Subject: [PATCH 7/7] Add test case for when new syntax feature flag is not enabled --- test-data/unit/check-typeddict.test | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 6ff7dd4b6c51..3a43b8a82f78 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -3707,6 +3707,12 @@ reveal_type(x) # N: Revealed type is "TypedDict({'a': builtins.int, 'b': builti [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] +[case testTypedDictInlinePEP764NotEnabled] +from typing import TypedDict +x: TypedDict[{"a": int}] # E: PEP 764 inline TypedDict is experimental, must be enabled with --enable-incomplete-feature=NewInlineTypedDict +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testTypedDictInlinePEP764OldStyleAlias] # flags: --enable-incomplete-feature=NewInlineTypedDict from typing import TypedDict