|
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 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