python-kacl 0.7.2__tar.gz → 0.7.3__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.3}/PKG-INFO +1 -1
  2. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/__init__.py +1 -1
  3. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/document.py +6 -2
  4. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/kacl_cli.py +14 -5
  5. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/serializer.py +20 -2
  6. {python_kacl-0.7.2 → python_kacl-0.7.3}/pyproject.toml +1 -1
  7. {python_kacl-0.7.2 → python_kacl-0.7.3/python_kacl.egg-info}/PKG-INFO +1 -1
  8. {python_kacl-0.7.2 → python_kacl-0.7.3}/tests/test_cli.py +135 -2
  9. {python_kacl-0.7.2 → python_kacl-0.7.3}/tests/test_kacl.py +147 -0
  10. {python_kacl-0.7.2 → python_kacl-0.7.3}/LICENSE +0 -0
  11. {python_kacl-0.7.2 → python_kacl-0.7.3}/MANIFEST.in +0 -0
  12. {python_kacl-0.7.2 → python_kacl-0.7.3}/README.md +0 -0
  13. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/__main__.py +0 -0
  14. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/changes.py +0 -0
  15. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/config/kacl-default.yml +0 -0
  16. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/config.py +0 -0
  17. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/element.py +0 -0
  18. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/exception.py +0 -0
  19. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/jira_client.py +0 -0
  20. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/link_provider.py +0 -0
  21. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/parser.py +0 -0
  22. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/utils.py +0 -0
  23. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/validation.py +0 -0
  24. {python_kacl-0.7.2 → python_kacl-0.7.3}/kacl/version.py +0 -0
  25. {python_kacl-0.7.2 → python_kacl-0.7.3}/python_kacl.egg-info/SOURCES.txt +0 -0
  26. {python_kacl-0.7.2 → python_kacl-0.7.3}/python_kacl.egg-info/dependency_links.txt +0 -0
  27. {python_kacl-0.7.2 → python_kacl-0.7.3}/python_kacl.egg-info/entry_points.txt +0 -0
  28. {python_kacl-0.7.2 → python_kacl-0.7.3}/python_kacl.egg-info/not-zip-safe +0 -0
  29. {python_kacl-0.7.2 → python_kacl-0.7.3}/python_kacl.egg-info/requires.txt +0 -0
  30. {python_kacl-0.7.2 → python_kacl-0.7.3}/python_kacl.egg-info/top_level.txt +0 -0
  31. {python_kacl-0.7.2 → python_kacl-0.7.3}/requirements.txt +0 -0
  32. {python_kacl-0.7.2 → python_kacl-0.7.3}/setup.cfg +0 -0
  33. {python_kacl-0.7.2 → python_kacl-0.7.3}/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.3
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.3"
3
3
 
4
4
  from kacl.document import KACLDocument
5
5
  from kacl.serializer import KACLMarkdownSerializer
@@ -205,7 +205,7 @@ class KACLDocument:
205
205
  try:
206
206
  v0 = versions[i]
207
207
  v1 = versions[i + 1]
