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.
Files changed (33) hide show
  1. {python_kacl-0.7.2/python_kacl.egg-info → python_kacl-0.7.4}/PKG-INFO +1 -1
  2. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/__init__.py +1 -1
  3. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/document.py +17 -5
  4. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/kacl_cli.py +14 -5
  5. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/serializer.py +20 -2
  6. {python_kacl-0.7.2 → python_kacl-0.7.4}/pyproject.toml +1 -1
  7. {python_kacl-0.7.2 → python_kacl-0.7.4/python_kacl.egg-info}/PKG-INFO +1 -1
  8. {python_kacl-0.7.2 → python_kacl-0.7.4}/tests/test_cli.py +163 -2
  9. {python_kacl-0.7.2 → python_kacl-0.7.4}/tests/test_kacl.py +171 -0
  10. {python_kacl-0.7.2 → python_kacl-0.7.4}/LICENSE +0 -0
  11. {python_kacl-0.7.2 → python_kacl-0.7.4}/MANIFEST.in +0 -0
  12. {python_kacl-0.7.2 → python_kacl-0.7.4}/README.md +0 -0
  13. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/__main__.py +0 -0
  14. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/changes.py +0 -0
  15. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/config/kacl-default.yml +0 -0
  16. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/config.py +0 -0
  17. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/element.py +0 -0
  18. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/exception.py +0 -0
  19. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/jira_client.py +0 -0
  20. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/link_provider.py +0 -0
  21. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/parser.py +0 -0
  22. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/utils.py +0 -0
  23. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/validation.py +0 -0
  24. {python_kacl-0.7.2 → python_kacl-0.7.4}/kacl/version.py +0 -0
  25. {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/SOURCES.txt +0 -0
  26. {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/dependency_links.txt +0 -0
  27. {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/entry_points.txt +0 -0
  28. {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/not-zip-safe +0 -0
  29. {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/requires.txt +0 -0
  30. {python_kacl-0.7.2 → python_kacl-0.7.4}/python_kacl.egg-info/top_level.txt +0 -0
  31. {python_kacl-0.7.2 → python_kacl-0.7.4}/requirements.txt +0 -0
  32. {python_kacl-0.7.2 → python_kacl-0.7.4}/setup.cfg +0 -0
  33. {python_kacl-0.7.2 → python_kacl-0.7.4}/tests/test_cli_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-kacl
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: Python module and CLI tool for validating and modifying Changelogs in "keep-a-changelog" format"
5
5
  Author-email: Matthias Schmieder <schmieder.matthias@gmail.com>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  # Version of the python-kacl package
2
- __version__ = "0.7.2"
2
+ __version__ = "0.7.4"
3
3
 
4
4
  from kacl.document import KACLDocument
5
5
  from kacl.serializer import KACLMarkdownSerializer
@@ -30,7 +30,7 @@ class KACLDocument:
30
30
  link_references=None,
31
31
  meta_data=None,
32
32
  file_path=None,
33
- config=KACLConfig(),
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.VersionInfo.compare(v0.version(), v1.version()) < 1:
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
- for i in range(2):
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(link_provider.initial_version(**fargs))
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
- def get(ctx, version, no_link):
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 no_link:
256
- kacl_version.set_link(None)
257
- kacl_changelog_content = kacl.dump(kacl_version)
258
- click.echo(kacl_changelog_content)
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
- for title, changes in obj.sections().items():
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.2"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-kacl
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: Python module and CLI tool for validating and modifying Changelogs in "keep-a-changelog" format"
5
5
  Author-email: Matthias Schmieder <schmieder.matthias@gmail.com>
6
6
  License: MIT
@@ -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 == 10 # spawns 8 errors
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"]) == 10
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