Skip to content

Commit c5c0d90

Browse files
committed
refactor: Stop using deprecated base classes
1 parent b946c36 commit c5c0d90

File tree

5 files changed

+329
-348
lines changed

5 files changed

+329
-348
lines changed

src/mkdocstrings_handlers/python/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33
from mkdocstrings_handlers.python.handler import get_handler
44

55
__all__ = ["get_handler"] # noqa: WPS410
6+
7+
# TODO: CSS classes everywhere in templates
8+
# TODO: name normalization (filenames, Jinja2 variables, HTML tags, CSS classes)
9+
# TODO: Jinja2 blocks everywhere in templates

src/mkdocstrings_handlers/python/collector.py

Lines changed: 0 additions & 92 deletions
This file was deleted.

src/mkdocstrings_handlers/python/handler.py

Lines changed: 177 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
"""This module implements a handler for the Python language."""
22

3+
from __future__ import annotations
4+
35
import posixpath
6+
from collections import ChainMap
7+
from contextlib import suppress
48
from typing import Any, BinaryIO, Iterator, Optional, Tuple
59

10+
from griffe.agents.extensions import load_extensions
11+
from griffe.collections import LinesCollection, ModulesCollection
12+
from griffe.docstrings.parsers import Parser
13+
from griffe.exceptions import AliasResolutionError
14+
from griffe.loader import GriffeLoader
615
from griffe.logger import patch_loggers
7-
from mkdocstrings.handlers.base import BaseHandler
16+
from markdown import Markdown
17+
from mkdocstrings.extension import PluginError
18+
from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem
819
from mkdocstrings.inventory import Inventory
920
from mkdocstrings.loggers import get_logger
1021

11-
from mkdocstrings_handlers.python.collector import PythonCollector
12-
from mkdocstrings_handlers.python.renderer import PythonRenderer
22+
from mkdocstrings_handlers.python import rendering
23+
24+
logger = get_logger(__name__)
1325

1426
patch_loggers(get_logger)
1527

