@@ -422,9 +422,11 @@ def full_name(self) -> str:
422422
423423 @property
424424 def all_data_types (self ) -> Iterator [DataType ]:
425- """Recursively yield all nested DataTypes including self."""
425+ """Recursively yield all nested DataTypes including self and dict_key ."""
426426 for data_type in self .data_types :
427427 yield from data_type .all_data_types
428+ if self .dict_key :
429+ yield from self .dict_key .all_data_types
428430 yield self
429431
430432 def find_source (self , source_type : type [SourceT ]) -> SourceT | None :
@@ -618,6 +620,120 @@ def is_union(self) -> bool:
618620 """Return whether this DataType represents a union of multiple types."""
619621 return len (self .data_types ) > 1
620622
623+ # Mapping from constrained type functions to their base Python types
624+ _CONSTRAINED_TYPE_TO_BASE : ClassVar [dict [str , str ]] = {
625+ "constr" : "str" ,
626+ "conint" : "int" ,
627+ "confloat" : "float" ,
628+ "condecimal" : "Decimal" ,
629+ "conbytes" : "bytes" ,
630+ }
631+
632+ @property
633+ def base_type_hint (self ) -> str : # noqa: PLR0912, PLR0915
634+ """Return the base type hint without constrained type kwargs.
635+
636+ For types like constr(pattern=..., min_length=...), this returns just 'str'.
637+ This works recursively for nested types like list[constr(pattern=...)] -> list[str].
638+
639+ This is useful when the pattern contains lookaround assertions that require
640+ regex_engine="python-re", which must be set in model_config. In such cases,
641+ the RootModel generic cannot use the constrained type because it would be
642+ evaluated at class definition time before model_config is processed.
643+ """
644+ if self .is_func and self .kwargs :
645+ type_ : str | None = self .alias or self .type
646+ if type_ :
647+ base_type = self ._CONSTRAINED_TYPE_TO_BASE .get (type_ , type_ )
648+ if self .is_optional and base_type != ANY :
649+ return get_optional_type (base_type , self .use_union_operator )
650+ return base_type
651+
652+ type_ : str | None = self .alias or self .type
653+ if not type_ :
654+ if self .is_tuple :
655+ tuple_type = STANDARD_TUPLE if self .use_standard_collections else TUPLE
656+ inner_types = [item .base_type_hint or ANY for item in self .data_types ]
657+ type_ = f"{ tuple_type } [{ ', ' .join (inner_types )} ]" if inner_types else f"{ tuple_type } [()]"
658+ elif self .is_union :
659+ data_types : list [str ] = []
660+ for data_type in self .data_types :
661+ data_type_type = data_type .base_type_hint
662+ if not data_type_type or data_type_type in data_types :
663+ continue
664+
665+ if data_type_type == NONE :
666+ self .is_optional = True
667+ continue
668+
669+ non_optional_data_type_type = _remove_none_from_union (
670+ data_type_type , use_union_operator = self .use_union_operator
671+ )
672+
673+ if non_optional_data_type_type != data_type_type :
674+ self .is_optional = True
675+
676+ data_types .append (non_optional_data_type_type )
677+ if not data_types :
678+ type_ = ANY
679+ self .import_ = self .import_ or IMPORT_ANY
680+ elif len (data_types ) == 1 :
681+ type_ = data_types [0 ]
682+ elif self .use_union_operator :
683+ type_ = UNION_OPERATOR_DELIMITER .join (data_types )
684+ else :
685+ type_ = f"{ UNION_PREFIX } { UNION_DELIMITER .join (data_types )} ]"
686+ elif len (self .data_types ) == 1 :
687+ type_ = self .data_types [0 ].base_type_hint
688+ elif self .enum_member_literals :
689+ parts = [f"{ enum_class } .{ member } " for enum_class , member in self .enum_member_literals ]
690+ type_ = f"{ LITERAL } [{ ', ' .join (parts )} ]"
691+ elif self .literals :
692+ type_ = f"{ LITERAL } [{ ', ' .join (repr (literal ) for literal in self .literals )} ]"
693+ elif self .reference :
694+ type_ = self .reference .short_name
695+ type_ = self ._get_wrapped_reference_type_hint (type_ )
696+ else :
697+ type_ = ""
698+ if self .reference :
699+ source = self .reference .source
700+ if isinstance (source , Nullable ) and source .nullable :
701+ self .is_optional = True
702+ if self .is_list :
703+ if self .use_generic_container :
704+ list_ = SEQUENCE
705+ elif self .use_standard_collections :
706+ list_ = STANDARD_LIST
707+ else :
708+ list_ = LIST
709+ type_ = f"{ list_ } [{ type_ } ]" if type_ else list_
710+ elif self .is_set :
711+ if self .use_generic_container :
712+ set_ = STANDARD_FROZEN_SET if self .use_standard_collections else FROZEN_SET
713+ elif self .use_standard_collections :
714+ set_ = STANDARD_SET
715+ else :
716+ set_ = SET
717+ type_ = f"{ set_ } [{ type_ } ]" if type_ else set_
718+ elif self .is_dict :
719+ if self .use_generic_container :
720+ dict_ = MAPPING
721+ elif self .use_standard_collections :
722+ dict_ = STANDARD_DICT
723+ else :
724+ dict_ = DICT
725+ if self .dict_key or type_ :
726+ key = self .dict_key .base_type_hint if self .dict_key else STR
727+ type_ = f"{ dict_ } [{ key } , { type_ or ANY } ]"
728+ else : # pragma: no cover
729+ type_ = dict_
730+
731+ if self .is_optional and type_ != ANY :
732+ return get_optional_type (type_ , self .use_union_operator )
733+ if self .is_func :
734+ return f"{ type_ } ()"
735+ return type_
736+
621737
622738DataTypeT = TypeVar ("DataTypeT" , bound = DataType )
623739
0 commit comments