8
8
import typing
9
9
import warnings
10
10
from abc import ABC , abstractmethod
11
- from argparse import SUPPRESS , ArgumentParser , HelpFormatter , Namespace , _SubParsersAction
11
+ from argparse import SUPPRESS , ArgumentParser , Namespace , RawDescriptionHelpFormatter , _SubParsersAction
12
12
from collections import deque
13
13
from dataclasses import asdict , is_dataclass
14
14
from enum import Enum
15
15
from pathlib import Path
16
+ from textwrap import dedent
16
17
from types import FunctionType
17
18
from typing import (
18
19
TYPE_CHECKING ,
@@ -424,7 +425,7 @@ class Settings(BaseSettings):
424
425
args = get_args (annotation )
425
426
if origin_is_union (get_origin (field .annotation )) and len (args ) == 2 and type (None ) in args :
426
427
for arg in args :
427
- if arg != type ( None ) :
428
+ if arg is not None :
428
429
annotation = arg
429
430
break
430
431
@@ -748,7 +749,7 @@ class Cfg(BaseSettings):
748
749
elif is_model_class (annotation ) or is_pydantic_dataclass (annotation ):
749
750
fields = (
750
751
annotation .__pydantic_fields__
751
- if is_pydantic_dataclass (annotation )
752
+ if is_pydantic_dataclass (annotation ) and hasattr ( annotation , '__pydantic_fields__' )
752
753
else cast (BaseModel , annotation ).model_fields
753
754
)
754
755
# `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[
812
813
if not allow_json_failure :
813
814
raise e
814
815
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 ] == {}:
816
817
env_var [last_key ] = env_val
817
818
818
819
return result
@@ -966,7 +967,7 @@ class CliSettingsSource(EnvSettingsSource, Generic[T]):
966
967
Defaults to `argparse._SubParsersAction.add_parser`.
967
968
add_subparsers_method: The root parser add subparsers (sub-commands) method.
968
969
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 `.
970
971
"""
971
972
972
973
def __init__ (
@@ -988,7 +989,7 @@ def __init__(
988
989
add_argument_group_method : Callable [..., Any ] | None = ArgumentParser .add_argument_group ,
989
990
add_parser_method : Callable [..., Any ] | None = _SubParsersAction .add_parser ,
990
991
add_subparsers_method : Callable [..., Any ] | None = ArgumentParser .add_subparsers ,
991
- formatter_class : Any = HelpFormatter ,
992
+ formatter_class : Any = RawDescriptionHelpFormatter ,
992
993
) -> None :
993
994
self .cli_prog_name = (
994
995
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__(
1040
1041
1041
1042
root_parser = (
1042
1043
_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 ,
1044
1048
)
1045
1049
if root_parser is None
1046
1050
else root_parser
@@ -1329,7 +1333,11 @@ def _get_resolved_names(
1329
1333
1330
1334
def _sort_arg_fields (self , model : type [BaseModel ]) -> list [tuple [str , FieldInfo ]]:
1331
1335
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
+ )
1333
1341
for field_name , field_info in fields .items ():
1334
1342
if _CliSubCommand in field_info .metadata :
1335
1343
if not field_info .is_required ():
@@ -1405,7 +1413,7 @@ def _connect_root_parser(
1405
1413
add_argument_group_method : Callable [..., Any ] | None = ArgumentParser .add_argument_group ,
1406
1414
add_parser_method : Callable [..., Any ] | None = _SubParsersAction .add_parser ,
1407
1415
add_subparsers_method : Callable [..., Any ] | None = ArgumentParser .add_subparsers ,
1408
- formatter_class : Any = HelpFormatter ,
1416
+ formatter_class : Any = RawDescriptionHelpFormatter ,
1409
1417
) -> None :
1410
1418
self ._root_parser = root_parser
1411
1419
self ._parse_args = self ._connect_parser_method (parse_args_method , 'parsed_args_method' )
@@ -1424,6 +1432,7 @@ def _connect_root_parser(
1424
1432
subcommand_prefix = self .env_prefix ,
1425
1433
group = None ,
1426
1434
alias_prefixes = [],
1435
+ model_default = PydanticUndefined ,
1427
1436
)
1428
1437
1429
1438
def _add_parser_args (
@@ -1435,6 +1444,7 @@ def _add_parser_args(
1435
1444
subcommand_prefix : str ,
1436
1445
group : Any ,
1437
1446
alias_prefixes : list [str ],
1447
+ model_default : Any ,
1438
1448
) -> ArgumentParser :
1439
1449
subparsers : Any = None
1440
1450
alias_path_args : dict [str , str ] = {}
@@ -1459,24 +1469,27 @@ def _add_parser_args(
1459
1469
field_name ,
1460
1470
help = field_info .description ,
1461
1471
formatter_class = self ._formatter_class ,
1462
- description = model .__doc__ ,
1472
+ description = None if model .__doc__ is None else dedent ( model . __doc__ ) ,
1463
1473
),
1464
1474
model = model ,
1465
1475
added_args = [],
1466
1476
arg_prefix = f'{ arg_prefix } { field_name } .' ,
1467
1477
subcommand_prefix = f'{ subcommand_prefix } { field_name } .' ,
1468
1478
group = None ,
1469
1479
alias_prefixes = [],
1480
+ model_default = PydanticUndefined ,
1470
1481
)
1471
1482
else :
1472
1483
resolved_names , is_alias_path_only = self ._get_resolved_names (field_name , field_info , alias_path_args )
1473
1484
arg_flag : str = '--'
1474
1485
kwargs : dict [str , Any ] = {}
1475
1486
kwargs ['default' ] = SUPPRESS
1476
- kwargs ['help' ] = self ._help_format (field_info )
1487
+ kwargs ['help' ] = self ._help_format (field_name , field_info , model_default )
1477
1488
kwargs ['dest' ] = f'{ arg_prefix } { resolved_names [0 ]} '
1478
1489
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
+ )
1480
1493
if kwargs ['dest' ] in added_args :
1481
1494
continue
1482
1495
if _annotation_contains_types (
@@ -1504,8 +1517,10 @@ def _add_parser_args(
1504
1517
arg_flag ,
1505
1518
arg_names ,
1506
1519
kwargs ,
1520
+ field_name ,
1507
1521
field_info ,
1508
1522
resolved_names ,
1523
+ model_default = model_default ,
1509
1524
)
1510
1525
elif is_alias_path_only :
1511
1526
continue
@@ -1544,17 +1559,33 @@ def _add_parser_submodels(
1544
1559
arg_flag : str ,
1545
1560
arg_names : list [str ],
1546
1561
kwargs : dict [str , Any ],
1562
+ field_name : str ,
1547
1563
field_info : FieldInfo ,
1548
1564
resolved_names : tuple [str , ...],
1565
+ model_default : Any ,
1549
1566
) -> None :
1550
1567
model_group : Any = None
1551
1568
model_group_kwargs : dict [str , Any ] = {}
1552
1569
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
+
1558
1589
if not self .cli_avoid_json :
1559
1590
added_args .append (arg_names [0 ])
1560
1591
kwargs ['help' ] = f'set { arg_names [0 ]} from JSON string'
@@ -1569,6 +1600,7 @@ def _add_parser_submodels(
1569
1600
subcommand_prefix = subcommand_prefix ,
1570
1601
group = model_group if model_group else model_group_kwargs ,
1571
1602
alias_prefixes = [f'{ arg_prefix } { name } .' for name in resolved_names [1 :]],
1603
+ model_default = model_default ,
1572
1604
)
1573
1605
1574
1606
def _add_parser_alias_paths (
@@ -1658,14 +1690,19 @@ def _metavar_format_recurse(self, obj: Any) -> str:
1658
1690
def _metavar_format (self , obj : Any ) -> str :
1659
1691
return self ._metavar_format_recurse (obj ).replace (', ' , ',' )
1660
1692
1661
- def _help_format (self , field_info : FieldInfo ) -> str :
1693
+ def _help_format (self , field_name : str , field_info : FieldInfo , model_default : Any ) -> str :
1662
1694
_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 ) :
1664
1696
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)'
1666
1699
else :
1667
1700
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 ):
1669
1706
default = f'(default: { field_info .default } )'
1670
1707
elif field_info .default_factory is not None :
1671
1708
default = f'(default: { field_info .default_factory } )'
0 commit comments