Skip to content

Commit 35c2fb9

Browse files
authored
Merge pull request winpython#2100 from stonebig/fix-changelog-release-level
Let a changelog with a release level be found
2 parents 3c269a8 + 0be4948 commit 35c2fb9

2 files changed

Lines changed: 224 additions & 10 deletions

File tree

‎tests/test_diff_versions.py‎

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# -*- coding: utf-8 -*-
2+
"""Which changelog a new one gets compared against.
3+
4+
`write_changelog` asks `find_previous_version` what the previous release was,
5+
and the answer comes from the file names in `changelogs/` alone. That made the
6+
naming of those files load-bearing: the pattern used to be digits and dots, so
7+
a version carrying a release level -- 3.14.7.1b1 -- matched nothing and the
8+
comparison skipped straight past it. PEP 440 orders those versions correctly
9+
already (3.14.7.0 < 3.14.7.1b1 < 3.14.7.1); only the file name pattern did not.
10+
11+
The layouts here are synthetic: the functions care about names, not contents,
12+
so an empty file under the right name exercises the real code.
13+
"""
14+
from pathlib import Path
15+
16+
import pytest
17+
18+
from wppm.diff import changelog_versions, copy_changelogs, find_previous_version
19+
20+
21+
def changelogs(searchdir: Path, *names: str) -> Path:
22+
"""A changelogs directory holding exactly these files."""
23+
searchdir.mkdir(parents=True, exist_ok=True)
24+
for name in names:
25+
(searchdir / name).write_text("", encoding="utf-8")
26+
return searchdir
27+
28+
29+
class TestFindPreviousVersion:
30+
def test_picks_the_nearest_earlier_version(self, tmp_path):
31+
searchdir = changelogs(
32+
tmp_path,
33+
"WinPythondot-64bit-3.13.15.0.md",
34+
"WinPythondot-64bit-3.14.7.0.md",
35+
"WinPythondot-64bit-3.14.7.1.md",
36+
)
37+
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.0"
38+
39+
def test_a_release_level_is_visible_and_ordered(self, tmp_path):
40+
"""The b1 sits between .0 and .1, so it is what .1 compares against.
41+
42+
This is the case the old digits-and-dots pattern dropped: it would
43+
have answered 3.14.7.0 and reported a beta's worth of changes twice.
44+
"""
45+
searchdir = changelogs(
46+
tmp_path,
47+
"WinPythondot-64bit-3.14.7.0.md",
48+
"WinPythondot-64bit-3.14.7.1b1.md",
49+
)
50+
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.1b1"
51+
52+
def test_a_release_level_compares_against_the_release_before_it(self, tmp_path):
53+
searchdir = changelogs(
54+
tmp_path,
55+
"WinPythondot-64bit-3.14.7.0.md",
56+
"WinPythondot-64bit-3.14.7.1b1.md",
57+
)
58+
assert find_previous_version("3.14.7.1b1", searchdir, "dot") == "3.14.7.0"
59+
60+
@pytest.mark.parametrize("level", ["b1", "b2", "rc1", "a1", "post1"])
61+
def test_every_level_winpython_has_used_parses(self, tmp_path, level):
62+
searchdir = changelogs(
63+
tmp_path,
64+
"WinPythondot-64bit-3.14.7.0.md",
65+
f"WinPythondot-64bit-3.14.7.1{level}.md",
66+
)
67+
assert find_previous_version("3.14.8.0", searchdir, "dot") == f"3.14.7.1{level}"
68+
69+
def test_history_companions_are_never_the_answer(self, tmp_path):
70+
"""`_History.md` sits beside every changelog and is not one."""
71+
searchdir = changelogs(
72+
tmp_path,
73+
"WinPythondot-64bit-3.14.7.0.md",
74+
"WinPythondot-64bit-3.14.7.0_History.md",
75+
)
76+
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.0"
77+
78+
def test_unrelated_files_are_ignored(self, tmp_path):
79+
searchdir = changelogs(
80+
tmp_path,
81+
"WinPythondot-64bit-3.14.7.0.md",
82+
"pylock.64-3_14_7_0dot.toml",
83+
"requir.64-3_14_7_0dot.txt",
84+
"README.md",
85+
)
86+
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.0"
87+
88+
def test_the_old_txt_changelogs_still_count(self, tmp_path):
89+
searchdir = changelogs(tmp_path, "WinPython-64bit-2.7.10.1.txt")
90+
assert find_previous_version("2.7.10.2", searchdir, "") == "2.7.10.1"
91+
92+
def test_nothing_earlier_returns_the_target_itself(self, tmp_path):
93+
"""The documented fallback: comparing a version against itself is empty."""
94+
searchdir = changelogs(tmp_path, "WinPythondot-64bit-3.14.7.1.md")
95+
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.1"
96+
97+
def test_flavors_do_not_see_each_other(self, tmp_path):
98+
"""dot and slim ship different package sets; a cross-flavor diff is noise."""
99+
searchdir = changelogs(
100+
tmp_path,
101+
"WinPythondot-64bit-3.14.7.0.md",
102+
"WinPythonslim-64bit-3.14.7.1.md",
103+
)
104+
assert find_previous_version("3.14.7.2", searchdir, "dot") == "3.14.7.0"
105+
assert find_previous_version("3.14.7.2", searchdir, "slim") == "3.14.7.1"
106+
107+
def test_a_flavor_is_not_a_prefix_of_another(self, tmp_path):
108+
"""dot must not match dotf: the free-threaded build is a separate line."""
109+
searchdir = changelogs(
110+
tmp_path,
111+
"WinPythondot-64bit-3.14.7.0.md",
112+
"WinPythondotf-64bit-3.14.7.1.md",
113+
)
114+
assert find_previous_version("3.14.7.2", searchdir, "dot") == "3.14.7.0"
115+
assert find_previous_version("3.14.7.2", searchdir, "dotf") == "3.14.7.1"
116+
117+
def test_the_unflavored_name_does_not_match_flavored_files(self, tmp_path):
118+
searchdir = changelogs(
119+
tmp_path,
120+
"WinPython-64bit-3.9.8.0.md",
121+
"WinPythondot-64bit-3.14.7.0.md",
122+
)
123+
assert find_previous_version("3.15.0.0", searchdir, "") == "3.9.8.0"
124+
125+
def test_architecture_separates_the_lines(self, tmp_path):
126+
searchdir = changelogs(
127+
tmp_path,
128+
"WinPythondot-32bit-3.9.0.0.md",
129+
"WinPythondot-64bit-3.8.0.0.md",
130+
)
131+
assert find_previous_version("3.14.0.0", searchdir, "dot", 64) == "3.8.0.0"
132+
assert find_previous_version("3.14.0.0", searchdir, "dot", 32) == "3.9.0.0"
133+
134+
135+
class TestChangelogVersions:
136+
def test_reports_the_file_each_version_came_from(self, tmp_path):
137+
searchdir = changelogs(
138+
tmp_path,
139+
"WinPythondot-64bit-3.14.7.1b1.md",
140+
"WinPythondot-64bit-3.14.7.0_History.md",
141+
)
142+
found = changelog_versions(searchdir, "dot")
143+
assert [(raw, name) for _, raw, name in found] == [
144+
("3.14.7.1b1", "WinPythondot-64bit-3.14.7.1b1.md")
145+
]
146+
147+
148+
class TestCopyChangelogs:
149+
@pytest.fixture
150+
def dest(self, tmp_path):
151+
target = tmp_path / "dest"
152+
target.mkdir()
153+
return target
154+
155+
def test_takes_the_whole_major_minor_including_levels(self, tmp_path, dest):
156+
src = changelogs(
157+
tmp_path / "src",
158+
"WinPythondot-64bit-3.14.7.0.md",
159+
"WinPythondot-64bit-3.14.7.1b1.md",
160+
"WinPythondot-64bit-3.13.15.0.md",
161+
)
162+
copy_changelogs("3.14.7.1", src, "dot", 64, dest)
163+
assert sorted(p.name for p in dest.iterdir()) == [
164+
"WinPythondot-64bit-3.14.7.0.md",
165+
"WinPythondot-64bit-3.14.7.1b1.md",
166+
]
167+
168+
def test_a_minor_is_not_a_string_prefix(self, tmp_path, dest):
169+
"""Selecting on the parsed version stops 3.1 from dragging in 3.15."""
170+
src = changelogs(
171+
tmp_path / "src",
172+
"WinPythondot-64bit-3.1.0.0.md",
173+
"WinPythondot-64bit-3.15.0.5.md",
174+
)
175+
copy_changelogs("3.1.0.0", src, "dot", 64, dest)
176+
assert [p.name for p in dest.iterdir()] == ["WinPythondot-64bit-3.1.0.0.md"]
177+
178+
def test_history_companions_are_not_copied(self, tmp_path, dest):
179+
src = changelogs(
180+
tmp_path / "src",
181+
"WinPythondot-64bit-3.14.7.0.md",
182+
"WinPythondot-64bit-3.14.7.0_History.md",
183+
)
184+
copy_changelogs("3.14.7.0", src, "dot", 64, dest)
185+
assert [p.name for p in dest.iterdir()] == ["WinPythondot-64bit-3.14.7.0.md"]

