Skip to content

Commit b47e916

Browse files
dhimmelkernc
authored andcommitted
Support for online source links (#102)
* Support for online source links Refs #100 * Activate online_source_link in doc/build.sh * Separate online_source_link from show_source_code in html.mako * html.mako: terminate if statement * Quote online_source_link --config https://travis-ci.org/pdoc3/pdoc/jobs/578458065#L231 * Set online_source_link as none in config.mako UserWarning: Unknown configuration variables (not in config.mako) https://travis-ci.org/pdoc3/pdoc/jobs/578459994#L220 * Log exception for debugging https://travis-ci.org/pdoc3/pdoc/jobs/578462696#L223 * file_path to path * simplify html_helpers * Some review revisions. _get_head_commit warnings * Use warnings not logging * Easy revisions based on PR review * get_repo_link_template: get commit only if needed Refs https://stackoverflow.com/a/22830468/4651668 * _project_relative_path function * Do not insert empty link * Show source / repo side-by-side display Co-authored-by: Vincent Rubinetti <vince.rubinetti@gmail.com> * inspect.unwrap to fix double decorator error * Update broken mathjax docs link * BUG: fix off-by-one error * ENH: more robust project root dir detection. fallback to cwd * REF: shift code around just a bit * DOC: reword documentation, more example presets * REF: modify html/css * MNT: move pdoc config overrides into a config.mako file * TST: get_repo_link on example module * REF: "repo_link" -> "git_link"; "Browse git" :-=) * TST: robuster regex
1 parent 742c7dd commit b47e916

File tree

6 files changed

+147
-10
lines changed

6 files changed

+147
-10
lines changed

doc/pdoc_template/config.mako

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<%!
2+
3+
git_link_template = 'https://github.com/pdoc3/pdoc/blob/{commit}/{path}#L{start_line}-L{end_line}'
4+
5+
%>

pdoc/html_helpers.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
Helper functions for HTML output.
33
"""
44
import inspect
5-
import os.path
5+
import os
66
import re
7+
import subprocess
8+
import traceback
79
from functools import partial, lru_cache
810
from typing import Callable, Match
911
from warnings import warn
@@ -430,3 +432,96 @@ def extract_toc(text: str):
430432
if toc.endswith('<p>'): # CUT was put into its own paragraph
431433
toc = toc[:-3].rstrip()
432434
return toc
435+
436+
437+
def format_git_link(template: str, dobj: pdoc.Doc):
438+
"""
439+
Interpolate `template` as a formatted string literal using values extracted
440+
from `dobj` and the working environment.
441+
"""
442+
if not template:
443+
return None
444+
try:
445+
if 'commit' in _str_template_fields(template):
446+
commit = _git_head_commit()
447+
abs_path = inspect.getfile(inspect.unwrap(dobj.obj))
448+
path = _project_relative_path(abs_path)
449+
lines, start_line = inspect.getsourcelines(dobj.obj)
450+
end_line = start_line + len(lines) - 1
451+
url = template.format(**locals())
452+
return url
453+
except Exception:
454+
warn('format_git_link for {} failed:\n{}'.format(dobj.obj, traceback.format_exc()))
455+
return None
456+
457+
458+
@lru_cache()
459+
def _git_head_commit():
460+
"""
461+
If the working directory is part of a git repository, return the
462+
head git commit hash. Otherwise, raise a CalledProcessError.
463+
"""
464+
process_args = ['git', 'rev-parse', 'HEAD']
465+
try:
466+
commit = subprocess.check_output(process_args, universal_newlines=True).strip()
467+
return commit
468+
except OSError as error:
469+
warn("git executable not found on system:\n{}".format(error))
470+
except subprocess.CalledProcessError as error:
471+
warn(
472+
"Ensure pdoc is run within a git repository.\n"
473+
"`{}` failed with output:\n{}"
474+
.format(' '.join(process_args), error.output)
475+
)
476+
return None
477+
478+
479+
@lru_cache()
480+
def _git_project_root():
481+
"""
482+
Return the path to project root directory or None if indeterminate.
483+
"""
484+
path = None
485+
for cmd in (['git', 'rev-parse', '--show-superproject-working-tree'],
486+
['git', 'rev-parse', '--show-toplevel']):
487+
try:
488+
path = subprocess.check_output(cmd, universal_newlines=True).rstrip('\r\n')
489+
if path:
490+
break
491+
except (subprocess.CalledProcessError, OSError):
492+
pass
493+
return path
494+
495+
496+
@lru_cache()
497+
def _project_relative_path(absolute_path):
498+
"""
499+
Convert an absolute path of a python source file to a project-relative path.
500+
Assumes the project's path is either the current working directory or
501+
Python library installation.
502+
"""
503+
from distutils.sysconfig import get_python_lib
504+
for prefix_path in (_git_project_root() or os.getcwd(),
505+
get_python_lib()):
506+
common_path = os.path.commonpath([prefix_path, absolute_path])
507+
if common_path == prefix_path:
508+
# absolute_path is a descendant of prefix_path
509+
return os.path.relpath(absolute_path, prefix_path)
510+
raise RuntimeError(
511+
"absolute path {!r} is not a descendant of the current working directory "
512+
"or of the system's python library."
513+
.format(absolute_path)
514+
)
515+
516+
517+
@lru_cache()
518+
def _str_template_fields(template):
519+
"""
520+
Return a list of `str.format` field names in a template string.
521+
"""
522+
from string import Formatter
523+
return [
524+
field_name
525+
for _, field_name, _, _ in Formatter().parse(template)
526+
if field_name is not None
527+
]

pdoc/templates/config.mako

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@
1212
# Disabling this can improve rendering speed of large modules.
1313
show_source_code = True
1414
15+
# If set, format links to objects in online source code repository
16+
# according to this template. Supported keywords for interpolation
17+
# are: commit, path, start_line, end_line.
18+
#git_link_template = 'https://github.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}'
19+
#git_link_template = 'https://gitlab.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}'
20+
#git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}'
21+
#git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start-line}'
22+
git_link_template = None
23+
1524
# A prefix to use for every HTML hyperlink in the generated documentation.
1625
# No prefix results in all links being relative.
1726
link_prefix = ''

pdoc/templates/css.mako

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,22 @@
196196
background: inherit; /* Don't grey-back parameters */
197197
}
198198
199-
.source summary {
199+
.source summary,
200+
.git-link-div {
200201
color: #666;
201202
text-align: right;
202203
font-weight: 400;
203204
font-size: .8em;
204205
text-transform: uppercase;
205-
cursor: pointer;
206206
}
207+
.source summary > * {
208+
white-space: nowrap;
209+
cursor: pointer;
210+
}
211+
.git-link {
212+
color: inherit;
213+
margin-left: 1em;
214+
}
207215
.source pre {
208216
max-height: 500px;
209217
overflow: auto;

pdoc/templates/html.mako

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
44
import pdoc
5-
from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html
5+
from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link
66
77
88
def link(d, name=None, fmt='{}'):
@@ -21,12 +21,22 @@
2121
<%def name="ident(name)"><span class="ident">${name}</span></%def>
2222

2323
<%def name="show_source(d)">
24-
% if show_source_code and d.source and d.obj is not getattr(d.inherits, 'obj', None):
25-
<details class="source">
26-
<summary>Source code</summary>
27-
<pre><code class="python">${d.source | h}</code></pre>
28-
</details>
24+
% if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None):
25+
<% git_link = format_git_link(git_link_template, d) %>
26+
% if show_source_code:
27+
<details class="source">
28+
<summary>
29+
<span>Expand source code</span>
30+
% if git_link:
31+
<a href="${git_link}" class="git-link">Browse git</a>
32+
%endif
33+
</summary>
34+
<pre><code class="python">${d.source | h}</code></pre>
35+
</details>
36+
% elif git_link:
37+
<div class="git-link-div"><a href="${git_link}" class="git-link">Browse git</a></div>
2938
%endif
39+
%endif
3040
</%def>
3141

3242
<%def name="show_desc(d, short=False)">

pdoc/test/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
import inspect
55
import os
6+
import shutil
67
import signal
78
import sys
89
import threading
@@ -26,7 +27,7 @@
2627
from pdoc.cli import main, parser
2728
from pdoc.html_helpers import (
2829
minify_css, minify_html, glimpse, to_html,
29-
ReferenceWarning, extract_toc,
30+
ReferenceWarning, extract_toc, format_git_link,
3031
)
3132

3233
TESTS_BASEDIR = os.path.abspath(os.path.dirname(__file__) or '.')
@@ -813,6 +814,15 @@ def test_extract_toc(self):
813814
toc = extract_toc(text)
814815
self.assertEqual(toc, expected)
815816

817+
@unittest.skipIf(shutil.which("git") is None, reason="test assumes git installed on system")
818+
def test_format_git_link(self):
819+
url = format_git_link(
820+
template='https://github.com/pdoc3/pdoc/blob/{commit}/{path}#L{start_line}-L{end_line}',
821+
dobj=pdoc.Module(EXAMPLE_MODULE).find_ident('module.foo'),
822+
)
823+
self.assertRegex(url, r"https://github.com/pdoc3/pdoc/blob/[0-9a-f]{40}"
824+
r"/pdoc/test/example_pkg/module.py#L\d+-L\d+")
825+
816826

817827
class Docformats(unittest.TestCase):
818828
@classmethod

0 commit comments

Comments
 (0)