Edit on GitHub

pdoc.render_helpers

  1from __future__ import annotations
  2
  3import html
  4
  5import inspect
  6import os
  7import re
  8import warnings
  9from collections.abc import Collection, Iterable, Mapping
 10from contextlib import contextmanager
 11from unittest.mock import patch
 12
 13import pygments.formatters
 14import pygments.lexers
 15from jinja2 import ext, nodes
 16
 17try:
 18    # Jinja2 >= 3.0
 19    from jinja2 import pass_context  # type: ignore
 20except ImportError:  # pragma: no cover
 21    from jinja2 import contextfilter as pass_context  # type: ignore
 22
 23from jinja2.runtime import Context
 24from markupsafe import Markup
 25
 26import pdoc.markdown2
 27
 28from . import docstrings
 29from ._compat import cache, removesuffix
 30
 31lexer = pygments.lexers.PythonLexer()
 32"""
 33The pygments lexer used for pdoc.render_helpers.highlight.
 34Overwrite this to configure pygments lexing.
 35"""
 36
 37formatter = pygments.formatters.HtmlFormatter(
 38    cssclass="pdoc-code codehilite",
 39    linenos="inline",
 40    anchorlinenos=True,
 41)
 42"""
 43The pygments formatter used for pdoc.render_helpers.highlight. 
 44Overwrite this to configure pygments highlighting of code blocks.
 45
 46The usage of the `.codehilite` CSS selector in custom templates is deprecated since pdoc 10, use `.pdoc-code` instead.
 47"""
 48
 49signature_formatter = pygments.formatters.HtmlFormatter(nowrap=True)
 50"""
 51The pygments formatter used for pdoc.render_helpers.format_signature. 
 52Overwrite this to configure pygments highlighting of signatures.
 53"""
 54
 55# Keep in sync with the documentation in pdoc/__init__.py!
 56markdown_extensions = {
 57    "code-friendly": None,
 58    "cuddled-lists": None,
 59    "fenced-code-blocks": {"cssclass": formatter.cssclass},
 60    "footnotes": None,
 61    "header-ids": None,
 62    "link-patterns": None,
 63    "markdown-in-html": None,
 64    "pyshell": None,
 65    "strike": None,
 66    "tables": None,
 67    "task_list": None,
 68    "toc": {"depth": 2},
 69}
 70"""
 71The default extensions loaded for `markdown2`.
 72Overwrite this to configure Markdown rendering.
 73"""
 74markdown_link_patterns = [
 75    (
 76        re.compile(
 77            r"""
 78            \b
 79            (
 80                (?:https?://|(?<!//)www\.)    # prefix - https:// or www.
 81                \w[\w_\-]*(?:\.\w[\w_\-]*)*   # host
 82                [^<>\s"']*                    # rest of url
 83                (?<![?!.,:*_~);])             # exclude trailing punctuation
 84                (?=[?!.,:*_~);]?(?:[<\s]|$))  # make sure that we're not followed by " or ', i.e. we're outside of href="...".
 85            )
 86        """,
 87            re.X,
 88        ),
 89        r"\1",
 90    )
 91]
 92"""
 93Link pattern used for markdown2's [`link-patterns` extra](https://github.com/trentm/python-markdown2/wiki/link-patterns).
 94"""
 95
 96
 97@cache
 98def highlight(doc: pdoc.doc.Doc) -> str:
 99    """Highlight the source code of a documentation object using pygments."""
