Edit on GitHub

pdoc.render_helpers

  1from __future__ import annotations
  2
  3from collections.abc import Collection
  4from collections.abc import Iterable
  5from collections.abc import Mapping
  6from contextlib import contextmanager
  7import html
  8import inspect
  9import os
 10import re
 11from unittest.mock import patch
 12import warnings
 13
 14from jinja2 import ext
 15from jinja2 import nodes
 16import pygments.formatters
 17import pygments.lexers
 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
 32from ._compat import removesuffix
 33
 34lexer = pygments.lexers.PythonLexer()
 35"""
 36The pygments lexer used for pdoc.render_helpers.highlight.
 37Overwrite this to configure pygments lexing.
 38"""
 39
 40formatter = pygments.formatters.HtmlFormatter(
 41    cssclass="pdoc-code codehilite",
 42    linenos="inline",
 43    anchorlinenos=True,
 44)
 45"""
 46The pygments formatter used for pdoc.render_helpers.highlight.
 47Overwrite this to configure pygments highlighting of code blocks.
 48
 49The usage of the `.codehilite` CSS selector in custom templates is deprecated since pdoc 10, use `.pdoc-code` instead.
 50"""
 51
 52signature_formatter = pygments.formatters.HtmlFormatter(nowrap=True)
 53"""
 54The pygments formatter used for pdoc.render_helpers.format_signature.
 55Overwrite this to configure pygments highlighting of signatures.
 56"""
 57
 58# Keep in sync with the documentation in pdoc/__init__.py!
 59markdown_extensions = {
 60    "code-friendly": None,
 61    "cuddled-lists": None,
 62    "fenced-code-blocks": {"cssclass": formatter.cssclass},
 63    "footnotes": None,
 64    "header-ids": None,
 65    "link-patterns": None,
 66    "markdown-in-html": None,
 67    "mermaid": None,
 68    "pyshell": None,
 69    "strike": None,
 70    "tables": None,
 71    "task_list": None,
 72    "toc": {"depth": 2},
 73}
 74"""
 75The default extensions loaded for `markdown2`.
 76Overwrite this to configure Markdown rendering.
 77"""
 78markdown_link_patterns = [
 79    (
 80        re.compile(
 81            r"""
 82            \b
 83            (
 84                (?:https?://|(?<!//)www\.)    # prefix - https:// or www.
 85                \w[\w_\-]*(?:\.\w[\w_\-]*)*   # host
 86                [^<>\s"']*                    # rest of url
 87                (?<![?!.,:*_~);])             # exclude trailing punctuation
 88                (?=[?!.,:*_~);]?(?:[<\s]|$))  # make sure that we're not followed by " or ', i.e. we're outside of href="...".
 89            )
 90        """,
 91            re.X,
 92        ),
 93        r"\1",
 94    )
 95]
 96"""
 97Link pattern used for markdown2's [`link-patterns` extra](https://github.com/trentm/python-markdown2/wiki/link-patterns).
 98"""
 99
100
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))
115
116
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)
168
169
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    )
183
184
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)
193
194
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)
198
199
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    >>> possible_sources(["examplepkg"], "examplepkg.Foo.bar")
212    examplepkg.Foo, bar
213    examplepkg, Foo.bar
214    """
215    if identifier in all_modules:
216        yield identifier, ""
217        return
218
219    modulename = identifier
220    qualname = None
221    while modulename:
222        modulename, _, add = modulename.rpartition(".")
223        qualname = f"{add}.{qualname}" if qualname else add
224        yield modulename, qualname
225        if modulename in all_modules:
226            return
227    raise ValueError(f"Invalid identifier: {identifier}")
228
229
230def split_identifier(all_modules: Collection[str], fullname: str) -> tuple[str, str]:
231    """
232    Split an identifier into a `(modulename, qualname)` tuple. For example, `pdoc.render_helpers.split_identifier`
233    would be split into `("pdoc.render_helpers","split_identifier")`. This is necessary to generate links to the
234    correct module.
235    """
236    warnings.warn(
237        "pdoc.render_helpers.split_identifier is deprecated and will be removed in a future release. "
238        "Use pdoc.render_helpers.possible_sources instead.",
239        DeprecationWarning,
240    )
241    *_, last = possible_sources(all_modules, fullname)
242    return last
243
244
245def _relative_link(current: list[str], target: list[str]) -> str:
246    if target == current:
247        return f"../{target[-1]}.html"
248    elif target[: len(current)] == current:
249        return "/".join(target[len(current) :]) + ".html"
250    else:
251        return "../" + _relative_link(current[:-1], target)
252
253
254@cache
255def relative_link(current_module: str, target_module: str) -> str:
256    """Compute the relative link to another module's HTML file."""
257    if current_module == target_module:
258        return ""
259    return _relative_link(
260        current_module.split(".")[:-1],
261        target_module.split("."),
262    )
263
264
265def qualname_candidates(identifier: str, context_qualname: str) -> list[str]:
266    """
267    Given an identifier in a current namespace, return all possible qualnames in the current module.
268    For example, if we are in Foo's subclass Bar and `baz()` is the identifier,
269    return `Foo.Bar.baz()`, `Foo.baz()`, and `baz()`.
270    """
271    end = len(context_qualname)
272    ret = []
273    while end > 0:
274        ret.append(f"{context_qualname[:end]}.{identifier}")
275        end = context_qualname.rfind(".", 0, end)
276    ret.append(identifier)
277    return ret
278
279
280def module_candidates(identifier: str, current_module: str) -> Iterable[str]:
281    """
282    Given an identifier and the current module name, return the module names we should look at
283    to find where the target object is exposed. Module names are ordered by preferences, i.e.
284    we always prefer the current module and then top-level modules over their children.
285
286    >>> module_candidates("foo.bar.baz", "qux")
287    qux
288    foo
289    foo.bar
290    foo.bar.baz
291    >>> module_candidates("foo.bar.baz", "foo.bar")
292    foo.bar
293    foo
294    foo.bar.baz
295    """
296    yield current_module
297
298    end = identifier.find(".")
299    while end > 0:
300        if (name := identifier[:end]) != current_module:
301            yield name
302        end = identifier.find(".", end + 1)
303
304    if identifier != current_module:
305        yield identifier
306
307
308@pass_context
309def linkify(context: Context, code: str, namespace: str = "") -> str:
310    """
311    Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that
312    are not rendered at the moment will be ignored.
313    A piece of text is considered to be an identifier if it either contains a `.` or is surrounded by `<code>` tags.
314    """
315
316    def linkify_repl(m: re.Match):
317        """
318        Resolve `text` to the most suitable documentation object.
319        """
320        text = m.group(0)
321        plain_text = text.replace(
322            '</span><span class="o">.</span><span class="n">', "."
323        )
324        identifier = removesuffix(plain_text, "()")
325        mod: pdoc.doc.Module = context["module"]
326
327        # Check if this is a relative reference. These cannot be local and need to be resolved.
328        if identifier.startswith("."):
329            taken_from_mod = mod
330            if namespace and (ns := mod.get(namespace)):
331                # Imported from somewhere else, so the relative reference should be from the original module.
332                taken_from_mod = context["all_modules"].get(ns.taken_from[0], mod)
333            if taken_from_mod.is_package:
334                # If we are in __init__.py, we want `.foo` to refer to a child module.
335                parent_module = taken_from_mod.modulename
336            else:
337                # If we are in a leaf module, we want `.foo` to refer to the adjacent module.
338                parent_module = taken_from_mod.modulename.rpartition(".")[0]
339            while identifier.startswith(".."):
340                identifier = identifier[1:]
341                parent_module = parent_module.rpartition(".")[0]
342            identifier = parent_module + identifier
343        else:
344            # Is this a local reference within this module?
345            for qualname in qualname_candidates(identifier, namespace):
346                doc = mod.get(qualname)
347                if doc and context["is_public"](doc).strip():
348                    return f'<a href="#{qualname}">{plain_text}</a>'
349
350        # Is this a reference pointing straight at a module?
351        if identifier in context["all_modules"]:
352            return f'<a href="{relative_link(context["module"].modulename, identifier)}">{identifier}</a>'
353
354        try:
355            sources = list(possible_sources(context["all_modules"], identifier))
356        except ValueError:
357            # possible_sources did not find a parent module.
358            return text
359
360        # Try to find the actual target object so that we can then later verify
361        # that objects exposed at a parent module with the same name point to it.
362        target_object = None
363        for module_name, qualname in sources:
364            if doc := context["all_modules"].get(module_name, {}).get(qualname):
365                target_object = doc.obj
366                break
367
368        # Look at the different modules where our target object may be exposed.
369        for module_name in module_candidates(identifier, mod.modulename):
370            module: pdoc.doc.Module | None = context["all_modules"].get(module_name)
371            if not module:
372                continue
373
374            for _, qualname in sources:
375                doc = module.get(qualname)
376                # Check if they have an object with the same name,
377                # and verify that it's pointing to the right thing and is public.
378                if (
379                    doc
380                    and (target_object is doc.obj or target_object is None)
381                    and context["is_public"](doc).strip()
382                ):
383                    if module == mod:
384                        url_text = qualname
385                    else:
386                        url_text = doc.fullname
387                    if plain_text.endswith("()"):
388                        url_text += "()"
389                    return f'<a href="{relative_link(context["module"].modulename, doc.modulename)}#{qualname}">{url_text}</a>'
390
391        # No matches found.
392        return text
393
394    return Markup(
395        re.sub(
396            r"""
397            # Part 1: foo.bar or foo.bar() (without backticks)
398            (?<![/=?#&\.])  # heuristic: not part of a URL
399            # First part of the identifier (e.g. "foo") - this is optional for relative references.
400            (?:
401                \b
402                (?!\d)[a-zA-Z0-9_]+
403                |
404                \.*  # We may also start with multiple dots.
405            )
406            # Rest of the identifier (e.g. ".bar" or "..bar")
407            (?:
408                # A single dot or a dot surrounded with pygments highlighting.
409                (?:\.|</span><span\ class="o">\.</span><span\ class="n">)
410                (?!\d)[a-zA-Z0-9_]+
411            )+
412            (?:\(\)|\b(?!\(\)))  # we either end on () or on a word boundary.
413            (?!</a>)  # not an existing link
414            (?![/#])  # heuristic: not part of a URL
415
416            | # Part 2: `foo` or `foo()`. `foo.bar` is already covered with part 1.
417            (?<=<code>)
418                 (?!\d)[a-zA-Z0-9_]+
419            (?:\(\))?
420            (?=</code>(?!</a>))
421            """,
422            linkify_repl,
423            code,
424            flags=re.VERBOSE,
425        )
426    )
427
428
429@pass_context
430def link(context: Context, spec: tuple[str, str], text: str | None = None) -> str:
431    """Create a link for a specific `(modulename, qualname)` tuple."""
432    mod: pdoc.doc.Module = context["module"]
433    modulename, qualname = spec
434
435    # Check if the object we are interested is also imported and re-exposed in the current namespace.
436    # https://github.com/mitmproxy/pdoc/issues/490: We need to do this for every level, not just the tail.
437    doc: pdoc.doc.Doc | None = mod
438    for part in qualname.split("."):
439        doc = doc.get(part) if isinstance(doc, pdoc.doc.Namespace) else None
440        if not (
441            doc
442            and doc.taken_from[0] == modulename
443            and context["is_public"](doc).strip()
444        ):
445            break
446    else:
447        # everything down to the tail is imported and re-exposed.
448        if text:
449            text = text.replace(f"{modulename}.", f"{mod.modulename}.")
450        modulename = mod.modulename
451
452    if mod.modulename == modulename:
453        fullname = qualname
454    else:
455        fullname = removesuffix(f"{modulename}.{qualname}", ".")
456
457    if qualname:
458        qualname = f"#{qualname}"
459    if modulename in context["all_modules"]:
460        return Markup(
461            f'<a href="{relative_link(context["module"].modulename, modulename)}{qualname}">{text or fullname}</a>'
462        )
463    return text or fullname
464
465
466def edit_url(
467    modulename: str, is_package: bool, mapping: Mapping[str, str]
468) -> str | None:
469    """Create a link to edit a particular file in the used version control system."""
470    for m, prefix in mapping.items():
471        if m == modulename or modulename.startswith(f"{m}."):
472            filename = modulename[len(m) + 1 :].replace(".", "/")
473            if is_package:
474                filename = f"{filename}/__init__.py".lstrip("/")
475            else:
476                filename += ".py"
477            return f"{prefix}{filename}"
478    return None
479
480
481def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None:
482    """
483    Return the name of the (unique) top-level module, or `None`
484    if no such module exists.
485
486    For example, assuming `foo`, `foo.bar`, and `foo.baz` are documented,
487    this function will return `foo`. If `foo` and `bar` are documented,
488    this function will return `None` as there is no unique top-level module.
489    """
490    shortest_name = min(all_modules, key=len, default=None)
491    prefix = f"{shortest_name}."
492    all_others_are_submodules = all(
493        x.startswith(prefix) or x == shortest_name for x in all_modules
494    )
495    if all_others_are_submodules:
496        return shortest_name
497    else:
498        return None
499
500
501def minify_css(css: str) -> str:
502    """Do some very basic CSS minification."""
503    css = re.sub(r"[ ]{4}|\n|(?<=[:{}]) | (?=[{}])", "", css)
504    css = re.sub(
505        r"/\*.+?\*/", lambda m: m.group(0) if m.group(0).startswith("/*!") else "", css
506    )
507    return Markup(css.replace("<style", "\n<style"))
508
509
510@contextmanager
511def defuse_unsafe_reprs():
512    """This decorator is applied by pdoc before calling an object's repr().
513    It applies some heuristics to patch our sensitive information.
514    For example, `os.environ`'s default `__repr__` implementation exposes all
515    local secrets.
516    """
517    with patch.object(os._Environ, "__repr__", lambda self: "os.environ"):
518        yield
519
520
521class DefaultMacroExtension(ext.Extension):
522    """
523    This extension provides a new `{% defaultmacro %}` statement, which defines a macro only if it does not exist.
524
525    For example,
526
527    ```html+jinja
528    {% defaultmacro example() %}
529        test 123
530    {% enddefaultmacro %}
531    ```
532
533    is equivalent to
534
535    ```html+jinja
536    {% macro default_example() %}
537    test 123
538    {% endmacro %}
539    {% if not example %}
540        {% macro example() %}
541            test 123
542        {% endmacro %}
543    {% endif %}
544    ```
545
546    Additionally, the default implementation is also available as `default_$macroname`, which makes it possible
547    to reference it in the override.
548    """
549
550    tags = {"defaultmacro"}
551
552    def parse(self, parser):
553        m = nodes.Macro(lineno=next(parser.stream).lineno)
554        name = parser.parse_assign_target(name_only=True).name
555        m.name = f"default_{name}"
556        parser.parse_signature(m)
557        m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True)
558
559        if_stmt = nodes.If(
560            nodes.Not(nodes.Name(name, "load")),
561            [nodes.Macro(name, m.args, m.defaults, m.body)],
562            [],
563            [],
564        )
565        return [m, if_stmt]
lexer = <pygments.lexers.PythonLexer>

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

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

