Skip to content

Commit 1b0886d

Browse files
committed
fix: exclude __pycache__ from Python package hashing; tighten workspace isolation test
- hash_python_package_recursive now skips __pycache__, .mypy_cache, .pytest_cache, .tox, .venv, venv, build, dist, and dot dirs at any depth; only hashes .py files so bytecode never affects the cache key - Workspace isolation test now uses a realistic layout (workspace/crates/a and workspace/crates/b) rather than two unrelated tempdirs - New test: python_pycache_dir_does_not_affect_hash
1 parent 0b143d9 commit 1b0886d

1 file changed

Lines changed: 59 additions & 11 deletions

File tree

src/cache.rs

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,30 @@ fn hash_python_packages(root: &Path, hasher: &mut DefaultHasher) {
7676
.collect();
7777
dirs.sort();
7878
for dir in dirs {
79-
hash_dir_if_exists(&dir, hasher);
79+
hash_python_package_recursive(&dir, hasher);
80+
}
81+
}
82+
83+
/// Recursively hash Python source files inside a package directory.
84+
/// Skips generated/cache directories at any depth so that bytecode and tool
85+
/// caches do not affect the structural cache key.
86+
fn hash_python_package_recursive(dir: &Path, hasher: &mut DefaultHasher) {
87+
let Ok(mut entries) = std::fs::read_dir(dir) else { return; };
88+
let mut paths: Vec<_> = entries.by_ref().flatten().map(|e| e.path()).collect();
89+
paths.sort();
90+
for path in paths {
91+
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
92+
if path.is_dir() {
93+
if matches!(name, "__pycache__" | ".mypy_cache" | ".pytest_cache"
94+
| ".tox" | ".venv" | "venv" | "build" | "dist")
95+
|| name.starts_with('.')
96+
{
97+
continue;
98+
}
99+
hash_python_package_recursive(&path, hasher);
100+
} else if path.extension().and_then(|e| e.to_str()) == Some("py") {
101+
hash_file_content(&path, hasher);
102+
}
80103
}
81104
}
82105

@@ -397,24 +420,49 @@ mod tests {
397420

398421
#[test]
399422
fn workspace_member_hash_is_isolated_from_sibling() {
400-
let member_a = tempdir().unwrap();
401-
let member_b = tempdir().unwrap();
423+
// Use a realistic workspace layout: workspace/crates/a and workspace/crates/b
424+
let ws = tempdir().unwrap();
425+
let crates = ws.path().join("crates");
426+
let member_a = crates.join("a");
427+
let member_b = crates.join("b");
428+
std::fs::create_dir_all(&member_a).unwrap();
429+
std::fs::create_dir_all(&member_b).unwrap();
402430

403-
std::fs::write(member_a.path().join("Cargo.toml"), b"[package]\nname='a'").unwrap();
404-
std::fs::write(member_b.path().join("Cargo.toml"), b"[package]\nname='b'").unwrap();
431+
std::fs::write(member_a.join("Cargo.toml"), b"[package]\nname='a'").unwrap();
432+
std::fs::write(member_b.join("Cargo.toml"), b"[package]\nname='b'").unwrap();
405433

406-
let ha1 = compute_project_hash(member_a.path(), Language::Rust);
407-
let hb1 = compute_project_hash(member_b.path(), Language::Rust);
434+
let ha1 = compute_project_hash(&member_a, Language::Rust);
435+
let hb1 = compute_project_hash(&member_b, Language::Rust);
408436

409437
// Modify member_b; member_a's hash must not change
410-
std::fs::write(member_b.path().join("Cargo.toml"), b"[package]\nname='b-changed'").unwrap();
411-
let ha2 = compute_project_hash(member_a.path(), Language::Rust);
412-
let hb2 = compute_project_hash(member_b.path(), Language::Rust);
438+
std::fs::write(member_b.join("Cargo.toml"), b"[package]\nname='b-changed'").unwrap();
439+
let ha2 = compute_project_hash(&member_a, Language::Rust);
440+
let hb2 = compute_project_hash(&member_b, Language::Rust);
413441

414-
assert_eq!(ha1, ha2, "member_a hash must not change when member_b changes");
442+
assert_eq!(ha1, ha2, "member_a hash must not change when sibling member_b changes");
415443
assert_ne!(hb1, hb2, "member_b hash must change after its own file changes");
416444
}
417445

446+
#[test]
447+
fn python_pycache_dir_does_not_affect_hash() {
448+
let dir = tempdir().unwrap();
449+
std::fs::write(dir.path().join("pyproject.toml"), b"[project]\nname='x'").unwrap();
450+
451+
let pkg = dir.path().join("mypackage");
452+
std::fs::create_dir(&pkg).unwrap();
453+
std::fs::write(pkg.join("__init__.py"), b"").unwrap();
454+
std::fs::write(pkg.join("core.py"), b"def run(): pass").unwrap();
455+
let h1 = compute_project_hash(dir.path(), Language::Python);
456+
457+
// Add a __pycache__ directory with bytecode — must not change the hash
458+
let pycache = pkg.join("__pycache__");
459+
std::fs::create_dir(&pycache).unwrap();
460+
std::fs::write(pycache.join("core.cpython-311.pyc"), b"\x00\x00bytecode").unwrap();
461+
let h2 = compute_project_hash(dir.path(), Language::Python);
462+
463+
assert_eq!(h1, h2, "__pycache__ contents must not affect the Python structural cache hash");
464+
}
465+
418466
// ── save / load round-trip ────────────────────────────────────────────────
419467

420468
#[test]

0 commit comments

Comments
 (0)