mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-02 04:48:18 +00:00
Respect overrides in all direct-dependency iterators (#2742)
## Summary We iterate over the project "requirements" directly in a variety of places. However, it's not always the case that an input "requirement" on its own will _actually_ be part of the resolution, since we support "overrides". Historically, then, overrides haven't worked as expected for _direct_ dependencies (and we have some tests that demonstrate the current, "wrong" behavior). This is just a bug, but it's not really one that comes up in practice, since it's rare to apply an override to your _own_ dependency. However, we're now considering expanding the lookahead concept to include local transitive dependencies. In this case, it's more and more important that overrides and constraints are handled consistently. This PR modifies all the locations in which we iterate over requirements directly, and modifies them to respect overrides (and constraints, where necessary).
This commit is contained in:
parent
f65f013066
commit
c669542a9e
9 changed files with 345 additions and 191 deletions
|
|
@ -1841,8 +1841,8 @@ fn allowed_transitive_url_path_dependency() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// A dependency with conflicting URLs in `requirements.in` and `constraints.txt` should arguably
|
||||
/// be ignored if the dependency has an override. However, we currently error in this case.
|
||||
/// A dependency with conflicting URLs in `requirements.in` and `constraints.txt` should be ignored
|
||||
/// if the dependency has an override.
|
||||
#[test]
|
||||
fn requirement_constraint_override_url() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
@ -1863,13 +1863,13 @@ fn requirement_constraint_override_url() -> Result<()> {
|
|||
.arg("--override")
|
||||
.arg("overrides.txt"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Requirements contain conflicting URLs for package `anyio`:
|
||||
- https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz
|
||||
- https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because there is no version of anyio==3.7.0 and you require
|
||||
anyio==3.7.0, we can conclude that the requirements are unsatisfiable.
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
@ -1877,8 +1877,8 @@ fn requirement_constraint_override_url() -> Result<()> {
|
|||
}
|
||||
|
||||
/// A dependency that uses a pre-release marker in `requirements.in` should be overridden by a
|
||||
/// non-pre-release version in `overrides.txt`. We currently allow Flask to use a pre-release below,
|
||||
/// but probably shouldn't.
|
||||
/// non-pre-release version in `overrides.txt`. We should _not_ allow Flask to be resolved to
|
||||
/// a pre-release version.
|
||||
#[test]
|
||||
fn requirement_override_prerelease() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
@ -1898,18 +1898,16 @@ fn requirement_override_prerelease() -> Result<()> {
|
|||
----- 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 requirements.in --override overrides.txt
|
||||
click==8.1.7
|
||||
click==7.1.2
|
||||
# via flask
|
||||
flask==2.0.0rc2
|
||||
itsdangerous==2.1.2
|
||||
flask==1.1.4
|
||||
itsdangerous==1.1.0
|
||||
# via flask
|
||||
jinja2==3.1.3
|
||||
jinja2==2.11.3
|
||||
# via flask
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==3.0.1
|
||||
# via jinja2
|
||||
werkzeug==1.0.1
|
||||
# via flask
|
||||
|
||||
----- stderr -----
|
||||
|
|
@ -6573,13 +6571,13 @@ fn pendulum_no_tzdata_on_windows() -> Result<()> {
|
|||
fn allow_recursive_url_local_path() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Create a standalone library.
|
||||
let lib2 = context.temp_dir.child("lib2");
|
||||
lib2.create_dir_all()?;
|
||||
let pyproject_toml = lib2.child("pyproject.toml");
|
||||
// Create a standalone library named "anyio".
|
||||
let anyio = context.temp_dir.child("anyio");
|
||||
anyio.create_dir_all()?;
|
||||
let pyproject_toml = anyio.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"[project]
|
||||
name = "lib2"
|
||||
name = "anyio"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"idna"
|
||||
|
|
@ -6589,19 +6587,19 @@ requires-python = ">3.8"
|
|||
)?;
|
||||
|
||||
// Create a library that depends on the standalone library.
|
||||
let lib1 = context.temp_dir.child("lib1");
|
||||
lib1.create_dir_all()?;
|
||||
let pyproject_toml = lib1.child("pyproject.toml");
|
||||
let lib = context.temp_dir.child("lib");
|
||||
lib.create_dir_all()?;
|
||||
let pyproject_toml = lib.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&format!(
|
||||
r#"[project]
|
||||
name = "lib1"
|
||||
name = "lib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"lib2 @ {}"
|
||||
"anyio @ {}"
|
||||
]
|
||||
requires-python = ">3.8"
|
||||
"#,
|
||||
Url::from_directory_path(lib2.path()).unwrap().as_str(),
|
||||
Url::from_directory_path(anyio.path()).unwrap().as_str(),
|
||||
))?;
|
||||
|
||||
// Create an application that depends on the library.
|
||||
|
|
@ -6613,12 +6611,11 @@ requires-python = ">3.8"
|
|||
name = "example"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyio",
|
||||
"lib1 @ {}"
|
||||
"lib @ {}"
|
||||
]
|
||||
requires-python = ">3.8"
|
||||
"#,
|
||||
Url::from_directory_path(lib1.path()).unwrap().as_str(),
|
||||
Url::from_directory_path(lib.path()).unwrap().as_str(),
|
||||
))?;
|
||||
|
||||
// Write to a requirements file.
|
||||
|
|
@ -6632,22 +6629,215 @@ requires-python = ">3.8"
|
|||
----- 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 requirements.in
|
||||
anyio==4.3.0
|
||||
# via example
|
||||
anyio @ file://[TEMP_DIR]/anyio/
|
||||
# via lib
|
||||
example @ ./app
|
||||
idna==3.6
|
||||
# via
|
||||
# anyio
|
||||
# lib2
|
||||
lib1 @ file://[TEMP_DIR]/lib1/
|
||||
# via anyio
|
||||
lib @ file://[TEMP_DIR]/lib/
|
||||
# via example
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allow URL dependencies recursively for local source trees, but respect overrides.
|
||||
#[test]
|
||||
fn allow_recursive_url_local_path_override() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Create a standalone library named "anyio".
|
||||
let anyio = context.temp_dir.child("anyio");
|
||||
anyio.create_dir_all()?;
|
||||
let pyproject_toml = anyio.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"[project]
|
||||
name = "anyio"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"idna"
|
||||
]
|
||||
requires-python = ">3.8"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Create a library that depends on the standalone library.
|
||||
let lib = context.temp_dir.child("lib");
|
||||
lib.create_dir_all()?;
|
||||
let pyproject_toml = lib.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&format!(
|
||||
r#"[project]
|
||||
name = "lib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyio @ {}"
|
||||
]
|
||||
requires-python = ">3.8"
|
||||
"#,
|
||||
Url::from_directory_path(anyio.path()).unwrap().as_str(),
|
||||
))?;
|
||||
|
||||
// Create an application that depends on the library.
|
||||
let app = context.temp_dir.child("app");
|
||||
app.create_dir_all()?;
|
||||
let pyproject_toml = app.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&format!(
|
||||
r#"[project]
|
||||
name = "example"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"lib @ {}"
|
||||
]
|
||||
requires-python = ">3.8"
|
||||
"#,
|
||||
Url::from_directory_path(lib.path()).unwrap().as_str(),
|
||||
))?;
|
||||
|
||||
// Write to a requirements file.
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("./app")?;
|
||||
|
||||
// Create an override that pulls `anyio` from PyPI.
|
||||
let overrides_txt = context.temp_dir.child("overrides.txt");
|
||||
overrides_txt.write_str("anyio==3.7.0")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.compile()
|
||||
.arg("requirements.in")
|
||||
.arg("--override")
|
||||
.arg("overrides.txt"), @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 requirements.in --override overrides.txt
|
||||
anyio==3.7.0
|
||||
# via lib
|
||||
example @ ./app
|
||||
idna==3.6
|
||||
# via anyio
|
||||
lib @ file://[TEMP_DIR]/lib/
|
||||
# via example
|
||||
lib2 @ file://[TEMP_DIR]/lib2/
|
||||
# via lib1
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
Resolved 5 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allow URL dependencies recursively for local source trees, but respect both overrides _and_
|
||||
/// constraints.
|
||||
#[test]
|
||||
fn allow_recursive_url_local_path_override_constraint() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Create a standalone library named "anyio".
|
||||
let anyio = context.temp_dir.child("anyio");
|
||||
anyio.create_dir_all()?;
|
||||
let pyproject_toml = anyio.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"[project]
|
||||
name = "anyio"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"idna"
|
||||
]
|
||||
requires-python = ">3.8"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Create a library that depends on the standalone library.
|
||||
let lib = context.temp_dir.child("lib");
|
||||
lib.create_dir_all()?;
|
||||
let pyproject_toml = lib.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&format!(
|
||||
r#"[project]
|
||||
name = "lib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyio @ {}"
|
||||
]
|
||||
requires-python = ">3.8"
|
||||
"#,
|
||||
Url::from_directory_path(anyio.path()).unwrap().as_str(),
|
||||
))?;
|
||||
|
||||
// Create an application that depends on the library.
|
||||
let app = context.temp_dir.child("app");
|
||||
app.create_dir_all()?;
|
||||
let pyproject_toml = app.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&format!(
|
||||
r#"[project]
|
||||
name = "example"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"lib @ {}"
|
||||
]
|
||||
requires-python = ">3.8"
|
||||
"#,
|
||||
Url::from_directory_path(lib.path()).unwrap().as_str(),
|
||||
))?;
|
||||
|
||||
// Write to a requirements file.
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("./app")?;
|
||||
|
||||
// Create an override that pulls `anyio` from PyPI.
|
||||
let overrides_txt = context.temp_dir.child("overrides.txt");
|
||||
overrides_txt.write_str("anyio==0.0.0")?;
|
||||
|
||||
// Ensure that resolution fails, since `0.0.0` does not exist on PyPI.
|
||||
uv_snapshot!(context.filters(), context.compile()
|
||||
.arg("requirements.in")
|
||||
.arg("--override")
|
||||
.arg("overrides.txt"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because there is no version of anyio==0.0.0 and lib==0.0.0 depends on
|
||||
anyio==0.0.0, we can conclude that lib==0.0.0 cannot be used.
|
||||
And because only lib==0.0.0 is available and example==0.0.0 depends on
|
||||
lib, we can conclude that example==0.0.0 cannot be used.
|
||||
And because only example==0.0.0 is available and you require example, we
|
||||
can conclude that the requirements are unsatisfiable.
|
||||
"###
|
||||
);
|
||||
|
||||
// Now constrain `anyio` to the local version.
|
||||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||||
constraints_txt.write_str("anyio @ ./anyio")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.compile()
|
||||
.arg("requirements.in")
|
||||
.arg("--override")
|
||||
.arg("overrides.txt")
|
||||
.arg("--constraint")
|
||||
.arg("constraints.txt"), @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 requirements.in --override overrides.txt --constraint constraints.txt
|
||||
anyio @ ./anyio
|
||||
# via lib
|
||||
example @ ./app
|
||||
idna==3.6
|
||||
# via anyio
|
||||
lib @ file://[TEMP_DIR]/lib
|
||||
# via example
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue