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:
konsti 2024-05-07 16:45:49 +02:00 committed by GitHub
parent 8e86cd0c73
commit 55f6e4e66b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 560 additions and 478 deletions

View file

@ -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<'_> {

View file

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

View file

@ -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()

View file

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

View file

@ -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)]

View file

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

View file

@ -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}")]