100    if isinstance(doc, str):  # pragma: no cover
101        warnings.warn(
102            "Passing a string to the `highlight` render helper is deprecated, pass a pdoc.doc.Doc object instead.",
103            DeprecationWarning,
104        )
105        return Markup(pygments.highlight(doc, lexer, formatter))
106
107    # set up correct line numbers and anchors
108    formatter.linespans = doc.qualname or "L"
109    formatter.linenostart = doc.source_lines[0] + 1 if doc.source_lines else 1
110    return Markup(pygments.highlight(doc.source, lexer, formatter))
111
112
113def format_signature(sig: inspect.Signature, colon: bool) -> str:
114    """Format and highlight a function signature using pygments. Returns HTML."""
115    # First get a list with all params as strings.
116    result = pdoc.doc._PrettySignature._params(sig)  # type: ignore
117    return_annot = pdoc.doc._PrettySignature._return_annotation_str(sig)  # type: ignore
118
119    multiline = (
120        sum(len(x) + 2 for x in result) + len(return_annot)
121        > pdoc.doc._PrettySignature.MULTILINE_CUTOFF
122    )
123
124    def _try_highlight(code: str) -> str:
125        """Try to highlight a piece of code using pygments, but return the input as-is if pygments detects errors."""
126        pretty = pygments.highlight(code, lexer, signature_formatter).strip()
127        if '<span class="err">' not in pretty:
128            return pretty
129        else:
130            return html.escape(code)
131
132    # Next, individually highlight each parameter using pygments and wrap it in a span.param.
133    # This later allows us to properly control line breaks.
134    pretty_result = []
135    for i, param in enumerate(result):
136        pretty = _try_highlight(param)
137        if multiline:
138            pretty = f"""<span class="param">\t{pretty},</span>"""
139        else:
140            pretty = f"""<span class="param">{pretty}, </span>"""
141        pretty_result.append(pretty)
142
143    # remove last comma.
144    if pretty_result:
145        pretty_result[-1] = pretty_result[-1].rpartition(",")[0] + "</span>"
146
147    # Add return annotation.
148    anno = ")"
149    if return_annot:
150        anno += f" -> {_try_highlight(return_annot)}"
151    if colon:
152        anno += ":"
153    if return_annot or colon:
154        anno = f'<span class="return-annotation">{anno}</span>'
155
156    rendered = "(" + "".join(pretty_result) + anno
157
158    if multiline:
159        rendered = f'<span class="signature pdoc-code multiline">{rendered}</span>'
160    else:
161        rendered = f'<span class="signature pdoc-code condensed">{rendered}</span>'
162
163    return Markup(rendered)
164
165
166@cache
167def to_html(docstring: str) -> str:
168    """
169    Convert `docstring` from Markdown to HTML.
170    """
171    # careful: markdown2 returns a subclass of str with an extra
172    # .toc_html attribute. don't further process the result,
173    # otherwise this attribute will be lost.
174    return pdoc.markdown2.markdown(  # type: ignore
175        docstring,
176        extras=markdown_extensions,
177        link_patterns=markdown_link_patterns,
178    )
179
180
181@pass_context
182def to_markdown_with_context(context: Context, docstring: str) -> str:
183    """
184    Converts `docstring` from a custom docformat to Markdown (if necessary), and then from Markdown to HTML.
185    """
186    module: pdoc.doc.Module = context["module"]
187    docformat: str = context["docformat"]
188    return to_markdown(docstring, module, docformat)
189
190
191def to_markdown(docstring: str, module: pdoc.doc.Module, default_docformat: str) -> str:
192    docformat = getattr(module.obj, "__docformat__", default_docformat) or ""
193    return docstrings.convert(docstring, docformat, module.source_file)
194
195
196def possible_sources(
197    all_modules: Collection[str], identifier: str
198) -> Iterable[tuple[str, str]]:
199    """
200    For a given identifier, return all possible sources where it could originate from.
201    For example, assume `examplepkg._internal.Foo` with all_modules=["examplepkg"].
202    This could be a Foo class in _internal.py, or a nested `class _internal: class Foo` in examplepkg.
203    We return both candidates as we don't know if _internal.py exists.
204    It may not be in all_modules because it's been excluded by `__all__`.
205    However, if `examplepkg._internal` is in all_modules we know that it can only be that option.
206    """
207    if identifier in all_modules:
208        yield identifier, ""
209        return
210
211    modulename = identifier
212    qualname = None
213    while modulename:
214        modulename, _, add = modulename.rpartition(".")
215        qualname = f"{add}.{qualname}" if qualname else add
216        yield modulename, qualname
217        if modulename in all_modules:
218            return
219    raise ValueError(f"Invalid identifier: {identifier}")
220
221
222def split_identifier(all_modules: Collection[str], fullname: str) -> tuple[str, str]:
223    """
224    Split an identifier into a `(modulename, qualname)` tuple. For example, `pdoc.render_helpers.split_identifier`
225    would be split into `("pdoc.render_helpers","split_identifier")`. This is necessary to generate links to the
226    correct module.
227    """
228    warnings.warn(
229        "pdoc.render_helpers.split_identifier is deprecated and will be removed in a future release. "
230        "Use pdoc.render_helpers.possible_sources instead.",
231        DeprecationWarning,
232    )
233    *_, last = possible_sources(all_modules, fullname)
234    return last
235
236
237def _relative_link(current: list[str], target: list[str]) -> str:
238    if target == current:
239        return f"../{target[-1]}.html"
240    elif target[: len(current)] == current:
241        return "/".join(target[len(current) :]) + ".html"
242    else:
243        return "../" + _relative_link(current[:-1], target)
244
245
246@cache
247def relative_link(current_module: str, target_module: str) -> str:
248    """Compute the relative link to another module's HTML file."""
249    if current_module == target_module:
250        return ""
251    return _relative_link(
252        current_module.split(".")[:-1],
253        target_module.split("."),
254    )
255
256
257def qualname_candidates(identifier: str, context_qualname: str) -> list[str]:
258    """
259    Given an identifier in a current namespace, return all possible qualnames in the current module.
260    For example, if we are in Foo's subclass Bar and `baz()` is the identifier,
261    return `Foo.Bar.baz()`, `Foo.baz()`, and `baz()`.
262    """
263    end = len(context_qualname)
264    ret = []
265    while end > 0:
266        ret.append(f"{context_qualname[:end]}.{identifier}")
267        end = context_qualname.rfind(".", 0, end)
268    ret.append(identifier)
269    return ret
270
271
272@pass_context
273def linkify(context: Context, code: str, namespace: str = "") -> str:
274    """
275    Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that
276    are not rendered at the moment will be ignored.
277    A piece of text is considered to be an identifier if it either contains a `.` or is surrounded by `<code>` tags.
278    """
279
280    def linkify_repl(m: re.Match):
281        text = m.group(0)
282        plain_text = text.replace(
283            '</span><span class="o">.</span><span class="n">', "."
284        )
285        identifier = removesuffix(plain_text, "()")
286
287        # Check if this is a local reference within this module?
288        mod: pdoc.doc.Module = context["module"]
289        for qualname in qualname_candidates(identifier, namespace):
290            doc = mod.get(qualname)
291            if doc and context["is_public"](doc).strip():
292                return f'<a href="#{qualname}">{plain_text}</a>'
293
294        module = ""
295        qualname = ""
296        try:
297            # Check if the object we are interested in is imported and re-exposed in the current namespace.
298            for module, qualname in possible_sources(
299                context["all_modules"], identifier
300            ):
301                doc = mod.get(qualname)
302                if (
303                    doc
304                    and doc.taken_from == (module, qualname)
305                    and context["is_public"](doc).strip()
306                ):
307                    if plain_text.endswith("()"):
308                        plain_text = f"{doc.fullname}()"
309                    else:
310                        plain_text = doc.fullname
311                    return f'<a href="#{qualname}">{plain_text}</a>'
312        except ValueError:
313            # possible_sources did not find a parent module.
314            return text
315        else:
316            # It's not, but we now know the parent module. Does the target exist?
317            doc = context["all_modules"][module]
318            if qualname:
319                assert isinstance(doc, pdoc.doc.Module)
320                doc = doc.get(qualname)
321            target_exists_and_public = (
322                doc is not None and context["is_public"](doc).strip()
323            )
324            if target_exists_and_public:
325                if qualname:
326                    qualname = f"#{qualname}"
327                return f'<a href="{relative_link(context["module"].modulename, module)}{qualname}">{plain_text}</a>'
328            else:
329                return text
330
331    return Markup(
332        re.sub(
333            r"""
334            # Part 1: foo.bar or foo.bar() (without backticks)
335            (?<![/=?#&])  # heuristic: not part of a URL
336            \b
337            
338            # First part of the identifier (e.g. "foo")    
339            (?!\d)[a-zA-Z0-9_]+
340            # Rest of the identifier (e.g. ".bar")
341            (?:
342                # A single dot or a dot surrounded with pygments highlighting.
343                (?:\.|</span><span\ class="o">\.</span><span\ class="n">)
344                (?!\d)[a-zA-Z0-9_]+
345            )+
346            (?:\(\)|\b(?!\(\)))  # we either end on () or on a word boundary.
347            (?!</a>)  # not an existing link
348            (?![/#])  # heuristic: not part of a URL
349
350            | # Part 2: `foo` or `foo()`. `foo.bar` is already covered with part 1.
351            (?<=<code>)
352                 (?!\d)[a-zA-Z0-9_]+
353            (?:\(\))?
354            (?=</code>(?!</a>))
355            """,
356            linkify_repl,
357            code,
358            flags=re.VERBOSE,
359        )
360    )
361
362
363@pass_context
364def link(context: Context, spec: tuple[str, str], text: str | None = None) -> str:
365    """Create a link for a specific `(modulename, qualname)` tuple."""
366    mod: pdoc.doc.Module = context["module"]
367    modulename, qualname = spec
368
369    # Check if the object we are interested is also imported and re-exposed in the current namespace.
370    doc = mod.get(qualname)
371    if doc and doc.taken_from == spec and context["is_public"](doc).strip():
372        if text:
373            text = text.replace(f"{modulename}.", f"{mod.modulename}.")
374        modulename = mod.modulename
375
376    if mod.modulename == modulename:
377        fullname = qualname
378    else:
379        fullname = removesuffix(f"{modulename}.{qualname}", ".")
380
381    if qualname:
382        qualname = f"#{qualname}"
383    if modulename in context["all_modules"]:
384        return Markup(
385            f'<a href="{relative_link(context["module"].modulename, modulename)}{qualname}">{text or fullname}</a>'
386        )
387    return text or fullname
388
389
390def edit_url(
391    modulename: str, is_package: bool, mapping: Mapping[str, str]
392) -> str | None:
393    """Create a link to edit a particular file in the used version control system."""
394    for m, prefix in mapping.items():
395        if m == modulename or modulename.startswith(f"{m}."):
396            filename = modulename[len(m) + 1 :].replace(".", "/")
397            if is_package:
398                filename = f"{filename}/__init__.py".lstrip("/")
399            else:
400                filename += ".py"
401            return f"{prefix}{filename}"
402    return None
403
404
405def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None:
406    """
407    Return the name of the (unique) top-level module, or `None`
408    if no such module exists.
409
410    For example, assuming `foo`, `foo.bar`, and `foo.baz` are documented,
411    this function will return `foo`. If `foo` and `bar` are documented,
412    this function will return `None` as there is no unique top-level module.
413    """
414    shortest_name = min(all_modules, key=len, default=None)
415    prefix = f"{shortest_name}."
416    all_others_are_submodules = all(
417        x.startswith(prefix) or x == shortest_name for x in all_modules
418    )
419    if all_others_are_submodules:
420        return shortest_name
421    else:
422        return None
423
424
425def minify_css(css: str) -> str:
426    """Do some very basic CSS minification."""
427    css = re.sub(r"[ ]{4}|\n|(?<=[:{}]) | (?=[{}])", "", css)
428    css = re.sub(
429        r"/\*.+?\*/", lambda m: m.group(0) if m.group(0).startswith("/*!") else "", css
430    )
431    return Markup(css.replace("<style", "\n<style"))
432
433
434@contextmanager
435def defuse_unsafe_reprs():
436    """This decorator is applied by pdoc before calling an object's repr().
437    It applies some heuristics to patch our sensitive information.
438    For example, `os.environ`'s default `__repr__` implementation exposes all
439    local secrets.
440    """
441    with (patch.object(os._Environ, "__repr__", lambda self: "os.environ")):
442        yield
443
444
445class DefaultMacroExtension(ext.Extension):
446    """
447    This extension provides a new `{% defaultmacro %}` statement, which defines a macro only if it does not exist.
448
449    For example,
450
451    ```html+jinja
452    {% defaultmacro example() %}
453        test 123
454    {% enddefaultmacro %}
455    ```
456
457    is equivalent to
458
459    ```html+jinja
460    {% macro default_example() %}
461    test 123
462    {% endmacro %}
463    {% if not example %}
464        {% macro example() %}
465            test 123
466        {% endmacro %}
467    {% endif %}
468    ```
469
470    Additionally, the default implementation is also available as `default_$macroname`, which makes it possible
471    to reference it in the override.
472    """
473
474    tags = {"defaultmacro"}
475
476    def parse(self, parser):
477        m = nodes.Macro(lineno=next(parser.stream).lineno)
478        name = parser.parse_assign_target(name_only=True).name
479        m.name = f"default_{name}"
480        parser.parse_signature(m)
481        m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True)
482
483        if_stmt = nodes.If(
484            nodes.Not(nodes.Name(name, "load")),
485            [nodes.Macro(name, m.args, m.defaults, m.body)],
486            [],
487            [],
488        )
489        return [m, if_stmt]
lexer = <pygments.lexers.PythonLexer>

