Allow `--with-requirements` to load extensionless inline-metadata scripts (#16805)

Reverts astral-sh/uv#16802
This commit is contained in:
Charlie Marsh 2025-11-21 11:53:41 -05:00 committed by GitHub
parent a8bf05d83b
commit f7f159234f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 131 additions and 46 deletions

View File

@ -6,6 +6,7 @@ use console::Term;
use uv_fs::{CWD, Simplified}; use uv_fs::{CWD, Simplified};
use uv_requirements_txt::RequirementsTxtRequirement; use uv_requirements_txt::RequirementsTxtRequirement;
use uv_scripts::Pep723Script;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum RequirementsSource { pub enum RequirementsSource {
@ -14,7 +15,7 @@ pub enum RequirementsSource {
/// An editable path was provided on the command line (e.g., `pip install -e ../flask`). /// An editable path was provided on the command line (e.g., `pip install -e ../flask`).
Editable(RequirementsTxtRequirement), Editable(RequirementsTxtRequirement),
/// Dependencies were provided via a PEP 723 script. /// Dependencies were provided via a PEP 723 script.
Pep723Script(PathBuf), Pep723Script(Box<Pep723ScriptSource>),
/// Dependencies were provided via a `pylock.toml` file. /// Dependencies were provided via a `pylock.toml` file.
PylockToml(PathBuf), PylockToml(PathBuf),
/// Dependencies were provided via a `requirements.txt` file (e.g., `pip install -r requirements.txt`). /// Dependencies were provided via a `requirements.txt` file (e.g., `pip install -r requirements.txt`).
@ -50,8 +51,7 @@ impl RequirementsSource {
.extension() .extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyw")) .is_some_and(|ext| ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyw"))
{ {
// TODO(blueraft): Support scripts without an extension. Ok(Self::Pep723Script(Pep723ScriptSource::new(path)))
Ok(Self::Pep723Script(path))
} else if path } else if path
.extension() .extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) .is_some_and(|ext| ext.eq_ignore_ascii_case("toml"))
@ -60,6 +60,24 @@ 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`)", "`{}` 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(), 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 == Path::new("-") {
// If the path is `-`, treat it as a requirements.txt file from stdin.
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 { } else {
Ok(Self::RequirementsTxt(path)) Ok(Self::RequirementsTxt(path))
} }
@ -291,14 +309,43 @@ 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 { impl std::fmt::Display for RequirementsSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Package(package) => write!(f, "{package:?}"), Self::Package(package) => write!(f, "{package:?}"),
Self::Editable(path) => write!(f, "-e {path:?}"), Self::Editable(path) => write!(f, "-e {path:?}"),
Self::Pep723Script(source) => {
write!(f, "{}", source.path().simplified_display())
}
Self::PylockToml(path) Self::PylockToml(path)
| Self::RequirementsTxt(path) | Self::RequirementsTxt(path)
| Self::Pep723Script(path)
| Self::PyprojectToml(path) | Self::PyprojectToml(path)
| Self::SetupPy(path) | Self::SetupPy(path)
| Self::SetupCfg(path) | Self::SetupCfg(path)

View File

@ -46,7 +46,7 @@ use uv_fs::{CWD, Simplified};
use uv_normalize::{ExtraName, PackageName, PipGroupName}; use uv_normalize::{ExtraName, PackageName, PipGroupName};
use uv_pypi_types::PyProjectToml; use uv_pypi_types::PyProjectToml;
use uv_requirements_txt::{RequirementsTxt, RequirementsTxtRequirement}; use uv_requirements_txt::{RequirementsTxt, RequirementsTxtRequirement};
use uv_scripts::{Pep723Error, Pep723Item, Pep723Script}; use uv_scripts::{Pep723Item, Pep723Script};
use uv_warnings::warn_user; use uv_warnings::warn_user;
use crate::{RequirementsSource, SourceTree}; use crate::{RequirementsSource, SourceTree};
@ -184,22 +184,20 @@ impl RequirementsSpecification {
..Self::default() ..Self::default()
} }
} }
RequirementsSource::Pep723Script(path) => { RequirementsSource::Pep723Script(source) => {
let script = match Pep723Script::read(&path).await { 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(Some(script)) => Pep723Item::Script(script),
Ok(None) => { Ok(None) => {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"`{}` does not contain inline script metadata", "`{}` does not contain inline script metadata",
path.user_display(), source.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()), Err(err) => return Err(err.into()),
}
}; };
let metadata = script.metadata(); let metadata = script.metadata();

View File

@ -175,28 +175,16 @@ 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> {
let contents = fs_err::tokio::read(&file).await?; let file = file.as_ref();
let contents = fs_err::tokio::read(file).await?;
Self::from_contents(file, &contents)
}
// Extract the `script` tag. /// Read the PEP 723 `script` metadata from a Python file using blocking I/O.
let ScriptTag { pub fn read_sync(file: impl AsRef<Path>) -> Result<Option<Self>, Pep723Error> {
prelude, let file = file.as_ref();
metadata, let contents = fs_err::read(file)?;
postlude, Self::from_contents(file, &contents)
} = 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. /// Reads a Python script and generates a default PEP 723 metadata table.
@ -349,6 +337,29 @@ impl Pep723Script {
.and_then(|uv| uv.sources.as_ref()) .and_then(|uv| uv.sources.as_ref())
.unwrap_or(&EMPTY) .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. /// PEP 723 metadata as parsed from a `script` comment block.

View File

@ -2646,8 +2646,7 @@ fn tool_run_with_incompatible_build_constraints() -> Result<()> {
fn tool_run_with_dependencies_from_script() -> Result<()> { fn tool_run_with_dependencies_from_script() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_counts(); let context = TestContext::new("3.12").with_filtered_counts();
let script = context.temp_dir.child("script.py"); let script_contents = indoc! {r#"
script.write_str(indoc! {r#"
# /// script # /// script
# requires-python = ">=3.11" # requires-python = ">=3.11"
# dependencies = [ # dependencies = [
@ -2656,7 +2655,13 @@ fn tool_run_with_dependencies_from_script() -> Result<()> {
# /// # ///
import anyio 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. // script dependencies (anyio) are now installed.
uv_snapshot!(context.filters(), context.tool_run() uv_snapshot!(context.filters(), context.tool_run()
@ -2684,6 +2689,20 @@ fn tool_run_with_dependencies_from_script() -> Result<()> {
+ sniffio==1.3.1 + 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. // Error when the script is not a valid PEP723 script.
let script = context.temp_dir.child("not_pep723_script.py"); let script = context.temp_dir.child("not_pep723_script.py");
script.write_str("import anyio")?; script.write_str("import anyio")?;
@ -2700,8 +2719,18 @@ fn tool_run_with_dependencies_from_script() -> Result<()> {
error: `not_pep723_script.py` does not contain inline script metadata 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. // Error when the script doesn't exist.
uv_snapshot!(context.filters(), context.tool_run() uv_snapshot!(filters, context.tool_run()
.arg("--with-requirements") .arg("--with-requirements")
.arg("missing_file.py") .arg("missing_file.py")
.arg("black"), @r" .arg("black"), @r"
@ -2710,7 +2739,7 @@ fn tool_run_with_dependencies_from_script() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: Failed to read `missing_file.py` (not found) error: failed to read from file `missing_file.py`: No such file or directory (os error 2)
"); ");
Ok(()) Ok(())