Skip to content

Commit c5cd150

Browse files
committed
feat: enhance documentation generation by skipping internal methods, improving docstring parsing, and adding orphaned file cleanup.
1 parent 9403eef commit c5cd150

File tree

1 file changed

+103
-39
lines changed

1 file changed

+103
-39
lines changed

praisonai_tools/docs_generator/generator.py

Lines changed: 103 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,13 @@ def sanitize_description(text: str, max_length: int = 150) -> str:
230230
SKIP_MODULES = {
231231
"__pycache__", "_config", "_lazy", "_logging", "_warning_patch",
232232
"_resolver_helpers", "audit", "lite", "profiling", "utils", "__init__",
233-
"_dev", "test", "tests", "__main__"
233+
"_dev", "test", "tests", "__main__", "setup", ".ipynb_checkpoints"
234+
}
235+
SKIP_METHODS = {
236+
"to_dict", "from_dict", "to_json", "from_json", "to_yaml", "from_yaml",
237+
"copy", "dict", "json", "__init__", "__call__", "__str__", "__repr__",
238+
"model_dump", "model_validate", "model_json_schema", "__post_init__",
239+
"__enter__", "__exit__", "__iter__", "__len__", "__getitem__", "__setitem__"
234240
}
235241

236242
ICON_MAP = {
@@ -565,8 +571,10 @@ def _parse_docstring(self, docstring: str) -> Dict[str, Any]:
565571
if not docstring:
566572
return result
567573

568-
# More robust splitting that handles optional trailing colon and optional newline
569-
sections = re.split(r'\n\s*(Args|Parameters|Returns|Raises|Example|Examples|Usage):?\s*(?:\n|$)', docstring, flags=re.IGNORECASE)
574+
# More robust splitting: only match sections at the start of original lines
575+
# This prevents picking up 'Examples:' inside an 'Args' description
576+
section_pattern = r'\n\s*(Args|Parameters|Returns|Raises|Example|Examples|Usage):?\s*\n'
577+
sections = re.split(section_pattern, '\n' + docstring)
570578
result["description"] = sections[0].strip()
571579

572580
for i in range(1, len(sections), 2):
@@ -601,7 +609,10 @@ def _parse_docstring(self, docstring: str) -> Dict[str, Any]:
601609
result["raises"].append((match.group(1), match.group(2)))
602610

603611
elif section_name in ("example", "examples", "usage"):
604-
result["examples"].append(section_content.strip())
612+
content = section_content.strip()
613+
# Strip existing triple backticks if they wrap the entire example
614+
content = re.sub(r'^```[a-z]*\n?(.*?)\n?```$', r'\1', content, flags=re.DOTALL)
615+
result["examples"].append(content.strip())
605616

606617
return result
607618

@@ -734,12 +745,12 @@ def _parse_functions(self, content: str) -> List[FunctionInfo]:
734745
class MDXGenerator:
735746
"""Generate MDX documentation files."""
736747

737-
def __init__(self, output_dir: Path, package_name: str, config: dict, layout: LayoutType = LayoutType.GRANULAR):
748+
def __init__(self, output_dir: Path, package_name: str, config: Dict[str, Any], layout: LayoutType = LayoutType.LEGACY):
738749
self.output_dir = output_dir
739750
self.package_name = package_name
740-
self.generated_files: List[str] = []
741751
self.config = config
742752
self.layout = layout
753+
self.generated_files = set() # Use set for faster lookups and cleanup
743754

744755
def generate_module_doc(self, info: ModuleInfo, dry_run: bool = False) -> List[Path]:
745756
"""Generate MDX documentation for a module (potentially multiple files)."""
@@ -784,8 +795,22 @@ def _generate_granular(self, info: ModuleInfo, dry_run: bool = False) -> List[Pa
784795
self._track_file(class_file)
785796
generated.append(class_file)
786797

787-
# 3. Class Methods (as separate function pages)
798+
# 2. Class Methods (as separate function pages)
799+
for m in cls.class_methods:
800+
if m.name in SKIP_METHODS:
801+
continue
802+
method_file = self.output_dir / "functions" / f"{cls.name}-{m.name}.mdx"
803+
method_content = self._render_function_page(m, info, cls_name=cls.name)
804+
if not dry_run:
805+
method_file.parent.mkdir(parents=True, exist_ok=True)
806+
method_file.write_text(method_content)
807+
self._track_file(method_file)
808+
generated.append(method_file)
809+
810+
# 3. Instance Methods (as separate function pages)
788811
for m in cls.methods:
812+
if m.name in SKIP_METHODS:
813+
continue
789814
method_file = self.output_dir / "functions" / f"{cls.name}-{m.name}.mdx"
790815
method_content = self._render_function_page(m, info, cls_name=cls.name)
791816
if not dry_run:
@@ -812,7 +837,7 @@ def _track_file(self, path: Path):
812837
docs_root = Path("/Users/praison/PraisonAIDocs")
813838
rel_path = str(path.relative_to(docs_root))
814839
nav_path = rel_path.replace('.mdx', '').replace('\\', '/')
815-
self.generated_files.append(nav_path)
840+
self.generated_files.add(nav_path)
816841

817842
def _render_module(self, info: ModuleInfo) -> str:
818843
safe_docstring = escape_mdx(info.docstring) if info.docstring else ""
@@ -906,14 +931,16 @@ def _render_module_granular(self, info: ModuleInfo) -> str:
906931
lines.append("</CardGroup>\n")
907932

908933
if info.functions:
909-
lines.append("## Functions\n")
910-
lines.append("<CardGroup cols={2}>")
911-
for func in info.functions:
912-
lines.append(f' <Card title="{func.name}()" icon="function" href="../functions/{func.name}">')
913-
func_desc = sanitize_description(func.docstring) or "Function definition."
914-
lines.append(f" {func_desc}")
915-
lines.append(" </Card>")
916-
lines.append("</CardGroup>\n")
934+
visible_functions = [f for f in info.functions if f.name not in SKIP_METHODS]
935+
if visible_functions:
936+
lines.append("## Functions\n")
937+
lines.append("<CardGroup cols={2}>")
938+
for func in visible_functions:
939+
lines.append(f' <Card title="{func.name}()" icon="function" href="../functions/{func.name}">')
940+
func_desc = sanitize_description(func.docstring) or "Function definition."
941+
lines.append(f" {func_desc}")
942+
lines.append(" </Card>")
943+
lines.append("</CardGroup>\n")
917944

918945
if info.constants:
919946
lines.extend([
@@ -980,19 +1007,31 @@ def _render_class_page(self, cls: ClassInfo, module_info: ModuleInfo) -> str:
9801007
])
9811008

9821009
if cls.methods or cls.class_methods:
983-
lines.append("## Methods\n")
984-
lines.append("<CardGroup cols={2}>")
985-
for m in cls.class_methods:
986-
lines.append(f' <Card title="{m.name}()" icon="function" href="../functions/{cls.name}-{m.name}">')
987-
m_desc = sanitize_description(m.docstring) or "Class method."
988-
lines.append(f" {m_desc}")
989-
lines.append(" </Card>")
990-
for m in cls.methods:
991-
lines.append(f' <Card title="{m.name}()" icon="function" href="../functions/{cls.name}-{m.name}">')
992-
m_desc = sanitize_description(m.docstring) or "Instance method."
993-
lines.append(f" {m_desc}")
994-
lines.append(" </Card>")
995-
lines.append("</CardGroup>\n")
1010+
visible_class_methods = [m for m in cls.class_methods if m.name not in SKIP_METHODS]
1011+
visible_methods = [m for m in cls.methods if m.name not in SKIP_METHODS]
1012+
1013+
if visible_class_methods or visible_methods:
1014+
lines.append("## Methods\n")
1015+
lines.append("<CardGroup cols={2}>")
1016+
for m in visible_class_methods:
1017+
lines.append(f' <Card title="{m.name}()" icon="function" href="../functions/{cls.name}-{m.name}">')
1018+
m_desc = sanitize_description(m.docstring) or "Class method."
1019+
lines.append(f" {m_desc}")
1020+
lines.append(" </Card>")
1021+
for m in visible_methods:
1022+
lines.append(f' <Card title="{m.name}()" icon="function" href="../functions/{cls.name}-{m.name}">')
1023+
m_desc = sanitize_description(m.docstring) or "Instance method."
1024+
lines.append(f" {m_desc}")
1025+
lines.append(" </Card>")
1026+
lines.append("</CardGroup>\n")
1027+
1028+
# List skipped methods in an accordion without links for completeness
1029+
skipped_methods = [m for m in (cls.class_methods + cls.methods) if m.name in SKIP_METHODS]
1030+
if skipped_methods:
1031+
lines.append('<Accordion title="Internal & Generic Methods">')
1032+
for m in skipped_methods:
1033+
lines.append(f"- **{m.name}**: {sanitize_description(m.docstring) or 'Generic utility method.'}")
1034+
lines.append("</Accordion>\n")
9961035

9971036
if cls.examples:
9981037
lines.append("## Usage\n")
@@ -1096,10 +1135,15 @@ def _render_function_page(self, func: FunctionInfo, module_info: ModuleInfo, cls
10961135
if len(func.examples) > 1:
10971136
lines.append("<CodeGroup>")
10981137
for i, ex in enumerate(func.examples):
1099-
lines.append(f"```python Example {i+1}\n{ex}\n```")
1138+
# Dedent example content for cleaner display
1139+
import textwrap
1140+
dedented_ex = textwrap.dedent(ex)
1141+
lines.append(f"```python Example {i+1}\n{dedented_ex}\n```")
11001142
lines.append("</CodeGroup>\n")
11011143
else:
1102-
lines.append(f"```python\n{func.examples[0]}\n```\n")
1144+
import textwrap
1145+
dedented_ex = textwrap.dedent(func.examples[0])
1146+
lines.append(f"```python\n{dedented_ex}\n```\n")
11031147

11041148
return "\n".join(lines)
11051149

@@ -1151,12 +1195,14 @@ def _render_class(self, cls: ClassInfo, package: str) -> List[str]:
11511195
lines.append("</Accordion>\n")
11521196

11531197
if cls.methods:
1154-
lines.append('<Accordion title="Methods">')
1155-
for m in cls.methods:
1156-
lines.append(f"- **{'async ' if m.is_async else ''}{m.name}**(`{escape_for_table(m.signature, is_type=True)}`) → `{escape_for_table(m.return_type, is_type=True)}`")
1157-
if m.docstring:
1158-
lines.append(f" {escape_mdx(m.docstring.split('\\n')[0][:80])}")
1159-
lines.append("</Accordion>\n")
1198+
visible_methods = [m for m in cls.methods if m.name not in SKIP_METHODS]
1199+
if visible_methods:
1200+
lines.append('<Accordion title="Methods">')
1201+
for m in visible_methods:
1202+
lines.append(f"- **{'async ' if m.is_async else ''}{m.name}**(`{escape_for_table(m.signature, is_type=True)}`) → `{escape_for_table(m.return_type, is_type=True)}`")
1203+
if m.docstring:
1204+
lines.append(f" {escape_mdx(m.docstring.split('\\n')[0][:80])}")
1205+
lines.append("</Accordion>\n")
11601206

11611207
return lines
11621208

@@ -1277,9 +1323,27 @@ def generate_package(self, package_name: str, dry_run: bool = False):
12771323
print(f" Generated: {r.name}")
12781324

12791325
if generator.generated_files and not dry_run:
1326+
print(f"\nCleaning up orphaned MDX files...")
1327+
self.cleanup_orphaned_files(config["output"], generator.generated_files)
1328+
12801329
print(f"\nUpdating docs.json navigation...")
1281-
self.update_docs_json(package_name, generator.generated_files)
1282-
print(f"Updated docs.json with {len(generator.generated_files)} pages")
1330+
self.update_docs_json(package_name, sorted(list(generator.generated_files)))
1331+
1332+
def cleanup_orphaned_files(self, output_dir: Path, current_files: set):
1333+
"""Delete MDX files that are no longer part of the generated set."""
1334+
docs_root = Path("/Users/praison/PraisonAIDocs")
1335+
for sub_dir in ["functions", "classes", "modules"]:
1336+
target_dir = output_dir / sub_dir
1337+
if not target_dir.exists():
1338+
continue
1339+
1340+
for mdx_file in target_dir.glob("*.mdx"):
1341+
rel_path = str(mdx_file.relative_to(docs_root))
1342+
nav_path = rel_path.replace('.mdx', '').replace('\\', '/')
1343+
if nav_path not in current_files:
1344+
print(f" Deleting orphaned file: {mdx_file.name}")
1345+
mdx_file.unlink()
1346+
print(f"Updated package with {len(current_files)} pages")
12831347

12841348

12851349
def update_docs_json(self, package_name: str, generated_pages: List[str]):

0 commit comments

Comments
 (0)