Skip to content
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
74 changes: 74 additions & 0 deletions crates/socket-patch-cli/tests/mode_migration_pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,80 @@ async fn uv_script_no_sources_in_a_parent_is_refused_in_both_modes() {
}
}

/// #1545: a uv project whose own pyproject has no `[tool.uv]` (and no
/// `uv.toml` beside it) takes uv's settings from a parent `uv.toml` or
/// `[tool.uv]`. The `[tool.uv]` table a wired source adds would shadow
/// that parent, dropping its `resolution` / `exclude-newer`, so the
/// lock written with them fails `uv sync --locked`. Both modes used to
/// wire it and exit 0; they must refuse naming the parent and write
/// nothing. A parent holding only a workspace root's own metadata sets
/// nothing the project inherits, and the project is still wired.
#[tokio::test]
async fn uv_project_under_parent_settings_is_refused_in_both_modes() {
let parent_pyproject = "[project]\nname = \"mono\"\nversion = \"0.1.0\"\n";
for (file, setting, refused) in [
(
"uv.toml",
"resolution = \"lowest-direct\"\n".to_string(),
true,
),
(
"pyproject.toml",
format!("{parent_pyproject}\n[tool.uv]\nexclude-newer = \"2024-01-01T00:00:00Z\"\n"),
true,
),
(
"pyproject.toml",
format!("{parent_pyproject}\n[tool.uv.workspace]\nmembers = []\n"),
false,
),
] {
let (_tmp, root) = project();
stage_uv(&root);
std::fs::write(root.parent().unwrap().join(file), &setting).unwrap();
let before = tree(&root);

let server = MockServer::start().await;
mount_hosted_api(&server, true).await;
let (code, env) = hosted_scan(&root, &server);
assert_eq!(code, 0, "hosted {file}: {env:#}");
if refused {
assert_eq!(hosted_pinned(&env), 0, "hosted {file}: {env:#}");
assert!(
env.to_string().contains("redirect_uv_project_unsupported")
&& env.to_string().contains(&format!("../{file}")),
"hosted {file}: {env:#}"
);
assert_eq!(tree(&root), before, "hosted {file} wrote nothing");
} else {
assert_eq!(hosted_pinned(&env), 1, "hosted {file}: {env:#}");
// Vendored: a fresh project, so hosted's wiring is not taken over.
let (_tmp, root) = project();
stage_uv(&root);
std::fs::write(root.parent().unwrap().join(file), &setting).unwrap();
stage_manifest(&root);
let (code, env) = run_cli(&root, &["vendor"], &[]);
assert_eq!(code, 0, "vendored {file}: {env:#}");
assert!(
!env.to_string().contains("pypi_uv_ancestor_settings"),
"vendored {file}: {env:#}"
);
continue;
}

stage_manifest(&root);
let before = tree(&root);
let (code, env) = run_cli(&root, &["vendor"], &[]);
assert_ne!(code, 0, "vendored {file}: {env:#}");
assert!(
env.to_string().contains("pypi_uv_ancestor_settings")
&& env.to_string().contains(&format!("../{file}")),
"vendored {file}: {env:#}"
);
assert_eq!(tree(&root), before, "vendored {file} wrote nothing");
}
}

