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:
Charlie Marsh 2025-03-17 14:56:10 -07:00 committed by GitHub
parent 040a5bbe5d
commit 7ea2f657fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 396 additions and 76 deletions

View file

@ -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),
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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###"

View file

@ -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(())
}

View file

@ -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"
"#
})?;

View file

@ -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

View file

@ -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:

View file

@ -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
View file

@ -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",