mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-31 20:09:09 +00:00
Allow user to constrain supported lock environments (#6210)
## Summary The strategy here is: if the user provides supported environments, we use those as the initial forks when resolving. As a result, we never add or explore branches that are disjoint with the supported environments. (If the supported environments change, we ignore the lockfile entirely, so we don't have to worry about any interactions between supported environments and the preference forks.) Closes https://github.com/astral-sh/uv/issues/6184.
This commit is contained in:
parent
d02c202eb2
commit
3395d24959
28 changed files with 790 additions and 49 deletions
2
STYLE.md
2
STYLE.md
|
|
@ -86,7 +86,7 @@ The documentation is divided into:
|
|||
1. When using `console` syntax, use `$` to indicate commands — everything else is output.
|
||||
1. Never use the `bash` syntax when displaying command output.
|
||||
1. Prefer `console` with `$` prefixed commands over `bash`.
|
||||
1. Command output should rarely be included — it's hard to keep up to date.
|
||||
1. Command output should rarely be included — it's hard to keep up-to-date.
|
||||
1. Use `title` for example files, e.g., `pyproject.toml`, `Dockerfile`, or `example.py`.
|
||||
|
||||
## CLI
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ called a re-validation request.
|
|||
|
||||
A re-validation request includes with it some metadata (usually an "entity tag"
|
||||
or `etag` for short) that was on the cached response (which is now stale).
|
||||
When we send this request, the server can compare it with its most up to date
|
||||
When we send this request, the server can compare it with its most up-to-date
|
||||
version of the resource. If its entity tag matches the one we gave it (among
|
||||
other possible criteria), then the server can skip returning the body and
|
||||
instead just return a small HTTP 304 NOT MODIFIED response. When we get this
|
||||
|
|
|
|||
|
|
@ -626,9 +626,9 @@ enum FastPathRev {
|
|||
/// date with what this rev resolves to on GitHub's server.
|
||||
UpToDate,
|
||||
/// The following SHA must be fetched in order for the local rev to become
|
||||
/// up to date.
|
||||
/// up-to-date.
|
||||
NeedsFetch(GitOid),
|
||||
/// Don't know whether local rev is up to date. We'll fetch _all_ branches
|
||||
/// Don't know whether local rev is up-to-date. We'll fetch _all_ branches
|
||||
/// and tags from the server and see what happens.
|
||||
Indeterminate,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ pub struct Lock {
|
|||
/// If this lockfile was built from a forking resolution with non-identical forks, store the
|
||||
/// forks in the lockfile so we can recreate them in subsequent resolutions.
|
||||
fork_markers: Vec<MarkerTree>,
|
||||
/// The list of supported environments specified by the user.
|
||||
supported_environments: Vec<MarkerTree>,
|
||||
/// The range of supported Python versions.
|
||||
requires_python: Option<RequiresPython>,
|
||||
/// We discard the lockfile if these options don't match.
|
||||
|
|
@ -161,6 +163,7 @@ impl Lock {
|
|||
requires_python,
|
||||
options,
|
||||
ResolverManifest::default(),
|
||||
vec![],
|
||||
graph.fork_markers.clone(),
|
||||
)?;
|
||||
Ok(lock)
|
||||
|
|
@ -173,6 +176,7 @@ impl Lock {
|
|||
requires_python: Option<RequiresPython>,
|
||||
options: ResolverOptions,
|
||||
manifest: ResolverManifest,
|
||||
supported_environments: Vec<MarkerTree>,
|
||||
fork_markers: Vec<MarkerTree>,
|
||||
) -> Result<Self, LockError> {
|
||||
// Put all dependencies for each package in a canonical order and
|
||||
|
|
@ -329,6 +333,7 @@ impl Lock {
|
|||
Ok(Self {
|
||||
version,
|
||||
fork_markers,
|
||||
supported_environments,
|
||||
requires_python,
|
||||
options,
|
||||
packages,
|
||||
|
|
@ -344,6 +349,13 @@ impl Lock {
|
|||
self
|
||||
}
|
||||
|
||||
/// Record the supported environments that were used to generate this lock.
|
||||
#[must_use]
|
||||
pub fn with_supported_environments(mut self, supported_environments: Vec<MarkerTree>) -> Self {
|
||||
self.supported_environments = supported_environments;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the number of packages in the lockfile.
|
||||
pub fn len(&self) -> usize {
|
||||
self.packages.len()
|
||||
|
|
@ -384,6 +396,11 @@ impl Lock {
|
|||
self.options.exclude_newer
|
||||
}
|
||||
|
||||
/// Returns the supported environments that were used to generate this lock.
|
||||
pub fn supported_environments(&self) -> &[MarkerTree] {
|
||||
&self.supported_environments
|
||||
}
|
||||
|
||||
/// If this lockfile was built from a forking resolution with non-identical forks, return the
|
||||
/// markers of those forks, otherwise `None`.
|
||||
pub fn fork_markers(&self) -> &[MarkerTree] {
|
||||
|
|
@ -486,6 +503,7 @@ impl Lock {
|
|||
if let Some(ref requires_python) = self.requires_python {
|
||||
doc.insert("requires-python", value(requires_python.to_string()));
|
||||
}
|
||||
|
||||
if !self.fork_markers.is_empty() {
|
||||
let fork_markers = each_element_on_its_line_array(
|
||||
self.fork_markers
|
||||
|
|
@ -496,6 +514,16 @@ impl Lock {
|
|||
doc.insert("resolution-markers", value(fork_markers));
|
||||
}
|
||||
|
||||
if !self.supported_environments.is_empty() {
|
||||
let supported_environments = each_element_on_its_line_array(
|
||||
self.supported_environments
|
||||
.iter()
|
||||
.filter_map(MarkerTree::contents)
|
||||
.map(|marker| marker.to_string()),
|
||||
);
|
||||
doc.insert("supported-markers", value(supported_environments));
|
||||
}
|
||||
|
||||
// Write the settings that were used to generate the resolution.
|
||||
// This enables us to invalidate the lockfile if the user changes
|
||||
// their settings.
|
||||
|
|
@ -951,6 +979,8 @@ struct LockWire {
|
|||
/// forks in the lockfile so we can recreate them in subsequent resolutions.
|
||||
#[serde(rename = "resolution-markers", default)]
|
||||
fork_markers: Vec<MarkerTree>,
|
||||
#[serde(rename = "supported-markers", default)]
|
||||
supported_environments: Vec<MarkerTree>,
|
||||
/// We discard the lockfile if these options match.
|
||||
#[serde(default)]
|
||||
options: ResolverOptions,
|
||||
|
|
@ -966,6 +996,7 @@ impl From<Lock> for LockWire {
|
|||
version: lock.version,
|
||||
requires_python: lock.requires_python,
|
||||
fork_markers: lock.fork_markers,
|
||||
supported_environments: lock.supported_environments,
|
||||
options: lock.options,
|
||||
manifest: lock.manifest,
|
||||
packages: lock.packages.into_iter().map(PackageWire::from).collect(),
|
||||
|
|
@ -1005,6 +1036,7 @@ impl TryFrom<LockWire> for Lock {
|
|||
wire.requires_python,
|
||||
wire.options,
|
||||
wire.manifest,
|
||||
wire.supported_environments,
|
||||
wire.fork_markers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,7 +174,18 @@ impl ResolutionGraph {
|
|||
vec![]
|
||||
}
|
||||
ResolverMarkers::Fork(_) => {
|
||||
panic!("A single fork must be universal");
|
||||
resolutions
|
||||
.iter()
|
||||
.map(|resolution| {
|
||||
resolution
|
||||
.markers
|
||||
.fork_markers()
|
||||
.expect("A non-forking resolution exists in forking mode")
|
||||
.clone()
|
||||
})
|
||||
// Any unsatisfiable forks were skipped.
|
||||
.filter(|fork| !fork.is_false())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Ok(
|
|||
Lock {
|
||||
version: 1,
|
||||
fork_markers: [],
|
||||
supported_environments: [],
|
||||
requires_python: None,
|
||||
options: ResolverOptions {
|
||||
resolution_mode: Highest,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@ pub struct Options {
|
|||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
dev_dependencies: serde::de::IgnoredAny,
|
||||
|
||||
#[serde(default, skip_serializing)]
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
environments: serde::de::IgnoredAny,
|
||||
|
||||
#[serde(default, skip_serializing)]
|
||||
#[cfg_attr(feature = "schemars", schemars(skip))]
|
||||
managed: serde::de::IgnoredAny,
|
||||
|
|
|
|||
78
crates/uv-workspace/src/environments.rs
Normal file
78
crates/uv-workspace/src/environments.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use serde::ser::SerializeSeq;
|
||||
|
||||
use pep508_rs::MarkerTree;
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub struct SupportedEnvironments(Vec<MarkerTree>);
|
||||
|
||||
impl SupportedEnvironments {
|
||||
/// Return the list of marker trees.
|
||||
pub fn as_markers(&self) -> &[MarkerTree] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Convert the [`SupportedEnvironments`] struct into a list of marker trees.
|
||||
pub fn into_markers(self) -> Vec<MarkerTree> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize a [`SupportedEnvironments`] struct into a list of marker strings.
|
||||
impl serde::Serialize for SupportedEnvironments {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||
for element in &self.0 {
|
||||
if let Some(contents) = element.contents() {
|
||||
seq.serialize_element(&contents)?;
|
||||
}
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize a marker string or list of marker strings into a [`SupportedEnvironments`] struct.
|
||||
impl<'de> serde::Deserialize<'de> for SupportedEnvironments {
|
||||
fn deserialize<D>(deserializer: D) -> Result<SupportedEnvironments, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct StringOrVecVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for StringOrVecVisitor {
|
||||
type Value = SupportedEnvironments;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string or a list of strings")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let marker = MarkerTree::from_str(value).map_err(serde::de::Error::custom)?;
|
||||
Ok(SupportedEnvironments(vec![marker]))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut markers = Vec::new();
|
||||
|
||||
while let Some(elem) = seq.next_element::<String>()? {
|
||||
let marker = MarkerTree::from_str(&elem).map_err(serde::de::Error::custom)?;
|
||||
markers.push(marker);
|
||||
}
|
||||
|
||||
Ok(SupportedEnvironments(markers))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(StringOrVecVisitor)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
pub use environments::SupportedEnvironments;
|
||||
pub use workspace::{
|
||||
check_nested_workspaces, DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace,
|
||||
WorkspaceError, WorkspaceMember,
|
||||
};
|
||||
|
||||
mod environments;
|
||||
pub mod pyproject;
|
||||
pub mod pyproject_mut;
|
||||
mod workspace;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize};
|
|||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
use crate::environments::SupportedEnvironments;
|
||||
use pep440_rs::VersionSpecifiers;
|
||||
use pypi_types::{RequirementSource, VerbatimParsedUrl};
|
||||
use uv_git::GitReference;
|
||||
|
|
@ -98,6 +99,8 @@ pub struct ToolUv {
|
|||
"#
|
||||
)]
|
||||
pub managed: Option<bool>,
|
||||
/// The project's development dependencies. Development dependencies will be installed by
|
||||
/// default in `uv run` and `uv sync`, but will not appear in the project's published metadata.
|
||||
#[cfg_attr(
|
||||
feature = "schemars",
|
||||
schemars(
|
||||
|
|
@ -105,12 +108,40 @@ pub struct ToolUv {
|
|||
description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
|
||||
)
|
||||
)]
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
dev_dependencies = ["ruff==0.5.0"]
|
||||
"#
|
||||
)]
|
||||
pub dev_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
||||
/// A list of supported environments against which to resolve dependencies.
|
||||
///
|
||||
/// By default, uv will resolve for all possible environments during a `uv lock` operation.
|
||||
/// However, you can restrict the set of supported environments to improve performance and avoid
|
||||
/// unsatisfiable branches in the solution space.
|
||||
#[cfg_attr(
|
||||
feature = "schemars",
|
||||
schemars(
|
||||
with = "Option<Vec<String>>",
|
||||
description = "PEP 508 style requirements, e.g. `ruff==0.5.0`, or `ruff @ https://...`."
|
||||
description = "A list of environment markers, e.g. `python_version >= '3.6'`."
|
||||
)
|
||||
)]
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = "str | list[str]",
|
||||
example = r#"
|
||||
# Resolve for macOS, but not for Linux or Windows.
|
||||
environments = ["sys_platform == 'darwin'"]
|
||||
"#
|
||||
)]
|
||||
pub environments: Option<SupportedEnvironments>,
|
||||
#[cfg_attr(
|
||||
feature = "schemars",
|
||||
schemars(
|
||||
with = "Option<Vec<String>>",
|
||||
description = "PEP 508-style requirements, e.g. `ruff==0.5.0`, or `ruff @ https://...`."
|
||||
)
|
||||
)]
|
||||
pub override_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use uv_fs::{absolutize_path, normalize_path, relative_to, Simplified};
|
|||
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::environments::SupportedEnvironments;
|
||||
use crate::pyproject::{Project, PyProjectToml, Source, ToolUvWorkspace};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
|
@ -367,6 +368,21 @@ impl Workspace {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the set of supported environments for the workspace.
|
||||
pub fn environments(&self) -> Option<&SupportedEnvironments> {
|
||||
let workspace_package = self
|
||||
.packages
|
||||
.values()
|
||||
.find(|workspace_package| workspace_package.root() == self.install_path())?;
|
||||
|
||||
workspace_package
|
||||
.pyproject_toml()
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.environments.as_ref())
|
||||
}
|
||||
|
||||
/// Returns the set of constraints for the workspace.
|
||||
pub fn constraints(&self) -> Vec<Requirement> {
|
||||
let Some(workspace_package) = self
|
||||
|
|
@ -1579,6 +1595,7 @@ mod tests {
|
|||
},
|
||||
"managed": null,
|
||||
"dev-dependencies": null,
|
||||
"environments": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null
|
||||
}
|
||||
|
|
@ -1651,6 +1668,7 @@ mod tests {
|
|||
},
|
||||
"managed": null,
|
||||
"dev-dependencies": null,
|
||||
"environments": null,
|
||||
"override-dependencies": null,
|
||||
"constraint-dependencies": null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ use uv_resolver::{
|
|||
};
|
||||
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::{DiscoveryOptions, Workspace};
|
||||
use uv_workspace::{DiscoveryOptions, SupportedEnvironments, Workspace};
|
||||
|
||||
use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
|
||||
use crate::commands::project::{find_requires_python, FoundInterpreter, ProjectError, SharedState};
|
||||
|
|
@ -290,6 +290,42 @@ async fn do_lock(
|
|||
members
|
||||
};
|
||||
|
||||
// Collect the list of supported environments.
|
||||
let environments = {
|
||||
let environments = workspace.environments();
|
||||
|
||||
// Ensure that the environments are disjoint.
|
||||
if let Some(environments) = &environments {
|
||||
for (lhs, rhs) in environments
|
||||
.as_markers()
|
||||
.iter()
|
||||
.zip(environments.as_markers().iter().skip(1))
|
||||
{
|
||||
if !lhs.is_disjoint(rhs) {
|
||||
let mut hint = lhs.negate();
|
||||
hint.and(rhs.clone());
|
||||
|
||||
let lhs = lhs
|
||||
.contents()
|
||||
.map(|contents| contents.to_string())
|
||||
.unwrap_or("true".to_string());
|
||||
let rhs = rhs
|
||||
.contents()
|
||||
.map(|contents| contents.to_string())
|
||||
.unwrap_or("true".to_string());
|
||||
let hint = hint
|
||||
.contents()
|
||||
.map(|contents| contents.to_string())
|
||||
.unwrap_or("true".to_string());
|
||||
|
||||
return Err(ProjectError::OverlappingMarkers(lhs, rhs, hint));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
environments
|
||||
};
|
||||
|
||||
// Determine the supported Python range. If no range is defined, and warn and default to the
|
||||
// current minor version.
|
||||
let requires_python = find_requires_python(workspace)?;
|
||||
|
|
@ -410,6 +446,7 @@ async fn do_lock(
|
|||
&members,
|
||||
&constraints,
|
||||
&overrides,
|
||||
environments,
|
||||
interpreter,
|
||||
&requires_python,
|
||||
index_locations,
|
||||
|
|
@ -444,14 +481,15 @@ async fn do_lock(
|
|||
_ => {
|
||||
debug!("Starting clean resolution");
|
||||
|
||||
// If an existing lockfile exists, build up a set of preferences.
|
||||
let LockedRequirements { preferences, git } = existing_lock
|
||||
.as_ref()
|
||||
.and_then(|lock| match &lock {
|
||||
// Determine whether we can reuse the existing package versions.
|
||||
let reusable_lock = existing_lock.as_ref().and_then(|lock| match &lock {
|
||||
ValidatedLock::Preferable(lock) => Some(lock),
|
||||
ValidatedLock::Satisfies(lock) => Some(lock),
|
||||
ValidatedLock::Unusable(_) => None,
|
||||
})
|
||||
});
|
||||
|
||||
// If an existing lockfile exists, build up a set of preferences.
|
||||
let LockedRequirements { preferences, git } = reusable_lock
|
||||
.map(|lock| read_lock_requirements(lock, upgrade))
|
||||
.unwrap_or_default();
|
||||
|
||||
|
|
@ -462,19 +500,20 @@ async fn do_lock(
|
|||
}
|
||||
|
||||
// When we run the same resolution from the lockfile again, we could get a different result the
|
||||
// second time due to the preferences causing us to skip a fork point (see
|
||||
// "preferences-dependent-forking" packse scenario). To avoid this, we store the forks in the
|
||||
// second time due to the preferences causing us to skip a fork point (see the
|
||||
// `preferences-dependent-forking` packse scenario). To avoid this, we store the forks in the
|
||||
// lockfile. We read those after all the lockfile filters, to allow the forks to change when
|
||||
// the environment changed, e.g. the python bound check above can lead to different forking.
|
||||
let resolver_markers = ResolverMarkers::universal(if upgrade.is_all() {
|
||||
// We're discarding all preferences, so we're also discarding the existing forks.
|
||||
vec![]
|
||||
} else {
|
||||
existing_lock
|
||||
.as_ref()
|
||||
.map(|existing_lock| existing_lock.lock().fork_markers().to_vec())
|
||||
let resolver_markers = ResolverMarkers::universal(
|
||||
reusable_lock
|
||||
.map(|lock| lock.fork_markers().to_vec())
|
||||
.unwrap_or_else(|| {
|
||||
environments
|
||||
.cloned()
|
||||
.map(SupportedEnvironments::into_markers)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Resolve the requirements.
|
||||
let resolution = pip::operations::resolve(
|
||||
|
|
@ -518,7 +557,13 @@ async fn do_lock(
|
|||
|
||||
let previous = existing_lock.map(ValidatedLock::into_lock);
|
||||
let lock = Lock::from_resolution_graph(&resolution)?
|
||||
.with_manifest(ResolverManifest::new(members, constraints, overrides));
|
||||
.with_manifest(ResolverManifest::new(members, constraints, overrides))
|
||||
.with_supported_environments(
|
||||
environments
|
||||
.cloned()
|
||||
.map(SupportedEnvironments::into_markers)
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
Ok(LockResult::Changed(previous, lock))
|
||||
}
|
||||
|
|
@ -544,6 +589,7 @@ impl ValidatedLock {
|
|||
members: &[PackageName],
|
||||
constraints: &[Requirement],
|
||||
overrides: &[Requirement],
|
||||
environments: Option<&SupportedEnvironments>,
|
||||
interpreter: &Interpreter,
|
||||
requires_python: &RequiresPython,
|
||||
index_locations: &IndexLocations,
|
||||
|
|
@ -601,12 +647,32 @@ impl ValidatedLock {
|
|||
}
|
||||
}
|
||||
|
||||
// If the user specified `--upgrade`, then at best we can prefer some of the existing
|
||||
// versions.
|
||||
if !upgrade.is_none() {
|
||||
// If the set of supported environments has changed, we have to perform a clean resolution.
|
||||
if lock.supported_environments()
|
||||
!= environments
|
||||
.map(SupportedEnvironments::as_markers)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let _ = writeln!(
|
||||
printer.stderr(),
|
||||
"Ignoring existing lockfile due to change in supported environments"
|
||||
);
|
||||
return Ok(Self::Unusable(lock));
|
||||
}
|
||||
|
||||
match upgrade {
|
||||
Upgrade::None => {}
|
||||
Upgrade::All => {
|
||||
// If the user specified `--upgrade`, then we can't use the existing lockfile.
|
||||
debug!("Ignoring existing lockfile due to `--upgrade`");
|
||||
return Ok(Self::Unusable(lock));
|
||||
}
|
||||
Upgrade::Packages(_) => {
|
||||
// If the user specified `--upgrade-package`, then at best we can prefer some of
|
||||
// the existing versions.
|
||||
return Ok(Self::Preferable(lock));
|
||||
}
|
||||
}
|
||||
|
||||
// If the Requires-Python bound in the lockfile is weaker or equivalent to the
|
||||
// Requires-Python bound in the workspace, we should have the necessary wheels to perform
|
||||
|
|
@ -627,7 +693,7 @@ impl ValidatedLock {
|
|||
// file), don't use the existing lockfile if it references any registries that are no longer
|
||||
// included in the current configuration.
|
||||
//
|
||||
// However, iIf _no_ indexes were provided, we assume that the user wants to reuse the existing
|
||||
// However, if _no_ indexes were provided, we assume that the user wants to reuse the existing
|
||||
// distributions, even though a failure to reuse the lockfile will result in re-resolving
|
||||
// against PyPI by default.
|
||||
let indexes = if index_locations.is_none() {
|
||||
|
|
@ -707,15 +773,6 @@ impl ValidatedLock {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the inner [`Lock`].
|
||||
fn lock(&self) -> &Lock {
|
||||
match self {
|
||||
ValidatedLock::Unusable(lock) => lock,
|
||||
ValidatedLock::Satisfies(lock) => lock,
|
||||
ValidatedLock::Preferable(lock) => lock,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the [`ValidatedLock`] into a [`Lock`].
|
||||
#[must_use]
|
||||
fn into_lock(self) -> Lock {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ pub(crate) enum ProjectError {
|
|||
#[error("The current Python version ({0}) is not compatible with the locked Python requirement: `{1}`")]
|
||||
LockedPythonIncompatibility(Version, RequiresPython),
|
||||
|
||||
#[error("The current Python platform is not compatible with the lockfile's supported environments: {0}")]
|
||||
LockedPlatformIncompatibility(String),
|
||||
|
||||
#[error("The requested Python interpreter ({0}) is incompatible with the project Python requirement: `{1}`")]
|
||||
RequestedPythonIncompatibility(Version, RequiresPython),
|
||||
|
||||
|
|
@ -72,6 +75,9 @@ pub(crate) enum ProjectError {
|
|||
PathBuf,
|
||||
),
|
||||
|
||||
#[error("Supported environments must be disjoint, but the following markers overlap: `{0}` and `{1}`.\n\n{hint}{colon} replace `{1}` with `{2}`.", hint = "hint".bold().cyan(), colon = ":".bold())]
|
||||
OverlappingMarkers(String, String, String),
|
||||
|
||||
#[error(transparent)]
|
||||
Python(#[from] uv_python::Error),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::{Context, Result};
|
||||
|
||||
use itertools::Itertools;
|
||||
use pep508_rs::MarkerTree;
|
||||
use uv_auth::store_credentials_from_url;
|
||||
use uv_cache::Cache;
|
||||
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||
|
|
@ -169,6 +170,21 @@ pub(super) async fn do_sync(
|
|||
}
|
||||
}
|
||||
|
||||
// Validate that the platform is supported by the lockfile.
|
||||
let environments = lock.supported_environments();
|
||||
if !environments.is_empty() {
|
||||
let platform = venv.interpreter().markers();
|
||||
if !environments.iter().any(|env| env.evaluate(platform, &[])) {
|
||||
return Err(ProjectError::LockedPlatformIncompatibility(
|
||||
environments
|
||||
.iter()
|
||||
.filter_map(MarkerTree::contents)
|
||||
.map(|env| format!("`{env}`"))
|
||||
.join(", "),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Include development dependencies, if requested.
|
||||
let dev = if dev {
|
||||
vec![DEV_DEPENDENCIES.clone()]
|
||||
|
|
|
|||
|
|
@ -9918,3 +9918,359 @@ fn lock_exclude_unnecessary_python_forks() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lock with a user-provided constraint on the space of supported environments.
|
||||
#[test]
|
||||
fn lock_constrained_environment() -> 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 = ["black"]
|
||||
|
||||
[tool.uv]
|
||||
environments = "platform_system != 'Windows'"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
Resolved 7 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
|
||||
// Because we're _not_ locking for Windows, `colorama` should not be included.
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
resolution-markers = [
|
||||
"platform_system != 'Windows'",
|
||||
]
|
||||
supported-markers = [
|
||||
"platform_system != 'Windows'",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25 00:00:00 UTC"
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click", marker = "platform_system != 'Windows'" },
|
||||
{ name = "mypy-extensions", marker = "platform_system != 'Windows'" },
|
||||
{ name = "packaging", marker = "platform_system != 'Windows'" },
|
||||
{ name = "pathspec", marker = "platform_system != 'Windows'" },
|
||||
{ name = "platformdirs", marker = "platform_system != 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/5f/bac24a952668c7482cfdb4ebf91ba57a796c9da8829363a772040c1a3312/black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", size = 634292 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/c6/1d174efa9ff02b22d0124c73fc5f4d4fb006d0d9a081aadc354d05754a13/black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", size = 1600822 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/ed/704731afffe460b8ff0672623b40fce9fe569f2ee617c15857e4d4440a3a/black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", size = 1429987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/05/8dd038e30caadab7120176d4bc109b7ca2f4457f12eef746b0560a583458/black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", size = 1755319 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/9d/e5fa1ff4ef1940be15a64883c0bb8d2fcf626efec996eab4ae5a8c691d2c/black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", size = 1385180 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/ea/31770a7e49f3eedfd8cd7b35e78b3a3aaad860400f8673994bc988318135/black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", size = 201493 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/dc/c1d911bf5bb0fdc58cc05010e9f3efe3b67970cef779ba7fbc3183b987a8/platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768", size = 20055 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/72/4898c44ee9ea6f43396fbc23d9bfaf3d06e01b83698bdf2e4c919deceb7c/platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", size = 17717 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "black", marker = "platform_system != 'Windows'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "black" }]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
Resolved 7 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 -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
Resolved 7 packages in [TIME]
|
||||
"###);
|
||||
|
||||
// Rewrite with a list, rather than a string.
|
||||
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 = ["black"]
|
||||
|
||||
[tool.uv]
|
||||
environments = ["platform_system != 'Windows'"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
Resolved 7 packages in [TIME]
|
||||
"###);
|
||||
|
||||
// Re-lock without the environment constraint.
|
||||
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 = ["black"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Re-run with `--locked`. This should fail.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
Ignoring existing lockfile due to change in supported environments
|
||||
Resolved 8 packages in [TIME]
|
||||
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
Ignoring existing lockfile due to change in supported environments
|
||||
Resolved 8 packages in [TIME]
|
||||
Added colorama v0.4.6
|
||||
"###);
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
|
||||
|
||||
// Because we're locking for Windows, `colorama` should be included.
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25 00:00:00 UTC"
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/5f/bac24a952668c7482cfdb4ebf91ba57a796c9da8829363a772040c1a3312/black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", size = 634292 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/c6/1d174efa9ff02b22d0124c73fc5f4d4fb006d0d9a081aadc354d05754a13/black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", size = 1600822 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/ed/704731afffe460b8ff0672623b40fce9fe569f2ee617c15857e4d4440a3a/black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", size = 1429987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/05/8dd038e30caadab7120176d4bc109b7ca2f4457f12eef746b0560a583458/black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", size = 1755319 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/9d/e5fa1ff4ef1940be15a64883c0bb8d2fcf626efec996eab4ae5a8c691d2c/black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", size = 1385180 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/ea/31770a7e49f3eedfd8cd7b35e78b3a3aaad860400f8673994bc988318135/black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", size = 201493 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/dc/c1d911bf5bb0fdc58cc05010e9f3efe3b67970cef779ba7fbc3183b987a8/platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768", size = 20055 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/72/4898c44ee9ea6f43396fbc23d9bfaf3d06e01b83698bdf2e4c919deceb7c/platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", size = 17717 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "black" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "black" }]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User-provided constraints must be disjoint.
|
||||
#[test]
|
||||
fn lock_overlapping_environment() -> 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.8"
|
||||
dependencies = ["black"]
|
||||
|
||||
[tool.uv]
|
||||
environments = ["platform_system != 'Windows'", "python_version > '3.10'"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv lock` is experimental and may change without warning
|
||||
error: Supported environments must be disjoint, but the following markers overlap: `platform_system != 'Windows'` and `python_full_version >= '3.11'`.
|
||||
|
||||
hint: replace `python_full_version >= '3.11'` with `python_full_version >= '3.11' and platform_system == 'Windows'`.
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -780,3 +780,38 @@ fn sync_relative_wheel() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Syncing against an unstable environment should fail (but locking should succeed).
|
||||
#[test]
|
||||
fn sync_environment() -> 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.10"
|
||||
dependencies = ["iniconfig"]
|
||||
|
||||
[tool.uv]
|
||||
environments = ["python_version < '3.11'"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning
|
||||
Resolved 2 packages in [TIME]
|
||||
error: The current Python platform is not compatible with the lockfile's supported environments: `python_full_version < '3.11'`
|
||||
"###);
|
||||
|
||||
assert!(context.temp_dir.child("uv.lock").exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ To run a command in the project environment, use `uv run`. Alternatively the pro
|
|||
be activated as normal for a virtual environment.
|
||||
|
||||
When `uv run` is invoked, it will create the project environment if it does not exist yet or ensure
|
||||
it is up to date if it exists. The project environment can also be explicitly created with
|
||||
it is up-to-date if it exists. The project environment can also be explicitly created with
|
||||
`uv sync`.
|
||||
|
||||
It is _not_ recommended to modify the project environment manually, e.g., with `uv pip install`. For
|
||||
|
|
@ -63,8 +63,26 @@ use [`uvx`](../guides/tools.md) or
|
|||
|
||||
uv creates a `uv.lock` file next to the `pyproject.toml`.
|
||||
|
||||
`uv.lock` is a _universal_ lockfile that captures the packages that would be installed across all
|
||||
possible Python markers such as operating system, architecture, and Python version.
|
||||
`uv.lock` is a _universal_ or _cross-platform_ lockfile that captures the packages that would be
|
||||
installed across all possible Python markers such as operating system, architecture, and Python
|
||||
version.
|
||||
|
||||
If your project supports a more limited set of platforms or Python versions, you can constrain the
|
||||
set of solved platforms via the `environments` setting, which accepts a list of PEP 508 environment
|
||||
markers. For example, to constrain the lockfile to macOS and Linux, and exclude Windows:
|
||||
|
||||
```toml title="pyproject.toml"
|
||||
[tool.uv]
|
||||
environments = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'linux'",
|
||||
]
|
||||
```
|
||||
|
||||
Entries in the `environments` setting must be disjoint (i.e., they must not overlap). For example,
|
||||
`sys_platform == 'darwin'` and `sys_platform == 'linux'` are disjoint, but
|
||||
`sys_platform == 'darwin'` and `python_version >= '3.9'` are not, since both could be true at the
|
||||
same time.
|
||||
|
||||
Unlike the `pyproject.toml`, which is used to specify the broad requirements of your project, the
|
||||
lockfile contains the exact resolved versions that are installed in the project environment. This
|
||||
|
|
@ -80,11 +98,11 @@ The lockfile is created and updated during uv invocations that use the project e
|
|||
|
||||
`uv.lock` is a human-readable TOML file but is managed by uv and should not be edited manually.
|
||||
There is no Python standard for lockfiles at this time, so the format of this file is specific to uv
|
||||
and not generally usable by other tools.
|
||||
and not usable by other tools.
|
||||
|
||||
To avoid updating the lockfile during `uv sync` and `uv run` invocations, use the `--frozen` flag.
|
||||
|
||||
To assert the lockfile is up to date, use the `--locked` flag. If the lockfile is not up to date, an
|
||||
To assert the lockfile is up-to-date, use the `--locked` flag. If the lockfile is not up-to-date, an
|
||||
error will be raised instead of updating the lockfile.
|
||||
|
||||
## Managing dependencies
|
||||
|
|
@ -146,7 +164,7 @@ environment:
|
|||
$ uv run python -c "import example"
|
||||
```
|
||||
|
||||
When using `run`, uv will ensure that the project environment is up to date before running the given
|
||||
When using `run`, uv will ensure that the project environment is up-to-date before running the given
|
||||
command.
|
||||
|
||||
The given command can be provided by the project environment or exist outside of it, e.g.:
|
||||
|
|
|
|||
|
|
@ -87,6 +87,63 @@ specified as `KEY=VALUE` pairs.
|
|||
|
||||
---
|
||||
|
||||
#### [`dev-dependencies`](#dev-dependencies) {: #dev-dependencies }
|
||||
|
||||
The project's development dependencies. Development dependencies will be installed by
|
||||
default in `uv run` and `uv sync`, but will not appear in the project's published metadata.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `list[str]`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.uv]
|
||||
dev_dependencies = ["ruff==0.5.0"]
|
||||
```
|
||||
=== "uv.toml"
|
||||
|
||||
```toml
|
||||
|
||||
dev_dependencies = ["ruff==0.5.0"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`environments`](#environments) {: #environments }
|
||||
|
||||
A list of supported environments against which to resolve dependencies.
|
||||
|
||||
By default, uv will resolve for all possible environments during a `uv lock` operation.
|
||||
However, you can restrict the set of supported environments to improve performance and avoid
|
||||
unsatisfiable branches in the solution space.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `str | list[str]`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.uv]
|
||||
# Resolve for macOS, but not for Linux or Windows.
|
||||
environments = ["sys_platform == 'darwin'"]
|
||||
```
|
||||
=== "uv.toml"
|
||||
|
||||
```toml
|
||||
|
||||
# Resolve for macOS, but not for Linux or Windows.
|
||||
environments = ["sys_platform == 'darwin'"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`exclude-newer`](#exclude-newer) {: #exclude-newer }
|
||||
|
||||
Limit candidate packages to those that were uploaded prior to the given date.
|
||||
|
|
|
|||
12
uv.schema.json
generated
12
uv.schema.json
generated
|
|
@ -48,6 +48,16 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"environments": {
|
||||
"description": "A list of environment markers, e.g. `python_version >= '3.6'`.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"exclude-newer": {
|
||||
"description": "Limit candidate packages to those that were uploaded prior to the given date.\n\nAccepts both [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same format (e.g., `2006-12-02`).",
|
||||
"anyOf": [
|
||||
|
|
@ -217,7 +227,7 @@
|
|||
]
|
||||
},
|
||||
"override-dependencies": {
|
||||
"description": "PEP 508 style requirements, e.g. `ruff==0.5.0`, or `ruff @ https://...`.",
|
||||
"description": "PEP 508-style requirements, e.g. `ruff==0.5.0`, or `ruff @ https://...`.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue