diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 12f35a6..90e5b37 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -1,5 +1,6 @@ from __future__ import annotations as _annotations +import inspect import json import os import re @@ -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, @@ -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 '' in obj.__qualname__ else obj.__qualname__ elif obj is ...: return '...' elif isinstance(obj, Representation): @@ -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 @@ -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) diff --git a/tests/test_settings.py b/tests/test_settings.py index 7cbf24d..0ba96a0 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -5,6 +5,7 @@ import pathlib import re import sys +import time import typing import uuid from datetime import datetime, timezone @@ -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: .Cfg. at - 0xffffffff>) + --boo int (default factory: ) """ ) @@ -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])