mirror of https://github.com/astral-sh/uv
Always treat paths after -e as directories
This commit is contained in:
parent
20ab80ad8f
commit
2688a12609
|
|
@ -40,7 +40,7 @@ pub use crate::marker::{
|
|||
};
|
||||
pub use crate::origin::RequirementOrigin;
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
pub use crate::unnamed::{UnnamedRequirement, UnnamedRequirementUrl};
|
||||
pub use crate::unnamed::{PathHint, UnnamedRequirement, UnnamedRequirementUrl};
|
||||
pub use crate::verbatim_url::{
|
||||
Scheme, VerbatimUrl, VerbatimUrlError, expand_env_vars, looks_like_git_repository,
|
||||
split_scheme, strip_host,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ use crate::{
|
|||
parse_extras_cursor, split_extras, split_scheme, strip_host,
|
||||
};
|
||||
|
||||
/// A hint about whether a path refers to a file or directory.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PathHint {
|
||||
/// The path is known to be a file (e.g., a wheel or source distribution).
|
||||
File,
|
||||
/// The path is known to be a directory (e.g., an editable install).
|
||||
Directory,
|
||||
}
|
||||
|
||||
/// An extension over [`Pep508Url`] that also supports parsing unnamed requirements, namely paths.
|
||||
///
|
||||
/// The error type is fixed to the same as the [`Pep508Url`] impl error.
|
||||
|
|
@ -21,9 +30,40 @@ pub trait UnnamedRequirementUrl: Pep508Url {
|
|||
fn parse_path(path: impl AsRef<Path>, working_dir: impl AsRef<Path>)
|
||||
-> Result<Self, Self::Err>;
|
||||
|
||||
/// Parse a URL from a relative or absolute path, with a hint about whether the path is a file
|
||||
/// or directory.
|
||||
///
|
||||
/// The hint is used to determine whether the path should be parsed as a file or directory. If
|
||||
/// no such hint is provided, we'll query for the path kind; if the path doesn't exist, it'll be
|
||||
/// treated as a directory if and only if it is extensionless.
|
||||
fn parse_path_with_hint(
|
||||
path: impl AsRef<Path>,
|
||||
working_dir: impl AsRef<Path>,
|
||||
hint: Option<PathHint>,
|
||||
) -> Result<Self, Self::Err> {
|
||||
// Default implementation ignores the hint.
|
||||
let _ = hint;
|
||||
Self::parse_path(path, working_dir)
|
||||
}
|
||||
|
||||
/// Parse a URL from an absolute path.
|
||||
fn parse_absolute_path(path: impl AsRef<Path>) -> Result<Self, Self::Err>;
|
||||
|
||||
/// Parse a URL from an absolute path, with a hint about whether the path is a file or
|
||||
/// directory.
|
||||
///
|
||||
/// The hint is used to determine whether the path should be parsed as a file or directory. If
|
||||
/// no such hint is provided, we'll query for the path kind; if the path doesn't exist, it'll be
|
||||
/// treated as a directory if and only if it is extensionless.
|
||||
fn parse_absolute_path_with_hint(
|
||||
path: impl AsRef<Path>,
|
||||
hint: Option<PathHint>,
|
||||
) -> Result<Self, Self::Err> {
|
||||
// Default implementation ignores the hint.
|
||||
let _ = hint;
|
||||
Self::parse_absolute_path(path)
|
||||
}
|
||||
|
||||
/// Parse a URL from a string.
|
||||
fn parse_unnamed_url(given: impl AsRef<str>) -> Result<Self, Self::Err>;
|
||||
|
||||
|
|
@ -110,10 +150,26 @@ impl<Url: UnnamedRequirementUrl> UnnamedRequirement<Url> {
|
|||
working_dir: impl AsRef<Path>,
|
||||
reporter: &mut impl Reporter,
|
||||
) -> Result<Self, Pep508Error<Url>> {
|
||||
parse_unnamed_requirement(
|
||||
Self::parse_with_hint(input, working_dir, reporter, None)
|
||||
}
|
||||
|
||||
/// Parse a PEP 508-like direct URL requirement without a package name, with a hint about
|
||||
/// whether the path is a file or directory.
|
||||
///
|
||||
/// The hint is used to determine whether the path should be parsed as a file or directory. If
|
||||
/// no such hint is provided, we'll query for the path kind; if the path doesn't exist, it'll be
|
||||
/// treated as a directory if and only if it is extensionless.
|
||||
pub fn parse_with_hint(
|
||||
input: &str,
|
||||
working_dir: impl AsRef<Path>,
|
||||
reporter: &mut impl Reporter,
|
||||
hint: Option<PathHint>,
|
||||
) -> Result<Self, Pep508Error<Url>> {
|
||||
parse_unnamed_requirement_with_hint(
|
||||
&mut Cursor::new(input),
|
||||
Some(working_dir.as_ref()),
|
||||
reporter,
|
||||
hint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -155,11 +211,22 @@ fn parse_unnamed_requirement<Url: UnnamedRequirementUrl>(
|
|||
cursor: &mut Cursor,
|
||||
working_dir: Option<&Path>,
|
||||
reporter: &mut impl Reporter,
|
||||
) -> Result<UnnamedRequirement<Url>, Pep508Error<Url>> {
|
||||
parse_unnamed_requirement_with_hint(cursor, working_dir, reporter, None)
|
||||
}
|
||||
|
||||
/// Parse a PEP 508-like direct URL specifier without a package name, with a hint about whether
|
||||
/// the path is a file or directory.
|
||||
fn parse_unnamed_requirement_with_hint<Url: UnnamedRequirementUrl>(
|
||||
cursor: &mut Cursor,
|
||||
working_dir: Option<&Path>,
|
||||
reporter: &mut impl Reporter,
|
||||
hint: Option<PathHint>,
|
||||
) -> Result<UnnamedRequirement<Url>, Pep508Error<Url>> {
|
||||
cursor.eat_whitespace();
|
||||
|
||||
// Parse the URL itself, along with any extras.
|
||||
let (url, extras) = parse_unnamed_url::<Url>(cursor, working_dir)?;
|
||||
let (url, extras) = parse_unnamed_url::<Url>(cursor, working_dir, hint)?;
|
||||
|
||||
// wsp*
|
||||
cursor.eat_whitespace();
|
||||
|
|
@ -213,6 +280,7 @@ fn preprocess_unnamed_url<Url: UnnamedRequirementUrl>(
|
|||
cursor: &Cursor,
|
||||
start: usize,
|
||||
len: usize,
|
||||
#[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused))] hint: Option<PathHint>,
|
||||
) -> Result<(Url, Vec<ExtraName>), Pep508Error<Url>> {
|
||||
// Split extras _before_ expanding the URL. We assume that the extras are not environment
|
||||
// variables. If we parsed the extras after expanding the URL, then the verbatim representation
|
||||
|
|
@ -251,7 +319,7 @@ fn preprocess_unnamed_url<Url: UnnamedRequirementUrl>(
|
|||
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
if let Some(working_dir) = working_dir {
|
||||
let url = Url::parse_path(path.as_ref(), working_dir)
|
||||
let url = Url::parse_path_with_hint(path.as_ref(), working_dir, hint)
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::UrlError(err),
|
||||
start,
|
||||
|
|
@ -262,7 +330,7 @@ fn preprocess_unnamed_url<Url: UnnamedRequirementUrl>(
|
|||
return Ok((url, extras));
|
||||
}
|
||||
|
||||
let url = Url::parse_absolute_path(path.as_ref())
|
||||
let url = Url::parse_absolute_path_with_hint(path.as_ref(), hint)
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::UrlError(err),
|
||||
start,
|
||||
|
|
@ -289,7 +357,7 @@ fn preprocess_unnamed_url<Url: UnnamedRequirementUrl>(
|
|||
// Ex) `C:\Users\ferris\wheel-0.42.0.tar.gz`
|
||||
_ => {
|
||||
if let Some(working_dir) = working_dir {
|
||||
let url = Url::parse_path(expanded.as_ref(), working_dir)
|
||||
let url = Url::parse_path_with_hint(expanded.as_ref(), working_dir, hint)
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::UrlError(err),
|
||||
start,
|
||||
|
|
@ -300,7 +368,7 @@ fn preprocess_unnamed_url<Url: UnnamedRequirementUrl>(
|
|||
return Ok((url, extras));
|
||||
}
|
||||
|
||||
let url = Url::parse_absolute_path(expanded.as_ref())
|
||||
let url = Url::parse_absolute_path_with_hint(expanded.as_ref(), hint)
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::UrlError(err),
|
||||
start,
|
||||
|
|
@ -314,7 +382,7 @@ fn preprocess_unnamed_url<Url: UnnamedRequirementUrl>(
|
|||
} else {
|
||||
// Ex) `../editable/`
|
||||
if let Some(working_dir) = working_dir {
|
||||
let url = Url::parse_path(expanded.as_ref(), working_dir)
|
||||
let url = Url::parse_path_with_hint(expanded.as_ref(), working_dir, hint)
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::UrlError(err),
|
||||
start,
|
||||
|
|
@ -325,7 +393,7 @@ fn preprocess_unnamed_url<Url: UnnamedRequirementUrl>(
|
|||
return Ok((url, extras));
|
||||
}
|
||||
|
||||
let url = Url::parse_absolute_path(expanded.as_ref())
|
||||
let url = Url::parse_absolute_path_with_hint(expanded.as_ref(), hint)
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::UrlError(err),
|
||||
start,
|
||||
|
|
@ -357,6 +425,7 @@ fn preprocess_unnamed_url<Url: UnnamedRequirementUrl>(
|
|||
fn parse_unnamed_url<Url: UnnamedRequirementUrl>(
|
||||
cursor: &mut Cursor,
|
||||
working_dir: Option<&Path>,
|
||||
hint: Option<PathHint>,
|
||||
) -> Result<(Url, Vec<ExtraName>), Pep508Error<Url>> {
|
||||
// wsp*
|
||||
cursor.eat_whitespace();
|
||||
|
|
@ -413,7 +482,7 @@ fn parse_unnamed_url<Url: UnnamedRequirementUrl>(
|
|||
});
|
||||
}
|
||||
|
||||
let url = preprocess_unnamed_url(url, working_dir, cursor, start, len)?;
|
||||
let url = preprocess_unnamed_url(url, working_dir, cursor, start, len, hint)?;
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ use uv_cache_key::{CacheKey, CacheKeyHasher};
|
|||
use uv_distribution_filename::{DistExtension, ExtensionError};
|
||||
use uv_git_types::{GitUrl, GitUrlParseError};
|
||||
use uv_pep508::{
|
||||
Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError, looks_like_git_repository,
|
||||
PathHint, Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError,
|
||||
looks_like_git_repository,
|
||||
};
|
||||
use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
|
||||
|
||||
|
|
@ -79,11 +80,26 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
|
|||
fn parse_path(
|
||||
path: impl AsRef<Path>,
|
||||
working_dir: impl AsRef<Path>,
|
||||
) -> Result<Self, Self::Err> {
|
||||
Self::parse_path_with_hint(path, working_dir, None)
|
||||
}
|
||||
|
||||
fn parse_path_with_hint(
|
||||
path: impl AsRef<Path>,
|
||||
working_dir: impl AsRef<Path>,
|
||||
hint: Option<PathHint>,
|
||||
) -> Result<Self, Self::Err> {
|
||||
let verbatim = VerbatimUrl::from_path(&path, &working_dir)?;
|
||||
let verbatim_path = verbatim.as_path()?;
|
||||
let is_dir = if let Ok(metadata) = verbatim_path.metadata() {
|
||||
metadata.is_dir()
|
||||
} else if DistExtension::from_path(&path).is_ok() {
|
||||
// If the path has a recognized distribution extension (like `.whl` or `.tar.gz`),
|
||||
// treat it as a file regardless of the hint.
|
||||
false
|
||||
} else if let Some(hint) = hint {
|
||||
// Use the hint for ambiguous paths (paths with unrecognized extensions like `.bar`).
|
||||
hint == PathHint::Directory
|
||||
} else {
|
||||
verbatim_path.extension().is_none()
|
||||
};
|
||||
|
|
@ -112,10 +128,24 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
|
|||
}
|
||||
|
||||
fn parse_absolute_path(path: impl AsRef<Path>) -> Result<Self, Self::Err> {
|
||||
Self::parse_absolute_path_with_hint(path, None)
|
||||
}
|
||||
|
||||
fn parse_absolute_path_with_hint(
|
||||
path: impl AsRef<Path>,
|
||||
hint: Option<PathHint>,
|
||||
) -> Result<Self, Self::Err> {
|
||||
let verbatim = VerbatimUrl::from_absolute_path(&path)?;
|
||||
let verbatim_path = verbatim.as_path()?;
|
||||
let is_dir = if let Ok(metadata) = verbatim_path.metadata() {
|
||||
metadata.is_dir()
|
||||
} else if DistExtension::from_path(&path).is_ok() {
|
||||
// If the path has a recognized distribution extension (like `.whl` or `.tar.gz`),
|
||||
// treat it as a file regardless of the hint.
|
||||
false
|
||||
} else if let Some(hint) = hint {
|
||||
// Use the hint for ambiguous paths (paths with unrecognized extensions like `.bar`).
|
||||
hint == PathHint::Directory
|
||||
} else {
|
||||
verbatim_path.extension().is_none()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1643,6 +1643,10 @@ mod test {
|
|||
#[cfg(unix)]
|
||||
#[test_case(Path::new("bare-url.txt"))]
|
||||
#[test_case(Path::new("editable.txt"))]
|
||||
// Note: `semicolon.txt` contains a syntax error (missing whitespace before `;`), but since
|
||||
// it's an editable requirement, we parse it as a directory path. The error will occur later
|
||||
// when the path doesn't exist.
|
||||
#[test_case(Path::new("semicolon.txt"))]
|
||||
#[tokio::test]
|
||||
async fn parse_unix(path: &Path) {
|
||||
let working_dir = workspace_test_data_dir().join("requirements-txt");
|
||||
|
|
@ -1662,7 +1666,6 @@ mod test {
|
|||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test_case(Path::new("semicolon.txt"))]
|
||||
#[test_case(Path::new("hash.txt"))]
|
||||
#[tokio::test]
|
||||
async fn parse_err(path: &Path) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ use std::path::Path;
|
|||
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep508::{
|
||||
Pep508Error, Pep508ErrorSource, RequirementOrigin, TracingReporter, UnnamedRequirement,
|
||||
PathHint, Pep508Error, Pep508ErrorSource, RequirementOrigin, TracingReporter,
|
||||
UnnamedRequirement,
|
||||
};
|
||||
use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl, VerbatimParsedUrl};
|
||||
|
||||
|
|
@ -132,15 +133,26 @@ impl RequirementsTxtRequirement {
|
|||
working_dir: impl AsRef<Path>,
|
||||
editable: bool,
|
||||
) -> Result<Self, Box<Pep508Error<VerbatimParsedUrl>>> {
|
||||
// When parsing an editable requirement, hint that the path is a directory. Editable
|
||||
// requirements always refer to local directories (containing a `pyproject.toml` or
|
||||
// `setup.py`), so when the path doesn't exist, we should assume it's a directory rather
|
||||
// than a file with an unrecognized extension (like `foo.bar`).
|
||||
let hint = if editable {
|
||||
Some(PathHint::Directory)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Attempt to parse as a PEP 508-compliant requirement.
|
||||
match uv_pep508::Requirement::parse(input, &working_dir) {
|
||||
Ok(requirement) => {
|
||||
// As a special-case, interpret `dagster` as `./dagster` if we're in editable mode.
|
||||
if editable && requirement.version_or_url.is_none() {
|
||||
Ok(Self::Unnamed(UnnamedRequirement::parse(
|
||||
Ok(Self::Unnamed(UnnamedRequirement::parse_with_hint(
|
||||
input,
|
||||
&working_dir,
|
||||
&mut TracingReporter,
|
||||
hint,
|
||||
)?))
|
||||
} else {
|
||||
Ok(Self::Named(requirement))
|
||||
|
|
@ -149,10 +161,11 @@ impl RequirementsTxtRequirement {
|
|||
Err(err) => match err.message {
|
||||
Pep508ErrorSource::UnsupportedRequirement(_) => {
|
||||
// If that fails, attempt to parse as a direct URL requirement.
|
||||
Ok(Self::Unnamed(UnnamedRequirement::parse(
|
||||
Ok(Self::Unnamed(UnnamedRequirement::parse_with_hint(
|
||||
input,
|
||||
&working_dir,
|
||||
&mut TracingReporter,
|
||||
hint,
|
||||
)?))
|
||||
}
|
||||
_ => Err(err),
|
||||
|
|
|
|||
|
|
@ -2,21 +2,67 @@
|
|||
source: crates/uv-requirements-txt/src/lib.rs
|
||||
expression: actual
|
||||
---
|
||||
RequirementsTxtFileError {
|
||||
file: "<REQUIREMENTS_DIR>/semicolon.txt",
|
||||
error: Pep508 {
|
||||
source: Pep508Error {
|
||||
message: UrlError(
|
||||
MissingExtensionPath(
|
||||
RequirementsTxt {
|
||||
requirements: [],
|
||||
constraints: [],
|
||||
editables: [
|
||||
RequirementEntry {
|
||||
requirement: Unnamed(
|
||||
UnnamedRequirement {
|
||||
url: VerbatimParsedUrl {
|
||||
parsed_url: Directory(
|
||||
ParsedDirectoryUrl {
|
||||
url: DisplaySafeUrl {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<REQUIREMENTS_DIR>/editable;python_version%20%3E=%20%223.9%22%20and%20os_name%20==%20%22posix%22",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
install_path: "<REQUIREMENTS_DIR>/editable;python_version >= \"3.9\" and os_name == \"posix\"",
|
||||
editable: Some(
|
||||
true,
|
||||
),
|
||||
virtual: None,
|
||||
},
|
||||
),
|
||||
verbatim: VerbatimUrl {
|
||||
url: DisplaySafeUrl {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<REQUIREMENTS_DIR>/editable;python_version%20%3E=%20%223.9%22%20and%20os_name%20==%20%22posix%22",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
given: Some(
|
||||
"./editable;python_version >= \"3.9\" and os_name == \"posix\"",
|
||||
Dist,
|
||||
),
|
||||
},
|
||||
},
|
||||
extras: [],
|
||||
marker: true,
|
||||
origin: Some(
|
||||
File(
|
||||
"<REQUIREMENTS_DIR>/semicolon.txt",
|
||||
),
|
||||
),
|
||||
start: 0,
|
||||
len: 57,
|
||||
input: "./editable;python_version >= \"3.9\" and os_name == \"posix\"",
|
||||
},
|
||||
start: 50,
|
||||
end: 107,
|
||||
),
|
||||
hashes: [],
|
||||
},
|
||||
],
|
||||
index_url: None,
|
||||
extra_index_urls: [],
|
||||
find_links: [],
|
||||
no_index: false,
|
||||
no_binary: None,
|
||||
only_binary: None,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue