diff --git a/great_docs/_renderer/_render/docclass.py b/great_docs/_renderer/_render/docclass.py index 404af7fa..ac75f197 100644 --- a/great_docs/_renderer/_render/docclass.py +++ b/great_docs/_renderer/_render/docclass.py @@ -71,7 +71,16 @@ def attribute_member_pages(self) -> list[layout.MemberPage]: def docstring_sections_content(self): items = super().docstring_sections_content titles = set(item[0] for item in items) - if not self.is_dataclass or "Parameters" in titles or not len(self.parameters): + # When the author documents the fields themselves (via either a + # `Parameters` or an `Attributes` docstring section), don't also + # synthesize a "Parameter Attributes" section — that would duplicate the + # same fields. Which of the two sections to use is left to the docs writer. + if ( + not self.is_dataclass + or "Parameters" in titles + or "Attributes" in titles + or not len(self.parameters) + ): return items # Create and insert Parameter Attributes diff --git a/test-packages/synthetic/specs/gdtest_dataclasses.py b/test-packages/synthetic/specs/gdtest_dataclasses.py index 4e454f56..80ffdf80 100644 --- a/test-packages/synthetic/specs/gdtest_dataclasses.py +++ b/test-packages/synthetic/specs/gdtest_dataclasses.py @@ -2,12 +2,15 @@ gdtest_dataclasses — @dataclass objects. Dimensions: A1, B1, C5, D1, E6, F6, G1, H7 -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). +Focus: 3 dataclasses: one with default_factory, one frozen (documented with an + `Attributes` section), one that also defines methods. Tests dataclass + field documentation, __init__ generation, that fields documented under + either a `Parameters` or an `Attributes` section render once (no + duplicated "Parameter Attributes"), 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 = { @@ -62,7 +65,10 @@ class Record: """ An immutable data record. - Parameters + Documented with an ``Attributes`` section (rather than + ``Parameters``) to exercise that rendering path. + + Attributes ---------- id Unique record identifier. diff --git a/tests/renderer/test_classes.py b/tests/renderer/test_classes.py index 0c30ace1..318db0bc 100644 --- a/tests/renderer/test_classes.py +++ b/tests/renderer/test_classes.py @@ -142,3 +142,66 @@ class Widget: qmd = render_code_variable(code, "Widget") assert "Widget(name, size=1)" in qmd + + +def test_dataclass_attributes_section_not_duplicated(): + """A dataclass documented with an `Attributes` section renders that section + once, without a synthesized "Parameter Attributes" duplicate. + + The author may document a dataclass's fields with either a `Parameters` or + an `Attributes` section; great-docs must not also auto-generate a + "Parameter Attributes" section listing the same fields. + """ + code = ''' + from dataclasses import dataclass + + @dataclass + class Widget: + """A widget. + + Attributes + ---------- + name + The widget name. + size + The widget size. + """ + + name: str + size: int = 1 + ''' + qmd = render_code_variable(code, "Widget") + + assert "## Attributes {.doc-attributes}" in qmd + assert "## Parameter Attributes {.doc-parameter-attributes}" not in qmd + # The author's descriptions must be preserved. + assert "The widget name." in qmd + + +def test_dataclass_parameters_section_not_duplicated(): + """A dataclass documented with a `Parameters` section renders that section + once, without a synthesized "Parameter Attributes" duplicate. + """ + code = ''' + from dataclasses import dataclass + + @dataclass + class Widget: + """A widget. + + Parameters + ---------- + name + The widget name. + size + The widget size. + """ + + name: str + size: int = 1 + ''' + qmd = render_code_variable(code, "Widget") + + assert "## Parameters {.doc-parameters}" in qmd + assert "## Parameter Attributes {.doc-parameter-attributes}" not in qmd + assert "The widget name." in qmd diff --git a/tests/test_gdg_rendered.py b/tests/test_gdg_rendered.py index f4b751f4..f2bd0154 100644 --- a/tests/test_gdg_rendered.py +++ b/tests/test_gdg_rendered.py @@ -974,6 +974,37 @@ def test_dataclass_with_methods_signature_has_fields(): ) +@pytest.mark.dedicated +@requires_bs4 +def test_dataclass_attributes_section_not_duplicated(): + """A dataclass documented with an `Attributes` section renders it once, + without a synthesized "Parameter Attributes" duplicate. + + Fields may be documented with either a `Parameters` or an `Attributes` + section; great-docs must not also auto-generate a "Parameter Attributes" + section listing the same fields. (`Record` in this package uses an + `Attributes` section.) + """ + pkg = "gdtest_dataclasses" + if not _has_rendered_site(pkg): + pytest.skip("gdtest_dataclasses not rendered") + + ref = _ref_dir(pkg) + page = ref / "Record.html" + if not page.exists(): + pytest.skip("Record.html not found") + + soup = _load_html(page) + + assert soup.select_one("section.doc-attributes") is not None, ( + "Record.html: expected an Attributes section" + ) + assert soup.select_one("section.doc-parameter-attributes") is None, ( + "Record.html: 'Attributes'-documented dataclass should not also render a " + "synthesized 'Parameter Attributes' section" + ) + + @pytest.mark.dedicated @requires_bs4 def test_async_functions_have_badge():