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
  7from functools import cache
  8import html
  9import inspect
 10import os
 11import re
 12from unittest.mock import patch
 13import warnings
 14
 15from jinja2 import ext
 16from jinja2 import nodes
 17import pygments.formatters
 18import pygments.lexers
 19
 20try:
 21    # Jinja2 >= 3.0
 22    from jinja2 import pass_context  # type: ignore
 23except ImportError:  # pragma: no cover
 24    from jinja2 import contextfilter as pass_context  # type: ignore
 25
 26from jinja2.runtime import Context
 27from markupsafe import Markup
 28
 29import pdoc.markdown2
 30
 31from . import docstrings
 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    "alerts": None,
 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(
310    context: Context, code: str, namespace: str = "", shorten: bool = True
311) -> str:
312    """
313    Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that
314    are not rendered at the moment will be ignored.
315    A piece of text is considered to be an identifier if it either contains a `.` or is surrounded by `<code>` tags.
316
317    If `shorten` is True, replace identifiers with short forms where possible.
318    For example, replace "current_module.Foo" with "Foo". This is useful for annotations
319    (which are verbose), but undesired for docstrings (where we want to preserve intent).
320    """
321
322    def linkify_repl(m: re.Match):
323        """
324        Resolve `text` to the most suitable documentation object.
325        """
326        text = m.group(0)
327        plain_text = text.replace(
328            '</span><span class="o">.</span><span class="n">', "."
329        )
330        identifier = plain_text.removesuffix("()")
331        mod: pdoc.doc.Module = context["module"]
332
333        # Check if this is a relative reference. These cannot be local and need to be resolved.
334        if identifier.startswith("."):
335            taken_from_mod = mod
336            if namespace and (ns := mod.get(namespace)):
337                # Imported from somewhere else, so the relative reference should be from the original module.
338                taken_from_mod = context["all_modules"].get(ns.taken_from[0], mod)
339            if taken_from_mod.is_package:
340                # If we are in __init__.py, we want `.foo` to refer to a child module.
341                parent_module = taken_from_mod.modulename
342            else:
343                # If we are in a leaf module, we want `.foo` to refer to the adjacent module.
344                parent_module = taken_from_mod.modulename.rpartition(".")[0]
345            while identifier.startswith(".."):
346                identifier = identifier[1:]
347                parent_module = parent_module.rpartition(".")[0]
348            identifier = parent_module + identifier
349        else:
350            # Is this a local reference within this module?
351            for qualname in qualname_candidates(identifier, namespace):
352                doc = mod.get(qualname)
353                if doc and context["is_public"](doc).strip():
354                    return f'<a href="#{qualname}">{plain_text}</a>'
355
356        # Is this a reference pointing straight at a module?
357        if identifier in context["all_modules"]:
358            return f'<a href="{relative_link(context["module"].modulename, identifier)}">{identifier}</a>'
359
360        try:
361            sources = list(possible_sources(context["all_modules"], identifier))
362        except ValueError:
363            # possible_sources did not find a parent module.
364            return text
365
366        # Try to find the actual target object so that we can then later verify
367        # that objects exposed at a parent module with the same name point to it.
368        target_object = None
369        for module_name, qualname in sources:
370            if doc := context["all_modules"].get(module_name, {}).get(qualname):
371                target_object = doc.obj
372                break
373
374        # Look at the different modules where our target object may be exposed.
375        for module_name in module_candidates(identifier, mod.modulename):
376            module: pdoc.doc.Module | None = context["all_modules"].get(module_name)
377            if not module:
378                continue
379
380            for _, qualname in sources:
381                doc = module.get(qualname)
382                # Check if they have an object with the same name,
383                # and verify that it's pointing to the right thing and is public.
384                if (
385                    doc
386                    and (target_object is doc.obj or target_object is None)
387                    and context["is_public"](doc).strip()
388                ):
389                    if shorten:
390                        if module == mod:
391                            url_text = qualname
392                        else:
393                            url_text = doc.fullname
394                        if plain_text.endswith("()"):
395                            url_text += "()"
396                    else:
397                        url_text = plain_text
398                    return f'<a href="{relative_link(context["module"].modulename, doc.modulename)}#{qualname}">{url_text}</a>'
399
400        # No matches found.
401        return text
402
403    return Markup(
404        re.sub(
405            r"""
406            # Part 1: foo.bar or foo.bar() (without backticks)
407            (?<![/=?#&\.])  # heuristic: not part of a URL
408            # First part of the identifier (e.g. "foo") - this is optional for relative references.
409            (?:
410                \b
411                (?!\d)[a-zA-Z0-9_]+
412                |
413                \.*  # We may also start with multiple dots.
414            )
415            # Rest of the identifier (e.g. ".bar" or "..bar")
416            (?:
417                # A single dot or a dot surrounded with pygments highlighting.
418                (?:\.|</span><span\ class="o">\.</span><span\ class="n">)
419                (?!\d)[a-zA-Z0-9_]+
420            )+
421            (?:\(\)|\b(?!\(\)))  # we either end on () or on a word boundary.
422            (?!</a>)  # not an existing link
423            (?![/#])  # heuristic: not part of a URL
424
425            | # Part 2: `foo` or `foo()`. `foo.bar` is already covered with part 1.
426            (?<=<code>)
427                 (?!\d)[a-zA-Z0-9_]+
428            (?:\(\))?
429            (?=</code>(?!</a>))
430            """,
431            linkify_repl,
432            code,
433            flags=re.VERBOSE,
434        )
435    )
436
437
438@pass_context
439def link(context: Context, spec: tuple[str, str], text: str | None = None) -> str:
440    """Create a link for a specific `(modulename, qualname)` tuple."""
441    mod: pdoc.doc.Module = context["module"]
442    modulename, qualname = spec
443
444    # Check if the object we are interested is also imported and re-exposed in the current namespace.
445    # https://github.com/mitmproxy/pdoc/issues/490: We need to do this for every level, not just the tail.
446    doc: pdoc.doc.Doc | None = mod
447    for part in qualname.split("."):
448        doc = doc.get(part) if isinstance(doc, pdoc.doc.Namespace) else None
449        if not (
450            doc
451            and doc.taken_from[0] == modulename
452            and context["is_public"](doc).strip()
453        ):
454            break
455    else:
456        # everything down to the tail is imported and re-exposed.
457        if text:
458            text = text.replace(f"{modulename}.", f"{mod.modulename}.")
459        modulename = mod.modulename
460
461    if mod.modulename == modulename:
462        fullname = qualname
463    else:
464        fullname = f"{modulename}.{qualname}".removesuffix(".")
465
466    if qualname:
467        qualname = f"#{qualname}"
468    if modulename in context["all_modules"]:
469        return Markup(
470            f'<a href="{relative_link(context["module"].modulename, modulename)}{qualname}">{text or fullname}</a>'
471        )
472    return text or fullname
473
474
475def edit_url(
476    modulename: str, is_package: bool, mapping: Mapping[str, str]
477) -> str | None:
478    """Create a link to edit a particular file in the used version control system."""
479    for m, prefix in mapping.items():
480        if m == modulename or modulename.startswith(f"{m}."):
481            filename = modulename[len(m) + 1 :].replace(".", "/")
482            if is_package:
483                filename = f"{filename}/__init__.py".lstrip("/")
484            else:
485                filename += ".py"
486            return f"{prefix}{filename}"
487    return None
488
489
490def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None:
491    """
492    Return the name of the (unique) top-level module, or `None`
493    if no such module exists.
494
495    For example, assuming `foo`, `foo.bar`, and `foo.baz` are documented,
496    this function will return `foo`. If `foo` and `bar` are documented,
497    this function will return `None` as there is no unique top-level module.
498    """
499    shortest_name = min(all_modules, key=len, default=None)
500    prefix = f"{shortest_name}."
501    all_others_are_submodules = all(
502        x.startswith(prefix) or x == shortest_name for x in all_modules
503    )
504    if all_others_are_submodules:
505        return shortest_name
506    else:
507        return None
508
509
510def minify_css(css: str) -> str:
511    """Do some very basic CSS minification."""
512    css = re.sub(r"[ ]{4}|\n|(?<=[:{}]) | (?=[{}])", "", css)
513    css = re.sub(
514        r"/\*.+?\*/", lambda m: m.group(0) if m.group(0).startswith("/*!") else "", css
515    )
516    return Markup(css.replace("<style", "\n<style"))
517
518
519@contextmanager
520def defuse_unsafe_reprs():
521    """This decorator is applied by pdoc before calling an object's repr().
522    It applies some heuristics to patch our sensitive information.
523    For example, `os.environ`'s default `__repr__` implementation exposes all
524    local secrets.
525    """
526    with patch.object(os._Environ, "__repr__", lambda self: "os.environ"):
527        yield
528
529
530class DefaultMacroExtension(ext.Extension):
531    """
532    This extension provides a new `{% defaultmacro %}` statement, which defines a macro only if it does not exist.
533
534    For example,
535
536    ```html+jinja
537    {% defaultmacro example() %}
538        test 123
539    {% enddefaultmacro %}
540    ```
541
542    is equivalent to
543
544    ```html+jinja
545    {% macro default_example() %}
546    test 123
547    {% endmacro %}
548    {% if not example %}
549        {% macro example() %}
550            test 123
551        {% endmacro %}
552    {% endif %}
553    ```
554
555    Additionally, the default implementation is also available as `default_$macroname`, which makes it possible
556    to reference it in the override.
557    """
558
559    tags = {"defaultmacro"}
560
561    def parse(self, parser):
562        m = nodes.Macro(lineno=next(parser.stream).lineno)
563        name = parser.parse_assign_target(name_only=True).name
564        m.name = f"default_{name}"
565        parser.parse_signature(m)
566        m.body = parser.parse_statements(("name:enddefaultmacro",), drop_needle=True)
567
568        if_stmt = nodes.If(
569            nodes.Not(nodes.Name(name, "load")),
570            [nodes.Macro(name, m.args, m.defaults, m.body)],
571            [],
572            [],
573        )
574        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 = {'alerts': None, '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: Collection[str], identifier: str) -> 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: 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, 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]:
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) -> 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 = '', shorten: bool = True) -> str:
309@pass_context
310def linkify(
311    context: Context, code: str, namespace: str = "", shorten: bool = True
312) -> str:
313    """
314    Link all identifiers in a block of text. Identifiers referencing unknown modules or modules that
315    are not rendered at the moment will be ignored.
316    A piece of text is considered to be an identifier if it either contains a `.` or is surrounded by `<code>` tags.
317
318    If `shorten` is True, replace identifiers with short forms where possible.
319    For example, replace "current_module.Foo" with "Foo". This is useful for annotations
320    (which are verbose), but undesired for docstrings (where we want to preserve intent).
321    """
322
323    def linkify_repl(m: re.Match):
324        """
325        Resolve `text` to the most suitable documentation object.
326        """
327        text = m.group(0)
328        plain_text = text.replace(
329            '</span><span class="o">.</span><span class="n">', "."
330        )
331        identifier = plain_text.removesuffix("()")
332        mod: pdoc.doc.Module = context["module"]
333
334        # Check if this is a relative reference. These cannot be local and need to be resolved.
335        if identifier.startswith("."):
336            taken_from_mod = mod
337            if namespace and (ns := mod.get(namespace)):
338                # Imported from somewhere else, so the relative reference should be from the original module.
339                taken_from_mod = context["all_modules"].get(ns.taken_from[0], mod)
340            if taken_from_mod.is_package:
341                # If we are in __init__.py, we want `.foo` to refer to a child module.
342                parent_module = taken_from_mod.modulename
343            else:
344                # If we are in a leaf module, we want `.foo` to refer to the adjacent module.
345                parent_module = taken_from_mod.modulename.rpartition(".")[0]
346            while identifier.startswith(".."):
347                identifier = identifier[1:]
348                parent_module = parent_module.rpartition(".")[0]
349            identifier = parent_module + identifier
350        else:
351            # Is this a local reference within this module?
352            for qualname in qualname_candidates(identifier, namespace):
353                doc = mod.get(qualname)
354                if doc and context["is_public"](doc).strip():
355                    return f'<a href="#{qualname}">{plain_text}</a>'
356
357        # Is this a reference pointing straight at a module?
358        if identifier in context["all_modules"]:
359            return f'<a href="{relative_link(context["module"].modulename, identifier)}">{identifier}</a>'
360
361        try:
362            sources = list(possible_sources(context["all_modules"], identifier))
363        except ValueError:
364            # possible_sources did not find a parent module.
365            return text
366
367        # Try to find the actual target object so that we can then later verify
368        # that objects exposed at a parent module with the same name point to it.
369        target_object = None
370        for module_name, qualname in sources:
371            if doc := context["all_modules"].get(module_name, {}).get(qualname):
372                target_object = doc.obj
373                break
374
375        # Look at the different modules where our target object may be exposed.
376        for module_name in module_candidates(identifier, mod.modulename):
377            module: pdoc.doc.Module | None = context["all_modules"].get(module_name)
378            if not module:
379                continue
380
381            for _, qualname in sources:
382                doc = module.get(qualname)
383                # Check if they have an object with the same name,
384                # and verify that it's pointing to the right thing and is public.
385                if (
386                    doc
387                    and (target_object is doc.obj or target_object is None)
388                    and context["is_public"](doc).strip()
389                ):
390                    if shorten:
391                        if module == mod:
392                            url_text = qualname
393                        else:
394                            url_text = doc.fullname
395                        if plain_text.endswith("()"):
396                            url_text += "()"
397                    else:
398                        url_text = plain_text
399                    return f'<a href="{relative_link(context["module"].modulename, doc.modulename)}#{qualname}">{url_text}</a>'
400
401        # No matches found.
402        return text
403
404    return Markup(
405        re.sub(
406            r"""
407            # Part 1: foo.bar or foo.bar() (without backticks)
408            (?<![/=?#&\.])  # heuristic: not part of a URL
409            # First part of the identifier (e.g. "foo") - this is optional for relative references.
410            (?:
411                \b
412                (?!\d)[a-zA-Z0-9_]+
413                |
414                \.*  # We may also start with multiple dots.
415            )
416            # Rest of the identifier (e.g. ".bar" or "..bar")
417            (?:
418                # A single dot or a dot surrounded with pygments highlighting.
419                (?:\.|</span><span\ class="o">\.</span><span\ class="n">)
420                (?!\d)[a-zA-Z0-9_]+
421            )+
422            (?:\(\)|\b(?!\(\)))  # we either end on () or on a word boundary.
423            (?!</a>)  # not an existing link
424            (?![/#])  # heuristic: not part of a URL
425
426            | # Part 2: `foo` or `foo()`. `foo.bar` is already covered with part 1.
427            (?<=<code>)
428                 (?!\d)[a-zA-Z0-9_]+
429            (?:\(\))?
430            (?=</code>(?!</a>))
431            """,
432            linkify_repl,
433            code,
434            flags=re.VERBOSE,
435        )
436    )

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.

