From 8b479efd2fd03d5f92ce68e9cd98cf27a23ebc82 Mon Sep 17 00:00:00 2001 From: chisato <67509746+yumeminami@users.noreply.github.com> Date: Mon, 3 Nov 2025 04:44:28 +0800 Subject: [PATCH] Add a `uv cache size` command (#16032) ## Summary Implement `uv cache size` to output the cache directory size in raw bytes by default, with a `--human` option for human-readable output. close #15821 --------- Co-authored-by: Charlie Marsh --- crates/uv-cli/src/lib.rs | 14 ++++++- crates/uv-preview/src/lib.rs | 3 ++ crates/uv/src/commands/cache_size.rs | 53 ++++++++++++++++++++++++ crates/uv/src/commands/mod.rs | 2 + crates/uv/src/lib.rs | 3 ++ crates/uv/tests/it/cache_size.rs | 59 ++++++++++++++++++++++++++ crates/uv/tests/it/common/mod.rs | 22 ++++++++++ crates/uv/tests/it/main.rs | 3 ++ crates/uv/tests/it/show_settings.rs | 4 +- docs/reference/cli.md | 62 ++++++++++++++++++++++++++++ 10 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 crates/uv/src/commands/cache_size.rs create mode 100644 crates/uv/tests/it/cache_size.rs diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index bb8179001..c8c4c6637 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -757,7 +757,6 @@ pub enum CacheCommand { Prune(PruneArgs), /// Show the cache directory. /// - /// /// By default, the cache is stored in `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Unix and /// `%LOCALAPPDATA%\uv\cache` on Windows. /// @@ -770,6 +769,12 @@ pub enum CacheCommand { /// Note that it is important for performance for the cache directory to be located on the same /// file system as the Python environment uv is operating on. Dir, + /// Show the cache size. + /// + /// Displays the total size of the cache directory. This includes all downloaded and built + /// wheels, source distributions, and other cached data. By default, outputs the size in raw + /// bytes; use `--human` for human-readable output. + Size(SizeArgs), } #[derive(Args, Debug)] @@ -811,6 +816,13 @@ pub struct PruneArgs { pub force: bool, } +#[derive(Args, Debug)] +pub struct SizeArgs { + /// Display the cache size in human-readable format (e.g., `1.2 GiB` instead of raw bytes). + #[arg(long = "human", short = 'H', alias = "human-readable")] + pub human: bool, +} + #[derive(Args)] pub struct PipNamespace { #[command(subcommand)] diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 2eb861f34..a2453e4d8 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -20,6 +20,7 @@ bitflags::bitflags! { const FORMAT = 1 << 8; const NATIVE_AUTH = 1 << 9; const S3_ENDPOINT = 1 << 10; + const CACHE_SIZE = 1 << 11; } } @@ -40,6 +41,7 @@ impl PreviewFeatures { Self::FORMAT => "format", Self::NATIVE_AUTH => "native-auth", Self::S3_ENDPOINT => "s3-endpoint", + Self::CACHE_SIZE => "cache-size", _ => panic!("`flag_as_str` can only be used for exactly one feature flag"), } } @@ -88,6 +90,7 @@ impl FromStr for PreviewFeatures { "format" => Self::FORMAT, "native-auth" => Self::NATIVE_AUTH, "s3-endpoint" => Self::S3_ENDPOINT, + "cache-size" => Self::CACHE_SIZE, _ => { warn_user_once!("Unknown preview feature: `{part}`"); continue; diff --git a/crates/uv/src/commands/cache_size.rs b/crates/uv/src/commands/cache_size.rs new file mode 100644 index 000000000..dfeecfc9c --- /dev/null +++ b/crates/uv/src/commands/cache_size.rs @@ -0,0 +1,53 @@ +use std::fmt::Write; + +use anyhow::Result; + +use crate::commands::{ExitStatus, human_readable_bytes}; +use crate::printer::Printer; +use uv_cache::Cache; +use uv_preview::{Preview, PreviewFeatures}; +use uv_warnings::warn_user; + +/// Display the total size of the cache. +pub(crate) fn cache_size( + cache: &Cache, + human_readable: bool, + printer: Printer, + preview: Preview, +) -> Result { + if !preview.is_enabled(PreviewFeatures::CACHE_SIZE) { + warn_user!( + "`uv cache size` is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::CACHE_SIZE + ); + } + + if !cache.root().exists() { + if human_readable { + writeln!(printer.stdout_important(), "0B")?; + } else { + writeln!(printer.stdout_important(), "0")?; + } + return Ok(ExitStatus::Success); + } + + // Walk the entire cache root + let total_bytes: u64 = walkdir::WalkDir::new(cache.root()) + .follow_links(false) + .into_iter() + .filter_map(Result::ok) + .filter_map(|entry| match entry.metadata() { + Ok(metadata) if metadata.is_file() => Some(metadata.len()), + _ => None, + }) + .sum(); + + if human_readable { + let (bytes, unit) = human_readable_bytes(total_bytes); + writeln!(printer.stdout_important(), "{bytes:.1}{unit}")?; + } else { + writeln!(printer.stdout_important(), "{total_bytes}")?; + } + + Ok(ExitStatus::Success) +} diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 20a09ca65..fc371530f 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -17,6 +17,7 @@ pub(crate) use build_frontend::build_frontend; pub(crate) use cache_clean::cache_clean; pub(crate) use cache_dir::cache_dir; pub(crate) use cache_prune::cache_prune; +pub(crate) use cache_size::cache_size; pub(crate) use help::help; pub(crate) use pip::check::pip_check; pub(crate) use pip::compile::pip_compile; @@ -75,6 +76,7 @@ mod build_frontend; mod cache_clean; mod cache_dir; mod cache_prune; +mod cache_size; mod diagnostics; mod help; pub(crate) mod pip; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 72d17c5ab..a4279e4d7 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1058,6 +1058,9 @@ async fn run(mut cli: Cli) -> Result { commands::cache_dir(&cache); Ok(ExitStatus::Success) } + Commands::Cache(CacheNamespace { + command: CacheCommand::Size(args), + }) => commands::cache_size(&cache, args.human, printer, globals.preview), Commands::Build(args) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::BuildSettings::resolve(args, filesystem, environment); diff --git a/crates/uv/tests/it/cache_size.rs b/crates/uv/tests/it/cache_size.rs new file mode 100644 index 000000000..7fbfde3c8 --- /dev/null +++ b/crates/uv/tests/it/cache_size.rs @@ -0,0 +1,59 @@ +use assert_cmd::assert::OutputAssertExt; + +use crate::common::{TestContext, uv_snapshot}; + +/// Test that `cache size` returns 0 for an empty cache directory (raw output). +#[test] +fn cache_size_empty_raw() { + let context = TestContext::new("3.12"); + + // Clean cache first to ensure truly empty state + context.clean().assert().success(); + + uv_snapshot!(context.cache_size().arg("--preview"), @r" + success: true + exit_code: 0 + ----- stdout ----- + 0 + + ----- stderr ----- + "); +} + +/// Test that `cache size` returns raw bytes after installing packages. +#[test] +fn cache_size_with_packages_raw() { + let context = TestContext::new("3.12"); + + // Install a requirement to populate the cache. + context.pip_install().arg("iniconfig").assert().success(); + + // Check cache size is now positive (raw bytes). + uv_snapshot!(context.with_filtered_cache_size().filters(), context.cache_size().arg("--preview"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [SIZE] + + ----- stderr ----- + "); +} + +/// Test that `cache size --human` returns human-readable format after installing packages. +#[test] +fn cache_size_with_packages_human() { + let context = TestContext::new("3.12"); + + // Install a requirement to populate the cache. + context.pip_install().arg("iniconfig").assert().success(); + + // Check cache size with --human flag + uv_snapshot!(context.with_filtered_cache_size().filters(), context.cache_size().arg("--preview").arg("--human"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [SIZE] + + ----- stderr ----- + "); +} diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 74decf8ea..e3145a7e9 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -169,6 +169,20 @@ impl TestContext { self } + /// Add extra filtering for cache size output + #[must_use] + pub fn with_filtered_cache_size(mut self) -> Self { + // Filter raw byte counts (numbers on their own line) + self.filters + .push((r"(?m)^\d+\n".to_string(), "[SIZE]\n".to_string())); + // Filter human-readable sizes (e.g., "384.2 KiB") + self.filters.push(( + r"(?m)^\d+(\.\d+)? [KMGT]i?B\n".to_string(), + "[SIZE]\n".to_string(), + )); + self + } + /// Add extra standard filtering for Windows-compatible missing file errors. pub fn with_filtered_missing_file_error(mut self) -> Self { // The exact message string depends on the system language, so we remove it. @@ -1265,6 +1279,14 @@ impl TestContext { command } + /// Create a `uv cache size` command. + pub fn cache_size(&self) -> Command { + let mut command = Self::new_command(); + command.arg("cache").arg("size"); + self.add_shared_options(&mut command, false); + command + } + /// Create a `uv build_backend` command. /// /// Note that this command is hidden and only invoking it through a build frontend is supported. diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 332179f06..38b82c1e0 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -19,6 +19,9 @@ mod cache_clean; #[cfg(all(feature = "python", feature = "pypi"))] mod cache_prune; +#[cfg(all(feature = "python", feature = "pypi"))] +mod cache_size; + #[cfg(all(feature = "python", feature = "pypi", feature = "test-ecosystem"))] mod ecosystem; diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index ec90005bf..01b6703c9 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -7831,7 +7831,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE, ), }, python_preference: Managed, @@ -8059,7 +8059,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE, ), }, python_preference: Managed, diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 1c0f55461..ebb9bc5dd 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -5919,6 +5919,7 @@ uv cache [OPTIONS]
uv cache clean

Clear the cache, removing all entries or those linked to specific packages

uv cache prune

Prune all unreachable objects from the cache

uv cache dir

Show the cache directory

+
uv cache size

Show the cache size

### uv cache clean @@ -6115,6 +6116,67 @@ uv cache dir [OPTIONS]

You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)

