Skip to content

Commit

Permalink
Fixes CLI help text for function types (#370)
Browse files Browse the repository at this point in the history
  • Loading branch information
kschwab authored Aug 26, 2024
1 parent 235dd02 commit cabcdee
Show file tree
Hide file tree
Showing 2 changed files with 15 additions and 8 deletions.
15 changes: 10 additions & 5 deletions pydantic_settings/sources.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations as _annotations

import inspect
import json
import os
import re
Expand All @@ -17,7 +18,6 @@
from enum import Enum
from pathlib import Path
from textwrap import dedent
from types import FunctionType
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -1718,8 +1718,9 @@ def _metavar_format_choices(self, args: list[str], obj_qualname: str | None = No
def _metavar_format_recurse(self, obj: Any) -> str:
"""Pretty metavar representation of a type. Adapts logic from `pydantic._repr.display_as_type`."""
obj = _strip_annotated(obj)
if isinstance(obj, FunctionType):
return obj.__name__
if _is_function(obj):
# If function is locally defined use __name__ instead of __qualname__
return obj.__name__ if '<locals>' in obj.__qualname__ else obj.__qualname__
elif obj is ...:
return '...'
elif isinstance(obj, Representation):
Expand Down Expand Up @@ -1762,13 +1763,13 @@ def _help_format(self, field_name: str, field_info: FieldInfo, model_default: An
default = f'(default: {self.cli_parse_none_str})'
if is_model_class(type(model_default)) or is_pydantic_dataclass(type(model_default)):
default = f'(default: {getattr(model_default, field_name)})'
elif model_default not in (PydanticUndefined, None) and callable(model_default):
elif model_default not in (PydanticUndefined, None) and _is_function(model_default):
default = f'(default factory: {self._metavar_format(model_default)})'
elif field_info.default not in (PydanticUndefined, None):
enum_name = _annotation_enum_val_to_name(field_info.annotation, field_info.default)
default = f'(default: {field_info.default if enum_name is None else enum_name})'
elif field_info.default_factory is not None:
default = f'(default: {field_info.default_factory})'
default = f'(default factory: {self._metavar_format(field_info.default_factory)})'
_help += f' {default}' if _help else default
return _help.replace('%', '%%') if issubclass(type(self._root_parser), ArgumentParser) else _help

Expand Down Expand Up @@ -2092,3 +2093,7 @@ def _annotation_enum_name_to_val(annotation: type[Any] | None, name: Any) -> Any
if name in tuple(val.name for val in type_):
return type_[name]
return None


def _is_function(obj: Any) -> bool:
return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.isroutine(obj) or inspect.ismethod(obj)
8 changes: 5 additions & 3 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pathlib
import re
import sys
import time
import typing
import uuid
from datetime import datetime, timezone
Expand Down Expand Up @@ -2553,9 +2554,7 @@ class Cfg(BaseSettings):
-h, --help show this help message and exit
--foo str (required)
--bar int (default: 123)
--boo int (default: <function
test_cli_help_differentiation.<locals>.Cfg.<lambda> at
0xffffffff>)
--boo int (default factory: <lambda>)
"""
)

Expand Down Expand Up @@ -3757,6 +3756,9 @@ class CfgWithSubCommand(BaseSettings):
(Annotated[SimpleSettings, 'annotation'], 'JSON'),
(DirectoryPath, 'Path'),
(FruitsEnum, '{pear,kiwi,lime}'),
(time.time_ns, 'time_ns'),
(foobar, 'foobar'),
(CliDummyParser.add_argument, 'CliDummyParser.add_argument'),
],
)
@pytest.mark.parametrize('hide_none_type', [True, False])
Expand Down

0 comments on commit cabcdee

Please sign in to comment.