Skip to content

Commit 290e1b1

Browse files
dcermaknforro
andcommitted
Extend ChangelogEntry class to support openSUSE style detached changelogs
- 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]>
1 parent 2c4d6df commit 290e1b1

File tree

2 files changed

+211
-8
lines changed

2 files changed

+211
-8
lines changed

specfile/changelog.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import re
1010
import shutil
1111
import subprocess
12+
from enum import Enum, auto, unique
1213
from typing import List, Optional, Union, overload
1314

1415
from specfile.exceptions import SpecfileException
@@ -34,6 +35,24 @@
3435
"Dec",
3536
)
3637

38+
_OPENSUSE_CHANGELOG_SEPARATOR = 67 * "-"
39+
40+
41+
@unique
42+
class ChangelogStyle(Enum):
43+
"""Style of changelog entries"""
44+
45+
#: standard changelog entries parseable by RPM (used in Fedora, RHEL, etc.):
46+
#: * $DATE $AUTHOR <$EMAIL> - $EVR
47+
#: $ENTRY
48+
standard = auto()
49+
50+
#: openSUSE/SUSE style detached changelog:
51+
#: -------------------------------------------------------------------
52+
#: $DATE - $AUTHOR <$EMAIL>
53+
#: $ENTRY
54+
openSUSE = auto()
55+
3756

3857
class ChangelogEntry:
3958
"""
@@ -168,6 +187,13 @@ def day_of_month_padding(self) -> str:
168187
return ""
169188
return m.group("wsp") + (m.group("zp") or "")
170189

190+
@property
191+
def style(self) -> ChangelogStyle:
192+
"""Style of this changelog entry (standard vs openSUSE)."""
193+
if self.header.startswith(_OPENSUSE_CHANGELOG_SEPARATOR):
194+
return ChangelogStyle.openSUSE
195+
return ChangelogStyle.standard
196+
171197
@classmethod
172198
def assemble(
173199
cls,
@@ -177,37 +203,63 @@ def assemble(
177203
evr: Optional[str] = None,
178204
day_of_month_padding: str = "0",
179205
append_newline: bool = True,
206+
style: ChangelogStyle = ChangelogStyle.standard,
180207
) -> "ChangelogEntry":
181208
"""
182209
Assembles a changelog entry.
183210
184211
Args:
185212
timestamp: Timestamp of the entry.
186213
Supply `datetime` rather than `date` for extended format.
214+
openSUSE-style changelog entries mandate extended format, so if a `date`
215+
is supplied, the timestamp will be set to noon of that day.
187216
author: Author of the entry.
188217
content: List of lines forming the content of the entry.
189218
evr: EVR (epoch, version, release) of the entry.
219+
Ignored if `style` is `ChangelogStyle.openSUSE`.
190220
day_of_month_padding: Padding to apply to day of month in the timestamp.
191221
append_newline: Whether the entry should be followed by an empty line.
222+
style: Which style of changelog should be created.
192223
193224
Returns:
194225
New instance of `ChangelogEntry` class.
195226
"""
196227
weekday = WEEKDAYS[timestamp.weekday()]
197228
month = MONTHS[timestamp.month - 1]
198-
header = f"* {weekday} {month}"
229+
230+
if style == ChangelogStyle.standard:
231+
header = "* "
232+
else:
233+
header = _OPENSUSE_CHANGELOG_SEPARATOR + "\n"
234+
header += f"{weekday} {month}"
235+
199236
if day_of_month_padding.endswith("0"):
200237
header += f" {day_of_month_padding[:-1]}{timestamp.day:02}"
201238
else:
202239
header += f" {day_of_month_padding}{timestamp.day}"
240+
241+
# convert to extended format for openSUSE style changelogs
242+
if style == ChangelogStyle.openSUSE and not isinstance(
243+
timestamp, datetime.datetime
244+
):
245+
timestamp = datetime.datetime(
246+
year=timestamp.year, month=timestamp.month, day=timestamp.day, hour=12
247+
)
248+
203249
if isinstance(timestamp, datetime.datetime):
204250
# extended format
205251
if not timestamp.tzinfo:
206252
timestamp = timestamp.replace(tzinfo=datetime.timezone.utc)
207253
header += f" {timestamp:%H:%M:%S %Z}"
208-
header += f" {timestamp:%Y} {author}"
209-
if evr is not None:
254+
header += f" {timestamp:%Y} "
255+
256+
if style == ChangelogStyle.openSUSE:
257+
header += "- "
258+
header += author
259+
260+
if evr is not None and style == ChangelogStyle.standard:
210261
header += f" - {evr}"
262+
211263
return cls(header, content, [""] if append_newline else None)
212264

213265

@@ -350,13 +402,25 @@ def extract_following_lines(content: List[str]) -> List[str]:
350402
predecessor = []
351403
header = None
352404
content: List[str] = []
353-
for line in section:
354-
if line.startswith("*"):
405+
406+
for i, line in enumerate(section):
407+
if line == _OPENSUSE_CHANGELOG_SEPARATOR:
408+
continue
409+
410+
prev_line_is_opensuse_separator = (
411+
i >= 1 and section[i - 1] == _OPENSUSE_CHANGELOG_SEPARATOR
412+
)
413+
if line.startswith("*") or prev_line_is_opensuse_separator:
355414
if header is None or "".join(content).strip():
356415
if header:
357416
following_lines = extract_following_lines(content)
358417
data.insert(0, ChangelogEntry(header, content, following_lines))
359-
header = line
418+
419+
if prev_line_is_opensuse_separator:
420+
header = _OPENSUSE_CHANGELOG_SEPARATOR + "\n"
421+
else:
422+
header = ""
423+
header += line
360424
content = []
361425
else:
362426
content.append(line)

tests/unit/test_changelog.py

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33

44
import copy
55
import datetime
6-
from typing import Optional
6+
from typing import List, Optional, Union
77

88
import pytest
99

10-
from specfile.changelog import Changelog, ChangelogEntry
10+
from specfile.changelog import (
11+
_OPENSUSE_CHANGELOG_SEPARATOR,
12+
Changelog,
13+
ChangelogEntry,
14+
ChangelogStyle,
15+
)
1116
from specfile.sections import Section
1217
from specfile.utils import EVR
1318

@@ -240,6 +245,140 @@ def test_parse():
240245
]
241246
assert not changelog[6].extended_timestamp
242247

248+
assert all(
249+
changelog_entry.style == ChangelogStyle.standard
250+
for changelog_entry in changelog
251+
)
252+
253+
254+
def test_suse_style_changelog_parse():
255+
changelog = Changelog.parse(
256+
Section(
257+
"changelog",
258+
data=[
259+
"-------------------------------------------------------------------",
260+
(
261+
hdr1 := "Tue Dec 17 14:21:37 UTC 2024 - "
262+
+ (dc := "Dan Čermák <[email protected]>")
263+
),
264+
"",
265+
(content1 := "- First version"),
266+
"",
267+
"-------------------------------------------------------------------",
268+
(hdr2 := f"Mon Nov 4 17:47:23 UTC 2024 - {dc}"),
269+
"",
270+
(content2 := "- # [0.9.37] - September 4th, 2024"),
271+
"",
272+
"-------------------------------------------------------------------",
273+
(
274+
hdr3 := "Fri May 17 09:14:20 UTC 2024 - "
275+
+ "Dominique Leuenberger <[email protected]>"
276+
),
277+
"",
278+
(content3 := "- Use %patch -P N instead of deprecated %patchN syntax."),
279+
"",
280+
"-------------------------------------------------------------------",
281+
(
282+
hdr4 := "Mon Oct 10 13:27:24 UTC 2022 - Stephan Kulow <[email protected]>"
283+
),
284+
"",
285+
(content4_1 := "updated to version 0.9.28"),
286+
(content4_2 := " see installed CHANGELOG.md"),
287+
"",
288+
"",
289+
"-------------------------------------------------------------------",
290+
(
291+
hdr5 := "Fri Jun 25 07:31:34 UTC 2021 - Dan Čermák <[email protected]>"
292+
),
293+
"",
294+
(content5_1 := "- New upstream release 0.9.26"),
295+
"",
296+
(content5_2 := " - Add support for Ruby 3.0 and fix tests"),
297+
(
298+
content5_3 := " - Fix support for `frozen_string_literal: false`"
299+
+ " magic comments (#1363)"
300+
),
301+
"",
302+
"",
303+
],
304+
)
305+
)
306+
307+
assert isinstance(changelog, Changelog)
308+
assert len(changelog) == 5
309+
310+
for changelog_entry, hdr, content in zip(
311+
changelog,
312+
reversed((hdr1, hdr2, hdr3, hdr4, hdr5)),
313+
reversed(
314+
(
315+
[content1],
316+
[content2],
317+
[content3],
318+
[content4_1, content4_2],
319+
[content5_1, "", content5_2, content5_3],
320+
)
321+
),
322+
):
323+
324+
assert isinstance(changelog_entry, ChangelogEntry)
325+
assert changelog_entry.evr is None
326+
assert changelog_entry.header == _OPENSUSE_CHANGELOG_SEPARATOR + "\n" + hdr
327+
assert changelog_entry.content == [""] + content
328+
assert changelog_entry.extended_timestamp
329+
assert changelog_entry.style == ChangelogStyle.openSUSE
330+
331+
332+
@pytest.mark.parametrize(
333+
"timestamp,author,content,entry",
334+
(
335+
[
336+
(
337+
datetime.datetime(2021, 6, 25, 7, 31, 34),
338+
"Dan Čermák <[email protected]>",
339+
content_1 := ["", "New upstream release 0.9.26"],
340+
ChangelogEntry(
341+
header=_OPENSUSE_CHANGELOG_SEPARATOR
342+
+ "\n"
343+
+ "Fri Jun 25 07:31:34 UTC 2021 - Dan Čermák <[email protected]>",
344+
content=content_1,
345+
),
346+
),
347+
(
348+
datetime.date(2021, 6, 25),
349+
"Dan Čermák <[email protected]>",
350+
content_2 := [
351+
"",
352+
"New upstream release 0.26",
353+
"Fixed a major regression in Foo",
354+
],
355+
ChangelogEntry(
356+
header=_OPENSUSE_CHANGELOG_SEPARATOR
357+
+ "\n"
358+
+ "Fri Jun 25 12:00:00 UTC 2021 - Dan Čermák <[email protected]>",
359+
content=content_2,
360+
),
361+
),
362+
]
363+
),
364+
)
365+
def test_create_opensuse_changelog_assemble(
366+
timestamp: Union[datetime.datetime, datetime.date],
367+
author: str,
368+
content: List[str],
369+
entry: ChangelogEntry,
370+
) -> None:
371+
assert (
372+
ChangelogEntry.assemble(
373+
timestamp,
374+
author,
375+
content,
376+
style=ChangelogStyle.openSUSE,
377+
append_newline=False,
378+
)
379+
== entry
380+
)
381+
243382

244383
def test_get_raw_section_data():
245384
tzinfo = datetime.timezone(datetime.timedelta(hours=2), name="CEST")

0 commit comments

Comments
 (0)