mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-20 03:49:54 +00:00
Add override namespace to pyproject.toml/uv.toml (#3839)
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary See #3834 . This PR adds a new namespace, `override-dependencies`, to pyproject.toml/uv.toml. This namespace assumes that the dependencies you want to override are written in the form of `requirements.txt`. a example of pyproject.toml ```toml [project] name = "example" version = "0.0.0" dependencies = [ "flask==3.0.0" ] [tool.uv] override-dependencies = [ "werkzeug==2.3.0" ] ``` This will improve usability by allowing you to override dependencies without having to specify the --override option when running `uv pip compile/install`. ## Test Plan added test to `crates/uv/tests/pip_compile.rs`. --------- Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
parent
1b769b054c
commit
5c776939d2
13 changed files with 293 additions and 8 deletions
|
|
@ -25,13 +25,25 @@ impl std::fmt::Display for SourceAnnotation {
|
|||
RequirementOrigin::Project(path, project_name) => {
|
||||
write!(f, "{project_name} ({})", path.portable_display())
|
||||
}
|
||||
RequirementOrigin::Workspace => {
|
||||
write!(f, "(workspace)")
|
||||
}
|
||||
},
|
||||
Self::Constraint(origin) => {
|
||||
write!(f, "-c {}", origin.path().portable_display())
|
||||
}
|
||||
Self::Override(origin) => {
|
||||
write!(f, "--override {}", origin.path().portable_display())
|
||||
}
|
||||
Self::Override(origin) => match origin {
|
||||
RequirementOrigin::File(path) => {
|
||||
write!(f, "--override {}", path.portable_display())
|
||||
}
|
||||
RequirementOrigin::Project(path, project_name) => {
|
||||
// Project is not used for override
|
||||
write!(f, "--override {project_name} ({})", path.portable_display())
|
||||
}
|
||||
RequirementOrigin::Workspace => {
|
||||
write!(f, "--override (workspace)")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ pub enum RequirementOrigin {
|
|||
File(PathBuf),
|
||||
/// The requirement was provided via a local project (e.g., a `pyproject.toml` file).
|
||||
Project(PathBuf, PackageName),
|
||||
/// The requirement was provided via a workspace.
|
||||
Workspace,
|
||||
}
|
||||
|
||||
impl RequirementOrigin {
|
||||
|
|
@ -17,6 +19,8 @@ impl RequirementOrigin {
|
|||
match self {
|
||||
RequirementOrigin::File(path) => path.as_path(),
|
||||
RequirementOrigin::Project(path, _) => path.as_path(),
|
||||
// Multiple toml are merged and difficult to track files where Requirement is defined. Returns a dummy path instead.
|
||||
RequirementOrigin::Workspace => Path::new("(workspace)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ workspace = true
|
|||
[dependencies]
|
||||
distribution-types = { workspace = true, features = ["schemars"] }
|
||||
install-wheel-rs = { workspace = true, features = ["schemars"] }
|
||||
pep508_rs = { workspace = true }
|
||||
pypi-types = { workspace = true }
|
||||
uv-configuration = { workspace = true, features = ["schemars"] }
|
||||
uv-fs = { workspace = true }
|
||||
uv-normalize = { workspace = true, features = ["schemars"] }
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ impl Combine for Options {
|
|||
preview: self.preview.combine(other.preview),
|
||||
cache_dir: self.cache_dir.combine(other.cache_dir),
|
||||
pip: self.pip.combine(other.pip),
|
||||
override_dependencies: self
|
||||
.override_dependencies
|
||||
.combine(other.override_dependencies),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use std::{num::NonZeroUsize, path::PathBuf};
|
||||
use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use distribution_types::{FlatIndexLocation, IndexUrl};
|
||||
use install_wheel_rs::linker::LinkMode;
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
use uv_configuration::{
|
||||
ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
|
||||
};
|
||||
|
|
@ -37,6 +38,14 @@ pub struct Options {
|
|||
pub preview: Option<bool>,
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
pub pip: Option<PipOptions>,
|
||||
#[cfg_attr(
|
||||
feature = "schemars",
|
||||
schemars(
|
||||
with = "Option<Vec<String>>",
|
||||
description = "PEP 508 style requirements, e.g. `flask==3.0.0`, or `black @ https://...`."
|
||||
)
|
||||
)]
|
||||
pub override_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
||||
}
|
||||
|
||||
/// A `[tool.uv.pip]` section.
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ impl Workspace {
|
|||
Ok(None) => {
|
||||
// Continue traversing the directory tree.
|
||||
}
|
||||
Err(err @ WorkspaceError::PyprojectToml(..)) => {
|
||||
Err(WorkspaceError::PyprojectToml(file, err)) => {
|
||||
// If we see an invalid `pyproject.toml`, warn but continue.
|
||||
warn_user!("{err}");
|
||||
warn_user!("Failed to parse `{file}`: {err}");
|
||||
}
|
||||
Err(err) => {
|
||||
// Otherwise, warn and stop.
|
||||
|
|
|
|||
|
|
@ -11,9 +11,13 @@ use anyhow::{anyhow, Result};
|
|||
use fs_err as fs;
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use pypi_types::Requirement;
|
||||
use tracing::debug;
|
||||
|
||||
use distribution_types::{IndexLocations, SourceAnnotation, SourceAnnotations, Verbatim};
|
||||
use distribution_types::{
|
||||
IndexLocations, SourceAnnotation, SourceAnnotations, UnresolvedRequirementSpecification,
|
||||
Verbatim,
|
||||
};
|
||||
use install_wheel_rs::linker::LinkMode;
|
||||
use platform_tags::Tags;
|
||||
use uv_auth::store_credentials_from_url;
|
||||
|
|
@ -57,6 +61,7 @@ pub(crate) async fn pip_compile(
|
|||
requirements: &[RequirementsSource],
|
||||
constraints: &[RequirementsSource],
|
||||
overrides: &[RequirementsSource],
|
||||
overrides_from_workspace: Vec<Requirement>,
|
||||
extras: ExtrasSpecification,
|
||||
output_file: Option<&Path>,
|
||||
resolution_mode: ResolutionMode,
|
||||
|
|
@ -398,6 +403,17 @@ pub(crate) async fn pip_compile(
|
|||
requirements
|
||||
};
|
||||
|
||||
// Merge workspace overrides.
|
||||
let overrides: Vec<UnresolvedRequirementSpecification> = overrides
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(
|
||||
overrides_from_workspace
|
||||
.into_iter()
|
||||
.map(UnresolvedRequirementSpecification::from),
|
||||
)
|
||||
.collect();
|
||||
|
||||
// Resolve the overrides from the provided sources.
|
||||
let overrides = NamedRequirementsResolver::new(
|
||||
overrides,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ use itertools::Itertools;
|
|||
use owo_colors::OwoColorize;
|
||||
use tracing::{debug, enabled, Level};
|
||||
|
||||
use distribution_types::{IndexLocations, Resolution};
|
||||
use distribution_types::{IndexLocations, Resolution, UnresolvedRequirementSpecification};
|
||||
use install_wheel_rs::linker::LinkMode;
|
||||
use platform_tags::Tags;
|
||||
use pypi_types::Requirement;
|
||||
use uv_auth::store_credentials_from_url;
|
||||
use uv_cache::Cache;
|
||||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||
|
|
@ -42,6 +43,7 @@ pub(crate) async fn pip_install(
|
|||
requirements: &[RequirementsSource],
|
||||
constraints: &[RequirementsSource],
|
||||
overrides: &[RequirementsSource],
|
||||
overrides_from_workspace: Vec<Requirement>,
|
||||
extras: &ExtrasSpecification,
|
||||
resolution_mode: ResolutionMode,
|
||||
prerelease_mode: PreReleaseMode,
|
||||
|
|
@ -106,6 +108,16 @@ pub(crate) async fn pip_install(
|
|||
)
|
||||
.await?;
|
||||
|
||||
let overrides: Vec<UnresolvedRequirementSpecification> = overrides
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(
|
||||
overrides_from_workspace
|
||||
.into_iter()
|
||||
.map(UnresolvedRequirementSpecification::from),
|
||||
)
|
||||
.collect();
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
let system = if system {
|
||||
SystemPython::Required
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
&requirements,
|
||||
&constraints,
|
||||
&overrides,
|
||||
args.overrides_from_workspace,
|
||||
args.shared.extras,
|
||||
args.shared.output_file.as_deref(),
|
||||
args.shared.resolution,
|
||||
|
|
@ -345,6 +346,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
&requirements,
|
||||
&constraints,
|
||||
&overrides,
|
||||
args.overrides_from_workspace,
|
||||
&args.shared.extras,
|
||||
args.shared.resolution,
|
||||
args.shared.prerelease,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ use std::str::FromStr;
|
|||
|
||||
use distribution_types::IndexLocations;
|
||||
use install_wheel_rs::linker::LinkMode;
|
||||
use pep508_rs::RequirementOrigin;
|
||||
use pypi_types::Requirement;
|
||||
use uv_cache::{CacheArgs, Refresh};
|
||||
use uv_client::Connectivity;
|
||||
use uv_configuration::{
|
||||
|
|
@ -229,6 +231,8 @@ pub(crate) struct PipCompileSettings {
|
|||
|
||||
// Shared settings.
|
||||
pub(crate) shared: PipSharedSettings,
|
||||
// Override dependencies from workspace.
|
||||
pub(crate) overrides_from_workspace: Vec<Requirement>,
|
||||
}
|
||||
|
||||
impl PipCompileSettings {
|
||||
|
|
@ -298,6 +302,21 @@ impl PipCompileSettings {
|
|||
compat_args: _,
|
||||
} = args;
|
||||
|
||||
let overrides_from_workspace: Vec<Requirement> = if let Some(workspace) = &workspace {
|
||||
workspace
|
||||
.options
|
||||
.override_dependencies
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|requirement| {
|
||||
Requirement::from(requirement.with_origin(RequirementOrigin::Workspace))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Self {
|
||||
// CLI-only settings.
|
||||
src_file,
|
||||
|
|
@ -309,6 +328,7 @@ impl PipCompileSettings {
|
|||
refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package),
|
||||
upgrade: Upgrade::from_args(flag(upgrade, no_upgrade), upgrade_package),
|
||||
uv_lock: flag(unstable_uv_lock_file, no_unstable_uv_lock_file).unwrap_or(false),
|
||||
overrides_from_workspace,
|
||||
|
||||
// Shared settings.
|
||||
shared: PipSharedSettings::combine(
|
||||
|
|
@ -503,6 +523,7 @@ pub(crate) struct PipInstallSettings {
|
|||
pub(crate) refresh: Refresh,
|
||||
pub(crate) dry_run: bool,
|
||||
pub(crate) uv_lock: Option<String>,
|
||||
pub(crate) overrides_from_workspace: Vec<Requirement>,
|
||||
|
||||
// Shared settings.
|
||||
pub(crate) shared: PipSharedSettings,
|
||||
|
|
@ -570,6 +591,22 @@ impl PipInstallSettings {
|
|||
compat_args: _,
|
||||
} = args;
|
||||
|
||||
let overrides_from_workspace: Vec<pypi_types::Requirement> =
|
||||
if let Some(workspace) = &workspace {
|
||||
workspace
|
||||
.options
|
||||
.override_dependencies
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|requirement| {
|
||||
Requirement::from(requirement.with_origin(RequirementOrigin::Workspace))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Self {
|
||||
// CLI-only settings.
|
||||
package,
|
||||
|
|
@ -585,6 +622,7 @@ impl PipInstallSettings {
|
|||
refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package),
|
||||
dry_run,
|
||||
uv_lock: unstable_uv_lock_file,
|
||||
overrides_from_workspace,
|
||||
|
||||
// Shared settings.
|
||||
shared: PipSharedSettings::combine(
|
||||
|
|
|
|||
|
|
@ -2880,6 +2880,126 @@ fn override_dependency() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that `tool.uv.override-dependencies` in `pyproject.toml` is respected.
|
||||
#[test]
|
||||
fn override_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 = [
|
||||
"flask==3.0.0"
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
override-dependencies = [
|
||||
"werkzeug==2.3.0"
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.compile()
|
||||
.arg("pyproject.toml")
|
||||
.current_dir(&context.temp_dir)
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml
|
||||
blinker==1.7.0
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
flask==3.0.0
|
||||
# via example (pyproject.toml)
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
jinja2==3.1.3
|
||||
# via flask
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==2.3.0
|
||||
# via
|
||||
# --override (workspace)
|
||||
# flask
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that `override-dependencies` in `uv.toml` is respected.
|
||||
#[test]
|
||||
fn override_dependency_from_specific_uv_toml() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let _ = context.temp_dir.child("project").create_dir_all();
|
||||
let pyproject_toml = context.temp_dir.child("project/pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"[project]
|
||||
name = "example"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"flask==3.0.0"
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let _ = context.temp_dir.child("uv").create_dir_all();
|
||||
let uv_toml: assert_fs::fixture::ChildPath = context.temp_dir.child("uv").child("uv.toml");
|
||||
uv_toml.write_str(
|
||||
r#"
|
||||
override-dependencies = [
|
||||
"werkzeug==2.3.0"
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.compile()
|
||||
.arg("pyproject.toml")
|
||||
.arg("--config-file")
|
||||
.arg("../uv/uv.toml")
|
||||
.current_dir(&context.temp_dir.child("project"))
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml --config-file ../uv/uv.toml
|
||||
blinker==1.7.0
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
flask==3.0.0
|
||||
# via example (pyproject.toml)
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
jinja2==3.1.3
|
||||
# via flask
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==2.3.0
|
||||
# via
|
||||
# --override (workspace)
|
||||
# flask
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Black==23.10.1 depends on tomli>=1.1.0 for Python versions below 3.11. Demonstrate that we can
|
||||
/// override it with a multi-line override.
|
||||
#[test]
|
||||
|
|
@ -2927,6 +3047,61 @@ fn override_multi_dependency() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Check how invalid `tool.uv.override-dependencies` is handled in `pyproject.toml`.
|
||||
// TODO(konsti): We should show a warnings here or better fail parsing.
|
||||
#[test]
|
||||
fn override_dependency_from_workspace_invalid_syntax() -> 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 = [
|
||||
"flask==3.0.0"
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
override-dependencies = [
|
||||
"werkzeug=2.3.0"
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.compile()
|
||||
.arg("pyproject.toml")
|
||||
.current_dir(&context.temp_dir)
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml
|
||||
blinker==1.7.0
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
flask==3.0.0
|
||||
# via example (pyproject.toml)
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
jinja2==3.1.3
|
||||
# via flask
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==3.0.1
|
||||
# via flask
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flask==3.0.0 depends on Werkzeug>=3.0.0. Demonstrate that we can override this
|
||||
/// requirement with a URL.
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue