Skip to content

Commit

Permalink
Extend ChangelogEntry class to support openSUSE style detached change…
Browse files Browse the repository at this point in the history
…logs

- 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ó <[email protected]>
  • Loading branch information
dcermak and nforro committed Jan 15, 2025
1 parent 2c4d6df commit 290e1b1
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 8 deletions.
76 changes: 70 additions & 6 deletions specfile/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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,
Expand All @@ -177,37 +203,63 @@ 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.
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)


Expand Down Expand Up @@ -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)
Expand Down
143 changes: 141 additions & 2 deletions tests/unit/test_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <[email protected]>")
),
"",
(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 <[email protected]>"
),
"",
(content3 := "- Use %patch -P N instead of deprecated %patchN syntax."),
"",
"-------------------------------------------------------------------",
(
hdr4 := "Mon Oct 10 13:27:24 UTC 2022 - Stephan Kulow <[email protected]>"
),
"",
(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 <[email protected]>"
),
"",
(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 <[email protected]>",
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 <[email protected]>",
content=content_1,
),
),
(
datetime.date(2021, 6, 25),
"Dan Čermák <[email protected]>",
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 <[email protected]>",
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")
Expand Down

0 comments on commit 290e1b1

Please sign in to comment.