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
25 changes: 24 additions & 1 deletion src/attune_author/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,11 +1040,15 @@ def _collect_function(
info: _SourceInfo,
) -> None:
first_line = _docstring_first_line(node)
signature = _format_function_signature(node)
params, returns = _split_signature(signature, node.name)
info.public_functions.append({"name": node.name, "doc": first_line, "file": rel_path})
info.function_signatures.append(
{
"name": node.name,
"signature": _format_function_signature(node),
"signature": signature,
"params": params,
"returns": returns,
"doc": first_line,
"file": rel_path,
"raises": _extract_raises(node),
Expand Down Expand Up @@ -1202,6 +1206,23 @@ def _format_function_signature(
return sig


def _split_signature(signature: str, name: str) -> tuple[str, str]:
# Splits ``name(params) -> R`` into ``(params, R)``. Returns
# ``("", "")`` if the signature doesn't match the expected
# shape — keeps rendering callers defensive.
prefix = f"{name}("
if not signature.startswith(prefix):
return ("", "")
rest = signature[len(prefix) :]
end = rest.rfind(")")
if end == -1:
return ("", "")
params = rest[:end]
tail = rest[end + 1 :].lstrip()
returns = tail[3:].strip() if tail.startswith("->") else ""
return (params, returns)


def _format_class_methods(node: ast.ClassDef) -> str:
"""Format a class's public method signatures.

Expand Down Expand Up @@ -1307,6 +1328,8 @@ def _render_template(
tags=feature.tags,
public_classes=source_info.public_classes,
public_functions=source_info.public_functions,
function_signatures=source_info.function_signatures,
class_signatures=source_info.class_signatures,
module_docstrings=source_info.module_docstrings,
config_keys=source_info.config_keys,
file_count=source_info.file_count,
Expand Down
19 changes: 16 additions & 3 deletions src/attune_author/meta_templates/reference.md.j2
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# {{ title }} reference
{% if description %}

{{ description }}
{% endif %}
{% if public_classes %}

## Classes

| Class | Description | File |
Expand All @@ -9,8 +13,17 @@
| `{{ cls.name }}` | {{ cls.doc if cls.doc else "—" }} | `{{ cls.file }}` |
{% endfor %}
{% endif %}
{% if function_signatures %}

## Functions

| Function | Parameters | Returns | Description | File |
|----------|------------|---------|-------------|------|
{% for fn in function_signatures %}
| `{{ fn.name }}` | {{ ("`" ~ fn.params ~ "`") if fn.params else "—" }} | {{ ("`" ~ fn.returns ~ "`") if fn.returns else "—" }} | {{ fn.doc if fn.doc else "—" }} | `{{ fn.file }}` |
{% endfor %}
{% elif public_functions %}

{% if public_functions %}
## Functions

| Function | Description | File |
Expand All @@ -19,8 +32,8 @@
| `{{ fn.name }}()` | {{ fn.doc if fn.doc else "—" }} | `{{ fn.file }}` |
{% endfor %}
{% endif %}

{% if config_keys %}

## Configuration

| Key | Description |
Expand All @@ -39,8 +52,8 @@
{% else %}
No file patterns configured.
{% endif %}

{% if tags %}

## Tags

{% for tag in tags %}`{{ tag }}`{{ ", " if not loop.last else "" }}{% endfor %}
Expand Down
8 changes: 4 additions & 4 deletions tests/__snapshots__/test_generated_templates_golden.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@

# Auth reference

Authentication and authorization

## Functions

| Function | Description | File |
|----------|-------------|------|
| `authenticate()` | Authenticate a user. | `src/auth/login.py` |

| Function | Parameters | Returns | Description | File |
|----------|------------|---------|-------------|------|
| `authenticate` | `username: str, password: str` | `bool` | Authenticate a user. | `src/auth/login.py` |

## Source files

Expand Down
68 changes: 68 additions & 0 deletions tests/test_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,79 @@

from attune_author.generator import (
GenerationResult,
_split_signature,
generate_feature_templates,
)
from attune_author.manifest import Feature


class TestSplitSignature:
"""Tests for ``_split_signature``."""

def test_typed_params_and_return(self) -> None:
params, returns = _split_signature(
"authenticate(username: str, password: str) -> bool", "authenticate"
)
assert params == "username: str, password: str"
assert returns == "bool"

def test_no_return_annotation(self) -> None:
params, returns = _split_signature("main()", "main")
assert params == ""
assert returns == ""

def test_complex_return_with_pipe(self) -> None:
params, returns = _split_signature("main(argv: list[str] | None = None) -> int", "main")
assert params == "argv: list[str] | None = None"
assert returns == "int"

def test_unexpected_shape_returns_empty(self) -> None:
# Defensive: anything not matching ``name(...)`` shouldn't crash.
assert _split_signature("not a signature", "anything") == ("", "")


class TestReferenceTemplateColumns:
"""Reference template must surface Parameters/Returns columns
from ``function_signatures`` without depending on the LLM polish
pass — otherwise a polish bypass (no API key, lenient mode,
cached miss) silently drops typed argument and return data that
the AST already has.
"""

def test_reference_table_includes_typed_params_and_return(
self, help_dir: Path, project_root: Path
) -> None:
feature = Feature(
name="auth",
description="Authentication and authorization",
files=["src/auth/**"],
tags=["security"],
)

with patch.dict("os.environ", {}, clear=False):
result = generate_feature_templates(
feature=feature,
help_dir=help_dir,
project_root=project_root,
depths=["reference"],
use_rag=False,
)

ref = next(t for t in result.templates if t.depth == "reference")
content = ref.path.read_text(encoding="utf-8")

# 4-column header is the structural fix — without polish,
# the old template emitted only ``Function | Description | File``.
assert "| Function | Parameters | Returns | Description | File |" in content
# Typed parameters from the AST are surfaced verbatim.
assert "`username: str, password: str`" in content
# Return annotation is surfaced verbatim.
assert "`bool`" in content
# Feature description from features.yaml lands directly under
# the title, not only via LLM-polish synthesis.
assert "Authentication and authorization" in content


class TestGenerateFeatureTemplates:
"""Tests for generate_feature_templates()."""

Expand Down
Loading