mirror of https://github.com/astral-sh/uv
186 lines
7.2 KiB
Rust
186 lines
7.2 KiB
Rust
use std::fmt::Display;
|
|
use std::path::Path;
|
|
|
|
use uv_normalize::PackageName;
|
|
use uv_pep508::{
|
|
PathHint, Pep508Error, Pep508ErrorSource, RequirementOrigin, TracingReporter,
|
|
UnnamedRequirement,
|
|
};
|
|
use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl, VerbatimParsedUrl};
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum EditableError {
|
|
#[error("Editable `{0}` must refer to a local directory")]
|
|
MissingVersion(PackageName),
|
|
|
|
#[error("Editable `{0}` must refer to a local directory, not a versioned package")]
|
|
Versioned(PackageName),
|
|
|
|
#[error("Editable `{0}` must refer to a local directory, not an archive: `{1}`")]
|
|
File(PackageName, String),
|
|
|
|
#[error("Editable `{0}` must refer to a local directory, not an HTTPS URL: `{1}`")]
|
|
Https(PackageName, String),
|
|
|
|
#[error("Editable `{0}` must refer to a local directory, not a Git URL: `{1}`")]
|
|
Git(PackageName, String),
|
|
|
|
#[error("Editable must refer to a local directory, not an archive: `{0}`")]
|
|
UnnamedFile(String),
|
|
|
|
#[error("Editable must refer to a local directory, not an HTTPS URL: `{0}`")]
|
|
UnnamedHttps(String),
|
|
|
|
#[error("Editable must refer to a local directory, not a Git URL: `{0}`")]
|
|
UnnamedGit(String),
|
|
}
|
|
|
|
/// A requirement specifier in a `requirements.txt` file.
|
|
///
|
|
/// Analog to `UnresolvedRequirement` but with `uv_pep508::Requirement` instead of
|
|
/// `distribution_types::Requirement`.
|
|
#[derive(Hash, Debug, Clone, Eq, PartialEq)]
|
|
pub enum RequirementsTxtRequirement {
|
|
/// The uv-specific superset over PEP 508 requirements specifier incorporating
|
|
/// `tool.uv.sources`.
|
|
Named(uv_pep508::Requirement<VerbatimParsedUrl>),
|
|
/// A PEP 508-like, direct URL dependency specifier.
|
|
Unnamed(UnnamedRequirement<VerbatimParsedUrl>),
|
|
}
|
|
|
|
impl RequirementsTxtRequirement {
|
|
/// Set the source file containing the requirement.
|
|
#[must_use]
|
|
pub fn with_origin(self, origin: RequirementOrigin) -> Self {
|
|
match self {
|
|
Self::Named(requirement) => Self::Named(requirement.with_origin(origin)),
|
|
Self::Unnamed(requirement) => Self::Unnamed(requirement.with_origin(origin)),
|
|
}
|
|
}
|
|
|
|
/// Convert the [`RequirementsTxtRequirement`] into an editable requirement.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`EditableError`] if the requirement cannot be interpreted as editable.
|
|
/// Specifically, only local directory URLs are supported.
|
|
pub fn into_editable(self) -> Result<Self, EditableError> {
|
|
match self {
|
|
Self::Named(requirement) => {
|
|
let Some(version_or_url) = requirement.version_or_url else {
|
|
return Err(EditableError::MissingVersion(requirement.name));
|
|
};
|
|
|
|
let uv_pep508::VersionOrUrl::Url(url) = version_or_url else {
|
|
return Err(EditableError::Versioned(requirement.name));
|
|
};
|
|
|
|
let parsed_url = match url.parsed_url {
|
|
ParsedUrl::Directory(parsed_url) => parsed_url,
|
|
ParsedUrl::Path(_) => {
|
|
return Err(EditableError::File(requirement.name, url.to_string()));
|
|
}
|
|
ParsedUrl::Archive(_) => {
|
|
return Err(EditableError::Https(requirement.name, url.to_string()));
|
|
}
|
|
ParsedUrl::Git(_) => {
|
|
return Err(EditableError::Git(requirement.name, url.to_string()));
|
|
}
|
|
};
|
|
|
|
Ok(Self::Named(uv_pep508::Requirement {
|
|
version_or_url: Some(uv_pep508::VersionOrUrl::Url(VerbatimParsedUrl {
|
|
verbatim: url.verbatim,
|
|
parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
|
|
editable: Some(true),
|
|
..parsed_url
|
|
}),
|
|
})),
|
|
..requirement
|
|
}))
|
|
}
|
|
Self::Unnamed(requirement) => {
|
|
let parsed_url = match requirement.url.parsed_url {
|
|
ParsedUrl::Directory(parsed_url) => parsed_url,
|
|
ParsedUrl::Path(_) => {
|
|
return Err(EditableError::UnnamedFile(requirement.to_string()));
|
|
}
|
|
ParsedUrl::Archive(_) => {
|
|
return Err(EditableError::UnnamedHttps(requirement.to_string()));
|
|
}
|
|
ParsedUrl::Git(_) => {
|
|
return Err(EditableError::UnnamedGit(requirement.to_string()));
|
|
}
|
|
};
|
|
|
|
Ok(Self::Unnamed(UnnamedRequirement {
|
|
url: VerbatimParsedUrl {
|
|
verbatim: requirement.url.verbatim,
|
|
parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
|
|
editable: Some(true),
|
|
..parsed_url
|
|
}),
|
|
},
|
|
..requirement
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parse a requirement as seen in a `requirements.txt` file.
|
|
pub fn parse(
|
|
input: &str,
|
|
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_with_hint(
|
|
input,
|
|
&working_dir,
|
|
&mut TracingReporter,
|
|
hint,
|
|
)?))
|
|
} else {
|
|
Ok(Self::Named(requirement))
|
|
}
|
|
}
|
|
Err(err) => match err.message {
|
|
Pep508ErrorSource::UnsupportedRequirement(_) => {
|
|
// If that fails, attempt to parse as a direct URL requirement.
|
|
Ok(Self::Unnamed(UnnamedRequirement::parse_with_hint(
|
|
input,
|
|
&working_dir,
|
|
&mut TracingReporter,
|
|
hint,
|
|
)?))
|
|
}
|
|
_ => Err(err),
|
|
},
|
|
}
|
|
.map_err(Box::new)
|
|
}
|
|
}
|
|
|
|
impl Display for RequirementsTxtRequirement {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Named(requirement) => Display::fmt(&requirement, f),
|
|
Self::Unnamed(requirement) => Display::fmt(&requirement, f),
|
|
}
|
|
}
|
|
}
|