Skip to content

Commit

Permalink
refactor: Split out the handler cache, expose it through the plugin
Browse files Browse the repository at this point in the history
This makes `handlers_cache` no longer be global but instead be confined to the Plugin. There will be only one instance of the plugin so it doesn't matter anyway. But actually this is also more correct, because what if someone tried to instantiate multiple handlers with different configs? It would work incorrectly previously.

But my main goal for this is to expose `MkdocstringsPlugin.get_handler(name)`. Then someone can use this inside a mkdocs hook:

    def on_files(self, files: Files, config: Config):
        crystal = config['plugins']['mkdocstrings'].get_handler('python').collector

So this is basically a prerequisite for issue mkdocstrings#179: one could query the collector to know which files to generate.
  • Loading branch information
oprypin committed Dec 7, 2020
1 parent b58e444 commit 400c8a7
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 76 deletions.
50 changes: 9 additions & 41 deletions src/mkdocstrings/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from markdown.extensions import Extension
from markdown.util import AtomicString

from mkdocstrings.handlers.base import CollectionError, get_handler
from mkdocstrings.handlers.base import CollectionError, Handlers
from mkdocstrings.loggers import get_logger
from mkdocstrings.references import AutoRefInlineProcessor

Expand Down Expand Up @@ -95,7 +95,7 @@ class AutoDocProcessor(BlockProcessor):
classname = "autodoc"
regex = re.compile(r"^(?P<heading>#{1,6} *|)::: ?(?P<name>.+?) *$", flags=re.MULTILINE)

def __init__(self, parser: BlockParser, md: Markdown, config: dict) -> None:
def __init__(self, parser: BlockParser, md: Markdown, config: dict, handlers: Handlers) -> None:
"""
Initialize the object.
Expand All @@ -108,6 +108,7 @@ def __init__(self, parser: BlockParser, md: Markdown, config: dict) -> None:
super().__init__(parser=parser)
self.md = md
self._config = config
self._handlers = handlers

def test(self, parent: Element, block: Element) -> bool:
"""
Expand Down Expand Up @@ -180,16 +181,11 @@ def process_block(self, identifier: str, yaml_block: str, heading_level: int = 0
A new XML element.
"""
config = yaml.safe_load(yaml_block) or {}
handler_name = self.get_handler_name(config)
handler_name = self._handlers.get_handler_name(config)

log.debug(f"Using handler '{handler_name}'")
handler_config = self.get_handler_config(handler_name)
handler = get_handler(
handler_name,
self._config["theme_name"],
self._config["mkdocstrings"]["custom_templates"],
**handler_config,
)
handler_config = self._handlers.get_handler_config(handler_name)
handler = self._handlers.get_handler(handler_name, handler_config)

selection, rendering = get_item_configs(handler_config, config)
if heading_level:
Expand Down Expand Up @@ -224,35 +220,6 @@ def process_block(self, identifier: str, yaml_block: str, heading_level: int = 0

return atomic_brute_cast(xml_contents) # type: ignore

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.
"""
if "handler" in config:
return config["handler"]
return self._config["mkdocstrings"]["default_handler"]

def get_handler_config(self, handler_name: str) -> dict:
"""
Return the global configuration of the given handler.
Arguments:
handler_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(handler_name, {})
return {}


def get_item_configs(handler_config: dict, config: dict) -> Tuple[Mapping, Mapping]:
"""
Expand Down Expand Up @@ -307,7 +274,7 @@ class MkdocstringsExtension(Extension):
blockprocessor_priority = 75 # Right before markdown.blockprocessors.HashHeaderProcessor
inlineprocessor_priority = 168 # Right after markdown.inlinepatterns.ReferenceInlineProcessor

def __init__(self, config: dict, **kwargs) -> None:
def __init__(self, config: dict, handlers: Handlers, **kwargs) -> None:
"""
Initialize the object.
Expand All @@ -318,6 +285,7 @@ def __init__(self, config: dict, **kwargs) -> None:
"""
super().__init__(**kwargs)
self._config = config
self._handlers = handlers

def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name)
"""
Expand All @@ -329,7 +297,7 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me
md: A `markdown.Markdown` instance.
"""
md.registerExtension(self)
processor = AutoDocProcessor(md.parser, md, self._config)
processor = AutoDocProcessor(md.parser, md, self._config, self._handlers)
md.parser.blockprocessors.register(processor, "mkdocstrings", self.blockprocessor_priority)
ref_processor = AutoRefInlineProcessor(md)
md.inlinePatterns.register(ref_processor, "mkdocstrings", self.inlineprocessor_priority)
94 changes: 65 additions & 29 deletions src/mkdocstrings/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,38 +260,74 @@ def __init__(self, collector: BaseCollector, renderer: BaseRenderer) -> None:
self.renderer = renderer