The pygments lexer used for pdoc.render_helpers.highlight. Overwrite this to configure pygments lexing.

formatter = <pygments.formatters.html.HtmlFormatter object>

The pygments formatter used for pdoc.render_helpers.highlight. Overwrite this to configure pygments highlighting of code blocks.

The usage of the .codehilite CSS selector in custom templates is deprecated since pdoc 10, use .pdoc-code instead.

signature_formatter = <pygments.formatters.html.HtmlFormatter object>

The pygments formatter used for pdoc.render_helpers.format_signature. Overwrite this to configure pygments highlighting of signatures.

markdown_extensions = {'code-friendly': None, 'cuddled-lists': None, 'fenced-code-blocks': {'cssclass': 'pdoc-code codehilite'}, 'footnotes': None, 'header-ids': None, 'link-patterns': None, 'markdown-in-html': None, 'pyshell': None, 'strike': None, 'tables': None, 'task_list': None, 'toc': {'depth': 2}}

The default extensions loaded for markdown2. Overwrite this to configure Markdown rendering.

@cache
def highlight(doc: pdoc.doc.Doc) -> str:
 98@cache
 99def highlight(doc: pdoc.doc.Doc) -> str:
100    """Highlight the source code of a documentation object using pygments."""
101    if isinstance(doc, str):  # pragma: no cover
102        warnings.warn(
103            "Passing a string to the `highlight` render helper is deprecated, pass a pdoc.doc.Doc object instead.",
104            DeprecationWarning,
105        )
106        return Markup(pygments.highlight(doc, lexer, formatter))
107
108    # set up correct line numbers and anchors
109    formatter.linespans = doc.qualname or "L"
110    formatter.linenostart = doc.source_lines[0] + 1 if doc.source_lines else 1
111    return Markup(pygments.highlight(doc.source, lexer, formatter))

