Allow users to provide pre-defined metadata for resolution (#7442)

## Summary

This PR enables users to provide pre-defined static metadata for
dependencies. It's intended for situations in which the user depends on
a package that does _not_ declare static metadata (e.g., a
`setup.py`-only sdist), and that is expensive to build or even cannot be
built on some architectures. For example, you might have a Linux-only
dependency that can't be built on ARM -- but we need to build that
package in order to generate the lockfile. By providing static metadata,
the user can instruct uv to avoid building that package at all.

For example, to override all `anyio` versions:

```toml
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio"]

[[tool.uv.dependency-metadata]]
name = "anyio"
requires-dist = ["iniconfig"]
```

Or, to override a specific version:

```toml
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio"]

[[tool.uv.dependency-metadata]]
name = "anyio"
version = "3.7.0"
requires-dist = ["iniconfig"]
```

The current implementation uses `Metadata23` directly, so we adhere to
the exact schema expected internally and defined by the standards. Any
entries are treated similarly to overrides, in that we won't even look
for `anyio@3.7.0` metadata in the above example. (In a way, this also
enables #4422, since you could remove a dependency for a specific
package, though it's probably too unwieldy to use in practice, since
you'd need to redefine the _rest_ of the metadata, and do that for every
package that requires the package you want to omit.)

This is under-documented, since I want to get feedback on the core ideas
and names involved.

Closes https://github.com/astral-sh/uv/issues/7393.
This commit is contained in:
Charlie Marsh
2024-09-17 23:18:05 -04:00
committed by GitHub
parent e5dd67f58e
commit fda227616c
37 changed files with 897 additions and 50 deletions
+8 -6
View File
@@ -83,7 +83,7 @@ mod resolver {
use anyhow::Result;
use distribution_types::{IndexCapabilities, IndexLocations};
use distribution_types::{DependencyMetadata, IndexCapabilities, IndexLocations};
use install_wheel_rs::linker::LinkMode;
use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder};
@@ -139,7 +139,7 @@ mod resolver {
interpreter: &Interpreter,
universal: bool,
) -> Result<ResolutionGraph> {
let build_isolation = BuildIsolation::Isolated;
let build_isolation = BuildIsolation::default();
let build_options = BuildOptions::default();
let concurrency = Concurrency::default();
let config_settings = ConfigSettings::default();
@@ -150,17 +150,18 @@ mod resolver {
.timestamp()
.into(),
);
let build_constraints = Constraints::default();
let capabilities = IndexCapabilities::default();
let flat_index = FlatIndex::default();
let git = GitResolver::default();
let capabilities = IndexCapabilities::default();
let hashes = HashStrategy::None;
let hashes = HashStrategy::default();
let in_flight = InFlight::default();
let index = InMemoryIndex::default();
let index_locations = IndexLocations::default();
let installed_packages = EmptyInstalledPackages;
let sources = SourceStrategy::default();
let options = OptionsBuilder::new().exclude_newer(exclude_newer).build();
let build_constraints = Constraints::default();
let sources = SourceStrategy::default();
let dependency_metadata = DependencyMetadata::default();
let python_requirement = if universal {
PythonRequirement::from_requires_python(
@@ -178,6 +179,7 @@ mod resolver {
interpreter,
&index_locations,
&flat_index,
&dependency_metadata,
&index,
&git,
&capabilities,
@@ -0,0 +1,76 @@
use pep440_rs::{Version, VersionSpecifiers};
use pep508_rs::Requirement;
use pypi_types::{Metadata23, VerbatimParsedUrl};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use uv_normalize::{ExtraName, PackageName};
/// Pre-defined [`StaticMetadata`] entries, indexed by [`PackageName`] and [`Version`].
#[derive(Debug, Clone, Default)]
pub struct DependencyMetadata(FxHashMap<PackageName, Vec<StaticMetadata>>);
impl DependencyMetadata {
/// Index a set of [`StaticMetadata`] entries by [`PackageName`] and [`Version`].
pub fn from_entries(entries: impl IntoIterator<Item = StaticMetadata>) -> Self {
let mut map = Self::default();
for entry in entries {
map.0.entry(entry.name.clone()).or_default().push(entry);
}
map
}
/// Retrieve a [`StaticMetadata`] entry by [`PackageName`] and [`Version`].
pub fn get(&self, package: &PackageName, version: &Version) -> Option<Metadata23> {
let versions = self.0.get(package)?;
// Search for an exact, then a global match.
let metadata = versions
.iter()
.find(|v| v.version.as_ref() == Some(version))
.or_else(|| versions.iter().find(|v| v.version.is_none()))?;
Some(Metadata23 {
name: metadata.name.clone(),
version: version.clone(),
requires_dist: metadata.requires_dist.clone(),
requires_python: metadata.requires_python.clone(),
provides_extras: metadata.provides_extras.clone(),
})
}
/// Retrieve all [`StaticMetadata`] entries.
pub fn values(&self) -> impl Iterator<Item = &StaticMetadata> {
self.0.values().flatten()
}
}
/// A subset of the Python Package Metadata 2.3 standard as specified in
/// <https://packaging.python.org/specifications/core-metadata/>.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct StaticMetadata {
// Mandatory fields
pub name: PackageName,
#[cfg_attr(
feature = "schemars",
schemars(
with = "String",
description = "PEP 440-style package version, e.g., `1.2.3`"
)
)]
pub version: Option<Version>,
// Optional fields
#[serde(default)]
pub requires_dist: Vec<Requirement<VerbatimParsedUrl>>,
#[cfg_attr(
feature = "schemars",
schemars(
with = "Option<String>",
description = "PEP 508-style Python requirement, e.g., `>=3.10`"
)
)]
pub requires_python: Option<VersionSpecifiers>,
#[serde(default)]
pub provides_extras: Vec<ExtraName>,
}
+2
View File
@@ -51,6 +51,7 @@ pub use crate::annotation::*;
pub use crate::any::*;
pub use crate::buildable::*;
pub use crate::cached::*;
pub use crate::dependency_metadata::*;
pub use crate::diagnostic::*;
pub use crate::error::*;
pub use crate::file::*;
@@ -68,6 +69,7 @@ mod annotation;
mod any;
mod buildable;
mod cached;
mod dependency_metadata;
mod diagnostic;
mod error;
mod file;
+5 -3
View File
@@ -135,7 +135,7 @@ create_exception!(
);
/// A PEP 508 dependency specifier.
#[derive(Hash, Debug, Clone, Eq, PartialEq)]
#[derive(Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Requirement<T: Pep508Url = VerbatimUrl> {
/// The distribution name such as `requests` in
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`.
@@ -480,7 +480,9 @@ impl<T: Pep508Url> schemars::JsonSchema for Requirement<T> {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
metadata: Some(Box::new(schemars::schema::Metadata {
description: Some("A PEP 508 dependency specifier".to_string()),
description: Some(
"A PEP 508 dependency specifier, e.g., `ruff >= 0.6.0`".to_string(),
),
..schemars::schema::Metadata::default()
})),
..schemars::schema::SchemaObject::default()
@@ -535,7 +537,7 @@ impl Extras {
}
/// The actual version specifier or URL to install.
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum VersionOrUrl<T: Pep508Url = VerbatimUrl> {
/// A PEP 440 version specifier set
VersionSpecifier(VersionSpecifiers),
+2
View File
@@ -271,6 +271,7 @@ pub fn resolver_options(
} else {
prerelease
},
dependency_metadata: None,
config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
no_build_isolation: flag(no_build_isolation, build_isolation),
@@ -364,6 +365,7 @@ pub fn resolver_installer_options(
} else {
prerelease
},
dependency_metadata: None,
config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
no_build_isolation: flag(no_build_isolation, build_isolation),
+9 -1
View File
@@ -12,7 +12,8 @@ use rustc_hash::FxHashMap;
use tracing::{debug, instrument};
use distribution_types::{
CachedDist, IndexCapabilities, IndexLocations, Name, Resolution, SourceDist, VersionOrUrlRef,
CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, Name, Resolution,
SourceDist, VersionOrUrlRef,
};
use pypi_types::Requirement;
use uv_build::{SourceBuild, SourceBuildContext};
@@ -45,6 +46,7 @@ pub struct BuildDispatch<'a> {
index: &'a InMemoryIndex,
git: &'a GitResolver,
capabilities: &'a IndexCapabilities,
dependency_metadata: &'a DependencyMetadata,
in_flight: &'a InFlight,
build_isolation: BuildIsolation<'a>,
link_mode: install_wheel_rs::linker::LinkMode,
@@ -66,6 +68,7 @@ impl<'a> BuildDispatch<'a> {
interpreter: &'a Interpreter,
index_locations: &'a IndexLocations,
flat_index: &'a FlatIndex,
dependency_metadata: &'a DependencyMetadata,
index: &'a InMemoryIndex,
git: &'a GitResolver,
capabilities: &'a IndexCapabilities,
@@ -90,6 +93,7 @@ impl<'a> BuildDispatch<'a> {
index,
git,
capabilities,
dependency_metadata,
in_flight,
index_strategy,
config_settings,
@@ -136,6 +140,10 @@ impl<'a> BuildContext for BuildDispatch<'a> {
self.capabilities
}
fn dependency_metadata(&self) -> &DependencyMetadata {
self.dependency_metadata
}
fn build_options(&self) -> &BuildOptions {
self.build_options
}
@@ -360,6 +360,15 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
dist: &BuiltDist,
hashes: HashPolicy<'_>,
) -> Result<ArchiveMetadata, Error> {
// If the metadata was provided by the user directly, prefer it.
if let Some(metadata) = self
.build_context
.dependency_metadata()
.get(dist.name(), dist.version())
{
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
}
// If hash generation is enabled, and the distribution isn't hosted on an index, get the
// entire wheel to ensure that the hashes are included in the response. If the distribution
// is hosted on an index, the hashes will be included in the simple metadata response.
@@ -415,6 +424,19 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
source: &BuildableSource<'_>,
hashes: HashPolicy<'_>,
) -> Result<ArchiveMetadata, Error> {
// If the metadata was provided by the user directly, prefer it.
if let Some(dist) = source.as_dist() {
if let Some(version) = dist.version() {
if let Some(metadata) = self
.build_context
.dependency_metadata()
.get(dist.name(), version)
{
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
}
}
}
// Optimization: Skip source dist download when we must not build them anyway.
if self
.build_context
+59 -4
View File
@@ -17,11 +17,11 @@ use url::Url;
use cache_key::RepositoryUrl;
use distribution_filename::{DistExtension, ExtensionError, SourceDistExtension, WheelFilename};
use distribution_types::{
BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist,
DistributionMetadata, FileLocation, FlatIndexLocation, GitSourceDist, HashPolicy,
BuiltDist, DependencyMetadata, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
Dist, DistributionMetadata, FileLocation, FlatIndexLocation, GitSourceDist, HashPolicy,
IndexLocations, IndexUrl, Name, PathBuiltDist, PathSourceDist, RegistryBuiltDist,
RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, ToUrlError,
UrlString,
RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, StaticMetadata,
ToUrlError, UrlString,
};
use pep440_rs::Version;
use pep508_rs::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError};
@@ -795,6 +795,40 @@ impl Lock {
manifest_table.insert("overrides", value(overrides));
}
if !self.manifest.dependency_metadata.is_empty() {
let mut tables = ArrayOfTables::new();
for metadata in &self.manifest.dependency_metadata {
let mut table = Table::new();
table.insert("name", value(metadata.name.to_string()));
if let Some(version) = metadata.version.as_ref() {
table.insert("version", value(version.to_string()));
}
if !metadata.requires_dist.is_empty() {
table.insert(
"requires-dist",
value(serde::Serialize::serialize(
&metadata.requires_dist,
toml_edit::ser::ValueSerializer::new(),
)?),
);
}
if let Some(requires_python) = metadata.requires_python.as_ref() {
table.insert("requires-python", value(requires_python.to_string()));
}
if !metadata.provides_extras.is_empty() {
table.insert(
"provides-extras",
value(serde::Serialize::serialize(
&metadata.provides_extras,
toml_edit::ser::ValueSerializer::new(),
)?),
);
}
tables.push(table);
}
manifest_table.insert("dependency-metadata", Item::ArrayOfTables(tables));
}
if !manifest_table.is_empty() {
doc.insert("manifest", Item::Table(manifest_table));
}
@@ -881,6 +915,7 @@ impl Lock {
requirements: &[Requirement],
constraints: &[Requirement],
overrides: &[Requirement],
dependency_metadata: &DependencyMetadata,
indexes: Option<&IndexLocations>,
build_options: &BuildOptions,
tags: &Tags,
@@ -995,6 +1030,18 @@ impl Lock {
}
}
// Validate that the lockfile was generated with the same static metadata.
{
let expected = dependency_metadata
.values()
.cloned()
.collect::<BTreeSet<_>>();
let actual = &self.manifest.dependency_metadata;
if expected != *actual {
return Ok(SatisfiesResult::MismatchedStaticMetadata(expected, actual));
}
}
// Collect the set of available indexes (both `--index-url` and `--find-links` entries).
let remotes = indexes.map(|locations| {
locations
@@ -1249,6 +1296,8 @@ pub enum SatisfiesResult<'lock> {
MismatchedConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
/// The lockfile uses a different set of overrides.
MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
/// The lockfile uses different static metadata.
MismatchedStaticMetadata(BTreeSet<StaticMetadata>, &'lock BTreeSet<StaticMetadata>),
/// The lockfile is missing a workspace member.
MissingRoot(PackageName),
/// The lockfile referenced a remote index that was not provided
@@ -1302,6 +1351,9 @@ pub struct ResolverManifest {
/// The overrides provided to the resolver.
#[serde(default)]
overrides: BTreeSet<Requirement>,
/// The static metadata provided to the resolver.
#[serde(default)]
dependency_metadata: BTreeSet<StaticMetadata>,
}
impl ResolverManifest {
@@ -1312,12 +1364,14 @@ impl ResolverManifest {
requirements: impl IntoIterator<Item = Requirement>,
constraints: impl IntoIterator<Item = Requirement>,
overrides: impl IntoIterator<Item = Requirement>,
dependency_metadata: impl IntoIterator<Item = StaticMetadata>,
) -> Self {
Self {
members: members.into_iter().collect(),
requirements: requirements.into_iter().collect(),
constraints: constraints.into_iter().collect(),
overrides: overrides.into_iter().collect(),
dependency_metadata: dependency_metadata.into_iter().collect(),
}
}
@@ -1340,6 +1394,7 @@ impl ResolverManifest {
.into_iter()
.map(|requirement| requirement.relative_to(workspace.install_path()))
.collect::<Result<BTreeSet<_>, _>>()?,
dependency_metadata: self.dependency_metadata,
})
}
}
@@ -105,6 +105,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
@@ -112,6 +112,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
@@ -104,6 +104,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
@@ -176,6 +176,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
@@ -176,6 +176,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
@@ -176,6 +176,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
@@ -85,6 +85,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
@@ -81,6 +81,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
@@ -71,6 +71,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
@@ -71,6 +71,7 @@ Ok(
requirements: {},
constraints: {},
overrides: {},
dependency_metadata: {},
},
},
)
+52 -1
View File
@@ -2,7 +2,7 @@ use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf};
use serde::{Deserialize, Serialize};
use distribution_types::{FlatIndexLocation, IndexUrl};
use distribution_types::{FlatIndexLocation, IndexUrl, StaticMetadata};
use install_wheel_rs::linker::LinkMode;
use pep508_rs::Requirement;
use pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
@@ -288,6 +288,7 @@ pub struct ResolverOptions {
pub allow_insecure_host: Option<Vec<TrustedHost>>,
pub resolution: Option<ResolutionMode>,
pub prerelease: Option<PrereleaseMode>,
pub dependency_metadata: Option<Vec<StaticMetadata>>,
pub config_settings: Option<ConfigSettings>,
pub exclude_newer: Option<ExcludeNewer>,
pub link_mode: Option<LinkMode>,
@@ -440,6 +441,29 @@ pub struct ResolverInstallerOptions {
possible_values = true
)]
pub prerelease: Option<PrereleaseMode>,
/// Pre-defined static metadata for dependencies of the project (direct or transitive). When
/// provided, enables the resolver to use the specified metadata instead of querying the
/// registry or building the relevant package from source.
///
/// Metadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/)
/// standard, though only the following fields are respected:
///
/// - `name`: The name of the package.
/// - (Optional) `version`: The version of the package. If omitted, the metadata will be applied
/// to all versions of the package.
/// - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`).
/// - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`).
/// - (Optional) `provides-extras`: The extras provided by the package.
#[option(
default = r#"[]"#,
value_type = "list[dict]",
example = r#"
dependency-metadata = [
{ name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" },
]
"#
)]
pub dependency_metadata: Option<Vec<StaticMetadata>>,
/// Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend,
/// specified as `KEY=VALUE` pairs.
#[option(
@@ -949,6 +973,29 @@ pub struct PipOptions {
possible_values = true
)]
pub prerelease: Option<PrereleaseMode>,
/// Pre-defined static metadata for dependencies of the project (direct or transitive). When
/// provided, enables the resolver to use the specified metadata instead of querying the
/// registry or building the relevant package from source.
///
/// Metadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/)
/// standard, though only the following fields are respected:
///
/// - `name`: The name of the package.
/// - (Optional) `version`: The version of the package. If omitted, the metadata will be applied
/// to all versions of the package.
/// - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`).
/// - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`).
/// - (Optional) `provides-extras`: The extras provided by the package.
#[option(
default = r#"[]"#,
value_type = "list[dict]",
example = r#"
dependency-metadata = [
{ name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" },
]
"#
)]
pub dependency_metadata: Option<Vec<StaticMetadata>>,
/// Write the requirements generated by `uv pip compile` to the given `requirements.txt` file.
///
/// If the file already exists, the existing versions will be preferred when resolving
@@ -1290,6 +1337,7 @@ impl From<ResolverInstallerOptions> for ResolverOptions {
allow_insecure_host: value.allow_insecure_host,
resolution: value.resolution,
prerelease: value.prerelease,
dependency_metadata: value.dependency_metadata,
config_settings: value.config_settings,
exclude_newer: value.exclude_newer,
link_mode: value.link_mode,
@@ -1351,6 +1399,7 @@ pub struct ToolOptions {
pub allow_insecure_host: Option<Vec<TrustedHost>>,
pub resolution: Option<ResolutionMode>,
pub prerelease: Option<PrereleaseMode>,
pub dependency_metadata: Option<Vec<StaticMetadata>>,
pub config_settings: Option<ConfigSettings>,
pub no_build_isolation: Option<bool>,
pub no_build_isolation_package: Option<Vec<PackageName>>,
@@ -1376,6 +1425,7 @@ impl From<ResolverInstallerOptions> for ToolOptions {
allow_insecure_host: value.allow_insecure_host,
resolution: value.resolution,
prerelease: value.prerelease,
dependency_metadata: value.dependency_metadata,
config_settings: value.config_settings,
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
@@ -1403,6 +1453,7 @@ impl From<ToolOptions> for ResolverInstallerOptions {
allow_insecure_host: value.allow_insecure_host,
resolution: value.resolution,
prerelease: value.prerelease,
dependency_metadata: value.dependency_metadata,
config_settings: value.config_settings,
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
+5 -1
View File
@@ -4,7 +4,8 @@ use std::path::{Path, PathBuf};
use anyhow::Result;
use distribution_types::{
CachedDist, IndexCapabilities, IndexLocations, InstalledDist, Resolution, SourceDist,
CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, InstalledDist, Resolution,
SourceDist,
};
use pep508_rs::PackageName;
use pypi_types::Requirement;
@@ -62,6 +63,9 @@ pub trait BuildContext {
/// Return a reference to the discovered registry capabilities.
fn capabilities(&self) -> &IndexCapabilities;
/// Return a reference to any pre-defined static metadata.
fn dependency_metadata(&self) -> &DependencyMetadata;
/// Whether source distribution building or pre-built wheels is disabled.
///
/// This [`BuildContext::setup_build`] calls will fail if builds are disabled.
+2
View File
@@ -115,6 +115,7 @@ async fn build_impl(
allow_insecure_host,
resolution: _,
prerelease: _,
dependency_metadata,
config_setting,
no_build_isolation,
no_build_isolation_package,
@@ -299,6 +300,7 @@ async fn build_impl(
&interpreter,
index_locations,
&flat_index,
dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
+3 -1
View File
@@ -8,7 +8,7 @@ use owo_colors::OwoColorize;
use tracing::debug;
use distribution_types::{
IndexCapabilities, IndexLocations, NameRequirementSpecification,
DependencyMetadata, IndexCapabilities, IndexLocations, NameRequirementSpecification,
UnresolvedRequirementSpecification, Verbatim,
};
use install_wheel_rs::linker::LinkMode;
@@ -75,6 +75,7 @@ pub(crate) async fn pip_compile(
include_index_annotation: bool,
index_locations: IndexLocations,
index_strategy: IndexStrategy,
dependency_metadata: DependencyMetadata,
keyring_provider: KeyringProviderType,
allow_insecure_host: Vec<TrustedHost>,
config_settings: ConfigSettings,
@@ -335,6 +336,7 @@ pub(crate) async fn pip_compile(
&interpreter,
&index_locations,
&flat_index,
&dependency_metadata,
&source_index,
&git,
&capabilities,
+4 -1
View File
@@ -6,7 +6,8 @@ use owo_colors::OwoColorize;
use tracing::{debug, enabled, Level};
use distribution_types::{
IndexLocations, NameRequirementSpecification, Resolution, UnresolvedRequirementSpecification,
DependencyMetadata, IndexLocations, NameRequirementSpecification, Resolution,
UnresolvedRequirementSpecification,
};
use install_wheel_rs::linker::LinkMode;
use pep508_rs::PackageName;
@@ -54,6 +55,7 @@ pub(crate) async fn pip_install(
upgrade: Upgrade,
index_locations: IndexLocations,
index_strategy: IndexStrategy,
dependency_metadata: DependencyMetadata,
keyring_provider: KeyringProviderType,
allow_insecure_host: Vec<TrustedHost>,
reinstall: Reinstall,
@@ -337,6 +339,7 @@ pub(crate) async fn pip_install(
interpreter,
&index_locations,
&flat_index,
&dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
+3 -1
View File
@@ -5,7 +5,7 @@ use anyhow::Result;
use owo_colors::OwoColorize;
use tracing::debug;
use distribution_types::{IndexLocations, Resolution};
use distribution_types::{DependencyMetadata, IndexLocations, Resolution};
use install_wheel_rs::linker::LinkMode;
use pep508_rs::PackageName;
use uv_auth::store_credentials_from_url;
@@ -47,6 +47,7 @@ pub(crate) async fn pip_sync(
hash_checking: Option<HashCheckingMode>,
index_locations: IndexLocations,
index_strategy: IndexStrategy,
dependency_metadata: DependencyMetadata,
keyring_provider: KeyringProviderType,
allow_insecure_host: Vec<TrustedHost>,
allow_empty_requirements: bool,
@@ -287,6 +288,7 @@ pub(crate) async fn pip_sync(
interpreter,
&index_locations,
&flat_index,
&dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
+1
View File
@@ -300,6 +300,7 @@ pub(crate) async fn add(
target.interpreter(),
&settings.index_locations,
&flat_index,
&settings.dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
+22 -3
View File
@@ -9,7 +9,8 @@ use rustc_hash::{FxBuildHasher, FxHashMap};
use tracing::debug;
use distribution_types::{
IndexLocations, NameRequirementSpecification, UnresolvedRequirementSpecification,
DependencyMetadata, IndexLocations, NameRequirementSpecification,
UnresolvedRequirementSpecification,
};
use pep440_rs::Version;
use pypi_types::{Requirement, SupportedEnvironments};
@@ -241,6 +242,7 @@ async fn do_lock(
allow_insecure_host,
resolution,
prerelease,
dependency_metadata,
config_setting,
no_build_isolation,
no_build_isolation_package,
@@ -409,6 +411,7 @@ async fn do_lock(
interpreter,
index_locations,
&flat_index,
dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
@@ -436,6 +439,7 @@ async fn do_lock(
&constraints,
&overrides,
environments,
dependency_metadata,
interpreter,
&requires_python,
index_locations,
@@ -552,8 +556,14 @@ async fn do_lock(
// Notify the user of any resolution diagnostics.
pip::operations::diagnose_resolution(resolution.diagnostics(), printer)?;
let manifest = ResolverManifest::new(members, requirements, constraints, overrides)
.relative_to(workspace)?;
let manifest = ResolverManifest::new(
members,
requirements,
constraints,
overrides,
dependency_metadata.values().cloned(),
)
.relative_to(workspace)?;
let previous = existing_lock.map(ValidatedLock::into_lock);
let lock = Lock::from_resolution_graph(&resolution, workspace.install_path())?
@@ -591,6 +601,7 @@ impl ValidatedLock {
constraints: &[Requirement],
overrides: &[Requirement],
environments: Option<&SupportedEnvironments>,
dependency_metadata: &DependencyMetadata,
interpreter: &Interpreter,
requires_python: &RequiresPython,
index_locations: &IndexLocations,
@@ -710,6 +721,7 @@ impl ValidatedLock {
requirements,
constraints,
overrides,
dependency_metadata,
indexes,
build_options,
interpreter.tags()?,
@@ -773,6 +785,13 @@ impl ValidatedLock {
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedStaticMetadata(expected, actual) => {
debug!(
"Ignoring existing lockfile due to mismatched static metadata:\n Expected: {:?}\n Actual: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingRoot(name) => {
debug!("Ignoring existing lockfile due to missing root package: `{name}`");
Ok(Self::Preferable(lock))
+8
View File
@@ -575,6 +575,7 @@ pub(crate) async fn resolve_names(
allow_insecure_host,
resolution: _,
prerelease: _,
dependency_metadata,
config_setting,
no_build_isolation,
no_build_isolation_package,
@@ -631,6 +632,7 @@ pub(crate) async fn resolve_names(
interpreter,
index_locations,
&flat_index,
dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
@@ -684,6 +686,7 @@ pub(crate) async fn resolve_environment<'a>(
allow_insecure_host,
resolution,
prerelease,
dependency_metadata,
config_setting,
no_build_isolation,
no_build_isolation_package,
@@ -774,6 +777,7 @@ pub(crate) async fn resolve_environment<'a>(
interpreter,
index_locations,
&flat_index,
dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
@@ -837,6 +841,7 @@ pub(crate) async fn sync_environment(
index_strategy,
keyring_provider,
allow_insecure_host,
dependency_metadata,
config_setting,
no_build_isolation,
no_build_isolation_package,
@@ -903,6 +908,7 @@ pub(crate) async fn sync_environment(
interpreter,
index_locations,
&flat_index,
dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
@@ -989,6 +995,7 @@ pub(crate) async fn update_environment(
allow_insecure_host,
resolution,
prerelease,
dependency_metadata,
config_setting,
no_build_isolation,
no_build_isolation_package,
@@ -1104,6 +1111,7 @@ pub(crate) async fn update_environment(
interpreter,
index_locations,
&flat_index,
dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
+2
View File
@@ -180,6 +180,7 @@ pub(super) async fn do_sync(
index_strategy,
keyring_provider,
allow_insecure_host,
dependency_metadata,
config_setting,
no_build_isolation,
no_build_isolation_package,
@@ -304,6 +305,7 @@ pub(super) async fn do_sync(
venv.interpreter(),
index_locations,
&flat_index,
dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
+5 -1
View File
@@ -9,7 +9,7 @@ use miette::{Diagnostic, IntoDiagnostic};
use owo_colors::OwoColorize;
use thiserror::Error;
use distribution_types::IndexLocations;
use distribution_types::{DependencyMetadata, IndexLocations};
use install_wheel_rs::linker::LinkMode;
use pypi_types::Requirement;
use uv_auth::store_credentials_from_url;
@@ -48,6 +48,7 @@ pub(crate) async fn venv(
link_mode: LinkMode,
index_locations: &IndexLocations,
index_strategy: IndexStrategy,
dependency_metadata: DependencyMetadata,
keyring_provider: KeyringProviderType,
allow_insecure_host: Vec<TrustedHost>,
prompt: uv_virtualenv::Prompt,
@@ -70,6 +71,7 @@ pub(crate) async fn venv(
link_mode,
index_locations,
index_strategy,
dependency_metadata,
keyring_provider,
allow_insecure_host,
prompt,
@@ -125,6 +127,7 @@ async fn venv_impl(
link_mode: LinkMode,
index_locations: &IndexLocations,
index_strategy: IndexStrategy,
dependency_metadata: DependencyMetadata,
keyring_provider: KeyringProviderType,
allow_insecure_host: Vec<TrustedHost>,
prompt: uv_virtualenv::Prompt,
@@ -317,6 +320,7 @@ async fn venv_impl(
interpreter,
index_locations,
&flat_index,
&dependency_metadata,
&state.index,
&state.git,
&state.capabilities,
+6 -3
View File
@@ -323,6 +323,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.settings.emit_index_annotation,
args.settings.index_locations,
args.settings.index_strategy,
args.settings.dependency_metadata,
args.settings.keyring_provider,
args.settings.allow_insecure_host,
args.settings.config_setting,
@@ -390,6 +391,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.settings.hash_checking,
args.settings.index_locations,
args.settings.index_strategy,
args.settings.dependency_metadata,
args.settings.keyring_provider,
args.settings.allow_insecure_host,
args.settings.allow_empty_requirements,
@@ -453,7 +455,6 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
.into_iter()
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();
let build_constraints = args
.build_constraint
.into_iter()
@@ -474,6 +475,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.settings.upgrade,
args.settings.index_locations,
args.settings.index_strategy,
args.settings.dependency_metadata,
args.settings.keyring_provider,
args.settings.allow_insecure_host,
args.settings.reinstall,
@@ -733,6 +735,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.settings.link_mode,
&args.settings.index_locations,
args.settings.index_strategy,
args.settings.dependency_metadata,
args.settings.keyring_provider,
args.settings.allow_insecure_host,
uv_virtualenv::Prompt::from_args(prompt),
@@ -884,7 +887,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
)
.collect::<Vec<_>>();
commands::tool_install(
Box::pin(commands::tool_install(
args.package,
args.editable,
args.from,
@@ -900,7 +903,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
globals.native_tls,
cache,
printer,
)
))
.await
}
Commands::Tool(ToolNamespace {
+26 -3
View File
@@ -4,7 +4,7 @@ use std::path::PathBuf;
use std::process;
use std::str::FromStr;
use distribution_types::IndexLocations;
use distribution_types::{DependencyMetadata, IndexLocations};
use install_wheel_rs::linker::LinkMode;
use pep508_rs::{ExtraName, RequirementOrigin};
use pypi_types::{Requirement, SupportedEnvironments};
@@ -1784,6 +1784,7 @@ pub(crate) struct InstallerSettingsRef<'a> {
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
pub(crate) allow_insecure_host: &'a [TrustedHost],
pub(crate) dependency_metadata: &'a DependencyMetadata,
pub(crate) config_setting: &'a ConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: &'a [PackageName],
@@ -1808,6 +1809,7 @@ pub(crate) struct ResolverSettings {
pub(crate) allow_insecure_host: Vec<TrustedHost>,
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) dependency_metadata: DependencyMetadata,
pub(crate) config_setting: ConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec<PackageName>,
@@ -1826,6 +1828,7 @@ pub(crate) struct ResolverSettingsRef<'a> {
pub(crate) allow_insecure_host: &'a [TrustedHost],
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) dependency_metadata: &'a DependencyMetadata,
pub(crate) config_setting: &'a ConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: &'a [PackageName],
@@ -1857,6 +1860,7 @@ impl ResolverSettings {
allow_insecure_host: &self.allow_insecure_host,
resolution: self.resolution,
prerelease: self.prerelease,
dependency_metadata: &self.dependency_metadata,
config_setting: &self.config_setting,
no_build_isolation: self.no_build_isolation,
no_build_isolation_package: &self.no_build_isolation_package,
@@ -1880,6 +1884,9 @@ impl From<ResolverOptions> for ResolverSettings {
),
resolution: value.resolution.unwrap_or_default(),
prerelease: value.prerelease.unwrap_or_default(),
dependency_metadata: DependencyMetadata::from_entries(
value.dependency_metadata.into_iter().flatten(),
),
index_strategy: value.index_strategy.unwrap_or_default(),
keyring_provider: value.keyring_provider.unwrap_or_default(),
allow_insecure_host: value.allow_insecure_host.unwrap_or_default(),
@@ -1914,6 +1921,7 @@ pub(crate) struct ResolverInstallerSettingsRef<'a> {
pub(crate) allow_insecure_host: &'a [TrustedHost],
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) dependency_metadata: &'a DependencyMetadata,
pub(crate) config_setting: &'a ConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: &'a [PackageName],
@@ -1932,8 +1940,7 @@ pub(crate) struct ResolverInstallerSettingsRef<'a> {
/// Represents the shared settings that are used across all uv commands outside the `pip` API.
/// Analogous to the settings contained in the `[tool.uv]` table, combined with [`ResolverInstallerArgs`].
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[derive(Debug, Clone, Default)]
pub(crate) struct ResolverInstallerSettings {
pub(crate) index_locations: IndexLocations,
pub(crate) index_strategy: IndexStrategy,
@@ -1941,6 +1948,7 @@ pub(crate) struct ResolverInstallerSettings {
pub(crate) allow_insecure_host: Vec<TrustedHost>,
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) dependency_metadata: DependencyMetadata,
pub(crate) config_setting: ConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec<PackageName>,
@@ -1977,6 +1985,7 @@ impl ResolverInstallerSettings {
allow_insecure_host: &self.allow_insecure_host,
resolution: self.resolution,
prerelease: self.prerelease,
dependency_metadata: &self.dependency_metadata,
config_setting: &self.config_setting,
no_build_isolation: self.no_build_isolation,
no_build_isolation_package: &self.no_build_isolation_package,
@@ -2002,6 +2011,9 @@ impl From<ResolverInstallerOptions> for ResolverInstallerSettings {
),
resolution: value.resolution.unwrap_or_default(),
prerelease: value.prerelease.unwrap_or_default(),
dependency_metadata: DependencyMetadata::from_entries(
value.dependency_metadata.into_iter().flatten(),
),
index_strategy: value.index_strategy.unwrap_or_default(),
keyring_provider: value.keyring_provider.unwrap_or_default(),
allow_insecure_host: value.allow_insecure_host.unwrap_or_default(),
@@ -2058,6 +2070,7 @@ pub(crate) struct PipSettings {
pub(crate) dependency_mode: DependencyMode,
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) dependency_metadata: DependencyMetadata,
pub(crate) output_file: Option<PathBuf>,
pub(crate) no_strip_extras: bool,
pub(crate) no_strip_markers: bool,
@@ -2117,6 +2130,7 @@ impl PipSettings {
allow_empty_requirements,
resolution,
prerelease,
dependency_metadata,
output_file,
no_strip_extras,
no_strip_markers,
@@ -2157,6 +2171,7 @@ impl PipSettings {
allow_insecure_host: top_level_allow_insecure_host,
resolution: top_level_resolution,
prerelease: top_level_prerelease,
dependency_metadata: top_level_dependency_metadata,
config_settings: top_level_config_settings,
no_build_isolation: top_level_no_build_isolation,
no_build_isolation_package: top_level_no_build_isolation_package,
@@ -2187,6 +2202,7 @@ impl PipSettings {
let allow_insecure_host = allow_insecure_host.combine(top_level_allow_insecure_host);
let resolution = resolution.combine(top_level_resolution);
let prerelease = prerelease.combine(top_level_prerelease);
let dependency_metadata = dependency_metadata.combine(top_level_dependency_metadata);
let config_settings = config_settings.combine(top_level_config_settings);
let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation);
let no_build_isolation_package =
@@ -2220,6 +2236,11 @@ impl PipSettings {
},
resolution: args.resolution.combine(resolution).unwrap_or_default(),
prerelease: args.prerelease.combine(prerelease).unwrap_or_default(),
dependency_metadata: DependencyMetadata::from_entries(
args.dependency_metadata
.combine(dependency_metadata)
.unwrap_or_default(),
),
output_file: args.output_file.combine(output_file),
no_strip_extras: args
.no_strip_extras
@@ -2364,6 +2385,7 @@ impl<'a> From<ResolverInstallerSettingsRef<'a>> for ResolverSettingsRef<'a> {
allow_insecure_host: settings.allow_insecure_host,
resolution: settings.resolution,
prerelease: settings.prerelease,
dependency_metadata: settings.dependency_metadata,
config_setting: settings.config_setting,
no_build_isolation: settings.no_build_isolation,
no_build_isolation_package: settings.no_build_isolation_package,
@@ -2383,6 +2405,7 @@ impl<'a> From<ResolverInstallerSettingsRef<'a>> for InstallerSettingsRef<'a> {
index_strategy: settings.index_strategy,
keyring_provider: settings.keyring_provider,
allow_insecure_host: settings.allow_insecure_host,
dependency_metadata: settings.dependency_metadata,
config_setting: settings.config_setting,
no_build_isolation: settings.no_build_isolation,
no_build_isolation_package: settings.no_build_isolation_package,
+218
View File
@@ -12627,6 +12627,224 @@ fn lock_python_upper_bound() -> Result<()> {
Ok(())
}
#[test]
fn lock_dependency_metadata() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[[tool.uv.dependency-metadata]]
name = "anyio"
version = "3.7.0"
requires-dist = ["iniconfig"]
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
"###);
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
[[manifest.dependency-metadata]]
name = "anyio"
version = "3.7.0"
requires-dist = ["iniconfig"]
[[package]]
name = "anyio"
version = "3.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "iniconfig" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 },
]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]
[[package]]
name = "project"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "anyio" },
]
[package.metadata]
requires-dist = [{ name = "anyio", specifier = "==3.7.0" }]
"###
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
"###);
// Re-run with `--offline`. We shouldn't need a network connection to validate an
// already-correct lockfile with immutable metadata.
uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
"###);
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==3.7.0
+ iniconfig==2.0.0
+ project==0.1.0 (from file://[TEMP_DIR]/)
"###);
// Update the static metadata.
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[[tool.uv.dependency-metadata]]
name = "anyio"
version = "3.7.0"
requires-dist = ["typing-extensions"]
"#,
)?;
// The lockfile should update.
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Removed iniconfig v2.0.0
Added typing-extensions v4.10.0
"###);
// Remove the static metadata.
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
// The lockfile should update.
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
Added idna v3.6
Added sniffio v1.3.1
Removed typing-extensions v4.10.0
"###);
// Use a blanket match.
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[[tool.uv.dependency-metadata]]
name = "anyio"
requires-dist = ["iniconfig"]
"#,
)?;
// The lockfile should update.
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Removed idna v3.6
Added iniconfig v2.0.0
Removed sniffio v1.3.1
"###);
Ok(())
}
#[test]
fn lock_strip_fragment() -> Result<()> {
let context = TestContext::new("3.12");
+70
View File
@@ -140,6 +140,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -279,6 +282,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -419,6 +425,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -591,6 +600,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -709,6 +721,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -859,6 +874,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -1046,6 +1064,9 @@ fn resolve_index_url() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -1232,6 +1253,9 @@ fn resolve_index_url() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -1396,6 +1420,9 @@ fn resolve_find_links() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -1536,6 +1563,9 @@ fn resolve_top_level() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -1714,6 +1744,9 @@ fn resolve_top_level() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -1875,6 +1908,9 @@ fn resolve_top_level() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -2015,6 +2051,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -2138,6 +2177,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -2261,6 +2303,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -2386,6 +2431,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -2509,6 +2557,7 @@ fn resolve_tool() -> anyhow::Result<()> {
LowestDirect,
),
prerelease: None,
dependency_metadata: None,
config_settings: None,
no_build_isolation: None,
no_build_isolation_package: None,
@@ -2543,6 +2592,9 @@ fn resolve_tool() -> anyhow::Result<()> {
allow_insecure_host: [],
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
config_setting: ConfigSettings(
{},
),
@@ -2683,6 +2735,9 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -2857,6 +2912,9 @@ fn resolve_both() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -3022,6 +3080,9 @@ fn resolve_config_file() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -3240,6 +3301,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -3366,6 +3430,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
@@ -3511,6 +3578,9 @@ fn allow_insecure_host() -> anyhow::Result<()> {
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
dependency_metadata: DependencyMetadata(
{},
),
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
+57 -3
View File
@@ -715,8 +715,8 @@ $ uv sync --extra build
$ uv sync --extra build --extra compile
```
Some packages, like `cchardet`, only require build dependencies for the _installation_ phase of
`uv sync`. Others, like `flash-attn`, require their build dependencies to be present even just to
Some packages, like `cchardet` above, only require build dependencies for the _installation_ phase
of `uv sync`. Others, like `flash-attn`, require their build dependencies to be present even just to
resolve the project's lockfile during the _resolution_ phase.
In such cases, the build dependencies must be installed prior to running any `uv lock` or `uv sync`
@@ -735,10 +735,64 @@ dependencies = ["flash-attn"]
no-build-isolation-package = ["flash-attn"]
```
You could run the following sequence of commands:
You could run the following sequence of commands to sync `flash-attn`:
```console
$ uv venv
$ uv pip install torch
$ uv sync
```
Alternatively, you can provide the `flash-attn` metadata upfront via the
[`dependency-metadata`](../reference/settings.md#dependency-metadata) setting, thereby forgoing the
need to build the package during the dependency resolution phase. For example, to provide the
`flash-attn` metadata upfront, include the following in your `pyproject.toml`:
```toml title="pyproject.toml"
[[tool.uv.dependency-metadata]]
name = "flash-attn"
version = "2.6.3"
requires-dist = ["torch", "einops"]
```
!!! tip
To determine the package metadata for a package like `flash-attn`, navigate to the appropriate Git repository,
or look it up on [PyPI](https://pypi.org/project/flash-attn) and download the package's source distribution.
The package requirements can typically be found in the `setup.py` or `setup.cfg` file.
(If the package includes a built distribution, you can unzip it to find the `METADATA` file; however, the presence
of a built distribution would negate the need to provide the metadata upfront, since it would already be available
to uv.)
Once included, you can again use the two-step `uv sync` process to install the build dependencies.
Given the following `pyproject.toml`:
```toml title="pyproject.toml"
[project]
name = "project"
version = "0.1.0"
description = "..."
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[project.optional-dependencies]
build = ["torch", "setuptools", "packaging"]
compile = ["flash-attn"]
[tool.uv]
no-build-isolation-package = ["flash-attn"]
[[tool.uv.dependency-metadata]]
name = "flash-attn"
version = "2.6.3"
requires-dist = ["torch", "einops"]
```
You could run the following sequence of commands to sync `flash-attn`:
```console
$ uv sync --extra build
$ uv sync --extra build --extra compile
```
+78 -17
View File
@@ -227,28 +227,35 @@ For more details, see
## Dependency constraints
uv supports constraints files (`--constraint constraints.txt`), like pip, which narrow the set of
acceptable versions for the given packages. Constraint files are like a regular requirements files,
but they do not add packages to the requirements — they only take affect if the package is requested
in a direct or transitive dependency. Constraints are often useful for reducing the range of
available versions for a transitive dependency without adding a direct requirement on the package.
Like pip, uv supports constraint files (`--constraint constraints.txt`) which narrow the set of
acceptable versions for the given packages. Constraint files are similar to requirements files, but
being listed as a constraint alone will not cause a package to be included to the resolution.
Instead, constraints only take effect if a requested package is already pulled in as a direct or
transitive dependency. Constraints are useful for reducing the range of available versions for a
transitive dependency. They can also be used to keep a resolution in sync with some other set of
resolved versions, regardless of which packages are overlapping between the two.
## Dependency overrides
Overrides allow bypassing failing or undesirable resolutions by overriding the declared dependencies
of a package. Overrides are a useful last resort for cases in which the you know that a dependency
is compatible with a newer version of a package than it declares, but the it has not yet been
updated to declare that compatibility.
Dependency overrides allow bypassing failing or undesirable resolutions by overriding a package's
declared dependencies. Overrides are a useful last resort for cases in which you _know_ that a
dependency is compatible with a certain version of a package, despite the metadata indicating
otherwise.
For example, if a transitive dependency declares the requirement `pydantic>=1.0,<2.0`, but _works_
with `pydantic>=2.0`, the user can override the declared dependency with `pydantic>=1.0,<3` to allow
the resolver to installer a newer version of `pydantic`.
For example, if a transitive dependency declares the requirement `pydantic>=1.0,<2.0`, but _does_
work with `pydantic>=2.0`, the user can override the declared dependency by including
`pydantic>=1.0,<3` in the overrides, thereby allowing the resolver to choose a newer version of
`pydantic`.
While constraints and dependencies are purely additive, and thus cannot expand the set of acceptable
versions for a package, overrides can expand the set of acceptable versions for a package, providing
an escape hatch for erroneous upper version bounds. As with constraints, overrides do not add a
dependency on the package and only take affect if the package is requested in a direct or transitive
dependency.
Concretely, if `pydantic>=1.0,<3` is included as an override, uv will ignore all declared
requirements on `pydantic`, replacing them with the override. In the above example, the
`pydantic>=1.0,<2.0` requirement would be ignored completely, and would instead be replaced with
`pydantic>=1.0,<3`.
While constraints can only _reduce_ the set of acceptable versions for a package, overrides can
_expand_ the set of acceptable versions, providing an escape hatch for erroneous upper version
bounds. As with constraints, overrides do not add a dependency on the package and only take effect
if the package is requested in a direct or transitive dependency.
In a `pyproject.toml`, use `tool.uv.override-dependencies` to define a list of overrides. In the
pip-compatible interface, the `--override` option can be used to pass files with the same format as
@@ -258,6 +265,60 @@ If multiple overrides are provided for the same package, they must be differenti
[markers](#platform-markers). If a package has a dependency with a marker, it is replaced
unconditionally when using overrides — it does not matter if the marker evaluates to true or false.
## Dependency metadata
During resolution, uv needs to resolve the metadata for each package it encounters, in order to
determine its dependencies. This metadata is often available as a static file in the package index;
however, for packages that only provide source distributions, the metadata may not be available
upfront.
In such cases, uv has to build the package to determine its metadata (e.g., by invoking `setup.py`).
This can introduce a performance penalty during resolution. Further, it imposes the requirement that
the package can be built on all platforms, which may not be true.
For example, you may have a package that should only be built and installed on Linux, but doesn't
build successfully on macOS or Windows. While uv can construct a perfectly valid lockfile for this
scenario, doing so would require building the package, which would fail on non-Linux platforms.
The `tool.uv.dependency-metadata` table can be used to provide static metadata for such dependencies
upfront, thereby allowing uv to skip the build step and use the provided metadata instead.
For example, to provide metadata for `chumpy` upfront, include its `dependency-metadata` in the
`pyproject.toml`:
```toml
[[tool.uv.dependency-metadata]]
name = "chumpy"
version = "0.70"
requires-dist = ["numpy>=1.8.1", "scipy>=0.13.0", "six>=1.11.0"]
```
These declarations are intended for cases in which a package does _not_ declare static metadata
upfront, though they are also useful for packages that require disabling build isolation. In such
cases, it may be easier to declare the package metadata upfront, rather than creating a custom build
environment prior to resolving the package.
For example, you can declare the metadata for `flash-attn`, allowing uv to resolve without building
the package from source (which itself requires installing `torch`):
```toml
[[tool.uv.dependency-metadata]]
name = "flash-attn"
version = "2.6.3"
requires-dist = ["torch", "einops"]
```
Like dependency overrides, `tool.uv.dependency-metadata` can also be used for cases in which a
package's metadata is incorrect or incomplete, or when a package is not available in the package
index. While dependency overrides allow overriding the allowed versions of a package globally,
metadata overrides allow overriding the declared metadata of a _specific package_.
Entries in the `tool.uv.dependency-metadata` table follow the
[Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) specification,
though only `name`, `version`, `requires-dist`, `requires-python`, and `provides-extra` are read by
uv. The `version` field is also considered optional. If omitted, the metadata will be used for all
versions of the specified package.
## Lower bounds
By default, `uv add` adds lower bounds to dependencies and, when using uv to manage projects, uv
+83
View File
@@ -451,6 +451,47 @@ specified as `KEY=VALUE` pairs.
---
### [`dependency-metadata`](#dependency-metadata) {: #dependency-metadata }
Pre-defined static metadata for dependencies of the project (direct or transitive). When
provided, enables the resolver to use the specified metadata instead of querying the
registry or building the relevant package from source.
Metadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/)
standard, though only the following fields are respected:
- `name`: The name of the package.
- (Optional) `version`: The version of the package. If omitted, the metadata will be applied
to all versions of the package.
- (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`).
- (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`).
- (Optional) `provides-extras`: The extras provided by the package.
**Default value**: `[]`
**Type**: `list[dict]`
**Example usage**:
=== "pyproject.toml"
```toml
[tool.uv]
dependency-metadata = [
{ name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" },
]
```
=== "uv.toml"
```toml
dependency-metadata = [
{ name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" },
]
```
---
### [`exclude-newer`](#exclude-newer) {: #exclude-newer }
Limit candidate packages to those that were uploaded prior to the given date.
@@ -1474,6 +1515,48 @@ Used to reflect custom build scripts and commands that wrap `uv pip compile`.
---
#### [`dependency-metadata`](#pip_dependency-metadata) {: #pip_dependency-metadata }
<span id="dependency-metadata"></span>
Pre-defined static metadata for dependencies of the project (direct or transitive). When
provided, enables the resolver to use the specified metadata instead of querying the
registry or building the relevant package from source.
Metadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/)
standard, though only the following fields are respected:
- `name`: The name of the package.
- (Optional) `version`: The version of the package. If omitted, the metadata will be applied
to all versions of the package.
- (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`).
- (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`).
- (Optional) `provides-extras`: The extras provided by the package.
**Default value**: `[]`
**Type**: `list[dict]`
**Example usage**:
=== "pyproject.toml"
```toml
[tool.uv.pip]
dependency-metadata = [
{ name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" },
]
```
=== "uv.toml"
```toml
[pip]
dependency-metadata = [
{ name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" },
]
```
---
#### [`emit-build-options`](#pip_emit-build-options) {: #pip_emit-build-options }
<span id="emit-build-options"></span>
+59 -1
View File
@@ -87,6 +87,16 @@
"type": "string"
}
},
"dependency-metadata": {
"description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When provided, enables the resolver to use the specified metadata instead of querying the registry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) standard, though only the following fields are respected:\n\n- `name`: The name of the package. - (Optional) `version`: The version of the package. If omitted, the metadata will be applied to all versions of the package. - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). - (Optional) `provides-extras`: The extras provided by the package.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/StaticMetadata"
}
},
"dev-dependencies": {
"description": "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`.",
"type": [
@@ -677,6 +687,16 @@
"null"
]
},
"dependency-metadata": {
"description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When provided, enables the resolver to use the specified metadata instead of querying the registry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) standard, though only the following fields are respected:\n\n- `name`: The name of the package. - (Optional) `version`: The version of the package. If omitted, the metadata will be applied to all versions of the package. - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). - (Optional) `provides-extras`: The extras provided by the package.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/StaticMetadata"
}
},
"emit-build-options": {
"description": "Include `--no-binary` and `--only-binary` entries in the output file generated by `uv pip compile`.",
"type": [
@@ -1153,7 +1173,7 @@
"pattern": "^3\\.\\d+(\\.\\d+)?$"
},
"Requirement": {
"description": "A PEP 508 dependency specifier",
"description": "A PEP 508 dependency specifier, e.g., `ruff >= 0.6.0`",
"type": "string"
},
"ResolutionMode": {
@@ -1347,6 +1367,44 @@
}
]
},
"StaticMetadata": {
"description": "A subset of the Python Package Metadata 2.3 standard as specified in <https://packaging.python.org/specifications/core-metadata/>.",
"type": "object",
"required": [
"name",
"version"
],
"properties": {
"name": {
"$ref": "#/definitions/PackageName"
},
"provides-extras": {
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/ExtraName"
}
},
"requires-dist": {
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/Requirement"
}
},
"requires-python": {
"description": "PEP 508-style Python requirement, e.g., `>=3.10`",
"type": [
"string",
"null"
]
},
"version": {
"description": "PEP 440-style package version, e.g., `1.2.3`",
"type": "string"
}
}
},
"String": {
"type": "string"
},