From 290e1b138c0062bd1da250bc5f2969dbb45e69cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Tue, 14 Jan 2025 12:22:32 +0100 Subject: [PATCH] Extend ChangelogEntry class to support openSUSE style detached changelogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add ChangelogStyle enum - add parameter style to ChangelogEntry.assemble to switch between changelog styles - extend Changelog.parse() to process openSUSE changelog entries Co-authored-by: Nikola Forró --- specfile/changelog.py | 76 +++++++++++++++++-- tests/unit/test_changelog.py | 143 ++++++++++++++++++++++++++++++++++- 2 files changed, 211 insertions(+), 8 deletions(-) diff --git a/specfile/changelog.py b/specfile/changelog.py index 342d203..8b37fa9 100644 --- a/specfile/changelog.py +++ b/specfile/changelog.py @@ -9,6 +9,7 @@ import re import shutil import subprocess +from enum import Enum, auto, unique from typing import List, Optional, Union, overload from specfile.exceptions import SpecfileException @@ -34,6 +35,24 @@ "Dec", ) +_OPENSUSE_CHANGELOG_SEPARATOR = 67 * "-" + + +@unique +class ChangelogStyle(Enum): + """Style of changelog entries""" + + #: standard changelog entries parseable by RPM (used in Fedora, RHEL, etc.): + #: * $DATE $AUTHOR <$EMAIL> - $EVR + #: $ENTRY + standard = auto() + + #: openSUSE/SUSE style detached changelog: + #: ------------------------------------------------------------------- + #: $DATE - $AUTHOR <$EMAIL> + #: $ENTRY + openSUSE = auto() + class ChangelogEntry: """ @@ -168,6 +187,13 @@ def day_of_month_padding(self) -> str: return "" return m.group("wsp") + (m.group("zp") or "") + @property + def style(self) -> ChangelogStyle: + """Style of this changelog entry (standard vs openSUSE).""" + if self.header.startswith(_OPENSUSE_CHANGELOG_SEPARATOR): + return ChangelogStyle.openSUSE + return ChangelogStyle.standard + @classmethod def assemble( cls, @@ -177,6 +203,7 @@ def assemble( evr: Optional[str] = None, day_of_month_padding: str = "0", append_newline: bool = True, + style: ChangelogStyle = ChangelogStyle.standard, ) -> "ChangelogEntry": """ Assembles a changelog entry. @@ -184,30 +211,55 @@ def assemble( Args: timestamp: Timestamp of the entry. Supply `datetime` rather than `date` for extended format. + openSUSE-style changelog entries mandate extended format, so if a `date` + is supplied, the timestamp will be set to noon of that day. author: Author of the entry. content: List of lines forming the content of the entry. evr: EVR (epoch, version, release) of the entry. + Ignored if `style` is `ChangelogStyle.openSUSE`. day_of_month_padding: Padding to apply to day of month in the timestamp. append_newline: Whether the entry should be followed by an empty line. + style: Which style of changelog should be created. Returns: New instance of `ChangelogEntry` class. """ weekday = WEEKDAYS[timestamp.weekday()] month = MONTHS[timestamp.month - 1] - header = f"* {weekday} {month}" + + if style == ChangelogStyle.standard: + header = "* " + else: + header = _OPENSUSE_CHANGELOG_SEPARATOR + "\n" + header += f"{weekday} {month}" + if day_of_month_padding.endswith("0"): header += f" {day_of_month_padding[:-1]}{timestamp.day:02}" else: header += f" {day_of_month_padding}{timestamp.day}" + + # convert to extended format for openSUSE style changelogs + if style == ChangelogStyle.openSUSE and not isinstance( + timestamp, datetime.datetime + ): + timestamp = datetime.datetime( + year=timestamp.year, month=timestamp.month, day=timestamp.day, hour=12 + ) + if isinstance(timestamp, datetime.datetime): # extended format if not timestamp.tzinfo: timestamp = timestamp.replace(tzinfo=datetime.timezone.utc) header += f" {timestamp:%H:%M:%S %Z}" - header += f" {timestamp:%Y} {author}" - if evr is not None: + header += f" {timestamp:%Y} " + + if style == ChangelogStyle.openSUSE: + header += "- " + header += author + + if evr is not None and style == ChangelogStyle.standard: header += f" - {evr}" + return cls(header, content, [""] if append_newline else None) @@ -350,13 +402,25 @@ def extract_following_lines(content: List[str]) -> List[str]: predecessor = [] header = None content: List[str] = [] - for line in section: - if line.startswith("*"): + + for i, line in enumerate(section): + if line == _OPENSUSE_CHANGELOG_SEPARATOR: + continue + + prev_line_is_opensuse_separator = ( + i >= 1 and section[i - 1] == _OPENSUSE_CHANGELOG_SEPARATOR + ) + if line.startswith("*") or prev_line_is_opensuse_separator: if header is None or "".join(content).strip(): if header: following_lines = extract_following_lines(content) data.insert(0, ChangelogEntry(header, content, following_lines)) - header = line + + if prev_line_is_opensuse_separator: + header = _OPENSUSE_CHANGELOG_SEPARATOR + "\n" + else: + header = "" + header += line content = [] else: content.append(line) diff --git a/tests/unit/test_changelog.py b/tests/unit/test_changelog.py index ace615e..6691381 100644 --- a/tests/unit/test_changelog.py +++ b/tests/unit/test_changelog.py @@ -3,11 +3,16 @@ import copy import datetime -from typing import Optional +from typing import List, Optional, Union import pytest -from specfile.changelog import Changelog, ChangelogEntry +from specfile.changelog import ( + _OPENSUSE_CHANGELOG_SEPARATOR, + Changelog, + ChangelogEntry, + ChangelogStyle, +) from specfile.sections import Section from specfile.utils import EVR @@ -240,6 +245,140 @@ def test_parse(): ] assert not changelog[6].extended_timestamp + assert all( + changelog_entry.style == ChangelogStyle.standard + for changelog_entry in changelog + ) + + +def test_suse_style_changelog_parse(): + changelog = Changelog.parse( + Section( + "changelog", + data=[ + "-------------------------------------------------------------------", + ( + hdr1 := "Tue Dec 17 14:21:37 UTC 2024 - " + + (dc := "Dan Čermák ") + ), + "", + (content1 := "- First version"), + "", + "-------------------------------------------------------------------", + (hdr2 := f"Mon Nov 4 17:47:23 UTC 2024 - {dc}"), + "", + (content2 := "- # [0.9.37] - September 4th, 2024"), + "", + "-------------------------------------------------------------------", + ( + hdr3 := "Fri May 17 09:14:20 UTC 2024 - " + + "Dominique Leuenberger " + ), + "", + (content3 := "- Use %patch -P N instead of deprecated %patchN syntax."), + "", + "-------------------------------------------------------------------", + ( + hdr4 := "Mon Oct 10 13:27:24 UTC 2022 - Stephan Kulow " + ), + "", + (content4_1 := "updated to version 0.9.28"), + (content4_2 := " see installed CHANGELOG.md"), + "", + "", + "-------------------------------------------------------------------", + ( + hdr5 := "Fri Jun 25 07:31:34 UTC 2021 - Dan Čermák " + ), + "", + (content5_1 := "- New upstream release 0.9.26"), + "", + (content5_2 := " - Add support for Ruby 3.0 and fix tests"), + ( + content5_3 := " - Fix support for `frozen_string_literal: false`" + + " magic comments (#1363)" + ), + "", + "", + ], + ) + ) + + assert isinstance(changelog, Changelog) + assert len(changelog) == 5 + + for changelog_entry, hdr, content in zip( + changelog, + reversed((hdr1, hdr2, hdr3, hdr4, hdr5)), + reversed( + ( + [content1], + [content2], + [content3], + [content4_1, content4_2], + [content5_1, "", content5_2, content5_3], + ) + ), + ): + + assert isinstance(changelog_entry, ChangelogEntry) + assert changelog_entry.evr is None + assert changelog_entry.header == _OPENSUSE_CHANGELOG_SEPARATOR + "\n" + hdr + assert changelog_entry.content == [""] + content + assert changelog_entry.extended_timestamp + assert changelog_entry.style == ChangelogStyle.openSUSE + + +@pytest.mark.parametrize( + "timestamp,author,content,entry", + ( + [ + ( + datetime.datetime(2021, 6, 25, 7, 31, 34), + "Dan Čermák ", + content_1 := ["", "New upstream release 0.9.26"], + ChangelogEntry( + header=_OPENSUSE_CHANGELOG_SEPARATOR + + "\n" + + "Fri Jun 25 07:31:34 UTC 2021 - Dan Čermák ", + content=content_1, + ), + ), + ( + datetime.date(2021, 6, 25), + "Dan Čermák ", + content_2 := [ + "", + "New upstream release 0.26", + "Fixed a major regression in Foo", + ], + ChangelogEntry( + header=_OPENSUSE_CHANGELOG_SEPARATOR + + "\n" + + "Fri Jun 25 12:00:00 UTC 2021 - Dan Čermák ", + content=content_2, + ), + ), + ] + ), +) +def test_create_opensuse_changelog_assemble( + timestamp: Union[datetime.datetime, datetime.date], + author: str, + content: List[str], + entry: ChangelogEntry, +) -> None: + assert ( + ChangelogEntry.assemble( + timestamp, + author, + content, + style=ChangelogStyle.openSUSE, + append_newline=False, + ) + == entry + ) + def test_get_raw_section_data(): tzinfo = datetime.timezone(datetime.timedelta(hours=2), name="CEST")