Skip to content

Commit a293ada

Browse files
kschwabhramezani
andauthored
CLI Improve Docstring Help Text (#359)
Co-authored-by: Hasan Ramezani <[email protected]>
1 parent dc325c3 commit a293ada

File tree

3 files changed

+52
-12
lines changed

3 files changed

+52
-12
lines changed

docs/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ parser methods that can be customised, along with their argparse counterparts (t
11201120
* `add_argument_group_method` - (`argparse.ArgumentParser.add_argument_group`)
11211121
* `add_parser_method` - (`argparse._SubParsersAction.add_parser`)
11221122
* `add_subparsers_method` - (`argparse.ArgumentParser.add_subparsers`)
1123-
* `formatter_class` - (`argparse.HelpFormatter`)
1123+
* `formatter_class` - (`argparse.RawDescriptionHelpFormatter`)
11241124

11251125
For a non-argparse parser the parser methods can be set to `None` if not supported. The CLI settings will only raise an
11261126
error when connecting to the root parser if a parser method is necessary but set to `None`.

pydantic_settings/sources.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
import typing
99
import warnings
1010
from abc import ABC, abstractmethod
11-
from argparse import SUPPRESS, ArgumentParser, HelpFormatter, Namespace, _SubParsersAction
11+
from argparse import SUPPRESS, ArgumentParser, Namespace, RawDescriptionHelpFormatter, _SubParsersAction
1212
from collections import deque
1313
from dataclasses import is_dataclass
1414
from enum import Enum
1515
from pathlib import Path
16+
from textwrap import dedent
1617
from types import FunctionType
1718
from typing import (
1819
TYPE_CHECKING,
@@ -916,7 +917,7 @@ class CliSettingsSource(EnvSettingsSource, Generic[T]):
916917
Defaults to `argparse._SubParsersAction.add_parser`.
917918
add_subparsers_method: The root parser add subparsers (sub-commands) method.
918919
Defaults to `argparse.ArgumentParser.add_subparsers`.
919-
formatter_class: A class for customizing the root parser help text. Defaults to `argparse.HelpFormatter`.
920+
formatter_class: A class for customizing the root parser help text. Defaults to `argparse.RawDescriptionHelpFormatter`.
920921
"""
921922

922923
def __init__(
@@ -938,7 +939,7 @@ def __init__(
938939
add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group,
939940
add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser,
940941
add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers,
941-
formatter_class: Any = HelpFormatter,
942+
formatter_class: Any = RawDescriptionHelpFormatter,
942943
) -> None:
943944
self.cli_prog_name = (
944945
cli_prog_name if cli_prog_name is not None else settings_cls.model_config.get('cli_prog_name', sys.argv[0])
@@ -990,7 +991,10 @@ def __init__(
990991

991992
root_parser = (
992993
_CliInternalArgParser(
993-
cli_exit_on_error=self.cli_exit_on_error, prog=self.cli_prog_name, description=settings_cls.__doc__
994+
cli_exit_on_error=self.cli_exit_on_error,
995+
prog=self.cli_prog_name,
996+
description=None if settings_cls.__doc__ is None else dedent(settings_cls.__doc__),
997+
formatter_class=formatter_class,
994998
)
995999
if root_parser is None
9961000
else root_parser
@@ -1359,7 +1363,7 @@ def _connect_root_parser(
13591363
add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group,
13601364
add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser,
13611365
add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers,
1362-
formatter_class: Any = HelpFormatter,
1366+
formatter_class: Any = RawDescriptionHelpFormatter,
13631367
) -> None:
13641368
self._root_parser = root_parser
13651369
self._parse_args = self._connect_parser_method(parse_args_method, 'parsed_args_method')
@@ -1413,7 +1417,7 @@ def _add_parser_args(
14131417
field_name,
14141418
help=field_info.description,
14151419
formatter_class=self._formatter_class,
1416-
description=model.__doc__,
1420+
description=None if model.__doc__ is None else dedent(model.__doc__),
14171421
),
14181422
model=model,
14191423
added_args=[],
@@ -1505,7 +1509,9 @@ def _add_parser_submodels(
15051509
model_group_kwargs: dict[str, Any] = {}
15061510
model_group_kwargs['title'] = f'{arg_names[0]} options'
15071511
model_group_kwargs['description'] = (
1508-
sub_models[0].__doc__
1512+
None
1513+
if sub_models[0].__doc__ is None
1514+
else dedent(sub_models[0].__doc__)
15091515
if self.cli_use_class_docs_for_groups and len(sub_models) == 1
15101516
else field_info.description
15111517
)

tests/test_settings.py

+38-4
Original file line numberDiff line numberDiff line change
@@ -2411,27 +2411,61 @@ class Cfg(BaseSettings):
24112411

24122412

24132413
def test_cli_help_string_format(capsys, monkeypatch):
2414-
class Cfg(BaseSettings):
2414+
class Cfg(BaseSettings, cli_parse_args=True):
24152415
date_str: str = '%Y-%m-%d'
24162416

2417-
argparse_options_text = 'options' if sys.version_info >= (3, 10) else 'optional arguments'
2417+
class MultilineDoc(BaseSettings, cli_parse_args=True):
2418+
"""
2419+
My
2420+
Multiline
2421+
Doc
2422+
"""
24182423

24192424
with monkeypatch.context() as m:
24202425
m.setattr(sys, 'argv', ['example.py', '--help'])
24212426

24222427
with pytest.raises(SystemExit):
2423-
Cfg(_cli_parse_args=True)
2428+
Cfg()
24242429

24252430
assert (
24262431
re.sub(r'0x\w+', '0xffffffff', capsys.readouterr().out, flags=re.MULTILINE)
24272432
== f"""usage: example.py [-h] [--date_str str]
24282433
2429-
{argparse_options_text}:
2434+
{ARGPARSE_OPTIONS_TEXT}:
24302435
-h, --help show this help message and exit
24312436
--date_str str (default: %Y-%m-%d)
24322437
"""
24332438
)
24342439

2440+
with pytest.raises(SystemExit):
2441+
MultilineDoc()
2442+
assert (
2443+
capsys.readouterr().out
2444+
== f"""usage: example.py [-h]
2445+
2446+
My
2447+
Multiline
2448+
Doc
2449+
2450+
{ARGPARSE_OPTIONS_TEXT}:
2451+
-h, --help show this help message and exit
2452+
"""
2453+
)
2454+
2455+
with pytest.raises(SystemExit):
2456+
cli_settings_source = CliSettingsSource(MultilineDoc, formatter_class=argparse.HelpFormatter)
2457+
MultilineDoc(_cli_settings_source=cli_settings_source(args=True))
2458+
assert (
2459+
capsys.readouterr().out
2460+
== f"""usage: example.py [-h]
2461+
2462+
My Multiline Doc
2463+
2464+
{ARGPARSE_OPTIONS_TEXT}:
2465+
-h, --help show this help message and exit
2466+
"""
2467+
)
2468+
24352469

24362470
def test_cli_nested_dataclass_arg():
24372471
@pydantic_dataclasses.dataclass

0 commit comments

Comments
 (0)