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_.
This commit is contained in:
Charlie Marsh 2025-05-04 12:56:33 -04:00 committed by GitHub
parent e2d105d045
commit c12ce84fbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 108 additions and 8 deletions

View file

@ -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(),

View file

@ -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: <https://github.com/astral-sh/uv/issues/13173>
#[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(())
}