1#![warn(missing_docs)]
18
19#[cfg(feature = "schemars")]
20use std::borrow::Cow;
21use std::error::Error;
22use std::fmt::{Debug, Display, Formatter};
23use std::path::Path;
24use std::str::FromStr;
25
26use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
27use thiserror::Error;
28use url::Url;
29
30use uv_cache_key::{CacheKey, CacheKeyHasher};
31use uv_normalize::{ExtraName, PackageName};
32
33use crate::cursor::Cursor;
34pub use crate::marker::{
35 CanonicalMarkerValueExtra, CanonicalMarkerValueString, CanonicalMarkerValueVersion,
36 ContainsMarkerTree, ExtraMarkerTree, ExtraOperator, InMarkerTree, MarkerEnvironment,
37 MarkerEnvironmentBuilder, MarkerExpression, MarkerOperator, MarkerTree, MarkerTreeContents,
38 MarkerTreeKind, MarkerValue, MarkerValueExtra, MarkerValueList, MarkerValueString,
39 MarkerValueVersion, MarkerWarningKind, StringMarkerTree, StringVersion, VersionMarkerTree,
40};
41pub use crate::origin::RequirementOrigin;
42#[cfg(feature = "non-pep508-extensions")]
43pub use crate::unnamed::{UnnamedRequirement, UnnamedRequirementUrl};
44pub use crate::verbatim_url::{
45 Scheme, VerbatimUrl, VerbatimUrlError, expand_env_vars, looks_like_git_repository,
46 split_scheme, strip_host,
47};
48pub use uv_pep440;
51use uv_pep440::{VersionSpecifier, VersionSpecifiers};
52
53mod cursor;
54pub mod marker;
55mod origin;
56#[cfg(feature = "non-pep508-extensions")]
57mod unnamed;
58mod verbatim_url;
59
60#[derive(Debug)]
62pub struct Pep508Error<T: Pep508Url = VerbatimUrl> {
63 pub message: Pep508ErrorSource<T>,
65 pub start: usize,
67 pub len: usize,
69 pub input: String,
71}
72
73#[derive(Debug, Error)]
75pub enum Pep508ErrorSource<T: Pep508Url = VerbatimUrl> {
76 #[error("{0}")]
78 String(String),
79 #[error(transparent)]
81 UrlError(T::Err),
82 #[error("{0}")]
84 UnsupportedRequirement(String),
85}
86
87impl<T: Pep508Url> Display for Pep508Error<T> {
88 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
90 let start_offset = self.input[..self.start]
92 .chars()
93 .filter_map(unicode_width::UnicodeWidthChar::width)
94 .sum::<usize>();
95 let underline_len = if self.start == self.input.len() {
96 assert!(
98 self.len <= 1,
99 "Can only go one past the input not {}",
100 self.len
101 );
102 1
103 } else {
104 self.input[self.start..self.start + self.len]
105 .chars()
106 .filter_map(unicode_width::UnicodeWidthChar::width)
107 .sum::<usize>()
108 };
109 write!(
110 f,
111 "{}\n{}\n{}{}",
112 self.message,
113 self.input,
114 " ".repeat(start_offset),
115 "^".repeat(underline_len)
116 )
117 }
118}
119
120impl<E: Error + Debug, T: Pep508Url<Err = E>> std::error::Error for Pep508Error<T> {}
122
123#[derive(Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
125pub struct Requirement<T: Pep508Url = VerbatimUrl> {
126 pub name: PackageName,
129 pub extras: Box<[ExtraName]>,
132 pub version_or_url: Option<VersionOrUrl<T>>,
136 pub marker: MarkerTree,
140 pub origin: Option<RequirementOrigin>,
142}
143
144impl<T: Pep508Url> Requirement<T> {
145 pub fn clear_url(&mut self) {
147 if matches!(self.version_or_url, Some(VersionOrUrl::Url(_))) {
148 self.version_or_url = None;
149 }
150 }
151
152 pub fn displayable_with_credentials(&self) -> impl Display {
154 RequirementDisplay {
155 requirement: self,
156 display_credentials: true,
157 }
158 }
159}
160
161impl<T: Pep508Url + Display> Display for Requirement<T> {
162 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
163 RequirementDisplay {
164 requirement: self,
165 display_credentials: false,
166 }
167 .fmt(f)
168 }
169}
170
171struct RequirementDisplay<'a, T>
172where
173 T: Pep508Url + Display,
174{
175 requirement: &'a Requirement<T>,
176 display_credentials: bool,
177}
178
179impl<T: Pep508Url + Display> Display for RequirementDisplay<'_, T> {
180 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
181 write!(f, "{}", self.requirement.name)?;
182 if !self.requirement.extras.is_empty() {
183 write!(
184 f,
185 "[{}]",
186 self.requirement
187 .extras
188 .iter()
189 .map(ToString::to_string)
190 .collect::<Vec<_>>()
191 .join(",")
192 )?;
193 }
194 if let Some(version_or_url) = &self.requirement.version_or_url {
195 match version_or_url {
196 VersionOrUrl::VersionSpecifier(version_specifier) => {
197 let version_specifier: Vec<String> =
198 version_specifier.iter().map(ToString::to_string).collect();
199 write!(f, "{}", version_specifier.join(","))?;
200 }
201 VersionOrUrl::Url(url) => {
202 let url_string = if self.display_credentials {
203 url.displayable_with_credentials().to_string()
204 } else {
205 url.to_string()
206 };
207 write!(f, " @ {url_string}")?;
209 }
210 }
211 }
212 if let Some(marker) = self.requirement.marker.contents() {
213 write!(f, " ; {marker}")?;
214 }
215 Ok(())
216 }
217}
218
219impl<'de, T: Pep508Url> Deserialize<'de> for Requirement<T> {
221 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
222 where
223 D: Deserializer<'de>,
224 {
225 struct RequirementVisitor<T>(std::marker::PhantomData<T>);
226
227 impl<T: Pep508Url> serde::de::Visitor<'_> for RequirementVisitor<T> {
228 type Value = Requirement<T>;
229
230 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
231 formatter.write_str("a string containing a PEP 508 requirement")
232 }
233
234 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
235 where
236 E: de::Error,
237 {
238 FromStr::from_str(v).map_err(de::Error::custom)
239 }
240 }
241
242 deserializer.deserialize_str(RequirementVisitor(std::marker::PhantomData))
243 }
244}
245
246impl<T: Pep508Url> Serialize for Requirement<T> {
248 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
249 where
250 S: Serializer,
251 {
252 serializer.collect_str(self)
253 }
254}
255
256impl<T: Pep508Url> CacheKey for Requirement<T> {
257 fn cache_key(&self, state: &mut CacheKeyHasher) {
258 self.name.as_str().cache_key(state);
259
260 self.extras.len().cache_key(state);
261 for extra in &self.extras {
262 extra.as_str().cache_key(state);
263 }
264
265 if let Some(version_or_url) = &self.version_or_url {
269 1u8.cache_key(state);
270 match version_or_url {
271 VersionOrUrl::VersionSpecifier(spec) => {
272 0u8.cache_key(state);
273 spec.len().cache_key(state);
274 for specifier in spec.iter() {
275 specifier.operator().as_str().cache_key(state);
276 specifier.version().cache_key(state);
277 }
278 }
279 VersionOrUrl::Url(url) => {
280 1u8.cache_key(state);
281 url.cache_key(state);
282 }
283 }
284 } else {
285 0u8.cache_key(state);
286 }
287
288 if let Some(marker) = self.marker.contents() {
289 1u8.cache_key(state);
290 marker.to_string().cache_key(state);
291 } else {
292 0u8.cache_key(state);
293 }
294
295 }
297}
298
299impl<T: Pep508Url> Requirement<T> {
300 pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {
302 self.marker.evaluate(env, extras)
303 }
304
305 #[must_use]
310 pub fn with_extra_marker(mut self, extra: &ExtraName) -> Self {
311 self.marker
312 .and(MarkerTree::expression(MarkerExpression::Extra {
313 operator: ExtraOperator::Equal,
314 name: MarkerValueExtra::Extra(extra.clone()),
315 }));
316
317 self
318 }
319
320 #[must_use]
322 pub fn with_origin(self, origin: RequirementOrigin) -> Self {
323 Self {
324 origin: Some(origin),
325 ..self
326 }
327 }
328}
329
330pub trait Pep508Url: Display + Debug + Sized + CacheKey {
332 type Err: Error + Debug;
334
335 fn parse_url(url: &str, working_dir: Option<&Path>) -> Result<Self, Self::Err>;
337
338 fn displayable_with_credentials(&self) -> impl Display;
340}
341
342impl Pep508Url for Url {
343 type Err = url::ParseError;
344
345 fn parse_url(url: &str, _working_dir: Option<&Path>) -> Result<Self, Self::Err> {
346 Self::parse(url)
347 }
348
349 fn displayable_with_credentials(&self) -> impl Display {
350 self
351 }
352}
353
354pub trait Reporter {
356 fn report(&mut self, kind: MarkerWarningKind, warning: String);
358}
359
360impl<F> Reporter for F
361where
362 F: FnMut(MarkerWarningKind, String),
363{
364 fn report(&mut self, kind: MarkerWarningKind, warning: String) {
365 (self)(kind, warning);
366 }
367}
368
369pub struct TracingReporter;
371
372impl Reporter for TracingReporter {
373 #[allow(unused_variables)]
374 fn report(&mut self, _kind: MarkerWarningKind, message: String) {
375 #[cfg(feature = "tracing")]
376 {
377 tracing::warn!("{message}");
378 }
379 }
380}
381
382#[cfg(feature = "schemars")]
383impl<T: Pep508Url> schemars::JsonSchema for Requirement<T> {
384 fn schema_name() -> Cow<'static, str> {
385 Cow::Borrowed("Requirement")
386 }
387
388 fn json_schema(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
389 schemars::json_schema!({
390 "type": "string",
391 "description": "A PEP 508 dependency specifier, e.g., `ruff >= 0.6.0`"
392 })
393 }
394}
395
396impl<T: Pep508Url> FromStr for Requirement<T> {
397 type Err = Pep508Error<T>;
398
399 fn from_str(input: &str) -> Result<Self, Self::Err> {
401 parse_pep508_requirement::<T>(&mut Cursor::new(input), None, &mut TracingReporter)
402 }
403}
404
405impl<T: Pep508Url> Requirement<T> {
406 pub fn parse(input: &str, working_dir: impl AsRef<Path>) -> Result<Self, Pep508Error<T>> {
408 parse_pep508_requirement(
409 &mut Cursor::new(input),
410 Some(working_dir.as_ref()),
411 &mut TracingReporter,
412 )
413 }
414
415 pub fn parse_reporter(
418 input: &str,
419 working_dir: impl AsRef<Path>,
420 reporter: &mut impl Reporter,
421 ) -> Result<Self, Pep508Error<T>> {
422 parse_pep508_requirement(
423 &mut Cursor::new(input),
424 Some(working_dir.as_ref()),
425 reporter,
426 )
427 }
428}
429
430#[derive(Debug, Clone, Eq, Hash, PartialEq)]
432pub struct Extras(Vec<ExtraName>);
433
434impl Extras {
435 pub fn parse<T: Pep508Url>(input: &str) -> Result<Self, Pep508Error<T>> {
437 Ok(Self(parse_extras_cursor(&mut Cursor::new(input))?))
438 }
439}
440
441#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
443pub enum VersionOrUrl<T: Pep508Url = VerbatimUrl> {
444 VersionSpecifier(VersionSpecifiers),
446 Url(T),
448}
449
450impl<T: Pep508Url> Display for VersionOrUrl<T> {
451 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
452 match self {
453 Self::VersionSpecifier(version_specifier) => Display::fmt(version_specifier, f),
454 Self::Url(url) => Display::fmt(url, f),
455 }
456 }
457}
458
459#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
461pub enum VersionOrUrlRef<'a, T: Pep508Url = VerbatimUrl> {
462 VersionSpecifier(&'a VersionSpecifiers),
464 Url(&'a T),
466}
467
468impl<T: Pep508Url> Display for VersionOrUrlRef<'_, T> {
469 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
470 match self {
471 Self::VersionSpecifier(version_specifier) => Display::fmt(version_specifier, f),
472 Self::Url(url) => Display::fmt(url, f),
473 }
474 }
475}
476
477impl<'a> From<&'a VersionOrUrl> for VersionOrUrlRef<'a> {
478 fn from(value: &'a VersionOrUrl) -> Self {
479 match value {
480 VersionOrUrl::VersionSpecifier(version_specifier) => {
481 VersionOrUrlRef::VersionSpecifier(version_specifier)
482 }
483 VersionOrUrl::Url(url) => VersionOrUrlRef::Url(url),
484 }
485 }
486}
487
488fn parse_name<T: Pep508Url>(cursor: &mut Cursor) -> Result<PackageName, Pep508Error<T>> {
489 let start = cursor.pos();
492
493 if let Some((index, char)) = cursor.next() {
494 if !matches!(char, 'A'..='Z' | 'a'..='z' | '0'..='9') {
495 let mut clone = cursor.clone().at(start);
498 return if looks_like_unnamed_requirement(&mut clone) {
499 Err(Pep508Error {
500 message: Pep508ErrorSource::UnsupportedRequirement("URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ /path/to/file`).".to_string()),
501 start,
502 len: clone.pos() - start,
503 input: clone.to_string(),
504 })
505 } else {
506 Err(Pep508Error {
507 message: Pep508ErrorSource::String(format!(
508 "Expected package name starting with an alphanumeric character, found `{char}`"
509 )),
510 start: index,
511 len: char.len_utf8(),
512 input: cursor.to_string(),
513 })
514 };
515 }
516 } else {
517 return Err(Pep508Error {
518 message: Pep508ErrorSource::String("Empty field is not allowed for PEP508".to_string()),
519 start: 0,
520 len: 1,
521 input: cursor.to_string(),
522 });
523 }
524
525 cursor.take_while(|char| matches!(char, 'A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '-' | '_'));
526 let len = cursor.pos() - start;
527 let last = cursor.slice(start, len).chars().last().unwrap();
529 if !matches!(last, 'A'..='Z' | 'a'..='z' | '0'..='9') {
531 return Err(Pep508Error {
532 message: Pep508ErrorSource::String(format!(
533 "Package name must end with an alphanumeric character, not `{last}`"
534 )),
535 start: cursor.pos() - last.len_utf8(),
536 len: last.len_utf8(),
537 input: cursor.to_string(),
538 });
539 }
540 Ok(PackageName::from_str(cursor.slice(start, len)).unwrap())
541}
542
543fn looks_like_unnamed_requirement(cursor: &mut Cursor) -> bool {
547 let (start, len) = cursor.take_while(|char| !char.is_whitespace());
549 let url = cursor.slice(start, len);
550
551 let expanded = expand_env_vars(url);
553
554 let url = split_extras(&expanded)
556 .map(|(url, _)| url)
557 .unwrap_or(&expanded);
558
559 let mut chars = url.chars();
561
562 let Some(first_char) = chars.next() else {
563 return false;
564 };
565
566 if first_char == '\\' || first_char == '/' || first_char == '.' {
568 return true;
569 }
570
571 if split_scheme(url).is_some() {
573 return true;
574 }
575
576 if url.contains('/') || url.contains('\\') {
578 return true;
579 }
580
581 if looks_like_archive(url) {
583 return true;
584 }
585
586 false
587}
588
589fn looks_like_archive(file: impl AsRef<Path>) -> bool {
594 let file = file.as_ref();
595
596 let Some(extension) = file.extension().and_then(|ext| ext.to_str()) else {
598 return false;
599 };
600
601 let pre_extension = file
603 .file_stem()
604 .and_then(|stem| Path::new(stem).extension().and_then(|ext| ext.to_str()));
605
606 matches!(
607 (pre_extension, extension),
608 (_, "whl" | "tbz" | "txz" | "tlz" | "zip" | "tgz" | "tar")
609 | (Some("tar"), "bz2" | "xz" | "lz" | "lzma" | "gz")
610 )
611}
612
613fn parse_extras_cursor<T: Pep508Url>(
615 cursor: &mut Cursor,
616) -> Result<Vec<ExtraName>, Pep508Error<T>> {
617 let Some(bracket_pos) = cursor.eat_char('[') else {
618 return Ok(vec![]);
619 };
620 cursor.eat_whitespace();
621
622 let mut extras = Vec::new();
623 let mut is_first_iteration = true;
624
625 loop {
626 if let Some(']') = cursor.peek_char() {
628 cursor.next();
629 break;
630 }
631
632 match (cursor.peek(), is_first_iteration) {
634 (Some((pos, ',')), true) => {
636 return Err(Pep508Error {
637 message: Pep508ErrorSource::String(
638 "Expected either alphanumerical character (starting the extra name) or `]` (ending the extras section), found `,`".to_string()
639 ),
640 start: pos,
641 len: 1,
642 input: cursor.to_string(),
643 });
644 }
645 (Some((_, ',')), false) => {
647 cursor.next();
648 }
649 (Some((pos, other)), false) => {
650 return Err(Pep508Error {
651 message: Pep508ErrorSource::String(format!(
652 "Expected either `,` (separating extras) or `]` (ending the extras section), found `{other}`"
653 )),
654 start: pos,
655 len: 1,
656 input: cursor.to_string(),
657 });
658 }
659 _ => {}
660 }
661
662 cursor.eat_whitespace();
664 let mut buffer = String::new();
665 let early_eof_error = Pep508Error {
666 message: Pep508ErrorSource::String(
667 "Missing closing bracket (expected ']', found end of dependency specification)"
668 .to_string(),
669 ),
670 start: bracket_pos,
671 len: 1,
672 input: cursor.to_string(),
673 };
674
675 match cursor.next() {
677 Some((_, alphanumeric @ ('a'..='z' | 'A'..='Z' | '0'..='9'))) => {
679 buffer.push(alphanumeric);
680 }
681 Some((pos, other)) => {
682 return Err(Pep508Error {
683 message: Pep508ErrorSource::String(format!(
684 "Expected an alphanumeric character starting the extra name, found `{other}`"
685 )),
686 start: pos,
687 len: other.len_utf8(),
688 input: cursor.to_string(),
689 });
690 }
691 None => return Err(early_eof_error),
692 }
693 let (start, len) = cursor
698 .take_while(|char| matches!(char, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.'));
699 buffer.push_str(cursor.slice(start, len));
700 match cursor.peek() {
701 Some((pos, char)) if char != ',' && char != ']' && !char.is_whitespace() => {
702 return Err(Pep508Error {
703 message: Pep508ErrorSource::String(format!(
704 "Invalid character in extras name, expected an alphanumeric character, `-`, `_`, `.`, `,` or `]`, found `{char}`"
705 )),
706 start: pos,
707 len: char.len_utf8(),
708 input: cursor.to_string(),
709 });
710 }
711 _ => {}
712 }
713 cursor.eat_whitespace();
715
716 extras.push(
718 ExtraName::from_str(&buffer)
719 .expect("`ExtraName` validation should match PEP 508 parsing"),
720 );
721 is_first_iteration = false;
722 }
723
724 Ok(extras)
725}
726
727fn parse_url<T: Pep508Url>(
745 cursor: &mut Cursor,
746 working_dir: Option<&Path>,
747) -> Result<T, Pep508Error<T>> {
748 cursor.eat_whitespace();
750 let (start, len) = {
752 let start = cursor.pos();
753 let mut len = 0;
754 while let Some((_, c)) = cursor.next() {
755 if matches!(c, '\r' | '\n') {
757 break;
758 }
759
760 if c.is_whitespace() {
763 let mut cursor = cursor.clone();
764 cursor.eat_whitespace();
765 if matches!(cursor.peek_char(), None | Some(';' | '#')) {
766 break;
767 }
768 }
769
770 len += c.len_utf8();
771
772 if cursor.peek_char().is_some_and(|c| matches!(c, ';' | '#')) {
774 let mut cursor = cursor.clone();
775 cursor.next();
776 if cursor.peek_char().is_some_and(char::is_whitespace) {
777 break;
778 }
779 }
780 }
781 (start, len)
782 };
783
784 let url = cursor.slice(start, len);
785 if url.is_empty() {
786 return Err(Pep508Error {
787 message: Pep508ErrorSource::String("Expected URL".to_string()),
788 start,
789 len,
790 input: cursor.to_string(),
791 });
792 }
793
794 let url = T::parse_url(url, working_dir).map_err(|err| Pep508Error {
795 message: Pep508ErrorSource::UrlError(err),
796 start,
797 len,
798 input: cursor.to_string(),
799 })?;
800
801 Ok(url)
802}
803
804pub fn split_extras(given: &str) -> Option<(&str, &str)> {
811 let mut chars = given.char_indices().rev();
812
813 if !matches!(chars.next(), Some((_, ']'))) {
815 return None;
816 }
817
818 let (index, _) = chars
820 .take_while(|(_, c)| *c != ']')
821 .find(|(_, c)| *c == '[')?;
822
823 Some(given.split_at(index))
824}
825
826fn parse_specifier<T: Pep508Url>(
828 cursor: &mut Cursor,
829 buffer: &str,
830 start: usize,
831 end: usize,
832) -> Result<VersionSpecifier, Pep508Error<T>> {
833 VersionSpecifier::from_str(buffer).map_err(|err| Pep508Error {
834 message: Pep508ErrorSource::String(err.to_string()),
835 start,
836 len: end - start,
837 input: cursor.to_string(),
838 })
839}
840
841fn parse_version_specifier<T: Pep508Url>(
847 cursor: &mut Cursor,
848) -> Result<Option<VersionOrUrl<T>>, Pep508Error<T>> {
849 let mut start = cursor.pos();
850 let mut specifiers = Vec::new();
851 let mut buffer = String::new();
852 let requirement_kind = loop {
853 match cursor.peek() {
854 Some((end, ',')) => {
855 let specifier = parse_specifier(cursor, &buffer, start, end)?;
856 specifiers.push(specifier);
857 buffer.clear();
858 cursor.next();
859 start = end + 1;
860 }
861 Some((_, ';')) | None => {
862 let end = cursor.pos();
863 let specifier = parse_specifier(cursor, &buffer, start, end)?;
864 specifiers.push(specifier);
865 break Some(VersionOrUrl::VersionSpecifier(
866 specifiers.into_iter().collect(),
867 ));
868 }
869 Some((_, char)) => {
870 buffer.push(char);
871 cursor.next();
872 }
873 }
874 };
875 Ok(requirement_kind)
876}
877
878fn parse_version_specifier_parentheses<T: Pep508Url>(
884 cursor: &mut Cursor,
885) -> Result<Option<VersionOrUrl<T>>, Pep508Error<T>> {
886 let brace_pos = cursor.pos();
887 cursor.next();
888 cursor.eat_whitespace();
890 let mut start = cursor.pos();
891 let mut specifiers = Vec::new();
892 let mut buffer = String::new();
893 let requirement_kind = loop {
894 match cursor.next() {
895 Some((end, ',')) => {
896 let specifier =
897 parse_specifier(cursor, &buffer, start, end)?;
898 specifiers.push(specifier);
899 buffer.clear();
900 start = end + 1;
901 }
902 Some((end, ')')) => {
903 let specifier = parse_specifier(cursor, &buffer, start, end)?;
904 specifiers.push(specifier);
905 break Some(VersionOrUrl::VersionSpecifier(specifiers.into_iter().collect()));
906 }
907 Some((_, char)) => buffer.push(char),
908 None => return Err(Pep508Error {
909 message: Pep508ErrorSource::String("Missing closing parenthesis (expected ')', found end of dependency specification)".to_string()),
910 start: brace_pos,
911 len: 1,
912 input: cursor.to_string(),
913 }),
914 }
915 };
916 Ok(requirement_kind)
917}
918
919fn parse_pep508_requirement<T: Pep508Url>(
921 cursor: &mut Cursor,
922 working_dir: Option<&Path>,
923 reporter: &mut impl Reporter,
924) -> Result<Requirement<T>, Pep508Error<T>> {
925 let start = cursor.pos();
926
927 cursor.eat_whitespace();
941 let name_start = cursor.pos();
943 let name = parse_name(cursor)?;
944 let name_end = cursor.pos();
945 cursor.eat_whitespace();
947 let extras = parse_extras_cursor(cursor)?;
949 cursor.eat_whitespace();
951
952 let requirement_kind = match cursor.peek_char() {
954 Some('@') => {
956 cursor.next();
957 Some(VersionOrUrl::Url(parse_url(cursor, working_dir)?))
958 }
959 Some('(') => parse_version_specifier_parentheses(cursor)?,
961 Some('<' | '=' | '>' | '~' | '!') => parse_version_specifier(cursor)?,
963 Some(';') | None => None,
965 Some(other) => {
966 let mut clone = cursor.clone().at(start);
970 return if looks_like_unnamed_requirement(&mut clone) {
971 Err(Pep508Error {
972 message: Pep508ErrorSource::UnsupportedRequirement("URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`).".to_string()),
973 start,
974 len: clone.pos() - start,
975 input: clone.to_string(),
976 })
977 } else {
978 Err(Pep508Error {
979 message: Pep508ErrorSource::String(format!(
980 "Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `{other}`"
981 )),
982 start: cursor.pos(),
983 len: other.len_utf8(),
984 input: cursor.to_string(),
985 })
986 };
987 }
988 };
989
990 if requirement_kind.is_none() {
996 if looks_like_archive(cursor.slice(name_start, name_end - name_start)) {
997 let clone = cursor.clone().at(start);
998 return Err(Pep508Error {
999 message: Pep508ErrorSource::UnsupportedRequirement("URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`).".to_string()),
1000 start,
1001 len: clone.pos() - start,
1002 input: clone.to_string(),
1003 });
1004 }
1005 }
1006
1007 cursor.eat_whitespace();
1009 let marker = if cursor.peek_char() == Some(';') {
1011 cursor.next();
1013 marker::parse::parse_markers_cursor(cursor, reporter)?
1014 } else {
1015 None
1016 };
1017
1018 cursor.eat_whitespace();
1020
1021 if let Some((pos, char)) = cursor.next().filter(|(_, c)| *c != '#') {
1022 let message = if char == '#' {
1023 format!(
1024 r"Expected end of input or `;`, found `{char}`; comments must be preceded by a leading space"
1025 )
1026 } else if marker.is_none() {
1027 format!(r"Expected end of input or `;`, found `{char}`")
1028 } else {
1029 format!(r"Expected end of input, found `{char}`")
1030 };
1031 return Err(Pep508Error {
1032 message: Pep508ErrorSource::String(message),
1033 start: pos,
1034 len: char.len_utf8(),
1035 input: cursor.to_string(),
1036 });
1037 }
1038
1039 Ok(Requirement {
1040 name,
1041 extras: extras.into_boxed_slice(),
1042 version_or_url: requirement_kind,
1043 marker: marker.unwrap_or_default(),
1044 origin: None,
1045 })
1046}
1047
1048#[cfg(feature = "rkyv")]
1049impl<T: Pep508Url + Display> rkyv::Archive for Requirement<T> {
1051 type Archived = rkyv::string::ArchivedString;
1052 type Resolver = rkyv::string::StringResolver;
1053
1054 #[inline]
1055 fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place<Self::Archived>) {
1056 let as_str = self.to_string();
1057 rkyv::string::ArchivedString::resolve_from_str(&as_str, resolver, out);
1058 }
1059}
1060
1061#[cfg(feature = "rkyv")]
1062impl<T: Pep508Url + Display, S> rkyv::Serialize<S> for Requirement<T>
1063where
1064 S: rkyv::rancor::Fallible + rkyv::ser::Allocator + rkyv::ser::Writer + ?Sized,
1065 S::Error: rkyv::rancor::Source,
1066{
1067 fn serialize(&self, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
1068 let as_str = self.to_string();
1069 rkyv::string::ArchivedString::serialize_from_str(&as_str, serializer)
1070 }
1071}
1072
1073#[cfg(feature = "rkyv")]
1074impl<T: Pep508Url + Display, D: rkyv::rancor::Fallible + ?Sized>
1075 rkyv::Deserialize<Requirement<T>, D> for rkyv::string::ArchivedString
1076{
1077 fn deserialize(&self, _deserializer: &mut D) -> Result<Requirement<T>, D::Error> {
1078 Ok(Requirement::<T>::from_str(self.as_str()).unwrap())
1080 }
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085 use std::env;
1088 use std::str::FromStr;
1089
1090 use insta::assert_snapshot;
1091 use url::Url;
1092
1093 use uv_normalize::{ExtraName, InvalidNameError, PackageName};
1094 use uv_pep440::{Operator, Version, VersionPattern, VersionSpecifier};
1095
1096 use crate::cursor::Cursor;
1097 use crate::marker::{MarkerExpression, MarkerTree, MarkerValueVersion, parse};
1098 use crate::{
1099 MarkerOperator, MarkerValueString, Requirement, TracingReporter, VerbatimUrl, VersionOrUrl,
1100 };
1101
1102 fn parse_pep508_err(input: &str) -> String {
1103 Requirement::<VerbatimUrl>::from_str(input)
1104 .unwrap_err()
1105 .to_string()
1106 }
1107
1108 #[cfg(feature = "non-pep508-extensions")]
1109 fn parse_unnamed_err(input: &str) -> String {
1110 crate::UnnamedRequirement::<VerbatimUrl>::from_str(input)
1111 .unwrap_err()
1112 .to_string()
1113 }
1114
1115 #[cfg(windows)]
1116 #[test]
1117 fn test_preprocess_url_windows() {
1118 use std::path::PathBuf;
1119
1120 let actual = crate::parse_url::<VerbatimUrl>(
1121 &mut Cursor::new("file:///C:/Users/ferris/wheel-0.42.0.tar.gz"),
1122 None,
1123 )
1124 .unwrap()
1125 .to_file_path();
1126 let expected = PathBuf::from(r"C:\Users\ferris\wheel-0.42.0.tar.gz");
1127 assert_eq!(actual, Ok(expected));
1128 }
1129
1130 #[test]
1131 fn error_empty() {
1132 assert_snapshot!(
1133 parse_pep508_err(""),
1134 @"
1135 Empty field is not allowed for PEP508
1136
1137 ^
1138 "
1139 );
1140 }
1141
1142 #[test]
1143 fn error_start() {
1144 assert_snapshot!(
1145 parse_pep508_err("_name"),
1146 @"
1147 Expected package name starting with an alphanumeric character, found `_`
1148 _name
1149 ^
1150 "
1151 );
1152 }
1153
1154 #[test]
1155 fn error_end() {
1156 assert_snapshot!(
1157 parse_pep508_err("name_"),
1158 @"
1159 Package name must end with an alphanumeric character, not `_`
1160 name_
1161 ^
1162 "
1163 );
1164 }
1165
1166 #[test]
1167 fn basic_examples() {
1168 let input = r"requests[security,tests]==2.8.*,>=2.8.1 ; python_full_version < '2.7'";
1169 let requests = Requirement::<Url>::from_str(input).unwrap();
1170 assert_eq!(input, requests.to_string());
1171 let expected = Requirement {
1172 name: PackageName::from_str("requests").unwrap(),
1173 extras: Box::new([
1174 ExtraName::from_str("security").unwrap(),
1175 ExtraName::from_str("tests").unwrap(),
1176 ]),
1177 version_or_url: Some(VersionOrUrl::VersionSpecifier(
1178 [
1179 VersionSpecifier::from_pattern(
1180 Operator::Equal,
1181 VersionPattern::wildcard(Version::new([2, 8])),
1182 )
1183 .unwrap(),
1184 VersionSpecifier::from_pattern(
1185 Operator::GreaterThanEqual,
1186 VersionPattern::verbatim(Version::new([2, 8, 1])),
1187 )
1188 .unwrap(),
1189 ]
1190 .into_iter()
1191 .collect(),
1192 )),
1193 marker: MarkerTree::expression(MarkerExpression::Version {
1194 key: MarkerValueVersion::PythonFullVersion,
1195 specifier: VersionSpecifier::from_pattern(
1196 Operator::LessThan,
1197 "2.7".parse().unwrap(),
1198 )
1199 .unwrap(),
1200 }),
1201 origin: None,
1202 };
1203 assert_eq!(requests, expected);
1204 }
1205
1206 #[test]
1207 fn leading_whitespace() {
1208 let numpy = Requirement::<Url>::from_str(" numpy").unwrap();
1209 assert_eq!(numpy.name.as_ref(), "numpy");
1210 }
1211
1212 #[test]
1213 fn parenthesized_single() {
1214 let numpy = Requirement::<Url>::from_str("numpy ( >=1.19 )").unwrap();
1215 assert_eq!(numpy.name.as_ref(), "numpy");
1216 }
1217
1218 #[test]
1219 fn parenthesized_double() {
1220 let numpy = Requirement::<Url>::from_str("numpy ( >=1.19, <2.0 )").unwrap();
1221 assert_eq!(numpy.name.as_ref(), "numpy");
1222 }
1223
1224 #[test]
1225 fn versions_single() {
1226 let numpy = Requirement::<Url>::from_str("numpy >=1.19 ").unwrap();
1227 assert_eq!(numpy.name.as_ref(), "numpy");
1228 }
1229
1230 #[test]
1231 fn versions_double() {
1232 let numpy = Requirement::<Url>::from_str("numpy >=1.19, <2.0 ").unwrap();
1233 assert_eq!(numpy.name.as_ref(), "numpy");
1234 }
1235
1236 #[test]
1237 #[cfg(feature = "non-pep508-extensions")]
1238 fn direct_url_no_extras() {
1239 let numpy = crate::UnnamedRequirement::<VerbatimUrl>::from_str("https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl").unwrap();
1240 assert_eq!(
1241 numpy.url.to_string(),
1242 "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl"
1243 );
1244 assert_eq!(*numpy.extras, []);
1245 }
1246
1247 #[test]
1248 #[cfg(all(unix, feature = "non-pep508-extensions"))]
1249 fn direct_url_extras() {
1250 let numpy = crate::UnnamedRequirement::<VerbatimUrl>::from_str(
1251 "/path/to/numpy-1.26.4-cp312-cp312-win32.whl[dev]",
1252 )
1253 .unwrap();
1254 assert_eq!(
1255 numpy.url.to_string(),
1256 "file:///path/to/numpy-1.26.4-cp312-cp312-win32.whl"
1257 );
1258 assert_eq!(*numpy.extras, [ExtraName::from_str("dev").unwrap()]);
1259 }
1260
1261 #[test]
1262 #[cfg(all(windows, feature = "non-pep508-extensions"))]
1263 fn direct_url_extras() {
1264 let numpy = crate::UnnamedRequirement::<VerbatimUrl>::from_str(
1265 "C:\\path\\to\\numpy-1.26.4-cp312-cp312-win32.whl[dev]",
1266 )
1267 .unwrap();
1268 assert_eq!(
1269 numpy.url.to_string(),
1270 "file:///C:/path/to/numpy-1.26.4-cp312-cp312-win32.whl"
1271 );
1272 assert_eq!(*numpy.extras, [ExtraName::from_str("dev").unwrap()]);
1273 }
1274
1275 #[test]
1276 fn error_extras_eof1() {
1277 assert_snapshot!(
1278 parse_pep508_err("black["),
1279 @"
1280 Missing closing bracket (expected ']', found end of dependency specification)
1281 black[
1282 ^
1283 "
1284 );
1285 }
1286
1287 #[test]
1288 fn error_extras_eof2() {
1289 assert_snapshot!(
1290 parse_pep508_err("black[d"),
1291 @"
1292 Missing closing bracket (expected ']', found end of dependency specification)
1293 black[d
1294 ^
1295 "
1296 );
1297 }
1298
1299 #[test]
1300 fn error_extras_eof3() {
1301 assert_snapshot!(
1302 parse_pep508_err("black[d,"),
1303 @"
1304 Missing closing bracket (expected ']', found end of dependency specification)
1305 black[d,
1306 ^
1307 "
1308 );
1309 }
1310
1311 #[test]
1312 fn error_extras_illegal_start1() {
1313 assert_snapshot!(
1314 parse_pep508_err("black[ö]"),
1315 @"
1316 Expected an alphanumeric character starting the extra name, found `ö`
1317 black[ö]
1318 ^
1319 "
1320 );
1321 }
1322
1323 #[test]
1324 fn error_extras_illegal_start2() {
1325 assert_snapshot!(
1326 parse_pep508_err("black[_d]"),
1327 @"
1328 Expected an alphanumeric character starting the extra name, found `_`
1329 black[_d]
1330 ^
1331 "
1332 );
1333 }
1334
1335 #[test]
1336 fn error_extras_illegal_start3() {
1337 assert_snapshot!(
1338 parse_pep508_err("black[,]"),
1339 @"
1340 Expected either alphanumerical character (starting the extra name) or `]` (ending the extras section), found `,`
1341 black[,]
1342 ^
1343 "
1344 );
1345 }
1346
1347 #[test]
1348 fn error_extras_illegal_character() {
1349 assert_snapshot!(
1350 parse_pep508_err("black[jüpyter]"),
1351 @"
1352 Invalid character in extras name, expected an alphanumeric character, `-`, `_`, `.`, `,` or `]`, found `ü`
1353 black[jüpyter]
1354 ^
1355 "
1356 );
1357 }
1358
1359 #[test]
1360 fn error_extras1() {
1361 let numpy = Requirement::<Url>::from_str("black[d]").unwrap();
1362 assert_eq!(*numpy.extras, [ExtraName::from_str("d").unwrap()]);
1363 }
1364
1365 #[test]
1366 fn error_extras2() {
1367 let numpy = Requirement::<Url>::from_str("black[d,jupyter]").unwrap();
1368 assert_eq!(
1369 *numpy.extras,
1370 [
1371 ExtraName::from_str("d").unwrap(),
1372 ExtraName::from_str("jupyter").unwrap(),
1373 ]
1374 );
1375 }
1376
1377 #[test]
1378 fn empty_extras() {
1379 let black = Requirement::<Url>::from_str("black[]").unwrap();
1380 assert_eq!(*black.extras, []);
1381 }
1382
1383 #[test]
1384 fn empty_extras_with_spaces() {
1385 let black = Requirement::<Url>::from_str("black[ ]").unwrap();
1386 assert_eq!(*black.extras, []);
1387 }
1388
1389 #[test]
1390 fn error_extra_with_trailing_comma() {
1391 assert_snapshot!(
1392 parse_pep508_err("black[d,]"),
1393 @"
1394 Expected an alphanumeric character starting the extra name, found `]`
1395 black[d,]
1396 ^
1397 "
1398 );
1399 }
1400
1401 #[test]
1402 fn error_parenthesized_pep440() {
1403 assert_snapshot!(
1404 parse_pep508_err("numpy ( ><1.19 )"),
1405 @r#"
1406 no such comparison operator "><", must be one of ~= == != <= >= < > ===
1407 numpy ( ><1.19 )
1408 ^^^^^^^
1409 "#
1410 );
1411 }
1412
1413 #[test]
1414 fn error_parenthesized_parenthesis() {
1415 assert_snapshot!(
1416 parse_pep508_err("numpy ( >=1.19"),
1417 @"
1418 Missing closing parenthesis (expected ')', found end of dependency specification)
1419 numpy ( >=1.19
1420 ^
1421 "
1422 );
1423 }
1424
1425 #[test]
1426 fn error_whats_that() {
1427 assert_snapshot!(
1428 parse_pep508_err("numpy % 1.16"),
1429 @"
1430 Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `%`
1431 numpy % 1.16
1432 ^
1433 "
1434 );
1435 }
1436
1437 #[test]
1438 fn url() {
1439 let pip_url =
1440 Requirement::from_str("pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686")
1441 .unwrap();
1442 let url = "https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686";
1443 let expected = Requirement {
1444 name: PackageName::from_str("pip").unwrap(),
1445 extras: Box::new([]),
1446 marker: MarkerTree::TRUE,
1447 version_or_url: Some(VersionOrUrl::Url(Url::parse(url).unwrap())),
1448 origin: None,
1449 };
1450 assert_eq!(pip_url, expected);
1451 }
1452
1453 #[test]
1454 fn test_marker_parsing() {
1455 let marker = r#"python_version == "2.7" and (sys_platform == "win32" or (os_name == "linux" and implementation_name == 'cpython'))"#;
1456 let actual = parse::parse_markers_cursor::<VerbatimUrl>(
1457 &mut Cursor::new(marker),
1458 &mut TracingReporter,
1459 )
1460 .unwrap()
1461 .unwrap();
1462
1463 let mut a = MarkerTree::expression(MarkerExpression::Version {
1464 key: MarkerValueVersion::PythonVersion,
1465 specifier: VersionSpecifier::from_pattern(Operator::Equal, "2.7".parse().unwrap())
1466 .unwrap(),
1467 });
1468 let mut b = MarkerTree::expression(MarkerExpression::String {
1469 key: MarkerValueString::SysPlatform,
1470 operator: MarkerOperator::Equal,
1471 value: arcstr::literal!("win32"),
1472 });
1473 let mut c = MarkerTree::expression(MarkerExpression::String {
1474 key: MarkerValueString::OsName,
1475 operator: MarkerOperator::Equal,
1476 value: arcstr::literal!("linux"),
1477 });
1478 let d = MarkerTree::expression(MarkerExpression::String {
1479 key: MarkerValueString::ImplementationName,
1480 operator: MarkerOperator::Equal,
1481 value: arcstr::literal!("cpython"),
1482 });
1483
1484 c.and(d);
1485 b.or(c);
1486 a.and(b);
1487
1488 assert_eq!(a, actual);
1489 }
1490
1491 #[test]
1492 fn name_and_marker() {
1493 Requirement::<Url>::from_str(r#"numpy; sys_platform == "win32" or (os_name == "linux" and implementation_name == 'cpython')"#).unwrap();
1494 }
1495
1496 #[test]
1497 fn error_marker_incomplete1() {
1498 assert_snapshot!(
1499 parse_pep508_err(r"numpy; sys_platform"),
1500 @"
1501 Expected a valid marker operator (such as `>=` or `not in`), found ``
1502 numpy; sys_platform
1503 ^
1504 "
1505 );
1506 }
1507
1508 #[test]
1509 fn error_marker_incomplete2() {
1510 assert_snapshot!(
1511 parse_pep508_err(r"numpy; sys_platform =="),
1512 @"
1513 Expected marker value, found end of dependency specification
1514 numpy; sys_platform ==
1515 ^
1516 "
1517 );
1518 }
1519
1520 #[test]
1521 fn error_marker_incomplete3() {
1522 assert_snapshot!(
1523 parse_pep508_err(r#"numpy; sys_platform == "win32" or"#),
1524 @r#"
1525 Expected marker value, found end of dependency specification
1526 numpy; sys_platform == "win32" or
1527 ^
1528 "#
1529 );
1530 }
1531
1532 #[test]
1533 fn error_marker_incomplete4() {
1534 assert_snapshot!(
1535 parse_pep508_err(r#"numpy; sys_platform == "win32" or (os_name == "linux""#),
1536 @r#"
1537 Expected ')', found end of dependency specification
1538 numpy; sys_platform == "win32" or (os_name == "linux"
1539 ^
1540 "#
1541 );
1542 }
1543
1544 #[test]
1545 fn error_marker_incomplete5() {
1546 assert_snapshot!(
1547 parse_pep508_err(r#"numpy; sys_platform == "win32" or (os_name == "linux" and"#),
1548 @r#"
1549 Expected marker value, found end of dependency specification
1550 numpy; sys_platform == "win32" or (os_name == "linux" and
1551 ^
1552 "#
1553 );
1554 }
1555
1556 #[test]
1557 fn error_pep440() {
1558 assert_snapshot!(
1559 parse_pep508_err(r"numpy >=1.1.*"),
1560 @"
1561 Operator >= cannot be used with a wildcard version specifier
1562 numpy >=1.1.*
1563 ^^^^^^^
1564 "
1565 );
1566 }
1567
1568 #[test]
1569 fn error_no_name() {
1570 assert_snapshot!(
1571 parse_pep508_err(r"==0.0"),
1572 @"
1573 Expected package name starting with an alphanumeric character, found `=`
1574 ==0.0
1575 ^
1576 "
1577 );
1578 }
1579
1580 #[test]
1581 fn error_unnamedunnamed_url() {
1582 assert_snapshot!(
1583 parse_pep508_err(r"git+https://github.com/pallets/flask.git"),
1584 @"
1585 URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`).
1586 git+https://github.com/pallets/flask.git
1587 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1588 "
1589 );
1590 }
1591
1592 #[test]
1593 fn error_unnamed_file_path() {
1594 assert_snapshot!(
1595 parse_pep508_err(r"/path/to/flask.tar.gz"),
1596 @"
1597 URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ /path/to/file`).
1598 /path/to/flask.tar.gz
1599 ^^^^^^^^^^^^^^^^^^^^^
1600 "
1601 );
1602 }
1603
1604 #[test]
1605 fn error_no_comma_between_extras() {
1606 assert_snapshot!(
1607 parse_pep508_err(r"name[bar baz]"),
1608 @"
1609 Expected either `,` (separating extras) or `]` (ending the extras section), found `b`
1610 name[bar baz]
1611 ^
1612 "
1613 );
1614 }
1615
1616 #[test]
1617 fn error_extra_comma_after_extras() {
1618 assert_snapshot!(
1619 parse_pep508_err(r"name[bar, baz,]"),
1620 @"
1621 Expected an alphanumeric character starting the extra name, found `]`
1622 name[bar, baz,]
1623 ^
1624 "
1625 );
1626 }
1627
1628 #[test]
1629 fn error_extras_not_closed() {
1630 assert_snapshot!(
1631 parse_pep508_err(r"name[bar, baz >= 1.0"),
1632 @"
1633 Expected either `,` (separating extras) or `]` (ending the extras section), found `>`
1634 name[bar, baz >= 1.0
1635 ^
1636 "
1637 );
1638 }
1639
1640 #[test]
1641 fn error_name_at_nothing() {
1642 assert_snapshot!(
1643 parse_pep508_err(r"name @"),
1644 @"
1645 Expected URL
1646 name @
1647 ^
1648 "
1649 );
1650 }
1651
1652 #[test]
1653 fn parse_name_with_star() {
1654 assert_snapshot!(
1655 parse_pep508_err("wheel-*.whl"),
1656 @"
1657 Package name must end with an alphanumeric character, not `-`
1658 wheel-*.whl
1659 ^
1660 ");
1661 assert_snapshot!(
1662 parse_pep508_err("wheelѦ"),
1663 @"
1664 Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `Ѧ`
1665 wheelѦ
1666 ^
1667 ");
1668 }
1669
1670 #[test]
1671 fn test_error_invalid_marker_key() {
1672 assert_snapshot!(
1673 parse_pep508_err(r"name; invalid_name"),
1674 @"
1675 Expected a quoted string or a valid marker name, found `invalid_name`
1676 name; invalid_name
1677 ^^^^^^^^^^^^
1678 "
1679 );
1680 }
1681
1682 #[test]
1683 fn error_markers_invalid_order() {
1684 assert_snapshot!(
1685 parse_pep508_err("name; '3.7' <= invalid_name"),
1686 @"
1687 Expected a quoted string or a valid marker name, found `invalid_name`
1688 name; '3.7' <= invalid_name
1689 ^^^^^^^^^^^^
1690 "
1691 );
1692 }
1693
1694 #[test]
1695 fn error_markers_notin() {
1696 assert_snapshot!(
1697 parse_pep508_err("name; '3.7' notin python_version"),
1698 @"
1699 Expected a valid marker operator (such as `>=` or `not in`), found `notin`
1700 name; '3.7' notin python_version
1701 ^^^^^
1702 "
1703 );
1704 }
1705
1706 #[test]
1707 fn error_missing_quote() {
1708 assert_snapshot!(
1709 parse_pep508_err("name; python_version == 3.10"),
1710 @"
1711 Expected a quoted string or a valid marker name, found `3.10`
1712 name; python_version == 3.10
1713 ^^^^
1714 "
1715 );
1716 }
1717
1718 #[test]
1719 fn error_markers_inpython_version() {
1720 assert_snapshot!(
1721 parse_pep508_err("name; '3.6'inpython_version"),
1722 @"
1723 Expected a valid marker operator (such as `>=` or `not in`), found `inpython_version`
1724 name; '3.6'inpython_version
1725 ^^^^^^^^^^^^^^^^
1726 "
1727 );
1728 }
1729
1730 #[test]
1731 fn error_markers_not_python_version() {
1732 assert_snapshot!(
1733 parse_pep508_err("name; '3.7' not python_version"),
1734 @"
1735 Expected `i`, found `p`
1736 name; '3.7' not python_version
1737 ^
1738 "
1739 );
1740 }
1741
1742 #[test]
1743 fn error_markers_invalid_operator() {
1744 assert_snapshot!(
1745 parse_pep508_err("name; '3.7' ~ python_version"),
1746 @"
1747 Expected a valid marker operator (such as `>=` or `not in`), found `~`
1748 name; '3.7' ~ python_version
1749 ^
1750 "
1751 );
1752 }
1753
1754 #[test]
1755 fn error_invalid_prerelease() {
1756 assert_snapshot!(
1757 parse_pep508_err("name==1.0.org1"),
1758 @"
1759 after parsing `1.0`, found `.org1`, which is not part of a valid version
1760 name==1.0.org1
1761 ^^^^^^^^^^
1762 "
1763 );
1764 }
1765
1766 #[test]
1767 fn error_no_version_value() {
1768 assert_snapshot!(
1769 parse_pep508_err("name=="),
1770 @"
1771 Unexpected end of version specifier, expected version
1772 name==
1773 ^^
1774 "
1775 );
1776 }
1777
1778 #[test]
1779 fn error_no_version_operator() {
1780 assert_snapshot!(
1781 parse_pep508_err("name 1.0"),
1782 @"
1783 Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `1`
1784 name 1.0
1785 ^
1786 "
1787 );
1788 }
1789
1790 #[test]
1791 fn error_random_char() {
1792 assert_snapshot!(
1793 parse_pep508_err("name >= 1.0 #"),
1794 @"
1795 Trailing `#` is not allowed
1796 name >= 1.0 #
1797 ^^^^^^^^
1798 "
1799 );
1800 }
1801
1802 #[test]
1803 #[cfg(feature = "non-pep508-extensions")]
1804 fn error_invalid_extra_unnamed_url() {
1805 assert_snapshot!(
1806 parse_unnamed_err("/foo-3.0.0-py3-none-any.whl[d,]"),
1807 @"
1808 Expected an alphanumeric character starting the extra name, found `]`
1809 /foo-3.0.0-py3-none-any.whl[d,]
1810 ^
1811 "
1812 );
1813 }
1814
1815 #[test]
1817 #[cfg(feature = "non-pep508-extensions")]
1818 fn non_pep508_paths() {
1819 let requirements = &[
1820 "foo @ file://./foo",
1821 "foo @ file://foo-3.0.0-py3-none-any.whl",
1822 "foo @ file:foo-3.0.0-py3-none-any.whl",
1823 "foo @ ./foo-3.0.0-py3-none-any.whl",
1824 ];
1825 let cwd = env::current_dir().unwrap();
1826
1827 for requirement in requirements {
1828 assert_eq!(
1829 Requirement::<VerbatimUrl>::parse(requirement, &cwd).is_ok(),
1830 cfg!(feature = "non-pep508-extensions"),
1831 "{}: {:?}",
1832 requirement,
1833 Requirement::<VerbatimUrl>::parse(requirement, &cwd)
1834 );
1835 }
1836 }
1837
1838 #[test]
1839 fn no_space_after_operator() {
1840 let requirement = Requirement::<Url>::from_str("pytest;python_version<='4.0'").unwrap();
1841 assert_eq!(
1842 requirement.to_string(),
1843 "pytest ; python_full_version < '4.1'"
1844 );
1845
1846 let requirement = Requirement::<Url>::from_str("pytest;'4.0'>=python_version").unwrap();
1847 assert_eq!(
1848 requirement.to_string(),
1849 "pytest ; python_full_version < '4.1'"
1850 );
1851 }
1852
1853 #[test]
1854 #[cfg(feature = "non-pep508-extensions")]
1855 fn path_with_fragment() {
1856 let requirements = if cfg!(windows) {
1857 &[
1858 "wheel @ file:///C:/Users/ferris/wheel-0.42.0.whl#hash=somehash",
1859 "wheel @ C:/Users/ferris/wheel-0.42.0.whl#hash=somehash",
1860 ]
1861 } else {
1862 &[
1863 "wheel @ file:///Users/ferris/wheel-0.42.0.whl#hash=somehash",
1864 "wheel @ /Users/ferris/wheel-0.42.0.whl#hash=somehash",
1865 ]
1866 };
1867
1868 for requirement in requirements {
1869 let Some(VersionOrUrl::Url(url)) = Requirement::<VerbatimUrl>::from_str(requirement)
1871 .unwrap()
1872 .version_or_url
1873 else {
1874 unreachable!("Expected a URL")
1875 };
1876
1877 assert_eq!(url.fragment(), Some("hash=somehash"));
1879 assert!(
1880 url.path().ends_with("/Users/ferris/wheel-0.42.0.whl"),
1881 "Expected the path to end with `/Users/ferris/wheel-0.42.0.whl`, found `{}`",
1882 url.path()
1883 );
1884 }
1885 }
1886
1887 #[test]
1888 fn add_extra_marker() -> Result<(), InvalidNameError> {
1889 let requirement = Requirement::<Url>::from_str("pytest").unwrap();
1890 let expected = Requirement::<Url>::from_str("pytest; extra == 'dotenv'").unwrap();
1891 let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?);
1892 assert_eq!(actual, expected);
1893
1894 let requirement = Requirement::<Url>::from_str("pytest; '4.0' >= python_version").unwrap();
1895 let expected =
1896 Requirement::from_str("pytest; '4.0' >= python_version and extra == 'dotenv'").unwrap();
1897 let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?);
1898 assert_eq!(actual, expected);
1899
1900 let requirement = Requirement::<Url>::from_str(
1901 "pytest; '4.0' >= python_version or sys_platform == 'win32'",
1902 )
1903 .unwrap();
1904 let expected = Requirement::from_str(
1905 "pytest; ('4.0' >= python_version or sys_platform == 'win32') and extra == 'dotenv'",
1906 )
1907 .unwrap();
1908 let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?);
1909 assert_eq!(actual, expected);
1910
1911 Ok(())
1912 }
1913}