Skip to content

Commit 4b40109

Browse files
committed
Read parent uv settings safely and only if needed
The parent lookup now skips the walk when the scanned root's own uv.toml or [tool.uv] already governs the script, opens each file through the FIFO-safe regular-file reader, and stops at a settings file that exists but cannot be read, the way uv does, instead of going on to a farther one. Assisted-by: Claude Code:claude-opus-5-5
1 parent c81db93 commit 4b40109

3 files changed

Lines changed: 44 additions & 14 deletions

File tree

‎crates/socket-patch-core/src/hosted/engine.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,12 @@ pub async fn read_candidate_files(
720720
.files
721721
.keys()
722722
.any(|path| crate::utils::python_lock::is_script_lock_name(path))
723+
&& !crate::utils::python_script::root_governs_script(
724+
out.files.get("pyproject.toml").map(String::as_str),
725+
out.files
726+
.get(crate::utils::python_script::UV_TOML)
727+
.map(String::as_str),
728+
)
723729
{
724730
let files = view
725731
.disk_root_reading(std::iter::empty::<&std::path::Path>())

‎crates/socket-patch-core/src/utils/python_script.rs‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,20 @@ pub struct UvConfig {
393393
pub text: String,
394394
}
395395

396+
impl UvConfig {
397+
/// Whether the file is a `uv.toml` (settings at its top level) rather
398+
/// than a `pyproject.toml` (settings under `[tool.uv]`).
399+
pub fn is_uv_toml(&self) -> bool {
400+
Path::new(&self.rel).file_name() == Some(UV_TOML.as_ref())
401+
}
402+
}
403+
404+
/// Whether a PEP 723 script in a root holding `pyproject` and `uv_toml`
405+
/// takes uv's settings from the root itself, so uv looks no further up.
406+
pub fn root_governs_script(pyproject: Option<&str>, uv_toml: Option<&str>) -> bool {
407+
uv_toml.is_some() || pyproject.is_some_and(pyproject_has_uv_settings)
408+
}
409+
396410
/// Whether uv takes a script's settings from `pyproject`: it has a
397411
/// `[tool.uv]` table. One that does not parse counts, since uv stops its
398412
/// lookup there and reports it.
@@ -416,11 +430,15 @@ fn pyproject_has_uv_settings(pyproject: &str) -> bool {
416430
/// (`~/.config/uv`) is the installing machine's and is not looked up.
417431
pub fn find_ancestor_uv_config(root: &Path) -> Option<UvConfig> {
418432
for (rel, path) in ancestor_uv_config_files(root) {
419-
let Ok(text) = std::fs::read_to_string(&path) else {
420-
continue;
433+
let text = match crate::utils::fs::read_regular_to_string_sync(&path) {
434+
Ok(text) => text,
435+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
436+
// uv stops at a settings file it cannot read and reports it.
437+
Err(_) => return None,
421438
};
422-
if rel.ends_with(&format!("/{UV_TOML}")) || pyproject_has_uv_settings(&text) {
423-
return Some(UvConfig { rel, text });
439+
let config = UvConfig { rel, text };
440+
if config.is_uv_toml() || pyproject_has_uv_settings(&config.text) {
441+
return Some(config);
424442
}
425443
}
426444
None
@@ -497,10 +515,9 @@ pub fn refuse_uv_script_no_sources(
497515
uv_toml: Option<&str>,
498516
ancestor: Option<&UvConfig>,
499517
) -> Result<(), String> {
500-
let root_governs = uv_toml.is_some() || pyproject.is_some_and(pyproject_has_uv_settings);
501518
match ancestor {
502-
Some(config) if !root_governs => {
503-
let project = if config.rel.ends_with(&format!("/{UV_TOML}")) {
519+
Some(config) if !root_governs_script(pyproject, uv_toml) => {
520+
let project = if config.is_uv_toml() {
504521
(config.rel.clone(), config.text.as_str(), &[][..])
505522
} else {
506523
(
@@ -1351,6 +1368,10 @@ mod ancestor_config_tests {
13511368
text: "offline = true\n".into(),
13521369
})
13531370
);
1371+
// uv stops at a settings file it cannot read (here a directory),
1372+
// so the farther one is never reached.
1373+
std::fs::create_dir(temp.path().join("a/b/uv.toml")).unwrap();
1374+
assert_eq!(find_ancestor_uv_config(&root), None);
13541375
}
13551376

13561377
#[test]

‎crates/socket-patch-core/src/vendor/pypi_lock.rs‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::utils::python_lock::{
1111
};
1212
use crate::utils::python_script::{
1313
find_ancestor_uv_config, refuse_uv_script_no_sources, replace_script_metadata,
14-
rewrite_script_metadata, script_metadata, UvConfig,
14+
rewrite_script_metadata, root_governs_script, script_metadata, UvConfig,
1515
};
1616

1717
use crate::patch::redirect::upstream::{respell_lock_specifier, LockRequirementArray};
@@ -249,15 +249,18 @@ async fn load_listed_locks(
249249
let script = read_file(&root.join(script_name)).await?;
250250
let (pyproject, uv_toml, ancestor) = match &project_config {
251251
Some(config) => config,
252-
None => project_config.insert((
253-
read_regular_to_string(&root.join("pyproject.toml"))
252+
None => {
253+
let pyproject = read_regular_to_string(&root.join("pyproject.toml"))
254254
.await
255-
.ok(),
256-
super::pypi_uv::read_uv_toml(root).await,
255+
.ok();
256+
let uv_toml = super::pypi_uv::read_uv_toml(root).await;
257257
// uv applies a parent's settings to the script when
258258
// the root holds none (#1501).
259-
find_ancestor_uv_config(root),
260-
)),
259+
let ancestor = (!root_governs_script(pyproject.as_deref(), uv_toml.as_deref()))
260+
.then(|| find_ancestor_uv_config(root))
261+
.flatten();
262+
project_config.insert((pyproject, uv_toml, ancestor))
263+
}
261264
};
262265
refuse_uv_script_no_sources(
263266
name,

0 commit comments

Comments
 (0)