From 8272200f077c8a6e58323e6967bbdd842b57f0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 2 Apr 2022 16:57:08 +0200 Subject: [PATCH] refactor: Deprecate BaseCollector and BaseRenderer The BaseCollector and BaseRenderer are merged into the BaseHandler (as mixins for now). Developers are still able to create collectors and renderers using these deprecated base classes, and pass instances of them when creating their handler. All the methods of the deprecated bases can now be defined on the BaseHandler subclass itself. Handlers can then be instantiated by passing the handler's name, the theme and the optional custom templates folder name/path. Reasoning: often times, the renderer and the collector need to communicate or share data. For example, the Crystal renderer uses the collector to lookup names when creating cross-references. The Python handlers are able to filter members when collecting/returning data, and need the same members list when rendering, to order the elements based on that list. This change is the first of two, where the second change will deprecate the use of `selection` and `rendering` keys in the YAML options or MkDocs configuration, in favor of a single `options` key that both the collection and rendering process will share. --- src/mkdocstrings/extension.py | 8 +- src/mkdocstrings/handlers/base.py | 150 +++++++++++++++++++++++++----- src/mkdocstrings/plugin.py | 2 +- tests/test_extension.py | 4 +- 4 files changed, 132 insertions(+), 32 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 60efaade..3b0e54fb 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -122,7 +122,7 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: # The final HTML is inserted as opaque to subsequent processing, and only revealed at the end. el.text = self.md.htmlStash.store(html) # So we need to duplicate the headings directly (and delete later), just so 'toc' can pick them up. - headings = handler.renderer.get_headings() + headings = handler.get_headings() el.extend(headings) page = self._autorefs.current_page @@ -179,7 +179,7 @@ def _process_block( log.debug("Collecting data") try: - data: CollectorItem = handler.collector.collect(identifier, selection) + data: CollectorItem = handler.collect(identifier, selection) except CollectionError as exception: log.error(str(exception)) if PluginError is SystemExit: # When MkDocs 1.2 is sufficiently common, this can be dropped. @@ -188,12 +188,12 @@ def _process_block( if not self._updated_env: log.debug("Updating renderer's env") - handler.renderer._update_env(self.md, self._config) # noqa: WPS437 (protected member OK) + handler._update_env(self.md, self._config) # noqa: WPS437 (protected member OK) self._updated_env = True log.debug("Rendering templates") try: - rendered = handler.renderer.render(data, rendering) + rendered = handler.render(data, rendering) except TemplateNotFound as exc: theme_name = self._config["theme_name"] log.error( diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 51e74bab..3bdf2336 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -8,9 +8,10 @@ - `teardown`, that will teardown all the cached handlers, and then clear the cache. """ +from __future__ import annotations + import importlib import warnings -from abc import ABC, abstractmethod from contextlib import suppress from pathlib import Path from typing import Any, Dict, Iterable, List, Optional, Sequence @@ -58,7 +59,7 @@ def do_any(seq: Sequence, attribute: str = None) -> bool: return any(_[attribute] for _ in seq) -class BaseRenderer(ABC): +class BaseRenderer: """The base renderer class. Inherit from this class to implement a renderer. @@ -74,7 +75,7 @@ class BaseRenderer(ABC): fallback_theme: str = "" extra_css = "" - def __init__(self, handler: str, theme: str, custom_templates: Optional[str] = None, directory: str = None) -> None: + def __init__(self, handler: 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 @@ -84,19 +85,14 @@ def __init__(self, handler: str, theme: str, custom_templates: Optional[str] = N handler: The name of the handler. theme: The name of theme to use. custom_templates: Directory containing custom templates. - directory: Deprecated and renamed as `handler`. """ - # TODO: remove at some point - if directory: - warnings.warn( - "The 'directory' keyword parameter is deprecated and renamed 'handler'. ", - DeprecationWarning, - ) - if not handler: - handler = directory - paths = [] + # TODO: remove once BaseRenderer is merged into BaseHandler + self._handler = handler + self._theme = theme + self._custom_templates = custom_templates + themes_dir = self.get_templates_dir(handler) paths.append(themes_dir / theme) @@ -123,7 +119,6 @@ def __init__(self, handler: str, theme: str, custom_templates: Optional[str] = N self._headings: List[Element] = [] self._md: Markdown = None # type: ignore # To be populated in `update_env`. - @abstractmethod def render(self, data: CollectorItem, config: dict) -> str: """Render a template using provided data and configuration options. @@ -313,7 +308,7 @@ def _update_env(self, md: Markdown, config: dict): self.update_env(new_md, config) -class BaseCollector(ABC): +class BaseCollector: """The base collector class. Inherit from this class to implement a collector. @@ -322,7 +317,6 @@ class BaseCollector(ABC): You can also implement the `teardown` method. """ - @abstractmethod def collect(self, identifier: str, config: dict) -> CollectorItem: """Collect data given an identifier and selection configuration. @@ -348,7 +342,7 @@ def teardown(self) -> None: """ -class BaseHandler: +class BaseHandler(BaseCollector, BaseRenderer): """The base handler class. Inherit from this class to implement a handler. @@ -359,20 +353,126 @@ class BaseHandler: domain: The cross-documentation domain/language for this handler. enable_inventory: Whether this handler is interested in enabling the creation of the `objects.inv` Sphinx inventory file. + fallback_config: The configuration used to collect item during autorefs fallback. """ domain: str = "default" enable_inventory: bool = False + fallback_config: dict = {} - def __init__(self, collector: BaseCollector, renderer: BaseRenderer) -> None: + # TODO: once the BaseCollector and BaseRenderer classes are removed, + # stop accepting the 'handler' parameter, and instead set a 'name' attribute on the Handler class. + # Then make the 'handler' parameter in 'get_templates_dir' optional, and use the class 'name' by default. + def __init__(self, *args: str | BaseCollector | BaseRenderer, **kwargs: str | BaseCollector | BaseRenderer) -> None: """Initialize the object. Arguments: - collector: A collector instance. - renderer: A renderer instance. + *args: Collector and renderer, or handler name, theme and custom_templates. + **kwargs: Same thing, but with keyword arguments. + + Raises: + ValueError: When the givin parameters are invalid. """ - self.collector = collector - self.renderer = renderer + # The method accepts *args and **kwargs temporarily, + # to support the transition period where the BaseCollector + # and BaseRenderer are deprecated, and the BaseHandler + # can be instantiated with both instances of collector/renderer, + # or renderer parameters, as positional parameters. + # Supported: + # handler = Handler(collector, renderer) + # handler = Handler(collector=collector, renderer=renderer) + # handler = Handler("python", "material") + # handler = Handler("python", "material", "templates") + # handler = Handler(handler="python", theme="material") + # handler = Handler(handler="python", theme="material", custom_templates="templates") + # Invalid: + # handler = Handler("python", "material", collector, renderer) + # handler = Handler("python", theme="material", collector=collector) + # handler = Handler(collector, renderer, "material") + # handler = Handler(collector, renderer, theme="material") + # handler = Handler(collector) + # handler = Handler(renderer) + # etc. + + collector = None + renderer = None + + # parsing positional arguments + str_args = [] + for arg in args: + if isinstance(arg, BaseCollector): + collector = arg + elif isinstance(arg, BaseRenderer): + renderer = arg + elif isinstance(arg, str): + str_args.append(arg) + + while len(str_args) != 3: + str_args.append(None) # type: ignore[arg-type] + + handler, theme, custom_templates = str_args + + # fetching values from keyword arguments + if "collector" in kwargs: + collector = kwargs.pop("collector") # type: ignore[assignment] + if "renderer" in kwargs: + renderer = kwargs.pop("renderer") # type: ignore[assignment] + if "handler" in kwargs: + handler = kwargs.pop("handler") # type: ignore[assignment] + if "theme" in kwargs: + theme = kwargs.pop("theme") # type: ignore[assignment] + if "custom_templates" in kwargs: + custom_templates = kwargs.pop("custom_templates") # type: ignore[assignment] + + if collector is None and renderer is not None or collector is not None and renderer is None: + raise ValueError("both 'collector' and 'renderer' must be provided") + + if collector is not None: + warnings.warn( + DeprecationWarning( + "The BaseCollector class is deprecated, and passing an instance of it " + "to your handler is deprecated as well. Instead, define the `collect` and `teardown` " + "methods directly on your handler class." + ) + ) + self.collector = collector + self.collect = collector.collect # type: ignore[assignment] + self.teardown = collector.teardown # type: ignore[assignment] + + if renderer is not None: + if {handler, theme, custom_templates} != {None}: + raise ValueError( + "'handler', 'theme' and 'custom_templates' must all be None when providing a renderer instance" + ) + warnings.warn( + DeprecationWarning( + "The BaseRenderer class is deprecated, and passing an instance of it " + "to your handler is deprecated as well. Instead, define the `render` method" + "directly on your handler class (as well as other methods and attributes like " + "`get_templates_dir`, `get_anchors`, `update_env` and `fallback_theme`, `extra_css`)." + ) + ) + self.renderer = renderer + self.render = renderer.render # type: ignore[assignment] + self.get_templates_dir = renderer.get_templates_dir # type: ignore[assignment] + self.get_anchors = renderer.get_anchors # type: ignore[assignment] + self.do_convert_markdown = renderer.do_convert_markdown # type: ignore[assignment] + self.do_heading = renderer.do_heading # type: ignore[assignment] + self.get_headings = renderer.get_headings # type: ignore[assignment] + self.update_env = renderer.update_env # type: ignore[assignment] + self._update_env = renderer._update_env # type: ignore[assignment] # noqa: WPS437 + self.fallback_theme = renderer.fallback_theme + self.extra_css = renderer.extra_css + renderer.__class__.__init__( # noqa: WPS609 + self, + renderer._handler, # noqa: WPS437 + renderer._theme, # noqa: WPS437 + renderer._custom_templates, # noqa: WPS437 + ) + else: + if handler is None or theme is None: + raise ValueError("'handler' and 'theme' cannot be None") + BaseRenderer.__init__(self, handler, theme, custom_templates) # noqa: WPS609 class Handlers: @@ -403,9 +503,9 @@ def get_anchors(self, identifier: str) -> Sequence[str]: A tuple of strings - anchors without '#', or an empty tuple if there isn't any identifier familiar with it. """ for handler in self._handlers.values(): - fallback_config = getattr(handler.collector, "fallback_config", {}) + fallback_config = getattr(handler, "fallback_config", {}) try: - anchors = handler.renderer.get_anchors(handler.collector.collect(identifier, fallback_config)) + anchors = handler.get_anchors(handler.collect(identifier, fallback_config)) except CollectionError: continue if anchors: @@ -490,5 +590,5 @@ def seen_handlers(self) -> Iterable[BaseHandler]: def teardown(self) -> None: """Teardown all cached handlers and clear the cache.""" for handler in self.seen_handlers: - handler.collector.teardown() + handler.teardown() self._handlers.clear() diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index a30c7b10..4f2fdda6 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -224,7 +224,7 @@ def on_env(self, env, config: Config, **kwargs): - Gather results from background inventory download tasks. """ if self._handlers: - css_content = "\n".join(handler.renderer.extra_css for handler in self.handlers.seen_handlers) + css_content = "\n".join(handler.extra_css for handler in self.handlers.seen_handlers) write_file(css_content.encode("utf-8"), os.path.join(config["site_dir"], self.css_filename)) if self.inventory_enabled: diff --git a/tests/test_extension.py b/tests/test_extension.py index 69e9aaef..8039e240 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -127,9 +127,9 @@ def test_use_custom_handler(ext_markdown): def test_dont_register_every_identifier_as_anchor(plugin): """Assert that we don't preemptively register all identifiers of a rendered object.""" - renderer = plugin._handlers.get_handler("python").renderer # noqa: WPS437 + handler = plugin._handlers.get_handler("python") # noqa: WPS437 ids = {"id1", "id2", "id3"} - renderer.get_anchors = lambda _: ids + handler.get_anchors = lambda _: ids plugin.md.convert("::: tests.fixtures.headings") autorefs = plugin.md.parser.blockprocessors["mkdocstrings"]._autorefs # noqa: WPS219,WPS437 for identifier in ids: