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:
Zanie Blue
2026-06-02 09:25:29 -05:00
committed by GitHub
parent d65588dd2a
commit b4d32e4356
177 changed files with 8347 additions and 3512 deletions
@@ -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
View File
@@ -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]]
+4 -1
View File
@@ -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
+20
View File
@@ -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());
}
}
-5
View File
@@ -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")]
+14 -2
View File
@@ -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 }
+134
View File
@@ -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())
);
}
}
+76
View File
@@ -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();
}
}
}
+7 -34
View File
@@ -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")
+26
View File
@@ -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")
}
+392
View File
@@ -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`"
);
}
}
+304
View File
@@ -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())
);
}
}
+419
View File
@@ -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}"
);
}
}
+336
View File
@@ -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
View File
@@ -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
View File
@@ -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.
"
);
File diff suppressed because it is too large Load Diff
+19 -18
View File
@@ -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]
"
);
+63 -98
View File
@@ -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 -20
View File
@@ -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
+8 -6
View File
@@ -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(())
-362
View File
@@ -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)
-173
View File
@@ -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" } },
]
-5
View File
@@ -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}}
-96
View File
@@ -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}}
-43
View File
@@ -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"]
@@ -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"]
+17
View File
@@ -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"]
+14
View File
@@ -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"]
+17
View File
@@ -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"]
+26
View File
@@ -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"]
+28
View File
@@ -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"]
+28
View File
@@ -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"]
+26
View File
@@ -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"]
+28
View File
@@ -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"]
+28
View File
@@ -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"]
+30
View File
@@ -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"
+20
View File
@@ -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"]
+10
View File
@@ -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"]
+16
View File
@@ -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