def get_handler(
name: str,
theme: str,
custom_templates: Optional[str] = None,
**config: Any,
) -> BaseHandler:
class Handlers:
"""
Get a handler thanks to its name.
A collection of handlers.
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.
Do not instantiate this directly. [The plugin][mkdocstrings.plugin.MkdocstringsPlugin] will keep one instance of
this for the purpose of caching. Use [mkdocstrings.plugin.MkdocstringsPlugin.get_handler][] for convenient access.
"""

Arguments:
name: The name of the handler. Really, it's the name of the Python module holding it.
theme: The name of the theme to use.
custom_templates: Directory containing custom templates.
config: Configuration passed to the handler.
def __init__(self, config: dict):
self._config = config
self._handlers: Dict[str, BaseHandler] = {}

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 handlers_cache:
module = importlib.import_module(f"mkdocstrings.handlers.{name}")
handlers_cache[name] = module.get_handler(theme, custom_templates, **config) # type: ignore
return handlers_cache[name]
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.
"""
config = self._config["mkdocstrings"]
if "handler" in config:
return config["handler"]
return config["default_handler"]

def get_handler_config(self, name: str) -> dict:
"""
Return the global configuration of the given handler.
def teardown() -> None:
"""Teardown all cached handlers and clear the cache."""
for handler in handlers_cache.values():
handler.collector.teardown()
handlers_cache.clear()
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 {}

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}")
return module.get_handler(self._config["theme_name"], self._config["mkdocstrings"]["custom_templates"], **handler_config) # type: ignore
return self._handlers[name]

def teardown(self):
"""Teardown all cached handlers and clear the cache."""
for handler in self._handlers.values():
handler.collector.teardown()
self._handlers.clear()
25 changes: 19 additions & 6 deletions src/mkdocstrings/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
and fixes them using the previously stored identifier-URL mapping.
Once the documentation is built, the [`on_post_build` event hook](https://www.mkdocs.org/user-guide/plugins/#on_post_build)
is triggered and calls the [`handlers.teardown()` method][mkdocstrings.handlers.base.teardown]. This method is used
to teardown the handlers that were instantiated during documentation buildup.
is triggered and calls the [`handlers.teardown()` method][mkdocstrings.handlers.base.Handlers.teardown]. This method is
used to teardown the handlers that were instantiated during documentation buildup.
Finally, when serving the documentation, it can add directories to watch
during the [`on_serve` event hook](https://www.mkdocs.org/user-guide/plugins/#on_serve).
Expand All @@ -34,7 +34,7 @@
from mkdocs.structure.toc import AnchorLink

from mkdocstrings.extension import MkdocstringsExtension
from mkdocstrings.handlers.base import teardown
from mkdocstrings.handlers.base import BaseHandler, Handlers
from mkdocstrings.loggers import get_logger
from mkdocstrings.references import fix_refs

Expand Down Expand Up @@ -102,8 +102,8 @@ class MkdocstringsPlugin(BasePlugin):
def __init__(self) -> None:
"""Initialize the object."""
super().__init__()
self.mkdocstrings_extension: Optional[MkdocstringsExtension] = None
self.url_map: Dict[Any, str] = {}
self.handlers: Optional[Handlers] = None

def on_serve(self, server: Server, builder: Callable = None, **kwargs) -> Server: # noqa: W0613 (unused arguments)
"""
Expand Down Expand Up @@ -164,7 +164,8 @@ def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613 (unused
"mkdocstrings": self.config,
}

self.mkdocstrings_extension = MkdocstringsExtension(config=extension_config)
self.handlers = Handlers(extension_config)
self.mkdocstrings_extension = MkdocstringsExtension(extension_config, self.handlers)
config["markdown_extensions"].append(self.mkdocstrings_extension)
return config

Expand Down Expand Up @@ -255,4 +256,16 @@ def on_post_build(self, **kwargs) -> None: # noqa: W0613,R0201 (unused argument
kwargs: Additional arguments passed by MkDocs.
"""
log.debug("Tearing handlers down")
teardown()
self.handlers.teardown()

def get_handler(self, handler_name: str) -> BaseHandler:
"""
Get a handler by its name. See [mkdocstrings.handlers.base.Handlers.get_handler][].
Arguments:
handler_name: The name of the handler.
Returns:
An instance of a subclass of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler].
"""
return self.handlers.get_handler(handler_name)

0 comments on commit 400c8a7

Please sign in to comment.