Respect sources in overrides and constraints (#9455)

## Summary

We still only respect overrides and constraints in the workspace root --
which we may want to change -- but overrides and constraints are now
correctly lowered.

Closes https://github.com/astral-sh/uv/issues/8148.
This commit is contained in:
Charlie Marsh 2024-11-27 08:56:14 -05:00 committed by GitHub
parent 8c8a1f071c
commit 7169b2c427
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 413 additions and 117 deletions

View file

@ -465,13 +465,12 @@ impl SourceBuild {
.or(package_name) .or(package_name)
{ {
let build_requires = uv_pypi_types::BuildRequires { let build_requires = uv_pypi_types::BuildRequires {
name: name.clone(), name: Some(name.clone()),
requires_dist: build_system.requires, requires_dist: build_system.requires,
}; };
let build_requires = BuildRequires::from_project_maybe_workspace( let build_requires = BuildRequires::from_project_maybe_workspace(
build_requires, build_requires,
install_path, install_path,
None,
locations, locations,
source_strategy, source_strategy,
LowerBound::Allow, LowerBound::Allow,
@ -905,25 +904,20 @@ async fn create_pep517_build_environment(
// If necessary, lower the requirements. // If necessary, lower the requirements.
let extra_requires = match source_strategy { let extra_requires = match source_strategy {
SourceStrategy::Enabled => { SourceStrategy::Enabled => {
if let Some(package_name) = package_name { let build_requires = uv_pypi_types::BuildRequires {
let build_requires = uv_pypi_types::BuildRequires { name: package_name.cloned(),
name: package_name.clone(), requires_dist: extra_requires,
requires_dist: extra_requires, };
}; let build_requires = BuildRequires::from_project_maybe_workspace(
let build_requires = BuildRequires::from_project_maybe_workspace( build_requires,
build_requires, install_path,
install_path, locations,
None, source_strategy,
locations, LowerBound::Allow,
source_strategy, )
LowerBound::Allow, .await
) .map_err(Error::Lowering)?;
.await build_requires.requires_dist
.map_err(Error::Lowering)?;
build_requires.requires_dist
} else {
extra_requires.into_iter().map(Requirement::from).collect()
}
} }
SourceStrategy::Disabled => extra_requires.into_iter().map(Requirement::from).collect(), SourceStrategy::Disabled => extra_requires.into_iter().map(Requirement::from).collect(),
}; };

View file

@ -5,14 +5,14 @@ use uv_configuration::{LowerBound, SourceStrategy};
use uv_distribution_types::IndexLocations; use uv_distribution_types::IndexLocations;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_workspace::pyproject::ToolUvSources; use uv_workspace::pyproject::ToolUvSources;
use uv_workspace::{DiscoveryOptions, ProjectWorkspace}; use uv_workspace::{DiscoveryOptions, ProjectWorkspace, Workspace};
use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError}; use crate::metadata::{LoweredRequirement, MetadataError};
/// Lowered requirements from a `[build-system.requires]` field in a `pyproject.toml` file. /// Lowered requirements from a `[build-system.requires]` field in a `pyproject.toml` file.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BuildRequires { pub struct BuildRequires {
pub name: PackageName, pub name: Option<PackageName>,
pub requires_dist: Vec<uv_pypi_types::Requirement>, pub requires_dist: Vec<uv_pypi_types::Requirement>,
} }
@ -35,27 +35,14 @@ impl BuildRequires {
pub async fn from_project_maybe_workspace( pub async fn from_project_maybe_workspace(
metadata: uv_pypi_types::BuildRequires, metadata: uv_pypi_types::BuildRequires,
install_path: &Path, install_path: &Path,
git_member: Option<&GitWorkspaceMember<'_>>,
locations: &IndexLocations, locations: &IndexLocations,
sources: SourceStrategy, sources: SourceStrategy,
lower_bound: LowerBound, lower_bound: LowerBound,
) -> Result<Self, MetadataError> { ) -> Result<Self, MetadataError> {
// TODO(konsti): Cache workspace discovery. // TODO(konsti): Cache workspace discovery.
let discovery_options = if let Some(git_member) = &git_member {
DiscoveryOptions {
stop_discovery_at: Some(
git_member
.fetch_root
.parent()
.expect("git checkout has a parent"),
),
..Default::default()
}
} else {
DiscoveryOptions::default()
};
let Some(project_workspace) = let Some(project_workspace) =
ProjectWorkspace::from_maybe_project_root(install_path, &discovery_options).await? ProjectWorkspace::from_maybe_project_root(install_path, &DiscoveryOptions::default())
.await?
else { else {
return Ok(Self::from_metadata23(metadata)); return Ok(Self::from_metadata23(metadata));
}; };
@ -63,7 +50,6 @@ impl BuildRequires {
Self::from_project_workspace( Self::from_project_workspace(
metadata, metadata,
&project_workspace, &project_workspace,
git_member,
locations, locations,
sources, sources,
lower_bound, lower_bound,
@ -71,10 +57,9 @@ impl BuildRequires {
} }
/// Lower the `build-system.requires` field from a `pyproject.toml` file. /// Lower the `build-system.requires` field from a `pyproject.toml` file.
fn from_project_workspace( pub fn from_project_workspace(
metadata: uv_pypi_types::BuildRequires, metadata: uv_pypi_types::BuildRequires,
project_workspace: &ProjectWorkspace, project_workspace: &ProjectWorkspace,
git_member: Option<&GitWorkspaceMember<'_>>,
locations: &IndexLocations, locations: &IndexLocations,
source_strategy: SourceStrategy, source_strategy: SourceStrategy,
lower_bound: LowerBound, lower_bound: LowerBound,
@ -118,7 +103,7 @@ impl BuildRequires {
let group = None; let group = None;
LoweredRequirement::from_requirement( LoweredRequirement::from_requirement(
requirement, requirement,
&metadata.name, metadata.name.as_ref(),
project_workspace.project_root(), project_workspace.project_root(),
project_sources, project_sources,
project_indexes, project_indexes,
@ -127,7 +112,84 @@ impl BuildRequires {
locations, locations,
project_workspace.workspace(), project_workspace.workspace(),
lower_bound, lower_bound,
git_member, None,
)
.map(move |requirement| match requirement {
Ok(requirement) => Ok(requirement.into_inner()),
Err(err) => Err(MetadataError::LoweringError(
requirement_name.clone(),
Box::new(err),
)),
})
})
.collect::<Result<Vec<_>, _>>()?,
SourceStrategy::Disabled => requires_dist
.into_iter()
.map(uv_pypi_types::Requirement::from)
.collect(),
};
Ok(Self {
name: metadata.name,
requires_dist,
})
}
/// Lower the `build-system.requires` field from a `pyproject.toml` file.
pub fn from_workspace(
metadata: uv_pypi_types::BuildRequires,
workspace: &Workspace,
locations: &IndexLocations,
source_strategy: SourceStrategy,
lower_bound: LowerBound,
) -> Result<Self, MetadataError> {
// Collect any `tool.uv.index` entries.
let empty = vec![];
let project_indexes = match source_strategy {
SourceStrategy::Enabled => workspace
.pyproject_toml()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.index.as_deref())
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};
// Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`.
let empty = BTreeMap::default();
let project_sources = match source_strategy {
SourceStrategy::Enabled => workspace
.pyproject_toml()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.map(ToolUvSources::inner)
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};
// Lower the requirements.
let requires_dist = metadata.requires_dist.into_iter();
let requires_dist = match source_strategy {
SourceStrategy::Enabled => requires_dist
.flat_map(|requirement| {
let requirement_name = requirement.name.clone();
let extra = requirement.marker.top_level_extra_name();
let group = None;
LoweredRequirement::from_requirement(
requirement,
None,
workspace.install_path(),
project_sources,
project_indexes,
extra.as_ref(),
group,
locations,
workspace,
lower_bound,
None,
) )
.map(move |requirement| match requirement { .map(move |requirement| match requirement {
Ok(requirement) => Ok(requirement.into_inner()), Ok(requirement) => Ok(requirement.into_inner()),

View file

@ -37,7 +37,7 @@ impl LoweredRequirement {
/// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`. /// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
pub(crate) fn from_requirement<'data>( pub(crate) fn from_requirement<'data>(
requirement: uv_pep508::Requirement<VerbatimParsedUrl>, requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
project_name: &'data PackageName, project_name: Option<&'data PackageName>,
project_dir: &'data Path, project_dir: &'data Path,
project_sources: &'data BTreeMap<PackageName, Sources>, project_sources: &'data BTreeMap<PackageName, Sources>,
project_indexes: &'data [Index], project_indexes: &'data [Index],
@ -89,7 +89,7 @@ impl LoweredRequirement {
})) }))
// ... except for recursive self-inclusion (extras that activate other extras), e.g. // ... except for recursive self-inclusion (extras that activate other extras), e.g.
// `framework[machine_learning]` depends on `framework[cuda]`. // `framework[machine_learning]` depends on `framework[cuda]`.
|| &requirement.name == project_name; || project_name.is_some_and(|project_name| *project_name == requirement.name);
if !workspace_package_declared { if !workspace_package_declared {
return Either::Left(std::iter::once(Err( return Either::Left(std::iter::once(Err(
LoweringError::UndeclaredWorkspacePackage, LoweringError::UndeclaredWorkspacePackage,
@ -102,7 +102,7 @@ impl LoweredRequirement {
// Support recursive editable inclusions. // Support recursive editable inclusions.
if has_sources if has_sources
&& requirement.version_or_url.is_none() && requirement.version_or_url.is_none()
&& &requirement.name != project_name && !project_name.is_some_and(|project_name| *project_name == requirement.name)
{ {
warn_user_once!( warn_user_once!(
"Missing version constraint (e.g., a lower bound) for `{}`", "Missing version constraint (e.g., a lower bound) for `{}`",
@ -211,11 +211,15 @@ impl LoweredRequirement {
index, index,
)); ));
}; };
let conflict = if let Some(extra) = extra { let conflict = project_name.and_then(|project_name| {
Some(ConflictItem::from((project_name.clone(), extra))) if let Some(extra) = extra {
} else { Some(ConflictItem::from((project_name.clone(), extra)))
group.map(|group| ConflictItem::from((project_name.clone(), group))) } else {
}; group.map(|group| {
ConflictItem::from((project_name.clone(), group))
})
}
});
let source = registry_source( let source = registry_source(
&requirement, &requirement,
index.into_url(), index.into_url(),

View file

@ -1,8 +1,6 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError};
use crate::Metadata;
use uv_configuration::{LowerBound, SourceStrategy}; use uv_configuration::{LowerBound, SourceStrategy};
use uv_distribution_types::IndexLocations; use uv_distribution_types::IndexLocations;
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES}; use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
@ -10,6 +8,9 @@ use uv_workspace::dependency_groups::FlatDependencyGroups;
use uv_workspace::pyproject::{Sources, ToolUvSources}; use uv_workspace::pyproject::{Sources, ToolUvSources};
use uv_workspace::{DiscoveryOptions, ProjectWorkspace}; use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError};
use crate::Metadata;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RequiresDist { pub struct RequiresDist {
pub name: PackageName, pub name: PackageName,
@ -164,7 +165,7 @@ impl RequiresDist {
let extra = None; let extra = None;
LoweredRequirement::from_requirement( LoweredRequirement::from_requirement(
requirement, requirement,
&metadata.name, Some(&metadata.name),
project_workspace.project_root(), project_workspace.project_root(),
project_sources, project_sources,
project_indexes, project_indexes,
@ -209,7 +210,7 @@ impl RequiresDist {
let group = None; let group = None;
LoweredRequirement::from_requirement( LoweredRequirement::from_requirement(
requirement, requirement,
&metadata.name, Some(&metadata.name),
project_workspace.project_root(), project_workspace.project_root(),
project_sources, project_sources,
project_indexes, project_indexes,

View file

@ -8,6 +8,6 @@ use crate::VerbatimParsedUrl;
/// See: <https://peps.python.org/pep-0518/> /// See: <https://peps.python.org/pep-0518/>
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BuildRequires { pub struct BuildRequires {
pub name: PackageName, pub name: Option<PackageName>,
pub requires_dist: Vec<Requirement<VerbatimParsedUrl>>, pub requires_dist: Vec<Requirement<VerbatimParsedUrl>>,
} }

View file

@ -86,6 +86,15 @@ impl Requirement {
let fragment = url.fragment()?; let fragment = url.fragment()?;
Hashes::parse_fragment(fragment).ok() Hashes::parse_fragment(fragment).ok()
} }
/// Set the source file containing the requirement.
#[must_use]
pub fn with_origin(self, origin: RequirementOrigin) -> Self {
Self {
origin: Some(origin),
..self
}
}
} }
impl From<Requirement> for uv_pep508::Requirement<VerbatimUrl> { impl From<Requirement> for uv_pep508::Requirement<VerbatimUrl> {

View file

@ -6,12 +6,13 @@ use std::path::{Path, PathBuf};
use glob::{glob, GlobError, PatternError}; use glob::{glob, GlobError, PatternError};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use uv_distribution_types::Index; use uv_distribution_types::Index;
use uv_fs::{Simplified, CWD}; use uv_fs::{Simplified, CWD};
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES}; use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
use uv_pep508::{MarkerTree, RequirementOrigin, VerbatimUrl}; use uv_pep508::{MarkerTree, VerbatimUrl};
use uv_pypi_types::{Conflicts, Requirement, RequirementSource, SupportedEnvironments}; use uv_pypi_types::{
Conflicts, Requirement, RequirementSource, SupportedEnvironments, VerbatimParsedUrl,
};
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
@ -307,6 +308,24 @@ impl Workspace {
}) })
} }
/// Returns the set of supported environments for the workspace.
pub fn environments(&self) -> Option<&SupportedEnvironments> {
self.pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.environments.as_ref())
}
/// Returns the set of conflicts for the workspace.
pub fn conflicts(&self) -> Conflicts {
let mut conflicting = Conflicts::empty();
for member in self.packages.values() {
conflicting.append(&mut member.pyproject_toml.conflicts());
}
conflicting
}
/// Returns any requirements that are exclusive to the workspace root, i.e., not included in /// Returns any requirements that are exclusive to the workspace root, i.e., not included in
/// any of the workspace members. /// any of the workspace members.
/// ///
@ -314,7 +333,9 @@ impl Workspace {
/// `pyproject.toml`. /// `pyproject.toml`.
/// ///
/// Otherwise, returns an empty list. /// Otherwise, returns an empty list.
pub fn non_project_requirements(&self) -> Result<Vec<Requirement>, DependencyGroupError> { pub fn non_project_requirements(
&self,
) -> Result<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>, DependencyGroupError> {
if self if self
.packages .packages
.values() .values()
@ -350,16 +371,7 @@ impl Workspace {
let dev_dependencies = dependency_groups let dev_dependencies = dependency_groups
.into_iter() .into_iter()
.flat_map(|(_, requirements)| requirements) .flat_map(|(_, requirements)| requirements)
.map(|requirement| { .chain(dev_dependencies.into_iter().flatten().cloned())
Requirement::from(requirement.with_origin(RequirementOrigin::Workspace))
})
.chain(dev_dependencies.into_iter().flatten().map(|requirement| {
Requirement::from(
requirement
.clone()
.with_origin(RequirementOrigin::Workspace),
)
}))
.collect(); .collect();
Ok(dev_dependencies) Ok(dev_dependencies)
@ -367,7 +379,7 @@ impl Workspace {
} }
/// Returns the set of overrides for the workspace. /// Returns the set of overrides for the workspace.
pub fn overrides(&self) -> Vec<Requirement> { pub fn overrides(&self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
let Some(overrides) = self let Some(overrides) = self
.pyproject_toml .pyproject_toml
.tool .tool
@ -377,39 +389,11 @@ impl Workspace {
else { else {
return vec![]; return vec![];
}; };
overrides.clone()
overrides
.iter()
.map(|requirement| {
Requirement::from(
requirement
.clone()
.with_origin(RequirementOrigin::Workspace),
)
})
.collect()
}
/// Returns the set of supported environments for the workspace.
pub fn environments(&self) -> Option<&SupportedEnvironments> {
self.pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.environments.as_ref())
}
/// Returns the set of conflicts for the workspace.
pub fn conflicts(&self) -> Conflicts {
let mut conflicting = Conflicts::empty();
for member in self.packages.values() {
conflicting.append(&mut member.pyproject_toml.conflicts());
}
conflicting
} }
/// Returns the set of constraints for the workspace. /// Returns the set of constraints for the workspace.
pub fn constraints(&self) -> Vec<Requirement> { pub fn constraints(&self) -> Vec<uv_pep508::Requirement<VerbatimParsedUrl>> {
let Some(constraints) = self let Some(constraints) = self
.pyproject_toml .pyproject_toml
.tool .tool
@ -419,17 +403,7 @@ impl Workspace {
else { else {
return vec![]; return vec![];
}; };
constraints.clone()
constraints
.iter()
.map(|requirement| {
Requirement::from(
requirement
.clone()
.with_origin(RequirementOrigin::Workspace),
)
})
.collect()
} }
/// Returns the set of all dependency group names defined in the workspace. /// Returns the set of all dependency group names defined in the workspace.

View file

@ -11,7 +11,8 @@ use tracing::debug;
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{ use uv_configuration::{
Concurrency, Constraints, ExtrasSpecification, LowerBound, Reinstall, TrustedHost, Upgrade, Concurrency, Constraints, ExtrasSpecification, LowerBound, Reinstall, SourceStrategy,
TrustedHost, Upgrade,
}; };
use uv_dispatch::BuildDispatch; use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase; use uv_distribution::DistributionDatabase;
@ -22,7 +23,8 @@ use uv_distribution_types::{
use uv_git::ResolvedRepositoryReference; use uv_git::ResolvedRepositoryReference;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
use uv_pypi_types::{Requirement, SupportedEnvironments}; use uv_pep508::RequirementOrigin;
use uv_pypi_types::{Requirement, SupportedEnvironments, VerbatimParsedUrl};
use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements}; use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements};
use uv_requirements::ExtrasResolver; use uv_requirements::ExtrasResolver;
@ -323,11 +325,16 @@ async fn do_lock(
// Collect the requirements, etc. // Collect the requirements, etc.
let requirements = workspace.non_project_requirements()?; let requirements = workspace.non_project_requirements()?;
let overrides = workspace.overrides().into_iter().collect::<Vec<_>>(); let overrides = workspace.overrides();
let constraints = workspace.constraints(); let constraints = workspace.constraints();
let dev = workspace.groups().into_iter().cloned().collect::<Vec<_>>(); let dev = workspace.groups().into_iter().cloned().collect::<Vec<_>>();
let source_trees = vec![]; let source_trees = vec![];
// If necessary, lower the overrides and constraints.
let requirements = lower(requirements, workspace, index_locations, sources)?;
let overrides = lower(overrides, workspace, index_locations, sources)?;
let constraints = lower(constraints, workspace, index_locations, sources)?;
// Collect the list of members. // Collect the list of members.
let members = { let members = {
let mut members = workspace.packages().keys().cloned().collect::<Vec<_>>(); let mut members = workspace.packages().keys().cloned().collect::<Vec<_>>();
@ -1116,3 +1123,36 @@ fn report_upgrades(
Ok(updated) Ok(updated)
} }
/// Lower a set of requirements, relative to the workspace root.
fn lower(
requirements: Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
workspace: &Workspace,
locations: &IndexLocations,
sources: SourceStrategy,
) -> Result<Vec<Requirement>, uv_distribution::MetadataError> {
let name = workspace
.pyproject_toml()
.project
.as_ref()
.map(|project| project.name.clone());
// We model these as `build-requires`, since, like build requirements, it doesn't define extras
// or dependency groups.
let metadata = uv_distribution::BuildRequires::from_workspace(
uv_pypi_types::BuildRequires {
name,
requires_dist: requirements,
},
workspace,
locations,
sources,
LowerBound::Warn,
)?;
Ok(metadata
.requires_dist
.into_iter()
.map(|requirement| requirement.with_origin(RequirementOrigin::Workspace))
.collect::<Vec<_>>())
}

View file

@ -196,6 +196,9 @@ pub(crate) enum ProjectError {
#[error(transparent)] #[error(transparent)]
Requirements(#[from] uv_requirements::Error), Requirements(#[from] uv_requirements::Error),
#[error(transparent)]
Metadata(#[from] uv_distribution::MetadataError),
#[error(transparent)] #[error(transparent)]
PyprojectMut(#[from] uv_workspace::pyproject_mut::Error), PyprojectMut(#[from] uv_workspace::pyproject_mut::Error),

View file

@ -1369,6 +1369,7 @@ fn lock_project_extra() -> Result<()> {
Ok(()) Ok(())
} }
/// Lock a project with `uv.tool.override-dependencies`.
#[test] #[test]
fn lock_project_with_overrides() -> Result<()> { fn lock_project_with_overrides() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
@ -1432,6 +1433,69 @@ fn lock_project_with_overrides() -> Result<()> {
Ok(()) Ok(())
} }
/// Lock a project with `uv.tool.override-dependencies` that reference `tool.uv.sources`.
#[test]
fn lock_project_with_override_sources() -> 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]
override-dependencies = ["idna==3.2"]
[tool.uv.sources]
idna = { url = "https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl" }
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
// Install the base dependencies 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 4 packages in [TIME]
+ anyio==3.7.0
+ idna==3.2 (from https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl)
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
Ok(())
}
/// Lock a project with `uv.tool.constraint-dependencies`. /// Lock a project with `uv.tool.constraint-dependencies`.
#[test] #[test]
fn lock_project_with_constraints() -> Result<()> { fn lock_project_with_constraints() -> Result<()> {
@ -1492,6 +1556,69 @@ fn lock_project_with_constraints() -> Result<()> {
Ok(()) Ok(())
} }
/// Lock a project with `uv.tool.constraint-dependencies` that reference `tool.uv.sources`.
#[test]
fn lock_project_with_constraint_sources() -> 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]
constraint-dependencies = ["idna<3.4"]
[tool.uv.sources]
idna = { url = "https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl" }
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
// Install the base dependencies 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 4 packages in [TIME]
+ anyio==3.7.0
+ idna==3.2 (from https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl)
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
Ok(())
}
/// Lock a project with a dependency that has an extra. /// Lock a project with a dependency that has an extra.
#[test] #[test]
fn lock_dependency_extra() -> Result<()> { fn lock_dependency_extra() -> Result<()> {
@ -14580,6 +14707,88 @@ fn lock_non_project_group() -> Result<()> {
Ok(()) Ok(())
} }
/// Lock a (legacy) non-project workspace root with `tool.uv.sources`.
#[test]
fn lock_non_project_sources() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[tool.uv.workspace]
members = []
[tool.uv]
dev-dependencies = ["idna"]
[tool.uv.sources]
idna = { url = "https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl" }
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
Resolved 1 package in [TIME]
"###);
let lock = context.read("uv.lock");
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]
requirements = [{ name = "idna", url = "https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl" }]
[[package]]
name = "idna"
version = "3.2"
source = { url = "https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a" },
]
"###
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
Resolved 1 package 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: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
Resolved 1 package in [TIME]
"###);
Ok(())
}
/// `coverage` defines a `toml` extra, but it doesn't enable any dependencies after Python 3.11. /// `coverage` defines a `toml` extra, but it doesn't enable any dependencies after Python 3.11.
#[test] #[test]
fn lock_dropped_dev_extra() -> Result<()> { fn lock_dropped_dev_extra() -> Result<()> {

View file

@ -1999,7 +1999,7 @@ fn sync_group_legacy_non_project_member() -> Result<()> {
"child", "child",
] ]
requirements = [ requirements = [
{ name = "child" }, { name = "child", editable = "child" },
{ name = "typing-extensions", specifier = ">=4" }, { name = "typing-extensions", specifier = ">=4" },
] ]