Skip to content

Commit 7312a92

Browse files
authored
Merge pull request #1 from fern-api/init-fern-render
feat: init boilerplate
2 parents 1f55a88 + 79c5f0f commit 7312a92

File tree

3 files changed

+179
-2
lines changed

3 files changed

+179
-2
lines changed

src/autodoc2/render/fern_.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Renderer for Fern."""
2+
3+
from __future__ import annotations
4+
5+
import re
6+
import typing as t
7+
8+
from autodoc2.render.base import RendererBase
9+
10+
if t.TYPE_CHECKING:
11+
from autodoc2.utils import ItemData
12+
13+
14+
_RE_DELIMS = re.compile(r"(\s*[\[\]\(\),]\s*)")
15+
16+
17+
class FernRenderer(RendererBase):
18+
"""Render the documentation as Fern-compatible Markdown."""
19+
20+
EXTENSION = ".md"
21+
22+
def render_item(self, full_name: str) -> t.Iterable[str]:
23+
"""Render a single item by dispatching to the appropriate method."""
24+
item = self.get_item(full_name)
25+
if item is None:
26+
raise ValueError(f"Item {full_name} does not exist")
27+
28+
type_ = item["type"]
29+
if type_ == "package":
30+
yield from self.render_package(item)
31+
elif type_ == "module":
32+
yield from self.render_module(item)
33+
elif type_ == "function":
34+
yield from self.render_function(item)
35+
elif type_ == "class":
36+
yield from self.render_class(item)
37+
elif type_ == "exception":
38+
yield from self.render_exception(item)
39+
elif type_ == "property":
40+
yield from self.render_property(item)
41+
elif type_ == "method":
42+
yield from self.render_method(item)
43+
elif type_ == "attribute":
44+
yield from self.render_attribute(item)
45+
elif type_ == "data":
46+
yield from self.render_data(item)
47+
else:
48+
self.warn(f"Unknown item type {type_!r} for {full_name!r}")
49+
50+
def render_function(self, item: ItemData) -> t.Iterable[str]:
51+
"""Create the content for a function."""
52+
short_name = item["full_name"].split(".")[-1]
53+
show_annotations = self.show_annotations(item)
54+
55+
# Build signature
56+
sig = f"{short_name}({self.format_args(item['args'], show_annotations)})"
57+
if show_annotations and item.get("return_annotation"):
58+
sig += f" -> {self.format_annotation(item['return_annotation'])}"
59+
60+
# Fern-style output (starting simple)
61+
yield f"## {sig}"
62+
yield ""
63+
64+
if self.show_docstring(item):
65+
yield item['doc'] # Direct docstring - no directives!
66+
yield ""
67+
68+
def render_module(self, item: ItemData) -> t.Iterable[str]:
69+
"""Create the content for a module."""
70+
# For now, delegate to package rendering
71+
yield from self.render_package(item)
72+
73+
def render_package(self, item: ItemData) -> t.Iterable[str]:
74+
"""Create the content for a package."""
75+
full_name = item["full_name"]
76+
yield f"# {full_name}"
77+
yield ""
78+
79+
if self.show_docstring(item):
80+
yield item['doc']
81+
yield ""
82+
83+
# Get visible children
84+
visible_children = [
85+
i["full_name"]
86+
for i in self.get_children(item)
87+
if i["type"] not in ("package", "module")
88+
]
89+
90+
for name in visible_children:
91+
yield from self.render_item(name)
92+
93+
# Placeholder methods - we'll implement these later
94+
def render_class(self, item: ItemData) -> t.Iterable[str]:
95+
"""Create the content for a class."""
96+
yield f"## {item['full_name'].split('.')[-1]} (class)"
97+
yield "TODO: Implement class rendering"
98+
yield ""
99+
100+
def render_exception(self, item: ItemData) -> t.Iterable[str]:
101+
"""Create the content for an exception."""
102+
yield from self.render_class(item)
103+
104+
def render_property(self, item: ItemData) -> t.Iterable[str]:
105+
"""Create the content for a property."""
106+
yield f"### {item['full_name'].split('.')[-1]} (property)"
107+
yield "TODO: Implement property rendering"
108+
yield ""
109+
110+
def render_method(self, item: ItemData) -> t.Iterable[str]:
111+
"""Create the content for a method."""
112+
yield from self.render_function(item) # Same as function for now
113+
114+
def render_attribute(self, item: ItemData) -> t.Iterable[str]:
115+
"""Create the content for an attribute."""
116+
yield from self.render_data(item)
117+
118+
def render_data(self, item: ItemData) -> t.Iterable[str]:
119+
"""Create the content for a data item."""
120+
short_name = item["full_name"].split(".")[-1]
121+
yield f"### {short_name}"
122+
123+
if item.get("annotation"):
124+
yield f"**Type**: `{self.format_annotation(item['annotation'])}`"
125+
126+
value = item.get("value")
127+
if value is not None:
128+
yield f"**Value**: `{value}`"
129+
130+
if self.show_docstring(item):
131+
yield ""
132+
yield item['doc']
133+
yield ""
134+
135+
def generate_summary(
136+
self, objects: list[ItemData], alias: dict[str, str] | None = None
137+
) -> t.Iterable[str]:
138+
"""Generate a summary of the objects."""
139+
alias = alias or {}
140+
141+
yield "| Name | Description |"
142+
yield "|------|-------------|"
143+
144+
for item in objects:
145+
full_name = item["full_name"]
146+
display_name = alias.get(full_name, full_name.split(".")[-1])
147+
148+
# Get first line of docstring for description
149+
doc = item.get('doc', '').strip()
150+
description = doc.split('\n')[0] if doc else ""
151+
if len(description) > 50:
152+
description = description[:47] + "..."
153+
154+
yield f"| `{display_name}` | {description} |"

tests/test_render.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from autodoc2.config import Config
99
from autodoc2.db import InMemoryDb
1010
from autodoc2.render.base import RendererBase
11+
from autodoc2.render.fern_ import FernRenderer
1112
from autodoc2.render.myst_ import MystRenderer
1213
from autodoc2.render.rst_ import RstRenderer
1314
from autodoc2.utils import yield_modules
@@ -21,8 +22,9 @@
2122
[
2223
(RstRenderer, ".rst"),
2324
(MystRenderer, ".md"),
25+
(FernRenderer, ".md"),
2426
],
25-
ids=["rst", "myst"],
27+
ids=["rst", "myst", "fern"],
2628
)
2729
def test_basic(renderer: RendererBase, extension: str, tmp_path: Path, file_regression):
2830
"""Test basic rendering."""
@@ -40,8 +42,9 @@ def test_basic(renderer: RendererBase, extension: str, tmp_path: Path, file_regr
4042
[
4143
(RstRenderer, ".rst"),
4244
(MystRenderer, ".md"),
45+
(FernRenderer, ".md"),
4346
],
44-
ids=["rst", "myst"],
47+
ids=["rst", "myst", "fern"],
4548
)
4649
def test_config_options(
4750
renderer: RendererBase, extension: str, tmp_path: Path, file_regression
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# package
2+
3+
This is a test package.
4+
5+
### __all__
6+
**Value**: `['p', 'a1', 'alias']`
7+
8+
9+
10+
### p
11+
**Value**: `1`
12+
13+
p can be documented here.
14+
15+
## func(a: str, b: int) -> package.a.c.ac1
16+
17+
This is a function.
18+
19+
## Class (class)
20+
TODO: Implement class rendering

0 commit comments

Comments
 (0)