From ca489ac3bbfd5f84d139ef06e89d036f180c33a3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 23 Feb 2024 15:01:54 -0500 Subject: [PATCH] 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. --- crates/uv/src/commands/pip_freeze.rs | 15 +++++- crates/uv/tests/pip_freeze.rs | 76 +++++++++++++++++----------- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/crates/uv/src/commands/pip_freeze.rs b/crates/uv/src/commands/pip_freeze.rs index ba2215bdd..cb03d6b0d 100644 --- a/crates/uv/src/commands/pip_freeze.rs +++ b/crates/uv/src/commands/pip_freeze.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use owo_colors::OwoColorize; use tracing::debug; -use distribution_types::Name; +use distribution_types::{InstalledDist, Name}; use platform_host::Platform; use uv_cache::Cache; use uv_fs::Normalized; @@ -34,7 +34,18 @@ pub(crate) fn pip_freeze(cache: &Cache, strict: bool, mut printer: Printer) -> R .iter() .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. diff --git a/crates/uv/tests/pip_freeze.rs b/crates/uv/tests/pip_freeze.rs index 4db47dd35..f219ce4d7 100644 --- a/crates/uv/tests/pip_freeze.rs +++ b/crates/uv/tests/pip_freeze.rs @@ -3,6 +3,7 @@ use std::process::Command; use anyhow::Result; +use assert_cmd::prelude::*; use assert_fs::prelude::*; use crate::common::{get_bin, uv_snapshot, TestContext}; @@ -22,19 +23,6 @@ fn command(context: &TestContext) -> 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. #[test] fn freeze_many() -> Result<()> { @@ -44,20 +32,15 @@ fn freeze_many() -> Result<()> { requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; // Run `pip sync`. - uv_snapshot!(sync_command(&context) - .arg("requirements.txt"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - Downloaded 2 packages in [TIME] - Installed 2 packages in [TIME] - + markupsafe==2.1.3 - + tomli==2.0.1 - "### - ); + 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) @@ -79,8 +62,6 @@ fn freeze_many() -> Result<()> { #[test] #[cfg(unix)] fn freeze_duplicate() -> Result<()> { - use assert_cmd::assert::OutputAssertExt; - use crate::common::{copy_dir_all, INSTA_FILTERS}; // Sync a version of `pip` into a virtual environment. @@ -157,3 +138,40 @@ fn freeze_duplicate() -> Result<()> { 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(()) +}