Move pep508-rs Cursor into its own module (#3401)
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
use crate::{Pep508Error, Pep508ErrorSource};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::Chars;
|
||||
|
||||
/// A [`Cursor`] over a string.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cursor<'a> {
|
||||
input: &'a str,
|
||||
chars: Chars<'a>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
/// Convert from `&str`.
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
Self {
|
||||
input,
|
||||
chars: input.chars(),
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new cursor starting at the given position.
|
||||
pub fn at(self, pos: usize) -> Self {
|
||||
Self {
|
||||
input: self.input,
|
||||
chars: self.input[pos..].chars(),
|
||||
pos,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current byte position of the cursor.
|
||||
pub(crate) fn pos(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
/// Returns a slice over the input string.
|
||||
pub(crate) fn slice(&self, start: usize, len: usize) -> &str {
|
||||
&self.input[start..start + len]
|
||||
}
|
||||
|
||||
/// Peeks the next character and position from the input stream without consuming it.
|
||||
pub(crate) fn peek(&self) -> Option<(usize, char)> {
|
||||
self.chars.clone().next().map(|char| (self.pos, char))
|
||||
}
|
||||
|
||||
/// Peeks the next character from the input stream without consuming it.
|
||||
pub(crate) fn peek_char(&self) -> Option<char> {
|
||||
self.chars.clone().next()
|
||||
}
|
||||
|
||||
/// Eats the next character from the input stream if it matches the given token.
|
||||
pub(crate) fn eat_char(&mut self, token: char) -> Option<usize> {
|
||||
let (start_pos, peek_char) = self.peek()?;
|
||||
if peek_char == token {
|
||||
self.next();
|
||||
Some(start_pos)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes whitespace from the cursor.
|
||||
pub(crate) fn eat_whitespace(&mut self) {
|
||||
while let Some(char) = self.peek_char() {
|
||||
if char.is_whitespace() {
|
||||
self.next();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the next character and position from the input stream and consumes it.
|
||||
pub(crate) fn next(&mut self) -> Option<(usize, char)> {
|
||||
let pos = self.pos;
|
||||
let char = self.chars.next()?;
|
||||
self.pos += char.len_utf8();
|
||||
Some((pos, char))
|
||||
}
|
||||
|
||||
pub(crate) fn remaining(&self) -> usize {
|
||||
self.chars.clone().count()
|
||||
}
|
||||
|
||||
/// Peeks over the cursor as long as the condition is met, without consuming it.
|
||||
pub(crate) fn peek_while(&mut self, condition: impl Fn(char) -> bool) -> (usize, usize) {
|
||||
let peeker = self.chars.clone();
|
||||
let start = self.pos();
|
||||
let len = peeker.take_while(|c| condition(*c)).count();
|
||||
(start, len)
|
||||
}
|
||||
|
||||
/// Consumes characters from the cursor as long as the condition is met.
|
||||
pub(crate) fn take_while(&mut self, condition: impl Fn(char) -> bool) -> (usize, usize) {
|
||||
let start = self.pos();
|
||||
let mut len = 0;
|
||||
while let Some(char) = self.peek_char() {
|
||||
if !condition(char) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.next();
|
||||
len += char.len_utf8();
|
||||
}
|
||||
(start, len)
|
||||
}
|
||||
|
||||
/// Consumes characters from the cursor, raising an error if it doesn't match the given token.
|
||||
pub(crate) fn next_expect_char(
|
||||
&mut self,
|
||||
expected: char,
|
||||
span_start: usize,
|
||||
) -> Result<(), Pep508Error> {
|
||||
match self.next() {
|
||||
None => Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String(format!(
|
||||
"Expected '{expected}', found end of dependency specification"
|
||||
)),
|
||||
start: span_start,
|
||||
len: 1,
|
||||
input: self.to_string(),
|
||||
}),
|
||||
Some((_, value)) if value == expected => Ok(()),
|
||||
Some((pos, other)) => Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String(format!(
|
||||
"Expected '{expected}', found '{other}'"
|
||||
)),
|
||||
start: pos,
|
||||
len: other.len_utf8(),
|
||||
input: self.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Cursor<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.input)
|
||||
}
|
||||
}
|
||||
+7
-134
@@ -23,8 +23,9 @@ use std::fmt::{Display, Formatter};
|
||||
#[cfg(feature = "pyo3")]
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::Path;
|
||||
use std::str::{Chars, FromStr};
|
||||
use std::str::FromStr;
|
||||
|
||||
use cursor::Cursor;
|
||||
#[cfg(feature = "pyo3")]
|
||||
use pep440_rs::PyVersion;
|
||||
#[cfg(feature = "pyo3")]
|
||||
@@ -49,6 +50,7 @@ pub use unnamed::UnnamedRequirement;
|
||||
pub use uv_normalize::{ExtraName, InvalidNameError, PackageName};
|
||||
pub use verbatim_url::{expand_env_vars, split_scheme, strip_host, Scheme, VerbatimUrl};
|
||||
|
||||
mod cursor;
|
||||
mod marker;
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
mod unnamed;
|
||||
@@ -479,136 +481,6 @@ impl<'a> From<&'a VersionOrUrl> for VersionOrUrlRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Cursor`] over a string.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cursor<'a> {
|
||||
input: &'a str,
|
||||
chars: Chars<'a>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
/// Convert from `&str`.
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
Self {
|
||||
input,
|
||||
chars: input.chars(),
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new cursor starting at the given position.
|
||||
pub fn at(self, pos: usize) -> Self {
|
||||
Self {
|
||||
input: self.input,
|
||||
chars: self.input[pos..].chars(),
|
||||
pos,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current byte position of the cursor.
|
||||
fn pos(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
/// Returns a slice over the input string.
|
||||
fn slice(&self, start: usize, len: usize) -> &str {
|
||||
&self.input[start..start + len]
|
||||
}
|
||||
|
||||
/// Peeks the next character and position from the input stream without consuming it.
|
||||
fn peek(&self) -> Option<(usize, char)> {
|
||||
self.chars.clone().next().map(|char| (self.pos, char))
|
||||
}
|
||||
|
||||
/// Peeks the next character from the input stream without consuming it.
|
||||
fn peek_char(&self) -> Option<char> {
|
||||
self.chars.clone().next()
|
||||
}
|
||||
|
||||
/// Eats the next character from the input stream if it matches the given token.
|
||||
fn eat_char(&mut self, token: char) -> Option<usize> {
|
||||
let (start_pos, peek_char) = self.peek()?;
|
||||
if peek_char == token {
|
||||
self.next();
|
||||
Some(start_pos)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes whitespace from the cursor.
|
||||
fn eat_whitespace(&mut self) {
|
||||
while let Some(char) = self.peek_char() {
|
||||
if char.is_whitespace() {
|
||||
self.next();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the next character and position from the input stream and consumes it.
|
||||
fn next(&mut self) -> Option<(usize, char)> {
|
||||
let pos = self.pos;
|
||||
let char = self.chars.next()?;
|
||||
self.pos += char.len_utf8();
|
||||
Some((pos, char))
|
||||
}
|
||||
|
||||
/// Peeks over the cursor as long as the condition is met, without consuming it.
|
||||
fn peek_while(&mut self, condition: impl Fn(char) -> bool) -> (usize, usize) {
|
||||
let peeker = self.chars.clone();
|
||||
let start = self.pos();
|
||||
let len = peeker.take_while(|c| condition(*c)).count();
|
||||
(start, len)
|
||||
}
|
||||
|
||||
/// Consumes characters from the cursor as long as the condition is met.
|
||||
fn take_while(&mut self, condition: impl Fn(char) -> bool) -> (usize, usize) {
|
||||
let start = self.pos();
|
||||
let mut len = 0;
|
||||
while let Some(char) = self.peek_char() {
|
||||
if !condition(char) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.next();
|
||||
len += char.len_utf8();
|
||||
}
|
||||
(start, len)
|
||||
}
|
||||
|
||||
/// Consumes characters from the cursor, raising an error if it doesn't match the given token.
|
||||
fn next_expect_char(&mut self, expected: char, span_start: usize) -> Result<(), Pep508Error> {
|
||||
match self.next() {
|
||||
None => Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String(format!(
|
||||
"Expected '{expected}', found end of dependency specification"
|
||||
)),
|
||||
start: span_start,
|
||||
len: 1,
|
||||
input: self.to_string(),
|
||||
}),
|
||||
Some((_, value)) if value == expected => Ok(()),
|
||||
Some((pos, other)) => Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String(format!(
|
||||
"Expected '{expected}', found '{other}'"
|
||||
)),
|
||||
start: pos,
|
||||
len: other.len_utf8(),
|
||||
input: self.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Cursor<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.input)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_name(cursor: &mut Cursor) -> Result<PackageName, Pep508Error> {
|
||||
// https://peps.python.org/pep-0508/#names
|
||||
// ^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$ with re.IGNORECASE
|
||||
@@ -1258,7 +1130,7 @@ fn parse_pep508_requirement(
|
||||
}
|
||||
};
|
||||
|
||||
let requirement_end = cursor.pos;
|
||||
let requirement_end = cursor.pos();
|
||||
|
||||
// wsp*
|
||||
cursor.eat_whitespace();
|
||||
@@ -1319,7 +1191,7 @@ fn parse_unnamed_requirement(
|
||||
|
||||
// Parse the URL itself, along with any extras.
|
||||
let (url, extras) = parse_unnamed_url(cursor, working_dir)?;
|
||||
let requirement_end = cursor.pos;
|
||||
let requirement_end = cursor.pos();
|
||||
|
||||
// wsp*
|
||||
cursor.eat_whitespace();
|
||||
@@ -1403,12 +1275,13 @@ mod tests {
|
||||
use pep440_rs::{Operator, Version, VersionPattern, VersionSpecifier};
|
||||
use uv_normalize::{ExtraName, InvalidNameError, PackageName};
|
||||
|
||||
use crate::cursor::Cursor;
|
||||
use crate::marker::{
|
||||
parse_markers_impl, MarkerExpression, MarkerOperator, MarkerTree, MarkerValue,
|
||||
MarkerValueString, MarkerValueVersion,
|
||||
};
|
||||
use crate::unnamed::UnnamedRequirement;
|
||||
use crate::{Cursor, Pep508Error, Requirement, VerbatimUrl, VersionOrUrl};
|
||||
use crate::{Pep508Error, Requirement, VerbatimUrl, VersionOrUrl};
|
||||
|
||||
fn parse_pepe508_err(input: &str) -> String {
|
||||
Requirement::from_str(input).unwrap_err().to_string()
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
//! outcomes. This implementation tries to carefully validate everything and emit warnings whenever
|
||||
//! bogus comparisons with unintended semantics are made.
|
||||
|
||||
use crate::{Cursor, Pep508Error, Pep508ErrorSource};
|
||||
use crate::cursor::Cursor;
|
||||
use crate::{Pep508Error, Pep508ErrorSource};
|
||||
use pep440_rs::{Version, VersionPattern, VersionSpecifier};
|
||||
#[cfg(feature = "pyo3")]
|
||||
use pyo3::{
|
||||
@@ -908,7 +909,7 @@ impl FromStr for MarkerExpression {
|
||||
"Unexpected character '{unexpected}', expected end of input"
|
||||
)),
|
||||
start: pos,
|
||||
len: chars.chars.clone().count(),
|
||||
len: chars.remaining(),
|
||||
input: chars.to_string(),
|
||||
});
|
||||
}
|
||||
@@ -1417,7 +1418,7 @@ fn parse_marker_op(
|
||||
}
|
||||
|
||||
/// ```text
|
||||
/// marker = marker_or
|
||||
/// marker = marker_or^
|
||||
/// ```
|
||||
pub(crate) fn parse_markers_impl(cursor: &mut Cursor) -> Result<MarkerTree, Pep508Error> {
|
||||
let marker = parse_marker_or(cursor)?;
|
||||
@@ -1430,7 +1431,7 @@ pub(crate) fn parse_markers_impl(cursor: &mut Cursor) -> Result<MarkerTree, Pep5
|
||||
"Unexpected character '{unexpected}', expected 'and', 'or' or end of input"
|
||||
)),
|
||||
start: pos,
|
||||
len: cursor.chars.clone().count(),
|
||||
len: cursor.remaining(),
|
||||
input: cursor.to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,7 +8,8 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use uv_normalize::ExtraName;
|
||||
|
||||
use crate::{Cursor, MarkerEnvironment, MarkerTree, Pep508Error, VerbatimUrl};
|
||||
use crate::cursor::Cursor;
|
||||
use crate::{MarkerEnvironment, MarkerTree, Pep508Error, VerbatimUrl};
|
||||
|
||||
/// A PEP 508-like, direct URL dependency specifier without a package name.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user