Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion tests/config/test_config_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand Down
39 changes: 28 additions & 11 deletions vllm/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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


Expand Down
Loading