mirror of
https://github.com/astral-sh/uv.git
synced 2025-12-04 00:54:42 +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>>,
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Disables discovery of virtual environments.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::fmt::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
|
|
@ -19,6 +20,7 @@ pub(crate) fn pip_freeze(
|
|||
strict: bool,
|
||||
python: Option<&str>,
|
||||
system: bool,
|
||||
paths: Option<Vec<PathBuf>>,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
|
|
@ -31,49 +33,68 @@ pub(crate) fn pip_freeze(
|
|||
|
||||
report_target_environment(&environment, cache, printer)?;
|
||||
|
||||
// Build the installed index.
|
||||
let site_packages = SitePackages::from_environment(&environment)?;
|
||||
for dist in site_packages
|
||||
// Collect all the `site-packages` directories.
|
||||
let site_packages = match paths {
|
||||
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()
|
||||
.flat_map(uv_installer::SitePackages::iter)
|
||||
.filter(|dist| !(exclude_editable && dist.is_editable()))
|
||||
.sorted_unstable_by(|a, b| a.name().cmp(b.name()).then(a.version().cmp(b.version())))
|
||||
{
|
||||
match dist {
|
||||
.map(|dist| match dist {
|
||||
InstalledDist::Registry(dist) => {
|
||||
writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?;
|
||||
format!("{}=={}", dist.name().bold(), dist.version)
|
||||
}
|
||||
InstalledDist::Url(dist) => {
|
||||
if dist.editable {
|
||||
writeln!(printer.stdout(), "-e {}", dist.url)?;
|
||||
format!("-e {}", dist.url)
|
||||
} else {
|
||||
writeln!(printer.stdout(), "{} @ {}", dist.name().bold(), dist.url)?;
|
||||
format!("{} @ {}", dist.name().bold(), dist.url)
|
||||
}
|
||||
}
|
||||
InstalledDist::EggInfoFile(dist) => {
|
||||
writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?;
|
||||
format!("{}=={}", dist.name().bold(), dist.version)
|
||||
}
|
||||
InstalledDist::EggInfoDirectory(dist) => {
|
||||
writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?;
|
||||
format!("{}=={}", dist.name().bold(), dist.version)
|
||||
}
|
||||
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.
|
||||
if strict {
|
||||
// Determine the markers to use for resolution.
|
||||
let markers = environment.interpreter().resolver_marker_environment();
|
||||
|
||||
for diagnostic in site_packages.diagnostics(&markers)? {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
diagnostic.message().bold()
|
||||
)?;
|
||||
for entry in site_packages {
|
||||
for diagnostic in entry.diagnostics(&markers)? {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
diagnostic.message().bold()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -627,6 +627,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
args.settings.strict,
|
||||
args.settings.python.as_deref(),
|
||||
args.settings.system,
|
||||
args.paths,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1883,6 +1883,7 @@ impl PipUninstallSettings {
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PipFreezeSettings {
|
||||
pub(crate) exclude_editable: bool,
|
||||
pub(crate) paths: Option<Vec<PathBuf>>,
|
||||
pub(crate) settings: PipSettings,
|
||||
}
|
||||
|
||||
|
|
@ -1894,6 +1895,7 @@ impl PipFreezeSettings {
|
|||
strict,
|
||||
no_strict,
|
||||
python,
|
||||
paths,
|
||||
system,
|
||||
no_system,
|
||||
compat_args: _,
|
||||
|
|
@ -1901,6 +1903,7 @@ impl PipFreezeSettings {
|
|||
|
||||
Self {
|
||||
exclude_editable,
|
||||
paths,
|
||||
settings: PipSettings::combine(
|
||||
PipOptions {
|
||||
python: python.and_then(Maybe::into_option),
|
||||
|
|
|
|||
|
|
@ -354,3 +354,102 @@ Version: 0.22.0
|
|||
|
||||
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>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>
|
||||
|
||||
<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