Skip to content

Commit 7b17f5d

Browse files
committed
Support globs as cache keys
1 parent cfa9299 commit 7b17f5d

File tree

8 files changed

+171
-21
lines changed

8 files changed

+171
-21
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-cache-info/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ workspace = true
1414

1515
[dependencies]
1616
fs-err = { workspace = true }
17+
glob = { workspace = true }
1718
schemars = { workspace = true, optional = true }
1819
serde = { workspace = true, features = ["derive"] }
1920
thiserror = { workspace = true }

crates/uv-cache-info/src/cache_info.rs

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use crate::commit_info::CacheCommit;
22
use crate::timestamp::Timestamp;
33

4+
use glob::MatchOptions;
45
use serde::Deserialize;
56
use std::cmp::max;
67
use std::io;
78
use std::path::{Path, PathBuf};
8-
use tracing::debug;
9+
use tracing::{debug, warn};
910

1011
/// The information used to determine whether a built distribution is up-to-date, based on the
1112
/// timestamps of relevant files, the current commit of a repository, etc.
@@ -64,24 +65,81 @@ impl CacheInfo {
6465
// If no cache keys were defined, use the defaults.
6566
let cache_keys = cache_keys.unwrap_or_else(|| {
6667
vec![
67-
CacheKey::Path(directory.join("pyproject.toml")),
68-
CacheKey::Path(directory.join("setup.py")),
69-
CacheKey::Path(directory.join("setup.cfg")),
68+
CacheKey::Path("pyproject.toml".to_string()),
69+
CacheKey::Path("setup.py".to_string()),
70+
CacheKey::Path("setup.cfg".to_string()),
7071
]
7172
});
7273

7374
// Incorporate any additional timestamps or VCS information.
7475
for cache_key in &cache_keys {
7576
match cache_key {
7677
CacheKey::Path(file) | CacheKey::File { file } => {
77-
timestamp = max(
78-
timestamp,
79-
file.metadata()
80-
.ok()
81-
.filter(std::fs::Metadata::is_file)
82-
.as_ref()
83-
.map(Timestamp::from_metadata),
84-
);
78+
if file.chars().any(|c| matches!(c, '*' | '?' | '[')) {
79+
// Treat the path as a glob.
80+
let path = directory.join(file);
81+
let Some(pattern) = path.to_str() else {
82+
warn!("Failed to convert pattern to string: {}", path.display());
83+
continue;
84+
};
85+
let paths = match glob::glob_with(
86+
pattern,
87+
MatchOptions {
88+
case_sensitive: false,
89+
require_literal_separator: true,
90+
require_literal_leading_dot: false,
91+
},
92+
) {
93+
Ok(paths) => paths,
94+
Err(err) => {
95+
warn!("Failed to parse glob pattern: {err}");
96+
continue;
97+
}
98+
};
99+
for entry in paths {
100+
let entry = match entry {
101+
Ok(entry) => entry,
102+
Err(err) => {
103+
warn!("Failed to read glob entry: {err}");
104+
continue;
105+
}
106+
};
107+
let metadata = match entry.metadata() {
108+
Ok(metadata) => metadata,
109+
Err(err) => {
110+
warn!("Failed to read metadata for glob entry: {err}");
111+
continue;
112+
}
113+
};
114+
if metadata.is_file() {
115+
timestamp =
116+
max(timestamp, Some(Timestamp::from_metadata(&metadata)));
117+
} else {
118+
warn!(
119+
"Expected file for cache key, but found directory: `{}`",
120+
entry.display()
121+
);
122+
}
123+
}
124+
} else {
125+
// Treat the path as a file.
126+
let path = directory.join(file);
127+
let metadata = match path.metadata() {
128+
Ok(metadata) => metadata,
129+
Err(err) => {
130+
warn!("Failed to read metadata for file: {err}");
131+
continue;
132+
}
133+
};
134+
if metadata.is_file() {
135+
timestamp = max(timestamp, Some(Timestamp::from_metadata(&metadata)));
136+
} else {
137+
warn!(
138+
"Expected file for cache key, but found directory: `{}`",
139+
path.display()
140+
);
141+
}
142+
}
85143
}
86144
CacheKey::Git { git: true } => match CacheCommit::from_repository(directory) {
87145
Ok(commit_info) => commit = Some(commit_info),
@@ -165,10 +223,15 @@ struct ToolUv {
165223
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
166224
#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
167225
pub enum CacheKey {
168-
/// Ex) `"Cargo.lock"`
169-
Path(PathBuf),
170-
/// Ex) `{ file = "Cargo.lock" }`
171-
File { file: PathBuf },
226+
/// Ex) `"Cargo.lock"` or `"**/*.toml"`
227+
Path(String),
228+
/// Ex) `{ file = "Cargo.lock" }` or `{ file = "**/*.toml" }`
229+
File { file: String },
172230
/// Ex) `{ git = true }`
173231
Git { git: bool },
174232
}
233+
234+
pub enum FilePattern {
235+
Glob(String),
236+
Path(PathBuf),
237+
}

crates/uv-settings/src/settings.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,20 @@ pub struct Options {
5858
/// to ensure that the project is rebuilt whenever the `requirements.txt` file is modified (in
5959
/// addition to watching the `pyproject.toml`).
6060
///
61+
/// Globs are supported, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html)
62+
/// crate. For example, to invalidate the cache whenever a `.toml` file in the project directory
63+
/// or any of its subdirectories is modified, you can specify `cache-keys = [{ file = "**/*.toml" }]`.
64+
/// Note that the use of globs can be expensive, as uv may need to walk the filesystem to
65+
/// determine whether any files have changed.
66+
///
6167
/// Cache keys can also include version control information. For example, if a project uses
6268
/// `setuptools_scm` to read its version from a Git tag, you can specify `cache-keys = [{ git = true }, { file = "pyproject.toml" }]`
6369
/// to include the current Git commit hash in the cache key (in addition to the
6470
/// `pyproject.toml`).
6571
///
6672
/// Cache keys only affect the project defined by the `pyproject.toml` in which they're
67-
/// specified (as opposed to, e.g., affecting all members in a workspace).
73+
/// specified (as opposed to, e.g., affecting all members in a workspace), and all paths and
74+
/// globs are interpreted as relative to the project directory.
6875
#[option(
6976
default = r#"[{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }]"#,
7077
value_type = "list[dict]",

crates/uv/tests/pip_install.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3265,6 +3265,62 @@ fn invalidate_path_on_cache_key() -> Result<()> {
32653265
"###
32663266
);
32673267

3268+
// Modify the `pyproject.toml` to use a glob.
3269+
pyproject_toml.write_str(
3270+
r#"[project]
3271+
name = "example"
3272+
version = "0.0.0"
3273+
dependencies = ["anyio==4.0.0"]
3274+
requires-python = ">=3.8"
3275+
3276+
[tool.uv]
3277+
cache-keys = [{ file = "**/*.txt" }]
3278+
"#,
3279+
)?;
3280+
3281+
// Write a new file.
3282+
editable_dir
3283+
.child("resources")
3284+
.child("data.txt")
3285+
.write_str("data")?;
3286+
3287+
// Installing again should update the package.
3288+
uv_snapshot!(context.filters(), context.pip_install()
3289+
.arg("example @ .")
3290+
.current_dir(editable_dir.path()), @r###"
3291+
success: true
3292+
exit_code: 0
3293+
----- stdout -----
3294+
3295+
----- stderr -----
3296+
Resolved 4 packages in [TIME]
3297+
Prepared 1 package in [TIME]
3298+
Uninstalled 1 package in [TIME]
3299+
Installed 1 package in [TIME]
3300+
~ example==0.0.0 (from file://[TEMP_DIR]/editable)
3301+
"###
3302+
);
3303+
3304+
// Write a new file in the current directory.
3305+
editable_dir.child("data.txt").write_str("data")?;
3306+
3307+
// Installing again should update the package.
3308+
uv_snapshot!(context.filters(), context.pip_install()
3309+
.arg("example @ .")
3310+
.current_dir(editable_dir.path()), @r###"
3311+
success: true
3312+
exit_code: 0
3313+
----- stdout -----
3314+
3315+
----- stderr -----
3316+
Resolved 4 packages in [TIME]
3317+
Prepared 1 package in [TIME]
3318+
Uninstalled 1 package in [TIME]
3319+
Installed 1 package in [TIME]
3320+
~ example==0.0.0 (from file://[TEMP_DIR]/editable)
3321+
"###
3322+
);
3323+
32683324
Ok(())
32693325
}
32703326

docs/concepts/cache.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@ the following to the project's `pyproject.toml`:
5252
cache-keys = [{ file = "requirements.txt" }]
5353
```
5454

55+
Globs are supported, following the syntax of the
56+
[`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html) crate. For example, to invalidate the
57+
cache whenever a `.toml` file in the project directory or any of its subdirectories is modified, use
58+
the following:
59+
60+
```toml title="pyproject.toml"
61+
[tool.uv]
62+
cache-keys = [{ file = "**/*.toml" }]
63+
```
64+
65+
!!! note
66+
67+
The use of globs can be expensive, as uv may need to walk the filesystem to determine whether any files have changed.
68+
This may, in turn, requiring traversal of large or deeply nested directories.
69+
5570
As an escape hatch, if a project uses `dynamic` metadata that isn't covered by `tool.uv.cache-keys`,
5671
you can instruct uv to _always_ rebuild and reinstall it by adding the project to the
5772
`tool.uv.reinstall-package` list:

docs/reference/settings.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,20 @@ As an example: if a project uses dynamic metadata to read its dependencies from
7676
to ensure that the project is rebuilt whenever the `requirements.txt` file is modified (in
7777
addition to watching the `pyproject.toml`).
7878

79+
Globs are supported, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html)
80+
crate. For example, to invalidate the cache whenever a `.toml` file in the project directory
81+
or any of its subdirectories is modified, you can specify `cache-keys = [{ file = "**/*.toml" }]`.
82+
Note that the use of globs can be expensive, as uv will need to walk the filesystem to
83+
determine whether any files have changed.
84+
7985
Cache keys can also include version control information. For example, if a project uses
8086
`setuptools_scm` to read its version from a Git tag, you can specify `cache-keys = [{ git = true }, { file = "pyproject.toml" }]`
8187
to include the current Git commit hash in the cache key (in addition to the
8288
`pyproject.toml`).
8389

8490
Cache keys only affect the project defined by the `pyproject.toml` in which they're
85-
specified (as opposed to, e.g., affecting all members in a workspace).
91+
specified (as opposed to, e.g., affecting all members in a workspace), and all paths and
92+
globs are interpreted as relative to the project directory.
8693

8794
**Default value**: `[{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }]`
8895

uv.schema.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)