Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 4 additions & 52 deletions crates/uv-requirements/src/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Pep723ScriptSource>),
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`).
Expand Down Expand Up @@ -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"))
Expand All @@ -60,25 +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() && path.is_file() {
// If we don't have an extension, attempt to detect a PEP 723 script, and
// fall back to `requirements.txt` format if not. (If the path isn't a file,
// we assume it's a readable file-like object in `requirements.txt` format, e.g.,
// `-r <( cat requirements.lock | grep -v nvidia | grep -v torch==)` or similar, in
// which case, reading the input would consume the stream, and only `requirements.txt`
// format is supported anyway.)
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))
}
Expand Down Expand Up @@ -310,43 +291,14 @@ impl RequirementsSource {
}
}

#[derive(Debug, Clone)]
pub struct Pep723ScriptSource {
path: PathBuf,
script: Option<Pep723Script>,
}

impl Pep723ScriptSource {
fn new(path: PathBuf) -> Box<Self> {
Box::new(Self { path, script: None })
}

fn with_script(path: PathBuf, script: Pep723Script) -> Box<Self> {
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)
Expand Down
30 changes: 16 additions & 14 deletions crates/uv-requirements/src/specification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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();
Expand Down
53 changes: 21 additions & 32 deletions crates/uv-scripts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,28 @@ impl Pep723Script {
///
/// See: <https://peps.python.org/pep-0723/>
pub async fn read(file: impl AsRef<Path>) -> Result<Option<Self>, 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<Path>) -> Result<Option<Self>, 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.
Expand Down Expand Up @@ -337,29 +349,6 @@ impl Pep723Script {
.and_then(|uv| uv.sources.as_ref())
.unwrap_or(&EMPTY)
}

fn from_contents(path: &Path, contents: &[u8]) -> Result<Option<Self>, 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.
Expand Down
39 changes: 5 additions & 34 deletions crates/uv/tests/it/tool_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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()
Expand Down Expand Up @@ -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")?;
Expand All @@ -2719,18 +2700,8 @@ fn tool_run_with_dependencies_from_script() -> Result<()> {
error: `not_pep723_script.py` does not contain inline script metadata
");

let filters = context
.filters()
.into_iter()
.chain([(
// The error message is different on Windows.
"The system cannot find the file specified.",
"No such file or directory",
)])
.collect::<Vec<_>>();

// Error when the script doesn't exist.
uv_snapshot!(filters, context.tool_run()
uv_snapshot!(context.filters(), context.tool_run()
.arg("--with-requirements")
.arg("missing_file.py")
.arg("black"), @r"
Expand All @@ -2739,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(())
Expand Down
Loading