From 16296d67918d675f894ba2b3f201e22b264b2a75 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 13 Feb 2025 12:29:49 -0600 Subject: [PATCH] Allow `-p` to use complex Python version requests in `uv pip compile` --- crates/uv-cli/src/lib.rs | 13 +- crates/uv/src/commands/pip/compile.rs | 67 +++++- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 3 + crates/uv/tests/it/pip_compile.rs | 321 ++++++++++++++++++++++++++ docs/reference/cli.md | 2 +- 6 files changed, 403 insertions(+), 4 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 1d5e4739a9ca2..7c7de43816785 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1170,9 +1170,20 @@ pub struct PipCompileArgs { /// /// If a patch version is omitted, the minimum patch version is assumed. For /// example, `3.8` is mapped to `3.8.0`. - #[arg(long, short, help_heading = "Python options")] + #[arg(long, help_heading = "Python options")] pub python_version: Option, + /// The Python interpreter or version to use for resolution. + /// + /// In previous versions of uv, `-p` was an alias for `--python-version` in `uv pip compile` but + /// an alias for `--python` in all other commands. This option is provided as a backwards + /// compatible shim, allowing `-p` to behave as `--python` without introducing a breaking + /// change. + /// + /// `UV_PYTHON` is respected, but overridden by `--python-version` or `--python`. + #[arg(short, hide = true, help_heading = "Python options")] + pub python_legacy: Option, + /// The platform for which requirements should be resolved. /// /// Represented as a "target triple", a string that describes the target platform in terms of diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 0dc21a51273c5..2fc3c2709dd22 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -38,6 +38,7 @@ use uv_resolver::{ InMemoryIndex, OptionsBuilder, PrereleaseMode, PythonRequirement, RequiresPython, ResolutionMode, ResolverEnvironment, }; +use uv_static::EnvVars; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_warnings::warn_user; @@ -94,6 +95,7 @@ pub(crate) async fn pip_compile( annotation_style: AnnotationStyle, link_mode: LinkMode, python: Option, + mut python_legacy: Option, system: bool, python_preference: PythonPreference, concurrency: Concurrency, @@ -103,6 +105,29 @@ pub(crate) async fn pip_compile( printer: Printer, preview: PreviewMode, ) -> Result { + // If the user requests both `-p` and `--python` or `--python-version`, error + if let Some(python_legacy) = python_legacy.as_ref() { + if let Some(python) = python.as_ref() { + return Err(anyhow!( + "Cannot specify both `-p` ({python_legacy}) and `--python` ({python}).", + )); + } + if let Some(python_version) = python_version.as_ref() { + return Err(anyhow!( + "Cannot specify both `-p` ({python_legacy}) and `--python-version` ({python_version}).", + )); + } + } + + // Respect `UV_PYTHON` with legacy behavior + if python_legacy.is_none() && python_version.is_none() && python.is_none() { + if let Ok(python) = std::env::var(EnvVars::UV_PYTHON) { + if !python.is_empty() { + python_legacy = Some(python); + } + } + } + // If the user requests `extras` but does not provide a valid source (e.g., a `pyproject.toml`), // return an error. if !extras.is_empty() && !requirements.iter().any(RequirementsSource::allows_extras) { @@ -188,9 +213,30 @@ pub(crate) async fn pip_compile( let interpreter = if let Some(python) = python.as_ref() { let request = PythonRequest::parse(python); PythonInstallation::find(&request, environment_preference, python_preference, &cache) + } else if let Some(python_legacy) = python_legacy.as_ref() { + // `-p` uses backwards compatible behavior and does not fail if it cannot find the requested + // version; previously, this was short for `--python-version` + let request = PythonRequest::parse(python_legacy); + match request { + PythonRequest::Version(..) => PythonInstallation::find_best( + &request, + environment_preference, + python_preference, + &cache, + ), + // For queries _other_ than a version, we fail as we would if `--python` were used. + // There's no backwards compatibility concern here because `-p` / `--python-version` + // was restricted to Python versions. + _ => PythonInstallation::find( + &request, + environment_preference, + python_preference, + &cache, + ), + } } else { - // TODO(zanieb): The split here hints at a problem with the abstraction; we should be able to use - // `PythonInstallation::find(...)` here. + // TODO(zanieb): The split here hints at a problem with the request abstraction; we should + // be able to use `PythonInstallation::find(...)` here. let request = if let Some(version) = python_version.as_ref() { // TODO(zanieb): We should consolidate `VersionRequest` and `PythonVersion` PythonRequest::Version(VersionRequest::from(version)) @@ -227,6 +273,23 @@ pub(crate) async fn pip_compile( } } + if let Some(python_legacy) = python_legacy.as_ref() { + let request = PythonRequest::parse(&python_legacy); + + // If the requested interpreter version does not match the interpreter we're using + if !no_build.is_none() && !request.satisfied(&interpreter, &cache) { + // Other cases should be unreachable, as we only use the legacy fallback for version + // requests + if let PythonRequest::Version(version) = request { + warn_user!( + "The requested Python version {} is not available; {} will be used to build dependencies instead.", + version, + interpreter.python_version(), + ); + } + } + } + // Create the shared state. let state = SharedState::default(); diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 930c61121cdbe..ff83b3a2361f2 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -428,6 +428,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.annotation_style, args.settings.link_mode, args.settings.python, + args.python_legacy, args.settings.system, globals.python_preference, globals.concurrency, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 91ba1eba2ebde..bba6e328715bd 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1533,6 +1533,7 @@ pub(crate) struct PipCompileSettings { pub(crate) overrides_from_workspace: Vec, pub(crate) environments: SupportedEnvironments, pub(crate) refresh: Refresh, + pub(crate) python_legacy: Option, pub(crate) settings: PipSettings, } @@ -1572,6 +1573,7 @@ impl PipCompileSettings { no_binary, only_binary, python_version, + python_legacy, python_platform, universal, no_universal, @@ -1641,6 +1643,7 @@ impl PipCompileSettings { overrides_from_workspace, environments, refresh: Refresh::from(refresh), + python_legacy, settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 53836facf568d..0906dfc2a8137 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -1309,6 +1309,89 @@ fn compile_python_312() -> Result<()> { "### ); + // This should work with the short flag + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("-p") + .arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.12 + black==23.10.1 + # via -r requirements.in + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + // And `--python` + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("--python") + .arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in --python 3.12 + black==23.10.1 + # via -r requirements.in + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + // And `UV_PYTHON` + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .env("UV_PYTHON", "3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in + black==23.10.1 + # via -r requirements.in + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + Ok(()) } @@ -1379,6 +1462,244 @@ fn compile_fallback_interpreter() -> Result<()> { "### ); + // This should work for the short flag too + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("-p") + .arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.12 + black==23.10.[X] + # via -r requirements.in + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + tomli==2.0.1 + # via black + typing-extensions==4.10.0 + # via black + + ----- stderr ----- + Resolved 8 packages in [TIME] + "### + ); + + // And for `UV_PYTHON` + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .env(EnvVars::UV_PYTHON, "3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in + black==23.10.[X] + # via -r requirements.in + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + tomli==2.0.1 + # via black + typing-extensions==4.10.0 + # via black + + ----- stderr ----- + Resolved 8 packages in [TIME] + "### + ); + + // But fail for `--python` + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("--python") + .arg("3.12"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No interpreter found for Python 3.12 in virtual environments, managed installations, or search path + "### + ); + + // The short flag also allows complex requests, like alternative implementations, but we should + // not fallback if that cannot be found + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("-p") + .arg("pypy"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No interpreter found for PyPy in virtual environments, managed installations, or search path + "### + ); + + Ok(()) +} + +#[test] +fn compile_python_conflicts() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("black==23.10.1")?; + + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("--python-version") + .arg("3.12") + .arg("-p") + .arg("3.12"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Cannot specify both `-p` (3.12) and `--python-version` (3.12). + "### + ); + + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("--python") + .arg("3.12") + .arg("-p") + .arg("3.12"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Cannot specify both `-p` (3.12) and `--python` (3.12). + "### + ); + + // `UV_PYTHON` should be usable with `-p` + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("-p") + .arg("3.12") + .env("UV_PYTHON", "3.11"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.12 + black==23.10.1 + # via -r requirements.in + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + // `UV_PYTHON` should be usable with `--python` + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("--python") + .arg("3.12") + .env("UV_PYTHON", "3.11"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in --python 3.12 + black==23.10.1 + # via -r requirements.in + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + // `UV_PYTHON` should be usable with `--python-version` + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("--python-version") + .arg("3.12") + .env("UV_PYTHON", "3.11"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in --python-version 3.12 + black==23.10.1 + # via -r requirements.in + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==24.0 + # via black + pathspec==0.12.1 + # via black + platformdirs==4.2.0 + # via black + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + // `--python-legacy` is not available in long-form + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("--python-legacy") + .arg("3.12"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: unexpected argument '--python-legacy' found + + tip: a similar argument exists: '--python' + + Usage: uv pip compile --cache-dir [CACHE_DIR] --python ... + + For more information, try '--help'. + "### + ); + Ok(()) } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 8d6be84c413b4..ab17a2654f3f6 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -5955,7 +5955,7 @@ uv pip compile [OPTIONS] ...
  • only-system: Only use system Python installations; never use managed Python installations
  • -
    --python-version, -p python-version

    The Python version to use for resolution.

    +
    --python-version python-version

    The Python version to use for resolution.

    For example, 3.8 or 3.8.17.