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:
Charlie Marsh 2024-03-31 14:03:49 -04:00 committed by GitHub
parent f65f013066
commit c669542a9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 345 additions and 191 deletions

View file

@ -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]
"###
);