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
1 change: 1 addition & 0 deletions quartodoc/builder/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions quartodoc/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 = []
Expand All @@ -370,6 +374,7 @@ def from_griffe(
"obj": obj,
"anchor": anchor,
"signature_name": signature_name,
"signature_summary": signature_summary,
}

if kind == "function":
Expand Down
100 changes: 98 additions & 2 deletions quartodoc/renderers/md_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down
109 changes: 109 additions & 0 deletions quartodoc/tests/test_renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading