docstring-format-checker 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,334 @@
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
+ # ============================================================================ #
13
+
14
+
15
+ # ---------------------------------------------------------------------------- #
16
+ # #
17
+ # Overview ####
18
+ # #
19
+ # ---------------------------------------------------------------------------- #
20
+
21
+
22
+ # ---------------------------------------------------------------------------- #
23
+ # Description ####
24
+ # ---------------------------------------------------------------------------- #
25
+
26
+
27
+ """
28
+ !!! note "Summary"
29
+ Configuration handling for the docstring format checker.
30
+ """
31
+
32
+
33
+ # ---------------------------------------------------------------------------- #
34
+ # #
35
+ # Setup ####
36
+ # #
37
+ # ---------------------------------------------------------------------------- #
38
+
39
+
40
+ ## --------------------------------------------------------------------------- #
41
+ ## Imports ####
42
+ ## --------------------------------------------------------------------------- #
43
+
44
+
45
+ # ## Python StdLib Imports ----
46
+ import sys
47
+ from dataclasses import dataclass
48
+ from pathlib import Path
49
+ from typing import Any, Literal, Optional, Union
50
+
51
+ # ## Local First Party Imports ----
52
+ from docstring_format_checker.utils.exceptions import (
53
+ InvalidConfigError,
54
+ InvalidConfigError_DuplicateOrderValues,
55
+ InvalidTypeValuesError,
56
+ )
57
+
58
+
59
+ if sys.version_info >= (3, 11):
60
+ # ## Python StdLib Imports ----
61
+ import tomllib
62
+ else:
63
+ # ## Python Third Party Imports ----
64
+ import tomli as tomllib
65
+
66
+
67
+ ## --------------------------------------------------------------------------- #
68
+ ## Exports ####
69
+ ## --------------------------------------------------------------------------- #
70
+
71
+
72
+ __all__: list[str] = [
73
+ "SectionConfig",
74
+ "DEFAULT_CONFIG",
75
+ "load_config",
76
+ "find_config_file",
77
+ ]
78
+
79
+
80
+ ## --------------------------------------------------------------------------- #
81
+ ## Constants ####
82
+ ## --------------------------------------------------------------------------- #
83
+
84
+
85
+ VALID_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
+ )
91
+
92
+
93
+ # ---------------------------------------------------------------------------- #
94
+ # #
95
+ # Helpers ####
96
+ # #
97
+ # ---------------------------------------------------------------------------- #
98
+
99
+
100
+ ## --------------------------------------------------------------------------- #
101
+ ## Classes ####
102
+ ## --------------------------------------------------------------------------- #
103
+
104
+
105
+ @dataclass
106
+ class SectionConfig:
107
+ """
108
+ Configuration for a docstring section.
109
+ """
110
+
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
118
+
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}")
123
+
124
+
125
+ ## --------------------------------------------------------------------------- #
126
+ ## Validations ####
127
+ ## --------------------------------------------------------------------------- #
128
+
129
+
130
+ def _validate_config_order(config_sections: list[SectionConfig]) -> None:
131
+
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()
136
+
137
+ for order in order_values:
138
+ if order in seen_orders:
139
+ duplicate_orders.add(order)
140
+ else:
141
+ seen_orders.add(order)
142
+
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
+ )
148
+
149
+
150
+ # ---------------------------------------------------------------------------- #
151
+ # #
152
+ # Main Section ####
153
+ # #
154
+ # ---------------------------------------------------------------------------- #
155
+
156
+
157
+ DEFAULT_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
+ ]
215
+
216
+
217
+ def 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.
221
+
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`.
227
+
228
+ Returns:
229
+ (list[SectionConfig]):
230
+ List of SectionConfig objects defining the docstring sections to check.
231
+
232
+ Raises:
233
+ (FileNotFoundError):
234
+ If the specified config file doesn't exist.
235
+ (InvalidConfigError):
236
+ If the configuration is invalid.
237
+ """
238
+
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
246
+
247
+ config_path = Path(config_path)
248
+ if not config_path.exists():
249
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
250
+
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
256
+
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"]
264
+
265
+ if tool_config is None:
266
+ return DEFAULT_CONFIG
267
+
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
285
+
286
+ if not sections_config:
287
+ return DEFAULT_CONFIG
288
+
289
+ # Validate no duplicate order values
290
+ _validate_config_order(config_sections=sections_config)
291
+
292
+ # Sort by order
293
+ sections_config.sort(key=lambda x: x.order)
294
+
295
+ return sections_config
296
+
297
+
298
+ def find_config_file(start_path: Optional[Path] = None) -> Optional[Path]:
299
+ """
300
+ !!! note "Summary"
301
+ Find configuration file by searching up the directory tree.
302
+
303
+ Params:
304
+ start_path (Optional[Path]):
305
+ Directory to start searching from.
306
+ If `None`, resolves to current directory.
307
+ Default: `None`.
308
+
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()
315
+
316
+ current_path: Path = start_path.resolve()
317
+
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
331
+
332
+ current_path = current_path.parent
333
+
334
+ return None