Support `--target` and `--prefix` in `uv pip list`, `uv pip freeze`, and `uv pip show` (#16955)

## Summary

Closes https://github.com/astral-sh/uv/issues/15112.
This commit is contained in:
Charlie Marsh 2025-12-03 04:49:11 -08:00 committed by GitHub
parent b1078fe595
commit 49b70e7225
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 409 additions and 7 deletions

View File

@ -2482,6 +2482,14 @@ pub struct PipFreezeArgs {
#[arg(long, overrides_with("system"), hide = true)]
pub no_system: bool,
/// List packages from the specified `--target` directory.
#[arg(long, conflicts_with_all = ["prefix", "paths"])]
pub target: Option<PathBuf>,
/// List packages from the specified `--prefix` directory.
#[arg(long, conflicts_with_all = ["target", "paths"])]
pub prefix: Option<PathBuf>,
#[command(flatten)]
pub compat_args: compat::PipGlobalCompatArgs,
}
@ -2557,6 +2565,14 @@ pub struct PipListArgs {
#[arg(long, overrides_with("system"), hide = true)]
pub no_system: bool,
/// List packages from the specified `--target` directory.
#[arg(long, conflicts_with = "prefix")]
pub target: Option<PathBuf>,
/// List packages from the specified `--prefix` directory.
#[arg(long, conflicts_with = "target")]
pub prefix: Option<PathBuf>,
#[command(flatten)]
pub compat_args: compat::PipListCompatArgs,
}
@ -2672,6 +2688,14 @@ pub struct PipShowArgs {
#[arg(long, overrides_with("system"), hide = true)]
pub no_system: bool,
/// Show a package from the specified `--target` directory.
#[arg(long, conflicts_with = "prefix")]
pub target: Option<PathBuf>,
/// Show a package from the specified `--prefix` directory.
#[arg(long, conflicts_with = "target")]
pub prefix: Option<PathBuf>,
#[command(flatten)]
pub compat_args: compat::PipGlobalCompatArgs,
}

View File

@ -4,13 +4,15 @@ use std::path::PathBuf;
use anyhow::Result;
use itertools::Itertools;
use owo_colors::OwoColorize;
use tracing::debug;
use uv_cache::Cache;
use uv_distribution_types::{Diagnostic, InstalledDistKind, Name};
use uv_fs::Simplified;
use uv_installer::SitePackages;
use uv_preview::Preview;
use uv_python::PythonPreference;
use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest};
use uv_python::{EnvironmentPreference, Prefix, PythonEnvironment, PythonRequest, Target};
use crate::commands::ExitStatus;
use crate::commands::pip::operations::report_target_environment;
@ -22,6 +24,8 @@ pub(crate) fn pip_freeze(
strict: bool,
python: Option<&str>,
system: bool,
target: Option<Target>,
prefix: Option<Prefix>,
paths: Option<Vec<PathBuf>>,
cache: &Cache,
printer: Printer,
@ -36,6 +40,23 @@ pub(crate) fn pip_freeze(
preview,
)?;
// Apply any `--target` or `--prefix` directories.
let environment = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
target.root().user_display()
);
environment.with_target(target)?
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
environment.with_prefix(prefix)?
} else {
environment
};
report_target_environment(&environment, cache, printer)?;
// Collect all the `site-packages` directories.

View File

@ -8,6 +8,7 @@ use owo_colors::OwoColorize;
use rustc_hash::FxHashMap;
use serde::Serialize;
use tokio::sync::Semaphore;
use tracing::debug;
use unicode_width::UnicodeWidthStr;
use uv_cache::{Cache, Refresh};
@ -25,7 +26,7 @@ use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_preview::Preview;
use uv_python::PythonRequest;
use uv_python::{EnvironmentPreference, PythonEnvironment, PythonPreference};
use uv_python::{EnvironmentPreference, Prefix, PythonEnvironment, PythonPreference, Target};
use uv_resolver::{ExcludeNewer, PrereleaseMode};
use crate::commands::ExitStatus;
@ -51,6 +52,8 @@ pub(crate) async fn pip_list(
exclude_newer: ExcludeNewer,
python: Option<&str>,
system: bool,
target: Option<Target>,
prefix: Option<Prefix>,
cache: &Cache,
printer: Printer,
preview: Preview,
@ -69,6 +72,23 @@ pub(crate) async fn pip_list(
preview,
)?;
// Apply any `--target` or `--prefix` directories.
let environment = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
target.root().user_display()
);
environment.with_target(target)?
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
environment.with_prefix(prefix)?
} else {
environment
};
report_target_environment(&environment, cache, printer)?;
// Build the installed index.

View File

@ -5,6 +5,7 @@ use fs_err::File;
use itertools::{Either, Itertools};
use owo_colors::OwoColorize;
use rustc_hash::FxHashMap;
use tracing::debug;
use uv_cache::Cache;
use uv_distribution_types::{Diagnostic, Name};
@ -13,7 +14,9 @@ use uv_install_wheel::read_record_file;
use uv_installer::SitePackages;
use uv_normalize::PackageName;
use uv_preview::Preview;
use uv_python::{EnvironmentPreference, PythonEnvironment, PythonPreference, PythonRequest};
use uv_python::{
EnvironmentPreference, Prefix, PythonEnvironment, PythonPreference, PythonRequest, Target,
};
use crate::commands::ExitStatus;
use crate::commands::pip::operations::report_target_environment;
@ -25,6 +28,8 @@ pub(crate) fn pip_show(
strict: bool,
python: Option<&str>,
system: bool,
target: Option<Target>,
prefix: Option<Prefix>,
files: bool,
cache: &Cache,
printer: Printer,
@ -52,6 +57,23 @@ pub(crate) fn pip_show(
preview,
)?;
// Apply any `--target` or `--prefix` directories.
let environment = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
target.root().user_display()
);
environment.with_target(target)?
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
environment.with_prefix(prefix)?
} else {
environment
};
report_target_environment(&environment, cache, printer)?;
// Build the installed index.

View File

@ -941,6 +941,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.settings.strict,
args.settings.python.as_deref(),
args.settings.system,
args.settings.target,
args.settings.prefix,
args.paths,
&cache,
printer,
@ -974,6 +976,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.settings.exclude_newer,
args.settings.python.as_deref(),
args.settings.system,
args.settings.target,
args.settings.prefix,
&cache,
printer,
globals.preview,
@ -995,6 +999,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.settings.strict,
args.settings.python.as_deref(),
args.settings.system,
args.settings.target,
args.settings.prefix,
args.files,
&cache,
printer,

View File

@ -2771,6 +2771,8 @@ impl PipFreezeSettings {
paths,
system,
no_system,
target,
prefix,
compat_args: _,
} = args;
@ -2782,6 +2784,8 @@ impl PipFreezeSettings {
python: python.and_then(Maybe::into_option),
system: flag(system, no_system, "system"),
strict: flag(strict, no_strict, "strict"),
target,
prefix,
..PipOptions::default()
},
filesystem,
@ -2821,6 +2825,8 @@ impl PipListSettings {
python,
system,
no_system,
target,
prefix,
compat_args: _,
} = args;
@ -2834,6 +2840,8 @@ impl PipListSettings {
python: python.and_then(Maybe::into_option),
system: flag(system, no_system, "system"),
strict: flag(strict, no_strict, "strict"),
target,
prefix,
..PipOptions::from(fetch)
},
filesystem,
@ -2866,6 +2874,8 @@ impl PipShowSettings {
python,
system,
no_system,
target,
prefix,
compat_args: _,
} = args;
@ -2877,6 +2887,8 @@ impl PipShowSettings {
python: python.and_then(Maybe::into_option),
system: flag(system, no_system, "system"),
strict: flag(strict, no_strict, "strict"),
target,
prefix,
..PipOptions::default()
},
filesystem,

View File

@ -482,3 +482,95 @@ fn freeze_with_quiet_flag() -> Result<()> {
Ok(())
}
#[test]
fn freeze_target() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?;
let target = context.temp_dir.child("target");
// Install packages to a target directory.
context
.pip_install()
.arg("-r")
.arg("requirements.txt")
.arg("--target")
.arg(target.path())
.assert()
.success();
// Freeze packages in the target directory.
uv_snapshot!(context.filters(), context.pip_freeze()
.arg("--target")
.arg(target.path()), @r###"
success: true
exit_code: 0
----- stdout -----
markupsafe==2.1.3
tomli==2.0.1
----- stderr -----
"###
);
// Without --target, the packages should not be visible.
uv_snapshot!(context.pip_freeze(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###
);
Ok(())
}
#[test]
fn freeze_prefix() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?;
let prefix = context.temp_dir.child("prefix");
// Install packages to a prefix directory.
context
.pip_install()
.arg("-r")
.arg("requirements.txt")
.arg("--prefix")
.arg(prefix.path())
.assert()
.success();
// Freeze packages in the prefix directory.
uv_snapshot!(context.filters(), context.pip_freeze()
.arg("--prefix")
.arg(prefix.path()), @r###"
success: true
exit_code: 0
----- stdout -----
markupsafe==2.1.3
tomli==2.0.1
----- stderr -----
"###
);
// Without --prefix, the packages should not be visible.
uv_snapshot!(context.pip_freeze(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###
);
Ok(())
}

View File