Highlight the source code of a documentation object using pygments.

def format_signature(sig: inspect.Signature, colon: bool) -> str:
114def format_signature(sig: inspect.Signature, colon: bool) -> str:
115    """Format and highlight a function signature using pygments. Returns HTML."""
116    # First get a list with all params as strings.
117    result = pdoc.doc._PrettySignature._params(sig)  # type: ignore
118    return_annot = pdoc.doc._PrettySignature._return_annotation_str(sig)  # type: ignore
119
120    multiline = (
121        sum(len(x) + 2 for x in result) + len(return_annot)
122        > pdoc.doc._PrettySignature.MULTILINE_CUTOFF
123    )
124
125    def _try_highlight(code: str) -> str:
126        """Try to highlight a piece of code using pygments, but return the input as-is if pygments detects errors."""
127        pretty = pygments.highlight(code, lexer, signature_formatter).strip()
128        if '<span class="err">' not in pretty:
129            return pretty
130        else:
131            return html.escape(code)
132
133    # Next, individually highlight each parameter using pygments and wrap it in a span.param.
134    # This later allows us to properly control line breaks.
135    pretty_result = []
136    for i, param in enumerate(result):
137        pretty = _try_highlight(param)
138        if multiline:
139            pretty = f"""<span class="param">\t{pretty},</span>"""
140        else:
141            pretty = f"""<span class="param">{pretty}, </span>"""
142        pretty_result.append(pretty)
143
144    # remove last comma.
145    if pretty_result:
146        pretty_result[-1] = pretty_result[-1].rpartition(",")[0] + "</span>"
147
148    # Add return annotation.
149    anno = ")"
150    if return_annot:
151        anno += f" -> {_try_highlight(return_annot)}"
152    if colon:
153        anno += ":"
154    if return_annot or colon:
155        anno = f'<span class="return-annotation">{anno}</span>'
156
157    rendered = "(" + "".join(pretty_result) + anno
158
159    if multiline:
160        rendered = f'<span class="signature pdoc-code multiline">{rendered}</span>'
161    else:
162        rendered = f'<span class="signature pdoc-code condensed">{rendered}</span>'
163
164    return Markup(rendered)

Format and highlight a function signature using pygments. Returns HTML.

@cache
def to_html(docstring: str) -> str:
167@cache
168def to_html(docstring: str) -> str:
169    """
170    Convert `docstring` from Markdown to HTML.
171    """
172    # careful: markdown2 returns a subclass of str with an extra
173    # .toc_html attribute. don't further process the result,
174    # otherwise this attribute will be lost.
175    return pdoc.markdown2.markdown(  # type: ignore
176        docstring,
177        extras=markdown_extensions,
178        link_patterns=markdown_link_patterns,
179    )

Convert docstring from Markdown to HTML.

@pass_context
def to_markdown_with_context(context: jinja2.runtime.Context, docstring: str) -> str:
182@pass_context
183def to_markdown_with_context(context: Context, docstring: str) -> str:
184    """
185    Converts `docstring` from a custom docformat to Markdown (if necessary), and then from Markdown to HTML.
186    """
187    module: pdoc.doc.Module = context["module"]
188    docformat: str = context["docformat"]
189    return to_markdown(docstring, module, docformat)

Converts docstring from a custom docformat to Markdown (if necessary), and then from Markdown to HTML.

def to_markdown(docstring: str, module: pdoc.doc.Module, default_docformat: str) -> str:
192def to_markdown(docstring: str, module: pdoc.doc.Module, default_docformat: str) -> str:
193    docformat = getattr(module.obj, "__docformat__", default_docformat) or ""
194    return docstrings.convert(docstring, docformat, module.source_file)
def possible_sources( all_modules: collections.abc.Collection[str], identifier: str) -> collections.abc.Iterable[tuple[str, str]]:
197def possible_sources(
198    all_modules: Collection[str], identifier: str
199) -> Iterable[tuple[str, str]]:
200    """
201    For a given identifier, return all possible sources where it could originate from.
202    For example, assume `examplepkg._internal.Foo` with all_modules=["examplepkg"].
203    This could be a Foo class in _internal.py, or a nested `class _internal: class Foo` in examplepkg.
204    We return both candidates as we don't know if _internal.py exists.
205    It may not be in all_modules because it's been excluded by `__all__`.
206    However, if `examplepkg._internal` is in all_modules we know that it can only be that option.
207    """
208    if identifier in all_modules:
209        yield identifier, ""
210        return
211
212    modulename = identifier
213    qualname = None
214    while modulename:
215        modulename, _, add = modulename.rpartition(".")
216        qualname = f"{add}.{qualname}" if qualname else add
217        yield modulename, qualname
218        if modulename in all_modules:
219            return
220    raise ValueError(f"Invalid identifier: {identifier}")

