Coverage for src/mkdocstrings_handlers/shell/_internal/config.py: 70.51%
70 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-26 22:03 +0100
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-26 22:03 +0100
1# Configuration and options dataclasses.
3from __future__ import annotations
5import sys
6from dataclasses import field
7from typing import TYPE_CHECKING, Annotated, Any, Literal
9from mkdocstrings import get_logger
11# YORE: EOL 3.10: Replace block with line 2.
12if sys.version_info >= (3, 11): 12 ↛ 15line 12 didn't jump to line 15 because the condition on line 12 was always true
13 from typing import Self
14else:
15 from typing_extensions import Self
18_logger = get_logger(__name__)
21try:
22 # When Pydantic is available, use it to validate options (done automatically).
23 # Users can therefore opt into validation by installing Pydantic in development/CI.
24 # When building the docs to deploy them, Pydantic is not required anymore.
26 # When building our own docs, Pydantic is always installed (see `docs` group in `pyproject.toml`)
27 # to allow automatic generation of a JSON Schema. The JSON Schema is then referenced by mkdocstrings,
28 # which is itself referenced by mkdocs-material's schema system. For example in VSCode:
29 #
30 # "yaml.schemas": {
31 # "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml"
32 # }
33 import pydantic
35 if getattr(pydantic, "__version__", "1.").startswith("1."): 35 ↛ 36line 35 didn't jump to line 36 because the condition on line 35 was never true
36 raise ImportError # noqa: TRY301
38 # YORE: EOL 3.9: Remove block.
39 if sys.version_info < (3, 10): 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true
40 try:
41 import eval_type_backport # noqa: F401
42 except ImportError:
43 _logger.debug(
44 "Pydantic needs the `eval-type-backport` package to be installed "
45 "for modern type syntax to work on Python 3.9. "
46 "Deactivating Pydantic validation for Shell handler options.",
47 )
48 raise
50 from inspect import cleandoc
52 from pydantic import Field as BaseField
53 from pydantic.dataclasses import dataclass
55 _base_url = "https://mkdocstrings.github.io/shell/usage"
57 def _Field( # noqa: N802
58 *args: Any,
59 description: str,
60 group: Literal["general"] | None = None,
61 parent: str | None = None,
62 **kwargs: Any,
63 ) -> None:
64 def _add_markdown_description(schema: dict[str, Any]) -> None:
65 url = f"{_base_url}/{f'configuration/{group}/' if group else ''}#{parent or schema['title']}"
66 schema["markdownDescription"] = f"[DOCUMENTATION]({url})\n\n{schema['description']}"
68 return BaseField(
69 *args,
70 description=cleandoc(description),
71 field_title_generator=lambda name, _: name,
72 json_schema_extra=_add_markdown_description,
73 **kwargs,
74 )
75except ImportError:
76 from dataclasses import dataclass # type: ignore[no-redef]
78 def _Field(*args: Any, **kwargs: Any) -> None: # type: ignore[misc] # noqa: N802
79 pass
82if TYPE_CHECKING:
83 from collections.abc import MutableMapping
86# YORE: EOL 3.9: Remove block.
87_dataclass_options = {"frozen": True}
88if sys.version_info >= (3, 10): 88 ↛ 94line 88 didn't jump to line 94 because the condition on line 88 was always true
89 _dataclass_options["kw_only"] = True
92# The input config class is useful to generate a JSON schema, see scripts/mkdocs_hooks.py.
93# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line.
94@dataclass(**_dataclass_options) # type: ignore[call-overload]
95class ShellInputOptions:
96 """Accepted input options."""
98 extra: Annotated[
99 dict[str, Any],
100 _Field(
101 group="general",
102 description="Extra options.",
103 ),
104 ] = field(default_factory=dict)
106 heading: Annotated[
107 str,
108 _Field(
109 group="headings",
110 description="A custom string to override the autogenerated heading of the root object.",
111 ),
112 ] = ""
114 heading_level: Annotated[
115 int,
116 _Field(
117 group="headings",
118 description="The initial heading level to use.",
119 ),
120 ] = 2
122 show_symbol_type_heading: Annotated[
123 bool,
124 _Field(
125 group="headings",
126 description="Show the symbol type in headings (e.g. mod, class, meth, func and attr).",
127 ),
128 ] = False
130 show_symbol_type_toc: Annotated[
131 bool,
132 _Field(
133 group="headings",
134 description="Show the symbol type in the Table of Contents (e.g. mod, class, methd, func and attr).",
135 ),
136 ] = False
138 toc_label: Annotated[
139 str,
140 _Field(
141 group="headings",
142 description="A custom string to override the autogenerated toc label of the root object.",
143 ),
144 ] = ""
146 @classmethod
147 def coerce(cls, **data: Any) -> MutableMapping[str, Any]:
148 """Coerce data."""
149 return data
151 @classmethod
152 def from_data(cls, **data: Any) -> Self:
153 """Create an instance from a dictionary."""
154 return cls(**cls.coerce(**data))
157# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line.
158@dataclass(**_dataclass_options) # type: ignore[call-overload]
159class ShellOptions(ShellInputOptions): # type: ignore[override,unused-ignore]
160 """Final options passed as template context."""
162 # Re-declare any field to modify/narrow its type.
164 @classmethod
165 def coerce(cls, **data: Any) -> MutableMapping[str, Any]:
166 """Create an instance from a dictionary."""
167 # Coerce any field into its final form.
168 return super().coerce(**data)
171# The input config class is useful to generate a JSON schema, see scripts/mkdocs_hooks.py.
172# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line.
173@dataclass(**_dataclass_options) # type: ignore[call-overload]
174class ShellInputConfig:
175 """Shell handler configuration."""
177 # We want to validate options early, so we load them as `ShellInputOptions`.
178 options: Annotated[
179 ShellInputOptions,
180 _Field(description="Configuration options for collecting and rendering objects."),
181 ] = field(default_factory=ShellInputOptions)
183 @classmethod
184 def coerce(cls, **data: Any) -> MutableMapping[str, Any]:
185 """Coerce data."""
186 return data
188 @classmethod
189 def from_data(cls, **data: Any) -> Self:
190 """Create an instance from a dictionary."""
191 return cls(**cls.coerce(**data))
194# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line.
195@dataclass(**_dataclass_options) # type: ignore[call-overload]
196class ShellConfig(ShellInputConfig): # type: ignore[override,unused-ignore]
197 """Shell handler configuration."""
199 # We want to keep a simple dictionary in order to later merge global and local options.
200 options: dict[str, Any] = field(default_factory=dict) # type: ignore[assignment]
201 """Global options in mkdocs.yml."""
203 @classmethod
204 def coerce(cls, **data: Any) -> MutableMapping[str, Any]:
205 """Coerce data."""
206 return super().coerce(**data)