From d5927c2d9dd8f5be0d1e2585ed0e9acf748ae167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 1 Apr 2022 20:54:46 +0200 Subject: [PATCH 01/27] chore: Remove fixsetup script --- scripts/fixsetup.sh | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100755 scripts/fixsetup.sh diff --git a/scripts/fixsetup.sh b/scripts/fixsetup.sh deleted file mode 100755 index 94b63559..00000000 --- a/scripts/fixsetup.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -e - -PYTHON_VERSIONS="${PYTHON_VERSIONS-3.7 3.8 3.9 3.10 3.11}" - -for python_version in ${PYTHON_VERSIONS}; do - rm -rf "__pypackages__/${python_version}/lib/mkdocstrings" - rm -f "__pypackages__/${python_version}/lib/mkdocstrings.pth" - cp -r ../mkdocstrings/src/mkdocstrings "__pypackages__/${python_version}/lib/" -done From 14cfeeda20a2b84deea6105da87985f6484ca96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 1 Apr 2022 20:55:07 +0200 Subject: [PATCH 02/27] docs: Fix docstrings --- src/mkdocstrings_handlers/python/collector.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/mkdocstrings_handlers/python/collector.py b/src/mkdocstrings_handlers/python/collector.py index ebddaaac..2fcc30e9 100644 --- a/src/mkdocstrings_handlers/python/collector.py +++ b/src/mkdocstrings_handlers/python/collector.py @@ -20,11 +20,7 @@ class PythonCollector(BaseCollector): - """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]. - """ + """The class responsible collecting objects data.""" default_config: dict = {"docstring_style": "google", "docstring_options": {}} """The default selection options. @@ -47,7 +43,7 @@ def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: WPS2 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`. + config: Selection options, used to alter the data collection done by Griffe. Raises: CollectionError: When there was a problem collecting the object documentation. From 0b48bddb9de1bbe2831d66f86acbbc3189f0f8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 1 Apr 2022 21:02:24 +0200 Subject: [PATCH 03/27] tests: Fix tests for new Griffe version --- tests/test_renderer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 31232e12..21313a84 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -1,7 +1,7 @@ """Tests for the `renderer` module.""" import pytest -from griffe.docstrings.dataclasses import DocstringSection, DocstringSectionKind +from griffe.docstrings.dataclasses import DocstringSectionExamples, DocstringSectionKind @pytest.mark.parametrize( @@ -19,8 +19,7 @@ def test_render_docstring_examples_section(renderer): Parameters: renderer: A renderer instance (parametrized). """ - section = DocstringSection( - DocstringSectionKind.examples, + section = DocstringSectionExamples( value=[ (DocstringSectionKind.text, "This is an example."), (DocstringSectionKind.examples, ">>> print('Hello')\nHello"), From b946c36789c6a12f376cecc7d28e91f7aaaed6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 1 Apr 2022 21:05:18 +0200 Subject: [PATCH 04/27] docs: Add visual indication on external links in code reference --- docs/css/mkdocstrings.css | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 42c77416..d32c66f5 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -4,3 +4,20 @@ div.doc-contents:not(.first) { border-left: 4px solid rgba(230, 230, 230); margin-bottom: 80px; } + +/* Mark external links as such */ +a.autorefs-external:hover::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + background-image: url('data:image/svg+xml,'); + content: ' '; + + display: inline-block; + position: absolute; + margin-left: 0.2em; + margin-top: 0.4em; + + height: 1em; + width: 1em; + border-radius: 100%; + background-color: var(--md-accent-fg-color); +} From d5ea1c5cf7884d8c019145f73685a84218e69840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 2 Apr 2022 16:58:40 +0200 Subject: [PATCH 05/27] refactor: Stop using deprecated base classes --- src/mkdocstrings_handlers/python/__init__.py | 4 + src/mkdocstrings_handlers/python/collector.py | 92 ------- src/mkdocstrings_handlers/python/handler.py | 184 ++++++++++++- src/mkdocstrings_handlers/python/renderer.py | 249 ------------------ src/mkdocstrings_handlers/python/rendering.py | 148 +++++++++++ 5 files changed, 329 insertions(+), 348 deletions(-) delete mode 100644 src/mkdocstrings_handlers/python/collector.py delete mode 100644 src/mkdocstrings_handlers/python/renderer.py create mode 100644 src/mkdocstrings_handlers/python/rendering.py diff --git a/src/mkdocstrings_handlers/python/__init__.py b/src/mkdocstrings_handlers/python/__init__.py index 4823a66f..706d85ee 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 2fcc30e9..00000000 --- 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 b1018e7f..84cfafc8 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 93c91195..00000000 --- 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 00000000..8a29256f --- /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 From 6407cf4f2375c894e0c528e932e9b76774a6455e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 16 Apr 2022 14:30:50 +0200 Subject: [PATCH 06/27] fix: Fix categories rendering Issue #14: https://github.com/mkdocstrings/python/issues/14 --- src/mkdocstrings_handlers/python/handler.py | 1 + src/mkdocstrings_handlers/python/rendering.py | 18 ++++++++++++++++++ .../templates/material/_base/children.html | 8 ++++---- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 84cfafc8..8c779815 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -219,6 +219,7 @@ def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore 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 + self.env.filters["filter_docstrings"] = rendering.do_filter_docstrings def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore missing docstring) try: diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index 8a29256f..e920a076 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -133,6 +133,24 @@ def repl(match): # noqa: WPS430 return Markup(text).format(**variables) +def do_filter_docstrings( + objects_dictionary: dict[str, Object | Alias], + keep_empty: bool = True, +) -> list[Object | Alias]: + """Filter a dictionary of objects based on their docstrings. + + Parameters: + objects_dictionary: The dictionary of objects. + keep_empty: Whether to keep objects with no/empty docstrings (recursive check). + + Returns: + A list of objects. + """ + if keep_empty: + return list(objects_dictionary.values()) + return [obj for obj in objects_dictionary.values() if obj.has_docstrings] + + @lru_cache(maxsize=1) def _get_black_formatter(): try: diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/children.html b/src/mkdocstrings_handlers/python/templates/material/_base/children.html index 6a5b40f5..d6fc61d6 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/children.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/children.html @@ -13,7 +13,7 @@ {% set extra_level = 0 %} {% endif %} - {% if config.show_category_heading and obj.attributes.values()|any %} + {% if config.show_category_heading and obj.attributes|filter_docstrings(config.show_if_no_docstring) %} {% filter heading(heading_level, id=html_id ~ "-attributes") %}Attributes{% endfilter %} {% endif %} {% with heading_level = heading_level + extra_level %} @@ -24,7 +24,7 @@ {% endfor %} {% endwith %} - {% if config.show_category_heading and obj.classes.values()|any %} + {% if config.show_category_heading and obj.classes|filter_docstrings(config.show_if_no_docstring) %} {% filter heading(heading_level, id=html_id ~ "-classes") %}Classes{% endfilter %} {% endif %} {% with heading_level = heading_level + extra_level %} @@ -35,7 +35,7 @@ {% endfor %} {% endwith %} - {% if config.show_category_heading and obj.functions.values()|any %} + {% if config.show_category_heading and obj.functions|filter_docstrings(config.show_if_no_docstring) %} {% filter heading(heading_level, id=html_id ~ "-functions") %}Functions{% endfilter %} {% endif %} {% with heading_level = heading_level + extra_level %} @@ -49,7 +49,7 @@ {% endwith %} {% if config.show_submodules %} - {% if config.show_category_heading and obj.modules.values()|any %} + {% if config.show_category_heading and obj.modules|filter_docstrings(config.show_if_no_docstring) %} {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %} {% endif %} {% with heading_level = heading_level + extra_level %} From fa91dd11a0b626cc17d31d4b9eedac2c6cc74d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 21 Apr 2022 13:39:15 +0200 Subject: [PATCH 07/27] docs: Update docs --- docs/css/mkdocstrings.css | 3 +- docs/usage.md | 5 +-- src/mkdocstrings_handlers/python/handler.py | 44 ++++++++++----------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index d32c66f5..0d305991 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -1,8 +1,7 @@ /* Indentation. */ div.doc-contents:not(.first) { padding-left: 25px; - border-left: 4px solid rgba(230, 230, 230); - margin-bottom: 80px; + border-left: .05rem solid var(--md-typeset-table-color); } /* Mark external links as such */ diff --git a/docs/usage.md b/docs/usage.md index d9329414..51e6dd6b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -41,7 +41,7 @@ Option | Description ### Rendering -::: mkdocstrings_handlers.python.renderer.PythonRenderer.default_config +::: mkdocstrings_handlers.python.handler.PythonHandler.default_rendering_config rendering: show_root_toc_entry: false @@ -162,8 +162,7 @@ Here are some CSS rules for the /* Indentation. */ div.doc-contents:not(.first) { padding-left: 25px; - border-left: .05rem solid var(--md-default-fg-color--lightest); - margin-bottom: 80px; + border-left: .05rem solid var(--md-typeset-table-color); } ``` diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 8c779815..d08b1fd0 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -74,29 +74,27 @@ class PythonHandler(BaseHandler): "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` + """ + Attributes: Default rendering options: + show_root_heading (bool): Show the heading of the object at the root of the documentation tree. Default: `False`. + show_root_toc_entry (bool): If the root heading is not shown, at least add a ToC entry for it. Default: `True`. + show_root_full_path (bool): Show the full Python path for the root object heading. Default: `True`. + show_root_members_full_path (bool): Show the full Python path of every object. Default: `False`. + show_object_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. Default: `False`. + show_category_heading (bool): When grouped by categories, show a heading for each category. Default: `False`. + show_if_no_docstring (bool): Show the object heading even if it has no docstring or children with docstrings. Default: `False`. + show_signature (bool): Show method and function signatures. Default: `True`. + show_signature_annotations (bool): Show the type annotations in method and function signatures. Default: `False`. + separate_signature (bool): Whether to put the whole signature in a code block below the heading. Default: `False`. + line_length (int): Maximum line length when formatting code. Default: `60`. + merge_init_into_class (bool): Whether to merge the `__init__` method into the class' signature and docstring. Default: `False`. + show_source (bool): Show the source code of this object. Default: `True`. + show_bases (bool): Show the base classes of a class. Default: `True`. + show_submodules (bool): When rendering a module, show its submodules recursively. Default: `True`. + group_by_category (bool): Group the object's children by categories: attributes, classes, functions, methods, and modules. Default: `True`. + heading_level (int): The initial heading level to use. Default: `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. Default: `alphabetical`. + docstring_section_style (str): The style used to render docstring sections. Options: `table`, `list`, `spacy`. Default: `table`. """ # noqa: E501 def __init__(self, *args, **kwargs) -> None: From 879509034b32816dc19c9d3075b0a032c218f0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 21 Apr 2022 13:39:40 +0200 Subject: [PATCH 08/27] tests: Fix tests --- tests/conftest.py | 12 ++++----- tests/test_collector.py | 31 --------------------- tests/test_handler.py | 60 +++++++++++++++++++++++++++++++++++++++++ tests/test_renderer.py | 44 ------------------------------ tests/test_rendering.py | 11 ++++++++ tests/test_themes.py | 6 ++--- 6 files changed, 80 insertions(+), 84 deletions(-) delete mode 100644 tests/test_collector.py create mode 100644 tests/test_handler.py delete mode 100644 tests/test_renderer.py create mode 100644 tests/test_rendering.py diff --git a/tests/conftest.py b/tests/conftest.py index ab919d29..6e7766b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -86,16 +86,16 @@ def fixture_ext_markdown(plugin): return plugin.md -@pytest.fixture(name="renderer") -def fixture_renderer(plugin): - """Return a PythonRenderer instance. +@pytest.fixture(name="handler") +def fixture_handler(plugin): + """Return a handler instance. Parameters: plugin: Pytest fixture: [tests.conftest.fixture_plugin][]. Returns: - A renderer instance. + A handler instance. """ handler = plugin.handlers.get_handler("python") - handler.renderer._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 - return handler.renderer + handler._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 + return handler diff --git a/tests/test_collector.py b/tests/test_collector.py deleted file mode 100644 index 53544c77..00000000 --- a/tests/test_collector.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Tests for the `collector` module.""" - -import pytest - -from mkdocstrings_handlers.python.collector import CollectionError, PythonCollector - - -def test_collect_missing_module(): - """Assert error is raised for missing modules.""" - collector = PythonCollector() - with pytest.raises(CollectionError): - collector.collect("aaaaaaaa", {}) - - -def test_collect_missing_module_item(): - """Assert error is raised for missing items within existing modules.""" - collector = PythonCollector() - with pytest.raises(CollectionError): - collector.collect("mkdocstrings.aaaaaaaa", {}) - - -def test_collect_module(): - """Assert existing module can be collected.""" - collector = PythonCollector() - assert collector.collect("mkdocstrings", {}) - - -def test_collect_with_null_parser(): - """Assert we can pass `None` as parser when collecting.""" - collector = PythonCollector() - assert collector.collect("mkdocstrings", {"docstring_style": None}) diff --git a/tests/test_handler.py b/tests/test_handler.py new file mode 100644 index 00000000..5a49a8b1 --- /dev/null +++ b/tests/test_handler.py @@ -0,0 +1,60 @@ +"""Tests for the `handler` module.""" + +import pytest +from griffe.docstrings.dataclasses import DocstringSectionExamples, DocstringSectionKind + +from mkdocstrings_handlers.python.handler import CollectionError, get_handler + + +def test_collect_missing_module(): + """Assert error is raised for missing modules.""" + handler = get_handler(theme="material") + with pytest.raises(CollectionError): + handler.collect("aaaaaaaa", {}) + + +def test_collect_missing_module_item(): + """Assert error is raised for missing items within existing modules.""" + handler = get_handler(theme="material") + with pytest.raises(CollectionError): + handler.collect("mkdocstrings.aaaaaaaa", {}) + + +def test_collect_module(): + """Assert existing module can be collected.""" + handler = get_handler(theme="material") + assert handler.collect("mkdocstrings", {}) + + +def test_collect_with_null_parser(): + """Assert we can pass `None` as parser when collecting.""" + handler = get_handler(theme="material") + assert handler.collect("mkdocstrings", {"docstring_style": None}) + + +@pytest.mark.parametrize( + "handler", + [ + {"theme": "mkdocs"}, + {"theme": "readthedocs"}, + {"theme": {"name": "material"}}, + ], + indirect=["handler"], +) +def test_render_docstring_examples_section(handler): + """Assert docstrings' examples section can be rendered. + + Parameters: + handler: A handler instance (parametrized). + """ + section = DocstringSectionExamples( + value=[ + (DocstringSectionKind.text, "This is an example."), + (DocstringSectionKind.examples, ">>> print('Hello')\nHello"), + ], + ) + template = handler.env.get_template("docstring/examples.html") + rendered = template.render(section=section) + assert "

This is an example.

" in rendered + assert "print" in rendered + assert "Hello" in rendered diff --git a/tests/test_renderer.py b/tests/test_renderer.py deleted file mode 100644 index 21313a84..00000000 --- a/tests/test_renderer.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Tests for the `renderer` module.""" - -import pytest -from griffe.docstrings.dataclasses import DocstringSectionExamples, DocstringSectionKind - - -@pytest.mark.parametrize( - "renderer", - [ - {"theme": "mkdocs"}, - {"theme": "readthedocs"}, - {"theme": {"name": "material"}}, - ], - indirect=["renderer"], -) -def test_render_docstring_examples_section(renderer): - """Assert docstrings' examples section can be rendered. - - Parameters: - renderer: A renderer instance (parametrized). - """ - section = DocstringSectionExamples( - value=[ - (DocstringSectionKind.text, "This is an example."), - (DocstringSectionKind.examples, ">>> print('Hello')\nHello"), - ], - ) - template = renderer.env.get_template("docstring/examples.html") - rendered = template.render(section=section) - assert "

This is an example.

" in rendered - assert "print" in rendered - assert "Hello" in rendered - - -def test_format_code_and_signature(renderer): - """Assert code and signatures can be Black-formatted. - - Parameters: - renderer: A renderer instance (parametrized). - """ - assert renderer.do_format_code("print('Hello')", 100) - assert renderer.do_format_code('print("Hello")', 100) - assert renderer.do_format_signature("(param: str = 'hello') -> 'Class'", 100) - assert renderer.do_format_signature('(param: str = "hello") -> "Class"', 100) diff --git a/tests/test_rendering.py b/tests/test_rendering.py new file mode 100644 index 00000000..3b59505a --- /dev/null +++ b/tests/test_rendering.py @@ -0,0 +1,11 @@ +"""Tests for the `rendering` module.""" + +from mkdocstrings_handlers.python import rendering + + +def test_format_code_and_signature(): + """Assert code and signatures can be Black-formatted.""" + assert rendering.do_format_code("print('Hello')", 100) + assert rendering.do_format_code('print("Hello")', 100) + assert rendering.do_format_signature("(param: str = 'hello') -> 'Class'", 100) + assert rendering.do_format_signature('(param: str = "hello") -> "Class"', 100) diff --git a/tests/test_themes.py b/tests/test_themes.py index 44426fb6..b1e7d5d5 100644 --- a/tests/test_themes.py +++ b/tests/test_themes.py @@ -35,6 +35,6 @@ def test_render_themes_templates_python(module, plugin): plugin: Pytest fixture: [tests.conftest.fixture_plugin][]. """ handler = plugin.handlers.get_handler("python") - handler.renderer._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 - data = handler.collector.collect(module, {}) - handler.renderer.render(data, {}) + handler._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 + data = handler.collect(module, {}) + handler.render(data, {}) From dd41182c210f0bb2675ead162adaa01dbbb1949f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 25 Apr 2022 18:13:24 +0200 Subject: [PATCH 09/27] feat: Add paths option Issue mkdocstrings/mkdocstrings#311: https://github.com/mkdocstrings/mkdocstrings/issues/311 PR #20: https://github.com/mkdocstrings/python/pull/20 --- src/mkdocstrings_handlers/python/handler.py | 33 +++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index d08b1fd0..029769c7 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -2,7 +2,9 @@ from __future__ import annotations +import os import posixpath +import sys from collections import ChainMap from contextlib import suppress from typing import Any, BinaryIO, Iterator, Optional, Tuple @@ -97,14 +99,30 @@ class PythonHandler(BaseHandler): docstring_section_style (str): The style used to render docstring sections. Options: `table`, `list`, `spacy`. Default: `table`. """ # noqa: E501 - def __init__(self, *args, **kwargs) -> None: + def __init__( + self, *args: Any, config_file_path: str | None = None, paths: list[str] | None = None, **kwargs: Any + ) -> None: """Initialize the handler. Parameters: *args: Handler name, theme and custom templates. + config_file_path: The MkDocs configuration file path. + paths: A list of paths to use as Griffe search paths. **kwargs: Same thing, but with keyword arguments. """ super().__init__(*args, **kwargs) + self._config_file_path = config_file_path + paths = paths or [] + if not paths and config_file_path: + paths.append(os.path.dirname(config_file_path)) + search_paths = [path for path in sys.path if path] # eliminate empty path + for path in reversed(paths): + if not os.path.isabs(path): + if config_file_path: + path = os.path.abspath(os.path.join(os.path.dirname(config_file_path), path)) + if path not in search_paths: + search_paths.insert(0, path) + self._paths = search_paths self._modules_collection: ModulesCollection = ModulesCollection() self._lines_collection: LinesCollection = LinesCollection() @@ -161,6 +179,7 @@ def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: WPS2 if unknown_module: loader = GriffeLoader( extensions=load_extensions(final_config.get("extensions", [])), + search_paths=self._paths, docstring_parser=parser, docstring_options=parser_options, modules_collection=self._modules_collection, @@ -229,6 +248,8 @@ def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore m def get_handler( theme: str, # noqa: W0613 (unused argument config) custom_templates: Optional[str] = None, + config_file_path: str | None = None, + paths: list[str] | None = None, **config: Any, ) -> PythonHandler: """Simply return an instance of `PythonHandler`. @@ -236,9 +257,17 @@ def get_handler( Arguments: theme: The theme to use when rendering contents. custom_templates: Directory containing custom templates. + config_file_path: The MkDocs configuration file path. + paths: A list of paths to use as Griffe search paths. **config: Configuration passed to the handler. Returns: An instance of `PythonHandler`. """ - return PythonHandler("python", theme, custom_templates) + return PythonHandler( + handler="python", + theme=theme, + custom_templates=custom_templates, + config_file_path=config_file_path, + paths=paths, + ) From 59104c4c51c86c774eed76d8508f9f4d3db5463f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 28 Apr 2022 18:59:53 +0200 Subject: [PATCH 10/27] refactor: Bring consistency on headings style --- .../python/templates/material/_base/class.html | 2 +- .../python/templates/material/_base/module.html | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/class.html b/src/mkdocstrings_handlers/python/templates/material/_base/class.html index 0e629dbe..1bc4edfe 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/class.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/class.html @@ -23,7 +23,7 @@ toc_label=class.name) %} {% if config.separate_signature %} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} {% elif config.merge_init_into_class and "__init__" in class.members -%} {%- with function = class.members["__init__"] -%} {%- filter highlight(language="python", inline=True) -%} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/module.html b/src/mkdocstrings_handlers/python/templates/material/_base/module.html index 54e4d4e4..c602f9b0 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/module.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/module.html @@ -22,7 +22,9 @@ class="doc doc-heading", toc_label=module.name) %} - {% if show_full_path %}{{ module.path }}{% else %}{{ module.name }}{% endif %} + {% if not config.separate_signature %}{% endif %} + {% if show_full_path %}{{ module.path }}{% else %}{{ module.name }}{% endif %} + {% if not config.separate_signature %}{% endif %} {% with labels = module.labels %} {% include "labels.html" with context %} From 8f4c85328e8b4a45db77f9fc3e536a5008686f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 28 Apr 2022 19:02:03 +0200 Subject: [PATCH 11/27] refactor: Respect `show_root_full_path` for ToC entries (hidden headings) --- .../python/templates/material/_base/attribute.html | 2 +- .../python/templates/material/_base/class.html | 2 +- .../python/templates/material/_base/function.html | 2 +- .../python/templates/material/_base/module.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html b/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html index 527c38fd..684e912f 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html @@ -53,7 +53,7 @@ {% filter heading(heading_level, role="data" if attribute.parent.kind.value == "module" else "attr", id=html_id, - toc_label=attribute.path, + toc_label=attribute.path if config.show_root_full_path else attribute.name, hidden=True) %} {% endfilter %} {% endif %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/class.html b/src/mkdocstrings_handlers/python/templates/material/_base/class.html index 1bc4edfe..30370e3c 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/class.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/class.html @@ -59,7 +59,7 @@ {% filter heading(heading_level, role="class", id=html_id, - toc_label=class.path, + toc_label=class.path if config.show_root_full_path else class.name, hidden=True) %} {% endfilter %} {% endif %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/function.html b/src/mkdocstrings_handlers/python/templates/material/_base/function.html index 13639f57..7c52b37c 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/function.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/function.html @@ -51,7 +51,7 @@ {% filter heading(heading_level, role="function", id=html_id, - toc_label=function.path, + toc_label=function.path if config.show_root_full_path else function.name, hidden=True) %} {% endfilter %} {% endif %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/module.html b/src/mkdocstrings_handlers/python/templates/material/_base/module.html index c602f9b0..baa5eebb 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/module.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/module.html @@ -37,7 +37,7 @@ {% filter heading(heading_level, role="module", id=html_id, - toc_label=module.path, + toc_label=module.path if config.show_root_full_path else module.name, hidden=True) %} {% endfilter %} {% endif %} From 24a6136ee6c04a6a49ee74b20e65177868a10ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 28 Apr 2022 19:06:41 +0200 Subject: [PATCH 12/27] feat: Add members and filters options --- src/mkdocstrings_handlers/python/handler.py | 13 +- src/mkdocstrings_handlers/python/rendering.py | 59 +++++- .../templates/material/_base/attribute.html | 109 +++++------ .../templates/material/_base/children.html | 94 +++++---- .../templates/material/_base/class.html | 181 +++++++++--------- .../templates/material/_base/function.html | 117 ++++++----- .../templates/material/_base/module.html | 99 +++++----- tests/test_rendering.py | 31 +++ 8 files changed, 400 insertions(+), 303 deletions(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 029769c7..66dd1845 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -4,6 +4,7 @@ import os import posixpath +import re import sys from collections import ChainMap from contextlib import suppress @@ -75,6 +76,8 @@ class PythonHandler(BaseHandler): "heading_level": 2, "members_order": rendering.Order.alphabetical.value, "docstring_section_style": "table", + "members": None, + "filters": ["!^_[^_]"], } """ Attributes: Default rendering options: @@ -97,6 +100,9 @@ class PythonHandler(BaseHandler): heading_level (int): The initial heading level to use. Default: `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. Default: `alphabetical`. docstring_section_style (str): The style used to render docstring sections. Options: `table`, `list`, `spacy`. Default: `table`. + members (list[str] | False | None): An explicit list of members to render. Default: `None`. + filters (list[str] | None): A list of filters applied to filter objects based on their name. + A filter starting with `!` will exclude matching objects instead of including them. Default: `["!^_[^_]"]`. """ # noqa: E501 def __init__( @@ -222,6 +228,11 @@ def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignor choices = "', '".join(item.value for item in rendering.Order) raise PluginError(f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.") + if final_config["filters"]: + final_config["filters"] = [ + (re.compile(filtr.lstrip("!")), filtr.startswith("!")) for filtr in final_config["filters"] + ] + return template.render( **{"config": final_config, data.kind.value: data, "heading_level": heading_level, "root": True}, ) @@ -236,7 +247,7 @@ def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore 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 - self.env.filters["filter_docstrings"] = rendering.do_filter_docstrings + self.env.filters["filter_objects"] = rendering.do_filter_objects def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore missing docstring) try: diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index e920a076..8e5f7d85 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -6,7 +6,7 @@ import re import sys from functools import lru_cache -from typing import Any, Sequence +from typing import Any, Pattern, Sequence from griffe.dataclasses import Alias, Object from markupsafe import Markup @@ -77,16 +77,28 @@ def do_format_signature(signature: str, line_length: int) -> str: return formatted[4:-5].strip()[:-1] -def do_order_members(members: Sequence[Object | Alias], order: Order) -> Sequence[Object | Alias]: +def do_order_members( + members: Sequence[Object | Alias], + order: Order, + members_list: list[str] | None, +) -> Sequence[Object | Alias]: """Order members given an ordering method. Parameters: members: The members to order. order: The ordering method. + members_list: An optional member list (manual ordering). Returns: The same members, ordered. """ + if members_list: + sorted_members = [] + members_dict = {member.name: member for member in members} + for name in members_list: + if name in members_dict: + sorted_members.append(members_dict[name]) + return sorted_members return sorted(members, key=order_map[order]) @@ -133,22 +145,53 @@ def repl(match): # noqa: WPS430 return Markup(text).format(**variables) -def do_filter_docstrings( +def _keep_object(name, filters): + keep = None + rules = set() + for regex, exclude in filters: + rules.add(exclude) + if regex.search(name): + keep = not exclude + if keep is None: + if rules == {False}: # noqa: WPS531 + # only included stuff, no match = reject + return False + # only excluded stuff, or included and excluded stuff, no match = keep + return True + return keep + + +def do_filter_objects( objects_dictionary: dict[str, Object | Alias], - keep_empty: bool = True, + filters: list[tuple[bool, Pattern]] | None = None, + members_list: list[str] | None = None, + keep_no_docstrings: bool = True, ) -> list[Object | Alias]: """Filter a dictionary of objects based on their docstrings. Parameters: objects_dictionary: The dictionary of objects. - keep_empty: Whether to keep objects with no/empty docstrings (recursive check). + filters: Filters to apply, based on members' names. + Each element is a tuple: a pattern, and a boolean indicating whether + to reject the object if the pattern matches. + members_list: An optional, explicit list of members to keep. + When given and empty, return an empty list. + When given and not empty, ignore filters and docstrings presence/absence. + keep_no_docstrings: Whether to keep objects with no/empty docstrings (recursive check). Returns: A list of objects. """ - if keep_empty: - return list(objects_dictionary.values()) - return [obj for obj in objects_dictionary.values() if obj.has_docstrings] + if members_list is not None: + if not members_list: + return [] + return [obj for obj in objects_dictionary.values() if obj.name in set(members_list)] + objects = list(objects_dictionary.values()) + if filters: + objects = [obj for obj in objects if _keep_object(obj.name, filters)] + if keep_no_docstrings: + return objects + return [obj for obj in objects if obj.has_docstrings] @lru_cache(maxsize=1) diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html b/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html index 684e912f..779b958c 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html @@ -1,72 +1,69 @@ {{ log.debug("Rendering " + attribute.path) }} -{% if config.show_if_no_docstring or attribute.has_docstrings %} -
- {% with html_id = attribute.path %} +
+{% with html_id = attribute.path %} - {% if not root or config.show_root_heading %} + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} - {% else %} - {% set show_full_path = config.show_object_full_path %} - {% endif %} - - {% filter heading(heading_level, - role="data" if attribute.parent.kind.value == "module" else "attr", - id=html_id, - class="doc doc-heading", - toc_label=attribute.name) %} + {% if not root or config.show_root_heading %} - {% if config.separate_signature %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} - {% else %} - {% filter highlight(language="python", inline=True) %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} - {% if attribute.annotation %}: {{ attribute.annotation }}{% endif %} - {% if attribute.value %} = {{ attribute.value }}{% endif %} - {% endfilter %} - {% endif %} - - {% with labels = attribute.labels %} - {% include "labels.html" with context %} - {% endwith %} - - {% endfilter %} + {% filter heading(heading_level, + role="data" if attribute.parent.kind.value == "module" else "attr", + id=html_id, + class="doc doc-heading", + toc_label=attribute.name) %} {% if config.separate_signature %} - {% filter highlight(language="python", inline=False) %} - {% filter format_code(config.line_length) %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} - {% if attribute.annotation %}: {{ attribute.annotation|safe }}{% endif %} - {% if attribute.value %} = {{ attribute.value|safe }}{% endif %} - {% endfilter %} + {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} + {% else %} + {% filter highlight(language="python", inline=True) %} + {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} + {% if attribute.annotation %}: {{ attribute.annotation }}{% endif %} + {% if attribute.value %} = {{ attribute.value }}{% endif %} {% endfilter %} {% endif %} - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="data" if attribute.parent.kind.value == "module" else "attr", - id=html_id, - toc_label=attribute.path if config.show_root_full_path else attribute.name, - hidden=True) %} + {% with labels = attribute.labels %} + {% include "labels.html" with context %} + {% endwith %} + + {% endfilter %} + + {% if config.separate_signature %} + {% filter highlight(language="python", inline=False) %} + {% filter format_code(config.line_length) %} + {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} + {% if attribute.annotation %}: {{ attribute.annotation|safe }}{% endif %} + {% if attribute.value %} = {{ attribute.value|safe }}{% endif %} {% endfilter %} - {% endif %} - {% set heading_level = heading_level - 1 %} + {% endfilter %} {% endif %} -
- {% with docstring_sections = attribute.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} -
+ {% else %} + {% if config.show_root_toc_entry %} + {% filter heading(heading_level, + role="data" if attribute.parent.kind.value == "module" else "attr", + id=html_id, + toc_label=attribute.path if config.show_root_full_path else attribute.name, + hidden=True) %} + {% endfilter %} + {% endif %} + {% set heading_level = heading_level - 1 %} + {% endif %} - {% endwith %} +
+ {% with docstring_sections = attribute.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %}
-{% endif %} +{% endwith %} +
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/children.html b/src/mkdocstrings_handlers/python/templates/material/_base/children.html index d6fc61d6..eac782cb 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/children.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/children.html @@ -3,6 +3,12 @@
+ {% if root_members %} + {% set members_list = config.members %} + {% else %} + {% set members_list = none %} + {% endif %} + {% if config.group_by_category %} {% with %} @@ -13,51 +19,67 @@ {% set extra_level = 0 %} {% endif %} - {% if config.show_category_heading and obj.attributes|filter_docstrings(config.show_if_no_docstring) %} - {% filter heading(heading_level, id=html_id ~ "-attributes") %}Attributes{% endfilter %} - {% endif %} - {% with heading_level = heading_level + extra_level %} - {% for attribute in obj.attributes.values()|order_members(config.members_order) %} - {% if not attribute.is_alias or attribute.is_explicitely_exported %} - {% include "attribute.html" with context %} + {% with attributes = obj.attributes|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% if attributes %} + {% if config.show_category_heading %} + {% filter heading(heading_level, id=html_id ~ "-attributes") %}Attributes{% endfilter %} {% endif %} - {% endfor %} + {% with heading_level = heading_level + extra_level %} + {% for attribute in attributes|order_members(config.members_order, members_list) %} + {% if not attribute.is_alias or attribute.is_explicitely_exported %} + {% include "attribute.html" with context %} + {% endif %} + {% endfor %} + {% endwith %} + {% endif %} {% endwith %} - {% if config.show_category_heading and obj.classes|filter_docstrings(config.show_if_no_docstring) %} - {% filter heading(heading_level, id=html_id ~ "-classes") %}Classes{% endfilter %} - {% endif %} - {% with heading_level = heading_level + extra_level %} - {% for class in obj.classes.values()|order_members(config.members_order) %} - {% if not class.is_alias or class.is_explicitely_exported %} - {% include "class.html" with context %} + {% with classes = obj.classes|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% if classes %} + {% if config.show_category_heading %} + {% filter heading(heading_level, id=html_id ~ "-classes") %}Classes{% endfilter %} {% endif %} - {% endfor %} + {% with heading_level = heading_level + extra_level %} + {% for class in classes|order_members(config.members_order, members_list) %} + {% if not class.is_alias or class.is_explicitely_exported %} + {% include "class.html" with context %} + {% endif %} + {% endfor %} + {% endwith %} + {% endif %} {% endwith %} - {% if config.show_category_heading and obj.functions|filter_docstrings(config.show_if_no_docstring) %} - {% filter heading(heading_level, id=html_id ~ "-functions") %}Functions{% endfilter %} - {% endif %} - {% with heading_level = heading_level + extra_level %} - {% for function in obj.functions.values()|order_members(config.members_order) %} - {% if not (obj.kind.value == "class" and function.name == "__init__" and config.merge_init_into_class) %} - {% if not function.is_alias or function.is_explicitely_exported %} - {% include "function.html" with context %} - {% endif %} + {% with functions = obj.functions|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% if functions %} + {% if config.show_category_heading %} + {% filter heading(heading_level, id=html_id ~ "-functions") %}Functions{% endfilter %} {% endif %} - {% endfor %} + {% with heading_level = heading_level + extra_level %} + {% for function in functions|order_members(config.members_order, members_list) %} + {% if not (obj.kind.value == "class" and function.name == "__init__" and config.merge_init_into_class) %} + {% if not function.is_alias or function.is_explicitely_exported %} + {% include "function.html" with context %} + {% endif %} + {% endif %} + {% endfor %} + {% endwith %} + {% endif %} {% endwith %} {% if config.show_submodules %} - {% if config.show_category_heading and obj.modules|filter_docstrings(config.show_if_no_docstring) %} - {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %} - {% endif %} - {% with heading_level = heading_level + extra_level %} - {% for module in obj.modules.values()|order_members(config.members_order) %} - {% if not module.is_alias or module.is_explicitely_exported %} - {% include "module.html" with context %} + {% with modules = obj.modules|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% if modules %} + {% if config.show_category_heading %} + {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %} {% endif %} - {% endfor %} + {% with heading_level = heading_level + extra_level %} + {% for module in modules|order_members(config.members_order, members_list) %} + {% if not module.is_alias or module.is_explicitely_exported %} + {% include "module.html" with context %} + {% endif %} + {% endfor %} + {% endwith %} + {% endif %} {% endwith %} {% endif %} @@ -65,7 +87,9 @@ {% else %} - {% for child in obj.members.values()|order_members(config.members_order) %} + {% for child in obj.members| + filter_objects(config.filters, members_list, config.show_if_no_docstring)| + order_members(config.members_order, members_list) %} {% if not (obj.kind.value == "class" and child.name == "__init__" and config.merge_init_into_class) %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/class.html b/src/mkdocstrings_handlers/python/templates/material/_base/class.html index 30370e3c..580cf708 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/class.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/class.html @@ -1,116 +1,113 @@ {{ log.debug("Rendering " + class.path) }} -{% if config.show_if_no_docstring or class.has_docstrings %} -
- {% with html_id = class.path %} - - {% if not root or config.show_root_heading %} - - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} +
+{% with html_id = class.path %} + + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} + + {% if not root or config.show_root_heading %} + + {% filter heading(heading_level, + role="class", + id=html_id, + class="doc doc-heading", + toc_label=class.name) %} + + {% if config.separate_signature %} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} + {% elif config.merge_init_into_class and "__init__" in class.members -%} + {%- with function = class.members["__init__"] -%} + {%- filter highlight(language="python", inline=True) -%} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} + {%- include "signature.html" with context -%} + {%- endfilter -%} + {%- endwith -%} {% else %} - {% set show_full_path = config.show_object_full_path %} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} {% endif %} - {% filter heading(heading_level, - role="class", - id=html_id, - class="doc doc-heading", - toc_label=class.name) %} + {% with labels = class.labels %} + {% include "labels.html" with context %} + {% endwith %} - {% if config.separate_signature %} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} - {% elif config.merge_init_into_class and "__init__" in class.members -%} - {%- with function = class.members["__init__"] -%} - {%- filter highlight(language="python", inline=True) -%} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} - {%- include "signature.html" with context -%} - {%- endfilter -%} - {%- endwith -%} - {% else %} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} - {% endif %} + {% endfilter %} - {% with labels = class.labels %} - {% include "labels.html" with context %} + {% if config.separate_signature and config.merge_init_into_class %} + {% if "__init__" in class.members %} + {% with function = class.members["__init__"] %} + {% filter highlight(language="python", inline=False) %} + {% filter format_signature(config.line_length) %} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} + {% include "signature.html" with context %} + {% endfilter %} + {% endfilter %} {% endwith %} + {% endif %} + {% endif %} + {% else %} + {% if config.show_root_toc_entry %} + {% filter heading(heading_level, + role="class", + id=html_id, + toc_label=class.path if config.show_root_full_path else class.name, + hidden=True) %} {% endfilter %} + {% endif %} + {% set heading_level = heading_level - 1 %} + {% endif %} + +
+ {% if config.show_bases and class.bases %} +

