From 05aa4d29eafdfa5aa6279a971ffabbe76d48bdf7 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sun, 28 Jun 2026 15:35:06 -0400 Subject: [PATCH 1/4] Better handle overload variants --- great_docs/_renderer/_render/mixin_call.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/great_docs/_renderer/_render/mixin_call.py b/great_docs/_renderer/_render/mixin_call.py index a7a2c289..50d54f9a 100644 --- a/great_docs/_renderer/_render/mixin_call.py +++ b/great_docs/_renderer/_render/mixin_call.py @@ -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) From 001ce0037e3a4eee6dc4f1f8023f7b78295eb1c8 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sun, 28 Jun 2026 15:37:49 -0400 Subject: [PATCH 2/4] Update test_classes.py --- tests/renderer/test_classes.py | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/renderer/test_classes.py b/tests/renderer/test_classes.py index 71339eb6..0c30ace1 100644 --- a/tests/renderer/test_classes.py +++ b/tests/renderer/test_classes.py @@ -55,9 +55,11 @@ def assert_in_qmd(name: str, annotation: str, default: str, co: str = ""): assert f"[[{default}]{{.{co}}}]{{.doc-parameter-default}}" 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") @@ -73,6 +75,7 @@ class Base: """ qmd = render_code_variable(code, "Base") + assert ": Parameter a" in qmd @@ -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 'meth()" 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 From 6a84f67acba0f0b7cf17f73b436e34820db09cee Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sun, 28 Jun 2026 15:37:56 -0400 Subject: [PATCH 3/4] Update gdtest_dataclasses.py --- .../synthetic/specs/gdtest_dataclasses.py | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/test-packages/synthetic/specs/gdtest_dataclasses.py b/test-packages/synthetic/specs/gdtest_dataclasses.py index 2cdd30f0..4e454f56 100644 --- a/test-packages/synthetic/specs/gdtest_dataclasses.py +++ b/test-packages/synthetic/specs/gdtest_dataclasses.py @@ -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 = { @@ -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 @@ -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 @@ -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"], + }, } From 68d0323760660a62d7057a2dcedc48d50b3a4920 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sun, 28 Jun 2026 15:39:21 -0400 Subject: [PATCH 4/4] Update test_gdg_rendered.py --- tests/test_gdg_rendered.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_gdg_rendered.py b/tests/test_gdg_rendered.py index 7156f20d..e039ccc0 100644 --- a/tests/test_gdg_rendered.py +++ b/tests/test_gdg_rendered.py @@ -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():