svdoc 0.1.0__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.
svdoc/__init__.py ADDED
File without changes
svdoc/build.py ADDED
@@ -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)
svdoc/cli.py ADDED
@@ -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())
svdoc/fixer.py ADDED
@@ -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
svdoc/ir.py ADDED
@@ -0,0 +1,166 @@
1
+ """Doc IR (v1): module-level only. See PLAN.md Phase 1."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import List, Optional
5
+
6
+
7
+ @dataclass
8
+ class Param:
9
+ """A single module/interface/subroutine parameter.
10
+
11
+ :ivar name: Parameter identifier.
12
+ :ivar type: Declared type as written in source (e.g. ``"int"``).
13
+ :ivar default: Default value expression, or ``None`` if unset.
14
+ :ivar doc: Extracted doc comment, or ``None`` if undocumented.
15
+ :ivar type_ref: Fully-qualified ``"package::type"`` name if resolved
16
+ cross-file via :func:`svdoc.parser.resolve_types`; ``None`` otherwise.
17
+ """
18
+
19
+ name: str
20
+ type: str
21
+ default: Optional[str]
22
+ doc: Optional[str]
23
+ type_ref: Optional[str] = None
24
+
25
+
26
+ @dataclass
27
+ class Port:
28
+ """A single module/interface/subroutine port or argument.
29
+
30
+ Also reused for subroutine arguments (see :attr:`Subroutine.args`), since
31
+ the shape (name, direction, type, doc) is identical.
32
+
33
+ :ivar name: Port identifier.
34
+ :ivar direction: ``"input"``, ``"output"``, ``"inout"``, etc.
35
+ :ivar type: Declared type as written in source, including any packed
36
+ dimensions (e.g. ``"logic [7:0]"``).
37
+ :ivar doc: Extracted doc comment, or ``None`` if undocumented.
38
+ :ivar type_ref: Fully-qualified ``"package::type"`` name if resolved
39
+ cross-file via :func:`svdoc.parser.resolve_types`; ``None`` otherwise.
40
+ :ivar modport_preview: For an interface-typed port (``direction ==
41
+ "interface"``), the resolved :class:`Modport` from the interface file,
42
+ if it was found among the files passed to
43
+ :func:`svdoc.parser.resolve_types`; ``None`` otherwise.
44
+ """
45
+
46
+ name: str
47
+ direction: str
48
+ type: str
49
+ doc: Optional[str]
50
+ type_ref: Optional[str] = None
51
+ modport_preview: Optional["Modport"] = None
52
+
53
+
54
+ @dataclass
55
+ class ModuleDoc:
56
+ """Doc IR for a single SystemVerilog module."""
57
+
58
+ name: str
59
+ doc: Optional[str]
60
+ params: List[Param] = field(default_factory=list)
61
+ ports: List[Port] = field(default_factory=list)
62
+
63
+
64
+ @dataclass
65
+ class Signal:
66
+ """A variable declared in an interface body (outside any modport)."""
67
+
68
+ name: str
69
+ type: str
70
+ doc: Optional[str]
71
+
72
+
73
+ @dataclass
74
+ class ModportPortGroup:
75
+ """One direction-grouped clause within a modport, e.g. ``output valid, data``."""
76
+
77
+ direction: str
78
+ signals: List[str]
79
+ doc: Optional[str]
80
+
81
+
82
+ @dataclass
83
+ class Modport:
84
+ """A single ``modport`` declaration inside an interface."""
85
+
86
+ name: str
87
+ doc: Optional[str]
88
+ port_groups: List[ModportPortGroup] = field(default_factory=list)
89
+
90
+
91
+ @dataclass
92
+ class InterfaceDoc:
93
+ """Doc IR for a single SystemVerilog interface, including its modports."""
94
+
95
+ name: str
96
+ doc: Optional[str]
97
+ params: List[Param] = field(default_factory=list)
98
+ ports: List[Port] = field(default_factory=list)
99
+ signals: List[Signal] = field(default_factory=list)
100
+ modports: List[Modport] = field(default_factory=list)
101
+
102
+
103
+ @dataclass
104
+ class EnumValue:
105
+ """A single named value within an ``enum`` typedef."""
106
+
107
+ name: str
108
+ value: Optional[str]
109
+ doc: Optional[str]
110
+
111
+
112
+ @dataclass
113
+ class StructField:
114
+ """A single field within a packed ``struct`` typedef."""
115
+
116
+ name: str
117
+ type: str
118
+ doc: Optional[str]
119
+
120
+
121
+ @dataclass
122
+ class Typedef:
123
+ """A single ``typedef`` declared in a package: enum, packed struct, or alias.
124
+
125
+ :ivar kind: One of ``"enum"``, ``"struct"``, or ``"alias"`` — determines
126
+ which of ``base_type``/``values``, ``fields``, or ``alias_type`` is populated.
127
+ :ivar base_type: For ``kind="enum"``, the underlying integer type
128
+ (e.g. ``"logic [1:0]"``).
129
+ :ivar alias_type: For ``kind="alias"``, the type being aliased.
130
+ :ivar values: For ``kind="enum"``, the enum's named values in declaration order.
131
+ :ivar fields: For ``kind="struct"``, the struct's fields in declaration order.
132
+ """
133
+
134
+ name: str
135
+ doc: Optional[str]
136
+ kind: str
137
+ base_type: Optional[str] = None
138
+ alias_type: Optional[str] = None
139
+ values: List[EnumValue] = field(default_factory=list)
140
+ fields: List[StructField] = field(default_factory=list)
141
+
142
+
143
+ @dataclass
144
+ class Subroutine:
145
+ """A ``function`` or ``task`` declared in a package.
146
+
147
+ :ivar kind: ``"function"`` or ``"task"``.
148
+ :ivar return_type: Declared return type; always ``None`` for tasks.
149
+ :ivar args: Arguments, reusing :class:`Port`'s name/direction/type/doc shape.
150
+ """
151
+
152
+ name: str
153
+ doc: Optional[str]
154
+ kind: str
155
+ return_type: Optional[str] = None
156
+ args: List[Port] = field(default_factory=list)
157
+
158
+
159
+ @dataclass
160
+ class PackageDoc:
161
+ """Doc IR for a single SystemVerilog package: its typedefs and subroutines."""
162
+
163
+ name: str
164
+ doc: Optional[str]
165
+ typedefs: List[Typedef] = field(default_factory=list)
166
+ subroutines: List[Subroutine] = field(default_factory=list)