Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow default indexes to be marked as explicit #8990

Merged
merged 1 commit into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 15 additions & 22 deletions crates/uv-distribution-types/src/index_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,14 @@ impl<'a> IndexLocations {
self.indexes
.iter()
.filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
.find(|index| index.default && !index.explicit)
.find(|index| index.default)
.or_else(|| Some(&DEFAULT_INDEX))
}
}

/// Return an iterator over the implicit [`Index`] entries.
///
/// Default and explicit indexes are excluded.
pub fn implicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
if self.no_index {
Either::Left(std::iter::empty())
Expand All @@ -249,22 +251,7 @@ impl<'a> IndexLocations {
self.indexes
.iter()
.filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
.filter(|index| !(index.default || index.explicit)),
)
}
}

/// Return an iterator over the explicit [`Index`] entries.
pub fn explicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
if self.no_index {
Either::Left(std::iter::empty())
} else {
let mut seen = FxHashSet::default();
Either::Right(
self.indexes
.iter()
.filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
.filter(|index| index.explicit),
.filter(|index| !index.default && !index.explicit),
)
}
}
Expand All @@ -278,7 +265,9 @@ impl<'a> IndexLocations {
/// If `no_index` was enabled, then this always returns an empty
/// iterator.
pub fn indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
self.implicit_indexes().chain(self.default_index())
self.implicit_indexes()
.chain(self.default_index())
.filter(|index| !index.explicit)
}

/// Return an iterator over the [`FlatIndexLocation`] entries.
Expand Down Expand Up @@ -319,7 +308,7 @@ impl<'a> IndexLocations {
.chain(self.flat_index.iter())
.filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
} {
if index.default && !index.explicit {
if index.default {
if default {
continue;
}
Expand Down Expand Up @@ -361,12 +350,14 @@ impl<'a> IndexUrls {
self.indexes
.iter()
.filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
.find(|index| index.default && !index.explicit)
.find(|index| index.default)
.or_else(|| Some(&DEFAULT_INDEX))
}
}

/// Return an iterator over the implicit [`Index`] entries.
///
/// Default and explicit indexes are excluded.
fn implicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
if self.no_index {
Either::Left(std::iter::empty())
Expand All @@ -376,7 +367,7 @@ impl<'a> IndexUrls {
self.indexes
.iter()
.filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
.filter(|index| !(index.default || index.explicit)),
.filter(|index| !index.default && !index.explicit),
)
}
}
Expand All @@ -389,7 +380,9 @@ impl<'a> IndexUrls {
/// If `no_index` was enabled, then this always returns an empty
/// iterator.
pub fn indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
self.implicit_indexes().chain(self.default_index())
self.implicit_indexes()
.chain(self.default_index())
.filter(|index| !index.explicit)
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/pubgrub/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ impl PubGrubReportFormatter<'_> {
}

// Add hints due to an index returning an unauthorized response.
for index in index_locations.indexes() {
for index in index_locations.allowed_indexes() {
if index_capabilities.unauthorized(&index.url) {
hints.insert(PubGrubHint::UnauthorizedIndex {
index: index.url.clone(),
Expand Down
164 changes: 164 additions & 0 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13208,6 +13208,170 @@ fn lock_explicit_index() -> Result<()> {
Ok(())
}

#[test]
fn lock_explicit_default_index() -> 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 = ["iniconfig==2.0.0"]

[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"

[tool.uv.sources]
iniconfig = { index = "test" }

[[tool.uv.index]]
name = "test"
url = "https://test.pypi.org/simple"
explicit = true
default = true
"#,
)?;

uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 2 packages in [TIME]
"###);

let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();

insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"

[options]
exclude-newer = "2024-03-25T00:00:00Z"

[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://test.pypi.org/simple" }
sdist = { url = "https://test-files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://test-files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]

[[package]]
name = "project"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "iniconfig" },
]

[package.metadata]
requires-dist = [{ name = "iniconfig", specifier = "==2.0.0", index = "https://test.pypi.org/simple" }]
"###
);
});

pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio"]

[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"

[[tool.uv.index]]
name = "test"
url = "https://test.pypi.org/simple"
explicit = true
default = true
"#,
)?;

uv_snapshot!(context.filters(), context.lock().arg("--verbose"), @r###"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
DEBUG uv [VERSION] ([COMMIT] DATE)
DEBUG Found workspace root: `[TEMP_DIR]/`
DEBUG Adding current workspace member: `[TEMP_DIR]/`
DEBUG Using Python request `>=3.12` from `requires-python` metadata
DEBUG The virtual environment's Python version satisfies `>=3.12`
DEBUG Using request timeout of [TIME]
DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/
DEBUG No workspace root found, using project root
DEBUG Ignoring existing lockfile due to mismatched `requires-dist` for: `project==0.1.0`
Expected: {Requirement { name: PackageName("anyio"), extras: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None }, origin: None }}
Actual: {Requirement { name: PackageName("iniconfig"), extras: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }) }, origin: None }}
DEBUG Solving with installed Python version: 3.12.[X]
DEBUG Solving with target Python version: >=3.12
DEBUG Adding direct dependency: project*
DEBUG Searching for a compatible version of project @ file://[TEMP_DIR]/ (*)
DEBUG Adding transitive dependency for project==0.1.0: anyio*
DEBUG Searching for a compatible version of anyio (*)
DEBUG No compatible version found for: anyio
DEBUG Searching for a compatible version of project @ file://[TEMP_DIR]/ (<0.1.0 | >0.1.0)
DEBUG No compatible version found for: project
× No solution found when resolving dependencies:
╰─▶ Because anyio was not found in the provided package locations and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable.

hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links <uri>`)
"###);

let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();

insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"

[options]
exclude-newer = "2024-03-25T00:00:00Z"

[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://test.pypi.org/simple" }
sdist = { url = "https://test-files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://test-files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]

[[package]]
name = "project"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "iniconfig" },
]

[package.metadata]
requires-dist = [{ name = "iniconfig", specifier = "==2.0.0", index = "https://test.pypi.org/simple" }]
"###
);
});

Ok(())
}

#[test]
fn lock_named_index() -> Result<()> {
let context = TestContext::new("3.12");
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration/indexes.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ Named indexes referenced via `tool.uv.sources` must be defined within the projec
file; indexes provided via the command-line, environment variables, or user-level configuration will
not be recognized.

If an index is marked as both `default = true` and `explicit = true`, it will be treated as an
explicit index (i.e., only usable via `tool.uv.sources`) while also removing PyPI as the default
index.

## Searching across multiple indexes

By default, uv will stop at the first index on which a given package is available, and limit
Expand Down
Loading