Skip to content

Commit 2a8ab58

Browse files
committed
feat(docs): link to github source code instead of local copy
1 parent 5ab4a2c commit 2a8ab58

File tree

2 files changed

+115
-4
lines changed

2 files changed

+115
-4
lines changed

docs/conf.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,24 @@
1515

1616
import os
1717
import sys
18-
import coverage
18+
1919

2020
sys.path.insert(0, os.path.abspath("."))
2121
sys.path.insert(0, os.path.abspath("../..")) # Source code dir relative to this file
2222

23+
# import after updating the path.
24+
from link_info import linkcode_resolve_file_suffix # noqa: E402
25+
2326
# -- Project information -----------------------------------------------------
2427

2528
project = "mri-nufft"
2629
copyright = "2022, MRI-NUFFT Contributors"
2730
author = "MRI-NUFFT Contributors"
2831

32+
33+
GITHUB_REPO = "https://github.com/mind-inria/mri-nufft"
34+
GITHUB_VERSION = "master"
35+
2936
# -- General configuration ---------------------------------------------------
3037

3138
# Add any Sphinx extension module names here, as strings. They can be
@@ -39,7 +46,7 @@
3946
"sphinx.ext.doctest",
4047
"sphinx.ext.intersphinx",
4148
"sphinx.ext.mathjax",
42-
"sphinx.ext.viewcode",
49+
"sphinx.ext.linkcode",
4350
"sphinx.ext.napoleon",
4451
"sphinxcontrib.video",
4552
"sphinx_gallery.gen_gallery",
@@ -121,7 +128,7 @@
121128
# so a file named "default.css" will overwrite the builtin "default.css".
122129
html_static_path = ["_static"]
123130
html_theme_options = {
124-
"repository_url": "https://github.com/mind-inria/mri-nufft/",
131+
"repository_url": GITHUB_REPO,
125132
"use_repository_button": True,
126133
"use_issues_button": True,
127134
"use_edit_page_button": True,
@@ -136,6 +143,13 @@
136143
html_context = {
137144
"github_user": "mind-inria",
138145
"github_repo": "mri-nufft",
139-
"github_version": "master",
146+
"github_version": GITHUB_VERSION,
140147
"doc_path": "docs/",
141148
}
149+
150+
151+
def linkcode_resolve(domain, info):
152+
file_suffix = linkcode_resolve_file_suffix(domain, info)
153+
if file_suffix is None:
154+
return None
155+
return f"{GITHUB_REPO}/blob/{GITHUB_VERSION}/" + file_suffix

docs/link_info.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Sphinx linkcode_resolve function to link to GitHub source code."""
2+
3+
import importlib as il
4+
import inspect
5+
import pathlib as pl
6+
7+
this_dir = pl.Path(__file__).parent # location of conf.py
8+
# project_root should be set to the root of the git repo
9+
project_root = this_dir.parent
10+
# module_src_abs_paths should be a list of absolute paths to folders which contain modules
11+
module_src_abs_paths = [project_root / "src"]
12+
13+
14+
# https://stackoverflow.com/questions/48298560/how-to-add-link-to-source-code-in-sphinx
15+
def linkcode_resolve_file_suffix(domain, info):
16+
if domain != "py":
17+
return None
18+
modulename, fullname = info.get("module", None), info.get("fullname", None)
19+
if not modulename and not fullname:
20+
return None
21+
filepath = None
22+
23+
# first, let's get the file where the object is defined
24+
25+
# import the module containing a reference to the object
26+
module = il.import_module(modulename)
27+
28+
# We don't know if the object is a class, module, function, method, etc.
29+
# The module name given might also not be where the object code is.
30+
# For instance, if `module` imports `obj` from `module.submodule.obj`.
31+
objname = fullname.split(".")[0] # first level object is guaranteed to be in module
32+
obj = getattr(module, objname) # get the object, i.e. `module.obj`
33+
# inspect will find the canonical module for the object
34+
realmodule = obj if inspect.ismodule(obj) else inspect.getmodule(obj)
35+
if realmodule is None or realmodule.__file__ is None:
36+
return
37+
abspath = pl.Path(
38+
realmodule.__file__
39+
) # absolute path to the file containing the object
40+
# If the package was installed via pip, then the abspath here is
41+
# probably in a site-packages folder.
42+
43+
# Let's find the abspath relative to the location of the top-level module.
44+
toplevel_name = modulename.split(".")[0]
45+
toplevel_module = il.import_module(toplevel_name)
46+
toplevel_paths = [
47+
pl.Path(path)
48+
for path in toplevel_module.__spec__.submodule_search_locations or []
49+
]
50+
51+
# There may be multiple top-level paths, so pick the first one that matches
52+
# the absolute path of the file we want to link to.
53+
for toplevel_path in toplevel_paths:
54+
toplevel_path = toplevel_path.parent
55+
if abspath.is_relative_to(toplevel_path):
56+
filepath = abspath.relative_to(toplevel_path)
57+
break
58+
59+
# Now let's make it relative to the same directory in the correct src folder.
60+
for src_path in module_src_abs_paths:
61+
if not (src_path / filepath).exists():
62+
msg = f"Could not find {filepath} in {src_path}"
63+
raise FileNotFoundError(msg)
64+
src_rel = src_path.relative_to(project_root) # get rid of the path anchor
65+
# src_rel is now the relative path from the project_root folder to the correct module folder
66+
filepath = (src_rel / filepath).as_posix()
67+
68+
# now, let's try to get the line number where the object is defined
69+
70+
# If fullname is something like `MyClass.property`, getsourcelines() will fail.
71+
# In this case, let's return the next best thing, which in this case is the line number of the class.
72+
name_parts = fullname.split(".") # get the different components to check
73+
74+
obj = module # start with the module
75+
try:
76+
lineno = inspect.getsourcelines(obj)[1]
77+
except TypeError:
78+
lineno = None # default to no line number
79+
# try getting line number for each component and stop on failure
80+
for child_name in name_parts:
81+
try:
82+
child = getattr(obj, child_name) # get the next level object
83+
except AttributeError:
84+
print(f"Failed to resolve {objname}.{child_name}")
85+
break
86+
try:
87+
lineno = inspect.getsourcelines(child)[
88+
1
89+
] # getsourcelines returns [str, int]
90+
except Exception:
91+
# getsourcelines throws TypeError if the object is not a class, module, function, method
92+
# i.e. if it's a @property, float, etc.
93+
break # if we can't get the line number, let it be that of the previous
94+
obj = child # update the object to the next level
95+
96+
suffix = f"#L{lineno}" if lineno else ""
97+
return f"{filepath}{suffix}"

0 commit comments

Comments
 (0)