From c12ce84fbd6f19f7fbae0681f06394942faed24b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 4 May 2025 12:56:33 -0400 Subject: [PATCH] Respect locked script preferences in `uv run --with` (#13283) ## Summary Part of https://github.com/astral-sh/uv/issues/13173, but doesn't close the issue. This just respects preferences if your script uses a lockfile, since we already support that for locked _projects_. --- crates/uv/src/commands/project/run.rs | 21 +++--- crates/uv/tests/it/run.rs | 95 +++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index b37d0da2f..2b6f89416 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -31,7 +31,7 @@ use uv_python::{ VersionFileDiscoveryOptions, }; use uv_requirements::{RequirementsSource, RequirementsSpecification}; -use uv_resolver::Lock; +use uv_resolver::{Installable, Lock}; use uv_scripts::Pep723Item; use uv_settings::PythonInstallMirrors; use uv_shell::runnable::WindowsRunnable; @@ -187,6 +187,9 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl // Initialize any output reporters. let download_reporter = PythonDownloadReporter::single(printer); + // The lockfile used for the base environment. + let mut base_lock: Option<(Lock, PathBuf)> = None; + // Determine whether the command to execute is a PEP 723 script. let temp_dir; let script_interpreter = if let Some(script) = script { @@ -318,6 +321,10 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl Err(err) => return Err(err.into()), } + // Respect any locked preferences when resolving `--with` dependencies downstream. + let install_path = target.install_path().to_path_buf(); + base_lock = Some((lock, install_path)); + Some(environment.into_interpreter()) } else { // If no lockfile is found, warn against `--locked` and `--frozen`. @@ -443,9 +450,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl None }; - // The lockfile used for the base environment. - let mut lock: Option<(Lock, PathBuf)> = None; - // Discover and sync the base environment. let workspace_cache = WorkspaceCache::default(); let temp_dir; @@ -659,7 +663,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl // If we're not syncing, we should still attempt to respect the locked preferences // in any `--with` requirements. if !isolated && !requirements.is_empty() { - lock = LockTarget::from(project.workspace()) + base_lock = LockTarget::from(project.workspace()) .read() .await .ok() @@ -802,7 +806,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl Err(err) => return Err(err.into()), } - lock = Some(( + base_lock = Some(( result.into_lock(), project.workspace().install_path().to_owned(), )); @@ -901,13 +905,14 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl debug!("Syncing ephemeral requirements"); // Read the build constraints from the lock file. - let build_constraints = lock + let build_constraints = base_lock .as_ref() .map(|(lock, path)| lock.build_constraints(path)); let result = CachedEnvironment::from_spec( EnvironmentSpecification::from(spec).with_lock( - lock.as_ref() + base_lock + .as_ref() .map(|(lock, install_path)| (lock, install_path.as_ref())), ), build_constraints.unwrap_or_default(), diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 3b56ff161..87957f480 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -4983,3 +4983,98 @@ fn run_windows_legacy_scripts() -> Result<()> { Ok(()) } + +/// If a `--with` requirement overlaps with a locked script requirement, respect the lockfile as a +/// preference. +/// +/// See: +#[test] +fn run_pep723_script_with_constraints_lock() -> Result<()> { + let context = TestContext::new("3.12"); + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "iniconfig<2", + # ] + # /// + + import iniconfig + + print("Hello, world!") + "# + })?; + + // Explicitly lock the script. + uv_snapshot!(context.filters(), context.lock().arg("--script").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + "###); + + let lock = context.read("main.py.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.11" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [{ name = "iniconfig", specifier = "<2" }] + + [[package]] + name = "iniconfig" + version = "1.1.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/23/a2/97899f6bd0e873fed3a7e67ae8d3a08b21799430fb4da15cfedf10d6e2c2/iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32", size = 8104, upload-time = "2020-10-14T10:20:18.572Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", size = 4990, upload-time = "2020-10-16T17:37:23.05Z" }, + ] + "# + ); + }); + + 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.10" + dependencies = [ + "iniconfig", + ] + "# + })?; + + uv_snapshot!(context.filters(), context.run().arg("--with").arg(".").arg("main.py"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Hello, world! + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==1.1.1 + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 2 packages in [TIME] + + foo==1.0.0 (from file://[TEMP_DIR]/) + + iniconfig==1.1.1 + "); + + Ok(()) +}