From 4f5913520b74c039f0cb7f53e30123e46c39c00f Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 19 Jul 2024 15:25:07 -0500 Subject: [PATCH 1/3] Add support for requirements files, constraints, and overrides in `uv run` --- crates/uv-cli/src/lib.rs | 30 ++ crates/uv-requirements/src/specification.rs | 5 + crates/uv/src/commands/project/add.rs | 2 +- crates/uv/src/commands/project/mod.rs | 6 +- crates/uv/src/commands/project/remove.rs | 2 +- crates/uv/src/commands/project/run.rs | 102 +++-- crates/uv/src/commands/project/sync.rs | 6 +- crates/uv/src/lib.rs | 17 + crates/uv/src/settings.rs | 18 + crates/uv/tests/run.rs | 397 +++++++++++++++++++- 10 files changed, 542 insertions(+), 43 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index b57e93650250..2c3adef88903 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1867,6 +1867,36 @@ pub struct RunArgs { /// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path. #[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)] pub python: Option, + + /// Require all packages listed in the given `requirements.txt` files. + /// + /// Using `pyproject.toml`, `setup.py`, or `setup.cfg` files is not allowed. + /// + /// If `-` is provided, then requirements will be read from stdin. + #[arg(long, short, value_parser = parse_maybe_file_path)] + pub requirements: Vec>, + + /// Constrain versions using the given requirements files. + /// + /// Constraints files are `requirements.txt`-like files that only control the _version_ of a + /// requirement that's installed. However, including a package in a constraints file will _not_ + /// trigger the installation of that package. + /// + /// This is equivalent to pip's `--constraint` option. + #[arg(long, short, env = "UV_CONSTRAINTS", value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub constraints: Vec>, + + /// Override versions using the given requirements files. + /// + /// Overrides files are `requirements.txt`-like files that force a specific version of a + /// requirement to be installed, regardless of the requirements declared by any constituent + /// package, and regardless of whether this would be considered an invalid resolution. + /// + /// While constraints are _additive_, in that they're combined with the requirements of the + /// constituent packages, overrides are _absolute_, in that they completely replace the + /// requirements of the constituent packages. + #[arg(long, env = "UV_OVERRIDES", value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub r#overrides: Vec>, } #[derive(Args)] diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index 1544714260d5..ac0c10c9174b 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -336,4 +336,9 @@ impl RequirementsSpecification { ..Self::default() } } + + /// Return true if the specification does not include any requirements to install. + pub fn is_empty(&self) -> bool { + self.requirements.is_empty() && self.source_trees.is_empty() && self.overrides.is_empty() + } } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 6a65cb35651f..672aea07a6fd 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -239,7 +239,7 @@ pub(crate) async fn add( &VirtualProject::Project(project), &venv, &lock, - extras, + &extras, dev, Modifications::Sufficient, settings.as_ref().into(), diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index ed84dc6b8f32..9780f6c39480 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -644,7 +644,11 @@ pub(crate) async fn update_environment( // Check if the current environment satisfies the requirements let site_packages = SitePackages::from_environment(&venv)?; - if spec.source_trees.is_empty() && reinstall.is_none() && upgrade.is_none() { + if spec.source_trees.is_empty() + && reinstall.is_none() + && upgrade.is_none() + && spec.overrides.is_empty() + { match site_packages.satisfies(&spec.requirements, &spec.constraints)? { // If the requirements are already satisfied, we're done. SatisfiesResult::Fresh { diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 5715631811d5..8c31c49a386b 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -132,7 +132,7 @@ pub(crate) async fn remove( &VirtualProject::Project(project), &venv, &lock, - extras, + &extras, dev, Modifications::Exact, settings.as_ref().into(), diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 36da04ed877a..4749a18329d5 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -3,7 +3,7 @@ use std::ffi::OsString; use std::fmt::Write; use std::path::PathBuf; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; use tokio::process::Command; @@ -23,10 +23,10 @@ use uv_python::{ PythonInstallation, PythonPreference, PythonRequest, VersionRequest, }; -use uv_requirements::{RequirementsSource, RequirementsSpecification}; +use uv_requirements::RequirementsSource; use uv_warnings::warn_user_once; -use crate::commands::pip::operations::Modifications; +use crate::commands::pip::operations::{self, Modifications}; use crate::commands::project::environment::CachedEnvironment; use crate::commands::project::ProjectError; use crate::commands::reporters::PythonDownloadReporter; @@ -39,6 +39,8 @@ use crate::settings::ResolverInstallerSettings; pub(crate) async fn run( command: ExternalCommand, requirements: Vec, + constraints: &[RequirementsSource], + overrides: &[RequirementsSource], locked: bool, frozen: bool, package: Option, @@ -60,6 +62,27 @@ pub(crate) async fn run( warn_user_once!("`uv run` is experimental and may change without warning"); } + // These cases seem quite complex because it changes the current package — let's just + // ban it entirely for now + if requirements + .iter() + .any(|source| matches!(source, RequirementsSource::PyprojectToml(_))) + { + bail!("Adding requirements from a `pyproject.toml` is not supported in `uv run`"); + } + if requirements + .iter() + .any(|source| matches!(source, RequirementsSource::SetupCfg(_))) + { + bail!("Adding requirements from a `setup.cfg` is not supported in `uv run`"); + } + if requirements + .iter() + .any(|source| matches!(source, RequirementsSource::SetupCfg(_))) + { + bail!("Adding requirements from a `setup.py` is not supported in `uv run`"); + } + // Parse the input command. let command = RunCommand::from(command); @@ -215,7 +238,7 @@ pub(crate) async fn run( &project, &venv, &lock, - extras, + &extras, dev, Modifications::Sufficient, settings.as_ref().into(), @@ -263,16 +286,22 @@ pub(crate) async fn run( ); } - // Read the `--with` requirements. - let spec = if requirements.is_empty() { + // Read the requirements. + let spec = if requirements.is_empty() && constraints.is_empty() && overrides.is_empty() { None } else { let client_builder = BaseClientBuilder::new() .connectivity(connectivity) .native_tls(native_tls); - let spec = - RequirementsSpecification::from_simple_sources(&requirements, &client_builder).await?; + let spec = operations::read_requirements( + &requirements, + constraints, + overrides, + &extras, + &client_builder, + ) + .await?; Some(spec) }; @@ -281,6 +310,7 @@ pub(crate) async fn run( // any `--with` requirements, and we already have a base environment, then there's no need to // create an additional environment. let skip_ephemeral = base_interpreter.as_ref().is_some_and(|base_interpreter| { + // No additional requrements let Some(spec) = spec.as_ref() else { return true; }; @@ -293,6 +323,13 @@ pub(crate) async fn run( return false; } + // `SitePackages::satisfies` handle overrides yet and constraints are only enforced on `--with` + // requirements instead of the project requirements so we perform a full resolution + // to ensure things are up to date + if !(spec.constraints.is_empty() && spec.overrides.is_empty()) { + return false; + } + match site_packages.satisfies(&spec.requirements, &spec.constraints) { // If the requirements are already satisfied, we're done. Ok(SatisfiesResult::Fresh { @@ -363,35 +400,28 @@ pub(crate) async fn run( false, )?; - if requirements.is_empty() { - Some(venv) - } else { - debug!("Syncing ephemeral requirements"); - - let client_builder = BaseClientBuilder::new() - .connectivity(connectivity) - .native_tls(native_tls); - - let spec = - RequirementsSpecification::from_simple_sources(&requirements, &client_builder) - .await?; - - // Install the ephemeral requirements. - Some( - project::update_environment( - venv, - spec, - &settings, - &state, - preview, - connectivity, - concurrency, - native_tls, - cache, - printer, + match spec { + None => Some(venv), + Some(spec) if spec.is_empty() => Some(venv), + Some(spec) => { + debug!("Syncing ephemeral requirements"); + // Install the ephemeral requirements. + Some( + project::update_environment( + venv, + spec, + &settings, + &state, + preview, + connectivity, + concurrency, + native_tls, + cache, + printer, + ) + .await?, ) - .await?, - ) + } } }; diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index a3a1c896a716..06ef7eed5215 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -95,7 +95,7 @@ pub(crate) async fn sync( &project, &venv, &lock, - extras, + &extras, dev, modifications, settings.as_ref().into(), @@ -117,7 +117,7 @@ pub(super) async fn do_sync( project: &VirtualProject, venv: &PythonEnvironment, lock: &Lock, - extras: ExtrasSpecification, + extras: &ExtrasSpecification, dev: bool, modifications: Modifications, settings: InstallerSettingsRef<'_>, @@ -163,7 +163,7 @@ pub(super) async fn do_sync( let tags = venv.interpreter().tags()?; // Read the lockfile. - let resolution = lock.to_resolution(project, markers, tags, &extras, &dev)?; + let resolution = lock.to_resolution(project, markers, tags, extras, &dev)?; // Initialize the registry client. let client = RegistryClientBuilder::new(cache.clone()) diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 23a1340bbd5e..e3f92e0cb09f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -848,11 +848,28 @@ async fn run_project( .with .into_iter() .map(RequirementsSource::from_package) + .chain( + args.requirements + .into_iter() + .map(RequirementsSource::from_requirements_file), + ) + .collect::>(); + let constraints = args + .constraints + .into_iter() + .map(RequirementsSource::from_constraints_txt) + .collect::>(); + let overrides = args + .r#overrides + .into_iter() + .map(RequirementsSource::from_overrides_txt) .collect::>(); commands::run( args.command, requirements, + &constraints, + &overrides, args.locked, args.frozen, args.package, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 2e64b4b68415..13fbd51a0d8b 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -186,6 +186,9 @@ pub(crate) struct RunSettings { pub(crate) with: Vec, pub(crate) package: Option, pub(crate) python: Option, + pub(crate) requirements: Vec, + pub(crate) constraints: Vec, + pub(crate) r#overrides: Vec, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, } @@ -209,6 +212,9 @@ impl RunSettings { refresh, package, python, + requirements, + constraints, + r#overrides, } = args; Self { @@ -224,6 +230,18 @@ impl RunSettings { package, python, refresh: Refresh::from(refresh), + requirements: requirements + .into_iter() + .filter_map(Maybe::into_option) + .collect(), + constraints: constraints + .into_iter() + .filter_map(Maybe::into_option) + .collect(), + r#overrides: r#overrides + .into_iter() + .filter_map(Maybe::into_option) + .collect(), settings: ResolverInstallerSettings::combine( resolver_installer_options(installer, build), filesystem, diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index 515c10c33159..ba71629387ef 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -2,7 +2,7 @@ use anyhow::Result; use assert_cmd::assert::OutputAssertExt; -use assert_fs::prelude::*; +use assert_fs::{fixture::ChildPath, prelude::*}; use indoc::indoc; use common::{uv_snapshot, TestContext}; @@ -541,3 +541,398 @@ fn run_frozen() -> Result<()> { Ok(()) } + +#[test] +fn run_empty_requirements_txt() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio", "sniffio==1.3.1"] + "# + })?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import sniffio + " + })?; + + let requirements_txt = + ChildPath::new(context.temp_dir.canonicalize()?.join("requirements.txt")); + requirements_txt.touch()?; + + // The project environment is synced on the first invocation. + uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + foo==1.0.0 (from file://[TEMP_DIR]/) + + idna==3.6 + + sniffio==1.3.1 + warning: Requirements file requirements.txt does not contain any dependencies + "###); + + // Then reused in subsequent invocations + uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Audited 4 packages in [TIME] + warning: Requirements file requirements.txt does not contain any dependencies + "###); + + Ok(()) +} + +#[test] +fn run_requirements_txt() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio", "sniffio==1.3.1"] + "# + })?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import sniffio + " + })?; + + // Requesting an unsatisfied requirement should install it. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("iniconfig")?; + + uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + foo==1.0.0 (from file://[TEMP_DIR]/) + + idna==3.6 + + sniffio==1.3.1 + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + // Requesting a satisfied requirement should use the base environment. + requirements_txt.write_str("sniffio")?; + + uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Audited 4 packages in [TIME] + "###); + + // Unless the user requests a different version. + requirements_txt.write_str("sniffio<1.3.1")?; + + uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Audited 4 packages in [TIME] + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + sniffio==1.3.0 + "###); + + // Or includes an unsatisfied requirement via `--with`. + requirements_txt.write_str("sniffio")?; + + uv_snapshot!(context.filters(), context.run() + .arg("-r") + .arg(requirements_txt.as_os_str()) + .arg("--with") + .arg("iniconfig") + .arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Audited 4 packages in [TIME] + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + sniffio==1.3.1 + "###); + + Ok(()) +} + +#[test] +fn run_constraints_txt() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio", "sniffio==1.3.1"] + "# + })?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import sniffio + " + })?; + + // Constraining an unrequested requirement should not include it. + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("iniconfig")?; + + uv_snapshot!(context.filters(), context.run().arg("--constraints").arg(constraints_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + foo==1.0.0 (from file://[TEMP_DIR]/) + + idna==3.6 + + sniffio==1.3.1 + "###); + + // Subsequent invocations should use the base environment if there's a compatible constraint. + constraints_txt.write_str("sniffio")?; + + uv_snapshot!(context.filters(), context.run().arg("--constraints").arg(constraints_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Audited 4 packages in [TIME] + "###); + + // Unless the user requests a different version of a base requirement. + constraints_txt.write_str("sniffio<1.3.1")?; + + uv_snapshot!(context.filters(), context.run().arg("--constraints").arg(constraints_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Audited 4 packages in [TIME] + "###); + + // Or includes an unsatisfied requirement via `--with`. + constraints_txt.write_str("iniconfig<2.0.0")?; + + uv_snapshot!(context.filters(), context.run() + .arg("--constraints") + .arg(constraints_txt.as_os_str()) + .arg("--with") + .arg("iniconfig") + .arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Audited 4 packages in [TIME] + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==1.1.1 + "###); + + Ok(()) +} + +#[test] +fn run_overrides_txt() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio", "sniffio==1.3.1"] + "# + })?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import sniffio + " + })?; + + // Overriding an unrequested requirement should not include it. + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("iniconfig==2.0.0")?; + + uv_snapshot!(context.filters(), context.run().arg("--overrides").arg(overrides_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + foo==1.0.0 (from file://[TEMP_DIR]/) + + idna==3.6 + + sniffio==1.3.1 + Resolved 0 packages in [TIME] + Audited 0 packages in [TIME] + "###); + + // Subsequent invocations should use the base environment if there is a compatible override. + overrides_txt.write_str("idna==3.6")?; + + uv_snapshot!(context.filters(), context.run().arg("--overrides").arg(overrides_txt.as_os_str()).arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + Resolved 6 packages in [TIME] + Audited 4 packages in [TIME] + Resolved 0 packages in [TIME] + Audited 0 packages in [TIME] + "###); + + // Overriding a dependency should result in an ephemeral environment. + overrides_txt.write_str("idna==3.5")?; + + uv_snapshot!(context.filters(), context.run().arg("-v") + .arg("--overrides") + .arg(overrides_txt.as_os_str()) + .arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + DEBUG uv [VERSION] ([COMMIT] DATE) + warning: `uv run` is experimental and may change without warning + DEBUG Found project root: `[TEMP_DIR]/` + DEBUG No workspace root found, using project root + DEBUG Discovered project `foo` at: [TEMP_DIR]/ + DEBUG Interpreter meets the requested Python: `Python >=3.8` + DEBUG Using request timeout of [TIME] + DEBUG Resolving with existing `uv.lock` + DEBUG Acquired lock for `[CACHE_DIR]/built-wheels-v3/editable/aa56cdf257401d2d` + DEBUG Preparing metadata for: foo @ file://[TEMP_DIR]/ + DEBUG No static `PKG-INFO` available for: foo @ file://[TEMP_DIR]/ (MissingPkgInfo) + DEBUG Found static `pyproject.toml` for: foo @ file://[TEMP_DIR]/ + DEBUG No workspace root found, using project root + DEBUG Solving with installed Python version: 3.12.[X] + DEBUG Solving with target Python version: >=3.8 + DEBUG Adding direct dependency: foo* + DEBUG Searching for a compatible version of foo @ file://[TEMP_DIR]/ (*) + DEBUG Adding transitive dependency for foo==1.0.0: anyio* + DEBUG Adding transitive dependency for foo==1.0.0: sniffio==1.3.1 + DEBUG Searching for a compatible version of sniffio (==1.3.1) + DEBUG Selecting: sniffio==1.3.1 [preference] (sniffio-1.3.1-py3-none-any.whl) + DEBUG Searching for a compatible version of anyio (*) + DEBUG Selecting: anyio==4.3.0 [preference] (anyio-4.3.0-py3-none-any.whl) + DEBUG Adding transitive dependency for anyio==4.3.0: exceptiongroup{python_version < '3.11'}==1.2.0 + DEBUG Adding transitive dependency for anyio==4.3.0: idna==3.6 + DEBUG Adding transitive dependency for anyio==4.3.0: sniffio==1.3.1 + DEBUG Adding transitive dependency for anyio==4.3.0: typing-extensions{python_version < '3.11'}==4.10.0 + DEBUG Searching for a compatible version of exceptiongroup{python_version < '3.11'} (==1.2.0) + DEBUG Selecting: exceptiongroup==1.2.0 [preference] (exceptiongroup-1.2.0-py3-none-any.whl) + DEBUG Adding transitive dependency for exceptiongroup==1.2.0: exceptiongroup==1.2.0 + DEBUG Adding transitive dependency for exceptiongroup==1.2.0: exceptiongroup{python_version < '3.11'}==1.2.0 + DEBUG Searching for a compatible version of exceptiongroup{python_version < '3.11'} (==1.2.0) + DEBUG Selecting: exceptiongroup==1.2.0 [preference] (exceptiongroup-1.2.0-py3-none-any.whl) + DEBUG Searching for a compatible version of exceptiongroup (==1.2.0) + DEBUG Selecting: exceptiongroup==1.2.0 [preference] (exceptiongroup-1.2.0-py3-none-any.whl) + DEBUG Searching for a compatible version of idna (==3.6) + DEBUG Selecting: idna==3.6 [preference] (idna-3.6-py3-none-any.whl) + DEBUG Searching for a compatible version of typing-extensions{python_version < '3.11'} (==4.10.0) + DEBUG Selecting: typing-extensions==4.10.0 [preference] (typing_extensions-4.10.0-py3-none-any.whl) + DEBUG Adding transitive dependency for typing-extensions==4.10.0: typing-extensions==4.10.0 + DEBUG Adding transitive dependency for typing-extensions==4.10.0: typing-extensions{python_version < '3.11'}==4.10.0 + DEBUG Searching for a compatible version of typing-extensions{python_version < '3.11'} (==4.10.0) + DEBUG Selecting: typing-extensions==4.10.0 [preference] (typing_extensions-4.10.0-py3-none-any.whl) + DEBUG Searching for a compatible version of typing-extensions (==4.10.0) + DEBUG Selecting: typing-extensions==4.10.0 [preference] (typing_extensions-4.10.0-py3-none-any.whl) + DEBUG Tried 6 versions: anyio 1, exceptiongroup 1, foo 1, idna 1, sniffio 1, typing-extensions 1 + DEBUG Split universal resolution took [TIME] + Resolved 6 packages in [TIME] + DEBUG Using request timeout of [TIME] + DEBUG Requirement already installed: anyio==4.3.0 + DEBUG Requirement already installed: foo==1.0.0 (from file://[TEMP_DIR]/) + DEBUG Requirement already installed: idna==3.6 + DEBUG Requirement already installed: sniffio==1.3.1 + Audited 4 packages in [TIME] + DEBUG Using Python 3.12.[X] interpreter at: [VENV]/bin/python3 + DEBUG Creating ephemeral environment + INFO Ignoring empty directory + DEBUG Syncing ephemeral requirements + DEBUG Using request timeout of [TIME] + DEBUG Solving with installed Python version: 3.12.[X] + DEBUG Tried 0 versions: + DEBUG Split specific environment resolution took [TIME] + Resolved 0 packages in [TIME] + Audited 0 packages in [TIME] + DEBUG Running `python main.py` + "###); + + Ok(()) +} From 6fb27cff60cace9bbdfe035706721683746e0443 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 23 Jul 2024 12:30:42 -0400 Subject: [PATCH 2/3] Remove overrides and constraints support --- crates/uv-cli/src/lib.rs | 38 +--- crates/uv/src/commands/project/run.rs | 57 +++--- crates/uv/src/lib.rs | 14 +- crates/uv/src/settings.rs | 24 +-- crates/uv/tests/run.rs | 246 +------------------------- 5 files changed, 44 insertions(+), 335 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index a869eeff881a..c99ff3b418d0 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1833,6 +1833,14 @@ pub struct RunArgs { #[arg(long)] pub with: Vec, + /// Run with all packages listed in the given `requirements.txt` files. + /// + /// Using `pyproject.toml`, `setup.py`, or `setup.cfg` files is not allowed. + /// + /// If `-` is provided, then requirements will be read from stdin. + #[arg(long, value_parser = parse_maybe_file_path)] + pub with_requirements: Vec>, + /// Assert that the `uv.lock` will remain unchanged. #[arg(long, conflicts_with = "frozen")] pub locked: bool, @@ -1867,36 +1875,6 @@ pub struct RunArgs { /// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path. #[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)] pub python: Option, - - /// Require all packages listed in the given `requirements.txt` files. - /// - /// Using `pyproject.toml`, `setup.py`, or `setup.cfg` files is not allowed. - /// - /// If `-` is provided, then requirements will be read from stdin. - #[arg(long, short, value_parser = parse_maybe_file_path)] - pub requirements: Vec>, - - /// Constrain versions using the given requirements files. - /// - /// Constraints files are `requirements.txt`-like files that only control the _version_ of a - /// requirement that's installed. However, including a package in a constraints file will _not_ - /// trigger the installation of that package. - /// - /// This is equivalent to pip's `--constraint` option. - #[arg(long, short, env = "UV_CONSTRAINTS", value_delimiter = ' ', value_parser = parse_maybe_file_path)] - pub constraints: Vec>, - - /// Override versions using the given requirements files. - /// - /// Overrides files are `requirements.txt`-like files that force a specific version of a - /// requirement to be installed, regardless of the requirements declared by any constituent - /// package, and regardless of whether this would be considered an invalid resolution. - /// - /// While constraints are _additive_, in that they're combined with the requirements of the - /// constituent packages, overrides are _absolute_, in that they completely replace the - /// requirements of the constituent packages. - #[arg(long, env = "UV_OVERRIDES", value_delimiter = ' ', value_parser = parse_maybe_file_path)] - pub r#overrides: Vec>, } #[derive(Args)] diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 26229ef04623..eb9f6d14a943 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -21,11 +21,11 @@ use uv_python::{ request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, PythonFetch, PythonInstallation, PythonPreference, PythonRequest, VersionRequest, }; -use uv_requirements::RequirementsSource; +use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_warnings::warn_user_once; use uv_workspace::{VirtualProject, Workspace, WorkspaceError}; -use crate::commands::pip::operations::{self, Modifications}; +use crate::commands::pip::operations::Modifications; use crate::commands::project::environment::CachedEnvironment; use crate::commands::project::ProjectError; use crate::commands::reporters::PythonDownloadReporter; @@ -38,8 +38,6 @@ use crate::settings::ResolverInstallerSettings; pub(crate) async fn run( command: ExternalCommand, requirements: Vec, - constraints: &[RequirementsSource], - overrides: &[RequirementsSource], locked: bool, frozen: bool, package: Option, @@ -63,23 +61,19 @@ pub(crate) async fn run( // These cases seem quite complex because (in theory) they should change the "current package". // Let's ban them entirely for now. - if requirements - .iter() - .any(|source| matches!(source, RequirementsSource::PyprojectToml(_))) - { - bail!("Adding requirements from a `pyproject.toml` is not supported in `uv run`"); - } - if requirements - .iter() - .any(|source| matches!(source, RequirementsSource::SetupCfg(_))) - { - bail!("Adding requirements from a `setup.cfg` is not supported in `uv run`"); - } - if requirements - .iter() - .any(|source| matches!(source, RequirementsSource::SetupCfg(_))) - { - bail!("Adding requirements from a `setup.py` is not supported in `uv run`"); + for source in &requirements { + match source { + RequirementsSource::PyprojectToml(_) => { + bail!("Adding requirements from a `pyproject.toml` is not supported in `uv run`"); + } + RequirementsSource::SetupPy(_) => { + bail!("Adding requirements from a `setup.py` is not supported in `uv run`"); + } + RequirementsSource::SetupCfg(_) => { + bail!("Adding requirements from a `setup.cfg` is not supported in `uv run`"); + } + _ => {} + } } // Parse the input command. @@ -286,21 +280,15 @@ pub(crate) async fn run( } // Read the requirements. - let spec = if requirements.is_empty() && constraints.is_empty() && overrides.is_empty() { + let spec = if requirements.is_empty() { None } else { let client_builder = BaseClientBuilder::new() .connectivity(connectivity) .native_tls(native_tls); - let spec = operations::read_requirements( - &requirements, - constraints, - overrides, - &extras, - &client_builder, - ) - .await?; + let spec = + RequirementsSpecification::from_simple_sources(&requirements, &client_builder).await?; Some(spec) }; @@ -309,7 +297,7 @@ pub(crate) async fn run( // any `--with` requirements, and we already have a base environment, then there's no need to // create an additional environment. let skip_ephemeral = base_interpreter.as_ref().is_some_and(|base_interpreter| { - // No additional requrements + // No additional requirements. let Some(spec) = spec.as_ref() else { return true; }; @@ -322,9 +310,10 @@ pub(crate) async fn run( return false; } - // `SitePackages::satisfies` handle overrides yet and constraints are only enforced on `--with` - // requirements instead of the project requirements so we perform a full resolution - // to ensure things are up to date + // `SitePackages::satisfies` doesn't handle overrides yet, and constraints are only enforced + // on `--with` requirements instead of the project requirements, so we perform a full + // resolution to ensure things are up-to-date. + // TODO(charlie): Support constraints and overrides in `uv run`. if !(spec.constraints.is_empty() && spec.overrides.is_empty()) { return false; } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e8b74ef28f63..ad70fe2e860b 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -853,27 +853,15 @@ async fn run_project( .into_iter() .map(RequirementsSource::from_package) .chain( - args.requirements + args.with_requirements .into_iter() .map(RequirementsSource::from_requirements_file), ) .collect::>(); - let constraints = args - .constraints - .into_iter() - .map(RequirementsSource::from_constraints_txt) - .collect::>(); - let overrides = args - .r#overrides - .into_iter() - .map(RequirementsSource::from_overrides_txt) - .collect::>(); commands::run( args.command, requirements, - &constraints, - &overrides, args.locked, args.frozen, args.package, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index dede0224dacb..19573c3d3eaa 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -184,11 +184,9 @@ pub(crate) struct RunSettings { pub(crate) dev: bool, pub(crate) command: ExternalCommand, pub(crate) with: Vec, + pub(crate) with_requirements: Vec, pub(crate) package: Option, pub(crate) python: Option, - pub(crate) requirements: Vec, - pub(crate) constraints: Vec, - pub(crate) r#overrides: Vec, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, } @@ -207,14 +205,12 @@ impl RunSettings { no_dev, command, with, + with_requirements, installer, build, refresh, package, python, - requirements, - constraints, - r#overrides, } = args; Self { @@ -227,21 +223,13 @@ impl RunSettings { dev: flag(dev, no_dev).unwrap_or(true), command, with, - package, - python, - refresh: Refresh::from(refresh), - requirements: requirements - .into_iter() - .filter_map(Maybe::into_option) - .collect(), - constraints: constraints - .into_iter() - .filter_map(Maybe::into_option) - .collect(), - r#overrides: r#overrides + with_requirements: with_requirements .into_iter() .filter_map(Maybe::into_option) .collect(), + package, + python, + refresh: Refresh::from(refresh), settings: ResolverInstallerSettings::combine( resolver_installer_options(installer, build), filesystem, diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index ba71629387ef..8fe85598f1cf 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -567,7 +567,7 @@ fn run_empty_requirements_txt() -> Result<()> { requirements_txt.touch()?; // The project environment is synced on the first invocation. - uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -585,7 +585,7 @@ fn run_empty_requirements_txt() -> Result<()> { "###); // Then reused in subsequent invocations - uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -624,7 +624,7 @@ fn run_requirements_txt() -> Result<()> { let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str("iniconfig")?; - uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -647,7 +647,7 @@ fn run_requirements_txt() -> Result<()> { // Requesting a satisfied requirement should use the base environment. requirements_txt.write_str("sniffio")?; - uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -661,7 +661,7 @@ fn run_requirements_txt() -> Result<()> { // Unless the user requests a different version. requirements_txt.write_str("sniffio<1.3.1")?; - uv_snapshot!(context.filters(), context.run().arg("-r").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with-requirements").arg(requirements_txt.as_os_str()).arg("main.py"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -680,7 +680,7 @@ fn run_requirements_txt() -> Result<()> { requirements_txt.write_str("sniffio")?; uv_snapshot!(context.filters(), context.run() - .arg("-r") + .arg("--with-requirements") .arg(requirements_txt.as_os_str()) .arg("--with") .arg("iniconfig") @@ -702,237 +702,3 @@ fn run_requirements_txt() -> Result<()> { Ok(()) } - -#[test] -fn run_constraints_txt() -> Result<()> { - let context = TestContext::new("3.12"); - - let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str(indoc! { r#" - [project] - name = "foo" - version = "1.0.0" - requires-python = ">=3.8" - dependencies = ["anyio", "sniffio==1.3.1"] - "# - })?; - - let test_script = context.temp_dir.child("main.py"); - test_script.write_str(indoc! { r" - import sniffio - " - })?; - - // Constraining an unrequested requirement should not include it. - let constraints_txt = context.temp_dir.child("constraints.txt"); - constraints_txt.write_str("iniconfig")?; - - uv_snapshot!(context.filters(), context.run().arg("--constraints").arg(constraints_txt.as_os_str()).arg("main.py"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - warning: `uv run` is experimental and may change without warning - Resolved 6 packages in [TIME] - Prepared 4 packages in [TIME] - Installed 4 packages in [TIME] - + anyio==4.3.0 - + foo==1.0.0 (from file://[TEMP_DIR]/) - + idna==3.6 - + sniffio==1.3.1 - "###); - - // Subsequent invocations should use the base environment if there's a compatible constraint. - constraints_txt.write_str("sniffio")?; - - uv_snapshot!(context.filters(), context.run().arg("--constraints").arg(constraints_txt.as_os_str()).arg("main.py"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - warning: `uv run` is experimental and may change without warning - Resolved 6 packages in [TIME] - Audited 4 packages in [TIME] - "###); - - // Unless the user requests a different version of a base requirement. - constraints_txt.write_str("sniffio<1.3.1")?; - - uv_snapshot!(context.filters(), context.run().arg("--constraints").arg(constraints_txt.as_os_str()).arg("main.py"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - warning: `uv run` is experimental and may change without warning - Resolved 6 packages in [TIME] - Audited 4 packages in [TIME] - "###); - - // Or includes an unsatisfied requirement via `--with`. - constraints_txt.write_str("iniconfig<2.0.0")?; - - uv_snapshot!(context.filters(), context.run() - .arg("--constraints") - .arg(constraints_txt.as_os_str()) - .arg("--with") - .arg("iniconfig") - .arg("main.py"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - warning: `uv run` is experimental and may change without warning - Resolved 6 packages in [TIME] - Audited 4 packages in [TIME] - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + iniconfig==1.1.1 - "###); - - Ok(()) -} - -#[test] -fn run_overrides_txt() -> Result<()> { - let context = TestContext::new("3.12"); - - let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str(indoc! { r#" - [project] - name = "foo" - version = "1.0.0" - requires-python = ">=3.8" - dependencies = ["anyio", "sniffio==1.3.1"] - "# - })?; - - let test_script = context.temp_dir.child("main.py"); - test_script.write_str(indoc! { r" - import sniffio - " - })?; - - // Overriding an unrequested requirement should not include it. - let overrides_txt = context.temp_dir.child("overrides.txt"); - overrides_txt.write_str("iniconfig==2.0.0")?; - - uv_snapshot!(context.filters(), context.run().arg("--overrides").arg(overrides_txt.as_os_str()).arg("main.py"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - warning: `uv run` is experimental and may change without warning - Resolved 6 packages in [TIME] - Prepared 4 packages in [TIME] - Installed 4 packages in [TIME] - + anyio==4.3.0 - + foo==1.0.0 (from file://[TEMP_DIR]/) - + idna==3.6 - + sniffio==1.3.1 - Resolved 0 packages in [TIME] - Audited 0 packages in [TIME] - "###); - - // Subsequent invocations should use the base environment if there is a compatible override. - overrides_txt.write_str("idna==3.6")?; - - uv_snapshot!(context.filters(), context.run().arg("--overrides").arg(overrides_txt.as_os_str()).arg("main.py"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - warning: `uv run` is experimental and may change without warning - Resolved 6 packages in [TIME] - Audited 4 packages in [TIME] - Resolved 0 packages in [TIME] - Audited 0 packages in [TIME] - "###); - - // Overriding a dependency should result in an ephemeral environment. - overrides_txt.write_str("idna==3.5")?; - - uv_snapshot!(context.filters(), context.run().arg("-v") - .arg("--overrides") - .arg(overrides_txt.as_os_str()) - .arg("main.py"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - DEBUG uv [VERSION] ([COMMIT] DATE) - warning: `uv run` is experimental and may change without warning - DEBUG Found project root: `[TEMP_DIR]/` - DEBUG No workspace root found, using project root - DEBUG Discovered project `foo` at: [TEMP_DIR]/ - DEBUG Interpreter meets the requested Python: `Python >=3.8` - DEBUG Using request timeout of [TIME] - DEBUG Resolving with existing `uv.lock` - DEBUG Acquired lock for `[CACHE_DIR]/built-wheels-v3/editable/aa56cdf257401d2d` - DEBUG Preparing metadata for: foo @ file://[TEMP_DIR]/ - DEBUG No static `PKG-INFO` available for: foo @ file://[TEMP_DIR]/ (MissingPkgInfo) - DEBUG Found static `pyproject.toml` for: foo @ file://[TEMP_DIR]/ - DEBUG No workspace root found, using project root - DEBUG Solving with installed Python version: 3.12.[X] - DEBUG Solving with target Python version: >=3.8 - DEBUG Adding direct dependency: foo* - DEBUG Searching for a compatible version of foo @ file://[TEMP_DIR]/ (*) - DEBUG Adding transitive dependency for foo==1.0.0: anyio* - DEBUG Adding transitive dependency for foo==1.0.0: sniffio==1.3.1 - DEBUG Searching for a compatible version of sniffio (==1.3.1) - DEBUG Selecting: sniffio==1.3.1 [preference] (sniffio-1.3.1-py3-none-any.whl) - DEBUG Searching for a compatible version of anyio (*) - DEBUG Selecting: anyio==4.3.0 [preference] (anyio-4.3.0-py3-none-any.whl) - DEBUG Adding transitive dependency for anyio==4.3.0: exceptiongroup{python_version < '3.11'}==1.2.0 - DEBUG Adding transitive dependency for anyio==4.3.0: idna==3.6 - DEBUG Adding transitive dependency for anyio==4.3.0: sniffio==1.3.1 - DEBUG Adding transitive dependency for anyio==4.3.0: typing-extensions{python_version < '3.11'}==4.10.0 - DEBUG Searching for a compatible version of exceptiongroup{python_version < '3.11'} (==1.2.0) - DEBUG Selecting: exceptiongroup==1.2.0 [preference] (exceptiongroup-1.2.0-py3-none-any.whl) - DEBUG Adding transitive dependency for exceptiongroup==1.2.0: exceptiongroup==1.2.0 - DEBUG Adding transitive dependency for exceptiongroup==1.2.0: exceptiongroup{python_version < '3.11'}==1.2.0 - DEBUG Searching for a compatible version of exceptiongroup{python_version < '3.11'} (==1.2.0) - DEBUG Selecting: exceptiongroup==1.2.0 [preference] (exceptiongroup-1.2.0-py3-none-any.whl) - DEBUG Searching for a compatible version of exceptiongroup (==1.2.0) - DEBUG Selecting: exceptiongroup==1.2.0 [preference] (exceptiongroup-1.2.0-py3-none-any.whl) - DEBUG Searching for a compatible version of idna (==3.6) - DEBUG Selecting: idna==3.6 [preference] (idna-3.6-py3-none-any.whl) - DEBUG Searching for a compatible version of typing-extensions{python_version < '3.11'} (==4.10.0) - DEBUG Selecting: typing-extensions==4.10.0 [preference] (typing_extensions-4.10.0-py3-none-any.whl) - DEBUG Adding transitive dependency for typing-extensions==4.10.0: typing-extensions==4.10.0 - DEBUG Adding transitive dependency for typing-extensions==4.10.0: typing-extensions{python_version < '3.11'}==4.10.0 - DEBUG Searching for a compatible version of typing-extensions{python_version < '3.11'} (==4.10.0) - DEBUG Selecting: typing-extensions==4.10.0 [preference] (typing_extensions-4.10.0-py3-none-any.whl) - DEBUG Searching for a compatible version of typing-extensions (==4.10.0) - DEBUG Selecting: typing-extensions==4.10.0 [preference] (typing_extensions-4.10.0-py3-none-any.whl) - DEBUG Tried 6 versions: anyio 1, exceptiongroup 1, foo 1, idna 1, sniffio 1, typing-extensions 1 - DEBUG Split universal resolution took [TIME] - Resolved 6 packages in [TIME] - DEBUG Using request timeout of [TIME] - DEBUG Requirement already installed: anyio==4.3.0 - DEBUG Requirement already installed: foo==1.0.0 (from file://[TEMP_DIR]/) - DEBUG Requirement already installed: idna==3.6 - DEBUG Requirement already installed: sniffio==1.3.1 - Audited 4 packages in [TIME] - DEBUG Using Python 3.12.[X] interpreter at: [VENV]/bin/python3 - DEBUG Creating ephemeral environment - INFO Ignoring empty directory - DEBUG Syncing ephemeral requirements - DEBUG Using request timeout of [TIME] - DEBUG Solving with installed Python version: 3.12.[X] - DEBUG Tried 0 versions: - DEBUG Split specific environment resolution took [TIME] - Resolved 0 packages in [TIME] - Audited 0 packages in [TIME] - DEBUG Running `python main.py` - "###); - - Ok(()) -} From 81ce36ca2b3f7211a271845b287bea740ee8b38d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 23 Jul 2024 12:43:03 -0400 Subject: [PATCH 3/3] Reject stdin --- crates/uv-cli/src/lib.rs | 2 -- crates/uv/src/commands/project/run.rs | 15 ++++++--------- crates/uv/tests/run.rs | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index c99ff3b418d0..6282017e4c17 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1836,8 +1836,6 @@ pub struct RunArgs { /// Run with all packages listed in the given `requirements.txt` files. /// /// Using `pyproject.toml`, `setup.py`, or `setup.cfg` files is not allowed. - /// - /// If `-` is provided, then requirements will be read from stdin. #[arg(long, value_parser = parse_maybe_file_path)] pub with_requirements: Vec>, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index eb9f6d14a943..364bea2eb2b0 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::ffi::OsString; use std::fmt::Write; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::{bail, Context, Result}; use itertools::Itertools; @@ -72,6 +72,11 @@ pub(crate) async fn run( RequirementsSource::SetupCfg(_) => { bail!("Adding requirements from a `setup.cfg` is not supported in `uv run`"); } + RequirementsSource::RequirementsTxt(path) => { + if path == Path::new("-") { + bail!("Reading requirements from stdin is not supported in `uv run`"); + } + } _ => {} } } @@ -310,14 +315,6 @@ pub(crate) async fn run( return false; } - // `SitePackages::satisfies` doesn't handle overrides yet, and constraints are only enforced - // on `--with` requirements instead of the project requirements, so we perform a full - // resolution to ensure things are up-to-date. - // TODO(charlie): Support constraints and overrides in `uv run`. - if !(spec.constraints.is_empty() && spec.overrides.is_empty()) { - return false; - } - match site_packages.satisfies(&spec.requirements, &spec.constraints) { // If the requirements are already satisfied, we're done. Ok(SatisfiesResult::Fresh { diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index 8fe85598f1cf..815d9866101b 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -700,5 +700,21 @@ fn run_requirements_txt() -> Result<()> { + sniffio==1.3.1 "###); + // But reject `-` as a requirements file. + uv_snapshot!(context.filters(), context.run() + .arg("--with-requirements") + .arg("-") + .arg("--with") + .arg("iniconfig") + .arg("main.py"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv run` is experimental and may change without warning + error: Reading requirements from stdin is not supported in `uv run` + "###); + Ok(()) }