@@ -118,6 +118,13 @@ def __init__(self, previous_version: Version, bump: str) -> None:
118118 patch = previous_version .patch + 1 ,
119119 release = "{RELEASE}" ,
120120 )
121+ elif bump == "release" :
122+ # Remove prerelease suffix to create a stable release
123+ self .next_version = Version (
124+ major = previous_version .major ,
125+ minor = previous_version .minor ,
126+ patch = previous_version .patch ,
127+ )
121128 elif bump == "previous" :
122129 self .next_version = previous_version
123130
@@ -162,23 +169,35 @@ def get_version_from_package_json(path: Path) -> Version | None:
162169 with package_json_path .open () as f :
163170 json_data = json .load (f )
164171 if version := json_data .get ("version" ):
165- return Version .from_str (version )
172+ try :
173+ return Version .from_str (version )
174+ except ValueError :
175+ return None
166176 return None
167177
168178
169179def get_version_from_pyproject_toml (path : Path ) -> Version | None :
170180 pyproject_toml_path = path / "pyproject.toml"
171- if pyproject_toml_path .exists ():
172- with pyproject_toml_path .open ("rb" ) as f :
173- toml_data = tomllib .load (f )
174- if version := toml_data .get ("project" , {}).get ("version" ):
175- return Version .from_str (version )
176- if version := toml_data .get ("tool" , {}).get ("poetry" , {}).get ("version" ):
177- return Version .from_str (version )
178- if version := toml_data .get ("tool" , {}).get ("flit" , {}).get ("metadata" , {}).get ("version" ):
179- return Version .from_str (version )
180- if version := toml_data .get ("tool" , {}).get ("setuptools" , {}).get ("setup_requires" , {}).get ("version" ):
181+ if not pyproject_toml_path .exists ():
182+ return None
183+
184+ with pyproject_toml_path .open ("rb" ) as f :
185+ toml_data = tomllib .load (f )
186+
187+ version_paths = [
188+ toml_data .get ("project" , {}).get ("version" ),
189+ toml_data .get ("tool" , {}).get ("poetry" , {}).get ("version" ),
190+ toml_data .get ("tool" , {}).get ("flit" , {}).get ("metadata" , {}).get ("version" ),
191+ toml_data .get ("tool" , {}).get ("setuptools" , {}).get ("setup_requires" , {}).get ("version" ),
192+ ]
193+
194+ for version in version_paths :
195+ if version :
196+ try :
181197 return Version .from_str (version )
198+ except ValueError :
199+ continue
200+
182201 return None
183202
184203
@@ -188,7 +207,10 @@ def get_version_from_setup_py(path: Path) -> Version | None:
188207 with setup_py_path .open () as f :
189208 setup_data = f .read ()
190209 if res := re .search (r"version=['\"]([^'\"]+)['\"]" , setup_data ):
191- return Version .from_str (res [1 ])
210+ try :
211+ return Version .from_str (res [1 ])
212+ except ValueError :
213+ return None
192214 return None
193215
194216
@@ -211,41 +233,42 @@ def get_version_from_cargo_toml(directory_path: Path) -> Version | None:
211233 if not cargo_toml_path .is_file ():
212234 console .print (f"Cargo.toml not found or is not a file at: { cargo_toml_path } " )
213235 return None
236+
237+ # 2. Load and parse the TOML file
214238 try :
215- # 2. Open and read the file
216239 with cargo_toml_path .open ("rb" ) as f :
217- try :
218- # 3. Parse TOML content
219- cargo_data = tomllib .load (f )
220- except tomllib .TOMLDecodeError as e :
221- console .print (f"Failed to decode TOML file { cargo_toml_path } : { e } " )
222- return None
223-
224- except OSError as e :
225- # Handle potential file reading errors (permissions, etc.)
226- console .print (f"Could not read file { cargo_toml_path } : { e } " )
240+ cargo_data = tomllib .load (f )
241+ except (OSError , tomllib .TOMLDecodeError ) as e :
242+ console .print (f"Could not read or parse TOML file { cargo_toml_path } : { e } " )
227243 return None
228244
229- # 4 . Safely access the package table
245+ # 3 . Safely access the package table and version string
230246 package_data = cargo_data .get ("package" )
231247 if not isinstance (package_data , dict ):
232248 console .print (f"Missing or invalid [package] table in { cargo_toml_path } " )
233249 return None
234250
235- # 5. Safely access the version string
236251 version_str = package_data .get ("version" ) # type: ignore
237252 if not isinstance (version_str , str ) or not version_str : # Check if it's a non-empty string
238253 console .print (f"Missing, empty, or invalid 'version' string in [package] table of { cargo_toml_path } " )
239254 return None
240- return Version .from_str (version_str )
255+
256+ # 4. Parse and return the version
257+ try :
258+ return Version .from_str (version_str )
259+ except ValueError :
260+ return None
241261
242262
243263def get_version_from_version_file (path : Path ) -> Version | None :
244264 version_path = path / "VERSION"
245265 if version_path .exists ():
246266 with version_path .open () as f :
247267 version = f .read ().strip ()
248- return Version .from_str (version )
268+ try :
269+ return Version .from_str (version )
270+ except ValueError :
271+ return None
249272 return None
250273
251274
@@ -254,7 +277,10 @@ def get_version_from_version_txt(path: Path) -> Version | None:
254277 if version_txt_path .exists ():
255278 with version_txt_path .open () as f :
256279 version = f .read ().strip ()
257- return Version .from_str (version )
280+ try :
281+ return Version .from_str (version )
282+ except ValueError :
283+ return None
258284 return None
259285
260286
@@ -269,7 +295,10 @@ def get_version_from_git(path: Path) -> Version | None:
269295 tags = status .stdout .decode ().split ("\n " )
270296 for tag in tags :
271297 if tag .startswith ("v" ):
272- return Version .from_str (tag [1 :])
298+ try :
299+ return Version .from_str (tag [1 :])
300+ except ValueError :
301+ continue
273302 return None
274303
275304
@@ -517,9 +546,15 @@ def _handle_explicit_version_args(args: VersionArgs, prev_version: Version) -> V
517546
518547
519548def _handle_interactive_version_selection (prev_version : Version , default_bump : str , verbose : int ) -> Version | None :
520- choices = [
521- VersionChoice (prev_version , bump ) for bump in ["patch" , "minor" , "major" , "prepatch" , "preminor" , "premajor" , "previous" , "custom" ]
522- ]
549+ bump_options = ["patch" , "minor" , "major" , "prepatch" , "preminor" , "premajor" ]
550+
551+ # Add "release" option if current version is a prerelease
552+ if prev_version .release :
553+ bump_options .insert (0 , "release" ) # Put it at the beginning for prominence
554+
555+ bump_options .extend (["previous" , "custom" ])
556+
557+ choices = [VersionChoice (prev_version , bump ) for bump in bump_options ]
523558 default_choice = next ((choice for choice in choices if choice .bump == default_bump ), None )
524559
525560 console .print (f"Auto bump based on commits: [cyan bold]{ default_bump } " )
@@ -569,6 +604,9 @@ def _apply_version_choice(target: VersionChoice, prev_version: Version) -> Versi
569604 next_version = custom_version
570605 else :
571606 return None
607+ elif target .bump == "release" :
608+ # Remove prerelease suffix - next_version is already set correctly in VersionChoice
609+ next_version = target .next_version
572610 else :
573611 # For regular bumps: patch, minor, major, previous
574612 bump_version (target , next_version )
0 commit comments