Skip to content

base.py

Base module for handlers.

This module contains the base classes for implementing collectors, renderers, and the combination of the two: handlers.

It also provides two methods:

  • get_handler, that will cache handlers into the HANDLERS_CACHE dictionary.
  • teardown, that will teardown all the cached handlers, and then clear the cache.

BaseCollector ¤

The base collector class.

Inherit from this class to implement a collector.

You will have to implement the collect method. You can also implement the teardown method.

collect(self, identifier, config) ¤

Collect data given an identifier and selection configuration.

In the implementation, you typically call a subprocess that returns JSON, and load that JSON again into a Python dictionary for example, though the implementation is completely free.

Parameters:

Name Type Description Default
identifier str

An identifier for which to collect data. For example, in Python, it would be 'mkdocstrings.handlers' to collect documentation about the handlers module. It can be anything that you can feed to the tool of your choice.

required
config dict

Configuration options for the tool you use to collect data. Typically called "selection" because these options modify how the objects or documentation are "selected" in the source code.

required

Returns:

Type Description
Any

Anything you want, as long as you can feed it to the renderer's render method.

Source code in handlers/base.py
@abstractmethod
def collect(self, identifier: str, config: dict) -> CollectorItem:
    """Collect data given an identifier and selection configuration.

    In the implementation, you typically call a subprocess that returns JSON, and load that JSON again into
    a Python dictionary for example, though the implementation is completely free.

    Arguments:
        identifier: An identifier for which to collect data. For example, in Python,
            it would be 'mkdocstrings.handlers' to collect documentation about the handlers module.
            It can be anything that you can feed to the tool of your choice.
        config: Configuration options for the tool you use to collect data. Typically called "selection" because
            these options modify how the objects or documentation are "selected" in the source code.

    Returns:
        Anything you want, as long as you can feed it to the renderer's `render` method.
    """  # noqa: DAR202 (excess return section)

teardown(self) ¤

Teardown the collector.

This method should be implemented to, for example, terminate a subprocess that was started when creating the collector instance.

Source code in handlers/base.py
def teardown(self) -> None:
    """Teardown the collector.

    This method should be implemented to, for example, terminate a subprocess
    that was started when creating the collector instance.
    """

BaseHandler ¤

The base handler class.

Inherit from this class to implement a handler.

It's usually just a combination of a collector and a renderer, but you can make it as complex as you need.

Attributes:

Name Type Description
domain str

The cross-documentation domain/language for this handler.

enable_inventory bool

Whether this handler is interested in enabling the creation of the objects.inv Sphinx inventory file.

__init__(self, collector, renderer) special ¤

Initialize the object.

Parameters:

Name Type Description Default
collector BaseCollector

A collector instance.

required
renderer BaseRenderer

A renderer instance.

required
Source code in handlers/base.py
def __init__(self, collector: BaseCollector, renderer: BaseRenderer) -> None:
    """Initialize the object.

    Arguments:
        collector: A collector instance.
        renderer: A renderer instance.
    """
    self.collector = collector
    self.renderer = renderer

BaseRenderer ¤

The base renderer class.

Inherit from this class to implement a renderer.

You will have to implement the render method. You can also override the update_env method, to add more filters to the Jinja environment, making them available in your Jinja templates.

To define a fallback theme, add a fallback_theme class-variable. To add custom CSS, add an extra_css variable or create an 'style.css' file beside the templates.

__init__(self, directory, theme, custom_templates=None) special ¤

Initialize the object.

If the given theme is not supported (it does not exist), it will look for a fallback_theme attribute in self to use as a fallback theme.

Parameters:

Name Type Description Default
directory str

The name of the directory containing the themes for this renderer.

required
theme str

The name of theme to use.

required
custom_templates Optional[str]

Directory containing custom templates.

None
Source code in handlers/base.py
def __init__(self, directory: str, theme: str, custom_templates: Optional[str] = None) -> None:
    """Initialize the object.

    If the given theme is not supported (it does not exist), it will look for a `fallback_theme` attribute
    in `self` to use as a fallback theme.

    Arguments:
        directory: The name of the directory containing the themes for this renderer.
        theme: The name of theme to use.
        custom_templates: Directory containing custom templates.
    """
    paths = []

    themes_dir = TEMPLATES_DIR / directory

    paths.append(themes_dir / theme)

    if self.fallback_theme:
        paths.append(themes_dir / self.fallback_theme)

    for path in paths:
        css_path = path / "style.css"
        if css_path.is_file():
            self.extra_css += "\n" + css_path.read_text(encoding="utf-8")  # noqa: WPS601
            break

    if custom_templates is not None:
        paths.insert(0, Path(custom_templates) / directory / theme)

    self.env = Environment(
        autoescape=True,
        loader=FileSystemLoader(paths),  # type: ignore
        auto_reload=False,  # Editing a template in the middle of a build is not useful.
    )  # type: ignore
    self.env.filters["any"] = do_any
    self.env.globals["log"] = get_template_logger()

    self._headings: List[Element] = []
    self._md: Markdown = None  # type: ignore  # To be populated in `update_env`.

