@@ -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,11 +148,57 @@ def bump(
145148VersionScheme : TypeAlias = type [VersionProtocol ]
146149
147150
151+ # Custom version pattern that extends packaging's PEP 440 regex to support
152+ # arbitrary semver pre-release labels (e.g., -release, -SNAPSHOT, -reallyweird).
153+ # Python's packaging library does not use semver; it predates it. We cannot fully
154+ # rely on packaging.version for semver-compatible parsing.
155+ # See: https://github.com/pypa/packaging/blob/14b83e15dbb9caa87c63646ba7808b2b5e460ce6/src/packaging/version.py#L117
156+ _VERSION_PATTERN = r"""^\s*
157+ v?
158+ (?:
159+ (?:(?P<epoch>[0-9]+)!)? # epoch
160+ (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
161+ (?P<pre> # pre-release
162+ [-_\.]?
163+ (?P<pre_l>
164+ (?! # negative lookahead to prevent
165+ [-_\.]? # matching post, rev, r, dev
166+ (post|rev|r|dev)
167+ [-_\.]?
168+ ([0-9]+)?
169+ $)
170+ [a-z]+? # match any letters (semver support)
171+ )
172+ [-_\.]?
173+ (?P<pre_n>[0-9]+)?
174+ )?
175+ (?P<post> # post release
176+ (?:-(?P<post_n1>[0-9]+))
177+ |
178+ (?:
179+ [-_\.]?
180+ (?P<post_l>post|rev|r)
181+ [-_\.]?
182+ (?P<post_n2>[0-9]+)?
183+ )
184+ )?
185+ (?P<dev> # dev release
186+ [-_\.]?
187+ (?P<dev_l>dev)
188+ [-_\.]?
189+ (?P<dev_n>[0-9]+)?
190+ )?
191+ )
192+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
193+ \s*$"""
194+
195+
148196class BaseVersion (_BaseVersion ):
149197 """
150198 A base class implementing the `VersionProtocol` for PEP440-like versions.
151199 """
152200
201+ _regex : re .Pattern = re .compile (_VERSION_PATTERN , re .VERBOSE | re .IGNORECASE )
153202 parser : ClassVar [re .Pattern ] = _DEFAULT_VERSION_PARSER
154203 """Regex capturing this version scheme into a `version` group"""
155204
@@ -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 ,
0 commit comments