The pygments formatter used for 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.pdoc-code instead.

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

The pygments formatter used for 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:
102@cache
103def highlight(doc: pdoc.doc.Doc) -> str:
104    """Highlight the source code of a documentation object using pygments."""
105    if isinstance(doc, str):  # pragma: no cover
106        warnings.warn(
107            "Passing a string to the `highlight` render helper is deprecated, pass a pdoc.doc.Doc object instead.",
108            DeprecationWarning,
109        )
110        return Markup(pygments.highlight(doc, lexer, formatter))
111
112    # set up correct line numbers and anchors
113    formatter.linespans = doc.qualname or "L"
114    formatter.linenostart = doc.source_lines[0] + 1 if doc.source_lines else 1
115    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:
118def format_signature(sig: inspect.Signature, colon: bool) -> str:
119    """Format and highlight a function signature using pygments. Returns HTML."""
120    # First get a list with all params as strings.
121    result = pdoc.doc._PrettySignature._params(sig)  # type: ignore
122    return_annot = pdoc.doc._PrettySignature._return_annotation_str(sig)  # type: ignore
123
124    multiline = (
125        sum(len(x) + 2 for x in result) + len(return_annot)
126        > pdoc.doc._PrettySignature.MULTILINE_CUTOFF
127    )
128
129    def _try_highlight(code: str) -> str:
130        """Try to highlight a piece of code using pygments, but return the input as-is if pygments detects errors."""
131        pretty = pygments.highlight(code, lexer, signature_formatter).strip()
132        if '<span class="err">' not in pretty:
133            return pretty
134        else:
135            return html.escape(code)
136
137    # Next, individually highlight each parameter using pygments and wrap it in a span.param.
138    # This later allows us to properly control line breaks.
139    pretty_result = []
140    for i, param in enumerate(result):
141        pretty = _try_highlight(param)
142        if multiline:
143            pretty = f"""<span class="param">\t{pretty},</span>"""
144        else:
145            pretty = f"""<span class="param">{pretty}, </span>"""
146        pretty_result.append(pretty)
147
148    # remove last comma.
149    if pretty_result:
150        pretty_result[-1] = pretty_result[-1].rpartition(",")[0] + "</span>"
151
152    # Add return annotation.
153    anno = ")"
154    if return_annot:
155        anno += f" -> {_try_highlight(return_annot)}"
156    if colon:
157        anno += ":"
158    if return_annot or colon:
159        anno = f'<span class="return-annotation">{anno}</span>'
160
161    rendered = "(" + "".join(pretty_result) + anno
162
163    if multiline:
164        rendered = f'<span class="signature pdoc-code multiline">{rendered}</span>'
165    else:
166        rendered = f'<span class="signature pdoc-code condensed">{rendered}</span>'
167
168    return Markup(rendered)

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

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

Convert docstring from Markdown to HTML.

@pass_context
def to_markdown_with_context(context: jinja2.runtime.Context, docstring: str) -> str:
186@pass_context
187def to_markdown_with_context(context: Context, docstring: str) -> str:
188    """
189    Converts `docstring` from a custom docformat to Markdown (if necessary), and then from Markdown to HTML.
190    """
191    module: pdoc.doc.Module = context["module"]
192    docformat: str = context["docformat"]
193    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:
196def to_markdown(docstring: str, module: pdoc.doc.Module, default_docformat: str) -> str:
197    docformat = getattr(module.obj, "__docformat__", default_docformat) or ""
198    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]]:
201def possible_sources(
202    all_modules: Collection[str], identifier: str
203) -> Iterable[tuple[str, str]]:
204    """
205    For a given identifier, return all possible sources where it could originate from.
206    For example, assume `examplepkg._internal.Foo` with all_modules=["examplepkg"].
207    This could be a Foo class in _internal.py, or a nested `class _internal: class Foo` in examplepkg.
208    We return both candidates as we don't know if _internal.py exists.
209    It may not be in all_modules because it's been excluded by `__all__`.
210    However, if `examplepkg._internal` is in all_modules we know that it can only be that option.
211
212    >>> possible_sources(["examplepkg"], "examplepkg.Foo.bar")
213    examplepkg.Foo, bar
214    examplepkg, Foo.bar
215    """
216    if identifier in all_modules:
217        yield identifier, ""
218        return
219
220    modulename = identifier
221    qualname = None
222    while modulename:
223        modulename, _, add = modulename.rpartition(".")
224        qualname = f"{add}.{qualname}" if qualname else add
225        yield modulename, qualname
226        if modulename in all_modules:
227            return
228    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.

