Add version selection

This commit is contained in:
Charlie Marsh 2023-10-04 20:44:59 -04:00
parent 44b444494e
commit 0f10595ac3
8 changed files with 131 additions and 29 deletions

View file

@ -19,3 +19,4 @@ async-std = { version = "1.12.0", features = [
"unstable",
] }
futures = "0.3.28"
pep508_rs = "0.2.3"

View file

@ -3,8 +3,10 @@ use std::str::FromStr;
use anyhow::Result;
use futures::{StreamExt, TryFutureExt};
use pep508_rs::VersionOrUrl;
use puffin_client::PypiClientBuilder;
use puffin_client::{PypiClientBuilder, SimpleJson};
use puffin_requirements::wheel::WheelName;
use puffin_requirements::Requirement;
use crate::commands::ExitStatus;
@ -26,7 +28,7 @@ pub(crate) async fn install(src: &Path) -> Result<ExitStatus> {
let mut package_stream = package_stream
.map(|requirement: Requirement| {
client
.simple(requirement.clone().name)
.simple(requirement.name.clone())
.map_ok(move |metadata| (metadata, requirement))
})
.buffer_unordered(32)
@ -42,11 +44,33 @@ pub(crate) async fn install(src: &Path) -> Result<ExitStatus> {
while let Some(chunk) = package_stream.next().await {
in_flight -= chunk.len();
for result in chunk {
let (metadata, requirement) = result?;
let (metadata, requirement): (SimpleJson, Requirement) = result?;
// TODO(charlie): Support URLs. Right now, we treat a URL as an unpinned dependency.
let specifiers = requirement.version_or_url.and_then(|version_or_url| {
match version_or_url {
VersionOrUrl::VersionSpecifier(specifiers) => Some(specifiers),
VersionOrUrl::Url(_) => None,
}
});
// Pick a version that satisfies the requirement.
let Some(file) = metadata.files.iter().rev().find(|file| {
// We only support wheels for now.
let Ok(name) = WheelName::from_str(file.filename.as_str()) else {
return false;
};
specifiers
.iter()
.all(|specifier| specifier.contains(&name.version))
}) else {
continue;
};
#[allow(clippy::print_stdout)]
{
println!("{metadata:#?}");
println!("{requirement:#?}");
println!("{}: {:?}", requirement.name, file);
}
}

View file

@ -50,49 +50,49 @@ impl PypiClient {
#[derive(Debug, Serialize, Deserialize)]
pub struct SimpleJson {
files: Vec<File>,
meta: Meta,
name: String,
versions: Vec<String>,
pub files: Vec<File>,
pub meta: Meta,
pub name: String,
pub versions: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct File {
core_metadata: Metadata,
data_dist_info_metadata: Metadata,
filename: String,
hashes: Hashes,
requires_python: Option<String>,
size: i64,
upload_time: String,
url: String,
yanked: Yanked,
pub struct File {
pub core_metadata: Metadata,
pub data_dist_info_metadata: Metadata,
pub filename: String,
pub hashes: Hashes,
pub requires_python: Option<String>,
pub size: i64,
pub upload_time: String,
pub url: String,
pub yanked: Yanked,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum Metadata {
pub enum Metadata {
Bool(bool),
Hashes(Hashes),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum Yanked {
pub enum Yanked {
Bool(bool),
Reason(String),
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Hashes {
sha256: String,
pub struct Hashes {
pub sha256: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct Meta {
pub struct Meta {
#[serde(rename = "_last-serial")]
last_serial: i64,
api_version: String,
pub last_serial: i64,
pub api_version: String,
}

View file

@ -11,6 +11,8 @@ use url::Url;
mod api;
mod error;
pub use api::SimpleJson;
#[derive(Debug, Clone)]
pub struct PypiClientBuilder {
registry: Url,

View file

@ -11,7 +11,10 @@ clap = { version = "4.4.6", features = ["derive"] }
colored = { version = "2.0.4" }
insta = "1.33.0"
memchr = { version = "2.6.4" }
once_cell = "1.18.0"
pep440_rs = "0.3.12"
pep508_rs = { version = "0.2.3" }
regex = "1.9.6"
[dev-dependencies]
criterion = "0.5.1"

View file

@ -1,4 +1,6 @@
pub mod wheel;
use std::borrow::Cow;
use std::ops::Deref;
use std::str::FromStr;

View file

@ -0,0 +1,73 @@
use std::str::FromStr;
use anyhow::{anyhow, bail};
use once_cell::sync::Lazy;
use pep440_rs::Version;
use regex::Regex;
#[derive(Debug, Clone)]
pub struct WheelName {
// TODO(charlie): Normalized package name.
pub distribution: String,
pub version: Version,
pub build_number: Option<u32>,
pub build_name: String,
pub py_tags: Vec<String>,
pub abi_tags: Vec<String>,
pub arch_tags: Vec<String>,
}
static BUILD_TAG_SPLIT: Lazy<Regex> = Lazy::new(|| Regex::new(r"(^[0-9]*)(.*)$").unwrap());
impl FromStr for WheelName {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let suffix = ".whl";
let stem = s
.strip_suffix(suffix)
.ok_or_else(|| anyhow!("expected wheel name to end with {:?}: {:?}", suffix, s))?;
let mut pieces: Vec<&str> = stem.split('-').collect();
let build_number: Option<u32>;
let build_name: String;
if pieces.len() == 6 {
let build_tag = pieces.remove(2);
if build_tag.is_empty() {
bail!("found empty build tag: {s:?}");
}
// unwrap safe because: the regex cannot fail
let captures = BUILD_TAG_SPLIT.captures(build_tag).unwrap();
build_number = captures.get(1).and_then(|m| m.as_str().parse().ok());
// unwrap safe because: this group will always match something, even
// if only the empty string
build_name = captures.get(2).unwrap().as_str().into();
} else {
build_number = None;
build_name = "".to_owned();
}
let [distribution, version, py_tags, abi_tags, arch_tags] = pieces.as_slice() else {
bail!("can't parse binary name {s:?}");
};
let distribution = distribution.to_string();
let version = Version::from_str(version)
.map_err(|e| anyhow!("failed to parse version {:?} from {:?}: {}", version, s, e))?;
let py_tags = py_tags.split('.').map(|tag| tag.into()).collect();
let abi_tags = abi_tags.split('.').map(|tag| tag.into()).collect();
let arch_tags = arch_tags.split('.').map(|tag| tag.into()).collect();
Ok(Self {
distribution,
version,
build_number,
build_name,
py_tags,
abi_tags,
arch_tags,
})
}
}

View file

@ -1,4 +1 @@
flask
black
jinja2
pyyaml