uv/crates/uv-requirements-txt/src/requirement.rs

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),
}
}
}