208
- if semver.VersionInfo.compare(v0.version(), v1.version()) < 1:
208
+ if semver.compare(v0.version(), v1.version()) < 1:
209
209
  validation.add_error(
210
210
  line=v1.raw(),
211
211
  line_number=v1.line_number(),
@@ -502,7 +502,11 @@ class KACLDocument:
502
502
 
503
503
  if auto_link:
504
504
  link_provider = self.__get_link_provider()
505
- for i in range(2):
505
+ # Only process the versions we just added/modified
506
+ # If add_unreleased=True: process Unreleased (index 0) and new version (index 1)
507
+ # If add_unreleased=False: only process new version (index 0)
508
+ versions_to_link = 2 if self.config.add_unreleased else 1
509
+ for i in range(versions_to_link):
506
510
  fargs = {
507
511
  "version": self.__versions[i].version(),
508
512
  "previous_version": None,
@@ -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.3"
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.3
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,7 @@ 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
21
 
22
22
 
23
23
  def test_verify_invalid_duplicates():
@@ -35,7 +35,7 @@ def test_verify_with_json_output():
35
35
  )
36
36
  validation_result = json.loads(result.output)
37
37
 
38
- assert len(validation_result["errors"]) == 10
38
+ assert len(validation_result["errors"]) == 11
39
39
  assert validation_result["valid"] is False
40
40
 
41
41
 
@@ -591,6 +591,31 @@ def test_get_suppress_available_link(tmp_path, snapshot):
591
591
  snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
592
592
 
593
593
 
594
+ def test_get_suppress_header(tmp_path, snapshot):
595
+ runner = CliRunner()
596
+ resources_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/")
597
+ changelog_file = os.path.join(resources_dir, "CHANGELOG_missing_link.md")
598
+
599
+ with runner.isolated_filesystem(temp_dir=tmp_path) as project_root_path:
600
+ result = runner.invoke(
601
+ cli,
602
+ [
603
+ "-c",
604
+ os.path.join(resources_dir, "extension-config.yml"),
605
+ "-f",
606
+ changelog_file,
607
+ "get",
608
+ "--no-header",
609
+ "1.0.0",
610
+ ],
611
+ catch_exceptions=False,
612
+ )
613
+ assert result.exit_code == 0, result.output
614
+ with open(os.path.join(project_root_path, "output.md"), "w") as file:
615
+ file.write(result.output)
616
+ snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
617
+
618
+
594
619
  @freeze_time("2023-01-01")
595
620
  def test_stash_no_pre_existing(tmp_path, snapshot):
596
621
  """Test stashing when there is no pre-existing stash file."""
@@ -740,3 +765,111 @@ def test_stash_without_modify(tmp_path, snapshot):
740
765
  assert "## 1.0.0" in result.output
741
766
 
742
767
  snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
768
+
769
+
770
+ @freeze_time("2023-01-01")
771
+ def test_release_no_unreleased_with_link_generation(tmp_path, snapshot):
772
+ """Test that release with add_unreleased=False and auto-link generates correct links."""
773
+ runner = CliRunner()
774
+ resources_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/")
775
+ changelog_file = os.path.join(resources_dir, "CHANGELOG_with_changes.md")
776
+
777
+ with runner.isolated_filesystem(temp_dir=tmp_path) as project_root_path:
778
+ shutil.copyfile(changelog_file, os.path.join(project_root_path, "CHANGELOG.md"))
779
+ result = runner.invoke(
780
+ cli,
781
+ [
782
+ "-c",
783
+ os.path.join(resources_dir, "no-unreleased-config.yml"),
784
+ "-f",
785
+ "CHANGELOG.md",
786
+ "release",
787
+ "2.0.0",
788
+ "-m",
789
+ "-g", # --auto-link
790
+ ],
791
+ catch_exceptions=False,
792
+ )
793
+ assert result.exit_code == 0, result.output
794
+
795
+ # Read the updated changelog
796
+ with open(os.path.join(project_root_path, "CHANGELOG.md"), "r") as f:
797
+ content = f.read()
798
+
799
+ # Verify no Unreleased section exists
800
+ assert "## Unreleased" not in content or content.index(
801
+ "## 2.0.0"
802
+ ) < content.index("## Unreleased")
803
+
804
+ # Verify the new version has a link reference
805
+ assert "[2.0.0]:" in content
806
+
807
+ snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
808
+
809
+
810
+ def test_get_no_header_with_no_link(tmp_path, snapshot):
811
+ """Test combining --no-header and --no-link flags."""
812
+ runner = CliRunner()
813
+ resources_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/")
814
+ changelog_file = os.path.join(resources_dir, "CHANGELOG_missing_link.md")
815
+
816
+ with runner.isolated_filesystem(temp_dir=tmp_path) as project_root_path:
817
+ result = runner.invoke(
818
+ cli,
819
+ [
820
+ "-c",
821
+ os.path.join(resources_dir, "extension-config.yml"),
822
+ "-f",
823
+ changelog_file,
824
+ "get",
825
+ "--no-header",
826
+ "--no-link",
827
+ "1.0.0",
828
+ ],
829
+ catch_exceptions=False,
830
+ )
831
+ assert result.exit_code == 0, result.output
832
+
833
+ # Verify no version header
834
+ assert "## 1.0.0" not in result.output
835
+ assert "## [1.0.0]" not in result.output
836
+
837
+ # Verify no link reference
838
+ assert "[1.0.0]:" not in result.output
839
+
840
+ # Verify sections are present
841
+ assert "### " in result.output
842
+
843
+ with open(os.path.join(project_root_path, "output.md"), "w") as file:
844
+ file.write(result.output)
845
+ snapshot_directory(snapshot=snapshot, directory_path=project_root_path)
846
+
847
+
848
+ def test_get_no_header_output_format(tmp_path):
849
+ """Test that --no-header outputs only sections without version header."""
850
+ runner = CliRunner()
851
+ resources_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/")
852
+ changelog_file = os.path.join(resources_dir, "CHANGELOG_with_changes.md")
853
+
854
+ result = runner.invoke(
855
+ cli,
856
+ [
857
+ "-f",
858
+ changelog_file,
859
+ "get",
860
+ "--no-header",
861
+ "1.0.0",
862
+ ],
863
+ catch_exceptions=False,
864
+ )
865
+ assert result.exit_code == 0, result.output
866
+
867
+ # Should not contain version header (## 1.0.0 or ## [1.0.0])
868
+ assert "## 1.0.0" not in result.output
869
+ assert "## [1.0.0]" not in result.output
870
+
871
+ # Should contain section headers (###)
872
+ assert "###" in result.output
873
+
874
+ # Should contain actual content
875
+ 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())
@@ -486,3 +517,119 @@ def test_complex_metadata(tmp_path):
486
517
  "---\nlist:\n- entry1\n- additional: true\n key: value\ntitle: CHANGELOG\n---\n"
