@@ -286,6 +286,7 @@ def generate_schema() -> None:
286286 "black" ,
287287 "isort" ,
288288 "--use-annotated" ,
289+ "--use-field-description" ,
289290 "--snake-case-field" ,
290291 ]
291292
@@ -511,6 +512,7 @@ def postprocess_generated_schema(output_path: Path) -> list[str]:
511512 _ProcessingStep ("apply default overrides" , _apply_default_overrides ),
512513 _ProcessingStep ("restore required nullable fields" , _restore_required_nullable_fields ),
513514 _ProcessingStep ("ensure custom BaseModel" , _ensure_custom_base_model ),
515+ _ProcessingStep ("enable RootModel attribute docstrings" , _enable_root_model_attribute_docstrings ),
514516 _ProcessingStep ("inject field validators" , _inject_field_validators ),
515517 _ProcessingStep ("inject deserialize defaults" , _inject_deserialize_defaults ),
516518 _ProcessingStep ("inject schema aliases" , _inject_schema_aliases ),
@@ -675,7 +677,7 @@ def _ensure_custom_base_model(content: str) -> str:
675677 lines [idx ] = "from pydantic import " + ", " .join (new_imports )
676678 to_insert = textwrap .dedent ("""\
677679 class BaseModel(_BaseModel):
678- model_config = ConfigDict(populate_by_name=True)
680+ model_config = ConfigDict(populate_by_name=True, use_attribute_docstrings=True )
679681
680682 def __getattr__(self, item: str) -> Any:
681683 if item.lower() != item:
@@ -691,6 +693,24 @@ def __getattr__(self, item: str) -> Any:
691693 return "\n " .join (lines ) + "\n "
692694
693695
696+ def _enable_root_model_attribute_docstrings (content : str ) -> str :
697+ lines = content .splitlines (keepends = True )
698+ tree = ast .parse (content )
699+ insertion_points = [
700+ node .body [0 ].lineno - 1
701+ for node in tree .body
702+ if isinstance (node , ast .ClassDef )
703+ and node .body
704+ and any (
705+ isinstance (base , ast .Subscript ) and isinstance (base .value , ast .Name ) and base .value .id == "RootModel"
706+ for base in node .bases
707+ )
708+ ]
709+ for line_index in reversed (insertion_points ):
710+ lines .insert (line_index , " model_config = ConfigDict(use_attribute_docstrings=True)\n \n " )
711+ return "" .join (lines )
712+
713+
694714def _ensure_pydantic_import (content : str , name : str ) -> str :
695715 """Add *name* to the ``from pydantic import ...`` line if not already present."""
696716 lines = content .splitlines ()
@@ -945,8 +965,14 @@ def _restore_required_nullable_fields(content: str, schema: dict[str, Any] | Non
945965 def restore_block (match : re .Match [str ], _field_names : list [str ] = field_names ) -> str :
946966 header , block = match .group (1 ), match .group (2 )
947967 for field_name in _field_names :
948- field_pattern = re .compile (rf"(\n\s+{ re .escape (field_name )} :\s+Annotated\[[\s\S]*?\n\s+\]\s*)=\s*None" )
949- block = field_pattern .sub (r"\1" , block , count = 1 )
968+ field_patterns = (
969+ re .compile (rf"(\n\s+{ re .escape (field_name )} :[^\n]*?)\s*=\s*None(?=\n)" ),
970+ re .compile (rf"(\n\s+{ re .escape (field_name )} :[^\n]*\[\s*\n[\s\S]*?\n\s+\]\s*)=\s*None" ),
971+ )
972+ for field_pattern in field_patterns :
973+ block , count = field_pattern .subn (r"\1" , block , count = 1 )
974+ if count :
975+ break
950976 return header + block
951977
952978 content = class_pattern .sub (restore_block , content , count = 1 )
@@ -955,18 +981,14 @@ def restore_block(match: re.Match[str], _field_names: list[str] = field_names) -
955981
956982def _apply_field_overrides (content : str ) -> str :
957983 for class_name , field_name , new_type , optional in FIELD_TYPE_OVERRIDES :
958- if optional :
959- pattern = re .compile (
960- rf"(class { class_name } \(BaseModel\):.*?\n\s+{ field_name } :\s+Annotated\[\s*)Optional\[str]," ,
961- re .DOTALL ,
962- )
963- content , count = pattern .subn (rf"\1Optional[{ new_type } ]," , content )
964- else :
965- pattern = re .compile (
966- rf"(class { class_name } \(BaseModel\):.*?\n\s+{ field_name } :\s+Annotated\[\s*)str," ,
967- re .DOTALL ,
968- )
969- content , count = pattern .subn (rf"\1{ new_type } ," , content )
984+ old_type = "Optional[str]" if optional else "str"
985+ replacement_type = f"Optional[{ new_type } ]" if optional else new_type
986+ pattern = re .compile (
987+ rf"(class { re .escape (class_name )} \(BaseModel\):.*?\n\s+{ re .escape (field_name )} :\s+"
988+ rf"(?:Annotated\[\s*)?){ re .escape (old_type )} (?=\s*(?:,|=|\n))" ,
989+ re .DOTALL ,
990+ )
991+ content , count = pattern .subn (rf"\g<1>{ replacement_type } " , content , count = 1 )
970992 if count == 0 :
971993 print (
972994 f"Warning: failed to apply type override for { class_name } .{ field_name } -> { new_type } " ,
@@ -992,7 +1014,8 @@ def replace_block(
9921014 field_patterns : tuple [tuple [re .Pattern [str ], Callable [[re .Match [str ]], str ]], ...] = (
9931015 (
9941016 re .compile (
995- rf"(\n\s+{ _field_name } :.*?\]\s*=\s*)([\s\S]*?)(?=\n\s{{4}}[A-Za-z_]|$)" ,
1017+ rf"(\n\s+{ _field_name } :.*?\]\s*=\s*)([\s\S]*?)"
1018+ rf"(?=\n\s{{4}}(?:[A-Za-z_][A-Za-z0-9_]*\s*:|[rRuUbBfF]*(?:'''|\"\"\"))|$)" ,
9961019 re .DOTALL ,
9971020 ),
9981021 lambda m , _rep = _replacement : m .group (1 ) + _rep ,
0 commit comments