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

Respect environment variable credentials for indexes outside root #10688

Merged
merged 2 commits into from
Jan 17, 2025
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
34 changes: 33 additions & 1 deletion crates/uv/src/commands/project/install_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::Path;
use std::str::FromStr;

use itertools::Either;

use uv_distribution_types::Index;
use uv_normalize::PackageName;
use uv_pypi_types::{LenientRequirement, VerbatimParsedUrl};
use uv_resolver::{Installable, Lock, Package};
Expand Down Expand Up @@ -88,6 +88,38 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
}

impl<'lock> InstallTarget<'lock> {
/// Return an iterator over the [`Index`] definitions in the target.
pub(crate) fn indexes(self) -> impl Iterator<Item = &'lock Index> {
match self {
Self::Project { workspace, .. }
| Self::Workspace { workspace, .. }
| Self::NonProjectWorkspace { workspace, .. } => {
Either::Left(workspace.indexes().iter().chain(
workspace.packages().values().flat_map(|member| {
member
.pyproject_toml()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.index.as_ref())
.into_iter()
.flatten()
}),
))
}
Self::Script { script, .. } => Either::Right(
script
.metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.top_level.index.as_deref())
.into_iter()
.flatten(),
),
}
}

/// Return an iterator over all [`Sources`] defined by the target.
pub(crate) fn sources(&self) -> impl Iterator<Item = &Source> {
match self {
Expand Down
6 changes: 6 additions & 0 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,12 @@ async fn do_lock(
}
}

for index in target.indexes() {
if let Some(credentials) = index.credentials() {
uv_auth::store_credentials(index.raw_url(), credentials);
}
}

// Initialize the registry client.
let client = RegistryClientBuilder::new(cache.clone())
.native_tls(native_tls)
Expand Down
30 changes: 29 additions & 1 deletion crates/uv/src/commands/project/lock_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use itertools::Either;

use uv_configuration::{LowerBound, SourceStrategy};
use uv_distribution::LoweredRequirement;
use uv_distribution_types::IndexLocations;
use uv_distribution_types::{Index, IndexLocations};
use uv_normalize::{GroupName, PackageName};
use uv_pep508::RequirementOrigin;
use uv_pypi_types::{Conflicts, Requirement, SupportedEnvironments, VerbatimParsedUrl};
Expand Down Expand Up @@ -159,6 +159,34 @@ impl<'lock> LockTarget<'lock> {
}
}

/// Return an iterator over the [`Index`] definitions in the [`LockTarget`].
pub(crate) fn indexes(self) -> impl Iterator<Item = &'lock Index> {
match self {
Self::Workspace(workspace) => Either::Left(workspace.indexes().iter().chain(
workspace.packages().values().flat_map(|member| {
member
.pyproject_toml()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.index.as_ref())
.into_iter()
.flatten()
}),
)),
Self::Script(script) => Either::Right(
script
.metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.top_level.index.as_deref())
.into_iter()
.flatten(),
),
}
}

/// Return the `Requires-Python` bound for the [`LockTarget`].
pub(crate) fn requires_python(self) -> Option<RequiresPython> {
match self {
Expand Down
13 changes: 10 additions & 3 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,8 @@ pub(super) async fn do_sync(
}
}

// Populate credentials from the workspace.
store_credentials_from_workspace(target);
// Populate credentials from the target.
store_credentials_from_target(target);

// Initialize the registry client.
let client = RegistryClientBuilder::new(cache.clone())
Expand Down Expand Up @@ -522,7 +522,14 @@ fn apply_editable_mode(resolution: Resolution, editable: EditableMode) -> Resolu
///
/// These credentials can come from any of `tool.uv.sources`, `tool.uv.dev-dependencies`,
/// `project.dependencies`, and `project.optional-dependencies`.
fn store_credentials_from_workspace(target: InstallTarget<'_>) {
fn store_credentials_from_target(target: InstallTarget<'_>) {
// Iterate over any idnexes in the target.
for index in target.indexes() {
if let Some(credentials) = index.credentials() {
store_credentials(index.raw_url(), credentials);
}
}

// Iterate over any sources in the target.
for source in target.sources() {
match source {
Expand Down
140 changes: 140 additions & 0 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7503,6 +7503,146 @@ fn lock_peer_member() -> Result<()> {
Ok(())
}

/// Lock a workspace in which a member defines an explicit index that requires authentication.
#[test]
fn lock_index_workspace_member() -> 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 = ["child"]

[tool.uv.workspace]
members = ["child"]

[tool.uv.sources]
child = { workspace = true }
"#,
)?;

let child = context.temp_dir.child("child");
fs_err::create_dir_all(&child)?;

let pyproject_toml = child.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=2"]

[[tool.uv.index]]
name = "my-index"
url = "https://pypi-proxy.fly.dev/basic-auth/simple"
explicit = true

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

[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;

// Locking without the necessary credentials should fail.
uv_snapshot!(context.filters(), context.lock(), @r###"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because iniconfig was not found in the package registry and child depends on iniconfig>=2, we can conclude that child's requirements are unsatisfiable.
And because your workspace requires child, we can conclude that your workspace's requirements are unsatisfiable.
"###);

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

----- stderr -----
Resolved 3 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"

[manifest]
members = [
"child",
"project",
]

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

[package.metadata]
requires-dist = [{ name = "iniconfig", specifier = ">=2", index = "https://pypi-proxy.fly.dev/basic-auth/simple" }]

[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi-proxy.fly.dev/basic-auth/simple" }
sdist = { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://pypi-proxy.fly.dev/basic-auth/files/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 = { virtual = "." }
dependencies = [
{ name = "child" },
]

[package.metadata]
requires-dist = [{ name = "child", editable = "child" }]
"###
);
});

// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock()
.env("UV_INDEX_MY_INDEX_USERNAME", "public")
.env("UV_INDEX_MY_INDEX_PASSWORD", "heron")
.arg("--locked"), @r###"
success: true
exit_code: 0
----- stdout -----

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

Ok(())
}

/// Ensure that development dependencies are omitted for non-workspace members. Below, `bar` depends
/// on `foo`, but `bar/uv.lock` should omit `anyio`, but should include `typing-extensions`.
#[test]
Expand Down
Loading