Skip to content

Commit

Permalink
Add constraint dependencies to pyproject.toml (#5248)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Di-Is authored Jul 21, 2024
1 parent a917cdb commit 32ad332
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 1 deletion.
1 change: 1 addition & 0 deletions crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions crates/uv-workspace/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
32 changes: 32 additions & 0 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions crates/uv/src/commands/pip/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions crates/uv/src/commands/pip/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![];

Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 32 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
49 changes: 49 additions & 0 deletions crates/uv/tests/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down
44 changes: 44 additions & 0 deletions crates/uv/tests/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down
40 changes: 40 additions & 0 deletions crates/uv/tests/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down
Loading

0 comments on commit 32ad332

Please sign in to comment.