python-semantic-release 9.5.0__py3-none-any.whl → 9.6.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 (26) hide show
  1. {python_semantic_release-9.5.0.dist-info → python_semantic_release-9.6.0.dist-info}/METADATA +1 -1
  2. {python_semantic_release-9.5.0.dist-info → python_semantic_release-9.6.0.dist-info}/RECORD +26 -25
  3. semantic_release/__init__.py +1 -1
  4. semantic_release/changelog/context.py +3 -1
  5. semantic_release/cli/commands/changelog.py +2 -1
  6. semantic_release/cli/commands/publish.py +8 -0
  7. semantic_release/cli/commands/version.py +79 -40
  8. semantic_release/cli/config.py +94 -19
  9. semantic_release/commit_parser/_base.py +13 -4
  10. semantic_release/commit_parser/angular.py +7 -2
  11. semantic_release/commit_parser/emoji.py +5 -0
  12. semantic_release/commit_parser/scipy.py +12 -13
  13. semantic_release/commit_parser/tag.py +5 -0
  14. semantic_release/errors.py +22 -1
  15. semantic_release/hvcs/__init__.py +10 -1
  16. semantic_release/hvcs/_base.py +32 -109
  17. semantic_release/hvcs/bitbucket.py +87 -140
  18. semantic_release/hvcs/gitea.py +61 -71
  19. semantic_release/hvcs/github.py +120 -156
  20. semantic_release/hvcs/gitlab.py +34 -76
  21. semantic_release/hvcs/remote_hvcs_base.py +193 -0
  22. {python_semantic_release-9.5.0.dist-info → python_semantic_release-9.6.0.dist-info}/AUTHORS.rst +0 -0
  23. {python_semantic_release-9.5.0.dist-info → python_semantic_release-9.6.0.dist-info}/LICENSE +0 -0
  24. {python_semantic_release-9.5.0.dist-info → python_semantic_release-9.6.0.dist-info}/WHEEL +0 -0
  25. {python_semantic_release-9.5.0.dist-info → python_semantic_release-9.6.0.dist-info}/entry_points.txt +0 -0
  26. {python_semantic_release-9.5.0.dist-info → python_semantic_release-9.6.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.5.0
3
+ Version: 9.6.0
4
4
  Summary: Automatic Semantic Versioning for Python projects
5
5
  Author-email: Rolf Erik Lekang <me@rolflekang.com>
6
6
  License: MIT
@@ -1,43 +1,44 @@
1
- semantic_release/__init__.py,sha256=OKGFjDLUMtF4yJFzPOCIxLjmnv_T7QJWrmqcCinoIDA,870
1
+ semantic_release/__init__.py,sha256=obnrcYcFeW96PuTVl8urJfNUD4ZZ3brgq6ORznMIPs4,870
2
2
  semantic_release/__main__.py,sha256=vXOARLV7KHeVXgpOMU0Q-8wovpdsOrtTsycxulI5xIE,106
3
3
  semantic_release/const.py,sha256=Z1o2QNh60wSLeF-_1TemMBjU3ZXbV0XghnUFsbTVfOs,831
4
4
  semantic_release/enums.py,sha256=D5B_reQGGKQQT22HO5PUtvn2Bok3fkht6TfJtXkmAUg,1020
5
- semantic_release/errors.py,sha256=070p5fRe4l2BYv1zbTZjnM5YwRcJmi9BVa8TdMFzvf0,1159
5
+ semantic_release/errors.py,sha256=2hrKuCRWN5O_0z-C2Owf6gcTUe2MpP4Y27nHoUzJBng,1695
6
6
  semantic_release/helpers.py,sha256=d1jOX0SNyqPc_3wr14xR25FfpqhMd4Ev7MNBOWlScc0,5581
7
7
  semantic_release/changelog/__init__.py,sha256=Bg6Xe5Vt32rWoMscW-hd4sUwiZqzWmsg4CD1EhMesMY,262
8
- semantic_release/changelog/context.py,sha256=ItSz5Bon2mcJkLxyQZzBTukN0zEdcaVQ4iXvz3jGCLc,986
8
+ semantic_release/changelog/context.py,sha256=YAtv_svSbRsRUqHfTbmfJRpE5xkOEUxmYaBINYJyXLE,1051
9
9
  semantic_release/changelog/release_history.py,sha256=ShzZpxTjhxka6TMbzOzYzG3EuCH8j3xjUQzll-FKcCo,6675
10
10
  semantic_release/changelog/template.py,sha256=JqZcjdozM9-059Sa8PfiHxYNeyZFa0jxk_beyRLv5sU,4640
11
11
  semantic_release/cli/__init__.py,sha256=gwAYhZO-4TR0tKCkQ4GqutWsL--0CII2_dcbrElI4M4,419
12
12
  semantic_release/cli/common.py,sha256=cVQUI7Uaac_nJ7VEXuJ7Q9pVJpuMVRvE7bBl_i96g20,1439
13
- semantic_release/cli/config.py,sha256=9NhaxZ1F1ux-8Qu7_KeE7kLH3eCsllkPV4Hy5YI1tv4,17574
13
+ semantic_release/cli/config.py,sha256=1q7tNFC4Re8gcctZyIGSKcj6qZ59W5mx9ed2yk5bgSA,20389
14
14
  semantic_release/cli/const.py,sha256=S8oNNpSaKdZxcAs7PAKEjQHic0czoJBGd7XHo57FOzw,39
15
15
  semantic_release/cli/github_actions_output.py,sha256=Wy4jEIpSFgoMyJqq4FD7scIpgovivutFLZ7Fs4y2CUY,2110
16
16
  semantic_release/cli/masking_filter.py,sha256=DxqjiJyABlzwwwZ1r8JGQpb6QrF00StJFm0-2-s5Fv0,3071
17
17
  semantic_release/cli/util.py,sha256=FyXaBkeL7nXKjy3X9rQLEwvn7p46xPekp2V8Z-5MVrk,3755
18
18
  semantic_release/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- semantic_release/cli/commands/changelog.py,sha256=Tk9DprnY9mDQXSE1krQd8oILn3W6dKN_stWx9EbLjhU,4087
19
+ semantic_release/cli/commands/changelog.py,sha256=rNZwWucEbR2BjbmgHQa9R1nJk5Fu0Ph3U3wh-daH42Q,4197
20
20
  semantic_release/cli/commands/cli_context.py,sha256=fOLTUKb4UnZGQ6zVhftuvdfZ7fK7bDDIvj7Ud5yU98c,3443
21
21
  semantic_release/cli/commands/generate_config.py,sha256=2xZOu3NpyhBp0pWr7d8ugKl_kjqQgpSsSMHq5wHTfrE,1699
22
22
  semantic_release/cli/commands/main.py,sha256=LuL-O0LGonI5fQTZn0AUtYv2Ei_CYyNqYHMF_wN4InI,2900
23
- semantic_release/cli/commands/publish.py,sha256=_pz7rKx65tCH7J2uyWiBghQ61MmmIbxhF9bE4AkE0N8,1703
24
- semantic_release/cli/commands/version.py,sha256=H_I4AmZzBQdMzYejRKgCyvMtx55omzpSHJFWMz-uQRc,23015
23
+ semantic_release/cli/commands/publish.py,sha256=ZA7DuEGxZdIT9wUwP1S24qi1RdcKV-1FnYci-PvyXIg,1958
24
+ semantic_release/cli/commands/version.py,sha256=08LLMJctxlegAzo88NXlhIqcFuKYOI3b9Psw8NqB-oU,24488
25
25
  semantic_release/commit_parser/__init__.py,sha256=cv5HFBdw7OJd4Laj4Ex8ZZ5Tml8GwXgQcXW6Pasr2Ao,615
26
- semantic_release/commit_parser/_base.py,sha256=aDS0dhFosT5i11tA5Ez8BTeikk6jwXjSEpJnJC5D2cs,2609
27
- semantic_release/commit_parser/angular.py,sha256=bS7Wrzpg_6zFil5Y4GIZkeGDPhvWQdTlFdWTtGVcpp0,4393
28
- semantic_release/commit_parser/emoji.py,sha256=6cFnqpvtjtxlwDYc8oB9y41RPqFGGD8PbqiAA53SVac,3289
29
- semantic_release/commit_parser/scipy.py,sha256=AaAtQYUsG_Y29YIgDVISl5mljBCkG8zyzUkn_3HYGeM,5777
30
- semantic_release/commit_parser/tag.py,sha256=0Y8GTRclyoLMhyDRrcMU8JKTM4lsbfpJbjqZ7qz5ifE,3194
26
+ semantic_release/commit_parser/_base.py,sha256=D2dY8dALV3QPK0XOLZru9btMW5dlx9YSmDIe3v5T0QA,3004
27
+ semantic_release/commit_parser/angular.py,sha256=ERWeTBTFNIBOaTIopY4Vff0KbeFwrSQWbADe3smVW-8,4579
28
+ semantic_release/commit_parser/emoji.py,sha256=M6zgqsXqegLK5FxZGyfsOPM3vOsJkPKVAjnGATOhwa4,3452
29
+ semantic_release/commit_parser/scipy.py,sha256=uRNLSXH4LApxm8xSyUil23WVO3C8VEFRCnwTIKZbyQk,5879
30
+ semantic_release/commit_parser/tag.py,sha256=SAop9INQvKOwktNmVypKFngd6xHmY_XZrLUWChUjz84,3353
31
31
  semantic_release/commit_parser/token.py,sha256=UxYAwEylPvp4x39RqNA9ITcgkaSRDEH7A5XDDJ3tdiM,1505
32
32
  semantic_release/commit_parser/util.py,sha256=vLcVDErZrExM55jMffos0hyMbNVQoJ-PeeVDG1Ej51I,730
33
33
  semantic_release/data/templates/CHANGELOG.md.j2,sha256=GB3YJwgVrhlcb-g5ESdEh_Gsysb_Texxv-Ip2LjulC0,1066
34
34
  semantic_release/data/templates/release_notes.md.j2,sha256=27uIHgG6iYmqdMaoWFP5LJRZcrO-3paqFzHHHsGpXXE,465
35
- semantic_release/hvcs/__init__.py,sha256=ulesZslSG4Lu8Iz3YzOrPXpdgvgbADq_MV17B-bSYQA,379
36
- semantic_release/hvcs/_base.py,sha256=qjjlU3rI-sOhIY_FfXsZ9OQGthNYxgzpL4PhlGSXEuA,5232
37
- semantic_release/hvcs/bitbucket.py,sha256=6vGOcRG7WexxG8SzLcWDIzp7zNpB0ruDLUTQ9GE2jGM,11004
38
- semantic_release/hvcs/gitea.py,sha256=OTThBTZ93xUP9am7tNIE0UVchhJwG9ASvI6zuiG8oLE,11450
39
- semantic_release/hvcs/github.py,sha256=DI0fK_dhVAzwd6S80zZMMVIAsz-b0zTGca-SMVJIUyE,19965
40
- semantic_release/hvcs/gitlab.py,sha256=qmowL22DCNOFa7RwmMTmNmno3BS0dyNYT9L36ujgZ8A,7409
35
+ semantic_release/hvcs/__init__.py,sha256=JwoaLOF-12L-OBo_9-tOXXhdiHKeVungA9865to2oZk,494
36
+ semantic_release/hvcs/_base.py,sha256=9-iTqTPSbiEevKbCBP9K2hq4c-2T4wPbeLWe-kAxBzo,2607
37
+ semantic_release/hvcs/bitbucket.py,sha256=tMpl69CbZSQ8cdgiqtIzf7WSz8OQwDInGpoq-9tQr3k,9216
38
+ semantic_release/hvcs/gitea.py,sha256=CSItRSJU39hohO3oE-ohX65Er_s2nVrDqvfc7KQtmv8,11152
39
+ semantic_release/hvcs/github.py,sha256=34ZMGttmqcTcY6_D-SRn-HxUJxNT4WwBNEwYlsHC420,18287
40
+ semantic_release/hvcs/gitlab.py,sha256=JtHWBaNGSMB8MTiznEhvuMm_M8lLceZPaGndzOx39Vk,6249
41
+ semantic_release/hvcs/remote_hvcs_base.py,sha256=plZkc5cSaiTxgIR5HhlEWrYoyWzzSXr-Jw3aXbMK_iA,6055
41
42
  semantic_release/hvcs/token_auth.py,sha256=ZjT56-NIPB4OKIt1qwHCu1TavXnrWFIBl9ARlg56hgU,663
42
43
  semantic_release/hvcs/util.py,sha256=PNKXApgL2np9lpnKjAbo77toRgElLPXrbS1q7621waI,2886
43
44
  semantic_release/version/__init__.py,sha256=CLhtGQry9dLIij5XyRa9ZevxU_1p8tjMTSQ-K_GMpWM,270
@@ -45,10 +46,10 @@ semantic_release/version/algorithm.py,sha256=-9ulTy1z4u-jUvbc-FrvEdFL7Omc3O3HhaP
45
46
  semantic_release/version/declaration.py,sha256=f6Ld7hIhrqvDrRBapJHr-KDimuyo-4IG8009Zu9BIgU,7357
46
47
  semantic_release/version/translator.py,sha256=P1noIsVBn8u6zNOFjG0xKYOWapxqf_PHSMvMeLJ9kXg,3050
47
48
  semantic_release/version/version.py,sha256=6PCtSbLP88U1daoxnCwHc--YguZo4waGNLqJ5JfeczE,14175
48
- python_semantic_release-9.5.0.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
49
- python_semantic_release-9.5.0.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
50
- python_semantic_release-9.5.0.dist-info/METADATA,sha256=7RR8GcLU8LGCvbkMVyzcxDjom5Fz8xP4lxLMDi8rHLM,5335
51
- python_semantic_release-9.5.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
52
- python_semantic_release-9.5.0.dist-info/entry_points.txt,sha256=qojUBx_Ywf4u7KgP6NWlDaBDymZVUGQ7m5wF2duC2F0,95
53
- python_semantic_release-9.5.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
54
- python_semantic_release-9.5.0.dist-info/RECORD,,
49
+ python_semantic_release-9.6.0.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
50
+ python_semantic_release-9.6.0.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
51
+ python_semantic_release-9.6.0.dist-info/METADATA,sha256=7s6FI6zlP-PBF4hz2SHNEioGTw8TYT3tBzcsYH1gZEw,5335
52
+ python_semantic_release-9.6.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
53
+ python_semantic_release-9.6.0.dist-info/entry_points.txt,sha256=qojUBx_Ywf4u7KgP6NWlDaBDymZVUGQ7m5wF2duC2F0,95
54
+ python_semantic_release-9.6.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
55
+ python_semantic_release-9.6.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.5.0"
27
+ __version__ = "9.6.0"
28
28
 
29
29
 
30
30
  def setup_hook(argv: list[str]) -> None:
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
14
14
  class ChangelogContext:
15
15
  repo_name: str
16
16
  repo_owner: str
17
+ hvcs_type: str
17
18
  history: ReleaseHistory
18
19
  filters: tuple[Callable[..., Any], ...] = ()
19
20
 
@@ -31,5 +32,6 @@ def make_changelog_context(
31
32
  repo_name=hvcs_client.repo_name,
32
33
  repo_owner=hvcs_client.owner,
33
34
  history=release_history,
34
- filters=(hvcs_client.pull_request_url, hvcs_client.commit_hash_url),
35
+ hvcs_type=hvcs_client.__class__.__name__.lower(),
36
+ filters=(*hvcs_client.get_changelog_context_filters(),),
35
37
  )
@@ -14,6 +14,7 @@ from semantic_release.cli.common import (
14
14
  render_release_notes,
15
15
  )
16
16
  from semantic_release.cli.util import noop_report
17
+ from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
17
18
 
18
19
  if TYPE_CHECKING:
19
20
  from semantic_release.cli.commands.cli_context import CliContextObj
@@ -79,7 +80,7 @@ def changelog(cli_ctx: CliContextObj, release_tag: str | None = None) -> None:
79
80
  else:
80
81
  recursive_render(template_dir, environment=env, _root_dir=repo.working_dir)
81
82
 
82
- if release_tag:
83
+ if release_tag and isinstance(hvcs_client, RemoteHvcsBase):
83
84
  if runtime.global_cli_options.noop:
84
85
  noop_report(
85
86
  f"would have posted changelog to the release for tag {release_tag}"
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
6
6
  import click
7
7
 
8
8
  from semantic_release.cli.util import noop_report
9
+ from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
9
10
  from semantic_release.version import tags_and_versions
10
11
 
11
12
  if TYPE_CHECKING:
@@ -45,6 +46,13 @@ def publish(cli_ctx: CliContextObj, tag: str = "latest") -> None:
45
46
  f"No tags found with format {translator.tag_format!r}, couldn't "
46
47
  "identify latest version"
47
48
  )
49
+
50
+ if not isinstance(hvcs_client, RemoteHvcsBase):
51
+ log.info(
52
+ "Remote does not support artifact upload. Exiting with no action taken..."
53
+ )
54
+ ctx.exit(0)
55
+
48
56
  if runtime.global_cli_options.noop:
49
57
  noop_report(
50
58
  "would have uploaded files matching any of the globs "
@@ -25,6 +25,7 @@ from semantic_release.cli.util import indented, noop_report, rprint
25
25
  from semantic_release.const import DEFAULT_SHELL, DEFAULT_VERSION
26
26
  from semantic_release.enums import LevelBump
27
27
  from semantic_release.errors import UnexpectedResponse
28
+ from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
28
29
  from semantic_release.version import Version, next_version, tags_and_versions
29
30
 
30
31
  log = logging.getLogger(__name__)
@@ -41,7 +42,7 @@ if TYPE_CHECKING: # pragma: no cover
41
42
 
42
43
 
43
44
  def is_forced_prerelease(
44
- force_prerelease: bool, force_level: str | None, prerelease: bool
45
+ as_prerelease: bool, forced_level_bump: LevelBump | None, prerelease: bool
45
46
  ) -> bool:
46
47
  """
47
48
  Determine if this release is forced to have prerelease on/off.
@@ -50,8 +51,17 @@ def is_forced_prerelease(
50
51
  it's False.
51
52
  Otherwise (``force_level is None``) use the value of ``prerelease``
52
53
  """
53
- log.debug(", ".join(f"{k} = {v}" for k, v in locals().items()))
54
- return force_prerelease or ((force_level is None) and prerelease)
54
+ local_vars = list(locals().items())
55
+ log.debug(
56
+ "%s: %s",
57
+ is_forced_prerelease.__name__,
58
+ ", ".join(f"{k} = {v}" for k, v in local_vars),
59
+ )
60
+ return (
61
+ as_prerelease
62
+ or forced_level_bump is LevelBump.PRERELEASE_REVISION
63
+ or ((forced_level_bump is None) and prerelease)
64
+ )
55
65
 
56
66
 
57
67
  def last_released(
@@ -62,16 +72,41 @@ def last_released(
62
72
 
63
73
 
64
74
  def version_from_forced_level(
65
- repo: Repo, level_bump: LevelBump, translator: VersionTranslator
75
+ repo: Repo, forced_level_bump: LevelBump, translator: VersionTranslator
66
76
  ) -> Version:
67
77
  ts_and_vs = tags_and_versions(repo.tags, translator)
68
78
 
69
79
  # If we have no tags, return the default version
70
80
  if not ts_and_vs:
71
- return Version.parse(DEFAULT_VERSION).bump(level_bump)
81
+ return Version.parse(DEFAULT_VERSION).bump(forced_level_bump)
72
82
 
73
83
  _, latest_version = ts_and_vs[0]
74
- return latest_version.bump(level_bump)
84
+ if forced_level_bump is not LevelBump.PRERELEASE_REVISION:
85
+ return latest_version.bump(forced_level_bump)
86
+
87
+ # We need to find the latest version with the prerelease token
88
+ # we're looking for, and return that version + an increment to
89
+ # the prerelease revision.
90
+
91
+ # NOTE this can probably be cleaned up.
92
+ # ts_and_vs are in order, so check if we're looking at prereleases
93
+ # for the same (major, minor, patch) as the latest version.
94
+ # If we are, we can increment the revision and we're done. If
95
+ # we don't find a prerelease targeting this version with the same
96
+ # token as the one we're looking to prerelease, we can use revision 1.
97
+ for _, version in ts_and_vs:
98
+ if not (
99
+ version.major == latest_version.major
100
+ and version.minor == latest_version.minor
101
+ and version.patch == latest_version.patch
102
+ ):
103
+ break
104
+ if (
105
+ version.is_prerelease
106
+ and version.prerelease_token == translator.prerelease_token
107
+ ):
108
+ return version.bump(LevelBump.PRERELEASE_REVISION)
109
+ return latest_version.to_prerelease(token=translator.prerelease_token, revision=1)
75
110
 
76
111
 
77
112
  def apply_version_to_source_files(
@@ -145,10 +180,10 @@ def shell(cmd: str, *, check: bool = True) -> subprocess.CompletedProcess:
145
180
  help="Print the last released version tag and exit",
146
181
  )
147
182
  @click.option(
148
- "--prerelease",
149
- "force_prerelease",
183
+ "--as-prerelease",
184
+ "as_prerelease",
150
185
  is_flag=True,
151
- help="Force the next version to be a prerelease",
186
+ help="Ensure the next version to be released is a prerelease version",
152
187
  )
153
188
  @click.option(
154
189
  "--prerelease-token",
@@ -160,19 +195,25 @@ def shell(cmd: str, *, check: bool = True) -> subprocess.CompletedProcess:
160
195
  "--major",
161
196
  "force_level",
162
197
  flag_value="major",
163
- help="force the next version to be a major release",
198
+ help="Force the next version to be a major release",
164
199
  )
165
200
  @click.option(
166
201
  "--minor",
167
202
  "force_level",
168
203
  flag_value="minor",
169
- help="force the next version to be a minor release",
204
+ help="Force the next version to be a minor release",
170
205
  )
171
206
  @click.option(
172
207
  "--patch",
173
208
  "force_level",
174
209
  flag_value="patch",
175
- help="force the next version to be a patch release",
210
+ help="Force the next version to be a patch release",
211
+ )
212
+ @click.option(
213
+ "--prerelease",
214
+ "force_level",
215
+ flag_value="prerelease_revision",
216
+ help="Force the next version to be a prerelease",
176
217
  )
177
218
  @click.option(
178
219
  "--commit/--no-commit",
@@ -224,7 +265,7 @@ def version( # noqa: C901
224
265
  print_only_tag: bool = False,
225
266
  print_last_released: bool = False,
226
267
  print_last_released_tag: bool = False,
227
- force_prerelease: bool = False,
268
+ as_prerelease: bool = False,
228
269
  prerelease_token: str | None = None,
229
270
  force_level: str | None = None,
230
271
  commit_changes: bool = True,
@@ -271,9 +312,10 @@ def version( # noqa: C901
271
312
  ctx.exit(0)
272
313
 
273
314
  parser = runtime.commit_parser
315
+ forced_level_bump = None if not force_level else LevelBump.from_string(force_level)
274
316
  prerelease = is_forced_prerelease(
275
- force_prerelease=force_prerelease,
276
- force_level=force_level,
317
+ as_prerelease=as_prerelease,
318
+ forced_level_bump=forced_level_bump,
277
319
  prerelease=runtime.prerelease,
278
320
  )
279
321
  hvcs_client = runtime.hvcs_client
@@ -306,23 +348,19 @@ def version( # noqa: C901
306
348
  log.info("No vcs release will be created because pushing changes is disabled")
307
349
  make_vcs_release &= push_changes
308
350
 
309
- if force_prerelease:
310
- log.warning("Forcing prerelease due to '--prerelease' command-line flag")
311
- elif force_level:
351
+ if forced_level_bump:
312
352
  log.warning(
313
- "Forcing prerelease=False due to '--%s' command-line flag and no "
314
- "'--prerelease' flag",
353
+ "Forcing a '%s' release due to '--%s' command-line flag",
315
354
  force_level,
316
- )
317
-
318
- if force_level:
319
- level_bump = LevelBump.from_string(force_level)
320
- log.warning(
321
- "Forcing a %s level bump due to '--force' command-line option", force_level
355
+ (
356
+ force_level
357
+ if forced_level_bump is not LevelBump.PRERELEASE_REVISION
358
+ else "prerelease"
359
+ ),
322
360
  )
323
361
 
324
362
  new_version = version_from_forced_level(
325
- repo=repo, level_bump=level_bump, translator=translator
363
+ repo=repo, forced_level_bump=forced_level_bump, translator=translator
326
364
  )
327
365
 
328
366
  # We only turn the forced version into a prerelease if the user has specified
@@ -347,6 +385,17 @@ def version( # noqa: C901
347
385
  if build_metadata:
348
386
  new_version.build_metadata = build_metadata
349
387
 
388
+ if as_prerelease:
389
+ before_conversion, new_version = (
390
+ new_version,
391
+ new_version.to_prerelease(token=translator.prerelease_token),
392
+ )
393
+ log.info(
394
+ "Converting %s to %s due to '--as-prerelease' command-line option",
395
+ before_conversion,
396
+ new_version,
397
+ )
398
+
350
399
  gha_output.released = False
351
400
  gha_output.version = new_version
352
401
  ctx.call_on_close(gha_output.write_if_possible)
@@ -601,7 +650,7 @@ def version( # noqa: C901
601
650
 
602
651
  gha_output.released = True
603
652
 
604
- if make_vcs_release:
653
+ if make_vcs_release and isinstance(hvcs_client, RemoteHvcsBase):
605
654
  if opts.noop:
606
655
  noop_report(
607
656
  f"would have created a release for the tag {new_version.as_tag()!r}"
@@ -630,10 +679,11 @@ def version( # noqa: C901
630
679
  noop_report(f"would have uploaded the following assets: {runtime.assets}")
631
680
  else:
632
681
  try:
633
- release_id = hvcs_client.create_or_update_release(
682
+ hvcs_client.create_release(
634
683
  tag=new_version.as_tag(),
635
684
  release_notes=release_notes,
636
685
  prerelease=new_version.is_prerelease,
686
+ assets=assets,
637
687
  )
638
688
  except HTTPError as err:
639
689
  log.exception(err)
@@ -654,15 +704,4 @@ def version( # noqa: C901
654
704
  log.exception(e)
655
705
  ctx.fail(str(e))
656
706
 
657
- for asset in assets:
658
- log.info("Uploading asset %s", asset)
659
- try:
660
- hvcs_client.upload_asset(release_id, asset)
661
- except HTTPError as err:
662
- log.exception(err)
663
- ctx.fail(str.join("\n", [str(err), "Failed to upload asset!"]))
664
- except Exception as e:
665
- log.exception(e)
666
- ctx.fail(str(e))
667
-
668
707
  return str(new_version)
@@ -39,8 +39,13 @@ from semantic_release.commit_parser import (
39
39
  TagCommitParser,
40
40
  )
41
41
  from semantic_release.const import COMMIT_MESSAGE, DEFAULT_COMMIT_AUTHOR, SEMVER_REGEX
42
- from semantic_release.errors import InvalidConfiguration, NotAReleaseBranch
42
+ from semantic_release.errors import (
43
+ InvalidConfiguration,
44
+ NotAReleaseBranch,
45
+ ParserLoadError,
46
+ )
43
47
  from semantic_release.helpers import dynamic_import
48
+ from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
44
49
  from semantic_release.version import VersionTranslator
45
50
  from semantic_release.version.declaration import (
46
51
  PatternVersionDeclaration,
@@ -141,13 +146,22 @@ class RemoteConfig(BaseModel):
141
146
  def set_default_token(self) -> Self:
142
147
  # Set the default token name for the given VCS when no user input is given
143
148
  if not self.token and self.type in _known_hvcs:
144
- default_token_name = _known_hvcs[self.type].DEFAULT_ENV_TOKEN_NAME
145
- if default_token_name:
146
- env_token = EnvConfigVar(env=default_token_name).getvalue()
147
- if env_token:
148
- self.token = env_token
149
+ if env_token := self._get_default_token():
150
+ self.token = env_token
149
151
  return self
150
152
 
153
+ def _get_default_token(self) -> str | None:
154
+ hvcs_client_class = _known_hvcs[self.type]
155
+ default_hvcs_instance = hvcs_client_class("git@example.com:owner/project.git")
156
+ if not isinstance(default_hvcs_instance, RemoteHvcsBase):
157
+ return None
158
+
159
+ default_token_name = default_hvcs_instance.DEFAULT_ENV_TOKEN_NAME
160
+ if not default_token_name:
161
+ return None
162
+
163
+ return EnvConfigVar(env=default_token_name).getvalue()
164
+
151
165
  @model_validator(mode="after")
152
166
  def check_url_scheme(self) -> Self:
153
167
  if self.url and isinstance(self.url, str):
@@ -222,18 +236,50 @@ class RawConfig(BaseModel):
222
236
  parser_opts_type = None
223
237
  # If the commit parser is a known one, pull the default options object from it
224
238
  if self.commit_parser in _known_commit_parsers:
239
+ # TODO: BREAKING CHANGE v10
240
+ # parser_opts_type = (
241
+ # _known_commit_parsers[self.commit_parser]
242
+ # .get_default_options()
243
+ # .__class__
244
+ # )
225
245
  parser_opts_type = _known_commit_parsers[
226
246
  self.commit_parser
227
247
  ].parser_options
228
248
  else:
229
- # if its a custom parser, try to import it and pull the default options object type
230
- custom_class = dynamic_import(self.commit_parser)
231
- if hasattr(custom_class, "parser_options"):
232
- parser_opts_type = custom_class.parser_options
249
+ try:
250
+ # if its a custom parser, try to import it and pull the default options object type
251
+ custom_class = dynamic_import(self.commit_parser)
252
+ # TODO: BREAKING CHANGE v10
253
+ # parser_opts_type = custom_class.get_default_options().__class__
254
+ if hasattr(custom_class, "parser_options"):
255
+ parser_opts_type = custom_class.parser_options
256
+
257
+ except ModuleNotFoundError as err:
258
+ raise ParserLoadError(
259
+ str.join(
260
+ "\n",
261
+ [
262
+ str(err),
263
+ "Unable to import your custom parser! Check your configuration!",
264
+ ],
265
+ )
266
+ ) from err
267
+
268
+ except AttributeError as err:
269
+ raise ParserLoadError(
270
+ str.join(
271
+ "\n",
272
+ [
273
+ str(err),
274
+ "Unable to find your custom parser class inside the given module.",
275
+ "Check your configuration!",
276
+ ],
277
+ )
278
+ ) from err
233
279
 
234
280
  # from either the custom opts class or the known parser opts class, create an instance
235
281
  if callable(parser_opts_type):
236
- opts_obj = parser_opts_type()
282
+ opts_obj: Any = parser_opts_type()
237
283
  # if the opts object is a dataclass, wrap it in a RootModel so it can be transformed to a Mapping
238
284
  opts_obj = (
239
285
  opts_obj if not is_dataclass(opts_obj) else RootModel(opts_obj)
@@ -373,15 +419,44 @@ class RuntimeContext:
373
419
  branch_config = cls.select_branch_options(raw.branches, active_branch)
374
420
 
375
421
  # commit_parser
376
- commit_parser_cls = (
377
- _known_commit_parsers[raw.commit_parser]
378
- if raw.commit_parser in _known_commit_parsers
379
- else dynamic_import(raw.commit_parser)
380
- )
422
+ try:
423
+ commit_parser_cls = (
424
+ _known_commit_parsers[raw.commit_parser]
425
+ if raw.commit_parser in _known_commit_parsers
426
+ else dynamic_import(raw.commit_parser)
427
+ )
428
+ except ModuleNotFoundError as err:
429
+ raise ParserLoadError(
430
+ str.join(
431
+ "\n",
432
+ [
433
+ str(err),
434
+ "Unable to import your custom parser! Check your configuration!",
435
+ ],
436
+ )
437
+ ) from err
438
+ except AttributeError as err:
439
+ raise ParserLoadError(
440
+ str.join(
441
+ "\n",
442
+ [
443
+ str(err),
444
+ "Unable to find the parser class inside the given module",
445
+ ],
446
+ )
447
+ ) from err
381
448
 
382
- commit_parser = commit_parser_cls(
383
- options=commit_parser_cls.parser_options(**raw.commit_parser_options)
384
- )
449
+ commit_parser_opts_class = commit_parser_cls.parser_options
450
+ # TODO: Breaking change v10
451
+ # commit_parser_opts_class = commit_parser_cls.get_default_options().__class__
452
+ try:
453
+ commit_parser = commit_parser_cls(
454
+ options=commit_parser_opts_class(**raw.commit_parser_options)
455
+ )
456
+ except TypeError as err:
457
+ raise ParserLoadError(
458
+ str.join("\n", [str(err), f"Failed to initialize {raw.commit_parser}"])
459
+ ) from err
385
460
 
386
461
  # We always exclude PSR's own release commits from the Changelog
387
462
  # when parsing commits
@@ -65,10 +65,19 @@ class CommitParser(ABC, Generic[_TT, _OPTS]):
65
65
  ...
66
66
  """
67
67
 
68
- parser_options: type[_OPTS]
69
-
70
- def __init__(self, options: _OPTS) -> None:
71
- self.options = options
68
+ # TODO: Deprecate in lieu of get_default_options()
69
+ parser_options: type[ParserOptions] = ParserOptions
70
+
71
+ def __init__(self, options: _OPTS | None = None) -> None:
72
+ self.options: _OPTS = (
73
+ options if options is not None else self.get_default_options()
74
+ )
75
+
76
+ # TODO: BREAKING CHANGE v10, add abstract method for all custom parsers
77
+ # @staticmethod
78
+ # @abstractmethod
79
+ def get_default_options(self) -> _OPTS:
80
+ return self.parser_options() # type: ignore
72
81
 
73
82
  @abstractmethod
74
83
  def parse(self, commit: Commit) -> _TT: ...
@@ -62,13 +62,14 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
62
62
  commits. See https://www.conventionalcommits.org/en/v1.0.0-beta.4/
63
63
  """
64
64
 
65
+ # TODO: Deprecate in lieu of get_default_options()
65
66
  parser_options = AngularParserOptions
66
67
 
67
- def __init__(self, options: AngularParserOptions) -> None:
68
+ def __init__(self, options: AngularParserOptions | None = None) -> None:
68
69
  super().__init__(options)
69
70
  self.re_parser = re.compile(
70
71
  rf"""
71
- (?P<type>{"|".join(options.allowed_tags)}) # e.g. feat
72
+ (?P<type>{"|".join(self.options.allowed_tags)}) # e.g. feat
72
73
  (?:\((?P<scope>[^\n]+)\))? # or feat(parser)
73
74
  (?P<break>!)?:\s+ # breaking if feat!:
74
75
  (?P<subject>[^\n]+) # commit subject
@@ -77,6 +78,10 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
77
78
  flags=re.VERBOSE | re.DOTALL,
78
79
  )
79
80
 
81
+ @staticmethod
82
+ def get_default_options() -> AngularParserOptions:
83
+ return AngularParserOptions()
84
+
80
85
  # Maybe this can be cached as an optimisation, similar to how
81
86
  # mypy/pytest use their own caching directories, for very large commit
82
87
  # histories?
@@ -57,8 +57,13 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
57
57
  the commit subject in the changelog.
58
58
  """
59
59
 
60
+ # TODO: Deprecate in lieu of get_default_options()
60
61
  parser_options = EmojiParserOptions
61
62
 
63
+ @staticmethod
64
+ def get_default_options() -> EmojiParserOptions:
65
+ return EmojiParserOptions()
66
+
62
67
  def parse(self, commit: Commit) -> ParseResult:
63
68
  all_emojis = (
64
69
  self.options.major_tags + self.options.minor_tags + self.options.patch_tags