|
10 | 10 | # add these directories to sys.path here. If the directory is relative to the |
11 | 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. |
12 | 12 | # |
| 13 | +import inspect |
13 | 14 | import os |
| 15 | +import subprocess |
14 | 16 | import sys |
15 | 17 |
|
| 18 | +import pyrtl |
| 19 | + |
16 | 20 | sys.path.insert(0, os.path.abspath("..")) |
17 | 21 |
|
18 | 22 |
|
19 | 23 | # -- Project information ----------------------------------------------------- |
20 | 24 |
|
21 | 25 | project = "PyRTL" |
22 | | -copyright = "2025, Timothy Sherwood" |
| 26 | +copyright = "2026, Timothy Sherwood" |
23 | 27 | author = "Timothy Sherwood" |
24 | 28 |
|
25 | 29 | # -- General configuration --------------------------------------------------- |
|
33 | 37 | "sphinx.ext.autodoc", |
34 | 38 | "sphinx.ext.inheritance_diagram", |
35 | 39 | "sphinx.ext.intersphinx", |
36 | | - "sphinx.ext.viewcode", |
| 40 | + "sphinx.ext.linkcode", |
37 | 41 | "sphinx_autodoc_typehints", |
38 | 42 | "sphinx_copybutton", |
39 | 43 | ] |
|
68 | 72 | # The theme to use for HTML and HTML Help pages. See the documentation for |
69 | 73 | # a list of builtin themes. |
70 | 74 | # |
| 75 | +html_baseurl = "https://pyrtl.readthedocs.io/en/latest" |
71 | 76 | html_theme = "furo" |
72 | 77 | html_theme_options = { |
73 | 78 | "sidebar_hide_name": True, |
|
96 | 101 | inheritance_graph_attrs = { |
97 | 102 | "bgcolor": "aliceblue", |
98 | 103 | } |
| 104 | + |
| 105 | +# -- linkcode_resolve -------------------------------------------------------- |
| 106 | + |
| 107 | +# linkcode generates GitHub "[source]" links for documentation. linkcode_resolve returns |
| 108 | +# a GitHub link for a documented item (class, method, object, etc). |
| 109 | +# |
| 110 | +# Based on https://gist.github.com/rainbowphysics/505e35a7a1e9545d5a6cde22f6ca9558, with |
| 111 | +# some hacks to support PyRTL's top-level globals `conditional_assignment` and |
| 112 | +# `otherwise`. |
| 113 | + |
| 114 | +# Link to your GitHub repo here: |
| 115 | +REPO_LINK = "https://github.com/UCSBarchlab/PyRTL" |
| 116 | +# Specify main branch |
| 117 | +MAIN_BRANCH = "development" |
| 118 | + |
| 119 | + |
| 120 | +def run_git_command(cmd: str) -> str | None: |
| 121 | + try: |
| 122 | + # Run command and get the output |
| 123 | + output: str = subprocess.check_output(cmd.split()).strip().decode("utf-8") |
| 124 | + |
| 125 | + # In case command failed, return None |
| 126 | + if output.startswith("fatal:"): |
| 127 | + return None |
| 128 | + |
| 129 | + # Return the raw command output |
| 130 | + return output |
| 131 | + except subprocess.CalledProcessError: |
| 132 | + return None |
| 133 | + |
| 134 | + |
| 135 | +# lock to current commit number |
| 136 | +head_commit = run_git_command("git log -n1 --pretty=%H") |
| 137 | +if head_commit is not None: |
| 138 | + linkcode_revision = head_commit |
| 139 | + |
| 140 | + # if we are on main's HEAD, use main as reference instead |
| 141 | + main_head_commit = run_git_command( |
| 142 | + f"git log --first-parent {MAIN_BRANCH} -n1 --pretty=%H" |
| 143 | + ) |
| 144 | + if head_commit == main_head_commit: |
| 145 | + linkcode_revision = MAIN_BRANCH |
| 146 | + |
| 147 | + # if we have a tag, use tag as reference |
| 148 | + tag = run_git_command(f"git describe --exact-match --tags {linkcode_revision}") |
| 149 | + if tag is not None: |
| 150 | + linkcode_revision = tag |
| 151 | + |
| 152 | +else: |
| 153 | + # If for some reason git command didn't work then default to main branch |
| 154 | + linkcode_revision = MAIN_BRANCH |
| 155 | + |
| 156 | + |
| 157 | +def get_line_range(obj): |
| 158 | + source, lineno = inspect.getsourcelines(obj) |
| 159 | + return lineno, lineno + len(source) - 1 |
| 160 | + |
| 161 | + |
| 162 | +def get_link_info( |
| 163 | + modname: str, fullname: str |
| 164 | +) -> tuple[str, tuple[int, int]] | tuple[str, None]: |
| 165 | + # Fallback in case git repo is missing or malformed, or another error occurs |
| 166 | + fallback = modname.replace(".", "/") |
| 167 | + |
| 168 | + # Get module based on module name |
| 169 | + module = sys.modules.get(modname) |
| 170 | + if module is None: |
| 171 | + return fallback, None |
| 172 | + |
| 173 | + repo_main_folder = run_git_command("git rev-parse --show-toplevel") |
| 174 | + if repo_main_folder is None: |
| 175 | + return fallback, None |
| 176 | + |
| 177 | + parent_obj = None |
| 178 | + obj = module |
| 179 | + for part in fullname.split("."): |
| 180 | + next_obj = getattr(obj, part, None) |
| 181 | + if next_obj is None: |
| 182 | + parent_obj = obj |
| 183 | + obj = part |
| 184 | + else: |
| 185 | + parent_obj = obj |
| 186 | + obj = next_obj |
| 187 | + |
| 188 | + if isinstance(obj, property): |
| 189 | + obj = obj.fget |
| 190 | + |
| 191 | + try: |
| 192 | + src_file = inspect.getsourcefile(obj) |
| 193 | + except TypeError: |
| 194 | + src_file = inspect.getsourcefile(parent_obj) |
| 195 | + |
| 196 | + # Special cases for `pyrtl.conditional_assignment` and `pyrtl.otherwise`. |
| 197 | + conditional_assignment = False |
| 198 | + if obj is pyrtl.conditional_assignment or obj is pyrtl.otherwise: |
| 199 | + conditional_assignment = True |
| 200 | + parent_obj = pyrtl.conditional |
| 201 | + src_file = inspect.getsourcefile(parent_obj) |
| 202 | + |
| 203 | + filepath = os.path.relpath(src_file, repo_main_folder) |
| 204 | + |
| 205 | + try: |
| 206 | + source, lineno = inspect.getsourcelines(obj) |
| 207 | + linestart, linestop = lineno, lineno + len(source) - 1 |
| 208 | + except OSError: |
| 209 | + return filepath, None |
| 210 | + except TypeError: |
| 211 | + source, lineno = inspect.getsourcelines(parent_obj) |
| 212 | + found_in_source = False |
| 213 | + for idx, line in enumerate(source): |
| 214 | + if line.lstrip().startswith(fullname.split(".")[-1]): |
| 215 | + linestart = lineno + idx |
| 216 | + if conditional_assignment: |
| 217 | + linestart = linestart + 1 |
| 218 | + linestop = linestart |
| 219 | + found_in_source = True |
| 220 | + break |
| 221 | + |
| 222 | + if not found_in_source: |
| 223 | + return filepath, None |
| 224 | + |
| 225 | + return filepath, (linestart, linestop) |
| 226 | + |
| 227 | + |
| 228 | +def linkcode_resolve(domain, info): |
| 229 | + if domain != "py" or not info["module"]: |
| 230 | + return None |
| 231 | + |
| 232 | + filepath, linenos = get_link_info(info["module"], info["fullname"]) |
| 233 | + result = f"{REPO_LINK}/blob/{linkcode_revision}/{filepath}" |
| 234 | + if linenos is not None: |
| 235 | + linestart, linestop = linenos |
| 236 | + result += f"#L{linestart}-L{linestop}" |
| 237 | + |
| 238 | + return result |
0 commit comments