diff --git a/src/mkdocstrings_handlers/python/__init__.py b/src/mkdocstrings_handlers/python/__init__.py
index 4823a66..706d85e 100644
--- a/src/mkdocstrings_handlers/python/__init__.py
+++ b/src/mkdocstrings_handlers/python/__init__.py
@@ -3,3 +3,7 @@
from mkdocstrings_handlers.python.handler import get_handler
__all__ = ["get_handler"] # noqa: WPS410
+
+# TODO: CSS classes everywhere in templates
+# TODO: name normalization (filenames, Jinja2 variables, HTML tags, CSS classes)
+# TODO: Jinja2 blocks everywhere in templates
diff --git a/src/mkdocstrings_handlers/python/collector.py b/src/mkdocstrings_handlers/python/collector.py
deleted file mode 100644
index 2fcc30e..0000000
--- a/src/mkdocstrings_handlers/python/collector.py
+++ /dev/null
@@ -1,92 +0,0 @@
-"""This module implements a collector for the Python language.
-
-It collects data with [Griffe](https://github.com/pawamoy/griffe).
-"""
-
-from __future__ import annotations
-
-from collections import ChainMap
-from contextlib import suppress
-
-from griffe.agents.extensions import load_extensions
-from griffe.collections import LinesCollection, ModulesCollection
-from griffe.docstrings.parsers import Parser
-from griffe.exceptions import AliasResolutionError
-from griffe.loader import GriffeLoader
-from mkdocstrings.handlers.base import BaseCollector, CollectionError, CollectorItem
-from mkdocstrings.loggers import get_logger
-
-logger = get_logger(__name__)
-
-
-class PythonCollector(BaseCollector):
- """The class responsible collecting objects data."""
-
- default_config: dict = {"docstring_style": "google", "docstring_options": {}}
- """The default selection options.
-
- Option | Type | Description | Default
- ------ | ---- | ----------- | -------
- **`docstring_style`** | `"google" | "numpy" | "sphinx" | None` | The docstring style to use. | `"google"`
- **`docstring_options`** | `dict[str, Any]` | The options for the docstring parser. | `{}`
- """
-
- fallback_config: dict = {"fallback": True}
-
- def __init__(self) -> None:
- """Initialize the collector."""
- self._modules_collection: ModulesCollection = ModulesCollection()
- self._lines_collection: LinesCollection = LinesCollection()
-
- def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: WPS231
- """Collect the documentation tree given an identifier and selection options.
-
- Arguments:
- identifier: The dotted-path of a Python object available in the Python path.
- config: Selection options, used to alter the data collection done by Griffe.
-
- Raises:
- CollectionError: When there was a problem collecting the object documentation.
-
- Returns:
- The collected object-tree.
- """
- module_name = identifier.split(".", 1)[0]
- unknown_module = module_name not in self._modules_collection
- if config.get("fallback", False) and unknown_module:
- raise CollectionError("Not loading additional modules during fallback")
-
- final_config = ChainMap(config, self.default_config)
- parser_name = final_config["docstring_style"]
- parser_options = final_config["docstring_options"]
- parser = parser_name and Parser(parser_name)
-
- if unknown_module:
- loader = GriffeLoader(
- extensions=load_extensions(final_config.get("extensions", [])),
- docstring_parser=parser,
- docstring_options=parser_options,
- modules_collection=self._modules_collection,
- lines_collection=self._lines_collection,
- )
- try:
- loader.load_module(module_name)
- except ImportError as error:
- raise CollectionError(str(error)) from error
-
- unresolved, iterations = loader.resolve_aliases(only_exported=True, only_known_modules=True)
- if unresolved:
- logger.warning(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations")
-
- try:
- doc_object = self._modules_collection[identifier]
- except KeyError as error: # noqa: WPS440
- raise CollectionError(f"{identifier} could not be found") from error
-
- if not unknown_module:
- with suppress(AliasResolutionError):
- if doc_object.docstring is not None:
- doc_object.docstring.parser = parser
- doc_object.docstring.parser_options = parser_options
-
- return doc_object
diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py
index b1018e7..84cfafc 100644
--- a/src/mkdocstrings_handlers/python/handler.py
+++ b/src/mkdocstrings_handlers/python/handler.py
@@ -1,15 +1,27 @@
"""This module implements a handler for the Python language."""
+from __future__ import annotations
+
import posixpath
+from collections import ChainMap
+from contextlib import suppress
from typing import Any, BinaryIO, Iterator, Optional, Tuple
+from griffe.agents.extensions import load_extensions
+from griffe.collections import LinesCollection, ModulesCollection
+from griffe.docstrings.parsers import Parser
+from griffe.exceptions import AliasResolutionError
+from griffe.loader import GriffeLoader
from griffe.logger import patch_loggers
-from mkdocstrings.handlers.base import BaseHandler
+from markdown import Markdown
+from mkdocstrings.extension import PluginError
+from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem
from mkdocstrings.inventory import Inventory
from mkdocstrings.loggers import get_logger
-from mkdocstrings_handlers.python.collector import PythonCollector
-from mkdocstrings_handlers.python.renderer import PythonRenderer
+from mkdocstrings_handlers.python import rendering
+
+logger = get_logger(__name__)
patch_loggers(get_logger)
@@ -21,10 +33,82 @@ class PythonHandler(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_theme: The fallback theme.
+ fallback_config: The configuration used to collect item during autorefs fallback.
+ default_collection_config: The default rendering options,
+ see [`default_collection_config`][mkdocstrings_handlers.python.handler.PythonHandler.default_collection_config].
+ default_rendering_config: The default rendering options,
+ see [`default_rendering_config`][mkdocstrings_handlers.python.handler.PythonHandler.default_rendering_config].
"""
domain: str = "py" # to match Sphinx's default domain
enable_inventory: bool = True
+ fallback_theme = "material"
+ fallback_config: dict = {"fallback": True}
+ default_collection_config: dict = {"docstring_style": "google", "docstring_options": {}}
+ """The default collection options.
+
+ Option | Type | Description | Default
+ ------ | ---- | ----------- | -------
+ **`docstring_style`** | `"google" | "numpy" | "sphinx" | None` | The docstring style to use. | `"google"`
+ **`docstring_options`** | `dict[str, Any]` | The options for the docstring parser. | `{}`
+ """
+ default_rendering_config: dict = {
+ "show_root_heading": False,
+ "show_root_toc_entry": True,
+ "show_root_full_path": True,
+ "show_root_members_full_path": False,
+ "show_object_full_path": False,
+ "show_category_heading": False,
+ "show_if_no_docstring": False,
+ "show_signature": True,
+ "show_signature_annotations": False,
+ "separate_signature": False,
+ "line_length": 60,
+ "merge_init_into_class": False,
+ "show_source": True,
+ "show_bases": True,
+ "show_submodules": True,
+ "group_by_category": True,
+ "heading_level": 2,
+ "members_order": rendering.Order.alphabetical.value,
+ "docstring_section_style": "table",
+ }
+ """The default rendering options.
+
+ Option | Type | Description | Default
+ ------ | ---- | ----------- | -------
+ **`show_root_heading`** | `bool` | Show the heading of the object at the root of the documentation tree. | `False`
+ **`show_root_toc_entry`** | `bool` | If the root heading is not shown, at least add a ToC entry for it. | `True`
+ **`show_root_full_path`** | `bool` | Show the full Python path for the root object heading. | `True`
+ **`show_object_full_path`** | `bool` | Show the full Python path of every object. | `False`
+ **`show_root_members_full_path`** | `bool` | Show the full Python path of objects that are children of the root object (for example, classes in a module). When False, `show_object_full_path` overrides. | `False`
+ **`show_category_heading`** | `bool` | When grouped by categories, show a heading for each category. | `False`
+ **`show_if_no_docstring`** | `bool` | Show the object heading even if it has no docstring or children with docstrings. | `False`
+ **`show_signature`** | `bool` | Show method and function signatures. | `True`
+ **`show_signature_annotations`** | `bool` | Show the type annotations in method and function signatures. | `False`
+ **`separate_signature`** | `bool` | Whether to put the whole signature in a code block below the heading. | `False`
+ **`line_length`** | `int` | Maximum line length when formatting code. | `60`
+ **`merge_init_into_class`** | `bool` | Whether to merge the `__init__` method into the class' signature and docstring. | `False`
+ **`show_source`** | `bool` | Show the source code of this object. | `True`
+ **`show_bases`** | `bool` | Show the base classes of a class. | `True`
+ **`show_submodules`** | `bool` | When rendering a module, show its submodules recursively. | `True`
+ **`group_by_category`** | `bool` | Group the object's children by categories: attributes, classes, functions, methods, and modules. | `True`
+ **`heading_level`** | `int` | The initial heading level to use. | `2`
+ **`members_order`** | `str` | The members ordering to use. Options: `alphabetical` - order by the members names, `source` - order members as they appear in the source file. | `alphabetical`
+ **`docstring_section_style`** | `str` | The style used to render docstring sections. Options: `table`, `list`, `spacy`. | `table`
+ """ # noqa: E501
+
+ def __init__(self, *args, **kwargs) -> None:
+ """Initialize the handler.
+
+ Parameters:
+ *args: Handler name, theme and custom templates.
+ **kwargs: Same thing, but with keyword arguments.
+ """
+ super().__init__(*args, **kwargs)
+ self._modules_collection: ModulesCollection = ModulesCollection()
+ self._lines_collection: LinesCollection = LinesCollection()
@classmethod
def load_inventory(
@@ -53,6 +137,95 @@ def load_inventory(
for item in Inventory.parse_sphinx(in_file, domain_filter=("py",)).values(): # noqa: WPS526
yield item.name, posixpath.join(base_url, item.uri)
+ def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: WPS231
+ """Collect the documentation tree given an identifier and selection options.
+
+ Arguments:
+ identifier: The dotted-path of a Python object available in the Python path.
+ config: Selection options, used to alter the data collection done by `pytkdocs`.
+
+ Raises:
+ CollectionError: When there was a problem collecting the object documentation.
+
+ Returns:
+ The collected object-tree.
+ """
+ module_name = identifier.split(".", 1)[0]
+ unknown_module = module_name not in self._modules_collection
+ if config.get("fallback", False) and unknown_module:
+ raise CollectionError("Not loading additional modules during fallback")
+
+ final_config = ChainMap(config, self.default_collection_config)
+ parser_name = final_config["docstring_style"]
+ parser_options = final_config["docstring_options"]
+ parser = parser_name and Parser(parser_name)
+
+ if unknown_module:
+ loader = GriffeLoader(
+ extensions=load_extensions(final_config.get("extensions", [])),
+ docstring_parser=parser,
+ docstring_options=parser_options,
+ modules_collection=self._modules_collection,
+ lines_collection=self._lines_collection,
+ )
+ try:
+ loader.load_module(module_name)
+ except ImportError as error:
+ raise CollectionError(str(error)) from error
+
+ unresolved, iterations = loader.resolve_aliases(only_exported=True, only_known_modules=True)
+ if unresolved:
+ logger.warning(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations")
+
+ try:
+ doc_object = self._modules_collection[identifier]
+ except KeyError as error: # noqa: WPS440
+ raise CollectionError(f"{identifier} could not be found") from error
+
+ if not unknown_module:
+ with suppress(AliasResolutionError):
+ if doc_object.docstring is not None:
+ doc_object.docstring.parser = parser
+ doc_object.docstring.parser_options = parser_options
+
+ return doc_object
+
+ def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring)
+ final_config = ChainMap(config, self.default_rendering_config)
+
+ template = self.env.get_template(f"{data.kind.value}.html")
+
+ # Heading level is a "state" variable, that will change at each step
+ # of the rendering recursion. Therefore, it's easier to use it as a plain value
+ # than as an item in a dictionary.
+ heading_level = final_config["heading_level"]
+ try:
+ final_config["members_order"] = rendering.Order(final_config["members_order"])
+ except ValueError:
+ choices = "', '".join(item.value for item in rendering.Order)
+ raise PluginError(f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.")
+
+ return template.render(
+ **{"config": final_config, data.kind.value: data, "heading_level": heading_level, "root": True},
+ )
+
+ def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore missing docstring)
+ super().update_env(md, config)
+ self.env.trim_blocks = True
+ self.env.lstrip_blocks = True
+ self.env.keep_trailing_newline = False
+ self.env.filters["crossref"] = rendering.do_crossref
+ self.env.filters["multi_crossref"] = rendering.do_multi_crossref
+ self.env.filters["order_members"] = rendering.do_order_members
+ self.env.filters["format_code"] = rendering.do_format_code
+ self.env.filters["format_signature"] = rendering.do_format_signature
+
+ def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore missing docstring)
+ try:
+ return list({data.path, data.canonical_path, *data.aliases})
+ except AliasResolutionError:
+ return [data.path]
+
def get_handler(
theme: str, # noqa: W0613 (unused argument config)
@@ -69,7 +242,4 @@ def get_handler(
Returns:
An instance of `PythonHandler`.
"""
- return PythonHandler(
- collector=PythonCollector(),
- renderer=PythonRenderer("python", theme, custom_templates),
- )
+ return PythonHandler("python", theme, custom_templates)
diff --git a/src/mkdocstrings_handlers/python/renderer.py b/src/mkdocstrings_handlers/python/renderer.py
deleted file mode 100644
index 93c9119..0000000
--- a/src/mkdocstrings_handlers/python/renderer.py
+++ /dev/null
@@ -1,249 +0,0 @@
-"""This module implements a renderer for the Python language."""
-
-from __future__ import annotations
-
-import enum
-import re
-import sys
-from collections import ChainMap
-from functools import lru_cache
-from typing import Any, Sequence
-
-from griffe.dataclasses import Alias, Object
-from griffe.exceptions import AliasResolutionError
-from markdown import Markdown
-from markupsafe import Markup
-from mkdocstrings.extension import PluginError
-from mkdocstrings.handlers.base import BaseRenderer, CollectorItem
-from mkdocstrings.loggers import get_logger
-
-logger = get_logger(__name__)
-# TODO: CSS classes everywhere in templates
-# TODO: name normalization (filenames, Jinja2 variables, HTML tags, CSS classes)
-# TODO: Jinja2 blocks everywhere in templates
-
-
-class Order(enum.Enum):
- """Enumeration for the possible members ordering."""
-
- alphabetical = "alphabetical"
- source = "source"
-
-
-def _sort_key_alphabetical(item: CollectorItem) -> Any:
- # chr(sys.maxunicode) is a string that contains the final unicode
- # character, so if 'name' isn't found on the object, the item will go to
- # the end of the list.
- return item.name or chr(sys.maxunicode)
-
-
-def _sort_key_source(item: CollectorItem) -> Any:
- # if 'lineno' is none, the item will go to the start of the list.
- return item.lineno if item.lineno is not None else -1
-
-
-order_map = {
- Order.alphabetical: _sort_key_alphabetical,
- Order.source: _sort_key_source,
-}
-
-
-class PythonRenderer(BaseRenderer):
- """The class responsible for loading Jinja templates and rendering them.
-
- It defines some configuration options, implements the `render` method,
- and overrides the `update_env` method of the [`BaseRenderer` class][mkdocstrings.handlers.base.BaseRenderer].
-
- Attributes:
- fallback_theme: The theme to fallback to.
- default_config: The default rendering options,
- see [`default_config`][mkdocstrings_handlers.python.renderer.PythonRenderer.default_config].
- """
-
- fallback_theme = "material"
-
- default_config: dict = {
- "show_root_heading": False,
- "show_root_toc_entry": True,
- "show_root_full_path": True,
- "show_root_members_full_path": False,
- "show_object_full_path": False,
- "show_category_heading": False,
- "show_if_no_docstring": False,
- "show_signature": True,
- "show_signature_annotations": False,
- "separate_signature": False,
- "line_length": 60,
- "merge_init_into_class": False,
- "show_source": True,
- "show_bases": True,
- "show_submodules": True,
- "group_by_category": True,
- "heading_level": 2,
- "members_order": Order.alphabetical.value,
- "docstring_section_style": "table",
- }
- """The default rendering options.
-
- Option | Type | Description | Default
- ------ | ---- | ----------- | -------
- **`show_root_heading`** | `bool` | Show the heading of the object at the root of the documentation tree. | `False`
- **`show_root_toc_entry`** | `bool` | If the root heading is not shown, at least add a ToC entry for it. | `True`
- **`show_root_full_path`** | `bool` | Show the full Python path for the root object heading. | `True`
- **`show_object_full_path`** | `bool` | Show the full Python path of every object. | `False`
- **`show_root_members_full_path`** | `bool` | Show the full Python path of objects that are children of the root object (for example, classes in a module). When False, `show_object_full_path` overrides. | `False`
- **`show_category_heading`** | `bool` | When grouped by categories, show a heading for each category. | `False`
- **`show_if_no_docstring`** | `bool` | Show the object heading even if it has no docstring or children with docstrings. | `False`
- **`show_signature`** | `bool` | Show method and function signatures. | `True`
- **`show_signature_annotations`** | `bool` | Show the type annotations in method and function signatures. | `False`
- **`separate_signature`** | `bool` | Whether to put the whole signature in a code block below the heading. | `False`
- **`line_length`** | `int` | Maximum line length when formatting code. | `60`
- **`merge_init_into_class`** | `bool` | Whether to merge the `__init__` method into the class' signature and docstring. | `False`
- **`show_source`** | `bool` | Show the source code of this object. | `True`
- **`show_bases`** | `bool` | Show the base classes of a class. | `True`
- **`show_submodules`** | `bool` | When rendering a module, show its submodules recursively. | `True`
- **`group_by_category`** | `bool` | Group the object's children by categories: attributes, classes, functions, methods, and modules. | `True`
- **`heading_level`** | `int` | The initial heading level to use. | `2`
- **`members_order`** | `str` | The members ordering to use. Options: `alphabetical` - order by the members names, `source` - order members as they appear in the source file. | `alphabetical`
- **`docstring_section_style`** | `str` | The style used to render docstring sections. Options: `table`, `list`, `spacy`. | `table`
- """ # noqa: E501
-
- def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring)
- final_config = ChainMap(config, self.default_config)
-
- template = self.env.get_template(f"{data.kind.value}.html")
-
- # Heading level is a "state" variable, that will change at each step
- # of the rendering recursion. Therefore, it's easier to use it as a plain value
- # than as an item in a dictionary.
- heading_level = final_config["heading_level"]
- try:
- final_config["members_order"] = Order(final_config["members_order"])
- except ValueError:
- choices = "', '".join(item.value for item in Order)
- raise PluginError(f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.")
-
- return template.render(
- **{"config": final_config, data.kind.value: data, "heading_level": heading_level, "root": True},
- )
-
- def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore missing docstring)
- try:
- return list({data.path, data.canonical_path, *data.aliases})
- except AliasResolutionError:
- return [data.path]
-
- def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore missing docstring)
- super().update_env(md, config)
- self.env.trim_blocks = True
- self.env.lstrip_blocks = True
- self.env.keep_trailing_newline = False
- self.env.filters["crossref"] = self.do_crossref
- self.env.filters["multi_crossref"] = self.do_multi_crossref
- self.env.filters["order_members"] = self.do_order_members
- self.env.filters["format_code"] = self.do_format_code
- self.env.filters["format_signature"] = self.do_format_signature
-
- def do_format_code(self, code: str, line_length: int) -> str:
- """Format code using Black.
-
- Parameters:
- code: The code to format.
- line_length: The line length to give to Black.
-
- Returns:
- The same code, formatted.
- """
- code = code.strip()
- if len(code) < line_length:
- return code
- formatter = _get_black_formatter()
- return formatter(code, line_length)
-
- def do_format_signature(self, signature: str, line_length: int) -> str:
- """Format a signature using Black.
-
- Parameters:
- signature: The signature to format.
- line_length: The line length to give to Black.
-
- Returns:
- The same code, formatted.
- """
- code = signature.strip()
- if len(code) < line_length:
- return code
- formatter = _get_black_formatter()
- formatted = formatter(f"def {code}: pass", line_length)
- # remove starting `def ` and trailing `: pass`
- return formatted[4:-5].strip()[:-1]
-
- def do_order_members(self, members: Sequence[Object | Alias], order: Order) -> Sequence[Object | Alias]:
- """Order members given an ordering method.
-
- Parameters:
- members: The members to order.
- order: The ordering method.
-
- Returns:
- The same members, ordered.
- """
- return sorted(members, key=order_map[order])
-
- def do_crossref(self, path: str, brief: bool = True) -> Markup:
- """Filter to create cross-references.
-
- Parameters:
- path: The path to link to.
- brief: Show only the last part of the path, add full path as hover.
-
- Returns:
- Markup text.
- """
- full_path = path
- if brief:
- path = full_path.split(".")[-1]
- return Markup("{path}").format(
- full_path=full_path, path=path
- )
-
- def do_multi_crossref(self, text: str, code: bool = True) -> Markup:
- """Filter to create cross-references.
-
- Parameters:
- text: The text to scan.
- code: Whether to wrap the result in a code tag.
-
- Returns:
- Markup text.
- """
- group_number = 0
- variables = {}
-
- def repl(match): # noqa: WPS430
- nonlocal group_number # noqa: WPS420
- group_number += 1
- path = match.group()
- path_var = f"path{group_number}"
- variables[path_var] = path
- return f"{{{path_var}}}"
-
- text = re.sub(r"([\w.]+)", repl, text)
- if code:
- text = f"{text}
"
- return Markup(text).format(**variables)
-
-
-@lru_cache(maxsize=1)
-def _get_black_formatter():
- try:
- from black import Mode, format_str
- except ModuleNotFoundError:
- logger.warning("Formatting signatures requires Black to be installed.")
- return lambda text, _: text
-
- def formatter(code, line_length): # noqa: WPS430
- mode = Mode(line_length=line_length)
- return format_str(code, mode=mode)
-
- return formatter
diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py
new file mode 100644
index 0000000..8a29256
--- /dev/null
+++ b/src/mkdocstrings_handlers/python/rendering.py
@@ -0,0 +1,148 @@
+"""This module implements rendering utilities."""
+
+from __future__ import annotations
+
+import enum
+import re
+import sys
+from functools import lru_cache
+from typing import Any, Sequence
+
+from griffe.dataclasses import Alias, Object
+from markupsafe import Markup
+from mkdocstrings.handlers.base import CollectorItem
+from mkdocstrings.loggers import get_logger
+
+logger = get_logger(__name__)
+
+
+class Order(enum.Enum):
+ """Enumeration for the possible members ordering."""
+
+ alphabetical = "alphabetical"
+ source = "source"
+
+
+def _sort_key_alphabetical(item: CollectorItem) -> Any:
+ # chr(sys.maxunicode) is a string that contains the final unicode
+ # character, so if 'name' isn't found on the object, the item will go to
+ # the end of the list.
+ return item.name or chr(sys.maxunicode)
+
+
+def _sort_key_source(item: CollectorItem) -> Any:
+ # if 'lineno' is none, the item will go to the start of the list.
+ return item.lineno if item.lineno is not None else -1
+
+
+order_map = {
+ Order.alphabetical: _sort_key_alphabetical,
+ Order.source: _sort_key_source,
+}
+
+
+def do_format_code(code: str, line_length: int) -> str:
+ """Format code using Black.
+
+ Parameters:
+ code: The code to format.
+ line_length: The line length to give to Black.
+
+ Returns:
+ The same code, formatted.
+ """
+ code = code.strip()
+ if len(code) < line_length:
+ return code
+ formatter = _get_black_formatter()
+ return formatter(code, line_length)
+
+
+def do_format_signature(signature: str, line_length: int) -> str:
+ """Format a signature using Black.
+
+ Parameters:
+ signature: The signature to format.
+ line_length: The line length to give to Black.
+
+ Returns:
+ The same code, formatted.
+ """
+ code = signature.strip()
+ if len(code) < line_length:
+ return code
+ formatter = _get_black_formatter()
+ formatted = formatter(f"def {code}: pass", line_length)
+ # remove starting `def ` and trailing `: pass`
+ return formatted[4:-5].strip()[:-1]
+
+
+def do_order_members(members: Sequence[Object | Alias], order: Order) -> Sequence[Object | Alias]:
+ """Order members given an ordering method.
+
+ Parameters:
+ members: The members to order.
+ order: The ordering method.
+
+ Returns:
+ The same members, ordered.
+ """
+ return sorted(members, key=order_map[order])
+
+
+def do_crossref(path: str, brief: bool = True) -> Markup:
+ """Filter to create cross-references.
+
+ Parameters:
+ path: The path to link to.
+ brief: Show only the last part of the path, add full path as hover.
+
+ Returns:
+ Markup text.
+ """
+ full_path = path
+ if brief:
+ path = full_path.split(".")[-1]
+ return Markup("{path}").format(full_path=full_path, path=path)
+
+
+def do_multi_crossref(text: str, code: bool = True) -> Markup:
+ """Filter to create cross-references.
+
+ Parameters:
+ text: The text to scan.
+ code: Whether to wrap the result in a code tag.
+
+ Returns:
+ Markup text.
+ """
+ group_number = 0
+ variables = {}
+
+ def repl(match): # noqa: WPS430
+ nonlocal group_number # noqa: WPS420
+ group_number += 1
+ path = match.group()
+ path_var = f"path{group_number}"
+ variables[path_var] = path
+ return f"{{{path_var}}}"
+
+ text = re.sub(r"([\w.]+)", repl, text)
+ if code:
+ text = f"{text}
"
+ return Markup(text).format(**variables)
+
+
+@lru_cache(maxsize=1)
+def _get_black_formatter():
+ try:
+ from black import Mode, format_str
+ except ModuleNotFoundError:
+ logger.warning("Formatting signatures requires Black to be installed.")
+ return lambda text, _: text
+
+ def formatter(code, line_length): # noqa: WPS430
+ mode = Mode(line_length=line_length)
+ return format_str(code, mode=mode)
+
+ return formatter