For a given identifier, return all possible sources where it could originate from. For example, assume examplepkg._internal.Foo with all_modules=["examplepkg"]. This could be a Foo class in _internal.py, or a nested class _internal: class Foo in examplepkg. We return both candidates as we don't know if _internal.py exists. It may not be in all_modules because it's been excluded by __all__. However, if examplepkg._internal is in all_modules we know that it can only be that option.

def split_identifier( all_modules: collections.abc.Collection[str], fullname: str) -> tuple[str, str]:
223def split_identifier(all_modules: Collection[str], fullname: str) -> tuple[str, str]:
224    """
225    Split an identifier into a `(modulename, qualname)` tuple. For example, `pdoc.render_helpers.split_identifier`
226    would be split into `("pdoc.render_helpers","split_identifier")`. This is necessary to generate links to the
227    correct module.
228    """
229    warnings.warn(
230        "pdoc.render_helpers.split_identifier is deprecated and will be removed in a future release. "
231        "Use pdoc.render_helpers.possible_sources instead.",
232        DeprecationWarning,
233    )
234    *_, last = possible_sources(all_modules, fullname)
235    return last

Split an identifier into a (modulename, qualname) tuple. For example, pdoc.render_helpers.split_identifier would be split into ("pdoc.render_helpers","split_identifier"). This is necessary to generate links to the correct module.

def qualname_candidates(identifier: str, context_qualname: str) -> list[str]:
258def qualname_candidates(identifier: str, context_qualname: str) -> list[str]:
259    """
260    Given an identifier in a current namespace, return all possible qualnames in the current module.
261    For example, if we are in Foo's subclass Bar and `baz()` is the identifier,
262    return `Foo.Bar.baz()`, `Foo.baz()`, and `baz()`.
263    """
264    end = len(context_qualname)
265    ret = []
266    while end > 0:
267        ret.append(f"{context_qualname[:end]}.{identifier}")
268        end = context_qualname.rfind(".", 0, end)
269    ret.append(identifier)
270    return ret

Given an identifier in a current namespace, return all possible qualnames in the current module. For example, if we are in Foo's subclass Bar and baz() is the identifier, return Foo.Bar.baz(), Foo.baz(), and baz().

