Skip to content

Commit d3bd554

Browse files
authored
Merge pull request #12 from SFTtech/milo/extend-debian-metadata-parsing
feat(common): add datamodels and parsing for package changelogs
2 parents b81a64c + a6cf24c commit d3bd554

File tree

13 files changed

+139
-15
lines changed

13 files changed

+139
-15
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class DebmagicError(RuntimeError):
2+
pass

packages/debmagic-common/src/debmagic/common/models/__init__.py

Whitespace-only changes.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from dataclasses import dataclass
2+
from datetime import datetime
3+
from pathlib import Path
4+
from typing import Self
5+
6+
from debian.changelog import Changelog as DebianChangelog
7+
8+
from ..errors import DebmagicError
9+
from ..type_utils import IterableDataSource
10+
from .package_version import PackageVersion
11+
12+
13+
class ChangelogFormatError(DebmagicError):
14+
pass
15+
16+
17+
def _parse_changelog_date(date: str) -> datetime:
18+
return datetime.strptime(date, "%a, %d %b %Y %H:%M:%S %z")
19+
20+
21+
def _parse_author(author: str | None) -> tuple[str | None, str | None]:
22+
if author is None:
23+
return None, None
24+
if "<" not in author:
25+
raise ChangelogFormatError()
26+
name, email = author.split("<")
27+
return name, email
28+
29+
30+
def _parse_distributions(distributions: str | None) -> list[str]:
31+
parsed: list[str] = [] if distributions is None else distributions.split(",")
32+
parsed = list(map(str.strip, parsed))
33+
return parsed
34+
35+
36+
@dataclass
37+
class ChangelogMetadata:
38+
urgency: str | None
39+
binary_only: bool = False
40+
41+
42+
@dataclass
43+
class ChangelogEntry:
44+
package: str | None
45+
version: PackageVersion
46+
distributions: list[str]
47+
metadata: ChangelogMetadata
48+
changes: list[str]
49+
author_name: str | None
50+
author_email: str | None
51+
date: datetime | None
52+
53+
54+
@dataclass
55+
class Changelog:
56+
entries: list[ChangelogEntry]
57+
58+
@classmethod
59+
def from_file(cls, file: IterableDataSource) -> Self:
60+
changelog = DebianChangelog()
61+
changelog.parse_changelog(file)
62+
entries = []
63+
for block in changelog:
64+
author_name, author_email = _parse_author(block.author)
65+
entries.append(
66+
ChangelogEntry(
67+
package=block.package,
68+
distributions=_parse_distributions(block.distributions),
69+
version=block.version,
70+
author_name=author_name,
71+
author_email=author_email,
72+
changes=block.changes(),
73+
date=_parse_changelog_date(block.date) if block.date is not None else None,
74+
metadata=ChangelogMetadata(
75+
urgency=block.urgency,
76+
),
77+
)
78+
)
79+
80+
return cls(entries=entries)
81+
82+
@classmethod
83+
def from_changelog_file(cls, changelog_file_path: Path) -> Self:
84+
with changelog_file_path.open() as f:
85+
return cls.from_file(f)

packages/debmagic-common/src/debmagic/common/package_version.py renamed to packages/debmagic-common/src/debmagic/common/models/package_version.py

File renamed without changes.

packages/debmagic-common/src/debmagic/common/package.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from debian import deb822
66

7+
from .models.changelog import Changelog
8+
79

810
@dataclass
911
class BinaryPackage:
@@ -16,13 +18,17 @@ class BinaryPackage:
1618
class SourcePackage:
1719
name: str
1820
binary_packages: list[BinaryPackage]
21+
changelog: Changelog
1922

2023
@classmethod
21-
def from_control_file(cls, control_file_path: Path) -> Self:
24+
def from_debian_directory(cls, debian_dir_path: Path) -> Self:
25+
control_file_path = debian_dir_path / "control"
2226
src_pkg: Self | None = None
2327
# which binary packages should be produced?
2428
bin_pkgs: list[BinaryPackage] = []
2529

30+
changelog = Changelog.from_changelog_file(debian_dir_path / "changelog")
31+
2632
for block in deb822.DebControl.iter_paragraphs(
2733
control_file_path.open(),
2834
use_apt_pkg=False, # don't depend on python3-apt for now.
@@ -31,10 +37,7 @@ def from_control_file(cls, control_file_path: Path) -> Self:
3137
if src_pkg is not None:
3238
raise RuntimeError("encountered multiple Source: blocks in control file")
3339
src_name = block["Source"]
34-
src_pkg = cls(
35-
src_name,
36-
bin_pkgs,
37-
)
40+
src_pkg = cls(src_name, bin_pkgs, changelog=changelog)
3841

3942
if "Package" in block:
4043
bin_pkg = BinaryPackage(
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from typing import IO, Iterable, Text
2+
3+
IterableDataSource = bytes | Text | IO[Text] | Iterable[Text] | Iterable[bytes]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pkg1 (0.1.0) trixie; urgency=medium
2+
3+
* dummy changelog entry.
4+
multiline description
5+
6+
* another entry.
7+
8+
-- Some Author <[email protected]> Sat, 27 Sep 2025 20:55:33 +0200

packages/debmagic-common/tests/assets/pkg1_minimal_control_file renamed to packages/debmagic-common/tests/assets/pkg1/debian/control

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
Source: pkg1
22
Section: devel
3-
Maintainer: Some Maintainers <maintainers@foo.com>
3+
Maintainer: Some Maintainers <maintainers@debian.org>
44
Uploaders:
5-
Some Uploader <uploader@foo.com>,
5+
Some Uploader <uploader@debian.org>,
66
Build-Depends:
77
debhelper-compat (= 13),
88
Rules-Requires-Root: no
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from datetime import datetime, timedelta, timezone
2+
from pathlib import Path
3+
4+
import pytest
5+
from debmagic.common.models.changelog import Changelog
6+
7+
asset_base = Path(__file__).parent / "assets"
8+
9+
10+
@pytest.mark.parametrize(
11+
"debian_folder_path, name",
12+
[
13+
(asset_base / "pkg1/debian", "pkg1"),
14+
],
15+
)
16+
def test_source_package_parsing(debian_folder_path: Path, name: str):
17+
changelog = Changelog.from_changelog_file(debian_folder_path / "changelog")
18+
19+
assert changelog.entries[0].package == name
20+
assert changelog.entries[0].date == datetime(
21+
year=2025, month=9, day=27, hour=20, minute=55, second=33, tzinfo=timezone(timedelta(hours=2))
22+
)
23+
assert changelog.entries[0].distributions == ["trixie"]

packages/debmagic-common/tests/test_package.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77

88

99
@pytest.mark.parametrize(
10-
"control_file_path, name",
10+
"debian_folder_path, name",
1111
[
12-
(asset_base / "pkg1_minimal_control_file", "pkg1"),
12+
(asset_base / "pkg1/debian", "pkg1"),
1313
],
1414
)
15-
def test_source_package_parsing(control_file_path: Path, name: str):
16-
package = SourcePackage.from_control_file(control_file_path)
15+
def test_source_package_parsing(debian_folder_path: Path, name: str):
16+
package = SourcePackage.from_debian_directory(debian_folder_path)
1717

1818
assert package.name == name

0 commit comments

Comments
 (0)