From 780d900933cfc91016a7079ed29d1a7ac0cda2ec Mon Sep 17 00:00:00 2001 From: Chenglun Hu Date: Sun, 26 Jul 2026 01:33:17 +0800 Subject: [PATCH] fix(cli): include inherited field docstrings in get_attr_docs get_attr_docs(cls) only AST-parses the source of cls itself, so CLI args inherited from a base dataclass (everything FrontendArgs gets from BaseFrontendArgs, e.g. --chat-template, --lora-modules) appear in `--help` with no documentation. Walk the MRO from base to derived so inherited attribute docstrings are collected, with subclass docstrings taking precedence. Fixes #49817. Signed-off-by: Chenglun Hu --- tests/config/test_config_utils.py | 51 ++++++++++++++++++++++++++++++- vllm/config/utils.py | 39 ++++++++++++++++------- 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/tests/config/test_config_utils.py b/tests/config/test_config_utils.py index 35bc1e167b52..c4506dfc08a5 100644 --- a/tests/config/test_config_utils.py +++ b/tests/config/test_config_utils.py @@ -7,7 +7,12 @@ import pytest from vllm.config.cache import CacheConfig -from vllm.config.utils import get_hash_factors, hash_factors, normalize_value +from vllm.config.utils import ( + get_attr_docs, + get_hash_factors, + hash_factors, + normalize_value, +) # Helpers @@ -167,6 +172,50 @@ class LocalDummy: assert endswith_fqname(LocalDummy, ".LocalDummy") +def test_get_attr_docs_includes_inherited_fields(): + """get_attr_docs must collect attribute docstrings inherited from base + classes, not just those declared in the class's own body. + + dataclasses.fields() exposes inherited fields as CLI arguments, so their + docstrings must also reach the generated help output (issue #49817). + """ + + @dataclass + class Base: + inherited: object = None + """doc for inherited (base).""" + overridden: object = None + """base version of overridden.""" + + @dataclass + class Derived(Base): + own: object = None + """doc for own (derived).""" + overridden: object = None + """derived override of overridden.""" + + docs = get_attr_docs(Derived) + + # Own field. + assert docs["own"] == "doc for own (derived)." + # Inherited field's docstring is picked up from the base class. + assert docs["inherited"] == "doc for inherited (base)." + # A field redefined on the subclass keeps the subclass docstring. + assert docs["overridden"] == "derived override of overridden." + + +def test_get_attr_docs_handles_object_in_mro(): + """Walking the MRO must not crash on classes without retrievable source + (e.g. ``object``).""" + + class Plain: + x: int = 1 + """doc for x.""" + + docs = get_attr_docs(Plain) + assert docs == {"x": "doc for x."} + + def test_envs_compile_factors_stable(): """Test that envs.compile_factors() hash is stable across fresh initializations. diff --git a/vllm/config/utils.py b/vllm/config/utils.py index 6ab346d1a030..da5425fdbddf 100644 --- a/vllm/config/utils.py +++ b/vllm/config/utils.py @@ -157,19 +157,17 @@ def getattr_iter( return default_factory() if default_factory is not None else default -def get_attr_docs(cls: type[Any]) -> dict[str, str]: - """ - Get any docstrings placed after attribute assignments in a class body. - - https://davidism.com/mit-license/ - """ - - cls_node = ast.parse(textwrap.dedent(inspect.getsource(cls))).body[0] +def _get_own_attr_docs(cls: type[Any], out: dict[str, str]) -> None: + """Collect attribute docstrings declared directly in ``cls``'s own body.""" + try: + source = inspect.getsource(cls) + except (OSError, TypeError): + # ``object`` and classes without retrievable source (e.g. builtins). + return + cls_node = ast.parse(textwrap.dedent(source)).body[0] if not isinstance(cls_node, ast.ClassDef): - raise TypeError("Given object was not a class.") - - out = {} + return # Consider each pair of nodes. for a, b in pairwise(cls_node.body): @@ -195,6 +193,25 @@ def get_attr_docs(cls: type[Any]) -> dict[str, str]: out[target.id] = doc + +def get_attr_docs(cls: type[Any]) -> dict[str, str]: + """ + Get any docstrings placed after attribute assignments in a class body. + + Also includes docstrings for attributes inherited from base classes, since + ``dataclasses.fields()`` exposes inherited fields as CLI arguments too. + Docstrings on ``cls`` take precedence over those from its base classes. + + https://davidism.com/mit-license/ + """ + + out: dict[str, str] = {} + + # Walk the MRO from the most-base class to ``cls`` so that a docstring + # redefined on a subclass overrides the one inherited from a base class. + for klass in reversed(cls.__mro__): + _get_own_attr_docs(klass, out) + return out