svdoc 0.1.0__tar.gz

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.
svdoc-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 svdoc contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
svdoc-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: svdoc
3
+ Version: 0.1.0
4
+ Summary: pydoc-style documentation generator for SystemVerilog RTL
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 svdoc contributors
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Classifier: Development Status :: 3 - Alpha
28
+ Classifier: Environment :: Console
29
+ Classifier: Intended Audience :: Developers
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: Topic :: Software Development :: Documentation
33
+ Classifier: Topic :: Utilities
34
+ Requires-Python: >=3.9
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: pyslang>=11.0.0
38
+ Provides-Extra: docs
39
+ Requires-Dist: sphinx; extra == "docs"
40
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
41
+ Provides-Extra: dev
42
+ Requires-Dist: ruff; extra == "dev"
43
+ Requires-Dist: pytest; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ # svdoc
47
+
48
+ A pydoc-style documentation generator for SystemVerilog RTL, built on
49
+ [`pyslang`](https://pypi.org/project/pyslang/) (Python bindings for the
50
+ `slang` SystemVerilog compiler).
51
+
52
+ Extracts Doxygen-style doc comments (`/** @brief ... */`, `///< ...`) from
53
+ modules, interfaces, and packages, and renders clean Markdown or HTML
54
+ documentation — no manual editing required.
55
+
56
+ ## What it documents today
57
+
58
+ - **Modules** — name, parameters, ports, doc comments
59
+ - **Interfaces** — parameters, ports, signals, modports
60
+ - **Packages** — typedefs (enums, packed structs, aliases), functions, tasks
61
+ - **Cross-file type resolution** — a module's port typed from a package in
62
+ another file resolves to its fully-qualified `package::type` name
63
+
64
+ Not yet supported: classes/UVM, instantiation hierarchy, block diagrams, full
65
+ multi-file `` `include`` resolution.
66
+
67
+ ## Install
68
+
69
+ ```
70
+ pip install svdoc
71
+ ```
72
+
73
+ ## Usage
74
+
75
+ ```
76
+ # print Markdown to stdout
77
+ svdoc my_module.sv
78
+
79
+ # write a .md or .html file instead
80
+ svdoc my_module.sv --out md
81
+ svdoc my_module.sv --out html
82
+
83
+ # resolve cross-file types (e.g. a module using a package's enum/struct)
84
+ svdoc my_module.sv my_package.sv --out html
85
+
86
+ # auto-scaffold missing doc comments in place (like `ruff --fix`)
87
+ svdoc my_module.sv --fix
88
+
89
+ # build a full linked HTML site from many files (any directory layout)
90
+ svdoc build my_module.sv my_interface.sv my_package.sv --out-dir site/
91
+ ```
92
+
93
+ ## Doc comment convention
94
+
95
+ Doxygen-style, both forms supported:
96
+
97
+ ```systemverilog
98
+ /**
99
+ * @brief A simple synchronous FIFO.
100
+ */
101
+ module fifo #(
102
+ parameter int DEPTH = 16 ///< Number of entries in the FIFO
103
+ ) (
104
+ input logic clk ///< Clock
105
+ );
106
+ ```
107
+
108
+ ## Development
109
+
110
+ ```
111
+ python3 -m venv .venv
112
+ source .venv/bin/activate
113
+ pip install -e ".[dev,docs]"
114
+ python -m pytest
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT — see `LICENSE`.
svdoc-0.1.0/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # svdoc
2
+
3
+ A pydoc-style documentation generator for SystemVerilog RTL, built on
4
+ [`pyslang`](https://pypi.org/project/pyslang/) (Python bindings for the
5
+ `slang` SystemVerilog compiler).
6
+
7
+ Extracts Doxygen-style doc comments (`/** @brief ... */`, `///< ...`) from
8
+ modules, interfaces, and packages, and renders clean Markdown or HTML
9
+ documentation — no manual editing required.
10
+
11
+ ## What it documents today
12
+
13
+ - **Modules** — name, parameters, ports, doc comments
14
+ - **Interfaces** — parameters, ports, signals, modports
15
+ - **Packages** — typedefs (enums, packed structs, aliases), functions, tasks
16
+ - **Cross-file type resolution** — a module's port typed from a package in
17
+ another file resolves to its fully-qualified `package::type` name
18
+
19
+ Not yet supported: classes/UVM, instantiation hierarchy, block diagrams, full
20
+ multi-file `` `include`` resolution.
21
+
22
+ ## Install
23
+
24
+ ```
25
+ pip install svdoc
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```
31
+ # print Markdown to stdout
32
+ svdoc my_module.sv
33
+
34
+ # write a .md or .html file instead
35
+ svdoc my_module.sv --out md
36
+ svdoc my_module.sv --out html
37
+
38
+ # resolve cross-file types (e.g. a module using a package's enum/struct)
39
+ svdoc my_module.sv my_package.sv --out html
40
+
41
+ # auto-scaffold missing doc comments in place (like `ruff --fix`)
42
+ svdoc my_module.sv --fix
43
+
44
+ # build a full linked HTML site from many files (any directory layout)
45
+ svdoc build my_module.sv my_interface.sv my_package.sv --out-dir site/
46
+ ```
47
+
48
+ ## Doc comment convention
49
+
50
+ Doxygen-style, both forms supported:
51
+
52
+ ```systemverilog
53
+ /**
54
+ * @brief A simple synchronous FIFO.
55
+ */
56
+ module fifo #(
57
+ parameter int DEPTH = 16 ///< Number of entries in the FIFO
58
+ ) (
59
+ input logic clk ///< Clock
60
+ );
61
+ ```
62
+
63
+ ## Development
64
+
65
+ ```
66
+ python3 -m venv .venv
67
+ source .venv/bin/activate
68
+ pip install -e ".[dev,docs]"
69
+ python -m pytest
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT — see `LICENSE`.
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "svdoc"
7
+ version = "0.1.0"
8
+ description = "pydoc-style documentation generator for SystemVerilog RTL"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.9"
12
+ dependencies = ["pyslang>=11.0.0"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Documentation",
20
+ "Topic :: Utilities",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ docs = ["sphinx", "sphinx-rtd-theme"]
25
+ dev = ["ruff", "pytest"]
26
+
27
+ [project.scripts]
28
+ svdoc = "svdoc.cli:main"
29
+
30
+ [tool.setuptools]
31
+ packages = ["svdoc"]
svdoc-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,71 @@
1
+ """svdoc build: a flat multi-page HTML site (one page per construct) with a
2
+ navigable index, so cross-links between pages always resolve correctly --
3
+ avoids the relative-path problem of generating standalone pages into
4
+ whatever directories the source files happen to live in.
5
+ """
6
+
7
+ import pathlib
8
+ import re
9
+ from html import escape
10
+
11
+ from . import render_html
12
+ from .ir import InterfaceDoc, ModuleDoc, PackageDoc
13
+ from .parser import parse_file, resolve_types
14
+
15
+ _UNSAFE_NAME_CHARS = re.compile(r"[^\w.-]")
16
+
17
+
18
+ def _safe_page_name(name: str) -> str:
19
+ """Sanitize a construct name for use as an output filename. SV escaped
20
+ identifiers (``\\../../foo``) parse without error and can contain path
21
+ separators, so page filenames can't trust ``doc.name`` directly."""
22
+ return _UNSAFE_NAME_CHARS.sub("_", name)
23
+
24
+ _RENDERERS = {
25
+ ModuleDoc: render_html.render,
26
+ InterfaceDoc: render_html.render_interface,
27
+ PackageDoc: render_html.render_package,
28
+ }
29
+ _KIND_LABELS = {
30
+ ModuleDoc: "Module",
31
+ InterfaceDoc: "Interface",
32
+ PackageDoc: "Package",
33
+ }
34
+
35
+
36
+ def build_site(paths: list, out_dir: str) -> str:
37
+ """Parse every file in ``paths`` and write one HTML page per construct
38
+ into a single flat ``out_dir``, plus an ``index.html`` linking to all of
39
+ them. Because every page lands in the same directory, the convention-based
40
+ cross-links in :mod:`svdoc.render_html` (``pkg_name.html#member``) always
41
+ resolve correctly, regardless of where the source ``.sv`` files live.
42
+
43
+ :param paths: All ``.sv`` files to document. Each is parsed independently
44
+ (one construct per file, same as :func:`svdoc.parser.parse_file`);
45
+ cross-file type resolution runs against the full set for every module.
46
+ :param out_dir: Directory to write the site into. Created if missing.
47
+ :returns: Path to the written ``index.html``.
48
+ :raises ValueError: If any file fails to parse cleanly.
49
+ """
50
+ out = pathlib.Path(out_dir)
51
+ out.mkdir(parents=True, exist_ok=True)
52
+
53
+ docs = [parse_file(p) for p in paths]
54
+ for doc in docs:
55
+ if isinstance(doc, ModuleDoc):
56
+ resolve_types(doc, paths)
57
+
58
+ index_lines = ["<h1>svdoc site</h1>", "<ul>"]
59
+ for doc in sorted(docs, key=lambda d: (_KIND_LABELS[type(d)], d.name)):
60
+ page_name = f"{_safe_page_name(doc.name)}.html"
61
+ (out / page_name).write_text(_RENDERERS[type(doc)](doc))
62
+ label = _KIND_LABELS[type(doc)]
63
+ index_lines.append(
64
+ f'<li>{label}: <a href="{escape(page_name)}">{escape(doc.name)}</a></li>'
65
+ )
66
+ index_lines.append("</ul>")
67
+
68
+ index_html = render_html.page("svdoc site", index_lines)
69
+ index_path = out / "index.html"
70
+ index_path.write_text(index_html)
71
+ return str(index_path)
@@ -0,0 +1,123 @@
1
+ """svdoc CLI (v1): `svdoc <file.sv>` prints to terminal; `--out md`/`--out html`
2
+ writes a file; `svdoc build <files...>` writes a multi-page linked HTML site.
3
+ """
4
+
5
+ import argparse
6
+ import pathlib
7
+ import sys
8
+
9
+ from . import render_html, render_md
10
+ from .build import build_site
11
+ from .fixer import fix_file
12
+ from .ir import InterfaceDoc, ModuleDoc, PackageDoc
13
+ from .parser import parse_file, resolve_types
14
+
15
+ _RENDERERS = {
16
+ "md": {
17
+ ModuleDoc: render_md.render,
18
+ InterfaceDoc: render_md.render_interface,
19
+ PackageDoc: render_md.render_package,
20
+ },
21
+ "html": {
22
+ ModuleDoc: render_html.render,
23
+ InterfaceDoc: render_html.render_interface,
24
+ PackageDoc: render_html.render_package,
25
+ },
26
+ }
27
+ _EXTENSIONS = {"md": ".md", "html": ".html"}
28
+
29
+
30
+ def _run_single(args):
31
+ if args.fix:
32
+ changed = fix_file(args.file)
33
+ print(f"{'fixed' if changed else 'no changes needed for'} {args.file}")
34
+ return
35
+
36
+ doc = parse_file(args.file)
37
+ if args.more_files and isinstance(doc, ModuleDoc):
38
+ resolve_types(doc, [args.file] + args.more_files)
39
+
40
+ if args.out:
41
+ text = _RENDERERS[args.out][type(doc)](doc)
42
+ out_path = pathlib.Path(args.file).with_suffix(_EXTENSIONS[args.out])
43
+ out_path.write_text(text)
44
+ print(f"wrote {out_path}")
45
+ else:
46
+ print(_RENDERERS["md"][type(doc)](doc))
47
+
48
+
49
+ def _run_build(args):
50
+ index_path = build_site(args.files, args.out_dir)
51
+ print(f"wrote {len(args.files)} file(s) to a site at {index_path}")
52
+
53
+
54
+ def _build_parser():
55
+ ap = argparse.ArgumentParser(prog="svdoc build")
56
+ ap.add_argument("files", nargs="+", help=".sv files to document")
57
+ ap.add_argument("--out-dir", required=True, help="directory to write the site into")
58
+ return ap
59
+
60
+
61
+ def _single_parser():
62
+ ap = argparse.ArgumentParser(
63
+ prog="svdoc",
64
+ epilog="Also available: `svdoc build <files...> --out-dir <dir>` to "
65
+ "generate a multi-page linked HTML site.",
66
+ )
67
+ ap.add_argument(
68
+ "file",
69
+ help="path to the .sv file containing the module/interface/package to document",
70
+ )
71
+ ap.add_argument(
72
+ "more_files",
73
+ nargs="*",
74
+ help="additional .sv files (e.g. packages it depends on) used only to "
75
+ "resolve cross-file types -- not documented themselves",
76
+ )
77
+ ap.add_argument(
78
+ "--out",
79
+ choices=["md", "html"],
80
+ help="write rendered doc to a file in this format instead of printing to stdout",
81
+ )
82
+ ap.add_argument(
83
+ "--fix",
84
+ action="store_true",
85
+ help="insert ///< TODO stubs next to undocumented ports/params in place",
86
+ )
87
+ return ap
88
+
89
+
90
+ def main(argv=None):
91
+ """Entry point for the ``svdoc`` command (registered via ``pyproject.toml``).
92
+
93
+ Two modes, dispatched on whether the first argument is the literal
94
+ ``build`` subcommand (argparse subparsers don't mix cleanly with a
95
+ same-position positional argument, so this is checked manually rather
96
+ than via ``add_subparsers``):
97
+
98
+ - ``svdoc <file.sv> [more_files...]`` — document a single module/interface/
99
+ package, optionally resolving cross-file types against ``more_files``,
100
+ printing Markdown to stdout or writing a single Markdown/HTML file
101
+ (``--out``), or scaffolding missing doc comments in place (``--fix``).
102
+ - ``svdoc build <files...> --out-dir <dir>`` — document every given file
103
+ into a single flat multi-page HTML site (one page per construct plus
104
+ an ``index.html``), so cross-links between pages always resolve
105
+ correctly regardless of where the source files live on disk.
106
+
107
+ :param argv: Argument list to parse (as :func:`sys.argv`\\ ``[1:]`` would
108
+ appear). Defaults to ``sys.argv[1:]`` when ``None``.
109
+ """
110
+ if argv is None:
111
+ argv = sys.argv[1:]
112
+
113
+ if argv and argv[0] == "build":
114
+ args = _build_parser().parse_args(argv[1:])
115
+ _run_build(args)
116
+ return
117
+
118
+ args = _single_parser().parse_args(argv)
119
+ _run_single(args)
120
+
121
+
122
+ if __name__ == "__main__":
123
+ sys.exit(main())
@@ -0,0 +1,91 @@
1
+ """svdoc --fix: insert `///< TODO` next to undocumented ports/params, and a
2
+ `/** @brief TODO */` stub above an undocumented module, in place. Mirrors
3
+ `ruff --fix` — scaffolds missing docs rather than guessing their content.
4
+ """
5
+
6
+ import pyslang
7
+
8
+ from .parser import _leading_doc, _trailing_doc
9
+
10
+
11
+ def _end_offset(node) -> int:
12
+ """node may be a raw Token (comma, closing paren) or a syntax node."""
13
+ tok = node.getLastToken() if hasattr(node, "getLastToken") else node
14
+ return tok.range.end.offset
15
+
16
+
17
+ def fix_file(path: str) -> bool:
18
+ """Scaffold missing doc comments in place, in ``path``, and save the result.
19
+
20
+ Inserts ``///< TODO`` next to any undocumented port/parameter, and a
21
+ ``/** @brief TODO */`` stub above the module/interface/package itself if
22
+ it has no doc comment. Idempotent: re-running on an already-documented
23
+ file makes no changes.
24
+
25
+ :param path: Path to the ``.sv`` file to fix in place.
26
+ :returns: ``True`` if the file was modified, ``False`` if it was already
27
+ fully documented.
28
+ :raises ValueError: If the file fails to parse cleanly.
29
+ """
30
+ tree = pyslang.syntax.SyntaxTree.fromFile(path, pyslang.SourceManager())
31
+ if tree.diagnostics:
32
+ raise ValueError(f"parse errors in {path}: {list(tree.diagnostics)}")
33
+
34
+ # module and interface declarations share the same syntax shape
35
+ # (ModuleDeclarationSyntax, differentiated only by .kind) -- --fix scaffolds
36
+ # their header (params/ports) doc comments identically. Body constructs
37
+ # (interface signals/modports) aren't scaffolded here yet.
38
+ mod = next(m for m in tree.root.members if hasattr(m, "header"))
39
+ header = mod.header
40
+
41
+ # (offset, text_to_insert), applied back-to-front so earlier offsets stay valid.
42
+ insertions = []
43
+
44
+ if _leading_doc(mod) is None:
45
+ offset = mod.getFirstToken().range.start.offset
46
+ insertions.append((offset, "/**\n * @brief TODO\n */\n"))
47
+
48
+ if header.parameters:
49
+ raw = list(header.parameters.declarations)
50
+ for i, node in enumerate(raw):
51
+ if node.kind != pyslang.syntax.SyntaxKind.ParameterDeclaration:
52
+ continue
53
+ has_comma = i + 1 < len(raw)
54
+ # trailing ///< doc for this item is leading trivia on the node
55
+ # AFTER its separator (see parser._trailing_doc) -- for the last
56
+ # item there's no comma, so the lookup and insertion point are
57
+ # both the item itself, right before the closing paren.
58
+ lookup = (
59
+ raw[i + 2]
60
+ if has_comma and i + 2 < len(raw)
61
+ else header.parameters.closeParen
62
+ )
63
+ if _trailing_doc(lookup) is None:
64
+ offset = _end_offset(raw[i + 1]) if has_comma else _end_offset(node)
65
+ insertions.append((offset, " ///< TODO"))
66
+
67
+ if header.ports:
68
+ raw = list(header.ports.ports)
69
+ for i, node in enumerate(raw):
70
+ if not hasattr(node, "declarator"):
71
+ continue
72
+ has_comma = i + 1 < len(raw)
73
+ lookup = (
74
+ raw[i + 2]
75
+ if has_comma and i + 2 < len(raw)
76
+ else header.ports.closeParen
77
+ )
78
+ if _trailing_doc(lookup) is None:
79
+ offset = _end_offset(raw[i + 1]) if has_comma else _end_offset(node)
80
+ insertions.append((offset, " ///< TODO"))
81
+
82
+ if not insertions:
83
+ return False
84
+
85
+ with open(path) as f:
86
+ text = f.read()
87
+ for offset, snippet in sorted(insertions, reverse=True):
88
+ text = text[:offset] + snippet + text[offset:]
89
+ with open(path, "w") as f:
90
+ f.write(text)
91
+ return True