Use separate types for stdin vs. file-based PEP 723 scripts (#8113)

This commit is contained in:
Charlie Marsh 2024-10-11 00:49:14 +02:00 committed by GitHub
parent e3775635d4
commit d864e1dbe5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 99 additions and 62 deletions

View file

@ -16,11 +16,38 @@ use uv_workspace::pyproject::Sources;
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script")); static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
/// A PEP 723 item, either read from a script on disk or provided via `stdin`.
#[derive(Debug)]
pub enum Pep723Item {
/// A PEP 723 script read from disk.
Script(Pep723Script),
/// A PEP 723 script provided via `stdin`.
Stdin(Pep723Stdin),
}
impl Pep723Item {
/// Return the [`Pep723Metadata`] associated with the item.
pub fn metadata(&self) -> &Pep723Metadata {
match self {
Self::Script(script) => &script.metadata,
Self::Stdin(stdin) => &stdin.metadata,
}
}
/// Consume the item and return the associated [`Pep723Metadata`].
pub fn into_metadata(self) -> Pep723Metadata {
match self {
Self::Script(script) => script.metadata,
Self::Stdin(stdin) => stdin.metadata,
}
}
}
/// A PEP 723 script, including its [`Pep723Metadata`]. /// A PEP 723 script, including its [`Pep723Metadata`].
#[derive(Debug)] #[derive(Debug)]
pub struct Pep723Script { pub struct Pep723Script {
/// The path to the Python script. /// The path to the Python script.
pub source: Source, pub path: PathBuf,
/// The parsed [`Pep723Metadata`] table from the script. /// The parsed [`Pep723Metadata`] table from the script.
pub metadata: Pep723Metadata, pub metadata: Pep723Metadata,
/// The content of the script before the metadata table. /// The content of the script before the metadata table.
@ -34,28 +61,18 @@ impl Pep723Script {
/// ///
/// See: <https://peps.python.org/pep-0723/> /// See: <https://peps.python.org/pep-0723/>
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> { pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
match fs_err::tokio::read(&file).await { let contents = match fs_err::tokio::read(&file).await {
Ok(contents) => { Ok(contents) => contents,
Self::parse_contents(&contents, Source::File(file.as_ref().to_path_buf())) Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
} Err(err) => return Err(err.into()),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), };
Err(err) => Err(err.into()),
}
}
/// Read the PEP 723 `script` metadata from stdin.
pub fn parse_stdin(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
Self::parse_contents(contents, Source::Stdin)
}
/// Parse the contents of a Python script and extract the `script` metadata block.
fn parse_contents(contents: &[u8], source: Source) -> Result<Option<Self>, Pep723Error> {
// Extract the `script` tag. // Extract the `script` tag.
let ScriptTag { let ScriptTag {
prelude, prelude,
metadata, metadata,
postlude, postlude,
} = match ScriptTag::parse(contents) { } = match ScriptTag::parse(&contents) {
Ok(Some(tag)) => tag, Ok(Some(tag)) => tag,
Ok(None) => return Ok(None), Ok(None) => return Ok(None),
Err(err) => return Err(err), Err(err) => return Err(err),
@ -65,7 +82,7 @@ impl Pep723Script {
let metadata = Pep723Metadata::from_str(&metadata)?; let metadata = Pep723Metadata::from_str(&metadata)?;
Ok(Some(Self { Ok(Some(Self {
source, path: file.as_ref().to_path_buf(),
metadata, metadata,
prelude, prelude,
postlude, postlude,
@ -94,7 +111,7 @@ impl Pep723Script {
let (shebang, postlude) = extract_shebang(&contents)?; let (shebang, postlude) = extract_shebang(&contents)?;
Ok(Self { Ok(Self {
source: Source::File(file.as_ref().to_path_buf()), path: file.as_ref().to_path_buf(),
prelude: if shebang.is_empty() { prelude: if shebang.is_empty() {
String::new() String::new()
} else { } else {
@ -159,21 +176,33 @@ impl Pep723Script {
self.postlude self.postlude
); );
if let Source::File(path) = &self.source { fs_err::tokio::write(&self.path, content).await?;
fs_err::tokio::write(&path, content).await?;
}
Ok(()) Ok(())
} }
} }
/// The source of a PEP 723 script. /// A PEP 723 script, provided via `stdin`.
#[derive(Debug)] #[derive(Debug)]
pub enum Source { pub struct Pep723Stdin {
/// The PEP 723 script is sourced from a file. metadata: Pep723Metadata,
File(PathBuf), }
/// The PEP 723 script is sourced from stdin.
Stdin, impl Pep723Stdin {
/// Parse the PEP 723 `script` metadata from `stdin`.
pub fn parse(contents: &[u8]) -> Result<Option<Self>, Pep723Error> {
// Extract the `script` tag.
let ScriptTag { metadata, .. } = match ScriptTag::parse(contents) {
Ok(Some(tag)) => tag,
Ok(None) => return Ok(None),
Err(err) => return Err(err),
};
// Parse the metadata.
let metadata = Pep723Metadata::from_str(&metadata)?;
Ok(Some(Self { metadata }))
}
} }
/// PEP 723 metadata as parsed from a `script` comment block. /// PEP 723 metadata as parsed from a `script` comment block.

View file

@ -379,10 +379,7 @@ pub(crate) async fn add(
(uv_pep508::Requirement::from(requirement), None) (uv_pep508::Requirement::from(requirement), None)
} }
Target::Script(ref script, _) => { Target::Script(ref script, _) => {
let uv_scripts::Source::File(path) = &script.source else { let script_path = std::path::absolute(&script.path)?;
unreachable!("script source is not a file");
};
let script_path = std::path::absolute(path)?;
let script_dir = script_path.parent().expect("script path has no parent"); let script_dir = script_path.parent().expect("script path has no parent");
resolve_requirement( resolve_requirement(
requirement, requirement,
@ -511,9 +508,11 @@ pub(crate) async fn add(
Target::Project(project, venv) => (project, venv), Target::Project(project, venv) => (project, venv),
// If `--script`, exit early. There's no reason to lock and sync. // If `--script`, exit early. There's no reason to lock and sync.
Target::Script(script, _) => { Target::Script(script, _) => {
if let uv_scripts::Source::File(path) = &script.source { writeln!(
writeln!(printer.stderr(), "Updated `{}`", path.user_display().cyan())?; printer.stderr(),
} "Updated `{}`",
script.path.user_display().cyan()
)?;
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
}; };

View file

@ -144,9 +144,11 @@ pub(crate) async fn remove(
Target::Project(project) => project, Target::Project(project) => project,
// If `--script`, exit early. There's no reason to lock and sync. // If `--script`, exit early. There's no reason to lock and sync.
Target::Script(script) => { Target::Script(script) => {
if let uv_scripts::Source::File(path) = &script.source { writeln!(
writeln!(printer.stderr(), "Updated `{}`", path.user_display().cyan())?; printer.stderr(),
} "Updated `{}`",
script.path.user_display().cyan()
)?;
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
}; };

View file

@ -29,7 +29,7 @@ use uv_python::{
}; };
use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::Lock; use uv_resolver::Lock;
use uv_scripts::Pep723Script; use uv_scripts::Pep723Item;
use uv_warnings::warn_user; use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace, WorkspaceError}; use uv_workspace::{DiscoveryOptions, InstallTarget, VirtualProject, Workspace, WorkspaceError};
@ -52,7 +52,7 @@ use crate::settings::ResolverInstallerSettings;
#[allow(clippy::fn_params_excessive_bools)] #[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn run( pub(crate) async fn run(
project_dir: &Path, project_dir: &Path,
script: Option<Pep723Script>, script: Option<Pep723Item>,
command: Option<RunCommand>, command: Option<RunCommand>,
requirements: Vec<RequirementsSource>, requirements: Vec<RequirementsSource>,
show_resolution: bool, show_resolution: bool,
@ -107,11 +107,11 @@ pub(crate) async fn run(
// Determine whether the command to execute is a PEP 723 script. // Determine whether the command to execute is a PEP 723 script.
let temp_dir; let temp_dir;
let script_interpreter = if let Some(script) = script { let script_interpreter = if let Some(script) = script {
if let uv_scripts::Source::File(path) = &script.source { if let Pep723Item::Script(script) = &script {
writeln!( writeln!(
printer.stderr(), printer.stderr(),
"Reading inline script metadata from: {}", "Reading inline script metadata from: {}",
path.user_display().cyan() script.path.user_display().cyan()
)?; )?;
} else { } else {
writeln!( writeln!(
@ -134,7 +134,7 @@ pub(crate) async fn run(
} else { } else {
// (3) `Requires-Python` in the script // (3) `Requires-Python` in the script
let request = script let request = script
.metadata .metadata()
.requires_python .requires_python
.as_ref() .as_ref()
.map(|requires_python| { .map(|requires_python| {
@ -163,7 +163,7 @@ pub(crate) async fn run(
.await? .await?
.into_interpreter(); .into_interpreter();
if let Some(requires_python) = script.metadata.requires_python.as_ref() { if let Some(requires_python) = script.metadata().requires_python.as_ref() {
if !requires_python.contains(interpreter.python_version()) { if !requires_python.contains(interpreter.python_version()) {
let err = match source { let err = match source {
PythonRequestSource::UserRequest => { PythonRequestSource::UserRequest => {
@ -190,13 +190,22 @@ pub(crate) async fn run(
} }
} }
// Determine the working directory for the script.
let script_dir = match &script {
Pep723Item::Script(script) => std::path::absolute(&script.path)?
.parent()
.expect("script path has no parent")
.to_owned(),
Pep723Item::Stdin(..) => std::env::current_dir()?,
};
let script = script.into_metadata();
// Install the script requirements, if necessary. Otherwise, use an isolated environment. // Install the script requirements, if necessary. Otherwise, use an isolated environment.
if let Some(dependencies) = script.metadata.dependencies { if let Some(dependencies) = script.dependencies {
// // Collect any `tool.uv.sources` from the script. // // Collect any `tool.uv.sources` from the script.
let empty = BTreeMap::default(); let empty = BTreeMap::default();
let script_sources = match settings.sources { let script_sources = match settings.sources {
SourceStrategy::Enabled => script SourceStrategy::Enabled => script
.metadata
.tool .tool
.as_ref() .as_ref()
.and_then(|tool| tool.uv.as_ref()) .and_then(|tool| tool.uv.as_ref())
@ -204,16 +213,6 @@ pub(crate) async fn run(
.unwrap_or(&empty), .unwrap_or(&empty),
SourceStrategy::Disabled => &empty, SourceStrategy::Disabled => &empty,
}; };
let script_dir = match &script.source {
uv_scripts::Source::File(path) => {
let script_path = std::path::absolute(path)?;
script_path
.parent()
.expect("script path has no parent")
.to_owned()
}
uv_scripts::Source::Stdin => std::env::current_dir()?,
};
let requirements = dependencies let requirements = dependencies
.into_iter() .into_iter()

View file

@ -28,7 +28,7 @@ use uv_cli::{SelfCommand, SelfNamespace, SelfUpdateArgs};
use uv_client::BaseClientBuilder; use uv_client::BaseClientBuilder;
use uv_fs::CWD; use uv_fs::CWD;
use uv_requirements::RequirementsSource; use uv_requirements::RequirementsSource;
use uv_scripts::Pep723Script; use uv_scripts::{Pep723Item, Pep723Script, Pep723Stdin};
use uv_settings::{Combine, FilesystemOptions, Options}; use uv_settings::{Combine, FilesystemOptions, Options};
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, Workspace}; use uv_workspace::{DiscoveryOptions, Workspace};
@ -217,8 +217,10 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
match run_command.as_ref() { match run_command.as_ref() {
Some( Some(
RunCommand::PythonScript(script, _) | RunCommand::PythonGuiScript(script, _), RunCommand::PythonScript(script, _) | RunCommand::PythonGuiScript(script, _),
) => Pep723Script::read(&script).await?, ) => Pep723Script::read(&script).await?.map(Pep723Item::Script),
Some(RunCommand::PythonStdin(contents)) => Pep723Script::parse_stdin(contents)?, Some(RunCommand::PythonStdin(contents)) => {
Pep723Stdin::parse(contents)?.map(Pep723Item::Stdin)
}
_ => None, _ => None,
} }
} else if let ProjectCommand::Remove(uv_cli::RemoveArgs { } else if let ProjectCommand::Remove(uv_cli::RemoveArgs {
@ -226,7 +228,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.. ..
}) = &**command }) = &**command
{ {
Pep723Script::read(&script).await? Pep723Script::read(&script).await?.map(Pep723Item::Script)
} else { } else {
None None
} }
@ -237,7 +239,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// If the target is a PEP 723 script, merge the metadata into the filesystem metadata. // If the target is a PEP 723 script, merge the metadata into the filesystem metadata.
let filesystem = script let filesystem = script
.as_ref() .as_ref()
.map(|script| &script.metadata) .map(Pep723Item::metadata)
.and_then(|metadata| metadata.tool.as_ref()) .and_then(|metadata| metadata.tool.as_ref())
.and_then(|tool| tool.uv.as_ref()) .and_then(|tool| tool.uv.as_ref())
.map(|uv| Options::simple(uv.globals.clone(), uv.top_level.clone())) .map(|uv| Options::simple(uv.globals.clone(), uv.top_level.clone()))
@ -1233,7 +1235,7 @@ async fn run_project(
project_command: Box<ProjectCommand>, project_command: Box<ProjectCommand>,
project_dir: &Path, project_dir: &Path,
command: Option<RunCommand>, command: Option<RunCommand>,
script: Option<Pep723Script>, script: Option<Pep723Item>,
globals: GlobalSettings, globals: GlobalSettings,
// TODO(zanieb): Determine a better story for passing `no_config` in here // TODO(zanieb): Determine a better story for passing `no_config` in here
no_config: bool, no_config: bool,
@ -1465,6 +1467,12 @@ async fn run_project(
.combine(Refresh::from(args.settings.upgrade.clone())), .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 remove` does not support stdin"),
});
commands::remove( commands::remove(
project_dir, project_dir,
args.locked, args.locked,