Skip to content

Commit

Permalink
fixup! feat: Allow extensions to add templates
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed May 23, 2023
1 parent 414065a commit b050182
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 2 deletions.
78 changes: 77 additions & 1 deletion docs/usage/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ is written for inspiration.
### Templates

Your handler's implementation should normally be backed by templates, which go
to the directory `mkdocstrings_handlers/custom_handler/templates/some_theme`.
to the directory `mkdocstrings_handlers/custom_handler/templates/some_theme`
(`custom_handler` here should be replaced with the actual name of your handler,
and `some_theme` should be the name of an actual MkDocs theme that you support,
e.g. `material`).
Expand Down Expand Up @@ -258,3 +258,79 @@ plugins:
some_config_option: "b"
other_config_option: 1
```

## Handler extensions

*mkdocstrings* provides a way for third-party packages
to extend or alter the behavior of handlers.
For example, an extension of the Python handler
could add specific support for another Python library.

NOTE: This feature is intended for developers.
If you are a user and want to customize how objects are rendered,
see [Theming / Customization](../theming/#customization).

Such extensions can register additional template folders
that will be used when rendering collected data.
Extensions are responsible for synchronizing
with the handler itself so that it uses the additional templates.

An extension is a Python package
that defines an entry-point for a specific handler:

```toml title="pyproject.toml"
[project.entry-points."mkdocstrings.python.templates"] # (1)!
extension-name = "extension_package:get_templates_path" # (2)!
```

1. Replace `python` by the name of the handler you want to add templates to.
1. Replace `extension-name` by any name you want,
and replace `extension_package:get_templates_path`
by the actual module path and function name in your package.

This entry-point assumes that the extension provides
a `get_templates_path` function directly under the `extension_package` package:

```tree
pyproject.toml
extension_package/
__init__.py
templates/
```

```python title="extension_package/__init__.py"
from pathlib import Path


def get_templates_path() -> Path:
return Path(__file__).parent / "templates"
```

This function doesn't accept any argument
and returns the path ([`pathlib.Path`][] or [`str`][])
to a directory containing templates.
The directory must contain one subfolder
for each supported theme, even if empty
(see "fallback theme" in [custom handlers templates](#templates_1)).
For example:

```tree
pyproject.toml
extension_package/
__init__.py
templates/
material/
readthedocs/
mkdocs/
```

*mkdocstrings* will add the folders corresponding to the user-selected theme,
and to the handler's defined fallback theme, as usual.

The names of the extension templates
must not overlap with the handler's original templates.

The extension is then responsible, in collaboration with its target handler,
for mutating the collected data in order to instruct the handler
to use one of the extension template when rendering particular objects.
See each handler's docs to see if they support extensions, and how.
60 changes: 59 additions & 1 deletion tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@

from __future__ import annotations

from contextlib import suppress
from pathlib import Path
from typing import TYPE_CHECKING

import pytest
from jinja2.exceptions import TemplateNotFound
from markdown import Markdown

from mkdocstrings.handlers.base import Highlighter
from mkdocstrings.handlers.base import BaseRenderer, Highlighter

if TYPE_CHECKING:
from mkdocstrings.plugin import MkdocstringsPlugin


@pytest.mark.parametrize("extension_name", ["codehilite", "pymdownx.highlight"])
Expand Down Expand Up @@ -43,3 +51,53 @@ def test_highlighter_basic(extension_name: str | None, inline: bool) -> None:
actual = hl.highlight("import foo", language="python", inline=inline)
assert "import" in actual
assert "import foo" not in actual # Highlighting has split it up.


@pytest.fixture(name="extended_templates")
def fixture_extended_templates(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: # noqa: D103
monkeypatch.setattr(BaseRenderer, "get_extended_templates_dirs", lambda self, handler: [tmp_path])
return tmp_path


def test_extended_templates(extended_templates: Path, plugin: MkdocstringsPlugin) -> None:
"""Test the extended templates functionality.
Parameters:
extended_templates: Temporary folder.
plugin: Instance of our plugin.
"""
handler = plugin._handlers.get_handler("python") # type: ignore[union-attr]

# assert mocked method added temp path to loader
search_paths = handler.env.loader.searchpath # type: ignore[union-attr]
assert any(str(extended_templates) in path for path in search_paths)

# assert "new" template is not found
for path in search_paths:
# TODO: use missing_ok=True once support for Python 3.7 is dropped
with suppress(FileNotFoundError):
Path(path).joinpath("new.html").unlink()
with pytest.raises(expected_exception=TemplateNotFound):
handler.env.get_template("new.html")

# check precedence: base theme, base fallback theme, extended theme, extended fallback theme
# start with last one and go back up
handler.env.cache = None

extended_fallback_theme = extended_templates.joinpath(handler.fallback_theme)
extended_fallback_theme.mkdir()
extended_fallback_theme.joinpath("new.html").write_text("extended fallback new")
assert handler.env.get_template("new.html").render() == "extended fallback new"

extended_theme = extended_templates.joinpath("mkdocs")
extended_theme.mkdir()
extended_theme.joinpath("new.html").write_text("extended new")
assert handler.env.get_template("new.html").render() == "extended new"

base_fallback_theme = Path(search_paths[1])
base_fallback_theme.joinpath("new.html").write_text("base fallback new")
assert handler.env.get_template("new.html").render() == "base fallback new"

base_theme = Path(search_paths[0])
base_theme.joinpath("new.html").write_text("base new")
assert handler.env.get_template("new.html").render() == "base new"

0 comments on commit b050182

Please sign in to comment.