@@ -21,10 +33,82 @@ class PythonHandler(BaseHandler):
2133
domain: The cross-documentation domain/language for this handler.
2234
enable_inventory: Whether this handler is interested in enabling the creation
2335
of the `objects.inv` Sphinx inventory file.
36+
fallback_theme: The fallback theme.
37+
fallback_config: The configuration used to collect item during autorefs fallback.
38+
default_collection_config: The default rendering options,
39+
see [`default_collection_config`][mkdocstrings_handlers.python.handler.PythonHandler.default_collection_config].
40+
default_rendering_config: The default rendering options,
41+
see [`default_rendering_config`][mkdocstrings_handlers.python.handler.PythonHandler.default_rendering_config].
2442
"""
2543

2644
domain: str = "py" # to match Sphinx's default domain
2745
enable_inventory: bool = True
46+
fallback_theme = "material"
47+
fallback_config: dict = {"fallback": True}
48+
default_collection_config: dict = {"docstring_style": "google", "docstring_options": {}}
49+
"""The default collection options.
50+
51+
Option | Type | Description | Default
52+
------ | ---- | ----------- | -------
53+
**`docstring_style`** | `"google" | "numpy" | "sphinx" | None` | The docstring style to use. | `"google"`
54+
**`docstring_options`** | `dict[str, Any]` | The options for the docstring parser. | `{}`
55+
"""
56+
default_rendering_config: dict = {
57+
"show_root_heading": False,
58+
"show_root_toc_entry": True,
59+
"show_root_full_path": True,
60+
"show_root_members_full_path": False,
61+
"show_object_full_path": False,
62+
"show_category_heading": False,
63+
"show_if_no_docstring": False,
64+
"show_signature": True,
65+
"show_signature_annotations": False,
66+
"separate_signature": False,
67+
"line_length": 60,
68+
"merge_init_into_class": False,
69+
"show_source": True,
70+
"show_bases": True,
71+
"show_submodules": True,
72+
"group_by_category": True,
73+
"heading_level": 2,
74+
"members_order": rendering.Order.alphabetical.value,
75+
"docstring_section_style": "table",
76+
}
77+
"""The default rendering options.
78+
79+
Option | Type | Description | Default
80+
------ | ---- | ----------- | -------
81+
**`show_root_heading`** | `bool` | Show the heading of the object at the root of the documentation tree. | `False`
82+
**`show_root_toc_entry`** | `bool` | If the root heading is not shown, at least add a ToC entry for it. | `True`
83+
**`show_root_full_path`** | `bool` | Show the full Python path for the root object heading. | `True`
84+
**`show_object_full_path`** | `bool` | Show the full Python path of every object. | `False`
85+
**`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`
86+
**`show_category_heading`** | `bool` | When grouped by categories, show a heading for each category. | `False`
87+
**`show_if_no_docstring`** | `bool` | Show the object heading even if it has no docstring or children with docstrings. | `False`
88+
**`show_signature`** | `bool` | Show method and function signatures. | `True`
89+
**`show_signature_annotations`** | `bool` | Show the type annotations in method and function signatures. | `False`
90+
**`separate_signature`** | `bool` | Whether to put the whole signature in a code block below the heading. | `False`
91+
**`line_length`** | `int` | Maximum line length when formatting code. | `60`
92+
**`merge_init_into_class`** | `bool` | Whether to merge the `__init__` method into the class' signature and docstring. | `False`
93+
**`show_source`** | `bool` | Show the source code of this object. | `True`
94+
**`show_bases`** | `bool` | Show the base classes of a class. | `True`
95+
**`show_submodules`** | `bool` | When rendering a module, show its submodules recursively. | `True`
96+
**`group_by_category`** | `bool` | Group the object's children by categories: attributes, classes, functions, methods, and modules. | `True`
97+
**`heading_level`** | `int` | The initial heading level to use. | `2`
98+
**`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`
99+
**`docstring_section_style`** | `str` | The style used to render docstring sections. Options: `table`, `list`, `spacy`. | `table`
100+
""" # noqa: E501
101+
102+
def __init__(self, *args, **kwargs) -> None:
103+
"""Initialize the handler.
104+
105+
Parameters:
106+
*args: Handler name, theme and custom templates.
107+
**kwargs: Same thing, but with keyword arguments.
108+
"""
109+
super().__init__(*args, **kwargs)
110+
self._modules_collection: ModulesCollection = ModulesCollection()
111+
self._lines_collection: LinesCollection = LinesCollection()
28112

29113
@classmethod
30114
def load_inventory(
@@ -53,6 +137,95 @@ def load_inventory(
53137
for item in Inventory.parse_sphinx(in_file, domain_filter=("py",)).values(): # noqa: WPS526
54138
yield item.name, posixpath.join(base_url, item.uri)
55139

140+
def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: WPS231
141+
"""Collect the documentation tree given an identifier and selection options.
142+
143+
Arguments:
144+
identifier: The dotted-path of a Python object available in the Python path.
145+
config: Selection options, used to alter the data collection done by `pytkdocs`.
146+
147+
Raises:
148+
CollectionError: When there was a problem collecting the object documentation.
149+
150+
Returns:
151+
The collected object-tree.
152+
"""
153+
module_name = identifier.split(".", 1)[0]
154+
unknown_module = module_name not in self._modules_collection
155+
if config.get("fallback", False) and unknown_module:
156+
raise CollectionError("Not loading additional modules during fallback")
157+
158+
final_config = ChainMap(config, self.default_collection_config)
159+
parser_name = final_config["docstring_style"]
160+
parser_options = final_config["docstring_options"]
161+
parser = parser_name and Parser(parser_name)
162+
163+
if unknown_module:
164+
loader = GriffeLoader(
165+
extensions=load_extensions(final_config.get("extensions", [])),
166+
docstring_parser=parser,
167+
docstring_options=parser_options,
168+
modules_collection=self._modules_collection,
169+
lines_collection=self._lines_collection,
170+
)
171+
try:
172+
loader.load_module(module_name)
173+
except ImportError as error:
174+
raise CollectionError(str(error)) from error
175+
176+
unresolved, iterations = loader.resolve_aliases(only_exported=True, only_known_modules=True)
177+
if unresolved:
178+
logger.warning(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations")
179+
180+
try:
181+
doc_object = self._modules_collection[identifier]
182+
except KeyError as error: # noqa: WPS440
183+
raise CollectionError(f"{identifier} could not be found") from error
184+
185+
if not unknown_module:
186+
with suppress(AliasResolutionError):
187+
if doc_object.docstring is not None:
188+
doc_object.docstring.parser = parser
189+
doc_object.docstring.parser_options = parser_options
190+
191+
return doc_object
192+
193+
def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring)
194+
final_config = ChainMap(config, self.default_rendering_config)
195+
196+
template = self.env.get_template(f"{data.kind.value}.html")
197+
198+
# Heading level is a "state" variable, that will change at each step
199+
# of the rendering recursion. Therefore, it's easier to use it as a plain value
200+
# than as an item in a dictionary.
201+
heading_level = final_config["heading_level"]
202+
try:
203+
final_config["members_order"] = rendering.Order(final_config["members_order"])
204+
except ValueError:
205+
choices = "', '".join(item.value for item in rendering.Order)
206+
raise PluginError(f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.")
207+
208+
return template.render(
209+
**{"config": final_config, data.kind.value: data, "heading_level": heading_level, "root": True},
210+
)
211+
212+
def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore missing docstring)
213+
super().update_env(md, config)
214+
self.env.trim_blocks = True
215+
self.env.lstrip_blocks = True
216+
self.env.keep_trailing_newline = False
217+
self.env.filters["crossref"] = rendering.do_crossref
218+
self.env.filters["multi_crossref"] = rendering.do_multi_crossref
219+
self.env.filters["order_members"] = rendering.do_order_members
220+
self.env.filters["format_code"] = rendering.do_format_code
221+
self.env.filters["format_signature"] = rendering.do_format_signature
222+
223+
def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore missing docstring)
224+
try:
225+
return list({data.path, data.canonical_path, *data.aliases})
226+
except AliasResolutionError:
227+
return [data.path]
228+
56229

57230
def get_handler(
58231
theme: str, # noqa: W0613 (unused argument config)
@@ -69,7 +242,4 @@ def get_handler(
69242
Returns:
70243
An instance of `PythonHandler`.
71244
"""
72-
return PythonHandler(
73-
collector=PythonCollector(),
74-
renderer=PythonRenderer("python", theme, custom_templates),
75-
)
245+
return PythonHandler("python", theme, custom_templates)

0 commit comments

Comments
 (0)