+### uv cache size + +Show the cache size. + +Displays the total size of the cache directory. This includes all downloaded and built wheels, source distributions, and other cached data. By default, outputs the size in raw bytes; use `--human` for human-readable output. + +

Usage

+ +``` +uv cache size [OPTIONS] +``` + +

Options

+ +
--allow-insecure-host, --trusted-host allow-insecure-host

Allow insecure connections to a host.

+

Can be provided multiple times.

+

Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).

+

WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

+

May also be set with the UV_INSECURE_HOST environment variable.

--cache-dir cache-dir

Path to the cache directory.

+

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+

To view the location of the cache directory, run uv cache dir.

+

May also be set with the UV_CACHE_DIR environment variable.

--color color-choice

Control the use of color in output.

+

By default, uv will automatically detect support for colors when writing to a terminal.

+

Possible values:

+
    +
  • auto: Enables colored output only when the output is going to a terminal or TTY with support
  • +
  • always: Enables colored output regardless of the detected environment
  • +
  • never: Disables colored output
  • +
--config-file config-file

The path to a uv.toml file to use for configuration.

+

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

+

May also be set with the UV_CONFIG_FILE environment variable.

--directory directory

Change to the given directory prior to running the command.

+

Relative paths are resolved with the given directory as the base.

+

See --project to only change the project root directory.

