docstring-format-checker 0.7.0__tar.gz → 0.9.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstring-format-checker
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: A CLI tool to check and validate Python docstring formatting and completeness
5
5
  Author: Chris Mahoney
6
6
  Author-email: Chris Mahoney <docstring-format-checker@data-science-extensions.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "docstring-format-checker"
3
- version = "v0.7.0"
3
+ version = "v0.9.0"
4
4
  description = "A CLI tool to check and validate Python docstring formatting and completeness"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -4,7 +4,7 @@ Docstring Format Checker.
4
4
  A CLI tool to check and validate Python docstring formatting and completeness.
5
5
  """
6
6
 
7
- __version__ = "v0.7.0"
7
+ __version__ = "v0.9.0"
8
8
  __author__ = "Chris Mahoney"
9
9
  __email__ = "docstring-format-checker@data-science-extensions.com"
10
10
 
@@ -56,7 +56,11 @@ from typer import Argument, CallbackParam, Context, Exit, Option, Typer, echo
56
56
 
57
57
  # ## Local First Party Imports ----
58
58
  from docstring_format_checker import __version__
59
- from docstring_format_checker.config import SectionConfig, find_config_file, load_config
59
+ from docstring_format_checker.config import (
60
+ Config,
61
+ find_config_file,
62
+ load_config,
63
+ )
60
64
  from docstring_format_checker.core import DocstringChecker, DocstringError
61
65
 
62
66
 
@@ -527,21 +531,21 @@ def check_docstrings(
527
531
  if not config_path.exists():
528
532
  console.print(_red(f"Error: Configuration file does not exist: {config}"))
529
533
  raise Exit(1)
530
- sections_config = load_config(config_path)
534
+ config_obj = load_config(config_path)
531
535
  else:
532
536
  # Try to find config file automatically
533
537
  found_config: Optional[Path] = find_config_file(target_path if target_path.is_dir() else target_path.parent)
534
538
  if found_config:
535
- sections_config: list[SectionConfig] = load_config(found_config)
539
+ config_obj: Config = load_config(found_config)
536
540
  else:
537
- sections_config: list[SectionConfig] = load_config()
541
+ config_obj: Config = load_config()
538
542
 
539
543
  except Exception as e:
540
544
  console.print(_red(f"Error loading configuration: {e}"))
541
545
  raise Exit(1)
542
546
 
543
547
  # Initialize checker
544
- checker = DocstringChecker(sections_config)
548
+ checker = DocstringChecker(config_obj)
545
549
 
546
550
  # Check files
547
551
  try:
@@ -70,7 +70,9 @@ else:
70
70
 
71
71
 
72
72
  __all__: list[str] = [
73
+ "GlobalConfig",
73
74
  "SectionConfig",
75
+ "Config",
74
76
  "DEFAULT_CONFIG",
75
77
  "load_config",
76
78
  "find_config_file",
@@ -92,13 +94,29 @@ VALID_TYPES: tuple[str, ...] = (
92
94
 
93
95
  # ---------------------------------------------------------------------------- #
94
96
  # #
95
- # Helpers ####
97
+ # Config ####
96
98
  # #
97
99
  # ---------------------------------------------------------------------------- #
98
100
 
99
101
 
100
102
  ## --------------------------------------------------------------------------- #
101
- ## Classes ####
103
+ ## GlobalConfig ####
104
+ ## --------------------------------------------------------------------------- #
105
+
106
+
107
+ @dataclass
108
+ class GlobalConfig:
109
+ """
110
+ Global configuration for docstring checking behavior.
111
+ """
112
+
113
+ allow_undefined_sections: bool = False
114
+ require_docstrings: bool = True
115
+ check_private: bool = False
116
+
117
+
118
+ ## --------------------------------------------------------------------------- #
119
+ ## SectionConfig ####
102
120
  ## --------------------------------------------------------------------------- #
103
121
 
104
122
 
@@ -176,12 +194,29 @@ def _validate_config_order(config_sections: list[SectionConfig]) -> None:
176
194
 
177
195
  # ---------------------------------------------------------------------------- #
178
196
  # #
179
- # Main Section ####
197
+ # Config Container ####
180
198
  # #
181
199
  # ---------------------------------------------------------------------------- #
182
200
 
183
201
 
184
- DEFAULT_CONFIG: list[SectionConfig] = [
202
+ @dataclass
203
+ class Config:
204
+ """
205
+ Complete configuration containing global settings and section definitions.
206
+ """
207
+
208
+ global_config: GlobalConfig
209
+ sections: list[SectionConfig]
210
+
211
+
212
+ # ---------------------------------------------------------------------------- #
213
+ # #
214
+ # Default Configuration ####
215
+ # #
216
+ # ---------------------------------------------------------------------------- #
217
+
218
+
219
+ DEFAULT_SECTIONS: list[SectionConfig] = [
185
220
  SectionConfig(
186
221
  order=1,
187
222
  name="summary",
@@ -241,7 +276,13 @@ DEFAULT_CONFIG: list[SectionConfig] = [
241
276
  ]
242
277
 
243
278
 
244
- def load_config(config_path: Optional[Union[str, Path]] = None) -> list[SectionConfig]:
279
+ DEFAULT_CONFIG: Config = Config(
280
+ global_config=GlobalConfig(),
281
+ sections=DEFAULT_SECTIONS,
282
+ )
283
+
284
+
285
+ def load_config(config_path: Optional[Union[str, Path]] = None) -> Config:
245
286
  """
