33# Licensed under the MIT License. See License.txt in the project root for
44# license information.
55# --------------------------------------------------------------------------
6+ import keyword
7+ import re
68from typing import Optional
79from ..models import ModelType , CodeModel
810from ..models .imports import FileImport , ImportType
1214from .import_serializer import FileImportSerializer
1315from .base_serializer import BaseSerializer
1416
17+ # Python builtin type names that can be shadowed by TypedDict field wire_names.
18+ # When a field name matches one of these, all references to that builtin in type
19+ # annotations within the same class are qualified as builtins.X.
20+ _BUILTIN_TYPE_NAMES = frozenset ({
21+ "int" , "str" , "float" , "bool" , "list" , "dict" , "tuple" , "set" ,
22+ "bytes" , "type" , "object" , "complex" , "frozenset" , "bytearray" , "memoryview" ,
23+ })
24+
25+
26+ def _qualify_shadowed_builtins (annotation : str , shadowed : frozenset [str ]) -> str :
27+ """Replace bare builtin type references with builtins.X when shadowed by a field name."""
28+ if not shadowed :
29+ return annotation
30+ for name in shadowed :
31+ annotation = re .sub (rf"\b{ name } \b" , f"builtins.{ name } " , annotation )
32+ return annotation
33+
1534
1635class TypesSerializer (BaseSerializer ):
1736 def __init__ (
@@ -39,7 +58,6 @@ def discriminated_base_models(self) -> list[ModelType]:
3958 """
4059 bases = [m for m in self ._models if m .base != "json" and m .discriminated_subtypes ]
4160 base_names = {m .name for m in bases }
42- # Sort: models whose subtypes include other discriminated bases must come after them
4361 sorted_bases : list [ModelType ] = []
4462 visited : set [str ] = set ()
4563
@@ -62,6 +80,30 @@ def discriminated_subtypes_union(self, model: ModelType) -> str:
6280 subtype_names = [s .name for s in subtypes ]
6381 return f"{ model .name } = Union[{ ', ' .join (subtype_names )} ]"
6482
83+ @staticmethod
84+ def has_keyword_wire_names (model : ModelType ) -> bool :
85+ """Whether any property wire_name is a Python keyword (requires functional TypedDict form)."""
86+ return any (keyword .iskeyword (p .wire_name ) for p in model .properties )
87+
88+ @staticmethod
89+ def get_shadowed_builtins (model : ModelType ) -> frozenset [str ]:
90+ """Return the set of builtin type names shadowed by property wire_names in this model.
91+
92+ Only includes a builtin if it is both used as a wire_name AND referenced
93+ in a type annotation within the same model (otherwise no shadowing occurs).
94+ """
95+ wire_builtins = {p .wire_name for p in model .properties if p .wire_name in _BUILTIN_TYPE_NAMES }
96+ if not wire_builtins :
97+ return frozenset ()
98+ # Check which of these builtins actually appear in type annotations
99+ used = set ()
100+ for prop in model .properties :
101+ annotation = prop .type_annotation ()
102+ for name in wire_builtins :
103+ if re .search (rf"\b{ name } \b" , annotation ):
104+ used .add (name )
105+ return frozenset (used )
106+
65107 def imports (self ) -> FileImport :
66108 file_import = FileImport (self .code_model )
67109
@@ -72,6 +114,7 @@ def imports(self) -> FileImport:
72114 if self .discriminated_base_models :
73115 file_import .add_submodule_import ("typing" , "Union" , ImportType .STDLIB )
74116 has_required = False
117+ needs_builtins = False
75118 for model in td_models :
76119 file_import .merge (
77120 model .imports (
@@ -90,6 +133,8 @@ def imports(self) -> FileImport:
90133 )
91134 if not (prop .optional or prop .client_default_value is not None ):
92135 has_required = True
136+ if self .get_shadowed_builtins (model ):
137+ needs_builtins = True
93138 for parent in model .parents :
94139 if parent .client_namespace != model .client_namespace and not parent .discriminated_subtypes :
95140 file_import .add_submodule_import (
@@ -102,27 +147,55 @@ def imports(self) -> FileImport:
102147 )
103148 if has_required :
104149 file_import .add_submodule_import ("typing_extensions" , "Required" , ImportType .STDLIB )
150+ if needs_builtins :
151+ file_import .add_import ("builtins" , ImportType .STDLIB )
105152 return file_import
106153
107154 def declare_model (self , model : ModelType ) -> str :
155+ """Generate the class declaration or functional form for a TypedDict model.
156+
157+ Uses functional form when any property wire_name is a Python keyword
158+ (e.g. 'and', 'class') since keywords can't be identifiers in class bodies.
159+ """
160+ if self .has_keyword_wire_names (model ):
161+ return "" # functional form is rendered separately
108162 non_discriminated_parents = [p for p in model .parents if not p .discriminated_subtypes ]
109163 if non_discriminated_parents :
110164 basename = ", " .join ([m .name for m in non_discriminated_parents ])
111165 return f"class { model .name } ({ basename } ):{ model .pylint_disable ()} "
112166 return f"class { model .name } (TypedDict, total=False):{ model .pylint_disable ()} "
113167
114- # Python builtin type names that can be shadowed by TypedDict field names
115- _BUILTIN_TYPE_NAMES = frozenset ({
116- "int" , "str" , "float" , "bool" , "list" , "dict" , "tuple" , "set" ,
117- "bytes" , "type" , "object" , "complex" , "frozenset" , "bytearray" , "memoryview" ,
118- })
168+ def declare_functional_model (self , model : ModelType ) -> str :
169+ """Generate a functional-form TypedDict for models with keyword wire_names.
170+
171+ Functional form is required when any field name is a Python keyword.
172+ All fields (including inherited) are included since functional form
173+ can't specify a base class.
174+ """
175+ shadowed = self .get_shadowed_builtins (model )
176+ entries : list [str ] = []
177+ for prop in model .properties :
178+ type_annotation = prop .type_annotation (
179+ serialize_namespace = self .serialize_namespace ,
180+ serialize_namespace_type = NamespaceType .TYPES_FILE ,
181+ )
182+ type_annotation = _qualify_shadowed_builtins (type_annotation , shadowed )
183+ is_optional = prop .optional or prop .client_default_value is not None
184+ if is_optional :
185+ entries .append (f' "{ prop .wire_name } ": { type_annotation } ,' )
186+ else :
187+ entries .append (f' "{ prop .wire_name } ": Required[{ type_annotation } ],' )
188+ fields = "\n " .join (entries )
189+ return f'{ model .name } = TypedDict("{ model .name } ", {{\n { fields } \n }}, total=False)'
119190
120191 @staticmethod
121192 def get_properties_to_declare (model : ModelType ) -> list [Property ]:
193+ if TypesSerializer .has_keyword_wire_names (model ):
194+ return [] # functional form handles all properties
122195 non_discriminated_parents = [p for p in model .parents if not p .discriminated_subtypes ]
123196 if non_discriminated_parents :
124197 parent_properties = [p for bm in non_discriminated_parents for p in bm .properties ]
125- properties_to_declare = [
198+ return [
126199 p
127200 for p in model .properties
128201 if not any (
@@ -132,20 +205,14 @@ def get_properties_to_declare(model: ModelType) -> list[Property]:
132205 for pp in parent_properties
133206 )
134207 ]
135- else :
136- properties_to_declare = list (model .properties )
137- # Move properties whose wire_name shadows a Python builtin type to the end,
138- # so they don't shadow the builtin in subsequent type annotations.
139- properties_to_declare .sort (
140- key = lambda p : p .wire_name in TypesSerializer ._BUILTIN_TYPE_NAMES
141- )
142- return properties_to_declare
208+ return list (model .properties )
143209
144- def declare_property (self , prop : Property ) -> str :
210+ def declare_property (self , prop : Property , shadowed_builtins : frozenset [ str ] ) -> str :
145211 type_annotation = prop .type_annotation (
146212 serialize_namespace = self .serialize_namespace ,
147213 serialize_namespace_type = NamespaceType .TYPES_FILE ,
148214 )
215+ type_annotation = _qualify_shadowed_builtins (type_annotation , shadowed_builtins )
149216 is_optional = prop .optional or prop .client_default_value is not None
150217 if is_optional :
151218 return f"{ prop .wire_name } : { type_annotation } "
0 commit comments