If shorten is True, replace identifiers with short forms where possible. For example, replace "current_module.Foo" with "Foo". This is useful for annotations (which are verbose), but undesired for docstrings (where we want to preserve intent).

def edit_url( modulename: str, is_package: bool, mapping: Mapping[str, str]) -> str | None:
476def edit_url(
477    modulename: str, is_package: bool, mapping: Mapping[str, str]
478) -> str | None:
479    """Create a link to edit a particular file in the used version control system."""
480    for m, prefix in mapping.items():
481        if m == modulename or modulename.startswith(f"{m}."):
482            filename = modulename[len(m) + 1 :].replace(".", "/")
483            if is_package:
484                filename = f"{filename}/__init__.py".lstrip("/")
485            else:
486                filename += ".py"
487            return f"{prefix}{filename}"
488    return None

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

def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None:
491def root_module_name(all_modules: Mapping[str, pdoc.doc.Module]) -> str | None:
492    """
493    Return the name of the (unique) top-level module, or `None`
494    if no such module exists.
495
496    For example, assuming `foo`, `foo.bar`, and `foo.baz` are documented,
497    this function will return `foo`. If `foo` and `bar` are documented,
498    this function will return `None` as there is no unique top-level module.
499    """
500    shortest_name = min(all_modules, key=len, default=None)
501    prefix = f"{shortest_name}."
502    all_others_are_submodules = all(
503        x.startswith(prefix) or x == shortest_name for x in all_modules
504    )
505    if all_others_are_submodules:
506        return shortest_name
507    else:
508        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:
511def minify_css(css: str) -> str:
512    """Do some very basic CSS minification."""
513    css = re.sub(r"[ ]{4}|\n|(?<=[:{}]) | (?=[{}])", "", css)
514    css = re.sub(
515        r"/\*.+?\*/", lambda m: m.group(0) if m.group(0).startswith("/*!") else "", css
516    )
517    return Markup(css.replace("<style", "\n<style"))

Do some very basic CSS minification.

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