do_convert_markdown(self, text, heading_level, html_id='') ¤

Render Markdown text; for use inside templates.

Parameters:

Name Type Description Default
text str

The text to convert.

required
heading_level int

The base heading level to start all Markdown headings from.

required
html_id str

The HTML id of the element that's considered the parent of this element.

''

Returns:

Type Description
Markup

An HTML string.

Source code in handlers/base.py
def do_convert_markdown(self, text: str, heading_level: int, html_id: str = "") -> Markup:
    """Render Markdown text; for use inside templates.

    Arguments:
        text: The text to convert.
        heading_level: The base heading level to start all Markdown headings from.
        html_id: The HTML id of the element that's considered the parent of this element.

    Returns:
        An HTML string.
    """
    treeprocessors = self._md.treeprocessors
    treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = heading_level
    treeprocessors[IdPrependingTreeprocessor.name].id_prefix = html_id and html_id + "--"
    try:
        return Markup(self._md.convert(text))
    finally:
        treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = 0
        treeprocessors[IdPrependingTreeprocessor.name].id_prefix = ""
        self._md.reset()

do_heading(self, content, heading_level, *, role=None, hidden=False, toc_label=None, **attributes) ¤

Render an HTML heading and register it for the table of contents. For use inside templates.

Parameters:

Name Type Description Default
content str

The HTML within the heading.

required
heading_level int

The level of heading (e.g. 3 -> h3).

required
role Optional[str]

An optional role for the object bound to this heading.

None
hidden bool

If True, only register it for the table of contents, don't render anything.

False
toc_label Optional[str]

The title to use in the table of contents ('data-toc-label' attribute).

None
attributes str

Any extra HTML attributes of the heading.

{}

Returns:

Type Description
Markup

An HTML string.

Source code in handlers/base.py
def do_heading(
    self,
    content: str,
    heading_level: int,
    *,
    role: Optional[str] = None,
    hidden: bool = False,
    toc_label: Optional[str] = None,
    **attributes: str,
) -> Markup:
    """Render an HTML heading and register it for the table of contents. For use inside templates.

    Arguments:
        content: The HTML within the heading.
        heading_level: The level of heading (e.g. 3 -> `h3`).
        role: An optional role for the object bound to this heading.
        hidden: If True, only register it for the table of contents, don't render anything.
        toc_label: The title to use in the table of contents ('data-toc-label' attribute).
        attributes: Any extra HTML attributes of the heading.

    Returns:
        An HTML string.
    """
    # First, produce the "fake" heading, for ToC only.
    el = Element(f"h{heading_level}", attributes)
    if toc_label is None:
        toc_label = content.unescape() if isinstance(el, Markup) else content  # type: ignore
    el.set("data-toc-label", toc_label)
    if role:
        el.set("data-role", role)
    self._headings.append(el)

    if hidden:
        return Markup('<a id="{0}"></a>').format(attributes["id"])

    # Now produce the actual HTML to be rendered. The goal is to wrap the HTML content into a heading.
    # Start with a heading that has just attributes (no text), and add a placeholder into it.
    el = Element(f"h{heading_level}", attributes)
    el.append(Element("mkdocstrings-placeholder"))
    # Tell the 'toc' extension to make its additions if configured so.
    toc = self._md.treeprocessors["toc"]
    if toc.use_anchors:
        toc.add_anchor(el, attributes["id"])
    if toc.use_permalinks:
        toc.add_permalink(el, attributes["id"])

    # The content we received is HTML, so it can't just be inserted into the tree. We had marked the middle
    # of the heading with a placeholder that can never occur (text can't directly contain angle brackets).
    # Now this HTML wrapper can be "filled" by replacing the placeholder.
    html_with_placeholder = tostring(el, encoding="unicode")
    assert (
        html_with_placeholder.count("<mkdocstrings-placeholder />") == 1
    ), f"Bug in mkdocstrings: failed to replace in {html_with_placeholder!r}"
    html = html_with_placeholder.replace("<mkdocstrings-placeholder />", content)
    return Markup(html)

get_anchor(self, data) ¤

Return the canonical identifier (HTML anchor) for a collected item.

This must match what the renderer would've actually rendered, e.g. if rendering the item contains <h2 id="foo">... then the return value should be "foo".

Parameters:

Name Type Description Default
data Any

