Skip to content
Merged
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
15 changes: 13 additions & 2 deletions great_docs/_renderer/_render/mixin_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,19 @@ def render_signature(self) -> BlockContent:
"""
name = self.signature_name if self.show_signature_name else ""

# Check for @overload variants
overloads: list[gf.Function] = getattr(self.obj, "overloads", [])
# Check for @overload variants.
# For functions, `.overloads` is a `list[Function]`. For classes it is a
# `dict[str, list[Function]]` keyed by member name, which is non-empty
# (and thus truthy) for any class that merely defines methods, even when
# none of them are actually overloaded. Flatten it so the check reflects
# real overloads and dataclass constructor signatures are not lost.
overloads_raw = getattr(self.obj, "overloads", []) or []
if isinstance(overloads_raw, dict):
overloads: list[gf.Function] = [
ov for ovs in overloads_raw.values() for ov in ovs
]
else:
overloads = list(overloads_raw)
if overloads:
return self._render_overload_signatures(name, overloads)

Expand Down
43 changes: 36 additions & 7 deletions test-packages/synthetic/specs/gdtest_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
gdtest_dataclasses — @dataclass objects.

Dimensions: A1, B1, C5, D1, E6, F6, G1, H7
Focus: 2 dataclasses — one with default_factory, one frozen.
Tests dataclass field documentation and __init__ generation.
Focus: 3 dataclasses: one with default_factory, one frozen, one that also
defines methods. Tests dataclass field documentation, __init__
generation, and that a dataclass which defines methods still renders its
constructor signature (regression: `Class.overloads` is a dict keyed by
member name, so any class with methods was rendered through the overload
path and lost its constructor parameters).
"""

SPEC = {
Expand All @@ -26,7 +30,7 @@
"""A test package with dataclass objects."""

__version__ = "0.1.0"
__all__ = ["Config", "Record"]
__all__ = ["Config", "Record", "Mutable"]

from dataclasses import dataclass, field

Expand Down Expand Up @@ -70,6 +74,31 @@ class Record:
id: int
value: str
timestamp: float = 0.0


@dataclass
class Mutable:
"""
A mutable record that also defines methods.

Parameters
----------
label
Human-readable label.
count
Current count.
"""
label: str
count: int = 0

def increment(self, by: int = 1) -> None:
"""Increase the count."""
self.count += by

@classmethod
def empty(cls) -> "Mutable":
"""Create an empty record."""
return cls(label="")
''',
"README.md": """\
# gdtest-dataclasses
Expand All @@ -81,10 +110,10 @@ class Record:
"detected_name": "gdtest-dataclasses",
"detected_module": "gdtest_dataclasses",
"detected_parser": "numpy",
"export_names": ["Config", "Record"],
"num_exports": 2,
"export_names": ["Config", "Record", "Mutable"],
"num_exports": 3,
"section_titles": ["Dataclasses"],
"has_user_guide": False,
"coverage_exclude": ['nodoc', 'bigcl', 'ug', 'supp'],
},
"coverage_exclude": ["nodoc", "bigcl", "ug", "supp"],
},
}
54 changes: 54 additions & 0 deletions tests/renderer/test_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ def assert_in_qmd(name: str, annotation: str, default: str, co: str = ""):
assert f"[[{default}]{{.{co}}}]{{.doc-parameter-default}}</code>" in qmd

assert "## Init Parameters {.doc-init-parameters}" in qmd

assert_in_qmd("a", "int", "1", "dv")

assert "## Parameter Attributes {.doc-parameter-attributes}" in qmd

assert_in_qmd("b", "float", "2", "dv")
assert_in_qmd("c", "float", "3.0", "fl")

Expand All @@ -73,6 +75,7 @@ class Base:
"""

qmd = render_code_variable(code, "Base")

assert ": Parameter a" in qmd


Expand All @@ -86,5 +89,56 @@ def meth(self):
"""

qmd = render_code_variable(code, "Base")

# Methods in summary tables use short name anchors (matching Quarto section IDs)
assert '<a href="#meth"' in qmd and ">meth()</a>" in qmd


def test_dataclass_with_methods_keeps_constructor_signature():
"""A dataclass that also defines methods must still render its constructor
signature.

Regression: `Class.overloads` is a `dict` keyed by member name, so any
class that merely defines a method had a non-empty (truthy) `overloads`
and was rendered through the overload path, producing an empty `Name()`
usage signature instead of the dataclass constructor.
"""
code = '''
from dataclasses import dataclass

@dataclass
class Widget:
"""A widget."""

name: str
size: int = 1

def resize(self, size: int) -> None:
"""Resize the widget."""
self.size = size
'''
qmd = render_code_variable(code, "Widget")

# The usage signature must include the constructor parameters, not a bare `Widget()`.
assert "Widget(name, size=1)" in qmd


def test_dataclass_without_methods_keeps_constructor_signature():
"""Control: a dataclass with only fields renders its constructor signature.

This already worked before the overloads fix and guards against a regression
in the other direction.
"""
code = '''
from dataclasses import dataclass

@dataclass
class Widget:
"""A widget."""

name: str
size: int = 1
'''
qmd = render_code_variable(code, "Widget")

assert "Widget(name, size=1)" in qmd
34 changes: 34 additions & 0 deletions tests/test_gdg_rendered.py
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,40 @@ def test_dataclass_fields_render():
assert "name" in param_names, f"Config.html: 'name' field not in parameters: {param_names}"


@pytest.mark.dedicated
@requires_bs4
def test_dataclass_with_methods_signature_has_fields():
"""A dataclass that also defines methods must still show its constructor
fields in the usage signature.

`Class.overloads` is a dict keyed by member name, so any class that merely
defines a method had a non-empty (truthy) `overloads` and was rendered through
the overload path, producing an empty `Name()` signature instead of the
dataclass constructor.
"""
pkg = "gdtest_dataclasses"
if not _has_rendered_site(pkg):
pytest.skip("gdtest_dataclasses not rendered")

ref = _ref_dir(pkg)
page = ref / "Mutable.html"
if not page.exists():
pytest.skip("Mutable.html not found")

soup = _load_html(page)

# Parameters of the rendered constructor signature (`span.va` tokens in the
# `.doc-signature` code block).
sig_block = soup.select_one("div.doc-signature")
assert sig_block is not None, "Mutable.html: no .doc-signature block"
sig_params = {s.get_text().strip() for s in sig_block.select("span.va")}

assert {"label", "count"} <= sig_params, (
f"Mutable.html: constructor fields missing from signature (got {sig_params}); "
"a dataclass that defines methods rendered an empty signature"
)


@pytest.mark.dedicated
@requires_bs4
def test_async_functions_have_badge():
Expand Down
Loading