docstring-format-checker 1.11.4__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,622 @@
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, field
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
+ "GlobalConfig",
74
+ "SectionConfig",
75
+ "Config",
76
+ "DEFAULT_CONFIG",
77
+ "load_config",
78
+ "find_config_file",
79
+ ]
80
+
81
+
82
+ ## --------------------------------------------------------------------------- #
83
+ ## Constants ####
84
+ ## --------------------------------------------------------------------------- #
85
+
86
+
87
+ VALID_TYPES: tuple[str, ...] = (
88
+ "free_text", # Free text sections (summary, details, examples, notes)
89
+ "list_name", # Simple name sections (name)
90
+ "list_type", # Simple type sections (raises, yields)
91
+ "list_name_and_type", # Params-style sections (name (type): description)
92
+ )
93
+
94
+
95
+ # ---------------------------------------------------------------------------- #
96
+ # #
97
+ # Config ####
98
+ # #
99
+ # ---------------------------------------------------------------------------- #
100
+
101
+
102
+ ## --------------------------------------------------------------------------- #
103
+ ## GlobalConfig ####
104
+ ## --------------------------------------------------------------------------- #
105
+
106
+
107
+ @dataclass
108
+ class GlobalConfig:
109
+ """
110
+ !!! note "Summary"
111
+ Global configuration for docstring checking behavior.
112
+ """
113
+
114
+ allow_undefined_sections: bool = field(
115
+ default=False,
116
+ metadata={
117
+ "title": "Allow Undefined Sections",
118
+ "description": "Allow sections not defined in the configuration.",
119
+ },
120
+ )
121
+ require_docstrings: bool = field(
122
+ default=True,
123
+ metadata={
124
+ "title": "Require Docstrings",
125
+ "description": "Require docstrings for all functions/methods.",
126
+ },
127
+ )
128
+ check_private: bool = field(
129
+ default=False,
130
+ metadata={
131
+ "title": "Check Private Members",
132
+ "description": "Check docstrings for private members (starting with an underscore).",
133
+ },
134
+ )
135
+ validate_param_types: bool = field(
136
+ default=True,
137
+ metadata={
138
+ "title": "Validate Parameter Types",
139
+ "description": "Validate that parameter types are provided in the docstring.",
140
+ },
141
+ )
142
+ optional_style: Literal["silent", "validate", "strict"] = field(
143
+ default="validate",
144
+ metadata={
145
+ "title": "Optional Style",
146
+ "description": "The style for reporting issues in optional sections.",
147
+ },
148
+ )
149
+
150
+
151
+ ## --------------------------------------------------------------------------- #
152
+ ## SectionConfig ####
153
+ ## --------------------------------------------------------------------------- #
154
+
155
+
156
+ @dataclass
157
+ class SectionConfig:
158
+ """
159
+ !!! note "Summary"
160
+ Configuration for a docstring section.
161
+ """
162
+
163
+ name: str = field(
164
+ metadata={
165
+ "title": "Name",
166
+ "description": "Name of the docstring section.",
167
+ },
168
+ )
169
+ type: Literal["free_text", "list_name", "list_type", "list_name_and_type"] = field(
170
+ metadata={
171
+ "title": "Type",
172
+ "description": "Type of the section content.",
173
+ },
174
+ )
175
+ order: Optional[int] = field(
176
+ default=None,
177
+ metadata={
178
+ "title": "Order",
179
+ "description": "Order of the section in the docstring.",
180
+ },
181
+ )
182
+ admonition: Union[bool, str] = field(
183
+ default=False,
184
+ metadata={
185
+ "title": "Admonition",
186
+ "description": "Admonition style for the section. Can be False (no admonition) or a string specifying the admonition type.",
187
+ },
188
+ )
189
+ prefix: str = field(
190
+ default="",
191
+ metadata={
192
+ "title": "Prefix",
193
+ "description": "Prefix string for the admonition values.",
194
+ },
195
+ )
196
+ required: bool = field(
197
+ default=False,
198
+ metadata={
199
+ "title": "Required",
200
+ "description": "Whether this section is required in the docstring.",
201
+ },
202
+ )
203
+ message: str = field(
204
+ default="",
205
+ metadata={
206
+ "title": "Message",
207
+ "description": "Optional message for validation errors.",
208
+ },
209
+ )
210
+
211
+ def __post_init__(self) -> None:
212
+ """
213
+ !!! note "Summary"
214
+ Validate configuration after initialization.
215
+ """
216
+ self._validate_types()
217
+ self._validate_admonition_prefix_combination()
218
+
219
+ def _validate_types(self) -> None:
220
+ """
221
+ !!! note "Summary"
222
+ Validate the 'type' field.
223
+ """
224
+ if self.type not in VALID_TYPES:
225
+ raise InvalidTypeValuesError(f"Invalid section type: {self.type}. Valid types: {VALID_TYPES}")
226
+
227
+ def _validate_admonition_prefix_combination(self) -> None:
228
+ """
229
+ !!! note "Summary"
230
+ Validate admonition and prefix combination rules.
231
+ """
232
+
233
+ if isinstance(self.admonition, bool):
234
+ # Rule: admonition cannot be True (only False or string)
235
+ if self.admonition is True:
236
+ raise ValueError(f"Section '{self.name}': admonition cannot be True, must be False or a string")
237
+
238
+ # Rule: if admonition is False, prefix cannot be provided
239
+ if self.admonition is False and self.prefix:
240
+ raise ValueError(f"Section '{self.name}': when admonition=False, prefix cannot be provided")
241
+
242
+ elif isinstance(self.admonition, str):
243
+ # Rule: if admonition is a string, prefix must be provided
244
+ if not self.prefix:
245
+ raise ValueError(f"Section '{self.name}': when admonition is a string, prefix must be provided")
246
+
247
+ else:
248
+ raise ValueError(
249
+ f"Section '{self.name}': admonition must be a boolean or string, got {type(self.admonition)}"
250
+ )
251
+
252
+
253
+ ## --------------------------------------------------------------------------- #
254
+ ## Validations ####
255
+ ## --------------------------------------------------------------------------- #
256
+
257
+
258
+ def _validate_config_order(config_sections: list[SectionConfig]) -> None:
259
+ """
260
+ !!! note "Summary"
261
+ Validate that section order values are unique.
262
+
263
+ Params:
264
+ config_sections (list[SectionConfig]):
265
+ List of section configurations to validate.
266
+
267
+ Raises:
268
+ (InvalidConfigError_DuplicateOrderValues):
269
+ If duplicate order values are found.
270
+
271
+ Returns:
272
+ (None):
273
+ Nothing is returned.
274
+ """
275
+
276
+ # Validate no duplicate order values
277
+ order_values: list[int] = [section.order for section in config_sections if section.order is not None]
278
+ seen_orders: set[int] = set()
279
+ duplicate_orders: set[int] = set()
280
+
281
+ for order in order_values:
282
+ if order in seen_orders:
283
+ duplicate_orders.add(order)
284
+ else:
285
+ seen_orders.add(order)
286
+
287
+ if duplicate_orders:
288
+ raise InvalidConfigError_DuplicateOrderValues(
289
+ f"Configuration contains duplicate order values: {sorted(duplicate_orders)}. "
290
+ "Each section must have a unique order value."
291
+ )
292
+
293
+
294
+ # ---------------------------------------------------------------------------- #
295
+ # #
296
+ # Config Container ####
297
+ # #
298
+ # ---------------------------------------------------------------------------- #
299
+
300
+
301
+ @dataclass
302
+ class Config:
303
+ """
304
+ !!! note "Summary"
305
+ Complete configuration containing global settings and section definitions.
306
+ """
307
+
308
+ global_config: GlobalConfig
309
+ sections: list[SectionConfig]
310
+
311
+
312
+ # ---------------------------------------------------------------------------- #
313
+ # #
314
+ # Default Configuration ####
315
+ # #
316
+ # ---------------------------------------------------------------------------- #
317
+
318
+
319
+ DEFAULT_SECTIONS: list[SectionConfig] = [
320
+ SectionConfig(
321
+ order=1,
322
+ name="summary",
323
+ type="free_text",
324
+ admonition="note",
325
+ prefix="!!!",
326
+ required=True,
327
+ ),
328
+ SectionConfig(
329
+ order=2,
330
+ name="details",
331
+ type="free_text",
332
+ admonition="info",
333
+ prefix="???+",
334
+ required=False,
335
+ ),
336
+ SectionConfig(
337
+ order=3,
338
+ name="params",
339
+ type="list_name_and_type",
340
+ required=True,
341
+ ),
342
+ SectionConfig(
343
+ order=4,
344
+ name="returns",
345
+ type="list_name_and_type",
346
+ required=False,
347
+ ),
348
+ SectionConfig(
349
+ order=5,
350
+ name="yields",
351
+ type="list_type",
352
+ required=False,
353
+ ),
354
+ SectionConfig(
355
+ order=6,
356
+ name="raises",
357
+ type="list_type",
358
+ required=False,
359
+ ),
360
+ SectionConfig(
361
+ order=7,
362
+ name="examples",
363
+ type="free_text",
364
+ admonition="example",
365
+ prefix="???+",
366
+ required=False,
367
+ ),
368
+ SectionConfig(
369
+ order=8,
370
+ name="notes",
371
+ type="free_text",
372
+ admonition="note",
373
+ prefix="???",
374
+ required=False,
375
+ ),
376
+ ]
377
+
378
+
379
+ DEFAULT_CONFIG: Config = Config(
380
+ global_config=GlobalConfig(),
381
+ sections=DEFAULT_SECTIONS,
382
+ )
383
+
384
+
385
+ def load_config(config_path: Optional[Union[str, Path]] = None) -> Config:
386
+ """
387
+ !!! note "Summary"
388
+ Load configuration from a TOML file or return default configuration.
389
+
390
+ Params:
391
+ config_path (Optional[Union[str, Path]]):
392
+ Path to the TOML configuration file.
393
+ If `None`, looks for `pyproject.toml` in current directory.
394
+ Default: `None`.
395
+
396
+ Raises:
397
+ (FileNotFoundError):
398
+ If the specified config file doesn't exist.
399
+ (InvalidConfigError):
400
+ If the configuration is invalid.
401
+
402
+ Returns:
403
+ (Config):
404
+ Configuration object containing global settings and section definitions.
405
+ """
406
+ # Resolve config file path
407
+ resolved_path = _resolve_config_path(config_path)
408
+ if resolved_path is None:
409
+ return DEFAULT_CONFIG
410
+
411
+ # Parse TOML configuration
412
+ config_data = _parse_toml_file(resolved_path)
413
+
414
+ # Extract tool configuration
415
+ tool_config = _extract_tool_config(config_data)
416
+ if tool_config is None:
417
+ return DEFAULT_CONFIG
418
+
419
+ # Parse configuration components
420
+ global_config = _parse_global_config(tool_config)
421
+ sections_config = _parse_sections_config(tool_config)
422
+
423
+ return Config(global_config=global_config, sections=sections_config)
424
+
425
+
426
+ def _resolve_config_path(config_path: Optional[Union[str, Path]]) -> Optional[Path]:
427
+ """
428
+ !!! note "Summary"
429
+ Resolve configuration file path.
430
+
431
+ Params:
432
+ config_path (Optional[Union[str, Path]]):
433
+ Optional path to configuration file.
434
+
435
+ Raises:
436
+ (FileNotFoundError):
437
+ If specified config file does not exist.
438
+
439
+ Returns:
440
+ (Optional[Path]):
441
+ Resolved Path object or None if no config found.
442
+ """
443
+ if config_path is None:
444
+ # Look for pyproject.toml in current directory
445
+ pyproject_path: Path = Path.cwd().joinpath("pyproject.toml")
446
+ if pyproject_path.exists():
447
+ return pyproject_path
448
+ else:
449
+ return None
450
+
451
+ # Convert to Path object and check existence
452
+ config_path = Path(config_path)
453
+ if not config_path.exists():
454
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
455
+
456
+ return config_path
457
+
458
+
459
+ def _parse_toml_file(config_path: Path) -> dict[str, Any]:
460
+ """
461
+ !!! note "Summary"
462
+ Parse TOML configuration file.
463
+
464
+ Params:
465
+ config_path (Path):
466
+ Path to TOML file to parse.
467
+
468
+ Raises:
469
+ (InvalidConfigError):
470
+ If TOML parsing fails.
471
+
472
+ Returns:
473
+ (dict[str, Any]):
474
+ Parsed TOML data as dictionary.
475
+ """
476
+ try:
477
+ with open(config_path, "rb") as f:
478
+ return tomllib.load(f)
479
+ except Exception as e:
480
+ raise InvalidConfigError(f"Failed to parse TOML file {config_path}: {e}") from e
481
+
482
+
483
+ def _extract_tool_config(config_data: dict[str, Any]) -> Optional[dict[str, Any]]:
484
+ """
485
+ !!! note "Summary"
486
+ Extract tool configuration from TOML data.
487
+
488
+ Params:
489
+ config_data (dict[str, Any]):
490
+ Parsed TOML data dictionary.
491
+
492
+ Returns:
493
+ (Optional[dict[str, Any]]):
494
+ Tool configuration dictionary or None if not found.
495
+ """
496
+ if "tool" not in config_data:
497
+ return None
498
+
499
+ tool_section = config_data["tool"]
500
+ if "dfc" in tool_section:
501
+ return tool_section["dfc"]
502
+ elif "docstring-format-checker" in tool_section:
503
+ return tool_section["docstring-format-checker"]
504
+
505
+ return None
506
+
507
+
508
+ def _parse_global_config(tool_config: dict[str, Any]) -> GlobalConfig:
509
+ """
510
+ !!! note "Summary"
511
+ Parse global configuration flags.
512
+
513
+ Params:
514
+ tool_config (dict[str, Any]):
515
+ Tool configuration dictionary.
516
+
517
+ Returns:
518
+ (GlobalConfig):
519
+ Parsed global configuration object.
520
+ """
521
+ # Validate optional_style if provided
522
+ optional_style: str = tool_config.get("optional_style", "validate")
523
+ valid_styles: tuple[str, str, str] = ("silent", "validate", "strict")
524
+ if optional_style not in valid_styles:
525
+ raise InvalidConfigError(
526
+ f"Invalid optional_style: '{optional_style}'. Must be one of: {', '.join(valid_styles)}"
527
+ )
528
+
529
+ return GlobalConfig(
530
+ allow_undefined_sections=tool_config.get("allow_undefined_sections", False),
531
+ require_docstrings=tool_config.get("require_docstrings", True),
532
+ check_private=tool_config.get("check_private", False),
533
+ validate_param_types=tool_config.get("validate_param_types", True),
534
+ optional_style=optional_style, # type:ignore
535
+ )
536
+
537
+
538
+ def _parse_sections_config(tool_config: dict[str, Any]) -> list[SectionConfig]:
539
+ """
540
+ !!! note "Summary"
541
+ Parse sections configuration.
542
+
543
+ Params:
544
+ tool_config (dict[str, Any]):
545
+ Tool configuration dictionary.
546
+
547
+ Returns:
548
+ (list[SectionConfig]):
549
+ List of section configuration objects or defaults.
550
+ """
551
+ if "sections" not in tool_config:
552
+ return DEFAULT_SECTIONS
553
+
554
+ sections_config: list[SectionConfig] = []
555
+ sections_data = tool_config["sections"]
556
+
557
+ for section_data in sections_data:
558
+ try:
559
+ # Get admonition value with proper default handling
560
+ admonition_value: Union[str, bool] = section_data.get("admonition")
561
+ if admonition_value is None:
562
+ admonition_value = False # Use SectionConfig default
563
+
564
+ section = SectionConfig(
565
+ order=section_data.get("order"),
566
+ name=section_data.get("name", ""),
567
+ type=section_data.get("type", ""),
568
+ admonition=admonition_value,
569
+ prefix=section_data.get("prefix", ""),
570
+ required=section_data.get("required", False),
571
+ )
572
+ sections_config.append(section)
573
+ except (KeyError, TypeError, ValueError, InvalidTypeValuesError) as e:
574
+ raise InvalidConfigError(f"Invalid section configuration: {section_data}. Error: {e}") from e
575
+
576
+ # Validate and sort sections
577
+ if sections_config:
578
+ _validate_config_order(config_sections=sections_config)
579
+ sections_config.sort(key=lambda x: x.order if x.order is not None else float("inf"))
580
+ else:
581
+ sections_config = DEFAULT_SECTIONS
582
+
583
+ return sections_config
584
+
585
+
586
+ def find_config_file(start_path: Optional[Path] = None) -> Optional[Path]:
587
+ """
588
+ !!! note "Summary"
589
+ Find configuration file by searching up the directory tree.
590
+
591
+ Params:
592
+ start_path (Optional[Path]):
593
+ Directory to start searching from.
594
+ If `None`, resolves to current directory.
595
+ Default: `None`.
596
+
597
+ Returns:
598
+ (Optional[Path]):
599
+ Path to the configuration file if found, None otherwise.
600
+ """
601
+ if start_path is None:
602
+ start_path = Path.cwd()
603
+
604
+ current_path: Path = start_path.resolve()
605
+
606
+ while current_path != current_path.parent:
607
+ pyproject_path: Path = current_path.joinpath("pyproject.toml")
608
+ if pyproject_path.exists():
609
+ # Check if it contains dfc configuration
610
+ try:
611
+ with open(pyproject_path, "rb") as f:
612
+ config_data: dict[str, Any] = tomllib.load(f)
613
+ if "tool" in config_data and (
614
+ "dfc" in config_data["tool"] or "docstring-format-checker" in config_data["tool"]
615
+ ):
616
+ return pyproject_path
617
+ except Exception:
618
+ pass
619
+
620
+ current_path = current_path.parent
621
+
622
+ return None