mirror of
https://github.com/astral-sh/uv.git
synced 2025-12-04 09:02:47 +00:00
Implement pip freeze --path (#10488)
## Summary Resolves #5952 Add a `--path` option to `uv pip freeze` to be compatible with `pip freeze` ## Test Plan New snapshot tests --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
parent
97c1877f6f
commit
f261c65bdd
6 changed files with 151 additions and 21 deletions
|
|
@ -1904,6 +1904,10 @@ pub struct PipFreezeArgs {
|
||||||
)]
|
)]
|
||||||
pub python: Option<Maybe<String>>,
|
pub python: Option<Maybe<String>>,
|
||||||
|
|
||||||
|
/// Restrict to the specified installation path for listing packages (can be used multiple times).
|
||||||
|
#[arg(long("path"), value_parser = parse_file_path)]
|
||||||
|
pub paths: Option<Vec<PathBuf>>,
|
||||||
|
|
||||||
/// List packages in the system Python environment.
|
/// List packages in the system Python environment.
|
||||||
///
|
///
|
||||||
/// Disables discovery of virtual environments.
|
/// Disables discovery of virtual environments.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
@ -19,6 +20,7 @@ pub(crate) fn pip_freeze(
|
||||||
strict: bool,
|
strict: bool,
|
||||||
python: Option<&str>,
|
python: Option<&str>,
|
||||||
system: bool,
|
system: bool,
|
||||||
|
paths: Option<Vec<PathBuf>>,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
|
|
@ -31,49 +33,68 @@ pub(crate) fn pip_freeze(
|
||||||
|
|
||||||
report_target_environment(&environment, cache, printer)?;
|
report_target_environment(&environment, cache, printer)?;
|
||||||
|
|
||||||
// Build the installed index.
|
// Collect all the `site-packages` directories.
|
||||||
let site_packages = SitePackages::from_environment(&environment)?;
|
let site_packages = match paths {
|
||||||
for dist in site_packages
|
Some(paths) => {
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|path| {
|
||||||
|
environment
|
||||||
|
.clone()
|
||||||
|
.with_target(uv_python::Target::from(path))
|
||||||
|
// Drop invalid paths as per `pip freeze`.
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.map(|environment| SitePackages::from_environment(&environment))
|
||||||
|
.collect::<Result<Vec<_>>>()?
|
||||||
|
}
|
||||||
|
None => vec![SitePackages::from_environment(&environment)?],
|
||||||
|
};
|
||||||
|
|
||||||
|
site_packages
|
||||||
.iter()
|
.iter()
|
||||||
|
.flat_map(uv_installer::SitePackages::iter)
|
||||||
.filter(|dist| !(exclude_editable && dist.is_editable()))
|
.filter(|dist| !(exclude_editable && dist.is_editable()))
|
||||||
.sorted_unstable_by(|a, b| a.name().cmp(b.name()).then(a.version().cmp(b.version())))
|
.sorted_unstable_by(|a, b| a.name().cmp(b.name()).then(a.version().cmp(b.version())))
|
||||||
{
|
.map(|dist| match dist {
|
||||||
match dist {
|
|
||||||
InstalledDist::Registry(dist) => {
|
InstalledDist::Registry(dist) => {
|
||||||
writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?;
|
format!("{}=={}", dist.name().bold(), dist.version)
|
||||||
}
|
}
|
||||||
InstalledDist::Url(dist) => {
|
InstalledDist::Url(dist) => {
|
||||||
if dist.editable {
|
if dist.editable {
|
||||||
writeln!(printer.stdout(), "-e {}", dist.url)?;
|
format!("-e {}", dist.url)
|
||||||
} else {
|
} else {
|
||||||
writeln!(printer.stdout(), "{} @ {}", dist.name().bold(), dist.url)?;
|
format!("{} @ {}", dist.name().bold(), dist.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InstalledDist::EggInfoFile(dist) => {
|
InstalledDist::EggInfoFile(dist) => {
|
||||||
writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?;
|
format!("{}=={}", dist.name().bold(), dist.version)
|
||||||
}
|
}
|
||||||
InstalledDist::EggInfoDirectory(dist) => {
|
InstalledDist::EggInfoDirectory(dist) => {
|
||||||
writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?;
|
format!("{}=={}", dist.name().bold(), dist.version)
|
||||||
}
|
}
|
||||||
InstalledDist::LegacyEditable(dist) => {
|
InstalledDist::LegacyEditable(dist) => {
|
||||||
writeln!(printer.stdout(), "-e {}", dist.target.display())?;
|
format!("-e {}", dist.target.display())
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
.dedup()
|
||||||
|
.try_for_each(|dist| writeln!(printer.stdout(), "{dist}"))?;
|
||||||
|
|
||||||
// Validate that the environment is consistent.
|
// Validate that the environment is consistent.
|
||||||
if strict {
|
if strict {
|
||||||
// Determine the markers to use for resolution.
|
// Determine the markers to use for resolution.
|
||||||
let markers = environment.interpreter().resolver_marker_environment();
|
let markers = environment.interpreter().resolver_marker_environment();
|
||||||
|
|
||||||
for diagnostic in site_packages.diagnostics(&markers)? {
|
for entry in site_packages {
|
||||||
writeln!(
|
for diagnostic in entry.diagnostics(&markers)? {
|
||||||
printer.stderr(),
|
writeln!(
|
||||||
"{}{} {}",
|
printer.stderr(),
|
||||||
"warning".yellow().bold(),
|
"{}{} {}",
|
||||||
":".bold(),
|
"warning".yellow().bold(),
|
||||||
diagnostic.message().bold()
|
":".bold(),
|
||||||
)?;
|
diagnostic.message().bold()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -627,6 +627,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
args.settings.strict,
|
args.settings.strict,
|
||||||
args.settings.python.as_deref(),
|
args.settings.python.as_deref(),
|
||||||
args.settings.system,
|
args.settings.system,
|
||||||
|
args.paths,
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1883,6 +1883,7 @@ impl PipUninstallSettings {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct PipFreezeSettings {
|
pub(crate) struct PipFreezeSettings {
|
||||||
pub(crate) exclude_editable: bool,
|
pub(crate) exclude_editable: bool,
|
||||||
|
pub(crate) paths: Option<Vec<PathBuf>>,
|
||||||
pub(crate) settings: PipSettings,
|
pub(crate) settings: PipSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1894,6 +1895,7 @@ impl PipFreezeSettings {
|
||||||
strict,
|
strict,
|
||||||
no_strict,
|
no_strict,
|
||||||
python,
|
python,
|
||||||
|
paths,
|
||||||
system,
|
system,
|
||||||
no_system,
|
no_system,
|
||||||
compat_args: _,
|
compat_args: _,
|
||||||
|
|
@ -1901,6 +1903,7 @@ impl PipFreezeSettings {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
exclude_editable,
|
exclude_editable,
|
||||||
|
paths,
|
||||||
settings: PipSettings::combine(
|
settings: PipSettings::combine(
|
||||||
PipOptions {
|
PipOptions {
|
||||||
python: python.and_then(Maybe::into_option),
|
python: python.and_then(Maybe::into_option),
|
||||||
|
|
|
||||||
|
|
@ -354,3 +354,102 @@ Version: 0.22.0
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freeze_path() -> 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("install-path");
|
||||||
|
|
||||||
|
// Run `pip sync`.
|
||||||
|
context
|
||||||
|
.pip_sync()
|
||||||
|
.arg(requirements_txt.path())
|
||||||
|
.arg("--target")
|
||||||
|
.arg(target.path())
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
// Run `pip freeze`.
|
||||||
|
uv_snapshot!(context.filters(), context.pip_freeze()
|
||||||
|
.arg("--path")
|
||||||
|
.arg(target.path()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
markupsafe==2.1.3
|
||||||
|
tomli==2.0.1
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freeze_multiple_paths() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let requirements_txt1 = context.temp_dir.child("requirements1.txt");
|
||||||
|
requirements_txt1.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?;
|
||||||
|
|
||||||
|
let requirements_txt2 = context.temp_dir.child("requirements2.txt");
|
||||||
|
requirements_txt2.write_str("MarkupSafe==2.1.3\nrequests==2.31.0")?;
|
||||||
|
|
||||||
|
let target1 = context.temp_dir.child("install-path1");
|
||||||
|
let target2 = context.temp_dir.child("install-path2");
|
||||||
|
|
||||||
|
// Run `pip sync`.
|
||||||
|
for (target, requirements_txt) in [
|
||||||
|
(target1.path(), requirements_txt1),
|
||||||
|
(target2.path(), requirements_txt2),
|
||||||
|
] {
|
||||||
|
context
|
||||||
|
.pip_sync()
|
||||||
|
.arg(requirements_txt.path())
|
||||||
|
.arg("--target")
|
||||||
|
.arg(target)
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run `pip freeze`.
|
||||||
|
uv_snapshot!(context.filters(), context.pip_freeze().arg("--path").arg(target1.path()).arg("--path").arg(target2.path()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
markupsafe==2.1.3
|
||||||
|
requests==2.31.0
|
||||||
|
tomli==2.0.1
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// We follow pip in just ignoring nonexistent paths
|
||||||
|
#[test]
|
||||||
|
fn freeze_nonexistent_path() {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let nonexistent_dir = {
|
||||||
|
let dir = context.temp_dir.child("blahblah");
|
||||||
|
assert!(!dir.exists());
|
||||||
|
dir
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run `pip freeze`.
|
||||||
|
uv_snapshot!(context.filters(), context.pip_freeze()
|
||||||
|
.arg("--path")
|
||||||
|
.arg(nonexistent_dir.path()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7045,6 +7045,8 @@ uv pip freeze [OPTIONS]
|
||||||
<p>When disabled, uv will only use locally cached data and locally available files.</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>
|
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p>
|
||||||
|
</dd><dt><code>--path</code> <i>paths</i></dt><dd><p>Restrict to the specified installation path for listing packages (can be used multiple times)</p>
|
||||||
|
|
||||||
</dd><dt><code>--project</code> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
|
</dd><dt><code>--project</code> <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>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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue