mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-26 12:09:12 +00:00
Use separate types for stdin vs. file-based PEP 723 scripts (#8113)
This commit is contained in:
parent
e3775635d4
commit
d864e1dbe5
5 changed files with 99 additions and 62 deletions
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue