mirror of
https://github.com/astral-sh/uv.git
synced 2025-12-23 09:19:48 +00:00
Add src to default cache keys (#12062)
## Summary This has come up a few times, so it seems worth addressing. If you migrate from a flat layout to a `src` layout or vice versa, we now invalidate the package metadata. Closes https://github.com/astral-sh/uv/issues/12047
This commit is contained in:
parent
040a5bbe5d
commit
7ea2f657fa
10 changed files with 396 additions and 76 deletions
|
|
@ -31,8 +31,11 @@ pub struct CacheInfo {
|
|||
/// The Git tags present at the time of the build.
|
||||
tags: Option<Tags>,
|
||||
/// Environment variables to include in the cache key.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
#[serde(default)]
|
||||
env: BTreeMap<String, Option<String>>,
|
||||
/// The timestamp or inode of any directories that should be considered in the cache key.
|
||||
#[serde(default)]
|
||||
directories: BTreeMap<String, Option<DirectoryTimestamp>>,
|
||||
}
|
||||
|
||||
impl CacheInfo {
|
||||
|
|
@ -59,6 +62,7 @@ impl CacheInfo {
|
|||
let mut commit = None;
|
||||
let mut tags = None;
|
||||
let mut timestamp = None;
|
||||
let mut directories = BTreeMap::new();
|
||||
let mut env = BTreeMap::new();
|
||||
|
||||
// Read the cache keys.
|
||||
|
|
@ -82,6 +86,9 @@ impl CacheInfo {
|
|||
CacheKey::Path("pyproject.toml".to_string()),
|
||||
CacheKey::Path("setup.py".to_string()),
|
||||
CacheKey::Path("setup.cfg".to_string()),
|
||||
CacheKey::Directory {
|
||||
dir: "src".to_string(),
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
|
|
@ -117,6 +124,51 @@ impl CacheInfo {
|
|||
}
|
||||
timestamp = max(timestamp, Some(Timestamp::from_metadata(&metadata)));
|
||||
}
|
||||
CacheKey::Directory { dir } => {
|
||||
// Treat the path as a directory.
|
||||
let path = directory.join(&dir);
|
||||
let metadata = match path.metadata() {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
directories.insert(dir, None);
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to read metadata for directory: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if !metadata.is_dir() {
|
||||
warn!(
|
||||
"Expected directory for cache key, but found file: `{}`",
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(created) = metadata.created() {
|
||||
// Prefer the creation time.
|
||||
directories.insert(
|
||||
dir,
|
||||
Some(DirectoryTimestamp::Timestamp(Timestamp::from(created))),
|
||||
);
|
||||
} else {
|
||||
// Fall back to the inode.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
directories
|
||||
.insert(dir, Some(DirectoryTimestamp::Inode(metadata.ino())));
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
warn!(
|
||||
"Failed to read creation time for directory: `{}`",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
CacheKey::Git {
|
||||
git: GitPattern::Bool(true),
|
||||
} => match Commit::from_repository(directory) {
|
||||
|
|
@ -186,11 +238,16 @@ impl CacheInfo {
|
|||
}
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Computed cache info: {timestamp:?}, {commit:?}, {tags:?}, {env:?}, {directories:?}"
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
timestamp,
|
||||
commit,
|
||||
tags,
|
||||
env,
|
||||
directories,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +268,7 @@ impl CacheInfo {
|
|||
&& self.commit.is_none()
|
||||
&& self.tags.is_none()
|
||||
&& self.env.is_empty()
|
||||
&& self.directories.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,6 +299,8 @@ pub enum CacheKey {
|
|||
Path(String),
|
||||
/// Ex) `{ file = "Cargo.lock" }` or `{ file = "**/*.toml" }`
|
||||
File { file: String },
|
||||
/// Ex) `{ dir = "src" }`
|
||||
Directory { dir: String },
|
||||
/// Ex) `{ git = true }` or `{ git = { commit = true, tags = false } }`
|
||||
Git { git: GitPattern },
|
||||
/// Ex) `{ env = "UV_CACHE_INFO" }`
|
||||
|
|
@ -267,3 +327,11 @@ pub enum FilePattern {
|
|||
Glob(String),
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
/// A timestamp used to measure changes to a directory.
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
enum DirectoryTimestamp {
|
||||
Timestamp(Timestamp),
|
||||
Inode(u64),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,3 +44,9 @@ impl Timestamp {
|
|||
Self(std::time::SystemTime::now())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::time::SystemTime> for Timestamp {
|
||||
fn from(system_time: std::time::SystemTime) -> Self {
|
||||
Self(system_time)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,10 +59,11 @@ pub struct Options {
|
|||
///
|
||||
/// Cache keys enable you to specify the files or directories that should trigger a rebuild when
|
||||
/// modified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`,
|
||||
/// or `setup.cfg` files in the project directory are modified, i.e.:
|
||||
/// or `setup.cfg` files in the project directory are modified, or if a `src` directory is
|
||||
/// added or removed, i.e.:
|
||||
///
|
||||
/// ```toml
|
||||
/// cache-keys = [{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }]
|
||||
/// cache-keys = [{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }, { dir = "src" }]
|
||||
/// ```
|
||||
///
|
||||
/// As an example: if a project uses dynamic metadata to read its dependencies from a
|
||||
|
|
|
|||
|
|
@ -10607,7 +10607,7 @@ fn lock_mixed_extras() -> Result<()> {
|
|||
[tool.uv.workspace]
|
||||
members = ["packages/*"]
|
||||
"#})?;
|
||||
workspace1.child("src/__init__.py").touch()?;
|
||||
workspace1.child("src/workspace1/__init__.py").touch()?;
|
||||
|
||||
let leaf1 = workspace1.child("packages").child("leaf1");
|
||||
leaf1.child("pyproject.toml").write_str(indoc! {r#"
|
||||
|
|
@ -10621,10 +10621,10 @@ fn lock_mixed_extras() -> Result<()> {
|
|||
async = ["iniconfig>=2"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#})?;
|
||||
leaf1.child("src/__init__.py").touch()?;
|
||||
leaf1.child("src/leaf1/__init__.py").touch()?;
|
||||
|
||||
// Create a second workspace (`workspace2`) with an extra of the same name.
|
||||
let workspace2 = context.temp_dir.child("workspace2");
|
||||
|
|
@ -10636,8 +10636,8 @@ fn lock_mixed_extras() -> Result<()> {
|
|||
dependencies = ["leaf2"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv.sources]
|
||||
leaf2 = { workspace = true }
|
||||
|
|
@ -10645,7 +10645,7 @@ fn lock_mixed_extras() -> Result<()> {
|
|||
[tool.uv.workspace]
|
||||
members = ["packages/*"]
|
||||
"#})?;
|
||||
workspace2.child("src/__init__.py").touch()?;
|
||||
workspace2.child("src/workspace2/__init__.py").touch()?;
|
||||
|
||||
let leaf2 = workspace2.child("packages").child("leaf2");
|
||||
leaf2.child("pyproject.toml").write_str(indoc! {r#"
|
||||
|
|
@ -10659,10 +10659,10 @@ fn lock_mixed_extras() -> Result<()> {
|
|||
async = ["packaging>=24"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#})?;
|
||||
leaf2.child("src/__init__.py").touch()?;
|
||||
leaf2.child("src/leaf2/__init__.py").touch()?;
|
||||
|
||||
// Lock the first workspace.
|
||||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace1), @r###"
|
||||
|
|
@ -10842,7 +10842,7 @@ fn lock_transitive_extra() -> Result<()> {
|
|||
[tool.uv.workspace]
|
||||
members = ["packages/*"]
|
||||
"#})?;
|
||||
workspace.child("src/__init__.py").touch()?;
|
||||
workspace.child("src/workspace/__init__.py").touch()?;
|
||||
|
||||
let leaf = workspace.child("packages").child("leaf");
|
||||
leaf.child("pyproject.toml").write_str(indoc! {r#"
|
||||
|
|
@ -10856,10 +10856,10 @@ fn lock_transitive_extra() -> Result<()> {
|
|||
async = ["iniconfig>=2"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#})?;
|
||||
leaf.child("src/__init__.py").touch()?;
|
||||
leaf.child("src/leaf/__init__.py").touch()?;
|
||||
|
||||
// Lock the workspace.
|
||||
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
|
||||
|
|
|
|||
|
|
@ -9988,3 +9988,217 @@ fn unsupported_git_scheme() {
|
|||
"###
|
||||
);
|
||||
}
|
||||
|
||||
/// Modify a project to use a `src` layout.
|
||||
#[test]
|
||||
fn change_layout_src() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str("-e .")?;
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
.child("src")
|
||||
.child("project")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
// Installing should build the package.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###
|
||||
);
|
||||
|
||||
// Reinstalling should have no effect.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
// Replace the `src` layout with a flat layout.
|
||||
fs_err::remove_dir_all(context.temp_dir.child("src").path())?;
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
.child("project")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
// Installing should rebuild the package.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
~ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###
|
||||
);
|
||||
|
||||
// Reinstalling should have no effect.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Modify a custom directory in the cache keys.
|
||||
#[test]
|
||||
fn change_layout_custom_directory() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.write_str("-e .")?;
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv]
|
||||
cache-keys = [{ dir = "build" }]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
.child("src")
|
||||
.child("project")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
// Installing should build the package.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###
|
||||
);
|
||||
|
||||
// Reinstalling should have no effect.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
// Create the `build` directory.
|
||||
fs_err::create_dir(context.temp_dir.child("build"))?;
|
||||
|
||||
// Installing should rebuild the package.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
~ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###
|
||||
);
|
||||
|
||||
// Reinstalling should have no effect.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
// Remove the `build` directory.
|
||||
fs_err::remove_dir(context.temp_dir.child("build"))?;
|
||||
|
||||
// Installing should rebuild the package.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
~ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###
|
||||
);
|
||||
|
||||
// Reinstalling should have no effect.
|
||||
uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1208,8 +1208,8 @@ fn run_in_workspace() -> Result<()> {
|
|||
dependencies = ["anyio>3"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child1", "child2"]
|
||||
|
|
@ -1236,8 +1236,8 @@ fn run_in_workspace() -> Result<()> {
|
|||
dependencies = ["iniconfig>1"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
child1
|
||||
|
|
@ -1256,8 +1256,8 @@ fn run_in_workspace() -> Result<()> {
|
|||
dependencies = ["typing-extensions>4"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
child2
|
||||
|
|
@ -1383,11 +1383,18 @@ fn run_with_editable() -> Result<()> {
|
|||
dependencies = ["anyio", "sniffio==1.3.1"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#
|
||||
})?;
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
.child("src")
|
||||
.child("foo")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
let test_script = context.temp_dir.child("main.py");
|
||||
test_script.write_str(indoc! { r"
|
||||
import sniffio
|
||||
|
|
@ -1449,8 +1456,8 @@ fn run_with_editable() -> Result<()> {
|
|||
dependencies = ["anyio", "sniffio==1.3.1"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv.sources]
|
||||
anyio = { path = "./src/anyio_local", editable = true }
|
||||
|
|
@ -2538,8 +2545,8 @@ fn run_isolated_python_version() -> Result<()> {
|
|||
dependencies = ["anyio"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#
|
||||
})?;
|
||||
|
||||
|
|
@ -2635,8 +2642,8 @@ fn run_no_project() -> Result<()> {
|
|||
dependencies = ["anyio"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#
|
||||
})?;
|
||||
|
||||
|
|
@ -2913,8 +2920,8 @@ fn run_isolated_incompatible_python() -> Result<()> {
|
|||
dependencies = ["iniconfig"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#
|
||||
})?;
|
||||
|
||||
|
|
|
|||
|
|
@ -367,16 +367,14 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> {
|
|||
members = ["child"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let src = context.temp_dir.child("src").child("albatross");
|
||||
src.create_dir_all()?;
|
||||
|
||||
let init = src.child("__init__.py");
|
||||
init.touch()?;
|
||||
context
|
||||
.temp_dir
|
||||
.child("src")
|
||||
.child("albatross")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
let child = context.temp_dir.child("child");
|
||||
fs_err::create_dir_all(&child)?;
|
||||
|
||||
let pyproject_toml = child.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
|
|
@ -387,16 +385,15 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> {
|
|||
dependencies = ["iniconfig>=1"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let src = child.child("src").child("albatross");
|
||||
src.create_dir_all()?;
|
||||
|
||||
let init = src.child("__init__.py");
|
||||
init.touch()?;
|
||||
child
|
||||
.child("src")
|
||||
.child("child")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
// Syncing with `--no-dev` should omit all dependencies except `iniconfig`.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r###"
|
||||
|
|
@ -521,15 +518,14 @@ fn sync_legacy_non_project_group() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
let src = context.temp_dir.child("src").child("albatross");
|
||||
src.create_dir_all()?;
|
||||
|
||||
let init = src.child("__init__.py");
|
||||
init.touch()?;
|
||||
context
|
||||
.temp_dir
|
||||
.child("src")
|
||||
.child("albatross")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
let child = context.temp_dir.child("child");
|
||||
fs_err::create_dir_all(&child)?;
|
||||
|
||||
let pyproject_toml = child.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
|
|
@ -543,16 +539,15 @@ fn sync_legacy_non_project_group() -> Result<()> {
|
|||
baz = ["typing-extensions"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let src = child.child("src").child("albatross");
|
||||
src.create_dir_all()?;
|
||||
|
||||
let init = src.child("__init__.py");
|
||||
init.touch()?;
|
||||
child
|
||||
.child("src")
|
||||
.child("child")
|
||||
.child("__init__.py")
|
||||
.touch()?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
|
|
@ -5911,8 +5906,8 @@ fn sync_all_extras() -> Result<()> {
|
|||
testing = ["packaging>=24"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
child
|
||||
|
|
@ -6028,8 +6023,8 @@ fn sync_all_extras_dynamic() -> Result<()> {
|
|||
async = ["anyio>3"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
|
|
@ -6058,6 +6053,9 @@ fn sync_all_extras_dynamic() -> Result<()> {
|
|||
[tool.setuptools.dynamic.optional-dependencies]
|
||||
dev = { file = "requirements-dev.txt" }
|
||||
|
||||
[tool.uv]
|
||||
cache-keys = ["pyproject.toml"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
|
@ -6168,8 +6166,8 @@ fn sync_all_groups() -> Result<()> {
|
|||
testing = ["packaging>=24"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"#,
|
||||
)?;
|
||||
child
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ explicitly on the command-line (e.g., `uv pip install .`).
|
|||
## Dynamic metadata
|
||||
|
||||
By default, uv will _only_ rebuild and reinstall local directory dependencies (e.g., editables) if
|
||||
the `pyproject.toml`, `setup.py`, or `setup.cfg` file in the directory root has changed. This is a
|
||||
heuristic and, in some cases, may lead to fewer re-installs than desired.
|
||||
the `pyproject.toml`, `setup.py`, or `setup.cfg` file in the directory root has changed, or if a
|
||||
`src` directory is added or removed. This is a heuristic and, in some cases, may lead to fewer
|
||||
re-installs than desired.
|
||||
|
||||
To incorporate additional information into the cache key for a given package, you can add cache key
|
||||
entries under [`tool.uv.cache-keys`](https://docs.astral.sh/uv/reference/settings/#cache-keys),
|
||||
|
|
@ -68,7 +69,7 @@ the following to the project's `pyproject.toml`:
|
|||
cache-keys = [{ file = "pyproject.toml" }, { file = "requirements.txt" }]
|
||||
```
|
||||
|
||||
Globs are supported, following the syntax of the
|
||||
Globs are supported for `file` keys, following the syntax of the
|
||||
[`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html) crate. For example, to invalidate the
|
||||
cache whenever a `.toml` file in the project directory or any of its subdirectories is modified, use
|
||||
the following:
|
||||
|
|
@ -91,6 +92,17 @@ project's `pyproject.toml` to invalidate the cache whenever the environment vari
|
|||
cache-keys = [{ file = "pyproject.toml" }, { env = "MY_ENV_VAR" }]
|
||||
```
|
||||
|
||||
Finally, to invalidate a project whenever a specific directory (like `src`) is created or removed,
|
||||
add the following to the project's `pyproject.toml`:
|
||||
|
||||
```toml title="pyproject.toml"
|
||||
[tool.uv]
|
||||
cache-keys = [{ file = "pyproject.toml" }, { dir = "src" }]
|
||||
```
|
||||
|
||||
Note that the `dir` key will only track changes to the directory itself, and not arbitrary changes
|
||||
within the directory.
|
||||
|
||||
As an escape hatch, if a project uses `dynamic` metadata that isn't covered by `tool.uv.cache-keys`,
|
||||
you can instruct uv to _always_ rebuild and reinstall it by adding the project to the
|
||||
`tool.uv.reinstall-package` list:
|
||||
|
|
|
|||
|
|
@ -477,10 +477,11 @@ The keys to consider when caching builds for the project.
|
|||
|
||||
Cache keys enable you to specify the files or directories that should trigger a rebuild when
|
||||
modified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`,
|
||||
or `setup.cfg` files in the project directory are modified, i.e.:
|
||||
or `setup.cfg` files in the project directory are modified, or if a `src` directory is
|
||||
added or removed, i.e.:
|
||||
|
||||
```toml
|
||||
cache-keys = [{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }]
|
||||
cache-keys = [{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }, { dir = "src" }]
|
||||
```
|
||||
|
||||
As an example: if a project uses dynamic metadata to read its dependencies from a
|
||||
|
|
|
|||
15
uv.schema.json
generated
15
uv.schema.json
generated
|
|
@ -32,7 +32,7 @@
|
|||
]
|
||||
},
|
||||
"cache-keys": {
|
||||
"description": "The keys to consider when caching builds for the project.\n\nCache keys enable you to specify the files or directories that should trigger a rebuild when modified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`, or `setup.cfg` files in the project directory are modified, i.e.:\n\n```toml cache-keys = [{ file = \"pyproject.toml\" }, { file = \"setup.py\" }, { file = \"setup.cfg\" }] ```\n\nAs an example: if a project uses dynamic metadata to read its dependencies from a `requirements.txt` file, you can specify `cache-keys = [{ file = \"requirements.txt\" }, { file = \"pyproject.toml\" }]` to ensure that the project is rebuilt whenever the `requirements.txt` file is modified (in addition to watching the `pyproject.toml`).\n\nGlobs are supported, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html) crate. For example, to invalidate the cache whenever a `.toml` file in the project directory or any of its subdirectories is modified, you can specify `cache-keys = [{ file = \"**/*.toml\" }]`. Note that the use of globs can be expensive, as uv may need to walk the filesystem to determine whether any files have changed.\n\nCache keys can also include version control information. For example, if a project uses `setuptools_scm` to read its version from a Git commit, you can specify `cache-keys = [{ git = { commit = true }, { file = \"pyproject.toml\" }]` to include the current Git commit hash in the cache key (in addition to the `pyproject.toml`). Git tags are also supported via `cache-keys = [{ git = { commit = true, tags = true } }]`.\n\nCache keys can also include environment variables. For example, if a project relies on `MACOSX_DEPLOYMENT_TARGET` or other environment variables to determine its behavior, you can specify `cache-keys = [{ env = \"MACOSX_DEPLOYMENT_TARGET\" }]` to invalidate the cache whenever the environment variable changes.\n\nCache keys only affect the project defined by the `pyproject.toml` in which they're specified (as opposed to, e.g., affecting all members in a workspace), and all paths and globs are interpreted as relative to the project directory.",
|
||||
"description": "The keys to consider when caching builds for the project.\n\nCache keys enable you to specify the files or directories that should trigger a rebuild when modified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`, or `setup.cfg` files in the project directory are modified, or if a `src` directory is added or removed, i.e.:\n\n```toml cache-keys = [{ file = \"pyproject.toml\" }, { file = \"setup.py\" }, { file = \"setup.cfg\" }, { dir = \"src\" }] ```\n\nAs an example: if a project uses dynamic metadata to read its dependencies from a `requirements.txt` file, you can specify `cache-keys = [{ file = \"requirements.txt\" }, { file = \"pyproject.toml\" }]` to ensure that the project is rebuilt whenever the `requirements.txt` file is modified (in addition to watching the `pyproject.toml`).\n\nGlobs are supported, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html) crate. For example, to invalidate the cache whenever a `.toml` file in the project directory or any of its subdirectories is modified, you can specify `cache-keys = [{ file = \"**/*.toml\" }]`. Note that the use of globs can be expensive, as uv may need to walk the filesystem to determine whether any files have changed.\n\nCache keys can also include version control information. For example, if a project uses `setuptools_scm` to read its version from a Git commit, you can specify `cache-keys = [{ git = { commit = true }, { file = \"pyproject.toml\" }]` to include the current Git commit hash in the cache key (in addition to the `pyproject.toml`). Git tags are also supported via `cache-keys = [{ git = { commit = true, tags = true } }]`.\n\nCache keys can also include environment variables. For example, if a project relies on `MACOSX_DEPLOYMENT_TARGET` or other environment variables to determine its behavior, you can specify `cache-keys = [{ env = \"MACOSX_DEPLOYMENT_TARGET\" }]` to invalidate the cache whenever the environment variable changes.\n\nCache keys only affect the project defined by the `pyproject.toml` in which they're specified (as opposed to, e.g., affecting all members in a workspace), and all paths and globs are interpreted as relative to the project directory.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
|
|
@ -602,6 +602,19 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Ex) `{ dir = \"src\" }`",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dir"
|
||||
],
|
||||
"properties": {
|
||||
"dir": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Ex) `{ git = true }` or `{ git = { commit = true, tags = false } }`",
|
||||
"type": "object",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue