[Bugfix][CLI] Include inherited field docstrings in get_attr_docs (#49821)

Signed-off-by: Chenglun Hu <chenglunhu@gmail.com> Signed-off-by: hclsys <chenglunhu@gmail.com> Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com> Co-authored-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>

hcl committed Oct 2, 2026 at 07:35 UTC e35082e7b627f403b36488cc68a21e291b8d6c8a
2 files changed +61 -7
tests/config/test_config_utils.py
+39 -2
@@ -8,7 +8,12 @@ import pytest
8
9 from vllm.config.cache import CacheConfig
10 from vllm.config.scheduler import SchedulerConfig
11 -from vllm.config.utils import get_hash_factors, hash_factors, normalize_value
11 +from vllm.config.utils import (
12 + get_attr_docs,
13 + get_hash_factors,
14 + hash_factors,
15 + normalize_value,
16 +)
17
18 # Helpers
19
@@ -114,7 +119,7 @@ def test_normalize_value_uuid_and_to_json():
119 [
120 (lambda x: x),
121 (type("CallableInstance", (), {"__call__": lambda self: 0}))(),
117 - (lambda: (lambda: 0))(), # nested function instance
122 + (lambda: lambda: 0)(), # nested function instance
123 ],
124 )
125 def test_error_cases(bad):
@@ -190,6 +195,31 @@ def test_ir_config_hash_initializes_provider_and_tracks_implementation(monkeypat
195 assert config.compute_hash() != first_hash
196
197
198 +def test_get_attr_docs_includes_inherited_fields():
199 + """get_attr_docs must collect attribute docstrings inherited from base
200 + classes, not just those declared in the class's own body."""
201 +
202 + @dataclass
203 + class Base:
204 + inherited: object = None
205 + """doc for inherited (base)."""
206 + overridden: object = None
207 + """base version of overridden."""
208 +
209 + @dataclass
210 + class Derived(Base):
211 + own: object = None
212 + """doc for own (derived)."""
213 + overridden: object = None
214 + """derived override of overridden."""
215 +
216 + docs = get_attr_docs(Derived)
217 +
218 + assert docs["own"] == "doc for own (derived)."
219 + assert docs["inherited"] == "doc for inherited (base)."
220 + assert docs["overridden"] == "derived override of overridden."
221 +
222 +
223 def test_envs_compile_factors_stable():
224 """Test that envs.compile_factors() hash is stable across fresh initializations.
225
@@ -325,3 +355,10 @@ print(hash_factors(envs.compile_factors()))
355 "HOME relocation changed the compile-cache env hash - a "
356 "location-only derived var is leaking into the key"
357 )
358 +
359 +
360 +def test_get_attr_docs_rejects_non_class():
361 + """The annotation forbids it, but the runtime guard still protects
362 + untyped callers, so keep asserting the documented TypeError."""
363 + with pytest.raises(TypeError, match="Given object was not a class."):
364 + get_attr_docs(object()) # type: ignore[arg-type]
vllm/config/utils.py
+22 -5
@@ -158,17 +158,20 @@ def getattr_iter(
158 return default_factory() if default_factory is not None else default
159
160
161 -def get_attr_docs(cls: type[Any]) -> dict[str, str]:
161 +def _get_own_attr_docs(cls: type[Any], out: dict[str, str]) -> None:
162 """Get any docstrings placed after attribute assignments in a class body.
163
164 https://davidism.com/mit-license/
165 """
166 - cls_node = ast.parse(textwrap.dedent(inspect.getsource(cls))).body[0]
166 + try:
167 + source = inspect.getsource(cls)
168 + except (OSError, TypeError):
169 + # ``object`` and classes without retrievable source (e.g. builtins).
170 + return
171
172 + cls_node = ast.parse(textwrap.dedent(source)).body[0]
173 if not isinstance(cls_node, ast.ClassDef):
169 - raise TypeError("Given object was not a class.")
170 -
171 - out = {}
174 + return
175
176 # Consider each pair of nodes.
177 for a, b in pairwise(cls_node.body):
@@ -194,6 +197,20 @@ def get_attr_docs(cls: type[Any]) -> dict[str, str]:
197
198 out[target.id] = doc
199
200 +
201 +def get_attr_docs(cls: type[Any]) -> dict[str, str]:
202 + """Get any docstrings placed after attribute assignments in a class body or
203 + its bases."""
204 + if not isinstance(cls, type):
205 + raise TypeError("Given object was not a class.")
206 +
207 + out: dict[str, str] = {}
208 +
209 + # Walk the MRO from the most-base class to ``cls`` so that a docstring
210 + # redefined on a subclass overrides the one inherited from a base class.
211 + for base in reversed(cls.__mro__):
212 + _get_own_attr_docs(base, out)
213 +
214 return out
215
216