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

1"""Parser for pydoclint output. 

2 

3This module provides parsing functionality for pydoclint's text output format. 

4Pydoclint is a Python docstring linter that validates docstrings match 

5function signatures. 

6""" 

7 

8from __future__ import annotations 

9 

10import re 

11 

12from loguru import logger 

13 

14from lintro.parsers.base_parser import strip_ansi_codes 

15from lintro.parsers.pydoclint.pydoclint_issue import PydoclintIssue 

16 

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) 

25 

26 

27def _safe_int(value: str, default: int = 0) -> int: 

28 """Safely convert a value to int with fallback. 

29 

30 Args: 

31 value: Value to convert. 

32 default: Default value if conversion fails. 

33 

34 Returns: 

35 Integer value or default. 

36 """ 

37 try: 

38 return int(value) 

39 except (TypeError, ValueError): 

40 return default 

41 

42 

43def parse_pydoclint_output(output: str | None) -> list[PydoclintIssue]: 

44 """Parse pydoclint text output into a list of PydoclintIssue objects. 

45 

46 Pydoclint outputs text in the following format: 

47 /path/to/file.py 

48 10: DOC101: Function `foo` has 1 argument(s) ... 

49 

50 The file path is on its own line, followed by indented issue lines. 

51 

52 Args: 

53 output: The raw text output from pydoclint, or None. 

54 

55 Returns: 

56 List of PydoclintIssue objects. 

57 """ 

58 issues: list[PydoclintIssue] = [] 

59 

60 # Handle None or empty output 

61 if output is None or not output.strip(): 

62 return issues 

63 

64 # Strip ANSI codes for consistent parsing across environments 

65 output = strip_ansi_codes(output) 

66 

67 current_file: str | None = None 

68 

69 for line in output.splitlines(): 

70 # Skip empty lines 

71 if not line.strip(): 

72 continue 

73 

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 

80 

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") 

88 

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}") 

108 

109 return issues