@pass_context
def linkify(context: jinja2.runtime.Context, code: str, namespace: str = '') -> str:
273@pass_context
274def linkify(context: Context, code: str, namespace: str = "") -> str:
275    """
276    Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that
277    are not rendered at the moment will be ignored.
278    A piece of text is considered to be an identifier if it either contains a `.` or is surrounded by `<code>` tags.
279    """
280
281    def linkify_repl(m: re.Match):
282        text = m.group(0)
283        plain_text = text.replace(
284            '</span><span class="o">.</span><span class="n">', "."
285        )
286        identifier = removesuffix(plain_text, "()")
287
288        # Check if this is a local reference within this module?
289        mod: pdoc.doc.Module = context["module"]
290        for qualname in qualname_candidates(identifier, namespace):
291            doc = mod.get(qualname)
292            if doc and context["is_public"](doc).strip():
293                return f'<a href="#{qualname}">{plain_text}</a>'
294
295        module = ""
296        qualname = ""
297        try:
298            # Check if the object we are interested in is imported and re-exposed in the current namespace.
299            for module, qualname in possible_sources(
300                context["all_modules"], identifier
301            ):
302                doc = mod.get(qualname)
303                if (
304                    doc
305                    and doc.taken_from == (module, qualname)
306                    and context["is_public"](doc).strip()
307                ):
308                    if plain_text.endswith("()"):
309                        plain_text = f"{doc.fullname}()"
310                    else:
311                        plain_text = doc.fullname
312                    return f'<a href="#{qualname}">{plain_text}</a>'
313        except ValueError:
314            # possible_sources did not find a parent module.
315            return text
316        else:
317            # It's not, but we now know the parent module. Does the target exist?
318            doc = context["all_modules"][module]
319            if qualname:
320                assert isinstance(doc, pdoc.doc.Module)
321                doc = doc.get(qualname)
322            target_exists_and_public = (
323                doc is not None and context["is_public"](doc).strip()
324            )
325            if target_exists_and_public:
326                if qualname:
327                    qualname = f"#{qualname}"
328                return f'<a href="{relative_link(context["module"].modulename, module)}{qualname}">{plain_text}</a>'
329            else:
330                return text
331
332    return Markup(
333        re.sub(
334            r"""
335            # Part 1: foo.bar or foo.bar() (without backticks)
336            (?<![/=?#&])  # heuristic: not part of a URL
337            \b
338            
339            # First part of the identifier (e.g. "foo")    
340            (?!\d)[a-zA-Z0-9_]+
341            # Rest of the identifier (e.g. ".bar")
342            (?:
343                # A single dot or a dot surrounded with pygments highlighting.
344                (?:\.|</span><span\ class="o">\.</span><span\ class="n">)
345                (?!\d)[a-zA-Z0-9_]+
346            )+
347            (?:\(\)|\b(?!\(\)))  # we either end on () or on a word boundary.
348            (?!</a>)  # not an existing link
349            (?![/#])  # heuristic: not part of a URL
350
351            | # Part 2: `foo` or `foo()`. `foo.bar` is already covered with part 1.
352            (?<=<code>)
353                 (?!\d)[a-zA-Z0-9_]+
354            (?:\(\))?
355            (?=</code>(?!</a>))
356            """,
357            linkify_repl,
358            code,
359            flags=re.VERBOSE,
360        )
361    )

Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that are not rendered at the moment will be ignored. A piece of text is considered to be an identifier if it either contains a . or is surrounded by <code> tags.

def edit_url( modulename: str, is_package: bool, mapping: collections.abc.Mapping[str, str]) -> str | None:
391def edit_url(
392    modulename: str, is_package: bool, mapping: Mapping[str, str]
393) -> str | None:
394    """Create a link to edit a particular file in the used version control system."""
395    for m, prefix in mapping.items():
396        if m == modulename or modulename.startswith(f"{m}."):
397            filename = modulename[len(m) + 1 :].replace(".", "/")
398            if is_package:
399                filename = f"{filename}/__init__.py".lstrip("/")
400            else:
401                filename += ".py"
402            return f"{prefix}{filename}"
403    return None

Create a link to edit a particular file in the used version control system.