+ Bases: {% for expression in class.bases -%} + {% include "expression.html" with context %}{% if not loop.last %}, {% endif %} + {% endfor -%} +

+ {% endif %} - {% if config.separate_signature and config.merge_init_into_class %} - {% if "__init__" in class.members %} - {% with function = class.members["__init__"] %} - {% filter highlight(language="python", inline=False) %} - {% filter format_signature(config.line_length) %} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} - {% endfilter %} - {% endwith %} - {% endif %} - {% endif %} + {% with docstring_sections = class.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="class", - id=html_id, - toc_label=class.path if config.show_root_full_path else class.name, - hidden=True) %} - {% endfilter %} + {% if config.merge_init_into_class %} + {% if "__init__" in class.members and class.members["__init__"].has_docstring %} + {% with docstring_sections = class.members["__init__"].docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} {% endif %} - {% set heading_level = heading_level - 1 %} {% endif %} -
- {% if config.show_bases and class.bases %} -

- Bases: {% for expression in class.bases -%} - {% include "expression.html" with context %}{% if not loop.last %}, {% endif %} - {% endfor -%} -

- {% endif %} - - {% with docstring_sections = class.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} - + {% if config.show_source %} {% if config.merge_init_into_class %} - {% if "__init__" in class.members and class.members["__init__"].has_docstring %} - {% with docstring_sections = class.members["__init__"].docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} - {% endif %} - {% endif %} - - {% if config.show_source %} - {% if config.merge_init_into_class %} - {% if "__init__" in class.members and class.members["__init__"].source %} -
- Source code in {{ class.relative_filepath }} - {{ class.members["__init__"].source|highlight(language="python", linestart=class.members["__init__"].lineno, linenums=True) }} -
- {% endif %} - {% elif class.source %} + {% if "__init__" in class.members and class.members["__init__"].source %}
Source code in {{ class.relative_filepath }} - {{ class.source|highlight(language="python", linestart=class.lineno, linenums=True) }} + {{ class.members["__init__"].source|highlight(language="python", linestart=class.members["__init__"].lineno, linenums=True) }}
{% endif %} + {% elif class.source %} +
+ Source code in {{ class.relative_filepath }} + {{ class.source|highlight(language="python", linestart=class.lineno, linenums=True) }} +
{% endif %} + {% endif %} - {% with obj = class %} - {% set root = False %} - {% set heading_level = heading_level + 1 %} - {% include "children.html" with context %} - {% endwith %} -
- - {% endwith %} + {% with obj = class %} + {% set root = False %} + {% set heading_level = heading_level + 1 %} + {% include "children.html" with context %} + {% endwith %}
-{% endif %} +{% endwith %} +
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/function.html b/src/mkdocstrings_handlers/python/templates/material/_base/function.html index 7c52b37c..9d5288b1 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/function.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/function.html @@ -1,77 +1,74 @@ {{ log.debug("Rendering " + function.path) }} -{% if config.show_if_no_docstring or function.has_docstrings %} -
- {% with html_id = function.path %} +
+{% with html_id = function.path %} - {% if not root or config.show_root_heading %} + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} - {% else %} - {% set show_full_path = config.show_object_full_path %} - {% endif %} + {% if not root or config.show_root_heading %} - {% filter heading(heading_level, - role="function", - id=html_id, - class="doc doc-heading", - toc_label=function.name ~ "()") %} - - {% if config.separate_signature %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% else %} - {% filter highlight(language="python", inline=True) %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} - {% endif %} - - {% with labels = function.labels %} - {% include "labels.html" with context %} - {% endwith %} - - {% endfilter %} + {% filter heading(heading_level, + role="function", + id=html_id, + class="doc doc-heading", + toc_label=function.name ~ "()") %} {% if config.separate_signature %} - {% filter highlight(language="python", inline=False) %} - {% filter format_signature(config.line_length) %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} + {% else %} + {% filter highlight(language="python", inline=True) %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} + {% include "signature.html" with context %} {% endfilter %} {% endif %} - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="function", - id=html_id, - toc_label=function.path if config.show_root_full_path else function.name, - hidden=True) %} + {% with labels = function.labels %} + {% include "labels.html" with context %} + {% endwith %} + + {% endfilter %} + + {% if config.separate_signature %} + {% filter highlight(language="python", inline=False) %} + {% filter format_signature(config.line_length) %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} + {% include "signature.html" with context %} {% endfilter %} - {% endif %} - {% set heading_level = heading_level - 1 %} + {% endfilter %} {% endif %} -
- {% with docstring_sections = function.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} + {% else %} + {% if config.show_root_toc_entry %} + {% filter heading(heading_level, + role="function", + id=html_id, + toc_label=function.path if config.show_root_full_path else function.name, + hidden=True) %} + {% endfilter %} + {% endif %} + {% set heading_level = heading_level - 1 %} + {% endif %} - {% if config.show_source and function.source %} -
- Source code in {{ function.relative_filepath }} - {{ function.source|highlight(language="python", linestart=function.lineno, linenums=True) }} -
- {% endif %} -
+
+ {% with docstring_sections = function.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} - {% endwith %} + {% if config.show_source and function.source %} +
+ Source code in {{ function.relative_filepath }} + {{ function.source|highlight(language="python", linestart=function.lineno, linenums=True) }} +
+ {% endif %}
-{% endif %} +{% endwith %} +
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/module.html b/src/mkdocstrings_handlers/python/templates/material/_base/module.html index baa5eebb..cd5d39be 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/module.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/module.html @@ -1,62 +1,59 @@ {{ log.debug("Rendering " + module.path) }} -{% if config.show_if_no_docstring or module.has_docstrings %} -
- {% with html_id = module.path %} - - {% if not root or config.show_root_heading %} +
+{% with html_id = module.path %} + + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} + + {% if not root or config.show_root_heading %} + + {% filter heading(heading_level, + role="module", + id=html_id, + class="doc doc-heading", + toc_label=module.name) %} + + {% if not config.separate_signature %}{% endif %} + {% if show_full_path %}{{ module.path }}{% else %}{{ module.name }}{% endif %} + {% if not config.separate_signature %}{% endif %} + + {% with labels = module.labels %} + {% include "labels.html" with context %} + {% endwith %} - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} - {% else %} - {% set show_full_path = config.show_object_full_path %} - {% endif %} + {% endfilter %} + {% else %} + {% if config.show_root_toc_entry %} {% filter heading(heading_level, role="module", id=html_id, - class="doc doc-heading", - toc_label=module.name) %} - - {% if not config.separate_signature %}{% endif %} - {% if show_full_path %}{{ module.path }}{% else %}{{ module.name }}{% endif %} - {% if not config.separate_signature %}{% endif %} - - {% with labels = module.labels %} - {% include "labels.html" with context %} - {% endwith %} - + toc_label=module.path if config.show_root_full_path else module.name, + hidden=True) %} {% endfilter %} - - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="module", - id=html_id, - toc_label=module.path if config.show_root_full_path else module.name, - hidden=True) %} - {% endfilter %} - {% endif %} - {% set heading_level = heading_level - 1 %} {% endif %} - -
- {% with docstring_sections = module.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} - - {% with obj = module %} - {% set root = False %} - {% set heading_level = heading_level + 1 %} - {% include "children.html" with context %} - {% endwith %} -
- - {% endwith %} + {% set heading_level = heading_level - 1 %} + {% endif %} + +
+ {% with docstring_sections = module.docstring.parsed %} + {% include "docstring.html" with context %} + {% endwith %} + + {% with obj = module %} + {% set root = False %} + {% set heading_level = heading_level + 1 %} + {% include "children.html" with context %} + {% endwith %}
-{% endif %} +{% endwith %} +
diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 3b59505a..533eaf34 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -1,5 +1,10 @@ """Tests for the `rendering` module.""" +import re +from dataclasses import dataclass + +import pytest + from mkdocstrings_handlers.python import rendering @@ -9,3 +14,29 @@ def test_format_code_and_signature(): assert rendering.do_format_code('print("Hello")', 100) assert rendering.do_format_signature("(param: str = 'hello') -> 'Class'", 100) assert rendering.do_format_signature('(param: str = "hello") -> "Class"', 100) + + +@dataclass +class _FakeObject: + name: str + + +@pytest.mark.parametrize( + ("names", "filter_params", "expected_names"), + [ + (["aa", "ab", "ac", "da"], {"filters": [(re.compile("^a[^b]"), True)]}, {"ab", "da"}), + (["aa", "ab", "ac", "da"], {"members_list": ["aa", "ab"]}, {"aa", "ab"}), + ], +) +def test_filter_objects(names, filter_params, expected_names): + """Assert the objects filter works correctly. + + Parameters: + names: Names of the objects. + filter_params: Parameters passed to the filter function. + expected_names: Names expected to be kept. + """ + objects = {name: _FakeObject(name) for name in names} + filtered = rendering.do_filter_objects(objects, **filter_params) + filtered_names = {obj.name for obj in filtered} + assert set(filtered_names) == set(expected_names) From 7e17fc41fda90d1d91d021ed53d3405f6c057b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 28 Apr 2022 19:06:55 +0200 Subject: [PATCH 13/27] docs: Update docs --- mkdocs.yml | 7 +++++-- src/mkdocstrings_handlers/python/handler.py | 16 ++-------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 0d35a92b..1174a294 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,17 +73,20 @@ plugins: - mkdocstrings: handlers: python: + paths: [src] import: - https://docs.python.org/3/objects.inv - https://mkdocstrings.github.io/objects.inv - https://mkdocstrings.github.io/griffe/objects.inv - selection: + options: docstring_style: google docstring_options: ignore_init_summary: yes - rendering: show_submodules: no merge_init_into_class: yes + separate_signature: yes + show_source: no + show_root_full_path: no watch: - src/mkdocstrings_handlers diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 66dd1845..25f65131 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -142,7 +142,7 @@ def load_inventory( ) -> Iterator[Tuple[str, str]]: """Yield items and their URLs from an inventory file streamed from `in_file`. - This implements mkdocstrings' `load_inventory` "protocol" (see plugin.py). + This implements mkdocstrings' `load_inventory` "protocol" (see [`mkdocstrings.plugin`][mkdocstrings.plugin]). Arguments: in_file: The binary file-like object to read the inventory from. @@ -159,19 +159,7 @@ 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. - """ + def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: D102,WPS231 module_name = identifier.split(".", 1)[0] unknown_module = module_name not in self._modules_collection if config.get("fallback", False) and unknown_module: From aaa79eea40d49a64a69badbe732bf5211fbf055a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 29 Apr 2022 19:31:20 +0200 Subject: [PATCH 14/27] feat: Add Jinja blocks around docstring section styles --- .../templates/material/_base/docstring/attributes.html | 6 ++++++ .../material/_base/docstring/other_parameters.html | 6 ++++++ .../templates/material/_base/docstring/parameters.html | 6 ++++++ .../python/templates/material/_base/docstring/raises.html | 6 ++++++ .../python/templates/material/_base/docstring/receives.html | 6 ++++++ .../python/templates/material/_base/docstring/returns.html | 6 ++++++ .../python/templates/material/_base/docstring/warns.html | 6 ++++++ .../python/templates/material/_base/docstring/yields.html | 6 ++++++ 8 files changed, 48 insertions(+) diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html index 5d0dc85f..9c80b827 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering attributes section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Attributes:" }}

