diff --git a/crates/rattler_conda_types/src/lib.rs b/crates/rattler_conda_types/src/lib.rs index 11d1b8506..eedff4a41 100644 --- a/crates/rattler_conda_types/src/lib.rs +++ b/crates/rattler_conda_types/src/lib.rs @@ -43,7 +43,8 @@ pub use repo_data::{ pub use repo_data_record::RepoDataRecord; pub use run_export::RunExportKind; pub use version::{ - Component, ParseVersionError, ParseVersionErrorKind, StrictVersion, Version, VersionWithSource, + Component, ParseVersionError, ParseVersionErrorKind, StrictVersion, Version, VersionBumpError, + VersionBumpType, VersionWithSource, }; pub use version_spec::VersionSpec; diff --git a/crates/rattler_conda_types/src/version/bump.rs b/crates/rattler_conda_types/src/version/bump.rs new file mode 100644 index 000000000..4c620bf71 --- /dev/null +++ b/crates/rattler_conda_types/src/version/bump.rs @@ -0,0 +1,39 @@ +use thiserror::Error; + +/// VersionBumpType is used to specify the type of bump to perform on a version. +#[derive(Clone)] +pub enum VersionBumpType { + /// Bump the major version number. + Major, + /// Bump the minor version number. + Minor, + /// Bump the patch version number. + Patch, + /// Bump the last version number. + Last, + /// Bump a given segment. If negative, count from the end. + Segment(i32), +} + +/// VersionBumpError is used to specify the type of error that occurred when bumping a version. +#[derive(Error, Debug, PartialEq)] +pub enum VersionBumpError { + /// Cannot bump the major segment of a version with less than 1 segment. + #[error("cannot bump the major segment of a version with less than 1 segment")] + NoMajorSegment, + /// Cannot bump the minor segment of a version with less than 2 segments. + #[error("cannot bump the minor segment of a version with less than 2 segments")] + NoMinorSegment, + /// Cannot bump the patch segment of a version with less than 3 segments. + #[error("cannot bump the patch segment of a version with less than 3 segments")] + NoPatchSegment, + /// Cannot bump the last segment of a version with no segments. + #[error("cannot bump the last segment of a version with no segments")] + NoLastSegment, + /// Invalid segment index. + #[error("cannot bump the segment '{index:?}' of a version if it's not present")] + InvalidSegment { + /// The segment index that was attempted to be bumped. + index: i32, + }, +} diff --git a/crates/rattler_conda_types/src/version/mod.rs b/crates/rattler_conda_types/src/version/mod.rs index 53439713f..fb66e2b0a 100644 --- a/crates/rattler_conda_types/src/version/mod.rs +++ b/crates/rattler_conda_types/src/version/mod.rs @@ -22,6 +22,9 @@ pub(crate) mod parse; mod segment; mod with_source; +pub(crate) mod bump; +pub use bump::{VersionBumpError, VersionBumpType}; + use flags::Flags; use segment::Segment; @@ -236,8 +239,8 @@ impl Version { }) } - /// Returns a new version where the last numerical segment of this version has been bumped. - pub fn bump(&self) -> Self { + /// Returns a new version after bumping it according to the specified bump type. + pub fn bump(&self, bump_type: VersionBumpType) -> Result { let mut components = ComponentVec::new(); let mut segments = SegmentVec::new(); let mut flags = Flags::default(); @@ -248,6 +251,42 @@ impl Version { flags = flags.with_has_epoch(true); } + // Sanity check whether the version has enough segments for this bump type. + let segment_count = self.segment_count(); + match bump_type { + VersionBumpType::Major => { + if segment_count < 1 { + return Err(VersionBumpError::NoMajorSegment); + } + } + VersionBumpType::Minor => { + if segment_count < 2 { + return Err(VersionBumpError::NoMinorSegment); + } + } + VersionBumpType::Patch => { + if segment_count < 3 { + return Err(VersionBumpError::NoPatchSegment); + } + } + VersionBumpType::Last => { + if segment_count == 0 { + return Err(VersionBumpError::NoLastSegment); + } + } + VersionBumpType::Segment(index) => { + let uindex = if index < 0 { + segment_count as i32 + index + } else { + index + }; + + if uindex < 0 || uindex >= segment_count as i32 { + return Err(VersionBumpError::InvalidSegment { index }); + } + } + } + // Copy over all the segments and bump the last segment. let segment_count = self.segment_count(); for (idx, segment_iter) in self.segments().enumerate() { @@ -256,14 +295,27 @@ impl Version { let mut segment_components = segment_iter.components().cloned().collect::(); - // If this is the last segment of the version bump the last number. Each segment must at - // least start with a number so this should always work. - if idx == (segment_count - 1) { + // Determine whether this is the segment that needs to be bumped. + let is_segment_to_bump = match bump_type { + VersionBumpType::Major => idx == 0, + VersionBumpType::Minor => idx == 1, + VersionBumpType::Patch => idx == 2, + VersionBumpType::Last => idx == (segment_count - 1), + VersionBumpType::Segment(mut index_to_bump) => { + if index_to_bump < 0 { + index_to_bump += segment_count as i32; + } + + idx == index_to_bump as usize + } + }; + + // Bump the segment if we need to. Each segment must at least start with a number so this should always work. + if is_segment_to_bump { let last_numeral_component = segment_components .iter_mut() .filter_map(Component::as_number_mut) - .rev() - .next() + .next_back() .expect("every segment must at least contain a single numeric component"); *last_numeral_component += 1; } @@ -299,11 +351,11 @@ impl Version { .expect("this should never fail because no new segments are added"); } - Self { + Ok(Self { components, segments, flags, - } + }) } /// Returns the segments that belong the local part of the version. @@ -1008,7 +1060,7 @@ mod test { use rand::seq::SliceRandom; - use crate::version::StrictVersion; + use crate::version::{StrictVersion, VersionBumpError, VersionBumpType}; use super::Version; @@ -1216,17 +1268,26 @@ mod test { } #[test] - fn bump() { + fn bump_last() { assert_eq!( - Version::from_str("1.1").unwrap().bump(), + Version::from_str("1.1") + .unwrap() + .bump(VersionBumpType::Last) + .unwrap(), Version::from_str("1.2").unwrap() ); assert_eq!( - Version::from_str("1.1l").unwrap().bump(), + Version::from_str("1.1l") + .unwrap() + .bump(VersionBumpType::Last) + .unwrap(), Version::from_str("1.2l").unwrap() ); assert_eq!( - Version::from_str("5!1.alpha+3.4").unwrap().bump(), + Version::from_str("5!1.alpha+3.4") + .unwrap() + .bump(VersionBumpType::Last) + .unwrap(), Version::from_str("5!1.1alpha+3.4").unwrap() ); } @@ -1361,4 +1422,164 @@ mod test { Version::from_str("3!4.5a.6b").unwrap() ); } + + #[test] + fn bump_major() { + assert_eq!( + Version::from_str("1.1") + .unwrap() + .bump(VersionBumpType::Major) + .unwrap(), + Version::from_str("2.1").unwrap() + ); + assert_eq!( + Version::from_str("2.1l") + .unwrap() + .bump(VersionBumpType::Major) + .unwrap(), + Version::from_str("3.1l").unwrap() + ); + assert_eq!( + Version::from_str("5!1.alpha+3.4") + .unwrap() + .bump(VersionBumpType::Major) + .unwrap(), + Version::from_str("5!2.alpha+3.4").unwrap() + ); + } + + #[test] + fn bump_minor() { + assert_eq!( + Version::from_str("1.1") + .unwrap() + .bump(VersionBumpType::Minor) + .unwrap(), + Version::from_str("1.2").unwrap() + ); + assert_eq!( + Version::from_str("2.1l") + .unwrap() + .bump(VersionBumpType::Minor) + .unwrap(), + Version::from_str("2.2l").unwrap() + ); + assert_eq!( + Version::from_str("5!1.alpha+3.4") + .unwrap() + .bump(VersionBumpType::Minor) + .unwrap(), + Version::from_str("5!1.1alpha+3.4").unwrap() + ); + } + + #[test] + fn bump_minor_fail() { + let err = Version::from_str("1") + .unwrap() + .bump(VersionBumpType::Minor) + .unwrap_err(); + + assert_eq!(err, VersionBumpError::NoMinorSegment); + } + + #[test] + fn bump_patch() { + assert_eq!( + Version::from_str("1.1.9") + .unwrap() + .bump(VersionBumpType::Patch) + .unwrap(), + Version::from_str("1.1.10").unwrap() + ); + assert_eq!( + Version::from_str("2.1l.5alpha") + .unwrap() + .bump(VersionBumpType::Patch) + .unwrap(), + Version::from_str("2.1l.6alpha").unwrap() + ); + assert_eq!( + Version::from_str("5!1.8.alpha+3.4") + .unwrap() + .bump(VersionBumpType::Patch) + .unwrap(), + Version::from_str("5!1.8.1alpha+3.4").unwrap() + ); + } + + #[test] + fn bump_patch_fail() { + let err = Version::from_str("1.3") + .unwrap() + .bump(VersionBumpType::Patch) + .unwrap_err(); + + assert_eq!(err, VersionBumpError::NoPatchSegment); + } + + #[test] + fn bump_segment() { + // Positive index + assert_eq!( + Version::from_str("1.1.9") + .unwrap() + .bump(VersionBumpType::Segment(0)) + .unwrap(), + Version::from_str("2.1.9").unwrap() + ); + assert_eq!( + Version::from_str("1.1.9") + .unwrap() + .bump(VersionBumpType::Segment(1)) + .unwrap(), + Version::from_str("1.2.9").unwrap() + ); + assert_eq!( + Version::from_str("1.1.9") + .unwrap() + .bump(VersionBumpType::Segment(2)) + .unwrap(), + Version::from_str("1.1.10").unwrap() + ); + // Negative index + assert_eq!( + Version::from_str("1.1.9") + .unwrap() + .bump(VersionBumpType::Segment(-1)) + .unwrap(), + Version::from_str("1.1.10").unwrap() + ); + assert_eq!( + Version::from_str("1.1.9") + .unwrap() + .bump(VersionBumpType::Segment(-2)) + .unwrap(), + Version::from_str("1.2.9").unwrap() + ); + assert_eq!( + Version::from_str("1.1.9") + .unwrap() + .bump(VersionBumpType::Segment(-3)) + .unwrap(), + Version::from_str("2.1.9").unwrap() + ); + } + + #[test] + fn bump_segment_fail() { + let err = Version::from_str("1.3") + .unwrap() + .bump(VersionBumpType::Segment(3)) + .unwrap_err(); + + assert_eq!(err, VersionBumpError::InvalidSegment { index: 3 }); + + let err = Version::from_str("1.3") + .unwrap() + .bump(VersionBumpType::Segment(-3)) + .unwrap_err(); + + assert_eq!(err, VersionBumpError::InvalidSegment { index: -3 }); + } } diff --git a/crates/rattler_conda_types/src/version/parse.rs b/crates/rattler_conda_types/src/version/parse.rs index 1ba2a1e35..a1d3b7210 100644 --- a/crates/rattler_conda_types/src/version/parse.rs +++ b/crates/rattler_conda_types/src/version/parse.rs @@ -205,7 +205,9 @@ fn trailing_dash_underscore_parser( dash_or_underscore: Option, ) -> IResult<&str, (Option, Option), ParseVersionErrorKind> { // Parse a - or _. Return early if it cannot be found. - let (rest, Some(separator)) = opt(one_of::<_,_,(&str, ErrorKind)>("-_"))(input).map_err(|e| e.map(|(_, kind)| ParseVersionErrorKind::Nom(kind)))? else { + let (rest, Some(separator)) = opt(one_of::<_, _, (&str, ErrorKind)>("-_"))(input) + .map_err(|e| e.map(|(_, kind)| ParseVersionErrorKind::Nom(kind)))? + else { return Ok((input, (None, dash_or_underscore))); }; diff --git a/crates/rattler_lock/src/conda.rs b/crates/rattler_lock/src/conda.rs index 2990531aa..37d98bf80 100644 --- a/crates/rattler_lock/src/conda.rs +++ b/crates/rattler_lock/src/conda.rs @@ -93,7 +93,7 @@ impl TryFrom for RepoDataRecord { .. } = value; let LockedDependencyKind::Conda(value) = specific else { - return Err(ConversionError::NotACondaRecord) + return Err(ConversionError::NotACondaRecord); }; let version = version.parse()?; diff --git a/crates/rattler_networking/src/authentication_storage/storage.rs b/crates/rattler_networking/src/authentication_storage/storage.rs index 49990e924..c6c4d7f0d 100644 --- a/crates/rattler_networking/src/authentication_storage/storage.rs +++ b/crates/rattler_networking/src/authentication_storage/storage.rs @@ -110,7 +110,7 @@ impl AuthenticationStorage { ) -> Result<(Url, Option), reqwest::Error> { let url = url.into_url()?; let Some(host) = url.host_str() else { - return Ok((url, None)) + return Ok((url, None)); }; match self.get(host) { @@ -121,7 +121,7 @@ impl AuthenticationStorage { // Check for credentials under e.g. `*.prefix.dev` let Some(mut domain) = url.domain() else { - return Ok((url, None)) + return Ok((url, None)); }; loop { diff --git a/crates/rattler_solve/src/resolvo/mod.rs b/crates/rattler_solve/src/resolvo/mod.rs index fac765123..a1a38e571 100644 --- a/crates/rattler_solve/src/resolvo/mod.rs +++ b/crates/rattler_solve/src/resolvo/mod.rs @@ -357,7 +357,9 @@ impl<'a> DependencyProvider> for CondaDependencyProvider<'a> } fn get_dependencies(&self, solvable: SolvableId) -> Dependencies { - let SolverPackageRecord::Record(rec) = self.pool.resolve_solvable(solvable).inner() else { return Dependencies::default() }; + let SolverPackageRecord::Record(rec) = self.pool.resolve_solvable(solvable).inner() else { + return Dependencies::default(); + }; let mut parse_match_spec_cache = self.parse_match_spec_cache.borrow_mut(); let mut dependencies = Dependencies::default(); diff --git a/py-rattler/rattler/exceptions.py b/py-rattler/rattler/exceptions.py index d645d1e3f..5ec6791e9 100644 --- a/py-rattler/rattler/exceptions.py +++ b/py-rattler/rattler/exceptions.py @@ -16,6 +16,7 @@ FetchRepoDataError, SolverError, ConvertSubdirError, + VersionBumpError, ) except ImportError: # They are only redefined for documentation purposes @@ -69,6 +70,9 @@ class SolverError(Exception): # type: ignore[no-redef] class ConvertSubdirError(Exception): # type: ignore[no-redef] """An error that can occur when parsing a platform from a string.""" + class VersionBumpError(Exception): # type: ignore[no-redef] + """An error that can occur when bumping a version.""" + __all__ = [ "ActivationError", @@ -87,4 +91,5 @@ class ConvertSubdirError(Exception): # type: ignore[no-redef] "SolverError", "TransactionError", "ConvertSubdirError", + "VersionBumpError", ] diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index 744db382b..8c39f47de 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -49,21 +49,100 @@ def epoch(self) -> Optional[str]: """ return self._version.epoch() - def bump(self) -> Version: + def bump_major(self) -> Version: """ - Returns a new version where the last numerical segment of this version has + Returns a new version where the major segment of this version has been bumped. Examples -------- ```python >>> v = Version('1.0') - >>> v.bump() + >>> v.bump_major() + Version("2.0") + >>> + ``` + """ + return Version._from_py_version(self._version.bump_major()) + + def bump_minor(self) -> Version: + """ + Returns a new version where the minor segment of this version has + been bumped. + + Examples + -------- + ```python + >>> v = Version('1.0') + >>> v.bump_minor() + Version("1.1") + >>> + >>> Version("1").bump_minor() # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + exceptions.VersionBumpException + >>> + ``` + """ + return Version._from_py_version(self._version.bump_minor()) + + def bump_patch(self) -> Version: + """ + Returns a new version where the patch segment of this version has + been bumped. + + Examples + -------- + ```python + >>> v = Version('1.0.5') + >>> v.bump_patch() + Version("1.0.6") + >>> + >>> Version("1.5").bump_patch() # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + exceptions.VersionBumpException + >>> + ``` + """ + return Version._from_py_version(self._version.bump_patch()) + + def bump_last(self) -> Version: + """ + Returns a new version where the last segment of this version has + been bumped. + + Examples + -------- + ```python + >>> v = Version('1.0') + >>> v.bump_last() + Version("1.1") + >>> + ``` + """ + return Version._from_py_version(self._version.bump_last()) + + def bump_segment(self, index: int) -> Version: + """ + Returns a new version where the last segment of this version has + been bumped. + + Examples + -------- + ```python + >>> v = Version('1.0') + >>> v.bump_segment(index=1) Version("1.1") >>> + >>> Version("1.5").bump_segment(-5) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + exceptions.VersionBumpException + >>> Version("1.5").bump_segment(5) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + exceptions.VersionBumpException + >>> ``` """ - return Version._from_py_version(self._version.bump()) + return Version._from_py_version(self._version.bump_segment(index)) @property def has_local(self) -> bool: diff --git a/py-rattler/src/error.rs b/py-rattler/src/error.rs index 463905527..2f376b3b7 100644 --- a/py-rattler/src/error.rs +++ b/py-rattler/src/error.rs @@ -5,7 +5,7 @@ use pyo3::{create_exception, PyErr}; use rattler::install::TransactionError; use rattler_conda_types::{ ConvertSubdirError, InvalidPackageNameError, ParseArchError, ParseChannelError, - ParseMatchSpecError, ParsePlatformError, ParseVersionError, + ParseMatchSpecError, ParsePlatformError, ParseVersionError, VersionBumpError, }; use rattler_repodata_gateway::fetch::FetchRepoDataError; use rattler_shell::activation::ActivationError; @@ -48,6 +48,8 @@ pub enum PyRattlerError { LinkError(String), #[error(transparent)] ConverSubdirError(#[from] ConvertSubdirError), + #[error(transparent)] + VersionBumpError(#[from] VersionBumpError), } impl From for PyErr { @@ -85,6 +87,7 @@ impl From for PyErr { PyRattlerError::ConverSubdirError(err) => { ConvertSubdirException::new_err(err.to_string()) } + PyRattlerError::VersionBumpError(err) => VersionBumpException::new_err(err.to_string()), } } } @@ -105,3 +108,4 @@ create_exception!(exceptions, SolverException, PyException); create_exception!(exceptions, TransactionException, PyException); create_exception!(exceptions, LinkException, PyException); create_exception!(exceptions, ConvertSubdirException, PyException); +create_exception!(exceptions, VersionBumpException, PyException); diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index bddd5c878..83d77dc74 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -23,7 +23,7 @@ use error::{ FetchRepoDataException, InvalidChannelException, InvalidMatchSpecException, InvalidPackageNameException, InvalidUrlException, InvalidVersionException, IoException, LinkException, ParseArchException, ParsePlatformException, PyRattlerError, SolverException, - TransactionException, + TransactionException, VersionBumpException, }; use generic_virtual_package::PyGenericVirtualPackage; use match_spec::PyMatchSpec; @@ -143,5 +143,7 @@ fn rattler(py: Python, m: &PyModule) -> PyResult<()> { py.get_type::(), ) .unwrap(); + m.add("VersionBumpError", py.get_type::()) + .unwrap(); Ok(()) } diff --git a/py-rattler/src/version/mod.rs b/py-rattler/src/version/mod.rs index 922e876ad..7c18a7738 100644 --- a/py-rattler/src/version/mod.rs +++ b/py-rattler/src/version/mod.rs @@ -2,8 +2,8 @@ mod component; use crate::PyRattlerError; use component::PyComponent; -use pyo3::{basic::CompareOp, pyclass, pymethods}; -use rattler_conda_types::Version; +use pyo3::{basic::CompareOp, pyclass, pymethods, PyResult}; +use rattler_conda_types::{Version, VersionBumpType}; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, @@ -132,11 +132,49 @@ impl PyVersion { } } - /// Returns a new version where the last numerical segment of this version has been bumped. - pub fn bump(&self) -> Self { - Self { - inner: self.inner.bump(), - } + /// Returns a new version where the major segment of this version has been bumped. + pub fn bump_major(&self) -> PyResult { + Ok(self + .inner + .bump(VersionBumpType::Major) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Returns a new version where the minor segment of this version has been bumped. + pub fn bump_minor(&self) -> PyResult { + Ok(self + .inner + .bump(VersionBumpType::Minor) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Returns a new version where the patch segment of this version has been bumped. + pub fn bump_patch(&self) -> PyResult { + Ok(self + .inner + .bump(VersionBumpType::Patch) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Returns a new version where the last segment of this version has been bumped. + pub fn bump_last(&self) -> PyResult { + Ok(self + .inner + .bump(VersionBumpType::Last) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Returns a new version where the given segment of this version has been bumped. + pub fn bump_segment(&self, index: i32) -> PyResult { + Ok(self + .inner + .bump(VersionBumpType::Segment(index)) + .map(Into::into) + .map_err(PyRattlerError::from)?) } /// Compute the hash of the version.