Coverage for src/docstring_format_checker/config.py: 100%
85 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-04 12:45 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-04 12:45 +0000
1# ============================================================================ #
2# #
3# Title: Configuration Management #
4# Purpose: Configuration for docstring format checking #
5# Notes: Notes #
6# Author: chrimaho #
7# Created: Created #
8# References: References #
9# Sources: Sources #
10# Edited: Edited #
11# #
12# ============================================================================ #
15# ---------------------------------------------------------------------------- #
16# #
17# Overview ####
18# #
19# ---------------------------------------------------------------------------- #
22# ---------------------------------------------------------------------------- #
23# Description ####
24# ---------------------------------------------------------------------------- #
27"""
28!!! note "Summary"
29 Configuration handling for the docstring format checker.
30"""
33# ---------------------------------------------------------------------------- #
34# #
35# Setup ####
36# #
37# ---------------------------------------------------------------------------- #
40## --------------------------------------------------------------------------- #
41## Imports ####
42## --------------------------------------------------------------------------- #
45# ## Python StdLib Imports ----
46import sys
47from dataclasses import dataclass
48from pathlib import Path
49from typing import Any, Literal, Optional, Union
51# ## Local First Party Imports ----
52from docstring_format_checker.utils.exceptions import (
53 InvalidConfigError,
54 InvalidConfigError_DuplicateOrderValues,
55 InvalidTypeValuesError,
56)
59if sys.version_info >= (3, 11):
60 # ## Python StdLib Imports ----
61 import tomllib
62else:
63 # ## Python Third Party Imports ----
64 import tomli as tomllib
67## --------------------------------------------------------------------------- #
68## Exports ####
69## --------------------------------------------------------------------------- #
72__all__: list[str] = [
73 "SectionConfig",
74 "DEFAULT_CONFIG",
75 "load_config",
76 "find_config_file",
77]
80## --------------------------------------------------------------------------- #
81## Constants ####
82## --------------------------------------------------------------------------- #
85VALID_TYPES: tuple[str, ...] = (
86 "free_text", # Free text sections (summary, details, examples, notes)
87 "list_name", # Simple name sections (name)
88 "list_type", # Simple type sections (raises, yields)
89 "list_name_and_type", # Params-style sections (name (type): description)
90)
93# ---------------------------------------------------------------------------- #
94# #
95# Helpers ####
96# #
97# ---------------------------------------------------------------------------- #
100## --------------------------------------------------------------------------- #
101## Classes ####
102## --------------------------------------------------------------------------- #
105@dataclass
106class SectionConfig:
107 """
108 Configuration for a docstring section.
109 """
111 order: int
112 name: str
113 type: Literal["free_text", "list_name", "list_type", "list_name_and_type"]
114 admonition: str = ""
115 prefix: str = "" # Support any prefix string
116 required: bool = False
117 message: str = "" # Optional message for validation errors
119 def __post_init__(self) -> None:
120 """Validate configuration after initialization."""
121 if self.type not in VALID_TYPES:
122 raise InvalidTypeValuesError(f"Invalid section type: {self.type}. Valid types: {VALID_TYPES}")
125## --------------------------------------------------------------------------- #
126## Validations ####
127## --------------------------------------------------------------------------- #
130def _validate_config_order(config_sections: list[SectionConfig]) -> None:
132 # Validate no duplicate order values
133 order_values: list[int] = [section.order for section in config_sections]
134 seen_orders: set[int] = set()
135 duplicate_orders: set[int] = set()
137 for order in order_values:
138 if order in seen_orders:
139 duplicate_orders.add(order)
140 else:
141 seen_orders.add(order)
143 if duplicate_orders:
144 raise InvalidConfigError_DuplicateOrderValues(
145 f"Configuration contains duplicate order values: {sorted(duplicate_orders)}. "
146 "Each section must have a unique order value."
147 )
150# ---------------------------------------------------------------------------- #
151# #
152# Main Section ####
153# #
154# ---------------------------------------------------------------------------- #
157DEFAULT_CONFIG: list[SectionConfig] = [
158 SectionConfig(
159 order=1,
160 name="summary",
161 type="free_text",
162 admonition="note",
163 prefix="!!!",
164 required=True,
165 ),
166 SectionConfig(
167 order=2,
168 name="details",
169 type="free_text",
170 admonition="info",
171 prefix="???+",
172 required=False,
173 ),
174 SectionConfig(
175 order=3,
176 name="params",
177 type="list_name_and_type",
178 required=True,
179 ),
180 SectionConfig(
181 order=4,
182 name="returns",
183 type="list_name_and_type",
184 required=False,
185 ),
186 SectionConfig(
187 order=5,
188 name="yields",
189 type="list_type",
190 required=False,
191 ),
192 SectionConfig(
193 order=6,
194 name="raises",
195 type="list_type",
196 required=False,
197 ),
198 SectionConfig(
199 order=7,
200 name="examples",
201 type="free_text",
202 admonition="example",
203 prefix="???+",
204 required=False,
205 ),
206 SectionConfig(
207 order=8,
208 name="notes",
209 type="free_text",
210 admonition="note",
211 prefix="???",
212 required=False,
213 ),
214]
217def load_config(config_path: Optional[Union[str, Path]] = None) -> list[SectionConfig]:
218 """
219 !!! note "Summary"
220 Load configuration from a TOML file or return default configuration.
222 Params:
223 config_path (Optional[Union[str, Path]]):
224 Path to the TOML configuration file.
225 If `None`, looks for `pyproject.toml` in current directory.
226 Default: `None`.
228 Returns:
229 (list[SectionConfig]):
230 List of SectionConfig objects defining the docstring sections to check.
232 Raises:
233 (FileNotFoundError):
234 If the specified config file doesn't exist.
235 (InvalidConfigError):
236 If the configuration is invalid.
237 """
239 if config_path is None:
240 # Look for pyproject.toml in current directory
241 pyproject_path: Path = Path.cwd().joinpath("pyproject.toml")
242 if pyproject_path.exists():
243 config_path = pyproject_path
244 else:
245 return DEFAULT_CONFIG
247 config_path = Path(config_path)
248 if not config_path.exists():
249 raise FileNotFoundError(f"Configuration file not found: {config_path}")
251 try:
252 with open(config_path, "rb") as f:
253 config_data: dict[str, Any] = tomllib.load(f)
254 except Exception as e:
255 raise InvalidConfigError(f"Failed to parse TOML file {config_path}: {e}") from e
257 # Try to find configuration under [tool.dfc] or [tool.docstring-format-checker]
258 tool_config = None
259 if "tool" in config_data:
260 if "dfc" in config_data["tool"]:
261 tool_config = config_data["tool"]["dfc"]
262 elif "docstring-format-checker" in config_data["tool"]:
263 tool_config = config_data["tool"]["docstring-format-checker"]
265 if tool_config is None:
266 return DEFAULT_CONFIG
268 # Parse sections configuration
269 sections_config: list[SectionConfig] = []
270 if "sections" in tool_config:
271 sections_data = tool_config["sections"]
272 for section_data in sections_data:
273 try:
274 section = SectionConfig(
275 order=section_data.get("order", 0),
276 name=section_data.get("name", ""),
277 type=section_data.get("type", ""),
278 admonition=section_data.get("admonition", ""),
279 prefix=section_data.get("prefix", ""),
280 required=section_data.get("required", False),
281 )
282 sections_config.append(section)
283 except (KeyError, TypeError, ValueError, InvalidTypeValuesError) as e:
284 raise InvalidConfigError(f"Invalid section configuration: {section_data}. Error: {e}") from e
286 if not sections_config:
287 return DEFAULT_CONFIG
289 # Validate no duplicate order values
290 _validate_config_order(config_sections=sections_config)
292 # Sort by order
293 sections_config.sort(key=lambda x: x.order)
295 return sections_config
298def find_config_file(start_path: Optional[Path] = None) -> Optional[Path]:
299 """
300 !!! note "Summary"
301 Find configuration file by searching up the directory tree.
303 Params:
304 start_path (Optional[Path]):
305 Directory to start searching from.
306 If `None`, resolves to current directory.
307 Default: `None`.
309 Returns:
310 (Optional[Path]):
311 Path to the configuration file if found, None otherwise.
312 """
313 if start_path is None:
314 start_path = Path.cwd()
316 current_path: Path = start_path.resolve()
318 while current_path != current_path.parent:
319 pyproject_path: Path = current_path.joinpath("pyproject.toml")
320 if pyproject_path.exists():
321 # Check if it contains dfc configuration
322 try:
323 with open(pyproject_path, "rb") as f:
324 config_data: dict[str, Any] = tomllib.load(f)
325 if "tool" in config_data and (
326 "dfc" in config_data["tool"] or "docstring-format-checker" in config_data["tool"]
327 ):
328 return pyproject_path
329 except Exception:
330 pass
332 current_path = current_path.parent
334 return None