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: