From 772644a1df1da29df7af3ef8a282783b91ed6d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 4 Apr 2023 11:49:14 +0200 Subject: [PATCH] wip --- config/ruff.toml | 1 + docs/recipes.md | 1 + docs/troubleshooting.md | 12 +++- src/mkdocstrings/extension.py | 29 ++++---- src/mkdocstrings/handlers/base.py | 91 +++++++++++++------------- src/mkdocstrings/handlers/rendering.py | 15 +++-- src/mkdocstrings/inventory.py | 12 +++- src/mkdocstrings/loggers.py | 4 +- src/mkdocstrings/plugin.py | 38 ++++++----- tests/conftest.py | 24 ++++--- tests/test_extension.py | 53 ++++++++------- tests/test_handlers.py | 6 +- tests/test_inventory.py | 6 +- tests/test_plugin.py | 8 ++- 14 files changed, 174 insertions(+), 126 deletions(-) diff --git a/config/ruff.toml b/config/ruff.toml index 1907e7ed..62f45f64 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -64,6 +64,7 @@ ignore = [ "D417", # Missing argument description in the docstring "E501", # Line too long "G004", # Logging statement uses f-string + "INP001", # File is part of an implicit namespace package "PLR0911", # Too many return statements "PLR0912", # Too many branches "PLR0913", # Too many arguments to function call diff --git a/docs/recipes.md b/docs/recipes.md index 67448ac7..c33130f0 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -321,6 +321,7 @@ and add global CSS rules to your site using MkDocs `extra_css` option: ```pycon >>> for word in ("Hello", "mkdocstrings!"): ... print(word, end=" ") +... Hello mkdocstrings! ``` ```` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3f2243fd..1b360ffb 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -151,7 +151,7 @@ Example: ```python def math_function(x, y): r""" - Look at these formulas: + Look at these formulas: ```math f(x) = \int_{-\infty}^\infty @@ -170,6 +170,7 @@ So instead of: ```python import enum + class MyEnum(enum.Enum): v1 = 1 #: The first choice. v2 = 2 #: The second choice. @@ -180,13 +181,15 @@ You can use: ```python import enum + class MyEnum(enum.Enum): """My enum. - + Attributes: v1: The first choice. v2: The second choice. """ + v1 = 1 v2 = 2 ``` @@ -196,6 +199,7 @@ Or: ```python import enum + class MyEnum(enum.Enum): v1 = 1 """The first choice.""" @@ -211,8 +215,9 @@ Use [`functools.wraps()`](https://docs.python.org/3.6/library/functools.html#fun ```python from functools import wraps + def my_decorator(function): - """The decorator docs.""" + """The decorator docs.""" @wraps(function) def wrapped_function(*args, **kwargs): @@ -222,6 +227,7 @@ def my_decorator(function): return wrapped_function + @my_decorator def my_function(*args, **kwargs): """The function docs.""" diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 9643a1f8..ca38cc6f 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -56,7 +56,12 @@ class AutoDocProcessor(BlockProcessor): regex = re.compile(r"^(?P#{1,6} *|)::: ?(?P.+?) *$", flags=re.MULTILINE) def __init__( - self, parser: BlockParser, md: Markdown, config: dict, handlers: Handlers, autorefs: AutorefsPlugin + self, + parser: BlockParser, + md: Markdown, + config: dict, + handlers: Handlers, + autorefs: AutorefsPlugin, ) -> None: """Initialize the object. @@ -75,7 +80,7 @@ def __init__( self._autorefs = autorefs self._updated_envs: set = set() - def test(self, parent: Element, block: str) -> bool: + def test(self, parent: Element, block: str) -> bool: # noqa: ARG002 """Match our autodoc instructions. Arguments: @@ -124,15 +129,15 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: page = self._autorefs.current_page if page: for heading in headings: - anchor = heading.attrib["id"] # noqa: WPS440 - self._autorefs.register_anchor(page, anchor) # noqa: WPS441 + anchor = heading.attrib["id"] + self._autorefs.register_anchor(page, anchor) if "data-role" in heading.attrib: self._handlers.inventory.register( - name=anchor, # noqa: WPS441 + name=anchor, domain=handler.domain, role=heading.attrib["data-role"], - uri=f"{page}#{anchor}", # noqa: WPS441 + uri=f"{page}#{anchor}", ) parent.append(el) @@ -187,14 +192,14 @@ def _process_block( try: data: CollectorItem = handler.collect(identifier, options) except CollectionError as exception: - log.error(str(exception)) + log.error(str(exception)) # noqa: TRY400 if PluginError is SystemExit: # When MkDocs 1.2 is sufficiently common, this can be dropped. - log.error(f"Error reading page '{self._autorefs.current_page}':") + log.error(f"Error reading page '{self._autorefs.current_page}':") # noqa: TRY400 raise PluginError(f"Could not collect '{identifier}'") from exception if handler_name not in self._updated_envs: # We haven't seen this handler before on this document. log.debug("Updating renderer's env") - handler._update_env(self.md, self._config) # noqa: WPS437 (protected member OK) + handler._update_env(self.md, self._config) # (protected member OK) self._updated_envs.add(handler_name) log.debug("Rendering templates") @@ -202,7 +207,7 @@ def _process_block( rendered = handler.render(data, options) except TemplateNotFound as exc: theme_name = self._config["theme_name"] - log.error( + log.error( # noqa: TRY400 f"Template '{exc.name}' not found for '{handler_name}' handler and theme '{theme_name}'.", ) raise @@ -211,12 +216,12 @@ def _process_block( @classmethod @functools.lru_cache(maxsize=None) # Warn only once - def _warn_about_options_key(cls): + def _warn_about_options_key(cls) -> None: log.info("DEPRECATION: 'selection' and 'rendering' are deprecated and merged into a single 'options' YAML key") class _PostProcessor(Treeprocessor): - def run(self, root: Element): + def run(self, root: Element) -> None: carry_text = "" for el in reversed(root): # Reversed mainly for the ability to mutate during iteration. if el.tag == "div" and el.get("class") == "mkdocstrings": diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index ee81cbae..3901b341 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -14,7 +14,7 @@ import warnings from contextlib import suppress from pathlib import Path -from typing import Any, BinaryIO, Dict, Iterable, Iterator, List, Mapping, MutableMapping, Optional, Sequence +from typing import Any, BinaryIO, Iterable, Iterator, Mapping, MutableMapping, Sequence from xml.etree.ElementTree import Element, tostring from jinja2 import Environment, FileSystemLoader @@ -38,7 +38,7 @@ class CollectionError(Exception): """An exception raised when some collection of data failed.""" -class ThemeNotSupported(Exception): +class ThemeNotSupported(Exception): # noqa: N818 """An exception raised to tell a theme is not supported.""" @@ -75,7 +75,7 @@ class BaseRenderer: fallback_theme: str = "" extra_css = "" - def __init__(self, handler: str, theme: str, custom_templates: Optional[str] = None) -> None: + def __init__(self, handler: str, theme: str, custom_templates: str | None = None) -> None: """Initialize the object. If the given theme is not supported (it does not exist), it will look for a `fallback_theme` attribute @@ -102,7 +102,7 @@ def __init__(self, handler: str, theme: str, custom_templates: Optional[str] = N 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 + self.extra_css += "\n" + css_path.read_text(encoding="utf-8") break if custom_templates is not None: @@ -116,7 +116,7 @@ def __init__(self, handler: str, theme: str, custom_templates: Optional[str] = N self.env.filters["any"] = do_any self.env.globals["log"] = get_template_logger() - self._headings: List[Element] = [] + self._headings: list[Element] = [] self._md: Markdown = None # type: ignore # To be populated in `update_env`. def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: @@ -128,7 +128,7 @@ def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: Returns: The rendered template as HTML. - """ # noqa: DAR202,DAR401 + """ raise NotImplementedError def get_templates_dir(self, handler: str) -> Path: @@ -156,7 +156,7 @@ def get_templates_dir(self, handler: str) -> Path: with suppress(ModuleNotFoundError): # TODO: catch at some point to warn about missing handlers import mkdocstrings_handlers - for path in mkdocstrings_handlers.__path__: # noqa: WPS609 + for path in mkdocstrings_handlers.__path__: theme_path = Path(path, handler, "templates") if theme_path.exists(): return theme_path @@ -165,7 +165,7 @@ def get_templates_dir(self, handler: str) -> Path: # as mkdocstrings will stop being a namespace package import mkdocstrings - for path in mkdocstrings.__path__: # noqa: WPS609,WPS440 + for path in mkdocstrings.__path__: theme_path = Path(path, "templates", handler) if theme_path.exists(): if handler != "python": @@ -173,6 +173,7 @@ def get_templates_dir(self, handler: str) -> Path: "Exposing templates in the mkdocstrings.templates namespace is deprecated. " "Put them in a templates folder inside your handler package instead.", DeprecationWarning, + stacklevel=1, ) return theme_path @@ -194,7 +195,12 @@ def get_anchors(self, data: CollectorItem) -> Sequence[str]: return () def do_convert_markdown( - self, text: str, heading_level: int, html_id: str = "", *, strip_paragraph: bool = False + self, + text: str, + heading_level: int, + html_id: str = "", + *, + strip_paragraph: bool = False, ) -> Markup: """Render Markdown text; for use inside templates. @@ -224,9 +230,9 @@ def do_heading( content: str, heading_level: int, *, - role: Optional[str] = None, + role: str | None = None, hidden: bool = False, - toc_label: Optional[str] = None, + toc_label: str | None = None, **attributes: str, ) -> Markup: """Render an HTML heading and register it for the table of contents. For use inside templates. @@ -269,7 +275,7 @@ def do_heading( # 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 ( + assert ( # noqa: S101 html_with_placeholder.count("") == 1 ), f"Bug in mkdocstrings: failed to replace in {html_with_placeholder!r}" html = html_with_placeholder.replace("", content) @@ -285,7 +291,7 @@ def get_headings(self) -> Sequence[Element]: self._headings.clear() return result - def update_env(self, md: Markdown, config: dict) -> None: # noqa: W0613 (unused argument 'config') + def update_env(self, md: Markdown, config: dict) -> None: # noqa: ARG002 """Update the Jinja environment. Arguments: @@ -298,7 +304,7 @@ def update_env(self, md: Markdown, config: dict) -> None: # noqa: W0613 (unused self.env.filters["convert_markdown"] = self.do_convert_markdown self.env.filters["heading"] = self.do_heading - def _update_env(self, md: Markdown, config: dict): + def _update_env(self, md: Markdown, config: dict) -> None: """Update our handler to point to our configured Markdown instance, grabbing some of the config from `md`.""" extensions = config["mdx"] + [MkdocstringsInnerExtension(self._headings)] @@ -333,7 +339,7 @@ def collect(self, identifier: str, config: MutableMapping[str, Any]) -> Collecto Returns: Anything you want, as long as you can feed it to the renderer's `render` method. - """ # noqa: DAR202,DAR401 + """ raise NotImplementedError def teardown(self) -> None: @@ -381,19 +387,7 @@ def __init__(self, *args: str | BaseCollector | BaseRenderer, **kwargs: str | Ba # 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 @@ -409,7 +403,7 @@ def __init__(self, *args: str | BaseCollector | BaseRenderer, **kwargs: str | Ba elif isinstance(arg, str): str_args.append(arg) - while len(str_args) != 3: + while len(str_args) != 3: # noqa: PLR2004 str_args.append(None) # type: ignore[arg-type] handler, theme, custom_templates = str_args @@ -434,8 +428,9 @@ def __init__(self, *args: str | BaseCollector | BaseRenderer, **kwargs: str | Ba 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." - ) + "methods directly on your handler class.", + ), + stacklevel=1, ) self.collector = collector self.collect = collector.collect # type: ignore[assignment] @@ -444,15 +439,16 @@ def __init__(self, *args: str | BaseCollector | BaseRenderer, **kwargs: str | Ba 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" + "'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`)." - ) + "`get_templates_dir`, `get_anchors`, `update_env` and `fallback_theme`, `extra_css`).", + ), + stacklevel=1, ) self.renderer = renderer self.render = renderer.render # type: ignore[assignment] @@ -462,27 +458,27 @@ def __init__(self, *args: str | BaseCollector | BaseRenderer, **kwargs: str | Ba 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._update_env = renderer._update_env # type: ignore[assignment] self.fallback_theme = renderer.fallback_theme self.extra_css = renderer.extra_css - renderer.__class__.__init__( # noqa: WPS609 + renderer.__class__.__init__( self, - renderer._handler, # noqa: WPS437 - renderer._theme, # noqa: WPS437 - renderer._custom_templates, # noqa: WPS437 + renderer._handler, + renderer._theme, + renderer._custom_templates, ) 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 + BaseRenderer.__init__(self, handler, theme, custom_templates) @classmethod def load_inventory( cls, - in_file: BinaryIO, - url: str, - base_url: Optional[str] = None, - **kwargs: Any, + in_file: BinaryIO, # noqa: ARG003 + url: str, # noqa: ARG003 + base_url: str | None = None, # noqa: ARG003 + **kwargs: Any, # noqa: ARG003 ) -> Iterator[tuple[str, str]]: """Yield items and their URLs from an inventory file streamed from `in_file`. @@ -513,7 +509,7 @@ def __init__(self, config: dict) -> None: of [mkdocstrings.plugin.MkdocstringsPlugin.on_config][] to see what's in this dictionary. """ self._config = config - self._handlers: Dict[str, BaseHandler] = {} + self._handlers: dict[str, BaseHandler] = {} self.inventory: Inventory = Inventory(project=self._config["mkdocs"]["site_name"]) def get_anchors(self, identifier: str) -> Sequence[str]: @@ -563,7 +559,7 @@ def get_handler_config(self, name: str) -> dict: return handlers.get(name, {}) return {} - def get_handler(self, name: str, handler_config: Optional[dict] = None) -> BaseHandler: + def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHandler: """Get a handler thanks to its name. This function dynamically imports a module named "mkdocstrings.handlers.NAME", calls its @@ -591,8 +587,9 @@ def get_handler(self, name: str, handler_config: Optional[dict] = None) -> BaseH warnings.warn( DeprecationWarning( "Using the mkdocstrings.handlers namespace is deprecated. " - "Handlers must now use the mkdocstrings_handlers namespace." - ) + "Handlers must now use the mkdocstrings_handlers namespace.", + ), + stacklevel=1, ) self._handlers[name] = module.get_handler( theme=self._config["theme_name"], diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py index 24ee6268..f467147c 100644 --- a/src/mkdocstrings/handlers/rendering.py +++ b/src/mkdocstrings/handlers/rendering.py @@ -53,7 +53,7 @@ class Highlighter(Highlight): "line_spans", "anchor_linenums", "line_anchors", - ) + ), ) def __init__(self, md: Markdown): @@ -73,7 +73,7 @@ def __init__(self, md: Markdown): self._css_class = config.pop("css_class", "highlight") super().__init__(**{name: opt for name, opt in config.items() if name in self._highlight_config_keys}) - def highlight( # noqa: W0221 (intentionally different params, we're extending the functionality) + def highlight( # (intentionally different params, we're extending the functionality) self, src: str, language: Optional[str] = None, @@ -133,7 +133,7 @@ def __init__(self, md: Markdown, id_prefix: str): super().__init__(md) self.id_prefix = id_prefix - def run(self, root: Element): # noqa: D102 (ignore missing docstring) + def run(self, root: Element) -> None: # noqa: D102 (ignore missing docstring) if not self.id_prefix: return for el in root.iter(): @@ -174,7 +174,7 @@ def __init__(self, md: Markdown, shift_by: int): super().__init__(md) self.shift_by = shift_by - def run(self, root: Element): # noqa: D102 (ignore missing docstring) + def run(self, root: Element) -> None: # noqa: D102 (ignore missing docstring) if not self.shift_by: return for el in root.iter(): @@ -198,13 +198,13 @@ def __init__(self, md: Markdown, headings: List[Element]): super().__init__(md) self.headings = headings - def run(self, root: Element): + def run(self, root: Element) -> None: for el in root.iter(): if self.regex.fullmatch(el.tag): - el = copy.copy(el) + el = copy.copy(el) # noqa: PLW2901 # 'toc' extension's first pass (which we require to build heading stubs/ids) also edits the HTML. # Undo the permalink edit so we can pass this heading to the outer pass of the 'toc' extension. - if len(el) > 0 and el[-1].get("class") == self.md.treeprocessors["toc"].permalink_class: # noqa: WPS507 + if len(el) > 0 and el[-1].get("class") == self.md.treeprocessors["toc"].permalink_class: del el[-1] self.headings.append(el) @@ -220,6 +220,7 @@ def run(self, root: Element): # noqa: D102 (ignore missing docstring) # Turn the single

element into the root element and inherit its tag name (it's significant!) root[0].tag = root.tag return root[0] + return None class MkdocstringsInnerExtension(Extension): diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/inventory.py index 9108d91d..7007366d 100644 --- a/src/mkdocstrings/inventory.py +++ b/src/mkdocstrings/inventory.py @@ -13,7 +13,13 @@ class InventoryItem: """Inventory item.""" def __init__( - self, name: str, domain: str, role: str, uri: str, priority: str = "1", dispname: Optional[str] = None + self, + name: str, + domain: str, + role: str, + uri: str, + priority: str = "1", + dispname: Optional[str] = None, ): """Initialize the object. @@ -80,7 +86,7 @@ def __init__(self, items: Optional[List[InventoryItem]] = None, project: str = " self.project = project self.version = version - def register(self, *args: str, **kwargs: str): + def register(self, *args: str, **kwargs: str) -> None: """Create and register an item. Arguments: @@ -103,7 +109,7 @@ def format_sphinx(self) -> bytes: # Project: {self.project} # Version: {self.version} # The remainder of this file is compressed using zlib. - """ + """, ) .lstrip() .encode("utf8") diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py index 24004f8f..a9c3f418 100644 --- a/src/mkdocstrings/loggers.py +++ b/src/mkdocstrings/loggers.py @@ -11,14 +11,14 @@ try: from jinja2 import pass_context except ImportError: # TODO: remove once Jinja2 < 3.1 is dropped - from jinja2 import contextfunction as pass_context # type: ignore # noqa: WPS440 + from jinja2 import contextfunction as pass_context # type: ignore try: import mkdocstrings_handlers except ImportError: TEMPLATES_DIRS: Sequence[Path] = () else: - TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) # type: ignore[arg-type] # noqa: WPS609 + TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) # type: ignore[arg-type] class LoggerAdapter(logging.LoggerAdapter): diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 10542d8c..1fa3290f 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -69,7 +69,7 @@ class MkdocstringsPlugin(BasePlugin): """ config_scheme: Tuple[Tuple[str, MkType]] = ( - ("watch", MkType(list, default=[])), # type: ignore + ("watch", MkType(list, default=[])), ("handlers", MkType(dict, default={})), ("default_handler", MkType(str, default="python")), ("custom_templates", MkType(str, default=None)), @@ -126,8 +126,13 @@ def handlers(self) -> Handlers: # TODO: remove once watch feature is removed def on_serve( - self, server: LiveReloadServer, config: Config, builder: Callable, *args: Any, **kwargs: Any - ) -> None: # noqa: W0613 (unused arguments) + self, + server: LiveReloadServer, + config: Config, # noqa: ARG002 + builder: Callable, + *args: Any, # noqa: ARG002 + **kwargs: Any, # noqa: ARG002 + ) -> None: """Watch directories. Hook for the [`on_serve` event](https://www.mkdocs.org/user-guide/plugins/#on_serve). @@ -149,7 +154,7 @@ def on_serve( log.debug(f"Adding directory '{element}' to watcher") server.watch(element, builder) - def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: W0613 (unused arguments) + def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002 """Instantiate our Markdown extension. Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config). @@ -172,16 +177,13 @@ def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: W0613 (un log.debug("Adding extension to the list") theme_name = None - if config["theme"].name is None: - theme_name = os.path.dirname(config["theme"].dirs[0]) - else: - theme_name = config["theme"].name + theme_name = os.path.dirname(config["theme"].dirs[0]) if config["theme"].name is None else config["theme"].name to_import: InventoryImportType = [] for handler_name, conf in self.config["handlers"].items(): for import_item in conf.pop("import", ()): if isinstance(import_item, str): - import_item = {"url": import_item} + import_item = {"url": import_item} # noqa: PLW2901 to_import.append((handler_name, import_item)) extension_config = { @@ -193,7 +195,7 @@ def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: W0613 (un } self._handlers = Handlers(extension_config) - try: # noqa: WPS229 + try: # If autorefs plugin is explicitly enabled, just use it. autorefs = config["plugins"]["autorefs"] log.debug(f"Picked up existing autorefs instance {autorefs!r}") @@ -214,9 +216,11 @@ def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: W0613 (un self._inv_futures = [] if to_import: inv_loader = futures.ThreadPoolExecutor(4) - for handler_name, import_item in to_import: # noqa: WPS440 + for handler_name, import_item in to_import: future = inv_loader.submit( - self._load_inventory, self.get_handler(handler_name).load_inventory, **import_item + self._load_inventory, + self.get_handler(handler_name).load_inventory, + **import_item, ) self._inv_futures.append(future) inv_loader.shutdown(wait=False) @@ -247,7 +251,7 @@ def plugin_enabled(self) -> bool: """ return self.config["enabled"] - def on_env(self, env, config: Config, *args, **kwargs) -> None: + def on_env(self, env, config: Config, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 """Extra actions that need to happen after all Markdown rendering and before HTML rendering. Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env). @@ -274,8 +278,10 @@ def on_env(self, env, config: Config, *args, **kwargs) -> None: self._inv_futures = [] def on_post_build( - self, config: Config, **kwargs: Any - ) -> None: # noqa: W0613,R0201 (unused arguments, cannot be static) + self, + config: Config, # noqa: ARG002 + **kwargs: Any, # noqa: ARG002 + ) -> None: """Teardown the handlers. Hook for the [`on_post_build` event](https://www.mkdocs.org/user-guide/plugins/#on_post_build). @@ -337,7 +343,7 @@ def _load_inventory(cls, loader: InventoryLoaderType, url: str, **kwargs: Any) - @classmethod @functools.lru_cache(maxsize=None) # Warn only once - def _warn_about_watch_option(cls): + def _warn_about_watch_option(cls) -> None: log.info( "DEPRECATION: mkdocstrings' watch feature is deprecated in favor of MkDocs' watch feature, " "see https://www.mkdocs.org/user-guide/configuration/#watch", diff --git a/tests/conftest.py b/tests/conftest.py index c79b04d0..385770e0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,27 @@ """Configuration for the pytest test suite.""" +from __future__ import annotations + from collections import ChainMap +from typing import TYPE_CHECKING import pytest from markdown.core import Markdown from mkdocs import config from mkdocs.config.defaults import get_schema +if TYPE_CHECKING: + from pathlib import Path + + from mkdocstrings.plugin import MkdocstringsPlugin + @pytest.fixture(name="mkdocs_conf") -def fixture_mkdocs_conf(request, tmp_path): +def fixture_mkdocs_conf(request: pytest.FixtureRequest, tmp_path: Path) -> config.Config: """Yield a MkDocs configuration object.""" conf = config.Config(schema=get_schema()) - while hasattr(request, "_parent_request") and hasattr(request._parent_request, "_parent_request"): # noqa: WPS437 - request = request._parent_request # noqa: WPS437 + while hasattr(request, "_parent_request") and hasattr(request._parent_request, "_parent_request"): + request = request._parent_request conf_dict = { "site_name": "foo", @@ -38,14 +46,12 @@ def fixture_mkdocs_conf(request, tmp_path): @pytest.fixture(name="plugin") -def fixture_plugin(mkdocs_conf): +def fixture_plugin(mkdocs_conf: config.Config) -> MkdocstringsPlugin: """Return a plugin instance.""" - plugin = mkdocs_conf["plugins"]["mkdocstrings"] - plugin.md = Markdown(extensions=mkdocs_conf["markdown_extensions"], extension_configs=mkdocs_conf["mdx_configs"]) - return plugin + return mkdocs_conf["plugins"]["mkdocstrings"] @pytest.fixture(name="ext_markdown") -def fixture_ext_markdown(plugin): +def fixture_ext_markdown(mkdocs_conf: config.Config) -> Markdown: """Return a Markdown instance with MkdocstringsExtension.""" - return plugin.md + return Markdown(extensions=mkdocs_conf["markdown_extensions"], extension_configs=mkdocs_conf["mdx_configs"]) diff --git a/tests/test_extension.py b/tests/test_extension.py index 3c41932c..a0b90872 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -1,14 +1,23 @@ """Tests for the extension module.""" + +from __future__ import annotations + import logging import re import sys from textwrap import dedent +from typing import TYPE_CHECKING import pytest +if TYPE_CHECKING: + from markdown import Markdown + + from mkdocstrings.plugin import MkdocstringsPlugin + @pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"footnotes": {}}]}], indirect=["ext_markdown"]) -def test_multiple_footnotes(ext_markdown): +def test_multiple_footnotes(ext_markdown: Markdown) -> None: """Assert footnotes don't get added to subsequent docstrings.""" output = ext_markdown.convert( dedent( @@ -30,7 +39,7 @@ def test_multiple_footnotes(ext_markdown): assert output.count("Top footnote") == 1 -def test_markdown_heading_level(ext_markdown): +def test_markdown_heading_level(ext_markdown: Markdown) -> None: """Assert that Markdown headings' level doesn't exceed heading_level.""" output = ext_markdown.convert("::: tests.fixtures.headings\n options:\n show_root_heading: true") assert ">Foo" in output @@ -38,7 +47,7 @@ def test_markdown_heading_level(ext_markdown): assert ">Baz" in output -def test_keeps_preceding_text(ext_markdown): +def test_keeps_preceding_text(ext_markdown: Markdown) -> None: """Assert that autodoc is recognized in the middle of a block and preceding text is kept.""" output = ext_markdown.convert("**preceding**\n::: tests.fixtures.headings") assert "preceding" in output @@ -46,21 +55,21 @@ def test_keeps_preceding_text(ext_markdown): assert ":::" not in output -def test_reference_inside_autodoc(ext_markdown): +def test_reference_inside_autodoc(ext_markdown: Markdown) -> None: """Assert cross-reference Markdown extension works correctly.""" output = ext_markdown.convert("::: tests.fixtures.cross_reference") assert re.search(r"Link to <.*something\.Else.*>something\.Else<.*>\.", output) @pytest.mark.skipif(sys.version_info < (3, 8), reason="typing.Literal requires Python 3.8") -def test_quote_inside_annotation(ext_markdown): +def test_quote_inside_annotation(ext_markdown: Markdown) -> None: """Assert that inline highlighting doesn't double-escape HTML.""" output = ext_markdown.convert("::: tests.fixtures.string_annotation.Foo") assert ";hi&" in output assert "&" not in output -def test_html_inside_heading(ext_markdown): +def test_html_inside_heading(ext_markdown: Markdown) -> None: """Assert that headings don't double-escape HTML.""" output = ext_markdown.convert("::: tests.fixtures.html_tokens") assert "'<" in output @@ -76,7 +85,7 @@ def test_html_inside_heading(ext_markdown): ], indirect=["ext_markdown"], ) -def test_no_double_toc(ext_markdown, expect_permalink): +def test_no_double_toc(ext_markdown: Markdown, expect_permalink: str) -> None: """Assert that the 'toc' extension doesn't apply its modification twice.""" output = ext_markdown.convert( dedent( @@ -88,12 +97,12 @@ def test_no_double_toc(ext_markdown, expect_permalink): show_root_toc_entry: false # bb - """ - ) + """, + ), ) assert output.count(expect_permalink) == 5 assert 'id="tests.fixtures.headings--foo"' in output - assert ext_markdown.toc_tokens == [ # noqa: E1101 (the member gets populated only with 'toc' extension) + assert ext_markdown.toc_tokens == [ # (the member gets populated only with 'toc' extension) { "level": 1, "id": "aa", @@ -109,43 +118,43 @@ def test_no_double_toc(ext_markdown, expect_permalink): "id": "tests.fixtures.headings--bar", "name": "Bar", "children": [ - {"level": 6, "id": "tests.fixtures.headings--baz", "name": "Baz", "children": []} + {"level": 6, "id": "tests.fixtures.headings--baz", "name": "Baz", "children": []}, ], - } + }, ], - } + }, ], }, {"level": 1, "id": "bb", "name": "bb", "children": []}, ] -def test_use_custom_handler(ext_markdown): +def test_use_custom_handler(ext_markdown: Markdown) -> None: """Assert that we use the custom handler declared in an individual autodoc instruction.""" with pytest.raises(ModuleNotFoundError): ext_markdown.convert("::: tests.fixtures.headings\n handler: not_here") -def test_dont_register_every_identifier_as_anchor(plugin): +def test_dont_register_every_identifier_as_anchor(plugin: MkdocstringsPlugin, ext_markdown: Markdown) -> None: """Assert that we don't preemptively register all identifiers of a rendered object.""" - handler = plugin._handlers.get_handler("python") # noqa: WPS437 + handler = plugin._handlers.get_handler("python") ids = {"id1", "id2", "id3"} handler.get_anchors = lambda _: ids - plugin.md.convert("::: tests.fixtures.headings") - autorefs = plugin.md.parser.blockprocessors["mkdocstrings"]._autorefs # noqa: WPS219,WPS437 + ext_markdown.convert("::: tests.fixtures.headings") + autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs for identifier in ids: - assert identifier not in autorefs._url_map # noqa: WPS437 - assert identifier not in autorefs._abs_url_map # noqa: WPS437 + assert identifier not in autorefs._url_map + assert identifier not in autorefs._abs_url_map -def test_use_deprecated_yaml_keys(ext_markdown, caplog): +def test_use_deprecated_yaml_keys(ext_markdown: Markdown, caplog: pytest.LogCaptureFixture) -> None: """Check that using the deprecated 'selection' and 'rendering' YAML keys emits a deprecation warning.""" caplog.set_level(logging.INFO) assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n rendering:\n heading_level: 2") assert "single 'options' YAML key" in caplog.text -def test_use_new_options_yaml_key(ext_markdown): +def test_use_new_options_yaml_key(ext_markdown: Markdown) -> None: """Check that using the new 'options' YAML key works as expected.""" assert "h1" in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 1") assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 2") diff --git a/tests/test_handlers.py b/tests/test_handlers.py index cfe04cd8..5fd25357 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -1,5 +1,7 @@ """Tests for the handlers.base module.""" +from __future__ import annotations + import pytest from markdown import Markdown @@ -7,7 +9,7 @@ @pytest.mark.parametrize("extension_name", ["codehilite", "pymdownx.highlight"]) -def test_highlighter_without_pygments(extension_name): +def test_highlighter_without_pygments(extension_name: str) -> None: """Assert that it's possible to disable Pygments highlighting. Arguments: @@ -28,7 +30,7 @@ def test_highlighter_without_pygments(extension_name): @pytest.mark.parametrize("extension_name", [None, "codehilite", "pymdownx.highlight"]) @pytest.mark.parametrize("inline", [False, True]) -def test_highlighter_basic(extension_name, inline): +def test_highlighter_basic(extension_name: str | None, inline: bool) -> None: """Assert that Pygments syntax highlighting works. Arguments: diff --git a/tests/test_inventory.py b/tests/test_inventory.py index afbaa9fe..ecbb3cd2 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -1,5 +1,7 @@ """Tests for the inventory module.""" +from __future__ import annotations + import sys from io import BytesIO from os.path import join @@ -22,7 +24,7 @@ Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#other_anchor")]), ], ) -def test_sphinx_load_inventory_file(our_inv): +def test_sphinx_load_inventory_file(our_inv: Inventory) -> None: """Perform the 'live' inventory load test.""" buffer = BytesIO(our_inv.format_sphinx()) sphinx_inv = sphinx.InventoryFile.load(buffer, "", join) @@ -35,7 +37,7 @@ def test_sphinx_load_inventory_file(our_inv): @pytest.mark.skipif(sys.version_info < (3, 7), reason="using plugins that require Python 3.7") -def test_sphinx_load_mkdocstrings_inventory_file(): +def test_sphinx_load_mkdocstrings_inventory_file() -> None: """Perform the 'live' inventory load test on mkdocstrings own inventory.""" mkdocs_config = load_config() mkdocs_config["plugins"].run_event("startup", command="build", dirty=False) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index c8649dc8..b8e8d2a5 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,11 +1,17 @@ """Tests for the mkdocstrings plugin.""" +from __future__ import annotations + +from typing import TYPE_CHECKING from mkdocs.commands.build import build from mkdocs.config import load_config +if TYPE_CHECKING: + from pathlib import Path + -def test_disabling_plugin(tmp_path): +def test_disabling_plugin(tmp_path: Path) -> None: """Test disabling plugin.""" docs_dir = tmp_path / "docs" site_dir = tmp_path / "site"