>>> possible_sources(["examplepkg"], "examplepkg.Foo.bar")
examplepkg.Foo, bar
examplepkg, Foo.bar
def split_identifier( all_modules: collections.abc.Collection[str], fullname: str) -> tuple[str, str]:
231def split_identifier(all_modules: Collection[str], fullname: str) -> tuple[str, str]:
232    """
233    Split an identifier into a `(modulename, qualname)` tuple. For example, `pdoc.render_helpers.split_identifier`
234    would be split into `("pdoc.render_helpers","split_identifier")`. This is necessary to generate links to the
235    correct module.
236    """
237    warnings.warn(
238        "pdoc.render_helpers.split_identifier is deprecated and will be removed in a future release. "
239        "Use pdoc.render_helpers.possible_sources instead.",
240        DeprecationWarning,
241    )
242    *_, last = possible_sources(all_modules, fullname)
243    return last

Split an identifier into a (modulename, qualname) tuple. For example, 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]:
266def qualname_candidates(identifier: str, context_qualname: str) -> list[str]:
267    """
268    Given an identifier in a current namespace, return all possible qualnames in the current module.
269    For example, if we are in Foo's subclass Bar and `baz()` is the identifier,
270    return `Foo.Bar.baz()`, `Foo.baz()`, and `baz()`.
271    """
272    end = len(context_qualname)
273    ret = []
274    while end > 0:
275        ret.append(f"{context_qualname[:end]}.{identifier}")
276        end = context_qualname.rfind(".", 0, end)
277    ret.append(identifier)
278    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().

