[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