The collected data.

required

Returns:

Type Description
Optional[str]

The HTML anchor (without '#') as a string, or None if this item doesn't have an anchor.

Source code in handlers/base.py
def get_anchor(self, data: CollectorItem) -> Optional[str]:
    """Return the canonical identifier (HTML anchor) for a collected item.

    This must match what the renderer would've actually rendered,
    e.g. if rendering the item contains `<h2 id="foo">...` then the return value should be "foo".

    Arguments:
        data: The collected data.

    Returns:
        The HTML anchor (without '#') as a string, or None if this item doesn't have an anchor.
    """  # noqa: DAR202 (excess return section)

get_headings(self) ¤

Return and clear the headings gathered so far.

Returns:

Type Description
Sequence[xml.etree.ElementTree.Element]

A list of HTML elements.

Source code in handlers/base.py
def get_headings(self) -> Sequence[Element]:
    """Return and clear the headings gathered so far.

    Returns:
        A list of HTML elements.
    """
    result = list(self._headings)
    self._headings.clear()
    return result

render(self, data, config) ¤

Render a template using provided data and configuration options.

Parameters:

Name Type Description Default
data Any

The collected data to render.

required
config dict

The rendering options.

required

Returns:

Type Description
str

The rendered template as HTML.

Source code in handlers/base.py
@abstractmethod
def render(self, data: CollectorItem, config: dict) -> str:
    """Render a template using provided data and configuration options.

    Arguments:
        data: The collected data to render.
        config: The rendering options.

    Returns:
        The rendered template as HTML.
    """  # noqa: DAR202 (excess return section)

update_env(self, md, config) ¤

Update the Jinja environment.

Parameters:

Name Type Description Default
md Markdown

The Markdown instance. Useful to add functions able to convert Markdown into the environment filters.

required
config dict

Configuration options for mkdocs and mkdocstrings, read from mkdocs.yml. See the source code of mkdocstrings.plugin.MkdocstringsPlugin.on_config to see what's in this dictionary.

required
Source code in handlers/base.py
def update_env(self, md: Markdown, config: dict) -> None:  # noqa: W0613 (unused argument 'config')
    """Update the Jinja environment.

    Arguments:
        md: The Markdown instance. Useful to add functions able to convert Markdown into the environment filters.
        config: Configuration options for `mkdocs` and `mkdocstrings`, read from `mkdocs.yml`. See the source code
            of [mkdocstrings.plugin.MkdocstringsPlugin.on_config][] to see what's in this dictionary.
    """
    self._md = md
    self.env.filters["highlight"] = Highlighter(md).highlight
    self.env.filters["convert_markdown"] = self.do_convert_markdown
    self.env.filters["heading"] = self.do_heading

CollectionError ¤

An exception raised when some collection of data failed.

Handlers ¤

A collection of handlers.

Do not instantiate this directly. The plugin will keep one instance of this for the purpose of caching. Use mkdocstrings.plugin.MkdocstringsPlugin.get_handler for convenient access.

seen_handlers: Iterable[mkdocstrings.handlers.base.BaseHandler] property readonly ¤

Get the handlers that were encountered so far throughout the build.

Returns:

Type Description
Iterable[mkdocstrings.handlers.base.BaseHandler]

An iterable of instances of BaseHandler (usable only to loop through it).

__init__(self, config) special ¤

Initialize the object.

Parameters:

Name Type Description Default
config dict

Configuration options for mkdocs and mkdocstrings, read from mkdocs.yml. See the source code of mkdocstrings.plugin.MkdocstringsPlugin.on_config to see what's in this dictionary.

required
Source code in handlers/base.py
def __init__(self, config: dict) -> None:
    """Initialize the object.

    Arguments:
        config: Configuration options for `mkdocs` and `mkdocstrings`, read from `mkdocs.yml`. See the source code
            of [mkdocstrings.plugin.MkdocstringsPlugin.on_config][] to see what's in this dictionary.
    """
    self._config = config
    self._handlers: Dict[str, BaseHandler] = {}
    self.inventory: Inventory = Inventory(project=self._config["site_name"])

get_anchor(self, identifier) ¤

Return the canonical HTML anchor for the identifier, if any of the seen handlers can collect it.

Parameters:

Name Type Description Default
identifier str

The identifier (one that collect can accept).

required

Returns:

Type Description
Optional[str]

A string - anchor without '#', or None if there isn't any identifier familiar with it.

