mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Add version selection
This commit is contained in:
parent
44b444494e
commit
0f10595ac3
8 changed files with 131 additions and 29 deletions
|
@ -19,3 +19,4 @@ async-std = { version = "1.12.0", features = [
|
|||
"unstable",
|
||||
] }
|
||||
futures = "0.3.28"
|
||||
pep508_rs = "0.2.3"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ use url::Url;
|
|||
mod api;
|
||||
mod error;
|
||||
|
||||
pub use api::SimpleJson;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PypiClientBuilder {
|
||||
registry: Url,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
pub mod wheel;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
|
73
crates/puffin-requirements/src/wheel.rs
Normal file
73
crates/puffin-requirements/src/wheel.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,4 +1 @@
|
|||
flask
|
||||
black
|
||||
jinja2
|
||||
pyyaml
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue