Coverage for lintro / parsers / pydoclint / pydoclint_parser.py: 92%
36 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Parser for pydoclint output.
3This module provides parsing functionality for pydoclint's text output format.
4Pydoclint is a Python docstring linter that validates docstrings match
5function signatures.
6"""
8from __future__ import annotations
10import re
12from loguru import logger
14from lintro.parsers.base_parser import strip_ansi_codes
15from lintro.parsers.pydoclint.pydoclint_issue import PydoclintIssue
17# Pydoclint output format:
18# /path/to/file.py
19# 10: DOC101: Function `foo` has 1 argument(s) ...
20# File path is on its own line, issues start with whitespace then line number
21PYDOCLINT_FILE_PATTERN = re.compile(r"^(?P<file>\S.*\.pyi?)$")
22PYDOCLINT_ISSUE_PATTERN = re.compile(
23 r"^\s+(?P<line>\d+):\s*(?P<code>DOC\d+):\s*(?P<message>.+)$",
24)
27def _safe_int(value: str, default: int = 0) -> int:
28 """Safely convert a value to int with fallback.
30 Args:
31 value: Value to convert.
32 default: Default value if conversion fails.
34 Returns:
35 Integer value or default.
36 """
37 try:
38 return int(value)
39 except (TypeError, ValueError):
40 return default
43def parse_pydoclint_output(output: str | None) -> list[PydoclintIssue]:
44 """Parse pydoclint text output into a list of PydoclintIssue objects.
46 Pydoclint outputs text in the following format:
47 /path/to/file.py
48 10: DOC101: Function `foo` has 1 argument(s) ...
50 The file path is on its own line, followed by indented issue lines.
52 Args:
53 output: The raw text output from pydoclint, or None.
55 Returns:
56 List of PydoclintIssue objects.
57 """
58 issues: list[PydoclintIssue] = []
60 # Handle None or empty output
61 if output is None or not output.strip():
62 return issues
64 # Strip ANSI codes for consistent parsing across environments
65 output = strip_ansi_codes(output)
67 current_file: str | None = None
69 for line in output.splitlines():
70 # Skip empty lines
71 if not line.strip():
72 continue
74 # Check if this is a file path line (no leading whitespace)
75 file_match = PYDOCLINT_FILE_PATTERN.match(line)
76 if file_match:
77 current_file = file_match.group("file")
78 logger.debug(f"Parsing issues for file: {current_file}")
79 continue
81 # Check if this is an issue line (has leading whitespace)
82 issue_match = PYDOCLINT_ISSUE_PATTERN.match(line)
83 if issue_match:
84 if current_file:
85 line_num = _safe_int(issue_match.group("line"))
86 code = issue_match.group("code")
87 message = issue_match.group("message")
89 issues.append(
90 PydoclintIssue(
91 file=current_file,
92 line=line_num,
93 column=0, # pydoclint doesn't provide column info
94 code=code,
95 message=message,
96 ),
97 )
98 else:
99 # Issue found but no file context - log for debugging
100 logger.warning(
101 f"Pydoclint issue found without file context: "
102 f"line={issue_match.group('line')}, "
103 f"code={issue_match.group('code')}, "
104 f"message={issue_match.group('message')}",
105 )
106 else:
107 logger.debug(f"Line did not match pydoclint pattern: {line}")
109 return issues