Main parser for CHANGES.txt file.
| 442 | |
| 443 | |
| 444 | class ChangesParser: |
| 445 | """Main parser for CHANGES.txt file.""" |
| 446 | |
| 447 | # Pattern to match version headers: ================== 10.0.0 ================== |
| 448 | # Also supports pre-release versions: 4.0.0-ALPHA, 4.0.0-BETA, 4.0.0-RC1, etc. |
| 449 | VERSION_HEADER_PATTERN = re.compile(r'=+\s+([\d.]+(?:-[A-Za-z0-9]+)?)\s+=+') |
| 450 | |
| 451 | # Pattern to match section headers: "Section Name" followed by dashes |
| 452 | # Matches patterns like "New Features\n---------------------" |
| 453 | SECTION_HEADER_PATTERN = re.compile(r'^([A-Za-z][A-Za-z0-9\s/&-]*?)\n\s*-+\s*$', re.MULTILINE) |
| 454 | |
| 455 | def __init__(self, changes_file_path: str): |
| 456 | self.changes_file_path = changes_file_path |
| 457 | self.versions: List[VersionSection] = [] |
| 458 | |
| 459 | def parse(self): |
| 460 | """Parse the CHANGES.txt file.""" |
| 461 | with open(self.changes_file_path, 'r', encoding='utf-8') as f: |
| 462 | content = f.read() |
| 463 | |
| 464 | # Split into version sections |
| 465 | version_matches = list(self.VERSION_HEADER_PATTERN.finditer(content)) |
| 466 | |
| 467 | for i, version_match in enumerate(version_matches): |
| 468 | version = version_match.group(1) |
| 469 | start_pos = version_match.end() |
| 470 | |
| 471 | # Find the end of this version section (start of next version or EOF) |
| 472 | if i + 1 < len(version_matches): |
| 473 | end_pos = version_matches[i + 1].start() |
| 474 | else: |
| 475 | end_pos = len(content) |
| 476 | |
| 477 | version_content = content[start_pos:end_pos] |
| 478 | version_section = self._parse_version_section(version, version_content) |
| 479 | self.versions.append(version_section) |
| 480 | |
| 481 | def _parse_version_section(self, version: str, content: str) -> VersionSection: |
| 482 | """Parse all entries within a single version section.""" |
| 483 | version_section = VersionSection(version) |
| 484 | |
| 485 | # Split into subsections (New Features, Bug Fixes, etc.) |
| 486 | section_matches = list(self.SECTION_HEADER_PATTERN.finditer(content)) |
| 487 | |
| 488 | for i, section_match in enumerate(section_matches): |
| 489 | section_name = section_match.group(1) |
| 490 | |
| 491 | # Skip sections that should not be migrated |
| 492 | if section_name in ChangeType.SKIP_SECTIONS: |
| 493 | continue |
| 494 | |
| 495 | section_type = ChangeType.get_type(section_name) |
| 496 | |
| 497 | start_pos = section_match.end() |
| 498 | |
| 499 | # Find the end of this section (start of next section or EOF) |
| 500 | if i + 1 < len(section_matches): |
| 501 | end_pos = section_matches[i + 1].start() |