def module_candidates(identifier: str, current_module: str) -> collections.abc.Iterable[str]:
281def module_candidates(identifier: str, current_module: str) -> Iterable[str]:
282    """
283    Given an identifier and the current module name, return the module names we should look at
284    to find where the target object is exposed. Module names are ordered by preferences, i.e.
285    we always prefer the current module and then top-level modules over their children.
286
287    >>> module_candidates("foo.bar.baz", "qux")
288    qux
289    foo
290    foo.bar
291    foo.bar.baz
292    >>> module_candidates("foo.bar.baz", "foo.bar")
293    foo.bar
294    foo
295    foo.bar.baz
296    """
297    yield current_module
298
299    end = identifier.find(".")
300    while end > 0:
301        if (name := identifier[:end]) != current_module:
302            yield name
303        end = identifier.find(".", end + 1)
304
305    if identifier != current_module:
306        yield identifier

Given an identifier and the current module name, return the module names we should look at to find where the target object is exposed. Module names are ordered by preferences, i.e. we always prefer the current module and then top-level modules over their children.

>>> module_candidates("foo.bar.baz", "qux")
qux
foo
foo.bar
foo.bar.baz
>>> module_candidates("foo.bar.baz", "foo.bar")
foo.bar
foo
foo.bar.baz
@pass_context
def linkify(context: jinja2.runtime.Context, code: str, namespace: str = '') -> str:
309@pass_context
310def linkify(context: Context, code: str, namespace: str = "") -> str:
311    """
312    Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that
313    are not rendered at the moment will be ignored.
314    A piece of text is considered to be an identifier if it either contains a `.` or is surrounded by `<code>` tags.
315    """
316
317    def linkify_repl(m: re.Match):
318        """
319        Resolve `text` to the most suitable documentation object.
320        """
321        text = m.group(0)
322        plain_text = text.replace(
323            '</span><span class="o">.</span><span class="n">', "."
324        )
325        identifier = removesuffix(plain_text, "()")
326        mod: pdoc.doc.Module = context["module"]
327
328        # Check if this is a relative reference. These cannot be local and need to be resolved.
329        if identifier.startswith("."):
330            taken_from_mod = mod
331            if namespace and (ns := mod.get(namespace)):
332                # Imported from somewhere else, so the relative reference should be from the original module.
333                taken_from_mod = context["all_modules"].get(ns.taken_from[0], mod)
334            if taken_from_mod.is_package:
335                # If we are in __init__.py, we want `.foo` to refer to a child module.
336                parent_module = taken_from_mod.modulename
337            else:
338                # If we are in a leaf module, we want `.foo` to refer to the adjacent module.
339                parent_module = taken_from_mod.modulename.rpartition(".")[0]
340            while identifier.startswith(".."):
341                identifier = identifier[1:]
342                parent_module = parent_module.rpartition(".")[0]
343            identifier = parent_module + identifier
344        else:
345            # Is this a local reference within this module?
346            for qualname in qualname_candidates(identifier, namespace):
347                doc = mod.get(qualname)
348                if doc and context["is_public"](doc).strip():
349                    return f'<a href="#{qualname}">{plain_text}</a>'
350
351        # Is this a reference pointing straight at a module?
352        if identifier in context["all_modules"]:
353            return f'<a href="{relative_link(context["module"].modulename, identifier)}">{identifier}</a>'
354
355        try:
356            sources = list(possible_sources(context["all_modules"], identifier))
357        except ValueError:
358            # possible_sources did not find a parent module.
359            return text
360
361        # Try to find the actual target object so that we can then later verify
362        # that objects exposed at a parent module with the same name point to it.
363        target_object = None
364        for module_name, qualname in sources:
365            if doc := context["all_modules"].get(module_name, {}).get(qualname):
366                target_object = doc.obj
367                break
368
369        # Look at the different modules where our target object may be exposed.
370        for module_name in module_candidates(identifier, mod.modulename):
371            module: pdoc.doc.Module | None = context["all_modules"].get(module_name)
372            if not module:
373                continue
374
375            for _, qualname in sources:
376                doc = module.get(qualname)
377                # Check if they have an object with the same name,
378                # and verify that it's pointing to the right thing and is public.
379                if (
380                    doc
381                    and (target_object is doc.obj or target_object is None)
382                    and context["is_public"](doc).strip()
383                ):
384                    if module == mod:
385                        url_text = qualname
386                    else:
387                        url_text = doc.fullname
388                    if plain_text.endswith("()"):
389                        url_text += "()"
390                    return f'<a href="{relative_link(context["module"].modulename, doc.modulename)}#{qualname}">{url_text}</a>'
391
392        # No matches found.
393        return text
394
395    return Markup(
396        re.sub(
397            r"""
398            # Part 1: foo.bar or foo.bar() (without backticks)
399            (?<![/=?#&\.])  # heuristic: not part of a URL
400            # First part of the identifier (e.g. "foo") - this is optional for relative references.
401            (?:
402                \b
403                (?!\d)[a-zA-Z0-9_]+
404                |
405                \.*  # We may also start with multiple dots.
406            )
407            # Rest of the identifier (e.g. ".bar" or "..bar")
408            (?:
409                # A single dot or a dot surrounded with pygments highlighting.
410                (?:\.|</span><span\ class="o">\.</span><span\ class="n">)
411                (?!\d)[a-zA-Z0-9_]+
412            )+
413            (?:\(\)|\b(?!\(\)))  # we either end on () or on a word boundary.
414            (?!</a>)  # not an existing link
415            (?![/#])  # heuristic: not part of a URL
416
417            | # Part 2: `foo` or `foo()`. `foo.bar` is already covered with part 1.
418            (?<=<code>)
419                 (?!\d)[a-zA-Z0-9_]+
420            (?:\(\))?
421            (?=</code>(?!</a>))
422            """,
423            linkify_repl,
424            code,
425            flags=re.VERBOSE,
426        )
427    )

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:
467def edit_url(
468    modulename: str, is_package: bool, mapping: Mapping[str, str]
469) -> str | None:
470    """Create a link to edit a particular file in the used version control system."""
471    for m, prefix in mapping.items():
472        if m == modulename or modulename.startswith(f"{m}."):
473            filename = modulename[len(m) + 1 :].replace(".", "/")
474            if is_package:
475                filename = f"{filename}/__init__.py".lstrip("/")
476            else:
477                filename += ".py"
478            return f"{prefix}{filename}"
479    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:
482def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None:
483    """
484    Return the name of the (unique) top-level module, or `None`
485    if no such module exists.
486
487    For example, assuming `foo`, `foo.bar`, and `foo.baz` are documented,
488    this function will return `foo`. If `foo` and `bar` are documented,
489    this function will return `None` as there is no unique top-level module.
490    """
491    shortest_name = min(all_modules, key=len, default=None)
492    prefix = f"{shortest_name}."
493    all_others_are_submodules = all(
494        x.startswith(prefix) or x == shortest_name for x in all_modules
495    )
496    if all_others_are_submodules:
497        return shortest_name
498    else:
499        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:
502def minify_css(css: str) -> str:
503    """Do some very basic CSS minification."""
504    css = re.sub(r"[ ]{4}|\n|(?<=[:{}]) | (?=[{}])", "", css)
505    css = re.sub(
506        r"/\*.+?\*/", lambda m: m.group(0) if m.group(0).startswith("/*!") else "", css
507    )
508    return Markup(css.replace("<style", "\n<style"))

