diff --git a/crates/uv-requirements/src/sources.rs b/crates/uv-requirements/src/sources.rs index 4cd72222d..8bba3213c 100644 --- a/crates/uv-requirements/src/sources.rs +++ b/crates/uv-requirements/src/sources.rs @@ -6,7 +6,6 @@ use console::Term; use uv_fs::{CWD, Simplified}; use uv_requirements_txt::RequirementsTxtRequirement; -use uv_scripts::Pep723Script; #[derive(Debug, Clone)] pub enum RequirementsSource { @@ -15,7 +14,7 @@ pub enum RequirementsSource { /// An editable path was provided on the command line (e.g., `pip install -e ../flask`). Editable(RequirementsTxtRequirement), /// Dependencies were provided via a PEP 723 script. - Pep723Script(Box), + Pep723Script(PathBuf), /// Dependencies were provided via a `pylock.toml` file. PylockToml(PathBuf), /// Dependencies were provided via a `requirements.txt` file (e.g., `pip install -r requirements.txt`). @@ -51,7 +50,8 @@ impl RequirementsSource { .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyw")) { - Ok(Self::Pep723Script(Pep723ScriptSource::new(path))) + // TODO(blueraft): Support scripts without an extension. + Ok(Self::Pep723Script(path)) } else if path .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) @@ -60,21 +60,6 @@ impl RequirementsSource { "`{}` is not a valid PEP 751 filename: expected TOML file to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`)", path.user_display(), )) - } else if path - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("txt") || ext.eq_ignore_ascii_case("in")) - { - Ok(Self::RequirementsTxt(path)) - } else if path.extension().is_none() { - // If we don't have an extension, attempt to detect a PEP 723 script, and - // fall back to `requirements.txt` format if not. - match Pep723Script::read_sync(&path) { - Ok(Some(script)) => Ok(Self::Pep723Script(Pep723ScriptSource::with_script( - path, script, - ))), - Ok(None) => Ok(Self::RequirementsTxt(path)), - Err(err) => Err(err.into()), - } } else { Ok(Self::RequirementsTxt(path)) } @@ -306,43 +291,14 @@ impl RequirementsSource { } } -#[derive(Debug, Clone)] -pub struct Pep723ScriptSource { - path: PathBuf, - script: Option, -} - -impl Pep723ScriptSource { - fn new(path: PathBuf) -> Box { - Box::new(Self { path, script: None }) - } - - fn with_script(path: PathBuf, script: Pep723Script) -> Box { - Box::new(Self { - path, - script: Some(script), - }) - } - - pub fn path(&self) -> &Path { - &self.path - } - - pub fn script(&self) -> Option<&Pep723Script> { - self.script.as_ref() - } -} - impl std::fmt::Display for RequirementsSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Package(package) => write!(f, "{package:?}"), Self::Editable(path) => write!(f, "-e {path:?}"), - Self::Pep723Script(source) => { - write!(f, "{}", source.path().simplified_display()) - } Self::PylockToml(path) | Self::RequirementsTxt(path) + | Self::Pep723Script(path) | Self::PyprojectToml(path) | Self::SetupPy(path) | Self::SetupCfg(path) diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index 5d862b7ab..bb8d8adbf 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -46,7 +46,7 @@ use uv_fs::{CWD, Simplified}; use uv_normalize::{ExtraName, PackageName, PipGroupName}; use uv_pypi_types::PyProjectToml; use uv_requirements_txt::{RequirementsTxt, RequirementsTxtRequirement}; -use uv_scripts::{Pep723Item, Pep723Script}; +use uv_scripts::{Pep723Error, Pep723Item, Pep723Script}; use uv_warnings::warn_user; use crate::{RequirementsSource, SourceTree}; @@ -184,20 +184,22 @@ impl RequirementsSpecification { ..Self::default() } } - RequirementsSource::Pep723Script(source) => { - let script = if let Some(script) = source.script() { - Pep723Item::Script(script.clone()) - } else { - match Pep723Script::read(source.path()).await { - Ok(Some(script)) => Pep723Item::Script(script), - Ok(None) => { - return Err(anyhow::anyhow!( - "`{}` does not contain inline script metadata", - source.path().user_display(), - )); - } - Err(err) => return Err(err.into()), + RequirementsSource::Pep723Script(path) => { + let script = match Pep723Script::read(&path).await { + Ok(Some(script)) => Pep723Item::Script(script), + Ok(None) => { + return Err(anyhow::anyhow!( + "`{}` does not contain inline script metadata", + path.user_display(), + )); } + Err(Pep723Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { + return Err(anyhow::anyhow!( + "Failed to read `{}` (not found)", + path.user_display(), + )); + } + Err(err) => return Err(err.into()), }; let metadata = script.metadata(); diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 1e5c7e3d4..17e4b0875 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -175,16 +175,28 @@ impl Pep723Script { /// /// See: pub async fn read(file: impl AsRef) -> Result, Pep723Error> { - let file = file.as_ref(); - let contents = fs_err::tokio::read(file).await?; - Self::from_contents(file, &contents) - } + let contents = fs_err::tokio::read(&file).await?; - /// Read the PEP 723 `script` metadata from a Python file using blocking I/O. - pub fn read_sync(file: impl AsRef) -> Result, Pep723Error> { - let file = file.as_ref(); - let contents = fs_err::read(file)?; - Self::from_contents(file, &contents) + // Extract the `script` tag. + let ScriptTag { + prelude, + metadata, + postlude, + } = 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 { + path: std::path::absolute(file)?, + metadata, + prelude, + postlude, + })) } /// Reads a Python script and generates a default PEP 723 metadata table. @@ -337,29 +349,6 @@ impl Pep723Script { .and_then(|uv| uv.sources.as_ref()) .unwrap_or(&EMPTY) } - - fn from_contents(path: &Path, contents: &[u8]) -> Result, Pep723Error> { - let script_tag = match ScriptTag::parse(contents) { - Ok(Some(tag)) => tag, - Ok(None) => return Ok(None), - Err(err) => return Err(err), - }; - - let ScriptTag { - prelude, - metadata, - postlude, - } = script_tag; - - let metadata = Pep723Metadata::from_str(&metadata)?; - - Ok(Some(Self { - path: std::path::absolute(path)?, - metadata, - prelude, - postlude, - })) - } } /// PEP 723 metadata as parsed from a `script` comment block. diff --git a/crates/uv/tests/it/tool_run.rs b/crates/uv/tests/it/tool_run.rs index ab0118dc4..07396b596 100644 --- a/crates/uv/tests/it/tool_run.rs +++ b/crates/uv/tests/it/tool_run.rs @@ -2646,7 +2646,8 @@ fn tool_run_with_incompatible_build_constraints() -> Result<()> { fn tool_run_with_dependencies_from_script() -> Result<()> { let context = TestContext::new("3.12").with_filtered_counts(); - let script_contents = indoc! {r#" + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! {r#" # /// script # requires-python = ">=3.11" # dependencies = [ @@ -2655,13 +2656,7 @@ fn tool_run_with_dependencies_from_script() -> Result<()> { # /// import anyio - "#}; - - let script = context.temp_dir.child("script.py"); - script.write_str(script_contents)?; - - let script_without_extension = context.temp_dir.child("script-no-ext"); - script_without_extension.write_str(script_contents)?; + "#})?; // script dependencies (anyio) are now installed. uv_snapshot!(context.filters(), context.tool_run() @@ -2689,20 +2684,6 @@ fn tool_run_with_dependencies_from_script() -> Result<()> { + sniffio==1.3.1 "); - uv_snapshot!(context.filters(), context.tool_run() - .arg("--with-requirements") - .arg("script-no-ext") - .arg("black") - .arg("script-no-ext") - .arg("-q"), @r" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved [N] packages in [TIME] - "); - // Error when the script is not a valid PEP723 script. let script = context.temp_dir.child("not_pep723_script.py"); script.write_str("import anyio")?; @@ -2729,7 +2710,7 @@ fn tool_run_with_dependencies_from_script() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: failed to read from file `missing_file.py`: No such file or directory (os error 2) + error: Failed to read `missing_file.py` (not found) "); Ok(())