487
518
  in str(content)
488
519
  )
520
+
521
+
522
+ def test_release_no_unreleased_with_autolink(tmp_path):
523
+ """Test that auto-linking works correctly when add_unreleased=False."""
524
+ from kacl.config import KACLConfig
525
+
526
+ changelog_file = os.path.join(
527
+ os.path.dirname(os.path.realpath(__file__)),
528
+ "data/CHANGELOG_with_changes.md",
529
+ )
530
+ config_file = os.path.join(
531
+ os.path.dirname(os.path.realpath(__file__)),
532
+ "data/no-unreleased-config.yml",
533
+ )
534
+
535
+ # Load changelog and set custom config
536
+ changelog = kacl.load(changelog_file)
537
+ changelog.config = KACLConfig(config_file)
538
+ assert changelog.is_valid()
539
+
540
+ # Release with auto-linking
541
+ changelog.release(version="2.0.0", auto_link=True)
542
+
543
+ # Verify the new version has a link
544
+ new_version = changelog.get("2.0.0")
545
+ assert new_version is not None
546
+ assert new_version.link() is not None
547
+
548
+ # Verify there's no Unreleased section after release
549
+ versions = changelog.versions()
550
+ assert (
551
+ versions[0].version() == "2.0.0"
552
+ ), "First version should be the new release, not Unreleased"
553
+
554
+ # Verify the link points to the correct comparison
555
+ content = kacl.dump(changelog)
556
+ assert "[2.0.0]:" in content, "New version should have a link reference"
557
+
558
+
559
+ def test_serializer_sections_dict(tmp_path):
560
+ """Test that serializer correctly handles version.sections() dict (for --no-header)."""
561
+ changelog_file = os.path.join(
562
+ os.path.dirname(os.path.realpath(__file__)),
563
+ "data/CHANGELOG_with_changes.md",
564
+ )
565
+ changelog = kacl.load(changelog_file)
566
+ version = changelog.get("1.0.0")
567
+
568
+ # Get sections dict (what's passed when using --no-header)
569
+ sections = version.sections()
570
+
571
+ # Serialize the sections dict
572
+ content = kacl.dump(sections)
573
+
574
+ # Verify output doesn't include version header
575
+ assert "## 1.0.0" not in content
576
+ assert "## [1.0.0]" not in content
577
+
578
+ # Verify it includes section headers and content
579
+ assert "### Added" in content
580
+ assert "New visual identity" in content
581
+
582
+
583
+ def test_serializer_invalid_dict():
584
+ """Test that serializer rejects invalid dict input."""
585
+ import pytest
586
+
587
+ # Try to serialize a plain dict that's not from version.sections()
588
+ invalid_dict = {"key": "value", "another": "item"}
589
+
590
+ with pytest.raises(TypeError) as exc_info:
591
+ kacl.dump(invalid_dict)
592
+
593
+ assert "KACLChanges" in str(exc_info.value)
594
+
595
+
596
+ def test_release_no_unreleased_multiple_versions(tmp_path):
597
+ """Test auto-link with add_unreleased=False doesn't overwrite existing version links."""
598
+ from kacl.config import KACLConfig
599
+
600
+ changelog_file = os.path.join(
601
+ os.path.dirname(os.path.realpath(__file__)),
602
+ "data/CHANGELOG_keepachangelog.com.md",
603
+ )
604
+ config_file = os.path.join(
605
+ os.path.dirname(os.path.realpath(__file__)),
606
+ "data/no-unreleased-config.yml",
607
+ )
608
+
609
+ # Load changelog and set custom config
610
+ changelog = kacl.load(changelog_file)
611
+ changelog.config = KACLConfig(config_file)
612
+
613
+ # Get the second version's link before release (should not be modified)
614
+ versions_before = changelog.versions()
615
+ if len(versions_before) > 1:
616
+ second_version_before = versions_before[1]
617
+ second_version_link_before = second_version_before.link()
618
+
619
+ # Release with auto-linking (allow_no_changes since the test file may not have unreleased changes)
620
+ changelog.release(version="2.0.0", auto_link=True, allow_no_changes=True)
621
+
622
+ # Get versions after release
623
+ versions_after = changelog.versions()
624
+
625
+ # First version should be the new release (not Unreleased)
626
+ assert versions_after[0].version() == "2.0.0"
627
+
628
+ # New version should have a link
629
+ assert versions_after[0].link() is not None
630
+
631
+ # Second version's link should not have been modified
632
+ if len(versions_before) > 1 and second_version_link_before:
633
+ second_version_after = versions_after[1]
634
+ # The link might be updated during auto-link, but shouldn't be broken
635
+ 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