diff --git a/quartodoc/builder/blueprint.py b/quartodoc/builder/blueprint.py index e796a894..24a9ab24 100644 --- a/quartodoc/builder/blueprint.py +++ b/quartodoc/builder/blueprint.py @@ -356,6 +356,7 @@ def enter(self, el: Auto): children, flat=is_flat, signature_name=el.signature_name, + signature_summary=el.signature_summary, ) def _fetch_members(self, el: Auto, obj: dc.Object | dc.Alias): diff --git a/quartodoc/layout.py b/quartodoc/layout.py index dcd8db62..d4996b99 100644 --- a/quartodoc/layout.py +++ b/quartodoc/layout.py @@ -202,12 +202,14 @@ class ChoicesChildren(Enum): SignatureOptions = Literal["full", "short", "relative"] +SignatureSummaryOptions = Literal["full", "parens", None] class AutoOptions(_Base): """Options available for Auto content layout element.""" signature_name: SignatureOptions = "relative" + signature_summary: SignatureSummaryOptions = None members: Optional[list[str]] = None include_private: bool = False include_imports: bool = False @@ -344,6 +346,7 @@ class Doc(_Docable): obj: Union[dc.Object, dc.Alias] anchor: str signature_name: SignatureOptions = "relative" + signature_summary: SignatureSummaryOptions = None class Config: arbitrary_types_allowed = True @@ -358,6 +361,7 @@ def from_griffe( anchor: str = None, flat: bool = False, signature_name: str = "relative", + signature_summary: str = None, ): if members is None: members = [] @@ -370,6 +374,7 @@ def from_griffe( "obj": obj, "anchor": anchor, "signature_name": signature_name, + "signature_summary": signature_summary, } if kind == "function": diff --git a/quartodoc/renderers/md_renderer.py b/quartodoc/renderers/md_renderer.py index af912527..62aa31fe 100644 --- a/quartodoc/renderers/md_renderer.py +++ b/quartodoc/renderers/md_renderer.py @@ -387,6 +387,95 @@ def signature( name = self._fetch_object_dispname(source or el) return f"`{name}`" + @dispatch + def _signature_summary( + self, + el: layout.Doc, + mode: Literal["full", "parens"] + ) -> str: + """Generate a short signature summary for index/TOC tables. + + Parameters + ---------- + el : layout.Doc + The documented element + mode : str + Either "full" (show all params) or "parens" (just empty parens) + + Returns + ------- + str + Signature summary like "(path, [object_name, parser, ...])" or "()" + """ + if mode == "parens": + return "()" + + obj = el.obj + + # Get parameters based on object type + if obj.is_class: + if "__init__" in obj.members: + params = self._fetch_method_parameters(obj.members["__init__"]) + else: + return "()" + elif obj.is_function: + params = self._fetch_method_parameters(obj) + else: + return "" # Attributes and modules don't have signatures + + if not params: + return "()" + + # Build parameter list + required_params = [] + optional_params = [] + + for param in params: + # Skip self/cls + if param.name in {"self", "cls"}: + continue + + # Format parameter name + if param.kind == dc.ParameterKind.var_positional: + name = "*" + param.name + elif param.kind == dc.ParameterKind.var_keyword: + name = "**" + param.name + else: + name = param.name + + # Categorize as required or optional + if param.required: + required_params.append(name) + else: + optional_params.append(name) + + # Build final signature with truncation + MAX_PARAMS = 3 # Show max 3 params before truncating + + parts = [] + + # Add required params (up to MAX_PARAMS) + if required_params: + if len(required_params) > MAX_PARAMS: + parts.extend(required_params[:MAX_PARAMS-1]) + parts.append("...") + else: + parts.extend(required_params) + + # Add optional params in brackets + if optional_params: + if parts and len(parts) >= MAX_PARAMS: + # Already at limit, just show [...] + parts.append("[...]") + elif len(optional_params) > 2: + # Show first optional then ... + parts.append(f"[{optional_params[0]}, ...]") + else: + # Show all optional params + parts.append("[" + ", ".join(optional_params) + "]") + + return "(" + ", ".join(parts) + ")" + @dispatch def render_header(self, el: layout.Doc) -> str: """Render the header of a docstring, including any anchors.""" @@ -930,13 +1019,20 @@ def summarize(self, el: layout.Interlaced, *args, **kwargs): def summarize( self, el: layout.Doc, path: Optional[str] = None, shorten: bool = False ): + # Build display name with signature if configured + # Only show signature on index (path is not None), not on TOC tables (path is None) + display_name = el.name + if el.signature_summary and path is not None: + sig_summary = self._signature_summary(el, el.signature_summary) + display_name = f"{el.name}{sig_summary}" + # When path is None, we're being called directly from render() for TOC # When path is provided, we're being called from Page for index if path is None: - link = f"[{el.name}](#{el.anchor})" + link = f"[{display_name}](#{el.anchor})" else: # TODO: assumes that files end with .qmd - link = f"[{el.name}]({path}.qmd#{el.anchor})" + link = f"[{display_name}]({path}.qmd#{el.anchor})" description = self.summarize(el.obj) return self._summary_row(link, description) diff --git a/quartodoc/tests/test_renderers.py b/quartodoc/tests/test_renderers.py index 7e3be118..4fd4d1a5 100644 --- a/quartodoc/tests/test_renderers.py +++ b/quartodoc/tests/test_renderers.py @@ -475,3 +475,112 @@ def test_render_full_numpydoc_description_list(snapshot): res = renderer.render(bp) assert res == snapshot + + +# Signature summary tests ------------------------------------------------------ + + +def test_signature_summary_parens_mode(renderer): + """Test that 'parens' mode returns just ()""" + package = "quartodoc.tests.example_signature" + auto = Auto(name="no_annotations", package=package) + bp = blueprint(auto) + + result = renderer._signature_summary(bp, "parens") + assert result == "()" + + +def test_signature_summary_full_mode_simple(renderer): + """Test 'full' mode with a simple function signature""" + package = "quartodoc.tests.example_signature" + # pos_only has signature: (x, /, a, b=2) + auto = Auto(name="pos_only", package=package) + bp = blueprint(auto) + + result = renderer._signature_summary(bp, "full") + # x and a are required, b is optional + assert result == "(x, a, [b])" + + +def test_signature_summary_full_mode_mixed_params(renderer): + """Test 'full' mode with mixed required and optional parameters""" + package = "quartodoc.tests.example_signature" + # no_annotations has: (a, b=1, *args, c, d=2, **kwargs) + auto = Auto(name="no_annotations", package=package) + bp = blueprint(auto) + + result = renderer._signature_summary(bp, "full") + # a and c are required, b, d, *args, **kwargs are optional + assert result == "(a, c, [b, ...])" + + +def test_signature_summary_full_mode_var_args(renderer): + """Test 'full' mode with *args and **kwargs""" + package = "quartodoc.tests.example_signature" + # early_args has: (x, *args, a, b=2, **kwargs) + auto = Auto(name="early_args", package=package) + bp = blueprint(auto) + + result = renderer._signature_summary(bp, "full") + # x and a are required + assert result == "(x, a, [*args, ...])" + + +def test_signature_summary_no_params(renderer): + """Test signature summary with function that has no parameters""" + package = "quartodoc.tests.example_signature" + auto = Auto(name="C", package=package) # Class with no __init__ + bp = blueprint(auto) + + result = renderer._signature_summary(bp, "full") + assert result == "()" + + +def test_summarize_with_signature_summary_full(renderer): + """Test that summarize includes signature when signature_summary='full'""" + package = "quartodoc.tests.example_signature" + auto = Auto(name="pos_only", package=package, signature_summary="full") + bp = blueprint(auto) + + result = renderer.summarize(bp, path="test") + # Should include the signature in the link text + assert "pos_only(x, a, [b])" in result.link + + +def test_summarize_with_signature_summary_parens(renderer): + """Test that summarize includes () when signature_summary='parens'""" + package = "quartodoc.tests.example_signature" + auto = Auto(name="pos_only", package=package, signature_summary="parens") + bp = blueprint(auto) + + result = renderer.summarize(bp, path="test") + # Should include () in the link text + assert "pos_only()" in result.link + + +def test_summarize_without_signature_summary(renderer): + """Test that summarize doesn't include signature when signature_summary is None""" + package = "quartodoc.tests.example_signature" + auto = Auto(name="pos_only", package=package) # No signature_summary + bp = blueprint(auto) + + result = renderer.summarize(bp, path="test") + # Should NOT include any parentheses in the link + assert "pos_only]" in result.link # Just the closing bracket of markdown link + assert "pos_only(" not in result.link + + +def test_summarize_signature_only_on_index_not_toc(renderer): + """Test that signatures appear on index but not on TOC tables""" + package = "quartodoc.tests.example_signature" + auto = Auto(name="pos_only", package=package, signature_summary="full") + bp = blueprint(auto) + + # Test index (path is provided) - should include signature + result_index = renderer.summarize(bp, path="test") + assert "pos_only(x, a, [b])" in result_index.link + + # Test TOC (path is None) - should NOT include signature + result_toc = renderer.summarize(bp, path=None) + assert "pos_only]" in result_toc.link + assert "pos_only(" not in result_toc.link