diff --git a/.codespell-whitelist.txt b/.codespell-whitelist.txt new file mode 100644 index 00000000..9ac17d57 --- /dev/null +++ b/.codespell-whitelist.txt @@ -0,0 +1 @@ +crate diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d370858..a6e17af2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,4 +36,5 @@ repos: - repo: https://github.com/codespell-project/codespell rev: v2.2.5 hooks: - - id: codespell \ No newline at end of file + - id: codespell + args: [--ignore-words=.codespell-whitelist.txt] diff --git a/README.md b/README.md index 7ecb3f02..a397b3d0 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ [crates-badge]: https://img.shields.io/crates/v/rattler_installs_packages.svg -`RIP` is a library that allows the resolving and installing of Python [PyPi](https://pypi.org/) packages from Rust into a virtual environment. +`RIP` is a library that allows the resolving and installing of Python [PyPI](https://pypi.org/) packages from Rust into a virtual environment. It's based on our experience with building [Rattler](https://github.com/mamba-org/rattler) and aims to provide the same -experience but for PyPi instead of Conda. +experience but for PyPI instead of Conda. It should be fast and easy to use. Like Rattler, this library is not a package manager itself but provides the low-level plumbing to be used in one. `RIP` is based on the quite excellent work of [posy](https://github.com/njsmith/posy) and we have tried to credit @@ -41,19 +41,19 @@ We've added a small binary to showcase this: ![flask-install](https://github.com/prefix-dev/rip/assets/4995967/5b0356b6-8e06-47bb-9424-94b3fdd9da09) -This showcases the downloading and caching of metadata from PyPi. As well as the package resolution using our solver, more on this below. +This showcases the downloading and caching of metadata from PyPI. As well as the package resolution using our solver, more on this below. We cache everything in a local directory so that we can re-use the metadata and don't have to download it again. ## Features This is a list of current and planned features of `RIP`, the biggest are listed below: -* [x] Downloading and aggressive caching of PyPi metadata. -* [x] Resolving of PyPi packages using [Resolvo](https://github.com/mamba-org/resolvo). +* [x] Downloading and aggressive caching of PyPI metadata. +* [x] Resolving of PyPI packages using [Resolvo](https://github.com/mamba-org/resolvo). * [ ] Installation of wheel files (planned) * [ ] Support sdist files (planned) -More intricacies of the PyPi ecosystem need to be implemented, see our GitHub issues for more details. +More intricacies of the PyPI ecosystem need to be implemented, see our GitHub issues for more details. # Solver @@ -65,6 +65,6 @@ This feature can be enabled with the `resolvo-pypi` feature flag. ## Contributing 😍 -We would love to have you contribute! -See the CONTRIBUTION.md for more info. For questions, requests or a casual chat, we are very active on our discord server. +We would love to have you contribute! +See the CONTRIBUTION.md for more info. For questions, requests or a casual chat, we are very active on our discord server. You can [join our discord server via this link][chat-url]. diff --git a/crates/rattler_installs_packages/src/artifact.rs b/crates/rattler_installs_packages/src/artifact.rs index af0a3b47..ed43e131 100644 --- a/crates/rattler_installs_packages/src/artifact.rs +++ b/crates/rattler_installs_packages/src/artifact.rs @@ -11,6 +11,8 @@ use std::io::Read; use std::str::FromStr; use zip::ZipArchive; +/// Trait that represents an artifact type in the PyPI ecosystem. +/// Currently implemented for [`Wheel`] files. #[async_trait] pub trait Artifact: Sized { /// The name of the artifact which describes the artifact. @@ -26,6 +28,9 @@ pub trait Artifact: Sized { fn name(&self) -> &Self::Name; } +/// Wheel file in the PyPI ecosystem. +/// See the [Reference Page](https://packaging.python.org/en/latest/specifications/binary-distribution-format/#binary-distribution-format) +/// for more information. pub struct Wheel { name: WheelName, archive: Mutex>>, @@ -47,8 +52,11 @@ impl Artifact for Wheel { } } +/// Trait that represents an artifact that contains metadata. +/// Currently implemented for [`Wheel`] files. #[async_trait] pub trait MetadataArtifact: Artifact { + /// Associated type for the metadata of this artifact. type Metadata; /// Parses the metadata associated with an artifact. diff --git a/crates/rattler_installs_packages/src/artifact_name.rs b/crates/rattler_installs_packages/src/artifact_name.rs index 6bf36d56..74a8d10e 100644 --- a/crates/rattler_installs_packages/src/artifact_name.rs +++ b/crates/rattler_installs_packages/src/artifact_name.rs @@ -26,7 +26,9 @@ use thiserror::Error; /// providing flexibility and clarity when working with Python package distributions. #[derive(Debug, Clone, PartialOrd, Ord, Eq, PartialEq, DeserializeFromStr, SerializeDisplay)] pub enum ArtifactName { + /// Wheel artifact Wheel(WheelName), + /// Sdist artifact SDist(SDistName), } @@ -70,7 +72,10 @@ impl Display for ArtifactName { } } -// https://packaging.python.org/specifications/binary-distribution-format/#file-name-convention +/// Structure that contains the information that is contained in a wheel name +/// See: [File Name Convention](https://www.python.org/dev/peps/pep-0427/#file-name-convention), +/// and: [PyPA Conventions](https://packaging.python.org/en/latest/specifications/), +/// for more details regarding the structure of a wheel name. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct WheelName { /// Distribution name, e.g. ‘django’, ‘pyramid’. @@ -83,14 +88,20 @@ pub struct WheelName { pub build_tag: Option, /// Language implementation and version tag + /// E.g. ‘py27’, ‘py2’, ‘py3’. pub py_tags: Vec, + /// ABI specific tags + /// E.g. ‘cp33m’, ‘abi3’, ‘none’. pub abi_tags: Vec, + /// Architecture specific tags + /// E.g. ‘linux_x86_64’, ‘any’, ‘manylinux_2_17_x86_64’ pub arch_tags: Vec, } impl WheelName { + /// Creates a set of all tags that are contained in this wheel name. pub fn all_tags(&self) -> HashSet { let mut retval = HashSet::new(); for py in &self.py_tags { @@ -139,6 +150,7 @@ impl Display for BuildTag { } } +/// Structure that contains the information that is contained in a source distribution name #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct SDistName { /// Distribution name, e.g. ‘django’, ‘pyramid’. @@ -172,6 +184,7 @@ impl Display for SDistName { /// Describes the format in which the source distribution is shipped. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[allow(missing_docs)] pub enum SDistFormat { Zip, TarGz, @@ -181,7 +194,9 @@ pub enum SDistFormat { Tar, } +/// An error that can occur when parsing an artifact name #[derive(Debug, Clone, Error)] +#[allow(missing_docs)] pub enum ParseArtifactNameError { #[error("invalid artifact name")] InvalidName, @@ -332,7 +347,10 @@ impl FromStr for ArtifactName { /// A trait to convert the general [`ArtifactName`] into a specialized artifact name. This is useful /// to generically fetch the underlying specialized name. +/// +/// Currently we provide implementations for wheels and sdists. pub trait InnerAsArtifactName { + /// Tries to convert the general [`ArtifactName`] into a specialized artifact name. fn try_as(name: &ArtifactName) -> Option<&Self>; } @@ -361,6 +379,19 @@ mod test { assert_eq!(sn.to_string(), "trio-0.19a0.tar.gz"); } + #[test] + fn test_many_linux() { + let n: WheelName = + "numpy-1.26.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + .parse() + .unwrap(); + + assert_eq!( + n.arch_tags, + vec!["manylinux_2_17_x86_64", "manylinux2014_x86_64"] + ); + } + #[test] fn test_wheel_name_from_str() { let n: WheelName = "trio-0.18.0-py3-none-any.whl".parse().unwrap(); diff --git a/crates/rattler_installs_packages/src/extra.rs b/crates/rattler_installs_packages/src/extra.rs index 0b991a46..60997c50 100644 --- a/crates/rattler_installs_packages/src/extra.rs +++ b/crates/rattler_installs_packages/src/extra.rs @@ -37,6 +37,7 @@ use std::str::FromStr; use thiserror::Error; #[derive(Debug, Clone, Eq, DeserializeFromStr)] +/// Structure that holds both the source string and the normalized version of an extra. pub struct Extra { /// The original string this instance was created from source: Box, diff --git a/crates/rattler_installs_packages/src/file_store.rs b/crates/rattler_installs_packages/src/file_store.rs index 7a0cdd4e..a955d71e 100644 --- a/crates/rattler_installs_packages/src/file_store.rs +++ b/crates/rattler_installs_packages/src/file_store.rs @@ -15,6 +15,7 @@ use std::{ /// Types that implement this can be used as keys of the [`FileStore`]. pub trait CacheKey { + /// Returns the path prefix that should be used to store the data for this key. fn key(&self) -> PathBuf; } @@ -62,6 +63,7 @@ impl CacheKey for ArtifactHashes { } #[derive(Debug)] +/// A cache that stores its data as cbor files on the filesystem. pub struct FileStore { base: PathBuf, tmp: PathBuf, @@ -259,7 +261,7 @@ fn lock(path: &Path, mode: LockMode) -> io::Result { open_options.write(true); // Only create the parent directories if the lock mode is set to `Lock`. In the other case we - // don't care if the file doesnt exist. + // don't care if the file doesn't exist. if mode == LockMode::Lock { let dir = lock_path .parent() @@ -271,7 +273,7 @@ fn lock(path: &Path, mode: LockMode) -> io::Result { // Open the lock file let lock = open_options.open(&lock_path)?; - // Lock the file. On unix this is apparently a thin wrapper around flock(2) and it doesnt + // Lock the file. On unix this is apparently a thin wrapper around flock(2) and it doesn't // properly handle EINTR so we keep retrying when that happens. retry_interrupted(|| lock.lock_exclusive())?; diff --git a/crates/rattler_installs_packages/src/html.rs b/crates/rattler_installs_packages/src/html.rs index 0ae51a56..adfd5924 100644 --- a/crates/rattler_installs_packages/src/html.rs +++ b/crates/rattler_installs_packages/src/html.rs @@ -1,16 +1,4 @@ -// Derived from -// https://github.com/servo/html5ever/blob/master/html5ever/examples/noop-tree-builder.rs -// Which has the following copyright header: -// -// Copyright 2014-2017 The html5ever Project Developers. See the -// COPYRIGHT file at the top-level directory of this distribution. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - +//! Module for parsing different HTML pages from PyPI repository use std::{borrow::Borrow, default::Default}; use crate::{ArtifactHashes, ArtifactName}; @@ -33,7 +21,7 @@ fn parse_hash(s: &str) -> Option { } } -pub fn into_artifact_info(base: &Url, tag: &HTMLTag) -> Option { +fn into_artifact_info(base: &Url, tag: &HTMLTag) -> Option { let attributes = tag.attributes(); // Get first href attribute to use as filename let href = attributes.get("href").flatten()?.as_utf8_str(); @@ -95,6 +83,7 @@ pub fn into_artifact_info(base: &Url, tag: &HTMLTag) -> Option { }) } +/// Parses information regarding the different artifacts for a project pub fn parse_project_info_html(base: &Url, body: &str) -> miette::Result { let dom = tl::parse(body, tl::ParserOptions::default()).into_diagnostic()?; let variants = dom.query_selector("a"); diff --git a/crates/rattler_installs_packages/src/lib.rs b/crates/rattler_installs_packages/src/lib.rs index 1ad24422..cfe39ba3 100644 --- a/crates/rattler_installs_packages/src/lib.rs +++ b/crates/rattler_installs_packages/src/lib.rs @@ -1,3 +1,9 @@ +//! RIP is a library that allows the resolving and installing of Python PyPI packages from Rust into a virtual environment. +//! It's based on our experience with building Rattler and aims to provide the same experience but for PyPI instead of Conda. +//! It should be fast and easy to use. +//! Like Rattler, this library is not a package manager itself but provides the low-level plumbing to be used in one. + +#![deny(missing_docs)] mod artifact; mod artifact_name; mod core_metadata; @@ -34,7 +40,7 @@ pub use package_name::{NormalizedPackageName, PackageName, ParsePackageNameError pub use pep440::Version; pub use project_info::{ArtifactHashes, ArtifactInfo, DistInfoMetadata, Meta, ProjectInfo, Yanked}; pub use requirement::{ - marker, PackageRequirement, ParseExtra, PythonRequirement, Requirement, UserRequirement, + marker, PackageRequirement, ParseExtraInEnv, PythonRequirement, Requirement, UserRequirement, }; pub use specifier::{CompareOp, Specifier, Specifiers}; diff --git a/crates/rattler_installs_packages/src/package_database.rs b/crates/rattler_installs_packages/src/package_database.rs index ba1fe4de..263c2f1d 100644 --- a/crates/rattler_installs_packages/src/package_database.rs +++ b/crates/rattler_installs_packages/src/package_database.rs @@ -23,6 +23,7 @@ use std::io::Read; use std::path::PathBuf; use url::Url; +/// Cache of the available packages, artifacts and their metadata. pub struct PackageDb { http: Http, diff --git a/crates/rattler_installs_packages/src/package_name.rs b/crates/rattler_installs_packages/src/package_name.rs index ef9c1f4a..f45f513c 100644 --- a/crates/rattler_installs_packages/src/package_name.rs +++ b/crates/rattler_installs_packages/src/package_name.rs @@ -36,6 +36,8 @@ impl PackageName { } #[derive(Debug, Clone, Error, Diagnostic)] +/// Error when parsing a package name +#[allow(missing_docs)] pub enum ParsePackageNameError { #[error("invalid package name '{0}'")] InvalidPackageName(String), @@ -99,6 +101,8 @@ impl Serialize for PackageName { } } +/// A normalized package name. This is a string that is guaranteed to be a valid python package string +/// this is described in [PEP 503 (Normalized Names)](https://www.python.org/dev/peps/pep-0503/#normalized-names). #[repr(transparent)] #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct NormalizedPackageName(Box); diff --git a/crates/rattler_installs_packages/src/project_info.rs b/crates/rattler_installs_packages/src/project_info.rs index e5d35e7a..bd865849 100644 --- a/crates/rattler_installs_packages/src/project_info.rs +++ b/crates/rattler_installs_packages/src/project_info.rs @@ -18,16 +18,24 @@ pub struct ProjectInfo { pub files: Vec, } +/// Describes a single artifact that is available for download. #[serde_as] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct ArtifactInfo { + /// Artifact name pub filename: ArtifactName, + /// Url to download the artifact pub url: url::Url, + /// Hashes of the artifact pub hashes: Option, + /// Python requirement pub requires_python: Option, #[serde(default)] + /// This attribute specified if the metadata is available + /// as a separate download described in [PEP 658](https://www.python.org/dev/peps/pep-0658/) pub dist_info_metadata: DistInfoMetadata, + /// Yanked information #[serde(default)] pub yanked: Yanked, } @@ -46,6 +54,7 @@ impl ArtifactInfo { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct ArtifactHashes { #[serde_as(as = "Option>")] + /// Contains the optional sha256 hash of the artifact pub sha256: Option, } @@ -61,7 +70,9 @@ impl ArtifactHashes { #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] #[serde(from = "Option")] pub struct DistInfoMetadata { + /// True if the metadata is available pub available: bool, + /// Hashes to verify the metadata file pub hashes: ArtifactHashes, } @@ -99,6 +110,7 @@ impl From> for DistInfoMetadata { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Meta { #[serde(rename = "api-version")] + /// Version of the API pub version: String, } @@ -117,10 +129,13 @@ enum RawYanked { WithReason(String), } +/// Struct that describes whether a package is yanked or not. #[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)] #[serde(from = "RawYanked")] pub struct Yanked { + /// This is true if the package is yanked. pub yanked: bool, + /// Optional reason why the package is yanked. pub reason: Option, } diff --git a/crates/rattler_installs_packages/src/reqparse.rs b/crates/rattler_installs_packages/src/reqparse.rs index f02ecb59..4f725001 100644 --- a/crates/rattler_installs_packages/src/reqparse.rs +++ b/crates/rattler_installs_packages/src/reqparse.rs @@ -5,7 +5,7 @@ pub use self::parser::{marker, requirement, versionspec}; use super::{ extra::Extra, package_name::PackageName, - requirement::{marker, ParseExtra, Requirement}, + requirement::{marker, ParseExtraInEnv, Requirement}, specifier::{CompareOp, Specifier, Specifiers}, }; @@ -79,7 +79,7 @@ peg::parser! { = s:(python_squote_str() / python_dquote_str()) { marker::Value::Literal(s.into()) } - rule env_var(parse_extra: ParseExtra) -> marker::Value + rule env_var(parse_extra: ParseExtraInEnv) -> marker::Value = var:$( "python_version" / "python_full_version" / "os_name" / "sys_platform" / "platform_release" / "platform_system" @@ -88,7 +88,7 @@ peg::parser! { / "implementation_version" / "extra" ) {? - if ParseExtra::NotAllowed == parse_extra && var == "extra" { + if ParseExtraInEnv::NotAllowed == parse_extra && var == "extra" { return Err("'extra' marker is not valid in this context") } Ok(marker::Value::Variable(var.to_owned())) @@ -110,12 +110,12 @@ peg::parser! { marker::Value::Variable("platform_python_implementation".into()) } - rule marker_value(parse_extra: ParseExtra) -> marker::Value + rule marker_value(parse_extra: ParseExtraInEnv) -> marker::Value = _ v:(env_var(parse_extra) / pep345_env_var() / setuptools_env_var() / python_str()) { v } - rule marker_expr(parse_extra: ParseExtra) -> marker::EnvMarkerExpr + rule marker_expr(parse_extra: ParseExtraInEnv) -> marker::EnvMarkerExpr = _ "(" m:marker(parse_extra) _ ")" { m } / lhs:marker_value(parse_extra) op:marker_op() rhs:marker_value(parse_extra) { @@ -136,20 +136,20 @@ peg::parser! { } } - rule marker_and(parse_extra: ParseExtra) -> marker::EnvMarkerExpr + rule marker_and(parse_extra: ParseExtraInEnv) -> marker::EnvMarkerExpr = lhs:marker_expr(parse_extra) _ "and" _ rhs:marker_and(parse_extra) { marker::EnvMarkerExpr::And(Box::new(lhs), Box::new(rhs)) } / marker_expr(parse_extra) - rule marker_or(parse_extra: ParseExtra) -> marker::EnvMarkerExpr + rule marker_or(parse_extra: ParseExtraInEnv) -> marker::EnvMarkerExpr = lhs:marker_and(parse_extra) _ "or" _ rhs:marker_or(parse_extra) { marker::EnvMarkerExpr::Or(Box::new(lhs), Box::new(rhs)) } / marker_and(parse_extra) - pub rule marker(parse_extra: ParseExtra) -> marker::EnvMarkerExpr + pub rule marker(parse_extra: ParseExtraInEnv) -> marker::EnvMarkerExpr = marker_or(parse_extra) - rule quoted_marker(parse_extra: ParseExtra) -> marker::EnvMarkerExpr + rule quoted_marker(parse_extra: ParseExtraInEnv) -> marker::EnvMarkerExpr = ";" _ m:marker(parse_extra) { m } rule identifier() -> &'input str @@ -164,7 +164,7 @@ peg::parser! { rule extras() -> Vec = "[" _ es:(extra() ** (_ "," _)) _ "]" { es } - rule name_req(parse_extra: ParseExtra) -> Requirement + rule name_req(parse_extra: ParseExtraInEnv) -> Requirement = name:name() _ extras:(extras() / "" { Vec::new() }) _ specifiers:(versionspec() / "" { Specifiers(Vec::new()) }) @@ -178,7 +178,7 @@ peg::parser! { } } - rule url_req(parse_extra: ParseExtra) -> Requirement + rule url_req(parse_extra: ParseExtraInEnv) -> Requirement = name:name() _ extras:(extras() / "" { Vec::new() }) _ url:urlspec() @@ -188,7 +188,7 @@ peg::parser! { unreachable!() } - pub rule requirement(parse_extra: ParseExtra) -> Requirement + pub rule requirement(parse_extra: ParseExtraInEnv) -> Requirement = _ r:( url_req(parse_extra) / name_req(parse_extra) ) _ { r } } } diff --git a/crates/rattler_installs_packages/src/requirement.rs b/crates/rattler_installs_packages/src/requirement.rs index d391b6a2..a776a0eb 100644 --- a/crates/rattler_installs_packages/src/requirement.rs +++ b/crates/rattler_installs_packages/src/requirement.rs @@ -43,6 +43,11 @@ // version it declares, so it can satisfy other dependencies that use the name or // versions. +// @tdejager: added some doc-comments to the posy code. + +//! Requirements for Python packages. +//! Essentially version specifications for packages. + use super::specifier::CompareOp; use crate::extra::Extra; use crate::package_name::PackageName; @@ -55,6 +60,7 @@ use std::ops::Deref; use std::str::FromStr; pub mod marker { + //! Environment marker expressions module use crate::extra::Extra; use std::collections::HashMap; use std::fmt::Display; @@ -62,12 +68,16 @@ pub mod marker { use super::*; + /// Value can either be a literal string or a variable name. + #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Value { Variable(String), Literal(String), } + /// Comparison operator + #[allow(missing_docs)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Op { Compare(CompareOp), @@ -75,6 +85,8 @@ pub mod marker { NotIn, } + /// Python environment markers: + #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum EnvMarkerExpr { And(Box, Box), @@ -82,7 +94,10 @@ pub mod marker { Operator { op: Op, lhs: Value, rhs: Value }, } + /// Env marker retrieval Trait pub trait Env { + /// Returns the value of the marker variable + /// or None if the variable is not set fn get_marker_var(&self, var: &str) -> Option<&str>; } @@ -93,6 +108,7 @@ pub mod marker { } impl Value { + /// Evaluate the value from an [`Env`] trait implementation pub fn eval<'a>(&'a self, env: &'a dyn Env) -> miette::Result<&'a str> { match self { Value::Variable(varname) => env.get_marker_var(varname).ok_or_else(|| { @@ -102,6 +118,7 @@ pub mod marker { } } + /// Returns whether the value is an Extra pub fn is_extra(&self) -> bool { match self { Value::Variable(varname) => varname == "extra", @@ -126,6 +143,7 @@ pub mod marker { } impl EnvMarkerExpr { + /// Evaluate the expression from an [`Env`] trait implementations pub fn eval(&self, env: &dyn Env) -> miette::Result { Ok(match self { EnvMarkerExpr::And(lhs, rhs) => lhs.eval(env)? && rhs.eval(env)?, @@ -217,29 +235,40 @@ impl FromStr for StandaloneMarkerExpr { type Err = miette::Report; fn from_str(value: &str) -> Result { - let expr = super::reqparse::marker(value, ParseExtra::NotAllowed) + let expr = super::reqparse::marker(value, ParseExtraInEnv::NotAllowed) .into_diagnostic() .wrap_err_with(|| format!("Failed parsing env marker expression {:?}", value))?; Ok(StandaloneMarkerExpr(expr)) } } +/// Defines whether its allowed to have extras in env markers #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum ParseExtra { +pub enum ParseExtraInEnv { + /// Extras are allowed in env markers Allowed, + /// Extras are not allowed in env markers NotAllowed, } +/// Specification for a package requirement +/// [PEP508](https://peps.python.org/pep-0508) and [PEP440](https://peps.python.org/pep-0440/) have a lot of information +/// regarding the specification of these requirements #[derive(Debug, Clone, PartialEq, Eq)] pub struct Requirement { + /// Name of the package pub name: PackageName, + /// Optional requirements pub extras: Vec, + /// Collection of version specifiers pub specifiers: Specifiers, + /// Optional env marker expression, see: pub env_marker_expr: Option, } impl Requirement { - pub fn parse(input: &str, parse_extra: ParseExtra) -> miette::Result { + /// Parses a python requirement string + pub fn parse(input: &str, parse_extra: ParseExtraInEnv) -> miette::Result { let req = super::reqparse::requirement(input, parse_extra) .into_diagnostic() .wrap_err_with(|| format!("Failed parsing requirement string {:?})", input))?; @@ -272,14 +301,18 @@ impl Display for Requirement { } } +/// Requirements used by packages for other packages +/// You would find these in the wheel metadata #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] pub struct PackageRequirement(Requirement); impl PackageRequirement { + /// Move the inner requirement out of the struct pub fn into_inner(self) -> Requirement { self.0 } + /// Returns a reference to the inner requirement pub fn as_inner(&self) -> &Requirement { &self.0 } @@ -297,7 +330,7 @@ impl FromStr for PackageRequirement { fn from_str(value: &str) -> Result { Ok(PackageRequirement(Requirement::parse( value, - ParseExtra::Allowed, + ParseExtraInEnv::Allowed, )?)) } } @@ -322,14 +355,19 @@ impl Deref for PackageRequirement { } } +/// Requirement used for root-level requirements +/// these do not allow extras in env markers +/// as that does not make sense for a root-level requirement #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] pub struct UserRequirement(Requirement); impl UserRequirement { + /// Move the inner requirement out of the struct pub fn into_inner(self) -> Requirement { self.0 } + /// Returns a reference to the inner requirement pub fn as_inner(&self) -> &Requirement { &self.0 } @@ -347,12 +385,13 @@ impl FromStr for UserRequirement { fn from_str(value: &str) -> Result { Ok(UserRequirement(Requirement::parse( value, - ParseExtra::NotAllowed, + ParseExtraInEnv::NotAllowed, )?)) } } #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] +/// Requirements for Python pub struct PythonRequirement(Requirement); impl Display for PythonRequirement { @@ -382,7 +421,7 @@ impl FromStr for PythonRequirement { type Err = miette::Report; fn from_str(value: &str) -> Result { - let r = Requirement::parse(value, ParseExtra::NotAllowed)?; + let r = Requirement::parse(value, ParseExtraInEnv::NotAllowed)?; r.try_into() } } diff --git a/crates/rattler_installs_packages/src/resolve.rs b/crates/rattler_installs_packages/src/resolve.rs index 6d7a17c9..9c4feb9b 100644 --- a/crates/rattler_installs_packages/src/resolve.rs +++ b/crates/rattler_installs_packages/src/resolve.rs @@ -1,3 +1,11 @@ +//! This module contains the [`resolve`] function which is used +//! to make the PyPI ecosystem compatible with the [`resolvo`] crate. +//! +//! To use this enable the `resolve` feature. +//! Note that this module can also serve an example to integrate an alternate packaging system +//! with [`resolvo`]. +//! +//! See the `rip_bin` crate for an example of how to use the [`resolve`] function in the: [RIP Repo](https://github.com/prefix-dev/rip) use crate::{ CompareOp, Extra, NormalizedPackageName, PackageDb, PackageName, Requirement, Specifier, Specifiers, UserRequirement, Version, Wheel, @@ -13,7 +21,8 @@ use tokio::task; #[repr(transparent)] #[derive(Clone, Debug, Hash, Eq, PartialEq)] -pub struct PypiVersionSet(Specifiers); +/// This is a wrapper around [`Specifiers`] that implements [`VersionSet`] +struct PypiVersionSet(Specifiers); impl From for PypiVersionSet { fn from(value: Specifiers) -> Self { @@ -29,7 +38,9 @@ impl Display for PypiVersionSet { #[repr(transparent)] #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] -pub struct PypiVersion(pub Version); +/// This is a wrapper around [`Version`] that serves a version +/// within the [`PypiVersionSet`] version set. +struct PypiVersion(pub Version); impl VersionSet for PypiVersionSet { type V = PypiVersion; @@ -52,12 +63,17 @@ impl Display for PypiVersion { } #[derive(PartialEq, Eq, Hash, Clone)] +/// This can either be a base package name or with an extra +/// this is used to support optional dependencies pub enum PypiPackageName { + /// Regular dependency Base(NormalizedPackageName), + /// Optional dependency Extra(NormalizedPackageName, Extra), } impl PypiPackageName { + /// Returns the actual package (normalized) name without the extra pub fn base(&self) -> &NormalizedPackageName { match self { PypiPackageName::Base(normalized) => normalized, @@ -65,6 +81,7 @@ impl PypiPackageName { } } + /// Retrieves the extra if it is available pub fn extra(&self) -> Option<&Extra> { match self { PypiPackageName::Base(_) => None, @@ -82,12 +99,15 @@ impl Display for PypiPackageName { } } -pub struct PypiDependencyProvider<'db> { +/// This is a [`DependencyProvider`] for PyPI packages +struct PypiDependencyProvider<'db> { pool: Pool, package_db: &'db PackageDb, } impl<'db> PypiDependencyProvider<'db> { + /// Creates a new PypiDependencyProvider + /// for use with the [`resolvo`] crate pub fn new(package_db: &'db PackageDb) -> Self { Self { pool: Pool::new(), @@ -96,7 +116,7 @@ impl<'db> PypiDependencyProvider<'db> { } } -impl<'db> DependencyProvider for PypiDependencyProvider<'db> { +impl DependencyProvider for PypiDependencyProvider<'_> { fn pool(&self) -> &Pool { &self.pool } diff --git a/crates/rattler_installs_packages/src/specifier.rs b/crates/rattler_installs_packages/src/specifier.rs index fa64d79f..6974ef34 100644 --- a/crates/rattler_installs_packages/src/specifier.rs +++ b/crates/rattler_installs_packages/src/specifier.rs @@ -8,20 +8,26 @@ use serde_with::{DeserializeFromStr, SerializeDisplay}; use smallvec::{smallvec, SmallVec}; use std::{fmt::Display, ops::Range, str::FromStr}; -// TODO: See if we can parse this a little better than just an operator and a string. Everytime +// TODO: See if we can parse this a little better than just an operator and a string. Every time // `satisfied_by` is called `to_ranges` is called. We can probably cache that. #[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// A specifier is a comparison operator and a version. +/// See [PEP-440](https://peps.python.org/pep-0440/#version-specifiers) pub struct Specifier { + /// Compartions operator pub op: CompareOp, + /// Version pub value: String, } impl Specifier { + /// Returns true if the specifier is satisfied by the given version. pub fn satisfied_by(&self, version: &Version) -> miette::Result { Ok(self.to_ranges()?.into_iter().any(|r| r.contains(version))) } + /// Converts the specifier to a set of ranges. pub fn to_ranges(&self) -> miette::Result; 1]>> { self.op.ranges(&self.value) } @@ -34,9 +40,11 @@ impl Display for Specifier { } #[derive(Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr, Default, Hash)] +/// A collection of specifiers, separated by commas. pub struct Specifiers(pub Vec); impl Specifiers { + /// Returns true if the set of specifiers is satisfied by the given version. pub fn satisfied_by(&self, version: &Version) -> miette::Result { for specifier in &self.0 { if !specifier.satisfied_by(version)? { @@ -73,6 +81,8 @@ impl FromStr for Specifiers { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[allow(missing_docs)] +/// Models a comparison operator in a version specifier. pub enum CompareOp { LessThanEqual, StrictlyLessThan, @@ -132,11 +142,11 @@ fn parse_version_wildcard(input: &str) -> miette::Result<(Version, bool)> { Ok((version, wildcard)) } -/// Converts a comparison like ">= 1.2" into a union of [half, open) ranges. -/// -/// Has to take a string, not a Version, because == and != can take "wildcards", which -/// are not valid versions. impl CompareOp { + /// Converts a comparison like ">= 1.2" into a union of [half, open) ranges. + /// + /// Has to take a string, not a Version, because == and != can take "wildcards", which + /// are not valid versions. pub fn ranges(&self, rhs: &str) -> miette::Result; 1]>> { use CompareOp::*; let (version, wildcard) = parse_version_wildcard(rhs)?;