@@ -25,7 +26,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Attributes:" }}

    {% for attribute in section.value %} @@ -40,7 +43,9 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} @@ -69,4 +74,5 @@ {% endfor %}
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html index 709a799c..9a42d41d 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering other parameters section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Other Parameters:" }}

@@ -25,7 +26,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Other Parameters:" }}

    {% for parameter in section.value %} @@ -40,7 +43,9 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} @@ -69,4 +74,5 @@ {% endfor %}
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html index 4a28ae1c..6665dd77 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering parameters section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Parameters:" }}

@@ -35,7 +36,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Parameters:" }}

    {% for parameter in section.value %} @@ -50,7 +53,9 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} @@ -87,4 +92,5 @@ {% endfor %}
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html index 71d76e1f..5d02fd32 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering raises section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Raises:" }}

@@ -23,7 +24,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Raises:" }}

    {% for raises in section.value %} @@ -38,7 +41,9 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} @@ -63,4 +68,5 @@ {% endfor %}
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html index 0a6c5b5c..61f2b48b 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering receives section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %} {% set name_column = section.value|selectattr("name")|any %}

{{ section.title or "Receives:" }}

@@ -26,7 +27,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Receives:" }}

    {% for receives in section.value %} @@ -43,7 +46,9 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} @@ -82,4 +87,5 @@ {% endfor %}
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html index b62c809a..a1c9c01d 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering returns section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %} {% set name_column = section.value|selectattr("name")|any %}

{{ section.title or "Returns:" }}

@@ -26,7 +27,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Returns:" }}

    {% for returns in section.value %} @@ -43,7 +46,9 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} @@ -82,4 +87,5 @@ {% endfor %}
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html index 4b2d6865..25a39b68 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering warns section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %}

{{ section.title or "Warns:" }}

@@ -23,7 +24,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Warns:" }}

    {% for warns in section.value %} @@ -38,7 +41,9 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} @@ -63,4 +68,5 @@ {% endfor %}
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html index b24ce805..891e67da 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html @@ -1,5 +1,6 @@ {{ log.debug("Rendering yields section") }} {% if config.docstring_section_style == "table" %} + {% block table_style %} {% set name_column = section.value|selectattr("name")|any %}

{{ section.title or "Yields:" }}

@@ -26,7 +27,9 @@ {% endfor %}
+ {% endblock table_style %} {% elif config.docstring_section_style == "list" %} + {% block list_style %}

{{ section.title or "Yields:" }}

    {% for yields in section.value %} @@ -43,7 +46,9 @@ {% endfor %}
+ {% endblock list_style %} {% elif config.docstring_section_style == "spacy" %} + {% block spacy_style %} @@ -82,4 +87,5 @@ {% endfor %}
+ {% endblock spacy_style %} {% endif %} \ No newline at end of file From 8fed314243e3981fc7b527c69cee628e87b10220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 29 Apr 2022 19:32:27 +0200 Subject: [PATCH 15/27] refactor: Reduce number of template debug logs --- .../python/templates/material/_base/children.html | 2 +- .../python/templates/material/_base/docstring.html | 2 +- .../python/templates/material/_base/labels.html | 2 +- .../python/templates/material/_base/signature.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/children.html b/src/mkdocstrings_handlers/python/templates/material/_base/children.html index eac782cb..71755ea7 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/children.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/children.html @@ -1,5 +1,5 @@ -{{ log.debug("Rendering children of " + obj.path) }} {% if obj.members %} + {{ log.debug("Rendering children of " + obj.path) }}
diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html index b473a8f8..bd1b6963 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html @@ -1,5 +1,5 @@ -{{ log.debug("Rendering docstring") }} {% if docstring_sections %} + {{ log.debug("Rendering docstring") }} {% for section in docstring_sections %} {% if section.kind.value == "text" %} {{ section.value|convert_markdown(heading_level, html_id) }} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/labels.html b/src/mkdocstrings_handlers/python/templates/material/_base/labels.html index a7e8ec38..e9c20f12 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/labels.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/labels.html @@ -1,5 +1,5 @@ -{{ log.debug("Rendering labels") }} {% if labels %} + {{ log.debug("Rendering labels") }} {% for label in labels %} {{ label }} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/signature.html b/src/mkdocstrings_handlers/python/templates/material/_base/signature.html index fb116880..ca4bbd44 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/signature.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/signature.html @@ -1,5 +1,5 @@ -{{ log.debug("Rendering signature of " + function.path) }} {%- if config.show_signature -%} + {{ log.debug("Rendering signature") }} {%- with -%} {%- set ns = namespace(render_pos_only_separator=True, render_kw_only_separator=True, equal="=") -%} From 0822ff9d3ffd3fb71fb619a8b557160661eff9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 29 Apr 2022 19:37:13 +0200 Subject: [PATCH 16/27] feat: Wrap objects names in spans to allow custom styling Spans are only used when signatures are separated. Issue mkdocstrings/mkdocstrings#240: https://github.com/mkdocstrings/mkdocstrings/issues/240 --- .../python/templates/material/_base/attribute.html | 2 +- .../python/templates/material/_base/class.html | 2 +- .../python/templates/material/_base/function.html | 2 +- .../python/templates/material/_base/module.html | 10 +++++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html b/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html index 779b958c..019e7fae 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/attribute.html @@ -22,7 +22,7 @@ toc_label=attribute.name) %} {% if config.separate_signature %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} + {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} {% else %} {% filter highlight(language="python", inline=True) %} {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/class.html b/src/mkdocstrings_handlers/python/templates/material/_base/class.html index 580cf708..ff102c88 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/class.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/class.html @@ -22,7 +22,7 @@ toc_label=class.name) %} {% if config.separate_signature %} - {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} + {% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %} {% elif config.merge_init_into_class and "__init__" in class.members -%} {%- with function = class.members["__init__"] -%} {%- filter highlight(language="python", inline=True) -%} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/function.html b/src/mkdocstrings_handlers/python/templates/material/_base/function.html index 9d5288b1..b9b1696c 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/function.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/function.html @@ -22,7 +22,7 @@ toc_label=function.name ~ "()") %} {% if config.separate_signature %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} + {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} {% else %} {% filter highlight(language="python", inline=True) %} {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/module.html b/src/mkdocstrings_handlers/python/templates/material/_base/module.html index cd5d39be..8e14d354 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/module.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/module.html @@ -21,9 +21,13 @@ class="doc doc-heading", toc_label=module.name) %} - {% if not config.separate_signature %}{% endif %} - {% if show_full_path %}{{ module.path }}{% else %}{{ module.name }}{% endif %} - {% if not config.separate_signature %}{% endif %} + {% with module_name = module.path if show_full_path else module.name %} + {% if config.separate_signature %} + {{ module_name }} + {% else %} + {{ module_name }} + {% endif %} + {% endwith %} {% with labels = module.labels %} {% include "labels.html" with context %} From fe16b54aea60473575343e3a3c428567b701bd7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 30 Apr 2022 12:16:05 +0200 Subject: [PATCH 17/27] feat: Use sections titles in SpaCy-styled docstrings --- .../python/templates/material/_base/docstring/attributes.html | 2 +- .../templates/material/_base/docstring/other_parameters.html | 2 +- .../python/templates/material/_base/docstring/parameters.html | 2 +- .../python/templates/material/_base/docstring/raises.html | 2 +- .../python/templates/material/_base/docstring/receives.html | 2 +- .../python/templates/material/_base/docstring/returns.html | 2 +- .../python/templates/material/_base/docstring/warns.html | 2 +- .../python/templates/material/_base/docstring/yields.html | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html index 9c80b827..9a1409c0 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/attributes.html @@ -49,7 +49,7 @@ - + diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html index 9a42d41d..4b9f7339 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html @@ -49,7 +49,7 @@
ATTRIBUTE{{ (section.title or "ATTRIBUTE").rstrip(":").upper() }} DESCRIPTION
- + diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html index 6665dd77..cc954596 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html @@ -59,7 +59,7 @@
PARAMETER{{ (section.title or "PARAMETER").rstrip(":").upper() }} DESCRIPTION
- + diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html index 5d02fd32..32ad3506 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html @@ -47,7 +47,7 @@
PARAMETER{{ (section.title or "PARAMETER").rstrip(":").upper() }} DESCRIPTION
- + diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html index 61f2b48b..7946329b 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html @@ -52,7 +52,7 @@
RAISES{{ (section.title or "RAISES").rstrip(":").upper() }} DESCRIPTION
- + diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html index a1c9c01d..0d620d12 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/returns.html @@ -52,7 +52,7 @@
RECEIVES{{ (section.title or "RECEIVES").rstrip(":").upper() }} DESCRIPTION
- + diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html index 25a39b68..e5a59a84 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/warns.html @@ -47,7 +47,7 @@
RETURNS{{ (section.title or "RETURNS").rstrip(":").upper() }} DESCRIPTION
- + diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html index 891e67da..22d6828f 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring/yields.html @@ -52,7 +52,7 @@
WARNS{{ (section.title or "WARNS").rstrip(":").upper() }} DESCRIPTION
- + From b6c989315fb028813a919319ad1818b0b1f597ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 30 Apr 2022 12:38:57 +0200 Subject: [PATCH 18/27] feat: Add config option for annotations paths verbosity --- src/mkdocstrings_handlers/python/handler.py | 2 ++ .../python/templates/material/_base/expression.html | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 25f65131..591a3129 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -78,6 +78,7 @@ class PythonHandler(BaseHandler): "docstring_section_style": "table", "members": None, "filters": ["!^_[^_]"], + "annotations_path": "brief", } """ Attributes: Default rendering options: @@ -88,6 +89,7 @@ class PythonHandler(BaseHandler): show_object_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. Default: `False`. show_category_heading (bool): When grouped by categories, show a heading for each category. Default: `False`. show_if_no_docstring (bool): Show the object heading even if it has no docstring or children with docstrings. Default: `False`. + annotations_path: The verbosity for annotations path: `brief` (recommended), or `source` (as written in the source). Default: `"brief"`. show_signature (bool): Show method and function signatures. Default: `True`. show_signature_annotations (bool): Show the type annotations in method and function signatures. Default: `False`. separate_signature (bool): Whether to put the whole signature in a code block below the heading. Default: `False`. diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/expression.html b/src/mkdocstrings_handlers/python/templates/material/_base/expression.html index 76a50da7..3347e272 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/expression.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/expression.html @@ -6,5 +6,7 @@ {%- elif original_expression is string -%} {{ original_expression }} {%- else -%} - {{ original_expression.source }} + {%- with annotation = original_expression|attr(config.annotations_path) -%} + {{ annotation }} + {%- endwith -%} {%- endif -%} From 347ce76d074c0e3841df2d5162b54d3938d00453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 30 Apr 2022 12:40:13 +0200 Subject: [PATCH 19/27] refactor: Merge default configuration options in handler --- docs/usage.md | 2 +- src/mkdocstrings_handlers/python/handler.py | 56 +++++++++++---------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 51e6dd6b..0f735825 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -41,7 +41,7 @@ Option | Description ### Rendering -::: mkdocstrings_handlers.python.handler.PythonHandler.default_rendering_config +::: mkdocstrings_handlers.python.handler.PythonHandler.default_config rendering: show_root_toc_entry: false diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 591a3129..1ca452e6 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -38,25 +38,17 @@ class PythonHandler(BaseHandler): 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]. + default_config: The default rendering options, + see [`default_config`][mkdocstrings_handlers.python.handler.PythonHandler.default_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 = { + default_config: dict = { + "docstring_style": "google", + "docstring_options": {}, "show_root_heading": False, "show_root_toc_entry": True, "show_root_full_path": True, @@ -81,30 +73,40 @@ class PythonHandler(BaseHandler): "annotations_path": "brief", } """ - Attributes: Default rendering options: + Attributes: Headings options: + heading_level (int): The initial heading level to use. Default: `2`. show_root_heading (bool): Show the heading of the object at the root of the documentation tree. Default: `False`. show_root_toc_entry (bool): If the root heading is not shown, at least add a ToC entry for it. Default: `True`. show_root_full_path (bool): Show the full Python path for the root object heading. Default: `True`. show_root_members_full_path (bool): Show the full Python path of every object. Default: `False`. show_object_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. Default: `False`. show_category_heading (bool): When grouped by categories, show a heading for each category. Default: `False`. + + Attributes: Members options: + members (list[str] | False | None): An explicit list of members to render. Default: `None`. + filters (list[str] | None): A list of filters applied to filter objects based on their name. + A filter starting with `!` will exclude matching objects instead of including them. Default: `["!^_[^_]"]`. + group_by_category (bool): Group the object's children by categories: attributes, classes, functions, methods, and modules. Default: `True`. + show_submodules (bool): When rendering a module, show its submodules recursively. Default: `True`. + 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. Default: `"alphabetical"`. + + Attributes: Docstrings options: + docstring_style (str): The docstring style to use: `google`, `numpy`, `sphinx`, or `None`. Default: `"google"`. + docstring_options (dict): The options for the docstring parser. See parsers under [`griffe.docstrings`][]. + docstring_section_style (str): The style used to render docstring sections. Options: `table`, `list`, `spacy`. Default: `"table"`. + line_length (int): Maximum line length when formatting code. Default: `60`. + merge_init_into_class (bool): Whether to merge the `__init__` method into the class' signature and docstring. Default: `False`. show_if_no_docstring (bool): Show the object heading even if it has no docstring or children with docstrings. Default: `False`. + + Attributes: Signature/annotations options: annotations_path: The verbosity for annotations path: `brief` (recommended), or `source` (as written in the source). Default: `"brief"`. show_signature (bool): Show method and function signatures. Default: `True`. show_signature_annotations (bool): Show the type annotations in method and function signatures. Default: `False`. separate_signature (bool): Whether to put the whole signature in a code block below the heading. Default: `False`. - line_length (int): Maximum line length when formatting code. Default: `60`. - merge_init_into_class (bool): Whether to merge the `__init__` method into the class' signature and docstring. Default: `False`. - show_source (bool): Show the source code of this object. Default: `True`. + + Attributes: Additional options: show_bases (bool): Show the base classes of a class. Default: `True`. - show_submodules (bool): When rendering a module, show its submodules recursively. Default: `True`. - group_by_category (bool): Group the object's children by categories: attributes, classes, functions, methods, and modules. Default: `True`. - heading_level (int): The initial heading level to use. Default: `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. Default: `alphabetical`. - docstring_section_style (str): The style used to render docstring sections. Options: `table`, `list`, `spacy`. Default: `table`. - members (list[str] | False | None): An explicit list of members to render. Default: `None`. - filters (list[str] | None): A list of filters applied to filter objects based on their name. - A filter starting with `!` will exclude matching objects instead of including them. Default: `["!^_[^_]"]`. + show_source (bool): Show the source code of this object. Default: `True`. """ # noqa: E501 def __init__( @@ -167,7 +169,7 @@ def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: D102 if config.get("fallback", False) and unknown_module: raise CollectionError("Not loading additional modules during fallback") - final_config = ChainMap(config, self.default_collection_config) + 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) @@ -204,7 +206,7 @@ def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: D102 return doc_object def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring) - final_config = ChainMap(config, self.default_rendering_config) + final_config = ChainMap(config, self.default_config) template = self.env.get_template(f"{data.kind.value}.html") From bc70245b846d0a3c76d1423f3280179ff73b46b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 30 Apr 2022 12:40:25 +0200 Subject: [PATCH 20/27] docs: Change external links styling --- docs/css/mkdocstrings.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 0d305991..1a806c4d 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -5,18 +5,23 @@ div.doc-contents:not(.first) { } /* Mark external links as such */ -a.autorefs-external:hover::after { +a.autorefs-external::after { /* https://primer.style/octicons/arrow-up-right-24 */ background-image: url('data:image/svg+xml,'); content: ' '; display: inline-block; - position: absolute; + position: relative; + top: 0.1em; margin-left: 0.2em; - margin-top: 0.4em; + margin-right: 0.1em; + /* padding-top: 0.8em; */ height: 1em; width: 1em; border-radius: 100%; + background-color: var(--md-typeset-a-color); +} +a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); } From b77cf3fb7596ea86b13f341d022c1366deca3636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 8 May 2022 11:35:42 +0200 Subject: [PATCH 21/27] docs: Improve documentation --- CONTRIBUTING.md | 30 +- README.md | 70 ++-- docs/css/mkdocstrings.css | 1 - docs/customization.md | 152 ++++++++ docs/logo.png | Bin 0 -> 58828 bytes docs/usage.md | 376 +++++++++++++------- logo.png | 1 + mkdocs.yml | 10 +- pyproject.toml | 1 + src/mkdocstrings_handlers/python/handler.py | 22 +- 10 files changed, 463 insertions(+), 200 deletions(-) create mode 100644 docs/customization.md create mode 100644 docs/logo.png create mode 120000 logo.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 734eb110..ea428541 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,21 +14,21 @@ cd python make setup ``` -!!! note - If it fails for some reason, - you'll need to install - [PDM](https://github.com/pdm-project/pdm) - manually. - - You can install it with: - - ```bash - python3 -m pip install --user pipx - pipx install pdm - ``` - - Now you can try running `make setup` again, - or simply `pdm install`. +> NOTE +> If it fails for some reason, +> you'll need to install +> [PDM](https://github.com/pdm-project/pdm) +> manually. +> +> You can install it with: +> +> ```bash +> python3 -m pip install --user pipx +> pipx install pdm +> ``` +> +> Now you can try running `make setup` again, +> or simply `pdm install`. You now have the dependencies installed. diff --git a/README.md b/README.md index 3cdd2c6d..f446ee8a 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,34 @@ -# mkdocstrings-python - -[![ci](https://github.com/mkdocstrings/python/workflows/ci/badge.svg)](https://github.com/mkdocstrings/python/actions?query=workflow%3Aci) -[![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://mkdocstrings.github.io/python/) -[![pypi version](https://img.shields.io/pypi/v/python.svg)](https://pypi.org/project/python/) -[![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/python) -[![gitter](https://badges.gitter.im/join%20chat.svg)](https://gitter.im/mkdocstrings/python) - -A Python handler for [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings). - - -![mkdocstrings_python_gif](https://user-images.githubusercontent.com/3999221/77157838-7184db80-6aa2-11ea-9f9a-fe77405202de.gif) - -## Requirements - -mkdocstrings-python requires Python 3.7 or above. - -
-To install Python 3.7, I recommend using pyenv. - -```bash -# install pyenv -git clone https://github.com/pyenv/pyenv ~/.pyenv - -# setup pyenv (you should also put these three lines in .bashrc or similar) -export PATH="${HOME}/.pyenv/bin:${PATH}" -export PYENV_ROOT="${HOME}/.pyenv" -eval "$(pyenv init -)" - -# install Python 3.7 -pyenv install 3.7.12 - -# make it available globally -pyenv global system 3.7.12 -``` -
+

mkdocstrings-python

+ +

A Python handler for mkdocstrings.

+ +

+ + ci + + + documentation + + + pypi version + + + gitpod + + + gitter + +

+ +--- + +

## Installation You can install this handler as a *mkdocstrings* extra: -```toml +```toml title="pyproject.toml" # PEP 621 dependencies declaration # adapt to your dependencies manager [project] @@ -50,7 +39,7 @@ dependencies = [ You can also explicitely depend on the handler: -```toml +```toml title="pyproject.toml" # PEP 621 dependencies declaration # adapt to your dependencies manager [project] @@ -59,6 +48,11 @@ dependencies = [ ] ``` +## Preview + + +![mkdocstrings_python_gif](https://user-images.githubusercontent.com/3999221/77157838-7184db80-6aa2-11ea-9f9a-fe77405202de.gif) + ## Features - **Data collection from source code**: collection of the object-tree and the docstrings is done thanks to diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 1a806c4d..e9e796dd 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -15,7 +15,6 @@ a.autorefs-external::after { top: 0.1em; margin-left: 0.2em; margin-right: 0.1em; - /* padding-top: 0.8em; */ height: 1em; width: 1em; diff --git a/docs/customization.md b/docs/customization.md new file mode 100644 index 00000000..7e4a50a5 --- /dev/null +++ b/docs/customization.md @@ -0,0 +1,152 @@ +# Customization + +It is possible to customize the output of the generated documentation with CSS +and/or by overriding templates. + +## CSS classes + +The following CSS classes are used in the generated HTML: + +- `doc`: on all the following elements +- `doc-children`: on `div`s containing the children of an object +- `doc-object`: on `div`s containing an object + - `doc-attribute`: on `div`s containing an attribute + - `doc-class`: on `div`s containing a class + - `doc-function`: on `div`s containing a function + - `doc-module`: on `div`s containing a module +- `doc-heading`: on objects headings + - `doc-object-name`: on `span`s wrapping objects names/paths in the heading + - `doc-KIND-name`: as above, specific to the kind of object (module, class, function, attribute) +- `doc-contents`: on `div`s wrapping the docstring then the children (if any) + - `first`: same, but only on the root object's contents `div` +- `doc-labels`: on `span`s wrapping the object's labels + - `doc-label`: on `small` elements containing a label + - `doc-label-LABEL`: same, where `LABEL` is replaced by the actual label + +!!! example "Example with colorful labels" + === "CSS" + ```css + .doc-label { border-radius: 15px; padding: 0 5px; } + .doc-label-special { background-color: blue; color: white; } + .doc-label-private { background-color: red; color: white; } + .doc-label-property { background-color: green; color: white; } + .doc-label-read-only { background-color: yellow; color: black; } + ``` + + === "Result" + +

+ special + private + property + read-only +

+ + +### Recommended style (Material) + +Here are some CSS rules for the +[*Material for MkDocs*](https://squidfunk.github.io/mkdocs-material/) theme: + +```css +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: .05rem solid var(--md-typeset-table-color); +} + +/* Mark external links as such. */ +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + background-image: url('data:image/svg+xml,'); + content: ' '; + + display: inline-block; + position: relative; + top: 0.1em; + margin-left: 0.2em; + margin-right: 0.1em; + + height: 1em; + width: 1em; + border-radius: 100%; + background-color: var(--md-typeset-a-color); +} +a.autorefs-external:hover::after { + background-color: var(--md-accent-fg-color); +} + +``` + +### Recommended style (ReadTheDocs) + +Here are some CSS rules for the built-in *ReadTheDocs* theme: + +```css +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: .05rem solid rgba(200, 200, 200, 0.2); +} +``` + +## Templates + +Templates are organized into the following tree: + +``` +πŸ“ theme/ +β”œβ”€β”€ πŸ“„ attribute.html +β”œβ”€β”€ πŸ“„ children.html +β”œβ”€β”€ πŸ“„ class.html +β”œβ”€β”€ πŸ“ docstring/ +β”‚Β Β  β”œβ”€β”€ πŸ“„ admonition.html +β”‚Β Β  β”œβ”€β”€ πŸ“„ attributes.html +β”‚Β Β  β”œβ”€β”€ πŸ“„ examples.html +β”‚Β Β  β”œβ”€β”€ πŸ“„ other_parameters.html +β”‚Β Β  β”œβ”€β”€ πŸ“„ parameters.html +β”‚Β Β  β”œβ”€β”€ πŸ“„ raises.html +β”‚Β Β  β”œβ”€β”€ πŸ“„ receives.html +β”‚Β Β  β”œβ”€β”€ πŸ“„ returns.html +β”‚Β Β  β”œβ”€β”€ πŸ“„ warns.html +β”‚Β Β  └── πŸ“„ yields.html +β”œβ”€β”€ πŸ“„ docstring.html +β”œβ”€β”€ πŸ“„ expression.html +β”œβ”€β”€ πŸ“„ function.html +β”œβ”€β”€ πŸ“„ labels.html +β”œβ”€β”€ πŸ“„ module.html +└── πŸ“„ signature.html +``` + +See them [in the repository](https://github.com/mkdocstrings/python/tree/master/src/mkdocstrings_handlers/python/templates/). +See the general *mkdocstrings* documentation to learn how to override them: https://mkdocstrings.github.io/theming/#templates. + +In preparation for Jinja2 blocks, which will improve customization, +each one of these templates extends in fact a base version in `theme/_base`. Example: + +```html+jinja title="theme/docstring/admonition.html" +{% extends "_base/docstring/admonition.html" %} +``` + +```html+jinja title="theme/_base/docstring/admonition.html" +{{ log.debug() }} +
+ {{ section.title|convert_markdown(heading_level, html_id, strip_paragraph=True) }} + {{ section.value.contents|convert_markdown(heading_level, html_id) }} +
+``` + +It means you will be able to customize only *parts* of a template +without having to fully copy-paste it in your project: + +```jinja title="templates/theme/docstring.html" +{% extends "_base/docstring.html" %} +{% block contents %} + {{ block.super }} + Additional contents +{% endblock contents %} +``` + +WARNING: **Block-level customization is not ready yet. We welcome [suggestions](https://github.com/mkdocstrings/python/issues/new).** diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5b42478c2b229087bbe8137fcbdc9f516af9938a GIT binary patch literal 58828 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%0DG171QpZ@Z3h3G5LK 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.7"] docs = [ + "markdown-callouts>=0.2.0", "mkdocs>=1.2", "mkdocs-coverage>=0.2", "mkdocs-gen-files>=0.3", diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 1ca452e6..f580608e 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -75,33 +75,35 @@ class PythonHandler(BaseHandler): """ Attributes: Headings options: heading_level (int): The initial heading level to use. Default: `2`. - show_root_heading (bool): Show the heading of the object at the root of the documentation tree. Default: `False`. + show_root_heading (bool): Show the heading of the object at the root of the documentation tree + (i.e. the object referenced by the identifier after `:::`). Default: `False`. show_root_toc_entry (bool): If the root heading is not shown, at least add a ToC entry for it. Default: `True`. show_root_full_path (bool): Show the full Python path for the root object heading. Default: `True`. - show_root_members_full_path (bool): Show the full Python path of every object. Default: `False`. - show_object_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. Default: `False`. + show_root_members_full_path (bool): Show the full Python path of the root members. Default: `False`. + show_object_full_path (bool): Show the full Python path of every object. Default: `False`. show_category_heading (bool): When grouped by categories, show a heading for each category. Default: `False`. Attributes: Members options: members (list[str] | False | None): An explicit list of members to render. Default: `None`. + 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. Default: `"alphabetical"`. filters (list[str] | None): A list of filters applied to filter objects based on their name. A filter starting with `!` will exclude matching objects instead of including them. Default: `["!^_[^_]"]`. - group_by_category (bool): Group the object's children by categories: attributes, classes, functions, methods, and modules. Default: `True`. + group_by_category (bool): Group the object's children by categories: attributes, classes, functions, and modules. Default: `True`. show_submodules (bool): When rendering a module, show its submodules recursively. Default: `True`. - 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. Default: `"alphabetical"`. Attributes: Docstrings options: docstring_style (str): The docstring style to use: `google`, `numpy`, `sphinx`, or `None`. Default: `"google"`. docstring_options (dict): The options for the docstring parser. See parsers under [`griffe.docstrings`][]. docstring_section_style (str): The style used to render docstring sections. Options: `table`, `list`, `spacy`. Default: `"table"`. - line_length (int): Maximum line length when formatting code. Default: `60`. + line_length (int): Maximum line length when formatting code/signatures. Default: `60`. merge_init_into_class (bool): Whether to merge the `__init__` method into the class' signature and docstring. Default: `False`. show_if_no_docstring (bool): Show the object heading even if it has no docstring or children with docstrings. Default: `False`. - Attributes: Signature/annotations options: - annotations_path: The verbosity for annotations path: `brief` (recommended), or `source` (as written in the source). Default: `"brief"`. - show_signature (bool): Show method and function signatures. Default: `True`. - show_signature_annotations (bool): Show the type annotations in method and function signatures. Default: `False`. + Attributes: Signatures/annotations options: + annotations_path (str): The verbosity for annotations path: `brief` (recommended), or `source` (as written in the source). Default: `"brief"`. + show_signature (bool): Show methods and functions signatures. Default: `True`. + show_signature_annotations (bool): Show the type annotations in methods and functions signatures. Default: `False`. separate_signature (bool): Whether to put the whole signature in a code block below the heading. Default: `False`. Attributes: Additional options: From 312a7092394aab968032cf08195af7445a85052f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 8 May 2022 11:35:59 +0200 Subject: [PATCH 22/27] fix: Fix CSS class on labels --- .../python/templates/material/_base/labels.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/labels.html b/src/mkdocstrings_handlers/python/templates/material/_base/labels.html index e9c20f12..4f2f72d9 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/labels.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/labels.html @@ -1,6 +1,6 @@ {% if labels %} {{ log.debug("Rendering labels") }} - + {% for label in labels %} {{ label }} {% endfor %} From 480d0c373904713313ec76b6e2570dbc35eb527b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 8 May 2022 11:36:23 +0200 Subject: [PATCH 23/27] refactor: Disable `show_submodules` by default --- src/mkdocstrings_handlers/python/handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index f580608e..55209bbe 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -63,7 +63,7 @@ class PythonHandler(BaseHandler): "merge_init_into_class": False, "show_source": True, "show_bases": True, - "show_submodules": True, + "show_submodules": False, "group_by_category": True, "heading_level": 2, "members_order": rendering.Order.alphabetical.value, @@ -90,7 +90,7 @@ class PythonHandler(BaseHandler): filters (list[str] | None): A list of filters applied to filter objects based on their name. A filter starting with `!` will exclude matching objects instead of including them. Default: `["!^_[^_]"]`. group_by_category (bool): Group the object's children by categories: attributes, classes, functions, and modules. Default: `True`. - show_submodules (bool): When rendering a module, show its submodules recursively. Default: `True`. + show_submodules (bool): When rendering a module, show its submodules recursively. Default: `False`. Attributes: Docstrings options: docstring_style (str): The docstring style to use: `google`, `numpy`, `sphinx`, or `None`. Default: `"google"`. From c15a10bdc072e20bb80b1730521f8e21d05a3457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 27 May 2022 21:58:18 +0200 Subject: [PATCH 24/27] chore: Template upgrade --- .copier-answers.yml | 2 +- .github/FUNDING.yml | 7 +-- .github/workflows/ci.yml | 6 +-- CONTRIBUTING.md | 11 ++-- docs/credits.md | 3 ++ docs/gen_credits.py | 62 ----------------------- docs/gen_ref_nav.py | 11 ++-- mkdocs.yml | 5 +- pyproject.toml | 12 ++--- scripts/gen_credits.py | 106 +++++++++++++++++++++++++++++++++++++++ scripts/setup.sh | 10 ++++ 11 files changed, 143 insertions(+), 92 deletions(-) create mode 100644 docs/credits.md delete mode 100644 docs/gen_credits.py create mode 100644 scripts/gen_credits.py diff --git a/.copier-answers.yml b/.copier-answers.yml index f2e7c7ac..97027f2b 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.9.0 +_commit: 0.9.7 _src_path: gh:pawamoy/copier-pdm author_email: pawamoy@pm.me author_fullname: TimothΓ©e Mazzucotelli diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c71a8d4e..cf5764f4 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,7 +1,4 @@ github: - - pawamoy -ko_fi: pawamoy -liberapay: pawamoy -patreon: pawamoy +- pawamoy custom: - - https://www.paypal.me/pawamoy +- https://www.paypal.me/pawamoy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9b12e44..201e8d52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v2 - name: Set up PDM - uses: pdm-project/setup-pdm@v2.5 + uses: pdm-project/setup-pdm@v2.6 with: python-version: "3.8" @@ -86,7 +86,7 @@ jobs: uses: actions/checkout@v2 - name: Set up PDM - uses: pdm-project/setup-pdm@v2.5 + uses: pdm-project/setup-pdm@v2.6 with: python-version: ${{ matrix.python-version }} @@ -105,7 +105,7 @@ jobs: key: tests-cache-${{ runner.os }}-${{ matrix.python-version }} - name: Install dependencies - run: pdm install -G duty -G tests + run: pdm install --no-editable -G duty -G tests - name: Run the test suite run: pdm run duty test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea428541..ba0c5d2b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ cd python make setup ``` -> NOTE +> NOTE: > If it fails for some reason, > you'll need to install > [PDM](https://github.com/pdm-project/pdm) @@ -57,17 +57,14 @@ As usual: 1. create a new branch: `git checkout -b feature-or-bugfix-name` 1. edit the code and/or the documentation -If you updated the documentation or the project dependencies: - -1. run `make docs-regen` -1. run `make docs-serve`, - go to http://localhost:8000 and check that everything looks good - **Before committing:** 1. run `make format` to auto-format the code 1. run `make check` to check everything (fix any warning) 1. run `make test` to run the tests (fix any issue) +1. if you updated the documentation or the project dependencies: + 1. run `make docs-serve` + 1. go to http://localhost:8000 and check that everything looks good 1. follow our [commit message convention](#commit-message-convention) If you are unsure about how to fix or ignore a warning, diff --git a/docs/credits.md b/docs/credits.md new file mode 100644 index 00000000..02e1dd81 --- /dev/null +++ b/docs/credits.md @@ -0,0 +1,3 @@ +```python exec="yes" +--8<-- "scripts/gen_credits.py" +``` diff --git a/docs/gen_credits.py b/docs/gen_credits.py deleted file mode 100644 index 370d2e7d..00000000 --- a/docs/gen_credits.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Generate the credits page.""" - -import functools -import re -from itertools import chain -from pathlib import Path -from urllib.request import urlopen - -import mkdocs_gen_files -import toml -from jinja2 import StrictUndefined -from jinja2.sandbox import SandboxedEnvironment - - -def get_credits_data() -> dict: - """Return data used to generate the credits file. - - Returns: - Data required to render the credits template. - """ - project_dir = Path(__file__).parent.parent - metadata = toml.load(project_dir / "pyproject.toml")["project"] - metadata_pdm = toml.load(project_dir / "pyproject.toml")["tool"]["pdm"] - lock_data = toml.load(project_dir / "pdm.lock") - project_name = metadata["name"] - - all_dependencies = chain( - metadata.get("dependencies", []), - chain(*metadata.get("optional-dependencies", {}).values()), - chain(*metadata_pdm.get("dev-dependencies", {}).values()), - ) - direct_dependencies = {re.sub(r"[^\w-].*$", "", dep) for dep in all_dependencies} - direct_dependencies = {dep.lower() for dep in direct_dependencies} - indirect_dependencies = {pkg["name"].lower() for pkg in lock_data["package"]} - indirect_dependencies -= direct_dependencies - - return { - "project_name": project_name, - "direct_dependencies": sorted(direct_dependencies), - "indirect_dependencies": sorted(indirect_dependencies), - "more_credits": "http://pawamoy.github.io/credits/", - } - - -@functools.lru_cache(maxsize=None) -def get_credits(): - """Return credits as Markdown. - - Returns: - The credits page Markdown. - """ - jinja_env = SandboxedEnvironment(undefined=StrictUndefined) - commit = "c78c29caa345b6ace19494a98b1544253cbaf8c1" - template_url = f"https://raw.githubusercontent.com/pawamoy/jinja-templates/{commit}/credits.md" - template_data = get_credits_data() - template_text = urlopen(template_url).read().decode("utf8") # noqa: S310 - return jinja_env.from_string(template_text).render(**template_data) - - -with mkdocs_gen_files.open("credits.md", "w") as fd: - fd.write(get_credits()) -mkdocs_gen_files.set_edit_path("credits.md", "gen_credits.py") diff --git a/docs/gen_ref_nav.py b/docs/gen_ref_nav.py index 15febda8..1b9fbda1 100755 --- a/docs/gen_ref_nav.py +++ b/docs/gen_ref_nav.py @@ -6,24 +6,25 @@ nav = mkdocs_gen_files.Nav() -for path in sorted(Path("src").glob("**/*.py")): +for path in sorted(Path("src").rglob("*.py")): module_path = path.relative_to("src").with_suffix("") doc_path = path.relative_to("src").with_suffix(".md") full_doc_path = Path("reference", doc_path) - parts = list(module_path.parts) + parts = tuple(module_path.parts) + if parts[-1] == "__init__": parts = parts[:-1] doc_path = doc_path.with_name("index.md") full_doc_path = full_doc_path.with_name("index.md") elif parts[-1] == "__main__": continue - nav_parts = list(parts) - nav[nav_parts] = doc_path + + nav[parts] = doc_path.as_posix() with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) - print("::: " + ident, file=fd) + fd.write(f"::: {ident}") mkdocs_gen_files.set_edit_path(full_doc_path, path) diff --git a/mkdocs.yml b/mkdocs.yml index 9dd9dc20..6b851eed 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,7 @@ site_url: "https://mkdocstrings.github.io/python" repo_url: "https://github.com/mkdocstrings/python" repo_name: "mkdocstrings/python" site_dir: "site" -watch: [src, README.md] +watch: [README.md, CONTRIBUTING.md, CHANGELOG.md, src/mkdocstrings_handlers] nav: - Home: @@ -27,6 +27,7 @@ theme: logo: logo.png features: - navigation.tabs + - navigation.tabs.sticky - navigation.top palette: - media: "(prefers-color-scheme: light)" @@ -65,9 +66,9 @@ markdown_extensions: plugins: - search +- markdown-exec - gen-files: scripts: - - docs/gen_credits.py - docs/gen_ref_nav.py - literate-nav: nav_file: SUMMARY.md diff --git a/pyproject.toml b/pyproject.toml index 2b417754..700a8ec0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "pdm.pep517.api" name = "mkdocstrings-python" description = "A Python handler for mkdocstrings." authors = [{name = "TimothΓ©e Mazzucotelli", email = "pawamoy@pm.me"}] -license = {file = "LICENSE"} +license-expression = "ISC" readme = "README.md" requires-python = ">=3.7" keywords = [] @@ -14,7 +14,6 @@ dynamic = ["version"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "License :: OSI Approved :: ISC License (ISCL)", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", @@ -47,18 +46,19 @@ Funding = "https://github.com/sponsors/mkdocstrings" [tool.pdm] version = {use_scm = true} includes = ["src/mkdocstrings_handlers"] +editable-backend = "editables" [tool.pdm.dev-dependencies] duty = ["duty>=0.7"] docs = [ - "markdown-callouts>=0.2.0", - "mkdocs>=1.2", + "mkdocs>=1.3", "mkdocs-coverage>=0.2", "mkdocs-gen-files>=0.3", "mkdocs-literate-nav>=0.4", "mkdocs-material>=7.3", "mkdocs-section-index>=0.3", - "mkdocstrings>=0.16", + "markdown-callouts>=0.2", + "markdown-exec>=0.5", "toml>=0.10", ] format = [ @@ -67,7 +67,6 @@ format = [ "isort>=5.10", ] maintain = [ - # TODO: remove this section when git-changelog is more powerful "git-changelog>=0.4", ] quality = [ @@ -89,7 +88,6 @@ tests = [ "pytest>=6.2", "pytest-cov>=3.0", "pytest-randomly>=3.10", - "pytest-sugar>=0.9", "pytest-xdist>=2.4", ] typing = [ diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py new file mode 100644 index 00000000..484b13e4 --- /dev/null +++ b/scripts/gen_credits.py @@ -0,0 +1,106 @@ +import re +from importlib.metadata import metadata, PackageNotFoundError +from itertools import chain +from pathlib import Path +from textwrap import dedent + +import toml +from jinja2 import StrictUndefined +from jinja2.sandbox import SandboxedEnvironment + +project_dir = Path(".") +pyproject = toml.load(project_dir / "pyproject.toml") +project = pyproject["project"] +pdm = pyproject["tool"]["pdm"] +lock_data = toml.load(project_dir / "pdm.lock") +lock_pkgs = {pkg["name"].lower(): pkg for pkg in lock_data["package"]} +project_name = project["name"] +regex = re.compile(r"(?P[\w.-]+)(?P.*)$") + +def get_license(pkg_name): + try: + data = metadata(pkg_name) + except PackageNotFoundError: + return "?" + license = data.get("License", "").strip() + multiple_lines = bool(license.count("\n")) + # TODO: remove author logic once all my packages licenses are fixed + author = "" + if multiple_lines or not license or license == "UNKNOWN": + for header, value in data.items(): + if header == "Classifier" and value.startswith("License ::"): + license = value.rsplit("::", 1)[1].strip() + elif header == "Author-email": + author = value + if license == "Other/Proprietary License" and "pawamoy" in author: + license = "ISC" + return license or "?" + +def get_deps(base_deps): + deps = {} + for dep in base_deps: + parsed = regex.match(dep).groupdict() + dep_name = parsed["dist"].lower() + deps[dep_name] = {"license": get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + + again = True + while again: + again = False + for pkg_name in lock_pkgs: + if pkg_name in deps: + for pkg_dependency in lock_pkgs[pkg_name].get("dependencies", []): + parsed = regex.match(pkg_dependency).groupdict() + dep_name = parsed["dist"].lower() + if dep_name not in deps: + deps[dep_name] = {"license": get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + again = True + + return deps + +dev_dependencies = get_deps(chain(*pdm.get("dev-dependencies", {}).values())) +prod_dependencies = get_deps( + chain( + project.get("dependencies", []), + chain(*project.get("optional-dependencies", {}).values()), + ) +) + +template_data = { + "project_name": project_name, + "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: dep["name"]), + "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: dep["name"]), + "more_credits": "http://pawamoy.github.io/credits/", +} +template_text = dedent( + """ + These projects were used to build `{{ project_name }}`. **Thank you!** + + [`python`](https://www.python.org/) | + [`pdm`](https://pdm.fming.dev/) | + [`copier-pdm`](https://github.com/pawamoy/copier-pdm) + + {% macro dep_line(dep) -%} + [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} + {%- endmacro %} + + ### Runtime dependencies + + Project | Summary | Version (accepted) | Version (last resolved) | License + ------- | ------- | ------------------ | ----------------------- | ------- + {% for dep in prod_dependencies -%} + {{ dep_line(dep) }} + {% endfor %} + + ### Development dependencies + + Project | Summary | Version (accepted) | Version (last resolved) | License + ------- | ------- | ------------------ | ----------------------- | ------- + {% for dep in dev_dependencies -%} + {{ dep_line(dep) }} + {% endfor %} + + {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} + """ +) +jinja_env = SandboxedEnvironment(undefined=StrictUndefined) +print(jinja_env.from_string(template_text).render(**template_data)) diff --git a/scripts/setup.sh b/scripts/setup.sh index f0a41cf8..188eaebc 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -14,7 +14,17 @@ install_with_pipx() { install_with_pipx pdm +restore_previous_python_version() { + if pdm use -f "$1" &>/dev/null; then + echo "> Restored previous Python version: ${1##*/}" + fi +} + if [ -n "${PYTHON_VERSIONS}" ]; then + if old_python_version="$(pdm config python.path 2>/dev/null)"; then + echo "> Currently selected Python version: ${old_python_version##*/}" + trap "restore_previous_python_version ${old_python_version}" EXIT + fi for python_version in ${PYTHON_VERSIONS}; do if pdm use -f "python${python_version}" &>/dev/null; then echo "> Using Python ${python_version} interpreter" From 59c86df1f70cce893a41509919b09285c8c517f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 28 May 2022 15:56:48 +0200 Subject: [PATCH 25/27] docs: Improve docs --- docs/customization.md | 44 +++++------ docs/usage.md | 84 ++++++++++----------- scripts/gen_credits.py | 6 +- src/mkdocstrings_handlers/python/handler.py | 4 +- 4 files changed, 72 insertions(+), 66 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 7e4a50a5..d1d02cca 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -96,28 +96,28 @@ div.doc-contents:not(.first) { Templates are organized into the following tree: -``` -πŸ“ theme/ -β”œβ”€β”€ πŸ“„ attribute.html -β”œβ”€β”€ πŸ“„ children.html -β”œβ”€β”€ πŸ“„ class.html -β”œβ”€β”€ πŸ“ docstring/ -β”‚Β Β  β”œβ”€β”€ πŸ“„ admonition.html -β”‚Β Β  β”œβ”€β”€ πŸ“„ attributes.html -β”‚Β Β  β”œβ”€β”€ πŸ“„ examples.html -β”‚Β Β  β”œβ”€β”€ πŸ“„ other_parameters.html -β”‚Β Β  β”œβ”€β”€ πŸ“„ parameters.html -β”‚Β Β  β”œβ”€β”€ πŸ“„ raises.html -β”‚Β Β  β”œβ”€β”€ πŸ“„ receives.html -β”‚Β Β  β”œβ”€β”€ πŸ“„ returns.html -β”‚Β Β  β”œβ”€β”€ πŸ“„ warns.html -β”‚Β Β  └── πŸ“„ yields.html -β”œβ”€β”€ πŸ“„ docstring.html -β”œβ”€β”€ πŸ“„ expression.html -β”œβ”€β”€ πŸ“„ function.html -β”œβ”€β”€ πŸ“„ labels.html -β”œβ”€β”€ πŸ“„ module.html -└── πŸ“„ signature.html +```tree result="text" +theme/ + attribute.html + children.html + class.html + docstring/ + admonition.html + attributes.html + examples.html + other_parameters.html + parameters.html + raises.html + receives.html + returns.html + warns.html + yields.html + docstring.html + expression.html + function.html + labels.html + module.html + signature.html ``` See them [in the repository](https://github.com/mkdocstrings/python/tree/master/src/mkdocstrings_handlers/python/templates/). diff --git a/docs/usage.md b/docs/usage.md index 5755326e..de28ca16 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,6 +1,6 @@ # Usage -WARNING: **This is the documentation for the NEW, EXPERIMENTAL Python handler.** +TIP: **This is the documentation for the NEW Python handler.** To read the documentation for the LEGACY handler, go to the [legacy handler documentation](https://mkdocstrings.github.io/python-legacy). @@ -17,7 +17,7 @@ Like every handler, the Python handler accepts both **global** and **local** opt Some options are **global only**, and go directly under the handler's name. -- `import`: This option is used to import Sphinx-compatible objects inventories from other +- `import`: this option is used to import Sphinx-compatible objects inventories from other documentation sites. For example, you can import the standard library objects inventory like this: @@ -138,10 +138,10 @@ so that the current working directory has no impact on the build process: TIP: **This is the recommended method.** 1. mkdocs.yml in root, package in root - ``` - πŸ“ root/ - β”œβ”€β”€ πŸ“„ mkdocs.yml - └── πŸ“ package/ + ```tree + root/ + mkdocs.yml + package/ ``` ```yaml title="mkdocs.yml" @@ -153,11 +153,11 @@ TIP: **This is the recommended method.** ``` 1. mkdocs.yml in root, package in subfolder - ``` - πŸ“ root/ - β”œβ”€β”€ πŸ“„ mkdocs.yml - └── πŸ“ src/ - └── πŸ“ package/ + ```tree + root/ + mkdocs.yml + src/ + package/ ``` ```yaml title="mkdocs.yml" @@ -169,11 +169,11 @@ TIP: **This is the recommended method.** ``` 1. mkdocs.yml in subfolder, package in root - ``` - πŸ“ root/ - β”œβ”€β”€ πŸ“ docs/ - β”‚ └── πŸ“„ mkdocs.yml - └── πŸ“ package/ + ```tree + root/ + docs/ + mkdocs.yml + package/ ``` ```yaml title="mkdocs.yml" @@ -185,12 +185,12 @@ TIP: **This is the recommended method.** ``` 1. mkdocs.yml in subfolder, package in subfolder - ``` - πŸ“ root/ - β”œβ”€β”€ πŸ“ docs/ - β”‚ └── πŸ“„ mkdocs.yml - └── πŸ“ src/ - └── πŸ“ package/ + ```tree + root/ + docs/ + mkdocs.yml + src/ + package/ ``` ```yaml title="mkdocs.yml" @@ -218,10 +218,10 @@ In Bash and other shells, you can run your command like this (note the prepended `PYTHONPATH=...`): 1. mkdocs.yml in root, package in root - ``` - πŸ“ root/ - β”œβ”€β”€ πŸ“„ mkdocs.yml - └── πŸ“ package/ + ```tree + root/ + mkdocs.yml + package/ ``` ```bash @@ -229,11 +229,11 @@ In Bash and other shells, you can run your command like this ``` 1. mkdocs.yml in root, package in subfolder - ``` - πŸ“ root/ - β”œβ”€β”€ πŸ“„ mkdocs.yml - └── πŸ“ src/ - └── πŸ“ package/ + ```tree + root/ + mkdocs.yml + src/ + package/ ``` ```bash @@ -241,11 +241,11 @@ In Bash and other shells, you can run your command like this ``` 1. mkdocs.yml in subfolder, package in root - ``` - πŸ“ root/ - β”œβ”€β”€ πŸ“ docs/ - β”‚ └── πŸ“„ mkdocs.yml - └── πŸ“ package/ + ```tree + root/ + docs/ + mkdocs.yml + package/ ``` ```bash @@ -253,12 +253,12 @@ In Bash and other shells, you can run your command like this ``` 1. mkdocs.yml in subfolder, package in subfolder - ``` - πŸ“ root/ - β”œβ”€β”€ πŸ“ docs/ - β”‚ └── πŸ“„ mkdocs.yml - └── πŸ“ src/ - └── πŸ“ package/ + ```tree + root/ + docs/ + mkdocs.yml + src/ + package/ ``` ```bash diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index 484b13e4..a21a1e4a 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -1,5 +1,4 @@ import re -from importlib.metadata import metadata, PackageNotFoundError from itertools import chain from pathlib import Path from textwrap import dedent @@ -8,6 +7,11 @@ from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment +try: + from importlib.metadata import metadata, PackageNotFoundError +except ImportError: + from importlib_metadata import metadata, PackageNotFoundError + project_dir = Path(".") pyproject = toml.load(project_dir / "pyproject.toml") project = pyproject["project"] diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 55209bbe..301f02fc 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -88,7 +88,9 @@ class PythonHandler(BaseHandler): 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. Default: `"alphabetical"`. filters (list[str] | None): A list of filters applied to filter objects based on their name. - A filter starting with `!` will exclude matching objects instead of including them. Default: `["!^_[^_]"]`. + A filter starting with `!` will exclude matching objects instead of including them. + The `members` option takes precedence over `filters` (filters will still be applied recursively + to lower members in the hierarchy). Default: `["!^_[^_]"]`. group_by_category (bool): Group the object's children by categories: attributes, classes, functions, and modules. Default: `True`. show_submodules (bool): When rendering a module, show its submodules recursively. Default: `False`. From b6a9a4799980c4590a7ce2838e12653f40e43be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 28 May 2022 17:42:20 +0200 Subject: [PATCH 26/27] build: Depend on mkdocstrings 0.19 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 700a8ec0..409a40be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "mkdocstrings>=0.18", + "mkdocstrings>=0.19", "griffe>=0.11.1", ] From bc1a8aefe684444037600b9a52be2ad0f3e02e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 28 May 2022 18:01:21 +0200 Subject: [PATCH 27/27] chore: Prepare release 0.7.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea3c8079..a13eb8dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.7.0](https://github.com/mkdocstrings/python/releases/tag/0.7.0) - 2022-05-28 + +[Compare with 0.6.6](https://github.com/mkdocstrings/python/compare/0.6.6...0.7.0) + +### Packaging / Dependencies +- Depend on mkdocstrings 0.19 ([b6a9a47](https://github.com/mkdocstrings/python/commit/b6a9a4799980c4590a7ce2838e12653f40e43be3) by TimothΓ©e Mazzucotelli). + +### Features +- Add config option for annotations paths verbosity ([b6c9893](https://github.com/mkdocstrings/python/commit/b6c989315fb028813a919319ad1818b0b1f597ac) by TimothΓ©e Mazzucotelli). +- Use sections titles in SpaCy-styled docstrings ([fe16b54](https://github.com/mkdocstrings/python/commit/fe16b54aea60473575343e3a3c428567b701bd7d) by TimothΓ©e Mazzucotelli). +- Wrap objects names in spans to allow custom styling ([0822ff9](https://github.com/mkdocstrings/python/commit/0822ff9d3ffd3fb71fb619a8b557160661eff9c3) by TimothΓ©e Mazzucotelli). [Issue mkdocstrings/mkdocstrings#240](https://github.com/mkdocstrings/mkdocstrings/issues/240) +- Add Jinja blocks around docstring section styles ([aaa79ee](https://github.com/mkdocstrings/python/commit/aaa79eea40d49a64a69badbe732bf5211fbf055a) by TimothΓ©e Mazzucotelli). +- Add members and filters options ([24a6136](https://github.com/mkdocstrings/python/commit/24a6136ee6c04a6a49ee74b20e65177868a10ea7) by TimothΓ©e Mazzucotelli). +- Add paths option ([dd41182](https://github.com/mkdocstrings/python/commit/dd41182c210f0bb2675ead162adaa01dbbb1949f) by TimothΓ©e Mazzucotelli). [Issue mkdocstrings/mkdocstrings#311](https://github.com/mkdocstrings/mkdocstrings/issues/311), [PR #20](https://github.com/mkdocstrings/python/issues/20) + +### Bug Fixes +- Fix CSS class on labels ([312a709](https://github.com/mkdocstrings/python/commit/312a7092394aab968032cf08195af7445a85052f) by TimothΓ©e Mazzucotelli). +- Fix categories rendering ([6407cf4](https://github.com/mkdocstrings/python/commit/6407cf4f2375c894e0c528e932e9b76774a6455e) by TimothΓ©e Mazzucotelli). [Issue #14](https://github.com/mkdocstrings/python/issues/14) + +### Code Refactoring +- Disable `show_submodules` by default ([480d0c3](https://github.com/mkdocstrings/python/commit/480d0c373904713313ec76b6e2570dbc35eb527b) by TimothΓ©e Mazzucotelli). +- Merge default configuration options in handler ([347ce76](https://github.com/mkdocstrings/python/commit/347ce76d074c0e3841df2d5162b54d3938d00453) by TimothΓ©e Mazzucotelli). +- Reduce number of template debug logs ([8fed314](https://github.com/mkdocstrings/python/commit/8fed314243e3981fc7b527c69cee628e87b10220) by TimothΓ©e Mazzucotelli). +- Respect `show_root_full_path` for ToC entries (hidden headings) ([8f4c853](https://github.com/mkdocstrings/python/commit/8f4c85328e8b4a45db77f9fc3e536a5008686f37) by TimothΓ©e Mazzucotelli). +- Bring consistency on headings style ([59104c4](https://github.com/mkdocstrings/python/commit/59104c4c51c86c774eed76d8508f9f4d3db5463f) by TimothΓ©e Mazzucotelli). +- Stop using deprecated base classes ([d5ea1c5](https://github.com/mkdocstrings/python/commit/d5ea1c5cf7884d8c019145f73685a84218e69840) by TimothΓ©e Mazzucotelli). + + ## [0.6.6](https://github.com/mkdocstrings/python/releases/tag/0.6.6) - 2022-03-06 [Compare with 0.6.5](https://github.com/mkdocstrings/python/compare/0.6.5...0.6.6)
YIELDS{{ (section.title or "YIELDS").rstrip(":").upper() }} DESCRIPTION