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 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
67dca00
feat: add version type to py-rattler
Wackyator Aug 22, 2023
447b246
Merge branch 'main' into feat/py-rattler-version
Wackyator Aug 23, 2023
26c2489
feat: add MatchSpec
Wackyator Aug 23, 2023
21ba578
chore: fix lints
Wackyator Aug 23, 2023
7e229b0
fix: make Version property names more explicit and raise exceptions i…
Wackyator Aug 24, 2023
afd820f
doc: add better docs and examples
Wackyator Aug 24, 2023
fbb36f4
fix: fix typo in segment_count name
Wackyator Aug 24, 2023
61413f4
fix: make segments and local_segments methods public
Wackyator Aug 24, 2023
78b2f22
feat: add segments and local_segments method to version
Wackyator Aug 24, 2023
201e25c
Merge branch 'main' into feat/py-rattler-version
Wackyator Aug 24, 2023
3e270c1
test: fix tests and remove reduntant unit tests
Wackyator Aug 24, 2023
b18daab
chore: fix formatting
Wackyator Aug 24, 2023
ed503e7
chore: move py-rattler classes and structs to seperate files
Wackyator Aug 25, 2023
b2145f7
doc: improve py-rattler docs
Wackyator Aug 25, 2023
94de68a
fix: fix type checking bug and make staticmethods classmethods
Wackyator Aug 25, 2023
7b3be8d
add docs to Component and expose it
Wackyator Aug 25, 2023
e032a13
fix: version segment and local segment should return segments as int …
Wackyator Aug 25, 2023
ec34de3
Merge branch 'main' into feat/py-rattler-init
Wackyator Aug 25, 2023
533ae8c
doc: add doc for NamelessMatchSpec
Wackyator Aug 25, 2023
72938bf
fix: remove redundant clone and make package record inner pub crate
Wackyator Aug 25, 2023
9ef815c
doc: fix incorrect doc
Wackyator Aug 25, 2023
164d644
doc: turn imperitive doc comments into descriptive
Wackyator Aug 28, 2023
6235474
fix: remove None variant from PyComponent and restructure version module
Wackyator Aug 28, 2023
6a20750
fix: remove Optional from segments and local_segments type signature
Wackyator Aug 28, 2023
94b55b5
test: add dash normalisation test
Wackyator 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
4 changes: 3 additions & 1 deletion crates/rattler_conda_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
16 changes: 12 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,8 @@ 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 Component.
Numeral(u64),

/// Post should always be ordered greater than anything else.
Expand All @@ -686,25 +687,29 @@ enum Component {

/// An underscore or dash.
UnderscoreOrDash {
/// Dash flag.
is_dash: bool,
},
}

impl Component {
/// Return a component as numeric value.
Wackyator marked this conversation as resolved.
Show resolved Hide resolved
pub fn as_number(&self) -> Option<u64> {
match self {
Component::Numeral(value) => Some(*value),
_ => None,
}
}

/// Return a component as mutable numeric value.
Wackyator marked this conversation as resolved.
Show resolved Hide resolved
pub fn as_number_mut(&mut self) -> Option<&mut u64> {
match self {
Component::Numeral(value) => Some(value),
_ => None,
}
}

/// Return a component as string value.
Wackyator marked this conversation as resolved.
Show resolved Hide resolved
#[allow(dead_code)]
pub fn as_string(&self) -> Option<&str> {
match self {
Expand All @@ -713,16 +718,19 @@ impl Component {
}
}

/// Check whether a component is [`Component::Post`]
Wackyator marked this conversation as resolved.
Show resolved Hide resolved
#[allow(dead_code)]
pub fn is_post(&self) -> bool {
matches!(self, Component::Post)
}

/// Check whether a component is [`Component::Dev`]
Wackyator marked this conversation as resolved.
Show resolved Hide resolved
#[allow(dead_code)]
pub fn is_dev(&self) -> bool {
matches!(self, Component::Dev)
}

/// Check whether a component is [`Component::Numeral`]
Wackyator marked this conversation as resolved.
Show resolved Hide resolved
pub fn is_numeric(&self) -> bool {
matches!(self, Component::Numeral(_))
}
Expand Down Expand Up @@ -887,7 +895,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"]
4 changes: 4 additions & 0 deletions py-rattler/rattler/match_spec/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from rattler.match_spec.match_spec import MatchSpec
from rattler.match_spec.nameless_match_spec import NamelessMatchSpec

__all__ = ["MatchSpec", "NamelessMatchSpec"]
117 changes: 117 additions & 0 deletions py-rattler/rattler/match_spec/match_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from __future__ import annotations
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


class MatchSpec:
Wackyator marked this conversation as resolved.
Show resolved Hide resolved
"""
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)
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) -> Self:
"""
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)

@classmethod
def from_nameless(cls, spec: NamelessMatchSpec, name: str) -> Self:
"""
Constructs a MatchSpec from a NamelessMatchSpec
and a name.
"""
return cls._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__()
58 changes: 58 additions & 0 deletions py-rattler/rattler/match_spec/nameless_match_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import annotations
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


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)
else:
raise TypeError(
"NamelessMatchSpec constructor received unsupported type"
f" {type(spec).__name__!r} for the 'spec' parameter"
)

def matches(self, package_record: PackageRecord) -> bool:
"""
Match a MatchSpec against a PackageRecord
"""
return self._nameless_match_spec.matches(package_record._package_record)
Wackyator marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def _from_py_nameless_match_spec(
cls, py_nameless_match_spec: PyNamelessMatchSpec
) -> Self:
"""
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

@classmethod
def from_match_spec(cls, spec: MatchSpec) -> Self:
"""
Constructs a NamelessMatchSpec from a MatchSpec.
"""
return cls._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"]
20 changes: 20 additions & 0 deletions py-rattler/rattler/repo_data/package_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

from rattler.rattler import PyPackageRecord


class PackageRecord:
Wackyator marked this conversation as resolved.
Show resolved Hide resolved
"""
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()

def __str__(self) -> str:
return self._package_record.as_str()

def __repr__(self) -> str:
return self.__str__()
Loading