@@ -121,7 +121,10 @@ def __ne__(self, other: object) -> bool:
121121 def bump (
122122 self ,
123123 increment : Increment | None ,
124- prerelease : Prerelease | None = None ,
124+ # str instead of Prerelease to support arbitrary semver pre-release labels
125+ # (e.g., "release", "SNAPSHOT") parsed from existing tags. The CLI still
126+ # restricts user input to alpha/beta/rc via argparse choices.
127+ prerelease : str | None = None ,
125128 prerelease_offset : int = 0 ,
126129 devrelease : int | None = None ,
127130 is_local_version : bool = False ,
@@ -145,6 +148,52 @@ def bump(
145148VersionScheme : TypeAlias = type [VersionProtocol ]
146149
147150
151+ # Custom version pattern for SemVer schemes that extends packaging's PEP 440
152+ # regex to support arbitrary semver pre-release labels (e.g., -release, -SNAPSHOT,
153+ # -pre-release). Python's packaging library does not use semver; it predates it.
154+ # We cannot fully rely on packaging.version for semver-compatible parsing.
155+ # This pattern is NOT applied to Pep440 scheme, which retains strict PEP 440 parsing.
156+ # See: https://github.com/pypa/packaging/blob/14b83e15dbb9caa87c63646ba7808b2b5e460ce6/src/packaging/version.py#L117
157+ _SEMVER_VERSION_PATTERN = r"""^\s*
158+ v?
159+ (?:
160+ (?:(?P<epoch>[0-9]+)!)? # epoch
161+ (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
162+ (?P<pre> # pre-release
163+ [-_\.]?
164+ (?P<pre_l>
165+ (?! # negative lookahead to prevent
166+ [-_\.]? # matching post, rev, r, dev
167+ (post|rev|r|dev) # (reserved PEP 440 segments)
168+ [-_\.]?
169+ ([0-9]+)?
170+ (\+|$)) # terminated by local segment or EOL
171+ [a-z]+(?:-[a-z]+)* # letters with optional hyphen-separated parts
172+ )
173+ [-_\.]?
174+ (?P<pre_n>[0-9]+)?
175+ )?
176+ (?P<post> # post release
177+ (?:-(?P<post_n1>[0-9]+))
178+ |
179+ (?:
180+ [-_\.]?
181+ (?P<post_l>post|rev|r)
182+ [-_\.]?
183+ (?P<post_n2>[0-9]+)?
184+ )
185+ )?
186+ (?P<dev> # dev release
187+ [-_\.]?
188+ (?P<dev_l>dev)
189+ [-_\.]?
190+ (?P<dev_n>[0-9]+)?
191+ )?
192+ )
193+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
194+ \s*$"""
195+
196+
148197class BaseVersion (_BaseVersion ):
149198 """
150199 A base class implementing the `VersionProtocol` for PEP440-like versions.
@@ -184,8 +233,26 @@ def generate_prerelease(
184233 # https://packaging.python.org/en/latest/specifications/version-specifiers/#pre-releases
185234 # https://semver.org/#spec-item-11
186235 if self .is_prerelease and self .pre :
187- prerelease = max (prerelease , self .pre [0 ])
188- if prerelease .startswith (self .pre [0 ]):
236+ current_pre_label = self .pre [0 ]
237+ # packaging normalizes "alpha"→"a", "beta"→"b", "rc"→"rc"
238+ _LABEL_TO_NORMALIZED = {"alpha" : "a" , "beta" : "b" , "rc" : "rc" }
239+ _KNOWN_PRE_LABELS = {"a" , "b" , "rc" }
240+ normalized_prerelease = _LABEL_TO_NORMALIZED .get (
241+ prerelease , prerelease .lower ()
242+ )
243+
244+ # The ordering logic (max) only makes sense for the known PEP 440
245+ # labels where "a" < "b" < "rc" lexicographically. For arbitrary
246+ # semver labels (e.g., "release", "SNAPSHOT"), we use strict equality
247+ # since there's no defined ordering between them.
248+ if (
249+ current_pre_label in _KNOWN_PRE_LABELS
250+ and normalized_prerelease in _KNOWN_PRE_LABELS
251+ ):
252+ prerelease = max (normalized_prerelease , current_pre_label )
253+ if prerelease == current_pre_label :
254+ offset = self .pre [1 ] + 1
255+ elif normalized_prerelease == current_pre_label :
189256 offset = self .pre [1 ] + 1
190257
191258 return f"{ prerelease } { offset } "
@@ -232,7 +299,7 @@ def increment_base(self, increment: Increment | None = None) -> str:
232299 def bump (
233300 self ,
234301 increment : Increment | None ,
235- prerelease : Prerelease | None = None ,
302+ prerelease : str | None = None , # str to support arbitrary semver labels
236303 prerelease_offset : int = 0 ,
237304 devrelease : int | None = None ,
238305 is_local_version : bool = False ,
@@ -300,6 +367,12 @@ class SemVer(BaseVersion):
300367 See: https://semver.org/spec/v1.0.0.html
301368 """
302369
370+ # Override the PEP 440 regex to accept arbitrary semver pre-release labels
371+ # (e.g., -release, -SNAPSHOT, -pre-release). SemVer2 inherits this.
372+ _regex : re .Pattern = re .compile (
373+ _SEMVER_VERSION_PATTERN , re .VERBOSE | re .IGNORECASE
374+ )
375+
303376 def __str__ (self ) -> str :
304377 parts : list [str ] = []
305378
0 commit comments