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
99 changes: 99 additions & 0 deletions crates/socket-patch-cli/tests/mode_migration_pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,105 @@ async fn uv_no_sources_is_refused_in_both_modes() {
}
}

/// #1501: a PEP 723 script takes uv's settings from the nearest directory
/// at or above it that holds a `uv.toml` or a `pyproject.toml` with
/// `[tool.uv]`. A parent project's `no-sources = true` therefore drops the
/// script's source too, so both modes must refuse a script scanned from a
/// subdirectory just as they do beside the setting. A parent pyproject
/// without `[tool.uv]` sets nothing, and the script is still wired.
#[tokio::test]
async fn uv_script_no_sources_in_a_parent_is_refused_in_both_modes() {
let parent_pyproject = "[project]\nname = \"root\"\nversion = \"0.1.0\"\n";
for (file, setting, refused) in [
("uv.toml", "no-sources = true\n".to_string(), true),
(
"pyproject.toml",
format!("{parent_pyproject}\n[tool.uv]\nno-sources = true\n"),
true,
),
(
"pyproject.toml",
format!("{parent_pyproject}\n[tool.uv]\nno-sources-package = [\"Six\"]\n"),
true,
),
("pyproject.toml", parent_pyproject.to_string(), false),
] {
let (_tmp, root) = project();
stage_script_lock(&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_script_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:#}");
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_no_sources")
&& 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
/// the run before the script lane's own lookup, in both modes.
#[tokio::test]
async fn uv_script_in_a_workspace_member_is_not_wired() {
let (_tmp, root) = project();
stage_script_lock(&root);
std::fs::write(
root.join("pyproject.toml"),
"[project]\nname = \"s\"\nversion = \"0.1.0\"\n\n[tool.uv]\npackage = false\n",
)
.unwrap();
std::fs::write(
root.parent().unwrap().join("pyproject.toml"),
"[project]\nname = \"b\"\nversion = \"0.1.0\"\n\n[tool.uv.workspace]\nmembers = [\"proj\"]\n\n[tool.uv]\nno-sources = true\n",
)
.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, 1, "hosted: {env:#}");
assert_eq!(
env["error"]["code"], "redirect_workspace_lockfile_elsewhere",
"hosted: {env:#}"
);
assert_eq!(tree(&root), before, "hosted wrote nothing");

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

/// A Hatch project; returns its wiring files.
fn stage_hatch(root: &Path) -> &'static [&'static str] {
std::fs::write(
Expand Down
27 changes: 27 additions & 0 deletions crates/socket-patch-core/src/hosted/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,33 @@ pub async fn read_candidate_files(
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))
&& !crate::utils::python_script::root_governs_script(
out.files.get("pyproject.toml").map(String::as_str),
out.files
.get(crate::utils::python_script::UV_TOML)
.map(String::as_str),
)
{
let files = view
.disk_root_reading(std::iter::empty::<&std::path::Path>())
.map(crate::utils::python_script::ancestor_uv_config_files)
.unwrap_or_default();
if let Some(config) = view
.disk_root_reading(files.iter().map(|(_, path)| path))
.and_then(crate::utils::python_script::find_ancestor_uv_config)
{
out.files.insert(
crate::patch::redirect::UV_ANCESTOR_CONFIG_KEY.to_string(),
format!("{}\n{}", config.rel, config.text),
);
}
}

// Rush monorepos have no root package.json/lock pair: the single pnpm
// source-of-truth lock lives at common/config/rush/pnpm-lock.yaml, and
Expand Down
105 changes: 95 additions & 10 deletions crates/socket-patch-core/src/patch/redirect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5948,16 +5948,29 @@ fn plan_python_metadata_with(
};
source_scope().map_err(unsupported)?;
let project_file = |rel: &str| files.get(rel).map(String::as_str);
crate::utils::python_script::refuse_uv_no_sources(
&dep.name,
script.then_some(original.as_str()),
if script {
project_file("pyproject.toml")
} else {
Some(&original)
},
project_file(crate::utils::python_script::UV_TOML),
)
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,
project_file("pyproject.toml"),
project_file(crate::utils::python_script::UV_TOML),
ancestor.as_ref(),
)
} else {
crate::utils::python_script::refuse_uv_no_sources(
&dep.name,
None,
Some(&original),
project_file(crate::utils::python_script::UV_TOML),
)
}
.map_err(unsupported)?;
let rewritten = if script {
rewrite_script_metadata(
Expand Down Expand Up @@ -6349,6 +6362,15 @@ pub const NUGET_INHERITED_MAPPING_KEY: &str = "<socket-patch:nuget-inherited-map
/// configs dropped nuget.org.
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
/// `<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`]).
///
/// [`find_ancestor_uv_config`]: crate::utils::python_script::find_ancestor_uv_config
pub const UV_ANCESTOR_CONFIG_KEY: &str = "<socket-patch:uv-ancestor-config>";

/// The synthetic candidate key carrying the locks the engine found every
/// project under the root restoring into (#353, #514), one per line:
/// `lock\t<rel>`, `unresolved\t<project>\t<why>` for a `NuGetLockFilePath`
Expand Down Expand Up @@ -25990,6 +26012,69 @@ 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.
#[test]
fn no_sources_above_the_root_refuses_a_script() {
let parent = |rel: &str, text: &str| format!("{rel}\n{text}");
let uv_toml = parent("../uv.toml", "no-sources = true\n");
let Err(warning) = plan(
"tool.py.lock",
&[("tool.py", SCRIPT), (UV_ANCESTOR_CONFIG_KEY, &uv_toml)],
&[],
) else {
panic!("a parent uv.toml must refuse");
};
assert_eq!(warning.code, "redirect_uv_script_unsupported");
assert!(
warning.detail.contains("../uv.toml sets no-sources = true"),
"{}",
warning.detail
);
let pyproject = parent("../../pyproject.toml", "[tool.uv]\nno-sources = true\n");
let Err(warning) = plan(
"tool.py.lock",
&[
("tool.py", SCRIPT),
("pyproject.toml", PYPROJECT),
(UV_ANCESTOR_CONFIG_KEY, &pyproject),
],
&[],
) else {
panic!("a parent pyproject [tool.uv] must refuse");
};
assert!(
warning
.detail
.contains("../../pyproject.toml [tool.uv] sets no-sources = true"),
"{}",
warning.detail
);

let wired = |path: &str, files: &[(&str, &str)]| {
let (edit, _) =
plan(path, files, &[]).unwrap_or_else(|w| panic!("{}: {}", w.code, w.detail));
assert!(edit.is_some(), "{path} with {files:?} must be wired");
};
wired(
"tool.py.lock",
&[
("tool.py", SCRIPT),
("uv.toml", "offline = false\n"),
(UV_ANCESTOR_CONFIG_KEY, &uv_toml),
],
);
wired(
"uv.lock",
&[
("pyproject.toml", PYPROJECT),
(UV_ANCESTOR_CONFIG_KEY, &uv_toml),
],
);
}
}

#[cfg(test)]
Expand Down
Loading
Loading