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

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

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

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

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

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

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

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

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

@cache
def highlight(doc: pdoc.doc.Doc) -> str:
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))

Highlight the source code of a documentation object using pygments.

def format_signature(sig: inspect.Signature, colon: bool) -> str:
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)

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

@cache
def to_html(docstring: str) -> str:
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    )

Convert docstring from Markdown to HTML.

@pass_context
def to_markdown_with_context(context: jinja2.runtime.Context, docstring: str) -> str:
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)

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:
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)
def possible_sources( all_modules: collections.abc.Collection[str], identifier: str) -> collections.abc.Iterable[tuple[str, str]]:
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}")

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]:
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

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]:
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

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:
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    )

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:
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

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:
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

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:
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"))

Do some very basic CSS minification.

@contextmanager
def defuse_unsafe_reprs():
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

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):
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]

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):
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]

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