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",
|
"unstable",
|
||||||
] }
|
] }
|
||||||
futures = "0.3.28"
|
futures = "0.3.28"
|
||||||
|
pep508_rs = "0.2.3"
|
||||||
|
|
|
@ -3,8 +3,10 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::{StreamExt, TryFutureExt};
|
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 puffin_requirements::Requirement;
|
||||||
|
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::ExitStatus;
|
||||||
|
@ -26,7 +28,7 @@ pub(crate) async fn install(src: &Path) -> Result<ExitStatus> {
|
||||||
let mut package_stream = package_stream
|
let mut package_stream = package_stream
|
||||||
.map(|requirement: Requirement| {
|
.map(|requirement: Requirement| {
|
||||||
client
|
client
|
||||||
.simple(requirement.clone().name)
|
.simple(requirement.name.clone())
|
||||||
.map_ok(move |metadata| (metadata, requirement))
|
.map_ok(move |metadata| (metadata, requirement))
|
||||||
})
|
})
|
||||||
.buffer_unordered(32)
|
.buffer_unordered(32)
|
||||||
|
@ -42,11 +44,33 @@ pub(crate) async fn install(src: &Path) -> Result<ExitStatus> {
|
||||||
while let Some(chunk) = package_stream.next().await {
|
while let Some(chunk) = package_stream.next().await {
|
||||||
in_flight -= chunk.len();
|
in_flight -= chunk.len();
|
||||||
for result in chunk {
|
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)]
|
#[allow(clippy::print_stdout)]
|
||||||
{
|
{
|
||||||
println!("{metadata:#?}");
|
println!("{}: {:?}", requirement.name, file);
|
||||||
println!("{requirement:#?}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,49 +50,49 @@ impl PypiClient {
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct SimpleJson {
|
pub struct SimpleJson {
|
||||||
files: Vec<File>,
|
pub files: Vec<File>,
|
||||||
meta: Meta,
|
pub meta: Meta,
|
||||||
name: String,
|
pub name: String,
|
||||||
versions: Vec<String>,
|
pub versions: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub(crate) struct File {
|
pub struct File {
|
||||||
core_metadata: Metadata,
|
pub core_metadata: Metadata,
|
||||||
data_dist_info_metadata: Metadata,
|
pub data_dist_info_metadata: Metadata,
|
||||||
filename: String,
|
pub filename: String,
|
||||||
hashes: Hashes,
|
pub hashes: Hashes,
|
||||||
requires_python: Option<String>,
|
pub requires_python: Option<String>,
|
||||||
size: i64,
|
pub size: i64,
|
||||||
upload_time: String,
|
pub upload_time: String,
|
||||||
url: String,
|
pub url: String,
|
||||||
yanked: Yanked,
|
pub yanked: Yanked,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub(crate) enum Metadata {
|
pub enum Metadata {
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
Hashes(Hashes),
|
Hashes(Hashes),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub(crate) enum Yanked {
|
pub enum Yanked {
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
Reason(String),
|
Reason(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct Hashes {
|
pub struct Hashes {
|
||||||
sha256: String,
|
pub sha256: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub(crate) struct Meta {
|
pub struct Meta {
|
||||||
#[serde(rename = "_last-serial")]
|
#[serde(rename = "_last-serial")]
|
||||||
last_serial: i64,
|
pub last_serial: i64,
|
||||||
api_version: String,
|
pub api_version: String,
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ use url::Url;
|
||||||
mod api;
|
mod api;
|
||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
|
pub use api::SimpleJson;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PypiClientBuilder {
|
pub struct PypiClientBuilder {
|
||||||
registry: Url,
|
registry: Url,
|
||||||
|
|
|
@ -11,7 +11,10 @@ clap = { version = "4.4.6", features = ["derive"] }
|
||||||
colored = { version = "2.0.4" }
|
colored = { version = "2.0.4" }
|
||||||
insta = "1.33.0"
|
insta = "1.33.0"
|
||||||
memchr = { version = "2.6.4" }
|
memchr = { version = "2.6.4" }
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
pep440_rs = "0.3.12"
|
||||||
pep508_rs = { version = "0.2.3" }
|
pep508_rs = { version = "0.2.3" }
|
||||||
|
regex = "1.9.6"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.5.1"
|
criterion = "0.5.1"
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
pub mod wheel;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::str::FromStr;
|
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
|
flask
|
||||||
black
|
|
||||||
jinja2
|
|
||||||
pyyaml
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue