11import json
22import re
33import subprocess
4+ from collections import defaultdict
45from pathlib import Path
56from textwrap import dedent
67from typing import (
2425 FileContents ,
2526 HandshakeType ,
2627 ListTypeExpr ,
28+ LiteralType ,
2729 LiteralTypeExpr ,
2830 ModuleName ,
2931 NoneTypeExpr ,
3335 TypeName ,
3436 UnionTypeExpr ,
3537 extract_inner_type ,
38+ normalize_special_chars ,
3639 render_literal_type ,
3740 render_type_expr ,
3841)
@@ -396,9 +399,12 @@ def {_field_name}(
396399 case NoneTypeExpr ():
397400 typeddict_encoder .append ("None" )
398401 case other :
399- _o2 : DictTypeExpr | OpenUnionTypeExpr | UnionTypeExpr = (
400- other
401- )
402+ _o2 : (
403+ DictTypeExpr
404+ | OpenUnionTypeExpr
405+ | UnionTypeExpr
406+ | LiteralType
407+ ) = other
402408 raise ValueError (f"What does it mean to have { _o2 } here?" )
403409 if permit_unknown_members :
404410 union = _make_open_union_type_expr (any_of )
@@ -491,7 +497,7 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
491497 return (NoneTypeExpr (), [], [], set ())
492498 elif type .type == "Date" :
493499 typeddict_encoder .append ("TODO: dstewart" )
494- return (TypeName ("datetime.datetime" ), [], [], set ())
500+ return (LiteralType ("datetime.datetime" ), [], [], set ())
495501 elif type .type == "array" and type .items :
496502 type_name , module_info , type_chunks , encoder_names = encode_type (
497503 type .items ,
@@ -524,6 +530,9 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
524530 # lambda x: ... vs lambda _: {}
525531 needs_binding = False
526532 encoder_names = set ()
533+ # Track effective field names to detect collisions after normalization
534+ # Maps effective name -> list of original field names
535+ effective_field_names : defaultdict [str , list [str ]] = defaultdict (list )
527536 if type .properties :
528537 needs_binding = True
529538 typeddict_encoder .append ("{" )
@@ -653,19 +662,37 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
653662 value = ""
654663 if base_model != "TypedDict" :
655664 value = f"= { field_value } "
665+ # Track $kind -> "kind" mapping for collision detection
666+ effective_field_names ["kind" ].append (name )
667+
656668 current_chunks .append (
657669 f" kind: Annotated[{ render_type_expr (type_name )} , Field(alias={
658670 repr (name )
659671 } )]{ value } "
660672 )
661673 else :
674+ specialized_name = normalize_special_chars (name )
675+ effective_name = name
676+ extras = []
677+ if name != specialized_name :
678+ if base_model != "BaseModel" :
679+ # TODO: alias support for TypedDict
680+ raise ValueError (
681+ f"Field { name } is not a valid Python identifier, but it is in the schema" # noqa: E501
682+ )
683+ # Pydantic doesn't allow leading underscores in field names
684+ effective_name = specialized_name .lstrip ("_" )
685+ extras .append (f"alias={ repr (name )} " )
686+
687+ effective_field_names [effective_name ].append (name )
688+
662689 if name not in type .required :
663690 if base_model == "TypedDict" :
664691 current_chunks .append (
665692 reindent (
666693 " " ,
667694 f"""\
668- { name } : NotRequired[{
695+ { effective_name } : NotRequired[{
669696 render_type_expr (
670697 UnionTypeExpr ([type_name , NoneTypeExpr ()])
671698 )
@@ -674,11 +701,13 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
674701 )
675702 )
676703 else :
704+ extras .append ("default=None" )
705+
677706 current_chunks .append (
678707 reindent (
679708 " " ,
680709 f"""\
681- { name } : {
710+ { effective_name } : {
682711 render_type_expr (
683712 UnionTypeExpr (
684713 [
@@ -687,15 +716,30 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
687716 ]
688717 )
689718 )
690- } = None
719+ } = Field( { ", " . join ( extras ) } )
691720 """ ,
692721 )
693722 )
694723 else :
724+ extras_str = ""
725+ if len (extras ) != 0 :
726+ extras_str = f" = Field({ ', ' .join (extras )} )"
727+
695728 current_chunks .append (
696- f" { name } : { render_type_expr (type_name )} "
729+ f" { effective_name } : { render_type_expr (type_name )} { extras_str } " # noqa: E501
697730 )
698731 typeddict_encoder .append ("," )
732+
733+ # Check for field name collisions after processing all fields
734+ for effective_name , original_names in effective_field_names .items ():
735+ if len (original_names ) > 1 :
736+ error_msg = (
737+ f"Field name collision: fields { original_names } all normalize "
738+ f"to the same effective name '{ effective_name } '"
739+ )
740+
741+ raise ValueError (error_msg )
742+
699743 typeddict_encoder .append ("}" )
700744 # exclude_none
701745 typeddict_encoder = (
0 commit comments