246
287
  !!! note "Summary"
247
288
  Load configuration from a TOML file or return default configuration.
@@ -253,8 +294,8 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> list[SectionC
253
294
  Default: `None`.
254
295
 
255
296
  Returns:
256
- (list[SectionConfig]):
257
- List of SectionConfig objects defining the docstring sections to check.
297
+ (Config):
298
+ Configuration object containing global settings and section definitions.
258
299
 
259
300
  Raises:
260
301
  (FileNotFoundError):
@@ -271,6 +312,7 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> list[SectionC
271
312
  else:
272
313
  return DEFAULT_CONFIG
273
314
 
315
+ # Convert to Path object and check existence
274
316
  config_path = Path(config_path)
275
317
  if not config_path.exists():
276
318
  raise FileNotFoundError(f"Configuration file not found: {config_path}")
@@ -292,6 +334,13 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> list[SectionC
292
334
  if tool_config is None:
293
335
  return DEFAULT_CONFIG
294
336
 
337
+ # Parse global configuration flags
338
+ global_config = GlobalConfig(
339
+ allow_undefined_sections=tool_config.get("allow_undefined_sections", False),
340
+ require_docstrings=tool_config.get("require_docstrings", True),
341
+ check_private=tool_config.get("check_private", False),
342
+ )
343
+
295
344
  # Parse sections configuration
296
345
  sections_config: list[SectionConfig] = []
297
346
  if "sections" in tool_config:
@@ -315,16 +364,17 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> list[SectionC
315
364
  except (KeyError, TypeError, ValueError, InvalidTypeValuesError) as e:
316
365
  raise InvalidConfigError(f"Invalid section configuration: {section_data}. Error: {e}") from e
317
366
 
367
+ # Use default sections if none provided, otherwise validate and sort
318
368
  if not sections_config:
319
- return DEFAULT_CONFIG
320
-
321
- # Validate no duplicate order values
322
- _validate_config_order(config_sections=sections_config)
369
+ sections_config = DEFAULT_SECTIONS
370
+ else:
371
+ # Validate no duplicate order values
372
+ _validate_config_order(config_sections=sections_config)
323
373
 
324
- # Sort by order
325
- sections_config.sort(key=lambda x: x.order)
374
+ # Sort by order
375
+ sections_config.sort(key=lambda x: x.order)
326
376
 
327
- return sections_config
377
+ return Config(global_config=global_config, sections=sections_config)
328
378
 
329
379
 
330
380
  def find_config_file(start_path: Optional[Path] = None) -> Optional[Path]:
@@ -50,7 +50,7 @@ from pathlib import Path
50
50
  from typing import Iterator, Literal, NamedTuple, Optional, Union
51
51
 
52
52
  # ## Local First Party Imports ----
53
- from docstring_format_checker.config import SectionConfig
53
+ from docstring_format_checker.config import Config, SectionConfig
54
54
  from docstring_format_checker.utils.exceptions import (
55
55
  DirectoryNotFoundError,
56
56
  DocstringError,
@@ -95,18 +95,19 @@ class DocstringChecker:
95
95
  Main class for checking docstring format and completeness.
96
96
  """
97
97
 
98
- def __init__(self, sections_config: list[SectionConfig]) -> None:
98
+ def __init__(self, config: Config) -> None:
99
99
  """
100
100
  !!! note "Summary"
101
101
  Initialize the docstring checker.
102
102
 
103
103
  Params:
104
- sections_config (list[SectionConfig]):
105
- List of section configurations to check against.
104
+ config (Config):
105
+ Configuration object containing global settings and section definitions.
106
106
  """
107
- self.sections_config: list[SectionConfig] = sections_config
108
- self.required_sections: list[SectionConfig] = [s for s in sections_config if s.required]
109
- self.optional_sections: list[SectionConfig] = [s for s in sections_config if not s.required]
107
+ self.config = config
108
+ self.sections_config: list[SectionConfig] = config.sections
109
+ self.required_sections: list[SectionConfig] = [s for s in config.sections if s.required]
110
+ self.optional_sections: list[SectionConfig] = [s for s in config.sections if not s.required]
110
111
 
111
112
  def check_file(self, file_path: Union[str, Path]) -> list[DocstringError]:
112
113
  """
@@ -279,7 +280,9 @@ class DocstringChecker:
279
280
  self.checker: DocstringChecker = checker
280
281
 
281
282
  def visit_ClassDef(self, node: ast.ClassDef) -> None:
282
- if not node.name.startswith("_"): # Skip private classes
283
+ # Skip private classes unless check_private is enabled
284
+ should_check: bool = self.checker.config.global_config.check_private or not node.name.startswith("_")
285
+ if should_check:
283
286
  items.append(
284
287
  FunctionAndClassDetails(
285
288
  item_type="class",
@@ -304,9 +307,10 @@ class DocstringChecker:
304
307
  def _visit_function(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> None:
305
308
  """Visit function definition node (sync or async)."""
306
309
 
307
- if not node.name.startswith("_"): # Skip private functions
310
+ # Skip private functions unless check_private is enabled
311
+ should_check: bool = self.checker.config.global_config.check_private or not node.name.startswith("_")
312
+ if should_check:
308
313
  # Skip @overload functions - they don't need docstrings
309
-
310
314
  if not self.checker._is_overload_function(node):
311
315
  item_type: Literal["function", "method"] = "method" if self.class_stack else "function"
312
316
  parent_class: Optional[str] = self.class_stack[-1] if self.class_stack else None
@@ -378,7 +382,8 @@ class DocstringChecker:
378
382
  applicable_sections.append(section)
379
383
 
380
384
  if not docstring:
381
- if requires_docstring:
385
+ # Only require docstrings if the global flag is enabled
386
+ if requires_docstring and self.config.global_config.require_docstrings:
382
387
  message: str = f"Missing docstring for {item.item_type}"
383
388
  raise DocstringError(
384
389
  message=message,
@@ -387,7 +392,7 @@ class DocstringChecker:
387
392
  item_name=item.name,
388
393
  item_type=item.item_type,
389
394
  )
390
- return # No docstring required
395
+ return # No docstring required or docstring requirement disabled
391
396
 
392
397
  # Validate docstring sections if docstring exists
393
398
  self._validate_docstring_sections(docstring, item, file_path)
@@ -452,9 +457,10 @@ class DocstringChecker:
452
457
  if self._has_both_returns_and_yields(docstring):
453
458
  errors.append("Docstring cannot have both Returns and Yields sections")
454
459
 
455
- # Check for undefined sections in docstring
456
- undefined_errors: list[str] = self._check_undefined_sections(docstring)
457
- errors.extend(undefined_errors)
460
+ # Check for undefined sections in docstring (only if not allowed)
461
+ if not self.config.global_config.allow_undefined_sections:
462
+ undefined_errors: list[str] = self._check_undefined_sections(docstring)
463
+ errors.extend(undefined_errors)
458
464
 
459
465
  # Check admonition values match configuration
460
466
  admonition_errors: list[str] = self._check_admonition_values(docstring)
@@ -1015,19 +1021,42 @@ class DocstringChecker:
1015
1021
  )
1016
1022
  # For list_name_and_type sections, check format like "name (type):" or "(type):"
1017
1023
  elif current_section.type == "list_name_and_type":
1018
- # Pattern: name (type): or (type):
1019
- # But skip if it doesn't look like a parameter definition (e.g., has multiple words before the colon)
1020
- colon_part = stripped_line.split(":")[0].strip()
1021
- # Skip if it contains phrases that indicate it's a description, not a parameter
1022
- if any(
1023
- word in colon_part.lower() for word in ["default", "output", "format", "show", "example"]
1024
- ):
1024
+ # Check if this line has parentheses and looks like a parameter definition
1025
+ if re.search(r"\([^)]+\):", stripped_line):
1026
+ # This is a valid parameter definition line, remember its indentation
1027
+ type_line_indent = current_indent
1025
1028
  continue
1029
+ else:
1030
+ # Check if this is likely a description line based on various criteria
1031
+ colon_part = stripped_line.split(":")[0].strip()
1032
+
1033
+ # Skip if it contains phrases that indicate it's a description, not a parameter
1034
+ if any(
1035
+ word in colon_part.lower()
1036
+ for word in ["default", "output", "format", "show", "example"]
1037
+ ):
1038
+ continue
1026
1039
 
1027
- if not re.search(r"\([^)]+\):", stripped_line):
1028
- errors.append(
1029
- f"Section '{current_section.name}' (type: '{current_section.type}') requires "
1030
- f"parenthesized types, see: '{stripped_line}'"
1031
- )
1040
+ # Skip if it starts with bullet points or list markers
1041
+ if stripped_line.strip().startswith(("-", "*", "•", "+")):
1042
+ continue
1043
+
1044
+ # If we have found a parameter definition, check if this is a description line
1045
+ if type_line_indent is not None:
1046
+ # Skip if this is more indented than the parameter definition (description line)
1047
+ if current_indent > type_line_indent:
1048
+ continue
1049
+
1050
+ # Skip if the line before the colon contains multiple words (likely description)
1051
+ words_before_colon = colon_part.split()
1052
+ if len(words_before_colon) > 2: # More than "param_name (type)"
1053
+ continue
1054
+
1055
+ # Only flag lines that could reasonably be parameter definitions
1056
+ if ":" in stripped_line and not stripped_line.strip().startswith("#"):
1057
+ errors.append(
1058
+ f"Section '{current_section.name}' (type: '{current_section.type}') requires "
1059
+ f"parenthesized types, see: '{stripped_line}'"
1060
+ )
1032
1061
 
1033
1062
  return errors