‎wppm/diff.py‎

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,38 @@ def compare_files(file1, file2, mode="full", header1=None, header2=None, header_
112112

113113
# --- ORIGINAL/HISTORICAL VERSION-TO-VERSION COMPARISON ---
114114

115+
def changelog_versions(searchdir, flavor="", architecture=64):
116+
"""The changelogs on disk, as (parsed version, version string, file name).
117+
118+
The file name is the only source of the version, so the pattern has to
119+
accept everything a WinPython version can be. It used to be digits and
120+
dots, which silently dropped any release level: 3.14.7.1b1 is a perfectly
121+
ordinary PEP 440 version -- it sorts after 3.14.7.0 and before 3.14.7.1 --
122+
but no b1 changelog was ever visible to the comparison. Now the name is
123+
handed to the version parser, which also drops the _History companions and
124+
anything else in the directory that is not a changelog, since they do not
125+
parse as versions.
126+
"""
127+
pattern = re.compile(rf"WinPython{re.escape(flavor)}-{architecture}bit-(.+)\.(?:txt|md)$")
128+
found = []
129+
for name in os.listdir(searchdir):
130+
match = pattern.match(name)
131+
if not match:
132+
continue
133+
try:
134+
found.append((version.parse(match.group(1)), match.group(1), name))
135+
except version.InvalidVersion:
136+
continue
137+
return found
138+
115139
def find_previous_version(target_version, searchdir=None, flavor="", architecture=64):
116140
search_dir = Path(searchdir) if searchdir else CHANGELOGS_DIR
117-
pattern = re.compile(rf"WinPython{flavor}-{architecture}bit-([0-9\.]+)\.(txt|md)")
118-
versions = [pattern.match(f).group(1) for f in os.listdir(search_dir) if pattern.match(f)]
119-
versions = [v for v in versions if version.parse(v) < version.parse(target_version)]
120-
return max(versions, key=version.parse, default=target_version)
141+
target = version.parse(target_version)
142+
earlier = [
143+
(parsed, raw) for parsed, raw, _ in changelog_versions(search_dir, flavor, architecture)
144+
if parsed < target
145+
]
146+
return max(earlier, default=(None, target_version))[1]
121147

122148
def load_version_markdown(version, searchdir, flavor="", architecture=64):
123149
filename = Path(searchdir) / f"WinPython{flavor}-{architecture}bit-{version}.md"
@@ -142,13 +168,16 @@ def compare_package_indexes(version2, version1=None, searchdir=None, flavor="",
142168
result += compare_markdown_sections(md1, md2, k, k, version1, version2) + "\n"
143169
return result+ "\n\n* * *\n"
144170

145-
def copy_changelogs(version, searchdir, flavor="", architecture=64, basedir=None):
146-
"""Copy all changelogs for a major.minor version into basedir."""
147-
basever = ".".join(str(version).split(".")[:2])
148-
pattern = re.compile(rf"WinPython{flavor}-{architecture}bit-{basever}[0-9\.]*\.(txt|md)")
171+
def copy_changelogs(target_version, searchdir, flavor="", architecture=64, basedir=None):
172+
"""Copy all changelogs for a major.minor version into basedir.
173+
174+
Selected on the parsed version rather than a string prefix, so a release
175+
level comes along and 3.1 cannot pick up 3.15.
176+
"""
177+
basever = version.parse(str(target_version)).release[:2]
149178
dest = Path(basedir)
150-
for fname in os.listdir(searchdir):
151-
if pattern.match(fname):
179+
for parsed, _, fname in changelog_versions(searchdir, flavor, architecture):
180+
if parsed.release[:2] == basever:
152181
shutil.copyfile(Path(searchdir) / fname, dest / fname)
153182

154183
def write_changelog(version2, version1=None, searchdir=None, flavor="", architecture=64, basedir=None):

0 commit comments

Comments
 (0)