@@ -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,124 @@ 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+ # Only constr is included because it's the only type with a 'pattern' parameter
625+ # that can trigger lookaround regex detection. Other constrained types (conint,
626+ # confloat, condecimal, conbytes) don't have pattern constraints, so they will
627+ # never need base_type_hint conversion in the regex_engine context.
628+ _CONSTRAINED_TYPE_TO_BASE : ClassVar [dict [str , str ]] = {
629+ "constr" : "str" ,
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_ : # pragma: no branch
647+ base_type = self ._CONSTRAINED_TYPE_TO_BASE .get (type_ )
648+ if base_type is None :
649+ # Not a constrained type we convert (e.g., conint, confloat)
650+ # Return the full type_hint with kwargs to avoid returning bare function name
651+ return self .type_hint
652+ if self .is_optional and base_type != ANY : # pragma: no cover
653+ return get_optional_type (base_type , self .use_union_operator )
654+ return base_type
655+
656+ type_ : str | None = self .alias or self .type
657+ if not type_ :
658+ if self .is_tuple : # pragma: no cover
659+ tuple_type = STANDARD_TUPLE if self .use_standard_collections else TUPLE
660+ inner_types = [item .base_type_hint or ANY for item in self .data_types ]
661+ type_ = f"{ tuple_type } [{ ', ' .join (inner_types )} ]" if inner_types else f"{ tuple_type } [()]"
662+ elif self .is_union :
663+ data_types : list [str ] = []
664+ for data_type in self .data_types :
665+ data_type_type = data_type .base_type_hint
666+ if not data_type_type or data_type_type in data_types : # pragma: no cover
667+ continue
668+
669+ if data_type_type == NONE :
670+ self .is_optional = True
671+ continue
672+
673+ non_optional_data_type_type = _remove_none_from_union (
674+ data_type_type , use_union_operator = self .use_union_operator
675+ )
676+
677+ if non_optional_data_type_type != data_type_type : # pragma: no cover
678+ self .is_optional = True
679+
680+ data_types .append (non_optional_data_type_type )
681+ if not data_types : # pragma: no cover
682+ type_ = ANY
683+ self .import_ = self .import_ or IMPORT_ANY
684+ elif len (data_types ) == 1 :
685+ type_ = data_types [0 ]
686+ elif self .use_union_operator :
687+ type_ = UNION_OPERATOR_DELIMITER .join (data_types )
688+ else : # pragma: no cover
689+ type_ = f"{ UNION_PREFIX } { UNION_DELIMITER .join (data_types )} ]"
690+ elif len (self .data_types ) == 1 :
691+ type_ = self .data_types [0 ].base_type_hint
692+ elif self .enum_member_literals : # pragma: no cover
693+ parts = [f"{ enum_class } .{ member } " for enum_class , member in self .enum_member_literals ]
694+ type_ = f"{ LITERAL } [{ ', ' .join (parts )} ]"
695+ elif self .literals : # pragma: no cover
696+ type_ = f"{ LITERAL } [{ ', ' .join (repr (literal ) for literal in self .literals )} ]"
697+ elif self .reference : # pragma: no cover
698+ type_ = self .reference .short_name
699+ type_ = self ._get_wrapped_reference_type_hint (type_ )
700+ else : # pragma: no cover
701+ type_ = ""
702+ if self .reference : # pragma: no cover
703+ source = self .reference .source
704+ if isinstance (source , Nullable ) and source .nullable :
705+ self .is_optional = True
706+ if self .is_list :
707+ if self .use_generic_container :
708+ list_ = SEQUENCE
709+ elif self .use_standard_collections :
710+ list_ = STANDARD_LIST
711+ else : # pragma: no cover
712+ list_ = LIST
713+ type_ = f"{ list_ } [{ type_ } ]" if type_ else list_
714+ elif self .is_set : # pragma: no cover
715+ if self .use_generic_container :
716+ set_ = STANDARD_FROZEN_SET if self .use_standard_collections else FROZEN_SET
717+ elif self .use_standard_collections :
718+ set_ = STANDARD_SET
719+ else :
720+ set_ = SET
721+ type_ = f"{ set_ } [{ type_ } ]" if type_ else set_
722+ elif self .is_dict :
723+ if self .use_generic_container :
724+ dict_ = MAPPING
725+ elif self .use_standard_collections :
726+ dict_ = STANDARD_DICT
727+ else : # pragma: no cover
728+ dict_ = DICT
729+ if self .dict_key or type_ :
730+ key = self .dict_key .base_type_hint if self .dict_key else STR
731+ type_ = f"{ dict_ } [{ key } , { type_ or ANY } ]"
732+ else : # pragma: no cover
733+ type_ = dict_
734+
735+ if self .is_optional and type_ != ANY :
736+ return get_optional_type (type_ , self .use_union_operator )
737+ if self .is_func : # pragma: no cover
738+ return f"{ type_ } ()"
739+ return type_
740+
621741
622742DataTypeT = TypeVar ("DataTypeT" , bound = DataType )
623743
0 commit comments