Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Version, MatchSpec, NamelessMatchSpec to py-rattler #292

Merged
merged 25 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
67dca00
feat: add version type to py-rattler
tarunps Aug 22, 2023
447b246
Merge branch 'main' into feat/py-rattler-version
tarunps Aug 23, 2023
26c2489
feat: add MatchSpec
tarunps Aug 23, 2023
21ba578
chore: fix lints
tarunps Aug 23, 2023
7e229b0
fix: make Version property names more explicit and raise exceptions i…
tarunps Aug 24, 2023
afd820f
doc: add better docs and examples
tarunps Aug 24, 2023
fbb36f4
fix: fix typo in segment_count name
tarunps Aug 24, 2023
61413f4
fix: make segments and local_segments methods public
tarunps Aug 24, 2023
78b2f22
feat: add segments and local_segments method to version
tarunps Aug 24, 2023
201e25c
Merge branch 'main' into feat/py-rattler-version
tarunps Aug 24, 2023
3e270c1
test: fix tests and remove reduntant unit tests
tarunps Aug 24, 2023
b18daab
chore: fix formatting
tarunps Aug 24, 2023
ed503e7
chore: move py-rattler classes and structs to seperate files
tarunps Aug 25, 2023
b2145f7
doc: improve py-rattler docs
tarunps Aug 25, 2023
94de68a
fix: fix type checking bug and make staticmethods classmethods
tarunps Aug 25, 2023
7b3be8d
add docs to Component and expose it
tarunps Aug 25, 2023
e032a13
fix: version segment and local segment should return segments as int …
tarunps Aug 25, 2023
ec34de3
Merge branch 'main' into feat/py-rattler-init
tarunps Aug 25, 2023
533ae8c
doc: add doc for NamelessMatchSpec
tarunps Aug 25, 2023
72938bf
fix: remove redundant clone and make package record inner pub crate
tarunps Aug 25, 2023
9ef815c
doc: fix incorrect doc
tarunps Aug 25, 2023
164d644
doc: turn imperitive doc comments into descriptive
tarunps Aug 28, 2023
6235474
fix: remove None variant from PyComponent and restructure version module
tarunps Aug 28, 2023
6a20750
fix: remove Optional from segments and local_segments type signature
tarunps Aug 28, 2023
94b55b5
test: add dash normalisation test
tarunps Aug 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.idea/

.vscode/

# Generated by Cargo
# will have compiled files and executables
debug/
Expand Down
8 changes: 4 additions & 4 deletions crates/rattler_conda_types/src/version/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ impl Version {
}

/// Returns the individual segments of the version.
fn segments(
pub fn segments(
&self,
) -> impl Iterator<Item = SegmentIter<'_>> + DoubleEndedIterator + ExactSizeIterator + '_ {
let mut idx = if self.has_epoch() { 1 } else { 0 };
Expand Down Expand Up @@ -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<Item = SegmentIter<'_>> + DoubleEndedIterator + ExactSizeIterator + '_ {
if let Some(start) = self.local_segment_index() {
Expand Down Expand Up @@ -671,7 +671,7 @@ impl<'v, I: Iterator<Item = SegmentIter<'v>> + '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.
Expand Down Expand Up @@ -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,

Expand Down
4 changes: 3 additions & 1 deletion py-rattler/rattler/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
3 changes: 3 additions & 0 deletions py-rattler/rattler/match_spec/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from rattler.match_spec.match_spec import MatchSpec, NamelessMatchSpec

__all__ = ["MatchSpec", "NamelessMatchSpec"]
86 changes: 86 additions & 0 deletions py-rattler/rattler/match_spec/match_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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__()
3 changes: 3 additions & 0 deletions py-rattler/rattler/repo_data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from rattler.repo_data.package_record import PackageRecord

__all__ = ["PackageRecord"]
14 changes: 14 additions & 0 deletions py-rattler/rattler/repo_data/package_record.py
Original file line number Diff line number Diff line change
@@ -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__()
187 changes: 185 additions & 2 deletions py-rattler/rattler/version/version.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
from __future__ import annotations

from typing import Optional
from typing import List, Optional, Tuple

from rattler.rattler import PyVersion
from rattler.rattler import PyVersion, InvalidVersionError


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)
Expand Down Expand Up @@ -54,6 +65,178 @@ def bump(self) -> Version:
"""
return Version._from_py_version(self._version.bump())

@property
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+3.2-alpha0')
>>> v.has_local
True
"""
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")
>>> v.segments()
[['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")
>>> v.local_segments()
[['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.
Requires a minimum of 2 segments in version to be split
into major and minor, returns `None` otherwise.

Examples
--------
>>> v = Version('1.0')
>>> v.as_major_minor()
(1, 0)
"""
return self._version.as_major_minor()

@property
def is_dev(self) -> bool:
"""
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.is_dev
>>> v_non_dev = Version('1.0.1')
>>> _ = v_non_dev > v
"""
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.
Minor versions changes are compatible with older versions,
major version changes are breaking and will not be compatible.

Examples
--------
>>> v1 = Version('1.0')
>>> 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. 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) # old version is still usable
2!1
>>> v.pop_segments(3) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
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("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. 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:
return self._from_py_version(new_py_version)
else:
raise InvalidVersionError("Invalid segment range provided")

@property
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())

def __eq__(self, other: Version) -> bool:
return self._version.equals(other._version)

Expand Down
8 changes: 7 additions & 1 deletion py-rattler/src/error.rs
Original file line number Diff line number Diff line change
@@ -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<PyRattlerError> for PyErr {
Expand All @@ -15,8 +17,12 @@ impl From<PyRattlerError> 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);
Loading