11#!/usr/bin/env python3
2- """Generate a Markdown file for each Python example .
2+ """Generate aggregated Markdown files for each examples subfolder .
33
44Usage:
55 python .scripts/examples_to_markdown_files.py \
66 --examples-dir examples \
7- --template templates/example_file.mustache \
87 --output-dir docs
98
10- The directory structure under ``examples`` is mirrored under the output
11- directory. Each ``.py`` file becomes a ``.md`` file with the same name and
12- relative path. Files named ``__init__.py`` are ignored and skipped.
9+ For every immediate subfolder in ``examples`` a single ``.md`` file is created
10+ in the output directory. The file name matches the subfolder name and contains
11+ all example files from that folder. Folder and file names are converted to
12+ title format: CamelCase words are split with spaces and acronyms remain in
13+ uppercase.
1314"""
1415
1516import argparse
1617import re
1718from pathlib import Path
1819
19- try :
20- import pystache
21- except ModuleNotFoundError as exc : # pragma: no cover - import guard
22- raise SystemExit (
23- "pystache is required to run this script. Install it via `pip install pystache`."
24- ) from exc
2520
2621
27- def parse_example (path : Path ) -> dict :
28- """Return the file name and entire contents of the example."""
29- with path .open ("r" , encoding = "utf-8" ) as f :
30- code = f .read ()
31- return {"name" : path .stem , "code" : code .rstrip ()}
22+ def to_title (name : str ) -> str :
23+ """Return *name* in title format with spaces and preserved acronyms."""
24+ # Remove numeric prefixes like ``01_``
25+ name = re .sub (r"^\d+[_-]?" , "" , name )
26+ parts = re .split (r"[_\-\s]+" , name )
27+ words = []
28+ for part in parts :
29+ if not part :
30+ continue
31+ if re .fullmatch (r"[A-Z0-9]+" , part ):
32+ words .append (part )
33+ elif re .fullmatch (r"[a-z]+" , part ) and len (part ) <= 4 :
34+ words .append (part .upper ())
35+ else :
36+ words .append (part .capitalize ())
37+ return " " .join (words )
3238
3339
34- def render_markdown (template : str , context : dict ) -> str :
35- """Render the Markdown using a Mustache template."""
36- renderer = pystache .Renderer ()
37- return renderer .render (template , context )
3840
3941
40- def to_camel_case (name : str ) -> str :
41- """Return *name* converted to CamelCase without leading digits."""
42- # Strip leading numeric prefixes like ``01_``
43- name = re .sub (r"^\d+_?" , "" , name )
44- parts = re .split (r"[_\-\s]+" , name )
45- return "" .join (word .capitalize () for word in parts if word )
42+ def generate_aggregate (folder : Path , output_dir : Path ) -> None :
43+ """Create a single Markdown file aggregating all examples in *folder*."""
44+ examples = []
45+ for py_file in sorted (folder .glob ("*.py" )):
46+ if py_file .name == "__init__.py" :
47+ continue
48+ with py_file .open ("r" , encoding = "utf-8" ) as f :
49+ code = f .read ().rstrip ()
50+ examples .append ((to_title (py_file .stem ), code ))
4651
52+ if not examples :
53+ return
4754
48- def process_file (py_path : Path , template : str , base_dir : Path , output_dir : Path ) -> None :
49- """Write a Markdown version of *py_path* under *output_dir*."""
50- context = parse_example (py_path )
51- markdown = render_markdown (template , context )
55+ title = to_title (folder .name )
56+ lines = [f"# { title } " , "" ]
57+ for name , code in examples :
58+ lines .append (f"## { name } " )
59+ lines .append ("" )
60+ lines .append ("```python" )
61+ lines .append (code )
62+ lines .append ("```" )
63+ lines .append ("" )
5264
53- relative = py_path .relative_to (base_dir ).with_suffix (".md" )
54- md_path = output_dir / relative
55- md_path .parent .mkdir (parents = True , exist_ok = True )
56- with md_path .open ("w" , encoding = "utf-8" ) as f :
57- f .write (markdown )
65+ agg_path = output_dir / f"{ folder .name } .md"
66+ with agg_path .open ("w" , encoding = "utf-8" ) as f :
67+ f .write ("\n " .join (lines ).rstrip () + "\n " )
5868
5969
6070def generate_aggregate (folder : Path , base_dir : Path , output_dir : Path ) -> None :
@@ -87,18 +97,13 @@ def generate_aggregate(folder: Path, base_dir: Path, output_dir: Path) -> None:
8797
8898def main () -> None :
8999 parser = argparse .ArgumentParser (
90- description = "Generate Markdown files mirroring the examples directory "
100+ description = "Generate aggregated Markdown files from the examples"
91101 )
92102 parser .add_argument (
93103 "--examples-dir" ,
94104 default = "examples" ,
95105 help = "Directory containing example .py files" ,
96106 )
97- parser .add_argument (
98- "--template" ,
99- default = "templates/example_file.mustache" ,
100- help = "Mustache template file" ,
101- )
102107 parser .add_argument (
103108 "--output-dir" ,
104109 default = "docs" ,
@@ -113,13 +118,11 @@ def main() -> None:
113118 # previously generated documentation.
114119 output_dir .mkdir (parents = True , exist_ok = True )
115120
116- with Path (args .template ).open ("r" , encoding = "utf-8" ) as f :
117- template = f .read ()
118121
119- for py_file in sorted ( examples_dir . rglob ( "*.py" )):
120- if py_file . name == "__init__.py" :
121- continue
122- process_file ( py_file , template , examples_dir , output_dir )
122+
123+ for folder in sorted ( examples_dir . iterdir ()) :
124+ if folder . is_dir ():
125+ generate_aggregate ( folder , output_dir )
123126
124127 for folder in sorted (examples_dir .iterdir ()):
125128 if folder .is_dir ():
0 commit comments