Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pdoc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@
"May be passed multiple times. "
"Example: pdoc=https://github.com/mitmproxy/pdoc/blob/main/pdoc/",
)
renderopts.add_argument(
"-l",
"--link-library",
action="append",
type=str,
default=[],
metavar="module=url",
help="A mapping between module names and URL prefixes, used to link modules to external documentation. "
"May be passed multiple times. "
"Example: pdoc=https://pdoc.dev/docs/pdoc.html",
)
renderopts.add_argument(
"--favicon",
type=str,
Expand Down Expand Up @@ -185,6 +196,7 @@ def cli(args: list[str] | None = None) -> None:
edit_url_map=dict(x.split("=", 1) for x in opts.edit_url),
favicon=opts.favicon,
footer_text=opts.footer_text,
link_library=dict(x.split("=", 1) for x in opts.link_library),
logo=opts.logo,
logo_link=opts.logo_link,
math=opts.math,
Expand Down
16 changes: 16 additions & 0 deletions pdoc/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def configure(
edit_url_map: Mapping[str, str] | None = None,
favicon: str | None = None,
footer_text: str = "",
link_library: Mapping[str, str] | None = None,
logo: str | None = None,
logo_link: str | None = None,
math: bool = False,
Expand All @@ -61,6 +62,20 @@ def configure(
renders the "Edit on GitHub" button on this page. The URL prefix can be modified to pin a particular version.
- `favicon` is an optional path/URL for a favicon image
- `footer_text` is additional text that should appear in the navigation footer.
- `link_library` is a mapping from module names to URL documentation. For example,

```json
{"pdoc": "https://pdoc.dev/docs/pdoc.html"}
```

will make all "pdoc" type annotation to link to the provided URL, but will not link if it is "pdoc.doc.Module".
It accept wildcard for such cases, for instance:

```json
{"pdoc*": "https://pdoc.dev/docs/pdoc.html"}
```

will also match "pdoc.doc.Module".
- `logo` is an optional URL to the project's logo image
- `logo_link` is an optional URL the logo should point to
- `math` enables math rendering by including MathJax into the rendered documentation.
Expand All @@ -83,6 +98,7 @@ def configure(
env.globals["mermaid"] = mermaid
env.globals["show_source"] = show_source
env.globals["favicon"] = favicon
env.globals["link_library"] = link_library or {}
env.globals["logo"] = logo
env.globals["logo_link"] = logo_link
env.globals["footer_text"] = footer_text
Expand Down
124 changes: 88 additions & 36 deletions pdoc/render_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
from collections.abc import Iterable
from collections.abc import Mapping
from contextlib import contextmanager
import fnmatch
from functools import cache
import html
import importlib
import inspect
import os
import re
import sys
import sysconfig
from unittest.mock import patch
import warnings

Expand Down Expand Up @@ -359,43 +363,47 @@ def linkify_repl(m: re.Match):

try:
sources = list(possible_sources(context["all_modules"], identifier))
except ValueError:
# possible_sources did not find a parent module.
return text

# Try to find the actual target object so that we can then later verify
# that objects exposed at a parent module with the same name point to it.
target_object = None
for module_name, qualname in sources:
if doc := context["all_modules"].get(module_name, {}).get(qualname):
target_object = doc.obj
break

# Look at the different modules where our target object may be exposed.
for module_name in module_candidates(identifier, mod.modulename):
module: pdoc.doc.Module | None = context["all_modules"].get(module_name)
if not module:
continue

for _, qualname in sources:
doc = module.get(qualname)
# Check if they have an object with the same name,
# and verify that it's pointing to the right thing and is public.
if (
doc
and (target_object is doc.obj or target_object is None)
and context["is_public"](doc).strip()
):
if shorten:
if module == mod:
url_text = qualname

# Try to find the actual target object so that we can then later verify
# that objects exposed at a parent module with the same name point to it.
target_object = None
for module_name, qualname in sources:
if doc := context["all_modules"].get(module_name, {}).get(qualname):
target_object = doc.obj
break

# Look at the different modules where our target object may be exposed.
for module_name in module_candidates(identifier, mod.modulename):
module: pdoc.doc.Module | None = context["all_modules"].get(module_name)
if not module:
continue

for _, qualname in sources:
doc = module.get(qualname)
# Check if they have an object with the same name,
# and verify that it's pointing to the right thing and is public.
if (
doc
and (target_object is doc.obj or target_object is None)
and context["is_public"](doc).strip()
):
if shorten:
if module == mod:
url_text = qualname
else:
url_text = doc.fullname
if plain_text.endswith("()"):
url_text += "()"
else:
url_text = doc.fullname
if plain_text.endswith("()"):
url_text += "()"
else:
url_text = plain_text
return f'<a href="{relative_link(context["module"].modulename, doc.modulename)}#{qualname}">{url_text}</a>'
url_text = plain_text
return f'<a href="{relative_link(context["module"].modulename, doc.modulename)}#{qualname}">{url_text}</a>'
except ValueError:
pass
for lib in context["link_library"]:
if fnmatch.fnmatch(plain_text, lib):
return f'<a href="{context["link_library"][lib]}">{plain_text}</a>'
if standard_link := get_stdlib_doc_link(plain_text):
return f'<a href="{standard_link}">{plain_text}</a>'

# No matches found.
return text
Expand Down Expand Up @@ -469,6 +477,13 @@ def link(context: Context, spec: tuple[str, str], text: str | None = None) -> Ma
return Markup(
f'<a href="{relative_link(context["module"].modulename, modulename)}{qualname}">{text or fullname}</a>'
)
for lib in context["link_library"]:
if fnmatch.fnmatch(text, lib):
return Markup(
f'<a href="{context["link_library"][lib]}">{text or fullname}</a>'
)
if standard_link := get_stdlib_doc_link(text or fullname):
return Markup(f'<a href="{standard_link}">{text or fullname}</a>')
return Markup.escape(text or fullname)


Expand Down Expand Up @@ -572,3 +587,40 @@ def parse(self, parser):
[],
)
return [m, if_stmt]


def get_stdlib_doc_link(path: str) -> str | None:
"""
If the object is from the standard library, return a hyperlink
to the official Python documentation (approximate).

Args:
path (str): e.g. 'math.sqrt' or 'datetime.datetime'

Returns:
str | None: URL to documentation or None if not valid/standard
"""
try:
module_path, obj_name = path.rsplit(".", 1)
module = importlib.import_module(module_path)

# Check if it's a standard library module
module_file: str | None = getattr(module, "__file__", None)
if module_file is None:
if module_path in sys.builtin_module_names:
base_url = f"https://docs.python.org/3/library/{module_path}.html"
return f"{base_url}#{module_path}.{obj_name}"
else:
return None

stdlib_path = sysconfig.get_paths()["stdlib"]
module_file = os.path.realpath(module_file)

if not module_file.startswith(os.path.realpath(stdlib_path)):
return None

base_module = module_path.split(".")[0] # Top-level module name
base_url = f"https://docs.python.org/3/library/{base_module}.html"
return f"{base_url}#{module_path}.{obj_name}"
except Exception:
return None
4 changes: 2 additions & 2 deletions test/testdata/collections_abc.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ <h2>API Documentation</h2>
<h1 class="modulename">
collections_abc </h1>

<div class="docstring"><p>Test that we remove 'collections.abc' from type signatures.</p>
<div class="docstring"><p>Test that we remove '<a href="https://docs.python.org/3/library/collections.html#collections.abc">collections.abc</a>' from type signatures.</p>
</div>

<input id="mod-collections_abc-view-source" class="view-source-toggle-state" type="checkbox" aria-hidden="true" tabindex="-1">
Expand Down Expand Up @@ -107,7 +107,7 @@ <h1 class="modulename">
<div class="attr class">

<span class="def">class</span>
<span class="name">Class</span><wbr>(<span class="base">collections.abc.Container[str]</span>):
<span class="name">Class</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/collections.html#collections.abc.Container[str]">collections.abc.Container[str]</a></span>):

<label class="view-source-button" for="Class-view-source"><span>View Source</span></label>

Expand Down
14 changes: 7 additions & 7 deletions test/testdata/demo_long.html
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ <h1 id="a-second-section">A Second Section</h1>


<div class="docstring"><p>This is a function with a fairly complex signature,
involving type annotations with <code>typing.Union</code>, a <code>typing.TypeVar</code> (~T),
involving type annotations with <code><a href="https://docs.python.org/3/library/typing.html#typing.Union">typing.Union</a></code>, a <code><a href="https://docs.python.org/3/library/typing.html#typing.TypeVar">typing.TypeVar</a></code> (~T),
as well as a keyword-only arguments (*).</p>
</div>

Expand Down Expand Up @@ -845,7 +845,7 @@ <h1 id="a-second-section">A Second Section</h1>
</span></pre></div>


<div class="docstring"><p>This is a <code>@functools.cached_property</code> attribute. pdoc will display it as a variable as well.</p>
<div class="docstring"><p>This is a <code>@<a href="https://docs.python.org/3/library/functools.html#functools.cached_property">functools.cached_property</a></code> attribute. pdoc will display it as a variable as well.</p>
</div>


Expand Down Expand Up @@ -1153,7 +1153,7 @@ <h5>Inherited Members</h5>
<div class="attr function">

<span class="def">def</span>
<span class="name">security</span><span class="signature pdoc-code condensed">(<span class="param"><span class="n">test</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span></span><span class="return-annotation">):</span></span>
<span class="name">security</span><span class="signature pdoc-code condensed">(<span class="param"><span class="n">test</span><span class="o">=</span><span class="n"><a href="https://docs.python.org/3/library/os.html#os.environ">os.environ</a></span></span><span class="return-annotation">):</span></span>

<label class="view-source-button" for="security-view-source"><span>View Source</span></label>

Expand All @@ -1169,7 +1169,7 @@ <h5>Inherited Members</h5>


<div class="docstring"><p>Default values are generally rendered using repr(),
but some special cases -- like os.environ -- are overridden to avoid leaking sensitive data.</p>
but some special cases -- like <a href="https://docs.python.org/3/library/os.html#os.environ">os.environ</a> -- are overridden to avoid leaking sensitive data.</p>
</div>


Expand All @@ -1179,7 +1179,7 @@ <h5>Inherited Members</h5>
<div class="attr class">

<span class="def">class</span>
<span class="name">DoubleInherit</span><wbr>(<span class="base"><a href="#Foo">Foo</a></span>, <span class="base"><a href="#Bar.Baz">Bar.Baz</a></span>, <span class="base">abc.ABC</span>):
<span class="name">DoubleInherit</span><wbr>(<span class="base"><a href="#Foo">Foo</a></span>, <span class="base"><a href="#Bar.Baz">Bar.Baz</a></span>, <span class="base"><a href="https://docs.python.org/3/library/abc.html#abc.ABC">abc.ABC</a></span>):

<label class="view-source-button" for="DoubleInherit-view-source"><span>View Source</span></label>

Expand Down Expand Up @@ -1353,7 +1353,7 @@ <h5>Inherited Members</h5>
</div>
<a class="headerlink" href="#DataDemo.b"></a>

<div class="docstring"><p>This property is assigned to <code>dataclasses.field()</code>, which works just as well.</p>
<div class="docstring"><p>This property is assigned to <code><a href="https://docs.python.org/3/library/dataclasses.html#dataclasses.field()">dataclasses.field()</a></code>, which works just as well.</p>
</div>


Expand Down Expand Up @@ -1425,7 +1425,7 @@ <h5>Inherited Members</h5>
<div class="attr class">

<span class="def">class</span>
<span class="name">EnumDemo</span><wbr>(<span class="base">enum.Enum</span>):
<span class="name">EnumDemo</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></span>):

<label class="view-source-button" for="EnumDemo-view-source"><span>View Source</span></label>

Expand Down
8 changes: 4 additions & 4 deletions test/testdata/enums.html
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ <h1 class="modulename">
<div class="attr class">

<span class="def">class</span>
<span class="name">EnumDemo</span><wbr>(<span class="base">enum.Enum</span>):
<span class="name">EnumDemo</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></span>):

<label class="view-source-button" for="EnumDemo-view-source"><span>View Source</span></label>

Expand Down Expand Up @@ -207,7 +207,7 @@ <h1 class="modulename">
<div class="attr class">

<span class="def">class</span>
<span class="name">EnumWithoutDocstrings</span><wbr>(<span class="base">enum.Enum</span>):
<span class="name">EnumWithoutDocstrings</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/enum.html#enum.Enum">enum.Enum</a></span>):

<label class="view-source-button" for="EnumWithoutDocstrings-view-source"><span>View Source</span></label>

Expand Down Expand Up @@ -251,7 +251,7 @@ <h1 class="modulename">
<div class="attr class">

<span class="def">class</span>
<span class="name">IntEnum</span><wbr>(<span class="base">enum.IntEnum</span>):
<span class="name">IntEnum</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/enum.html#enum.IntEnum">enum.IntEnum</a></span>):

<label class="view-source-button" for="IntEnum-view-source"><span>View Source</span></label>

Expand Down Expand Up @@ -295,7 +295,7 @@ <h1 class="modulename">
<div class="attr class">

<span class="def">class</span>
<span class="name">StrEnum</span><wbr>(<span class="base">enum.StrEnum</span>):
<span class="name">StrEnum</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/enum.html#enum.StrEnum">enum.StrEnum</a></span>):

<label class="view-source-button" for="StrEnum-view-source"><span>View Source</span></label>

Expand Down
2 changes: 1 addition & 1 deletion test/testdata/flavors_google.html
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,7 @@ <h6 id="examples">Examples:</h6>
<div class="attr class">

<span class="def">class</span>
<span class="name">ExampleError</span><wbr>(<span class="base">builtins.Exception</span>):
<span class="name">ExampleError</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/builtins.html#builtins.Exception">builtins.Exception</a></span>):

<label class="view-source-button" for="ExampleError-view-source"><span>View Source</span></label>

Expand Down
2 changes: 1 addition & 1 deletion test/testdata/flavors_numpy.html
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,7 @@ <h6 id="examples">Examples</h6>
<div class="attr class">

<span class="def">class</span>
<span class="name">ExampleError</span><wbr>(<span class="base">builtins.Exception</span>):
<span class="name">ExampleError</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/builtins.html#builtins.Exception">builtins.Exception</a></span>):

<label class="view-source-button" for="ExampleError-view-source"><span>View Source</span></label>

Expand Down
8 changes: 4 additions & 4 deletions test/testdata/misc.html
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,7 @@ <h1 class="modulename">
<div class="attr class">

<span class="def">class</span>
<span class="name">GenericParent</span><wbr>(<span class="base">typing.Generic[~T]</span>):
<span class="name">GenericParent</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/typing.html#typing.Generic[~T]">typing.Generic[~T]</a></span>):

<label class="view-source-button" for="GenericParent-view-source"><span>View Source</span></label>

Expand Down Expand Up @@ -2256,7 +2256,7 @@ <h6 id="heading-6">Heading 6</h6>
<div class="attr class">

<span class="def">class</span>
<span class="name">scheduler</span><wbr>(<span class="base">sched.scheduler</span>):
<span class="name">scheduler</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/sched.html#sched.scheduler">sched.scheduler</a></span>):

<label class="view-source-button" for="scheduler-view-source"><span>View Source</span></label>

Expand Down Expand Up @@ -2532,7 +2532,7 @@ <h6 id="parameters">Parameters</h6>
<div class="decorator decorator-dataclass">@dataclass(init=False)</div>

<span class="def">class</span>
<span class="name">DataclassStructure</span><wbr>(<span class="base">_ctypes.Structure</span>):
<span class="name">DataclassStructure</span><wbr>(<span class="base"><a href="https://docs.python.org/3/library/_ctypes.html#_ctypes.Structure">_ctypes.Structure</a></span>):

<label class="view-source-button" for="DataclassStructure-view-source"><span>View Source</span></label>

Expand All @@ -2544,7 +2544,7 @@ <h6 id="parameters">Parameters</h6>
</span></pre></div>


<div class="docstring"><p>DataclassStructure raises for <code>inspect.signature</code>.</p>
<div class="docstring"><p>DataclassStructure raises for <code><a href="https://docs.python.org/3/library/inspect.html#inspect.signature">inspect.signature</a></code>.</p>
</div>


Expand Down
2 changes: 1 addition & 1 deletion test/testdata/misc_py310.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ <h1 class="modulename">
<section id="OldStyleDict">
<div class="attr variable">
<span class="name">OldStyleDict</span> =
<span class="default_value">typing.Dict[str, str]</span>
<span class="default_value"><a href="https://docs.python.org/3/library/typing.html#typing.Dict">typing.Dict</a>[str, str]</span>


</div>
Expand Down
Loading
Loading