88import typing
99import warnings
1010from abc import ABC , abstractmethod
11- from argparse import SUPPRESS , ArgumentParser , HelpFormatter , Namespace , _SubParsersAction
11+ from argparse import SUPPRESS , ArgumentParser , Namespace , RawDescriptionHelpFormatter , _SubParsersAction
1212from collections import deque
1313from dataclasses import asdict , is_dataclass
1414from enum import Enum
1515from pathlib import Path
16+ from textwrap import dedent
1617from types import FunctionType
1718from typing import (
1819 TYPE_CHECKING ,
@@ -424,7 +425,7 @@ class Settings(BaseSettings):
424425 args = get_args (annotation )
425426 if origin_is_union (get_origin (field .annotation )) and len (args ) == 2 and type (None ) in args :
426427 for arg in args :
427- if arg != type ( None ) :
428+ if arg is not None :
428429 annotation = arg
429430 break
430431
@@ -748,7 +749,7 @@ class Cfg(BaseSettings):
748749 elif is_model_class (annotation ) or is_pydantic_dataclass (annotation ):
749750 fields = (
750751 annotation .__pydantic_fields__
751- if is_pydantic_dataclass (annotation )
752+ if is_pydantic_dataclass (annotation ) and hasattr ( annotation , '__pydantic_fields__' )
752753 else cast (BaseModel , annotation ).model_fields
753754 )
754755 # `case_sensitive is None` is here to be compatible with the old behavior.
@@ -812,7 +813,7 @@ def explode_env_vars(self, field_name: str, field: FieldInfo, env_vars: Mapping[
812813 if not allow_json_failure :
813814 raise e
814815 if isinstance (env_var , dict ):
815- if last_key not in env_var or not isinstance (env_val , EnvNoneType ) or env_var [last_key ] is {}:
816+ if last_key not in env_var or not isinstance (env_val , EnvNoneType ) or env_var [last_key ] == {}:
816817 env_var [last_key ] = env_val
817818
818819 return result
@@ -966,7 +967,7 @@ class CliSettingsSource(EnvSettingsSource, Generic[T]):
966967 Defaults to `argparse._SubParsersAction.add_parser`.
967968 add_subparsers_method: The root parser add subparsers (sub-commands) method.
968969 Defaults to `argparse.ArgumentParser.add_subparsers`.
969- formatter_class: A class for customizing the root parser help text. Defaults to `argparse.HelpFormatter `.
970+ formatter_class: A class for customizing the root parser help text. Defaults to `argparse.RawDescriptionHelpFormatter `.
970971 """
971972
972973 def __init__ (
@@ -988,7 +989,7 @@ def __init__(
988989 add_argument_group_method : Callable [..., Any ] | None = ArgumentParser .add_argument_group ,
989990 add_parser_method : Callable [..., Any ] | None = _SubParsersAction .add_parser ,
990991 add_subparsers_method : Callable [..., Any ] | None = ArgumentParser .add_subparsers ,
991- formatter_class : Any = HelpFormatter ,
992+ formatter_class : Any = RawDescriptionHelpFormatter ,
992993 ) -> None :
993994 self .cli_prog_name = (
994995 cli_prog_name if cli_prog_name is not None else settings_cls .model_config .get ('cli_prog_name' , sys .argv [0 ])
@@ -1040,7 +1041,10 @@ def __init__(
10401041
10411042 root_parser = (
10421043 _CliInternalArgParser (
1043- cli_exit_on_error = self .cli_exit_on_error , prog = self .cli_prog_name , description = settings_cls .__doc__
1044+ cli_exit_on_error = self .cli_exit_on_error ,
1045+ prog = self .cli_prog_name ,
1046+ description = None if settings_cls .__doc__ is None else dedent (settings_cls .__doc__ ),
1047+ formatter_class = formatter_class ,
10441048 )
10451049 if root_parser is None
10461050 else root_parser
@@ -1329,7 +1333,11 @@ def _get_resolved_names(
13291333
13301334 def _sort_arg_fields (self , model : type [BaseModel ]) -> list [tuple [str , FieldInfo ]]:
13311335 positional_args , subcommand_args , optional_args = [], [], []
1332- fields = model .__pydantic_fields__ if is_pydantic_dataclass (model ) else model .model_fields
1336+ fields = (
1337+ model .__pydantic_fields__
1338+ if hasattr (model , '__pydantic_fields__' ) and is_pydantic_dataclass (model )
1339+ else model .model_fields
1340+ )
13331341 for field_name , field_info in fields .items ():
13341342 if _CliSubCommand in field_info .metadata :
13351343 if not field_info .is_required ():
@@ -1405,7 +1413,7 @@ def _connect_root_parser(
14051413 add_argument_group_method : Callable [..., Any ] | None = ArgumentParser .add_argument_group ,
14061414 add_parser_method : Callable [..., Any ] | None = _SubParsersAction .add_parser ,
14071415 add_subparsers_method : Callable [..., Any ] | None = ArgumentParser .add_subparsers ,
1408- formatter_class : Any = HelpFormatter ,
1416+ formatter_class : Any = RawDescriptionHelpFormatter ,
14091417 ) -> None :
14101418 self ._root_parser = root_parser
14111419 self ._parse_args = self ._connect_parser_method (parse_args_method , 'parsed_args_method' )
@@ -1424,6 +1432,7 @@ def _connect_root_parser(
14241432 subcommand_prefix = self .env_prefix ,
14251433 group = None ,
14261434 alias_prefixes = [],
1435+ model_default = PydanticUndefined ,
14271436 )
14281437
14291438 def _add_parser_args (
@@ -1435,6 +1444,7 @@ def _add_parser_args(
14351444 subcommand_prefix : str ,
14361445 group : Any ,
14371446 alias_prefixes : list [str ],
1447+ model_default : Any ,
14381448 ) -> ArgumentParser :
14391449 subparsers : Any = None
14401450 alias_path_args : dict [str , str ] = {}
@@ -1459,24 +1469,27 @@ def _add_parser_args(
14591469 field_name ,
14601470 help = field_info .description ,
14611471 formatter_class = self ._formatter_class ,
1462- description = model .__doc__ ,
1472+ description = None if model .__doc__ is None else dedent ( model . __doc__ ) ,
14631473 ),
14641474 model = model ,
14651475 added_args = [],
14661476 arg_prefix = f'{ arg_prefix } { field_name } .' ,
14671477 subcommand_prefix = f'{ subcommand_prefix } { field_name } .' ,
14681478 group = None ,
14691479 alias_prefixes = [],
1480+ model_default = PydanticUndefined ,
14701481 )
14711482 else :
14721483 resolved_names , is_alias_path_only = self ._get_resolved_names (field_name , field_info , alias_path_args )
14731484 arg_flag : str = '--'
14741485 kwargs : dict [str , Any ] = {}
14751486 kwargs ['default' ] = SUPPRESS
1476- kwargs ['help' ] = self ._help_format (field_info )
1487+ kwargs ['help' ] = self ._help_format (field_name , field_info , model_default )
14771488 kwargs ['dest' ] = f'{ arg_prefix } { resolved_names [0 ]} '
14781489 kwargs ['metavar' ] = self ._metavar_format (field_info .annotation )
1479- kwargs ['required' ] = self .cli_enforce_required and field_info .is_required ()
1490+ kwargs ['required' ] = (
1491+ self .cli_enforce_required and field_info .is_required () and model_default is PydanticUndefined
1492+ )
14801493 if kwargs ['dest' ] in added_args :
14811494 continue
14821495 if _annotation_contains_types (
@@ -1504,8 +1517,10 @@ def _add_parser_args(
15041517 arg_flag ,
15051518 arg_names ,
15061519 kwargs ,
1520+ field_name ,
15071521 field_info ,
15081522 resolved_names ,
1523+ model_default = model_default ,
15091524 )
15101525 elif is_alias_path_only :
15111526 continue
@@ -1544,17 +1559,33 @@ def _add_parser_submodels(
15441559 arg_flag : str ,
15451560 arg_names : list [str ],
15461561 kwargs : dict [str , Any ],
1562+ field_name : str ,
15471563 field_info : FieldInfo ,
15481564 resolved_names : tuple [str , ...],
1565+ model_default : Any ,
15491566 ) -> None :
15501567 model_group : Any = None
15511568 model_group_kwargs : dict [str , Any ] = {}
15521569 model_group_kwargs ['title' ] = f'{ arg_names [0 ]} options'
1553- model_group_kwargs ['description' ] = (
1554- sub_models [0 ].__doc__
1555- if self .cli_use_class_docs_for_groups and len (sub_models ) == 1
1556- else field_info .description
1557- )
1570+ model_group_kwargs ['description' ] = field_info .description
1571+ if self .cli_use_class_docs_for_groups and len (sub_models ) == 1 :
1572+ model_group_kwargs ['description' ] = None if sub_models [0 ].__doc__ is None else dedent (sub_models [0 ].__doc__ )
1573+
1574+ if model_default not in (PydanticUndefined , None ):
1575+ if is_model_class (type (model_default )) or is_pydantic_dataclass (type (model_default )):
1576+ model_default = getattr (model_default , field_name )
1577+ else :
1578+ if field_info .default is not PydanticUndefined :
1579+ model_default = field_info .default
1580+ elif field_info .default_factory is not None :
1581+ model_default = field_info .default_factory
1582+ if model_default is None :
1583+ desc_header = f'default: { self .cli_parse_none_str } (undefined)'
1584+ if model_group_kwargs ['description' ] is not None :
1585+ model_group_kwargs ['description' ] = dedent (f'{ desc_header } \n { model_group_kwargs ["description" ]} ' )
1586+ else :
1587+ model_group_kwargs ['description' ] = desc_header
1588+
15581589 if not self .cli_avoid_json :
15591590 added_args .append (arg_names [0 ])
15601591 kwargs ['help' ] = f'set { arg_names [0 ]} from JSON string'
@@ -1569,6 +1600,7 @@ def _add_parser_submodels(
15691600 subcommand_prefix = subcommand_prefix ,
15701601 group = model_group if model_group else model_group_kwargs ,
15711602 alias_prefixes = [f'{ arg_prefix } { name } .' for name in resolved_names [1 :]],
1603+ model_default = model_default ,
15721604 )
15731605
15741606 def _add_parser_alias_paths (
@@ -1658,14 +1690,19 @@ def _metavar_format_recurse(self, obj: Any) -> str:
16581690 def _metavar_format (self , obj : Any ) -> str :
16591691 return self ._metavar_format_recurse (obj ).replace (', ' , ',' )
16601692
1661- def _help_format (self , field_info : FieldInfo ) -> str :
1693+ def _help_format (self , field_name : str , field_info : FieldInfo , model_default : Any ) -> str :
16621694 _help = field_info .description if field_info .description else ''
1663- if field_info .is_required ():
1695+ if field_info .is_required () and model_default in ( PydanticUndefined , None ) :
16641696 if _CliPositionalArg not in field_info .metadata :
1665- _help += ' (required)' if _help else '(required)'
1697+ ifdef = 'ifdef: ' if model_default is None else ''
1698+ _help += f' ({ ifdef } required)' if _help else f'({ ifdef } required)'
16661699 else :
16671700 default = f'(default: { self .cli_parse_none_str } )'
1668- if field_info .default not in (PydanticUndefined , None ):
1701+ if is_model_class (type (model_default )) or is_pydantic_dataclass (type (model_default )):
1702+ default = f'(default: { getattr (model_default , field_name )} )'
1703+ elif model_default not in (PydanticUndefined , None ) and callable (model_default ):
1704+ default = f'(default factory: { self ._metavar_format (model_default )} )'
1705+ elif field_info .default not in (PydanticUndefined , None ):
16691706 default = f'(default: { field_info .default } )'
16701707 elif field_info .default_factory is not None :
16711708 default = f'(default: { field_info .default_factory } )'
0 commit comments