From 67dca00a12d925da0fa944508c0ab4bec1800ef6 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Tue, 22 Aug 2023 20:32:45 +0530 Subject: [PATCH 01/22] feat: add version type to py-rattler --- py-rattler/rattler/version/version.py | 95 ++++++++++++++++++++++++++- py-rattler/src/version.rs | 59 ++++++++++++++++- py-rattler/tests/unit/test_version.py | 37 +++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index 8787af572..b3ba744fd 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional +from typing import Optional, Tuple from rattler.rattler import PyVersion @@ -54,6 +54,99 @@ def bump(self) -> Version: """ return Version._from_py_version(self._version.bump()) + @property + def local(self) -> bool: + """ + Returns true if this version has a local segment defined. + + Examples + -------- + >>> v = Version('1.0') + >>> v.local + False + """ + return self._version.has_local() + + def as_major_minor(self) -> Optional[Tuple[int, int]]: + """ + Returns the major and minor segments from the version. + + Examples + -------- + >>> v = Version('1.0') + >>> v.as_major_minor() + (1, 0) + """ + return self._version.as_major_minor() + + @property + def dev(self) -> bool: + """ + Returns true if the version contains a component name "dev". + + Examples + -------- + >>> v = Version('1.0.1dev') + >>> v.dev + True + """ + return self._version.is_dev() + + def starts_with(self, other: Version) -> bool: + """ + Checks if the version and local segment start + same as other version. + + Examples + -------- + >>> v1 = Version('1.0.1') + >>> v2 = Version('1.0') + >>> v1.starts_with(v2) + True + """ + return self._version.starts_with(other._version) + + def compatible_with(self, other: Version) -> bool: + """ + Checks if this version is compatible with other version. + """ + return self._version.compatible_with(other._version) + + def pop_segments(self, n: int = 1) -> Optional[Version]: + """ + Pops `n` number of segments from the version and returns + the new version. Returns `None` if the version becomes + invalid due to the operation. + """ + new_py_version = self._version.pop_segments(n) + if new_py_version: + # maybe it should raise an exception instead? + return self._from_py_version(new_py_version) + + def with_segments(self, start: int, stop: int) -> Optional[Version]: + """ + Returns new version with with segments ranging from `start` to `stop`. + `stop` is exclusive. + """ + new_py_version = self._version.with_segments(start, stop) + if new_py_version: + return self._from_py_version(new_py_version) + else: + # maybe it should raise an exception instead? + return None + + def segment_count(self) -> int: + """ + Returns the number of segments in the version. + """ + return self._version.segment_count() + + def strip_local(self) -> Version: + """ + Returns a new version with local segment stripped. + """ + return self._from_py_version(self._version.strip_local()) + def __eq__(self, other: Version) -> bool: return self._version.equals(other._version) diff --git a/py-rattler/src/version.rs b/py-rattler/src/version.rs index 89de82601..e79f235ce 100644 --- a/py-rattler/src/version.rs +++ b/py-rattler/src/version.rs @@ -30,11 +30,68 @@ impl PyVersion { format!("{}", self.inner) } - /// Returns the epoch of the version + /// Returns the epoch of the version. pub fn epoch(&self) -> Option { self.inner.epoch_opt() } + /// Returns true if this version has a local segment defined. + pub fn has_local(&self) -> bool { + self.inner.has_local() + } + + /// Returns the major and minor segments from the version. + pub fn as_major_minor(&self) -> Option<(u64, u64)> { + self.inner.as_major_minor() + } + + /// Returns true if the version contains a component name "dev". + pub fn is_dev(&self) -> bool { + self.inner.is_dev() + } + + /// Checks if the version and local segment start + /// same as other version. + pub fn starts_with(&self, other: &Self) -> bool { + self.inner.starts_with(&other.inner) + } + + /// Checks if this version is compatible with other version. + pub fn compatible_with(&self, other: &Self) -> bool { + self.inner.compatible_with(&other.inner) + } + + /// Pops `n` number of segments from the version and returns + /// the new version. Returns `None` if the version becomes + /// invalid due to the operation. + pub fn pop_segments(&self, n: usize) -> Option { + Some(Self { + inner: self.inner.pop_segments(n)?, + }) + } + + /// Returns new version with with segments ranging from `start` to `stop`. + /// `stop` is exclusive. + pub fn with_segments(&self, start: usize, stop: usize) -> Option { + let range = start..stop; + + Some(Self { + inner: self.inner.with_segments(range)?, + }) + } + + /// Returns the number of segments in the version. + pub fn segnment_count(&self) -> usize { + self.inner.segment_count() + } + + /// Create a new version with local segment stripped. + pub fn strip_local(&self) -> Self { + Self { + inner: self.inner.strip_local().into_owned(), + } + } + /// Returns a new version where the last numerical segment of this version has been bumped. pub fn bump(&self) -> Self { Self { diff --git a/py-rattler/tests/unit/test_version.py b/py-rattler/tests/unit/test_version.py index 902099e6e..494d82d91 100644 --- a/py-rattler/tests/unit/test_version.py +++ b/py-rattler/tests/unit/test_version.py @@ -22,3 +22,40 @@ def test_bump(): def test_epoch(): assert Version("1!1.0").epoch == 1 assert Version("1.0").epoch is None + + +def test_dev(): + assert Version("1.2-alpha.3-beta-dev0").dev is True + assert Version("1.2-alpha.3").dev is False + + +def test_local(): + assert Version("1.0+1.2").local is True + assert Version("1.0").local is False + + +def test_as_major_minor(): + assert Version("2.3.4").as_major_minor() == (2, 3) + assert Version("1.2-alpha.3-beta-dev0").as_major_minor() == (1, 2) + + +def test_starts_with(): + assert Version("1.0.6").starts_with(Version("1.0")) is True + assert Version("1.0.6").starts_with(Version("1.6")) is False + + +def test_compatible_with(): + assert Version("1.6").compatible_with(Version("1.5")) is True + assert Version("1.6").compatible_with(Version("1.7")) is False + assert Version("1.6").compatible_with(Version("2.0")) is False + + +def test_pop_segments(): + assert Version("1.6.0").pop_segments() == Version("1.6") + assert Version("1.6.0").pop_segments(2) == Version("1") + assert Version("1.6.0").pop_segments(3) is None + + +def test_strip_local(): + assert Version("1.6+2.0").strip_local() == Version("1.6") + assert Version("1.6").strip_local() == Version("1.6") From 26c2489ba1a9482a0a9bb87a634c530d828b97d0 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Wed, 23 Aug 2023 20:31:06 +0530 Subject: [PATCH 02/22] feat: add MatchSpec --- .gitignore | 2 + py-rattler/Cargo.lock | 6 +- py-rattler/rattler/__init__.py | 4 +- py-rattler/rattler/match_spec/__init__.py | 3 + py-rattler/rattler/match_spec/match_spec.py | 84 ++++++++++++++ py-rattler/rattler/repo_data/__init__.py | 3 + .../rattler/repo_data/package_record.py | 14 +++ py-rattler/src/error.rs | 8 +- py-rattler/src/lib.rs | 19 +++- py-rattler/src/match_spec.rs | 104 ++++++++++++++++++ py-rattler/src/repo_data/mod.rs | 1 + py-rattler/src/repo_data/package_record.rs | 30 +++++ 12 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 py-rattler/rattler/match_spec/__init__.py create mode 100644 py-rattler/rattler/match_spec/match_spec.py create mode 100644 py-rattler/rattler/repo_data/__init__.py create mode 100644 py-rattler/rattler/repo_data/package_record.py create mode 100644 py-rattler/src/match_spec.rs create mode 100644 py-rattler/src/repo_data/mod.rs create mode 100644 py-rattler/src/repo_data/package_record.rs diff --git a/.gitignore b/.gitignore index 7df89a2fc..67b3ae888 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .idea/ +.vscode/ + # Generated by Cargo # will have compiled files and executables debug/ diff --git a/py-rattler/Cargo.lock b/py-rattler/Cargo.lock index 955348d8f..3835340a3 100644 --- a/py-rattler/Cargo.lock +++ b/py-rattler/Cargo.lock @@ -583,7 +583,7 @@ dependencies = [ [[package]] name = "rattler_conda_types" -version = "0.7.0" +version = "0.8.0" dependencies = [ "chrono", "fxhash", @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "rattler_digest" -version = "0.7.0" +version = "0.8.0" dependencies = [ "blake2", "digest", @@ -623,7 +623,7 @@ dependencies = [ [[package]] name = "rattler_macros" -version = "0.7.0" +version = "0.8.0" dependencies = [ "quote", "syn 2.0.29", diff --git a/py-rattler/rattler/__init__.py b/py-rattler/rattler/__init__.py index 1ffed8702..4d084172b 100644 --- a/py-rattler/rattler/__init__.py +++ b/py-rattler/rattler/__init__.py @@ -1,3 +1,5 @@ from rattler.version import Version +from rattler.match_spec import MatchSpec, NamelessMatchSpec +from rattler.repo_data import PackageRecord -__all__ = ["Version"] +__all__ = ["Version", "MatchSpec", "NamelessMatchSpec", "PackageRecord"] diff --git a/py-rattler/rattler/match_spec/__init__.py b/py-rattler/rattler/match_spec/__init__.py new file mode 100644 index 000000000..1701a0eb4 --- /dev/null +++ b/py-rattler/rattler/match_spec/__init__.py @@ -0,0 +1,3 @@ +from rattler.match_spec.match_spec import MatchSpec, NamelessMatchSpec + +__all__ = ["MatchSpec", "NamelessMatchSpec"] diff --git a/py-rattler/rattler/match_spec/match_spec.py b/py-rattler/rattler/match_spec/match_spec.py new file mode 100644 index 000000000..6641cfcb1 --- /dev/null +++ b/py-rattler/rattler/match_spec/match_spec.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from rattler.rattler import PyMatchSpec, PyNamelessMatchSpec +from rattler.repo_data import PackageRecord + + +class MatchSpec: + def __init__(self, spec: str): + if isinstance(spec, str): + self._match_spec = PyMatchSpec(spec) + else: + raise TypeError( + "MatchSpec constructor received unsupported type" + f" {type(spec).__name__!r} for the 'spec' parameter" + ) + + @classmethod + def _from_py_match_spec(cls, py_match_spec: PyMatchSpec) -> MatchSpec: + """ + Construct py-rattler MatchSpec from PyMatchSpec FFI object. + """ + match_spec = cls.__new__(cls) + match_spec._match_spec = py_match_spec + + return match_spec + + def matches(self, record: PackageRecord) -> bool: + """Match a MatchSpec against a PackageRecord.""" + return self._match_spec.matches(record._package_record) + + @staticmethod + def from_nameless(spec: NamelessMatchSpec, name: str) -> MatchSpec: + """ + Constructs a MatchSpec from a NamelessMatchSpec + and a name. + """ + return MatchSpec._from_py_match_spec( + PyMatchSpec.from_nameless(spec._nameless_match_spec, name) + ) + + def __str__(self) -> str: + return self._match_spec.as_str() + + def __repr__(self) -> str: + return self.__str__() + + +class NamelessMatchSpec: + def __init__(self, spec: str): + if isinstance(spec, str): + self._nameless_match_spec = PyNamelessMatchSpec(spec) + else: + raise TypeError( + "NamelessMatchSpec constructor received unsupported type" + f" {type(spec).__name__!r} for the 'spec' parameter" + ) + + def matches(self, package_record: PackageRecord) -> bool: + return self._nameless_match_spec.matches(package_record._package_record) + + @classmethod + def _from_py_nameless_match_spec(cls, py_nameless_match_spec: PyNamelessMatchSpec) -> NamelessMatchSpec: + """ + Construct py-rattler NamelessMatchSpec from PyNamelessMatchSpec FFI object. + """ + nameless_match_spec = cls.__new__(cls) + nameless_match_spec._nameless_match_spec = py_nameless_match_spec + + return nameless_match_spec + + @staticmethod + def from_match_spec(spec: MatchSpec) -> NamelessMatchSpec: + """ + Constructs a PyNamelessMatchSpec from a PyMatchSpec. + """ + return NamelessMatchSpec._from_py_nameless_match_spec( + PyNamelessMatchSpec.from_match_spec(spec._match_spec) + ) + + def __str__(self) -> str: + return self._nameless_match_spec.as_str() + + def __repr__(self) -> str: + return self.__str__() diff --git a/py-rattler/rattler/repo_data/__init__.py b/py-rattler/rattler/repo_data/__init__.py new file mode 100644 index 000000000..c2e09d680 --- /dev/null +++ b/py-rattler/rattler/repo_data/__init__.py @@ -0,0 +1,3 @@ +from rattler.repo_data.package_record import PackageRecord + +__all__ = ["PackageRecord"] diff --git a/py-rattler/rattler/repo_data/package_record.py b/py-rattler/rattler/repo_data/package_record.py new file mode 100644 index 000000000..be02fd06d --- /dev/null +++ b/py-rattler/rattler/repo_data/package_record.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from rattler.rattler import PyPackageRecord + + +class PackageRecord: + def __init__(self): + self._package_record = PyPackageRecord() + + def __str__(self) -> str: + return self._package_record.as_str() + + def __repr__(self) -> str: + return self.__str__() diff --git a/py-rattler/src/error.rs b/py-rattler/src/error.rs index 8eb34322a..982ca38dc 100644 --- a/py-rattler/src/error.rs +++ b/py-rattler/src/error.rs @@ -1,12 +1,14 @@ use pyo3::exceptions::PyException; use pyo3::{create_exception, PyErr}; -use rattler_conda_types::ParseVersionError; +use rattler_conda_types::{ParseMatchSpecError, ParseVersionError}; use thiserror::Error; #[derive(Error, Debug)] pub enum PyRattlerError { #[error(transparent)] InvalidVersion(#[from] ParseVersionError), + #[error(transparent)] + InvalidMatchSpec(#[from] ParseMatchSpecError), } impl From for PyErr { @@ -15,8 +17,12 @@ impl From for PyErr { PyRattlerError::InvalidVersion(err) => { InvalidVersionException::new_err(err.to_string()) } + PyRattlerError::InvalidMatchSpec(err) => { + InvalidMatchSpecException::new_err(err.to_string()) + } } } } create_exception!(exceptions, InvalidVersionException, PyException); +create_exception!(exceptions, InvalidMatchSpecException, PyException); diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index fac921917..967baa59a 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -1,20 +1,35 @@ mod error; +mod match_spec; +mod repo_data; mod version; -use error::{InvalidVersionException, PyRattlerError}; +use error::{InvalidMatchSpecException, InvalidVersionException, PyRattlerError}; +use match_spec::{PyMatchSpec, PyNamelessMatchSpec}; +use repo_data::package_record::PyPackageRecord; +use version::PyVersion; use pyo3::prelude::*; -use version::PyVersion; #[pymodule] fn rattler(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); + + m.add_class::().unwrap(); + // Exceptions m.add( "InvalidVersionError", py.get_type::(), ) .unwrap(); + m.add( + "InvalidMatchSpecError", + py.get_type::(), + ) + .unwrap(); + Ok(()) } diff --git a/py-rattler/src/match_spec.rs b/py-rattler/src/match_spec.rs new file mode 100644 index 000000000..dfc06bd1c --- /dev/null +++ b/py-rattler/src/match_spec.rs @@ -0,0 +1,104 @@ +use pyo3::{pyclass, pymethods}; +use rattler_conda_types::{MatchSpec, NamelessMatchSpec}; +use std::str::FromStr; + +use crate::{error::PyRattlerError, repo_data::package_record::PyPackageRecord}; + +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PyMatchSpec { + inner: MatchSpec, +} + +impl From for PyMatchSpec { + fn from(value: MatchSpec) -> Self { + Self { inner: value } + } +} + +impl Into for PyMatchSpec { + fn into(self) -> MatchSpec { + self.inner + } +} + +#[pymethods] +impl PyMatchSpec { + #[new] + pub fn __init__(spec: &str) -> pyo3::PyResult { + Ok(MatchSpec::from_str(spec) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Returns a string representation of MatchSpec + pub fn as_str(&self) -> String { + format!("{}", self.inner) + } + + /// Match a MatchSpec against a PackageRecord + pub fn matches(&self, record: &PyPackageRecord) -> bool { + self.inner.matches(&record.clone().into()) + } + + /// Constructs a PyMatchSpec from a PyNamelessMatchSpec and a name. + #[staticmethod] + pub fn from_nameless(spec: &PyNamelessMatchSpec, name: String) -> Self { + Self { + inner: MatchSpec::from_nameless(spec.clone().into(), Some(name)), + } + } +} + +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PyNamelessMatchSpec { + inner: NamelessMatchSpec, +} + +impl From for PyNamelessMatchSpec { + fn from(value: NamelessMatchSpec) -> Self { + Self { inner: value } + } +} + +impl Into for PyNamelessMatchSpec { + fn into(self) -> NamelessMatchSpec { + self.inner + } +} + +impl From for PyNamelessMatchSpec { + fn from(value: PyMatchSpec) -> Self { + let inner: NamelessMatchSpec = Into::::into(value).into(); + Self { inner } + } +} + +#[pymethods] +impl PyNamelessMatchSpec { + #[new] + pub fn __init__(spec: &str) -> pyo3::PyResult { + Ok(NamelessMatchSpec::from_str(spec) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Returns a string representation of MatchSpec + pub fn as_str(&self) -> String { + format!("{}", self.inner) + } + + /// Match a PyNamelessMatchSpec against a PyPackageRecord + pub fn matches(&self, record: &PyPackageRecord) -> bool { + self.inner.matches(&record.clone().into()) + } + + /// Constructs a [`PyNamelessMatchSpec`] from a [`PyMatchSpec`]. + #[staticmethod] + pub fn from_match_spec(spec: &PyMatchSpec) -> Self { + Into::::into(spec.clone()) + } +} diff --git a/py-rattler/src/repo_data/mod.rs b/py-rattler/src/repo_data/mod.rs new file mode 100644 index 000000000..da0d5b5f7 --- /dev/null +++ b/py-rattler/src/repo_data/mod.rs @@ -0,0 +1 @@ +pub mod package_record; diff --git a/py-rattler/src/repo_data/package_record.rs b/py-rattler/src/repo_data/package_record.rs new file mode 100644 index 000000000..419d6f6fa --- /dev/null +++ b/py-rattler/src/repo_data/package_record.rs @@ -0,0 +1,30 @@ +use rattler_conda_types::PackageRecord; + +use pyo3::{pyclass, pymethods}; + +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PyPackageRecord { + inner: PackageRecord, +} + +impl From for PyPackageRecord { + fn from(value: PackageRecord) -> Self { + Self { inner: value } + } +} + +impl Into for PyPackageRecord { + fn into(self) -> PackageRecord { + self.inner + } +} + +#[pymethods] +impl PyPackageRecord { + /// Returns a string representation of PyPackageRecord + fn as_str(&self) -> String { + format!("{}", self.inner) + } +} From 21ba5781f8c7fb86f590a883da316a2b0e229720 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Wed, 23 Aug 2023 20:37:23 +0530 Subject: [PATCH 03/22] chore: fix lints --- py-rattler/rattler/match_spec/match_spec.py | 4 +++- py-rattler/src/match_spec.rs | 12 ++++++------ py-rattler/src/repo_data/package_record.rs | 6 +++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/py-rattler/rattler/match_spec/match_spec.py b/py-rattler/rattler/match_spec/match_spec.py index 6641cfcb1..f8e7ca968 100644 --- a/py-rattler/rattler/match_spec/match_spec.py +++ b/py-rattler/rattler/match_spec/match_spec.py @@ -59,7 +59,9 @@ def matches(self, package_record: PackageRecord) -> bool: return self._nameless_match_spec.matches(package_record._package_record) @classmethod - def _from_py_nameless_match_spec(cls, py_nameless_match_spec: PyNamelessMatchSpec) -> NamelessMatchSpec: + def _from_py_nameless_match_spec( + cls, py_nameless_match_spec: PyNamelessMatchSpec + ) -> NamelessMatchSpec: """ Construct py-rattler NamelessMatchSpec from PyNamelessMatchSpec FFI object. """ diff --git a/py-rattler/src/match_spec.rs b/py-rattler/src/match_spec.rs index dfc06bd1c..f003ae99c 100644 --- a/py-rattler/src/match_spec.rs +++ b/py-rattler/src/match_spec.rs @@ -17,9 +17,9 @@ impl From for PyMatchSpec { } } -impl Into for PyMatchSpec { - fn into(self) -> MatchSpec { - self.inner +impl From for MatchSpec { + fn from(value: PyMatchSpec) -> Self { + value.inner } } @@ -64,9 +64,9 @@ impl From for PyNamelessMatchSpec { } } -impl Into for PyNamelessMatchSpec { - fn into(self) -> NamelessMatchSpec { - self.inner +impl From for NamelessMatchSpec { + fn from(val: PyNamelessMatchSpec) -> Self { + val.inner } } diff --git a/py-rattler/src/repo_data/package_record.rs b/py-rattler/src/repo_data/package_record.rs index 419d6f6fa..98933685b 100644 --- a/py-rattler/src/repo_data/package_record.rs +++ b/py-rattler/src/repo_data/package_record.rs @@ -15,9 +15,9 @@ impl From for PyPackageRecord { } } -impl Into for PyPackageRecord { - fn into(self) -> PackageRecord { - self.inner +impl From for PackageRecord { + fn from(val: PyPackageRecord) -> Self { + val.inner } } From 7e229b0dc5a2e5044a08e9e038783dc1cc3675f0 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Thu, 24 Aug 2023 14:03:24 +0530 Subject: [PATCH 04/22] fix: make Version property names more explicit and raise exceptions instead of implicitly returning None --- py-rattler/rattler/version/version.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index b3ba744fd..0da68996e 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple -from rattler.rattler import PyVersion +from rattler.rattler import PyVersion, InvalidVersionError class Version: @@ -55,7 +55,7 @@ def bump(self) -> Version: return Version._from_py_version(self._version.bump()) @property - def local(self) -> bool: + def has_local(self) -> bool: """ Returns true if this version has a local segment defined. @@ -80,7 +80,7 @@ def as_major_minor(self) -> Optional[Tuple[int, int]]: return self._version.as_major_minor() @property - def dev(self) -> bool: + def is_dev(self) -> bool: """ Returns true if the version contains a component name "dev". @@ -112,7 +112,7 @@ def compatible_with(self, other: Version) -> bool: """ return self._version.compatible_with(other._version) - def pop_segments(self, n: int = 1) -> Optional[Version]: + def pop_segments(self, n: int = 1) -> Version: """ Pops `n` number of segments from the version and returns the new version. Returns `None` if the version becomes @@ -120,10 +120,11 @@ def pop_segments(self, n: int = 1) -> Optional[Version]: """ new_py_version = self._version.pop_segments(n) if new_py_version: - # maybe it should raise an exception instead? return self._from_py_version(new_py_version) + else: + raise InvalidVersionError("Version must have atleast 1 valid segment") - def with_segments(self, start: int, stop: int) -> Optional[Version]: + def with_segments(self, start: int, stop: int) -> Version: """ Returns new version with with segments ranging from `start` to `stop`. `stop` is exclusive. @@ -132,9 +133,9 @@ def with_segments(self, start: int, stop: int) -> Optional[Version]: if new_py_version: return self._from_py_version(new_py_version) else: - # maybe it should raise an exception instead? - return None + raise InvalidVersionError("Invalid segment range provided") + @property def segment_count(self) -> int: """ Returns the number of segments in the version. From afd820f3ab6fe07e895b941d2174e4fbd2ed45f1 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Thu, 24 Aug 2023 17:14:26 +0530 Subject: [PATCH 05/22] doc: add better docs and examples --- py-rattler/rattler/version/version.py | 88 ++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index 0da68996e..156873515 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -6,6 +6,17 @@ class Version: + """ + This class implements an order relation between version strings. + Version strings can contain the usual alphanumeric characters + (A-Za-z0-9), separated into segments by dots and underscores. + Empty segments (i.e. two consecutive dots, a leading/trailing + underscore) are not permitted. An optional epoch number - an + integer followed by '!' - can precede the actual version string + (this is useful to indicate a change in the versioning scheme itself). + Version comparison is case-insensitive. + """ + def __init__(self, version: str): if isinstance(version, str): self._version = PyVersion(version) @@ -58,18 +69,21 @@ def bump(self) -> Version: def has_local(self) -> bool: """ Returns true if this version has a local segment defined. + The local part of a version is the part behind the (optional) `+`. Examples -------- - >>> v = Version('1.0') - >>> v.local - False + >>> v = Version('1.0+3.2-alpha0') + >>> v.has_local + True """ return self._version.has_local() def as_major_minor(self) -> Optional[Tuple[int, int]]: """ Returns the major and minor segments from the version. + Requires a minimum of 2 segments in version to be split + into major and minor, returns `None` otherwise. Examples -------- @@ -82,12 +96,16 @@ def as_major_minor(self) -> Optional[Tuple[int, int]]: @property def is_dev(self) -> bool: """ - Returns true if the version contains a component name "dev". + Returns true if the version contains a component name "dev", + dev versions are sorted before non-dev version. Examples -------- >>> v = Version('1.0.1dev') - >>> v.dev + >>> v.is_dev + True + >>> v_non_dev = Version('1.0.1') + >>> v_non_dev > v True """ return self._version.is_dev() @@ -109,25 +127,65 @@ def starts_with(self, other: Version) -> bool: def compatible_with(self, other: Version) -> bool: """ Checks if this version is compatible with other version. + Minor versions changes are compatible with older versions, + major version changes are breaking and will not be compatible. + + Examples + -------- + >>> v1 = Version('1.0.1') + >>> v2 = Version('1.2') + >>> v_major = Version('2.0') + >>> v1.compatible_with(v2) + False + >>> v2.compatible_with(v1) + True + >>> v_major.compatible_with(v2) + False + >>> v2.compatible_with(v_major) + False """ return self._version.compatible_with(other._version) def pop_segments(self, n: int = 1) -> Version: """ Pops `n` number of segments from the version and returns - the new version. Returns `None` if the version becomes - invalid due to the operation. + the new version. Raises `InvalidVersionError` if version + becomes invalid due to the operation. + + Examples + -------- + >>> v = Version('2!1.0.1') + >>> v.pop_segments() # `n` defaults to 1 if left empty + 2!1.0 + >>> v.pop_segments(2) # `pop_segments` returns new Version object so old + version is still usable + 2!1 + >>> v.pop_segments(3) + Traceback (most recent call last): + File "", line 1, in + File "/rattler/version/version.py", line 132, in pop_segments + def pop_segments(self, n: int = 1) -> Version: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + exceptions.InvalidVersionException: new Version must have atleast 1 valid + segment """ new_py_version = self._version.pop_segments(n) if new_py_version: return self._from_py_version(new_py_version) else: - raise InvalidVersionError("Version must have atleast 1 valid segment") + raise InvalidVersionError("new Version must have atleast 1 valid segment") def with_segments(self, start: int, stop: int) -> Version: """ Returns new version with with segments ranging from `start` to `stop`. - `stop` is exclusive. + `stop` is exclusive. Raises `InvalidVersionError` if the provided range + is invalid. + + Examples + -------- + >>> v = Version('2!1.2.3') + >>> v.with_segments(0, 2) + 2!1.2 """ new_py_version = self._version.with_segments(start, stop) if new_py_version: @@ -139,12 +197,24 @@ def with_segments(self, start: int, stop: int) -> Version: def segment_count(self) -> int: """ Returns the number of segments in the version. + + Examples + -------- + >>> v = Version('2!1.2.3') + >>> v.segment_count + 3 """ return self._version.segment_count() def strip_local(self) -> Version: """ Returns a new version with local segment stripped. + + Examples + -------- + >>> v = Version('1.2.3+4.alpha-5') + >>> v.strip_local() + 1.2.3 """ return self._from_py_version(self._version.strip_local()) From fbb36f46891fe8b06cbab6136a6b31d430f704f1 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Thu, 24 Aug 2023 17:14:51 +0530 Subject: [PATCH 06/22] fix: fix typo in segment_count name --- py-rattler/src/version.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py-rattler/src/version.rs b/py-rattler/src/version.rs index e79f235ce..c8ff811a6 100644 --- a/py-rattler/src/version.rs +++ b/py-rattler/src/version.rs @@ -81,7 +81,7 @@ impl PyVersion { } /// Returns the number of segments in the version. - pub fn segnment_count(&self) -> usize { + pub fn segment_count(&self) -> usize { self.inner.segment_count() } From 61413f4bfb7d7d3cede7bb00844b12a26db929b2 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Thu, 24 Aug 2023 19:00:55 +0530 Subject: [PATCH 07/22] fix: make segments and local_segments methods public --- crates/rattler_conda_types/src/version/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/rattler_conda_types/src/version/mod.rs b/crates/rattler_conda_types/src/version/mod.rs index e575b7bf4..409139dd8 100644 --- a/crates/rattler_conda_types/src/version/mod.rs +++ b/crates/rattler_conda_types/src/version/mod.rs @@ -207,7 +207,7 @@ impl Version { } /// Returns the individual segments of the version. - fn segments( + pub fn segments( &self, ) -> impl Iterator> + DoubleEndedIterator + ExactSizeIterator + '_ { let mut idx = if self.has_epoch() { 1 } else { 0 }; @@ -305,7 +305,7 @@ impl Version { /// 1.2+3.2.1-alpha0 /// ^^^^^^^^^^^^ This is the local part of the version /// ``` - fn local_segments( + pub fn local_segments( &self, ) -> impl Iterator> + DoubleEndedIterator + ExactSizeIterator + '_ { if let Some(start) = self.local_segment_index() { @@ -671,7 +671,7 @@ impl<'v, I: Iterator> + 'v> fmt::Display for SegmentForma /// Either a number, literal or the infinity. #[derive(Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -enum Component { +pub enum Component { Numeral(u64), /// Post should always be ordered greater than anything else. @@ -887,7 +887,7 @@ impl<'de> Deserialize<'de> for Version { } } -struct SegmentIter<'v> { +pub struct SegmentIter<'v> { /// Information about the segment we are iterating. segment: Segment, From 78b2f224d686fdefcc6672ff2179038f995aaeb4 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Thu, 24 Aug 2023 19:07:38 +0530 Subject: [PATCH 08/22] feat: add segments and local_segments method to version --- py-rattler/rattler/version/version.py | 26 +++++++++++++++++++++++++- py-rattler/src/version.rs | 14 ++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index 156873515..7eee5cc35 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Tuple +from typing import List, Optional, Tuple from rattler.rattler import PyVersion, InvalidVersionError @@ -79,6 +79,30 @@ def has_local(self) -> bool: """ return self._version.has_local() + def segments(self) -> List[List[str]]: + """ + Returns a list of segments of the version. It does not contain + the local segment of the version. + + Examples + -------- + >>> v = Version("1.2dev.3-alpha4.5+6.8") + [['1'], ['2', 'dev'], ['3'], ['0', 'alpha', '4'], ['5']] + """ + return self._version.segments() + + def local_segments(self) -> List[List[str]]: + """ + Returns a list of local segments of the version. It does not + contain the non-local segment of the version. + + Examples + -------- + >>> v = Version("1.2dev.3-alpha4.5+6.8") + [['6'], ['8']] + """ + return self._version.local_segments() + def as_major_minor(self) -> Optional[Tuple[int, int]]: """ Returns the major and minor segments from the version. diff --git a/py-rattler/src/version.rs b/py-rattler/src/version.rs index c8ff811a6..fecf4c0d0 100644 --- a/py-rattler/src/version.rs +++ b/py-rattler/src/version.rs @@ -70,6 +70,20 @@ impl PyVersion { }) } + pub fn segments(&self) -> Vec> { + self.inner + .segments() + .map(|s| s.components().map(|c| format!("{c}")).collect::>()) + .collect::>() + } + + pub fn local_segments(&self) -> Vec> { + self.inner + .local_segments() + .map(|s| s.components().map(|c| format!("{c}")).collect::>()) + .collect::>() + } + /// Returns new version with with segments ranging from `start` to `stop`. /// `stop` is exclusive. pub fn with_segments(&self, start: usize, stop: usize) -> Option { From 3e270c1122e5a4ab7d643ea55602a885e999c29d Mon Sep 17 00:00:00 2001 From: Wackyator Date: Thu, 24 Aug 2023 21:00:56 +0530 Subject: [PATCH 09/22] test: fix tests and remove reduntant unit tests --- py-rattler/rattler/version/version.py | 22 ++++------ py-rattler/tests/unit/test_version.py | 61 --------------------------- 2 files changed, 9 insertions(+), 74 deletions(-) delete mode 100644 py-rattler/tests/unit/test_version.py diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index 7eee5cc35..528385ef2 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -87,6 +87,7 @@ def segments(self) -> List[List[str]]: Examples -------- >>> v = Version("1.2dev.3-alpha4.5+6.8") + >>> v.segments() [['1'], ['2', 'dev'], ['3'], ['0', 'alpha', '4'], ['5']] """ return self._version.segments() @@ -99,6 +100,7 @@ def local_segments(self) -> List[List[str]]: Examples -------- >>> v = Version("1.2dev.3-alpha4.5+6.8") + >>> v.local_segments() [['6'], ['8']] """ return self._version.local_segments() @@ -126,11 +128,9 @@ def is_dev(self) -> bool: Examples -------- >>> v = Version('1.0.1dev') - >>> v.is_dev - True + >>> _ = v.is_dev >>> v_non_dev = Version('1.0.1') - >>> v_non_dev > v - True + >>> _ = v_non_dev > v """ return self._version.is_dev() @@ -156,7 +156,7 @@ def compatible_with(self, other: Version) -> bool: Examples -------- - >>> v1 = Version('1.0.1') + >>> v1 = Version('1.0') >>> v2 = Version('1.2') >>> v_major = Version('2.0') >>> v1.compatible_with(v2) @@ -181,15 +181,10 @@ def pop_segments(self, n: int = 1) -> Version: >>> v = Version('2!1.0.1') >>> v.pop_segments() # `n` defaults to 1 if left empty 2!1.0 - >>> v.pop_segments(2) # `pop_segments` returns new Version object so old - version is still usable + >>> v.pop_segments(2) # old version is still usable 2!1 - >>> v.pop_segments(3) + >>> v.pop_segments(3) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): - File "", line 1, in - File "/rattler/version/version.py", line 132, in pop_segments - def pop_segments(self, n: int = 1) -> Version: - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ exceptions.InvalidVersionException: new Version must have atleast 1 valid segment """ @@ -197,7 +192,8 @@ def pop_segments(self, n: int = 1) -> Version: if new_py_version: return self._from_py_version(new_py_version) else: - raise InvalidVersionError("new Version must have atleast 1 valid segment") + raise InvalidVersionError( + "new Version must have atleast 1 valid segment") def with_segments(self, start: int, stop: int) -> Version: """ diff --git a/py-rattler/tests/unit/test_version.py b/py-rattler/tests/unit/test_version.py deleted file mode 100644 index 494d82d91..000000000 --- a/py-rattler/tests/unit/test_version.py +++ /dev/null @@ -1,61 +0,0 @@ -from rattler import Version - - -def test_version_comparision(): - assert Version("1.0") < Version("2.0") - assert Version("1.0") <= Version("2.0") - assert Version("2.0") > Version("1.0") - assert Version("2.0") >= Version("1.0") - assert Version("1.0.0") == Version("1.0") - assert Version("1.0") != Version("2.0") - - -def test_bump(): - assert Version("1.0").bump() == Version("1.1") - assert Version("1.a").bump() == Version("1.1a") - assert Version("1dev").bump() == Version("2dev") - assert Version("1dev0").bump() == Version("1dev1") - assert Version("1!0").bump() == Version("1!1") - assert Version("1.2-alpha.3-beta-dev0").bump() == Version("1.2-alpha.3-beta-dev1") - - -def test_epoch(): - assert Version("1!1.0").epoch == 1 - assert Version("1.0").epoch is None - - -def test_dev(): - assert Version("1.2-alpha.3-beta-dev0").dev is True - assert Version("1.2-alpha.3").dev is False - - -def test_local(): - assert Version("1.0+1.2").local is True - assert Version("1.0").local is False - - -def test_as_major_minor(): - assert Version("2.3.4").as_major_minor() == (2, 3) - assert Version("1.2-alpha.3-beta-dev0").as_major_minor() == (1, 2) - - -def test_starts_with(): - assert Version("1.0.6").starts_with(Version("1.0")) is True - assert Version("1.0.6").starts_with(Version("1.6")) is False - - -def test_compatible_with(): - assert Version("1.6").compatible_with(Version("1.5")) is True - assert Version("1.6").compatible_with(Version("1.7")) is False - assert Version("1.6").compatible_with(Version("2.0")) is False - - -def test_pop_segments(): - assert Version("1.6.0").pop_segments() == Version("1.6") - assert Version("1.6.0").pop_segments(2) == Version("1") - assert Version("1.6.0").pop_segments(3) is None - - -def test_strip_local(): - assert Version("1.6+2.0").strip_local() == Version("1.6") - assert Version("1.6").strip_local() == Version("1.6") From b18daab26f401f159ae83b58a6856f5c7dd0fba6 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Thu, 24 Aug 2023 21:04:01 +0530 Subject: [PATCH 10/22] chore: fix formatting --- py-rattler/rattler/version/version.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index 528385ef2..12cd8f544 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -192,8 +192,7 @@ def pop_segments(self, n: int = 1) -> Version: if new_py_version: return self._from_py_version(new_py_version) else: - raise InvalidVersionError( - "new Version must have atleast 1 valid segment") + raise InvalidVersionError("new Version must have atleast 1 valid segment") def with_segments(self, start: int, stop: int) -> Version: """ From ed503e7fc8dd27dba656c802140d29190e77043c Mon Sep 17 00:00:00 2001 From: Wackyator Date: Fri, 25 Aug 2023 17:03:37 +0530 Subject: [PATCH 11/22] chore: move py-rattler classes and structs to seperate files --- py-rattler/rattler/match_spec/__init__.py | 3 +- py-rattler/rattler/match_spec/match_spec.py | 44 +------------- .../rattler/match_spec/nameless_match_spec.py | 46 +++++++++++++++ py-rattler/src/lib.rs | 4 +- py-rattler/src/match_spec.rs | 59 ++----------------- py-rattler/src/nameless_match_spec.rs | 59 +++++++++++++++++++ 6 files changed, 117 insertions(+), 98 deletions(-) create mode 100644 py-rattler/rattler/match_spec/nameless_match_spec.py create mode 100644 py-rattler/src/nameless_match_spec.rs diff --git a/py-rattler/rattler/match_spec/__init__.py b/py-rattler/rattler/match_spec/__init__.py index 1701a0eb4..a6be9d545 100644 --- a/py-rattler/rattler/match_spec/__init__.py +++ b/py-rattler/rattler/match_spec/__init__.py @@ -1,3 +1,4 @@ -from rattler.match_spec.match_spec import MatchSpec, NamelessMatchSpec +from rattler.match_spec.match_spec import MatchSpec +from rattler.match_spec.nameless_match_spec import NamelessMatchSpec __all__ = ["MatchSpec", "NamelessMatchSpec"] diff --git a/py-rattler/rattler/match_spec/match_spec.py b/py-rattler/rattler/match_spec/match_spec.py index f8e7ca968..cef1b454c 100644 --- a/py-rattler/rattler/match_spec/match_spec.py +++ b/py-rattler/rattler/match_spec/match_spec.py @@ -1,6 +1,7 @@ from __future__ import annotations -from rattler.rattler import PyMatchSpec, PyNamelessMatchSpec +from rattler.match_spec import NamelessMatchSpec +from rattler.rattler import PyMatchSpec from rattler.repo_data import PackageRecord @@ -43,44 +44,3 @@ def __str__(self) -> str: def __repr__(self) -> str: return self.__str__() - - -class NamelessMatchSpec: - def __init__(self, spec: str): - if isinstance(spec, str): - self._nameless_match_spec = PyNamelessMatchSpec(spec) - else: - raise TypeError( - "NamelessMatchSpec constructor received unsupported type" - f" {type(spec).__name__!r} for the 'spec' parameter" - ) - - def matches(self, package_record: PackageRecord) -> bool: - return self._nameless_match_spec.matches(package_record._package_record) - - @classmethod - def _from_py_nameless_match_spec( - cls, py_nameless_match_spec: PyNamelessMatchSpec - ) -> NamelessMatchSpec: - """ - Construct py-rattler NamelessMatchSpec from PyNamelessMatchSpec FFI object. - """ - nameless_match_spec = cls.__new__(cls) - nameless_match_spec._nameless_match_spec = py_nameless_match_spec - - return nameless_match_spec - - @staticmethod - def from_match_spec(spec: MatchSpec) -> NamelessMatchSpec: - """ - Constructs a PyNamelessMatchSpec from a PyMatchSpec. - """ - return NamelessMatchSpec._from_py_nameless_match_spec( - PyNamelessMatchSpec.from_match_spec(spec._match_spec) - ) - - def __str__(self) -> str: - return self._nameless_match_spec.as_str() - - def __repr__(self) -> str: - return self.__str__() diff --git a/py-rattler/rattler/match_spec/nameless_match_spec.py b/py-rattler/rattler/match_spec/nameless_match_spec.py new file mode 100644 index 000000000..eb0679165 --- /dev/null +++ b/py-rattler/rattler/match_spec/nameless_match_spec.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from rattler.match_spec import MatchSpec +from rattler.rattler import PyNamelessMatchSpec +from rattler.repo_data import PackageRecord + + +class NamelessMatchSpec: + def __init__(self, spec: str): + if isinstance(spec, str): + self._nameless_match_spec = PyNamelessMatchSpec(spec) + else: + raise TypeError( + "NamelessMatchSpec constructor received unsupported type" + f" {type(spec).__name__!r} for the 'spec' parameter" + ) + + def matches(self, package_record: PackageRecord) -> bool: + return self._nameless_match_spec.matches(package_record._package_record) + + @classmethod + def _from_py_nameless_match_spec( + cls, py_nameless_match_spec: PyNamelessMatchSpec + ) -> NamelessMatchSpec: + """ + Construct py-rattler NamelessMatchSpec from PyNamelessMatchSpec FFI object. + """ + nameless_match_spec = cls.__new__(cls) + nameless_match_spec._nameless_match_spec = py_nameless_match_spec + + return nameless_match_spec + + @staticmethod + def from_match_spec(spec: MatchSpec) -> NamelessMatchSpec: + """ + Constructs a NamelessMatchSpec from a MatchSpec. + """ + return NamelessMatchSpec._from_py_nameless_match_spec( + PyNamelessMatchSpec.from_match_spec(spec._match_spec) + ) + + def __str__(self) -> str: + return self._nameless_match_spec.as_str() + + def __repr__(self) -> str: + return self.__str__() diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index 967baa59a..e6d32d4e8 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -1,10 +1,12 @@ mod error; mod match_spec; +mod nameless_match_spec; mod repo_data; mod version; use error::{InvalidMatchSpecException, InvalidVersionException, PyRattlerError}; -use match_spec::{PyMatchSpec, PyNamelessMatchSpec}; +use match_spec::PyMatchSpec; +use nameless_match_spec::PyNamelessMatchSpec; use repo_data::package_record::PyPackageRecord; use version::PyVersion; diff --git a/py-rattler/src/match_spec.rs b/py-rattler/src/match_spec.rs index f003ae99c..d28da0250 100644 --- a/py-rattler/src/match_spec.rs +++ b/py-rattler/src/match_spec.rs @@ -1,8 +1,11 @@ use pyo3::{pyclass, pymethods}; -use rattler_conda_types::{MatchSpec, NamelessMatchSpec}; +use rattler_conda_types::MatchSpec; use std::str::FromStr; -use crate::{error::PyRattlerError, repo_data::package_record::PyPackageRecord}; +use crate::{ + error::PyRattlerError, nameless_match_spec::PyNamelessMatchSpec, + repo_data::package_record::PyPackageRecord, +}; #[pyclass] #[repr(transparent)] @@ -50,55 +53,3 @@ impl PyMatchSpec { } } } - -#[pyclass] -#[repr(transparent)] -#[derive(Clone)] -pub struct PyNamelessMatchSpec { - inner: NamelessMatchSpec, -} - -impl From for PyNamelessMatchSpec { - fn from(value: NamelessMatchSpec) -> Self { - Self { inner: value } - } -} - -impl From for NamelessMatchSpec { - fn from(val: PyNamelessMatchSpec) -> Self { - val.inner - } -} - -impl From for PyNamelessMatchSpec { - fn from(value: PyMatchSpec) -> Self { - let inner: NamelessMatchSpec = Into::::into(value).into(); - Self { inner } - } -} - -#[pymethods] -impl PyNamelessMatchSpec { - #[new] - pub fn __init__(spec: &str) -> pyo3::PyResult { - Ok(NamelessMatchSpec::from_str(spec) - .map(Into::into) - .map_err(PyRattlerError::from)?) - } - - /// Returns a string representation of MatchSpec - pub fn as_str(&self) -> String { - format!("{}", self.inner) - } - - /// Match a PyNamelessMatchSpec against a PyPackageRecord - pub fn matches(&self, record: &PyPackageRecord) -> bool { - self.inner.matches(&record.clone().into()) - } - - /// Constructs a [`PyNamelessMatchSpec`] from a [`PyMatchSpec`]. - #[staticmethod] - pub fn from_match_spec(spec: &PyMatchSpec) -> Self { - Into::::into(spec.clone()) - } -} diff --git a/py-rattler/src/nameless_match_spec.rs b/py-rattler/src/nameless_match_spec.rs new file mode 100644 index 000000000..e1a052561 --- /dev/null +++ b/py-rattler/src/nameless_match_spec.rs @@ -0,0 +1,59 @@ +use pyo3::{pyclass, pymethods}; +use rattler_conda_types::{MatchSpec, NamelessMatchSpec}; +use std::str::FromStr; + +use crate::{ + error::PyRattlerError, match_spec::PyMatchSpec, repo_data::package_record::PyPackageRecord, +}; + +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PyNamelessMatchSpec { + inner: NamelessMatchSpec, +} + +impl From for PyNamelessMatchSpec { + fn from(value: NamelessMatchSpec) -> Self { + Self { inner: value } + } +} + +impl From for NamelessMatchSpec { + fn from(val: PyNamelessMatchSpec) -> Self { + val.inner + } +} + +impl From for PyNamelessMatchSpec { + fn from(value: PyMatchSpec) -> Self { + let inner: NamelessMatchSpec = Into::::into(value).into(); + Self { inner } + } +} + +#[pymethods] +impl PyNamelessMatchSpec { + #[new] + pub fn __init__(spec: &str) -> pyo3::PyResult { + Ok(NamelessMatchSpec::from_str(spec) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Returns a string representation of MatchSpec + pub fn as_str(&self) -> String { + format!("{}", self.inner) + } + + /// Match a PyNamelessMatchSpec against a PyPackageRecord + pub fn matches(&self, record: &PyPackageRecord) -> bool { + self.inner.matches(&record.clone().into()) + } + + /// Constructs a [`PyNamelessMatchSpec`] from a [`PyMatchSpec`]. + #[staticmethod] + pub fn from_match_spec(spec: &PyMatchSpec) -> Self { + Into::::into(spec.clone()) + } +} From b2145f74b926c6aaf9b776228e07616146c738a6 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Fri, 25 Aug 2023 18:44:41 +0530 Subject: [PATCH 12/22] doc: improve py-rattler docs --- py-rattler/rattler/match_spec/match_spec.py | 68 +++++++++++++++++++ .../rattler/match_spec/nameless_match_spec.py | 6 ++ .../rattler/repo_data/package_record.py | 6 ++ py-rattler/rattler/version/version.py | 10 ++- py-rattler/src/version.rs | 5 ++ 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/py-rattler/rattler/match_spec/match_spec.py b/py-rattler/rattler/match_spec/match_spec.py index cef1b454c..8622c3b4d 100644 --- a/py-rattler/rattler/match_spec/match_spec.py +++ b/py-rattler/rattler/match_spec/match_spec.py @@ -6,6 +6,74 @@ class MatchSpec: + """ + A `MatchSpec` is a query language for conda packages. + It can be composed of any of the attributes of `PackageRecord`. + + `MatchSpec` can be composed of keyword arguments, where keys are + any of the attributes of `PackageRecord`. Values for keyword arguments + are exact values the attributes should match against. Many fields can + be matched against non-exact values by including wildcard `*` and `>`/`<` + ranges where supported. Any non-specified field is the equivalent of a + full wildcard match. + + MatchSpecs can also be composed using a single positional argument, with optional + keyword arguments. Keyword arguments also override any conflicting information + provided in the positional argument. Conda has historically had several string + representations for equivalent MatchSpecs. + + A series of rules are now followed for creating the canonical string + representation of a MatchSpec instance. The canonical string representation can + generically be represented by: + + `(channel(/subdir):(namespace):)name(version(build))[key1=value1,key2=value2]` + + where `()` indicate optional fields. + + The rules for constructing a canonical string representation are: + + 1. `name` (i.e. "package name") is required, but its value can be '*'. Its + position is always outside the key-value brackets. + 2. If `version` is an exact version, it goes outside the key-value brackets and + is prepended by `==`. If `version` is a "fuzzy" value (e.g. `1.11.*`), it goes + outside the key-value brackets with the `.*` left off and is prepended by `=`. + Otherwise `version` is included inside key-value brackets. + 3. If `version` is an exact version, and `build` is an exact value, `build` goes + outside key-value brackets prepended by a `=`. Otherwise, `build` goes inside + key-value brackets. `build_string` is an alias for `build`. + 4. The `namespace` position is being held for a future feature. It is currently + ignored. + 5. If `channel` is included and is an exact value, a `::` separator is used between + `channel` and `name`. `channel` can either be a canonical channel name or a + channel url. In the canonical string representation, the canonical channel name + will always be used. + 6. If `channel` is an exact value and `subdir` is an exact value, `subdir` is + appended to `channel` with a `/` separator. Otherwise, `subdir` is included in + the key-value brackets. + 7. Key-value brackets can be delimited by comma, space, or comma+space. Value can + optionally be wrapped in single or double quotes, but must be wrapped if `value` + contains a comma, space, or equal sign. The canonical format uses comma delimiters + and single quotes. + 8. When constructing a `MatchSpec` instance from a string, any key-value pair given + inside the key-value brackets overrides any matching parameter given outside the + brackets. + + When `MatchSpec` attribute values are simple strings, the are interpreted using the + following conventions: + - If the string begins with `^` and ends with `$`, it is converted to a regex. + - If the string contains an asterisk (`*`), it is transformed from a glob to a + regex. + - Otherwise, an exact match to the string is sought. + + To fully-specify a package with a full, exact spec, the following fields must be + given as exact values: + - channel + - subdir + - name + - version + - build + """ + def __init__(self, spec: str): if isinstance(spec, str): self._match_spec = PyMatchSpec(spec) diff --git a/py-rattler/rattler/match_spec/nameless_match_spec.py b/py-rattler/rattler/match_spec/nameless_match_spec.py index eb0679165..f64f3ed92 100644 --- a/py-rattler/rattler/match_spec/nameless_match_spec.py +++ b/py-rattler/rattler/match_spec/nameless_match_spec.py @@ -6,6 +6,12 @@ class NamelessMatchSpec: + """ + Similar to a `MatchSpec` but does not include the package name. + This is useful in places where the package name is already known + (e.g. `foo = "3.4.1 *cuda"`). + """ + def __init__(self, spec: str): if isinstance(spec, str): self._nameless_match_spec = PyNamelessMatchSpec(spec) diff --git a/py-rattler/rattler/repo_data/package_record.py b/py-rattler/rattler/repo_data/package_record.py index be02fd06d..ddc576802 100644 --- a/py-rattler/rattler/repo_data/package_record.py +++ b/py-rattler/rattler/repo_data/package_record.py @@ -4,6 +4,12 @@ class PackageRecord: + """ + A single record in the Conda repodata. A single + record refers to a single binary distribution + of a package on a Conda channel. + """ + def __init__(self): self._package_record = PyPackageRecord() diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index 12cd8f544..5f40ea321 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -76,6 +76,9 @@ def has_local(self) -> bool: >>> v = Version('1.0+3.2-alpha0') >>> v.has_local True + >>> v2 = Version('1.0') + >>> v.has_local + False """ return self._version.has_local() @@ -128,9 +131,11 @@ def is_dev(self) -> bool: Examples -------- >>> v = Version('1.0.1dev') - >>> _ = v.is_dev + >>> v.is_dev + True >>> v_non_dev = Version('1.0.1') - >>> _ = v_non_dev > v + >>> v_non_dev >= v + False """ return self._version.is_dev() @@ -216,6 +221,7 @@ def with_segments(self, start: int, stop: int) -> Version: def segment_count(self) -> int: """ Returns the number of segments in the version. + This does not include epoch or local segment of the version Examples -------- diff --git a/py-rattler/src/version.rs b/py-rattler/src/version.rs index fecf4c0d0..32aaabbdb 100644 --- a/py-rattler/src/version.rs +++ b/py-rattler/src/version.rs @@ -70,6 +70,9 @@ impl PyVersion { }) } + /// Returns a list of segments of the version. It does not contain + /// the local segment of the version. See `local_segments` for + /// local segments in version. pub fn segments(&self) -> Vec> { self.inner .segments() @@ -77,6 +80,8 @@ impl PyVersion { .collect::>() } + /// Returns a list of local segments of the version. It does not + /// contain the non-local segment of the version. pub fn local_segments(&self) -> Vec> { self.inner .local_segments() From 94de68ad6fada7844e84923ad29eaf79338cb1f3 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Fri, 25 Aug 2023 19:53:15 +0530 Subject: [PATCH 13/22] fix: fix type checking bug and make staticmethods classmethods --- py-rattler/rattler/match_spec/match_spec.py | 14 ++++++++------ .../rattler/match_spec/nameless_match_spec.py | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/py-rattler/rattler/match_spec/match_spec.py b/py-rattler/rattler/match_spec/match_spec.py index 8622c3b4d..a66c78350 100644 --- a/py-rattler/rattler/match_spec/match_spec.py +++ b/py-rattler/rattler/match_spec/match_spec.py @@ -1,8 +1,10 @@ from __future__ import annotations +from typing import Self, TYPE_CHECKING -from rattler.match_spec import NamelessMatchSpec from rattler.rattler import PyMatchSpec -from rattler.repo_data import PackageRecord +if TYPE_CHECKING: + from rattler.match_spec import NamelessMatchSpec + from rattler.repo_data import PackageRecord class MatchSpec: @@ -84,7 +86,7 @@ def __init__(self, spec: str): ) @classmethod - def _from_py_match_spec(cls, py_match_spec: PyMatchSpec) -> MatchSpec: + def _from_py_match_spec(cls, py_match_spec: PyMatchSpec) -> Self: """ Construct py-rattler MatchSpec from PyMatchSpec FFI object. """ @@ -97,13 +99,13 @@ def matches(self, record: PackageRecord) -> bool: """Match a MatchSpec against a PackageRecord.""" return self._match_spec.matches(record._package_record) - @staticmethod - def from_nameless(spec: NamelessMatchSpec, name: str) -> MatchSpec: + @classmethod + def from_nameless(cls, spec: NamelessMatchSpec, name: str) -> Self: """ Constructs a MatchSpec from a NamelessMatchSpec and a name. """ - return MatchSpec._from_py_match_spec( + return cls._from_py_match_spec( PyMatchSpec.from_nameless(spec._nameless_match_spec, name) ) diff --git a/py-rattler/rattler/match_spec/nameless_match_spec.py b/py-rattler/rattler/match_spec/nameless_match_spec.py index f64f3ed92..8e95dc434 100644 --- a/py-rattler/rattler/match_spec/nameless_match_spec.py +++ b/py-rattler/rattler/match_spec/nameless_match_spec.py @@ -1,8 +1,10 @@ from __future__ import annotations +from typing import Self, TYPE_CHECKING -from rattler.match_spec import MatchSpec from rattler.rattler import PyNamelessMatchSpec -from rattler.repo_data import PackageRecord +if TYPE_CHECKING: + from rattler.match_spec import MatchSpec + from rattler.repo_data import PackageRecord class NamelessMatchSpec: @@ -27,7 +29,7 @@ def matches(self, package_record: PackageRecord) -> bool: @classmethod def _from_py_nameless_match_spec( cls, py_nameless_match_spec: PyNamelessMatchSpec - ) -> NamelessMatchSpec: + ) -> Self: """ Construct py-rattler NamelessMatchSpec from PyNamelessMatchSpec FFI object. """ @@ -36,12 +38,12 @@ def _from_py_nameless_match_spec( return nameless_match_spec - @staticmethod - def from_match_spec(spec: MatchSpec) -> NamelessMatchSpec: + @classmethod + def from_match_spec(cls, spec: MatchSpec) -> Self: """ Constructs a NamelessMatchSpec from a MatchSpec. """ - return NamelessMatchSpec._from_py_nameless_match_spec( + return cls._from_py_nameless_match_spec( PyNamelessMatchSpec.from_match_spec(spec._match_spec) ) From 7b3be8db5727980abea8ced248bb438d4a2eb747 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Fri, 25 Aug 2023 21:01:30 +0530 Subject: [PATCH 14/22] add docs to Component and expose it --- crates/rattler_conda_types/src/lib.rs | 4 +++- crates/rattler_conda_types/src/version/mod.rs | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/rattler_conda_types/src/lib.rs b/crates/rattler_conda_types/src/lib.rs index 5c2d8a928..671d34a35 100644 --- a/crates/rattler_conda_types/src/lib.rs +++ b/crates/rattler_conda_types/src/lib.rs @@ -37,7 +37,9 @@ pub use repo_data::patches::{PackageRecordPatch, PatchInstructions, RepoDataPatc pub use repo_data::{ChannelInfo, ConvertSubdirError, PackageRecord, RepoData}; pub use repo_data_record::RepoDataRecord; pub use run_export::RunExportKind; -pub use version::{ParseVersionError, ParseVersionErrorKind, Version, VersionWithSource}; +pub use version::{ + Component, ParseVersionError, ParseVersionErrorKind, Version, VersionWithSource, +}; pub use version_spec::VersionSpec; #[cfg(test)] diff --git a/crates/rattler_conda_types/src/version/mod.rs b/crates/rattler_conda_types/src/version/mod.rs index 409139dd8..9e7a63bf0 100644 --- a/crates/rattler_conda_types/src/version/mod.rs +++ b/crates/rattler_conda_types/src/version/mod.rs @@ -672,6 +672,7 @@ impl<'v, I: Iterator> + 'v> fmt::Display for SegmentForma /// Either a number, literal or the infinity. #[derive(Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum Component { + /// Numeral Component. Numeral(u64), /// Post should always be ordered greater than anything else. @@ -686,11 +687,13 @@ pub enum Component { /// An underscore or dash. UnderscoreOrDash { + /// Dash flag. is_dash: bool, }, } impl Component { + /// Return a component as numeric value. pub fn as_number(&self) -> Option { match self { Component::Numeral(value) => Some(*value), @@ -698,6 +701,7 @@ impl Component { } } + /// Return a component as mutable numeric value. pub fn as_number_mut(&mut self) -> Option<&mut u64> { match self { Component::Numeral(value) => Some(value), @@ -705,6 +709,7 @@ impl Component { } } + /// Return a component as string value. #[allow(dead_code)] pub fn as_string(&self) -> Option<&str> { match self { @@ -713,16 +718,19 @@ impl Component { } } + /// Check whether a component is [`Component::Post`] #[allow(dead_code)] pub fn is_post(&self) -> bool { matches!(self, Component::Post) } + /// Check whether a component is [`Component::Dev`] #[allow(dead_code)] pub fn is_dev(&self) -> bool { matches!(self, Component::Dev) } + /// Check whether a component is [`Component::Numeral`] pub fn is_numeric(&self) -> bool { matches!(self, Component::Numeral(_)) } From e032a13b6560088c9cfbc388804b10239714a530 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Fri, 25 Aug 2023 21:04:48 +0530 Subject: [PATCH 15/22] fix: version segment and local segment should return segments as int and str --- py-rattler/rattler/match_spec/match_spec.py | 1 + .../rattler/match_spec/nameless_match_spec.py | 1 + py-rattler/rattler/version/version.py | 12 ++++---- py-rattler/src/component.rs | 30 +++++++++++++++++++ py-rattler/src/lib.rs | 1 + py-rattler/src/version.rs | 18 +++++++---- 6 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 py-rattler/src/component.rs diff --git a/py-rattler/rattler/match_spec/match_spec.py b/py-rattler/rattler/match_spec/match_spec.py index a66c78350..d8b4a705c 100644 --- a/py-rattler/rattler/match_spec/match_spec.py +++ b/py-rattler/rattler/match_spec/match_spec.py @@ -2,6 +2,7 @@ from typing import Self, TYPE_CHECKING from rattler.rattler import PyMatchSpec + if TYPE_CHECKING: from rattler.match_spec import NamelessMatchSpec from rattler.repo_data import PackageRecord diff --git a/py-rattler/rattler/match_spec/nameless_match_spec.py b/py-rattler/rattler/match_spec/nameless_match_spec.py index 8e95dc434..0835a937b 100644 --- a/py-rattler/rattler/match_spec/nameless_match_spec.py +++ b/py-rattler/rattler/match_spec/nameless_match_spec.py @@ -2,6 +2,7 @@ from typing import Self, TYPE_CHECKING from rattler.rattler import PyNamelessMatchSpec + if TYPE_CHECKING: from rattler.match_spec import MatchSpec from rattler.repo_data import PackageRecord diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index 5f40ea321..5413b3ad6 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from rattler.rattler import PyVersion, InvalidVersionError @@ -77,12 +77,12 @@ def has_local(self) -> bool: >>> v.has_local True >>> v2 = Version('1.0') - >>> v.has_local + >>> v2.has_local False """ return self._version.has_local() - def segments(self) -> List[List[str]]: + def segments(self) -> List[List[Optional[Union[str, int]]]]: """ Returns a list of segments of the version. It does not contain the local segment of the version. @@ -91,11 +91,11 @@ def segments(self) -> List[List[str]]: -------- >>> v = Version("1.2dev.3-alpha4.5+6.8") >>> v.segments() - [['1'], ['2', 'dev'], ['3'], ['0', 'alpha', '4'], ['5']] + [[1], [2, 'dev'], [3], [0, 'alpha', 4], [5]] """ return self._version.segments() - def local_segments(self) -> List[List[str]]: + def local_segments(self) -> List[List[Optional[Union[str, int]]]]: """ Returns a list of local segments of the version. It does not contain the non-local segment of the version. @@ -104,7 +104,7 @@ def local_segments(self) -> List[List[str]]: -------- >>> v = Version("1.2dev.3-alpha4.5+6.8") >>> v.local_segments() - [['6'], ['8']] + [[6], [8]] """ return self._version.local_segments() diff --git a/py-rattler/src/component.rs b/py-rattler/src/component.rs new file mode 100644 index 000000000..753ce5f60 --- /dev/null +++ b/py-rattler/src/component.rs @@ -0,0 +1,30 @@ +use pyo3::{IntoPy, PyObject, Python}; +use rattler_conda_types::Component; + +pub enum PyComponent { + String(String), + Number(u64), + None, +} + +impl IntoPy for PyComponent { + fn into_py(self, py: Python) -> PyObject { + match self { + Self::Number(val) => val.into_py(py), + Self::String(val) => val.into_py(py), + Self::None => py.None(), + } + } +} + +impl From for PyComponent { + fn from(value: Component) -> Self { + match value { + Component::Iden(v) => Self::String(v.to_string()), + Component::Numeral(n) => Self::Number(n), + Component::Dev => Self::String("dev".to_string()), + Component::Post => Self::String("post".to_string()), + _ => Self::None, + } + } +} diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index e6d32d4e8..6f06c0b97 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -1,3 +1,4 @@ +mod component; mod error; mod match_spec; mod nameless_match_spec; diff --git a/py-rattler/src/version.rs b/py-rattler/src/version.rs index 32aaabbdb..ef21f3cf1 100644 --- a/py-rattler/src/version.rs +++ b/py-rattler/src/version.rs @@ -1,4 +1,4 @@ -use crate::PyRattlerError; +use crate::{component::PyComponent, PyRattlerError}; use pyo3::{pyclass, pymethods}; use rattler_conda_types::Version; use std::str::FromStr; @@ -73,19 +73,27 @@ impl PyVersion { /// Returns a list of segments of the version. It does not contain /// the local segment of the version. See `local_segments` for /// local segments in version. - pub fn segments(&self) -> Vec> { + pub fn segments(&self) -> Vec> { self.inner .segments() - .map(|s| s.components().map(|c| format!("{c}")).collect::>()) + .map(|s| { + s.components() + .map(|c| c.to_owned().into()) + .collect::>() + }) .collect::>() } /// Returns a list of local segments of the version. It does not /// contain the non-local segment of the version. - pub fn local_segments(&self) -> Vec> { + pub fn local_segments(&self) -> Vec> { self.inner .local_segments() - .map(|s| s.components().map(|c| format!("{c}")).collect::>()) + .map(|s| { + s.components() + .map(|c| c.to_owned().into()) + .collect::>() + }) .collect::>() } From 533ae8c3ee7221f3dbc0aec03b804af9697f460d Mon Sep 17 00:00:00 2001 From: Wackyator Date: Fri, 25 Aug 2023 21:08:51 +0530 Subject: [PATCH 16/22] doc: add doc for NamelessMatchSpec --- py-rattler/rattler/match_spec/nameless_match_spec.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/py-rattler/rattler/match_spec/nameless_match_spec.py b/py-rattler/rattler/match_spec/nameless_match_spec.py index 0835a937b..032c33bdc 100644 --- a/py-rattler/rattler/match_spec/nameless_match_spec.py +++ b/py-rattler/rattler/match_spec/nameless_match_spec.py @@ -25,6 +25,9 @@ def __init__(self, spec: str): ) def matches(self, package_record: PackageRecord) -> bool: + """ + Match a MatchSpec against a PackageRecord + """ return self._nameless_match_spec.matches(package_record._package_record) @classmethod From 72938bf903698636aee47023a36c3a7dc364ed7b Mon Sep 17 00:00:00 2001 From: Wackyator Date: Fri, 25 Aug 2023 21:12:00 +0530 Subject: [PATCH 17/22] fix: remove redundant clone and make package record inner pub crate --- py-rattler/src/match_spec.rs | 2 +- py-rattler/src/repo_data/package_record.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/py-rattler/src/match_spec.rs b/py-rattler/src/match_spec.rs index d28da0250..ed6de86c0 100644 --- a/py-rattler/src/match_spec.rs +++ b/py-rattler/src/match_spec.rs @@ -42,7 +42,7 @@ impl PyMatchSpec { /// Match a MatchSpec against a PackageRecord pub fn matches(&self, record: &PyPackageRecord) -> bool { - self.inner.matches(&record.clone().into()) + self.inner.matches(&record.inner) } /// Constructs a PyMatchSpec from a PyNamelessMatchSpec and a name. diff --git a/py-rattler/src/repo_data/package_record.rs b/py-rattler/src/repo_data/package_record.rs index 98933685b..0f1330a7b 100644 --- a/py-rattler/src/repo_data/package_record.rs +++ b/py-rattler/src/repo_data/package_record.rs @@ -6,7 +6,7 @@ use pyo3::{pyclass, pymethods}; #[repr(transparent)] #[derive(Clone)] pub struct PyPackageRecord { - inner: PackageRecord, + pub(crate) inner: PackageRecord, } impl From for PyPackageRecord { From 9ef815c299b3bffbdecd79abfee19e578cfced46 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Fri, 25 Aug 2023 21:12:58 +0530 Subject: [PATCH 18/22] doc: fix incorrect doc --- py-rattler/rattler/version/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index 5413b3ad6..d7815a419 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -135,7 +135,7 @@ def is_dev(self) -> bool: True >>> v_non_dev = Version('1.0.1') >>> v_non_dev >= v - False + True """ return self._version.is_dev() From 164d6447233cb38d14552773aafee159ade7e96f Mon Sep 17 00:00:00 2001 From: Wackyator Date: Mon, 28 Aug 2023 14:32:11 +0530 Subject: [PATCH 19/22] doc: turn imperitive doc comments into descriptive --- crates/rattler_conda_types/src/version/mod.rs | 12 ++++++------ py-rattler/src/match_spec.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/rattler_conda_types/src/version/mod.rs b/crates/rattler_conda_types/src/version/mod.rs index f96bafc65..239f52fce 100644 --- a/crates/rattler_conda_types/src/version/mod.rs +++ b/crates/rattler_conda_types/src/version/mod.rs @@ -693,7 +693,7 @@ pub enum Component { } impl Component { - /// Return a component as numeric value. + /// Returns a component as numeric value. pub fn as_number(&self) -> Option { match self { Component::Numeral(value) => Some(*value), @@ -701,7 +701,7 @@ impl Component { } } - /// Return a component as mutable numeric value. + /// Returns a component as mutable numeric value. pub fn as_number_mut(&mut self) -> Option<&mut u64> { match self { Component::Numeral(value) => Some(value), @@ -709,7 +709,7 @@ impl Component { } } - /// Return a component as string value. + /// Returns a component as string value. #[allow(dead_code)] pub fn as_string(&self) -> Option<&str> { match self { @@ -718,19 +718,19 @@ impl Component { } } - /// Check whether a component is [`Component::Post`] + /// Checks whether a component is [`Component::Post`] #[allow(dead_code)] pub fn is_post(&self) -> bool { matches!(self, Component::Post) } - /// Check whether a component is [`Component::Dev`] + /// Checks whether a component is [`Component::Dev`] #[allow(dead_code)] pub fn is_dev(&self) -> bool { matches!(self, Component::Dev) } - /// Check whether a component is [`Component::Numeral`] + /// Checks whether a component is [`Component::Numeral`] pub fn is_numeric(&self) -> bool { matches!(self, Component::Numeral(_)) } diff --git a/py-rattler/src/match_spec.rs b/py-rattler/src/match_spec.rs index ed6de86c0..f29cd076a 100644 --- a/py-rattler/src/match_spec.rs +++ b/py-rattler/src/match_spec.rs @@ -40,7 +40,7 @@ impl PyMatchSpec { format!("{}", self.inner) } - /// Match a MatchSpec against a PackageRecord + /// Matches a MatchSpec against a PackageRecord pub fn matches(&self, record: &PyPackageRecord) -> bool { self.inner.matches(&record.inner) } From 623547408830fa4dea5ef12483f0b99224fc5a32 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Mon, 28 Aug 2023 14:57:40 +0530 Subject: [PATCH 20/22] fix: remove None variant from PyComponent and restructure version module --- py-rattler/src/lib.rs | 1 - py-rattler/src/{ => version}/component.rs | 4 +--- py-rattler/src/{version.rs => version/mod.rs} | 5 ++++- 3 files changed, 5 insertions(+), 5 deletions(-) rename py-rattler/src/{ => version}/component.rs (90%) rename py-rattler/src/{version.rs => version/mod.rs} (98%) diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index 6f06c0b97..e6d32d4e8 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -1,4 +1,3 @@ -mod component; mod error; mod match_spec; mod nameless_match_spec; diff --git a/py-rattler/src/component.rs b/py-rattler/src/version/component.rs similarity index 90% rename from py-rattler/src/component.rs rename to py-rattler/src/version/component.rs index 753ce5f60..eb72c0898 100644 --- a/py-rattler/src/component.rs +++ b/py-rattler/src/version/component.rs @@ -4,7 +4,6 @@ use rattler_conda_types::Component; pub enum PyComponent { String(String), Number(u64), - None, } impl IntoPy for PyComponent { @@ -12,7 +11,6 @@ impl IntoPy for PyComponent { match self { Self::Number(val) => val.into_py(py), Self::String(val) => val.into_py(py), - Self::None => py.None(), } } } @@ -24,7 +22,7 @@ impl From for PyComponent { Component::Numeral(n) => Self::Number(n), Component::Dev => Self::String("dev".to_string()), Component::Post => Self::String("post".to_string()), - _ => Self::None, + Component::UnderscoreOrDash { .. } => Self::String("_".to_string()), } } } diff --git a/py-rattler/src/version.rs b/py-rattler/src/version/mod.rs similarity index 98% rename from py-rattler/src/version.rs rename to py-rattler/src/version/mod.rs index ef21f3cf1..db5ffa372 100644 --- a/py-rattler/src/version.rs +++ b/py-rattler/src/version/mod.rs @@ -1,4 +1,7 @@ -use crate::{component::PyComponent, PyRattlerError}; +mod component; + +use crate::PyRattlerError; +use component::PyComponent; use pyo3::{pyclass, pymethods}; use rattler_conda_types::Version; use std::str::FromStr; From 6a20750ed2998961bb4119ab933386c6dc2f562f Mon Sep 17 00:00:00 2001 From: Wackyator Date: Mon, 28 Aug 2023 15:00:27 +0530 Subject: [PATCH 21/22] fix: remove Optional from segments and local_segments type signature --- py-rattler/rattler/version/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py-rattler/rattler/version/version.py b/py-rattler/rattler/version/version.py index d7815a419..73aaf64c3 100644 --- a/py-rattler/rattler/version/version.py +++ b/py-rattler/rattler/version/version.py @@ -82,7 +82,7 @@ def has_local(self) -> bool: """ return self._version.has_local() - def segments(self) -> List[List[Optional[Union[str, int]]]]: + def segments(self) -> List[List[Union[str, int]]]: """ Returns a list of segments of the version. It does not contain the local segment of the version. @@ -95,7 +95,7 @@ def segments(self) -> List[List[Optional[Union[str, int]]]]: """ return self._version.segments() - def local_segments(self) -> List[List[Optional[Union[str, int]]]]: + def local_segments(self) -> List[List[Union[str, int]]]: """ Returns a list of local segments of the version. It does not contain the non-local segment of the version. From 94b55b59a22215565d1030f298fc20986229b171 Mon Sep 17 00:00:00 2001 From: Wackyator Date: Mon, 28 Aug 2023 16:54:15 +0530 Subject: [PATCH 22/22] test: add dash normalisation test --- py-rattler/tests/unit/test_version.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 py-rattler/tests/unit/test_version.py diff --git a/py-rattler/tests/unit/test_version.py b/py-rattler/tests/unit/test_version.py new file mode 100644 index 000000000..7e7a72797 --- /dev/null +++ b/py-rattler/tests/unit/test_version.py @@ -0,0 +1,19 @@ +import pytest +from rattler import Version + + +def test_version_dash_normalisation(): + assert Version("1.0-").segments() == [[1], [0, "_"]] + assert Version("1.0_").segments() == [[1], [0, "_"]] + assert Version("1.0dev-+2.3").segments() == [[1], [0, "dev", "_"]] + assert Version("1.0dev_").segments() == [[1], [0, "dev", "_"]] + + assert Version("1.0dev-+2.3").local_segments() == [[2], [3]] + assert Version("1.0dev+3.4-dev").local_segments() == [[3], [4], [0, "dev"]] + assert Version("1.0dev+3.4-").local_segments() == [[3], [4, "_"]] + + with pytest.raises(Exception): + Version("1-.0dev-") + + with pytest.raises(Exception): + Version("1-.0dev+3.4-")