@ -1,4 +1,5 @@
use anyhow::Result;
use assert_cmd::prelude::*;
use assert_fs::fixture::ChildPath;
use assert_fs::fixture::FileWriteStr;
use assert_fs::fixture::PathChild;
@ -788,3 +789,99 @@ fn list_ignores_quiet_flag_format_freeze() {
"###
);
}
#[test]
fn list_target() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?;
let target = context.temp_dir.child("target");
// Install packages to a target directory.
context
.pip_install()
.arg("-r")
.arg("requirements.txt")
.arg("--target")
.arg(target.path())
.assert()
.success();
// List packages in the target directory.
uv_snapshot!(context.filters(), context.pip_list()
.arg("--target")
.arg(target.path()), @r###"
success: true
exit_code: 0
----- stdout -----
Package Version
---------- -------
markupsafe 2.1.3
tomli 2.0.1
----- stderr -----
"###
);
// Without --target, the packages should not be visible.
uv_snapshot!(context.pip_list(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###
);
Ok(())
}
#[test]
fn list_prefix() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?;
let prefix = context.temp_dir.child("prefix");
// Install packages to a prefix directory.
context
.pip_install()
.arg("-r")
.arg("requirements.txt")
.arg("--prefix")
.arg(prefix.path())
.assert()
.success();
// List packages in the prefix directory.
uv_snapshot!(context.filters(), context.pip_list()
.arg("--prefix")
.arg(prefix.path()), @r###"
success: true
exit_code: 0
----- stdout -----
Package Version
---------- -------
markupsafe 2.1.3
tomli 2.0.1
----- stderr -----
"###
);
// Without --prefix, the packages should not be visible.
uv_snapshot!(context.pip_list(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###
);
Ok(())
}

View File

@ -534,3 +534,105 @@ fn show_files() {
----- stderr -----
"#);
}
#[test]
fn show_target() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("MarkupSafe==2.1.3")?;
let target = context.temp_dir.child("target");
// Install packages to a target directory.
context
.pip_install()
.arg("-r")
.arg("requirements.txt")
.arg("--target")
.arg(target.path())
.assert()
.success();
// Show package in the target directory.
uv_snapshot!(context.filters(), context.pip_show()
.arg("markupsafe")
.arg("--target")
.arg(target.path()), @r###"
success: true
exit_code: 0
----- stdout -----
Name: markupsafe
Version: 2.1.3
Location: [TEMP_DIR]/target
Requires:
Required-by:
----- stderr -----
"###
);
// Without --target, the package should not be found.
uv_snapshot!(context.pip_show().arg("markupsafe"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: Package(s) not found for: markupsafe
"###
);
Ok(())
}
#[test]
fn show_prefix() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("MarkupSafe==2.1.3")?;
let prefix = context.temp_dir.child("prefix");
// Install packages to a prefix directory.
context
.pip_install()
.arg("-r")
.arg("requirements.txt")
.arg("--prefix")
.arg(prefix.path())
.assert()
.success();
// Show package in the prefix directory.
uv_snapshot!(context.filters(), context.pip_show()
.arg("markupsafe")
.arg("--prefix")
.arg(prefix.path()), @r###"
success: true
exit_code: 0
----- stdout -----
Name: markupsafe
Version: 2.1.3
Location: [TEMP_DIR]/prefix/[PYTHON-LIB]/site-packages
Requires:
Required-by:
----- stderr -----
"###
);
// Without --prefix, the package should not be found.
uv_snapshot!(context.pip_show().arg("markupsafe"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: Package(s) not found for: markupsafe
"###
);
Ok(())
}

View File

@ -5062,6 +5062,7 @@ uv pip freeze [OPTIONS]
</dd><dt id="uv-pip-freeze--offline"><a href="#uv-pip-freeze--offline"><code>--offline</code></a></dt><dd><p>Disable network access.</p>
<p>When disabled, uv will only use locally cached data and locally available files.</p>
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-pip-freeze--path"><a href="#uv-pip-freeze--path"><code>--path</code></a> <i>paths</i></dt><dd><p>Restrict to the specified installation path for listing packages (can be used multiple times)</p>
</dd><dt id="uv-pip-freeze--prefix"><a href="#uv-pip-freeze--prefix"><code>--prefix</code></a> <i>prefix</i></dt><dd><p>List packages from the specified <code>--prefix</code> directory</p>
</dd><dt id="uv-pip-freeze--project"><a href="#uv-pip-freeze--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code>, <code>uv.toml</code>, and <code>.python-version</code> files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (<code>.venv</code>).</p>
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
@ -5077,7 +5078,8 @@ Python environment if no virtual environment is found.</p>
</dd><dt id="uv-pip-freeze--system"><a href="#uv-pip-freeze--system"><code>--system</code></a></dt><dd><p>List packages in the system Python environment.</p>
<p>Disables discovery of virtual environments.</p>
<p>See <a href="#uv-python">uv python</a> for details on Python discovery.</p>
<p>May also be set with the <code>UV_SYSTEM_PYTHON</code> environment variable.</p></dd><dt id="uv-pip-freeze--verbose"><a href="#uv-pip-freeze--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>May also be set with the <code>UV_SYSTEM_PYTHON</code> environment variable.</p></dd><dt id="uv-pip-freeze--target"><a href="#uv-pip-freeze--target"><code>--target</code></a> <i>target</i></dt><dd><p>List packages from the specified <code>--target</code> directory</p>
</dd><dt id="uv-pip-freeze--verbose"><a href="#uv-pip-freeze--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
</dd></dl>
@ -5172,6 +5174,7 @@ uv pip list [OPTIONS]
<p>When disabled, uv will only use locally cached data and locally available files.</p>
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-pip-list--outdated"><a href="#uv-pip-list--outdated"><code>--outdated</code></a></dt><dd><p>List outdated packages.</p>
<p>The latest version of each package will be shown alongside the installed version. Up-to-date packages will be omitted from the output.</p>
</dd><dt id="uv-pip-list--prefix"><a href="#uv-pip-list--prefix"><code>--prefix</code></a> <i>prefix</i></dt><dd><p>List packages from the specified <code>--prefix</code> directory</p>
</dd><dt id="uv-pip-list--project"><a href="#uv-pip-list--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code>, <code>uv.toml</code>, and <code>.python-version</code> files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (<code>.venv</code>).</p>
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
@ -5187,7 +5190,8 @@ Python environment if no virtual environment is found.</p>
</dd><dt id="uv-pip-list--system"><a href="#uv-pip-list--system"><code>--system</code></a></dt><dd><p>List packages in the system Python environment.</p>
<p>Disables discovery of virtual environments.</p>
<p>See <a href="#uv-python">uv python</a> for details on Python discovery.</p>
<p>May also be set with the <code>UV_SYSTEM_PYTHON</code> environment variable.</p></dd><dt id="uv-pip-list--verbose"><a href="#uv-pip-list--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>May also be set with the <code>UV_SYSTEM_PYTHON</code> environment variable.</p></dd><dt id="uv-pip-list--target"><a href="#uv-pip-list--target"><code>--target</code></a> <i>target</i></dt><dd><p>List packages from the specified <code>--target</code> directory</p>
</dd><dt id="uv-pip-list--verbose"><a href="#uv-pip-list--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
</dd></dl>
@ -5244,7 +5248,8 @@ uv pip show [OPTIONS] [PACKAGE]...
<p>May also be set with the <code>UV_NO_PROGRESS</code> environment variable.</p></dd><dt id="uv-pip-show--no-python-downloads"><a href="#uv-pip-show--no-python-downloads"><code>--no-python-downloads</code></a></dt><dd><p>Disable automatic downloads of Python.</p>
</dd><dt id="uv-pip-show--offline"><a href="#uv-pip-show--offline"><code>--offline</code></a></dt><dd><p>Disable network access.</p>
<p>When disabled, uv will only use locally cached data and locally available files.</p>
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-pip-show--project"><a href="#uv-pip-show--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-pip-show--prefix"><a href="#uv-pip-show--prefix"><code>--prefix</code></a> <i>prefix</i></dt><dd><p>Show a package from the specified <code>--prefix</code> directory</p>
</dd><dt id="uv-pip-show--project"><a href="#uv-pip-show--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code>, <code>uv.toml</code>, and <code>.python-version</code> files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (<code>.venv</code>).</p>
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
<p>See <code>--directory</code> to change the working directory entirely.</p>
@ -5259,7 +5264,8 @@ system Python environment if no virtual environment is found.</p>
</dd><dt id="uv-pip-show--system"><a href="#uv-pip-show--system"><code>--system</code></a></dt><dd><p>Show a package in the system Python environment.</p>
<p>Disables discovery of virtual environments.</p>
<p>See <a href="#uv-python">uv python</a> for details on Python discovery.</p>
<p>May also be set with the <code>UV_SYSTEM_PYTHON</code> environment variable.</p></dd><dt id="uv-pip-show--verbose"><a href="#uv-pip-show--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>May also be set with the <code>UV_SYSTEM_PYTHON</code> environment variable.</p></dd><dt id="uv-pip-show--target"><a href="#uv-pip-show--target"><code>--target</code></a> <i>target</i></dt><dd><p>Show a package from the specified <code>--target</code> directory</p>
</dd><dt id="uv-pip-show--verbose"><a href="#uv-pip-show--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
</dd></dl>