python-semantic-release 9.19.0__py3-none-any.whl → 9.20.0__py3-none-any.whl

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 (24) hide show
  1. {python_semantic_release-9.19.0.dist-info → python_semantic_release-9.20.0.dist-info}/METADATA +4 -2
  2. {python_semantic_release-9.19.0.dist-info → python_semantic_release-9.20.0.dist-info}/RECORD +24 -19
  3. semantic_release/__init__.py +1 -1
  4. semantic_release/__main__.py +7 -3
  5. semantic_release/cli/commands/main.py +3 -6
  6. semantic_release/cli/commands/version.py +30 -15
  7. semantic_release/cli/config.py +48 -42
  8. semantic_release/cli/util.py +1 -1
  9. semantic_release/data/templates/angular/md/.components/changes.md.j2 +4 -4
  10. semantic_release/data/templates/angular/md/.release_notes.md.j2 +2 -2
  11. semantic_release/data/templates/angular/rst/.components/changes.rst.j2 +4 -4
  12. semantic_release/globals.py +4 -2
  13. semantic_release/helpers.py +6 -0
  14. semantic_release/version/declaration.py +38 -132
  15. semantic_release/version/declarations/__init__.py +0 -0
  16. semantic_release/version/declarations/enum.py +12 -0
  17. semantic_release/version/declarations/i_version_replacer.py +67 -0
  18. semantic_release/version/declarations/pattern.py +241 -0
  19. semantic_release/version/declarations/toml.py +148 -0
  20. {python_semantic_release-9.19.0.dist-info → python_semantic_release-9.20.0.dist-info}/AUTHORS.rst +0 -0
  21. {python_semantic_release-9.19.0.dist-info → python_semantic_release-9.20.0.dist-info}/LICENSE +0 -0
  22. {python_semantic_release-9.19.0.dist-info → python_semantic_release-9.20.0.dist-info}/WHEEL +0 -0
  23. {python_semantic_release-9.19.0.dist-info → python_semantic_release-9.20.0.dist-info}/entry_points.txt +0 -0
  24. {python_semantic_release-9.19.0.dist-info → python_semantic_release-9.20.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-semantic-release
3
- Version: 9.19.0
3
+ Version: 9.20.0
4
4
  Summary: Automatic Semantic Versioning for Python projects
5
5
  Author-email: Rolf Erik Lekang <me@rolflekang.com>
6
6
  License: MIT
@@ -33,6 +33,7 @@ Requires-Dist: importlib-resources~=6.0
33
33
  Requires-Dist: pydantic~=2.0
34
34
  Requires-Dist: rich~=13.0
35
35
  Requires-Dist: shellingham~=1.5
36
+ Requires-Dist: Deprecated~=1.2
36
37
  Provides-Extra: build
37
38
  Requires-Dist: build~=1.2; extra == "build"
38
39
  Provides-Extra: dev
@@ -45,7 +46,8 @@ Requires-Dist: sphinxcontrib-apidoc==0.5.0; extra == "docs"
45
46
  Requires-Dist: sphinx-autobuild==2024.2.4; extra == "docs"
46
47
  Requires-Dist: furo~=2024.1; extra == "docs"
47
48
  Provides-Extra: mypy
48
- Requires-Dist: mypy==1.14.1; extra == "mypy"
49
+ Requires-Dist: mypy==1.15.0; extra == "mypy"
50
+ Requires-Dist: types-Deprecated~=1.2; extra == "mypy"
49
51
  Requires-Dist: types-requests~=2.32.0; extra == "mypy"
50
52
  Requires-Dist: types-pyyaml~=6.0; extra == "mypy"
51
53
  Provides-Extra: test
@@ -1,11 +1,11 @@
1
- semantic_release/__init__.py,sha256=GB2rbgAWEPV8Ke_LT5SweTJGMfuCbPkhjXovRKWl0AQ,1229
2
- semantic_release/__main__.py,sha256=KOIBOvLruqfi5ArXcWK3ucIZ7NB55kfCbycJaxx6aQg,1485
1
+ semantic_release/__init__.py,sha256=3oeARQKsIRDyheNZV8Tz48-9tVR4TZcIbaK1QTt1V1U,1229
2
+ semantic_release/__main__.py,sha256=pksxr6g1vkKq98Q1lShsxG8tk55IMiSMHzAHKyFU5x0,1704
3
3
  semantic_release/const.py,sha256=wInJR7vcOgT1ysm5VuJQ6lD_ZGYnCwRVKz7Uz3htQc4,861
4
4
  semantic_release/enums.py,sha256=vrEw1UNRcNrFjPqOFnuUzfeoqKj0ChixVVlyk5fqbng,1744
5
5
  semantic_release/errors.py,sha256=PY9rmviSFBZkqawW6VXbUfmF9C_RNOIObcmeGxLefMo,2904
6
6
  semantic_release/gitproject.py,sha256=G4XrucN-ZwT1Kj4RMrABcr1vWb0bjKgurEeJjcL-61c,9422
7
- semantic_release/globals.py,sha256=imI9WKGa6MS2pTRAZiWZ2qIJup2eWnBz3OZmIj2YIHM,158
8
- semantic_release/helpers.py,sha256=8yQTYUS3InvEnEqqhzJPM_R-69Pk6k9gF1NgPlgQx1Q,9774
7
+ semantic_release/globals.py,sha256=TV0c_Ir1eZIahPvLOBdm_SeE8IRL5X3h5ZiVwPjDHNM,256
8
+ semantic_release/helpers.py,sha256=H-23ZRg_zq2-eXGQJnCPwzAh6txmhCUBogxFIR6Qssc,9979
9
9
  semantic_release/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  semantic_release/changelog/__init__.py,sha256=Bg6Xe5Vt32rWoMscW-hd4sUwiZqzWmsg4CD1EhMesMY,262
11
11
  semantic_release/changelog/context.py,sha256=WeLQ2BvYEWunIF8XEl6ldQE4IRg0d7r8mYSBi-TqK7o,5988
@@ -14,17 +14,17 @@ semantic_release/changelog/template.py,sha256=R3V5m-7kv9ES23e_g37fe17tk-ESgvwV0C
14
14
  semantic_release/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  semantic_release/cli/changelog_writer.py,sha256=92DntVFFGdzFc5eBUgrRXbSOSV9KVvV769NyhLOTirg,9281
16
16
  semantic_release/cli/cli_context.py,sha256=Nop71LdVCJOeSUHgTXunMyK3xAu_QKQC2cRp1QBVkX0,4134
17
- semantic_release/cli/config.py,sha256=S57MXS2feHW6p9xWOl7waMKyivFoAfH6RLmsh0b2msk,33830
17
+ semantic_release/cli/config.py,sha256=-8xKYBwlMINnWQz9e7ppRtS_-GwUGHxfqelAf4k0FQ8,33516
18
18
  semantic_release/cli/const.py,sha256=h7XE2D0D__TAZSrUUtVszwvzpkHTMOiQCf97XQNbEvA,163
19
19
  semantic_release/cli/github_actions_output.py,sha256=6oNwjnQBg9XF5QgGc4TgbwX_-W0aj65VwGSL4ALvqVg,2296
20
20
  semantic_release/cli/masking_filter.py,sha256=ric34rnXfN5RiAVVaKnhiMJOxTnEl26kI06jQqZPZoQ,3072
21
- semantic_release/cli/util.py,sha256=FyXaBkeL7nXKjy3X9rQLEwvn7p46xPekp2V8Z-5MVrk,3755
21
+ semantic_release/cli/util.py,sha256=kkn_bJmlbC5hR4GjV1l4uBpL1SfluSkb9WkDUTg9G0o,3720
22
22
  semantic_release/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  semantic_release/cli/commands/changelog.py,sha256=i3j6jhcfhFo3d7Bgrp1tD01vJkuTZvtaWX7bPteLQbA,5345
24
24
  semantic_release/cli/commands/generate_config.py,sha256=2xZOu3NpyhBp0pWr7d8ugKl_kjqQgpSsSMHq5wHTfrE,1699
25
- semantic_release/cli/commands/main.py,sha256=237rn_Od4LOWfjUjiUKI_jSV820MfcCtRpwPjxjLbyU,4312
25
+ semantic_release/cli/commands/main.py,sha256=P0YlSPLNXxB6CRQFQStslw1fCrURvN5skUd7-Ij5VKw,4271
26
26
  semantic_release/cli/commands/publish.py,sha256=y_LalPti_kZeQJzl2CR2pTZUK8DCMvNSTe4NaMC5TJA,2875
27
- semantic_release/cli/commands/version.py,sha256=BMAOXToqOC_U0nh3d-QszAOS7xhN86aGhFiaePSdfws,25287
27
+ semantic_release/cli/commands/version.py,sha256=zwxZoi76vP3oWUXiQ86V1rb6meCRe5gouwFGJ-4dD58,25578
28
28
  semantic_release/commit_parser/__init__.py,sha256=6euiDgj9bwOx1rP96vUjq090usviXkbo7OVOnRBGfcw,742
29
29
  semantic_release/commit_parser/_base.py,sha256=oDifeTmFDpS238cp_DDrGzfidaKeAD5olCB5IM4Q6z8,3058
30
30
  semantic_release/commit_parser/angular.py,sha256=UM88ethWT34__kDgsBXXb26d0V8FN1ZFjcU7psuOhgY,19259
@@ -34,12 +34,12 @@ semantic_release/commit_parser/scipy.py,sha256=0rYZglJ7uib-1Deu4J30wHh7AZS8KfO0e
34
34
  semantic_release/commit_parser/tag.py,sha256=oGB3lgyp2Eu3Tg3jjxqNzN86N6bokSaFu6f4Ir6IS_k,3546
35
35
  semantic_release/commit_parser/token.py,sha256=ECgi7eeSgk3Biq1Y_ChbFJZQLkrUpNvGhIaEOXrNC4M,7904
36
36
  semantic_release/commit_parser/util.py,sha256=_ACiopznjwINn4t1zPHl8bxZEc0zOAURTycNU-sXs3M,3959
37
- semantic_release/data/templates/angular/md/.release_notes.md.j2,sha256=3d4ihVVY1u2rAm7XgydRKQq92YfQkFWanpEP5uQ8xmM,2512
37
+ semantic_release/data/templates/angular/md/.release_notes.md.j2,sha256=DlMVAJMGqE27TwJ-2kviYaFhd3uWqXiU6Ikl15Ukne8,2512
38
38
  semantic_release/data/templates/angular/md/CHANGELOG.md.j2,sha256=FZmrQ-qOIoSoJmAa_NFaRelfmqUpypU2xlDeScdGOf4,729
39
39
  semantic_release/data/templates/angular/md/.components/changelog_header.md.j2,sha256=qNxTuSr59CV_yyimVU_RYp5azCnK0l6nJ03Zf0u5Ugg,166
40
40
  semantic_release/data/templates/angular/md/.components/changelog_init.md.j2,sha256=MyEQdWUemGXKWDhvpTmubZdozd3iaLECZAI1VRlE7mg,862
41
41
  semantic_release/data/templates/angular/md/.components/changelog_update.md.j2,sha256=uVF4wbbjTMvl6Kbsq9xy3YIrj-uhBnHylEfA7S76aHI,2606
42
- semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=NIdXNNao4Oq9nWHaq6X4ZbGMMJ2H-7yxqAP5HmBmSV8,5965
42
+ semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=hGBKJhZwnyNRPpSeNA9k3n46_gCn9fvKPRKfzg-bfXw,5965
43
43
  semantic_release/data/templates/angular/md/.components/first_release.md.j2,sha256=-S2aJV3ka4YicLs8UF6BEV-CDnL8iXLcNRRIiENFrYE,421
44
44
  semantic_release/data/templates/angular/md/.components/macros.md.j2,sha256=RzBVwKSCb-WmRIMFVQL9U157mAih-PzZ_iRcfyWkG20,8196
45
45
  semantic_release/data/templates/angular/md/.components/unreleased_changes.md.j2,sha256=HRLj6cyRfPZXC0s-0Av6s0Gp3jKxWg9AIEtIXBVqJuY,177
@@ -48,7 +48,7 @@ semantic_release/data/templates/angular/rst/CHANGELOG.rst.j2,sha256=VmkXEMHiPBdZ
48
48
  semantic_release/data/templates/angular/rst/.components/changelog_header.rst.j2,sha256=c9xN1SEYLFwMvPYXYKt-ZbYPn2-Ss0V7zepEtFFj3Os,200
49
49
  semantic_release/data/templates/angular/rst/.components/changelog_init.rst.j2,sha256=XD0l3eTyz1yydLKsmSqBk-u8RnO-RdQ2Q8uWezHMAWw,866
50
50
  semantic_release/data/templates/angular/rst/.components/changelog_update.rst.j2,sha256=x23-qk9owJrOQaHx8SgSnIZECITjPf1R2awfv9EOHN0,2604
51
- semantic_release/data/templates/angular/rst/.components/changes.rst.j2,sha256=IZdUJ5tt-8P5AyPgS1hsTuu2vg10GDINJ9wLJ4CpP68,7352
51
+ semantic_release/data/templates/angular/rst/.components/changes.rst.j2,sha256=XIJQOzH7vzPOZszMaAJBa5O4m-syfwraBLpHnX3QmU4,7352
52
52
  semantic_release/data/templates/angular/rst/.components/first_release.rst.j2,sha256=huaO-B3BRs7h2LRks2a6-656W2qzURkzLiyuKvYxMTg,462
53
53
  semantic_release/data/templates/angular/rst/.components/macros.rst.j2,sha256=WLNUD2H2V-5vrwT7TKelwQ2wclLcZxFs0E2Lk3Ld10U,9096
54
54
  semantic_release/data/templates/angular/rst/.components/unreleased_changes.rst.j2,sha256=ARBhc1ZpKwehGKDvOMqukmN59mTJiHzHsS7rOfKYCt8,202
@@ -64,13 +64,18 @@ semantic_release/hvcs/token_auth.py,sha256=ZjT56-NIPB4OKIt1qwHCu1TavXnrWFIBl9ARl
64
64
  semantic_release/hvcs/util.py,sha256=guxisysY_IW5tv7aaV-iVPEVJzgbOs375kiRRpSquTI,2879
65
65
  semantic_release/version/__init__.py,sha256=CLhtGQry9dLIij5XyRa9ZevxU_1p8tjMTSQ-K_GMpWM,270
66
66
  semantic_release/version/algorithm.py,sha256=s5lso4Py-PiVzuPhgeJOz6IDzIdqc6EeoUirmBZ8IXY,16696
67
- semantic_release/version/declaration.py,sha256=f6Ld7hIhrqvDrRBapJHr-KDimuyo-4IG8009Zu9BIgU,7357
67
+ semantic_release/version/declaration.py,sha256=aDpgfh0G-35TLXU6x4y3V0nFr8WBR4bw1GDldf0v8_Q,3638
68
68
  semantic_release/version/translator.py,sha256=P1noIsVBn8u6zNOFjG0xKYOWapxqf_PHSMvMeLJ9kXg,3050
69
69
  semantic_release/version/version.py,sha256=6PCtSbLP88U1daoxnCwHc--YguZo4waGNLqJ5JfeczE,14175
70
- python_semantic_release-9.19.0.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
71
- python_semantic_release-9.19.0.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
72
- python_semantic_release-9.19.0.dist-info/METADATA,sha256=fpNQ4-lx5yz8INyq7I4wZr0m6hTN76h9kctnkHWAiRc,3812
73
- python_semantic_release-9.19.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
74
- python_semantic_release-9.19.0.dist-info/entry_points.txt,sha256=r2Jql3GTQyugQnvf34l2eXk1O_Qx6llR_xixG1ZWgD0,105
75
- python_semantic_release-9.19.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
76
- python_semantic_release-9.19.0.dist-info/RECORD,,
70
+ semantic_release/version/declarations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
+ semantic_release/version/declarations/enum.py,sha256=3n5Py9DoFkmItIdsmtQrJgmAhepTv_1EnogzSdXu1Wg,244
72
+ semantic_release/version/declarations/i_version_replacer.py,sha256=oP6BxJuxwI44roI6448tomShv1sMoy9ry8TlhhIQtfc,2416
73
+ semantic_release/version/declarations/pattern.py,sha256=Hv2TBdWdS6d00FYOs-Ja_tfafzEw3OS4Wn5atS1z9Z0,8295
74
+ semantic_release/version/declarations/toml.py,sha256=IKAwG2iu5OWUOytCW0CqKgskU4VifsnD7I6Dkm2EKjo,4945
75
+ python_semantic_release-9.20.0.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
76
+ python_semantic_release-9.20.0.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
77
+ python_semantic_release-9.20.0.dist-info/METADATA,sha256=hTj0AawcG8vb-Wo-b6NuLtDVA8RcwQzeb3ULXHL3dho,3897
78
+ python_semantic_release-9.20.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
79
+ python_semantic_release-9.20.0.dist-info/entry_points.txt,sha256=r2Jql3GTQyugQnvf34l2eXk1O_Qx6llR_xixG1ZWgD0,105
80
+ python_semantic_release-9.20.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
81
+ python_semantic_release-9.20.0.dist-info/RECORD,,
@@ -24,7 +24,7 @@ from semantic_release.version import (
24
24
  tags_and_versions,
25
25
  )
26
26
 
27
- __version__ = "9.19.0"
27
+ __version__ = "9.20.0"
28
28
 
29
29
  __all__ = [
30
30
  "CommitParser",
@@ -8,6 +8,7 @@ from traceback import format_exception
8
8
 
9
9
  from semantic_release import globals
10
10
  from semantic_release.cli.commands.main import main as cli_main
11
+ from semantic_release.enums import SemanticReleaseLogLevels
11
12
 
12
13
 
13
14
  def main() -> None:
@@ -18,7 +19,7 @@ def main() -> None:
18
19
  print("\n-- User Abort! --", file=sys.stderr)
19
20
  sys.exit(127)
20
21
  except Exception as err: # noqa: BLE001, graceful error handling across application
21
- if globals.debug:
22
+ if globals.log_level <= SemanticReleaseLogLevels.DEBUG:
22
23
  print(f"{err.__class__.__name__}: {err}\n", file=sys.stderr)
23
24
  etype, value, traceback = sys.exc_info()
24
25
  print(
@@ -35,9 +36,12 @@ def main() -> None:
35
36
  file=sys.stderr,
36
37
  )
37
38
 
38
- print(f"::ERROR:: {err}", file=sys.stderr)
39
+ print(
40
+ str.join("\n", [f"::ERROR:: {line}" for line in str(err).splitlines()]),
41
+ file=sys.stderr,
42
+ )
39
43
 
40
- if not globals.debug:
44
+ if globals.log_level > SemanticReleaseLogLevels.DEBUG:
41
45
  print(
42
46
  "Run semantic-release in very verbose mode (-vv) to see the full traceback.",
43
47
  file=sys.stderr,
@@ -116,10 +116,10 @@ def main(
116
116
  SemanticReleaseLogLevels.SILLY,
117
117
  ]
118
118
 
119
- log_level = log_levels[verbosity]
119
+ globals.log_level = log_levels[verbosity]
120
120
 
121
121
  logging.basicConfig(
122
- level=log_level,
122
+ level=globals.log_level,
123
123
  format=FORMAT,
124
124
  datefmt="[%X]",
125
125
  handlers=[
@@ -130,10 +130,7 @@ def main(
130
130
  )
131
131
 
132
132
  logger = logging.getLogger(__name__)
133
- logger.debug("logging level set to: %s", logging.getLevelName(log_level))
134
-
135
- if log_level <= logging.DEBUG:
136
- globals.debug = True
133
+ logger.debug("logging level set to: %s", logging.getLevelName(globals.log_level))
137
134
 
138
135
  if noop:
139
136
  rprint(
@@ -39,12 +39,12 @@ from semantic_release.version.translator import VersionTranslator
39
39
 
40
40
  if TYPE_CHECKING: # pragma: no cover
41
41
  from pathlib import Path
42
- from typing import Iterable, Mapping
42
+ from typing import Mapping, Sequence
43
43
 
44
44
  from git.refs.tag import Tag
45
45
 
46
46
  from semantic_release.cli.cli_context import CliContextObj
47
- from semantic_release.version.declaration import VersionDeclarationABC
47
+ from semantic_release.version.declaration import IVersionReplacer
48
48
  from semantic_release.version.version import Version
49
49
 
50
50
 
@@ -135,28 +135,43 @@ def version_from_forced_level(
135
135
 
136
136
  def apply_version_to_source_files(
137
137
  repo_dir: Path,
138
- version_declarations: Iterable[VersionDeclarationABC],
138
+ version_declarations: Sequence[IVersionReplacer],
139
139
  version: Version,
140
140
  noop: bool = False,
141
141
  ) -> list[str]:
142
- paths = [
143
- str(declaration.path.resolve().relative_to(repo_dir))
144
- for declaration in version_declarations
142
+ if len(version_declarations) < 1:
143
+ return []
144
+
145
+ if not noop:
146
+ log.debug("Updating version %s in repository files...", version)
147
+
148
+ paths = list(
149
+ map(
150
+ lambda decl, new_version=version, noop=noop: ( # type: ignore[misc]
151
+ decl.update_file_w_version(new_version=new_version, noop=noop)
152
+ ),
153
+ version_declarations,
154
+ )
155
+ )
156
+
157
+ repo_filepaths = [
158
+ str(updated_file.relative_to(repo_dir))
159
+ for updated_file in paths
160
+ if updated_file is not None
145
161
  ]
146
162
 
147
163
  if noop:
148
164
  noop_report(
149
- "would have updated versions in the following paths:"
150
- + "".join(f"\n {path}" for path in paths)
165
+ str.join(
166
+ "",
167
+ [
168
+ "would have updated versions in the following paths:",
169
+ *[f"\n {filepath}" for filepath in repo_filepaths],
170
+ ],
171
+ )
151
172
  )
152
- return paths
153
-
154
- log.debug("writing version %s to source paths %s", version, paths)
155
- for declaration in version_declarations:
156
- new_content = declaration.replace(new_version=version)
157
- declaration.path.write_text(new_content)
158
173
 
159
- return paths
174
+ return repo_filepaths
160
175
 
161
176
 
162
177
  def shell(
@@ -46,7 +46,7 @@ from semantic_release.commit_parser import (
46
46
  ScipyCommitParser,
47
47
  TagCommitParser,
48
48
  )
49
- from semantic_release.const import COMMIT_MESSAGE, DEFAULT_COMMIT_AUTHOR, SEMVER_REGEX
49
+ from semantic_release.const import COMMIT_MESSAGE, DEFAULT_COMMIT_AUTHOR
50
50
  from semantic_release.errors import (
51
51
  DetachedHeadGitError,
52
52
  InvalidConfiguration,
@@ -55,11 +55,9 @@ from semantic_release.errors import (
55
55
  ParserLoadError,
56
56
  )
57
57
  from semantic_release.helpers import dynamic_import
58
- from semantic_release.version.declaration import (
59
- PatternVersionDeclaration,
60
- TomlVersionDeclaration,
61
- VersionDeclarationABC,
62
- )
58
+ from semantic_release.version.declarations.i_version_replacer import IVersionReplacer
59
+ from semantic_release.version.declarations.pattern import PatternVersionDeclaration
60
+ from semantic_release.version.declarations.toml import TomlVersionDeclaration
63
61
  from semantic_release.version.translator import VersionTranslator
64
62
 
65
63
  log = logging.getLogger(__name__)
@@ -555,7 +553,7 @@ class RuntimeContext:
555
553
  commit_author: Actor
556
554
  commit_message: str
557
555
  changelog_excluded_commit_patterns: Tuple[Pattern[str], ...]
558
- version_declarations: Tuple[VersionDeclarationABC, ...]
556
+ version_declarations: Tuple[IVersionReplacer, ...]
559
557
  hvcs_client: hvcs.HvcsBase
560
558
  changelog_insertion_flag: str
561
559
  changelog_mask_initial_release: bool
@@ -665,6 +663,17 @@ class RuntimeContext:
665
663
  if raw.commit_parser in _known_commit_parsers
666
664
  else dynamic_import(raw.commit_parser)
667
665
  )
666
+ except ValueError as err:
667
+ raise ParserLoadError(
668
+ str.join(
669
+ "\n",
670
+ [
671
+ f"Unrecognized commit parser value: {raw.commit_parser!r}.",
672
+ str(err),
673
+ "Unable to load the given parser! Check your configuration!",
674
+ ],
675
+ )
676
+ ) from err
668
677
  except ModuleNotFoundError as err:
669
678
  raise ParserLoadError(
670
679
  str.join(
@@ -727,44 +736,41 @@ class RuntimeContext:
727
736
 
728
737
  commit_author = Actor(*_commit_author_valid.groups())
729
738
 
730
- version_declarations: list[VersionDeclarationABC] = []
731
- for decl in () if raw.version_toml is None else raw.version_toml:
732
- try:
733
- path, search_text = decl.split(":", maxsplit=1)
734
- # VersionDeclarationABC handles path existence check
735
- vd = TomlVersionDeclaration(path, search_text)
736
- except ValueError as exc:
737
- log.exception("Invalid TOML declaration %r", decl)
738
- raise InvalidConfiguration(
739
- f"Invalid TOML declaration {decl!r}"
740
- ) from exc
741
-
742
- version_declarations.append(vd)
743
-
744
- for decl in () if raw.version_variables is None else raw.version_variables:
745
- try:
746
- path, variable = decl.split(":", maxsplit=1)
747
- # VersionDeclarationABC handles path existence check
748
- search_text = str.join(
749
- "",
739
+ version_declarations: list[IVersionReplacer] = []
740
+
741
+ try:
742
+ version_declarations.extend(
743
+ TomlVersionDeclaration.from_string_definition(definition)
744
+ for definition in iter(raw.version_toml or ())
745
+ )
746
+ except ValueError as err:
747
+ raise InvalidConfiguration(
748
+ str.join(
749
+ "\n",
750
750
  [
751
- # Supports optional matching quotations around variable name
752
- # Negative lookbehind to ensure we don't match part of a variable name
753
- f"""(?x)(?P<quote1>['"])?(?<![\\w.-]){variable}(?P=quote1)?""",
754
- # Supports walrus, equals sign, or colon as assignment operator ignoring whitespace separation
755
- r"\s*(:=|[:=])\s*",
756
- # Supports optional matching quotations around version number of a SEMVER pattern
757
- f"""(?P<quote2>['"])?(?P<version>{SEMVER_REGEX.pattern})(?P=quote2)?""",
751
+ "Invalid 'version_toml' configuration",
752
+ str(err),
758
753
  ],
759
754
  )
760
- pd = PatternVersionDeclaration(path, search_text)
761
- except ValueError as exc:
762
- log.exception("Invalid variable declaration %r", decl)
763
- raise InvalidConfiguration(
764
- f"Invalid variable declaration {decl!r}"
765
- ) from exc
766
-
767
- version_declarations.append(pd)
755
+ ) from err
756
+
757
+ try:
758
+ version_declarations.extend(
759
+ PatternVersionDeclaration.from_string_definition(
760
+ definition, raw.tag_format
761
+ )
762
+ for definition in iter(raw.version_variables or ())
763
+ )
764
+ except ValueError as err:
765
+ raise InvalidConfiguration(
766
+ str.join(
767
+ "\n",
768
+ [
769
+ "Invalid 'version_variables' configuration",
770
+ str(err),
771
+ ],
772
+ )
773
+ ) from err
768
774
 
769
775
  # Provide warnings if the token is missing
770
776
  if not raw.remote.token:
@@ -28,7 +28,7 @@ def noop_report(msg: str) -> None:
28
28
  Rich-prints a msg with a standard prefix to report when an action is not being
29
29
  taken due to a "noop" flag
30
30
  """
31
- fullmsg = "[bold cyan]:shield: semantic-release 'noop' mode is enabled! " + msg
31
+ fullmsg = "[bold cyan][:shield: NOP] " + msg
32
32
  rprint(fullmsg)
33
33
 
34
34
 
@@ -18,7 +18,7 @@ EXAMPLE:
18
18
  - Fix bug ([#11](https://domain.com/namespace/repo/pull/11),
19
19
  [`abcdef1`](https://domain.com/namespace/repo/commit/HASH))
20
20
 
21
- ### BREAKING CHANGES
21
+ ### Breaking Changes
22
22
 
23
23
  - With the change _____, the change causes ___ effect. Ultimately, this section
24
24
  it is a more detailed description of the breaking change. With an optional
@@ -27,7 +27,7 @@ EXAMPLE:
27
27
  - **scope**: this breaking change has a scope to identify the part of the code that
28
28
  this breaking change applies to for better context.
29
29
 
30
- ### ADDITIONAL RELEASE INFORMATION
30
+ ### Additional Release Information
31
31
 
32
32
  - This is a release note that provides additional information about the release
33
33
  that is not a breaking change or a feature/bug fix.
@@ -96,7 +96,7 @@ EXAMPLE:
96
96
  %}{#
97
97
  # # PRINT BREAKING CHANGE DESCRIPTIONS (header & descriptions)
98
98
  #}{{ "\n"
99
- }}{{ "### BREAKING CHANGES\n"
99
+ }}{{ "### Breaking Changes\n"
100
100
  }}{{
101
101
  "\n%s\n" | format(brking_descriptions | unique | join("\n\n"))
102
102
  }}{#
@@ -129,7 +129,7 @@ EXAMPLE:
129
129
  %}{#
130
130
  # # PRINT RELEASE NOTICE INFORMATION (header & descriptions)
131
131
  #}{{ "\n"
132
- }}{{ "### ADDITIONAL RELEASE INFORMATION\n"
132
+ }}{{ "### Additional Release Information\n"
133
133
  }}{{
134
134
  "\n%s\n" | format(release_notices | unique | join("\n\n"))
135
135
  }}{#
@@ -14,13 +14,13 @@ _This release is published under the MIT License._
14
14
 
15
15
  - Fix bug (#11, [`abcdef1`](https://domain.com/namespace/repo/commit/HASH))
16
16
 
17
- ### BREAKING CHANGES
17
+ ### Breaking Changes
18
18
 
19
19
  - With the change _____, the change causes ___ effect. Ultimately, this section it is a more detailed description of the breaking change. With an optional scope prefix like the commit messages above.
20
20
 
21
21
  - **scope**: this breaking change has a scope to identify the part of the code that this breaking change applies to for better context.
22
22
 
23
- ### ADDITIONAL RELEASE INFORMATION
23
+ ### Additional Release Information
24
24
 
25
25
  - This is a release note that provides additional information about the release that is not a breaking change or a feature/bug fix.
26
26
 
@@ -18,7 +18,7 @@ Bug Fixes
18
18
 
19
19
  * Fix bug (`#11`_, `8a7b8ec`_)
20
20
 
21
- BREAKING CHANGES
21
+ Breaking Changes
22
22
  ----------------
23
23
 
24
24
  * With the change _____, the change causes ___ effect. Ultimately, this section
@@ -28,7 +28,7 @@ BREAKING CHANGES
28
28
  * **scope**: this breaking change has a scope to identify the part of the code that
29
29
  this breaking change applies to for better context.
30
30
 
31
- ADDITIONAL RELEASE INFORMATION
31
+ Additional Release Information
32
32
  ------------------------------
33
33
 
34
34
  * This is a release note that provides additional information about the release
@@ -124,7 +124,7 @@ ADDITIONAL RELEASE INFORMATION
124
124
  %}{#
125
125
  # # PRINT BREAKING CHANGE DESCRIPTIONS (header & descriptions)
126
126
  #}{{ "\n"
127
- }}{{ "BREAKING CHANGES\n"
127
+ }}{{ "Breaking Changes\n"
128
128
  }}{{ '----------------\n'
129
129
  }}{{
130
130
  "\n%s\n" | format(brking_descriptions | unique | join("\n\n"))
@@ -158,7 +158,7 @@ ADDITIONAL RELEASE INFORMATION
158
158
  %}{#
159
159
  # # PRINT RELEASE NOTICE INFORMATION (header & descriptions)
160
160
  #}{{ "\n"
161
- }}{{ "ADDITIONAL RELEASE INFORMATION\n"
161
+ }}{{ "Additional Release Information\n"
162
162
  }}{{ "------------------------------\n"
163
163
  }}{{
164
164
  "\n%s\n" | format(release_notices | unique | join("\n\n"))
@@ -2,5 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- debug: bool = False
6
- """bool: Enable debug level logging and runtime actions."""
5
+ from semantic_release.enums import SemanticReleaseLogLevels
6
+
7
+ log_level: SemanticReleaseLogLevels = SemanticReleaseLogLevels.WARNING
8
+ """int: Logging level for semantic-release"""
@@ -157,6 +157,12 @@ def dynamic_import(import_path: str) -> Any:
157
157
  Dynamically import an object from a conventionally formatted "module:attribute"
158
158
  string
159
159
  """
160
+ if ":" not in import_path:
161
+ raise ValueError(
162
+ f"Invalid import path {import_path!r}, must use 'module:Class' format"
163
+ )
164
+
165
+ # Split the import path into module and attribute
160
166
  module_name, attr = import_path.split(":", maxsplit=1)
161
167
 
162
168
  # Check if the module is a file path, if it can be resolved and exists on disk then import as a file
@@ -1,19 +1,43 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
- import re
3
+ # TODO: Remove v10
5
4
  from abc import ABC, abstractmethod
5
+ from logging import getLogger
6
6
  from pathlib import Path
7
- from typing import Any, Dict, cast
8
-
9
- import tomlkit
10
- from dotty_dict import Dotty # type: ignore[import]
11
-
12
- from semantic_release.version.version import Version
13
-
14
- log = logging.getLogger(__name__)
15
-
16
-
7
+ from typing import TYPE_CHECKING
8
+
9
+ from deprecated.sphinx import deprecated
10
+
11
+ from semantic_release.version.declarations.enum import VersionStampType
12
+ from semantic_release.version.declarations.i_version_replacer import IVersionReplacer
13
+ from semantic_release.version.declarations.pattern import PatternVersionDeclaration
14
+ from semantic_release.version.declarations.toml import TomlVersionDeclaration
15
+
16
+ if TYPE_CHECKING: # pragma: no cover
17
+ from semantic_release.version.version import Version
18
+
19
+
20
+ # Globals
21
+ __all__ = [
22
+ "IVersionReplacer",
23
+ "VersionStampType",
24
+ "PatternVersionDeclaration",
25
+ "TomlVersionDeclaration",
26
+ "VersionDeclarationABC",
27
+ ]
28
+ log = getLogger(__name__)
29
+
30
+
31
+ @deprecated(
32
+ version="9.20.0",
33
+ reason=str.join(
34
+ " ",
35
+ [
36
+ "Refactored to composition paradigm using the new IVersionReplacer interface.",
37
+ "This class will be removed in a future release",
38
+ ],
39
+ ),
40
+ )
17
41
  class VersionDeclarationABC(ABC):
18
42
  """
19
43
  ABC for classes representing a location in which a version is declared somewhere
@@ -40,13 +64,8 @@ class VersionDeclarationABC(ABC):
40
64
  self._content = self.path.read_text()
41
65
  return self._content
42
66
 
43
- # mypy doesn't like properties?
44
- @content.setter # type: ignore[attr-defined]
45
- def _(self, _: Any) -> None:
46
- raise AttributeError("'content' cannot be set directly")
47
-
48
- @content.deleter # type: ignore[attr-defined]
49
- def _(self) -> None:
67
+ @content.deleter
68
+ def content(self) -> None:
50
69
  log.debug("resetting instance-stored source file contents")
51
70
  self._content = None
52
71
 
@@ -86,116 +105,3 @@ class VersionDeclarationABC(ABC):
86
105
  log.debug("writing content to %r", self.path.resolve())
87
106
  self.path.write_text(content)
88
107
  self._content = None
89
-
90
-
91
- class TomlVersionDeclaration(VersionDeclarationABC):
92
- """VersionDeclarationABC implementation which manages toml-format source files."""
93
-
94
- def _load(self) -> Dotty:
95
- """Load the content of the source file into a Dotty for easier searching"""
96
- loaded = tomlkit.loads(self.content)
97
- return Dotty(loaded)
98
-
99
- def parse(self) -> set[Version]:
100
- """Look for the version in the source content"""
101
- content = self._load()
102
- maybe_version: str = content.get(self.search_text) # type: ignore[return-value]
103
- if maybe_version is not None:
104
- log.debug(
105
- "Found a key %r that looks like a version (%r)",
106
- self.search_text,
107
- maybe_version,
108
- )
109
- valid_version = Version.parse(maybe_version)
110
- return {valid_version} if valid_version else set()
111
- # Maybe in future raise error if not found?
112
- return set()
113
-
114
- def replace(self, new_version: Version) -> str:
115
- """
116
- Replace the version in the source content with `new_version`, and return the
117
- updated content.
118
- """
119
- content = self._load()
120
- if self.search_text in content:
121
- log.info(
122
- "found %r in source file contents, replacing with %s",
123
- self.search_text,
124
- new_version,
125
- )
126
- content[self.search_text] = str(new_version)
127
-
128
- return tomlkit.dumps(cast(Dict[str, Any], content))
129
-
130
-
131
- class PatternVersionDeclaration(VersionDeclarationABC):
132
- """
133
- VersionDeclarationABC implementation representing a version number in a particular
134
- file. The version number is identified by a regular expression, which should be
135
- provided in `search_text`.
136
- """
137
-
138
- _VERSION_GROUP_NAME = "version"
139
-
140
- def __init__(self, path: Path | str, search_text: str) -> None:
141
- super().__init__(path, search_text)
142
- self.search_re = re.compile(self.search_text, flags=re.MULTILINE)
143
- if self._VERSION_GROUP_NAME not in self.search_re.groupindex:
144
- raise ValueError(
145
- f"Invalid search text {self.search_text!r}; must use 'version' as a "
146
- "named group, for example (?P<version>...) . For more info on named "
147
- "groups see https://docs.python.org/3/library/re.html"
148
- )
149
-
150
- # The pattern should be a regular expression with a single group,
151
- # containing the version to replace.
152
- def parse(self) -> set[Version]:
153
- """
154
- Return the versions matching this pattern.
155
- Because a pattern can match in multiple places, this method returns a
156
- set of matches. Generally, there should only be one element in this
157
- set (i.e. even if the version is specified in multiple places, it
158
- should be the same version in each place), but it falls on the caller
159
- to check for this condition.
160
- """
161
- versions = {
162
- Version.parse(m.group(self._VERSION_GROUP_NAME))
163
- for m in self.search_re.finditer(self.content, re.MULTILINE)
164
- }
165
-
166
- log.debug(
167
- "Parsing current version: path=%r pattern=%r num_matches=%s",
168
- self.path.resolve(),
169
- self.search_text,
170
- len(versions),
171
- )
172
- return versions
173
-
174
- def replace(self, new_version: Version) -> str:
175
- """
176
- Update the versions.
177
- This method reads the underlying file, replaces each occurrence of the
178
- matched pattern, then writes the updated file.
179
- :param new_version: The new version number as a `Version` instance
180
- """
181
- n = 0
182
-
183
- def swap_version(m: re.Match[str]) -> str:
184
- nonlocal n
185
- n += 1
186
- s = m.string
187
- i, j = m.span()
188
- log.debug("match spans characters %s:%s", i, j)
189
- ii, jj = m.span(self._VERSION_GROUP_NAME)
190
- log.debug("version group spans characters %s:%s", ii, jj)
191
- return s[i:ii] + str(new_version) + s[jj:j]
192
-
193
- new_content, n_matches = self.search_re.subn(
194
- swap_version, self.content, re.MULTILINE
195
- )
196
-
197
- log.debug(
198
- "path=%r pattern=%r num_matches=%r", self.path, self.search_text, n_matches
199
- )
200
-
201
- return new_content
File without changes
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class VersionStampType(str, Enum):
7
+ """Enum for the type of version declaration"""
8
+
9
+ # The version is a number format, e.g. 1.2.3
10
+ NUMBER_FORMAT = "nf"
11
+
12
+ TAG_FORMAT = "tf"
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABCMeta, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING: # pragma: no cover
7
+ from pathlib import Path
8
+
9
+ from semantic_release.version.version import Version
10
+
11
+
12
+ class IVersionReplacer(metaclass=ABCMeta):
13
+ """
14
+ Interface for subclasses that replace a version string in a source file.
15
+
16
+ Methods generally have a base implementation are implemented here but
17
+ likely just provide a not-supported message but return gracefully
18
+
19
+ This class cannot be instantiated directly but must be inherited from
20
+ and implement the designated abstract methods.
21
+ """
22
+
23
+ @classmethod
24
+ def __subclasshook__(cls, subclass: type) -> bool:
25
+ # Validate that the subclass implements all of the abstract methods.
26
+ # This supports isinstance and issubclass checks.
27
+ return bool(
28
+ cls is IVersionReplacer
29
+ and all(
30
+ bool(hasattr(subclass, method) and callable(getattr(subclass, method)))
31
+ for method in IVersionReplacer.__abstractmethods__
32
+ )
33
+ )
34
+
35
+ @abstractmethod
36
+ def parse(self) -> set[Version]:
37
+ """
38
+ Return a set of the versions which can be parsed from the file.
39
+ Because a source can match in multiple places, this method returns a
40
+ set of matches. Generally, there should only be one element in this
41
+ set (i.e. even if the version is specified in multiple places, it
42
+ should be the same version in each place), but enforcing that condition
43
+ is not mandatory or expected.
44
+ """
45
+ raise NotImplementedError # pragma: no cover
46
+
47
+ @abstractmethod
48
+ def replace(self, new_version: Version) -> str:
49
+ """
50
+ Replace the version in the source content with `new_version`, and return
51
+ the updated content.
52
+
53
+ :param new_version: The new version number as a `Version` instance
54
+ """
55
+ raise NotImplementedError # pragma: no cover
56
+
57
+ @abstractmethod
58
+ def update_file_w_version(
59
+ self, new_version: Version, noop: bool = False
60
+ ) -> Path | None:
61
+ """
62
+ This method reads the underlying file, replaces each occurrence of the
63
+ matched pattern, then writes the updated file.
64
+
65
+ :param new_version: The new version number as a `Version` instance
66
+ """
67
+ raise NotImplementedError # pragma: no cover
@@ -0,0 +1,241 @@
1
+ from __future__ import annotations
2
+
3
+ from logging import getLogger
4
+ from pathlib import Path
5
+ from re import (
6
+ MULTILINE,
7
+ compile as regexp,
8
+ error as RegExpError, # noqa: N812
9
+ escape as regex_escape,
10
+ )
11
+ from typing import TYPE_CHECKING
12
+
13
+ from deprecated.sphinx import deprecated
14
+
15
+ from semantic_release.cli.util import noop_report
16
+ from semantic_release.const import SEMVER_REGEX
17
+ from semantic_release.version.declarations.enum import VersionStampType
18
+ from semantic_release.version.declarations.i_version_replacer import IVersionReplacer
19
+ from semantic_release.version.version import Version
20
+
21
+ if TYPE_CHECKING: # pragma: no cover
22
+ from re import Match
23
+
24
+
25
+ log = getLogger(__name__)
26
+
27
+
28
+ class VersionSwapper:
29
+ """Callable to replace a version number in a string with a new version number."""
30
+
31
+ def __init__(self, new_version_str: str, group_match_name: str) -> None:
32
+ self.version_str = new_version_str
33
+ self.group_match_name = group_match_name
34
+
35
+ def __call__(self, match: Match[str]) -> str:
36
+ i, j = match.span()
37
+ ii, jj = match.span(self.group_match_name)
38
+ return f"{match.string[i:ii]}{self.version_str}{match.string[jj:j]}"
39
+
40
+
41
+ class PatternVersionDeclaration(IVersionReplacer):
42
+ """
43
+ VersionDeclarationABC implementation representing a version number in a particular
44
+ file. The version number is identified by a regular expression, which should be
45
+ provided in `search_text`.
46
+ """
47
+
48
+ _VERSION_GROUP_NAME = "version"
49
+
50
+ def __init__(
51
+ self, path: Path | str, search_text: str, stamp_format: VersionStampType
52
+ ) -> None:
53
+ self._content: str | None = None
54
+ self._path = Path(path).resolve()
55
+ self._stamp_format = stamp_format
56
+
57
+ try:
58
+ self._search_pattern = regexp(search_text, flags=MULTILINE)
59
+ except RegExpError as err:
60
+ raise ValueError(
61
+ f"Invalid regular expression for search text: {search_text!r}"
62
+ ) from err
63
+
64
+ if self._VERSION_GROUP_NAME not in self._search_pattern.groupindex:
65
+ raise ValueError(
66
+ str.join(
67
+ " ",
68
+ [
69
+ f"Invalid search text {search_text!r}; must use",
70
+ f"'{self._VERSION_GROUP_NAME}' as a named group, for example",
71
+ f"(?P<{self._VERSION_GROUP_NAME}>...) . For more info on named",
72
+ "groups see https://docs.python.org/3/library/re.html",
73
+ ],
74
+ )
75
+ )
76
+
77
+ @property
78
+ def content(self) -> str:
79
+ """A cached property that stores the content of the configured source file."""
80
+ if self._content is None:
81
+ log.debug("No content stored, reading from source file %s", self._path)
82
+
83
+ if not self._path.exists():
84
+ raise FileNotFoundError(f"path {self._path!r} does not exist")
85
+
86
+ self._content = self._path.read_text()
87
+
88
+ return self._content
89
+
90
+ @content.deleter
91
+ def content(self) -> None:
92
+ self._content = None
93
+
94
+ @deprecated(
95
+ version="9.20.0",
96
+ reason="Function is unused and will be removed in a future release",
97
+ )
98
+ def parse(self) -> set[Version]: # pragma: no cover
99
+ """
100
+ Return the versions matching this pattern.
101
+ Because a pattern can match in multiple places, this method returns a
102
+ set of matches. Generally, there should only be one element in this
103
+ set (i.e. even if the version is specified in multiple places, it
104
+ should be the same version in each place), but it falls on the caller
105
+ to check for this condition.
106
+ """
107
+ versions = {
108
+ Version.parse(m.group(self._VERSION_GROUP_NAME))
109
+ for m in self._search_pattern.finditer(self.content)
110
+ }
111
+
112
+ log.debug(
113
+ "Parsing current version: path=%r pattern=%r num_matches=%s",
114
+ self._path.resolve(),
115
+ self._search_pattern,
116
+ len(versions),
117
+ )
118
+ return versions
119
+
120
+ def replace(self, new_version: Version) -> str:
121
+ """
122
+ Replace the version in the source content with `new_version`, and return
123
+ the updated content.
124
+
125
+ :param new_version: The new version number as a `Version` instance
126
+ """
127
+ new_content, n_matches = self._search_pattern.subn(
128
+ VersionSwapper(
129
+ new_version_str=(
130
+ new_version.as_tag()
131
+ if self._stamp_format == VersionStampType.TAG_FORMAT
132
+ else str(new_version)
133
+ ),
134
+ group_match_name=self._VERSION_GROUP_NAME,
135
+ ),
136
+ self.content,
137
+ )
138
+
139
+ log.debug(
140
+ "path=%r pattern=%r num_matches=%r",
141
+ self._path,
142
+ self._search_pattern,
143
+ n_matches,
144
+ )
145
+
146
+ return new_content
147
+
148
+ def update_file_w_version(
149
+ self, new_version: Version, noop: bool = False
150
+ ) -> Path | None:
151
+ if noop:
152
+ if not self._path.exists():
153
+ noop_report(
154
+ f"FILE NOT FOUND: cannot stamp version in non-existent file {self._path}",
155
+ )
156
+ return None
157
+
158
+ if len(self._search_pattern.findall(self.content)) < 1:
159
+ noop_report(
160
+ f"VERSION PATTERN NOT FOUND: no version to stamp in file {self._path}",
161
+ )
162
+ return None
163
+
164
+ return self._path
165
+
166
+ new_content = self.replace(new_version)
167
+ if new_content == self.content:
168
+ return None
169
+
170
+ self._path.write_text(new_content)
171
+ del self.content
172
+
173
+ return self._path
174
+
175
+ @classmethod
176
+ def from_string_definition(
177
+ cls, replacement_def: str, tag_format: str
178
+ ) -> PatternVersionDeclaration:
179
+ """
180
+ create an instance of self from a string representing one item
181
+ of the "version_variables" list in the configuration
182
+ """
183
+ parts = replacement_def.split(":", maxsplit=2)
184
+
185
+ if len(parts) <= 1:
186
+ raise ValueError(
187
+ f"Invalid replacement definition {replacement_def!r}, missing ':'"
188
+ )
189
+
190
+ if len(parts) == 2:
191
+ # apply default version_type of "number_format" (ie. "1.2.3")
192
+ parts = [*parts, VersionStampType.NUMBER_FORMAT.value]
193
+
194
+ path, variable, version_type = parts
195
+
196
+ try:
197
+ stamp_type = VersionStampType(version_type)
198
+ except ValueError as err:
199
+ raise ValueError(
200
+ str.join(
201
+ " ",
202
+ [
203
+ "Invalid stamp type, must be one of:",
204
+ str.join(", ", [e.value for e in VersionStampType]),
205
+ ],
206
+ )
207
+ ) from err
208
+
209
+ # DEFAULT: naked (no v-prefixed) semver version
210
+ value_replace_pattern_str = (
211
+ f"(?P<{cls._VERSION_GROUP_NAME}>{SEMVER_REGEX.pattern})"
212
+ )
213
+
214
+ if version_type == VersionStampType.TAG_FORMAT.value:
215
+ tag_parts = tag_format.strip().split(r"{version}", maxsplit=1)
216
+ value_replace_pattern_str = str.join(
217
+ "",
218
+ [
219
+ f"(?P<{cls._VERSION_GROUP_NAME}>",
220
+ regex_escape(tag_parts[0]),
221
+ SEMVER_REGEX.pattern,
222
+ (regex_escape(tag_parts[1]) if len(tag_parts) > 1 else ""),
223
+ ")",
224
+ ],
225
+ )
226
+
227
+ search_text = str.join(
228
+ "",
229
+ [
230
+ # Supports optional matching quotations around variable name
231
+ # Negative lookbehind to ensure we don't match part of a variable name
232
+ f"""(?x)(?P<quote1>['"])?(?<![\\w.-]){regex_escape(variable)}(?P=quote1)?""",
233
+ # Supports walrus, equals sign, colon, or @ as assignment operator
234
+ # ignoring whitespace separation
235
+ r"\s*(:=|[:=@])\s*",
236
+ # Supports optional matching quotations around a version pattern (tag or raw format)
237
+ f"""(?P<quote2>['"])?{value_replace_pattern_str}(?P=quote2)?""",
238
+ ],
239
+ )
240
+
241
+ return cls(path, search_text, stamp_type)
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from logging import getLogger
4
+ from pathlib import Path
5
+ from typing import Any, Dict, cast
6
+
7
+ import tomlkit
8
+ from deprecated.sphinx import deprecated
9
+ from dotty_dict import Dotty
10
+
11
+ from semantic_release.cli.util import noop_report
12
+ from semantic_release.version.declarations.enum import VersionStampType
13
+ from semantic_release.version.declarations.i_version_replacer import IVersionReplacer
14
+ from semantic_release.version.version import Version
15
+
16
+ # globals
17
+ log = getLogger(__name__)
18
+
19
+
20
+ class TomlVersionDeclaration(IVersionReplacer):
21
+ def __init__(
22
+ self, path: Path | str, search_text: str, stamp_format: VersionStampType
23
+ ) -> None:
24
+ self._content: str | None = None
25
+ self._path = Path(path).resolve()
26
+ self._stamp_format = stamp_format
27
+ self._search_text = search_text
28
+
29
+ @property
30
+ def content(self) -> str:
31
+ """A cached property that stores the content of the configured source file."""
32
+ if self._content is None:
33
+ log.debug("No content stored, reading from source file %s", self._path)
34
+
35
+ if not self._path.exists():
36
+ raise FileNotFoundError(f"path {self._path!r} does not exist")
37
+
38
+ self._content = self._path.read_text()
39
+
40
+ return self._content
41
+
42
+ @content.deleter
43
+ def content(self) -> None:
44
+ self._content = None
45
+
46
+ @deprecated(
47
+ version="9.20.0",
48
+ reason="Function is unused and will be removed in a future release",
49
+ )
50
+ def parse(self) -> set[Version]: # pragma: no cover
51
+ """Look for the version in the source content"""
52
+ content = self._load()
53
+ maybe_version: str = content.get(self._search_text) # type: ignore[return-value]
54
+ if maybe_version is not None:
55
+ log.debug(
56
+ "Found a key %r that looks like a version (%r)",
57
+ self._search_text,
58
+ maybe_version,
59
+ )
60
+ valid_version = Version.parse(maybe_version)
61
+ return {valid_version} if valid_version else set()
62
+ # Maybe in future raise error if not found?
63
+ return set()
64
+
65
+ def replace(self, new_version: Version) -> str:
66
+ """
67
+ Replace the version in the source content with `new_version`, and return the
68
+ updated content.
69
+ """
70
+ content = self._load()
71
+ if self._search_text in content:
72
+ log.info(
73
+ "found %r in source file contents, replacing with %s",
74
+ self._search_text,
75
+ new_version,
76
+ )
77
+ content[self._search_text] = (
78
+ new_version.as_tag()
79
+ if self._stamp_format == VersionStampType.TAG_FORMAT
80
+ else str(new_version)
81
+ )
82
+
83
+ return tomlkit.dumps(cast(Dict[str, Any], content))
84
+
85
+ def _load(self) -> Dotty:
86
+ """Load the content of the source file into a Dotty for easier searching"""
87
+ return Dotty(tomlkit.loads(self.content))
88
+
89
+ def update_file_w_version(
90
+ self, new_version: Version, noop: bool = False
91
+ ) -> Path | None:
92
+ if noop:
93
+ if not self._path.exists():
94
+ noop_report(
95
+ f"FILE NOT FOUND: cannot stamp version in non-existent file {self._path!r}",
96
+ )
97
+ return None
98
+
99
+ if self._search_text not in self._load():
100
+ noop_report(
101
+ f"VERSION PATTERN NOT FOUND: no version to stamp in file {self._path!r}",
102
+ )
103
+ return None
104
+
105
+ return self._path
106
+
107
+ new_content = self.replace(new_version)
108
+ if new_content == self.content:
109
+ return None
110
+
111
+ self._path.write_text(new_content)
112
+ del self.content
113
+
114
+ return self._path
115
+
116
+ @classmethod
117
+ def from_string_definition(cls, replacement_def: str) -> TomlVersionDeclaration:
118
+ """
119
+ create an instance of self from a string representing one item
120
+ of the "version_toml" list in the configuration
121
+ """
122
+ parts = replacement_def.split(":", maxsplit=2)
123
+
124
+ if len(parts) <= 1:
125
+ raise ValueError(
126
+ f"Invalid TOML replacement definition {replacement_def!r}, missing ':'"
127
+ )
128
+
129
+ if len(parts) == 2:
130
+ # apply default version_type of "number_format" (ie. "1.2.3")
131
+ parts = [*parts, VersionStampType.NUMBER_FORMAT.value]
132
+
133
+ path, search_text, version_type = parts
134
+
135
+ try:
136
+ stamp_type = VersionStampType(version_type)
137
+ except ValueError as err:
138
+ raise ValueError(
139
+ str.join(
140
+ " ",
141
+ [
142
+ "Invalid stamp type, must be one of:",
143
+ str.join(", ", [e.value for e in VersionStampType]),
144
+ ],
145
+ )
146
+ ) from err
147
+
148
+ return cls(path, search_text, stamp_type)