Edit on GitHub

pdoc.render_helpers

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

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

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

Convert docstring from Markdown to HTML.

@pass_context
def to_markdown_with_context(context: jinja2.runtime.Context, docstring: str) -> str:
185@pass_context
186def to_markdown_with_context(context: Context, docstring: str) -> str:
187    """
188    Converts `docstring` from a custom docformat to Markdown (if necessary), and then from Markdown to HTML.
189    """
190    module: pdoc.doc.Module = context["module"]
191    docformat: str = context["docformat"]
192    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:
195def to_markdown(docstring: str, module: pdoc.doc.Module, default_docformat: str) -> str:
196    docformat = getattr(module.obj, "__docformat__", default_docformat) or ""
197    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]]:
200def possible_sources(
201    all_modules: Collection[str], identifier: str
202) -> Iterable[tuple[str, str]]:
203    """
204    For a given identifier, return all possible sources where it could originate from.
205    For example, assume `examplepkg._internal.Foo` with all_modules=["examplepkg"].
206    This could be a Foo class in _internal.py, or a nested `class _internal: class Foo` in examplepkg.
207    We return both candidates as we don't know if _internal.py exists.
208    It may not be in all_modules because it's been excluded by `__all__`.
209    However, if `examplepkg._internal` is in all_modules we know that it can only be that option.
210    """
211    if identifier in all_modules:
212        yield identifier, ""
213        return
214
215    modulename = identifier
216    qualname = None
217    while modulename:
218        modulename, _, add = modulename.rpartition(".")
219        qualname = f"{add}.{qualname}" if qualname else add
220        yield modulename, qualname
221        if modulename in all_modules:
222            return
223    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]:
226def split_identifier(all_modules: Collection[str], fullname: str) -> tuple[str, str]:
227    """
228    Split an identifier into a `(modulename, qualname)` tuple. For example, `pdoc.render_helpers.split_identifier`
229    would be split into `("pdoc.render_helpers","split_identifier")`. This is necessary to generate links to the
230    correct module.
231    """
232    warnings.warn(
233        "pdoc.render_helpers.split_identifier is deprecated and will be removed in a future release. "
234        "Use pdoc.render_helpers.possible_sources instead.",
235        DeprecationWarning,
236    )
237    *_, last = possible_sources(all_modules, fullname)
238    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]:
261def qualname_candidates(identifier: str, context_qualname: str) -> list[str]:
262    """
263    Given an identifier in a current namespace, return all possible qualnames in the current module.
264    For example, if we are in Foo's subclass Bar and `baz()` is the identifier,
265    return `Foo.Bar.baz()`, `Foo.baz()`, and `baz()`.
266    """
267    end = len(context_qualname)
268    ret = []
269    while end > 0:
270        ret.append(f"{context_qualname[:end]}.{identifier}")
271        end = context_qualname.rfind(".", 0, end)
272    ret.append(identifier)
273    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:
276@pass_context
277def linkify(context: Context, code: str, namespace: str = "") -> str:
278    """
279    Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that
280    are not rendered at the moment will be ignored.
281    A piece of text is considered to be an identifier if it either contains a `.` or is surrounded by `<code>` tags.
282    """
283
284    def linkify_repl(m: re.Match):
285        text = m.group(0)
286        plain_text = text.replace(
287            '</span><span class="o">.</span><span class="n">', "."
288        )
289        identifier = removesuffix(plain_text, "()")
290
291        # Check if this is a local reference within this module?
292        mod: pdoc.doc.Module = context["module"]
293        for qualname in qualname_candidates(identifier, namespace):
294            doc = mod.get(qualname)
295            if doc and context["is_public"](doc).strip():
296                return f'<a href="#{qualname}">{plain_text}</a>'
297
298        module = ""
299        qualname = ""
300        try:
301            # Check if the object we are interested in is imported and re-exposed in the current namespace.
302            for module, qualname in possible_sources(
303                context["all_modules"], identifier
304            ):
305                doc = mod.get(qualname)
306                if (
307                    doc
308                    and doc.taken_from == (module, qualname)
309                    and context["is_public"](doc).strip()
310                ):
311                    if plain_text.endswith("()"):
312                        plain_text = f"{doc.fullname}()"
313                    else:
314                        plain_text = doc.fullname
315                    return f'<a href="#{qualname}">{plain_text}</a>'
316        except ValueError:
317            # possible_sources did not find a parent module.
318            return text
319        else:
320            # It's not, but we now know the parent module. Does the target exist?
321            doc = context["all_modules"][module]
322            if qualname:
323                assert isinstance(doc, pdoc.doc.Module)
324                doc = doc.get(qualname)
325            target_exists_and_public = (
326                doc is not None and context["is_public"](doc).strip()
327            )
328            if target_exists_and_public:
329                if qualname:
330                    qualname = f"#{qualname}"
331                return f'<a href="{relative_link(context["module"].modulename, module)}{qualname}">{plain_text}</a>'
332            else:
333                return text
334
335    return Markup(
336        re.sub(
337            r"""
338            # Part 1: foo.bar or foo.bar() (without backticks)
339            (?<![/=?#&])  # heuristic: not part of a URL
340            \b
341            
342            # First part of the identifier (e.g. "foo")    
343            (?!\d)[a-zA-Z0-9_]+
344            # Rest of the identifier (e.g. ".bar")
345            (?:
346                # A single dot or a dot surrounded with pygments highlighting.
347                (?:\.|</span><span\ class="o">\.</span><span\ class="n">)
348                (?!\d)[a-zA-Z0-9_]+
349            )+
350            (?:\(\)|\b(?!\(\)))  # we either end on () or on a word boundary.
351            (?!</a>)  # not an existing link
352            (?![/#])  # heuristic: not part of a URL
353
354            | # Part 2: `foo` or `foo()`. `foo.bar` is already covered with part 1.
355            (?<=<code>)
356                 (?!\d)[a-zA-Z0-9_]+
357            (?:\(\))?
358            (?=</code>(?!</a>))
359            """,
360            linkify_repl,
361            code,
362            flags=re.VERBOSE,
363        )
364    )

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:
404def edit_url(
405    modulename: str, is_package: bool, mapping: Mapping[str, str]
406) -> str | None:
407    """Create a link to edit a particular file in the used version control system."""
408    for m, prefix in mapping.items():
409        if m == modulename or modulename.startswith(f"{m}."):
410            filename = modulename[len(m) + 1 :].replace(".", "/")
411            if is_package:
412                filename = f"{filename}/__init__.py".lstrip("/")
413            else:
414                filename += ".py"
415            return f"{prefix}{filename}"
416    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:
419def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None:
420    """
421    Return the name of the (unique) top-level module, or `None`
422    if no such module exists.
423
424    For example, assuming `foo`, `foo.bar`, and `foo.baz` are documented,
425    this function will return `foo`. If `foo` and `bar` are documented,
426    this function will return `None` as there is no unique top-level module.
427    """
428    shortest_name = min(all_modules, key=len, default=None)
429    prefix = f"{shortest_name}."
430    all_others_are_submodules = all(
431        x.startswith(prefix) or x == shortest_name for x in all_modules
432    )
433    if all_others_are_submodules:
434        return shortest_name
435    else:
436        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:
439def minify_css(css: str) -> str:
440    """Do some very basic CSS minification."""
441    css = re.sub(r"[ ]{4}|\n|(?<=[:{}]) | (?=[{}])", "", css)
442    css = re.sub(
443        r"/\*.+?\*/", lambda m: m.group(0) if m.group(0).startswith("/*!") else "", css
444    )
445    return Markup(css.replace("<style", "\n<style"))

