thailint 0.2.0__py3-none-any.whl → 0.5.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.
Files changed (52) hide show
  1. src/cli.py +646 -36
  2. src/config.py +6 -2
  3. src/core/base.py +90 -5
  4. src/core/config_parser.py +31 -4
  5. src/linters/dry/block_filter.py +5 -2
  6. src/linters/dry/cache.py +46 -92
  7. src/linters/dry/config.py +17 -13
  8. src/linters/dry/duplicate_storage.py +17 -80
  9. src/linters/dry/file_analyzer.py +11 -48
  10. src/linters/dry/linter.py +5 -12
  11. src/linters/dry/python_analyzer.py +188 -37
  12. src/linters/dry/storage_initializer.py +9 -18
  13. src/linters/dry/token_hasher.py +63 -9
  14. src/linters/dry/typescript_analyzer.py +7 -5
  15. src/linters/dry/violation_filter.py +4 -1
  16. src/linters/file_header/__init__.py +24 -0
  17. src/linters/file_header/atemporal_detector.py +87 -0
  18. src/linters/file_header/config.py +66 -0
  19. src/linters/file_header/field_validator.py +69 -0
  20. src/linters/file_header/linter.py +313 -0
  21. src/linters/file_header/python_parser.py +86 -0
  22. src/linters/file_header/violation_builder.py +78 -0
  23. src/linters/file_placement/linter.py +15 -4
  24. src/linters/magic_numbers/__init__.py +48 -0
  25. src/linters/magic_numbers/config.py +82 -0
  26. src/linters/magic_numbers/context_analyzer.py +247 -0
  27. src/linters/magic_numbers/linter.py +516 -0
  28. src/linters/magic_numbers/python_analyzer.py +76 -0
  29. src/linters/magic_numbers/typescript_analyzer.py +218 -0
  30. src/linters/magic_numbers/violation_builder.py +98 -0
  31. src/linters/nesting/__init__.py +6 -2
  32. src/linters/nesting/config.py +6 -3
  33. src/linters/nesting/linter.py +8 -19
  34. src/linters/nesting/typescript_analyzer.py +1 -0
  35. src/linters/print_statements/__init__.py +53 -0
  36. src/linters/print_statements/config.py +83 -0
  37. src/linters/print_statements/linter.py +430 -0
  38. src/linters/print_statements/python_analyzer.py +155 -0
  39. src/linters/print_statements/typescript_analyzer.py +135 -0
  40. src/linters/print_statements/violation_builder.py +98 -0
  41. src/linters/srp/__init__.py +3 -3
  42. src/linters/srp/config.py +12 -6
  43. src/linters/srp/linter.py +33 -24
  44. src/orchestrator/core.py +12 -2
  45. src/templates/thailint_config_template.yaml +158 -0
  46. src/utils/project_root.py +135 -16
  47. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/METADATA +387 -81
  48. thailint-0.5.0.dist-info/RECORD +96 -0
  49. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
  50. thailint-0.2.0.dist-info/RECORD +0 -75
  51. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
  52. {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,87 @@
1
+ """
2
+ File: src/linters/file_header/atemporal_detector.py
3
+ Purpose: Detects temporal language patterns in file headers
4
+ Exports: AtemporalDetector class
5
+ Depends: re module for regex matching
6
+ Implements: Regex-based pattern matching with configurable patterns
7
+ Related: linter.py for detector usage, violation_builder.py for violation creation
8
+
9
+ Overview:
10
+ Implements pattern-based detection of temporal language that violates atemporal
11
+ documentation requirements. Detects dates, temporal qualifiers, state change language,
12
+ and future references using regex patterns. Provides violation details for each pattern match.
13
+
14
+ Usage:
15
+ detector = AtemporalDetector()
16
+ violations = detector.detect_violations(header_text)
17
+
18
+ Notes: Four pattern categories - dates, temporal qualifiers, state changes, future references
19
+ """
20
+
21
+ import re
22
+
23
+
24
+ class AtemporalDetector:
25
+ """Detects temporal language patterns in text."""
26
+
27
+ # Date patterns
28
+ DATE_PATTERNS = [
29
+ (r"\d{4}-\d{2}-\d{2}", "ISO date format (YYYY-MM-DD)"),
30
+ (
31
+ r"(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}",
32
+ "Month Year format",
33
+ ),
34
+ (r"(?:Created|Updated|Modified):\s*\d{4}", "Date metadata"),
35
+ ]
36
+
37
+ # Temporal qualifiers
38
+ TEMPORAL_QUALIFIERS = [
39
+ (r"\bcurrently\b", 'temporal qualifier "currently"'),
40
+ (r"\bnow\b", 'temporal qualifier "now"'),
41
+ (r"\brecently\b", 'temporal qualifier "recently"'),
42
+ (r"\bsoon\b", 'temporal qualifier "soon"'),
43
+ (r"\bfor now\b", 'temporal qualifier "for now"'),
44
+ ]
45
+
46
+ # State change language
47
+ STATE_CHANGE = [
48
+ (r"\breplaces?\b", 'state change "replaces"'),
49
+ (r"\bmigrated from\b", 'state change "migrated from"'),
50
+ (r"\bformerly\b", 'state change "formerly"'),
51
+ (r"\bold implementation\b", 'state change "old"'),
52
+ (r"\bnew implementation\b", 'state change "new"'),
53
+ ]
54
+
55
+ # Future references
56
+ FUTURE_REFS = [
57
+ (r"\bwill be\b", 'future reference "will be"'),
58
+ (r"\bplanned\b", 'future reference "planned"'),
59
+ (r"\bto be added\b", 'future reference "to be added"'),
60
+ (r"\bcoming soon\b", 'future reference "coming soon"'),
61
+ ]
62
+
63
+ def detect_violations( # thailint: ignore[nesting]
64
+ self, text: str
65
+ ) -> list[tuple[str, str, int]]:
66
+ """Detect all temporal language violations in text.
67
+
68
+ Args:
69
+ text: Text to check for temporal language
70
+
71
+ Returns:
72
+ List of (pattern, description, line_number) tuples for each violation
73
+ """
74
+ violations = []
75
+
76
+ # Check all pattern categories
77
+ all_patterns = (
78
+ self.DATE_PATTERNS + self.TEMPORAL_QUALIFIERS + self.STATE_CHANGE + self.FUTURE_REFS
79
+ )
80
+
81
+ lines = text.split("\n")
82
+ for line_num, line in enumerate(lines, start=1):
83
+ for pattern, description in all_patterns:
84
+ if re.search(pattern, line, re.IGNORECASE):
85
+ violations.append((pattern, description, line_num))
86
+
87
+ return violations
@@ -0,0 +1,66 @@
1
+ """
2
+ File: src/linters/file_header/config.py
3
+ Purpose: Configuration model for file header linter
4
+ Exports: FileHeaderConfig dataclass
5
+ Depends: dataclasses, pathlib
6
+ Implements: Configuration with validation and defaults
7
+ Related: linter.py for configuration usage
8
+
9
+ Overview:
10
+ Defines configuration structure for file header linter including required fields
11
+ per language, ignore patterns, and validation options. Provides defaults matching
12
+ ai-doc-standard.md requirements and supports loading from .thailint.yaml configuration.
13
+
14
+ Usage:
15
+ config = FileHeaderConfig()
16
+ config = FileHeaderConfig.from_dict(config_dict, "python")
17
+
18
+ Notes: Dataclass with validation and language-specific defaults
19
+ """
20
+
21
+ from dataclasses import dataclass, field
22
+
23
+
24
+ @dataclass
25
+ class FileHeaderConfig:
26
+ """Configuration for file header linting."""
27
+
28
+ # Required fields by language
29
+ required_fields_python: list[str] = field(
30
+ default_factory=lambda: [
31
+ "Purpose",
32
+ "Scope",
33
+ "Overview",
34
+ "Dependencies",
35
+ "Exports",
36
+ "Interfaces",
37
+ "Implementation",
38
+ ]
39
+ )
40
+
41
+ # Enforce atemporal language checking
42
+ enforce_atemporal: bool = True
43
+
44
+ # Patterns to ignore (file paths)
45
+ ignore: list[str] = field(
46
+ default_factory=lambda: ["test/**", "**/migrations/**", "**/__init__.py"]
47
+ )
48
+
49
+ @classmethod
50
+ def from_dict(cls, config_dict: dict, language: str) -> "FileHeaderConfig":
51
+ """Create config from dictionary.
52
+
53
+ Args:
54
+ config_dict: Dictionary of configuration values
55
+ language: Programming language for language-specific config
56
+
57
+ Returns:
58
+ FileHeaderConfig instance with values from dictionary
59
+ """
60
+ return cls(
61
+ required_fields_python=config_dict.get("required_fields", {}).get(
62
+ "python", cls().required_fields_python
63
+ ),
64
+ enforce_atemporal=config_dict.get("enforce_atemporal", True),
65
+ ignore=config_dict.get("ignore", cls().ignore),
66
+ )
@@ -0,0 +1,69 @@
1
+ """
2
+ File: src/linters/file_header/field_validator.py
3
+ Purpose: Validates mandatory fields in file headers
4
+ Exports: FieldValidator class
5
+ Depends: FileHeaderConfig for field requirements
6
+ Implements: Configuration-driven validation with field presence checking
7
+ Related: linter.py for validator usage, config.py for configuration
8
+
9
+ Overview:
10
+ Validates presence and quality of mandatory header fields. Checks that all
11
+ required fields are present, non-empty, and meet minimum content requirements.
12
+ Supports language-specific required fields and provides detailed violation messages.
13
+
14
+ Usage:
15
+ validator = FieldValidator(config)
16
+ violations = validator.validate_fields(fields, "python")
17
+
18
+ Notes: Language-specific field requirements defined in config
19
+ """
20
+
21
+ from .config import FileHeaderConfig
22
+
23
+
24
+ class FieldValidator:
25
+ """Validates mandatory fields in headers."""
26
+
27
+ def __init__(self, config: FileHeaderConfig):
28
+ """Initialize validator with configuration.
29
+
30
+ Args:
31
+ config: File header configuration with required fields
32
+ """
33
+ self.config = config
34
+
35
+ def validate_fields( # thailint: ignore[nesting]
36
+ self, fields: dict[str, str], language: str
37
+ ) -> list[tuple[str, str]]:
38
+ """Validate all required fields are present.
39
+
40
+ Args:
41
+ fields: Dictionary of parsed header fields
42
+ language: File language (python, typescript, etc.)
43
+
44
+ Returns:
45
+ List of (field_name, error_message) tuples for missing/invalid fields
46
+ """
47
+ violations = []
48
+ required_fields = self._get_required_fields(language)
49
+
50
+ for field_name in required_fields:
51
+ if field_name not in fields:
52
+ violations.append((field_name, f"Missing mandatory field: {field_name}"))
53
+ elif not fields[field_name] or len(fields[field_name].strip()) == 0:
54
+ violations.append((field_name, f"Empty mandatory field: {field_name}"))
55
+
56
+ return violations
57
+
58
+ def _get_required_fields(self, language: str) -> list[str]:
59
+ """Get required fields for language.
60
+
61
+ Args:
62
+ language: Programming language
63
+
64
+ Returns:
65
+ List of required field names for the language
66
+ """
67
+ if language == "python":
68
+ return self.config.required_fields_python
69
+ return [] # Other languages in PR5
@@ -0,0 +1,313 @@
1
+ """
2
+ File: src/linters/file_header/linter.py
3
+ Purpose: Main file header linter rule implementation
4
+ Exports: FileHeaderRule class
5
+ Depends: BaseLintRule, PythonHeaderParser, FieldValidator, AtemporalDetector, ViolationBuilder
6
+ Implements: Composition pattern with helper classes, AST-based Python parsing
7
+ Related: config.py for configuration, python_parser.py for extraction
8
+
9
+ Overview:
10
+ Orchestrates file header validation for Python files using focused helper classes.
11
+ Coordinates docstring extraction, field validation, atemporal language detection, and
12
+ violation building. Supports configuration from .thailint.yaml and ignore directives.
13
+ Validates headers against mandatory field requirements and atemporal language standards.
14
+
15
+ Usage:
16
+ rule = FileHeaderRule()
17
+ violations = rule.check(context)
18
+
19
+ Notes: Follows composition pattern from magic_numbers linter for maintainability
20
+ """
21
+
22
+ from pathlib import Path
23
+
24
+ from src.core.base import BaseLintContext, BaseLintRule
25
+ from src.core.linter_utils import load_linter_config
26
+ from src.core.types import Violation
27
+ from src.linter_config.ignore import IgnoreDirectiveParser
28
+
29
+ from .atemporal_detector import AtemporalDetector
30
+ from .config import FileHeaderConfig
31
+ from .field_validator import FieldValidator
32
+ from .python_parser import PythonHeaderParser
33
+ from .violation_builder import ViolationBuilder
34
+
35
+
36
+ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
37
+ """Validates file headers for mandatory fields and atemporal language.
38
+
39
+ Method count (17) exceeds SRP guideline (8) because proper A-grade complexity
40
+ refactoring requires extracting helper methods. Class maintains single responsibility
41
+ of file header validation - all methods support this core purpose through composition
42
+ pattern with focused helper classes (parser, validator, detector, builder).
43
+ """
44
+
45
+ def __init__(self) -> None:
46
+ """Initialize the file header rule."""
47
+ self._violation_builder = ViolationBuilder(self.rule_id)
48
+ self._ignore_parser = IgnoreDirectiveParser()
49
+
50
+ @property
51
+ def rule_id(self) -> str:
52
+ """Unique identifier for this rule.
53
+
54
+ Returns:
55
+ Rule identifier string
56
+ """
57
+ return "file-header.validation"
58
+
59
+ @property
60
+ def rule_name(self) -> str:
61
+ """Human-readable name for this rule.
62
+
63
+ Returns:
64
+ Rule name string
65
+ """
66
+ return "File Header Validation"
67
+
68
+ @property
69
+ def description(self) -> str:
70
+ """Description of what this rule checks.
71
+
72
+ Returns:
73
+ Rule description string
74
+ """
75
+ return "Validates file headers for mandatory fields and atemporal language"
76
+
77
+ def check(self, context: BaseLintContext) -> list[Violation]:
78
+ """Check file header for violations.
79
+
80
+ Args:
81
+ context: Lint context with file information
82
+
83
+ Returns:
84
+ List of violations found in file header
85
+ """
86
+ # Only Python for now (PR3), multi-language in PR5
87
+ if context.language != "python":
88
+ return []
89
+
90
+ # Check for file-level ignore directives first
91
+ if self._has_file_ignore(context):
92
+ return []
93
+
94
+ # Load configuration
95
+ config = self._load_config(context)
96
+
97
+ # Check if file should be ignored by pattern
98
+ if self._should_ignore_file(context, config):
99
+ return []
100
+
101
+ # Extract and validate header
102
+ return self._check_python_header(context, config)
103
+
104
+ def _has_file_ignore(self, context: BaseLintContext) -> bool:
105
+ """Check if file has file-level ignore directive.
106
+
107
+ Args:
108
+ context: Lint context
109
+
110
+ Returns:
111
+ True if file has ignore-file directive
112
+ """
113
+ file_content = context.file_content or ""
114
+
115
+ if self._has_standard_ignore(file_content):
116
+ return True
117
+
118
+ return self._has_custom_ignore_syntax(file_content)
119
+
120
+ def _has_standard_ignore(self, file_content: str) -> bool: # thailint: ignore[nesting]
121
+ """Check standard ignore parser for file-level ignores."""
122
+ # Check first 10 lines for standard ignore directives
123
+ first_lines = file_content.splitlines()[:10]
124
+ for line in first_lines:
125
+ if self._ignore_parser._has_ignore_directive_marker(line): # pylint: disable=protected-access
126
+ if self._ignore_parser._check_specific_rule_ignore(line, self.rule_id): # pylint: disable=protected-access
127
+ return True
128
+ if self._ignore_parser._check_general_ignore(line): # pylint: disable=protected-access
129
+ return True
130
+ return False
131
+
132
+ def _has_custom_ignore_syntax(self, file_content: str) -> bool:
133
+ """Check custom file-level ignore syntax."""
134
+ first_lines = file_content.splitlines()[:10]
135
+ return any(self._is_ignore_line(line) for line in first_lines)
136
+
137
+ def _is_ignore_line(self, line: str) -> bool:
138
+ """Check if line contains ignore directive."""
139
+ line_lower = line.lower()
140
+ return "# thailint-ignore-file:" in line_lower or "# thailint-ignore" in line_lower
141
+
142
+ def _load_config(self, context: BaseLintContext) -> FileHeaderConfig:
143
+ """Load configuration from context.
144
+
145
+ Args:
146
+ context: Lint context
147
+
148
+ Returns:
149
+ FileHeaderConfig with loaded or default values
150
+ """
151
+ # Try production config first
152
+ if hasattr(context, "metadata") and isinstance(context.metadata, dict):
153
+ if "file_header" in context.metadata:
154
+ return load_linter_config(context, "file_header", FileHeaderConfig) # type: ignore[type-var]
155
+
156
+ # Use defaults
157
+ return FileHeaderConfig()
158
+
159
+ def _should_ignore_file(self, context: BaseLintContext, config: FileHeaderConfig) -> bool:
160
+ """Check if file matches ignore patterns.
161
+
162
+ Args:
163
+ context: Lint context
164
+ config: File header configuration
165
+
166
+ Returns:
167
+ True if file should be ignored
168
+ """
169
+ if not context.file_path:
170
+ return False
171
+
172
+ file_path = Path(context.file_path)
173
+ return any(self._matches_ignore_pattern(file_path, p) for p in config.ignore)
174
+
175
+ def _matches_ignore_pattern(self, file_path: Path, pattern: str) -> bool:
176
+ """Check if file path matches a single ignore pattern."""
177
+ if file_path.match(pattern):
178
+ return True
179
+
180
+ if self._matches_directory_pattern(file_path, pattern):
181
+ return True
182
+
183
+ if self._matches_file_pattern(file_path, pattern):
184
+ return True
185
+
186
+ return pattern in str(file_path)
187
+
188
+ def _matches_directory_pattern(self, file_path: Path, pattern: str) -> bool:
189
+ """Match directory patterns like **/migrations/**."""
190
+ if pattern.startswith("**/") and pattern.endswith("/**"):
191
+ dir_name = pattern[3:-3]
192
+ return dir_name in file_path.parts
193
+ return False
194
+
195
+ def _matches_file_pattern(self, file_path: Path, pattern: str) -> bool:
196
+ """Match file patterns like **/__init__.py."""
197
+ if pattern.startswith("**/"):
198
+ filename_pattern = pattern[3:]
199
+ path_str = str(file_path)
200
+ return file_path.name == filename_pattern or path_str.endswith(filename_pattern)
201
+ return False
202
+
203
+ def _check_python_header(
204
+ self, context: BaseLintContext, config: FileHeaderConfig
205
+ ) -> list[Violation]:
206
+ """Check Python file header.
207
+
208
+ Args:
209
+ context: Lint context
210
+ config: Configuration
211
+
212
+ Returns:
213
+ List of violations filtered through ignore directives
214
+ """
215
+ parser = PythonHeaderParser()
216
+ header = parser.extract_header(context.file_content or "")
217
+
218
+ if not header:
219
+ return self._build_missing_header_violations(context)
220
+
221
+ fields = parser.parse_fields(header)
222
+ violations = self._validate_header_fields(fields, context, config)
223
+ violations.extend(self._check_atemporal_violations(header, context, config))
224
+
225
+ return self._filter_ignored_violations(violations, context)
226
+
227
+ def _build_missing_header_violations(self, context: BaseLintContext) -> list[Violation]:
228
+ """Build violations for missing header."""
229
+ return [
230
+ self._violation_builder.build_missing_field(
231
+ "docstring", str(context.file_path or ""), 1
232
+ )
233
+ ]
234
+
235
+ def _validate_header_fields(
236
+ self, fields: dict[str, str], context: BaseLintContext, config: FileHeaderConfig
237
+ ) -> list[Violation]:
238
+ """Validate mandatory header fields."""
239
+ violations = []
240
+ field_validator = FieldValidator(config)
241
+ field_violations = field_validator.validate_fields(fields, context.language)
242
+
243
+ for field_name, _error_message in field_violations:
244
+ violations.append(
245
+ self._violation_builder.build_missing_field(
246
+ field_name, str(context.file_path or ""), 1
247
+ )
248
+ )
249
+ return violations
250
+
251
+ def _check_atemporal_violations(
252
+ self, header: str, context: BaseLintContext, config: FileHeaderConfig
253
+ ) -> list[Violation]:
254
+ """Check for atemporal language violations."""
255
+ if not config.enforce_atemporal:
256
+ return []
257
+
258
+ violations = []
259
+ atemporal_detector = AtemporalDetector()
260
+ atemporal_violations = atemporal_detector.detect_violations(header)
261
+
262
+ for pattern, description, line_num in atemporal_violations:
263
+ violations.append(
264
+ self._violation_builder.build_atemporal_violation(
265
+ pattern, description, str(context.file_path or ""), line_num
266
+ )
267
+ )
268
+ return violations
269
+
270
+ def _filter_ignored_violations(
271
+ self, violations: list[Violation], context: BaseLintContext
272
+ ) -> list[Violation]:
273
+ """Filter out violations that should be ignored.
274
+
275
+ Args:
276
+ violations: List of violations to filter
277
+ context: Lint context with file content
278
+
279
+ Returns:
280
+ Filtered list of violations
281
+ """
282
+ file_content = context.file_content or ""
283
+ lines = file_content.splitlines()
284
+
285
+ filtered = []
286
+ for v in violations:
287
+ # Check standard ignore directives
288
+ if self._ignore_parser.should_ignore_violation(v, file_content):
289
+ continue
290
+
291
+ # Check custom line-level ignore syntax: # thailint-ignore-line:
292
+ if self._has_line_level_ignore(lines, v):
293
+ continue
294
+
295
+ filtered.append(v)
296
+
297
+ return filtered
298
+
299
+ def _has_line_level_ignore(self, lines: list[str], violation: Violation) -> bool:
300
+ """Check for thailint-ignore-line directive.
301
+
302
+ Args:
303
+ lines: File content split into lines
304
+ violation: Violation to check
305
+
306
+ Returns:
307
+ True if line has ignore directive
308
+ """
309
+ if violation.line <= 0 or violation.line > len(lines):
310
+ return False
311
+
312
+ line_content = lines[violation.line - 1] # Convert to 0-indexed
313
+ return "# thailint-ignore-line:" in line_content.lower()
@@ -0,0 +1,86 @@
1
+ """
2
+ File: src/linters/file_header/python_parser.py
3
+ Purpose: Python docstring extraction and parsing for file headers
4
+ Exports: PythonHeaderParser class
5
+ Depends: Python ast module
6
+ Implements: AST-based docstring extraction with field parsing
7
+ Related: linter.py for parser usage, field_validator.py for field validation
8
+
9
+ Overview:
10
+ Extracts module-level docstrings from Python files using AST parsing.
11
+ Parses structured header fields from docstring content and handles both
12
+ well-formed and malformed headers. Provides field extraction and validation
13
+ support for FileHeaderRule.
14
+
15
+ Usage:
16
+ parser = PythonHeaderParser()
17
+ header = parser.extract_header(code)
18
+ fields = parser.parse_fields(header)
19
+
20
+ Notes: Uses ast.get_docstring() for reliable module-level docstring extraction
21
+ """
22
+
23
+ import ast
24
+
25
+
26
+ class PythonHeaderParser:
27
+ """Extracts and parses Python file headers from docstrings."""
28
+
29
+ def extract_header(self, code: str) -> str | None:
30
+ """Extract module-level docstring from Python code.
31
+
32
+ Args:
33
+ code: Python source code
34
+
35
+ Returns:
36
+ Module docstring or None if not found or parse error
37
+ """
38
+ try:
39
+ tree = ast.parse(code)
40
+ return ast.get_docstring(tree)
41
+ except SyntaxError:
42
+ return None
43
+
44
+ def parse_fields(self, header: str) -> dict[str, str]: # thailint: ignore[nesting]
45
+ """Parse structured fields from header text.
46
+
47
+ Args:
48
+ header: Header docstring text
49
+
50
+ Returns:
51
+ Dictionary mapping field_name -> field_value
52
+ """
53
+ fields: dict[str, str] = {}
54
+ current_field: str | None = None
55
+ current_value: list[str] = []
56
+
57
+ for line in header.split("\n"):
58
+ if self._is_new_field_line(line):
59
+ current_field = self._save_and_start_new_field(
60
+ fields, current_field, current_value, line
61
+ )
62
+ current_value = [line.split(":", 1)[1].strip()]
63
+ elif current_field:
64
+ current_value.append(line.strip())
65
+
66
+ self._save_current_field(fields, current_field, current_value)
67
+ return fields
68
+
69
+ def _is_new_field_line(self, line: str) -> bool:
70
+ """Check if line starts a new field."""
71
+ return ":" in line and not line.startswith(" ")
72
+
73
+ def _save_and_start_new_field(
74
+ self, fields: dict[str, str], current_field: str | None, current_value: list[str], line: str
75
+ ) -> str:
76
+ """Save current field and start new one."""
77
+ if current_field:
78
+ fields[current_field] = "\n".join(current_value).strip()
79
+ return line.split(":", 1)[0].strip()
80
+
81
+ def _save_current_field(
82
+ self, fields: dict[str, str], current_field: str | None, current_value: list[str]
83
+ ) -> None:
84
+ """Save the last field."""
85
+ if current_field:
86
+ fields[current_field] = "\n".join(current_value).strip()