/// In a uv workspace, uv takes a member's script settings from the
/// workspace root, so a member's own `[tool.uv]` does not shield the script
/// from the root's `no-sources`. The #1138 workspace-member guard refuses
Expand Down
16 changes: 7 additions & 9 deletions crates/socket-patch-core/src/hosted/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,22 +704,20 @@ pub async fn read_candidate_files(
// `no-sources`, under which uv ignores the `[tool.uv.sources]` entry the
// rewrite would add (#564). Advisory: never rewritten, so a link or an
// unreadable in-memory entry is left out rather than refused.
if out
let uv_lane = out
.files
.keys()
.any(|path| path == "uv.lock" || crate::utils::python_lock::is_script_lock_name(path))
{
.any(|path| path == "uv.lock" || crate::utils::python_lock::is_script_lock_name(path));
if uv_lane {
let rel = crate::utils::python_script::UV_TOML;
if let Some(text) = read_advisory(view, unreadable, rel).await {
out.files.insert(rel.to_string(), text);
}
}
// A PEP 723 script also takes uv's settings from above the root when
// the root holds none (#1501); only a disk view has an above.
if out
.files
.keys()
.any(|path| crate::utils::python_lock::is_script_lock_name(path))
// A PEP 723 script (#1501) or a project (#1545) also takes uv's
// settings from above the root when the root holds none; only a disk
// view has an above.
if uv_lane
&& !crate::utils::python_script::root_governs_script(
out.files.get("pyproject.toml").map(String::as_str),
out.files
Expand Down
77 changes: 66 additions & 11 deletions crates/socket-patch-core/src/patch/redirect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5928,14 +5928,14 @@ fn plan_python_metadata_with(
};
source_scope().map_err(unsupported)?;
let project_file = |rel: &str| files.get(rel).map(String::as_str);
let ancestor = files.get(UV_ANCESTOR_CONFIG_KEY).and_then(|value| {
let (rel, text) = value.split_once('\n')?;
Some(crate::utils::python_script::UvConfig {
rel: rel.to_string(),
text: text.to_string(),
})
});
if script {
let ancestor = files.get(UV_ANCESTOR_CONFIG_KEY).and_then(|value| {
let (rel, text) = value.split_once('\n')?;
Some(crate::utils::python_script::UvConfig {
rel: rel.to_string(),
text: text.to_string(),
})
});
crate::utils::python_script::refuse_uv_script_no_sources(
&dep.name,
&original,
Expand All @@ -5950,6 +5950,13 @@ fn plan_python_metadata_with(
Some(&original),
project_file(crate::utils::python_script::UV_TOML),
)
.and_then(|()| {
crate::utils::python_script::refuse_uv_project_shadowing(
&original,
project_file(crate::utils::python_script::UV_TOML),
ancestor.as_ref(),
)
})
}
.map_err(unsupported)?;
let rewritten = if script {
Expand Down Expand Up @@ -6343,7 +6350,8 @@ pub const NUGET_INHERITED_MAPPING_KEY: &str = "<socket-patch:nuget-inherited-map
const EMPTY_NUGET_CONFIG: &str = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n <packageSources>\n </packageSources>\n</configuration>\n";

/// The synthetic candidate key carrying the uv settings file above the
/// root that uv applies to a PEP 723 script there (#1501), as
/// root that uv applies to a PEP 723 script (#1501) or a project (#1545)
/// there, as
/// `<rel>\n<text>` ([`find_ancestor_uv_config`]). Absent (the in-memory
/// engine, unit tests, no such file), only the root's own files are read.
/// Never a path (see [`sbt::SYNTHETIC_KEY_PREFIX`]).
Expand Down Expand Up @@ -25995,8 +26003,7 @@ mod python_metadata_pairing_tests {

/// #1501: a script takes uv's settings from above the root when the
/// root sets none; the engine hands that file over under
/// [`UV_ANCESTOR_CONFIG_KEY`]. The root's own settings stop the lookup,
/// and a project lane never reads it.
/// [`UV_ANCESTOR_CONFIG_KEY`]. The root's own settings stop the lookup.
#[test]
fn no_sources_above_the_root_refuses_a_script() {
let parent = |rel: &str, text: &str| format!("{rel}\n{text}");
Expand Down Expand Up @@ -26050,11 +26057,59 @@ mod python_metadata_pairing_tests {
wired(
"uv.lock",
&[
("pyproject.toml", PYPROJECT),
("pyproject.toml", &format!("{PYPROJECT}[tool.uv]\n")),
(UV_ANCESTOR_CONFIG_KEY, &uv_toml),
],
);
}

/// #1545: a project whose root sets no uv settings takes them from the
/// parent file the engine hands over; the `[tool.uv]` table the rewrite
/// adds would shadow it, so the project lane refuses before writing.
#[test]
fn ancestor_settings_refuse_a_project_that_would_shadow_them() {
let parent = |rel: &str, text: &str| format!("{rel}\n{text}");
for (ancestor, named) in [
(
parent("../uv.toml", "resolution = \"lowest-direct\"\n"),
"../uv.toml sets resolution",
),
(
parent(
"../pyproject.toml",
"[tool.uv]\nexclude-newer = \"2024-01-01T00:00:00Z\"\n",
),
"../pyproject.toml [tool.uv] sets exclude-newer",
),
] {
let Err(warning) = plan(
"uv.lock",
&[
("pyproject.toml", PYPROJECT),
(UV_ANCESTOR_CONFIG_KEY, &ancestor),
],
&[],
) else {
panic!("{ancestor} must refuse");
};
assert_eq!(warning.code, "redirect_uv_project_unsupported");
assert!(warning.detail.contains(named), "{}", warning.detail);
}
// A parent workspace root's own metadata is not inherited.
let (edit, _) = plan(
"uv.lock",
&[
("pyproject.toml", PYPROJECT),
(
UV_ANCESTOR_CONFIG_KEY,
&parent("../pyproject.toml", "[tool.uv.workspace]\nmembers = []\n"),
),
],
&[],
)
.unwrap_or_else(|w| panic!("{}: {}", w.code, w.detail));
assert!(edit.is_some());
}
}

#[cfg(test)]
Expand Down
Loading
Loading