Do some very basic CSS minification.

@contextmanager
def defuse_unsafe_reprs():
448@contextmanager
449def defuse_unsafe_reprs():
450    """This decorator is applied by pdoc before calling an object's repr().
451    It applies some heuristics to patch our sensitive information.
452    For example, `os.environ`'s default `__repr__` implementation exposes all
453    local secrets.
454    """
455    with patch.object(os._Environ, "__repr__", lambda self: "os.environ"):
456        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):
459class DefaultMacroExtension(ext.Extension):
460    """
461    This extension provides a new `{% defaultmacro %}` statement, which defines a macro only if it does not exist.
462
463    For example,
464
465    ```html+jinja
466    {% defaultmacro example() %}
467        test 123
468    {% enddefaultmacro %}
469    ```
470
471    is equivalent to
472
473    ```html+jinja
474    {% macro default_example() %}
475    test 123
476    {% endmacro %}
477    {% if not example %}
478        {% macro example() %}
479            test 123
480        {% endmacro %}
481    {% endif %}
482    ```
483
484    Additionally, the default implementation is also available as `default_$macroname`, which makes it possible
485    to reference it in the override.
486    """
487
488    tags = {"defaultmacro"}
489
490    def parse(self, parser):
491        m = nodes.Macro(lineno=next(parser.stream).lineno)
492        name = parser.parse_assign_target(name_only=True).name
493        m.name = f"default_{name}"
494        parser.parse_signature(m)
495        m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True)
496
497        if_stmt = nodes.If(
498            nodes.Not(nodes.Name(name, "load")),
499            [nodes.Macro(name, m.args, m.defaults, m.body)],
500            [],
501            [],
502        )
503        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):
490    def parse(self, parser):
491        m = nodes.Macro(lineno=next(parser.stream).lineno)
492        name = parser.parse_assign_target(name_only=True).name
493        m.name = f"default_{name}"
494        parser.parse_signature(m)
495        m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True)
496
497        if_stmt = nodes.If(
498            nodes.Not(nodes.Name(name, "load")),
499            [nodes.Macro(name, m.args, m.defaults, m.body)],
500            [],
501            [],
502        )
503        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