|
1 |
| -# Copyright (c) 2022=2023. Analog Devices Inc. |
| 1 | +# Copyright (c) 2022-2025. Analog Devices Inc. |
2 | 2 | #
|
3 | 3 | # Licensed under the Apache License, Version 2.0 (the "License");
|
4 | 4 | # you may not use this file except in compliance with the License.
|
|
17 | 17 |
|
18 | 18 | from __future__ import annotations
|
19 | 19 |
|
20 |
| -from collections import ChainMap |
| 20 | +import sys |
| 21 | +from dataclasses import dataclass, fields |
21 | 22 | from pathlib import Path
|
22 |
| -from typing import Any, List, Mapping, Optional |
| 23 | +from textwrap import dedent |
| 24 | +from typing import Annotated, Any, ClassVar, Mapping, MutableMapping, Optional |
| 25 | +from warnings import warn |
23 | 26 |
|
24 |
| -from griffe.dataclasses import Object |
| 27 | +from mkdocs.config.defaults import MkDocsConfig |
| 28 | +from mkdocstrings.handlers.base import CollectorItem |
25 | 29 | from mkdocstrings.loggers import get_logger
|
| 30 | +from mkdocstrings_handlers.python.config import PythonOptions, Field, PythonConfig |
26 | 31 | from mkdocstrings_handlers.python.handler import PythonHandler
|
27 | 32 |
|
28 | 33 | from .crossref import substitute_relative_crossrefs
|
|
33 | 38 |
|
34 | 39 | logger = get_logger(__name__)
|
35 | 40 |
|
| 41 | + |
| 42 | +# TODO mkdocstrings 0.28 |
| 43 | +# - `name` and `domain` (py) must be specified as class attributes |
| 44 | +# - `handler` arg to superclass is deprecated |
| 45 | +# - add `mdx` arg to constructor to pass on to superclass |
| 46 | +# - `config_file_path` arg will no longer be passed |
| 47 | +# |
| 48 | + |
| 49 | +# TODO python 3.9 - remove when 3.9 support is dropped |
| 50 | +_dataclass_options = {"frozen": True} |
| 51 | +if sys.version_info >= (3, 10): |
| 52 | + _dataclass_options["kw_only"] = True |
| 53 | + |
| 54 | +@dataclass(**_dataclass_options) |
| 55 | +class PythonRelXRefOptions(PythonOptions): |
| 56 | + check_crossrefs: Annotated[ |
| 57 | + bool, |
| 58 | + Field( |
| 59 | + group="docstrings", |
| 60 | + parent="docstring_options", |
| 61 | + description=dedent( |
| 62 | + """ |
| 63 | + Enables early checking of all cross-references. |
| 64 | + |
| 65 | + Note that this option only takes affect if **relative_crossrefs** is |
| 66 | + also true. This option is true by default, so this option is used to |
| 67 | + disable checking. Checking can also be disabled on a per-case basis by |
| 68 | + prefixing the reference with '?', e.g. `[something][?dontcheckme]`. |
| 69 | + """ |
| 70 | + ), |
| 71 | + ), |
| 72 | + ] = True |
| 73 | + |
36 | 74 | class PythonRelXRefHandler(PythonHandler):
|
37 | 75 | """Extended version of mkdocstrings Python handler
|
38 | 76 |
|
39 | 77 | * Converts relative cross-references into full references
|
40 | 78 | * Checks cross-references early in order to produce errors with source location
|
41 | 79 | """
|
42 | 80 |
|
43 |
| - handler_name: str = __name__.rsplit('.', 2)[1] |
44 |
| - |
45 |
| - default_config = dict( |
46 |
| - PythonHandler.default_config, |
47 |
| - relative_crossrefs = False, |
48 |
| - check_crossrefs = True, |
49 |
| - ) |
50 |
| - |
51 |
| - def __init__(self, |
52 |
| - theme: str, |
53 |
| - custom_templates: Optional[str] = None, |
54 |
| - config_file_path: Optional[str] = None, |
55 |
| - paths: Optional[List[str]] = None, |
56 |
| - locale: str = "en", |
57 |
| - **_config: Any, |
58 |
| - ): |
59 |
| - super().__init__( |
60 |
| - handler = self.handler_name, |
61 |
| - theme = theme, |
62 |
| - custom_templates = custom_templates, |
63 |
| - config_file_path = config_file_path, |
64 |
| - paths = paths, |
65 |
| - locale=locale, |
| 81 | + name: ClassVar[str] = "python_xref" |
| 82 | + """Override the handler name""" |
| 83 | + |
| 84 | + def __init__(self, config: PythonConfig, base_dir: Path, **kwargs: Any) -> None: |
| 85 | + """Initialize the handler. |
| 86 | +
|
| 87 | + Parameters: |
| 88 | + config: The handler configuration. |
| 89 | + base_dir: The base directory of the project. |
| 90 | + **kwargs: Arguments passed to the parent constructor. |
| 91 | + """ |
| 92 | + check_crossrefs = config.options.pop('check_crossrefs', None) # Remove |
| 93 | + super().__init__(config, base_dir, **kwargs) |
| 94 | + if check_crossrefs is not None: |
| 95 | + self.global_options["check_crossrefs"] = check_crossrefs |
| 96 | + |
| 97 | + def get_options(self, local_options: Mapping[str, Any]) -> PythonRelXRefOptions: |
| 98 | + local_options = dict(local_options) |
| 99 | + check_crossrefs = local_options.pop('check_crossrefs', None) |
| 100 | + _opts = super().get_options(local_options) |
| 101 | + opts = PythonRelXRefOptions( |
| 102 | + **{field.name: getattr(_opts, field.name) for field in fields(_opts)} |
66 | 103 | )
|
67 |
| - |
68 |
| - def render(self, data: Object, config: Mapping[str,Any]) -> str: |
69 |
| - final_config = ChainMap(config, self.default_config) # type: ignore[arg-type] |
70 |
| - |
71 |
| - if final_config["relative_crossrefs"]: |
72 |
| - checkref = self._check_ref if final_config["check_crossrefs"] else None |
| 104 | + if check_crossrefs is not None: |
| 105 | + opts.check_crossrefs = bool(check_crossrefs) |
| 106 | + return opts |
| 107 | + |
| 108 | + def render(self, data: CollectorItem, options: PythonOptions) -> str: |
| 109 | + if options.relative_crossrefs: |
| 110 | + if isinstance(options, PythonRelXRefOptions): |
| 111 | + checkref = self._check_ref if options.check_crossrefs else None |
| 112 | + else: |
| 113 | + checkref = None |
73 | 114 | substitute_relative_crossrefs(data, checkref=checkref)
|
74 | 115 |
|
75 | 116 | try:
|
76 |
| - return super().render(data, config) |
| 117 | + return super().render(data, options) |
77 | 118 | except Exception: # pragma: no cover
|
78 | 119 | print(f"{data.path=}")
|
79 | 120 | raise
|
80 | 121 |
|
81 | 122 | def get_templates_dir(self, handler: Optional[str] = None) -> Path:
|
82 | 123 | """See [render][.barf]"""
|
83 |
| - if handler == self.handler_name: |
| 124 | + if handler == self.name: |
84 | 125 | handler = 'python'
|
85 | 126 | return super().get_templates_dir(handler)
|
86 | 127 |
|
87 | 128 | def _check_ref(self, ref:str) -> bool:
|
88 | 129 | """Check for existence of reference"""
|
89 | 130 | try:
|
90 |
| - self.collect(ref, {}) |
| 131 | + self.collect(ref, PythonOptions()) |
91 | 132 | return True
|
92 | 133 | except Exception: # pylint: disable=broad-except
|
93 | 134 | # Only expect a CollectionError but we may as well catch everything.
|
94 | 135 | return False
|
95 | 136 |
|
| 137 | +def get_handler( |
| 138 | + handler_config: MutableMapping[str, Any], |
| 139 | + tool_config: MkDocsConfig, |
| 140 | + **kwargs: Any, |
| 141 | +) -> PythonHandler: |
| 142 | + """Simply return an instance of `PythonRelXRefHandler`. |
| 143 | +
|
| 144 | + Arguments: |
| 145 | + handler_config: The handler configuration. |
| 146 | + tool_config: The tool (SSG) configuration. |
| 147 | +
|
| 148 | + Returns: |
| 149 | + An instance of `PythonRelXRefHandler`. |
| 150 | + """ |
| 151 | + base_dir = Path(tool_config.config_file_path or "./mkdocs.yml").parent |
| 152 | + if "inventories" not in handler_config and "import" in handler_config: |
| 153 | + warn("The 'import' key is renamed 'inventories' for the Python handler", FutureWarning, stacklevel=1) |
| 154 | + handler_config["inventories"] = handler_config.pop("import", []) |
| 155 | + return PythonRelXRefHandler( |
| 156 | + config=PythonConfig.from_data(**handler_config), |
| 157 | + base_dir=base_dir, |
| 158 | + **kwargs, |
| 159 | + ) |
0 commit comments