def root_module_name(all_modules: collections.abc.Mapping[str, pdoc.doc.Module]) -> str | None:
406def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None:
407    """
408    Return the name of the (unique) top-level module, or `None`
409    if no such module exists.
410
411    For example, assuming `foo`, `foo.bar`, and `foo.baz` are documented,
412    this function will return `foo`. If `foo` and `bar` are documented,
413    this function will return `None` as there is no unique top-level module.
414    """
415    shortest_name = min(all_modules, key=len, default=None)
416    prefix = f"{shortest_name}."
417    all_others_are_submodules = all(
418        x.startswith(prefix) or x == shortest_name for x in all_modules
419    )
420    if all_others_are_submodules:
421        return shortest_name
422    else:
423        return None

Return the name of the (unique) top-level module, or None if no such module exists.

For example, assuming foo, foo.bar, and foo.baz are documented, this function will return foo. If foo and bar are documented, this function will return None as there is no unique top-level module.

def minify_css(css: str) -> str:
426def minify_css(css: str) -> str:
427    """Do some very basic CSS minification."""
428    css = re.sub(r"[ ]{4}|\n|(?<=[:{}]) | (?=[{}])", "", css)
429    css = re.sub(
430        r"/\*.+?\*/", lambda m: m.group(0) if m.group(0).startswith("/*!") else "", css
431    )
432    return Markup(css.replace("<style", "\n<style"))

Do some very basic CSS minification.

@contextmanager
def defuse_unsafe_reprs():
435@contextmanager
436def defuse_unsafe_reprs():
437    """This decorator is applied by pdoc before calling an object's repr().
438    It applies some heuristics to patch our sensitive information.
439    For example, `os.environ`'s default `__repr__` implementation exposes all
440    local secrets.
441    """
442    with (patch.object(os._Environ, "__repr__", lambda self: "os.environ")):
443        yield

This decorator is applied by pdoc before calling an object's repr(). It applies some heuristics to patch our sensitive information. For example, os.environ's default __repr__ implementation exposes all local secrets.

class DefaultMacroExtension(jinja2.ext.Extension):
446class DefaultMacroExtension(ext.Extension):
447    """
448    This extension provides a new `{% defaultmacro %}` statement, which defines a macro only if it does not exist.
449
450    For example,
451
452    ```html+jinja
453    {% defaultmacro example() %}
454        test 123
455    {% enddefaultmacro %}
456    ```
457
458    is equivalent to
459
460    ```html+jinja
461    {% macro default_example() %}
462    test 123
463    {% endmacro %}
464    {% if not example %}
465        {% macro example() %}
466            test 123
467        {% endmacro %}
468    {% endif %}
469    ```
470
471    Additionally, the default implementation is also available as `default_$macroname`, which makes it possible
472    to reference it in the override.
473    """
474
475    tags = {"defaultmacro"}
476
477    def parse(self, parser):
478        m = nodes.Macro(lineno=next(parser.stream).lineno)
479        name = parser.parse_assign_target(name_only=True).name
480        m.name = f"default_{name}"
481        parser.parse_signature(m)
482        m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True)
483
484        if_stmt = nodes.If(
485            nodes.Not(nodes.Name(name, "load")),
486            [nodes.Macro(name, m.args, m.defaults, m.body)],
487            [],
488            [],
489        )
490        return [m, if_stmt]

This extension provides a new {% defaultmacro %} statement, which defines a macro only if it does not exist.

For example,

{% defaultmacro example() %}
    test 123
{% enddefaultmacro %}

is equivalent to

{% macro default_example() %}
test 123
{% endmacro %}
{% if not example %}
    {% macro example() %}
        test 123
    {% endmacro %}
{% endif %}

Additionally, the default implementation is also available as default_$macroname, which makes it possible to reference it in the override.

def parse(self, parser):
477    def parse(self, parser):
478        m = nodes.Macro(lineno=next(parser.stream).lineno)
479        name = parser.parse_assign_target(name_only=True).name
480        m.name = f"default_{name}"
481        parser.parse_signature(m)
482        m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True)
483
484        if_stmt = nodes.If(
485            nodes.Not(nodes.Name(name, "load")),
486            [nodes.Macro(name, m.args, m.defaults, m.body)],
487            [],
488            [],
489        )
490        return [m, if_stmt]

If any of the tags matched this method is called with the parser as first argument. The token the parser stream is pointing at is the name token that matched. This method has to return one or a list of multiple nodes.

Inherited Members
jinja2.ext.Extension
Extension
bind
preprocess
filter_stream
attr
call_method