mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Make Requirement
generic over url type (#3253)
This change allows switching out the url type for requirements. The original idea was to allow different types for different requirement origins, so that core metadata reads can ban non-pep 508 requirements while we only allow them for requirements.txt. This didn't work out because we expect `&Requirement`s from all sources to match. I also tried to split `pep508_rs` into a PEP 508 compliant crate and into our extensions, but they are to tightly coupled. I think this change is an improvement still as it reduces the hardcoded dependence on `VerbatimUrl`.
This commit is contained in:
parent
8e86cd0c73
commit
55f6e4e66b
8 changed files with 560 additions and 478 deletions
|
@ -41,7 +41,7 @@ use url::Url;
|
|||
|
||||
use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
|
||||
use pep440_rs::Version;
|
||||
use pep508_rs::{Scheme, VerbatimUrl};
|
||||
use pep508_rs::{Pep508Url, Scheme, VerbatimUrl};
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
pub use crate::any::*;
|
||||
|
@ -81,11 +81,11 @@ mod specified_requirement;
|
|||
mod traits;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum VersionOrUrlRef<'a> {
|
||||
pub enum VersionOrUrlRef<'a, T: Pep508Url = VerbatimUrl> {
|
||||
/// A PEP 440 version specifier, used to identify a distribution in a registry.
|
||||
Version(&'a Version),
|
||||
/// A URL, used to identify a distribution at an arbitrary location.
|
||||
Url(&'a VerbatimUrl),
|
||||
Url(&'a T),
|
||||
}
|
||||
|
||||
impl Verbatim for VersionOrUrlRef<'_> {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use crate::{Pep508Error, Pep508ErrorSource};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::Chars;
|
||||
|
||||
use crate::{Pep508Error, Pep508ErrorSource, Pep508Url};
|
||||
|
||||
/// A [`Cursor`] over a string.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cursor<'a> {
|
||||
pub(crate) struct Cursor<'a> {
|
||||
input: &'a str,
|
||||
chars: Chars<'a>,
|
||||
pos: usize,
|
||||
|
@ -12,7 +13,7 @@ pub struct Cursor<'a> {
|
|||
|
||||
impl<'a> Cursor<'a> {
|
||||
/// Convert from `&str`.
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
pub(crate) fn new(input: &'a str) -> Self {
|
||||
Self {
|
||||
input,
|
||||
chars: input.chars(),
|
||||
|
@ -21,7 +22,7 @@ impl<'a> Cursor<'a> {
|
|||
}
|
||||
|
||||
/// Returns a new cursor starting at the given position.
|
||||
pub fn at(self, pos: usize) -> Self {
|
||||
pub(crate) fn at(self, pos: usize) -> Self {
|
||||
Self {
|
||||
input: self.input,
|
||||
chars: self.input[pos..].chars(),
|
||||
|
@ -107,11 +108,11 @@ impl<'a> Cursor<'a> {
|
|||
}
|
||||
|
||||
/// Consumes characters from the cursor, raising an error if it doesn't match the given token.
|
||||
pub(crate) fn next_expect_char(
|
||||
pub(crate) fn next_expect_char<T: Pep508Url>(
|
||||
&mut self,
|
||||
expected: char,
|
||||
span_start: usize,
|
||||
) -> Result<(), Pep508Error> {
|
||||
) -> Result<(), Pep508Error<T>> {
|
||||
match self.next() {
|
||||
None => Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String(format!(
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -9,21 +9,24 @@
|
|||
//! outcomes. This implementation tries to carefully validate everything and emit warnings whenever
|
||||
//! bogus comparisons with unintended semantics are made.
|
||||
|
||||
use crate::cursor::Cursor;
|
||||
use crate::{Pep508Error, Pep508ErrorSource};
|
||||
use pep440_rs::{Version, VersionPattern, VersionSpecifier};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
use pyo3::{
|
||||
basic::CompareOp, exceptions::PyValueError, pyclass, pymethods, types::PyAnyMethods, PyResult,
|
||||
Python,
|
||||
};
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
use pep440_rs::{Version, VersionPattern, VersionSpecifier};
|
||||
use uv_normalize::ExtraName;
|
||||
|
||||
use crate::cursor::Cursor;
|
||||
use crate::{Pep508Error, Pep508ErrorSource, Pep508Url};
|
||||
|
||||
/// Ways in which marker evaluation can fail
|
||||
#[derive(Debug, Eq, Hash, Ord, PartialOrd, PartialEq, Clone, Copy)]
|
||||
#[cfg_attr(feature = "pyo3", pyclass(module = "pep508"))]
|
||||
|
@ -1231,7 +1234,9 @@ impl Display for MarkerTree {
|
|||
/// marker_op = version_cmp | (wsp* 'in') | (wsp* 'not' wsp+ 'in')
|
||||
/// ```
|
||||
/// The `wsp*` has already been consumed by the caller.
|
||||
fn parse_marker_operator(cursor: &mut Cursor) -> Result<MarkerOperator, Pep508Error> {
|
||||
fn parse_marker_operator<T: Pep508Url>(
|
||||
cursor: &mut Cursor,
|
||||
) -> Result<MarkerOperator, Pep508Error<T>> {
|
||||
let (start, len) = if cursor.peek_char().is_some_and(|c| c.is_alphabetic()) {
|
||||
// "in" or "not"
|
||||
cursor.take_while(|char| !char.is_whitespace() && char != '\'' && char != '"')
|
||||
|
@ -1284,7 +1289,7 @@ fn parse_marker_operator(cursor: &mut Cursor) -> Result<MarkerOperator, Pep508Er
|
|||
/// '`os_name`', '`sys_platform`', '`platform_release`', '`platform_system`', '`platform_version`',
|
||||
/// '`platform_machine`', '`platform_python_implementation`', '`implementation_name`',
|
||||
/// '`implementation_version`', 'extra'
|
||||
fn parse_marker_value(cursor: &mut Cursor) -> Result<MarkerValue, Pep508Error> {
|
||||
fn parse_marker_value<T: Pep508Url>(cursor: &mut Cursor) -> Result<MarkerValue, Pep508Error<T>> {
|
||||
// > User supplied constants are always encoded as strings with either ' or " quote marks. Note
|
||||
// > that backslash escapes are not defined, but existing implementations do support them. They
|
||||
// > are not included in this specification because they add complexity and there is no observable
|
||||
|
@ -1328,7 +1333,9 @@ fn parse_marker_value(cursor: &mut Cursor) -> Result<MarkerValue, Pep508Error> {
|
|||
/// ```text
|
||||
/// marker_var:l marker_op:o marker_var:r
|
||||
/// ```
|
||||
fn parse_marker_key_op_value(cursor: &mut Cursor) -> Result<MarkerExpression, Pep508Error> {
|
||||
fn parse_marker_key_op_value<T: Pep508Url>(
|
||||
cursor: &mut Cursor,
|
||||
) -> Result<MarkerExpression, Pep508Error<T>> {
|
||||
cursor.eat_whitespace();
|
||||
let lvalue = parse_marker_value(cursor)?;
|
||||
cursor.eat_whitespace();
|
||||
|
@ -1349,7 +1356,7 @@ fn parse_marker_key_op_value(cursor: &mut Cursor) -> Result<MarkerExpression, Pe
|
|||
/// marker_expr = marker_var:l marker_op:o marker_var:r -> (o, l, r)
|
||||
/// | wsp* '(' marker:m wsp* ')' -> m
|
||||
/// ```
|
||||
fn parse_marker_expr(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error> {
|
||||
fn parse_marker_expr<T: Pep508Url>(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error<T>> {
|
||||
cursor.eat_whitespace();
|
||||
if let Some(start_pos) = cursor.eat_char('(') {
|
||||
let marker = parse_marker_or(cursor)?;
|
||||
|
@ -1364,7 +1371,7 @@ fn parse_marker_expr(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error> {
|
|||
/// marker_and = marker_expr:l wsp* 'and' marker_expr:r -> ('and', l, r)
|
||||
/// | marker_expr:m -> m
|
||||
/// ```
|
||||
fn parse_marker_and(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error> {
|
||||
fn parse_marker_and<T: Pep508Url>(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error<T>> {
|
||||
parse_marker_op(cursor, "and", MarkerTree::And, parse_marker_expr)
|
||||
}
|
||||
|
||||
|
@ -1372,17 +1379,17 @@ fn parse_marker_and(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error> {
|
|||
/// marker_or = marker_and:l wsp* 'or' marker_and:r -> ('or', l, r)
|
||||
/// | marker_and:m -> m
|
||||
/// ```
|
||||
fn parse_marker_or(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error> {
|
||||
fn parse_marker_or<T: Pep508Url>(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error<T>> {
|
||||
parse_marker_op(cursor, "or", MarkerTree::Or, parse_marker_and)
|
||||
}
|
||||
|
||||
/// Parses both `marker_and` and `marker_or`
|
||||
fn parse_marker_op(
|
||||
fn parse_marker_op<T: Pep508Url>(
|
||||
cursor: &mut Cursor,
|
||||
op: &str,
|
||||
op_constructor: fn(Vec<MarkerTree>) -> MarkerTree,
|
||||
parse_inner: fn(&mut Cursor) -> Result<MarkerTree, Pep508Error>,
|
||||
) -> Result<MarkerTree, Pep508Error> {
|
||||
parse_inner: fn(&mut Cursor) -> Result<MarkerTree, Pep508Error<T>>,
|
||||
) -> Result<MarkerTree, Pep508Error<T>> {
|
||||
// marker_and or marker_expr
|
||||
let first_element = parse_inner(cursor)?;
|
||||
// wsp*
|
||||
|
@ -1420,7 +1427,9 @@ fn parse_marker_op(
|
|||
/// ```text
|
||||
/// marker = marker_or^
|
||||
/// ```
|
||||
pub(crate) fn parse_markers_impl(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error> {
|
||||
pub(crate) fn parse_markers_cursor<T: Pep508Url>(
|
||||
cursor: &mut Cursor,
|
||||
) -> Result<MarkerTree, Pep508Error<T>> {
|
||||
let marker = parse_marker_or(cursor)?;
|
||||
cursor.eat_whitespace();
|
||||
if let Some((pos, unexpected)) = cursor.next() {
|
||||
|
@ -1440,21 +1449,24 @@ pub(crate) fn parse_markers_impl(cursor: &mut Cursor) -> Result<MarkerTree, Pep5
|
|||
|
||||
/// Parses markers such as `python_version < '3.8'` or
|
||||
/// `python_version == "3.10" and (sys_platform == "win32" or (os_name == "linux" and implementation_name == 'cpython'))`
|
||||
fn parse_markers(markers: &str) -> Result<MarkerTree, Pep508Error> {
|
||||
fn parse_markers<T: Pep508Url>(markers: &str) -> Result<MarkerTree, Pep508Error<T>> {
|
||||
let mut chars = Cursor::new(markers);
|
||||
parse_markers_impl(&mut chars)
|
||||
parse_markers_cursor(&mut chars)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use uv_normalize::ExtraName;
|
||||
|
||||
use crate::marker::{MarkerEnvironment, StringVersion};
|
||||
use crate::{
|
||||
MarkerExpression, MarkerOperator, MarkerTree, MarkerValue, MarkerValueString,
|
||||
MarkerValueVersion,
|
||||
};
|
||||
use insta::assert_snapshot;
|
||||
use std::str::FromStr;
|
||||
use uv_normalize::ExtraName;
|
||||
|
||||
fn parse_err(input: &str) -> String {
|
||||
MarkerTree::from_str(input).unwrap_err().to_string()
|
||||
|
|
|
@ -2,14 +2,17 @@ use std::fmt::{Display, Formatter};
|
|||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
use pyo3::pyclass;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use uv_fs::normalize_url_path;
|
||||
use uv_normalize::ExtraName;
|
||||
|
||||
use crate::cursor::Cursor;
|
||||
use crate::{MarkerEnvironment, MarkerTree, Pep508Error, VerbatimUrl};
|
||||
use crate::marker::parse_markers_cursor;
|
||||
use crate::{
|
||||
expand_env_vars, parse_extras_cursor, split_extras, split_scheme, strip_host, Cursor,
|
||||
MarkerEnvironment, MarkerTree, Pep508Error, Pep508ErrorSource, Scheme, VerbatimUrl,
|
||||
VerbatimUrlError,
|
||||
};
|
||||
|
||||
/// A PEP 508-like, direct URL dependency specifier without a package name.
|
||||
///
|
||||
|
@ -17,7 +20,6 @@ use crate::{MarkerEnvironment, MarkerTree, Pep508Error, VerbatimUrl};
|
|||
/// dependencies. This isn't compliant with PEP 508, but is common in `requirements.txt`, which
|
||||
/// is implementation-defined.
|
||||
#[derive(Hash, Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "pyo3", pyclass(module = "pep508"))]
|
||||
pub struct UnnamedRequirement {
|
||||
/// The direct URL that defines the version specifier.
|
||||
pub url: VerbatimUrl,
|
||||
|
@ -84,17 +86,223 @@ impl Serialize for UnnamedRequirement {
|
|||
}
|
||||
|
||||
impl FromStr for UnnamedRequirement {
|
||||
type Err = Pep508Error;
|
||||
type Err = Pep508Error<VerbatimUrl>;
|
||||
|
||||
/// Parse a PEP 508-like direct URL requirement without a package name.
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
crate::parse_unnamed_requirement(&mut Cursor::new(input), None)
|
||||
parse_unnamed_requirement(&mut Cursor::new(input), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl UnnamedRequirement {
|
||||
/// Parse a PEP 508-like direct URL requirement without a package name.
|
||||
pub fn parse(input: &str, working_dir: impl AsRef<Path>) -> Result<Self, Pep508Error> {
|
||||
crate::parse_unnamed_requirement(&mut Cursor::new(input), Some(working_dir.as_ref()))
|
||||
pub fn parse(
|
||||
input: &str,
|
||||
working_dir: impl AsRef<Path>,
|
||||
) -> Result<Self, Pep508Error<VerbatimUrl>> {
|
||||
parse_unnamed_requirement(&mut Cursor::new(input), Some(working_dir.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a PEP 508-like direct URL specifier without a package name.
|
||||
///
|
||||
/// Unlike pip, we allow extras on URLs and paths.
|
||||
fn parse_unnamed_requirement(
|
||||
cursor: &mut Cursor,
|
||||
working_dir: Option<&Path>,
|
||||
) -> Result<UnnamedRequirement, Pep508Error<VerbatimUrl>> {
|
||||
cursor.eat_whitespace();
|
||||
|
||||
// Parse the URL itself, along with any extras.
|
||||
let (url, extras) = parse_unnamed_url(cursor, working_dir)?;
|
||||
let requirement_end = cursor.pos();
|
||||
|
||||
// wsp*
|
||||
cursor.eat_whitespace();
|
||||
// quoted_marker?
|
||||
let marker = if cursor.peek_char() == Some(';') {
|
||||
// Skip past the semicolon
|
||||
cursor.next();
|
||||
Some(parse_markers_cursor(cursor)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// wsp*
|
||||
cursor.eat_whitespace();
|
||||
if let Some((pos, char)) = cursor.next() {
|
||||
if let Some(given) = url.given() {
|
||||
if given.ends_with(';') && marker.is_none() {
|
||||
return Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String(
|
||||
"Missing space before ';', the end of the URL is ambiguous".to_string(),
|
||||
),
|
||||
start: requirement_end - ';'.len_utf8(),
|
||||
len: ';'.len_utf8(),
|
||||
input: cursor.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let message = if marker.is_none() {
|
||||
format!(r#"Expected end of input or ';', found '{char}'"#)
|
||||
} else {
|
||||
format!(r#"Expected end of input, found '{char}'"#)
|
||||
};
|
||||
return Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String(message),
|
||||
start: pos,
|
||||
len: char.len_utf8(),
|
||||
input: cursor.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(UnnamedRequirement {
|
||||
url,
|
||||
extras,
|
||||
marker,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a `VerbatimUrl` to represent the requirement, and extracts any extras at the end of the
|
||||
/// URL, to comply with the non-PEP 508 extensions.
|
||||
fn preprocess_unnamed_url(
|
||||
url: &str,
|
||||
#[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused))] working_dir: Option<&Path>,
|
||||
cursor: &Cursor,
|
||||
start: usize,
|
||||
len: usize,
|
||||
) -> Result<(VerbatimUrl, Vec<ExtraName>), Pep508Error<VerbatimUrl>> {
|
||||
// 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
|
||||
// of the URL itself would be ambiguous, since it would consist of the environment variable,
|
||||
// which would expand to _more_ than the URL.
|
||||
let (url, extras) = if let Some((url, extras)) = split_extras(url) {
|
||||
(url, Some(extras))
|
||||
} else {
|
||||
(url, None)
|
||||
};
|
||||
|
||||
// Parse the extras, if provided.
|
||||
let extras = if let Some(extras) = extras {
|
||||
parse_extras_cursor(&mut Cursor::new(extras)).map_err(|err| Pep508Error {
|
||||
message: err.message,
|
||||
start: start + url.len() + err.start,
|
||||
len: err.len,
|
||||
input: cursor.to_string(),
|
||||
})?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Expand environment variables in the URL.
|
||||
let expanded = expand_env_vars(url);
|
||||
|
||||
if let Some((scheme, path)) = split_scheme(&expanded) {
|
||||
match Scheme::parse(scheme) {
|
||||
// Ex) `file:///home/ferris/project/scripts/...`, `file://localhost/home/ferris/project/scripts/...`, or `file:../ferris/`
|
||||
Some(Scheme::File) => {
|
||||
// Strip the leading slashes, along with the `localhost` host, if present.
|
||||
let path = strip_host(path);
|
||||
|
||||
// Transform, e.g., `/C:/Users/ferris/wheel-0.42.0.tar.gz` to `C:\Users\ferris\wheel-0.42.0.tar.gz`.
|
||||
let path = normalize_url_path(path);
|
||||
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
if let Some(working_dir) = working_dir {
|
||||
let url = VerbatimUrl::parse_path(path.as_ref(), working_dir)
|
||||
.with_given(url.to_string());
|
||||
return Ok((url, extras));
|
||||
}
|
||||
|
||||
let url = VerbatimUrl::parse_absolute_path(path.as_ref())
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::<VerbatimUrl>::UrlError(err),
|
||||
start,
|
||||
len,
|
||||
input: cursor.to_string(),
|
||||
})?
|
||||
.with_given(url.to_string());
|
||||
Ok((url, extras))
|
||||
}
|
||||
// Ex) `https://download.pytorch.org/whl/torch_stable.html`
|
||||
Some(_) => {
|
||||
// Ex) `https://download.pytorch.org/whl/torch_stable.html`
|
||||
let url = VerbatimUrl::parse_url(expanded.as_ref())
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::<VerbatimUrl>::UrlError(VerbatimUrlError::Url(
|
||||
err,
|
||||
)),
|
||||
start,
|
||||
len,
|
||||
input: cursor.to_string(),
|
||||
})?
|
||||
.with_given(url.to_string());
|
||||
Ok((url, extras))
|
||||
}
|
||||
|
||||
// Ex) `C:\Users\ferris\wheel-0.42.0.tar.gz`
|
||||
_ => {
|
||||
if let Some(working_dir) = working_dir {
|
||||
let url = VerbatimUrl::parse_path(expanded.as_ref(), working_dir)
|
||||
.with_given(url.to_string());
|
||||
return Ok((url, extras));
|
||||
}
|
||||
|
||||
let url = VerbatimUrl::parse_absolute_path(expanded.as_ref())
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::UrlError(err),
|
||||
start,
|
||||
len,
|
||||
input: cursor.to_string(),
|
||||
})?
|
||||
.with_given(url.to_string());
|
||||
Ok((url, extras))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ex) `../editable/`
|
||||
if let Some(working_dir) = working_dir {
|
||||
let url =
|
||||
VerbatimUrl::parse_path(expanded.as_ref(), working_dir).with_given(url.to_string());
|
||||
return Ok((url, extras));
|
||||
}
|
||||
|
||||
let url = VerbatimUrl::parse_absolute_path(expanded.as_ref())
|
||||
.map_err(|err| Pep508Error {
|
||||
message: Pep508ErrorSource::UrlError(err),
|
||||
start,
|
||||
len,
|
||||
input: cursor.to_string(),
|
||||
})?
|
||||
.with_given(url.to_string());
|
||||
Ok((url, extras))
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`crate::parse_url`], but allows for extras to be present at the end of the URL, to comply
|
||||
/// with the non-PEP 508 extensions.
|
||||
///
|
||||
/// For example:
|
||||
/// - `https://download.pytorch.org/whl/torch_stable.html[dev]`
|
||||
/// - `../editable[dev]`
|
||||
fn parse_unnamed_url(
|
||||
cursor: &mut Cursor,
|
||||
working_dir: Option<&Path>,
|
||||
) -> Result<(VerbatimUrl, Vec<ExtraName>), Pep508Error<VerbatimUrl>> {
|
||||
// wsp*
|
||||
cursor.eat_whitespace();
|
||||
// <URI_reference>
|
||||
let (start, len) = cursor.take_while(|char| !char.is_whitespace());
|
||||
let url = cursor.slice(start, len);
|
||||
if url.is_empty() {
|
||||
return Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String("Expected URL".to_string()),
|
||||
start,
|
||||
len,
|
||||
input: cursor.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let url = preprocess_unnamed_url(url, working_dir, cursor, start, len)?;
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
|
|
@ -5,9 +5,12 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use thiserror::Error;
|
||||
use url::{ParseError, Url};
|
||||
|
||||
use uv_fs::normalize_path;
|
||||
use uv_fs::{normalize_path, normalize_url_path};
|
||||
|
||||
use crate::Pep508Url;
|
||||
|
||||
/// A wrapper around [`Url`] that preserves the original string.
|
||||
#[derive(Debug, Clone, Eq, derivative::Derivative, serde::Deserialize, serde::Serialize)]
|
||||
|
@ -182,8 +185,78 @@ impl Deref for VerbatimUrl {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Url> for VerbatimUrl {
|
||||
fn from(url: Url) -> Self {
|
||||
VerbatimUrl::from_url(url)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pep508Url for VerbatimUrl {
|
||||
type Err = VerbatimUrlError;
|
||||
|
||||
/// Create a `VerbatimUrl` to represent the requirement.
|
||||
fn parse_url(
|
||||
url: &str,
|
||||
#[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused_variables))]
|
||||
working_dir: Option<&Path>,
|
||||
) -> Result<Self, Self::Err> {
|
||||
// Expand environment variables in the URL.
|
||||
let expanded = expand_env_vars(url);
|
||||
|
||||
if let Some((scheme, path)) = split_scheme(&expanded) {
|
||||
match Scheme::parse(scheme) {
|
||||
// Ex) `file:///home/ferris/project/scripts/...`, `file://localhost/home/ferris/project/scripts/...`, or `file:../ferris/`
|
||||
Some(Scheme::File) => {
|
||||
// Strip the leading slashes, along with the `localhost` host, if present.
|
||||
let path = strip_host(path);
|
||||
|
||||
// Transform, e.g., `/C:/Users/ferris/wheel-0.42.0.tar.gz` to `C:\Users\ferris\wheel-0.42.0.tar.gz`.
|
||||
let path = normalize_url_path(path);
|
||||
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
if let Some(working_dir) = working_dir {
|
||||
return Ok(VerbatimUrl::parse_path(path.as_ref(), working_dir)
|
||||
.with_given(url.to_string()));
|
||||
}
|
||||
|
||||
Ok(
|
||||
VerbatimUrl::parse_absolute_path(path.as_ref())?
|
||||
.with_given(url.to_string()),
|
||||
)
|
||||
}
|
||||
// Ex) `https://download.pytorch.org/whl/torch_stable.html`
|
||||
Some(_) => {
|
||||
// Ex) `https://download.pytorch.org/whl/torch_stable.html`
|
||||
Ok(VerbatimUrl::parse_url(expanded.as_ref())?.with_given(url.to_string()))
|
||||
}
|
||||
|
||||
// Ex) `C:\Users\ferris\wheel-0.42.0.tar.gz`
|
||||
_ => {
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
if let Some(working_dir) = working_dir {
|
||||
return Ok(VerbatimUrl::parse_path(expanded.as_ref(), working_dir)
|
||||
.with_given(url.to_string()));
|
||||
}
|
||||
|
||||
Ok(VerbatimUrl::parse_absolute_path(expanded.as_ref())?
|
||||
.with_given(url.to_string()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ex) `../editable/`
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
if let Some(working_dir) = working_dir {
|
||||
return Ok(VerbatimUrl::parse_path(expanded.as_ref(), working_dir)
|
||||
.with_given(url.to_string()));
|
||||
}
|
||||
|
||||
Ok(VerbatimUrl::parse_absolute_path(expanded.as_ref())?.with_given(url.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that can occur when parsing a [`VerbatimUrl`].
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VerbatimUrlError {
|
||||
/// Failed to parse a URL.
|
||||
#[error(transparent)]
|
||||
|
|
|
@ -7,7 +7,7 @@ use serde::{de, Deserialize, Deserializer, Serialize};
|
|||
use tracing::warn;
|
||||
|
||||
use pep440_rs::{VersionSpecifiers, VersionSpecifiersParseError};
|
||||
use pep508_rs::{Pep508Error, Requirement};
|
||||
use pep508_rs::{Pep508Error, Pep508Url, Requirement, VerbatimUrl};
|
||||
|
||||
/// Ex) `>=7.2.0<8.0.0`
|
||||
static MISSING_COMMA: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d)([<>=~^!])").unwrap());
|
||||
|
@ -114,18 +114,18 @@ fn parse_with_fixups<Err, T: FromStr<Err = Err>>(input: &str, type_name: &str) -
|
|||
|
||||
/// Like [`Requirement`], but attempts to correct some common errors in user-provided requirements.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct LenientRequirement(Requirement);
|
||||
pub struct LenientRequirement<T: Pep508Url = VerbatimUrl>(Requirement<T>);
|
||||
|
||||
impl FromStr for LenientRequirement {
|
||||
type Err = Pep508Error;
|
||||
impl<T: Pep508Url> FromStr for LenientRequirement<T> {
|
||||
type Err = Pep508Error<T>;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self(parse_with_fixups(input, "requirement")?))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LenientRequirement> for Requirement {
|
||||
fn from(requirement: LenientRequirement) -> Self {
|
||||
impl<T: Pep508Url> From<LenientRequirement<T>> for Requirement<T> {
|
||||
fn from(requirement: LenientRequirement<T>) -> Self {
|
||||
requirement.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use thiserror::Error;
|
|||
use tracing::warn;
|
||||
|
||||
use pep440_rs::{Version, VersionParseError, VersionSpecifiers, VersionSpecifiersParseError};
|
||||
use pep508_rs::{Pep508Error, Requirement};
|
||||
use pep508_rs::{Pep508Error, Requirement, VerbatimUrl};
|
||||
use uv_normalize::{ExtraName, InvalidNameError, PackageName};
|
||||
|
||||
use crate::lenient_requirement::LenientRequirement;
|
||||
|
@ -29,7 +29,7 @@ pub struct Metadata23 {
|
|||
pub name: PackageName,
|
||||
pub version: Version,
|
||||
// Optional fields
|
||||
pub requires_dist: Vec<Requirement>,
|
||||
pub requires_dist: Vec<Requirement<VerbatimUrl>>,
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
pub provides_extras: Vec<ExtraName>,
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ pub enum MetadataError {
|
|||
#[error(transparent)]
|
||||
Pep440Error(#[from] VersionSpecifiersParseError),
|
||||
#[error(transparent)]
|
||||
Pep508Error(#[from] Pep508Error),
|
||||
Pep508Error(#[from] Pep508Error<VerbatimUrl>),
|
||||
#[error(transparent)]
|
||||
InvalidName(#[from] InvalidNameError),
|
||||
#[error("Invalid `Metadata-Version` field: {0}")]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue