Skip to content

Commit 91251df

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ó <nforro@redhat.com>
1 parent 2c4d6df commit 91251df

File tree

2 files changed

+206
-8
lines changed

2 files changed

+206
-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 as used by 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: 136 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

@@ -241,6 +246,135 @@ def test_parse():
241246
assert not changelog[6].extended_timestamp
242247

243248

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

0 commit comments

Comments
 (0)