+

May also be set with the UV_WORKING_DIRECTORY environment variable.

--help, -h

Display the concise help for this command

+
--human, --human-readable, -H

Display the cache size in human-readable format (e.g., 1.2 GiB instead of raw bytes)

+
--managed-python

Require use of uv-managed Python versions.

+

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

+

May also be set with the UV_MANAGED_PYTHON environment variable.

--native-tls

Whether to load TLS certificates from the platform's native certificate store.

+

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

+

However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.

+

May also be set with the UV_NATIVE_TLS environment variable.

--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

+

May also be set with the UV_NO_CACHE environment variable.

--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

+

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

+

May also be set with the UV_NO_CONFIG environment variable.

--no-managed-python

Disable use of uv-managed Python versions.

+

Instead, uv will search for a suitable Python version on the system.

+

May also be set with the UV_NO_MANAGED_PYTHON environment variable.

--no-progress

Hide all progress outputs.

+

For example, spinners or progress bars.

+

May also be set with the UV_NO_PROGRESS environment variable.

--no-python-downloads

Disable automatic downloads of Python.

+
--offline

Disable network access.

+

When disabled, uv will only use locally cached data and locally available files.

+

May also be set with the UV_OFFLINE environment variable.

--project project

Run the command within the given project directory.

+

All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (.venv).

+

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

+

See --directory to change the working directory entirely.

+

This setting has no effect when used in the uv pip interface.

+

May also be set with the UV_PROJECT environment variable.

--quiet, -q

Use quiet output.

+

Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

+
--verbose, -v

Use verbose output.

+

You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)

+
+ ## uv self Manage the uv executable