Initialize PEP 723 script in uv lock --script (#11717)

## Summary

Like `uv add --script`, `uv lock --script` will now initialize a PEP 723
script tag if it doesn't already exist.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
Charlie Marsh 2025-02-24 07:40:45 -10:00 committed by GitHub
parent c18c8f478e
commit f3ebd04a9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 92 additions and 26 deletions

View file

@ -10,7 +10,7 @@ use rustc_hash::{FxBuildHasher, FxHashMap};
use tracing::debug;
use uv_cache::Cache;
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DevGroupsSpecification, DryRun, ExtrasSpecification, PreviewMode,
Reinstall, TrustedHost, Upgrade,
@ -41,10 +41,11 @@ use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceMember};
use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState,
init_script_python_requirement, ProjectError, ProjectInterpreter, ScriptInterpreter,
UniversalState,
};
use crate::commands::reporters::ResolverReporter;
use crate::commands::{diagnostics, pip, ExitStatus};
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{diagnostics, pip, ExitStatus, ScriptPath};
use crate::printer::Printer;
use crate::settings::{ResolverSettings, ResolverSettingsRef};
@ -83,7 +84,7 @@ pub(crate) async fn lock(
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: ResolverSettings,
script: Option<Pep723Script>,
script: Option<ScriptPath>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
connectivity: Connectivity,
@ -95,6 +96,33 @@ pub(crate) async fn lock(
printer: Printer,
preview: PreviewMode,
) -> anyhow::Result<ExitStatus> {
// If necessary, initialize the PEP 723 script.
let script = match script {
Some(ScriptPath::Path(path)) => {
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls)
.allow_insecure_host(allow_insecure_host.to_vec());
let reporter = PythonDownloadReporter::single(printer);
let requires_python = init_script_python_requirement(
python.as_deref(),
&install_mirrors,
project_dir,
false,
python_preference,
python_downloads,
no_config,
&client_builder,
cache,
&reporter,
)
.await?;
Some(Pep723Script::init(&path, requires_python.specifiers()).await?)
}
Some(ScriptPath::Script(script)) => Some(script),
None => None,
};
// Find the project requirements.
let workspace;
let target = if let Some(script) = script.as_ref() {

View file

@ -193,22 +193,25 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
) => Pep723Metadata::parse(contents)?.map(Pep723Item::Stdin),
_ => None,
},
// For `uv add --script` and `uv lock --script`, we'll create a PEP 723 tag if it
// doesn't already exist.
ProjectCommand::Add(uv_cli::AddArgs {
script: Some(script),
..
})
| ProjectCommand::Lock(uv_cli::LockArgs {
script: Some(script),
..
}) => match Pep723Script::read(&script).await {
Ok(Some(script)) => Some(Pep723Item::Script(script)),
Ok(None) => None,
Err(err) => return Err(err.into()),
},
// For the remaining commands, the PEP 723 tag must exist already.
ProjectCommand::Remove(uv_cli::RemoveArgs {
script: Some(script),
..
})
| ProjectCommand::Lock(uv_cli::LockArgs {
script: Some(script),
..
})
| ProjectCommand::Sync(uv_cli::SyncArgs {
script: Some(script),
..
@ -223,8 +226,6 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
}) => match Pep723Script::read(&script).await {
Ok(Some(script)) => Some(Pep723Item::Script(script)),
Ok(None) => {
// TODO(charlie): `uv lock --script` should initialize the tag, if it doesn't
// exist.
bail!(
"`{}` does not contain a PEP 723 metadata tag; run `{}` to initialize the script",
script.user_display().cyan(),
@ -1582,12 +1583,18 @@ async fn run_project(
.combine(Refresh::from(args.settings.upgrade.clone())),
);
// Unwrap the script.
let script = script.map(|script| match script {
Pep723Item::Script(script) => script,
Pep723Item::Stdin(..) => unreachable!("`uv lock` does not support stdin"),
Pep723Item::Remote(..) => unreachable!("`uv lock` does not support remote files"),
});
// If the script already exists, use it; otherwise, propagate the file path and we'll
// initialize it later on.
let script = script
.map(|script| match script {
Pep723Item::Script(script) => script,
Pep723Item::Stdin(..) => unreachable!("`uv add` does not support stdin"),
Pep723Item::Remote(..) => {
unreachable!("`uv add` does not support remote files")
}
})
.map(ScriptPath::Script)
.or(args.script.map(ScriptPath::Path));
Box::pin(commands::lock(
project_dir,

View file

@ -152,6 +152,19 @@ impl TestContext {
self
}
/// Add extra standard filtering for Windows-compatible missing file errors.
pub fn with_filtered_missing_file_error(mut self) -> Self {
self.filters.push((
regex::escape("The system cannot find the file specified. (os error 2)"),
"[OS ERROR 2]".to_string(),
));
self.filters.push((
regex::escape("No such file or directory (os error 2)"),
"[OS ERROR 2]".to_string(),
));
self
}
/// Add extra standard filtering for executable suffixes on the current platform e.g.
/// drops `.exe` on Windows.
#[must_use]

View file

@ -23619,34 +23619,52 @@ fn lock_script_path() -> Result<()> {
Ok(())
}
/// `uv lock --script` should add a PEP 723 tag, if it doesn't exist already.
#[test]
fn lock_script_error() -> Result<()> {
let context = TestContext::new("3.12");
fn lock_script_initialize() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_missing_file_error();
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###"
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to read `script.py` (not found); run `uv init --script script.py` to create a PEP 723 script
"###);
error: failed to read from file `script.py`: [OS ERROR 2]
");
let script = context.temp_dir.child("script.py");
script.write_str(indoc! { r"
import anyio
print('Hello, world!')
"
})?;
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###"
success: false
exit_code: 2
success: true
exit_code: 0
----- stdout -----
----- stderr -----
error: `script.py` does not contain a PEP 723 metadata tag; run `uv init --script script.py` to initialize the script
Resolved in [TIME]
"###);
let lock = context.read("script.py.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
revision = 1
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
"###
);
});
Ok(())
}