Add constraint dependencies to pyproject.toml (#5248)

Resolves #4467.

## Summary

This PR implements the following

1. Add `tool.uv.constraint-dependencies` to pyproject.toml
1. Support to refer `tool.uv.constraint-dependencies` in `uv lock`
1. Support to refer `tool.uv.constraint-dependencies` in `uv pip
compile/install`

These are analogues of the override features implemented in #3839 and
#4369.

## Test Plan

Add test.
This commit is contained in:
Di-Is 2024-07-22 08:45:04 +09:00 committed by GitHub
parent a917cdba51
commit 32ad3323a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 243 additions and 1 deletions

View file

@ -48,6 +48,7 @@ pub struct Options {
)
)]
pub override_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
pub constraint_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
}
/// Global settings, relevant to all invocations.

View file

@ -104,6 +104,7 @@ pub struct ToolUv {
)
)]
pub override_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
pub constraint_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
}
#[derive(Serialize, Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]

View file

@ -254,6 +254,38 @@ impl Workspace {
.collect()
}
/// Returns the set of constraints for the workspace.
pub fn constraints(&self) -> Vec<Requirement> {
let Some(workspace_package) = self
.packages
.values()
.find(|workspace_package| workspace_package.root() == self.install_path())
else {
return vec![];
};
let Some(constraints) = workspace_package
.pyproject_toml()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.constraint_dependencies.as_ref())
else {
return vec![];
};
constraints
.iter()
.map(|requirement| {
Requirement::from(
requirement
.clone()
.with_origin(RequirementOrigin::Workspace),
)
})
.collect()
}
/// The path to the workspace root, the directory containing the top level `pyproject.toml` with
/// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project.
pub fn install_path(&self) -> &PathBuf {

View file

@ -48,6 +48,7 @@ pub(crate) async fn pip_compile(
requirements: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
constraints_from_workspace: Vec<Requirement>,
overrides_from_workspace: Vec<Requirement>,
extras: ExtrasSpecification,
output_file: Option<&Path>,
@ -126,6 +127,12 @@ pub(crate) async fn pip_compile(
)
.await?;
let constraints = constraints
.iter()
.cloned()
.chain(constraints_from_workspace.into_iter())
.collect();
let overrides: Vec<UnresolvedRequirementSpecification> = overrides
.iter()
.cloned()

View file

@ -40,6 +40,7 @@ pub(crate) async fn pip_install(
requirements: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
constraints_from_workspace: Vec<Requirement>,
overrides_from_workspace: Vec<Requirement>,
extras: &ExtrasSpecification,
resolution_mode: ResolutionMode,
@ -104,6 +105,12 @@ pub(crate) async fn pip_install(
)
.await?;
let constraints: Vec<Requirement> = constraints
.iter()
.cloned()
.chain(constraints_from_workspace.into_iter())
.collect();
let overrides: Vec<UnresolvedRequirementSpecification> = overrides
.iter()
.cloned()

View file

@ -211,7 +211,7 @@ pub(super) async fn do_lock(
.into_iter()
.map(UnresolvedRequirementSpecification::from)
.collect::<Vec<_>>();
let constraints = vec![];
let constraints = workspace.constraints();
let dev = vec![DEV_DEPENDENCIES.clone()];
let source_trees = vec![];

View file

@ -200,6 +200,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
&requirements,
&constraints,
&overrides,
args.constraints_from_workspace,
args.overrides_from_workspace,
args.settings.extras,
args.settings.output_file.as_deref(),
@ -349,6 +350,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
&requirements,
&constraints,
&overrides,
args.constraints_from_workspace,
args.overrides_from_workspace,
&args.settings.extras,
args.settings.resolution,

View file

@ -763,6 +763,7 @@ pub(crate) struct PipCompileSettings {
pub(crate) src_file: Vec<PathBuf>,
pub(crate) constraint: Vec<PathBuf>,
pub(crate) r#override: Vec<PathBuf>,
pub(crate) constraints_from_workspace: Vec<Requirement>,
pub(crate) overrides_from_workspace: Vec<Requirement>,
pub(crate) refresh: Refresh,
pub(crate) settings: PipSettings,
@ -824,6 +825,20 @@ impl PipCompileSettings {
compat_args: _,
} = args;
let constraints_from_workspace = if let Some(configuration) = &filesystem {
configuration
.constraint_dependencies
.clone()
.unwrap_or_default()
.into_iter()
.map(|requirement| {
Requirement::from(requirement.with_origin(RequirementOrigin::Workspace))
})
.collect()
} else {
Vec::new()
};
let overrides_from_workspace = if let Some(configuration) = &filesystem {
configuration
.override_dependencies
@ -848,6 +863,7 @@ impl PipCompileSettings {
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
constraints_from_workspace,
overrides_from_workspace,
refresh: Refresh::from(refresh),
settings: PipSettings::combine(
@ -988,6 +1004,7 @@ pub(crate) struct PipInstallSettings {
pub(crate) constraint: Vec<PathBuf>,
pub(crate) r#override: Vec<PathBuf>,
pub(crate) dry_run: bool,
pub(crate) constraints_from_workspace: Vec<Requirement>,
pub(crate) overrides_from_workspace: Vec<Requirement>,
pub(crate) refresh: Refresh,
pub(crate) settings: PipSettings,
@ -1036,6 +1053,20 @@ impl PipInstallSettings {
compat_args: _,
} = args;
let constraints_from_workspace = if let Some(configuration) = &filesystem {
configuration
.constraint_dependencies
.clone()
.unwrap_or_default()
.into_iter()
.map(|requirement| {
Requirement::from(requirement.with_origin(RequirementOrigin::Workspace))
})
.collect()
} else {
Vec::new()
};
let overrides_from_workspace = if let Some(configuration) = &filesystem {
configuration
.override_dependencies
@ -1063,6 +1094,7 @@ impl PipInstallSettings {
.filter_map(Maybe::into_option)
.collect(),
dry_run,
constraints_from_workspace,
overrides_from_workspace,
refresh: Refresh::from(refresh),
settings: PipSettings::combine(

View file

@ -644,6 +644,55 @@ fn lock_project_with_overrides() -> Result<()> {
Ok(())
}
/// Lock a project with a uv.tool.constraint-dependencies.
#[test]
fn lock_project_with_constraints() -> 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"]
[tool.uv]
constraint-dependencies = ["idna<3.4"]
"#,
)?;
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 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 -----
warning: `uv sync` is experimental and may change without warning
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==3.7.0
+ idna==3.3
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
Ok(())
}
/// Lock a project with a dependency that has an extra.
#[test]
fn lock_dependency_extra() -> Result<()> {

View file

@ -3043,6 +3043,50 @@ fn override_dependency_from_pyproject() -> Result<()> {
Ok(())
}
/// Check that `tool.uv.constraint-dependencies` in `pyproject.toml` is respected.
#[test]
fn constraint_dependency_from_pyproject() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==3.7.0"
]
[tool.uv]
constraint-dependencies = [
"idna<3.4"
]
"#,
)?;
uv_snapshot!(context.pip_compile()
.arg("pyproject.toml"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] pyproject.toml
anyio==3.7.0
# via example (pyproject.toml)
idna==3.3
# via
# -c (workspace)
# anyio
sniffio==1.3.1
# via anyio
----- stderr -----
Resolved 3 packages in [TIME]
"###
);
Ok(())
}
/// Check that `override-dependencies` in `uv.toml` is respected.
#[test]
fn override_dependency_from_specific_uv_toml() -> Result<()> {

View file

@ -2429,6 +2429,46 @@ fn install_constraints_txt() -> Result<()> {
Ok(())
}
/// Check that `tool.uv.constraint-dependencies` in `pyproject.toml` is respected.
#[test]
fn install_constraints_from_pyproject() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==3.7.0"
]
[tool.uv]
constraint-dependencies = [
"idna<3.4"
]
"#,
)?;
uv_snapshot!(context.pip_install()
.arg("-r")
.arg("pyproject.toml"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==3.7.0
+ idna==3.3
+ sniffio==1.3.1
"###
);
Ok(())
}
/// Install a package from a `requirements.txt` file, with an inline constraint.
#[test]
fn install_constraints_inline() -> Result<()> {

View file

@ -73,6 +73,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -206,6 +207,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -340,6 +342,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -506,6 +509,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -641,6 +645,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -762,6 +767,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -920,6 +926,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -1078,6 +1085,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -1281,6 +1289,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -1438,6 +1447,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -1565,6 +1575,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -1720,6 +1731,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -1899,6 +1911,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -2016,6 +2029,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -2133,6 +2147,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -2252,6 +2267,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -2396,6 +2412,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(
@ -2541,6 +2558,7 @@ fn resolve_both() -> anyhow::Result<()> {
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
refresh: None(
Timestamp(

9
uv.schema.json generated
View file

@ -29,6 +29,15 @@
}
]
},
"constraint-dependencies": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Requirement"
}
},
"dev-dependencies": {
"description": "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`.",
"type": [