Allow round-trip via freeze command (#1936)

## Summary

We're printing the `Display` representation of `InstalledDist`, which
isn't guaranteed to be (and in fact isn't) a valid PEP 508 requirement,
making it impossible to use the `freeze` output as an input to an
install.

Closes https://github.com/astral-sh/uv/issues/1931.
This commit is contained in:
Charlie Marsh 2024-02-23 15:01:54 -05:00 committed by GitHub
parent 8ff6182815
commit ca489ac3bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 31 deletions

View file

@ -6,7 +6,7 @@ use itertools::Itertools;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use tracing::debug; use tracing::debug;
use distribution_types::Name; use distribution_types::{InstalledDist, Name};
use platform_host::Platform; use platform_host::Platform;
use uv_cache::Cache; use uv_cache::Cache;
use uv_fs::Normalized; use uv_fs::Normalized;
@ -34,7 +34,18 @@ pub(crate) fn pip_freeze(cache: &Cache, strict: bool, mut printer: Printer) -> R
.iter() .iter()
.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())))
{ {
println!("{dist}"); match dist {
InstalledDist::Registry(dist) => {
println!("{}=={}", dist.name().bold(), dist.version);
}
InstalledDist::Url(dist) => {
if dist.editable {
println!("-e {}", dist.url);
} else {
println!("{} @ {}", dist.name().bold(), dist.url);
}
}
}
} }
// Validate that the environment is consistent. // Validate that the environment is consistent.

View file

@ -3,6 +3,7 @@
use std::process::Command; use std::process::Command;
use anyhow::Result; use anyhow::Result;
use assert_cmd::prelude::*;
use assert_fs::prelude::*; use assert_fs::prelude::*;
use crate::common::{get_bin, uv_snapshot, TestContext}; use crate::common::{get_bin, uv_snapshot, TestContext};
@ -22,19 +23,6 @@ fn command(context: &TestContext) -> Command {
command command
} }
/// Create a `pip sync` command with options shared across scenarios.
fn sync_command(context: &TestContext) -> Command {
let mut command = Command::new(get_bin());
command
.arg("pip")
.arg("sync")
.arg("--cache-dir")
.arg(context.cache_dir.path())
.env("VIRTUAL_ENV", context.venv.as_os_str())
.current_dir(&context.temp_dir);
command
}
/// List multiple installed packages in a virtual environment. /// List multiple installed packages in a virtual environment.
#[test] #[test]
fn freeze_many() -> Result<()> { fn freeze_many() -> Result<()> {
@ -44,20 +32,15 @@ fn freeze_many() -> Result<()> {
requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?;
// Run `pip sync`. // Run `pip sync`.
uv_snapshot!(sync_command(&context) Command::new(get_bin())
.arg("requirements.txt"), @r###" .arg("pip")
success: true .arg("sync")
exit_code: 0 .arg(requirements_txt.path())
----- stdout ----- .arg("--cache-dir")
.arg(context.cache_dir.path())
----- stderr ----- .env("VIRTUAL_ENV", context.venv.as_os_str())
Resolved 2 packages in [TIME] .assert()
Downloaded 2 packages in [TIME] .success();
Installed 2 packages in [TIME]
+ markupsafe==2.1.3
+ tomli==2.0.1
"###
);
// Run `pip freeze`. // Run `pip freeze`.
uv_snapshot!(command(&context) uv_snapshot!(command(&context)
@ -79,8 +62,6 @@ fn freeze_many() -> Result<()> {
#[test] #[test]
#[cfg(unix)] #[cfg(unix)]
fn freeze_duplicate() -> Result<()> { fn freeze_duplicate() -> Result<()> {
use assert_cmd::assert::OutputAssertExt;
use crate::common::{copy_dir_all, INSTA_FILTERS}; use crate::common::{copy_dir_all, INSTA_FILTERS};
// Sync a version of `pip` into a virtual environment. // Sync a version of `pip` into a virtual environment.
@ -157,3 +138,40 @@ fn freeze_duplicate() -> Result<()> {
Ok(()) Ok(())
} }
/// List a direct URL package in a virtual environment.
#[test]
fn freeze_url() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("anyio\niniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl")?;
// Run `pip sync`.
Command::new(get_bin())
.arg("pip")
.arg("sync")
.arg(requirements_txt.path())
.arg("--cache-dir")
.arg(context.cache_dir.path())
.env("VIRTUAL_ENV", context.venv.as_os_str())
.assert()
.success();
// Run `pip freeze`.
uv_snapshot!(command(&context)
.arg("--strict"), @r###"
success: true
exit_code: 0
----- stdout -----
anyio==4.3.0
iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl
----- stderr -----
warning: The package `anyio` requires `idna >=2.8`, but it's not installed.
warning: The package `anyio` requires `sniffio >=1.1`, but it's not installed.
"###
);
Ok(())
}