mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Vendor packse scenarios and serve them from an in-memory index (#18084)
This makes it easier for us to add resolver scenarios by 1. Rewriting the scenario package generation in Rust 2. Serving scenario packages from memory in a wiremock index 3. Rewriting the scenario test case generation in Rust 4. Dropping all dependencies on packse / the packse index --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -33,6 +33,10 @@ jobs:
|
||||
run: cargo dev generate-all --mode dry-run
|
||||
- name: "Check sysconfig mappings"
|
||||
run: cargo dev generate-sysconfig-metadata --mode check
|
||||
- name: "Install Rustfmt"
|
||||
run: rustup component add rustfmt
|
||||
- name: "Check Packse scenario tests"
|
||||
run: cargo dev generate-scenario-tests --mode check
|
||||
- name: "Check JSON schema"
|
||||
if: ${{ inputs.schema-changed == 'true' }}
|
||||
run: cargo dev generate-json-schema --mode check
|
||||
|
||||
Generated
+23
-7
@@ -1485,7 +1485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2354,7 +2354,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2415,7 +2415,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4148,7 +4148,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4205,7 +4205,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4910,7 +4910,7 @@ dependencies = [
|
||||
"getrandom 0.4.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6262,7 +6262,9 @@ dependencies = [
|
||||
"uv-git",
|
||||
"uv-installer",
|
||||
"uv-macros",
|
||||
"uv-normalize",
|
||||
"uv-options-metadata",
|
||||
"uv-pep440",
|
||||
"uv-pep508",
|
||||
"uv-performance-memory-allocator",
|
||||
"uv-preview",
|
||||
@@ -6270,6 +6272,7 @@ dependencies = [
|
||||
"uv-python",
|
||||
"uv-settings",
|
||||
"uv-static",
|
||||
"uv-test",
|
||||
"uv-workspace",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -7243,7 +7246,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"assert_fs",
|
||||
"astral-tokio-tar",
|
||||
"astral_async_zip",
|
||||
"base64",
|
||||
"flate2",
|
||||
"fs-err",
|
||||
"futures",
|
||||
"ignore",
|
||||
@@ -7253,17 +7259,27 @@ dependencies = [
|
||||
"predicates",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"similar 3.1.0",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"uv-cache",
|
||||
"uv-client",
|
||||
"uv-configuration",
|
||||
"uv-distribution-filename",
|
||||
"uv-fs",
|
||||
"uv-normalize",
|
||||
"uv-pep440",
|
||||
"uv-pep508",
|
||||
"uv-preview",
|
||||
"uv-python",
|
||||
"uv-static",
|
||||
"uv-version",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7709,7 +7725,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -18,7 +18,7 @@ workspace = true
|
||||
uv-cache = { workspace = true, features = ["clap"] }
|
||||
uv-cli = { workspace = true }
|
||||
uv-client = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-configuration = { workspace = true, features = ["clap"] }
|
||||
uv-distribution-filename = { workspace = true }
|
||||
uv-distribution-types = { workspace = true }
|
||||
uv-errors = { workspace = true }
|
||||
@@ -26,13 +26,16 @@ uv-extract = { workspace = true }
|
||||
uv-git = { workspace = true }
|
||||
uv-installer = { workspace = true }
|
||||
uv-macros = { workspace = true }
|
||||
uv-normalize = { workspace = true }
|
||||
uv-options-metadata = { workspace = true }
|
||||
uv-pep440 = { workspace = true }
|
||||
uv-pep508 = { workspace = true }
|
||||
uv-pypi-types = { workspace = true }
|
||||
uv-preview = { workspace = true }
|
||||
uv-python = { workspace = true }
|
||||
uv-settings = { workspace = true, features = ["schemars"] }
|
||||
uv-static = { workspace = true }
|
||||
uv-test = { workspace = true }
|
||||
uv-workspace = { workspace = true, features = ["schemars"] }
|
||||
|
||||
# Any dependencies that are exclusively used in `uv-dev` should be listed as non-workspace
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ use crate::generate_cli_reference::Args as GenerateCliReferenceArgs;
|
||||
use crate::generate_env_vars_reference::Args as GenerateEnvVarsReferenceArgs;
|
||||
use crate::generate_json_schema::Args as GenerateJsonSchemaArgs;
|
||||
use crate::generate_options_reference::Args as GenerateOptionsReferenceArgs;
|
||||
use crate::generate_scenarios::Args as GenerateScenarioTestsArgs;
|
||||
use crate::generate_sysconfig_mappings::Args as GenerateSysconfigMetadataArgs;
|
||||
use crate::list_packages::ListPackagesArgs;
|
||||
#[cfg(feature = "render")]
|
||||
@@ -27,6 +28,7 @@ mod generate_cli_reference;
|
||||
mod generate_env_vars_reference;
|
||||
mod generate_json_schema;
|
||||
mod generate_options_reference;
|
||||
mod generate_scenarios;
|
||||
mod generate_sysconfig_mappings;
|
||||
mod list_packages;
|
||||
mod render_benchmarks;
|
||||
@@ -57,6 +59,8 @@ enum Cli {
|
||||
GenerateCliReference(GenerateCliReferenceArgs),
|
||||
/// Generate the environment variables reference for the documentation.
|
||||
GenerateEnvVarsReference(GenerateEnvVarsReferenceArgs),
|
||||
/// Generate the Packse scenario integration tests.
|
||||
GenerateScenarioTests(GenerateScenarioTestsArgs),
|
||||
/// Generate the sysconfig metadata from derived targets.
|
||||
GenerateSysconfigMetadata(GenerateSysconfigMetadataArgs),
|
||||
#[cfg(feature = "render")]
|
||||
@@ -79,9 +83,25 @@ pub async fn run() -> Result<()> {
|
||||
Cli::GenerateOptionsReference(args) => generate_options_reference::main(&args)?,
|
||||
Cli::GenerateCliReference(args) => generate_cli_reference::main(&args)?,
|
||||
Cli::GenerateEnvVarsReference(args) => generate_env_vars_reference::main(&args)?,
|
||||
Cli::GenerateScenarioTests(args) => generate_scenarios::main(&args)?,
|
||||
Cli::GenerateSysconfigMetadata(args) => generate_sysconfig_mappings::main(&args).await?,
|
||||
#[cfg(feature = "render")]
|
||||
Cli::RenderBenchmarks(args) => render_benchmarks::render_benchmarks(&args)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::CommandFactory;
|
||||
|
||||
use super::Cli;
|
||||
|
||||
#[test]
|
||||
fn scenario_tests_command_uses_explicit_name() {
|
||||
let command = Cli::command();
|
||||
|
||||
assert!(command.find_subcommand("generate-scenario-tests").is_some());
|
||||
assert!(command.find_subcommand("generate-scenarios").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1202,11 +1202,6 @@ impl EnvVars {
|
||||
#[attr_added_in("0.7.21")]
|
||||
pub const UV_TEST_NO_HTTP_RETRY_DELAY: &'static str = "UV_TEST_NO_HTTP_RETRY_DELAY";
|
||||
|
||||
/// Used to set a packse index url for tests.
|
||||
#[attr_hidden]
|
||||
#[attr_added_in("0.2.12")]
|
||||
pub const UV_TEST_PACKSE_INDEX: &'static str = "UV_TEST_PACKSE_INDEX";
|
||||
|
||||
/// Used for testing named indexes in tests.
|
||||
#[attr_hidden]
|
||||
#[attr_added_in("0.5.21")]
|
||||
|
||||
@@ -11,7 +11,6 @@ license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -26,16 +25,23 @@ tracing-durations-export = []
|
||||
uv-cache = { workspace = true, features = ["clap"] }
|
||||
uv-client = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-fs = { workspace = true }
|
||||
uv-distribution-filename = { workspace = true }
|
||||
uv-fs = { workspace = true, features = ["tokio"] }
|
||||
uv-normalize = { workspace = true }
|
||||
uv-pep440 = { workspace = true }
|
||||
uv-pep508 = { workspace = true }
|
||||
uv-preview = { workspace = true }
|
||||
uv-python = { workspace = true }
|
||||
uv-static = { workspace = true }
|
||||
uv-version = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
astral-tokio-tar = { workspace = true }
|
||||
assert_cmd = { workspace = true }
|
||||
assert_fs = { workspace = true }
|
||||
async_zip = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
fs-err = { workspace = true, features = ["tokio"] }
|
||||
futures = { workspace = true }
|
||||
ignore = { workspace = true }
|
||||
@@ -45,6 +51,12 @@ itertools = { workspace = true }
|
||||
predicates = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
similar = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
wiremock = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//! A local HTTP server that serves a directory of files as a PEP 503 flat link page.
|
||||
//!
|
||||
//! Useful for testing `--find-links` with an HTTP URL.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use wiremock::{Request, ResponseTemplate};
|
||||
|
||||
use crate::http_server::{HttpServer, content_type_for_filename};
|
||||
use crate::vendor::{VendorArtifact, vendor_artifacts};
|
||||
|
||||
enum FileData {
|
||||
Bytes(Arc<[u8]>),
|
||||
Vendor(&'static VendorArtifact),
|
||||
}
|
||||
|
||||
impl FileData {
|
||||
fn bytes(&self) -> anyhow::Result<Arc<[u8]>> {
|
||||
match self {
|
||||
Self::Bytes(bytes) => Ok(Arc::clone(bytes)),
|
||||
Self::Vendor(artifact) => artifact.bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A running HTTP server that serves files from a directory as a flat links page.
|
||||
pub struct FindLinksServer {
|
||||
server: HttpServer,
|
||||
}
|
||||
|
||||
impl FindLinksServer {
|
||||
/// Start a server that serves all files in the given directory.
|
||||
pub fn new(directory: &Path) -> Self {
|
||||
let mut files: HashMap<String, FileData> = HashMap::new();
|
||||
let mut filenames: Vec<String> = Vec::new();
|
||||
|
||||
for entry in fs_err::read_dir(directory).expect("failed to read find-links directory") {
|
||||
let entry = entry.expect("failed to read directory entry");
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let Some(filename) = path.file_name().map(|n| n.to_string_lossy().to_string()) else {
|
||||
continue;
|
||||
};
|
||||
let bytes = fs_err::read(&path).expect("failed to read file");
|
||||
files.insert(filename.clone(), FileData::Bytes(bytes.into()));
|
||||
filenames.push(filename);
|
||||
}
|
||||
filenames.sort();
|
||||
|
||||
let files = Arc::new(files);
|
||||
let filenames = Arc::new(filenames);
|
||||
let server = HttpServer::start(move |request, server_uri| {
|
||||
handle_request(request, server_uri, &files, &filenames)
|
||||
});
|
||||
|
||||
Self { server }
|
||||
}
|
||||
|
||||
/// Start a server that serves the pinned registry artifacts used by tests.
|
||||
pub fn vendor() -> Self {
|
||||
let mut files: HashMap<String, FileData> = HashMap::new();
|
||||
let mut filenames: Vec<String> = Vec::new();
|
||||
|
||||
for artifact in vendor_artifacts() {
|
||||
files.insert(artifact.filename.to_string(), FileData::Vendor(artifact));
|
||||
filenames.push(artifact.filename.to_string());
|
||||
}
|
||||
filenames.sort();
|
||||
|
||||
let files = Arc::new(files);
|
||||
let filenames = Arc::new(filenames);
|
||||
let server = HttpServer::start(move |request, server_uri| {
|
||||
handle_request(request, server_uri, &files, &filenames)
|
||||
});
|
||||
|
||||
Self { server }
|
||||
}
|
||||
|
||||
/// The base URL of the server (for use with `--find-links`).
|
||||
pub fn url(&self) -> &str {
|
||||
self.server.url()
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
request: &Request,
|
||||
server_uri: &str,
|
||||
files: &HashMap<String, FileData>,
|
||||
filenames: &[String],
|
||||
) -> ResponseTemplate {
|
||||
let path = request.url.path();
|
||||
|
||||
if path == "/" {
|
||||
let links = filenames
|
||||
.iter()
|
||||
.map(|filename| format!("<a href=\"{server_uri}/{filename}\">{filename}</a>"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let html = format!("<!DOCTYPE html>\n<html><body>\n{links}\n</body></html>");
|
||||
return ResponseTemplate::new(200).set_body_raw(html, "text/html");
|
||||
}
|
||||
|
||||
let filename = path.trim_start_matches('/');
|
||||
if let Some(file) = files.get(filename) {
|
||||
return match file.bytes() {
|
||||
Ok(bytes) => ResponseTemplate::new(200)
|
||||
.set_body_raw(bytes.to_vec(), content_type_for_filename(filename)),
|
||||
Err(error) => ResponseTemplate::new(500).set_body_string(format!("{error:#}")),
|
||||
};
|
||||
}
|
||||
|
||||
ResponseTemplate::new(404)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FindLinksServer;
|
||||
use crate::vendor::vendor_artifacts;
|
||||
|
||||
#[test]
|
||||
fn vendor_server_construction_does_not_load_artifacts() {
|
||||
let _server = FindLinksServer::vendor();
|
||||
|
||||
assert!(
|
||||
vendor_artifacts()
|
||||
.iter()
|
||||
.all(|artifact| !artifact.is_loaded())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::path::Path;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use wiremock::matchers::any;
|
||||
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
|
||||
|
||||
/// Background wiremock server shared by the local test indexes in this crate.
|
||||
pub(crate) struct HttpServer {
|
||||
url: String,
|
||||
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
thread: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
pub(crate) fn start(
|
||||
handler: impl Fn(&Request, &str) -> ResponseTemplate + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
let (url_tx, url_rx) = std::sync::mpsc::channel::<String>();
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
let thread = thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("failed to create tokio runtime for local HTTP test server");
|
||||
|
||||
runtime.block_on(async move {
|
||||
let server = MockServer::start().await;
|
||||
let server_uri = server.uri();
|
||||
|
||||
Mock::given(any())
|
||||
.respond_with(move |request: &Request| handler(request, &server_uri))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
url_tx.send(server.uri()).ok();
|
||||
let _ = shutdown_rx.await;
|
||||
});
|
||||
});
|
||||
|
||||
let url = url_rx
|
||||
.recv_timeout(Duration::from_secs(30))
|
||||
.expect("timed out waiting for local HTTP test server to start");
|
||||
|
||||
Self {
|
||||
url,
|
||||
shutdown: Some(shutdown_tx),
|
||||
thread: Some(thread),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn url(&self) -> &str {
|
||||
&self.url
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn content_type_for_filename(filename: &str) -> &'static str {
|
||||
if Path::new(filename)
|
||||
.extension()
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("whl"))
|
||||
{
|
||||
"application/zip"
|
||||
} else {
|
||||
"application/gzip"
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HttpServer {
|
||||
fn drop(&mut self) {
|
||||
drop(self.shutdown.take());
|
||||
if let Some(thread) = self.thread.take() {
|
||||
thread.join().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
// The `unreachable_pub` is to silence false positives in RustRover.
|
||||
#![allow(dead_code, unreachable_pub)]
|
||||
|
||||
pub mod find_links;
|
||||
mod http_server;
|
||||
pub mod packse;
|
||||
mod vendor;
|
||||
|
||||
use std::borrow::BorrowMut;
|
||||
use std::ffi::OsString;
|
||||
use std::io::Write as _;
|
||||
@@ -36,7 +41,6 @@ use uv_static::EnvVars;
|
||||
// Shared test timestamp for deterministic package availability and relative times.
|
||||
static TEST_TIMESTAMP: &str = "2024-03-25T00:00:00Z";
|
||||
|
||||
pub const PACKSE_VERSION: &str = "0.3.59";
|
||||
pub const DEFAULT_PYTHON_VERSION: &str = "3.12";
|
||||
|
||||
// The expected latest patch version for each Python minor version.
|
||||
@@ -47,26 +51,6 @@ pub const LATEST_PYTHON_3_12: &str = "3.12.13";
|
||||
pub const LATEST_PYTHON_3_11: &str = "3.11.15";
|
||||
pub const LATEST_PYTHON_3_10: &str = "3.10.20";
|
||||
|
||||
/// Using a find links url allows using `--index-url` instead of `--extra-index-url` in tests
|
||||
/// to prevent dependency confusion attacks against our test suite.
|
||||
pub fn build_vendor_links_url() -> String {
|
||||
env::var(EnvVars::UV_TEST_PACKSE_INDEX)
|
||||
.map(|url| format!("{}/vendor/", url.trim_end_matches('/')))
|
||||
.ok()
|
||||
.unwrap_or(format!(
|
||||
"https://astral-sh.github.io/packse/{PACKSE_VERSION}/vendor/"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn packse_index_url() -> String {
|
||||
env::var(EnvVars::UV_TEST_PACKSE_INDEX)
|
||||
.map(|url| format!("{}/simple-html/", url.trim_end_matches('/')))
|
||||
.ok()
|
||||
.unwrap_or(format!(
|
||||
"https://astral-sh.github.io/packse/{PACKSE_VERSION}/simple-html/"
|
||||
))
|
||||
}
|
||||
|
||||
/// Create a new [`TestContext`] with the given Python version.
|
||||
///
|
||||
/// Creates a virtual environment for the test.
|
||||
@@ -1057,19 +1041,6 @@ impl TestContext {
|
||||
// Destroy any remaining UNC prefixes (Windows only)
|
||||
filters.push((r"\\\\\?\\".to_string(), String::new()));
|
||||
|
||||
// Remove the version from the packse url in lockfile snapshots. This avoids having a huge
|
||||
// diff any time we upgrade packse
|
||||
filters.push((
|
||||
format!("https://astral-sh.github.io/packse/{PACKSE_VERSION}"),
|
||||
"https://astral-sh.github.io/packse/PACKSE_VERSION".to_string(),
|
||||
));
|
||||
// Developer convenience
|
||||
if let Ok(packse_test_index) = env::var(EnvVars::UV_TEST_PACKSE_INDEX) {
|
||||
filters.push((
|
||||
packse_test_index.trim_end_matches('/').to_string(),
|
||||
"https://astral-sh.github.io/packse/PACKSE_VERSION".to_string(),
|
||||
));
|
||||
}
|
||||
// For wiremock tests
|
||||
filters.push((r"127\.0\.0\.1:\d*".to_string(), "[LOCALHOST]".to_string()));
|
||||
// Avoid breaking the tests when bumping the uv version
|
||||
@@ -1202,6 +1173,8 @@ impl TestContext {
|
||||
.env_remove(EnvVars::VIRTUAL_ENV)
|
||||
// Disable wrapping of uv output for readability / determinism in snapshots.
|
||||
.env(EnvVars::UV_NO_WRAP, "1")
|
||||
// Avoid reading host system configuration unless a test opts in.
|
||||
.env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
|
||||
// While we disable wrapping in uv above, invoked tools may still wrap their output so
|
||||
// we set a fixed `COLUMNS` value for isolation from terminal width.
|
||||
.env(EnvVars::COLUMNS, "100")
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//! Local mock index for packse scenario tests.
|
||||
//!
|
||||
//! This module provides a [`PackseServer`] that reads packse scenario TOML definitions
|
||||
//! and serves a PEP 691 Simple API + wheel/sdist downloads via a local wiremock server.
|
||||
//! Each test gets its own server instance, so package names need no prefix mangling.
|
||||
|
||||
pub mod scenario;
|
||||
mod server;
|
||||
mod wheel;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use server::PackseServer;
|
||||
|
||||
fn workspace_root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.map(Path::to_path_buf)
|
||||
.expect("CARGO_MANIFEST_DIR should be nested under workspace root")
|
||||
}
|
||||
|
||||
/// Base directory containing the vendored packse scenario TOML files.
|
||||
fn scenarios_dir() -> PathBuf {
|
||||
workspace_root().join("test").join("scenarios")
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
//! Typed representation of the vendored Packse scenario TOML files.
|
||||
//!
|
||||
//! The nested TOML tables map directly onto [`Scenario::packages`]:
|
||||
//! `[packages.<name>.versions.<version>]` becomes a [`PackageName`] key, then a [`Version`] key.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
use uv_configuration::TargetTriple;
|
||||
use uv_distribution_filename::WheelFilename;
|
||||
use uv_normalize::{ExtraName, PackageName};
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_pep508::{MarkerTree, Requirement};
|
||||
use uv_python::PythonVersion;
|
||||
|
||||
/// A complete packse scenario definition.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Scenario {
|
||||
/// The scenario name (e.g., `"fork-basic"`).
|
||||
pub name: String,
|
||||
|
||||
/// Human-readable description.
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Packages keyed by the TOML segment in `[packages.<name>]`.
|
||||
#[serde(default)]
|
||||
pub packages: BTreeMap<PackageName, Package>,
|
||||
|
||||
/// The root (entrypoint) requirements.
|
||||
pub root: RootPackage,
|
||||
|
||||
/// What we expect the resolver to produce.
|
||||
pub expected: Expected,
|
||||
|
||||
/// Metadata about the Python environment.
|
||||
#[serde(default)]
|
||||
pub environment: Environment,
|
||||
|
||||
/// Additional resolver options.
|
||||
#[serde(default)]
|
||||
pub resolver_options: ResolverOptions,
|
||||
}
|
||||
|
||||
impl Scenario {
|
||||
/// Parse a single scenario from a TOML file path.
|
||||
pub fn from_path(path: &Path) -> Result<Self> {
|
||||
let contents = fs_err::read_to_string(path)
|
||||
.with_context(|| format!("failed to read scenario file `{}`", path.display()))?;
|
||||
toml::from_str(&contents)
|
||||
.with_context(|| format!("failed to parse scenario file `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Construct an otherwise-empty scenario for indexes that should only expose vendored files.
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
name: String::new(),
|
||||
description: None,
|
||||
packages: BTreeMap::new(),
|
||||
root: RootPackage {
|
||||
requires_python: None,
|
||||
requires: Vec::new(),
|
||||
},
|
||||
expected: Expected {
|
||||
satisfiable: true,
|
||||
packages: BTreeMap::new(),
|
||||
explanation: None,
|
||||
},
|
||||
environment: Environment::default(),
|
||||
resolver_options: ResolverOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A package with one or more versions.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Package {
|
||||
pub versions: BTreeMap<Version, PackageMetadata>,
|
||||
}
|
||||
|
||||
/// Metadata for a single version of a package.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PackageMetadata {
|
||||
/// The `Requires-Python` specifier. Defaults to `">=3.12"`.
|
||||
#[serde(default = "default_requires_python")]
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
|
||||
/// Dependency requirements.
|
||||
#[serde(default)]
|
||||
pub requires: Vec<Requirement>,
|
||||
|
||||
/// Extra names mapped to their optional dependency requirements.
|
||||
#[serde(default)]
|
||||
pub extras: BTreeMap<ExtraName, Vec<Requirement>>,
|
||||
|
||||
/// Whether to produce a source distribution.
|
||||
#[serde(default = "default_true")]
|
||||
pub sdist: bool,
|
||||
|
||||
/// Whether to produce a wheel.
|
||||
#[serde(default = "default_true")]
|
||||
pub wheel: bool,
|
||||
|
||||
/// Whether this version is yanked.
|
||||
#[serde(default)]
|
||||
pub yanked: bool,
|
||||
|
||||
/// Specific wheel tags to produce (e.g., `["cp312-abi3-win_amd64"]`).
|
||||
/// An empty list means produce only the default `py3-none-any` wheel.
|
||||
#[serde(default)]
|
||||
pub wheel_tags: Vec<WheelTag>,
|
||||
}
|
||||
|
||||
/// A validated three-component compatibility tag for generated wheels.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WheelTag(String);
|
||||
|
||||
impl WheelTag {
|
||||
/// Return the compatibility tag as it should appear in a wheel filename.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for WheelTag {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(tag: &str) -> Result<Self, Self::Err> {
|
||||
if tag.split('-').count() != 3 {
|
||||
return Err(format!(
|
||||
"wheel tag `{tag}` must have exactly three components"
|
||||
));
|
||||
}
|
||||
WheelFilename::from_str(&format!("package-0-{tag}.whl"))
|
||||
.map_err(|error| format!("wheel tag `{tag}` is invalid: {error}"))?;
|
||||
Ok(Self(tag.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for WheelTag {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let tag = String::deserialize(deserializer)?;
|
||||
Self::from_str(&tag).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// The root/entrypoint package.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RootPackage {
|
||||
/// `Requires-Python` for the root.
|
||||
#[serde(default = "default_requires_python")]
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
|
||||
/// Top-level requirements.
|
||||
#[serde(default)]
|
||||
pub requires: Vec<Requirement>,
|
||||
}
|
||||
|
||||
/// Expected resolution outcome.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Expected {
|
||||
/// Whether the scenario is satisfiable.
|
||||
pub satisfiable: bool,
|
||||
|
||||
/// Expected installed package names mapped to resolved versions.
|
||||
#[serde(default)]
|
||||
pub packages: BTreeMap<PackageName, Version>,
|
||||
|
||||
/// Optional explanation.
|
||||
#[serde(default)]
|
||||
pub explanation: Option<String>,
|
||||
}
|
||||
|
||||
/// Python environment metadata.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Environment {
|
||||
/// Active Python version.
|
||||
#[serde(default = "default_python")]
|
||||
pub python: PythonVersion,
|
||||
|
||||
/// Additional Python versions available on the system.
|
||||
#[serde(default)]
|
||||
pub additional_python: Vec<PythonVersion>,
|
||||
}
|
||||
|
||||
impl Default for Environment {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
python: default_python(),
|
||||
additional_python: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional resolver options.
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ResolverOptions {
|
||||
/// Python version override for resolution.
|
||||
#[serde(default)]
|
||||
pub python: Option<PythonVersion>,
|
||||
|
||||
/// Enable pre-release selection.
|
||||
#[serde(default)]
|
||||
pub prereleases: bool,
|
||||
|
||||
/// Packages that must use pre-built wheels (no building from source).
|
||||
#[serde(default)]
|
||||
pub no_build: Vec<PackageName>,
|
||||
|
||||
/// Packages that must NOT use pre-built wheels (must build from source).
|
||||
#[serde(default)]
|
||||
pub no_binary: Vec<PackageName>,
|
||||
|
||||
/// Universal (multi-platform) resolution mode.
|
||||
#[serde(default)]
|
||||
pub universal: bool,
|
||||
|
||||
/// Python platform to resolve for.
|
||||
#[serde(default)]
|
||||
pub python_platform: Option<TargetTriple>,
|
||||
|
||||
/// Required environments (platform markers).
|
||||
#[serde(default)]
|
||||
pub required_environments: Vec<MarkerTree>,
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)] // Must return `Option` for serde `default`
|
||||
fn default_requires_python() -> Option<VersionSpecifiers> {
|
||||
Some(VersionSpecifiers::from_str(">=3.12").expect("default requires-python should be valid"))
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_python() -> PythonVersion {
|
||||
PythonVersion::from_str("3.12").expect("default Python version should be valid")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_basic_scenario() {
|
||||
let toml = r#"
|
||||
name = "fork-basic"
|
||||
description = "An extremely basic test."
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
"#;
|
||||
let scenario: Scenario = toml::from_str(toml).expect("scenario should parse");
|
||||
let package_name = PackageName::from_str("a").expect("valid package name");
|
||||
assert_eq!(scenario.name, "fork-basic");
|
||||
assert!(scenario.resolver_options.universal);
|
||||
assert_eq!(scenario.packages.len(), 1);
|
||||
assert_eq!(scenario.packages[&package_name].versions.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_extras_scenario() {
|
||||
let toml = r#"
|
||||
name = "all-extras-required"
|
||||
description = "Multiple optional dependencies."
|
||||
|
||||
[root]
|
||||
requires = ["a[all]"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "1.0.0"
|
||||
c = "1.0.0"
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
[packages.c.versions."1.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0".extras]
|
||||
all = ["a[extra_b]", "a[extra_c]"]
|
||||
extra_b = ["b"]
|
||||
extra_c = ["c"]
|
||||
"#;
|
||||
let scenario: Scenario = toml::from_str(toml).expect("scenario should parse");
|
||||
let package_name = PackageName::from_str("a").expect("valid package name");
|
||||
let version = Version::from_str("1.0.0").expect("valid version");
|
||||
let extra_name = ExtraName::from_str("extra_b").expect("valid extra name");
|
||||
assert_eq!(scenario.name, "all-extras-required");
|
||||
let a_meta = &scenario.packages[&package_name].versions[&version];
|
||||
assert_eq!(a_meta.extras.len(), 3);
|
||||
assert_eq!(
|
||||
a_meta.extras[&extra_name],
|
||||
vec![Requirement::from_str("b").expect("valid requirement")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_invalid_requires_python() {
|
||||
let toml = r#"
|
||||
name = "invalid-requires-python"
|
||||
|
||||
[root]
|
||||
requires = []
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires_python = "not a specifier"
|
||||
"#;
|
||||
|
||||
assert!(toml::from_str::<Scenario>(toml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_unknown_metadata_field() {
|
||||
let toml = r#"
|
||||
name = "unknown-metadata-field"
|
||||
|
||||
[root]
|
||||
requires = ["a"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
wheels = false
|
||||
"#;
|
||||
|
||||
assert!(toml::from_str::<Scenario>(toml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_invalid_wheel_tag() {
|
||||
let toml = r#"
|
||||
name = "invalid-wheel-tag"
|
||||
|
||||
[root]
|
||||
requires = ["a"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
wheel_tags = ["1-py3-none-any"]
|
||||
"#;
|
||||
|
||||
assert!(toml::from_str::<Scenario>(toml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_is_included_in_parse_errors() {
|
||||
let temporary_directory =
|
||||
tempfile::tempdir().expect("temporary directory should be created");
|
||||
let path = temporary_directory.path().join("invalid.toml");
|
||||
fs_err::write(&path, "not valid TOML = [").expect("invalid scenario should be written");
|
||||
|
||||
let error = Scenario::from_path(&path).expect_err("scenario should fail to parse");
|
||||
insta::assert_snapshot!(
|
||||
error
|
||||
.to_string()
|
||||
.replace(temporary_directory.path().to_string_lossy().as_ref(), "[TEMP_DIR]")
|
||||
.replace('\\', "/"),
|
||||
@"failed to parse scenario file `[TEMP_DIR]/invalid.toml`"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
//! A per-test wiremock server that serves packse scenario packages.
|
||||
//!
|
||||
//! Each [`PackseServer`] reads a single scenario TOML file and serves:
|
||||
//! - PEP 691 Simple API at `/simple/{package}/`
|
||||
//! - Distribution downloads at `/files/{filename}`
|
||||
//!
|
||||
//! Cached build dependencies are exposed through the same `/simple/*` and
|
||||
//! `/files/*` routes as scenario packages.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use wiremock::{Request, ResponseTemplate};
|
||||
|
||||
use uv_distribution_filename::WheelFilename;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::VersionSpecifiers;
|
||||
|
||||
use crate::http_server::{HttpServer, content_type_for_filename};
|
||||
use crate::vendor::{VendorArtifact, vendor_artifacts};
|
||||
|
||||
use super::scenario::{Scenario, WheelTag};
|
||||
use super::scenarios_dir;
|
||||
use super::wheel::{generate_sdist, generate_wheel, sha256_hex};
|
||||
|
||||
const PACKSE_UPLOAD_TIME: &str = "2024-03-24T00:00:00Z";
|
||||
|
||||
/// Information about a single distribution file (metadata for the Simple API).
|
||||
struct DistInfo {
|
||||
filename: String,
|
||||
sha256: String,
|
||||
requires_python: Option<VersionSpecifiers>,
|
||||
upload_time: &'static str,
|
||||
yanked: bool,
|
||||
}
|
||||
|
||||
/// All distributions for a given package name, across versions.
|
||||
struct PackageEntry {
|
||||
dists: Vec<DistInfo>,
|
||||
}
|
||||
|
||||
enum FileData {
|
||||
Bytes(Arc<[u8]>),
|
||||
Vendor(&'static VendorArtifact),
|
||||
}
|
||||
|
||||
impl FileData {
|
||||
fn bytes(&self) -> anyhow::Result<Arc<[u8]>> {
|
||||
match self {
|
||||
Self::Bytes(bytes) => Ok(Arc::clone(bytes)),
|
||||
Self::Vendor(artifact) => artifact.bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The complete pre-indexed database for a server instance.
|
||||
struct ServerIndex {
|
||||
/// Simple API: normalized package name → list of distribution metadata.
|
||||
packages: HashMap<PackageName, PackageEntry>,
|
||||
/// File downloads: filename → generated bytes or a lazy vendored artifact.
|
||||
files: HashMap<String, FileData>,
|
||||
}
|
||||
|
||||
/// A running mock PyPI server for a single packse scenario.
|
||||
///
|
||||
/// The server runs on a background thread with its own single-threaded tokio runtime.
|
||||
/// When [`PackseServer`] is dropped, the background thread and server are shut down.
|
||||
pub struct PackseServer {
|
||||
server: HttpServer,
|
||||
}
|
||||
|
||||
impl PackseServer {
|
||||
/// Load a scenario from a TOML path (relative to the vendored scenarios directory)
|
||||
/// and start a mock server for it.
|
||||
pub fn new(scenario_path: &str) -> Self {
|
||||
let full_path = scenarios_dir().join(scenario_path);
|
||||
let scenario =
|
||||
Scenario::from_path(&full_path).expect("vendored Packse scenario should parse");
|
||||
Self::from_scenario(&scenario)
|
||||
}
|
||||
|
||||
/// Start a mock server with no packages (only cached build dependencies).
|
||||
///
|
||||
/// Useful as a dummy index that will 404 for any non-cached package lookup.
|
||||
pub fn empty() -> Self {
|
||||
Self::from_scenario(&Scenario::empty())
|
||||
}
|
||||
|
||||
/// Start a mock server for the given scenario.
|
||||
pub fn from_scenario(scenario: &Scenario) -> Self {
|
||||
let index = Arc::new(build_server_index(scenario));
|
||||
let server = HttpServer::start(move |request, server_uri| {
|
||||
handle_request(request, server_uri, &index)
|
||||
});
|
||||
|
||||
Self { server }
|
||||
}
|
||||
|
||||
/// The Simple API index URL (e.g., `http://127.0.0.1:PORT/simple/`).
|
||||
pub fn index_url(&self) -> String {
|
||||
format!("{}/simple/", self.server.url())
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the complete [`ServerIndex`] from a scenario and cached build dependencies.
|
||||
fn build_server_index(scenario: &Scenario) -> ServerIndex {
|
||||
let mut packages = HashMap::new();
|
||||
let mut files: HashMap<String, FileData> = HashMap::new();
|
||||
|
||||
for (package_name, package) in &scenario.packages {
|
||||
let mut dists = Vec::new();
|
||||
|
||||
for (version, meta) in &package.versions {
|
||||
if meta.wheel {
|
||||
let tags = if meta.wheel_tags.is_empty() {
|
||||
vec!["py3-none-any"]
|
||||
} else {
|
||||
meta.wheel_tags.iter().map(WheelTag::as_str).collect()
|
||||
};
|
||||
|
||||
for tag in tags {
|
||||
let (filename, bytes) = generate_wheel(
|
||||
package_name,
|
||||
version,
|
||||
&meta.requires,
|
||||
&meta.extras,
|
||||
meta.requires_python.as_ref(),
|
||||
tag,
|
||||
);
|
||||
let sha256 = sha256_hex(&bytes);
|
||||
files.insert(filename.clone(), FileData::Bytes(bytes.into()));
|
||||
dists.push(DistInfo {
|
||||
filename,
|
||||
sha256,
|
||||
requires_python: meta.requires_python.clone(),
|
||||
upload_time: PACKSE_UPLOAD_TIME,
|
||||
yanked: meta.yanked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if meta.sdist {
|
||||
let (filename, bytes) = generate_sdist(
|
||||
package_name,
|
||||
version,
|
||||
&meta.requires,
|
||||
&meta.extras,
|
||||
meta.requires_python.as_ref(),
|
||||
);
|
||||
let sha256 = sha256_hex(&bytes);
|
||||
files.insert(filename.clone(), FileData::Bytes(bytes.into()));
|
||||
dists.push(DistInfo {
|
||||
filename,
|
||||
sha256,
|
||||
requires_python: meta.requires_python.clone(),
|
||||
upload_time: PACKSE_UPLOAD_TIME,
|
||||
yanked: meta.yanked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
packages.insert(package_name.clone(), PackageEntry { dists });
|
||||
}
|
||||
|
||||
for artifact in vendor_artifacts() {
|
||||
if !Path::new(artifact.filename)
|
||||
.extension()
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("whl"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let wheel_filename =
|
||||
WheelFilename::from_str(artifact.filename).expect("invalid vendor wheel filename");
|
||||
|
||||
files.insert(artifact.filename.to_string(), FileData::Vendor(artifact));
|
||||
packages
|
||||
.entry(wheel_filename.name)
|
||||
.or_insert_with(|| PackageEntry { dists: Vec::new() })
|
||||
.dists
|
||||
.push(DistInfo {
|
||||
filename: artifact.filename.to_string(),
|
||||
sha256: artifact.sha256.to_string(),
|
||||
requires_python: None,
|
||||
upload_time: PACKSE_UPLOAD_TIME,
|
||||
yanked: false,
|
||||
});
|
||||
}
|
||||
|
||||
ServerIndex { packages, files }
|
||||
}
|
||||
|
||||
fn handle_request(req: &Request, server_uri: &str, index: &ServerIndex) -> ResponseTemplate {
|
||||
let path = req.url.path();
|
||||
|
||||
if let Some(pkg) = extract_package_name(path) {
|
||||
let Ok(package_name) = PackageName::from_str(pkg) else {
|
||||
return ResponseTemplate::new(404);
|
||||
};
|
||||
|
||||
if let Some(entry) = index.packages.get(&package_name) {
|
||||
return build_simple_api_response(pkg, entry, server_uri);
|
||||
}
|
||||
return ResponseTemplate::new(404);
|
||||
}
|
||||
|
||||
if let Some(filename) = path.strip_prefix("/files/") {
|
||||
if let Some(file) = index.files.get(filename) {
|
||||
return match file.bytes() {
|
||||
Ok(bytes) => ResponseTemplate::new(200)
|
||||
.set_body_raw(bytes.to_vec(), content_type_for_filename(filename)),
|
||||
Err(error) => ResponseTemplate::new(500).set_body_string(format!("{error:#}")),
|
||||
};
|
||||
}
|
||||
return ResponseTemplate::new(404);
|
||||
}
|
||||
|
||||
ResponseTemplate::new(404)
|
||||
}
|
||||
|
||||
/// Build PEP 691 JSON response for a package.
|
||||
fn build_simple_api_response(
|
||||
package_name: &str,
|
||||
entry: &PackageEntry,
|
||||
server_uri: &str,
|
||||
) -> ResponseTemplate {
|
||||
let files: Vec<serde_json::Value> = entry
|
||||
.dists
|
||||
.iter()
|
||||
.map(|dist| {
|
||||
let url = format!("{server_uri}/files/{}", dist.filename);
|
||||
let mut file_obj = json!({
|
||||
"filename": dist.filename,
|
||||
"url": url,
|
||||
"hashes": {
|
||||
"sha256": dist.sha256,
|
||||
},
|
||||
"upload-time": dist.upload_time,
|
||||
});
|
||||
if let Some(rp) = &dist.requires_python {
|
||||
file_obj["requires-python"] = json!(rp);
|
||||
}
|
||||
if dist.yanked {
|
||||
file_obj["yanked"] = json!(true);
|
||||
}
|
||||
file_obj
|
||||
})
|
||||
.collect();
|
||||
|
||||
let body = json!({
|
||||
"meta": { "api-version": "1.1" },
|
||||
"name": package_name,
|
||||
"files": files,
|
||||
});
|
||||
|
||||
let body_str = body.to_string();
|
||||
ResponseTemplate::new(200)
|
||||
.insert_header("Content-Type", "application/vnd.pypi.simple.v1+json")
|
||||
.set_body_raw(body_str, "application/vnd.pypi.simple.v1+json")
|
||||
}
|
||||
|
||||
/// Extract the package name from `/simple/{package}` or `/simple/{package}/`.
|
||||
fn extract_package_name(path: &str) -> Option<&str> {
|
||||
let rest = path.strip_prefix("/simple/")?;
|
||||
let pkg = rest.strip_suffix('/').unwrap_or(rest);
|
||||
if pkg.is_empty() || pkg.contains('/') {
|
||||
return None;
|
||||
}
|
||||
Some(pkg)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::vendor::vendor_artifacts;
|
||||
|
||||
use super::{Scenario, build_server_index, extract_package_name};
|
||||
|
||||
#[test]
|
||||
fn extract_package_name_accepts_with_or_without_trailing_slash() {
|
||||
assert_eq!(extract_package_name("/simple/foo/"), Some("foo"));
|
||||
assert_eq!(extract_package_name("/simple/foo"), Some("foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_package_name_rejects_invalid_paths() {
|
||||
assert_eq!(extract_package_name("/simple/"), None);
|
||||
assert_eq!(extract_package_name("/simple"), None);
|
||||
assert_eq!(extract_package_name("/simple/foo/bar"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_index_construction_does_not_load_vendor_artifacts() {
|
||||
let _index = build_server_index(&Scenario::empty());
|
||||
|
||||
assert!(
|
||||
vendor_artifacts()
|
||||
.iter()
|
||||
.all(|artifact| !artifact.is_loaded())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
//! Generate minimal Python wheels and source distributions in memory.
|
||||
//!
|
||||
//! Packse scenario packages are trivial: they contain only metadata and a stub
|
||||
//! `__init__.py`. We generate them directly without invoking a Python build backend.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Write as _;
|
||||
use std::io::Cursor;
|
||||
|
||||
use async_zip::base::write::ZipFileWriter;
|
||||
use async_zip::{Compression as ZipCompression, ZipEntryBuilder};
|
||||
use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD as base64};
|
||||
use flate2::Compression;
|
||||
use flate2::write::GzEncoder;
|
||||
use futures::executor::block_on;
|
||||
use futures::io::AllowStdIo;
|
||||
use indoc::formatdoc;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, FuturesAsyncWriteCompatExt};
|
||||
|
||||
use uv_normalize::{ExtraName, PackageName};
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_pep508::Requirement;
|
||||
|
||||
/// Generate a wheel (`.whl`) as an in-memory ZIP archive.
|
||||
///
|
||||
/// Returns `(filename, bytes)`.
|
||||
pub fn generate_wheel(
|
||||
name: &PackageName,
|
||||
version: &Version,
|
||||
requires: &[Requirement],
|
||||
extras: &BTreeMap<ExtraName, Vec<Requirement>>,
|
||||
requires_python: Option<&VersionSpecifiers>,
|
||||
tag: &str,
|
||||
) -> (String, Vec<u8>) {
|
||||
let normalized = name.as_dist_info_name();
|
||||
let dist_info = format!("{normalized}-{version}.dist-info");
|
||||
|
||||
let mut zip = ZipFileWriter::new(Vec::new());
|
||||
|
||||
let entries = [
|
||||
(
|
||||
format!("{normalized}/__init__.py"),
|
||||
format!("__version__ = \"{version}\"\n"),
|
||||
),
|
||||
(
|
||||
format!("{dist_info}/METADATA"),
|
||||
build_metadata(name, version, requires, extras, requires_python),
|
||||
),
|
||||
(
|
||||
format!("{dist_info}/WHEEL"),
|
||||
format!(
|
||||
"Wheel-Version: 1.0\n\
|
||||
Generator: uv-test\n\
|
||||
Root-Is-Purelib: true\n\
|
||||
Tag: {tag}\n"
|
||||
),
|
||||
),
|
||||
];
|
||||
for (path, contents) in &entries {
|
||||
let entry = ZipEntryBuilder::new(path.clone().into(), ZipCompression::Stored);
|
||||
block_on(zip.write_entry_whole(entry, contents.as_bytes()))
|
||||
.expect("failed to write wheel file");
|
||||
}
|
||||
|
||||
let record = build_record(&dist_info, &entries);
|
||||
let record_entry =
|
||||
ZipEntryBuilder::new(format!("{dist_info}/RECORD").into(), ZipCompression::Stored);
|
||||
block_on(zip.write_entry_whole(record_entry, record.as_bytes()))
|
||||
.expect("failed to write RECORD file");
|
||||
|
||||
let bytes = block_on(zip.close()).expect("failed to finish in-memory wheel");
|
||||
let filename = format!("{normalized}-{version}-{tag}.whl");
|
||||
(filename, bytes)
|
||||
}
|
||||
|
||||
/// Build the `RECORD` metadata for a generated wheel.
|
||||
fn build_record(dist_info: &str, entries: &[(String, String)]) -> String {
|
||||
let mut record = String::new();
|
||||
for (path, contents) in entries {
|
||||
let contents = contents.as_bytes();
|
||||
let hash = base64.encode(Sha256::digest(contents));
|
||||
writeln!(&mut record, "{path},sha256={hash},{}", contents.len())
|
||||
.expect("writing RECORD metadata into a string should succeed");
|
||||
}
|
||||
writeln!(&mut record, "{dist_info}/RECORD,,")
|
||||
.expect("writing RECORD metadata into a string should succeed");
|
||||
record
|
||||
}
|
||||
|
||||
/// Generate a source distribution (`.tar.gz`) as an in-memory tarball.
|
||||
///
|
||||
/// The sdist contains a `pyproject.toml` using `hatchling` as build backend,
|
||||
/// `PKG-INFO` with full metadata, and a stub module.
|
||||
///
|
||||
/// Returns `(filename, bytes)`.
|
||||
pub fn generate_sdist(
|
||||
name: &PackageName,
|
||||
version: &Version,
|
||||
requires: &[Requirement],
|
||||
extras: &BTreeMap<ExtraName, Vec<Requirement>>,
|
||||
requires_python: Option<&VersionSpecifiers>,
|
||||
) -> (String, Vec<u8>) {
|
||||
let normalized = name.as_dist_info_name();
|
||||
let prefix = format!("{normalized}-{version}");
|
||||
|
||||
let buf = Vec::new();
|
||||
let encoder = GzEncoder::new(buf, Compression::fast());
|
||||
let mut tar = tokio_tar::Builder::new_non_terminated(AllowStdIo::new(encoder).compat_write());
|
||||
|
||||
let pyproject = build_pyproject_toml(name, version, requires, extras, requires_python);
|
||||
append_tar_file(
|
||||
&mut tar,
|
||||
&format!("{prefix}/pyproject.toml"),
|
||||
pyproject.as_bytes(),
|
||||
);
|
||||
|
||||
let pkg_info = build_metadata(name, version, requires, extras, requires_python);
|
||||
append_tar_file(&mut tar, &format!("{prefix}/PKG-INFO"), pkg_info.as_bytes());
|
||||
|
||||
let init_py = format!("__version__ = \"{version}\"\n");
|
||||
append_tar_file(
|
||||
&mut tar,
|
||||
&format!("{prefix}/src/{normalized}/__init__.py"),
|
||||
init_py.as_bytes(),
|
||||
);
|
||||
|
||||
let writer = block_on(tar.into_inner()).expect("failed to finish in-memory source archive");
|
||||
let encoder = writer.into_inner().into_inner();
|
||||
let bytes = encoder
|
||||
.finish()
|
||||
.expect("failed to finish in-memory gzip stream");
|
||||
let filename = format!("{normalized}-{version}.tar.gz");
|
||||
(filename, bytes)
|
||||
}
|
||||
|
||||
/// Build PEP 566 / PEP 643 metadata content.
|
||||
fn build_metadata(
|
||||
name: &PackageName,
|
||||
version: &Version,
|
||||
requires: &[Requirement],
|
||||
extras: &BTreeMap<ExtraName, Vec<Requirement>>,
|
||||
requires_python: Option<&VersionSpecifiers>,
|
||||
) -> String {
|
||||
let mut metadata = String::new();
|
||||
writeln!(&mut metadata, "Metadata-Version: 2.3")
|
||||
.expect("writing metadata into a string should succeed");
|
||||
writeln!(&mut metadata, "Name: {name}").expect("writing metadata into a string should succeed");
|
||||
writeln!(&mut metadata, "Version: {version}")
|
||||
.expect("writing metadata into a string should succeed");
|
||||
if let Some(requires_python) = requires_python {
|
||||
writeln!(&mut metadata, "Requires-Python: {requires_python}")
|
||||
.expect("writing metadata into a string should succeed");
|
||||
}
|
||||
|
||||
for extra_name in extras.keys() {
|
||||
writeln!(&mut metadata, "Provides-Extra: {extra_name}")
|
||||
.expect("writing metadata into a string should succeed");
|
||||
}
|
||||
|
||||
for requirement in requires {
|
||||
writeln!(&mut metadata, "Requires-Dist: {requirement}")
|
||||
.expect("writing metadata into a string should succeed");
|
||||
}
|
||||
for (extra_name, extra_requirements) in extras {
|
||||
for requirement in extra_requirements {
|
||||
let requirement = requirement.clone().with_extra_marker(extra_name);
|
||||
writeln!(&mut metadata, "Requires-Dist: {requirement}")
|
||||
.expect("writing metadata into a string should succeed");
|
||||
}
|
||||
}
|
||||
|
||||
metadata
|
||||
}
|
||||
|
||||
/// Build a minimal `pyproject.toml` for an sdist using hatchling.
|
||||
fn build_pyproject_toml(
|
||||
name: &PackageName,
|
||||
version: &Version,
|
||||
requires: &[Requirement],
|
||||
extras: &BTreeMap<ExtraName, Vec<Requirement>>,
|
||||
requires_python: Option<&VersionSpecifiers>,
|
||||
) -> String {
|
||||
let normalized = name.as_dist_info_name();
|
||||
let dependencies = if requires.is_empty() {
|
||||
"dependencies = []\n".to_string()
|
||||
} else {
|
||||
let mut dependencies = String::from("dependencies = [\n");
|
||||
for requirement in requires {
|
||||
writeln!(&mut dependencies, " \"{requirement}\",")
|
||||
.expect("writing dependencies into a string should succeed");
|
||||
}
|
||||
dependencies.push_str("]\n");
|
||||
dependencies
|
||||
};
|
||||
let requires_python = requires_python
|
||||
.map(|requires_python| format!("requires-python = \"{requires_python}\"\n"))
|
||||
.unwrap_or_default();
|
||||
let optional_dependencies = if extras.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let mut optional_dependencies = String::from("\n[project.optional-dependencies]\n");
|
||||
for (extra_name, extra_requirements) in extras {
|
||||
writeln!(&mut optional_dependencies, "{extra_name} = [")
|
||||
.expect("writing optional dependencies into a string should succeed");
|
||||
for requirement in extra_requirements {
|
||||
writeln!(&mut optional_dependencies, " \"{requirement}\",")
|
||||
.expect("writing optional dependencies into a string should succeed");
|
||||
}
|
||||
optional_dependencies.push_str("]\n");
|
||||
}
|
||||
optional_dependencies
|
||||
};
|
||||
|
||||
formatdoc! {
|
||||
r#"
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/{normalized}"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
only-include = ["src/{normalized}"]
|
||||
|
||||
[project]
|
||||
name = "{name}"
|
||||
version = "{version}"
|
||||
{dependencies}{requires_python}{optional_dependencies}
|
||||
"#
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a file entry to a tar archive from a byte slice.
|
||||
fn append_tar_file<W>(tar: &mut tokio_tar::Builder<W>, path: &str, data: &[u8])
|
||||
where
|
||||
W: tokio::io::AsyncWrite + Unpin + Send,
|
||||
{
|
||||
let mut header = tokio_tar::Header::new_gnu();
|
||||
header.set_entry_type(tokio_tar::EntryType::Regular);
|
||||
header.set_size(data.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
block_on(tar.append_data(
|
||||
&mut header,
|
||||
path,
|
||||
AllowStdIo::new(Cursor::new(data)).compat(),
|
||||
))
|
||||
.expect("failed to append file to in-memory source archive");
|
||||
}
|
||||
|
||||
/// Compute the SHA-256 hex digest of a byte slice.
|
||||
pub fn sha256_hex(data: &[u8]) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
|
||||
use futures::StreamExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generate_simple_wheel() {
|
||||
let requires = vec![Requirement::from_str("dep>=1.0").expect("valid requirement")];
|
||||
let requires_python =
|
||||
VersionSpecifiers::from_str(">=3.12").expect("valid version specifier");
|
||||
let (filename, bytes) = generate_wheel(
|
||||
&PackageName::from_str("my-package").expect("valid package name"),
|
||||
&Version::from_str("1.0.0").expect("valid version"),
|
||||
&requires,
|
||||
&BTreeMap::new(),
|
||||
Some(&requires_python),
|
||||
"py3-none-any",
|
||||
);
|
||||
assert_eq!(filename, "my_package-1.0.0-py3-none-any.whl");
|
||||
|
||||
let archive = block_on(async_zip::base::read::mem::ZipFileReader::new(bytes))
|
||||
.expect("wheel should be a valid zip");
|
||||
let names: Vec<_> = archive
|
||||
.file()
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
entry
|
||||
.filename()
|
||||
.as_str()
|
||||
.expect("wheel path should be UTF-8")
|
||||
})
|
||||
.collect();
|
||||
assert!(names.contains(&"my_package/__init__.py"));
|
||||
assert!(names.contains(&"my_package-1.0.0.dist-info/METADATA"));
|
||||
assert!(names.contains(&"my_package-1.0.0.dist-info/WHEEL"));
|
||||
assert!(names.contains(&"my_package-1.0.0.dist-info/RECORD"));
|
||||
|
||||
let record_index = archive
|
||||
.file()
|
||||
.entries()
|
||||
.iter()
|
||||
.position(|entry| {
|
||||
entry
|
||||
.filename()
|
||||
.as_str()
|
||||
.is_ok_and(|name| name == "my_package-1.0.0.dist-info/RECORD")
|
||||
})
|
||||
.expect("wheel RECORD should exist");
|
||||
let mut record = String::new();
|
||||
block_on(async {
|
||||
let mut reader = archive
|
||||
.reader_with_entry(record_index)
|
||||
.await
|
||||
.expect("wheel RECORD should be readable");
|
||||
reader
|
||||
.read_to_string_checked(&mut record)
|
||||
.await
|
||||
.expect("wheel RECORD should be valid UTF-8");
|
||||
});
|
||||
insta::assert_snapshot!(record, @"
|
||||
my_package/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
||||
my_package-1.0.0.dist-info/METADATA,sha256=5SmEsY5VDieqZOfjFTk5WyL4HQZueAVyhthkJaH2xC4,102
|
||||
my_package-1.0.0.dist-info/WHEEL,sha256=ujr00BDMtYYidJ71ulklWmNFpiGqy5NyjK1fX-JwFO4,78
|
||||
my_package-1.0.0.dist-info/RECORD,,
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_simple_sdist() {
|
||||
let requires = vec![Requirement::from_str("dep>=1.0").expect("valid requirement")];
|
||||
let requires_python =
|
||||
VersionSpecifiers::from_str(">=3.12").expect("valid version specifier");
|
||||
let (filename, bytes) = generate_sdist(
|
||||
&PackageName::from_str("my-package").expect("valid package name"),
|
||||
&Version::from_str("1.0.0").expect("valid version"),
|
||||
&requires,
|
||||
&BTreeMap::new(),
|
||||
Some(&requires_python),
|
||||
);
|
||||
assert_eq!(filename, "my_package-1.0.0.tar.gz");
|
||||
|
||||
let decoder = flate2::read::GzDecoder::new(Cursor::new(bytes));
|
||||
let mut archive = tokio_tar::Archive::new(AllowStdIo::new(decoder).compat());
|
||||
let names = block_on(async {
|
||||
let mut entries = archive.entries().expect("sdist archive should be readable");
|
||||
let mut entries = Pin::new(&mut entries);
|
||||
let mut names = Vec::new();
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry.expect("sdist archive entry should be readable");
|
||||
names.push(
|
||||
entry
|
||||
.path()
|
||||
.expect("sdist archive entry should have a path")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
names
|
||||
});
|
||||
|
||||
assert!(names.contains(&"my_package-1.0.0/pyproject.toml".to_string()));
|
||||
assert!(names.contains(&"my_package-1.0.0/PKG-INFO".to_string()));
|
||||
assert!(names.contains(&"my_package-1.0.0/src/my_package/__init__.py".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_deps_with_markers_include_the_extra_marker() {
|
||||
let mut extras = BTreeMap::new();
|
||||
extras.insert(
|
||||
ExtraName::from_str("binary").expect("valid extra name"),
|
||||
vec![
|
||||
Requirement::from_str("dep-binary; implementation_name != 'pypy'")
|
||||
.expect("valid requirement"),
|
||||
],
|
||||
);
|
||||
let metadata = build_metadata(
|
||||
&PackageName::from_str("pkg").expect("valid package name"),
|
||||
&Version::from_str("1.0.0").expect("valid version"),
|
||||
&[],
|
||||
&extras,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
metadata.contains(
|
||||
"Requires-Dist: dep-binary ; implementation_name != 'pypy' and extra == 'binary'"
|
||||
),
|
||||
"metadata should retain the existing marker and add the extra marker:\n{metadata}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_deps_with_or_markers_preserve_precedence() {
|
||||
let mut extras = BTreeMap::new();
|
||||
extras.insert(
|
||||
ExtraName::from_str("compat").expect("valid extra name"),
|
||||
vec![
|
||||
Requirement::from_str("dep; sys_platform == 'linux' or sys_platform == 'darwin'")
|
||||
.expect("valid requirement"),
|
||||
],
|
||||
);
|
||||
let metadata = build_metadata(
|
||||
&PackageName::from_str("pkg").expect("valid package name"),
|
||||
&Version::from_str("1.0.0").expect("valid version"),
|
||||
&[],
|
||||
&extras,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
metadata.contains(
|
||||
"Requires-Dist: dep ; (sys_platform == 'darwin' and extra == 'compat') or (sys_platform == 'linux' and extra == 'compat')"
|
||||
),
|
||||
"metadata should preserve the original or-marker semantics:\n{metadata}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
//! Pinned registry artifacts used by local test servers.
|
||||
|
||||
use std::io::Write as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use uv_configuration::TrustedHost;
|
||||
use uv_fs::{LockedFile, LockedFileMode, persist_with_retry_sync, tempfile_in};
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use crate::TestContext;
|
||||
|
||||
pub(crate) struct VendorArtifact {
|
||||
pub(crate) filename: &'static str,
|
||||
url: &'static str,
|
||||
pub(crate) sha256: &'static str,
|
||||
bytes: OnceLock<Arc<[u8]>>,
|
||||
}
|
||||
|
||||
impl VendorArtifact {
|
||||
pub(crate) fn bytes(&self) -> Result<Arc<[u8]>> {
|
||||
if let Some(bytes) = self.bytes.get() {
|
||||
return Ok(Arc::clone(bytes));
|
||||
}
|
||||
|
||||
let bytes = load_vendor_file(self)?;
|
||||
Ok(Arc::clone(self.bytes.get_or_init(|| bytes)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn is_loaded(&self) -> bool {
|
||||
self.bytes.get().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
static VENDOR_ARTIFACTS: [VendorArtifact; 17] = [
|
||||
VendorArtifact {
|
||||
filename: "calver-2022.6.26-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/f7/39/e421c06f42ca00fa9cf8929c2466e58a837e8e97b8ab3ff4f4ff9a15e33e/calver-2022.6.26-py3-none-any.whl",
|
||||
sha256: "a1d7fcdd67797afc52ee36ffb8c8adf6643173864306547bfd1380cbce6310a0",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "editables-0.5-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/6b/be/0f2f4a5e8adc114a02b63d92bf8edbfa24db6fc602fca83c885af2479e0e/editables-0.5-py3-none-any.whl",
|
||||
sha256: "61e5ffa82629e0d8bfe09bc44a07db3c1ab8ed1ce78a6980732870f19b5e7d4c",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "flit_core-3.9.0-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/38/45/618e84e49a6c51e5dd15565ec2fcd82ab273434f236b8f108f065ded517a/flit_core-3.9.0-py3-none-any.whl",
|
||||
sha256: "7aada352fb0c7f5538c4fafeddf314d3a6a92ee8e2b1de70482329e42de70301",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "futzed_bz2-0.1.0-py3-none-any.whl",
|
||||
url: "https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_bz2-0.1.0-py3-none-any.whl",
|
||||
sha256: "3c106ea0433a90f767271c322a8cb8bf5145f210adf8a357152796475b00b316",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "futzed_lzma-0.1.0-py3-none-any.whl",
|
||||
url: "https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_lzma-0.1.0-py3-none-any.whl",
|
||||
sha256: "9f39b4686926ffbed723450d700cca524001af259949eb5e4b973e7846df23d2",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "hatchling-1.20.0-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/87/4a/7d22a92b55809c579d8deafb0dabeb102b411a0ef9439949cccef3071527/hatchling-1.20.0-py3-none-any.whl",
|
||||
sha256: "872c63aa7e8aca85e8dba07b05c6a9b28d5a149fe00638f1a47e36930197248f",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "packaging-23.2-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl",
|
||||
sha256: "8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "packaging-23.2.tar.gz",
|
||||
url: "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz",
|
||||
sha256: "048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "pathspec-0.12.1-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl",
|
||||
sha256: "a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "pip-24.0-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl",
|
||||
sha256: "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "pluggy-1.3.0-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/05/b8/42ed91898d4784546c5f06c60506400548db3f7a4b3fb441cba4e5c17952/pluggy-1.3.0-py3-none-any.whl",
|
||||
sha256: "d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "setuptools-69.0.2-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/bb/e1/ed2dd0850446b8697ad28d118df885ad04140c64ace06c4bd559f7c8a94f/setuptools-69.0.2-py3-none-any.whl",
|
||||
sha256: "1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "setuptools_scm-8.0.4-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/0e/a3/b9a8b0adfe672bf0df5901707aa929d30a97ee390ba651910186776746d2/setuptools_scm-8.0.4-py3-none-any.whl",
|
||||
sha256: "b47844cd2a84b83b3187a5782c71128c28b4c94cad8bfb871da2784a5cb54c4f",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "tomli-2.0.1-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl",
|
||||
sha256: "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "trove_classifiers-2023.11.29-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/f8/50/e223fe762fe21fefb7a3f37c10e9693ea5f63cb54b5ae39daa876b780abc/trove_classifiers-2023.11.29-py3-none-any.whl",
|
||||
sha256: "02307750cbbac2b3d13078662f8a5bf077732bf506e9c33c97204b7f68f3699e",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "typing_extensions-4.9.0-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/b7/f4/6a90020cd2d93349b442bfcb657d0dc91eee65491600b2cb1d388bc98e6b/typing_extensions-4.9.0-py3-none-any.whl",
|
||||
sha256: "af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
VendorArtifact {
|
||||
filename: "wheel-0.42.0-py3-none-any.whl",
|
||||
url: "https://files.pythonhosted.org/packages/c7/c3/55076fc728723ef927521abaa1955213d094933dc36d4a2008d5101e1af5/wheel-0.42.0-py3-none-any.whl",
|
||||
sha256: "177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d",
|
||||
bytes: OnceLock::new(),
|
||||
},
|
||||
];
|
||||
|
||||
pub(crate) fn vendor_artifacts() -> &'static [VendorArtifact] {
|
||||
&VENDOR_ARTIFACTS
|
||||
}
|
||||
|
||||
fn vendor_cache_dir() -> PathBuf {
|
||||
TestContext::test_bucket_dir().join("vendor")
|
||||
}
|
||||
|
||||
fn load_vendor_file(artifact: &VendorArtifact) -> Result<Arc<[u8]>> {
|
||||
// HTTP responders run in a Tokio runtime; load on another thread instead of nesting runtimes.
|
||||
let bytes = std::thread::scope(|scope| {
|
||||
scope
|
||||
.spawn(|| load_vendor_file_async(artifact))
|
||||
.join()
|
||||
.map_err(|_| anyhow!("vendor artifact loader panicked"))
|
||||
})??;
|
||||
|
||||
Ok(bytes.into())
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn load_vendor_file_async(artifact: &VendorArtifact) -> Result<Vec<u8>> {
|
||||
let cache_dir = vendor_cache_dir();
|
||||
fs_err::create_dir_all(&cache_dir)
|
||||
.with_context(|| format!("failed to create vendor cache at `{}`", cache_dir.display()))?;
|
||||
let path = cache_dir.join(artifact.filename);
|
||||
ensure_cached_artifact(artifact, &path).await?;
|
||||
let bytes = fs_err::read(&path)
|
||||
.with_context(|| format!("failed to read cached vendor artifact `{}`", path.display()))?;
|
||||
verify_bytes(artifact, &bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
async fn ensure_cached_artifact(artifact: &VendorArtifact, path: &Path) -> Result<()> {
|
||||
if cached_artifact_matches(artifact, path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let lock_path = artifact_lock_path(path)?;
|
||||
let _lock = LockedFile::acquire(
|
||||
&lock_path,
|
||||
LockedFileMode::Exclusive,
|
||||
format_args!("vendor artifact `{}`", artifact.filename),
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to lock cached vendor artifact `{}`",
|
||||
artifact.filename
|
||||
)
|
||||
})?;
|
||||
|
||||
if cached_artifact_matches(artifact, path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let trusted_hosts = std::env::var(EnvVars::UV_INSECURE_HOST)
|
||||
.unwrap_or_default()
|
||||
.split(' ')
|
||||
.filter(|host| !host.is_empty())
|
||||
.map(TrustedHost::from_str)
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
let client = uv_client::BaseClientBuilder::default()
|
||||
.allow_insecure_host(trusted_hosts)
|
||||
.build()
|
||||
.context("failed to build vendor artifact client")?;
|
||||
|
||||
let url = artifact
|
||||
.url
|
||||
.parse()
|
||||
.with_context(|| format!("invalid vendor artifact URL `{}`", artifact.url))?;
|
||||
let response = client
|
||||
.for_host(&url)
|
||||
.get(reqwest::Url::from(url))
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("failed to download `{}`", artifact.url))?
|
||||
.error_for_status()
|
||||
.with_context(|| format!("failed to download `{}`", artifact.url))?;
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.with_context(|| format!("failed to read `{}`", artifact.url))?;
|
||||
verify_bytes(artifact, &bytes)?;
|
||||
|
||||
let parent = path
|
||||
.parent()
|
||||
.context("vendor artifact cache path should have a parent")?;
|
||||
let mut temp = tempfile_in(parent).with_context(|| {
|
||||
format!(
|
||||
"failed to create vendor temp file in `{}`",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
temp.write_all(&bytes)
|
||||
.with_context(|| format!("failed to write `{}`", artifact.filename))?;
|
||||
temp.as_file_mut()
|
||||
.sync_all()
|
||||
.with_context(|| format!("failed to sync `{}`", artifact.filename))?;
|
||||
|
||||
match persist_with_retry_sync(temp, path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) => {
|
||||
if let Ok(bytes) = fs_err::read(path)
|
||||
&& verify_bytes(artifact, &bytes).is_ok()
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error).with_context(|| {
|
||||
format!(
|
||||
"failed to persist cached vendor artifact `{}`",
|
||||
path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_artifact_matches(artifact: &VendorArtifact, path: &Path) -> bool {
|
||||
fs_err::read(path).is_ok_and(|bytes| verify_bytes(artifact, &bytes).is_ok())
|
||||
}
|
||||
|
||||
fn artifact_lock_path(path: &Path) -> Result<PathBuf> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.context("vendor artifact cache path should have a parent")?;
|
||||
let mut filename = path
|
||||
.file_name()
|
||||
.context("vendor artifact cache path should have a filename")?
|
||||
.to_os_string();
|
||||
filename.push(".lock");
|
||||
Ok(parent.join(filename))
|
||||
}
|
||||
|
||||
fn verify_bytes(artifact: &VendorArtifact, bytes: &[u8]) -> Result<()> {
|
||||
let actual = format!("{:x}", Sha256::digest(bytes));
|
||||
if actual == artifact.sha256 {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(
|
||||
"hash mismatch for cached vendor artifact `{}`: expected `{}`, found `{}`",
|
||||
artifact.filename,
|
||||
artifact.sha256,
|
||||
actual
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn artifact_lock_path_is_per_artifact() {
|
||||
let path = Path::new("vendor").join("example-1.0.0-py3-none-any.whl");
|
||||
|
||||
assert_eq!(
|
||||
artifact_lock_path(&path).expect("path should have a parent"),
|
||||
Path::new("vendor").join("example-1.0.0-py3-none-any.whl.lock")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_bytes_are_held_per_artifact() {
|
||||
let bytes = OnceLock::new();
|
||||
bytes
|
||||
.set(Arc::from(b"available".as_slice()))
|
||||
.expect("artifact should not already have cached bytes");
|
||||
let loaded = VendorArtifact {
|
||||
filename: "available.whl",
|
||||
url: "unused",
|
||||
sha256: "unused",
|
||||
bytes,
|
||||
};
|
||||
let unloaded = VendorArtifact {
|
||||
filename: "unrequested.whl",
|
||||
url: "unused",
|
||||
sha256: "unused",
|
||||
bytes: OnceLock::new(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
loaded
|
||||
.bytes()
|
||||
.expect("preloaded bytes should be returned")
|
||||
.as_ref(),
|
||||
b"available"
|
||||
);
|
||||
assert!(unloaded.bytes.get().is_none());
|
||||
}
|
||||
}
|
||||
+13
-12
@@ -26,7 +26,7 @@ use uv_cache_key::{RepositoryUrl, cache_digest};
|
||||
use uv_fs::Simplified;
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use uv_test::{packse_index_url, uv_snapshot, venv_bin_path};
|
||||
use uv_test::{uv_snapshot, venv_bin_path};
|
||||
|
||||
/// Add a PyPI requirement.
|
||||
#[test]
|
||||
@@ -5106,6 +5106,7 @@ fn add_lower_bound_optional() -> Result<()> {
|
||||
/// Omit the local segment when adding dependencies (since `>=1.2.3+local` is invalid).
|
||||
#[test]
|
||||
fn add_lower_bound_local() -> Result<()> {
|
||||
let server = uv_test::packse::PackseServer::new("local/local-simple.toml");
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
@@ -5117,8 +5118,8 @@ fn add_lower_bound_local() -> Result<()> {
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
// Adding `torch` should include a lower-bound, but no local segment.
|
||||
uv_snapshot!(context.filters(), context.add().arg("local-simple-a").arg("--index").arg(packse_index_url()).env_remove(EnvVars::UV_EXCLUDE_NEWER), @"
|
||||
// Adding `a` should include a lower-bound, but no local segment.
|
||||
uv_snapshot!(context.filters(), context.add().arg("a").arg("--index").arg(server.index_url()).env_remove(EnvVars::UV_EXCLUDE_NEWER), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -5127,7 +5128,7 @@ fn add_lower_bound_local() -> Result<()> {
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ local-simple-a==1.2.3+foo
|
||||
+ a==1.2.3+foo
|
||||
");
|
||||
|
||||
let pyproject_toml = context.read("pyproject.toml");
|
||||
@@ -5142,11 +5143,11 @@ fn add_lower_bound_local() -> Result<()> {
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"local-simple-a>=1.2.3",
|
||||
"a>=1.2.3",
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/"
|
||||
url = "http://[LOCALHOST]/simple/"
|
||||
"#
|
||||
);
|
||||
});
|
||||
@@ -5163,12 +5164,12 @@ fn add_lower_bound_local() -> Result<()> {
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "local-simple-a"
|
||||
name = "a"
|
||||
version = "1.2.3+foo"
|
||||
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }
|
||||
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/local_simple_a-1.2.3+foo.tar.gz", hash = "sha256:cd1855a98a7b0dce1f4617f2f2089906936344392d4bdd7720503e9f3c0b1544" }
|
||||
source = { registry = "http://[LOCALHOST]/simple/" }
|
||||
sdist = { url = "http://[LOCALHOST]/files/a-1.2.3+foo.tar.gz", hash = "sha256:d8c42806a0bf7b47451778ed2a909a2f9d22fe3a4bac673934fc8fb2537a886a", upload-time = "2024-03-24T00:00:00Z" }
|
||||
wheels = [
|
||||
{ url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/local_simple_a-1.2.3+foo-py3-none-any.whl", hash = "sha256:9a430e6d5e9cd3ab906ea412b00ea8a1bad7c59fd64df2278a2527a60a665751" },
|
||||
{ url = "http://[LOCALHOST]/files/a-1.2.3+foo-py3-none-any.whl", hash = "sha256:2fd8eec176cad72b6c4372682485914aff65d215fb8cd71c85e9ed4511fa8bbe", upload-time = "2024-03-24T00:00:00Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5176,11 +5177,11 @@ fn add_lower_bound_local() -> Result<()> {
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "local-simple-a" },
|
||||
{ name = "a" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "local-simple-a", specifier = ">=1.2.3" }]
|
||||
requires-dist = [{ name = "a", specifier = ">=1.2.3" }]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
+27
-23
@@ -7,11 +7,10 @@ use url::Url;
|
||||
|
||||
use uv_fs::Simplified;
|
||||
use uv_static::EnvVars;
|
||||
use uv_test::packse::PackseServer;
|
||||
#[cfg(feature = "test-git")]
|
||||
use uv_test::{READ_ONLY_GITHUB_TOKEN, decode_token};
|
||||
use uv_test::{
|
||||
build_vendor_links_url, download_to_disk, packse_index_url, uv_snapshot, venv_bin_path,
|
||||
};
|
||||
use uv_test::{download_to_disk, uv_snapshot, venv_bin_path};
|
||||
|
||||
#[test]
|
||||
fn lock_wheel_registry() -> Result<()> {
|
||||
@@ -12535,12 +12534,13 @@ fn lock_upgrade_group_no_project_table() -> Result<()> {
|
||||
#[test]
|
||||
fn lock_upgrade_drop_fork_markers() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let server = PackseServer::new("fork/fork-upgrade.toml");
|
||||
|
||||
let requirements = r#"[project]
|
||||
name = "forking"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["fork-upgrade-foo==1"]
|
||||
dependencies = ["foo==1"]
|
||||
|
||||
[build-system]
|
||||
requires = ["flit_core>=3.8,<4"]
|
||||
@@ -12552,7 +12552,7 @@ fn lock_upgrade_drop_fork_markers() -> Result<()> {
|
||||
context
|
||||
.lock()
|
||||
.arg("--index-url")
|
||||
.arg(packse_index_url())
|
||||
.arg(server.index_url())
|
||||
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
|
||||
.assert()
|
||||
.success();
|
||||
@@ -12560,11 +12560,11 @@ fn lock_upgrade_drop_fork_markers() -> Result<()> {
|
||||
assert!(lock.contains("resolution-markers"));
|
||||
|
||||
// Remove the bound and lock with `--upgrade`.
|
||||
pyproject_toml.write_str(&requirements.replace("fork-upgrade-foo==1", "fork-upgrade-foo"))?;
|
||||
pyproject_toml.write_str(&requirements.replace("foo==1", "foo"))?;
|
||||
context
|
||||
.lock()
|
||||
.arg("--index-url")
|
||||
.arg(packse_index_url())
|
||||
.arg(server.index_url())
|
||||
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
|
||||
.arg("--upgrade")
|
||||
.assert()
|
||||
@@ -13072,6 +13072,7 @@ fn lock_find_links_local_sdist() -> Result<()> {
|
||||
#[test]
|
||||
fn lock_find_links_http_wheel() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let vendor = uv_test::find_links::FindLinksServer::vendor();
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&formatdoc! { r#"
|
||||
@@ -13082,10 +13083,10 @@ fn lock_find_links_http_wheel() -> Result<()> {
|
||||
dependencies = ["packaging==23.2"]
|
||||
|
||||
[tool.uv]
|
||||
find-links = ["{}"]
|
||||
find-links = ["{vendor_url}"]
|
||||
no-build-package = ["packaging"]
|
||||
"#,
|
||||
build_vendor_links_url()
|
||||
vendor_url = vendor.url()
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @"
|
||||
@@ -13114,10 +13115,10 @@ fn lock_find_links_http_wheel() -> Result<()> {
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/vendor/" }
|
||||
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/vendor/build/packaging-23.2.tar.gz" }
|
||||
source = { registry = "http://[LOCALHOST]/" }
|
||||
sdist = { url = "http://[LOCALHOST]/packaging-23.2.tar.gz" }
|
||||
wheels = [
|
||||
{ url = "https://astral-sh.github.io/packse/PACKSE_VERSION/vendor/build/packaging-23.2-py3-none-any.whl" },
|
||||
{ url = "http://[LOCALHOST]/packaging-23.2-py3-none-any.whl" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13163,6 +13164,7 @@ fn lock_find_links_http_wheel() -> Result<()> {
|
||||
#[test]
|
||||
fn lock_find_links_http_sdist() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let vendor = uv_test::find_links::FindLinksServer::vendor();
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&formatdoc! { r#"
|
||||
@@ -13173,10 +13175,10 @@ fn lock_find_links_http_sdist() -> Result<()> {
|
||||
dependencies = ["packaging==23.2"]
|
||||
|
||||
[tool.uv]
|
||||
find-links = ["{}"]
|
||||
find-links = ["{vendor_url}"]
|
||||
no-binary-package = ["packaging"]
|
||||
"#,
|
||||
build_vendor_links_url()
|
||||
vendor_url = vendor.url()
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @"
|
||||
@@ -13205,10 +13207,10 @@ fn lock_find_links_http_sdist() -> Result<()> {
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/vendor/" }
|
||||
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/vendor/build/packaging-23.2.tar.gz" }
|
||||
source = { registry = "http://[LOCALHOST]/" }
|
||||
sdist = { url = "http://[LOCALHOST]/packaging-23.2.tar.gz" }
|
||||
wheels = [
|
||||
{ url = "https://astral-sh.github.io/packse/PACKSE_VERSION/vendor/build/packaging-23.2-py3-none-any.whl" },
|
||||
{ url = "http://[LOCALHOST]/packaging-23.2-py3-none-any.whl" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -21302,9 +21304,10 @@ fn lock_repeat_named_index_cli() -> Result<()> {
|
||||
);
|
||||
});
|
||||
|
||||
// Resolve to PyPI, since the PyTorch index is replaced by the Packse index, which doesn't
|
||||
// Resolve to PyPI, since the PyTorch index is replaced by an empty index, which doesn't
|
||||
// include `jinja2`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--index").arg(format!("pytorch={}", packse_index_url())), @"
|
||||
let empty_index = PackseServer::empty();
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--index").arg(format!("pytorch={}", empty_index.index_url())), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -35615,6 +35618,7 @@ fn lock_unsupported_wheel_url_required_platform() -> Result<()> {
|
||||
#[test]
|
||||
fn lock_required_environment_cycle_reports_resolution_error() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let server = PackseServer::new("wheels/no-sdist-no-wheels-with-matching-platform.toml");
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
@@ -35635,7 +35639,7 @@ fn lock_required_environment_cycle_reports_resolution_error() -> Result<()> {
|
||||
url = "{}"
|
||||
default = true
|
||||
"#,
|
||||
packse_index_url()
|
||||
server.index_url()
|
||||
})?;
|
||||
|
||||
let pkg_a = context.temp_dir.child("pkg-a");
|
||||
@@ -35646,7 +35650,7 @@ fn lock_required_environment_cycle_reports_resolution_error() -> Result<()> {
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"no-sdist-no-wheels-with-matching-platform-a",
|
||||
"a",
|
||||
"pkg-b",
|
||||
]
|
||||
|
||||
@@ -35691,8 +35695,8 @@ fn lock_required_environment_cycle_reports_resolution_error() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies for split (markers: platform_machine == 'arm64'):
|
||||
╰─▶ Because no-sdist-no-wheels-with-matching-platform-a==1.0.0 has no `platform_machine == 'arm64'`-compatible wheels and only no-sdist-no-wheels-with-matching-platform-a==1.0.0 is available, we can conclude that all versions of no-sdist-no-wheels-with-matching-platform-a cannot be used.
|
||||
And because pkg-a depends on no-sdist-no-wheels-with-matching-platform-a and your workspace requires pkg-a, we can conclude that your workspace's requirements are unsatisfiable.
|
||||
╰─▶ Because a==1.0.0 has no `platform_machine == 'arm64'`-compatible wheels and only a==1.0.0 is available, we can conclude that all versions of a cannot be used.
|
||||
And because pkg-a depends on a and your workspace requires pkg-a, we can conclude that your workspace's requirements are unsatisfiable.
|
||||
"
|
||||
);
|
||||
|
||||
|
||||
+1264
-1280
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,7 @@ use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
use uv_fs::Simplified;
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use uv_test::{
|
||||
DEFAULT_PYTHON_VERSION, TestContext, download_to_disk, packse_index_url, uv_snapshot,
|
||||
};
|
||||
use uv_test::{DEFAULT_PYTHON_VERSION, TestContext, download_to_disk, uv_snapshot};
|
||||
|
||||
fn write_tar_gz(file: File, entries: &[(&str, &str)]) -> Result<()> {
|
||||
let enc = GzEncoder::new(file, flate2::Compression::default());
|
||||
@@ -14397,6 +14395,8 @@ fn universal_constrained_environment() -> Result<()> {
|
||||
#[test]
|
||||
fn universal_required_environment() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let server =
|
||||
uv_test::packse::PackseServer::new("wheels/no-sdist-no-wheels-with-matching-platform.toml");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&indoc::formatdoc! {r#"
|
||||
@@ -14404,7 +14404,7 @@ fn universal_required_environment() -> Result<()> {
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["no-sdist-no-wheels-with-matching-platform-a"]
|
||||
dependencies = ["a"]
|
||||
|
||||
[tool.uv]
|
||||
required-environments = ["platform_machine == 'arm64'"]
|
||||
@@ -14414,7 +14414,7 @@ fn universal_required_environment() -> Result<()> {
|
||||
url = "{}"
|
||||
default = true
|
||||
"#,
|
||||
packse_index_url()
|
||||
server.index_url()
|
||||
})?;
|
||||
|
||||
let filters: Vec<_> = context
|
||||
@@ -14431,7 +14431,7 @@ fn universal_required_environment() -> Result<()> {
|
||||
.arg("pyproject.toml")
|
||||
.arg("--universal")
|
||||
.arg("--exclude-newer-package")
|
||||
.arg("no-sdist-no-wheels-with-matching-platform-a=false")
|
||||
.arg("a=false")
|
||||
.env_remove(EnvVars::UV_EXCLUDE_NEWER), @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
@@ -14439,8 +14439,8 @@ fn universal_required_environment() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies for split (markers: platform_machine == 'arm64'):
|
||||
╰─▶ Because only no-sdist-no-wheels-with-matching-platform-a==1.0.0 is available and no-sdist-no-wheels-with-matching-platform-a==1.0.0 has no `platform_machine == 'arm64'`-compatible wheels, we can conclude that all versions of no-sdist-no-wheels-with-matching-platform-a cannot be used.
|
||||
And because project depends on no-sdist-no-wheels-with-matching-platform-a, we can conclude that your requirements are unsatisfiable.
|
||||
╰─▶ Because only a==1.0.0 is available and a==1.0.0 has no `platform_machine == 'arm64'`-compatible wheels, we can conclude that all versions of a cannot be used.
|
||||
And because project depends on a, we can conclude that your requirements are unsatisfiable.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
@@ -15369,19 +15369,20 @@ fn universal_conflicting_override_urls() -> Result<()> {
|
||||
#[test]
|
||||
fn compile_lowest_extra_unpinned_warning() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let server = uv_test::packse::PackseServer::new("extras/all-extras-required.toml");
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str(indoc::indoc! {r"
|
||||
all-extras-required-a[extra_b,extra_c]
|
||||
all-extras-required-b==1
|
||||
all-extras-required-c==1
|
||||
a[extra_b,extra_c]
|
||||
b==1
|
||||
c==1
|
||||
"})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_compile()
|
||||
.arg("--resolution")
|
||||
.arg("lowest")
|
||||
.arg("--index-url")
|
||||
.arg(packse_index_url())
|
||||
.arg(server.index_url())
|
||||
.arg(requirements_in.path())
|
||||
.env_remove(EnvVars::UV_EXCLUDE_NEWER), @"
|
||||
success: true
|
||||
@@ -15389,19 +15390,19 @@ fn compile_lowest_extra_unpinned_warning() -> Result<()> {
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --resolution lowest [TEMP_DIR]/requirements.in
|
||||
all-extras-required-a==1.0.0
|
||||
a==1.0.0
|
||||
# via -r requirements.in
|
||||
all-extras-required-b==1.0.0
|
||||
b==1.0.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# all-extras-required-a
|
||||
all-extras-required-c==1.0.0
|
||||
# a
|
||||
c==1.0.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# all-extras-required-a
|
||||
# a
|
||||
|
||||
----- stderr -----
|
||||
warning: The direct dependency `all-extras-required-a` is unpinned. Consider setting a lower bound when using `--resolution lowest` or `--resolution lowest-direct` to avoid using outdated versions.
|
||||
warning: The direct dependency `a` is unpinned. Consider setting a lower bound when using `--resolution lowest` or `--resolution lowest-direct` to avoid using outdated versions.
|
||||
Resolved 3 packages in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
//! DO NOT EDIT
|
||||
//!
|
||||
//! Generated with `./scripts/sync_scenarios.sh`
|
||||
//! Scenarios from <https://github.com/astral-sh/packse/tree/0.3.59/scenarios>
|
||||
//! Generated with `cargo dev generate-scenario-tests`
|
||||
//! Scenarios from <test/scenarios>
|
||||
//!
|
||||
#![cfg(all(feature = "test-python", feature = "test-pypi", unix))]
|
||||
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -14,14 +13,11 @@ use assert_fs::fixture::{FileWriteStr, PathChild};
|
||||
use predicates::prelude::predicate;
|
||||
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use uv_test::{
|
||||
TestContext, build_vendor_links_url, get_bin, packse_index_url, python_path_with_versions,
|
||||
uv_snapshot,
|
||||
};
|
||||
use uv_test::packse::PackseServer;
|
||||
use uv_test::{TestContext, get_bin, python_path_with_versions, uv_snapshot};
|
||||
|
||||
/// Provision python binaries and return a `pip compile` command with options shared across all scenarios.
|
||||
fn command(context: &TestContext, python_versions: &[&str]) -> Command {
|
||||
fn command(context: &TestContext, python_versions: &[&str], server: &PackseServer) -> Command {
|
||||
let python_path = python_path_with_versions(&context.temp_dir, python_versions)
|
||||
.expect("Failed to create Python test path");
|
||||
let mut command = Command::new(get_bin!());
|
||||
@@ -30,9 +26,7 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command {
|
||||
.arg("compile")
|
||||
.arg("requirements.in")
|
||||
.arg("--index-url")
|
||||
.arg(packse_index_url())
|
||||
.arg("--find-links")
|
||||
.arg(build_vendor_links_url());
|
||||
.arg(server.index_url());
|
||||
context.add_shared_options(&mut command, true);
|
||||
command.env_remove(EnvVars::UV_EXCLUDE_NEWER);
|
||||
command.env(EnvVars::UV_PYTHON_SEARCH_PATH, python_path);
|
||||
@@ -57,15 +51,12 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command {
|
||||
fn compatible_python_incompatible_override() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.11");
|
||||
let python_versions = &[];
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((r"compatible-python-incompatible-override-", "package-"));
|
||||
let server = PackseServer::new("requires_python/compatible-python-incompatible-override.toml");
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("compatible-python-incompatible-override-a==1.0.0")?;
|
||||
requirements_in.write_str("a==1.0.0")?;
|
||||
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
let output = uv_snapshot!(context.filters(), command(&context, python_versions, &server)
|
||||
.arg("--python-version=3.9")
|
||||
, @"
|
||||
success: false
|
||||
@@ -75,10 +66,10 @@ fn compatible_python_incompatible_override() -> Result<()> {
|
||||
----- stderr -----
|
||||
warning: The requested Python version 3.9 is not available; 3.11.[X] will be used to build dependencies instead.
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because the requested Python version (>=3.9) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used.
|
||||
And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable.
|
||||
╰─▶ Because the requested Python version (>=3.9) does not satisfy Python>=3.10 and a==1.0.0 depends on Python>=3.10, we can conclude that a==1.0.0 cannot be used.
|
||||
And because you require a==1.0.0, we can conclude that your requirements are unsatisfiable.
|
||||
|
||||
hint: The `--python-version` value (>=3.9) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.10). Consider using a higher `--python-version` value.
|
||||
hint: The `--python-version` value (>=3.9) includes Python versions that are not supported by your dependencies (e.g., a==1.0.0 only supports >=3.10). Consider using a higher `--python-version` value.
|
||||
"
|
||||
);
|
||||
|
||||
@@ -105,20 +96,15 @@ fn compatible_python_incompatible_override() -> Result<()> {
|
||||
fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.9");
|
||||
let python_versions = &["3.11"];
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((
|
||||
r"incompatible-python-compatible-override-available-no-wheels-",
|
||||
"package-",
|
||||
));
|
||||
let server = PackseServer::new(
|
||||
"requires_python/incompatible-python-compatible-override-available-no-wheels.toml",
|
||||
);
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in
|
||||
.write_str("incompatible-python-compatible-override-available-no-wheels-a==1.0.0")?;
|
||||
requirements_in.write_str("a==1.0.0")?;
|
||||
|
||||
// Since there is a compatible Python version available on the system, it should be used to build the source distributions.
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
let output = uv_snapshot!(context.filters(), command(&context, python_versions, &server)
|
||||
.arg("--python-version=3.11")
|
||||
, @"
|
||||
success: true
|
||||
@@ -126,7 +112,7 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> {
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
|
||||
package-a==1.0.0
|
||||
a==1.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
@@ -134,9 +120,10 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> {
|
||||
"
|
||||
);
|
||||
|
||||
output.assert().success().stdout(predicate::str::contains(
|
||||
"incompatible-python-compatible-override-available-no-wheels-a==1.0.0",
|
||||
));
|
||||
output
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("a==1.0.0"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -158,20 +145,15 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> {
|
||||
fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.9");
|
||||
let python_versions = &[];
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((
|
||||
r"incompatible-python-compatible-override-no-compatible-wheels-",
|
||||
"package-",
|
||||
));
|
||||
let server = PackseServer::new(
|
||||
"requires_python/incompatible-python-compatible-override-no-compatible-wheels.toml",
|
||||
);
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in
|
||||
.write_str("incompatible-python-compatible-override-no-compatible-wheels-a==1.0.0")?;
|
||||
requirements_in.write_str("a==1.0.0")?;
|
||||
|
||||
// Since there are no compatible wheels for the package and it is not compatible with the local installation, we cannot build the source distribution to determine its dependencies. However, the source distribution includes static metadata, which we can use to determine dependencies without building the package.
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
let output = uv_snapshot!(context.filters(), command(&context, python_versions, &server)
|
||||
.arg("--python-version=3.11")
|
||||
, @"
|
||||
success: true
|
||||
@@ -179,7 +161,7 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()>
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
|
||||
package-a==1.0.0
|
||||
a==1.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
@@ -213,19 +195,15 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()>
|
||||
fn incompatible_python_compatible_override_other_wheel() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.9");
|
||||
let python_versions = &[];
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((
|
||||
r"incompatible-python-compatible-override-other-wheel-",
|
||||
"package-",
|
||||
));
|
||||
let server = PackseServer::new(
|
||||
"requires_python/incompatible-python-compatible-override-other-wheel.toml",
|
||||
);
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("incompatible-python-compatible-override-other-wheel-a")?;
|
||||
requirements_in.write_str("a")?;
|
||||
|
||||
// Since there are no wheels for the version of the package compatible with the target and it is not compatible with the local installation, we cannot build the source distribution to determine its dependencies. However, the source distribution includes static metadata, which we can use to determine dependencies without building the package.
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
let output = uv_snapshot!(context.filters(), command(&context, python_versions, &server)
|
||||
.arg("--python-version=3.11")
|
||||
, @"
|
||||
success: true
|
||||
@@ -233,7 +211,7 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> {
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
|
||||
package-a==1.0.0
|
||||
a==1.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
@@ -264,20 +242,15 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> {
|
||||
fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.9");
|
||||
let python_versions = &[];
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((
|
||||
r"incompatible-python-compatible-override-unavailable-no-wheels-",
|
||||
"package-",
|
||||
));
|
||||
let server = PackseServer::new(
|
||||
"requires_python/incompatible-python-compatible-override-unavailable-no-wheels.toml",
|
||||
);
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in
|
||||
.write_str("incompatible-python-compatible-override-unavailable-no-wheels-a==1.0.0")?;
|
||||
requirements_in.write_str("a==1.0.0")?;
|
||||
|
||||
// Since there are no wheels for the package and it is not compatible with the local installation, we cannot build the source distribution to determine its dependencies. However, the source distribution includes static metadata, which we can use to determine dependencies without building the package.
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
let output = uv_snapshot!(context.filters(), command(&context, python_versions, &server)
|
||||
.arg("--python-version=3.11")
|
||||
, @"
|
||||
success: true
|
||||
@@ -285,7 +258,7 @@ fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()>
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
|
||||
package-a==1.0.0
|
||||
a==1.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
@@ -316,15 +289,12 @@ fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()>
|
||||
fn incompatible_python_compatible_override() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.9");
|
||||
let python_versions = &[];
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((r"incompatible-python-compatible-override-", "package-"));
|
||||
let server = PackseServer::new("requires_python/incompatible-python-compatible-override.toml");
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("incompatible-python-compatible-override-a==1.0.0")?;
|
||||
requirements_in.write_str("a==1.0.0")?;
|
||||
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
let output = uv_snapshot!(context.filters(), command(&context, python_versions, &server)
|
||||
.arg("--python-version=3.11")
|
||||
, @"
|
||||
success: true
|
||||
@@ -332,7 +302,7 @@ fn incompatible_python_compatible_override() -> Result<()> {
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
|
||||
package-a==1.0.0
|
||||
a==1.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
@@ -341,9 +311,10 @@ fn incompatible_python_compatible_override() -> Result<()> {
|
||||
"
|
||||
);
|
||||
|
||||
output.assert().success().stdout(predicate::str::contains(
|
||||
"incompatible-python-compatible-override-a==1.0.0",
|
||||
));
|
||||
output
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("a==1.0.0"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -359,23 +330,20 @@ fn incompatible_python_compatible_override() -> Result<()> {
|
||||
/// │ └── satisfied by a-1.0.0
|
||||
/// └── a
|
||||
/// └── a-1.0.0
|
||||
/// └── requires python>=3.9.4
|
||||
/// └── requires python>=3.9.4 (incompatible with environment)
|
||||
/// ```
|
||||
#[cfg(feature = "test-python-patch")]
|
||||
#[test]
|
||||
fn python_patch_override_no_patch() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.9.21");
|
||||
let python_versions = &[];
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((r"python-patch-override-no-patch-", "package-"));
|
||||
let server = PackseServer::new("requires_python/python-patch-override-no-patch.toml");
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("python-patch-override-no-patch-a==1.0.0")?;
|
||||
requirements_in.write_str("a==1.0.0")?;
|
||||
|
||||
// Since the resolver is asked to solve with 3.9, the minimum compatible Python requirement is treated as 3.9.0.
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
let output = uv_snapshot!(context.filters(), command(&context, python_versions, &server)
|
||||
.arg("--python-version=3.9")
|
||||
, @"
|
||||
success: false
|
||||
@@ -384,10 +352,10 @@ fn python_patch_override_no_patch() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because the requested Python version (>=3.9) does not satisfy Python>=3.9.4 and package-a==1.0.0 depends on Python>=3.9.4, we can conclude that package-a==1.0.0 cannot be used.
|
||||
And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable.
|
||||
╰─▶ Because the requested Python version (>=3.9) does not satisfy Python>=3.9.4 and a==1.0.0 depends on Python>=3.9.4, we can conclude that a==1.0.0 cannot be used.
|
||||
And because you require a==1.0.0, we can conclude that your requirements are unsatisfiable.
|
||||
|
||||
hint: The `--python-version` value (>=3.9) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.9.4). Consider using a higher `--python-version` value.
|
||||
hint: The `--python-version` value (>=3.9) includes Python versions that are not supported by your dependencies (e.g., a==1.0.0 only supports >=3.9.4). Consider using a higher `--python-version` value.
|
||||
"
|
||||
);
|
||||
|
||||
@@ -407,22 +375,18 @@ fn python_patch_override_no_patch() -> Result<()> {
|
||||
/// │ └── satisfied by a-1.0.0
|
||||
/// └── a
|
||||
/// └── a-1.0.0
|
||||
/// └── requires python>=3.9.0
|
||||
/// ```
|
||||
#[cfg(feature = "test-python-patch")]
|
||||
#[test]
|
||||
fn python_patch_override_patch_compatible() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.9.21");
|
||||
let python_versions = &[];
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((r"python-patch-override-patch-compatible-", "package-"));
|
||||
let server = PackseServer::new("requires_python/python-patch-override-patch-compatible.toml");
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("python-patch-override-patch-compatible-a==1.0.0")?;
|
||||
requirements_in.write_str("a==1.0.0")?;
|
||||
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
let output = uv_snapshot!(context.filters(), command(&context, python_versions, &server)
|
||||
.arg("--python-version=3.9.0")
|
||||
, @"
|
||||
success: true
|
||||
@@ -430,7 +394,7 @@ fn python_patch_override_patch_compatible() -> Result<()> {
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.9.0
|
||||
package-a==1.0.0
|
||||
a==1.0.0
|
||||
# via -r requirements.in
|
||||
|
||||
----- stderr -----
|
||||
@@ -439,9 +403,10 @@ fn python_patch_override_patch_compatible() -> Result<()> {
|
||||
"
|
||||
);
|
||||
|
||||
output.assert().success().stdout(predicate::str::contains(
|
||||
"python-patch-override-patch-compatible-a==1.0.0",
|
||||
));
|
||||
output
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("a==1.0.0"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -27,9 +27,11 @@ use uv_fs::{PortablePath, Simplified};
|
||||
use uv_static::EnvVars;
|
||||
#[cfg(feature = "test-git")]
|
||||
use uv_test::decode_token;
|
||||
use uv_test::find_links::FindLinksServer;
|
||||
use uv_test::packse::PackseServer;
|
||||
use uv_test::{
|
||||
DEFAULT_PYTHON_VERSION, TestContext, apply_filters, build_vendor_links_url, download_to_disk,
|
||||
get_bin, packse_index_url, uv_snapshot, venv_bin_path,
|
||||
DEFAULT_PYTHON_VERSION, TestContext, apply_filters, download_to_disk, get_bin, uv_snapshot,
|
||||
venv_bin_path,
|
||||
};
|
||||
|
||||
fn write_tar_gz(file: File, entries: &[(&str, &str)]) -> Result<()> {
|
||||
@@ -4274,6 +4276,7 @@ fn install_git_source_respects_offline_mode() {
|
||||
#[test]
|
||||
fn build_prerelease_hint() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let server = PackseServer::new("prereleases/transitive-package-only-prereleases-in-range.toml");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
@@ -4283,12 +4286,12 @@ fn build_prerelease_hint() -> Result<()> {
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["transitive-package-only-prereleases-in-range-a"]
|
||||
requires = ["a"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
"#})?;
|
||||
|
||||
let mut command = context.pip_install();
|
||||
command.arg("--index-url").arg(packse_index_url()).arg(".");
|
||||
command.arg("--index-url").arg(server.index_url()).arg(".");
|
||||
command.env_remove(EnvVars::UV_EXCLUDE_NEWER);
|
||||
|
||||
uv_snapshot!(
|
||||
@@ -4303,11 +4306,11 @@ fn build_prerelease_hint() -> Result<()> {
|
||||
Resolved 1 package in [TIME]
|
||||
× Failed to build `project @ file://[TEMP_DIR]/`
|
||||
├─▶ Failed to resolve requirements from `build-system.requires`
|
||||
├─▶ No solution found when resolving: `transitive-package-only-prereleases-in-range-a`
|
||||
╰─▶ Because only transitive-package-only-prereleases-in-range-b<=0.1 is available and transitive-package-only-prereleases-in-range-a==0.1.0 depends on transitive-package-only-prereleases-in-range-b>0.1, we can conclude that transitive-package-only-prereleases-in-range-a==0.1.0 cannot be used.
|
||||
And because only transitive-package-only-prereleases-in-range-a==0.1.0 is available and you require transitive-package-only-prereleases-in-range-a, we can conclude that your requirements are unsatisfiable.
|
||||
├─▶ No solution found when resolving: `a`
|
||||
╰─▶ Because only b<=0.1 is available and a==0.1.0 depends on b>0.1, we can conclude that a==0.1.0 cannot be used.
|
||||
And because only a==0.1.0 is available and you require a, we can conclude that your requirements are unsatisfiable.
|
||||
|
||||
hint: Only pre-releases of `transitive-package-only-prereleases-in-range-b` (e.g., 1.0.0a1) match these build requirements, and build environments can't enable pre-releases automatically. Add `transitive-package-only-prereleases-in-range-b>=1.0.0a1` to `build-system.requires`, `[tool.uv.extra-build-dependencies]`, or supply it via `uv build --build-constraint`.
|
||||
hint: Only pre-releases of `b` (e.g., 1.0.0a1) match these build requirements, and build environments can't enable pre-releases automatically. Add `b>=1.0.0a1` to `build-system.requires`, `[tool.uv.extra-build-dependencies]`, or supply it via `uv build --build-constraint`.
|
||||
"
|
||||
);
|
||||
|
||||
@@ -6862,6 +6865,7 @@ fn already_installed_remote_dependencies() {
|
||||
#[test]
|
||||
fn already_installed_dependent_editable() {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let vendor = FindLinksServer::vendor();
|
||||
let root_path = context
|
||||
.workspace_root
|
||||
.join("test/packages/dependent_locals");
|
||||
@@ -6890,7 +6894,7 @@ fn already_installed_dependent_editable() {
|
||||
// Disable the index to guard this test against dependency confusion attacks
|
||||
.arg("--no-index")
|
||||
.arg("--find-links")
|
||||
.arg(build_vendor_links_url()), @"
|
||||
.arg(vendor.url()), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -6931,7 +6935,7 @@ fn already_installed_dependent_editable() {
|
||||
// Disable the index to guard this test against dependency confusion attacks
|
||||
.arg("--no-index")
|
||||
.arg("--find-links")
|
||||
.arg(build_vendor_links_url()), @"
|
||||
.arg(vendor.url()), @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -6968,6 +6972,7 @@ fn already_installed_dependent_editable() {
|
||||
#[test]
|
||||
fn already_installed_local_path_dependent() {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
let vendor = FindLinksServer::vendor();
|
||||
let root_path = context
|
||||
.workspace_root
|
||||
.join("test/packages/dependent_locals");
|
||||
@@ -6994,7 +6999,7 @@ fn already_installed_local_path_dependent() {
|
||||
// Disable the index to guard this test against dependency confusion attacks
|
||||
.arg("--no-index")
|
||||
.arg("--find-links")
|
||||
.arg(build_vendor_links_url()), @"
|
||||
.arg(vendor.url()), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -7050,7 +7055,7 @@ fn already_installed_local_path_dependent() {
|
||||
// Disable the index to guard this test against dependency confusion attacks
|
||||
.arg("--no-index")
|
||||
.arg("--find-links")
|
||||
.arg(build_vendor_links_url()), @"
|
||||
.arg(vendor.url()), @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -7092,7 +7097,7 @@ fn already_installed_local_path_dependent() {
|
||||
// Disable the index to guard this test against dependency confusion attacks
|
||||
.arg("--no-index")
|
||||
.arg("--find-links")
|
||||
.arg(build_vendor_links_url()), @"
|
||||
.arg(vendor.url()), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -7117,7 +7122,7 @@ fn already_installed_local_path_dependent() {
|
||||
// Disable the index to guard this test against dependency confusion attacks
|
||||
.arg("--no-index")
|
||||
.arg("--find-links")
|
||||
.arg(build_vendor_links_url()), @"
|
||||
.arg(vendor.url()), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -15836,11 +15841,12 @@ fn abi_compatibility_on_nondebug_python_with_debug_wheel() {
|
||||
#[test]
|
||||
fn warn_on_bz2_wheel() {
|
||||
let context = uv_test::test_context!("3.14");
|
||||
let vendor = FindLinksServer::vendor();
|
||||
|
||||
uv_snapshot!(
|
||||
context.filters(),
|
||||
context.pip_install()
|
||||
.arg("futzed_bz2 @ https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_bz2-0.1.0-py3-none-any.whl"),
|
||||
.arg(format!("futzed_bz2 @ {}/futzed_bz2-0.1.0-py3-none-any.whl", vendor.url())),
|
||||
@"
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -15848,10 +15854,10 @@ fn warn_on_bz2_wheel() {
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
warning: One or more file entries in 'https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_bz2-0.1.0-py3-none-any.whl' use the 'bzip2' compression method, which is not widely supported. A future version of uv will reject ZIP archives containing entries compressed with this method. Entries must be compressed with the 'stored', 'DEFLATE', or 'zstd' compression methods.
|
||||
warning: One or more file entries in 'http://[LOCALHOST]/futzed_bz2-0.1.0-py3-none-any.whl' use the 'bzip2' compression method, which is not widely supported. A future version of uv will reject ZIP archives containing entries compressed with this method. Entries must be compressed with the 'stored', 'DEFLATE', or 'zstd' compression methods.
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ futzed-bz2==0.1.0 (from https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_bz2-0.1.0-py3-none-any.whl)
|
||||
+ futzed-bz2==0.1.0 (from http://[LOCALHOST]/futzed_bz2-0.1.0-py3-none-any.whl)
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -15859,20 +15865,21 @@ fn warn_on_bz2_wheel() {
|
||||
#[test]
|
||||
fn warn_on_lzma_wheel() {
|
||||
let context = uv_test::test_context!("3.14");
|
||||
let vendor = FindLinksServer::vendor();
|
||||
|
||||
uv_snapshot!(
|
||||
context.filters(),
|
||||
context.pip_install()
|
||||
.arg("futzed_lzma @ https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_lzma-0.1.0-py3-none-any.whl"),
|
||||
.arg(format!("futzed_lzma @ {}/futzed_lzma-0.1.0-py3-none-any.whl", vendor.url())),
|
||||
@"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Failed to download `futzed-lzma @ https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_lzma-0.1.0-py3-none-any.whl`
|
||||
× Failed to download `futzed-lzma @ http://[LOCALHOST]/futzed_lzma-0.1.0-py3-none-any.whl`
|
||||
├─▶ Request failed after 3 retries in [TIME]
|
||||
├─▶ Failed to read metadata: `https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_lzma-0.1.0-py3-none-any.whl`
|
||||
├─▶ Failed to read metadata: `http://[LOCALHOST]/futzed_lzma-0.1.0-py3-none-any.whl`
|
||||
├─▶ Failed to read from zip file
|
||||
├─▶ an upstream reader returned an error: stream/file format not recognized
|
||||
╰─▶ stream/file format not recognized
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
use uv_fs::Simplified;
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use uv_test::{TestContext, download_to_disk, packse_index_url, uv_snapshot, venv_bin_path};
|
||||
use uv_test::{TestContext, download_to_disk, uv_snapshot, venv_bin_path};
|
||||
|
||||
#[test]
|
||||
fn sync() -> Result<()> {
|
||||
@@ -13807,6 +13807,8 @@ fn direct_url_dependency_metadata() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
fn sync_required_environment_hint() -> Result<()> {
|
||||
let server =
|
||||
uv_test::packse::PackseServer::new("wheels/no-sdist-no-wheels-with-matching-platform.toml");
|
||||
let context = uv_test::test_context!("3.13");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
@@ -13815,14 +13817,14 @@ fn sync_required_environment_hint() -> Result<()> {
|
||||
name = "example"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["no-sdist-no-wheels-with-matching-platform-a"]
|
||||
dependencies = ["a"]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "packse"
|
||||
url = "{}"
|
||||
url = "{index_url}"
|
||||
default = true
|
||||
"#,
|
||||
packse_index_url()
|
||||
index_url = server.index_url()
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @"
|
||||
@@ -13851,9 +13853,9 @@ fn sync_required_environment_hint() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
error: Distribution `no-sdist-no-wheels-with-matching-platform-a==1.0.0 @ registry+https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/` can't be installed because it doesn't have a source distribution or wheel for the current platform
|
||||
error: Distribution `a==1.0.0 @ registry+http://[LOCALHOST]/simple/` can't be installed because it doesn't have a source distribution or wheel for the current platform
|
||||
|
||||
hint: You're on [PLATFORM] (`[TAG]`), but `no-sdist-no-wheels-with-matching-platform-a` (v1.0.0) only has wheels for the following platform: `macosx_10_0_ppc64`; consider adding "sys_platform == '[PLATFORM]' and platform_machine == '[MACHINE]'" to `tool.uv.required-environments` to ensure uv resolves to a version with compatible wheels
|
||||
hint: You're on [PLATFORM] (`[TAG]`), but `a` (v1.0.0) only has wheels for the following platform: `macosx_10_0_ppc64`; consider adding "sys_platform == '[PLATFORM]' and platform_machine == '[MACHINE]'" to `tool.uv.required-environments` to ensure uv resolves to a version with compatible wheels
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generates and updates snapshot test cases from packse scenarios.
|
||||
|
||||
Important:
|
||||
|
||||
This script is the backend called by `./scripts/sync_scenarios.sh`, consider using that
|
||||
if not developing scenarios.
|
||||
|
||||
Requirements:
|
||||
|
||||
$ uv pip install -r scripts/scenarios/pylock.toml
|
||||
|
||||
Uses `git`, `rustfmt`, and `cargo insta test` requirements from the project.
|
||||
|
||||
Usage:
|
||||
|
||||
Regenerate the scenario test files using the given scenarios:
|
||||
|
||||
$ ./scripts/scenarios/generate.py <path>
|
||||
|
||||
Scenarios can be developed locally with the following workflow:
|
||||
|
||||
Serve scenarios on a local index using packse
|
||||
|
||||
$ packse serve --no-hash <path to scenarios>
|
||||
|
||||
Override the uv package index and update the tests
|
||||
|
||||
$ UV_TEST_PACKSE_INDEX="http://localhost:3141" ./scripts/scenarios/generate.py <path to scenarios>
|
||||
|
||||
If an editable version of packse is installed, this script will use its bundled scenarios by default.
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import importlib.metadata
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from enum import StrEnum, auto
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
TOOL_ROOT = Path(__file__).parent
|
||||
TEMPLATES = TOOL_ROOT / "templates"
|
||||
PACKSE = TOOL_ROOT / "packse-scenarios"
|
||||
REQUIREMENTS = TOOL_ROOT / "pylock.toml"
|
||||
PROJECT_ROOT = TOOL_ROOT.parent.parent
|
||||
TESTS = PROJECT_ROOT / "crates" / "uv" / "tests" / "it"
|
||||
TESTS_COMMON_MOD_RS = PROJECT_ROOT / "crates" / "uv-test" / "src" / "lib.rs"
|
||||
|
||||
try:
|
||||
import packse
|
||||
import packse.inspect
|
||||
except ImportError:
|
||||
print(
|
||||
f"missing requirement `packse`: install the requirements at {REQUIREMENTS.relative_to(PROJECT_ROOT)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
import chevron_blue
|
||||
except ImportError:
|
||||
print(
|
||||
f"missing requirement `chevron-blue`: install the requirements at {REQUIREMENTS.relative_to(PROJECT_ROOT)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
exit(1)
|
||||
|
||||
|
||||
class TemplateKind(StrEnum):
|
||||
install = auto()
|
||||
compile = auto()
|
||||
lock = auto()
|
||||
|
||||
def template_file(self) -> Path:
|
||||
return TEMPLATES / f"{self.name}.mustache"
|
||||
|
||||
def test_file(self) -> Path:
|
||||
match self.value:
|
||||
case TemplateKind.install:
|
||||
return TESTS / "pip_install_scenarios.rs"
|
||||
case TemplateKind.compile:
|
||||
return TESTS / "pip_compile_scenarios.rs"
|
||||
case TemplateKind.lock:
|
||||
return TESTS / "lock_scenarios.rs"
|
||||
case _:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def main(
|
||||
scenarios: list[Path],
|
||||
template_kinds: list[TemplateKind],
|
||||
snapshot_update: bool = True,
|
||||
):
|
||||
# Fetch packse version
|
||||
packse_version = importlib.metadata.version("packse")
|
||||
|
||||
debug = logging.getLogger().getEffectiveLevel() <= logging.DEBUG
|
||||
|
||||
# Don't update the version to `0.0.0` to preserve the `UV_TEST_PACKSE_URL`
|
||||
# in local tests.
|
||||
if packse_version != "0.0.0":
|
||||
update_common_mod_rs(packse_version)
|
||||
|
||||
if not scenarios:
|
||||
if packse_version == "0.0.0":
|
||||
path = packse.__development_base_path__ / "scenarios"
|
||||
if path.exists():
|
||||
logging.info(
|
||||
"Detected development version of packse, using scenarios from %s",
|
||||
path,
|
||||
)
|
||||
scenarios = [path]
|
||||
else:
|
||||
logging.error(
|
||||
"No scenarios provided. Found development version of packse but is missing scenarios. Is it installed as an editable?"
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.error("No scenarios provided, nothing to do.")
|
||||
return
|
||||
|
||||
targets = []
|
||||
for target in scenarios:
|
||||
if target.is_dir():
|
||||
targets.extend(target.glob("**/*.json"))
|
||||
targets.extend(target.glob("**/*.toml"))
|
||||
targets.extend(target.glob("**/*.yaml"))
|
||||
else:
|
||||
targets.append(target)
|
||||
|
||||
logging.info("Loading scenario metadata...")
|
||||
data = packse.inspect.variables_for_templates(
|
||||
targets=targets,
|
||||
no_hash=True,
|
||||
)
|
||||
|
||||
data["scenarios"] = [
|
||||
scenario
|
||||
for scenario in data["scenarios"]
|
||||
# Drop example scenarios
|
||||
if not scenario["name"].startswith("example")
|
||||
]
|
||||
|
||||
# We have a mixture of long singe-line descriptions (json scenarios) we need to
|
||||
# wrap and manually formatted markdown in toml and yaml scenarios we want to
|
||||
# preserve.
|
||||
for scenario in data["scenarios"]:
|
||||
if scenario["_textwrap"]:
|
||||
scenario["description"] = textwrap.wrap(scenario["description"], width=80)
|
||||
else:
|
||||
scenario["description"] = scenario["description"].splitlines()
|
||||
# Don't drop empty lines like chevron would.
|
||||
scenario["description"] = "\n/// ".join(scenario["description"])
|
||||
|
||||
# Apply the same wrapping to the expected explanation
|
||||
for scenario in data["scenarios"]:
|
||||
expected = scenario["expected"]
|
||||
if explanation := expected["explanation"]:
|
||||
if scenario["_textwrap"]:
|
||||
expected["explanation"] = textwrap.wrap(explanation, width=80)
|
||||
else:
|
||||
expected["explanation"] = explanation.splitlines()
|
||||
expected["explanation"] = "\n// ".join(expected["explanation"])
|
||||
|
||||
# Hack to track which scenarios require a specific Python patch version
|
||||
for scenario in data["scenarios"]:
|
||||
if "patch" in scenario["name"]:
|
||||
scenario["python_patch"] = True
|
||||
else:
|
||||
scenario["python_patch"] = False
|
||||
|
||||
# Split scenarios into `install`, `compile` and `lock` cases
|
||||
install_scenarios = []
|
||||
compile_scenarios = []
|
||||
lock_scenarios = []
|
||||
|
||||
for scenario in data["scenarios"]:
|
||||
resolver_options = scenario["resolver_options"] or {}
|
||||
# Avoid writing the empty `required-environments = []`
|
||||
resolver_options["has_required_environments"] = bool(
|
||||
resolver_options.get("required_environments", [])
|
||||
)
|
||||
if resolver_options.get("universal"):
|
||||
lock_scenarios.append(scenario)
|
||||
elif resolver_options.get("python") is not None:
|
||||
compile_scenarios.append(scenario)
|
||||
else:
|
||||
install_scenarios.append(scenario)
|
||||
|
||||
template_kinds_and_scenarios: list[tuple[TemplateKind, list[Any]]] = [
|
||||
(TemplateKind.install, install_scenarios),
|
||||
(TemplateKind.compile, compile_scenarios),
|
||||
(TemplateKind.lock, lock_scenarios),
|
||||
]
|
||||
for template_kind, scenarios in template_kinds_and_scenarios:
|
||||
if template_kind not in template_kinds:
|
||||
continue
|
||||
|
||||
data = {"scenarios": scenarios}
|
||||
|
||||
ref = "HEAD" if packse_version == "0.0.0" else packse_version
|
||||
|
||||
# Add generated metadata
|
||||
data["generated_from"] = (
|
||||
f"https://github.com/astral-sh/packse/tree/{ref}/scenarios"
|
||||
)
|
||||
data["generated_with"] = "./scripts/sync_scenarios.sh"
|
||||
data["vendor_links"] = (
|
||||
f"https://raw.githubusercontent.com/astral-sh/packse/{ref}/vendor/links.html"
|
||||
)
|
||||
|
||||
data["index_url"] = (
|
||||
os.environ.get(
|
||||
"UV_TEST_PACKSE_INDEX",
|
||||
f"https://astral-sh.github.io/packse/{ref}",
|
||||
)
|
||||
+ "/simple-html"
|
||||
)
|
||||
|
||||
# Render the template
|
||||
logging.info(f"Rendering template {template_kind.name}")
|
||||
output = chevron_blue.render(
|
||||
template=template_kind.template_file().read_text(),
|
||||
data=data,
|
||||
no_escape=True,
|
||||
warn=True,
|
||||
)
|
||||
|
||||
# Update the test files
|
||||
logging.info(
|
||||
f"Updating test file at `{template_kind.test_file().relative_to(PROJECT_ROOT)}`...",
|
||||
)
|
||||
with open(template_kind.test_file(), "w") as test_file:
|
||||
test_file.write(output)
|
||||
|
||||
# Format
|
||||
logging.info(
|
||||
"Formatting test file...",
|
||||
)
|
||||
subprocess.check_call(
|
||||
["rustfmt", template_kind.test_file()],
|
||||
stderr=subprocess.STDOUT,
|
||||
stdout=sys.stderr if debug else subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
# Update snapshots
|
||||
if snapshot_update:
|
||||
logging.info("Updating snapshots...")
|
||||
env = os.environ.copy()
|
||||
command = [
|
||||
"cargo",
|
||||
"insta",
|
||||
"test",
|
||||
"--features",
|
||||
"test-pypi,test-python,test-python-patch",
|
||||
"--accept",
|
||||
"--test-runner",
|
||||
"nextest",
|
||||
"--test",
|
||||
"it",
|
||||
"--",
|
||||
template_kind.test_file().with_suffix("").name,
|
||||
]
|
||||
logging.debug(f"Running {' '.join(command)}")
|
||||
exit_code = subprocess.call(
|
||||
command,
|
||||
cwd=PROJECT_ROOT,
|
||||
stderr=subprocess.STDOUT,
|
||||
stdout=sys.stderr if debug else subprocess.DEVNULL,
|
||||
env=env,
|
||||
)
|
||||
if exit_code != 0:
|
||||
logging.warning(
|
||||
f"Snapshot update failed with exit code {exit_code} (use -v to show details)"
|
||||
)
|
||||
else:
|
||||
logging.info("Skipping snapshot update")
|
||||
|
||||
logging.info("Done!")
|
||||
|
||||
|
||||
def update_common_mod_rs(packse_version: str):
|
||||
"""Update the value of `PACKSE_VERSION` used in non-scenario tests.
|
||||
|
||||
Example:
|
||||
```rust
|
||||
pub const PACKSE_VERSION: &str = "0.3.30";
|
||||
```
|
||||
"""
|
||||
test_common = TESTS_COMMON_MOD_RS.read_text()
|
||||
before_version = 'pub const PACKSE_VERSION: &str = "'
|
||||
after_version = '";'
|
||||
build_vendor_links_url = f"{before_version}{packse_version}{after_version}"
|
||||
if build_vendor_links_url in test_common:
|
||||
logging.info(f"Up-to-date: {TESTS_COMMON_MOD_RS}")
|
||||
else:
|
||||
logging.info(f"Updating: {TESTS_COMMON_MOD_RS}")
|
||||
url_matcher = re.compile(
|
||||
re.escape(before_version) + '[^"]+' + re.escape(after_version)
|
||||
)
|
||||
assert len(url_matcher.findall(test_common)) == 1, (
|
||||
f"PACKSE_VERSION not found in {TESTS_COMMON_MOD_RS}"
|
||||
)
|
||||
test_common = url_matcher.sub(build_vendor_links_url, test_common)
|
||||
TESTS_COMMON_MOD_RS.write_text(test_common)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generates and updates snapshot test cases from packse scenarios.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"scenarios",
|
||||
type=Path,
|
||||
nargs="*",
|
||||
help="The scenario files to use",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--templates",
|
||||
type=TemplateKind,
|
||||
choices=list(TemplateKind),
|
||||
default=list(TemplateKind),
|
||||
nargs="*",
|
||||
help="The templates to render. By default, all templates are rendered",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Enable debug logging",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
"--quiet",
|
||||
action="store_true",
|
||||
help="Disable logging",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-snapshot-update",
|
||||
action="store_true",
|
||||
help="Disable automatic snapshot updates",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.quiet:
|
||||
log_level = logging.CRITICAL
|
||||
elif args.verbose:
|
||||
log_level = logging.DEBUG
|
||||
else:
|
||||
log_level = logging.INFO
|
||||
|
||||
logging.basicConfig(level=log_level, format="%(message)s")
|
||||
|
||||
main(args.scenarios, args.templates, snapshot_update=not args.no_snapshot_update)
|
||||
@@ -1,173 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --group scripts/scenarios/pyproject.toml:packse -o scripts/scenarios/pylock.toml --universal -p 3.12 --refresh-package packse
|
||||
lock-version = "1.0"
|
||||
created-by = "uv"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[packages]]
|
||||
name = "chevron-blue"
|
||||
version = "0.3.0"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/05/2110389caf5789648f83c4374d9aa83215a50bb028bfb5139ccb51a8e3e3/chevron_blue-0.3.0.tar.gz", upload-time = 2025-07-16T16:56:23Z, size = 7730, hashes = { sha256 = "7099e1b8fa8d8e81beea31fcc448091f845de3533da1a36eea0c19327ab8521d" } }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/36/94/1ffcf1651c4ca3b7ecf8d20cc4e2ce13298f264ea31fa77bc53c0b5ddf56/chevron_blue-0.3.0-py3-none-any.whl", upload-time = 2025-07-16T16:56:21Z, size = 9829, hashes = { sha256 = "7ebea4ae16a410cc0416e98a158b0fad615173f67764cfea55bd24776bb0cf29" } }]
|
||||
|
||||
[[packages]]
|
||||
name = "hatchling"
|
||||
version = "1.27.0"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc1debe3514da292094f1c3a700e4ca25442489731ef7c0814358816bb03/hatchling-1.27.0.tar.gz", upload-time = 2024-12-15T17:08:11Z, size = 54983, hashes = { sha256 = "971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6" } }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", upload-time = 2024-12-15T17:08:10Z, size = 75794, hashes = { sha256 = "d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b" } }]
|
||||
|
||||
[[packages]]
|
||||
name = "msgspec"
|
||||
version = "0.19.0"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", upload-time = 2024-12-27T17:40:28Z, size = 216934, hashes = { sha256 = "604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e" } }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/40/817282b42f58399762267b30deb8ac011d8db373f8da0c212c85fbe62b8f/msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", upload-time = 2024-12-27T17:39:13Z, size = 190019, hashes = { sha256 = "d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/99/bd7ed738c00f223a8119928661167a89124140792af18af513e6519b0d54/msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", upload-time = 2024-12-27T17:39:17Z, size = 183680, hashes = { sha256 = "0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/27/322badde18eb234e36d4a14122b89edd4e2973cdbc3da61ca7edf40a1ccd/msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-12-27T17:39:19Z, size = 209334, hashes = { sha256 = "fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/65/080509c5774a1592b2779d902a70b5fe008532759927e011f068145a16cb/msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-12-27T17:39:21Z, size = 211551, hashes = { sha256 = "00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/2e/1c23c6b4ca6f4285c30a39def1054e2bee281389e4b681b5e3711bd5a8c9/msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", upload-time = 2024-12-27T17:39:24Z, size = 215099, hashes = { sha256 = "3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/fe/95f9654518879f3359d1e76bc41189113aa9102452170ab7c9a9a4ee52f6/msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", upload-time = 2024-12-27T17:39:27Z, size = 218211, hashes = { sha256 = "2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/f6/71ca7e87a1fb34dfe5efea8156c9ef59dd55613aeda2ca562f122cd22012/msgspec-0.19.0-cp310-cp310-win_amd64.whl", upload-time = 2024-12-27T17:39:29Z, size = 186174, hashes = { sha256 = "695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", upload-time = 2024-12-27T17:39:32Z, size = 187939, hashes = { sha256 = "aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", upload-time = 2024-12-27T17:39:33Z, size = 182202, hashes = { sha256 = "047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-12-27T17:39:35Z, size = 209029, hashes = { sha256 = "e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-12-27T17:39:36Z, size = 210682, hashes = { sha256 = "6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", upload-time = 2024-12-27T17:39:39Z, size = 214003, hashes = { sha256 = "f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", upload-time = 2024-12-27T17:39:41Z, size = 216833, hashes = { sha256 = "45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", upload-time = 2024-12-27T17:39:43Z, size = 186184, hashes = { sha256 = "70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", upload-time = 2024-12-27T17:39:44Z, size = 190485, hashes = { sha256 = "f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", upload-time = 2024-12-27T17:39:46Z, size = 183910, hashes = { sha256 = "43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-12-27T17:39:49Z, size = 210633, hashes = { sha256 = "4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-12-27T17:39:51Z, size = 213594, hashes = { sha256 = "d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", upload-time = 2024-12-27T17:39:52Z, size = 214053, hashes = { sha256 = "757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", upload-time = 2024-12-27T17:39:55Z, size = 219081, hashes = { sha256 = "5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", upload-time = 2024-12-27T17:39:56Z, size = 187467, hashes = { sha256 = "067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", upload-time = 2024-12-27T17:40:00Z, size = 190498, hashes = { sha256 = "f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", upload-time = 2024-12-27T17:40:04Z, size = 183950, hashes = { sha256 = "82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-12-27T17:40:05Z, size = 210647, hashes = { sha256 = "19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-12-27T17:40:10Z, size = 213563, hashes = { sha256 = "60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", upload-time = 2024-12-27T17:40:12Z, size = 213996, hashes = { sha256 = "ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", upload-time = 2024-12-27T17:40:14Z, size = 219087, hashes = { sha256 = "a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", upload-time = 2024-12-27T17:40:16Z, size = 187432, hashes = { sha256 = "317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/d0/323f867eaec1f2236ba30adf613777b1c97a7e8698e2e881656b21871fa4/msgspec-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", upload-time = 2024-12-27T17:40:18Z, size = 189926, hashes = { sha256 = "15c1e86fff77184c20a2932cd9742bf33fe23125fa3fcf332df9ad2f7d483044" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/37/c3e1b39bdae90a7258d77959f5f5e36ad44b40e2be91cff83eea33c54d43/msgspec-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", upload-time = 2024-12-27T17:40:20Z, size = 183873, hashes = { sha256 = "3b5541b2b3294e5ffabe31a09d604e23a88533ace36ac288fa32a420aa38d229" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/a2/48f2c15c7644668e51f4dce99d5f709bd55314e47acb02e90682f5880f35/msgspec-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-12-27T17:40:21Z, size = 209272, hashes = { sha256 = "0f5c043ace7962ef188746e83b99faaa9e3e699ab857ca3f367b309c8e2c6b12" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/3c/aa339cf08b990c3f07e67b229a3a8aa31bf129ed974b35e5daa0df7d9d56/msgspec-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-12-27T17:40:22Z, size = 211396, hashes = { sha256 = "ca06aa08e39bf57e39a258e1996474f84d0dd8130d486c00bec26d797b8c5446" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/00/c7fb9d524327c558b2803973cc3f988c5100a1708879970a9e377bdf6f4f/msgspec-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", upload-time = 2024-12-27T17:40:24Z, size = 215002, hashes = { sha256 = "e695dad6897896e9384cf5e2687d9ae9feaef50e802f93602d35458e20d1fb19" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/bf/d9f9fff026c1248cde84a5ce62b3742e8a63a3c4e811f99f00c8babf7615/msgspec-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", upload-time = 2024-12-27T17:40:25Z, size = 218132, hashes = { sha256 = "3be5c02e1fee57b54130316a08fe40cca53af92999a302a6054cd451700ea7db" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/03/b92011210f79794958167a3a3ea64a71135d9a2034cfb7597b545a42606d/msgspec-0.19.0-cp39-cp39-win_amd64.whl", upload-time = 2024-12-27T17:40:27Z, size = 186301, hashes = { sha256 = "0684573a821be3c749912acf5848cce78af4298345cb2d7a8b8948a0a5a27cfe" } },
|
||||
]
|
||||
|
||||
[[packages]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", upload-time = 2025-04-19T11:48:59Z, size = 165727, hashes = { sha256 = "d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" } }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", upload-time = 2025-04-19T11:48:57Z, size = 66469, hashes = { sha256 = "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484" } }]
|
||||
|
||||
[[packages]]
|
||||
name = "packse"
|
||||
version = "0.3.59"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/90/51404d8933506bd9554f607f5054f4715e0a5e1d34e4c6542580553e8b75/packse-0.3.59.tar.gz", upload-time = 2026-02-18T17:44:17Z, size = 5880109, hashes = { sha256 = "718bcca5dd1e9321f5c2918d5975ffd772f0c3b150cf29b03979677a003d73d2" } }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/8d/95/b4a997b57a1f46f0f7575c1c021ee15e8fc1470b6ce74010239079d1d9aa/packse-0.3.59-py3-none-any.whl", upload-time = 2026-02-18T17:44:16Z, size = 34107, hashes = { sha256 = "10a3689ce0c00805cd7f1589862ab2d8f9fd4ab38c117342c351bfb13daa5c69" } }]
|
||||
|
||||
[[packages]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", upload-time = 2023-12-10T22:30:45Z, size = 51043, hashes = { sha256 = "a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" } }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", upload-time = 2023-12-10T22:30:43Z, size = 31191, hashes = { sha256 = "a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08" } }]
|
||||
|
||||
[[packages]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", upload-time = 2025-05-15T12:30:07Z, size = 69412, hashes = { sha256 = "7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" } }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", upload-time = 2025-05-15T12:30:06Z, size = 20538, hashes = { sha256 = "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" } }]
|
||||
|
||||
[[packages]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", upload-time = 2024-08-06T20:33:50Z, size = 130631, hashes = { sha256 = "d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e" } }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", upload-time = 2024-08-06T20:31:40Z, size = 184199, hashes = { sha256 = "0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:31:42Z, size = 171758, hashes = { sha256 = "29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:31:44Z, size = 718463, hashes = { sha256 = "8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:31:50Z, size = 719280, hashes = { sha256 = "7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:31:52Z, size = 751239, hashes = { sha256 = "ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:31:53Z, size = 695802, hashes = { sha256 = "936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:31:55Z, size = 720527, hashes = { sha256 = "23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", upload-time = 2024-08-06T20:31:56Z, size = 144052, hashes = { sha256 = "2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", upload-time = 2024-08-06T20:31:58Z, size = 161774, hashes = { sha256 = "a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", upload-time = 2024-08-06T20:32:03Z, size = 184612, hashes = { sha256 = "cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:32:04Z, size = 172040, hashes = { sha256 = "1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:32:06Z, size = 736829, hashes = { sha256 = "5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:32:08Z, size = 764167, hashes = { sha256 = "5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:32:14Z, size = 762952, hashes = { sha256 = "3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:32:16Z, size = 735301, hashes = { sha256 = "ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:32:18Z, size = 756638, hashes = { sha256 = "797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", upload-time = 2024-08-06T20:32:19Z, size = 143850, hashes = { sha256 = "11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", upload-time = 2024-08-06T20:32:21Z, size = 161980, hashes = { sha256 = "e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", upload-time = 2024-08-06T20:32:25Z, size = 183873, hashes = { sha256 = "c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:32:26Z, size = 173302, hashes = { sha256 = "ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:32:28Z, size = 739154, hashes = { sha256 = "1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:32:30Z, size = 766223, hashes = { sha256 = "9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:32:31Z, size = 767542, hashes = { sha256 = "80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:32:37Z, size = 731164, hashes = { sha256 = "0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:32:38Z, size = 756611, hashes = { sha256 = "8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", upload-time = 2024-08-06T20:32:40Z, size = 140591, hashes = { sha256 = "ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", upload-time = 2024-08-06T20:32:41Z, size = 156338, hashes = { sha256 = "7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", upload-time = 2024-08-06T20:32:43Z, size = 181309, hashes = { sha256 = "efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:32:44Z, size = 171679, hashes = { sha256 = "50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:32:46Z, size = 733428, hashes = { sha256 = "0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:32:51Z, size = 763361, hashes = { sha256 = "17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:32:53Z, size = 759523, hashes = { sha256 = "70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:32:54Z, size = 726660, hashes = { sha256 = "41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:32:56Z, size = 751597, hashes = { sha256 = "68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", upload-time = 2024-08-06T20:33:03Z, size = 140527, hashes = { sha256 = "bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", upload-time = 2024-08-06T20:33:04Z, size = 156446, hashes = { sha256 = "8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", upload-time = 2024-08-06T20:33:06Z, size = 183218, hashes = { sha256 = "24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:33:07Z, size = 728067, hashes = { sha256 = "d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:33:12Z, size = 757812, hashes = { sha256 = "d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:33:14Z, size = 746531, hashes = { sha256 = "9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:33:16Z, size = 800820, hashes = { sha256 = "82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", upload-time = 2024-08-06T20:33:22Z, size = 145514, hashes = { sha256 = "43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", upload-time = 2024-08-06T20:33:23Z, size = 162702, hashes = { sha256 = "01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", upload-time = 2024-08-06T20:33:25Z, size = 184777, hashes = { sha256 = "688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", upload-time = 2024-08-06T20:33:27Z, size = 172318, hashes = { sha256 = "a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-08-06T20:33:28Z, size = 720891, hashes = { sha256 = "d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2024-08-06T20:33:34Z, size = 722614, hashes = { sha256 = "f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-08-06T20:33:35Z, size = 737360, hashes = { sha256 = "3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", upload-time = 2024-08-06T20:33:37Z, size = 699006, hashes = { sha256 = "0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", upload-time = 2024-08-06T20:33:39Z, size = 723577, hashes = { sha256 = "a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", upload-time = 2024-08-06T20:33:46Z, size = 144593, hashes = { sha256 = "6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", upload-time = 2024-08-06T20:33:49Z, size = 162312, hashes = { sha256 = "39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8" } },
|
||||
]
|
||||
|
||||
[[packages]]
|
||||
name = "trove-classifiers"
|
||||
version = "2025.9.11.17"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/9a/778622bc06632529817c3c524c82749a112603ae2bbcf72ee3eb33a2c4f1/trove_classifiers-2025.9.11.17.tar.gz", upload-time = 2025-09-11T17:07:50Z, size = 16975, hashes = { sha256 = "931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd" } }
|
||||
wheels = [{ url = "https://files.pythonhosted.org/packages/e1/85/a4ff8758c66f1fc32aa5e9a145908394bf9cf1c79ffd1113cfdeb77e74e4/trove_classifiers-2025.9.11.17-py3-none-any.whl", upload-time = 2025-09-11T17:07:49Z, size = 14158, hashes = { sha256 = "5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd" } }]
|
||||
|
||||
[[packages]]
|
||||
name = "uv"
|
||||
version = "0.8.17"
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/4c/c270c6b8ed3e8c7fe38ea0b99df9eff09c332421b93d55a158371f75220e/uv-0.8.17.tar.gz", upload-time = 2025-09-10T21:51:25Z, size = 3615060, hashes = { sha256 = "2afd4525a53c8ab3a11a5a15093c503d27da67e76257a649b05e4f0bc2ebb5ae" } }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/7d/bbaa45c88b2c91e02714a8a5c9e787c47e4898bddfdd268569163492ba45/uv-0.8.17-py3-none-linux_armv6l.whl", upload-time = 2025-09-10T21:50:18Z, size = 20242144, hashes = { sha256 = "c51c9633ca93ef63c07df2443941e6264efd2819cc9faabfd9fe11899c6a0d6a" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/34/609b72034df0c62bcfb0c0ad4b11e2b55e537c0f0817588b5337d3dcca71/uv-0.8.17-py3-none-macosx_10_12_x86_64.whl", upload-time = 2025-09-10T21:50:22Z, size = 19363081, hashes = { sha256 = "c28fba6d7bb5c34ade2c8da5000faebe8425a287f42a043ca01ceb24ebc81590" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/bc/9417df48f0c18a9d54c2444096e03f2f56a3534c5b869f50ac620729cbc8/uv-0.8.17-py3-none-macosx_11_0_arm64.whl", upload-time = 2025-09-10T21:50:25Z, size = 17943513, hashes = { sha256 = "b009f1ec9e28de00f76814ad66e35aaae82c98a0f24015de51943dcd1c2a1895" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/1c/14fd54c852fd592a2b5da4b7960f3bf4a15c7e51eb20eaddabe8c8cca32d/uv-0.8.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", upload-time = 2025-09-10T21:50:29Z, size = 19507222, hashes = { sha256 = "84d56ae50ca71aec032577adf9737974554a82a94e52cee57722745656c1d383" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/47/f6a68cc310feca37c965bcbd57eb999e023d35eaeda9c9759867bf3ed232/uv-0.8.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", upload-time = 2025-09-10T21:50:32Z, size = 19865652, hashes = { sha256 = "85c2140f8553b9a4387a7395dc30cd151ef94046785fe8b198f13f2c380fb39b" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/6a/fdeb2d4a2635a6927c6d549b07177bcaf6ce15bdef58e8253e75c1b70f54/uv-0.8.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2025-09-10T21:50:37Z, size = 20831760, hashes = { sha256 = "2076119783e4a6d3c9e25638956cb123f0eabf4d7d407d9661cdf7f84818dcb9" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/4c/bd58b8a76015aa9ac49d6b4e1211ae1ca98a0aade0c49e1a5f645fb5cd38/uv-0.8.17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", upload-time = 2025-09-10T21:50:41Z, size = 22209056, hashes = { sha256 = "707a55660d302924fdbcb509e63dfec8842e19d35b69bcc17af76c25db15ad6f" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/2e/28f59c00a2ed6532502fb1e27da9394e505fb7b41cc0274475104b43561b/uv-0.8.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2025-09-10T21:50:45Z, size = 21871684, hashes = { sha256 = "1824b76911a14aaa9eee65ad9e180e6a4d2d7c86826232c2f28ae86aee56ed0e" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/1d/a8a4fc08de1f767316467e7a1989bb125734b7ed9cd98ce8969386a70653/uv-0.8.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2025-09-10T21:50:50Z, size = 21145154, hashes = { sha256 = "bb9b515cc813fb1b08f1e7592f76e437e2fb44945e53cde4fee11dee3b16d0c3" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/35/cb47d2d07a383c07b0e5043c6fe5555f0fd79683c6d7f9760222987c8be9/uv-0.8.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-09-10T21:50:54Z, size = 21106619, hashes = { sha256 = "b6d30d02fb65193309fc12a20f9e1a9fab67f469d3e487a254ca1145fd06788f" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/93/c310f0153b9dfe79bdd7f7eaef6380a8545c8939dbfc4e6bdee8f3ee7050/uv-0.8.17-py3-none-manylinux_2_28_aarch64.whl", upload-time = 2025-09-10T21:50:57Z, size = 19777591, hashes = { sha256 = "3941cecd9a6a46d3d4505753912c9cf3e8ae5eea30b9d0813f3656210f8c5d01" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/4f/971d3c84c2f09cf8df4536c33644e6b97e10a259d8630a0c1696c1fa6e94/uv-0.8.17-py3-none-manylinux_2_31_riscv64.whl", upload-time = 2025-09-10T21:51:01Z, size = 20845039, hashes = { sha256 = "cd0ad366cfe4cbe9212bd660b5b9f3a827ff35a7601cefdac2d153bfc8079eb7" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/29/8ad9038e75cb91f54b81cc933dd14fcfa92fa6f8706117d43d4251a8a662/uv-0.8.17-py3-none-musllinux_1_1_armv7l.whl", upload-time = 2025-09-10T21:51:04Z, size = 19820370, hashes = { sha256 = "505854bc75c497b95d2c65590291dc820999a4a7d9dfab4f44a9434a6cff7b5f" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/c9/fc8482d1e7dfe187c6e03dcefbac0db41a5dd72aa7b017c0f80f91a04444/uv-0.8.17-py3-none-musllinux_1_1_i686.whl", upload-time = 2025-09-10T21:51:08Z, size = 20289951, hashes = { sha256 = "dc479f661da449df37d68b36fdffa641e89fb53ad38c16a5c9f98f3211785b63" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/84/ad878ed045f02aa973be46636c802d494f8270caf5ea8bd04b7bbc68aa23/uv-0.8.17-py3-none-musllinux_1_1_x86_64.whl", upload-time = 2025-09-10T21:51:12Z, size = 21234644, hashes = { sha256 = "a1d11cd805be6d137ffef4a8227905f87f459031c645ac5031c30a3bcd08abd6" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/03/3fa2641513922988e641050b3adbc87de527f44c2cc8328510703616be6a/uv-0.8.17-py3-none-win32.whl", upload-time = 2025-09-10T21:51:16Z, size = 19216757, hashes = { sha256 = "d13a616eb0b2b33c7aa09746cc85860101d595655b58653f0b499af19f33467c" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/c4/0082f437bac162ab95e5a3a389a184c122d45eb5593960aab92fdf80374b/uv-0.8.17-py3-none-win_amd64.whl", upload-time = 2025-09-10T21:51:19Z, size = 21125811, hashes = { sha256 = "cf85b84b81b41d57a9b6eeded8473ec06ace8ee959ad0bb57e102b5ad023bd34" } },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/a2/29f57b118b3492c9d5ab1a99ba4906e7d7f8b658881d31bc2c4408d64d07/uv-0.8.17-py3-none-win_arm64.whl", upload-time = 2025-09-10T21:51:22Z, size = 19564631, hashes = { sha256 = "64d649a8c4c3732b05dc712544963b004cf733d95fdc5d26f43c5493553ff0a7" } },
|
||||
]
|
||||
@@ -1,5 +0,0 @@
|
||||
[dependency-groups]
|
||||
packse = [
|
||||
"chevron-blue",
|
||||
"packse>=0.3.59"
|
||||
]
|
||||
@@ -1,106 +0,0 @@
|
||||
//! DO NOT EDIT
|
||||
//!
|
||||
//! Generated with `{{generated_with}}`
|
||||
//! Scenarios from <{{generated_from}}>
|
||||
//!
|
||||
#![cfg(all(feature = "test-python", feature = "test-pypi", unix))]
|
||||
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_fs::fixture::{FileWriteStr, PathChild};
|
||||
use predicates::prelude::predicate;
|
||||
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use uv_test::{
|
||||
TestContext, build_vendor_links_url, get_bin, packse_index_url, python_path_with_versions,
|
||||
uv_snapshot,
|
||||
};
|
||||
|
||||
/// Provision python binaries and return a `pip compile` command with options shared across all scenarios.
|
||||
fn command(context: &TestContext, python_versions: &[&str]) -> Command {
|
||||
let python_path = python_path_with_versions(&context.temp_dir, python_versions)
|
||||
.expect("Failed to create Python test path");
|
||||
let mut command = Command::new(get_bin!());
|
||||
command
|
||||
.arg("pip")
|
||||
.arg("compile")
|
||||
.arg("requirements.in")
|
||||
.arg("--index-url")
|
||||
.arg(packse_index_url())
|
||||
.arg("--find-links")
|
||||
.arg(build_vendor_links_url());
|
||||
context.add_shared_options(&mut command, true);
|
||||
command.env_remove(EnvVars::UV_EXCLUDE_NEWER);
|
||||
command.env(EnvVars::UV_PYTHON_SEARCH_PATH, python_path);
|
||||
|
||||
command
|
||||
}
|
||||
|
||||
{{#scenarios}}
|
||||
|
||||
/// {{description}}
|
||||
///
|
||||
/// ```text
|
||||
/// {{name}}
|
||||
{{#tree}}
|
||||
/// {{.}}
|
||||
{{/tree}}
|
||||
/// ```
|
||||
{{#python_patch}}
|
||||
#[cfg(feature = "test-python-patch")]
|
||||
{{/python_patch}}
|
||||
#[test]
|
||||
fn {{module_name}}() -> Result<()> {
|
||||
let context = uv_test::test_context!("{{environment.python}}");
|
||||
let python_versions = &[{{#environment.additional_python}}"{{.}}", {{/environment.additional_python}}];
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((r"{{name}}-", "package-"));
|
||||
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
{{#root.requires}}
|
||||
requirements_in.write_str("{{requirement}}")?;
|
||||
{{/root.requires}}
|
||||
|
||||
{{#expected.explanation}}
|
||||
// {{expected.explanation}}
|
||||
{{/expected.explanation}}
|
||||
let output = uv_snapshot!(filters, command(&context, python_versions)
|
||||
{{#resolver_options.prereleases}}
|
||||
.arg("--prerelease=allow")
|
||||
{{/resolver_options.prereleases}}
|
||||
{{#resolver_options.no_build}}
|
||||
.arg("--only-binary")
|
||||
.arg("{{.}}")
|
||||
{{/resolver_options.no_build}}
|
||||
{{#resolver_options.no_binary}}
|
||||
.arg("--no-binary")
|
||||
.arg("{{.}}")
|
||||
{{/resolver_options.no_binary}}
|
||||
{{#resolver_options.python}}
|
||||
.arg("--python-version={{.}}")
|
||||
{{/resolver_options.python}}, @r###"<snapshot>
|
||||
"###
|
||||
);
|
||||
|
||||
output
|
||||
.assert()
|
||||
{{#expected.satisfiable}}
|
||||
.success()
|
||||
{{#expected.packages}}
|
||||
.stdout(predicate::str::contains("{{name}}=={{version}}"))
|
||||
{{/expected.packages}}
|
||||
{{/expected.satisfiable}}
|
||||
{{^expected.satisfiable}}
|
||||
.failure()
|
||||
{{/expected.satisfiable}}
|
||||
;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
{{/scenarios}}
|
||||
@@ -1,81 +0,0 @@
|
||||
//! DO NOT EDIT
|
||||
//!
|
||||
//! Generated with `{{generated_with}}`
|
||||
//! Scenarios from <{{generated_from}}>
|
||||
//!
|
||||
#![cfg(all(feature = "test-python", feature = "test-pypi", unix))]
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use uv_test::{TestContext, build_vendor_links_url, packse_index_url, uv_snapshot};
|
||||
|
||||
/// Create a `pip install` command with options shared across all scenarios.
|
||||
fn command(context: &TestContext) -> Command {
|
||||
let mut command = context.pip_install();
|
||||
command
|
||||
.arg("--index-url")
|
||||
.arg(packse_index_url())
|
||||
.arg("--find-links")
|
||||
.arg(build_vendor_links_url());
|
||||
command.env_remove(EnvVars::UV_EXCLUDE_NEWER);
|
||||
command
|
||||
}
|
||||
|
||||
{{#scenarios}}
|
||||
|
||||
/// {{description}}
|
||||
///
|
||||
/// ```text
|
||||
/// {{name}}
|
||||
{{#tree}}
|
||||
/// {{.}}
|
||||
{{/tree}}
|
||||
/// ```
|
||||
{{#python_patch}}
|
||||
#[cfg(feature = "test-python-patch")]
|
||||
{{/python_patch}}
|
||||
#[test]
|
||||
fn {{module_name}}() {
|
||||
let context = uv_test::test_context!("{{environment.python}}");
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((r"{{name}}-", "package-"));
|
||||
|
||||
uv_snapshot!(filters, command(&context)
|
||||
{{#resolver_options.prereleases}}
|
||||
.arg("--prerelease=allow")
|
||||
{{/resolver_options.prereleases}}
|
||||
{{#resolver_options.no_build}}
|
||||
.arg("--only-binary")
|
||||
.arg("{{.}}")
|
||||
{{/resolver_options.no_build}}
|
||||
{{#resolver_options.no_binary}}
|
||||
.arg("--no-binary")
|
||||
.arg("{{.}}")
|
||||
{{/resolver_options.no_binary}}
|
||||
{{#resolver_options.python_platform}}
|
||||
.arg("--python-platform={{.}}")
|
||||
{{/resolver_options.python_platform}}
|
||||
{{#root.requires}}
|
||||
.arg("{{requirement}}")
|
||||
{{/root.requires}}, @r#"<snapshot>
|
||||
"#);
|
||||
|
||||
{{#expected.explanation}}
|
||||
// {{expected.explanation}}
|
||||
{{/expected.explanation}}
|
||||
{{#expected.satisfiable}}
|
||||
{{#expected.packages}}
|
||||
context.assert_installed("{{module_name}}", "{{version}}");
|
||||
{{/expected.packages}}
|
||||
{{/expected.satisfiable}}
|
||||
{{^expected.satisfiable}}
|
||||
{{#root.requires}}
|
||||
context.assert_not_installed("{{module_name}}");
|
||||
{{/root.requires}}
|
||||
{{/expected.satisfiable}}
|
||||
}
|
||||
{{/scenarios}}
|
||||
@@ -1,96 +0,0 @@
|
||||
//! DO NOT EDIT
|
||||
//!
|
||||
//! Generated with `{{generated_with}}`
|
||||
//! Scenarios from <{{generated_from}}>
|
||||
//!
|
||||
#![cfg(all(feature = "test-python", feature = "test-pypi"))]
|
||||
#![expect(clippy::needless_raw_string_hashes)]
|
||||
#![expect(clippy::doc_markdown)]
|
||||
|
||||
use anyhow::Result;
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_fs::prelude::*;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use uv_test::{packse_index_url, uv_snapshot};
|
||||
|
||||
{{#scenarios}}
|
||||
|
||||
/// {{description}}
|
||||
///
|
||||
/// ```text
|
||||
/// {{name}}
|
||||
{{#tree}}
|
||||
/// {{.}}
|
||||
{{/tree}}
|
||||
/// ```
|
||||
#[test]
|
||||
fn {{module_name}}() -> Result<()> {
|
||||
let context = uv_test::test_context!("{{environment.python}}");
|
||||
|
||||
// In addition to the standard filters, swap out package names for shorter messages
|
||||
let mut filters = context.filters();
|
||||
filters.push((r"{{name}}-", "package-"));
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r###"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
{{#root.requires}}
|
||||
'''{{requirement}}''',
|
||||
{{/root.requires}}
|
||||
]
|
||||
{{#root.requires_python}}
|
||||
requires-python = "{{.}}"
|
||||
{{/root.requires_python}}
|
||||
{{#resolver_options.has_required_environments}}
|
||||
[tool.uv]
|
||||
required-environments = [
|
||||
{{#resolver_options.required_environments}}
|
||||
'''{{.}}''',
|
||||
{{/resolver_options.required_environments}}
|
||||
]
|
||||
{{/resolver_options.has_required_environments}}
|
||||
"###
|
||||
)?;
|
||||
|
||||
let mut cmd = context.lock();
|
||||
cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER);
|
||||
cmd.arg("--index-url").arg(packse_index_url());
|
||||
{{#expected.explanation}}
|
||||
// {{expected.explanation}}
|
||||
{{/expected.explanation}}
|
||||
uv_snapshot!(filters, cmd, @r###"<snapshot>
|
||||
"###
|
||||
);
|
||||
|
||||
{{#expected.satisfiable}}
|
||||
let lock = context.read("uv.lock");
|
||||
insta::with_settings!({
|
||||
filters => filters,
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"<snapshot>
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Assert the idempotence of `uv lock` when resolving from the lockfile (`--locked`).
|
||||
context
|
||||
.lock()
|
||||
.arg("--locked")
|
||||
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
|
||||
.arg("--index-url")
|
||||
.arg(packse_index_url())
|
||||
.assert()
|
||||
.success();
|
||||
{{/expected.satisfiable}}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
{{/scenarios}}
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Sync test scenarios with the pinned version of packse.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# Install the pinned packse version in a temporary virtual environment, fetch scenarios, and regenerate test cases and snapshots:
|
||||
#
|
||||
# $ ./scripts/sync_scenarios.sh
|
||||
#
|
||||
# Additional arguments are passed to `./scripts/scenarios/generate.py`, for example:
|
||||
#
|
||||
# $ ./scripts/sync_scenarios.sh --verbose --no-snapshot-update
|
||||
#
|
||||
# For development purposes, the `./scripts/scenarios/generate.py` script can be used directly to generate
|
||||
# test cases from a local set of scenarios.
|
||||
#
|
||||
# To update the packse version, run the following command first:
|
||||
#
|
||||
# $ uv pip compile --group scripts/scenarios/pyproject.toml:packse -o scripts/scenarios/pylock.toml --upgrade-package packse --universal -p 3.12
|
||||
#
|
||||
# See `scripts/scenarios/` for supporting files.
|
||||
set -eu
|
||||
|
||||
script_root="$(realpath "$(dirname "$0")")"
|
||||
|
||||
|
||||
cd "$script_root/scenarios"
|
||||
echo "Setting up a temporary environment..."
|
||||
uv venv -p 3.12 -c
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source ".venv/bin/activate"
|
||||
uv pip install -r "$script_root/scenarios/pylock.toml" --refresh-package packse
|
||||
|
||||
echo "Fetching packse scenarios..."
|
||||
packse fetch --dest "$script_root/scenarios/.downloads" --force
|
||||
|
||||
unset VIRTUAL_ENV # Avoid warning due to venv mismatch
|
||||
.venv/bin/python "$script_root/scenarios/generate.py" "$script_root/scenarios/.downloads" "$@"
|
||||
|
||||
# Cleanup
|
||||
rm -r "$script_root/scenarios/.downloads"
|
||||
@@ -0,0 +1,23 @@
|
||||
name = "backtrack-to-missing-package"
|
||||
description = """There are two packages, `a` and `b`. All versions of `b` require a specific
|
||||
version of `a`, but that version requires a package `c` that does not exist. The resolver
|
||||
must backtrack through all versions of `b` and eventually fail because no solution exists."""
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[root]
|
||||
requires = ["a", "b"]
|
||||
|
||||
[packages.a.versions]
|
||||
# This version works but is incompatible with all versions of b
|
||||
"2.0.0" = {}
|
||||
# This version is required by b but depends on a missing package
|
||||
"1.0.0" = { requires = ["c"] }
|
||||
|
||||
[packages.b.versions]
|
||||
"1.0.0" = { requires = ["a==1.0.0"] }
|
||||
"2.0.0" = { requires = ["a==1.0.0"] }
|
||||
"3.0.0" = { requires = ["a==1.0.0"] }
|
||||
|
||||
# Note: package `c` is intentionally not defined (missing package)
|
||||
@@ -0,0 +1,27 @@
|
||||
name = "backtrack-with-missing-package"
|
||||
description = """There are two packages, `a` and `b`. The latest version of `b` requires
|
||||
a specific version of `a`. The older version of `b` requires a package `c` that does not
|
||||
exist. The resolver should backtrack on `a` (not `b`) to find a solution without needing
|
||||
to try `b==1.0.0` which would fail due to the missing package."""
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "2.0.0"
|
||||
|
||||
[root]
|
||||
requires = ["a", "b"]
|
||||
|
||||
[packages.a.versions]
|
||||
"1.0.0" = {}
|
||||
"2.0.0" = {}
|
||||
|
||||
[packages.b.versions]
|
||||
# Old version requires a missing package - resolver should not need this
|
||||
"1.0.0" = { requires = ["c"] }
|
||||
# New version requires specific a version - should work with backtracking
|
||||
"2.0.0" = { requires = ["a==1.0.0"] }
|
||||
|
||||
# Note: package `c` is intentionally not defined (missing package)
|
||||
@@ -0,0 +1,38 @@
|
||||
name = "wrong-backtracking-basic"
|
||||
description = """There are two packages, `a` and `b`. We select `a` with `a==2.0.0` first, and then `b`, but `a==2.0.0` conflicts with all new versions of `b`, so we backtrack through versions of `b`.
|
||||
|
||||
We need to detect this conflict and prioritize `b` over `a` instead of backtracking down to the too old version of `b==1.0.0` that doesn't depend on `a` anymore."""
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "2.0.9"
|
||||
|
||||
[root]
|
||||
requires = ["a", "b"]
|
||||
|
||||
[packages.a.versions]
|
||||
"1.0.0" = {}
|
||||
"2.0.0" = {}
|
||||
|
||||
[packages.too-old.versions]
|
||||
"1.0.0" = {}
|
||||
|
||||
[packages.b.versions]
|
||||
# We must not backtrack to this very old versions
|
||||
"1.0.0" = { requires = ["too-old"] }
|
||||
"2.0.0" = { requires = ["a==1.0.0"] }
|
||||
"2.0.1" = { requires = ["a==1.0.0"] }
|
||||
"2.0.2" = { requires = ["a==1.0.0"] }
|
||||
"2.0.3" = { requires = ["a==1.0.0"] }
|
||||
"2.0.4" = { requires = ["a==1.0.0"] }
|
||||
"2.0.5" = { requires = ["a==1.0.0"] }
|
||||
"2.0.6" = { requires = ["a==1.0.0"] }
|
||||
"2.0.7" = { requires = ["a==1.0.0"] }
|
||||
"2.0.8" = { requires = ["a==1.0.0"] }
|
||||
"2.0.9" = { requires = ["a==1.0.0"] }
|
||||
@@ -0,0 +1,45 @@
|
||||
name = "wrong-backtracking-indirect"
|
||||
description = """There are three packages, `a`, `b` and `b-inner`. Unlike wrong-backtracking-basic, `b` depends on `b-inner` and `a` and `b-inner` conflict, to add a layer of indirection.
|
||||
|
||||
We select `a` with `a==2.0.0` first, then `b`, and then `b-inner`, but `a==2.0.0` conflicts with all new versions of `b-inner`, so we backtrack through versions of `b-inner`.
|
||||
|
||||
We need to detect this conflict and prioritize `b` and `b-inner` over `a` instead of backtracking down to the too old version of `b-inner==1.0.0` that doesn't depend on `a` anymore."""
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
# TODO: https://github.com/astral-sh/uv/issues/12060
|
||||
# [expected.packages]
|
||||
# a = "1.0.0"
|
||||
# b = "1.0.0"
|
||||
# b-inner = "2.0.9"
|
||||
|
||||
[root]
|
||||
requires = ["a", "b"]
|
||||
|
||||
[packages.a.versions]
|
||||
"1.0.0" = {}
|
||||
"2.0.0" = {}
|
||||
|
||||
[packages.too-old.versions]
|
||||
"1.0.0" = {}
|
||||
|
||||
[packages.b.versions]
|
||||
"1.0.0" = { requires = ["b-inner"] }
|
||||
|
||||
[packages.b-inner.versions]
|
||||
# We must not backtrack to this very old versions
|
||||
"1.0.0" = { requires = ["too-old"] }
|
||||
"2.0.0" = { requires = ["a==1.0.0"] }
|
||||
"2.0.1" = { requires = ["a==1.0.0"] }
|
||||
"2.0.2" = { requires = ["a==1.0.0"] }
|
||||
"2.0.3" = { requires = ["a==1.0.0"] }
|
||||
"2.0.4" = { requires = ["a==1.0.0"] }
|
||||
"2.0.5" = { requires = ["a==1.0.0"] }
|
||||
"2.0.6" = { requires = ["a==1.0.0"] }
|
||||
"2.0.7" = { requires = ["a==1.0.0"] }
|
||||
"2.0.8" = { requires = ["a==1.0.0"] }
|
||||
"2.0.9" = { requires = ["a==1.0.0"] }
|
||||
@@ -0,0 +1,10 @@
|
||||
name = "requires-exact-version-does-not-exist"
|
||||
description = "The user requires an exact version of package `a` but only other versions exist"
|
||||
|
||||
[root]
|
||||
requires = ["a==2.0.0"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
@@ -0,0 +1,12 @@
|
||||
name = "requires-greater-version-does-not-exist"
|
||||
description = "The user requires a version of `a` greater than `1.0.0` but only smaller or equal versions exist"
|
||||
|
||||
[root]
|
||||
requires = ["a>1.0.0"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."0.1.0"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
@@ -0,0 +1,14 @@
|
||||
name = "requires-less-version-does-not-exist"
|
||||
description = "The user requires a version of `a` less than `1.0.0` but only larger versions exist"
|
||||
|
||||
[root]
|
||||
requires = ["a<2.0.0"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.a.versions."3.0.0"]
|
||||
|
||||
[packages.a.versions."4.0.0"]
|
||||
@@ -0,0 +1,10 @@
|
||||
name = "requires-package-does-not-exist"
|
||||
description = "The user requires any version of package `a` which does not exist."
|
||||
|
||||
[root]
|
||||
requires = ["a"]
|
||||
|
||||
[packages]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
@@ -0,0 +1,11 @@
|
||||
name = "transitive-requires-package-does-not-exist"
|
||||
description = "The user requires package `a` but `a` requires package `b` which does not exist"
|
||||
|
||||
[root]
|
||||
requires = ["a"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b"]
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
name = "dependency-excludes-non-contiguous-range-of-compatible-versions"
|
||||
description = "There is a non-contiguous range of compatible versions for the requested package `a`, but another dependency `c` excludes the range. This is the same as `dependency-excludes-range-of-compatible-versions` but some of the versions of `a` are incompatible for another reason e.g. dependency on non-existent package `d`."
|
||||
|
||||
[root]
|
||||
requires = ["a", "b>=2.0.0,<3.0.0", "c"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
explanation = "Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`, but all available versions of `c` exclude that range of `a` so resolution fails."
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b==1.0.0"]
|
||||
|
||||
[packages.a.versions."2.0.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.a.versions."2.1.0"]
|
||||
requires = ["b==2.0.0", "d"]
|
||||
|
||||
[packages.a.versions."2.2.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.a.versions."2.3.0"]
|
||||
requires = ["b==2.0.0", "d"]
|
||||
|
||||
[packages.a.versions."2.4.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.a.versions."3.0.0"]
|
||||
requires = ["b==3.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."3.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
requires = ["a<2.0.0"]
|
||||
|
||||
[packages.c.versions."2.0.0"]
|
||||
requires = ["a>=3.0.0"]
|
||||
@@ -0,0 +1,39 @@
|
||||
name = "dependency-excludes-range-of-compatible-versions"
|
||||
description = "There is a range of compatible versions for the requested package `a`, but another dependency `c` excludes that range."
|
||||
|
||||
[root]
|
||||
requires = ["a", "b>=2.0.0,<3.0.0", "c"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
explanation = "Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`, but all available versions of `c` exclude that range of `a` so resolution fails."
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b==1.0.0"]
|
||||
|
||||
[packages.a.versions."2.0.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.a.versions."2.1.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.a.versions."2.2.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.a.versions."2.3.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.a.versions."3.0.0"]
|
||||
requires = ["b==3.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."3.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
requires = ["a<2.0.0"]
|
||||
|
||||
[packages.c.versions."2.0.0"]
|
||||
requires = ["a>=3.0.0"]
|
||||
@@ -0,0 +1,24 @@
|
||||
name = "excluded-only-compatible-version"
|
||||
description = "Only one version of the requested package `a` is compatible, but the user has banned that version."
|
||||
|
||||
[root]
|
||||
requires = ["a!=2.0.0", "b>=2.0.0,<3.0.0"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
explanation = "Only `a==1.2.0` is available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`. The user has excluded that version of `a` so resolution fails."
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b==1.0.0"]
|
||||
|
||||
[packages.a.versions."2.0.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.a.versions."3.0.0"]
|
||||
requires = ["b==3.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."3.0.0"]
|
||||
@@ -0,0 +1,11 @@
|
||||
name = "excluded-only-version"
|
||||
description = "Only one version of the requested package is available, but the user has banned that version."
|
||||
|
||||
[root]
|
||||
requires = ["a!=1.0.0"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
explanation = "Only `a==1.0.0` is available but the user excluded it."
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "all-extras-required"
|
||||
description = "Multiple optional dependencies are requested for the package via an 'all' extra."
|
||||
|
||||
[root]
|
||||
requires = ["a[all]"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "1.0.0"
|
||||
c = "1.0.0"
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0".extras]
|
||||
all = ["a[extra_b]", "a[extra_c]"]
|
||||
extra_b = ["b"]
|
||||
extra_c = ["c"]
|
||||
@@ -0,0 +1,21 @@
|
||||
name = "extra-does-not-exist-backtrack"
|
||||
description = "Optional dependencies are requested for the package, the extra is only available on an older version."
|
||||
|
||||
[root]
|
||||
requires = ["a[extra]"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "The resolver should not backtrack to `a==1.0.0` because missing extras are allowed during resolution. `b` should not be installed."
|
||||
|
||||
[expected.packages]
|
||||
a = "3.0.0"
|
||||
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.a.versions."3.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0".extras]
|
||||
extra = ["b==1.0.0"]
|
||||
@@ -0,0 +1,21 @@
|
||||
name = "extra-incompatible-with-extra-not-requested"
|
||||
description = "One of two incompatible optional dependencies are requested for the package."
|
||||
|
||||
[root]
|
||||
requires = ["a[extra_c]"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "Because the user does not request both extras, it is okay that one is incompatible with the other."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "2.0.0"
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0".extras]
|
||||
extra_b = ["b==1.0.0"]
|
||||
extra_c = ["b==2.0.0"]
|
||||
@@ -0,0 +1,17 @@
|
||||
name = "extra-incompatible-with-extra"
|
||||
description = "Multiple optional dependencies are requested for the package, but they have conflicting requirements with each other."
|
||||
|
||||
[root]
|
||||
requires = ["a[extra_b,extra_c]"]
|
||||
|
||||
[expected]
|
||||
explanation = "Because both `extra_b` and `extra_c` are requested and they require incompatible versions of `b`, `a` cannot be installed."
|
||||
satisfiable = false
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0".extras]
|
||||
extra_b = ["b==1.0.0"]
|
||||
extra_c = ["b==2.0.0"]
|
||||
@@ -0,0 +1,16 @@
|
||||
name = "extra-incompatible-with-root"
|
||||
description = "Optional dependencies are requested for the package, but the extra is not compatible with other requested versions."
|
||||
|
||||
[root]
|
||||
requires = ["a[extra]", "b==2.0.0"]
|
||||
|
||||
[expected]
|
||||
explanation = "Because the user requested `b==2.0.0` but the requested extra requires `b==1.0.0`, the dependencies cannot be satisfied."
|
||||
satisfiable = false
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0".extras]
|
||||
extra = ["b==1.0.0"]
|
||||
@@ -0,0 +1,17 @@
|
||||
name = "extra-required"
|
||||
description = "Optional dependencies are requested for the package."
|
||||
|
||||
[root]
|
||||
requires = ["a[extra]"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "1.0.0"
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0".extras]
|
||||
extra = ["b"]
|
||||
@@ -0,0 +1,14 @@
|
||||
name = "missing-extra"
|
||||
description = "Optional dependencies are requested for the package, but the extra does not exist."
|
||||
|
||||
[root]
|
||||
requires = ["a[extra]"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "Missing extras are ignored during resolution."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
@@ -0,0 +1,21 @@
|
||||
name = "multiple-extras-required"
|
||||
description = "Multiple optional dependencies are requested for the package."
|
||||
|
||||
[root]
|
||||
requires = ["a[extra_b,extra_c]"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "1.0.0"
|
||||
c = "1.0.0"
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0".extras]
|
||||
extra_b = ["b"]
|
||||
extra_c = ["c"]
|
||||
@@ -0,0 +1,24 @@
|
||||
name = "fork-allows-non-conflicting-non-overlapping-dependencies"
|
||||
description = '''
|
||||
This test ensures that multiple non-conflicting but also
|
||||
non-overlapping dependency specifications with the same package name
|
||||
are allowed and supported.
|
||||
|
||||
At time of writing, this provokes a fork in the resolver, but it
|
||||
arguably shouldn't since the requirements themselves do not conflict
|
||||
with one another. However, this does impact resolution. Namely, it
|
||||
leaves the `a>=1` fork free to choose `a==2.0.0` since it behaves as if
|
||||
the `a<2` constraint doesn't exist.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=1 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
@@ -0,0 +1,26 @@
|
||||
name = "fork-allows-non-conflicting-repeated-dependencies"
|
||||
description = '''
|
||||
This test ensures that multiple non-conflicting dependency
|
||||
specifications with the same package name are allowed and supported.
|
||||
|
||||
This test exists because the universal resolver forks itself based on
|
||||
duplicate dependency specifications by looking at package name. So at
|
||||
first glance, a case like this could perhaps cause an errant fork.
|
||||
While it's difficult to test for "does not create a fork" (at time of
|
||||
writing, the implementation does not fork), we can at least check that
|
||||
this case is handled correctly without issue. Namely, forking should
|
||||
only occur when there are duplicate dependency specifications with
|
||||
disjoint marker expressions.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=1", "a<2"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
@@ -0,0 +1,17 @@
|
||||
name = "fork-basic"
|
||||
description = '''
|
||||
An extremely basic test of universal resolution. In this case, the resolution
|
||||
should contain two distinct versions of `a` depending on `sys_platform`.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
@@ -0,0 +1,26 @@
|
||||
name = "conflict-in-fork"
|
||||
description = '''
|
||||
We have a conflict after forking. This scenario exists to test the error message.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'os1'", "a<2 ; sys_platform == 'os2'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b", "c"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["d==1"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
requires = ["d==2"]
|
||||
|
||||
[packages.d.versions."1.0.0"]
|
||||
[packages.d.versions."2.0.0"]
|
||||
@@ -0,0 +1,23 @@
|
||||
name = "fork-conflict-unsatisfiable"
|
||||
description = '''
|
||||
This test ensures that conflicting dependency specifications lead to an
|
||||
unsatisfiable result.
|
||||
|
||||
In particular, this is a case that should not fork even though there
|
||||
are conflicting requirements because their marker expressions are
|
||||
overlapping. (Well, there aren't any marker expressions here, which
|
||||
means they are both unconditional.)
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[root]
|
||||
requires = ["a>=2", "a<2"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
[packages.a.versions."3.0.0"]
|
||||
@@ -0,0 +1,39 @@
|
||||
name = "fork-filter-sibling-dependencies"
|
||||
description = '''
|
||||
This tests that sibling dependencies of a package that provokes a
|
||||
fork are correctly filtered out of forks where they are otherwise
|
||||
impossible.
|
||||
|
||||
In this case, a previous version of the universal resolver would
|
||||
include both `b` and `c` in *both* of the forks produced by the
|
||||
conflicting dependency specifications on `a`. This in turn led to
|
||||
transitive dependency specifications on both `d==1.0.0` and `d==2.0.0`.
|
||||
Since the universal resolver only forks based on local conditions, this
|
||||
led to a failed resolution.
|
||||
|
||||
The correct thing to do here is to ensure that `b` is only part of the
|
||||
`a==4.4.0` fork and `c` is only par of the `a==4.3.0` fork.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"a==4.4.0 ; sys_platform == 'linux'",
|
||||
"a==4.3.0 ; sys_platform == 'darwin'",
|
||||
"b==1.0.0 ; sys_platform == 'linux'",
|
||||
"c==1.0.0 ; sys_platform == 'darwin'",
|
||||
]
|
||||
|
||||
[packages.a.versions."4.3.0"]
|
||||
[packages.a.versions."4.4.0"]
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["d==1.0.0"]
|
||||
[packages.c.versions."1.0.0"]
|
||||
requires = ["d==2.0.0"]
|
||||
[packages.d.versions."1.0.0"]
|
||||
[packages.d.versions."2.0.0"]
|
||||
@@ -0,0 +1,28 @@
|
||||
name = "fork-upgrade"
|
||||
description = '''
|
||||
This test checks that we discard fork markers when using `--upgrade`.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["foo"]
|
||||
|
||||
[packages.foo.versions."1.0.0"]
|
||||
requires = [
|
||||
# Provoke a fork
|
||||
"bar==1; sys_platform == 'linux'",
|
||||
"bar==2; sys_platform != 'linux'",
|
||||
]
|
||||
[packages.foo.versions."2.0.0"]
|
||||
requires = [
|
||||
# No fork
|
||||
"bar==2",
|
||||
]
|
||||
|
||||
[packages.bar.versions."1.0.0"]
|
||||
[packages.bar.versions."2.0.0"]
|
||||
@@ -0,0 +1,29 @@
|
||||
name = "fork-incomplete-markers"
|
||||
description = '''
|
||||
The root cause the resolver to fork over `a`, but the markers on the variant
|
||||
of `a` don't cover the entire marker space, they are missing Python 3.13.
|
||||
Later, we have a dependency this very hole, which we still need to select,
|
||||
instead of having two forks around but without Python 3.13 and omitting
|
||||
`c` from the solution.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"a==1; python_version < '3.13'",
|
||||
"a==2; python_version >= '3.14'",
|
||||
"b",
|
||||
]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c; python_version == '3.13'"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
@@ -0,0 +1,28 @@
|
||||
name = "fork-marker-accrue"
|
||||
description = '''
|
||||
This is actually a non-forking test case that tests the tracking of marker
|
||||
expressions in general. In this case, the dependency on `c` should have its
|
||||
marker expressions automatically combined. In this case, it's `linux OR
|
||||
darwin`, even though `linux OR darwin` doesn't actually appear verbatim as a
|
||||
marker expression for any dependency on `c`.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"a==1.0.0 ; implementation_name == 'cpython'",
|
||||
"b==1.0.0 ; implementation_name == 'pypy'",
|
||||
]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["c==1.0.0 ; sys_platform == 'linux'"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c==1.0.0 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
@@ -0,0 +1,26 @@
|
||||
name = "fork-marker-disjoint"
|
||||
description = '''
|
||||
A basic test that ensures, at least in this one basic case, that forking in
|
||||
universal resolution happens only when the corresponding marker expressions are
|
||||
completely disjoint. Here, we provide two completely incompatible dependency
|
||||
specifications with equivalent markers. Thus, they are trivially not disjoint,
|
||||
and resolution should fail.
|
||||
|
||||
NOTE: This acts a regression test for the initial version of universal
|
||||
resolution that would fork whenever a package was repeated in the list of
|
||||
dependency specifications. So previously, this would produce a resolution with
|
||||
both `1.0.0` and `2.0.0` of `a`. But of course, the correct behavior is to fail
|
||||
resolving.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'linux'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
@@ -0,0 +1,31 @@
|
||||
name = "fork-marker-inherit-combined-allowed"
|
||||
description = '''
|
||||
This test builds on `fork-marker-inherit-combined`. Namely, we add
|
||||
`or implementation_name == 'pypy'` to the dependency on `c`. While
|
||||
`sys_platform == 'linux'` cannot be true because of the first fork,
|
||||
the second fork which includes `b==1.0.0` happens precisely when
|
||||
`implementation_name == 'pypy'`. So in this case, `c` should be
|
||||
included.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = [
|
||||
"b>=2 ; implementation_name == 'cpython'",
|
||||
"b<2 ; implementation_name == 'pypy'",
|
||||
]
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c ; sys_platform == 'linux' or implementation_name == 'pypy'"]
|
||||
[packages.b.versions."2.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
@@ -0,0 +1,32 @@
|
||||
name = "fork-marker-inherit-combined-disallowed"
|
||||
description = '''
|
||||
This test builds on `fork-marker-inherit-combined`. Namely, we add
|
||||
`or implementation_name == 'cpython'` to the dependency on `c`.
|
||||
While `sys_platform == 'linux'` cannot be true because of the first
|
||||
fork, the second fork which includes `b==1.0.0` happens precisely
|
||||
when `implementation_name == 'pypy'`, which is *also* disjoint with
|
||||
`implementation_name == 'cpython'`. Therefore, `c` should not be
|
||||
included here.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = [
|
||||
"b>=2 ; implementation_name == 'cpython'",
|
||||
"b<2 ; implementation_name == 'pypy'",
|
||||
]
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c ; sys_platform == 'linux' or implementation_name == 'cpython'"]
|
||||
[packages.b.versions."2.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
@@ -0,0 +1,33 @@
|
||||
name = "fork-marker-inherit-combined"
|
||||
description = '''
|
||||
In this test, we check that marker expressions which provoke a fork
|
||||
are carried through to subsequent forks. Here, the `a>=2` and `a<2`
|
||||
dependency specifications create a fork, and then the `a<2` fork leads
|
||||
to `a==1.0.0` with dependency specifications on `b>=2` and `b<2` that
|
||||
provoke yet another fork. Finally, in the `b<2` fork, a dependency on
|
||||
`c` is introduced whose marker expression is disjoint with the marker
|
||||
expression that provoked the *first* fork. Therefore, `c` should be
|
||||
entirely excluded from the resolution.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = [
|
||||
"b>=2 ; implementation_name == 'cpython'",
|
||||
"b<2 ; implementation_name == 'pypy'",
|
||||
]
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c ; sys_platform == 'linux'"]
|
||||
[packages.b.versions."2.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
@@ -0,0 +1,25 @@
|
||||
name = "fork-marker-inherit-isolated"
|
||||
description = '''
|
||||
This is like `fork-marker-inherit`, but where both `a>=2` and `a<2`
|
||||
have a conditional dependency on `b`. For `a>=2`, the conditional
|
||||
dependency on `b` has overlap with the `a>=2` marker expression, and
|
||||
thus, `b` should be included *only* in the dependencies for `a==2.0.0`.
|
||||
As with `fork-marker-inherit`, the `a<2` path should exclude `b==1.0.0`
|
||||
since their marker expressions are disjoint.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b ; sys_platform == 'linux'"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
requires = ["b ; sys_platform == 'linux'"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
@@ -0,0 +1,30 @@
|
||||
name = "fork-marker-inherit-transitive"
|
||||
description = '''
|
||||
This is like `fork-marker-inherit`, but tests that the marker
|
||||
expressions that provoke a fork are carried transitively through the
|
||||
dependency graph. In this case, `a<2 -> b -> c -> d`, but where the
|
||||
last dependency on `d` requires a marker expression that is disjoint
|
||||
with the initial `a<2` dependency. Therefore, it ought to be completely
|
||||
excluded from the resolution.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
requires = ["d ; sys_platform == 'linux'"]
|
||||
|
||||
[packages.d.versions."1.0.0"]
|
||||
@@ -0,0 +1,28 @@
|
||||
name = "fork-marker-inherit"
|
||||
description = '''
|
||||
This tests that markers which provoked a fork in the universal resolver
|
||||
are used to ignore dependencies which cannot possibly be installed by a
|
||||
resolution produced by that fork.
|
||||
|
||||
In this example, the `a<2` dependency is only active on Darwin
|
||||
platforms. But the `a==1.0.0` distribution has a dependency on `b`
|
||||
that is only active on Linux, where as `a==2.0.0` does not. Therefore,
|
||||
when the fork provoked by the `a<2` dependency considers `b`, it should
|
||||
ignore it because it isn't possible for `sys_platform == 'linux'` and
|
||||
`sys_platform == 'darwin'` to be simultaneously true.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a>=2 ; sys_platform == 'linux'", "a<2 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b ; sys_platform == 'linux'"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
@@ -0,0 +1,34 @@
|
||||
name = "fork-marker-limited-inherit"
|
||||
description = '''
|
||||
This is like `fork-marker-inherit`, but it tests that dependency
|
||||
filtering only occurs in the context of a fork.
|
||||
|
||||
For example, as in `fork-marker-inherit`, the `c` dependency of
|
||||
`a<2` should be entirely excluded here since it is possible for
|
||||
`sys_platform` to be simultaneously equivalent to Darwin and Linux.
|
||||
However, the unconditional dependency on `b`, which in turn depends on
|
||||
`c` for Linux only, should still incorporate `c` as the dependency is
|
||||
not part of any fork.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"a>=2 ; sys_platform == 'linux'",
|
||||
"a<2 ; sys_platform == 'darwin'",
|
||||
"b",
|
||||
]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["c ; sys_platform == 'linux'"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c ; sys_platform == 'linux'"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
@@ -0,0 +1,28 @@
|
||||
name = "fork-marker-selection"
|
||||
description = '''
|
||||
This tests a case where the resolver forks because of non-overlapping marker
|
||||
expressions on `b`. In the original universal resolver implementation, this
|
||||
resulted in multiple versions of `a` being unconditionally included in the lock
|
||||
file. So this acts as a regression test to ensure that only one version of `a`
|
||||
is selected.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"a",
|
||||
"b>=2 ; sys_platform == 'linux'",
|
||||
"b<2 ; sys_platform == 'darwin'",
|
||||
]
|
||||
|
||||
[packages.a.versions."0.1.0"]
|
||||
[packages.a.versions."0.2.0"]
|
||||
requires = ["b>=2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
[packages.b.versions."2.0.0"]
|
||||
@@ -0,0 +1,30 @@
|
||||
name = "fork-marker-track"
|
||||
description = '''
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"a",
|
||||
"b>=2.8 ; sys_platform == 'linux'",
|
||||
"b<2.8 ; sys_platform == 'darwin'",
|
||||
]
|
||||
|
||||
[packages.a.versions."1.3.1"]
|
||||
requires = ["c ; implementation_name == 'iron'"]
|
||||
[packages.a.versions."2.0.0"]
|
||||
requires = ["b>=2.8", "c ; implementation_name == 'cpython'"]
|
||||
[packages.a.versions."3.1.0"]
|
||||
requires = ["b>=2.8", "c ; implementation_name == 'pypy'"]
|
||||
[packages.a.versions."4.3.0"]
|
||||
requires = ["b>=2.8"]
|
||||
|
||||
[packages.b.versions."2.7"]
|
||||
[packages.b.versions."2.8"]
|
||||
|
||||
[packages.c.versions."1.10"]
|
||||
@@ -0,0 +1,24 @@
|
||||
name = "fork-non-fork-marker-transitive"
|
||||
description = '''
|
||||
This is the same setup as `non-local-fork-marker-transitive`, but the disjoint
|
||||
dependency specifications on `c` use the same constraints and thus depend on
|
||||
the same version of `c`. In this case, there is no conflict.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["a==1.0.0", "b==1.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["c>=2.0.0 ; sys_platform == 'linux'"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c>=2.0.0 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
[packages.c.versions."2.0.0"]
|
||||
@@ -0,0 +1,28 @@
|
||||
name = "fork-non-local-fork-marker-direct"
|
||||
description = '''
|
||||
This is like `non-local-fork-marker-transitive`, but the marker expressions are
|
||||
placed on sibling dependency specifications. However, the actual dependency on
|
||||
`c` is indirect, and thus, there's no fork detected by the universal resolver.
|
||||
This in turn results in an unresolvable conflict on `c`.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"a==1.0.0 ; sys_platform == 'linux'",
|
||||
"b==1.0.0 ; sys_platform == 'darwin'",
|
||||
]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["c<2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c>=2.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
[packages.c.versions."2.0.0"]
|
||||
@@ -0,0 +1,31 @@
|
||||
name = "fork-non-local-fork-marker-transitive"
|
||||
description = '''
|
||||
This setup introduces dependencies on two distinct versions of `c`, where
|
||||
each such dependency has a marker expression attached that would normally
|
||||
make them disjoint. In a non-universal resolver, this is no problem. But in a
|
||||
forking resolver that tries to create one universal resolution, this can lead
|
||||
to two distinct versions of `c` in the resolution. This is in and of itself
|
||||
not a problem, since that is an expected scenario for universal resolution.
|
||||
The problem in this case is that because the dependency specifications for
|
||||
`c` occur in two different points (i.e., they are not sibling dependency
|
||||
specifications) in the dependency graph, the forking resolver does not "detect"
|
||||
it, and thus never forks and thus this results in "no resolution."
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[root]
|
||||
requires = ["a==1.0.0", "b==1.0.0"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["c<2.0.0 ; sys_platform == 'linux'"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c>=2.0.0 ; sys_platform == 'darwin'"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
[packages.c.versions."2.0.0"]
|
||||
@@ -0,0 +1,48 @@
|
||||
name = "fork-overlapping-markers-basic"
|
||||
description = '''
|
||||
This scenario tests a very basic case of overlapping markers. Namely,
|
||||
it emulates a common pattern in the ecosystem where marker expressions
|
||||
are used to progressively increase the version constraints of a package
|
||||
as the Python version increases.
|
||||
|
||||
In this case, there is actually a split occurring between
|
||||
`python_version < '3.13'` and the other marker expressions, so this
|
||||
isn't just a scenario with overlapping but non-disjoint markers.
|
||||
|
||||
In particular, this serves as a regression test. uv used to create a
|
||||
lock file with a dependency on `a` with the following markers:
|
||||
|
||||
python_version < '3.13' or python_version >= '3.14'
|
||||
|
||||
But this implies that `a` won't be installed for Python 3.13, which is
|
||||
clearly wrong.
|
||||
|
||||
The issue was that uv was intersecting *all* marker expressions. So
|
||||
that `a>=1.1.0` and `a>=1.2.0` fork was getting `python_version >=
|
||||
'3.13' and python_version >= '3.14'`, which, of course, simplifies
|
||||
to `python_version >= '3.14'`. But this is wrong! It should be
|
||||
`python_version >= '3.13' or python_version >= '3.14'`, which of course
|
||||
simplifies to `python_version >= '3.13'`. And thus, the resulting forks
|
||||
are not just disjoint but complete in this case.
|
||||
|
||||
Since there are no other constraints on `a`, this causes uv to select
|
||||
`1.2.0` unconditionally. (The marker expressions get normalized out
|
||||
entirely.)
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"a>=1.0.0 ; python_version < '3.13'",
|
||||
"a>=1.1.0 ; python_version >= '3.13'",
|
||||
"a>=1.2.0 ; python_version >= '3.14'",
|
||||
]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
[packages.a.versions."1.1.0"]
|
||||
[packages.a.versions."1.2.0"]
|
||||
@@ -0,0 +1,77 @@
|
||||
name = "preferences-dependent-forking-bistable"
|
||||
description = '''
|
||||
This test contains a bistable resolution scenario when not using ahead-of-time
|
||||
splitting of resolution forks: We meet one of two fork points depending on the
|
||||
preferences, creating a resolution whose preferences lead us the other fork
|
||||
point.
|
||||
|
||||
In the first case, we are in cleaver 2 and fork on `sys_platform`, in the
|
||||
second case, we are in foo 1 or bar 1 amd fork over `os_name`.
|
||||
|
||||
First case: We select cleaver 2, fork on `sys_platform`, we reject cleaver 2
|
||||
(missing fork `os_name`), we select cleaver 1 and don't fork on `os_name` in
|
||||
`fork-if-not-forked`, done.
|
||||
Second case: We have preference cleaver 1, fork on `os_name` in
|
||||
`fork-if-not-forked`, we reject cleaver 1, we select cleaver 2, we fork on
|
||||
`sys_platform`, we accept cleaver 2 since we forked on `os_name`, done.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["cleaver"]
|
||||
|
||||
[packages.cleaver.versions."2.0.0"]
|
||||
requires = [
|
||||
# Provoke a fork on sys_platform.
|
||||
"fork-sys-platform==1; sys_platform == 'linux'",
|
||||
"fork-sys-platform==2; sys_platform != 'linux'",
|
||||
# Reject cleaver 2 if we didn't fork on os_name, without forking on os_name.
|
||||
"reject-cleaver2==1; os_name == 'posix'",
|
||||
"reject-cleaver2-proxy",
|
||||
]
|
||||
|
||||
[packages.fork-sys-platform.versions."1.0.0"]
|
||||
[packages.fork-sys-platform.versions."2.0.0"]
|
||||
|
||||
[packages.reject-cleaver2-proxy.versions."1.0.0"]
|
||||
requires = ["reject-cleaver2==2; os_name != 'posix'"]
|
||||
|
||||
[packages.reject-cleaver2.versions."1.0.0"]
|
||||
[packages.reject-cleaver2.versions."2.0.0"]
|
||||
|
||||
[packages.cleaver.versions."1.0.0"]
|
||||
requires = [
|
||||
# Provoke a fork on os-name, but only if we didn't fork before.
|
||||
"fork-if-not-forked!=2; sys_platform == 'linux'",
|
||||
"fork-if-not-forked-proxy; sys_platform != 'linux'",
|
||||
# Reject cleaver 1 if we didn't fork on sys_platform, without forking on sys_platform.
|
||||
"reject-cleaver1==1; sys_platform == 'linux'",
|
||||
"reject-cleaver1-proxy",
|
||||
]
|
||||
|
||||
[packages.fork-if-not-forked-proxy.versions."1.0.0"]
|
||||
requires = ["fork-if-not-forked!=3"]
|
||||
|
||||
[packages.reject-cleaver1-proxy.versions."1.0.0"]
|
||||
requires = ["reject-cleaver1==2; sys_platform != 'linux'"]
|
||||
|
||||
[packages.reject-cleaver1.versions."1.0.0"]
|
||||
[packages.reject-cleaver1.versions."2.0.0"]
|
||||
|
||||
[packages.fork-os-name.versions."1.0.0"]
|
||||
[packages.fork-os-name.versions."2.0.0"]
|
||||
|
||||
[packages.fork-if-not-forked.versions."1.0.0"]
|
||||
requires = [
|
||||
# Actually provoke the fork for cleaver 1.
|
||||
"fork-os-name==1; os_name == 'posix'",
|
||||
"fork-os-name==2; os_name != 'posix'",
|
||||
"reject-cleaver1-proxy",
|
||||
]
|
||||
[packages.fork-if-not-forked.versions."2.0.0"]
|
||||
[packages.fork-if-not-forked.versions."3.0.0"]
|
||||
@@ -0,0 +1,79 @@
|
||||
name = "preferences-dependent-forking-conflicting"
|
||||
description = '''
|
||||
Like `preferences-dependent-forking`, but when we don't fork the resolution fails.
|
||||
|
||||
Consider a fresh run without preferences:
|
||||
* We start with cleaver 2
|
||||
* We fork
|
||||
* We reject cleaver 2
|
||||
* We find cleaver solution in fork 1 with foo 2 with bar 1
|
||||
* We find cleaver solution in fork 2 with foo 1 with bar 2
|
||||
* We write cleaver 1, foo 1, foo 2, bar 1 and bar 2 to the lockfile
|
||||
|
||||
In a subsequent run, we read the preference cleaver 1 from the lockfile (the preferences for foo and bar don't matter):
|
||||
* We start with cleaver 1
|
||||
* We're in universal mode, cleaver requires foo 1, bar 1
|
||||
* foo 1 requires bar 2, conflict
|
||||
|
||||
Design sketch:
|
||||
```text
|
||||
root -> clear, foo, bar
|
||||
# Cause a fork, then forget that version.
|
||||
cleaver 2 -> unrelated-dep==1; fork==1
|
||||
cleaver 2 -> unrelated-dep==2; fork==2
|
||||
cleaver 2 -> reject-cleaver-2
|
||||
# Allow different versions when forking, but force foo 1, bar 1 in universal mode without forking.
|
||||
cleaver 1 -> foo==1; fork==1
|
||||
cleaver 1 -> bar==1; fork==2
|
||||
# When we selected foo 1, bar 1 in universal mode for cleaver, this causes a conflict, otherwise we select bar 2.
|
||||
foo 1 -> bar==2
|
||||
```
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"cleaver",
|
||||
# The reporter packages.
|
||||
"foo",
|
||||
"bar",
|
||||
]
|
||||
|
||||
[packages.cleaver.versions."2.0.0"]
|
||||
requires = [
|
||||
# Provoke a fork
|
||||
"unrelated-dep==1; sys_platform == 'linux'",
|
||||
"unrelated-dep==2; sys_platform != 'linux'",
|
||||
"reject-cleaver-2",
|
||||
]
|
||||
|
||||
[packages.reject-cleaver-2.versions."1.0.0"]
|
||||
requires = [
|
||||
# That's a conflict with `cleaver==2`.
|
||||
"unrelated-dep==3",
|
||||
]
|
||||
|
||||
[packages.unrelated-dep.versions."1.0.0"]
|
||||
[packages.unrelated-dep.versions."2.0.0"]
|
||||
[packages.unrelated-dep.versions."3.0.0"]
|
||||
|
||||
[packages.cleaver.versions."1.0.0"]
|
||||
requires = [
|
||||
# Allow different versions when forking, but force foo 1, bar 1 in universal mode without forking.
|
||||
"foo==1; sys_platform == 'linux'",
|
||||
"bar==1; sys_platform != 'linux'",
|
||||
]
|
||||
|
||||
[packages.foo.versions."1.0.0"]
|
||||
requires = [
|
||||
# When we selected foo 1, bar 1 in universal mode for cleaver, this causes a conflict, otherwise we select bar 2.
|
||||
"bar==2",
|
||||
]
|
||||
[packages.foo.versions."2.0.0"]
|
||||
[packages.bar.versions."1.0.0"]
|
||||
[packages.bar.versions."2.0.0"]
|
||||
@@ -0,0 +1,86 @@
|
||||
name = "preferences-dependent-forking-tristable"
|
||||
description = '''
|
||||
This test case is like "preferences-dependent-forking-bistable", but with three
|
||||
states instead of two. The first two locks are in a different state, then we
|
||||
enter the tristable state.
|
||||
|
||||
It's not polished, but it's useful to have something with a higher period
|
||||
than 2 in our test suite.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = ["cleaver", "foo", "bar"]
|
||||
|
||||
[packages.cleaver.versions."2.0.0"]
|
||||
requires = [
|
||||
"unrelated-dep==1; sys_platform == 'linux'",
|
||||
"unrelated-dep==2; sys_platform != 'linux'",
|
||||
"a",
|
||||
"b",
|
||||
]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["unrelated-dep3==1; os_name == 'posix'"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["unrelated-dep3==2; os_name != 'posix'"]
|
||||
|
||||
[packages.reject-cleaver-2.versions."1.0.0"]
|
||||
requires = ["unrelated-dep3==3"]
|
||||
|
||||
[packages.unrelated-dep.versions."1.0.0"]
|
||||
[packages.unrelated-dep.versions."2.0.0"]
|
||||
[packages.unrelated-dep.versions."3.0.0"]
|
||||
|
||||
[packages.unrelated-dep3.versions."1.0.0"]
|
||||
[packages.unrelated-dep3.versions."2.0.0"]
|
||||
[packages.unrelated-dep3.versions."3.0.0"]
|
||||
|
||||
[packages.cleaver.versions."1.0.0"]
|
||||
requires = [
|
||||
"foo==1; sys_platform == 'linux'",
|
||||
"bar==1; sys_platform != 'linux'",
|
||||
]
|
||||
|
||||
[packages.foo.versions."1.0.0"]
|
||||
requires = [
|
||||
"c!=3; sys_platform == 'linux'",
|
||||
"c!=2; sys_platform != 'linux'",
|
||||
"reject-cleaver-1",
|
||||
]
|
||||
[packages.foo.versions."2.0.0"]
|
||||
[packages.bar.versions."1.0.0"]
|
||||
requires = [
|
||||
"c!=3; sys_platform == 'linux'",
|
||||
"d; sys_platform != 'linux'",
|
||||
"reject-cleaver-1",
|
||||
]
|
||||
[packages.bar.versions."2.0.0"]
|
||||
|
||||
[packages.reject-cleaver-1.versions."1.0.0"]
|
||||
requires = [
|
||||
"unrelated-dep2==1; sys_platform == 'linux'",
|
||||
"unrelated-dep2==2; sys_platform != 'linux'",
|
||||
]
|
||||
|
||||
[packages.unrelated-dep2.versions."1.0.0"]
|
||||
[packages.unrelated-dep2.versions."2.0.0"]
|
||||
[packages.unrelated-dep2.versions."3.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
requires = [
|
||||
"unrelated-dep2==1; os_name == 'posix'",
|
||||
"unrelated-dep2==2; os_name != 'posix'",
|
||||
"reject-cleaver-1",
|
||||
]
|
||||
[packages.c.versions."2.0.0"]
|
||||
[packages.c.versions."3.0.0"]
|
||||
|
||||
[packages.d.versions."1.0.0"]
|
||||
requires = ["c!=2"]
|
||||
@@ -0,0 +1,76 @@
|
||||
name = "preferences-dependent-forking"
|
||||
description = '''
|
||||
This test contains a scenario where the solution depends on whether we fork, and whether we fork depends on the
|
||||
preferences.
|
||||
|
||||
Consider a fresh run without preferences:
|
||||
* We start with cleaver 2
|
||||
* We fork
|
||||
* We reject cleaver 2
|
||||
* We find cleaver solution in fork 1 with foo 2 with bar 1
|
||||
* We find cleaver solution in fork 2 with foo 1 with bar 2
|
||||
* We write cleaver 1, foo 1, foo 2, bar 1 and bar 2 to the lockfile
|
||||
|
||||
In a subsequent run, we read the preference cleaver 1 from the lockfile (the preferences for foo and bar don't matter):
|
||||
* We start with cleaver 1
|
||||
* We're in universal mode, we resolve foo 1 and bar 1
|
||||
* We write cleaver 1 and bar 1 to the lockfile
|
||||
|
||||
We call a resolution that's different on the second run to the first unstable.
|
||||
|
||||
Design sketch:
|
||||
```text
|
||||
root -> clear, foo, bar
|
||||
# Cause a fork, then forget that version.
|
||||
cleaver 2 -> unrelated-dep==1; fork==1
|
||||
cleaver 2 -> unrelated-dep==2; fork==2
|
||||
cleaver 2 -> reject-cleaver-2
|
||||
# Allow different versions when forking, but force foo 1, bar 1 in universal mode without forking.
|
||||
cleaver 1 -> foo==1; fork==1
|
||||
cleaver 1 -> bar==1; fork==2
|
||||
```
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"cleaver",
|
||||
# The reporter packages.
|
||||
"foo",
|
||||
"bar",
|
||||
]
|
||||
|
||||
[packages.cleaver.versions."2.0.0"]
|
||||
requires = [
|
||||
# Provoke a fork
|
||||
"unrelated-dep==1; sys_platform == 'linux'",
|
||||
"unrelated-dep==2; sys_platform != 'linux'",
|
||||
"reject-cleaver-2",
|
||||
]
|
||||
|
||||
[packages.reject-cleaver-2.versions."1.0.0"]
|
||||
requires = [
|
||||
# That's a conflict with `cleaver==2`.
|
||||
"unrelated-dep==3",
|
||||
]
|
||||
|
||||
[packages.unrelated-dep.versions."1.0.0"]
|
||||
[packages.unrelated-dep.versions."2.0.0"]
|
||||
[packages.unrelated-dep.versions."3.0.0"]
|
||||
|
||||
[packages.cleaver.versions."1.0.0"]
|
||||
requires = [
|
||||
# Allow different versions when forking, but force foo 1, bar 1 in universal mode without forking.
|
||||
"foo==1; sys_platform == 'linux'",
|
||||
"bar==1; sys_platform != 'linux'",
|
||||
]
|
||||
|
||||
[packages.foo.versions."1.0.0"]
|
||||
[packages.foo.versions."2.0.0"]
|
||||
[packages.bar.versions."1.0.0"]
|
||||
[packages.bar.versions."2.0.0"]
|
||||
@@ -0,0 +1,47 @@
|
||||
name = "fork-remaining-universe-partitioning"
|
||||
description = '''
|
||||
This scenario tries to check that the "remaining universe" handling in
|
||||
the universal resolver is correct. Namely, whenever we create forks
|
||||
from disjoint markers that don't union to the universe, we need to
|
||||
create *another* fork corresponding to the difference between the
|
||||
universe and the union of the forks.
|
||||
|
||||
But when we do this, that remaining universe fork needs to be created
|
||||
like any other fork: it should start copying whatever set of forks
|
||||
existed by the time we got to this point, intersecting the markers with
|
||||
the markers describing the remaining universe and then filtering out
|
||||
any dependencies that are disjoint with the resulting markers.
|
||||
|
||||
This test exercises that logic by ensuring that a package `z` in the
|
||||
remaining universe is excluded based on the combination of markers
|
||||
from a parent fork. That is, if the remaining universe fork does not
|
||||
pick up the markers from the parent forks, then `z` would be included
|
||||
because the remaining universe for _just_ the `b` dependencies of `a`
|
||||
is `os_name != 'linux' and os_name != 'darwin'`, which is satisfied by
|
||||
`z`'s marker of `sys_platform == 'windows'`. However, `a 1.0.0` is only
|
||||
selected in the context of `a < 2 ; sys_platform == 'illumos'`, so `z`
|
||||
should never appear in the resolution.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[root]
|
||||
requires = [
|
||||
"a>=2 ; sys_platform == 'windows'",
|
||||
"a<2 ; sys_platform == 'illumos'",
|
||||
]
|
||||
|
||||
[packages.z.versions."1.0.0"]
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = [
|
||||
"b>=2 ; os_name == 'linux'",
|
||||
"b<2 ; os_name == 'darwin'",
|
||||
"z ; sys_platform == 'windows'",
|
||||
]
|
||||
[packages.a.versions."2.0.0"]
|
||||
[packages.b.versions."1.0.0"]
|
||||
[packages.b.versions."2.0.0"]
|
||||
@@ -0,0 +1,23 @@
|
||||
name = "fork-requires-python-full-prerelease"
|
||||
description = '''
|
||||
This tests that a `Requires-Python` specifier will result in the
|
||||
exclusion of dependency specifications that cannot possibly satisfy it.
|
||||
|
||||
In particular, this is tested via the `python_full_version` marker with
|
||||
a pre-release version.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[environment]
|
||||
python = "3.12"
|
||||
|
||||
[root]
|
||||
requires_python = ">=3.10"
|
||||
requires = ["a==1.0.0 ; python_full_version == '3.9b1'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
@@ -0,0 +1,23 @@
|
||||
name = "fork-requires-python-full"
|
||||
description = '''
|
||||
This tests that a `Requires-Python` specifier will result in the
|
||||
exclusion of dependency specifications that cannot possibly satisfy it.
|
||||
|
||||
In particular, this is tested via the `python_full_version` marker
|
||||
instead of the more common `python_version` marker.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[environment]
|
||||
python = "3.12"
|
||||
|
||||
[root]
|
||||
requires_python = ">=3.10"
|
||||
requires = ["a==1.0.0 ; python_full_version == '3.9'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
@@ -0,0 +1,28 @@
|
||||
name = "fork-requires-python-patch-overlap"
|
||||
description = '''
|
||||
This tests that a `Requires-Python` specifier that includes a Python
|
||||
patch version will not result in excluded a dependency specification
|
||||
with a `python_version == '3.10'` marker.
|
||||
|
||||
This is a regression test for the universal resolver where it would
|
||||
convert a `Requires-Python: >=3.10.1` specifier into a
|
||||
`python_version >= '3.10.1'` marker expression, which would be
|
||||
considered disjoint with `python_version == '3.10'`. Thus, the
|
||||
dependency `a` below was erroneously excluded. It should be included.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[environment]
|
||||
python = "3.12"
|
||||
|
||||
[root]
|
||||
requires_python = ">=3.10.1"
|
||||
requires = ["a==1.0.0 ; python_version == '3.10'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires_python = ">=3.10"
|
||||
@@ -0,0 +1,20 @@
|
||||
name = "fork-requires-python"
|
||||
description = '''
|
||||
This tests that a `Requires-Python` specifier will result in the
|
||||
exclusion of dependency specifications that cannot possibly satisfy it.
|
||||
'''
|
||||
|
||||
[resolver_options]
|
||||
universal = true
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[environment]
|
||||
python = "3.12"
|
||||
|
||||
[root]
|
||||
requires_python = ">=3.10"
|
||||
requires = ["a==1.0.0 ; python_version == '3.9'"]
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
@@ -0,0 +1,12 @@
|
||||
name = "direct-incompatible-versions"
|
||||
description = "The user requires two incompatible, existing versions of package `a`"
|
||||
|
||||
[root]
|
||||
requires = ["a==1.0.0", "a==2.0.0"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
|
||||
[packages.a.versions."2.0.0"]
|
||||
@@ -0,0 +1,11 @@
|
||||
name = "transitive-incompatible-versions"
|
||||
description = "The user requires `a`, which requires two incompatible, existing versions of package `b`"
|
||||
|
||||
[root]
|
||||
requires = ["a==1.0.0"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b==2.0.0", "b==1.0.0"]
|
||||
@@ -0,0 +1,15 @@
|
||||
name = "transitive-incompatible-with-root-version"
|
||||
description = "The user requires packages `a` and `b` but `a` requires a different version of `b`"
|
||||
|
||||
[root]
|
||||
requires = ["a", "b==1.0.0"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0"]
|
||||
@@ -0,0 +1,18 @@
|
||||
name = "transitive-incompatible-with-transitive"
|
||||
description = "The user requires package `a` and `b`; `a` and `b` require different versions of `c`"
|
||||
|
||||
[root]
|
||||
requires = ["a", "b"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["c==1.0.0"]
|
||||
|
||||
[packages.b.versions."1.0.0"]
|
||||
requires = ["c==2.0.0"]
|
||||
|
||||
[packages.c.versions."1.0.0"]
|
||||
|
||||
[packages.c.versions."2.0.0"]
|
||||
@@ -0,0 +1,16 @@
|
||||
name = "local-greater-than-or-equal"
|
||||
description = "A local version should be included in inclusive ordered comparisons."
|
||||
|
||||
[root]
|
||||
requires = ["a>=1.2.3"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "The version '1.2.3+foo' satisfies the constraint '>=1.2.3'."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.2.3+foo"
|
||||
|
||||
[packages.a.versions."1.2.3+bar"]
|
||||
|
||||
[packages.a.versions."1.2.3+foo"]
|
||||
@@ -0,0 +1,10 @@
|
||||
name = "local-greater-than"
|
||||
description = "A local version should be excluded in exclusive ordered comparisons."
|
||||
|
||||
[root]
|
||||
requires = ["a>1.2.3"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.2.3+foo"]
|
||||
@@ -0,0 +1,16 @@
|
||||
name = "local-less-than-or-equal"
|
||||
description = "A local version should be included in inclusive ordered comparisons."
|
||||
|
||||
[root]
|
||||
requires = ["a<=1.2.3"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "The version '1.2.3+foo' satisfies the constraint '<=1.2.3'."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.2.3+foo"
|
||||
|
||||
[packages.a.versions."1.2.3+bar"]
|
||||
|
||||
[packages.a.versions."1.2.3+foo"]
|
||||
@@ -0,0 +1,10 @@
|
||||
name = "local-less-than"
|
||||
description = "A local version should be excluded in exclusive ordered comparisons."
|
||||
|
||||
[root]
|
||||
requires = ["a<1.2.3"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.2.3+foo"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "local-not-latest"
|
||||
description = "Tests that we can select an older version with a local segment when newer versions are incompatible."
|
||||
|
||||
[root]
|
||||
requires = ["a>=1"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
|
||||
[expected.packages]
|
||||
a = "1.2.1+foo"
|
||||
|
||||
[packages.a.versions."1.2.3"]
|
||||
wheel_tags = ["py3-any-macosx_10_0_ppc64"]
|
||||
sdist = false
|
||||
|
||||
[packages.a.versions."1.2.2+foo"]
|
||||
wheel_tags = ["py3-any-macosx_10_0_ppc64"]
|
||||
sdist = false
|
||||
|
||||
[packages.a.versions."1.2.1+foo"]
|
||||
sdist = true
|
||||
@@ -0,0 +1,18 @@
|
||||
name = "local-not-used-with-sdist"
|
||||
description = "If there is a 1.2.3 version with an sdist published and no compatible wheels, then the sdist will be used."
|
||||
|
||||
[root]
|
||||
requires = ["a==1.2.3"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "The version '1.2.3' with an sdist satisfies the constraint '==1.2.3'."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.2.3+foo"
|
||||
|
||||
[packages.a.versions."1.2.3"]
|
||||
wheel_tags = ["py3-any-macosx_10_0_ppc64"]
|
||||
sdist = true
|
||||
|
||||
[packages.a.versions."1.2.3+foo"]
|
||||
@@ -0,0 +1,16 @@
|
||||
name = "local-simple"
|
||||
description = "A simple version constraint should not exclude published versions with local segments."
|
||||
|
||||
[root]
|
||||
requires = ["a==1.2.3"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "The version '1.2.3+foo' satisfies the constraint '==1.2.3'."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.2.3+foo"
|
||||
|
||||
[packages.a.versions."1.2.3+bar"]
|
||||
|
||||
[packages.a.versions."1.2.3+foo"]
|
||||
@@ -0,0 +1,23 @@
|
||||
name = "local-transitive-backtrack"
|
||||
description = "A dependency depends on a conflicting local version of a direct dependency, but we can backtrack to a compatible version."
|
||||
|
||||
[root]
|
||||
requires = ["a", "b==2.0.0+foo"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "Backtracking to '1.0.0' gives us compatible local versions of b."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "2.0.0+foo"
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.a.versions."2.0.0"]
|
||||
requires = ["b==2.0.0+bar"]
|
||||
|
||||
[packages.b.versions."2.0.0+bar"]
|
||||
|
||||
[packages.b.versions."2.0.0+foo"]
|
||||
@@ -0,0 +1,15 @@
|
||||
name = "local-transitive-conflicting"
|
||||
description = "A dependency depends on a conflicting local version of a direct dependency."
|
||||
|
||||
[root]
|
||||
requires = ["a", "b==2.0.0+foo"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b==2.0.0+bar"]
|
||||
|
||||
[packages.b.versions."2.0.0+bar"]
|
||||
|
||||
[packages.b.versions."2.0.0+foo"]
|
||||
@@ -0,0 +1,24 @@
|
||||
name = "local-transitive-confounding"
|
||||
description = "A transitive dependency has both a non-local and local version published, but the non-local version is unusable."
|
||||
|
||||
[root]
|
||||
requires = ["a"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "The version '2.0.0+foo' satisfies the constraint '==2.0.0'."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "2.0.0+foo"
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b==2.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0"]
|
||||
wheel_tags = ["py3-any-macosx_10_0_ppc64"]
|
||||
sdist = false
|
||||
|
||||
[packages.b.versions."2.0.0+bar"]
|
||||
|
||||
[packages.b.versions."2.0.0+foo"]
|
||||
@@ -0,0 +1,20 @@
|
||||
name = "local-transitive-greater-than-or-equal"
|
||||
description = "A transitive constraint on a local version should match an inclusive ordered operator."
|
||||
|
||||
[root]
|
||||
requires = ["a", "b==2.0.0+foo"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "The version '2.0.0+foo' satisfies both >=2.0.0 and ==2.0.0+foo."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "2.0.0+foo"
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b>=2.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0+bar"]
|
||||
|
||||
[packages.b.versions."2.0.0+foo"]
|
||||
@@ -0,0 +1,15 @@
|
||||
name = "local-transitive-greater-than"
|
||||
description = "A transitive constraint on a local version should not match an exclusive ordered operator."
|
||||
|
||||
[root]
|
||||
requires = ["a", "b==2.0.0+foo"]
|
||||
|
||||
[expected]
|
||||
satisfiable = false
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b>2.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0+bar"]
|
||||
|
||||
[packages.b.versions."2.0.0+foo"]
|
||||
@@ -0,0 +1,20 @@
|
||||
name = "local-transitive-less-than-or-equal"
|
||||
description = "A transitive constraint on a local version should match an inclusive ordered operator."
|
||||
|
||||
[root]
|
||||
requires = ["a", "b==2.0.0+foo"]
|
||||
|
||||
[expected]
|
||||
satisfiable = true
|
||||
explanation = "The version '2.0.0+foo' satisfies both <=2.0.0 and ==2.0.0+foo."
|
||||
|
||||
[expected.packages]
|
||||
a = "1.0.0"
|
||||
b = "2.0.0+foo"
|
||||
|
||||
[packages.a.versions."1.0.0"]
|
||||
requires = ["b<=2.0.0"]
|
||||
|
||||
[packages.b.versions."2.0.0+bar"]
|
||||
|
||||
[packages.b.versions."2.0.0+foo"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user