Revert "Allow `--with-requirements` to load extensionless inline-metadata scripts" (#16861)

Reverts https://github.com/astral-sh/uv/pull/16805 /
https://github.com/astral-sh/uv/pull/16744

This also invalidates

- https://github.com/astral-sh/uv/pull/16855
- #16857 

There's probably a way we can make this work, but detecting whether a
file is safe to read repeatedly is non-trivial, `is_file` returns `true`
for `/dev/stdin` on macOS so the approach from #16857 is not sufficient.
I spent a while trying to add `is_char_device` detection for macOS but
unfortunately that didn't work.
This commit is contained in:
Zanie Blue 2025-11-26 08:57:45 -06:00 committed by GitHub
parent 4d747f6e86
commit ca62066194
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 46 additions and 132 deletions

View File

@ -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<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`).
@ -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,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))
}
@ -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)

View File

@ -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 {
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",
source.path().user_display(),
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();

View File

@ -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.
@ -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.

View File

@ -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")?;
@ -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"
@ -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(())