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
95 changes: 95 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,101 @@ async fn uv_no_sources_is_refused_in_both_modes() {
}
}

/// #1603: under `no-binary = true` or a `no-binary-package` list naming the
/// package (pyproject's `[tool.uv]`, a project `uv.toml`, or a PEP 723
/// script's own `[tool.uv]`), uv installs the package only from an sdist,
/// so the patched wheel both modes wire fails every `uv sync`. Both used to
/// wire it and exit 0; they must refuse and write nothing. `no-build-package`
/// allows wheels, so it is still wired.
#[tokio::test]
async fn uv_no_binary_is_refused_in_both_modes() {
for (script, file, setting, refused) in [
(
false,
"pyproject.toml",
"\n[tool.uv]\nno-binary-package = [\"Six\"]\n",
true,
),
(
false,
"pyproject.toml",
"\n[tool.uv]\nno-binary = true\n",
true,
),
(false, "uv.toml", "no-binary-package = [\"six\"]\n", true),
(true, "job.py", "", true),
(
false,
"pyproject.toml",
"\n[tool.uv]\nno-build-package = [\"six\"]\n",
false,
),
] {
let (_tmp, root) = project();
let label = if script { "script" } else { file };
if script {
stage_script_lock(&root);
let path = root.join(file);
let text = std::fs::read_to_string(&path).unwrap().replace(
"# ///\n",
"#\n# [tool.uv]\n# no-binary-package = [\"six\"]\n# ///\n",
);
assert!(text.contains("no-binary-package"), "{text}");
std::fs::write(&path, text).unwrap();
} else {
stage_uv(&root);
let path = root.join(file);
let mut text = std::fs::read_to_string(&path).unwrap_or_default();
text.push_str(setting);
std::fs::write(&path, text).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 {label}: {env:#}");
if !refused {
assert_eq!(hosted_pinned(&env), 1, "hosted {label}: {env:#}");
let (_tmp, root) = project();
stage_uv(&root);
let path = root.join(file);
let mut text = std::fs::read_to_string(&path).unwrap_or_default();
text.push_str(setting);
std::fs::write(&path, text).unwrap();
stage_manifest(&root);
let (code, env) = run_cli(&root, &["vendor"], &[]);
assert_eq!(code, 0, "vendored {label}: {env:#}");
assert!(
!env.to_string().contains("pypi_uv_no_binary"),
"vendored {label}: {env:#}"
);
continue;
}
assert_eq!(hosted_pinned(&env), 0, "hosted {label}: {env:#}");
let unsupported = if script {
"redirect_uv_script_unsupported"
} else {
"redirect_uv_project_unsupported"
};
assert!(
env.to_string().contains(unsupported) && env.to_string().contains("no-binary"),
"hosted {label}: {env:#}"
);
assert_eq!(tree(&root), before, "hosted {label} wrote nothing");

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

/// #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
Expand Down
3 changes: 2 additions & 1 deletion crates/socket-patch-core/src/hosted/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,8 @@ pub async fn read_candidate_files(

// Beside a uv project or script lock, the project's `uv.toml` may set
// `no-sources`, under which uv ignores the `[tool.uv.sources]` entry the
// rewrite would add (#564). Advisory: never rewritten, so a link or an
// rewrite would add (#564), or `no-binary`, under which uv refuses the
// patched wheel (#1603). Advisory: never rewritten, so a link or an
// unreadable in-memory entry is left out rather than refused.
let uv_lane = out
.files
Expand Down
123 changes: 123 additions & 0 deletions crates/socket-patch-core/src/patch/redirect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6019,13 +6019,32 @@ fn plan_python_metadata_with(
project_file(crate::utils::python_script::UV_TOML),
ancestor.as_ref(),
)
.and_then(|()| {
// Every patched artifact is a wheel, which uv refuses to
// install under no-binary (#1603).
crate::utils::python_script::refuse_uv_script_no_binary(
&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),
)
.and_then(|()| {
crate::utils::python_script::refuse_uv_no_binary(
&dep.name,
None,
Some(&original),
project_file(crate::utils::python_script::UV_TOML),
)
})
.and_then(|()| {
crate::utils::python_script::refuse_uv_project_shadowing(
&original,
Expand Down Expand Up @@ -26357,6 +26376,110 @@ mod python_metadata_pairing_tests {
);
}

/// #1603: under `no-binary = true`, or a `no-binary-package` list naming
/// the package, uv installs it only from an sdist, so the patched wheel
/// a source pins fails every `uv sync`. The planner refuses before
/// writing, with the same file precedence as `no-sources`.
#[test]
fn no_binary_refuses_a_wheel_uv_would_not_install() {
let refused = |path: &str, files: &[(&str, &str)]| match plan(path, files, &[]) {
Err(warning) => {
assert!(warning.detail.contains("no-binary"), "{}", warning.detail);
// The refusal names the package, also under the bool.
assert!(
warning.detail.contains("installs click only from an sdist"),
"{}",
warning.detail
);
warning.code
}
Ok(_) => panic!("{path} with {files:?} must refuse"),
};
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");
};
let project = |setting: &str| format!("{PYPROJECT}\n[tool.uv]\n{setting}\n");
let script = |setting: &str| {
SCRIPT.replace("# ///\n", &format!("#\n# [tool.uv]\n# {setting}\n# ///\n"))
};
for setting in ["no-binary = true", "no-binary-package = [\"Click\"]"] {
assert_eq!(
refused("uv.lock", &[("pyproject.toml", &project(setting))]),
"redirect_uv_project_unsupported",
"{setting}"
);
assert_eq!(
refused(
"uv.lock",
&[
("pyproject.toml", PYPROJECT),
("uv.toml", &format!("{setting}\n"))
]
),
"redirect_uv_project_unsupported",
"{setting}"
);
assert_eq!(
refused("tool.py.lock", &[("tool.py", &script(setting))]),
"redirect_uv_script_unsupported",
"{setting}"
);
assert_eq!(
refused(
"tool.py.lock",
&[("tool.py", SCRIPT), ("pyproject.toml", &project(setting))]
),
"redirect_uv_script_unsupported",
"{setting}"
);
// uv.toml replaces pyproject's [tool.uv] entirely.
wired(
"uv.lock",
&[
("pyproject.toml", &project(setting)),
("uv.toml", "offline = false\n"),
],
);
}
// Another package's entry, no-build, or a list disabled by
// no-binary = false leave the wheel installable.
for setting in [
"no-binary-package = [\"requests\"]",
"no-build-package = [\"click\"]",
"no-build = true",
"no-binary = false\nno-binary-package = [\"click\"]",
] {
wired("uv.lock", &[("pyproject.toml", &project(setting))]);
}
// A script's own no-binary = false wins over the project's list.
wired(
"tool.py.lock",
&[
("tool.py", &script("no-binary = false")),
("uv.toml", "no-binary-package = [\"click\"]\n"),
],
);
// A parent's setting reaches a script whose root sets none.
let Err(warning) = plan(
"tool.py.lock",
&[
("tool.py", SCRIPT),
(UV_ANCESTOR_CONFIG_KEY, "../uv.toml\nno-binary = true\n"),
],
&[],
) else {
panic!("a parent uv.toml must refuse");
};
assert_eq!(warning.code, "redirect_uv_script_unsupported");
assert!(
warning.detail.contains("../uv.toml sets no-binary = true"),
"{}",
warning.detail
);
}

/// #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.
Expand Down
Loading
Loading