Do some very basic CSS minification.

@contextmanager
def defuse_unsafe_reprs():
511@contextmanager
512def defuse_unsafe_reprs():
513    """This decorator is applied by pdoc before calling an object's repr().
514    It applies some heuristics to patch our sensitive information.
515    For example, `os.environ`'s default `__repr__` implementation exposes all
516    local secrets.
517    """
518    with patch.object(os._Environ, "__repr__", lambda self: "os.environ"):
519        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):
522class DefaultMacroExtension(ext.Extension):
523    """
524    This extension provides a new `{% defaultmacro %}` statement, which defines a macro only if it does not exist.
525
526    For example,
527
528    ```html+jinja
529    {% defaultmacro example() %}
530        test 123
531    {% enddefaultmacro %}
532    ```
533
534    is equivalent to
535
536    ```html+jinja
537    {% macro default_example() %}
538    test 123
539    {% endmacro %}
540    {% if not example %}
541        {% macro example() %}
542            test 123
543        {% endmacro %}
544    {% endif %}
545    ```
546
547    Additionally, the default implementation is also available as `default_$macroname`, which makes it possible
548    to reference it in the override.
549    """
550
551    tags = {"defaultmacro"}
552
553    def parse(self, parser):
554        m = nodes.Macro(lineno=next(parser.stream).lineno)
555        name = parser.parse_assign_target(name_only=True).name
556        m.name = f"default_{name}"
557        parser.parse_signature(m)
558        m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True)
559
560        if_stmt = nodes.If(
561            nodes.Not(nodes.Name(name, "load")),
562            [nodes.Macro(name, m.args, m.defaults, m.body)],
563            [],
564            [],
565        )
566        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.

tags = {'defaultmacro'}
def parse(self, parser):
553    def parse(self, parser):
554        m = nodes.Macro(lineno=next(parser.stream).lineno)
555        name = parser.parse_assign_target(name_only=True).name
556        m.name = f"default_{name}"
557        parser.parse_signature(m)
558        m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True)
559
560        if_stmt = nodes.If(
561            nodes.Not(nodes.Name(name, "load")),
562            [nodes.Macro(name, m.args, m.defaults, m.body)],
563            [],
564            [],
565        )
566        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.

identifier: ClassVar[str] = 'DefaultMacroExtension'
Inherited Members
jinja2.ext.Extension
Extension
priority
environment
bind
preprocess
filter_stream
attr
call_method