bst-docs 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bst_docs/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """API reference generator for buildstream projects."""
bst_docs/__main__.py ADDED
@@ -0,0 +1,16 @@
1
+ """CLI entry point for bst_docs."""
2
+
3
+ import click
4
+
5
+ from bst_docs.build import build
6
+
7
+
8
+ @click.group()
9
+ def cli() -> None:
10
+ """CLI group for bst_docs commands."""
11
+
12
+
13
+ cli.add_command(build)
14
+
15
+ if __name__ == "__main__":
16
+ cli()
bst_docs/build.py ADDED
@@ -0,0 +1,77 @@
1
+ """Build command for generating documentation."""
2
+
3
+ import logging
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ import click
8
+ import tqdm
9
+
10
+ from bst_docs.load_contents import load_project
11
+
12
+
13
+ @click.command()
14
+ @click.option(
15
+ "--depth",
16
+ default=3,
17
+ required=False,
18
+ help="Depth of network crawling to perform",
19
+ )
20
+ @click.option(
21
+ "--output",
22
+ default="./output",
23
+ required=False,
24
+ help="Destination folder for markdown files",
25
+ )
26
+ @click.option(
27
+ "--clean",
28
+ default=False,
29
+ required=False,
30
+ help="Remove all files from cache & output directories",
31
+ )
32
+ @click.option(
33
+ "--html-elements",
34
+ default=True,
35
+ required=False,
36
+ help=(
37
+ "If true, html elements will be embedded in markdown files"
38
+ "otherwise pure Commonmark markdown will be used"
39
+ ),
40
+ )
41
+ @click.argument("path", default=".")
42
+ def build(
43
+ path: str,
44
+ output: str,
45
+ **kwargs: str,
46
+ ) -> None:
47
+ """Generate documentation for a BuildStream project."""
48
+ html_elements: bool = bool(kwargs.get("html_elements"))
49
+
50
+ if kwargs.get("clean"):
51
+ shutil.rmtree(".cache")
52
+ logging.warning("Nuking cache: %s", ".cache")
53
+
54
+ dst = Path(output)
55
+ try:
56
+ dst.mkdir()
57
+ except Exception:
58
+ logging.warning("Overwriting %s", dst)
59
+ shutil.rmtree(dst)
60
+ dst.mkdir()
61
+ files, _config = load_project(path)
62
+
63
+ for f in tqdm.tqdm(files, "Finding dependants"):
64
+ for kind, dependencies in f.all_dependencies().items():
65
+ for dep in dependencies:
66
+ dep = dep if isinstance(dep, str) else dep["filename"]
67
+ try:
68
+ element_dep = next(x for x in files if x.bst_name == dep)
69
+ element_dep.dependants[kind].append(f.bst_name)
70
+ except StopIteration:
71
+ logging.warning(str(f"Unfound dependency {dep} for {f.name}"))
72
+ for f in tqdm.tqdm(files, "Writing Output"):
73
+ f.write_file(dst, use_html=html_elements)
74
+
75
+
76
+ if __name__ == "__main__":
77
+ build.main()
bst_docs/element.py ADDED
@@ -0,0 +1,166 @@
1
+ """Element data model for BuildStream project elements."""
2
+
3
+ import dataclasses
4
+ import logging
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+
9
+ import mdformat
10
+
11
+ from bst_docs.render import add_summary_block, render_code_section, render_value
12
+
13
+
14
+ # TODO: replace this with builstream element
15
+ @dataclasses.dataclass
16
+ class Element:
17
+ """BST Element representation."""
18
+
19
+ _conf: dict
20
+ raw: str
21
+
22
+ def __init__(self, _conf: dict, raw: str) -> None:
23
+ """Initialize Element."""
24
+ self._conf = _conf
25
+ self.raw = raw
26
+ self.dependants: dict[str, list] = {
27
+ "depends": [],
28
+ "build-depends": [],
29
+ "runtime-depends": [],
30
+ }
31
+
32
+ @property
33
+ def name(self) -> str:
34
+ """Return the element path."""
35
+ return str(self._conf.get("path", ""))
36
+
37
+ @property
38
+ def bst_name(self) -> str:
39
+ """Return the element path."""
40
+ return re.sub(f"^{self._conf.get('element_path', '')}/", "", self.name)
41
+
42
+ @property
43
+ def doc_path(self) -> str:
44
+ """Return the markdown file path for this element."""
45
+ return self.name.replace(".bst", ".md")
46
+
47
+ @property
48
+ def project_base(self) -> Path:
49
+ """Return the project base path."""
50
+ return Path(self._conf.get("project_base", ""))
51
+
52
+ @property
53
+ def element_base(self) -> Path:
54
+ """Return the element subdirectory path."""
55
+ return self.project_base / self._conf.get("element_path", "")
56
+
57
+ @property
58
+ def absolute_path(self) -> Path:
59
+ """Return the absolute filesystem path to this element."""
60
+ return self.project_base / self._conf.get("path", "")
61
+
62
+ @property
63
+ def description(self) -> str:
64
+ """Return the element description with a summary header."""
65
+ return (
66
+ ("## Summary\n\n" + self._conf.get("description", "") + "\n")
67
+ if self._conf.get("description")
68
+ else ""
69
+ )
70
+
71
+ def _format_link_list(
72
+ self,
73
+ depend_kind: str,
74
+ use_html: bool,
75
+ depends_dict: dict,
76
+ ) -> str:
77
+ """Format a dependency list as HTML or Markdown links."""
78
+ depend_items = depends_dict.get(depend_kind, [])
79
+ if not depend_items:
80
+ return ""
81
+
82
+ link_list = []
83
+ for item in depend_items:
84
+ rendered = ""
85
+
86
+ match item:
87
+ case str():
88
+ rel_path = os.path.relpath(
89
+ (self.element_base / item),
90
+ self.absolute_path.parent,
91
+ ).replace(".bst", ".md")
92
+ rendered = (
93
+ (
94
+ f'<a href="{rel_path}">{item}</a>'
95
+ if use_html
96
+ else f"[{item}]({rel_path})"
97
+ )
98
+ if ":" not in item
99
+ else render_value(item)
100
+ )
101
+ case _others:
102
+ rendered = render_value(item)
103
+ link_list.append(f"<li>{rendered}</li>" if use_html else f"- {rendered}")
104
+ content = (
105
+ f"<ul>\n{'\n'.join(link_list)}\n<ul>" # no hard tabs
106
+ if use_html
107
+ else "\n".join(link_list)
108
+ )
109
+ return add_summary_block(depend_kind.title(), content, use_html)
110
+
111
+ def all_dependencies(self) -> dict[str, list]:
112
+ """All dependencies of this element."""
113
+ output = {}
114
+ for depend_kind in ["depends", "runtime-depends", "build-depends"]:
115
+ output[depend_kind] = self._conf.get(depend_kind, [])
116
+ return output
117
+
118
+ def _render_field(self, element: str, use_html: bool) -> str:
119
+ """Render a named configuration field as a section."""
120
+ return (
121
+ f"## {element.title()}\n\n"
122
+ + render_value(self._conf.get(element, {}), use_html)
123
+ if self._conf.get(element, {})
124
+ else ""
125
+ )
126
+
127
+ def summary_page(self, use_html: bool) -> str:
128
+ """Generate the full summary page content for this element."""
129
+ return f"""
130
+ # {Path(self.name).name}
131
+
132
+ `{self._conf.get("kind", "Unknown")}`
133
+ {self.description or "--"}
134
+
135
+ ## Dependencies
136
+
137
+ {self._format_link_list("depends", use_html, self._conf)}
138
+ {self._format_link_list("build-depends", use_html, self._conf)}
139
+ {self._format_link_list("runtime-depends", use_html, self._conf)}
140
+
141
+ ## Dependants
142
+ {self._format_link_list("depends", use_html, self.dependants)}
143
+ {self._format_link_list("build-depends", use_html, self.dependants)}
144
+ {self._format_link_list("runtime-depends", use_html, self.dependants)}
145
+
146
+ {self._render_field("variables", use_html)}
147
+ {self._render_field("environment", use_html)}
148
+ {self._render_field("environment-nocache", use_html)}
149
+ {self._render_field("config", use_html)}
150
+ {self._render_field("public", use_html)}
151
+ {self._render_field("sources", use_html)}
152
+
153
+ ## Raw Content
154
+
155
+ {add_summary_block("Raw Text", render_code_section(self.raw, use_html), use_html)}
156
+
157
+ """
158
+
159
+ def write_file(self, parent: Path, use_html: bool) -> None:
160
+ """Write the rendered summary page to disk."""
161
+ dst = parent / self.doc_path
162
+ dst.parent.mkdir(parents=True, exist_ok=True)
163
+ text = self.summary_page(use_html)
164
+ text = mdformat.text(text).replace("\t", " ")
165
+ dst.write_text(text)
166
+ logging.debug("Logged element % to %", self.name, dst)
@@ -0,0 +1,54 @@
1
+ """Load and parse BuildStream project configuration."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import validators
7
+ from git import Repo
8
+ from yaml import safe_load
9
+
10
+ from bst_docs.element import Element
11
+
12
+ DEFAULT_PROJECT_CONF = "project.conf"
13
+ CACHE_DIR = Path("./.cache")
14
+
15
+
16
+ def load_project(path: str | Path) -> tuple[list[Element], dict]:
17
+ """Load a BuildStream project from a URL or local path."""
18
+ output = ([], {})
19
+ if validators.url(path):
20
+ git_repo_name = str(path).split("/")[-1].replace(".git", "")
21
+ dst_repo = CACHE_DIR / git_repo_name
22
+ if not dst_repo.exists():
23
+ Repo.clone_from(path, dst_repo)
24
+ else:
25
+ logging.info("Using cached %s", git_repo_name)
26
+ path = CACHE_DIR / git_repo_name
27
+ if Path(path).resolve().exists():
28
+ output = load_local_project(Path(path).resolve())
29
+ return output
30
+
31
+
32
+ def load_local_project(path: Path) -> tuple[list[Element], dict]:
33
+ """Load a BuildStream project from a local filesystem path."""
34
+ conf_path: Path = path / DEFAULT_PROJECT_CONF
35
+ assert (conf_path).exists(), "Project could not be loaded (no configuration file)"
36
+ conf = safe_load(conf_path.read_text())
37
+ element_path: Path = path / conf.get("element-path", "elements")
38
+ assert (conf_path).exists(), "Project could not be loaded (no element path)"
39
+ elements: list[Element] = []
40
+ for f in element_path.glob("**/*.bst"):
41
+ raw = f.read_text("utf-8")
42
+ yml = safe_load(raw)
43
+ elements.append(
44
+ Element(
45
+ yml
46
+ | {
47
+ "path": f.relative_to(path),
48
+ "project_base": path,
49
+ "element_path": element_path.relative_to(path),
50
+ },
51
+ raw,
52
+ ),
53
+ )
54
+ return elements, conf
bst_docs/render.py ADDED
@@ -0,0 +1,87 @@
1
+ """Rendering utilities for HTML and Markdown output."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def html_ul_from_list(list_obj: list) -> str:
7
+ """Render a list as an HTML unordered list."""
8
+ return f"""
9
+ <ul>
10
+ {"\n".join(f"<li>{render_value(x)}</li>" for x in list_obj)}
11
+ </ul>
12
+ """
13
+
14
+
15
+ def html_br_from_list(list_obj: list) -> str:
16
+ """Render a list with HTML line breaks."""
17
+ return f"""
18
+ {"\n<br />".join(render_value(x, True) for x in list_obj)}
19
+ """
20
+
21
+
22
+ def html_table_from_dict(d: dict) -> str:
23
+ """Render a dictionary as an HTML table."""
24
+ table_elements: str = "\n".join(
25
+ f"<tr><td>{key}</td><td>{render_value(val)}</td></tr>" for key, val in d.items()
26
+ )
27
+ return f"""
28
+ <table>
29
+ {table_elements}
30
+ </table>
31
+ """
32
+
33
+
34
+ def md_table_from_dict(d: dict) -> str:
35
+ """Render a dictionary as a Markdown table."""
36
+ return f"""
37
+ | | |
38
+ |--|--|
39
+ {"\n".join(f"|{key}|{val!s}|" for key, val in d.items())}
40
+ """
41
+
42
+
43
+ def render_value(v: Any, use_html: bool = True) -> str: # noqa: ANN401
44
+ """Render a value (dict, list, or scalar) as HTML or Markdown."""
45
+ out = ""
46
+ match v:
47
+ case dict():
48
+ out = html_table_from_dict(v) if use_html else md_table_from_dict(v)
49
+ case list() | set():
50
+ out = (
51
+ (
52
+ html_ul_from_list(v)
53
+ if v and not isinstance(v[0], dict)
54
+ else html_br_from_list(v)
55
+ )
56
+ if use_html
57
+ else "\n- ".join(render_value(x, use_html) for x in v)
58
+ )
59
+ case _others:
60
+ out = (
61
+ f"<code>{str(v).strip().replace('\n', '</br>')}</code>"
62
+ if use_html
63
+ else f"`{str(v).strip()}`"
64
+ )
65
+ return out
66
+
67
+
68
+ def add_summary_block(summary: str, content: str, use_html: bool) -> str:
69
+ """Wrap content in an HTML details/summary block or plain text."""
70
+ if not use_html:
71
+ return summary + "\n" + content
72
+ return f"""
73
+ <details>
74
+ <summary>{summary}</summary>
75
+ {content}
76
+ </details>
77
+ """
78
+
79
+
80
+ def render_code_section(content: str, use_html: bool) -> str:
81
+ """Render a multiline code block."""
82
+ output = content
83
+ if use_html:
84
+ output = f"<pre><code>{content}</code></pre>"
85
+ else:
86
+ output = f"```\n{content.replace('```', r'\`\`\`')}\n```"
87
+ return output
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: bst-docs
3
+ Version: 0.0.1
4
+ Summary: API Reference Generator for Buildstream Projects
5
+ Author-email: Aedan McHale <aedan.mchale@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/Aedan.mchale/bst-docs
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Software Development :: Build Tools
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: buildstream>=2.7.0
20
+ Requires-Dist: click>=8.4.1
21
+ Requires-Dist: gitpython>=3.1.50
22
+ Requires-Dist: mdformat>=1.0.0
23
+ Requires-Dist: pyyaml>=6.0.3
24
+ Requires-Dist: tqdm>=4.68.2
25
+ Requires-Dist: ty>=0.0.43
26
+ Requires-Dist: validators>=0.35.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.5.0; extra == "dev"
29
+ Requires-Dist: prek>=0.4.4; extra == "dev"
30
+ Requires-Dist: ruff>=0.15.15; extra == "dev"
31
+ Requires-Dist: twine>=6.2.0; extra == "dev"
32
+ Requires-Dist: zensical>=0.0.44; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # bst-docs
36
+
37
+ This is a
38
+
39
+ ## Usage
40
+
41
+ To build a documentation folder for a given repo:
42
+ `uv run bst-docs build $REPO_PATH`
43
+
44
+ Or to build a documentation folder for a network repo (clones using your `.netrc`):
45
+ `uv run bst-docs build $REPO_URL`
46
+
47
+ These commands will produce a folder full of markdown files which can then be imported into the documentation portal of your choosing.
48
+
49
+ An example zensical.toml has been provided to view the results in `./output`, to use:
50
+ `uv run zensical serve`
51
+
52
+ ### Configuration Options
53
+
54
+ ```shell
55
+ Usage: bst-docs build [OPTIONS] [PATH]
56
+
57
+ Options:
58
+ --depth INTEGER Depth of network crawling to perform
59
+ --output TEXT Destination folder for markdown files
60
+ --clean BOOLEAN Remove all files from cache & output directories
61
+ --html-elements BOOLEAN If true, html elements will be embedded in markdown
62
+ files otherwise pure Commonmark markdown will be
63
+ used
64
+ --overwrite BOOLEAN Overwrite output directory if true, otherwise
65
+ errors if output files exist
66
+ --help Show this message and exit.
67
+ ```
68
+
69
+ ## Design Choices
70
+
71
+ - Should be possible to render everything as MD, html is for nicenesses
@@ -0,0 +1,12 @@
1
+ bst_docs/__init__.py,sha256=bVzVAPrxVBdHhgcGaigNV_yoNY_oPxGSXD7uLpDxxDg,56
2
+ bst_docs/__main__.py,sha256=g56B2qPR7PS8zZhbMptnQiIhPs1f-jftnVlb6CVwqbg,226
3
+ bst_docs/build.py,sha256=e2xjP5KGj8_Z-L3DwT5aqihc4j7S16TCsOqe177lcjU,2009
4
+ bst_docs/element.py,sha256=QoM3y85Qgg-HvbBgOAy9iBWlzjp0MG4qjZTVIAFANZk,5359
5
+ bst_docs/load_contents.py,sha256=O3GeeQbD2ny0-KWCpazja0WuNj3fUyy79TqW5WTQULA,1815
6
+ bst_docs/render.py,sha256=Z3Vn5knMo84_vQyja5tfU1Jrjk3jM1Y9MMrieYvz7cw,2328
7
+ bst_docs-0.0.1.dist-info/licenses/LICENSE,sha256=IMK1Cqf2Y26SwoVQqgqEeii3KR-HKi7O3Qf3Jecc5vM,1061
8
+ bst_docs-0.0.1.dist-info/METADATA,sha256=ZzsNrqUil3JiZkZifehQblpANhMlxss4mKIIe9Tm-cw,2469
9
+ bst_docs-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ bst_docs-0.0.1.dist-info/entry_points.txt,sha256=abhS86AQY9SzCI1mTzD8mmPd-6oRbc7xEqUvi3fy8-4,51
11
+ bst_docs-0.0.1.dist-info/top_level.txt,sha256=3fYXMvehhejDN7TTvOT0B_rX8MO3Ob4ZcX7H6iQR_qk,9
12
+ bst_docs-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bst-docs = bst_docs.__main__:cli
@@ -0,0 +1,8 @@
1
+ Copyright 2026 Aedan McHale
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
@@ -0,0 +1 @@
1
+ bst_docs