python-kacl 0.7.2__tar.gz → 0.7.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_kacl-0.7.2/python_kacl.egg-info → python_kacl-0.7.4}/PKG-INFO +1 -1
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/__init__.py +1 -1
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/document.py +17 -5
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/kacl_cli.py +14 -5
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/serializer.py +20 -2
- {python_kacl-0.7.2 → python_kacl-0.7.4}/pyproject.toml +1 -1
- {python_kacl-0.7.2 → python_kacl-0.7.4/python_kacl.egg-info}/PKG-INFO +1 -1
- {python_kacl-0.7.2 → python_kacl-0.7.4}/tests/test_cli.py +163 -2
- {python_kacl-0.7.2 → python_kacl-0.7.4}/tests/test_kacl.py +171 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/LICENSE +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/MANIFEST.in +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/README.md +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/__main__.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/changes.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/config/kacl-default.yml +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/config.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/element.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/exception.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/jira_client.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/link_provider.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/parser.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/utils.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/validation.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/version.py +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/SOURCES.txt +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/dependency_links.txt +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/entry_points.txt +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/not-zip-safe +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/requires.txt +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/top_level.txt +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/requirements.txt +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/setup.cfg +0 -0
- {python_kacl-0.7.2 → python_kacl-0.7.4}/tests/test_cli_workflow.py +0 -0
|
@@ -30,7 +30,7 @@ class KACLDocument:
|
|
|
30
30
|
link_references=None,
|
|
31
31
|
meta_data=None,
|
|
32
32
|
file_path=None,
|
|
33
|
-
config=
|
|
33
|
+
config=None,
|
|
34
34
|
):
|
|
35
35
|
self.__data = data
|
|
36
36
|
self.__headers = headers if headers else []
|
|
@@ -39,7 +39,7 @@ class KACLDocument:
|
|
|
39
39
|
if not self.__link_references:
|
|
40
40
|
self.__link_references = dict()
|
|
41
41
|
self.__meta_data = meta_data
|
|
42
|
-
self.config = config
|
|
42
|
+
self.config = config if config is not None else KACLConfig()
|
|
43
43
|
self.stash = []
|
|
44
44
|
self.file_path = file_path
|
|
45
45
|
|
|
@@ -82,6 +82,9 @@ class KACLDocument:
|
|
|
82
82
|
data = fh.read()
|
|
83
83
|
try:
|
|
84
84
|
doc = KACLDocument.parse(data, None)
|
|
85
|
+
# stash documents must be validated against the same
|
|
86
|
+
# config as the changelog they belong to
|
|
87
|
+
doc.config = self.config
|
|
85
88
|
self.stash.append(doc)
|
|
86
89
|
except Exception:
|
|
87
90
|
continue
|
|
@@ -205,7 +208,7 @@ class KACLDocument:
|
|
|
205
208
|
try:
|
|
206
209
|
v0 = versions[i]
|
|
207
210
|
v1 = versions[i + 1]
|
|
208
|
-
if semver.
|
|
211
|
+
if semver.Version.parse(v0.version()).compare(v1.version()) < 1:
|
|
209
212
|
validation.add_error(
|
|
210
213
|
line=v1.raw(),
|
|
211
214
|
line_number=v1.line_number(),
|
|
@@ -502,7 +505,11 @@ class KACLDocument:
|
|
|
502
505
|
|
|
503
506
|
if auto_link:
|
|
504
507
|
link_provider = self.__get_link_provider()
|
|
505
|
-
|
|
508
|
+
# Only process the versions we just added/modified
|
|
509
|
+
# If add_unreleased=True: process Unreleased (index 0) and new version (index 1)
|
|
510
|
+
# If add_unreleased=False: only process new version (index 0)
|
|
511
|
+
versions_to_link = 2 if self.config.add_unreleased else 1
|
|
512
|
+
for i in range(versions_to_link):
|
|
506
513
|
fargs = {
|
|
507
514
|
"version": self.__versions[i].version(),
|
|
508
515
|
"previous_version": None,
|
|
@@ -615,7 +622,12 @@ class KACLDocument:
|
|
|
615
622
|
versions[i].set_link(link_provider.unreleased_changes(**fargs))
|
|
616
623
|
else:
|
|
617
624
|
versions[i].set_link(link_provider.compare_versions(**fargs))
|
|
618
|
-
versions[-1].set_link(
|
|
625
|
+
versions[-1].set_link(
|
|
626
|
+
link_provider.initial_version(
|
|
627
|
+
version=versions[-1].version(),
|
|
628
|
+
latest_version=self.current_version(),
|
|
629
|
+
)
|
|
630
|
+
)
|
|
619
631
|
elif len(versions) == 1:
|
|
620
632
|
fargs = {
|
|
621
633
|
"version": versions[0].version(),
|
|
@@ -244,7 +244,12 @@ def next(ctx, version):
|
|
|
244
244
|
is_flag=True,
|
|
245
245
|
help="This option suppresses a link, even if available.",
|
|
246
246
|
)
|
|
247
|
-
|
|
247
|
+
@click.option(
|
|
248
|
+
"--no-header",
|
|
249
|
+
is_flag=True,
|
|
250
|
+
help="This option suppresses the header and link.",
|
|
251
|
+
)
|
|
252
|
+
def get(ctx, version, no_link, no_header):
|
|
248
253
|
"""Returns a given version from the Changelog."""
|
|
249
254
|
kacl_changelog = load_changelog(ctx)
|
|
250
255
|
|
|
@@ -252,10 +257,14 @@ def get(ctx, version, no_link):
|
|
|
252
257
|
kacl_version = kacl_changelog.get(version)
|
|
253
258
|
|
|
254
259
|
if kacl_version:
|
|
255
|
-
if
|
|
256
|
-
kacl_version.
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
if no_header:
|
|
261
|
+
kacl_changelog_content = kacl.dump(kacl_version.sections())
|
|
262
|
+
click.echo(kacl_changelog_content)
|
|
263
|
+
else:
|
|
264
|
+
if no_link:
|
|
265
|
+
kacl_version.set_link(None)
|
|
266
|
+
kacl_changelog_content = kacl.dump(kacl_version)
|
|
267
|
+
click.echo(kacl_changelog_content)
|
|
259
268
|
else:
|
|
260
269
|
click.echo(
|
|
261
270
|
click.style("Error: ", fg="red")
|
|
@@ -36,6 +36,15 @@ class KACLMarkdownSerializer:
|
|
|
36
36
|
if document.has_link_reference():
|
|
37
37
|
link_references.append(self.__serialize_link_reference(document))
|
|
38
38
|
|
|
39
|
+
elif isinstance(document, dict):
|
|
40
|
+
# Validate that the dict contains KACLChanges objects (from version.sections())
|
|
41
|
+
if document and all(isinstance(v, KACLChanges) for v in document.values()):
|
|
42
|
+
data.extend([self.__serialize_sections(document), ""])
|
|
43
|
+
else:
|
|
44
|
+
raise TypeError(
|
|
45
|
+
"Dict input must contain KACLChanges objects (from version.sections())"
|
|
46
|
+
)
|
|
47
|
+
|
|
39
48
|
data.extend(link_references)
|
|
40
49
|
|
|
41
50
|
if data[-1] != "":
|
|
@@ -72,7 +81,17 @@ class KACLMarkdownSerializer:
|
|
|
72
81
|
|
|
73
82
|
def __serialize_version(self, obj):
|
|
74
83
|
lines = [self.__serialize_header(obj), ""]
|
|
75
|
-
|
|
84
|
+
lines.append(self.__serialize_sections(obj.sections()))
|
|
85
|
+
return "\n".join(lines).strip()
|
|
86
|
+
|
|
87
|
+
def __serialize_sections(self, sections_dict):
|
|
88
|
+
"""Serialize a dictionary of section title -> KACLChanges.
|
|
89
|
+
|
|
90
|
+
Used by both __serialize_version (for full version with header)
|
|
91
|
+
and for standalone sections (for --no-header output).
|
|
92
|
+
"""
|
|
93
|
+
lines = []
|
|
94
|
+
for title, changes in sections_dict.items():
|
|
76
95
|
lines.extend(
|
|
77
96
|
[
|
|
78
97
|
self.__serialize_header(changes),
|
|
@@ -81,7 +100,6 @@ class KACLMarkdownSerializer:
|
|
|
81
100
|
"",
|
|
82
101
|
]
|
|
83
102
|
)
|
|
84
|
-
|
|
85
103
|
return "\n".join(lines).strip()
|
|
86
104
|
|
|
87
105
|
def __serialize_list(self, obj):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-kacl"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.4"
|
|
8
8
|
description = "Python module and CLI tool for validating and modifying Changelogs in \"keep-a-changelog\" format\""
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -17,7 +17,35 @@ def test_verify():
|
|
|
17
17
|
assert result.output == "Success\n"
|
|
18
18
|
|
|
19
19
|
result = runner.invoke(cli, ["-f", "tests/data/CHANGELOG_invalid.md", "verify"])
|
|
20
|
-
assert result.exit_code ==
|
|
20
|
+
assert result.exit_code == 11 # spawns 11 errors
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_verify_stash_with_custom_sections(tmp_path):
|
|
24
|
+
"""Stash files must be validated against the loaded config, not the built-in defaults."""
|
|
25
|
+
runner = CliRunner()
|
|
26
|
+
resources_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/")
|
|
27
|
+
changelog_file = os.path.join(resources_dir, "CHANGELOG.md")
|
|
28
|
+
stash_dir_local = os.path.join(resources_dir, "stash-custom-section")
|
|
29
|
+
config_file = os.path.join(resources_dir, "custom-sections-config.yml")
|
|
30
|
+
|
|
31
|
+
with runner.isolated_filesystem(temp_dir=tmp_path) as project_root_path:
|
|
32
|
+
shutil.copyfile(changelog_file, os.path.join(project_root_path, "CHANGELOG.md"))
|
|
33
|
+
shutil.copytree(stash_dir_local, os.path.join(project_root_path, ".kacl_stash"))
|
|
34
|
+
|
|
35
|
+
# without the custom config the stashed "Chore" section is rejected
|
|
36
|
+
result = runner.invoke(
|
|
37
|
+
cli, ["-f", "CHANGELOG.md", "verify"], catch_exceptions=False
|
|
38
|
+
)
|
|
39
|
+
assert result.exit_code != 0, result.output
|
|
40
|
+
assert '"Chore" is not a valid section' in result.output
|
|
41
|
+
|
|
42
|
+
# with a config that allows "Chore" the stashed changes must validate
|
|
43
|
+
result = runner.invoke(
|
|
44
|
+
cli,
|
|
45
|
+
["-c", config_file, "-f", "CHANGELOG.md", "verify"],
|
|
46
|
+
catch_exceptions=False,
|
|
47
|
+
)
|
|
48
|
+
assert result.exit_code == 0, result.output
|
|
21
49
|
|
|
22
50
|
|
|
23
51
|
def test_verify_invalid_duplicates():
|
|
@@ -35,7 +63,7 @@ def test_verify_with_json_output():
|
|
|
35
63
|
)
|
|
36
64
|
validation_result = json.loads(result.output)
|
|
37
65
|
|
|
38
|
-
assert len(validation_result["errors"]) ==
|
|
66
|
+
assert len(validation_result["errors"]) == 11
|
|
39
67
|
assert validation_result["valid"] is False
|
|
40
68
|
|
|
41
69
|
|
|
@@ -591,6 +619,31 @@ def test_get_suppress_available_link(tmp_path, snapshot):
|
|
|
591
619
|
snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
|
|
592
620
|
|
|
593
621
|
|
|
622
|
+
def test_get_suppress_header(tmp_path, snapshot):
|
|
623
|
+
runner = CliRunner()
|
|
624
|
+
resources_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/")
|
|
625
|
+
changelog_file = os.path.join(resources_dir, "CHANGELOG_missing_link.md")
|
|
626
|
+
|
|
627
|
+
with runner.isolated_filesystem(temp_dir=tmp_path) as project_root_path:
|
|
628
|
+
result = runner.invoke(
|
|
629
|
+
cli,
|
|
630
|
+
[
|
|
631
|
+
"-c",
|
|
632
|
+
os.path.join(resources_dir, "extension-config.yml"),
|
|
633
|
+
"-f",
|
|
634
|
+
changelog_file,
|
|
635
|
+
"get",
|
|
636
|
+
"--no-header",
|
|
637
|
+
"1.0.0",
|
|
638
|
+
],
|
|
639
|
+
catch_exceptions=False,
|
|
640
|
+
)
|
|
641
|
+
assert result.exit_code == 0, result.output
|
|
642
|
+
with open(os.path.join(project_root_path, "output.md"), "w") as file:
|
|
643
|
+
file.write(result.output)
|
|
644
|
+
snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
|
|
645
|
+
|
|
646
|
+
|
|
594
647
|
@freeze_time("2023-01-01")
|
|
595
648
|
def test_stash_no_pre_existing(tmp_path, snapshot):
|
|
596
649
|
"""Test stashing when there is no pre-existing stash file."""
|
|
@@ -740,3 +793,111 @@ def test_stash_without_modify(tmp_path, snapshot):
|
|
|
740
793
|
assert "## 1.0.0" in result.output
|
|
741
794
|
|
|
742
795
|
snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
@freeze_time("2023-01-01")
|
|
799
|
+
def test_release_no_unreleased_with_link_generation(tmp_path, snapshot):
|
|
800
|
+
"""Test that release with add_unreleased=False and auto-link generates correct links."""
|
|
801
|
+
runner = CliRunner()
|
|
802
|
+
resources_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/")
|
|
803
|
+
changelog_file = os.path.join(resources_dir, "CHANGELOG_with_changes.md")
|
|
804
|
+
|
|
805
|
+
with runner.isolated_filesystem(temp_dir=tmp_path) as project_root_path:
|
|
806
|
+
shutil.copyfile(changelog_file, os.path.join(project_root_path, "CHANGELOG.md"))
|
|
807
|
+
result = runner.invoke(
|
|
808
|
+
cli,
|
|
809
|
+
[
|
|
810
|
+
"-c",
|
|
811
|
+
os.path.join(resources_dir, "no-unreleased-config.yml"),
|
|
812
|
+
"-f",
|
|
813
|
+
"CHANGELOG.md",
|
|
814
|
+
"release",
|
|
815
|
+
"2.0.0",
|
|
816
|
+
"-m",
|
|
817
|
+
"-g", # --auto-link
|
|
818
|
+
],
|
|
819
|
+
catch_exceptions=False,
|
|
820
|
+
)
|
|
821
|
+
assert result.exit_code == 0, result.output
|
|
822
|
+
|
|
823
|
+
# Read the updated changelog
|
|
824
|
+
with open(os.path.join(project_root_path, "CHANGELOG.md"), "r") as f:
|
|
825
|
+
content = f.read()
|
|
826
|
+
|
|
827
|
+
# Verify no Unreleased section exists
|
|
828
|
+
assert "## Unreleased" not in content or content.index(
|
|
829
|
+
"## 2.0.0"
|
|
830
|
+
) < content.index("## Unreleased")
|
|
831
|
+
|
|
832
|
+
# Verify the new version has a link reference
|
|
833
|
+
assert "[2.0.0]:" in content
|
|
834
|
+
|
|
835
|
+
snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def test_get_no_header_with_no_link(tmp_path, snapshot):
|
|
839
|
+
"""Test combining --no-header and --no-link flags."""
|
|
840
|
+
runner = CliRunner()
|
|
841
|
+
resources_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/")
|
|
842
|
+
changelog_file = os.path.join(resources_dir, "CHANGELOG_missing_link.md")
|
|
843
|
+
|
|
844
|
+
with runner.isolated_filesystem(temp_dir=tmp_path) as project_root_path:
|
|
845
|
+
result = runner.invoke(
|
|
846
|
+
cli,
|
|
847
|
+
[
|
|
848
|
+
"-c",
|
|
849
|
+
os.path.join(resources_dir, "extension-config.yml"),
|
|
850
|
+
"-f",
|
|
851
|
+
changelog_file,
|
|
852
|
+
"get",
|
|
853
|
+
"--no-header",
|
|
854
|
+
"--no-link",
|
|
855
|
+
"1.0.0",
|
|
856
|
+
],
|
|
857
|
+
catch_exceptions=False,
|
|
858
|
+
)
|
|
859
|
+
assert result.exit_code == 0, result.output
|
|
860
|
+
|
|
861
|
+
# Verify no version header
|
|
862
|
+
assert "## 1.0.0" not in result.output
|
|
863
|
+
assert "## [1.0.0]" not in result.output
|
|
864
|
+
|
|
865
|
+
# Verify no link reference
|
|
866
|
+
assert "[1.0.0]:" not in result.output
|
|
867
|
+
|
|
868
|
+
# Verify sections are present
|
|
869
|
+
assert "### " in result.output
|
|
870
|
+
|
|
871
|
+
with open(os.path.join(project_root_path, "output.md"), "w") as file:
|
|
872
|
+
file.write(result.output)
|
|
873
|
+
snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def test_get_no_header_output_format(tmp_path):
|
|
877
|
+
"""Test that --no-header outputs only sections without version header."""
|
|
878
|
+
runner = CliRunner()
|
|
879
|
+
resources_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/")
|
|
880
|
+
changelog_file = os.path.join(resources_dir, "CHANGELOG_with_changes.md")
|
|
881
|
+
|
|
882
|
+
result = runner.invoke(
|
|
883
|
+
cli,
|
|
884
|
+
[
|
|
885
|
+
"-f",
|
|
886
|
+
changelog_file,
|
|
887
|
+
"get",
|
|
888
|
+
"--no-header",
|
|
889
|
+
"1.0.0",
|
|
890
|
+
],
|
|
891
|
+
catch_exceptions=False,
|
|
892
|
+
)
|
|
893
|
+
assert result.exit_code == 0, result.output
|
|
894
|
+
|
|
895
|
+
# Should not contain version header (## 1.0.0 or ## [1.0.0])
|
|
896
|
+
assert "## 1.0.0" not in result.output
|
|
897
|
+
assert "## [1.0.0]" not in result.output
|
|
898
|
+
|
|
899
|
+
# Should contain section headers (###)
|
|
900
|
+
assert "###" in result.output
|
|
901
|
+
|
|
902
|
+
# Should contain actual content
|
|
903
|
+
assert "New visual identity" in result.output
|
|
@@ -156,6 +156,37 @@ class TestKacl(TestCase):
|
|
|
156
156
|
validation = changelog.validate()
|
|
157
157
|
self.assertGreaterEqual(len(validation.errors()), 0)
|
|
158
158
|
|
|
159
|
+
def test_invalid_version_ordering(self):
|
|
160
|
+
invalid_files = [
|
|
161
|
+
"CHANGELOG_duplicate_version.md",
|
|
162
|
+
"CHANGELOG_ascending_versions.md",
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
for filename in invalid_files:
|
|
166
|
+
changelog_file = os.path.join(
|
|
167
|
+
os.path.dirname(os.path.realpath(__file__)), "data", filename
|
|
168
|
+
)
|
|
169
|
+
changelog = kacl.load(changelog_file)
|
|
170
|
+
self.assertFalse(changelog.is_valid())
|
|
171
|
+
|
|
172
|
+
error_messages = [e.error_message() for e in changelog.validate().errors()]
|
|
173
|
+
self.assertTrue(
|
|
174
|
+
any("descending" in m for m in error_messages), error_messages
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def test_valid_descending_versions_with_prerelease(self):
|
|
178
|
+
changelog_file = os.path.join(
|
|
179
|
+
os.path.dirname(os.path.realpath(__file__)),
|
|
180
|
+
"data/CHANGELOG_descending_prerelease.md",
|
|
181
|
+
)
|
|
182
|
+
changelog = kacl.load(changelog_file)
|
|
183
|
+
|
|
184
|
+
error_messages = [e.error_message() for e in changelog.validate().errors()]
|
|
185
|
+
ordering_errors = [
|
|
186
|
+
m for m in error_messages if "descending" in m or "Duplicate" in m
|
|
187
|
+
]
|
|
188
|
+
self.assertEqual(ordering_errors, [], error_messages)
|
|
189
|
+
|
|
159
190
|
def test_load_empty(self):
|
|
160
191
|
changelog = kacl.parse("")
|
|
161
192
|
self.assertFalse(changelog.is_valid())
|
|
@@ -396,6 +427,30 @@ class TestKacl(TestCase):
|
|
|
396
427
|
for v in versions:
|
|
397
428
|
self.assertIsNotNone(v.link())
|
|
398
429
|
|
|
430
|
+
def test_link_generation_initial_version(self):
|
|
431
|
+
changelog_file = os.path.join(
|
|
432
|
+
os.path.dirname(os.path.realpath(__file__)), "data", "CHANGELOG.md"
|
|
433
|
+
)
|
|
434
|
+
changelog = kacl.load(changelog_file)
|
|
435
|
+
host_url = "https://github.com/user/repository"
|
|
436
|
+
changelog.generate_links(host_url=host_url)
|
|
437
|
+
|
|
438
|
+
versions = changelog.versions()
|
|
439
|
+
|
|
440
|
+
# the oldest version has to link to its own tag, not the penultimate one
|
|
441
|
+
initial_version = versions[-1]
|
|
442
|
+
self.assertEqual(
|
|
443
|
+
initial_version.link(),
|
|
444
|
+
f"{host_url}/tree/{initial_version.version()}",
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# every version in between has to compare against its predecessor
|
|
448
|
+
for i in range(1, len(versions) - 1):
|
|
449
|
+
self.assertEqual(
|
|
450
|
+
versions[i].link(),
|
|
451
|
+
f"{host_url}/compare/{versions[i + 1].version()}...{versions[i].version()}",
|
|
452
|
+
)
|
|
453
|
+
|
|
399
454
|
|
|
400
455
|
def test_squash(tmp_path, snapshot):
|
|
401
456
|
changelog_file = os.path.join(
|
|
@@ -486,3 +541,119 @@ def test_complex_metadata(tmp_path):
|
|
|
486
541
|
"---\nlist:\n- entry1\n- additional: true\n key: value\ntitle: CHANGELOG\n---\n"
|
|
487
542
|
in str(content)
|
|
488
543
|
)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def test_release_no_unreleased_with_autolink(tmp_path):
|
|
547
|
+
"""Test that auto-linking works correctly when add_unreleased=False."""
|
|
548
|
+
from kacl.config import KACLConfig
|
|
549
|
+
|
|
550
|
+
changelog_file = os.path.join(
|
|
551
|
+
os.path.dirname(os.path.realpath(__file__)),
|
|
552
|
+
"data/CHANGELOG_with_changes.md",
|
|
553
|
+
)
|
|
554
|
+
config_file = os.path.join(
|
|
555
|
+
os.path.dirname(os.path.realpath(__file__)),
|
|
556
|
+
"data/no-unreleased-config.yml",
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Load changelog and set custom config
|
|
560
|
+
changelog = kacl.load(changelog_file)
|
|
561
|
+
changelog.config = KACLConfig(config_file)
|
|
562
|
+
assert changelog.is_valid()
|
|
563
|
+
|
|
564
|
+
# Release with auto-linking
|
|
565
|
+
changelog.release(version="2.0.0", auto_link=True)
|
|
566
|
+
|
|
567
|
+
# Verify the new version has a link
|
|
568
|
+
new_version = changelog.get("2.0.0")
|
|
569
|
+
assert new_version is not None
|
|
570
|
+
assert new_version.link() is not None
|
|
571
|
+
|
|
572
|
+
# Verify there's no Unreleased section after release
|
|
573
|
+
versions = changelog.versions()
|
|
574
|
+
assert (
|
|
575
|
+
versions[0].version() == "2.0.0"
|
|
576
|
+
), "First version should be the new release, not Unreleased"
|
|
577
|
+
|
|
578
|
+
# Verify the link points to the correct comparison
|
|
579
|
+
content = kacl.dump(changelog)
|
|
580
|
+
assert "[2.0.0]:" in content, "New version should have a link reference"
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def test_serializer_sections_dict(tmp_path):
|
|
584
|
+
"""Test that serializer correctly handles version.sections() dict (for --no-header)."""
|
|
585
|
+
changelog_file = os.path.join(
|
|
586
|
+
os.path.dirname(os.path.realpath(__file__)),
|
|
587
|
+
"data/CHANGELOG_with_changes.md",
|
|
588
|
+
)
|
|
589
|
+
changelog = kacl.load(changelog_file)
|
|
590
|
+
version = changelog.get("1.0.0")
|
|
591
|
+
|
|
592
|
+
# Get sections dict (what's passed when using --no-header)
|
|
593
|
+
sections = version.sections()
|
|
594
|
+
|
|
595
|
+
# Serialize the sections dict
|
|
596
|
+
content = kacl.dump(sections)
|
|
597
|
+
|
|
598
|
+
# Verify output doesn't include version header
|
|
599
|
+
assert "## 1.0.0" not in content
|
|
600
|
+
assert "## [1.0.0]" not in content
|
|
601
|
+
|
|
602
|
+
# Verify it includes section headers and content
|
|
603
|
+
assert "### Added" in content
|
|
604
|
+
assert "New visual identity" in content
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def test_serializer_invalid_dict():
|
|
608
|
+
"""Test that serializer rejects invalid dict input."""
|
|
609
|
+
import pytest
|
|
610
|
+
|
|
611
|
+
# Try to serialize a plain dict that's not from version.sections()
|
|
612
|
+
invalid_dict = {"key": "value", "another": "item"}
|
|
613
|
+
|
|
614
|
+
with pytest.raises(TypeError) as exc_info:
|
|
615
|
+
kacl.dump(invalid_dict)
|
|
616
|
+
|
|
617
|
+
assert "KACLChanges" in str(exc_info.value)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def test_release_no_unreleased_multiple_versions(tmp_path):
|
|
621
|
+
"""Test auto-link with add_unreleased=False doesn't overwrite existing version links."""
|
|
622
|
+
from kacl.config import KACLConfig
|
|
623
|
+
|
|
624
|
+
changelog_file = os.path.join(
|
|
625
|
+
os.path.dirname(os.path.realpath(__file__)),
|
|
626
|
+
"data/CHANGELOG_keepachangelog.com.md",
|
|
627
|
+
)
|
|
628
|
+
config_file = os.path.join(
|
|
629
|
+
os.path.dirname(os.path.realpath(__file__)),
|
|
630
|
+
"data/no-unreleased-config.yml",
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Load changelog and set custom config
|
|
634
|
+
changelog = kacl.load(changelog_file)
|
|
635
|
+
changelog.config = KACLConfig(config_file)
|
|
636
|
+
|
|
637
|
+
# Get the second version's link before release (should not be modified)
|
|
638
|
+
versions_before = changelog.versions()
|
|
639
|
+
if len(versions_before) > 1:
|
|
640
|
+
second_version_before = versions_before[1]
|
|
641
|
+
second_version_link_before = second_version_before.link()
|
|
642
|
+
|
|
643
|
+
# Release with auto-linking (allow_no_changes since the test file may not have unreleased changes)
|
|
644
|
+
changelog.release(version="2.0.0", auto_link=True, allow_no_changes=True)
|
|
645
|
+
|
|
646
|
+
# Get versions after release
|
|
647
|
+
versions_after = changelog.versions()
|
|
648
|
+
|
|
649
|
+
# First version should be the new release (not Unreleased)
|
|
650
|
+
assert versions_after[0].version() == "2.0.0"
|
|
651
|
+
|
|
652
|
+
# New version should have a link
|
|
653
|
+
assert versions_after[0].link() is not None
|
|
654
|
+
|
|
655
|
+
# Second version's link should not have been modified
|
|
656
|
+
if len(versions_before) > 1 and second_version_link_before:
|
|
657
|
+
second_version_after = versions_after[1]
|
|
658
|
+
# The link might be updated during auto-link, but shouldn't be broken
|
|
659
|
+
assert second_version_after.link() is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|