Source code in handlers/base.py
def get_anchor(self, identifier: str) -> Optional[str]:
    """Return the canonical HTML anchor for the identifier, if any of the seen handlers can collect it.

    Arguments:
        identifier: The identifier (one that [collect][mkdocstrings.handlers.base.BaseCollector.collect] can accept).

    Returns:
        A string - anchor without '#', or None if there isn't any identifier familiar with it.
    """
    for handler in self._handlers.values():
        try:
            anchor = handler.renderer.get_anchor(handler.collector.collect(identifier, {}))
        except CollectionError:
            continue
        if anchor is not None:
            return anchor
    return None

get_handler(self, name, handler_config=None) ¤

Get a handler thanks to its name.

This function dynamically imports a module named "mkdocstrings.handlers.NAME", calls its get_handler method to get an instance of a handler, and caches it in dictionary. It means that during one run (for each reload when serving, or once when building), a handler is instantiated only once, and reused for each "autodoc" instruction asking for it.

Parameters:

Name Type Description Default
name str

The name of the handler. Really, it's the name of the Python module holding it.

required
handler_config Optional[dict]

Configuration passed to the handler.

None

Returns:

Type Description
BaseHandler

An instance of a subclass of BaseHandler, as instantiated by the get_handler method of the handler's module.

Source code in handlers/base.py
def get_handler(self, name: str, handler_config: Optional[dict] = None) -> BaseHandler:
    """Get a handler thanks to its name.

    This function dynamically imports a module named "mkdocstrings.handlers.NAME", calls its
    `get_handler` method to get an instance of a handler, and caches it in dictionary.
    It means that during one run (for each reload when serving, or once when building),
    a handler is instantiated only once, and reused for each "autodoc" instruction asking for it.

    Arguments:
        name: The name of the handler. Really, it's the name of the Python module holding it.
        handler_config: Configuration passed to the handler.

    Returns:
        An instance of a subclass of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler],
        as instantiated by the `get_handler` method of the handler's module.
    """
    if name not in self._handlers:
        if handler_config is None:
            handler_config = self.get_handler_config(name)
        module = importlib.import_module(f"mkdocstrings.handlers.{name}")
        self._handlers[name] = module.get_handler(  # type: ignore
            self._config["theme_name"],
            self._config["mkdocstrings"]["custom_templates"],
            **handler_config,
        )  # type: ignore
    return self._handlers[name]

get_handler_config(self, name) ¤

Return the global configuration of the given handler.

Parameters:

Name Type Description Default
name str

The name of the handler to get the global configuration of.

required

Returns:

Type Description
dict

The global configuration of the given handler. It can be an empty dictionary.

Source code in handlers/base.py
def get_handler_config(self, name: str) -> dict:
    """Return the global configuration of the given handler.

    Arguments:
        name: The name of the handler to get the global configuration of.

    Returns:
        The global configuration of the given handler. It can be an empty dictionary.
    """
    handlers = self._config["mkdocstrings"].get("handlers", {})
    if handlers:
        return handlers.get(name, {})
    return {}

get_handler_name(self, config) ¤

Return the handler name defined in an "autodoc" instruction YAML configuration, or the global default handler.

Parameters:

Name Type Description Default
config dict

A configuration dictionary, obtained from YAML below the "autodoc" instruction.

required

Returns:

Type Description
str

The name of the handler to use.

Source code in handlers/base.py
def get_handler_name(self, config: dict) -> str:
    """Return the handler name defined in an "autodoc" instruction YAML configuration, or the global default handler.

    Arguments:
        config: A configuration dictionary, obtained from YAML below the "autodoc" instruction.

    Returns:
        The name of the handler to use.
    """
    global_config = self._config["mkdocstrings"]
    if "handler" in config:
        return config["handler"]
    return global_config["default_handler"]

teardown(self) ¤

Teardown all cached handlers and clear the cache.

Source code in handlers/base.py
def teardown(self) -> None:
    """Teardown all cached handlers and clear the cache."""
    for handler in self.seen_handlers:
        handler.collector.teardown()
    self._handlers.clear()

ThemeNotSupported ¤

An exception raised to tell a theme is not supported.

do_any(seq, attribute=None) ¤

Check if at least one of the item in the sequence evaluates to true.

The any builtin as a filter for Jinja templates.

Parameters:

Name Type Description Default
seq Sequence

An iterable object.

required
attribute str

The attribute name to use on each object of the iterable.

None

Returns:

Type Description
bool

A boolean telling if any object of the iterable evaluated to True.

Source code in handlers/base.py
def do_any(seq: Sequence, attribute: str = None) -> bool:
    """Check if at least one of the item in the sequence evaluates to true.

    The `any` builtin as a filter for Jinja templates.

    Arguments:
        seq: An iterable object.
        attribute: The attribute name to use on each object of the iterable.

    Returns:
        A boolean telling if any object of the iterable evaluated to True.
    """
    if attribute is None:
        return any(seq)
    return any(_[attribute] for _ in seq)
Back to top